mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-10-31 14:27:23 +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"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"webui4frpc/internal/appconfig"
|
||||||
"webui4frpc/internal/cluster"
|
"webui4frpc/internal/cluster"
|
||||||
"webui4frpc/internal/httpapi"
|
"webui4frpc/internal/httpapi"
|
||||||
"webui4frpc/internal/install"
|
"webui4frpc/internal/install"
|
||||||
@ -30,18 +31,68 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
addr := flag.String("addr", "127.0.0.1:7500", "listen address")
|
addrF := flag.String("addr", "127.0.0.1:7500", "listen address")
|
||||||
user := flag.String("user", "admin", "basic auth user")
|
userF := flag.String("user", "admin", "basic auth user")
|
||||||
pass := flag.String("password", "admin", "basic auth password")
|
passF := flag.String("password", "admin", "basic auth password")
|
||||||
workdir := flag.String("workdir", "./webui-frpc", "data directory (db, configs, logs, bin)")
|
workdirF := 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)")
|
binF := 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)")
|
frpcF := flag.String("frpc", "", "default worker frpc binary path (fallback when settings has none)")
|
||||||
var peers multiFlag
|
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)")
|
||||||
flag.Var(&peers, "peer", "cluster peer host:port (repeatable; same creds)")
|
var peersF multiFlag
|
||||||
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)")
|
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()
|
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 {
|
if err != nil {
|
||||||
log.Fatalf("resolve workdir: %v", err)
|
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
|
// account setup. The auth middleware's Basic branch still falls back to the
|
||||||
// raw flag creds, so inter-node cluster traffic (which uses SetBasicAuth
|
// raw flag creds, so inter-node cluster traffic (which uses SetBasicAuth
|
||||||
// with these flags) never breaks even before this row exists.
|
// 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)
|
log.Fatalf("sync system user: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed settings with the initial binary path if provided.
|
// Seed settings with the initial binary path if provided.
|
||||||
if *bin != "" {
|
if v.Bin != "" {
|
||||||
cfg, _ := st.Settings()
|
cfg, _ := st.Settings()
|
||||||
cfg.BinaryPath = *bin
|
cfg.BinaryPath = v.Bin
|
||||||
_ = st.UpdateSettings(cfg)
|
_ = st.UpdateSettings(cfg)
|
||||||
}
|
}
|
||||||
|
|
||||||
@ -78,10 +129,10 @@ func main() {
|
|||||||
// own webui binary as frpc. So fallback is empty unless -bin is set; the UI
|
// own webui binary as frpc. So fallback is empty unless -bin is set; the UI
|
||||||
// then asks the user to install or provide frpc.
|
// then asks the user to install or provide frpc.
|
||||||
fallbackBin := ""
|
fallbackBin := ""
|
||||||
if *frpc != "" {
|
if v.FRPC != "" {
|
||||||
fallbackBin = *frpc
|
fallbackBin = v.FRPC
|
||||||
} else if *bin != "" {
|
} else if v.Bin != "" {
|
||||||
fallbackBin = *bin
|
fallbackBin = v.Bin
|
||||||
}
|
}
|
||||||
|
|
||||||
pm := process.NewManager(process.Options{
|
pm := process.NewManager(process.Options{
|
||||||
@ -113,15 +164,15 @@ func main() {
|
|||||||
|
|
||||||
reg := cluster.NewRegistry(filepath.Join(wd, "bin"))
|
reg := cluster.NewRegistry(filepath.Join(wd, "bin"))
|
||||||
// M6: register cluster peers from -peer flags (same creds as this node).
|
// M6: register cluster peers from -peer flags (same creds as this node).
|
||||||
for _, p := range peers {
|
for _, p := range v.Peers {
|
||||||
reg.RegisterPeer(p, p, *user, *pass)
|
reg.RegisterPeer(p, p, v.User, v.Password)
|
||||||
}
|
}
|
||||||
|
|
||||||
// M6 token ring: this node is the initial leader of the cluster ring
|
// M6 token ring: this node is the initial leader of the cluster ring
|
||||||
// (creation node). Engine drives two-phase cycles; transport POSTs the
|
// (creation node). Engine drives two-phase cycles; transport POSTs the
|
||||||
// token to the successor over HTTP.
|
// token to the successor over HTTP.
|
||||||
selfID := reachableAddr(*addr)
|
selfID := reachableAddr(v.Addr)
|
||||||
ringTransport := &cluster.TokenTransport{User: *user, Pass: *pass}
|
ringTransport := &cluster.TokenTransport{User: v.User, Pass: v.Password}
|
||||||
// nodeKey is this node's cluster admission key. It is NOT generated at
|
// 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.
|
// 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
|
// A creation node (no -peer) generates its key on CreateCluster; a joiner
|
||||||
@ -134,7 +185,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
var ring *cluster.Engine
|
var ring *cluster.Engine
|
||||||
ring = cluster.NewEngine(
|
ring = cluster.NewEngine(
|
||||||
selfID, selfID, *user, *pass, version.Version, nil,
|
selfID, selfID, v.User, v.Password, version.Version, nil,
|
||||||
&cluster.AppHandler{
|
&cluster.AppHandler{
|
||||||
LoadFn: func() (float64, float64) {
|
LoadFn: func() (float64, float64) {
|
||||||
// NetPct: real NIC saturation from /proc/net/dev deltas vs
|
// NetPct: real NIC saturation from /proc/net/dev deltas vs
|
||||||
@ -354,7 +405,7 @@ func main() {
|
|||||||
}
|
}
|
||||||
return nodeID
|
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
|
// Install the persistence callbacks so ensureNodeKey (called from
|
||||||
// CreateCluster / AdoptState) and detachAsStandalone can persist/clear
|
// 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
|
// 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
|
// would break peer reconnect, so fall back to the hostname (container
|
||||||
// name inside docker is a routable addr on the compose network).
|
// name inside docker is a routable addr on the compose network).
|
||||||
SelfAddr: reachableAddr(*addr),
|
SelfAddr: reachableAddr(v.Addr),
|
||||||
User: *user,
|
User: v.User,
|
||||||
Password: *pass,
|
Password: v.Password,
|
||||||
BinaryPath: func() string {
|
BinaryPath: func() string {
|
||||||
s, _ := st.Settings()
|
s, _ := st.Settings()
|
||||||
if s.BinaryPath != "" {
|
if s.BinaryPath != "" {
|
||||||
@ -530,10 +581,10 @@ func main() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
log.Fatalf("build handler: %v", err)
|
log.Fatalf("build handler: %v", err)
|
||||||
}
|
}
|
||||||
srv := &http.Server{Addr: *addr, Handler: handler}
|
srv := &http.Server{Addr: v.Addr, Handler: handler}
|
||||||
|
|
||||||
go func() {
|
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 {
|
if err := srv.ListenAndServe(); err != nil && err != http.ErrServerClosed {
|
||||||
log.Fatalf("server error: %v", err)
|
log.Fatalf("server error: %v", err)
|
||||||
}
|
}
|
||||||
@ -566,8 +617,8 @@ func main() {
|
|||||||
}
|
}
|
||||||
if persistedPeers != "" {
|
if persistedPeers != "" {
|
||||||
go retryRejoinCached(clusterCtx, ring, persistedPeers)
|
go retryRejoinCached(clusterCtx, ring, persistedPeers)
|
||||||
} else if len(peers) > 0 {
|
} else if len(v.Peers) > 0 {
|
||||||
go retryJoinCluster(clusterCtx, ring, peers[0], *joinKey)
|
go retryJoinCluster(clusterCtx, ring, v.Peers[0], v.JoinKey)
|
||||||
} else {
|
} else {
|
||||||
if err := ring.CreateCluster(); err != nil {
|
if err := ring.CreateCluster(); err != nil {
|
||||||
log.Printf("ring[%s] create cluster at startup: %v", selfID, err)
|
log.Printf("ring[%s] create cluster at startup: %v", selfID, err)
|
||||||
|
|||||||
Reference in New Issue
Block a user