Files
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

76 lines
2.5 KiB
Go

package service
import (
"strings"
"unicode/utf8"
"github.com/gin-gonic/gin"
)
// maxPersistedSessionIDLength bounds the persisted client session identifier to the
// usage_logs.session_id column width (VARCHAR(255)). Longer values are rejected so
// distinct identifiers can never alias through truncation.
const maxPersistedSessionIDLength = 255
// clientSessionIDHeaders extends the OpenAI-compatible sticky-session signals with
// native protocol identifiers that are safe to persist but must not alter OpenAI
// scheduling behavior.
var clientSessionIDHeaders = append(
append([]string(nil), explicitOpenAIHeaderSessionNames...),
claudeCodeSessionHeader,
)
// ExtractClientSessionID resolves the explicit client-provided session identifier from
// request headers for usage-log correlation and returns it sanitized. It is
// protocol-agnostic and shared by every gateway handler so all supported protocols
// record session_id through one seam. Returns "" when no valid identifier is present.
//
// This value feeds only usage_logs.session_id persistence. It does NOT affect sticky
// routing, account selection, request_id semantics, or upstream prompt caching, which
// keep their own (intentionally broader) session-signal resolution.
func ExtractClientSessionID(c *gin.Context) string {
if c == nil || c.Request == nil {
return ""
}
for _, header := range clientSessionIDHeaders {
if sessionID := sanitizeSessionID(c.GetHeader(header)); sessionID != "" {
return sessionID
}
}
if isGrokRequestContext(c) {
if sessionID := sanitizeSessionID(c.GetHeader(grokConversationIDHeader)); sessionID != "" {
return sessionID
}
}
return ""
}
// sanitizeSessionID normalizes a raw client-supplied session identifier for safe
// persistence: it trims surrounding whitespace, rejects the value outright if it
// contains any control character (CR/LF/tab/NUL/…) so a log- or header-injection style
// payload cannot slip into stored correlation data, and rejects values longer than
// the DB column bound. Absent or invalid input yields "".
func sanitizeSessionID(raw string) string {
if !utf8.ValidString(raw) {
return ""
}
trimmed := strings.TrimSpace(raw)
if trimmed == "" {
return ""
}
count := 0
for _, r := range trimmed {
if r < 0x20 || r == 0x7f {
// An explicit correlation id never legitimately contains control
// characters; drop the whole value rather than persist a mangled or
// partially-injected identifier.
return ""
}
count++
if count > maxPersistedSessionIDLength {
return ""
}
}
return trimmed
}