初始化蓝鸟文件快递柜源码与部署文档
This commit is contained in:
commit
5643d22242
70 files changed
+18669
No files matched your search
@@ -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)
|
||||
}
|
||||
Reference in new issue
Block a user