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) }