package app import ( "database/sql" "encoding/json" "errors" "fmt" "io" "log/slog" "net" "net/http" "os" "path/filepath" "strings" "sync" "time" "filippo.io/age" "golang.org/x/sys/unix" _ "modernc.org/sqlite" ) type App struct { requestWG sync.WaitGroup cfg Config db *sql.DB root *os.Root lock *os.File secret []byte identity *age.X25519Identity mu sync.Mutex passwordSlots chan struct{} rateMu sync.Mutex rates map[string]*rateEntry transferMu sync.Mutex transfers map[string]int activeFiles map[string]bool activeObjects map[string]int extraDisk int64 proxies []*net.IPNet now func() time.Time log *slog.Logger } const schema = ` CREATE TABLE IF NOT EXISTS schema_version(version INTEGER NOT NULL); INSERT INTO schema_version SELECT 1 WHERE NOT EXISTS(SELECT 1 FROM schema_version); CREATE TABLE IF NOT EXISTS settings(id INTEGER PRIMARY KEY CHECK(id=1),json TEXT NOT NULL); CREATE TABLE IF NOT EXISTS runtime_settings(id INTEGER PRIMARY KEY CHECK(id=1),json TEXT NOT NULL); CREATE TABLE IF NOT EXISTS admins(id INTEGER PRIMARY KEY CHECK(id=1),username TEXT NOT NULL,password_hash TEXT NOT NULL); CREATE TABLE IF NOT EXISTS admin_sessions(token_hash TEXT PRIMARY KEY,expires INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS visitors(id TEXT PRIMARY KEY,token_hash TEXT UNIQUE NOT NULL,expires INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS parcels(id TEXT PRIMARY KEY,visitor TEXT NOT NULL,ip TEXT NOT NULL,state TEXT NOT NULL,created INTEGER NOT NULL,expires INTEGER NOT NULL DEFAULT 0,hard_expires INTEGER NOT NULL DEFAULT 0,code_hash TEXT UNIQUE,password_hash TEXT NOT NULL,manage_hash TEXT NOT NULL,version INTEGER NOT NULL DEFAULT 1,days INTEGER NOT NULL,downloads INTEGER NOT NULL,renewal_days INTEGER NOT NULL,renewal_mode TEXT NOT NULL,lifetime_days INTEGER NOT NULL,upload_expires INTEGER NOT NULL,usage_day TEXT NOT NULL); CREATE TABLE IF NOT EXISTS objects(id TEXT PRIMARY KEY,digest TEXT UNIQUE NOT NULL,size INTEGER NOT NULL,stored_bytes INTEGER NOT NULL,encrypted INTEGER NOT NULL,key_id TEXT NOT NULL,created INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS files(id TEXT PRIMARY KEY,parcel TEXT NOT NULL REFERENCES parcels(id) ON DELETE CASCADE,name TEXT NOT NULL,size INTEGER NOT NULL,status TEXT NOT NULL,object_id TEXT REFERENCES objects(id),reserved INTEGER NOT NULL,remaining INTEGER NOT NULL,received INTEGER NOT NULL DEFAULT 0,charged INTEGER NOT NULL DEFAULT 0,attempts INTEGER NOT NULL DEFAULT 0,expires INTEGER NOT NULL DEFAULT 0,hard_expires INTEGER NOT NULL DEFAULT 0); CREATE TABLE IF NOT EXISTS grants(token_hash TEXT PRIMARY KEY,parcel TEXT NOT NULL REFERENCES parcels(id) ON DELETE CASCADE,version INTEGER NOT NULL,expires INTEGER NOT NULL); CREATE TABLE IF NOT EXISTS usage(subject TEXT NOT NULL,day TEXT NOT NULL,bytes INTEGER NOT NULL,uploads INTEGER NOT NULL,PRIMARY KEY(subject,day)); CREATE TABLE IF NOT EXISTS downloads(id TEXT PRIMARY KEY,file_id TEXT NOT NULL,created INTEGER NOT NULL,started INTEGER NOT NULL DEFAULT 0,completed INTEGER NOT NULL DEFAULT 0,refunded INTEGER NOT NULL DEFAULT 0); CREATE TABLE IF NOT EXISTS audit(id INTEGER PRIMARY KEY AUTOINCREMENT,created INTEGER NOT NULL,action TEXT NOT NULL,target TEXT NOT NULL,source TEXT NOT NULL); CREATE INDEX IF NOT EXISTS audit_created ON audit(created); CREATE INDEX IF NOT EXISTS audit_action_id ON audit(action,id DESC); CREATE INDEX IF NOT EXISTS parcels_created_id ON parcels(created DESC,id DESC); CREATE INDEX IF NOT EXISTS files_parcel ON files(parcel); CREATE INDEX IF NOT EXISTS files_object ON files(object_id); CREATE INDEX IF NOT EXISTS parcels_expiry ON parcels(state,expires); CREATE INDEX IF NOT EXISTS parcels_visitor ON parcels(visitor); ` func Open(c Config) (*App, error) { return open(c, false) } func OpenForRestore(c Config) (*App, error) { return open(c, true) } func open(c Config, restoring bool) (*App, error) { if e := c.Validate(); e != nil { return nil, e } if e := os.MkdirAll(c.DataDir, 0700); e != nil { return nil, e } root, e := os.OpenRoot(c.DataDir) if e != nil { return nil, e } if !restoring { if _, me := root.Stat("restore.incomplete"); me == nil { root.Close() return nil, errors.New("恢复未完成,拒绝启动;请检查恢复错误并重新恢复到空目录") } } a := &App{cfg: c, root: root, now: time.Now, log: slog.Default(), passwordSlots: make(chan struct{}, 2), rates: map[string]*rateEntry{}, transfers: map[string]int{}, activeFiles: map[string]bool{}, activeObjects: map[string]int{}} fail := func(e error) (*App, error) { a.Close(); return nil, e } for _, d := range []string{"db", "objects", "tmp", "secrets", "quarantine"} { if e = root.Mkdir(d, 0700); e != nil && !errors.Is(e, os.ErrExist) { return fail(e) } st, se := root.Lstat(d) if se != nil { return fail(se) } if !st.IsDir() || st.Mode()&os.ModeSymlink != 0 { return fail(errors.New("数据子目录必须为真实目录")) } } a.lock, e = root.OpenFile("service.lock", os.O_CREATE|os.O_RDWR, 0600) if e != nil { return fail(e) } if e = unix.Flock(int(a.lock.Fd()), unix.LOCK_EX|unix.LOCK_NB); e != nil { return fail(errors.New("数据目录已被另一进程使用")) } secretPath := "secrets/hmac.key" if _, de := root.Stat("db/bluebird.db"); de == nil { if _, se := root.Lstat(secretPath); se != nil { return fail(errors.New("既有数据库缺少 HMAC 密钥")) } } a.secret, e = readOrCreate(root, secretPath, func() ([]byte, error) { return []byte(randomToken()), nil }) if e != nil { return fail(e) } if len(a.secret) < 32 { return fail(errors.New("HMAC 密钥损坏")) } if st, se := root.Lstat("secrets/age.key"); se == nil && !st.Mode().IsRegular() { return fail(errors.New("加密私钥必须为普通文件")) } key, e := root.ReadFile("secrets/age.key") if e == nil { a.identity, e = age.ParseX25519Identity(strings.TrimSpace(string(key))) if e != nil { return fail(e) } } else if !errors.Is(e, os.ErrNotExist) { return fail(e) } for _, path := range []string{"db/bluebird.db", "db/bluebird.db-wal", "db/bluebird.db-shm", "service.lock"} { if st, se := root.Lstat(path); se == nil && !st.Mode().IsRegular() { return fail(errors.New("数据库和锁文件必须为普通文件")) } } dbPath := filepath.Join(c.DataDir, "db", "bluebird.db") if st, se := root.Lstat("db/bluebird.db"); se == nil && st.Mode()&os.ModeSymlink != 0 { return fail(errors.New("禁止数据库符号链接")) } a.db, e = sql.Open("sqlite", dbPath) if e != nil { return fail(e) } a.db.SetMaxOpenConns(1) if _, e = a.db.Exec("PRAGMA journal_mode=WAL; PRAGMA foreign_keys=ON; PRAGMA synchronous=FULL; PRAGMA busy_timeout=5000; PRAGMA temp_store=MEMORY; " + schema); e != nil { return fail(e) } var version int if e = a.db.QueryRow("SELECT version FROM schema_version").Scan(&version); e != nil || version != 1 { return fail(errors.New("不支持的数据库版本")) } _ = root.Chmod("db/bluebird.db", 0600) if a.identity == nil { var n int if e = a.db.QueryRow("SELECT count(*) FROM objects WHERE encrypted=1").Scan(&n); e != nil { return fail(e) } if n > 0 { return fail(errors.New("加密私钥丢失,拒绝生成替代密钥")) } if c.Encryption { b, e := readOrCreate(root, "secrets/age.key", func() ([]byte, error) { key, e := age.GenerateX25519Identity() if e != nil { return nil, e } return []byte(key.String() + "\n"), nil }) if e != nil { return fail(e) } a.identity, e = age.ParseX25519Identity(strings.TrimSpace(string(b))) if e != nil { return fail(e) } } } b, e := json.Marshal(c.Policy) if e != nil { return fail(e) } if _, e = a.db.Exec("INSERT OR IGNORE INTO settings VALUES(1,?)", string(b)); e != nil { return fail(e) } if _, e = a.policy(); e != nil { return fail(e) } b, e = json.Marshal(c.InitialRuntimeSettings()) if e != nil { return fail(e) } if _, e = a.db.Exec("INSERT OR IGNORE INTO runtime_settings VALUES(1,?)", string(b)); e != nil { return fail(e) } if _, e = a.runtimeSettings(); e != nil { return fail(e) } if e = a.initializeAuditUsage(); e != nil { return fail(e) } for _, p := range c.TrustedProxies { _, n, _ := net.ParseCIDR(p) a.proxies = append(a.proxies, n) } if !restoring { if e = a.recover(); e != nil { return fail(e) } } return a, nil } func readOrCreate(root *os.Root, path string, generate func() ([]byte, error)) ([]byte, error) { if st, e := root.Lstat(path); e == nil && !st.Mode().IsRegular() { return nil, errors.New("密钥必须为普通文件") } b, e := root.ReadFile(path) if e == nil { return b, nil } if !errors.Is(e, os.ErrNotExist) { return nil, e } b, e = generate() if e != nil { return nil, e } f, e := root.OpenFile(path, os.O_CREATE|os.O_EXCL|os.O_WRONLY, 0600) if e != nil { return nil, e } _, e = f.Write(b) if e == nil { e = f.Sync() } ce := f.Close() if e == nil { e = ce } if e == nil { e = syncDir(root, filepath.Dir(path)) } return b, e } func syncDir(root *os.Root, path string) error { f, e := root.Open(path) if e != nil { return e } defer f.Close() return f.Sync() } func (a *App) WaitRequests() { a.requestWG.Wait() } func (a *App) Close() error { var e error if a.db != nil { e = a.db.Close() } if a.root != nil { a.root.Close() } if a.lock != nil { a.lock.Close() } return e } func (a *App) policy() (Policy, error) { var s string p := Policy{} e := a.db.QueryRow("SELECT json FROM settings WHERE id=1").Scan(&s) if e == nil { e = json.Unmarshal([]byte(s), &p) } if e == nil { e = p.Validate() } return p, e } func (a *App) audit(action, target, source string) { tx, e := a.db.Begin() if e != nil { a.log.Error("audit failed", "error", e) return } defer tx.Rollback() _, e = tx.Exec("INSERT INTO audit(created,action,target,source) VALUES(?,?,?,?)", a.now().Unix(), action, target, source) if e == nil { e = a.trimAuditTx(tx) } if e == nil { e = tx.Commit() } if e != nil { a.log.Error("audit failed", "action", action, "error", e) } } func (a *App) SetAdmin(username, password string) error { if len(username) < 1 || len(username) > 64 { return errors.New("管理员名称范围错误") } h, e := hashPassword(password) if e != nil { return e } tx, e := a.db.Begin() if e != nil { return e } defer tx.Rollback() if _, e = tx.Exec("INSERT INTO admins VALUES(1,?,?) ON CONFLICT(id) DO UPDATE SET username=excluded.username,password_hash=excluded.password_hash", username, h); e != nil { return e } if _, e = tx.Exec("DELETE FROM admin_sessions"); e != nil { return e } return tx.Commit() } func (a *App) Initialized() bool { var n int a.db.QueryRow("SELECT count(*) FROM admins").Scan(&n) return n == 1 } func (a *App) Handler(static http.Handler) http.Handler { mux := http.NewServeMux() mux.HandleFunc("GET /api/v1/health", a.health) mux.HandleFunc("GET /api/v1/policy", a.publicPolicy) mux.HandleFunc("POST /api/v1/uploads", a.createUpload) mux.HandleFunc("PUT /api/v1/uploads/{parcel}/files/{file}", a.uploadFile) mux.HandleFunc("POST /api/v1/uploads/{parcel}/publish", a.publish) mux.HandleFunc("GET /api/v1/uploads/{parcel}", a.uploadStatus) mux.HandleFunc("GET /api/v1/mine", a.mine) mux.HandleFunc("POST /api/v1/parcels/{parcel}/revoke", a.revoke) mux.HandleFunc("POST /api/v1/parcels/{parcel}/code", a.regenerate) mux.HandleFunc("POST /api/v1/pickup/resolve", a.resolve) mux.HandleFunc("GET /api/v1/pickup", a.pickupHandler) mux.HandleFunc("GET /api/v1/pickup/files/{file}/download", a.download) mux.HandleFunc("POST /api/v1/admin/login", a.adminLogin) mux.HandleFunc("POST /api/v1/admin/logout", a.adminLogout) mux.HandleFunc("GET /api/v1/admin/status", a.adminStatus) mux.HandleFunc("GET /api/v1/admin/stats", a.adminStats) mux.HandleFunc("GET /api/v1/admin/runtime", a.adminRuntime) mux.HandleFunc("PUT /api/v1/admin/runtime", a.adminRuntime) mux.HandleFunc("GET /api/v1/admin/parcels", a.adminParcels) mux.HandleFunc("POST /api/v1/admin/parcels/{parcel}/delete", a.adminDelete) mux.HandleFunc("GET /api/v1/admin/policy", a.adminPolicy) mux.HandleFunc("PUT /api/v1/admin/policy", a.adminPolicy) mux.HandleFunc("GET /api/v1/admin/audit", a.adminAudit) mux.HandleFunc("POST /api/v1/admin/cleanup", a.adminCleanup) mux.HandleFunc("POST /api/v1/admin/files/{file}/delete", a.adminDeleteFile) mux.HandleFunc("POST /api/v1/admin/parcels/{parcel}/reissue", a.adminReissue) mux.HandleFunc("/api/", func(w http.ResponseWriter, r *http.Request) { fail(w, 404, "接口不存在") }) mux.Handle("/", static) return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { a.requestWG.Add(1) defer a.requestWG.Done() w.Header().Set("X-Content-Type-Options", "nosniff") w.Header().Set("Referrer-Policy", "no-referrer") w.Header().Set("X-Frame-Options", "DENY") w.Header().Set("Content-Security-Policy", "default-src 'self'; script-src 'self'; style-src 'self' 'unsafe-inline'; img-src 'self' data: blob:; connect-src 'self'; font-src 'self'; frame-ancestors 'none'; base-uri 'none'; form-action 'self'") if strings.HasPrefix(r.URL.Path, "/api/") { w.Header().Set("Cache-Control", "no-store") } if strings.HasPrefix(r.URL.Path, "/api/") { var e error r, e = a.withAccess(r) if e != nil { fail(w, 403, e.Error()) return } } if r.Method != "GET" && r.Method != "HEAD" && r.Method != "OPTIONS" { origin := r.Header.Get("Origin") if r.Header.Get("Sec-Fetch-Site") == "cross-site" { fail(w, 403, "跨站请求被拒绝") return } if origin == "" && strings.Contains(r.Header.Get("Cookie"), "bb_") { fail(w, 403, "携带会话的修改请求必须有 Origin") return } } if !a.Initialized() && r.URL.Path != "/api/v1/health" { fail(w, 503, "请先在服务器上初始化管理员") return } mux.ServeHTTP(w, r) }) } func decode(w http.ResponseWriter, r *http.Request, v any) bool { r.Body = http.MaxBytesReader(w, r.Body, 1<<20) d := json.NewDecoder(r.Body) d.DisallowUnknownFields() if e := d.Decode(v); e != nil { fail(w, 400, "请求格式无效") return false } if e := d.Decode(&struct{}{}); e != io.EOF { fail(w, 400, "请求包含多余内容") return false } return true } func respond(w http.ResponseWriter, status int, v any) { w.Header().Set("Content-Type", "application/json; charset=utf-8") w.WriteHeader(status) _ = json.NewEncoder(w).Encode(v) } func fail(w http.ResponseWriter, status int, message string) { respond(w, status, map[string]string{"error": message}) } func (a *App) serverError(w http.ResponseWriter, e error) { a.log.Error("operation failed", "error", e) fail(w, 500, "服务器操作失败,请稍后重试") } func (a *App) health(w http.ResponseWriter, r *http.Request) { if e := a.db.Ping(); e != nil { fail(w, 503, "数据库不可用") return } respond(w, 200, map[string]any{"ready": a.Initialized(), "version": "0.1.0"}) } func (a *App) publicPolicy(w http.ResponseWriter, r *http.Request) { if !a.allow(r, "policy", 60) { fail(w, 429, "访问过于频繁") return } a.mu.Lock() defer a.mu.Unlock() p, e := a.policy() if e != nil { a.serverError(w, e) return } visitor, e := a.visitor(w, r) if e != nil { fail(w, 429, "匿名会话暂不可用") return } var visitorExpires int64 if e = a.db.QueryRow("SELECT expires FROM visitors WHERE id=?", visitor).Scan(&visitorExpires); e != nil { a.serverError(w, e) return } respond(w, 200, map[string]any{"policy": p, "public_url": a.accessFor(r).base, "access_urls": a.cfg.accessOrigins(), "encryption": a.cfg.Encryption, "deduplication": a.cfg.Deduplication, "visitor_expires": visitorExpires}) } func (a *App) RunMaintenance(stop <-chan struct{}) { t := time.NewTicker(time.Minute) defer t.Stop() for { select { case <-stop: return case <-t.C: if e := a.Cleanup(); e != nil { a.log.Error("cleanup failed", "error", e) } } } } func (a *App) Check() error { var result string if e := a.db.QueryRow("PRAGMA integrity_check").Scan(&result); e != nil { return e } if result != "ok" { return fmt.Errorf("SQLite: %s", result) } return a.verifyObjects() }