mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-10-31 22:37:20 +00:00
feat(startup): 从配置文件启动,口令不再出现在命令行
main() 改用 internal/appconfig 合并后的配置,所有 flag 指针只在构造 Values 时读取一次,后续一律使用合并结果,杜绝「某处漏改又读回默认值」。 - 新增 -config <path>;未传时按 $W4F_CONFIG → <workdir>/config.json 查找。 - 启动日志只打印每个字段的来源与脱敏摘要,口令与 join-key 固定输出为 <redacted>;配置文件解析失败、未知键、未定义环境变量直接 Fatal 退出。 - 配置文件与命令行都没给口令时保持旧行为(回落到内置默认值),但打一行 明确的 WARNING,并区分「空口令」这一更危险的形态。 systemd 单元因此可以只写 `webui4frpc -config /etc/webui4frpc/config.json`, `ps` 与单元文件里都不再出现明文口令。
This commit is contained in:
@ -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 <workdir>/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)
|
||||
|
||||
Reference in New Issue
Block a user