初始化蓝鸟文件快递柜源码与部署文档

This commit is contained in:
lhc committed 2026-10-05 23:41:07 +08:00
commit 5643d22242
70 files changed
+18669

No files matched your search

+254
View File
@@ -0,0 +1,254 @@
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)
}