255 lines
6.5 KiB
Go
255 lines
6.5 KiB
Go
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)
|
||
}
|