Files

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