Skip to content

Commit cca216a

Browse files
authored
Merge pull request #1744 from seydx/secrets-file
Secrets Management
2 parents 6a67fc3 + e953e94 commit cca216a

File tree

9 files changed

+252
-37
lines changed

9 files changed

+252
-37
lines changed

internal/app/config.go

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -7,7 +7,7 @@ import (
77
"strings"
88
"sync"
99

10-
"github.com/AlexxIT/go2rtc/pkg/shell"
10+
"github.com/AlexxIT/go2rtc/pkg/creds"
1111
"github.com/AlexxIT/go2rtc/pkg/yaml"
1212
)
1313

@@ -71,13 +71,15 @@ func initConfig(confs flagConfig) {
7171
// config as file
7272
if ConfigPath == "" {
7373
ConfigPath = conf
74+
initStorage()
7475
}
7576

7677
if data, _ = os.ReadFile(conf); data == nil {
7778
continue
7879
}
7980

80-
data = []byte(shell.ReplaceEnvVars(string(data)))
81+
loadEnv(data)
82+
data = creds.ReplaceVars(data)
8183
configs = append(configs, data)
8284
}
8385
}

internal/app/log.go

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ import (
66
"strings"
77
"sync"
88

9+
"github.com/AlexxIT/go2rtc/pkg/creds"
910
"github.com/mattn/go-isatty"
1011
"github.com/rs/zerolog"
1112
)
@@ -88,6 +89,8 @@ func initLogger() {
8889
writer = MemoryLog
8990
}
9091

92+
writer = creds.SecretWriter(writer)
93+
9194
lvl, _ := zerolog.ParseLevel(modules["level"])
9295
Logger = zerolog.New(writer).Level(lvl)
9396

internal/app/storage.go

Lines changed: 56 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,56 @@
1+
package app
2+
3+
import (
4+
"sync"
5+
6+
"github.com/AlexxIT/go2rtc/pkg/creds"
7+
"github.com/AlexxIT/go2rtc/pkg/yaml"
8+
)
9+
10+
func initStorage() {
11+
storage = &envStorage{data: make(map[string]string)}
12+
creds.SetStorage(storage)
13+
}
14+
15+
func loadEnv(data []byte) {
16+
var cfg struct {
17+
Env map[string]string `yaml:"env"`
18+
}
19+
20+
if err := yaml.Unmarshal(data, &cfg); err != nil {
21+
return
22+
}
23+
24+
storage.mu.Lock()
25+
for name, value := range cfg.Env {
26+
storage.data[name] = value
27+
creds.AddSecret(value)
28+
}
29+
storage.mu.Unlock()
30+
}
31+
32+
var storage *envStorage
33+
34+
type envStorage struct {
35+
data map[string]string
36+
mu sync.Mutex
37+
}
38+
39+
func (s *envStorage) SetValue(name, value string) error {
40+
if err := PatchConfig([]string{"env", name}, value); err != nil {
41+
return err
42+
}
43+
44+
s.mu.Lock()
45+
s.data[name] = value
46+
s.mu.Unlock()
47+
48+
return nil
49+
}
50+
51+
func (s *envStorage) GetValue(name string) (value string, ok bool) {
52+
s.mu.Lock()
53+
value, ok = s.data[name]
54+
s.mu.Unlock()
55+
return
56+
}

internal/streams/api.go

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -6,10 +6,13 @@ import (
66
"github.com/AlexxIT/go2rtc/internal/api"
77
"github.com/AlexxIT/go2rtc/internal/app"
88
"github.com/AlexxIT/go2rtc/pkg/core"
9+
"github.com/AlexxIT/go2rtc/pkg/creds"
910
"github.com/AlexxIT/go2rtc/pkg/probe"
1011
)
1112

1213
func apiStreams(w http.ResponseWriter, r *http.Request) {
14+
w = creds.SecretResponse(w)
15+
1316
query := r.URL.Query()
1417
src := query.Get("src")
1518

@@ -121,6 +124,8 @@ func apiStreamsDOT(w http.ResponseWriter, r *http.Request) {
121124
}
122125
dot = append(dot, '}')
123126

127+
dot = []byte(creds.SecretString(string(dot)))
128+
124129
api.Response(w, dot, "text/vnd.graphviz")
125130
}
126131

pkg/creds/README.md

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,7 @@
1+
# Credentials
2+
3+
This module allows you to get variables:
4+
5+
- from custom storage (ex. config file)
6+
- from [credential files](https://systemd.io/CREDENTIALS/)
7+
- from environment variables

pkg/creds/creds.go

Lines changed: 79 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
package creds
2+
3+
import (
4+
"errors"
5+
"os"
6+
"path/filepath"
7+
"regexp"
8+
"strings"
9+
)
10+
11+
type Storage interface {
12+
SetValue(name, value string) error
13+
GetValue(name string) (string, bool)
14+
}
15+
16+
var storage Storage
17+
18+
func SetStorage(s Storage) {
19+
storage = s
20+
}
21+
22+
func SetValue(name, value string) error {
23+
if storage == nil {
24+
return errors.New("credentials: storage not initialized")
25+
}
26+
if err := storage.SetValue(name, value); err != nil {
27+
return err
28+
}
29+
AddSecret(value)
30+
return nil
31+
}
32+
33+
func GetValue(name string) (value string, ok bool) {
34+
value, ok = getValue(name)
35+
AddSecret(value)
36+
return
37+
}
38+
39+
func getValue(name string) (string, bool) {
40+
if storage != nil {
41+
if value, ok := storage.GetValue(name); ok {
42+
return value, true
43+
}
44+
}
45+
46+
if dir, ok := os.LookupEnv("CREDENTIALS_DIRECTORY"); ok {
47+
if value, _ := os.ReadFile(filepath.Join(dir, name)); value != nil {
48+
return strings.TrimSpace(string(value)), true
49+
}
50+
}
51+
52+
return os.LookupEnv(name)
53+
}
54+
55+
// ReplaceVars - support format ${CAMERA_PASSWORD} and ${RTSP_USER:admin}
56+
func ReplaceVars(data []byte) []byte {
57+
re := regexp.MustCompile(`\${([^}{]+)}`)
58+
return re.ReplaceAllFunc(data, func(match []byte) []byte {
59+
key := string(match[2 : len(match)-1])
60+
61+
var def string
62+
var defok bool
63+
64+
if i := strings.IndexByte(key, ':'); i > 0 {
65+
key, def = key[:i], key[i+1:]
66+
defok = true
67+
}
68+
69+
if value, ok := GetValue(key); ok {
70+
return []byte(value)
71+
}
72+
73+
if defok {
74+
return []byte(def)
75+
}
76+
77+
return match
78+
})
79+
}

pkg/creds/secrets.go

Lines changed: 83 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,83 @@
1+
package creds
2+
3+
import (
4+
"io"
5+
"net/http"
6+
"slices"
7+
"strings"
8+
"sync"
9+
)
10+
11+
func AddSecret(value string) {
12+
if value == "" {
13+
return
14+
}
15+
16+
secretsMu.Lock()
17+
defer secretsMu.Unlock()
18+
19+
if slices.Contains(secrets, value) {
20+
return
21+
}
22+
23+
secrets = append(secrets, value)
24+
secretsReplacer = nil
25+
}
26+
27+
var secrets []string
28+
var secretsMu sync.Mutex
29+
var secretsReplacer *strings.Replacer
30+
31+
func getReplacer() *strings.Replacer {
32+
secretsMu.Lock()
33+
defer secretsMu.Unlock()
34+
35+
if secretsReplacer == nil {
36+
oldnew := make([]string, 0, 2*len(secrets))
37+
for _, s := range secrets {
38+
oldnew = append(oldnew, s, "***")
39+
}
40+
secretsReplacer = strings.NewReplacer(oldnew...)
41+
}
42+
43+
return secretsReplacer
44+
}
45+
46+
func SecretString(s string) string {
47+
re := getReplacer()
48+
return re.Replace(s)
49+
}
50+
51+
func SecretWriter(w io.Writer) io.Writer {
52+
return &secretWriter{w}
53+
}
54+
55+
type secretWriter struct {
56+
w io.Writer
57+
}
58+
59+
func (s *secretWriter) Write(b []byte) (int, error) {
60+
re := getReplacer()
61+
return re.WriteString(s.w, string(b))
62+
}
63+
64+
type secretResponse struct {
65+
w http.ResponseWriter
66+
}
67+
68+
func (s *secretResponse) Header() http.Header {
69+
return s.w.Header()
70+
}
71+
72+
func (s *secretResponse) Write(b []byte) (int, error) {
73+
re := getReplacer()
74+
return re.WriteString(s.w, string(b))
75+
}
76+
77+
func (s *secretResponse) WriteHeader(statusCode int) {
78+
s.w.WriteHeader(statusCode)
79+
}
80+
81+
func SecretResponse(w http.ResponseWriter) http.ResponseWriter {
82+
return &secretResponse{w}
83+
}

pkg/creds/secrets_test.go

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
package creds
2+
3+
import (
4+
"testing"
5+
6+
"github.com/stretchr/testify/require"
7+
)
8+
9+
func TestString(t *testing.T) {
10+
AddSecret("admin")
11+
AddSecret("pa$$word")
12+
13+
s := SecretString("rtsp://admin:[email protected]/stream1")
14+
require.Equal(t, "rtsp://***:***@192.168.1.123/stream1", s)
15+
}

pkg/shell/shell.go

Lines changed: 0 additions & 35 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@ package shell
33
import (
44
"os"
55
"os/signal"
6-
"path/filepath"
7-
"regexp"
86
"strings"
97
"syscall"
108
)
@@ -38,39 +36,6 @@ func QuoteSplit(s string) []string {
3836
return a
3937
}
4038

41-
// ReplaceEnvVars - support format ${CAMERA_PASSWORD} and ${RTSP_USER:admin}
42-
func ReplaceEnvVars(text string) string {
43-
re := regexp.MustCompile(`\${([^}{]+)}`)
44-
return re.ReplaceAllStringFunc(text, func(match string) string {
45-
key := match[2 : len(match)-1]
46-
47-
var def string
48-
var dok bool
49-
50-
if i := strings.IndexByte(key, ':'); i > 0 {
51-
key, def = key[:i], key[i+1:]
52-
dok = true
53-
}
54-
55-
if dir, vok := os.LookupEnv("CREDENTIALS_DIRECTORY"); vok {
56-
value, err := os.ReadFile(filepath.Join(dir, key))
57-
if err == nil {
58-
return strings.TrimSpace(string(value))
59-
}
60-
}
61-
62-
if value, vok := os.LookupEnv(key); vok {
63-
return value
64-
}
65-
66-
if dok {
67-
return def
68-
}
69-
70-
return match
71-
})
72-
}
73-
7439
func RunUntilSignal() {
7540
sigs := make(chan os.Signal, 1)
7641
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)

0 commit comments

Comments
 (0)