package reqctx import ( "context" "crypto/rand" "encoding/hex" "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 { TraceID string 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 "" } // 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 } // NewID returns a 16-byte hex id for trace_id / request_id. func NewID() string { var b [16]byte if _, err := rand.Read(b[:]); err != nil { return "00000000000000000000000000000000" } return hex.EncodeToString(b[:]) }