Files
HomeAgent/cmd/initconfig/main.go
JianFeeeee 6a3439a49c fix(initconfig): 必须带 cgo 构建,且失败不再静默
cmd/initconfig 通过 database/sql 使用 mattn/go-sqlite3,而 build.sh 一直用
CGO_ENABLED=0 构建它:该库在非 cgo 下退化成 static_mock.go 里的桩,sql.Open
是惰性的所以不报错、第一次 Exec 才失败,而 main.go 丢掉了所有返回值。合起来
是一个完全静默的空操作——打印凭据、退出码 0、config.db 里一个字节都没写。
安装脚本把这份凭据写进 credentials.txt,用户照着登录必然失败,全程无报错。

- build.sh: initconfig 改 CGO_ENABLED=1,并写明为何不能图省事去掉 cgo
- main.go: 每个 Exec 都检查;写完**回读比对**(不看返回码,看真实落盘内容),
  不一致即非零退出

反向验证:仍用 CGO_ENABLED=0 构建时,现在 stderr 报
"Binary was compiled with 'CGO_ENABLED=0', go-sqlite3 requires cgo to work"
且 exit 1(此前是 exit 0 并把凭据照打印出来)。
2026-09-12 08:10:26 +08:00

121 lines
3.8 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package main
import (
"crypto/rand"
"database/sql"
"encoding/hex"
"flag"
"fmt"
"os"
_ "github.com/mattn/go-sqlite3"
)
func randomSecret(n int) string {
b := make([]byte, n)
rand.Read(b)
return hex.EncodeToString(b)
}
// must 让失败真正停下来。
//
// 这里曾经把所有 db.Exec 的返回值丢掉,配合 CGO_ENABLED=0 构建go-sqlite3
// 退化成静态桩),得到的是一个**完全静默的空操作**:打印凭据、退出码 0、
// config.db 里一个字节都没写。调用方(安装脚本)无法区分成败,用户装完
// 照着 credentials.txt 登录必然失败。
func must(err error) {
if err != nil {
fmt.Fprintf(os.Stderr, "initconfig: %v\n", err)
os.Exit(1)
}
}
// verify 回读刚写入的值。
//
// 只看 Exec 有没有报错不够:驱动被换掉(如上面的桩)、路径不对、写入被丢弃,
// 都可能返回 nil 而什么都没落下。这里把真实落盘的值读回来,与预期逐一比对,
// 不一致就非零退出——"初始化脚本说自己成功了"必须由数据库内容佐证。
func verify(db *sql.DB, table, key, want string) {
var got string
if err := db.QueryRow(fmt.Sprintf(`SELECT value FROM %s WHERE key = ?`, table), key).Scan(&got); err != nil {
fmt.Fprintf(os.Stderr, "initconfig: 回读 %s.%s 失败: %v\n", table, key, err)
os.Exit(1)
}
if got != want {
fmt.Fprintf(os.Stderr, "initconfig: %s.%s 与写入值不一致(读回 %q\n", table, key, got)
os.Exit(1)
}
}
func main() {
dataDir := flag.String("data", "", "data directory")
webuiUsername := flag.String("username", "admin", "webui username")
webuiPassword := flag.String("password", "", "webui password (auto-generated if empty)")
webuiApiKey := flag.String("apikey", "", "api key (auto-generated if empty)")
flag.Parse()
if *dataDir == "" {
fmt.Fprintln(os.Stderr, "-data is required")
os.Exit(1)
}
os.MkdirAll(*dataDir, 0755)
dbPath := *dataDir + "/config.db"
db, err := sql.Open("sqlite3", dbPath)
must(err)
defer db.Close()
// 尽早验证数据库真的可用sql.Open 是惰性的,不碰一次不会暴露驱动问题。
if _, err := db.Exec("PRAGMA journal_mode=WAL"); err != nil {
fmt.Fprintf(os.Stderr, "initconfig: 打开数据库 %s 失败: %v\n", dbPath, err)
os.Exit(1)
}
if _, err := db.Exec(`CREATE TABLE IF NOT EXISTS config (key TEXT PRIMARY KEY, value TEXT NOT NULL)`); err != nil {
fmt.Fprintf(os.Stderr, "initconfig: 创建 config 表失败: %v\n", err)
os.Exit(1)
}
const listenAddr = ":8080"
if _, err := db.Exec(`INSERT OR IGNORE INTO config (key, value) VALUES (?, ?)`, "webui.listen_addr", listenAddr); err != nil {
fmt.Fprintf(os.Stderr, "initconfig: 写入 webui.listen_addr 失败: %v\n", err)
os.Exit(1)
}
pw := *webuiPassword
if pw == "" {
pw = randomSecret(12)
}
apiKey := *webuiApiKey
if apiKey == "" {
apiKey = randomSecret(16)
}
const pt = "config_webui"
if _, err := db.Exec(fmt.Sprintf(`CREATE TABLE IF NOT EXISTS %s (key TEXT PRIMARY KEY, value TEXT NOT NULL)`, pt)); err != nil {
fmt.Fprintf(os.Stderr, "initconfig: 创建 %s 表失败: %v\n", pt, err)
os.Exit(1)
}
ws := fmt.Sprintf(`INSERT OR REPLACE INTO %s (key, value) VALUES (?, ?)`, pt)
for _, kv := range [][2]string{
{"api_key", apiKey},
{"username", *webuiUsername},
{"password", pw},
{"session_ttl_hours", "24"},
} {
if _, err := db.Exec(ws, kv[0], kv[1]); err != nil {
fmt.Fprintf(os.Stderr, "initconfig: 写入 %s.%s 失败: %v\n", pt, kv[0], err)
os.Exit(1)
}
}
verify(db, pt, "api_key", apiKey)
verify(db, pt, "username", *webuiUsername)
verify(db, pt, "password", pw)
verify(db, "config", "webui.listen_addr", listenAddr)
fmt.Printf("API_KEY=%s\n", apiKey)
fmt.Printf("WEBUI_USERNAME=%s\n", *webuiUsername)
fmt.Printf("WEBUI_PASSWORD=%s\n", pw)
}