144 lines
3.2 KiB
Go
144 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
|
|
AppID uint32
|
|
}
|
|
|
|
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)
|
|
}
|