spark-server/internal/pkg/reqctx/reqctx.go
2026-09-18 17:14:38 +08:00

143 lines
3.2 KiB
Go

package reqctx
import (
"context"
"crypto/rand"
"encoding/binary"
"math/big"
"net"
"strings"
"time"
"spark-server/internal/pkg/i18n"
)
type ctxKey struct{}
// RequestContext is the per-request identity. Use RealIP for rate limit and audit, never 127.0.0.1 from gnet.RemoteAddr.
type RequestContext struct {
RequestID string
UserID int64
DeviceID uint64
Language string
AppVersion string
ConnectionID string
RemoteAddr string
RealIP string
JTI string
TokenExpires time.Time
}
func With(ctx context.Context, rc *RequestContext) context.Context {
return context.WithValue(ctx, ctxKey{}, rc)
}
func From(ctx context.Context) *RequestContext {
rc, _ := ctx.Value(ctxKey{}).(*RequestContext)
return rc
}
// RealIP returns the client IP from context (PROXY / X-Real-IP), not gnet RemoteAddr.
func RealIP(ctx context.Context) string {
if rc := From(ctx); rc != nil {
return rc.RealIP
}
return ""
}
// RequestID returns the server-generated request id stored on context.
func RequestID(ctx context.Context) string {
if rc := From(ctx); rc != nil {
return rc.RequestID
}
return ""
}
// Lang is the canonical UI language (zh-CN / en / zh-TW) for errors and notifications.
func Lang(ctx context.Context) i18n.Lang {
if rc := From(ctx); rc != nil {
return i18n.Parse(rc.Language)
}
return i18n.Default
}
// Language returns the canonical language tag stored on context.
func Language(ctx context.Context) string {
return Lang(ctx).String()
}
// ParseHTTPRealIP prefers X-Real-IP, then X-Forwarded-For, then RemoteAddr host.
func ParseHTTPRealIP(realIPHeader, forwardedFor, remoteAddr string) string {
if ip := strings.TrimSpace(realIPHeader); ip != "" {
return stripHost(ip)
}
if forwardedFor != "" {
first, _, _ := strings.Cut(forwardedFor, ",")
if ip := stripHost(strings.TrimSpace(first)); ip != "" {
return ip
}
}
return stripHost(remoteAddr)
}
func stripHost(v string) string {
v = strings.TrimSpace(v)
if v == "" {
return ""
}
host, _, err := net.SplitHostPort(v)
if err == nil {
return host
}
return v
}
const (
base62Alphabet = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz"
base62Len = 11
objectNameLen = 16
objectNameSrc = 12
)
// NewID returns an 8-byte base62 id (11 chars) for request_id and JWT jti.
func NewID() string {
var b [8]byte
if _, err := rand.Read(b[:]); err != nil {
return "00000000000"
}
return encodeBase62(binary.BigEndian.Uint64(b[:]))
}
// NewObjectName returns a 12-byte base62 id (16 chars) for object_key filenames. Not Snowflake.
func NewObjectName() string {
return randomBase62(objectNameSrc, objectNameLen)
}
func encodeBase62(n uint64) string {
var out [base62Len]byte
for i := base62Len - 1; i >= 0; i-- {
out[i] = base62Alphabet[n%62]
n /= 62
}
return string(out[:])
}
func randomBase62(byteN, width int) string {
if byteN < 1 || width < 1 {
return ""
}
b := make([]byte, byteN)
if _, err := rand.Read(b); err != nil {
return strings.Repeat("0", width)
}
n := new(big.Int).SetBytes(b)
out := make([]byte, width)
base := big.NewInt(62)
r := new(big.Int)
for i := width - 1; i >= 0; i-- {
n.DivMod(n, base, r)
out[i] = base62Alphabet[r.Int64()]
}
return string(out)
}