Code refactoring for #1744

This commit is contained in:
Alex X
2025-10-07 13:25:42 +03:00
parent 670370056c
commit fe2cc4b525
20 changed files with 269 additions and 362 deletions
+7
View File
@@ -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
+79
View File
@@ -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
})
}
+83
View File
@@ -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}
}
+15
View File
@@ -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)
}
-44
View File
@@ -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)
}
-143
View File
@@ -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
}
}