session_id.go 3.0 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687
  1. package service
  2. import (
  3. "strings"
  4. "unicode/utf8"
  5. "github.com/gin-gonic/gin"
  6. )
  7. // maxPersistedSessionIDLength bounds the persisted client session identifier to the
  8. // usage_logs.session_id column width (VARCHAR(255)). Longer values are rejected so
  9. // distinct identifiers can never alias through truncation.
  10. const maxPersistedSessionIDLength = 255
  11. // clientSessionIDHeaders extends the OpenAI-compatible sticky-session signals with
  12. // native protocol identifiers that are safe to persist but must not alter OpenAI
  13. // scheduling behavior.
  14. var clientSessionIDHeaders = append(
  15. append([]string(nil), explicitOpenAIHeaderSessionNames...),
  16. claudeCodeSessionHeader,
  17. )
  18. // ClaudeCodeSessionIDFromHeader returns the stable Claude Code conversation
  19. // identifier carried by X-Claude-Code-Session-Id. It is intentionally exposed
  20. // separately from ExtractClientSessionID: callers that use it for routing must
  21. // make that scope explicit rather than accidentally changing every protocol's
  22. // session semantics.
  23. func ClaudeCodeSessionIDFromHeader(c *gin.Context) string {
  24. if c == nil || c.Request == nil {
  25. return ""
  26. }
  27. return sanitizeSessionID(c.GetHeader(claudeCodeSessionHeader))
  28. }
  29. // ExtractClientSessionID resolves the explicit client-provided session identifier from
  30. // request headers for usage-log correlation and returns it sanitized. It is
  31. // protocol-agnostic and shared by every gateway handler so all supported protocols
  32. // record session_id through one seam. Returns "" when no valid identifier is present.
  33. //
  34. // This value feeds only usage_logs.session_id persistence. It does NOT affect sticky
  35. // routing, account selection, request_id semantics, or upstream prompt caching, which
  36. // keep their own (intentionally broader) session-signal resolution.
  37. func ExtractClientSessionID(c *gin.Context) string {
  38. if c == nil || c.Request == nil {
  39. return ""
  40. }
  41. for _, header := range clientSessionIDHeaders {
  42. if sessionID := sanitizeSessionID(c.GetHeader(header)); sessionID != "" {
  43. return sessionID
  44. }
  45. }
  46. if isGrokRequestContext(c) {
  47. if sessionID := sanitizeSessionID(c.GetHeader(grokConversationIDHeader)); sessionID != "" {
  48. return sessionID
  49. }
  50. }
  51. return ""
  52. }
  53. // sanitizeSessionID normalizes a raw client-supplied session identifier for safe
  54. // persistence: it trims surrounding whitespace, rejects the value outright if it
  55. // contains any control character (CR/LF/tab/NUL/…) so a log- or header-injection style
  56. // payload cannot slip into stored correlation data, and rejects values longer than
  57. // the DB column bound. Absent or invalid input yields "".
  58. func sanitizeSessionID(raw string) string {
  59. if !utf8.ValidString(raw) {
  60. return ""
  61. }
  62. trimmed := strings.TrimSpace(raw)
  63. if trimmed == "" {
  64. return ""
  65. }
  66. count := 0
  67. for _, r := range trimmed {
  68. if r < 0x20 || r == 0x7f {
  69. // An explicit correlation id never legitimately contains control
  70. // characters; drop the whole value rather than persist a mangled or
  71. // partially-injected identifier.
  72. return ""
  73. }
  74. count++
  75. if count > maxPersistedSessionIDLength {
  76. return ""
  77. }
  78. }
  79. return trimmed
  80. }