Files
Scoreboard/server/internal/middleware/context.go
T
AskaEth 7f9701c465 fix: 审查修复波次(P0x4 + P1x30+)
- domain/rules: 计分与排名解耦、破纪录按组别、tieBreak 生效、调整分不丢、移除 aggregate 假能力
- realtime: 新建 internal/dto,快照三项契约对齐,/ws 只认会话身份
- httpapi: 流水幂等、standings 整表载荷、广播接线、强制改密、XFF 防伪造、records 写入、审计、赛事包内容、发布前备份、result.points、实例列表
- web: FormData 上传、YAML 解析、快照归一化、大屏解析层、bootstrap 降级、访客判定、主题切换
- console: 优雅关闭握手、中文化、免验证改管理员账号、选屏与日志轮转修复
- docs: 用户文档三份 + README
- 主代理: 破纪录加分接线(levels 未加载导致恒 0)
2026-09-09 21:06:40 +08:00

127 lines
3.3 KiB
Go
Raw 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 middleware 提供请求日志、错误恢复、认证/授权与审计等 HTTP 中间件。
package middleware
import (
"context"
"fmt"
"net"
"net/http"
"strings"
"sync/atomic"
"git.yeij.top/AskaEth/Scoreboard/server/internal/auth"
)
type ctxKey int
const (
ctxKeyUser ctxKey = iota
ctxKeyRequestID
ctxKeyAudit
)
// User 返回当前请求的身份;未经 Authenticate 中间件时是 guest。
func User(ctx context.Context) *auth.User {
if u, ok := ctx.Value(ctxKeyUser).(*auth.User); ok {
return u
}
return &auth.User{Role: auth.RoleGuest}
}
// WithUser 把身份写入 context,供测试或非标准链路使用。
func WithUser(ctx context.Context, u *auth.User) context.Context {
return context.WithValue(ctx, ctxKeyUser, u)
}
// GetRequestID 返回本次请求的 request_id。
func GetRequestID(ctx context.Context) string {
if v, ok := ctx.Value(ctxKeyRequestID).(string); ok {
return v
}
return ""
}
// trustedProxies 是可信代理网段白名单;默认空,表示任何代理头都不可信。
var trustedProxies atomic.Pointer[[]*net.IPNet]
// SetTrustedProxies 配置可信代理(IP 或 CIDR 列表);传空表示不信任任何代理头。
func SetTrustedProxies(entries []string) error {
nets := make([]*net.IPNet, 0, len(entries))
for _, raw := range entries {
entry := strings.TrimSpace(raw)
if entry == "" {
continue
}
if strings.Contains(entry, "/") {
_, n, err := net.ParseCIDR(entry)
if err != nil {
return fmt.Errorf("middleware: 非法可信代理 %q: %w", entry, err)
}
nets = append(nets, n)
continue
}
ip := net.ParseIP(entry)
if ip == nil {
return fmt.Errorf("middleware: 非法可信代理 %q", entry)
}
bits := 128
if ip.To4() != nil {
bits = 32
}
nets = append(nets, &net.IPNet{IP: ip, Mask: net.CIDRMask(bits, bits)})
}
trustedProxies.Store(&nets)
return nil
}
func isTrustedProxy(ip net.IP) bool {
nets := trustedProxies.Load()
if nets == nil || ip == nil {
return false
}
for _, n := range *nets {
if n.Contains(ip) {
return true
}
}
return false
}
func remoteHost(addr string) string {
host, _, err := net.SplitHostPort(addr)
if err != nil {
return addr
}
return host
}
// ClientIP 解析客户端 IP。
// 只有当直连对端(RemoteAddr)属于可信代理白名单时才采信 X-Forwarded-For / X-Real-IP,
// 否则一律用 RemoteAddr——代理头是客户端可伪造的,据此限流等于没有限流。
func ClientIP(r *http.Request) string {
remote := net.ParseIP(remoteHost(r.RemoteAddr))
if remote == nil || !isTrustedProxy(remote) {
return remoteHost(r.RemoteAddr)
}
if xff := r.Header.Get("X-Forwarded-For"); xff != "" {
parts := strings.Split(xff, ",")
// 从右往左取第一个非代理地址:这是可信代理链上离服务端最近的真实客户端。
for i := len(parts) - 1; i >= 0; i-- {
ip := net.ParseIP(strings.TrimSpace(parts[i]))
if ip == nil || isTrustedProxy(ip) {
continue
}
return ip.String()
}
// 整条链路都是可信代理时,最左侧即原始客户端。
if ip := net.ParseIP(strings.TrimSpace(parts[0])); ip != nil {
return ip.String()
}
}
if ip := net.ParseIP(strings.TrimSpace(r.Header.Get("X-Real-IP"))); ip != nil {
return ip.String()
}
return remoteHost(r.RemoteAddr)
}