package app import ( "crypto/hmac" "crypto/rand" "crypto/sha256" "crypto/subtle" "encoding/base64" "encoding/hex" "errors" "fmt" "io" "net" "net/http" "strconv" "strings" "time" "golang.org/x/crypto/argon2" "golang.org/x/time/rate" ) func randomToken() string { b := make([]byte, 32) if _, e := rand.Read(b); e != nil { panic(e) } return base64.RawURLEncoding.EncodeToString(b) } func randomID() string { b := make([]byte, 16) if _, e := rand.Read(b); e != nil { panic(e) } return hex.EncodeToString(b) } func tokenHash(s string) string { h := sha256.Sum256([]byte(s)); return hex.EncodeToString(h[:]) } func (a *App) keyed(s string) string { h := hmac.New(sha256.New, a.secret) h.Write([]byte(s)) return hex.EncodeToString(h.Sum(nil)) } func pickupCode() string { b := make([]byte, 10) if _, e := rand.Read(b); e != nil { panic(e) } const alphabet = "0123456789ABCDEFGHJKMNPQRSTVWXYZ" for i := range b { b[i] = alphabet[b[i]&31] } return string(b) } func normalizeCode(s string) string { s = strings.ToUpper(strings.ReplaceAll(strings.TrimSpace(s), "-", "")) s = strings.ReplaceAll(s, "O", "0") s = strings.ReplaceAll(s, "I", "1") return strings.ReplaceAll(s, "L", "1") } func hashPassword(p string) (string, error) { if len(p) < 8 || len(p) > 128 { return "", errors.New("密码需为 8–128 字节") } salt := make([]byte, 16) if _, e := rand.Read(salt); e != nil { return "", e } h := argon2.IDKey([]byte(p), salt, 2, 32*1024, 1, 32) return fmt.Sprintf("v1$%s$%s", base64.RawStdEncoding.EncodeToString(salt), base64.RawStdEncoding.EncodeToString(h)), nil } func verifyPassword(p, h string) bool { parts := strings.Split(h, "$") if len(p) > 128 || len(parts) != 3 || parts[0] != "v1" { return false } salt, e := base64.RawStdEncoding.DecodeString(parts[1]) if e != nil || len(salt) != 16 { return false } want, e := base64.RawStdEncoding.DecodeString(parts[2]) if e != nil || len(want) != 32 { return false } got := argon2.IDKey([]byte(p), salt, 2, 32*1024, 1, 32) return subtle.ConstantTimeCompare(want, got) == 1 } func (a *App) remoteIP(r *http.Request) string { host, _, e := net.SplitHostPort(r.RemoteAddr) if e != nil { host = r.RemoteAddr } ip := net.ParseIP(host) if ip == nil { return "unknown" } trusted := func(v net.IP) bool { for _, n := range a.proxies { if n.Contains(v) { return true } } return false } if trusted(ip) { parts := strings.Split(r.Header.Get("X-Forwarded-For"), ",") if len(parts) <= 16 { for i := len(parts) - 1; i >= 0; i-- { candidate := net.ParseIP(strings.TrimSpace(parts[i])) if candidate == nil { break } ip = candidate if !trusted(ip) { break } } } } if v := ip.To4(); v != nil { return v.String() } return ip.Mask(net.CIDRMask(64, 128)).String() + "/64" } func (a *App) cookie(w http.ResponseWriter, r *http.Request, name, value, path string, seconds int) { state := a.accessFor(r) if state.httpCookie { name += "_http" } http.SetCookie(w, &http.Cookie{Name: name, Value: value, Path: path, MaxAge: seconds, HttpOnly: true, Secure: strings.HasPrefix(state.base, "https://"), SameSite: http.SameSiteStrictMode}) } func (a *App) visitor(w http.ResponseWriter, r *http.Request) (string, error) { now := a.now().Unix() if token := cookieValue(r, "bb_visitor"); token != "" && len(token) < 128 { var id string e := a.db.QueryRow("SELECT id FROM visitors WHERE token_hash=? AND expires>?", tokenHash(token), now).Scan(&id) if e == nil { return id, nil } } var count int if e := a.db.QueryRow("SELECT count(*) FROM visitors").Scan(&count); e != nil { return "", e } if count >= a.cfg.MaxRecords { return "", errors.New("匿名会话已满,请稍后重试") } id, t := randomID(), randomToken() _, e := a.db.Exec("INSERT INTO visitors(id,token_hash,expires) VALUES(?,?,?)", id, tokenHash(t), now+7*86400) if e == nil { a.cookie(w, r, "bb_visitor", t, "/api/v1", 7*86400) } return id, e } func (a *App) currentVisitor(r *http.Request) string { token := cookieValue(r, "bb_visitor") if token == "" { return "" } var id string a.db.QueryRow("SELECT id FROM visitors WHERE token_hash=? AND expires>?", tokenHash(token), a.now().Unix()).Scan(&id) return id } func (a *App) isAdmin(r *http.Request) bool { token := cookieValue(r, "bb_admin") if token == "" { return false } var n int e := a.db.QueryRow("SELECT count(*) FROM admin_sessions WHERE token_hash=? AND expires>?", tokenHash(token), a.now().Unix()).Scan(&n) return e == nil && n == 1 } type rateEntry struct { limiter *rate.Limiter touched time.Time } func (a *App) allow(r *http.Request, kind string, perMinute int) bool { a.rateMu.Lock() defer a.rateMu.Unlock() now := a.now() key := kind + ":" + a.remoteIP(r) entry := a.rates[key] if entry == nil { if len(a.rates) >= 10000 { for k, v := range a.rates { if now.Sub(v.touched) > 10*time.Minute { delete(a.rates, k) } } if len(a.rates) >= 10000 { return false } } entry = &rateEntry{limiter: rate.NewLimiter(rate.Limit(float64(perMinute)/60), max(2, perMinute/3))} a.rates[key] = entry } entry.touched = now return entry.limiter.Allow() } func (a *App) acquire(kind, ip string) bool { a.transferMu.Lock() defer a.transferMu.Unlock() limit, perIP := a.cfg.MaxUploads, a.cfg.MaxIPUploads if kind == "download" { limit, perIP = a.cfg.MaxDownloads, a.cfg.MaxIPDownloads } if a.transfers[kind] >= limit || a.transfers[kind+":"+ip] >= perIP { return false } a.transfers[kind]++ a.transfers[kind+":"+ip]++ return true } func (a *App) release(kind, ip string) { a.transferMu.Lock() defer a.transferMu.Unlock() a.transfers[kind]-- a.transfers[kind+":"+ip]-- if a.transfers[kind+":"+ip] == 0 { delete(a.transfers, kind+":"+ip) } } func (a *App) deadlines(w http.ResponseWriter, r *http.Request) { c := http.NewResponseController(w) end := time.Now().Add(time.Duration(a.cfg.MaxTransferSeconds) * time.Second) _ = c.SetReadDeadline(end) _ = c.SetWriteDeadline(end) } func cookieValue(r *http.Request, name string) string { for _, key := range cookieNames(r, name) { if c, err := r.Cookie(key); err == nil && c.Value != "" { return c.Value } } return "" } func parseInt(s string) int { v, _ := strconv.Atoi(s); return v } type idleReader struct { reader io.Reader controller *http.ResponseController total time.Time idle time.Duration } func (r *idleReader) Read(p []byte) (int, error) { end := time.Now().Add(r.idle) if end.After(r.total) { end = r.total } _ = r.controller.SetReadDeadline(end) return r.reader.Read(p) }