From a96335dbf1392732b738b5ca73799fc398b667a7 Mon Sep 17 00:00:00 2001 From: JianFeeeee Date: Fri, 23 Oct 2026 09:12:24 +0800 Subject: [PATCH] =?UTF-8?q?feat(startup):=20=E4=BB=8E=E9=85=8D=E7=BD=AE?= =?UTF-8?q?=E6=96=87=E4=BB=B6=E5=90=AF=E5=8A=A8=EF=BC=8C=E5=8F=A3=E4=BB=A4?= =?UTF-8?q?=E4=B8=8D=E5=86=8D=E5=87=BA=E7=8E=B0=E5=9C=A8=E5=91=BD=E4=BB=A4?= =?UTF-8?q?=E8=A1=8C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit main() 改用 internal/appconfig 合并后的配置,所有 flag 指针只在构造 Values 时读取一次,后续一律使用合并结果,杜绝「某处漏改又读回默认值」。 - 新增 -config ;未传时按 $W4F_CONFIG → /config.json 查找。 - 启动日志只打印每个字段的来源与脱敏摘要,口令与 join-key 固定输出为 ;配置文件解析失败、未知键、未定义环境变量直接 Fatal 退出。 - 配置文件与命令行都没给口令时保持旧行为(回落到内置默认值),但打一行 明确的 WARNING,并区分「空口令」这一更危险的形态。 systemd 单元因此可以只写 `webui4frpc -config /etc/webui4frpc/config.json`, `ps` 与单元文件里都不再出现明文口令。 --- cmd/webui4frpc/main.go | 111 ++++++++++++++++++++++++++++++----------- 1 file changed, 81 insertions(+), 30 deletions(-) diff --git a/cmd/webui4frpc/main.go b/cmd/webui4frpc/main.go index c2050a9..616b7bc 100644 --- a/cmd/webui4frpc/main.go +++ b/cmd/webui4frpc/main.go @@ -20,6 +20,7 @@ import ( "syscall" "time" + "webui4frpc/internal/appconfig" "webui4frpc/internal/cluster" "webui4frpc/internal/httpapi" "webui4frpc/internal/install" @@ -30,18 +31,68 @@ import ( ) func main() { - addr := flag.String("addr", "127.0.0.1:7500", "listen address") - user := flag.String("user", "admin", "basic auth user") - pass := flag.String("password", "admin", "basic auth password") - workdir := flag.String("workdir", "./webui-frpc", "data directory (db, configs, logs, bin)") - bin := flag.String("bin", "", "initial frpc binary path (optional; defaults to manual/auto-install)") - frpc := flag.String("frpc", "", "default worker frpc binary path (fallback when settings has none)") - var peers multiFlag - flag.Var(&peers, "peer", "cluster peer host:port (repeatable; same creds)") - joinKey := flag.String("join-key", os.Getenv("W4F_JOIN_KEY"), "cluster join key = the sponsor node's nodeKey (required with -peer to join an existing cluster)") + addrF := flag.String("addr", "127.0.0.1:7500", "listen address") + userF := flag.String("user", "admin", "basic auth user") + passF := flag.String("password", "admin", "basic auth password") + workdirF := flag.String("workdir", "./webui-frpc", "data directory (db, configs, logs, bin)") + binF := flag.String("bin", "", "initial frpc binary path (optional; defaults to manual/auto-install)") + frpcF := flag.String("frpc", "", "default worker frpc binary path (fallback when settings has none)") + joinKeyF := flag.String("join-key", os.Getenv("W4F_JOIN_KEY"), "cluster join key = the sponsor node's nodeKey (required with -peer to join an existing cluster)") + var peersF multiFlag + flag.Var(&peersF, "peer", "cluster peer host:port (repeatable; same creds)") + configF := flag.String("config", "", "config file path (default /config.json, or $W4F_CONFIG; a missing file is not an error)") flag.Parse() - wd, err := filepath.Abs(*workdir) + // Merge config file with flags. Priority: explicit flag > config file > + // built-in default. The explicit set comes from flag.Visit because Go + // hands us each flag's default unconditionally — reading the pointer + // blindly would let a default silently clobber the file. + explicit := appconfig.ExplicitSet(flag.CommandLine) + cfgPath := *configF + if cfgPath == "" { + // W4F_CONFIG (packaged systemd units use it) outranks the default + // location, but never an explicit -config. + cfgPath = strings.TrimSpace(os.Getenv("W4F_CONFIG")) + } + if cfgPath == "" { + cfgPath = appconfig.DefaultPath(*workdirF) + } + cfgFile, err := appconfig.Load(cfgPath) + if err != nil { + // Present but unparsable/undeclared-key config fails loudly: silently + // falling back to built-in defaults would quietly boot admin/admin. + log.Fatalf("%v", err) + } + v, srcs := appconfig.Resolve( + appconfig.Defaults{Addr: "127.0.0.1:7500", User: "admin", Password: "admin", WorkDir: "./webui-frpc"}, + appconfig.Values{ + Addr: *addrF, User: *userF, Password: *passF, WorkDir: *workdirF, + Bin: *binF, FRPC: *frpcF, Peers: []string(peersF), JoinKey: *joinKeyF, + }, + explicit, cfgFile, + ) + // Redacted summary + per-field sources. Never the values themselves: the + // basic-auth password must not reach stdout, the journal, or a log file. + if cfgFile == nil { + log.Printf("config: no file at %s (flags only); %s", cfgPath, v.Summary()) + } else { + log.Printf("config: loaded %s; %s", cfgPath, v.Summary()) + } + log.Printf("config: precedence flag>file>default — addr<-%s user<-%s workdir<-%s", srcs.Addr, srcs.User, srcs.WorkDir) + switch srcs.Password { + case appconfig.SourceFile: + log.Printf("config: password <- file (value not logged)") + case appconfig.SourceDefault: + // Neither -password nor the config file provided one. Behaviour is + // unchanged (the legacy default still applies) but say so out loud: + // a node serving the UI with a well-known password deserves a warning. + log.Printf("WARNING: no password configured (neither -password nor the config file); using the built-in default — change it before exposing this node") + } + if v.Password == "" { + log.Printf("WARNING: password is empty — basic auth admin login will not work") + } + + wd, err := filepath.Abs(v.WorkDir) if err != nil { log.Fatalf("resolve workdir: %v", err) } @@ -62,14 +113,14 @@ func main() { // account setup. The auth middleware's Basic branch still falls back to the // raw flag creds, so inter-node cluster traffic (which uses SetBasicAuth // with these flags) never breaks even before this row exists. - if err := st.SyncSystemUser(*user, *pass); err != nil { + if err := st.SyncSystemUser(v.User, v.Password); err != nil { log.Fatalf("sync system user: %v", err) } // Seed settings with the initial binary path if provided. - if *bin != "" { + if v.Bin != "" { cfg, _ := st.Settings() - cfg.BinaryPath = *bin + cfg.BinaryPath = v.Bin _ = st.UpdateSettings(cfg) } @@ -78,10 +129,10 @@ func main() { // own webui binary as frpc. So fallback is empty unless -bin is set; the UI // then asks the user to install or provide frpc. fallbackBin := "" - if *frpc != "" { - fallbackBin = *frpc - } else if *bin != "" { - fallbackBin = *bin + if v.FRPC != "" { + fallbackBin = v.FRPC + } else if v.Bin != "" { + fallbackBin = v.Bin } pm := process.NewManager(process.Options{ @@ -113,15 +164,15 @@ func main() { reg := cluster.NewRegistry(filepath.Join(wd, "bin")) // M6: register cluster peers from -peer flags (same creds as this node). - for _, p := range peers { - reg.RegisterPeer(p, p, *user, *pass) + for _, p := range v.Peers { + reg.RegisterPeer(p, p, v.User, v.Password) } // M6 token ring: this node is the initial leader of the cluster ring // (creation node). Engine drives two-phase cycles; transport POSTs the // token to the successor over HTTP. - selfID := reachableAddr(*addr) - ringTransport := &cluster.TokenTransport{User: *user, Pass: *pass} + selfID := reachableAddr(v.Addr) + ringTransport := &cluster.TokenTransport{User: v.User, Pass: v.Password} // nodeKey is this node's cluster admission key. It is NOT generated at // startup — a fresh node has no key until it creates or joins a cluster. // A creation node (no -peer) generates its key on CreateCluster; a joiner @@ -134,7 +185,7 @@ func main() { } var ring *cluster.Engine ring = cluster.NewEngine( - selfID, selfID, *user, *pass, version.Version, nil, + selfID, selfID, v.User, v.Password, version.Version, nil, &cluster.AppHandler{ LoadFn: func() (float64, float64) { // NetPct: real NIC saturation from /proc/net/dev deltas vs @@ -354,7 +405,7 @@ func main() { } return nodeID }), - selfID, len(peers) == 0, nodeKey, // creation node (no -peer) is the initial leader + selfID, len(v.Peers) == 0, nodeKey, // creation node (no -peer) is the initial leader ) // Install the persistence callbacks so ensureNodeKey (called from // CreateCluster / AdoptState) and detachAsStandalone can persist/clear @@ -423,9 +474,9 @@ func main() { // SelfAddr must be reachable by peers. 0.0.0.0/empty on a real host // would break peer reconnect, so fall back to the hostname (container // name inside docker is a routable addr on the compose network). - SelfAddr: reachableAddr(*addr), - User: *user, - Password: *pass, + SelfAddr: reachableAddr(v.Addr), + User: v.User, + Password: v.Password, BinaryPath: func() string { s, _ := st.Settings() if s.BinaryPath != "" { @@ -530,10 +581,10 @@ func main() { if err != nil { log.Fatalf("build handler: %v", err) } - srv := &http.Server{Addr: *addr, Handler: handler} + srv := &http.Server{Addr: v.Addr, Handler: handler} go func() { - log.Printf("webui-frpc listening on %s (workdir %s)", *addr, wd) + log.Printf("webui-frpc listening on %s (workdir %s)", v.Addr, wd) if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed { log.Fatalf("server error: %v", err) } @@ -566,8 +617,8 @@ func main() { } if persistedPeers != "" { go retryRejoinCached(clusterCtx, ring, persistedPeers) - } else if len(peers) > 0 { - go retryJoinCluster(clusterCtx, ring, peers[0], *joinKey) + } else if len(v.Peers) > 0 { + go retryJoinCluster(clusterCtx, ring, v.Peers[0], v.JoinKey) } else { if err := ring.CreateCluster(); err != nil { log.Printf("ring[%s] create cluster at startup: %v", selfID, err)