mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-10-31 06:17:20 +00:00
新增 internal/appconfig:把「从配置文件启动」所需的解析、密钥展开和
优先级合并抽成可单测的包,避免这块逻辑散落在 main 里。
- 优先级严格为 显式 flag > 配置文件 > 内置默认值;「是否显式传入」由
flag.Visit 判定,绝不拿解析后的值与默认值比较——Go 的 flag 会无条件
把默认值写进变量,直接读变量就会让默认值压过配置文件。
- 字段覆盖现有全部 flag:addr/user/password/workdir/bin/frpc/peers/join_key
(join_key 兼容 joinKey 写法)。
- 字符串字段支持 ${VAR} 与 file:/path 两种展开;环境变量未定义/为空、
secrets 文件读不到都直接报错,不静默降级成空口令。$$ 转义字面量 $。
- 文件缺失返回 (nil,nil) 不算错误;JSON 非法、未知键(如把 password 拼成
passwd)、尾部多余内容一律报错,交给调用方响亮退出——静默回落到
admin/admin 才是真正的安全问题。以 "_" 开头的键作注释键忽略,
示例配置因此可以内嵌说明。
- -peer 显式传入时整体替换文件里的 peers,而不是追加;-join-key 的内置
默认来自 $W4F_JOIN_KEY,属于更具体的进程级覆盖,仍优先于文件。
- Values 实现 String()/Summary() 并对手令与 join-key 脱敏,即使误用
%v 打印也不会泄漏。
回归测试覆盖上述每一条,其中 TestResolveFileBeatsFlagDefaults 专门守
「默认值压过配置文件」这个坑。
141 lines
4.0 KiB
Go
141 lines
4.0 KiB
Go
package appconfig
|
|
|
|
import (
|
|
"flag"
|
|
"fmt"
|
|
"strings"
|
|
)
|
|
|
|
// Field sources, for logging only — never for values.
|
|
const (
|
|
SourceFlag = "flag"
|
|
SourceEnv = "env"
|
|
SourceFile = "file"
|
|
SourceDefault = "default"
|
|
)
|
|
|
|
// Defaults are the built-in defaults, i.e. exactly what the flags declare.
|
|
type Defaults struct {
|
|
Addr string
|
|
User string
|
|
Password string
|
|
WorkDir string
|
|
}
|
|
|
|
// Values is the effective runtime configuration after merging.
|
|
type Values struct {
|
|
Addr string
|
|
User string
|
|
Password string
|
|
WorkDir string
|
|
Bin string
|
|
FRPC string
|
|
Peers []string
|
|
JoinKey string
|
|
}
|
|
|
|
// Sources records where each resolved field came from ("" = not configured).
|
|
type Sources struct {
|
|
Addr string
|
|
User string
|
|
Password string
|
|
WorkDir string
|
|
Bin string
|
|
FRPC string
|
|
Peers string
|
|
JoinKey string
|
|
}
|
|
|
|
// ExplicitSet returns the flag names the operator actually typed.
|
|
//
|
|
// flag.Visit walks only flags that were set on the command line, which is
|
|
// precisely the test required here: an unset flag must NOT override a config
|
|
// file value with its default.
|
|
func ExplicitSet(fs *flag.FlagSet) map[string]bool {
|
|
set := map[string]bool{}
|
|
fs.Visit(func(f *flag.Flag) { set[f.Name] = true })
|
|
return set
|
|
}
|
|
|
|
// Resolve merges per field: explicit flag > config file (non-empty) >
|
|
// built-in default.
|
|
func Resolve(def Defaults, flags Values, explicit map[string]bool, f *File) (Values, Sources) {
|
|
var out Values
|
|
var src Sources
|
|
var file File
|
|
if f != nil {
|
|
file = *f
|
|
}
|
|
|
|
// pick covers the four fields that have a built-in default.
|
|
pick := func(flagName, flagVal, defVal, fileVal string) (string, string) {
|
|
if explicit[flagName] {
|
|
return flagVal, SourceFlag
|
|
}
|
|
if fileVal != "" {
|
|
return fileVal, SourceFile
|
|
}
|
|
return defVal, SourceDefault
|
|
}
|
|
|
|
out.Addr, src.Addr = pick("addr", flags.Addr, def.Addr, file.Addr)
|
|
out.User, src.User = pick("user", flags.User, def.User, file.User)
|
|
out.Password, src.Password = pick("password", flags.Password, def.Password, file.Password)
|
|
out.WorkDir, src.WorkDir = pick("workdir", flags.WorkDir, def.WorkDir, file.WorkDir)
|
|
|
|
// No built-in default for these: empty means "not configured".
|
|
pickOpt := func(flagName, flagVal, fileVal string) (string, string) {
|
|
if explicit[flagName] {
|
|
return flagVal, SourceFlag
|
|
}
|
|
if fileVal != "" {
|
|
return fileVal, SourceFile
|
|
}
|
|
return "", ""
|
|
}
|
|
out.Bin, src.Bin = pickOpt("bin", flags.Bin, file.Bin)
|
|
out.FRPC, src.FRPC = pickOpt("frpc", flags.FRPC, file.FRPC)
|
|
|
|
// join-key is special: its flag default comes from $W4F_JOIN_KEY, i.e. a
|
|
// per-process override that is more specific than a value baked into the
|
|
// config file. Dropping it here would silently disable env-based cluster
|
|
// joining whenever a file happened to be present.
|
|
switch {
|
|
case explicit["join-key"]:
|
|
out.JoinKey, src.JoinKey = flags.JoinKey, SourceFlag
|
|
case flags.JoinKey != "":
|
|
out.JoinKey, src.JoinKey = flags.JoinKey, SourceEnv
|
|
case file.JoinKey != "":
|
|
out.JoinKey, src.JoinKey = file.JoinKey, SourceFile
|
|
}
|
|
|
|
// peers is repeatable (-peer a -peer b). An explicit flag REPLACES the
|
|
// file's list instead of appending: appending would resurrect peers the
|
|
// operator deliberately dropped on the command line.
|
|
if explicit["peer"] {
|
|
out.Peers, src.Peers = flags.Peers, SourceFlag
|
|
} else if len(file.Peers) > 0 {
|
|
out.Peers, src.Peers = file.Peers, SourceFile
|
|
}
|
|
return out, src
|
|
}
|
|
|
|
// Summary renders the resolved configuration for the startup log with every
|
|
// secret redacted. Using this instead of ad-hoc formatting means an accidental
|
|
// log.Printf("%v", values) still cannot leak the basic-auth password or the
|
|
// cluster join key.
|
|
func (v Values) Summary() string {
|
|
return fmt.Sprintf("addr=%s user=%s password=<redacted> workdir=%s bin=%s frpc=%s peers=%s join-key=<redacted>",
|
|
v.Addr, v.User, v.WorkDir, orDash(v.Bin), orDash(v.FRPC), orDash(strings.Join(v.Peers, ",")))
|
|
}
|
|
|
|
func orDash(s string) string {
|
|
if s == "" {
|
|
return "-"
|
|
}
|
|
return s
|
|
}
|
|
|
|
// String implements fmt.Stringer so %v/%s on Values is always redacted.
|
|
func (v Values) String() string { return v.Summary() }
|