From 0830d8342ecdd5c8abd35726aa9e20dd81ed691c Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 20 May 2025 12:07:46 +0200 Subject: [PATCH 01/18] add secret management functions --- internal/app/app.go | 9 ++++++++ internal/app/config.go | 47 ++++++++++++++++++++++++++++++++++++++++++ 2 files changed, 56 insertions(+) diff --git a/internal/app/app.go b/internal/app/app.go index eb803584..02b8d68c 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -13,6 +13,7 @@ var ( Version string UserAgent string ConfigPath string + SecretPath string Info = make(map[string]any) ) @@ -25,11 +26,14 @@ const usage = `Usage of go2rtc: func Init() { var config flagConfig + var secret string var daemon bool var version bool flag.Var(&config, "config", "") flag.Var(&config, "c", "") + flag.StringVar(&secret, "secret", "go2rtc.secret", "") + flag.StringVar(&secret, "s", "go2rtc.secret", "") flag.BoolVar(&daemon, "daemon", false, "") flag.BoolVar(&daemon, "d", false, "") flag.BoolVar(&version, "version", false, "") @@ -67,6 +71,7 @@ func Init() { Info["revision"] = revision initConfig(config) + initSecret(secret) initLogger() platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) @@ -76,6 +81,10 @@ func Init() { if ConfigPath != "" { Logger.Info().Str("path", ConfigPath).Msg("config") } + + if SecretPath != "" { + Logger.Info().Str("path", SecretPath).Msg("secrets") + } } func readRevisionTime() (revision, vcsTime string) { diff --git a/internal/app/config.go b/internal/app/config.go index 9d4480b7..16cf53b5 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -18,6 +18,14 @@ func LoadConfig(v any) { } } +func LoadSecret(v any) { + for _, data := range secrets { + if err := yaml.Unmarshal(data, v); err != nil { + Logger.Warn().Err(err).Send() + } + } +} + func PatchConfig(path []string, value any) error { if ConfigPath == "" { return errors.New("config file disabled") @@ -34,6 +42,27 @@ func PatchConfig(path []string, value any) error { return os.WriteFile(ConfigPath, b, 0644) } +func PatchSecret(path []string, value any) error { + if SecretPath == "" { + return errors.New("secret file disabled") + } + + // empty config is OK + b, _ := os.ReadFile(SecretPath) + + b, err := yaml.Patch(b, path, value) + if err != nil { + return err + } + + if err := os.WriteFile(SecretPath, b, 0644); err == nil { + secrets = [][]byte{b} + } + + return err +} + + type flagConfig []string func (c *flagConfig) String() string { @@ -46,6 +75,7 @@ func (c *flagConfig) Set(value string) error { } var configs [][]byte +var secrets [][]byte func initConfig(confs flagConfig) { if confs == nil { @@ -86,6 +116,23 @@ func initConfig(confs flagConfig) { } } +func initSecret(secret string) { + if secret == "" { + secret = "go2rtc.secrets" + } + + SecretPath = secret + + if SecretPath != "" { + if !filepath.IsAbs(SecretPath) { + if cwd, err := os.Getwd(); err == nil { + SecretPath = filepath.Join(cwd, SecretPath) + } + } + Info["secret_path"] = SecretPath + } +} + func parseConfString(s string) []byte { i := strings.IndexByte(s, '=') if i < 0 { From e5e55b7a50e51911a53d69c82aaa3dae29ef555a Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 20 May 2025 13:05:11 +0200 Subject: [PATCH 02/18] improve secret vars and parse url with secrets --- internal/app/config.go | 47 ------------- internal/app/secrets.go | 129 +++++++++++++++++++++++++++++++++++ internal/streams/handlers.go | 8 ++- 3 files changed, 136 insertions(+), 48 deletions(-) create mode 100644 internal/app/secrets.go diff --git a/internal/app/config.go b/internal/app/config.go index 16cf53b5..9d4480b7 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -18,14 +18,6 @@ func LoadConfig(v any) { } } -func LoadSecret(v any) { - for _, data := range secrets { - if err := yaml.Unmarshal(data, v); err != nil { - Logger.Warn().Err(err).Send() - } - } -} - func PatchConfig(path []string, value any) error { if ConfigPath == "" { return errors.New("config file disabled") @@ -42,27 +34,6 @@ func PatchConfig(path []string, value any) error { return os.WriteFile(ConfigPath, b, 0644) } -func PatchSecret(path []string, value any) error { - if SecretPath == "" { - return errors.New("secret file disabled") - } - - // empty config is OK - b, _ := os.ReadFile(SecretPath) - - b, err := yaml.Patch(b, path, value) - if err != nil { - return err - } - - if err := os.WriteFile(SecretPath, b, 0644); err == nil { - secrets = [][]byte{b} - } - - return err -} - - type flagConfig []string func (c *flagConfig) String() string { @@ -75,7 +46,6 @@ func (c *flagConfig) Set(value string) error { } var configs [][]byte -var secrets [][]byte func initConfig(confs flagConfig) { if confs == nil { @@ -116,23 +86,6 @@ func initConfig(confs flagConfig) { } } -func initSecret(secret string) { - if secret == "" { - secret = "go2rtc.secrets" - } - - SecretPath = secret - - if SecretPath != "" { - if !filepath.IsAbs(SecretPath) { - if cwd, err := os.Getwd(); err == nil { - SecretPath = filepath.Join(cwd, SecretPath) - } - } - Info["secret_path"] = SecretPath - } -} - func parseConfString(s string) []byte { i := strings.IndexByte(s, '=') if i < 0 { diff --git a/internal/app/secrets.go b/internal/app/secrets.go new file mode 100644 index 00000000..12fce1ef --- /dev/null +++ b/internal/app/secrets.go @@ -0,0 +1,129 @@ +package app + +import ( + "errors" + "fmt" + "os" + "path/filepath" + "regexp" + "strings" + + "github.com/AlexxIT/go2rtc/pkg/yaml" +) + +var secrets [][]byte + +var templateRegex = regexp.MustCompile(`\{\{\s*([^\}]+)\s*\}\}`) + +func ResolveSecrets(template string) string { + if !templateRegex.MatchString(template) { + return template + } + + var secretsMap map[string]interface{} + LoadSecret(&secretsMap) + + // ex template: rtsp://{{ my_camera.username }}:{{ my_camera.password }}@192.168.178.1:554/stream + result := templateRegex.ReplaceAllStringFunc(template, func(match string) string { + varName := strings.TrimSpace(templateRegex.FindStringSubmatch(match)[1]) + pathParts := strings.Split(varName, ".") + value := getNestedValue(secretsMap, pathParts) + + if value != nil { + return stringify(value) + } + + return "" + }) + + return result +} + +func LoadSecret(v any) { + for _, data := range secrets { + if err := yaml.Unmarshal(data, v); err != nil { + Logger.Warn().Err(err).Send() + } + } +} + +func PatchSecret(path []string, value any) error { + if SecretPath == "" { + return errors.New("secret file disabled") + } + + // empty config is OK + b, _ := os.ReadFile(SecretPath) + + b, err := yaml.Patch(b, path, value) + if err != nil { + return err + } + + if err := os.WriteFile(SecretPath, b, 0644); err == nil { + secrets = [][]byte{b} + } + + return err +} + +func initSecret(secret string) { + if secret == "" { + secret = "go2rtc.secrets" + } + + SecretPath = secret + + if SecretPath != "" { + if !filepath.IsAbs(SecretPath) { + if cwd, err := os.Getwd(); err == nil { + SecretPath = filepath.Join(cwd, SecretPath) + } + } + Info["secret_path"] = SecretPath + } +} + +func getNestedValue(m map[string]interface{}, path []string) interface{} { + if len(path) == 0 || m == nil { + return nil + } + + key := path[0] + value, exists := m[key] + if !exists { + return nil + } + + if len(path) == 1 { + return value + } + + // Für verschachtelte Maps + switch nextMap := value.(type) { + case map[string]interface{}: + return getNestedValue(nextMap, path[1:]) + case map[interface{}]interface{}: + // Konvertiere map[interface{}]interface{} zu map[string]interface{} + stringMap := make(map[string]interface{}) + for k, v := range nextMap { + if keyStr, ok := k.(string); ok { + stringMap[keyStr] = v + } + } + return getNestedValue(stringMap, path[1:]) + default: + return nil + } +} + +func stringify(value interface{}) string { + switch v := value.(type) { + case string: + return v + case int, int64, float64, bool: + return fmt.Sprintf("%v", v) + default: + return "" + } +} \ No newline at end of file diff --git a/internal/streams/handlers.go b/internal/streams/handlers.go index 3240abb5..bd394fc8 100644 --- a/internal/streams/handlers.go +++ b/internal/streams/handlers.go @@ -4,6 +4,7 @@ import ( "errors" "strings" + "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/pkg/core" ) @@ -46,7 +47,8 @@ func GetProducer(url string) (core.Producer, error) { } if handler, ok := handlers[scheme]; ok { - return handler(url) + parsedURL := ParseURL(url) + return handler(parsedURL) } } @@ -95,3 +97,7 @@ func GetConsumer(url string) (core.Consumer, func(), error) { return nil, nil, errors.New("streams: unsupported scheme: " + url) } + +func ParseURL(url string) string { + return app.ResolveSecrets(url) +} \ No newline at end of file From a2beea1bbd6ecb8ae83803bcf3f41ee0fc65c87b Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 20 May 2025 13:59:46 +0200 Subject: [PATCH 03/18] refactor --- internal/streams/handlers.go | 9 +++------ pkg/core/listener.go | 10 ++++++++++ 2 files changed, 13 insertions(+), 6 deletions(-) diff --git a/internal/streams/handlers.go b/internal/streams/handlers.go index bd394fc8..d2b99d65 100644 --- a/internal/streams/handlers.go +++ b/internal/streams/handlers.go @@ -47,7 +47,7 @@ func GetProducer(url string) (core.Producer, error) { } if handler, ok := handlers[scheme]; ok { - parsedURL := ParseURL(url) + parsedURL := app.ResolveSecrets(url) return handler(parsedURL) } } @@ -91,13 +91,10 @@ func GetConsumer(url string) (core.Consumer, func(), error) { scheme := url[:i] if handler, ok := consumerHandlers[scheme]; ok { - return handler(url) + parsedURL := app.ResolveSecrets(url) + return handler(parsedURL) } } return nil, nil, errors.New("streams: unsupported scheme: " + url) } - -func ParseURL(url string) string { - return app.ResolveSecrets(url) -} \ No newline at end of file diff --git a/pkg/core/listener.go b/pkg/core/listener.go index 75d9202a..2b7e198b 100644 --- a/pkg/core/listener.go +++ b/pkg/core/listener.go @@ -1,5 +1,7 @@ package core +import "github.com/AlexxIT/go2rtc/internal/app" + type EventFunc func(msg any) // Listener base struct for all classes with support feedback @@ -16,3 +18,11 @@ func (l *Listener) Fire(msg any) { f(msg) } } + +func (l *Listener) ParseSource(url string) string { + return app.ResolveSecrets(url) +} + +func (l *Listener) SaveSource(path []string, value any) error { + return app.PatchSecret(path, value) +} \ No newline at end of file From 2fcbb1d836153c7208e135de206566cfeb4f912d Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 20 May 2025 15:51:15 +0200 Subject: [PATCH 04/18] refactor --- internal/app/secrets.go | 54 +++++++++++++++++++++++++++-------------- 1 file changed, 36 insertions(+), 18 deletions(-) diff --git a/internal/app/secrets.go b/internal/app/secrets.go index 12fce1ef..2e279278 100644 --- a/internal/app/secrets.go +++ b/internal/app/secrets.go @@ -16,9 +16,9 @@ var secrets [][]byte var templateRegex = regexp.MustCompile(`\{\{\s*([^\}]+)\s*\}\}`) func ResolveSecrets(template string) string { - if !templateRegex.MatchString(template) { - return template - } + if !templateRegex.MatchString(template) { + return template + } var secretsMap map[string]interface{} LoadSecret(&secretsMap) @@ -28,23 +28,38 @@ func ResolveSecrets(template string) string { varName := strings.TrimSpace(templateRegex.FindStringSubmatch(match)[1]) pathParts := strings.Split(varName, ".") value := getNestedValue(secretsMap, pathParts) - + if value != nil { return stringify(value) } - + return "" }) - + return result } func LoadSecret(v any) { - for _, data := range secrets { - if err := yaml.Unmarshal(data, v); err != nil { - Logger.Warn().Err(err).Send() - } - } + for _, data := range secrets { + var tempData map[string]interface{} + + if err := yaml.Unmarshal(data, &tempData); err != nil { + Logger.Warn().Err(err).Send() + continue + } + + if secretData, exists := tempData["secret"]; exists { + secretBytes, err := yaml.Encode(secretData, 2) + if err != nil { + Logger.Warn().Err(err).Send() + continue + } + + if err := yaml.Unmarshal(secretBytes, v); err != nil { + Logger.Warn().Err(err).Send() + } + } + } } func PatchSecret(path []string, value any) error { @@ -55,7 +70,7 @@ func PatchSecret(path []string, value any) error { // empty config is OK b, _ := os.ReadFile(SecretPath) - b, err := yaml.Patch(b, path, value) + b, err := yaml.Patch(b, append([]string{"secret"}, path...), value) if err != nil { return err } @@ -82,29 +97,32 @@ func initSecret(secret string) { } Info["secret_path"] = SecretPath } + + if data, err := os.ReadFile(SecretPath); err == nil { + secrets = append(secrets, data) + } } func getNestedValue(m map[string]interface{}, path []string) interface{} { if len(path) == 0 || m == nil { return nil } - + key := path[0] value, exists := m[key] if !exists { return nil } - + if len(path) == 1 { return value } - - // Für verschachtelte Maps + + // Check nested maps switch nextMap := value.(type) { case map[string]interface{}: return getNestedValue(nextMap, path[1:]) case map[interface{}]interface{}: - // Konvertiere map[interface{}]interface{} zu map[string]interface{} stringMap := make(map[string]interface{}) for k, v := range nextMap { if keyStr, ok := k.(string); ok { @@ -126,4 +144,4 @@ func stringify(value interface{}) string { default: return "" } -} \ No newline at end of file +} From a0145b4b241dc187e83925c7aa702876eb87e650 Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 20 May 2025 15:52:26 +0200 Subject: [PATCH 05/18] revert handlers --- internal/streams/handlers.go | 7 ++----- 1 file changed, 2 insertions(+), 5 deletions(-) diff --git a/internal/streams/handlers.go b/internal/streams/handlers.go index d2b99d65..3240abb5 100644 --- a/internal/streams/handlers.go +++ b/internal/streams/handlers.go @@ -4,7 +4,6 @@ import ( "errors" "strings" - "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/pkg/core" ) @@ -47,8 +46,7 @@ func GetProducer(url string) (core.Producer, error) { } if handler, ok := handlers[scheme]; ok { - parsedURL := app.ResolveSecrets(url) - return handler(parsedURL) + return handler(url) } } @@ -91,8 +89,7 @@ func GetConsumer(url string) (core.Consumer, func(), error) { scheme := url[:i] if handler, ok := consumerHandlers[scheme]; ok { - parsedURL := app.ResolveSecrets(url) - return handler(parsedURL) + return handler(url) } } From 7f87c6e478c26ba7277b7155013ebd19be05ae33 Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 20 May 2025 21:40:33 +0200 Subject: [PATCH 06/18] refactor --- internal/app/app.go | 5 +- internal/app/secrets.go | 195 +++++++++++++++++++++++++++------------- pkg/core/listener.go | 8 +- 3 files changed, 137 insertions(+), 71 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 02b8d68c..706841ec 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -26,14 +26,11 @@ const usage = `Usage of go2rtc: func Init() { var config flagConfig - var secret string var daemon bool var version bool flag.Var(&config, "config", "") flag.Var(&config, "c", "") - flag.StringVar(&secret, "secret", "go2rtc.secret", "") - flag.StringVar(&secret, "s", "go2rtc.secret", "") flag.BoolVar(&daemon, "daemon", false, "") flag.BoolVar(&daemon, "d", false, "") flag.BoolVar(&version, "version", false, "") @@ -71,7 +68,7 @@ func Init() { Info["revision"] = revision initConfig(config) - initSecret(secret) + initSecrets() initLogger() platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) diff --git a/internal/app/secrets.go b/internal/app/secrets.go index 2e279278..b7773b65 100644 --- a/internal/app/secrets.go +++ b/internal/app/secrets.go @@ -1,33 +1,97 @@ package app import ( - "errors" "fmt" - "os" - "path/filepath" "regexp" "strings" + "sync" "github.com/AlexxIT/go2rtc/pkg/yaml" ) -var secrets [][]byte +var secrets = make(map[string]*Secret) +var secretsMu sync.Mutex var templateRegex = regexp.MustCompile(`\{\{\s*([^\}]+)\s*\}\}`) -func ResolveSecrets(template string) string { +type Secrets interface { + Get(key string) any + Set(key string, value any) + Parse(template string) string + Marshal(v any) ([]byte, error) + Unmarshal(v any) error + Save() error +} + +type Secret struct { + Secrets + + Name string + Values map[string]any +} + +func NewSecret(name string, values interface{}) *Secret { + secretsMu.Lock() + defer secretsMu.Unlock() + + if s, exists := secrets[name]; exists { + return s + } + + s := &Secret{Name: name, Values: make(map[string]any)} + + switch v := values.(type) { + case map[string]any: + s.Values = v + default: + data, err := yaml.Encode(values, 2) + if err == nil { + var mapValues map[string]any + if err := yaml.Unmarshal(data, &mapValues); err == nil { + s.Values = mapValues + } + } + } + + secrets[name] = s + return s +} + +func (s *Secret) Get(key string) any { + secretsMu.Lock() + defer secretsMu.Unlock() + + return s.Values[key] +} + +func (s *Secret) Set(key string, value any) { + secretsMu.Lock() + defer secretsMu.Unlock() + + if s.Values == nil { + s.Values = make(map[string]any) + } + + s.Values[key] = value + secrets[s.Name] = s +} + +func (s *Secret) Parse(template string) string { if !templateRegex.MatchString(template) { return template } - var secretsMap map[string]interface{} - LoadSecret(&secretsMap) + secretsMu.Lock() + defer secretsMu.Unlock() + + if _, exists := secrets[s.Name]; !exists { + return template + } - // ex template: rtsp://{{ my_camera.username }}:{{ my_camera.password }}@192.168.178.1:554/stream result := templateRegex.ReplaceAllStringFunc(template, func(match string) string { varName := strings.TrimSpace(templateRegex.FindStringSubmatch(match)[1]) pathParts := strings.Split(varName, ".") - value := getNestedValue(secretsMap, pathParts) + value := getNestedValue(s.Values, pathParts) if value != nil { return stringify(value) @@ -39,71 +103,80 @@ func ResolveSecrets(template string) string { return result } -func LoadSecret(v any) { - for _, data := range secrets { - var tempData map[string]interface{} +func (s *Secret) Marshal(v any) ([]byte, error) { + secretsMu.Lock() + defer secretsMu.Unlock() - if err := yaml.Unmarshal(data, &tempData); err != nil { - Logger.Warn().Err(err).Send() - continue - } - - if secretData, exists := tempData["secret"]; exists { - secretBytes, err := yaml.Encode(secretData, 2) - if err != nil { - Logger.Warn().Err(err).Send() - continue - } - - if err := yaml.Unmarshal(secretBytes, v); err != nil { - Logger.Warn().Err(err).Send() - } - } - } -} - -func PatchSecret(path []string, value any) error { - if SecretPath == "" { - return errors.New("secret file disabled") + if s.Values == nil { + return nil, fmt.Errorf("no values in secret %s", s.Name) } - // empty config is OK - b, _ := os.ReadFile(SecretPath) - - b, err := yaml.Patch(b, append([]string{"secret"}, path...), value) + data, err := yaml.Encode(s.Values, 2) if err != nil { - return err + return nil, fmt.Errorf("error encoding secret values: %w", err) } - if err := os.WriteFile(SecretPath, b, 0644); err == nil { - secrets = [][]byte{b} - } - - return err + return data, nil } -func initSecret(secret string) { - if secret == "" { - secret = "go2rtc.secrets" +func (s *Secret) Unmarshal(v any) error { + secretsMu.Lock() + defer secretsMu.Unlock() + + if s.Values == nil { + return fmt.Errorf("no values in secret %s", s.Name) } - SecretPath = secret - - if SecretPath != "" { - if !filepath.IsAbs(SecretPath) { - if cwd, err := os.Getwd(); err == nil { - SecretPath = filepath.Join(cwd, SecretPath) - } - } - Info["secret_path"] = SecretPath + data, err := yaml.Encode(s.Values, 2) + if err != nil { + return fmt.Errorf("error encoding secret values: %w", err) } - if data, err := os.ReadFile(SecretPath); err == nil { - secrets = append(secrets, data) + if err := yaml.Unmarshal(data, v); err != nil { + return fmt.Errorf("error unmarshaling secret values: %w", err) + } + + return nil +} + +func (s *Secret) Save() error { + secretsMu.Lock() + defer secretsMu.Unlock() + return saveSecret(s.Name, s.Values) +} + +func initSecrets() { + var cfg struct { + Secrets map[string]map[string]any `yaml:"secrets"` + } + + /* + Example config: + secrets: + test_camera: + username: test + password: test + */ + + LoadConfig(&cfg) + + if cfg.Secrets == nil { + return + } + + secretsMu.Lock() + defer secretsMu.Unlock() + + for name, values := range cfg.Secrets { + secrets[name] = &Secret{Name: name, Values: values} } } -func getNestedValue(m map[string]interface{}, path []string) interface{} { +func saveSecret(name string, secret map[string]any) error { + return PatchConfig([]string{"secrets", name}, secret) +} + +func getNestedValue(m map[string]any, path []string) interface{} { if len(path) == 0 || m == nil { return nil } @@ -120,10 +193,10 @@ func getNestedValue(m map[string]interface{}, path []string) interface{} { // Check nested maps switch nextMap := value.(type) { - case map[string]interface{}: + case map[string]any: return getNestedValue(nextMap, path[1:]) case map[interface{}]interface{}: - stringMap := make(map[string]interface{}) + stringMap := make(map[string]any) for k, v := range nextMap { if keyStr, ok := k.(string); ok { stringMap[keyStr] = v diff --git a/pkg/core/listener.go b/pkg/core/listener.go index 2b7e198b..2840880d 100644 --- a/pkg/core/listener.go +++ b/pkg/core/listener.go @@ -19,10 +19,6 @@ func (l *Listener) Fire(msg any) { } } -func (l *Listener) ParseSource(url string) string { - return app.ResolveSecrets(url) -} - -func (l *Listener) SaveSource(path []string, value any) error { - return app.PatchSecret(path, value) +func (l *Listener) NewSecret(name string, defaultValues interface{}) *app.Secret { + return app.NewSecret(name, defaultValues) } \ No newline at end of file From a1f0b86ab3e0e0f00752c4aafe15bb35e2f37ddf Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 20 May 2025 22:29:27 +0200 Subject: [PATCH 07/18] format --- internal/app/app.go | 5 ----- internal/app/secrets.go | 44 ++++++++++++++++++++--------------------- pkg/core/listener.go | 2 +- 3 files changed, 23 insertions(+), 28 deletions(-) diff --git a/internal/app/app.go b/internal/app/app.go index 706841ec..4b89daa9 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -13,7 +13,6 @@ var ( Version string UserAgent string ConfigPath string - SecretPath string Info = make(map[string]any) ) @@ -78,10 +77,6 @@ func Init() { if ConfigPath != "" { Logger.Info().Str("path", ConfigPath).Msg("config") } - - if SecretPath != "" { - Logger.Info().Str("path", SecretPath).Msg("secrets") - } } func readRevisionTime() (revision, vcsTime string) { diff --git a/internal/app/secrets.go b/internal/app/secrets.go index b7773b65..3c3cbbfe 100644 --- a/internal/app/secrets.go +++ b/internal/app/secrets.go @@ -26,35 +26,35 @@ type Secrets interface { type Secret struct { Secrets - Name string + Name string Values map[string]any } func NewSecret(name string, values interface{}) *Secret { - secretsMu.Lock() - defer secretsMu.Unlock() + secretsMu.Lock() + defer secretsMu.Unlock() - if s, exists := secrets[name]; exists { - return s - } + if s, exists := secrets[name]; exists { + return s + } - s := &Secret{Name: name, Values: make(map[string]any)} - - switch v := values.(type) { - case map[string]any: - s.Values = v - default: - data, err := yaml.Encode(values, 2) - if err == nil { - var mapValues map[string]any - if err := yaml.Unmarshal(data, &mapValues); err == nil { - s.Values = mapValues - } - } - } + s := &Secret{Name: name, Values: make(map[string]any)} - secrets[name] = s - return s + switch v := values.(type) { + case map[string]any: + s.Values = v + default: + data, err := yaml.Encode(values, 2) + if err == nil { + var mapValues map[string]any + if err := yaml.Unmarshal(data, &mapValues); err == nil { + s.Values = mapValues + } + } + } + + secrets[name] = s + return s } func (s *Secret) Get(key string) any { diff --git a/pkg/core/listener.go b/pkg/core/listener.go index 2840880d..7a512741 100644 --- a/pkg/core/listener.go +++ b/pkg/core/listener.go @@ -21,4 +21,4 @@ func (l *Listener) Fire(msg any) { func (l *Listener) NewSecret(name string, defaultValues interface{}) *app.Secret { return app.NewSecret(name, defaultValues) -} \ No newline at end of file +} From 24310e2f7a6d6ae8d731f72024b1d89278533d93 Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 20 May 2025 22:44:07 +0200 Subject: [PATCH 08/18] remove parse --- internal/app/secrets.go | 31 ------------------------------- 1 file changed, 31 deletions(-) diff --git a/internal/app/secrets.go b/internal/app/secrets.go index 3c3cbbfe..e0acc901 100644 --- a/internal/app/secrets.go +++ b/internal/app/secrets.go @@ -2,8 +2,6 @@ package app import ( "fmt" - "regexp" - "strings" "sync" "github.com/AlexxIT/go2rtc/pkg/yaml" @@ -12,8 +10,6 @@ import ( var secrets = make(map[string]*Secret) var secretsMu sync.Mutex -var templateRegex = regexp.MustCompile(`\{\{\s*([^\}]+)\s*\}\}`) - type Secrets interface { Get(key string) any Set(key string, value any) @@ -76,33 +72,6 @@ func (s *Secret) Set(key string, value any) { secrets[s.Name] = s } -func (s *Secret) Parse(template string) string { - if !templateRegex.MatchString(template) { - return template - } - - secretsMu.Lock() - defer secretsMu.Unlock() - - if _, exists := secrets[s.Name]; !exists { - return template - } - - result := templateRegex.ReplaceAllStringFunc(template, func(match string) string { - varName := strings.TrimSpace(templateRegex.FindStringSubmatch(match)[1]) - pathParts := strings.Split(varName, ".") - value := getNestedValue(s.Values, pathParts) - - if value != nil { - return stringify(value) - } - - return "" - }) - - return result -} - func (s *Secret) Marshal(v any) ([]byte, error) { secretsMu.Lock() defer secretsMu.Unlock() From e0687db9e2178c49fa62f922589b820720b9ea8e Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 20 May 2025 23:07:04 +0200 Subject: [PATCH 09/18] add template parsing --- internal/app/secrets.go | 35 +++++++++++++++++++++++++++++++++++ internal/streams/handlers.go | 13 +++++++++++++ 2 files changed, 48 insertions(+) diff --git a/internal/app/secrets.go b/internal/app/secrets.go index e0acc901..c3c4f95e 100644 --- a/internal/app/secrets.go +++ b/internal/app/secrets.go @@ -2,6 +2,8 @@ package app import ( "fmt" + "regexp" + "strings" "sync" "github.com/AlexxIT/go2rtc/pkg/yaml" @@ -10,6 +12,8 @@ import ( var secrets = make(map[string]*Secret) var secretsMu sync.Mutex +var templateRegex = regexp.MustCompile(`\{\{\s*([^\}]+)\s*\}\}`) + type Secrets interface { Get(key string) any Set(key string, value any) @@ -53,6 +57,10 @@ func NewSecret(name string, values interface{}) *Secret { return s } +func GetSecret(name string) *Secret { + return secrets[name] +} + func (s *Secret) Get(key string) any { secretsMu.Lock() defer secretsMu.Unlock() @@ -72,6 +80,33 @@ func (s *Secret) Set(key string, value any) { secrets[s.Name] = s } +func (s *Secret) Parse(template string) string { + if !templateRegex.MatchString(template) { + return template + } + + secretsMu.Lock() + defer secretsMu.Unlock() + + if _, exists := secrets[s.Name]; !exists { + return template + } + + result := templateRegex.ReplaceAllStringFunc(template, func(match string) string { + varName := strings.TrimSpace(templateRegex.FindStringSubmatch(match)[1]) + pathParts := strings.Split(varName, ".") + value := getNestedValue(s.Values, pathParts) + + if value != nil { + return stringify(value) + } + + return "" + }) + + return result +} + func (s *Secret) Marshal(v any) ([]byte, error) { secretsMu.Lock() defer secretsMu.Unlock() diff --git a/internal/streams/handlers.go b/internal/streams/handlers.go index 3240abb5..991b69c9 100644 --- a/internal/streams/handlers.go +++ b/internal/streams/handlers.go @@ -4,6 +4,7 @@ import ( "errors" "strings" + "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/pkg/core" ) @@ -46,6 +47,18 @@ func GetProducer(url string) (core.Producer, error) { } if handler, ok := handlers[scheme]; ok { + index := strings.IndexByte(url, '#') + if index > 0 { + _, query := url[:index], ParseQuery(url[index+1:]) + secretsName := query.Get("secrets") + if secretsName != "" { + secrets := app.GetSecret(secretsName) + if secrets != nil { + url = secrets.Parse(url) + } + } + } + return handler(url) } } From bf45f64a7e3f9eaab0c977e2555c4541da240f72 Mon Sep 17 00:00:00 2001 From: seydx Date: Mon, 26 May 2025 21:56:45 +0200 Subject: [PATCH 10/18] - refactor secrets - add support for env in config - redact sensitive information in logs/responses --- internal/api/api.go | 20 ++++- internal/app/secrets.go | 162 +++++++++-------------------------- internal/echo/echo.go | 2 +- internal/expr/expr.go | 3 +- internal/hass/hass.go | 3 +- internal/hls/hls.go | 7 +- internal/onvif/onvif.go | 5 +- internal/streams/dot.go | 4 +- internal/streams/handlers.go | 13 --- internal/streams/producer.go | 11 +-- internal/streams/streams.go | 3 +- pkg/core/helpers.go | 6 ++ pkg/core/listener.go | 6 -- pkg/shell/shell.go | 117 +++++++++++++++++++++++++ 14 files changed, 202 insertions(+), 160 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 419e2bdf..241bedf3 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -1,6 +1,7 @@ package api import ( + "bytes" "crypto/tls" "encoding/json" "fmt" @@ -14,6 +15,7 @@ import ( "time" "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" ) @@ -164,10 +166,20 @@ func ResponseJSON(w http.ResponseWriter, v any) { } func ResponsePrettyJSON(w http.ResponseWriter, v any) { - w.Header().Set("Content-Type", MimeJSON) - enc := json.NewEncoder(w) + w.Header().Set("Content-Type", "application/json") + + var buf bytes.Buffer + enc := json.NewEncoder(&buf) enc.SetIndent("", " ") - _ = enc.Encode(v) + err := enc.Encode(v) + + if err != nil { + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + redactedJSON := shell.Redact(buf.String()) + w.Write([]byte(redactedJSON)) } func Response(w http.ResponseWriter, body any, contentType string) { @@ -190,7 +202,7 @@ var log zerolog.Logger func middlewareLog(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Trace().Msgf("[api] %s %s %s", r.Method, r.URL, r.RemoteAddr) + log.Trace().Msgf("[api] %s %s %s", r.Method, shell.Redact(r.URL.String()), r.RemoteAddr) next.ServeHTTP(w, r) }) } diff --git a/internal/app/secrets.go b/internal/app/secrets.go index c3c4f95e..4735c27c 100644 --- a/internal/app/secrets.go +++ b/internal/app/secrets.go @@ -1,23 +1,19 @@ package app import ( - "fmt" - "regexp" - "strings" "sync" "github.com/AlexxIT/go2rtc/pkg/yaml" ) -var secrets = make(map[string]*Secret) -var secretsMu sync.Mutex - -var templateRegex = regexp.MustCompile(`\{\{\s*([^\}]+)\s*\}\}`) +var ( + secrets = make(map[string]*Secret) + secretsMu sync.Mutex +) type Secrets interface { Get(key string) any Set(key string, value any) - Parse(template string) string Marshal(v any) ([]byte, error) Unmarshal(v any) error Save() error @@ -27,37 +23,36 @@ type Secret struct { Secrets Name string - Values map[string]any + Values map[string]string } -func NewSecret(name string, values interface{}) *Secret { +func NewSecret(name string, values interface{}) (*Secret, error) { secretsMu.Lock() defer secretsMu.Unlock() if s, exists := secrets[name]; exists { - return s + return s, nil } - s := &Secret{Name: name, Values: make(map[string]any)} + s := &Secret{Name: name, Values: make(map[string]string)} - switch v := values.(type) { - case map[string]any: - s.Values = v - default: - data, err := yaml.Encode(values, 2) - if err == nil { - var mapValues map[string]any - if err := yaml.Unmarshal(data, &mapValues); err == nil { - s.Values = mapValues - } - } + data, err := yaml.Encode(values, 2) + if err != nil { + return nil, err + } + + if err := yaml.Unmarshal(data, &s.Values); err != nil { + return nil, err } secrets[name] = s - return s + + return s, nil } func GetSecret(name string) *Secret { + secretsMu.Lock() + defer secretsMu.Unlock() return secrets[name] } @@ -65,79 +60,50 @@ func (s *Secret) Get(key string) any { secretsMu.Lock() defer secretsMu.Unlock() + if s.Values == nil { + return nil + } + return s.Values[key] } -func (s *Secret) Set(key string, value any) { +func (s *Secret) Set(key string, value string) { secretsMu.Lock() defer secretsMu.Unlock() if s.Values == nil { - s.Values = make(map[string]any) + s.Values = make(map[string]string) } s.Values[key] = value - secrets[s.Name] = s } -func (s *Secret) Parse(template string) string { - if !templateRegex.MatchString(template) { - return template - } - - secretsMu.Lock() - defer secretsMu.Unlock() - - if _, exists := secrets[s.Name]; !exists { - return template - } - - result := templateRegex.ReplaceAllStringFunc(template, func(match string) string { - varName := strings.TrimSpace(templateRegex.FindStringSubmatch(match)[1]) - pathParts := strings.Split(varName, ".") - value := getNestedValue(s.Values, pathParts) - - if value != nil { - return stringify(value) - } - - return "" - }) - - return result -} - -func (s *Secret) Marshal(v any) ([]byte, error) { +func (s *Secret) Marshal() (interface{}, error) { secretsMu.Lock() defer secretsMu.Unlock() if s.Values == nil { - return nil, fmt.Errorf("no values in secret %s", s.Name) + return make(map[string]any), nil } - data, err := yaml.Encode(s.Values, 2) - if err != nil { - return nil, fmt.Errorf("error encoding secret values: %w", err) - } - - return data, nil + return s.Values, nil } -func (s *Secret) Unmarshal(v any) error { +func (s *Secret) Unmarshal(value any) error { secretsMu.Lock() defer secretsMu.Unlock() if s.Values == nil { - return fmt.Errorf("no values in secret %s", s.Name) + s.Values = make(map[string]string) } - data, err := yaml.Encode(s.Values, 2) + data, err := yaml.Encode(value, 2) if err != nil { - return fmt.Errorf("error encoding secret values: %w", err) + return err } - if err := yaml.Unmarshal(data, v); err != nil { - return fmt.Errorf("error unmarshaling secret values: %w", err) + if err := yaml.Unmarshal(data, value); err != nil { + return err } return nil @@ -151,17 +117,9 @@ func (s *Secret) Save() error { func initSecrets() { var cfg struct { - Secrets map[string]map[string]any `yaml:"secrets"` + Secrets map[string]map[string]string `yaml:"secrets"` } - /* - Example config: - secrets: - test_camera: - username: test - password: test - */ - LoadConfig(&cfg) if cfg.Secrets == nil { @@ -172,53 +130,13 @@ func initSecrets() { defer secretsMu.Unlock() for name, values := range cfg.Secrets { - secrets[name] = &Secret{Name: name, Values: values} - } -} - -func saveSecret(name string, secret map[string]any) error { - return PatchConfig([]string{"secrets", name}, secret) -} - -func getNestedValue(m map[string]any, path []string) interface{} { - if len(path) == 0 || m == nil { - return nil - } - - key := path[0] - value, exists := m[key] - if !exists { - return nil - } - - if len(path) == 1 { - return value - } - - // Check nested maps - switch nextMap := value.(type) { - case map[string]any: - return getNestedValue(nextMap, path[1:]) - case map[interface{}]interface{}: - stringMap := make(map[string]any) - for k, v := range nextMap { - if keyStr, ok := k.(string); ok { - stringMap[keyStr] = v - } + secrets[name] = &Secret{ + Name: name, + Values: values, } - return getNestedValue(stringMap, path[1:]) - default: - return nil } } -func stringify(value interface{}) string { - switch v := value.(type) { - case string: - return v - case int, int64, float64, bool: - return fmt.Sprintf("%v", v) - default: - return "" - } +func saveSecret(name string, secretValues map[string]string) error { + return PatchConfig([]string{"secrets", name}, secretValues) } diff --git a/internal/echo/echo.go b/internal/echo/echo.go index fb105cec..df88dd64 100644 --- a/internal/echo/echo.go +++ b/internal/echo/echo.go @@ -22,7 +22,7 @@ func Init() { b = bytes.TrimSpace(b) - log.Debug().Str("url", url).Msgf("[echo] %s", b) + log.Debug().Str("url", shell.Redact(url)).Msgf("[echo] %s", b) return string(b), nil }) diff --git a/internal/expr/expr.go b/internal/expr/expr.go index 8fd6c9c2..4d8aa5ce 100644 --- a/internal/expr/expr.go +++ b/internal/expr/expr.go @@ -6,6 +6,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/expr" + "github.com/AlexxIT/go2rtc/pkg/shell" ) func Init() { @@ -17,7 +18,7 @@ func Init() { return "", err } - log.Debug().Msgf("[expr] url=%s", url) + log.Debug().Msgf("[expr] url=%s", shell.Redact(url)) if url = v.(string); url == "" { return "", errors.New("expr: result is empty") diff --git a/internal/hass/hass.go b/internal/hass/hass.go index ea172b02..e2132ad6 100644 --- a/internal/hass/hass.go +++ b/internal/hass/hass.go @@ -15,6 +15,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/hass" + "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" ) @@ -177,7 +178,7 @@ func importConfig(config string) error { continue } - log.Debug().Str("url", "hass:"+entrie.Title).Msg("[hass] load config") + log.Debug().Str("url", "hass:"+shell.Redact(entrie.Title)).Msg("[hass] load config") //streams.Get("hass:" + entrie.Title) } diff --git a/internal/hls/hls.go b/internal/hls/hls.go index 5c136450..2344b62e 100644 --- a/internal/hls/hls.go +++ b/internal/hls/hls.go @@ -12,6 +12,7 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/mpegts" + "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" ) @@ -142,7 +143,7 @@ func handlerSegmentTS(w http.ResponseWriter, r *http.Request) { data := session.Segment() if data == nil { - log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery) + log.Warn().Msgf("[hls] can't get segment %s", shell.Redact(r.URL.RawQuery)) http.NotFound(w, r) return } @@ -172,7 +173,7 @@ func handlerInit(w http.ResponseWriter, r *http.Request) { data := session.Init() if data == nil { - log.Warn().Msgf("[hls] can't get init %s", r.URL.RawQuery) + log.Warn().Msgf("[hls] can't get init %s", shell.Redact(r.URL.RawQuery)) http.NotFound(w, r) return } @@ -206,7 +207,7 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) { data := session.Segment() if data == nil { - log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery) + log.Warn().Msgf("[hls] can't get segment %s", shell.Redact(r.URL.RawQuery)) http.NotFound(w, r) return } diff --git a/internal/onvif/onvif.go b/internal/onvif/onvif.go index 6dfa633a..36b3843c 100644 --- a/internal/onvif/onvif.go +++ b/internal/onvif/onvif.go @@ -15,6 +15,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/onvif" + "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" ) @@ -165,12 +166,12 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) { for _, rawURL := range urls { u, err := url.Parse(rawURL) if err != nil { - log.Warn().Str("url", rawURL).Msg("[onvif] broken") + log.Warn().Str("url", shell.Redact(rawURL)).Msg("[onvif] broken") continue } if u.Scheme != "http" { - log.Warn().Str("url", rawURL).Msg("[onvif] unsupported") + log.Warn().Str("url", shell.Redact(rawURL)).Msg("[onvif] unsupported") continue } diff --git a/internal/streams/dot.go b/internal/streams/dot.go index e0417972..2c357f77 100644 --- a/internal/streams/dot.go +++ b/internal/streams/dot.go @@ -4,6 +4,8 @@ import ( "encoding/json" "fmt" "strings" + + "github.com/AlexxIT/go2rtc/pkg/shell" ) func AppendDOT(dot []byte, stream *Stream) []byte { @@ -166,7 +168,7 @@ func (c *conn) label() string { sb.WriteString("\nsource=" + c.Source) } if c.URL != "" { - sb.WriteString("\nurl=" + c.URL) + sb.WriteString("\nurl=" + shell.Redact(c.URL)) } if c.UserAgent != "" { sb.WriteString("\nuser_agent=" + c.UserAgent) diff --git a/internal/streams/handlers.go b/internal/streams/handlers.go index 991b69c9..3240abb5 100644 --- a/internal/streams/handlers.go +++ b/internal/streams/handlers.go @@ -4,7 +4,6 @@ import ( "errors" "strings" - "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/pkg/core" ) @@ -47,18 +46,6 @@ func GetProducer(url string) (core.Producer, error) { } if handler, ok := handlers[scheme]; ok { - index := strings.IndexByte(url, '#') - if index > 0 { - _, query := url[:index], ParseQuery(url[index+1:]) - secretsName := query.Get("secrets") - if secretsName != "" { - secrets := app.GetSecret(secretsName) - if secrets != nil { - url = secrets.Parse(url) - } - } - } - return handler(url) } } diff --git a/internal/streams/producer.go b/internal/streams/producer.go index 09e2dcc5..4260198e 100644 --- a/internal/streams/producer.go +++ b/internal/streams/producer.go @@ -8,6 +8,7 @@ import ( "time" "github.com/AlexxIT/go2rtc/pkg/core" + "github.com/AlexxIT/go2rtc/pkg/shell" ) type state byte @@ -149,7 +150,7 @@ func (p *Producer) start() { return } - log.Debug().Msgf("[streams] start producer url=%s", p.url) + log.Debug().Msgf("[streams] start producer url=%s", shell.Redact(p.url)) p.state = stateStart p.workerID++ @@ -167,7 +168,7 @@ func (p *Producer) worker(conn core.Producer, workerID int) { return } - log.Warn().Err(err).Str("url", p.url).Caller().Send() + log.Warn().Err(err).Str("url", shell.Redact(p.url)).Caller().Send() } p.reconnect(workerID, 0) @@ -178,11 +179,11 @@ func (p *Producer) reconnect(workerID, retry int) { defer p.mu.Unlock() if p.workerID != workerID { - log.Trace().Msgf("[streams] stop reconnect url=%s", p.url) + log.Trace().Msgf("[streams] stop reconnect url=%s", shell.Redact(p.url)) return } - log.Debug().Msgf("[streams] retry=%d to url=%s", retry, p.url) + log.Debug().Msgf("[streams] retry=%d to url=%s", retry, shell.Redact(p.url)) conn, err := GetProducer(p.url) if err != nil { @@ -257,7 +258,7 @@ func (p *Producer) stop() { p.workerID++ } - log.Debug().Msgf("[streams] stop producer url=%s", p.url) + log.Debug().Msgf("[streams] stop producer url=%s", shell.Redact(p.url)) if p.conn != nil { _ = p.conn.Stop() diff --git a/internal/streams/streams.go b/internal/streams/streams.go index dcbaba28..8731ae68 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -9,6 +9,7 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" ) @@ -127,7 +128,7 @@ func GetOrPatch(query url.Values) *Stream { // check if name param provided if name := query.Get("name"); name != "" { - log.Info().Msgf("[streams] create new stream url=%s", source) + log.Info().Msgf("[streams] create new stream url=%s", shell.Redact(source)) return Patch(name, source) } diff --git a/pkg/core/helpers.go b/pkg/core/helpers.go index 72afe897..f39f53b4 100644 --- a/pkg/core/helpers.go +++ b/pkg/core/helpers.go @@ -6,6 +6,8 @@ import ( "strconv" "strings" "time" + + "github.com/AlexxIT/go2rtc/internal/app" ) const ( @@ -77,3 +79,7 @@ func Caller() string { _, file, line, _ := runtime.Caller(1) return file + ":" + strconv.Itoa(line) } + +func NewSecret(name string, defaultValues interface{}) (*app.Secret, error) { + return app.NewSecret(name, defaultValues) +} diff --git a/pkg/core/listener.go b/pkg/core/listener.go index 7a512741..75d9202a 100644 --- a/pkg/core/listener.go +++ b/pkg/core/listener.go @@ -1,7 +1,5 @@ package core -import "github.com/AlexxIT/go2rtc/internal/app" - type EventFunc func(msg any) // Listener base struct for all classes with support feedback @@ -18,7 +16,3 @@ func (l *Listener) Fire(msg any) { f(msg) } } - -func (l *Listener) NewSecret(name string, defaultValues interface{}) *app.Secret { - return app.NewSecret(name, defaultValues) -} diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index 75df671f..5388503a 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -1,12 +1,22 @@ package shell import ( + "fmt" "os" "os/signal" "path/filepath" "regexp" "strings" + "sync" "syscall" + + "github.com/AlexxIT/go2rtc/pkg/yaml" +) + +var ( + secretReplacer *strings.Replacer + secretValues map[string]bool // Tracker für alle bekannten Secret-Werte + secretMutex sync.RWMutex ) func QuoteSplit(s string) []string { @@ -40,6 +50,15 @@ func QuoteSplit(s string) []string { // ReplaceEnvVars - support format ${CAMERA_PASSWORD} and ${RTSP_USER:admin} func ReplaceEnvVars(text string) string { + var cfg struct { + Env map[string]string `yaml:"env"` + Secrets map[string]map[string]string `yaml:"secrets"` + } + + yaml.Unmarshal([]byte(text), &cfg) + + buildSecretReplacer(cfg) + re := regexp.MustCompile(`\${([^}{]+)}`) return re.ReplaceAllStringFunc(text, func(match string) string { key := match[2 : len(match)-1] @@ -63,6 +82,23 @@ func ReplaceEnvVars(text string) string { return value } + if cfg.Env != nil { + if value, ok := cfg.Env[key]; ok { + return value + } + } + + if cfg.Secrets != nil { + for secretName, secretValues := range cfg.Secrets { + for k, v := range secretValues { + name := fmt.Sprintf("%s_%s", secretName, k) + if key == name { + return v + } + } + } + } + if dok { return def } @@ -76,3 +112,84 @@ func RunUntilSignal() { signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) println("exit with signal:", (<-sigs).String()) } + +func Redact(text string) string { + secretMutex.RLock() + defer secretMutex.RUnlock() + + if secretReplacer == nil { + return text + } + + return secretReplacer.Replace(text) +} + +func buildSecretReplacer(cfg struct { + Env map[string]string `yaml:"env"` + Secrets map[string]map[string]string `yaml:"secrets"` +}) { + secretMutex.Lock() + defer secretMutex.Unlock() + + if secretValues == nil { + secretValues = make(map[string]bool) + } + + var newSecrets []string + + if dir, ok := os.LookupEnv("CREDENTIALS_DIRECTORY"); ok { + entries, err := os.ReadDir(dir) + if err == nil { + for _, entry := range entries { + if !entry.IsDir() { + value, err := os.ReadFile(filepath.Join(dir, entry.Name())) + if err == nil { + cleanValue := strings.TrimSpace(string(value)) + if len(cleanValue) > 0 && !secretValues[cleanValue] { + secretValues[cleanValue] = true + newSecrets = append(newSecrets, cleanValue) + } + } + } + } + } + } + + if cfg.Env != nil { + for _, value := range cfg.Env { + if len(value) > 0 && !secretValues[value] { + secretValues[value] = true + newSecrets = append(newSecrets, value) + } + } + } + + if cfg.Secrets != nil { + for _, secretMap := range cfg.Secrets { + for _, value := range secretMap { + if len(value) > 0 && !secretValues[value] { + secretValues[value] = true + newSecrets = append(newSecrets, value) + } + } + } + } + + if len(newSecrets) > 0 { + rebuildReplacer() + } +} + +func rebuildReplacer() { + var replacements []string + + for secret := range secretValues { + replacements = append(replacements, secret, "*****") + } + + if len(replacements) > 0 { + secretReplacer = strings.NewReplacer(replacements...) + } else { + secretReplacer = nil + } +} \ No newline at end of file From 7c17e64090ff6ceec8fd8165d671ace6eccfd393 Mon Sep 17 00:00:00 2001 From: seydx Date: Mon, 26 May 2025 22:21:33 +0200 Subject: [PATCH 11/18] format --- internal/api/api.go | 6 +++--- pkg/shell/shell.go | 22 +++++++++++----------- 2 files changed, 14 insertions(+), 14 deletions(-) diff --git a/internal/api/api.go b/internal/api/api.go index 241bedf3..f66f59f4 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -167,17 +167,17 @@ func ResponseJSON(w http.ResponseWriter, v any) { func ResponsePrettyJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", "application/json") - + var buf bytes.Buffer enc := json.NewEncoder(&buf) enc.SetIndent("", " ") err := enc.Encode(v) - + if err != nil { http.Error(w, "Internal Server Error", http.StatusInternalServerError) return } - + redactedJSON := shell.Redact(buf.String()) w.Write([]byte(redactedJSON)) } diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index 5388503a..c478b435 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -116,11 +116,11 @@ func RunUntilSignal() { func Redact(text string) string { secretMutex.RLock() defer secretMutex.RUnlock() - + if secretReplacer == nil { return text } - + return secretReplacer.Replace(text) } @@ -130,13 +130,13 @@ func buildSecretReplacer(cfg struct { }) { secretMutex.Lock() defer secretMutex.Unlock() - + if secretValues == nil { secretValues = make(map[string]bool) } - + var newSecrets []string - + if dir, ok := os.LookupEnv("CREDENTIALS_DIRECTORY"); ok { entries, err := os.ReadDir(dir) if err == nil { @@ -154,7 +154,7 @@ func buildSecretReplacer(cfg struct { } } } - + if cfg.Env != nil { for _, value := range cfg.Env { if len(value) > 0 && !secretValues[value] { @@ -163,7 +163,7 @@ func buildSecretReplacer(cfg struct { } } } - + if cfg.Secrets != nil { for _, secretMap := range cfg.Secrets { for _, value := range secretMap { @@ -174,7 +174,7 @@ func buildSecretReplacer(cfg struct { } } } - + if len(newSecrets) > 0 { rebuildReplacer() } @@ -182,14 +182,14 @@ func buildSecretReplacer(cfg struct { func rebuildReplacer() { var replacements []string - + for secret := range secretValues { replacements = append(replacements, secret, "*****") } - + if len(replacements) > 0 { secretReplacer = strings.NewReplacer(replacements...) } else { secretReplacer = nil } -} \ No newline at end of file +} From 759f979182d248f14ccd7d2350817cc61471eb50 Mon Sep 17 00:00:00 2001 From: seydx Date: Mon, 26 May 2025 22:23:24 +0200 Subject: [PATCH 12/18] dont redact config.env values --- pkg/shell/shell.go | 9 --------- 1 file changed, 9 deletions(-) diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index c478b435..49fb432a 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -155,15 +155,6 @@ func buildSecretReplacer(cfg struct { } } - if cfg.Env != nil { - for _, value := range cfg.Env { - if len(value) > 0 && !secretValues[value] { - secretValues[value] = true - newSecrets = append(newSecrets, value) - } - } - } - if cfg.Secrets != nil { for _, secretMap := range cfg.Secrets { for _, value := range secretMap { From 42a67f8ad5ecb684b2d530c2b884a299604d6588 Mon Sep 17 00:00:00 2001 From: seydx Date: Fri, 6 Jun 2025 02:18:00 +0200 Subject: [PATCH 13/18] comments --- pkg/shell/shell.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index 49fb432a..64931a91 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -15,7 +15,7 @@ import ( var ( secretReplacer *strings.Replacer - secretValues map[string]bool // Tracker für alle bekannten Secret-Werte + secretValues map[string]bool secretMutex sync.RWMutex ) From 3a0e4078fdb63cb9d496b7376f008c4fd183943e Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 30 Sep 2025 14:48:58 +0200 Subject: [PATCH 14/18] Dont redact hass entry title --- internal/hass/hass.go | 3 +-- 1 file changed, 1 insertion(+), 2 deletions(-) diff --git a/internal/hass/hass.go b/internal/hass/hass.go index e2132ad6..ea172b02 100644 --- a/internal/hass/hass.go +++ b/internal/hass/hass.go @@ -15,7 +15,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/hass" - "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" ) @@ -178,7 +177,7 @@ func importConfig(config string) error { continue } - log.Debug().Str("url", "hass:"+shell.Redact(entrie.Title)).Msg("[hass] load config") + log.Debug().Str("url", "hass:"+entrie.Title).Msg("[hass] load config") //streams.Get("hass:" + entrie.Title) } From 50d5fa93b6d01cfaec0579052c5026f1f7303984 Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 30 Sep 2025 14:50:57 +0200 Subject: [PATCH 15/18] Set Content-Type header to MimeJSON in ResponsePrettyJSON function --- internal/api/api.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/internal/api/api.go b/internal/api/api.go index f66f59f4..3dd16d3d 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -166,7 +166,7 @@ func ResponseJSON(w http.ResponseWriter, v any) { } func ResponsePrettyJSON(w http.ResponseWriter, v any) { - w.Header().Set("Content-Type", "application/json") + w.Header().Set("Content-Type", MimeJSON) var buf bytes.Buffer enc := json.NewEncoder(&buf) From 0c5a2bf02ba8b9a4cd48bc0b49c5b8cbb11d2ba6 Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 30 Sep 2025 15:21:30 +0200 Subject: [PATCH 16/18] Remove NewSecret function and related import from helpers.go --- pkg/core/helpers.go | 6 ------ 1 file changed, 6 deletions(-) diff --git a/pkg/core/helpers.go b/pkg/core/helpers.go index c39a1eee..161a5504 100644 --- a/pkg/core/helpers.go +++ b/pkg/core/helpers.go @@ -7,8 +7,6 @@ import ( "strconv" "strings" "time" - - "github.com/AlexxIT/go2rtc/internal/app" ) const ( @@ -91,7 +89,3 @@ func StripUserinfo(s string) string { sanitizer := regexp.MustCompile(`://[` + userinfo + `]+@`) return sanitizer.ReplaceAllString(s, `://***@`) } - -func NewSecret(name string, defaultValues interface{}) (*app.Secret, error) { - return app.NewSecret(name, defaultValues) -} From 670370056cfd986ea1632af05e9b6c1d1252f08d Mon Sep 17 00:00:00 2001 From: seydx Date: Tue, 30 Sep 2025 15:35:32 +0200 Subject: [PATCH 17/18] Refactor secrets management --- internal/app/secrets.go | 46 ++++++++++++++++++----------------------- pkg/secrets/secrets.go | 44 +++++++++++++++++++++++++++++++++++++++ 2 files changed, 64 insertions(+), 26 deletions(-) create mode 100644 pkg/secrets/secrets.go diff --git a/internal/app/secrets.go b/internal/app/secrets.go index 4735c27c..e1ce7509 100644 --- a/internal/app/secrets.go +++ b/internal/app/secrets.go @@ -3,34 +3,23 @@ package app import ( "sync" + "github.com/AlexxIT/go2rtc/pkg/secrets" "github.com/AlexxIT/go2rtc/pkg/yaml" ) var ( - secrets = make(map[string]*Secret) - secretsMu sync.Mutex + secretsMap = make(map[string]*Secret) + secretsMu sync.Mutex ) -type Secrets interface { - Get(key string) any - Set(key string, value any) - Marshal(v any) ([]byte, error) - Unmarshal(v any) error - Save() error -} +// SecretsManager implements secrets.SecretsManager interface +type SecretsManager struct{} -type Secret struct { - Secrets - - Name string - Values map[string]string -} - -func NewSecret(name string, values interface{}) (*Secret, error) { +func (m *SecretsManager) NewSecret(name string, values interface{}) (secrets.Secret, error) { secretsMu.Lock() defer secretsMu.Unlock() - if s, exists := secrets[name]; exists { + if s, exists := secretsMap[name]; exists { return s, nil } @@ -45,15 +34,21 @@ func NewSecret(name string, values interface{}) (*Secret, error) { return nil, err } - secrets[name] = s + secretsMap[name] = s return s, nil } -func GetSecret(name string) *Secret { +func (m *SecretsManager) GetSecret(name string) secrets.Secret { secretsMu.Lock() defer secretsMu.Unlock() - return secrets[name] + return secretsMap[name] +} + +// Secret implements secrets.Secret interface +type Secret struct { + Name string + Values map[string]string } func (s *Secret) Get(key string) any { @@ -112,7 +107,7 @@ func (s *Secret) Unmarshal(value any) error { func (s *Secret) Save() error { secretsMu.Lock() defer secretsMu.Unlock() - return saveSecret(s.Name, s.Values) + return PatchConfig([]string{"secrets", s.Name}, s.Values) } func initSecrets() { @@ -130,13 +125,12 @@ func initSecrets() { defer secretsMu.Unlock() for name, values := range cfg.Secrets { - secrets[name] = &Secret{ + secretsMap[name] = &Secret{ Name: name, Values: values, } } -} -func saveSecret(name string, secretValues map[string]string) error { - return PatchConfig([]string{"secrets", name}, secretValues) + // Register + secrets.SetManager(&SecretsManager{}) } diff --git a/pkg/secrets/secrets.go b/pkg/secrets/secrets.go new file mode 100644 index 00000000..071d9526 --- /dev/null +++ b/pkg/secrets/secrets.go @@ -0,0 +1,44 @@ +package secrets + +import ( + "errors" + "sync" +) + +type SecretsManager interface { + NewSecret(name string, defaultValues interface{}) (Secret, error) + GetSecret(name string) Secret +} + +type Secret interface { + Get(key string) any + Set(key string, value string) + Marshal() (interface{}, error) + Unmarshal(value any) error + Save() error +} + +var manager SecretsManager +var once sync.Once + +func SetManager(m SecretsManager) { + once.Do(func() { + manager = m + }) +} + +// NewSecret creates or retrieves a secret +func NewSecret(name string, defaultValues interface{}) (Secret, error) { + if manager == nil { + return nil, errors.New("secrets manager not initialized") + } + return manager.NewSecret(name, defaultValues) +} + +// GetSecret retrieves an existing secret +func GetSecret(name string) Secret { + if manager == nil { + return nil + } + return manager.GetSecret(name) +} From fe2cc4b525ba50812a0f1d419de989e3fc365cdf Mon Sep 17 00:00:00 2001 From: Alex X Date: Tue, 7 Oct 2025 13:25:42 +0300 Subject: [PATCH 18/18] Code refactoring for #1744 --- internal/api/api.go | 18 +---- internal/app/app.go | 1 - internal/app/config.go | 6 +- internal/app/log.go | 3 + internal/app/secrets.go | 136 --------------------------------- internal/app/storage.go | 56 ++++++++++++++ internal/echo/echo.go | 2 +- internal/expr/expr.go | 3 +- internal/hls/hls.go | 7 +- internal/onvif/onvif.go | 5 +- internal/streams/api.go | 5 ++ internal/streams/dot.go | 4 +- internal/streams/producer.go | 11 ++- internal/streams/streams.go | 3 +- pkg/creds/README.md | 7 ++ pkg/creds/creds.go | 79 +++++++++++++++++++ pkg/creds/secrets.go | 83 ++++++++++++++++++++ pkg/creds/secrets_test.go | 15 ++++ pkg/secrets/secrets.go | 44 ----------- pkg/shell/shell.go | 143 ----------------------------------- 20 files changed, 269 insertions(+), 362 deletions(-) delete mode 100644 internal/app/secrets.go create mode 100644 internal/app/storage.go create mode 100644 pkg/creds/README.md create mode 100644 pkg/creds/creds.go create mode 100644 pkg/creds/secrets.go create mode 100644 pkg/creds/secrets_test.go delete mode 100644 pkg/secrets/secrets.go diff --git a/internal/api/api.go b/internal/api/api.go index 3dd16d3d..419e2bdf 100644 --- a/internal/api/api.go +++ b/internal/api/api.go @@ -1,7 +1,6 @@ package api import ( - "bytes" "crypto/tls" "encoding/json" "fmt" @@ -15,7 +14,6 @@ import ( "time" "github.com/AlexxIT/go2rtc/internal/app" - "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" ) @@ -167,19 +165,9 @@ func ResponseJSON(w http.ResponseWriter, v any) { func ResponsePrettyJSON(w http.ResponseWriter, v any) { w.Header().Set("Content-Type", MimeJSON) - - var buf bytes.Buffer - enc := json.NewEncoder(&buf) + enc := json.NewEncoder(w) enc.SetIndent("", " ") - err := enc.Encode(v) - - if err != nil { - http.Error(w, "Internal Server Error", http.StatusInternalServerError) - return - } - - redactedJSON := shell.Redact(buf.String()) - w.Write([]byte(redactedJSON)) + _ = enc.Encode(v) } func Response(w http.ResponseWriter, body any, contentType string) { @@ -202,7 +190,7 @@ var log zerolog.Logger func middlewareLog(next http.Handler) http.Handler { return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - log.Trace().Msgf("[api] %s %s %s", r.Method, shell.Redact(r.URL.String()), r.RemoteAddr) + log.Trace().Msgf("[api] %s %s %s", r.Method, r.URL, r.RemoteAddr) next.ServeHTTP(w, r) }) } diff --git a/internal/app/app.go b/internal/app/app.go index 4b89daa9..eb803584 100644 --- a/internal/app/app.go +++ b/internal/app/app.go @@ -67,7 +67,6 @@ func Init() { Info["revision"] = revision initConfig(config) - initSecrets() initLogger() platform := fmt.Sprintf("%s/%s", runtime.GOOS, runtime.GOARCH) diff --git a/internal/app/config.go b/internal/app/config.go index f0eb36e0..0f95894a 100644 --- a/internal/app/config.go +++ b/internal/app/config.go @@ -7,7 +7,7 @@ import ( "strings" "sync" - "github.com/AlexxIT/go2rtc/pkg/shell" + "github.com/AlexxIT/go2rtc/pkg/creds" "github.com/AlexxIT/go2rtc/pkg/yaml" ) @@ -71,13 +71,15 @@ func initConfig(confs flagConfig) { // config as file if ConfigPath == "" { ConfigPath = conf + initStorage() } if data, _ = os.ReadFile(conf); data == nil { continue } - data = []byte(shell.ReplaceEnvVars(string(data))) + loadEnv(data) + data = creds.ReplaceVars(data) configs = append(configs, data) } } diff --git a/internal/app/log.go b/internal/app/log.go index b8ca4aa5..9ec89a2c 100644 --- a/internal/app/log.go +++ b/internal/app/log.go @@ -6,6 +6,7 @@ import ( "strings" "sync" + "github.com/AlexxIT/go2rtc/pkg/creds" "github.com/mattn/go-isatty" "github.com/rs/zerolog" ) @@ -88,6 +89,8 @@ func initLogger() { writer = MemoryLog } + writer = creds.SecretWriter(writer) + lvl, _ := zerolog.ParseLevel(modules["level"]) Logger = zerolog.New(writer).Level(lvl) diff --git a/internal/app/secrets.go b/internal/app/secrets.go deleted file mode 100644 index e1ce7509..00000000 --- a/internal/app/secrets.go +++ /dev/null @@ -1,136 +0,0 @@ -package app - -import ( - "sync" - - "github.com/AlexxIT/go2rtc/pkg/secrets" - "github.com/AlexxIT/go2rtc/pkg/yaml" -) - -var ( - secretsMap = make(map[string]*Secret) - secretsMu sync.Mutex -) - -// SecretsManager implements secrets.SecretsManager interface -type SecretsManager struct{} - -func (m *SecretsManager) NewSecret(name string, values interface{}) (secrets.Secret, error) { - secretsMu.Lock() - defer secretsMu.Unlock() - - if s, exists := secretsMap[name]; exists { - return s, nil - } - - s := &Secret{Name: name, Values: make(map[string]string)} - - data, err := yaml.Encode(values, 2) - if err != nil { - return nil, err - } - - if err := yaml.Unmarshal(data, &s.Values); err != nil { - return nil, err - } - - secretsMap[name] = s - - return s, nil -} - -func (m *SecretsManager) GetSecret(name string) secrets.Secret { - secretsMu.Lock() - defer secretsMu.Unlock() - return secretsMap[name] -} - -// Secret implements secrets.Secret interface -type Secret struct { - Name string - Values map[string]string -} - -func (s *Secret) Get(key string) any { - secretsMu.Lock() - defer secretsMu.Unlock() - - if s.Values == nil { - return nil - } - - return s.Values[key] -} - -func (s *Secret) Set(key string, value string) { - secretsMu.Lock() - defer secretsMu.Unlock() - - if s.Values == nil { - s.Values = make(map[string]string) - } - - s.Values[key] = value -} - -func (s *Secret) Marshal() (interface{}, error) { - secretsMu.Lock() - defer secretsMu.Unlock() - - if s.Values == nil { - return make(map[string]any), nil - } - - return s.Values, nil -} - -func (s *Secret) Unmarshal(value any) error { - secretsMu.Lock() - defer secretsMu.Unlock() - - if s.Values == nil { - s.Values = make(map[string]string) - } - - data, err := yaml.Encode(value, 2) - if err != nil { - return err - } - - if err := yaml.Unmarshal(data, value); err != nil { - return err - } - - return nil -} - -func (s *Secret) Save() error { - secretsMu.Lock() - defer secretsMu.Unlock() - return PatchConfig([]string{"secrets", s.Name}, s.Values) -} - -func initSecrets() { - var cfg struct { - Secrets map[string]map[string]string `yaml:"secrets"` - } - - LoadConfig(&cfg) - - if cfg.Secrets == nil { - return - } - - secretsMu.Lock() - defer secretsMu.Unlock() - - for name, values := range cfg.Secrets { - secretsMap[name] = &Secret{ - Name: name, - Values: values, - } - } - - // Register - secrets.SetManager(&SecretsManager{}) -} diff --git a/internal/app/storage.go b/internal/app/storage.go new file mode 100644 index 00000000..cfa1ca91 --- /dev/null +++ b/internal/app/storage.go @@ -0,0 +1,56 @@ +package app + +import ( + "sync" + + "github.com/AlexxIT/go2rtc/pkg/creds" + "github.com/AlexxIT/go2rtc/pkg/yaml" +) + +func initStorage() { + storage = &envStorage{data: make(map[string]string)} + creds.SetStorage(storage) +} + +func loadEnv(data []byte) { + var cfg struct { + Env map[string]string `yaml:"env"` + } + + if err := yaml.Unmarshal(data, &cfg); err != nil { + return + } + + storage.mu.Lock() + for name, value := range cfg.Env { + storage.data[name] = value + creds.AddSecret(value) + } + storage.mu.Unlock() +} + +var storage *envStorage + +type envStorage struct { + data map[string]string + mu sync.Mutex +} + +func (s *envStorage) SetValue(name, value string) error { + if err := PatchConfig([]string{"env", name}, value); err != nil { + return err + } + + s.mu.Lock() + s.data[name] = value + s.mu.Unlock() + + return nil +} + +func (s *envStorage) GetValue(name string) (value string, ok bool) { + s.mu.Lock() + value, ok = s.data[name] + s.mu.Unlock() + return +} diff --git a/internal/echo/echo.go b/internal/echo/echo.go index df88dd64..fb105cec 100644 --- a/internal/echo/echo.go +++ b/internal/echo/echo.go @@ -22,7 +22,7 @@ func Init() { b = bytes.TrimSpace(b) - log.Debug().Str("url", shell.Redact(url)).Msgf("[echo] %s", b) + log.Debug().Str("url", url).Msgf("[echo] %s", b) return string(b), nil }) diff --git a/internal/expr/expr.go b/internal/expr/expr.go index 4d8aa5ce..8fd6c9c2 100644 --- a/internal/expr/expr.go +++ b/internal/expr/expr.go @@ -6,7 +6,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/app" "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/expr" - "github.com/AlexxIT/go2rtc/pkg/shell" ) func Init() { @@ -18,7 +17,7 @@ func Init() { return "", err } - log.Debug().Msgf("[expr] url=%s", shell.Redact(url)) + log.Debug().Msgf("[expr] url=%s", url) if url = v.(string); url == "" { return "", errors.New("expr: result is empty") diff --git a/internal/hls/hls.go b/internal/hls/hls.go index 2344b62e..5c136450 100644 --- a/internal/hls/hls.go +++ b/internal/hls/hls.go @@ -12,7 +12,6 @@ import ( "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/mp4" "github.com/AlexxIT/go2rtc/pkg/mpegts" - "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" ) @@ -143,7 +142,7 @@ func handlerSegmentTS(w http.ResponseWriter, r *http.Request) { data := session.Segment() if data == nil { - log.Warn().Msgf("[hls] can't get segment %s", shell.Redact(r.URL.RawQuery)) + log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery) http.NotFound(w, r) return } @@ -173,7 +172,7 @@ func handlerInit(w http.ResponseWriter, r *http.Request) { data := session.Init() if data == nil { - log.Warn().Msgf("[hls] can't get init %s", shell.Redact(r.URL.RawQuery)) + log.Warn().Msgf("[hls] can't get init %s", r.URL.RawQuery) http.NotFound(w, r) return } @@ -207,7 +206,7 @@ func handlerSegmentMP4(w http.ResponseWriter, r *http.Request) { data := session.Segment() if data == nil { - log.Warn().Msgf("[hls] can't get segment %s", shell.Redact(r.URL.RawQuery)) + log.Warn().Msgf("[hls] can't get segment %s", r.URL.RawQuery) http.NotFound(w, r) return } diff --git a/internal/onvif/onvif.go b/internal/onvif/onvif.go index 36b3843c..6dfa633a 100644 --- a/internal/onvif/onvif.go +++ b/internal/onvif/onvif.go @@ -15,7 +15,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/streams" "github.com/AlexxIT/go2rtc/pkg/core" "github.com/AlexxIT/go2rtc/pkg/onvif" - "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" ) @@ -166,12 +165,12 @@ func apiOnvif(w http.ResponseWriter, r *http.Request) { for _, rawURL := range urls { u, err := url.Parse(rawURL) if err != nil { - log.Warn().Str("url", shell.Redact(rawURL)).Msg("[onvif] broken") + log.Warn().Str("url", rawURL).Msg("[onvif] broken") continue } if u.Scheme != "http" { - log.Warn().Str("url", shell.Redact(rawURL)).Msg("[onvif] unsupported") + log.Warn().Str("url", rawURL).Msg("[onvif] unsupported") continue } diff --git a/internal/streams/api.go b/internal/streams/api.go index 061e61c2..189178b6 100644 --- a/internal/streams/api.go +++ b/internal/streams/api.go @@ -5,10 +5,13 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/app" + "github.com/AlexxIT/go2rtc/pkg/creds" "github.com/AlexxIT/go2rtc/pkg/probe" ) func apiStreams(w http.ResponseWriter, r *http.Request) { + w = creds.SecretResponse(w) + query := r.URL.Query() src := query.Get("src") @@ -120,5 +123,7 @@ func apiStreamsDOT(w http.ResponseWriter, r *http.Request) { } dot = append(dot, '}') + dot = []byte(creds.SecretString(string(dot))) + api.Response(w, dot, "text/vnd.graphviz") } diff --git a/internal/streams/dot.go b/internal/streams/dot.go index 2c357f77..e0417972 100644 --- a/internal/streams/dot.go +++ b/internal/streams/dot.go @@ -4,8 +4,6 @@ import ( "encoding/json" "fmt" "strings" - - "github.com/AlexxIT/go2rtc/pkg/shell" ) func AppendDOT(dot []byte, stream *Stream) []byte { @@ -168,7 +166,7 @@ func (c *conn) label() string { sb.WriteString("\nsource=" + c.Source) } if c.URL != "" { - sb.WriteString("\nurl=" + shell.Redact(c.URL)) + sb.WriteString("\nurl=" + c.URL) } if c.UserAgent != "" { sb.WriteString("\nuser_agent=" + c.UserAgent) diff --git a/internal/streams/producer.go b/internal/streams/producer.go index 4260198e..09e2dcc5 100644 --- a/internal/streams/producer.go +++ b/internal/streams/producer.go @@ -8,7 +8,6 @@ import ( "time" "github.com/AlexxIT/go2rtc/pkg/core" - "github.com/AlexxIT/go2rtc/pkg/shell" ) type state byte @@ -150,7 +149,7 @@ func (p *Producer) start() { return } - log.Debug().Msgf("[streams] start producer url=%s", shell.Redact(p.url)) + log.Debug().Msgf("[streams] start producer url=%s", p.url) p.state = stateStart p.workerID++ @@ -168,7 +167,7 @@ func (p *Producer) worker(conn core.Producer, workerID int) { return } - log.Warn().Err(err).Str("url", shell.Redact(p.url)).Caller().Send() + log.Warn().Err(err).Str("url", p.url).Caller().Send() } p.reconnect(workerID, 0) @@ -179,11 +178,11 @@ func (p *Producer) reconnect(workerID, retry int) { defer p.mu.Unlock() if p.workerID != workerID { - log.Trace().Msgf("[streams] stop reconnect url=%s", shell.Redact(p.url)) + log.Trace().Msgf("[streams] stop reconnect url=%s", p.url) return } - log.Debug().Msgf("[streams] retry=%d to url=%s", retry, shell.Redact(p.url)) + log.Debug().Msgf("[streams] retry=%d to url=%s", retry, p.url) conn, err := GetProducer(p.url) if err != nil { @@ -258,7 +257,7 @@ func (p *Producer) stop() { p.workerID++ } - log.Debug().Msgf("[streams] stop producer url=%s", shell.Redact(p.url)) + log.Debug().Msgf("[streams] stop producer url=%s", p.url) if p.conn != nil { _ = p.conn.Stop() diff --git a/internal/streams/streams.go b/internal/streams/streams.go index 8731ae68..dcbaba28 100644 --- a/internal/streams/streams.go +++ b/internal/streams/streams.go @@ -9,7 +9,6 @@ import ( "github.com/AlexxIT/go2rtc/internal/api" "github.com/AlexxIT/go2rtc/internal/app" - "github.com/AlexxIT/go2rtc/pkg/shell" "github.com/rs/zerolog" ) @@ -128,7 +127,7 @@ func GetOrPatch(query url.Values) *Stream { // check if name param provided if name := query.Get("name"); name != "" { - log.Info().Msgf("[streams] create new stream url=%s", shell.Redact(source)) + log.Info().Msgf("[streams] create new stream url=%s", source) return Patch(name, source) } diff --git a/pkg/creds/README.md b/pkg/creds/README.md new file mode 100644 index 00000000..1909a206 --- /dev/null +++ b/pkg/creds/README.md @@ -0,0 +1,7 @@ +# Credentials + +This module allows you to get variables: + +- from custom storage (ex. config file) +- from [credential files](https://systemd.io/CREDENTIALS/) +- from environment variables diff --git a/pkg/creds/creds.go b/pkg/creds/creds.go new file mode 100644 index 00000000..84bc275a --- /dev/null +++ b/pkg/creds/creds.go @@ -0,0 +1,79 @@ +package creds + +import ( + "errors" + "os" + "path/filepath" + "regexp" + "strings" +) + +type Storage interface { + SetValue(name, value string) error + GetValue(name string) (string, bool) +} + +var storage Storage + +func SetStorage(s Storage) { + storage = s +} + +func SetValue(name, value string) error { + if storage == nil { + return errors.New("credentials: storage not initialized") + } + if err := storage.SetValue(name, value); err != nil { + return err + } + AddSecret(value) + return nil +} + +func GetValue(name string) (value string, ok bool) { + value, ok = getValue(name) + AddSecret(value) + return +} + +func getValue(name string) (string, bool) { + if storage != nil { + if value, ok := storage.GetValue(name); ok { + return value, true + } + } + + if dir, ok := os.LookupEnv("CREDENTIALS_DIRECTORY"); ok { + if value, _ := os.ReadFile(filepath.Join(dir, name)); value != nil { + return strings.TrimSpace(string(value)), true + } + } + + return os.LookupEnv(name) +} + +// ReplaceVars - support format ${CAMERA_PASSWORD} and ${RTSP_USER:admin} +func ReplaceVars(data []byte) []byte { + re := regexp.MustCompile(`\${([^}{]+)}`) + return re.ReplaceAllFunc(data, func(match []byte) []byte { + key := string(match[2 : len(match)-1]) + + var def string + var defok bool + + if i := strings.IndexByte(key, ':'); i > 0 { + key, def = key[:i], key[i+1:] + defok = true + } + + if value, ok := GetValue(key); ok { + return []byte(value) + } + + if defok { + return []byte(def) + } + + return match + }) +} diff --git a/pkg/creds/secrets.go b/pkg/creds/secrets.go new file mode 100644 index 00000000..a9a0094e --- /dev/null +++ b/pkg/creds/secrets.go @@ -0,0 +1,83 @@ +package creds + +import ( + "io" + "net/http" + "slices" + "strings" + "sync" +) + +func AddSecret(value string) { + if value == "" { + return + } + + secretsMu.Lock() + defer secretsMu.Unlock() + + if slices.Contains(secrets, value) { + return + } + + secrets = append(secrets, value) + secretsReplacer = nil +} + +var secrets []string +var secretsMu sync.Mutex +var secretsReplacer *strings.Replacer + +func getReplacer() *strings.Replacer { + secretsMu.Lock() + defer secretsMu.Unlock() + + if secretsReplacer == nil { + oldnew := make([]string, 0, 2*len(secrets)) + for _, s := range secrets { + oldnew = append(oldnew, s, "***") + } + secretsReplacer = strings.NewReplacer(oldnew...) + } + + return secretsReplacer +} + +func SecretString(s string) string { + re := getReplacer() + return re.Replace(s) +} + +func SecretWriter(w io.Writer) io.Writer { + return &secretWriter{w} +} + +type secretWriter struct { + w io.Writer +} + +func (s *secretWriter) Write(b []byte) (int, error) { + re := getReplacer() + return re.WriteString(s.w, string(b)) +} + +type secretResponse struct { + w http.ResponseWriter +} + +func (s *secretResponse) Header() http.Header { + return s.w.Header() +} + +func (s *secretResponse) Write(b []byte) (int, error) { + re := getReplacer() + return re.WriteString(s.w, string(b)) +} + +func (s *secretResponse) WriteHeader(statusCode int) { + s.w.WriteHeader(statusCode) +} + +func SecretResponse(w http.ResponseWriter) http.ResponseWriter { + return &secretResponse{w} +} diff --git a/pkg/creds/secrets_test.go b/pkg/creds/secrets_test.go new file mode 100644 index 00000000..83f1908a --- /dev/null +++ b/pkg/creds/secrets_test.go @@ -0,0 +1,15 @@ +package creds + +import ( + "testing" + + "github.com/stretchr/testify/require" +) + +func TestString(t *testing.T) { + AddSecret("admin") + AddSecret("pa$$word") + + s := SecretString("rtsp://admin:pa$$word@192.168.1.123/stream1") + require.Equal(t, "rtsp://***:***@192.168.1.123/stream1", s) +} diff --git a/pkg/secrets/secrets.go b/pkg/secrets/secrets.go deleted file mode 100644 index 071d9526..00000000 --- a/pkg/secrets/secrets.go +++ /dev/null @@ -1,44 +0,0 @@ -package secrets - -import ( - "errors" - "sync" -) - -type SecretsManager interface { - NewSecret(name string, defaultValues interface{}) (Secret, error) - GetSecret(name string) Secret -} - -type Secret interface { - Get(key string) any - Set(key string, value string) - Marshal() (interface{}, error) - Unmarshal(value any) error - Save() error -} - -var manager SecretsManager -var once sync.Once - -func SetManager(m SecretsManager) { - once.Do(func() { - manager = m - }) -} - -// NewSecret creates or retrieves a secret -func NewSecret(name string, defaultValues interface{}) (Secret, error) { - if manager == nil { - return nil, errors.New("secrets manager not initialized") - } - return manager.NewSecret(name, defaultValues) -} - -// GetSecret retrieves an existing secret -func GetSecret(name string) Secret { - if manager == nil { - return nil - } - return manager.GetSecret(name) -} diff --git a/pkg/shell/shell.go b/pkg/shell/shell.go index 64931a91..e04a58c4 100644 --- a/pkg/shell/shell.go +++ b/pkg/shell/shell.go @@ -1,22 +1,10 @@ package shell import ( - "fmt" "os" "os/signal" - "path/filepath" - "regexp" "strings" - "sync" "syscall" - - "github.com/AlexxIT/go2rtc/pkg/yaml" -) - -var ( - secretReplacer *strings.Replacer - secretValues map[string]bool - secretMutex sync.RWMutex ) func QuoteSplit(s string) []string { @@ -48,139 +36,8 @@ func QuoteSplit(s string) []string { return a } -// ReplaceEnvVars - support format ${CAMERA_PASSWORD} and ${RTSP_USER:admin} -func ReplaceEnvVars(text string) string { - var cfg struct { - Env map[string]string `yaml:"env"` - Secrets map[string]map[string]string `yaml:"secrets"` - } - - yaml.Unmarshal([]byte(text), &cfg) - - buildSecretReplacer(cfg) - - re := regexp.MustCompile(`\${([^}{]+)}`) - return re.ReplaceAllStringFunc(text, func(match string) string { - key := match[2 : len(match)-1] - - var def string - var dok bool - - if i := strings.IndexByte(key, ':'); i > 0 { - key, def = key[:i], key[i+1:] - dok = true - } - - if dir, vok := os.LookupEnv("CREDENTIALS_DIRECTORY"); vok { - value, err := os.ReadFile(filepath.Join(dir, key)) - if err == nil { - return strings.TrimSpace(string(value)) - } - } - - if value, vok := os.LookupEnv(key); vok { - return value - } - - if cfg.Env != nil { - if value, ok := cfg.Env[key]; ok { - return value - } - } - - if cfg.Secrets != nil { - for secretName, secretValues := range cfg.Secrets { - for k, v := range secretValues { - name := fmt.Sprintf("%s_%s", secretName, k) - if key == name { - return v - } - } - } - } - - if dok { - return def - } - - return match - }) -} - func RunUntilSignal() { sigs := make(chan os.Signal, 1) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM) println("exit with signal:", (<-sigs).String()) } - -func Redact(text string) string { - secretMutex.RLock() - defer secretMutex.RUnlock() - - if secretReplacer == nil { - return text - } - - return secretReplacer.Replace(text) -} - -func buildSecretReplacer(cfg struct { - Env map[string]string `yaml:"env"` - Secrets map[string]map[string]string `yaml:"secrets"` -}) { - secretMutex.Lock() - defer secretMutex.Unlock() - - if secretValues == nil { - secretValues = make(map[string]bool) - } - - var newSecrets []string - - if dir, ok := os.LookupEnv("CREDENTIALS_DIRECTORY"); ok { - entries, err := os.ReadDir(dir) - if err == nil { - for _, entry := range entries { - if !entry.IsDir() { - value, err := os.ReadFile(filepath.Join(dir, entry.Name())) - if err == nil { - cleanValue := strings.TrimSpace(string(value)) - if len(cleanValue) > 0 && !secretValues[cleanValue] { - secretValues[cleanValue] = true - newSecrets = append(newSecrets, cleanValue) - } - } - } - } - } - } - - if cfg.Secrets != nil { - for _, secretMap := range cfg.Secrets { - for _, value := range secretMap { - if len(value) > 0 && !secretValues[value] { - secretValues[value] = true - newSecrets = append(newSecrets, value) - } - } - } - } - - if len(newSecrets) > 0 { - rebuildReplacer() - } -} - -func rebuildReplacer() { - var replacements []string - - for secret := range secretValues { - replacements = append(replacements, secret, "*****") - } - - if len(replacements) > 0 { - secretReplacer = strings.NewReplacer(replacements...) - } else { - secretReplacer = nil - } -}