Code refactoring for #1744
This commit is contained in:
@@ -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
|
||||
@@ -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
|
||||
})
|
||||
}
|
||||
@@ -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}
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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)
|
||||
}
|
||||
@@ -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
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user