mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-06-20 03:17:20 +08:00
feat(executor): implement identity obfuscation for Codex requests and responses
- Added `applyCodexIdentityConfuse*` functions for remapping request and response payloads and headers to enhance security. - Updated WebSocket and HTTP logic to handle identity state transformations seamlessly. - Introduced unit tests to verify remapping and restoration of identity-related fields.
This commit is contained in:
@@ -197,7 +197,7 @@ func TestApplyCodexWebsocketHeadersPassesThroughClientIdentityHeaders(t *testing
|
||||
"Version": "0.115.0-alpha.27",
|
||||
"X-Codex-Turn-Metadata": `{"turn_id":"turn-1"}`,
|
||||
"X-Client-Request-Id": "019d2233-e240-7162-992d-38df0a2a0e0d",
|
||||
"session_id": "sess-client",
|
||||
"session_id": "legacy-session",
|
||||
})
|
||||
|
||||
headers := applyCodexWebsocketHeaders(ctx, http.Header{}, auth, "", nil)
|
||||
@@ -217,11 +217,8 @@ func TestApplyCodexWebsocketHeadersPassesThroughClientIdentityHeaders(t *testing
|
||||
if got := headers.Get("X-Client-Request-Id"); got != "019d2233-e240-7162-992d-38df0a2a0e0d" {
|
||||
t.Fatalf("X-Client-Request-Id = %s, want %s", got, "019d2233-e240-7162-992d-38df0a2a0e0d")
|
||||
}
|
||||
if got := headerValueCaseInsensitive(headers, "session_id"); got != "sess-client" {
|
||||
t.Fatalf("session_id = %s, want sess-client", got)
|
||||
}
|
||||
if _, ok := headers["session_id"]; !ok {
|
||||
t.Fatalf("expected lowercase session_id header key, got %#v", headers)
|
||||
if got := headerValueCaseInsensitive(headers, "session_id"); got != "" {
|
||||
t.Fatalf("session_id = %q, want empty", got)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -344,22 +341,101 @@ func TestApplyCodexWebsocketHeadersPreservesExplicitAPIKeyUserAgent(t *testing.T
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyCodexPromptCacheHeadersSetsLowercaseSessionAndLegacyConversation(t *testing.T) {
|
||||
func TestApplyCodexPromptCacheHeadersSetsLegacyConversationOnly(t *testing.T) {
|
||||
req := cliproxyexecutor.Request{Model: "gpt-5-codex", Payload: []byte(`{"prompt_cache_key":"cache-1"}`)}
|
||||
|
||||
_, headers := applyCodexPromptCacheHeaders("openai-response", req, []byte(`{"model":"gpt-5-codex"}`))
|
||||
|
||||
if got := headerValueCaseInsensitive(headers, "session_id"); got != "cache-1" {
|
||||
t.Fatalf("session_id = %s, want cache-1", got)
|
||||
}
|
||||
if _, ok := headers["session_id"]; !ok {
|
||||
t.Fatalf("expected lowercase session_id key, got %#v", headers)
|
||||
if got := headerValueCaseInsensitive(headers, "session_id"); got != "" {
|
||||
t.Fatalf("session_id = %q, want empty", got)
|
||||
}
|
||||
if got := headers.Get("Conversation_id"); got != "cache-1" {
|
||||
t.Fatalf("Conversation_id = %s, want cache-1", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyCodexWebsocketHeadersIdentityConfuseRemapsPromptCacheKey(t *testing.T) {
|
||||
cfg := &config.Config{
|
||||
Routing: config.RoutingConfig{SessionAffinity: true},
|
||||
Codex: config.CodexConfig{IdentityConfuse: true},
|
||||
}
|
||||
auth := &cliproxyauth.Auth{ID: "auth-ws-1", Provider: "codex"}
|
||||
req := cliproxyexecutor.Request{
|
||||
Model: "gpt-5-codex",
|
||||
Payload: []byte(`{"prompt_cache_key":"cache-ws-1","client_metadata":{"x-codex-installation-id":"install-ws-1"}}`),
|
||||
}
|
||||
|
||||
body, headers := applyCodexPromptCacheHeaders("openai-response", req, []byte(`{"model":"gpt-5-codex"}`))
|
||||
body, identityState := applyCodexIdentityConfuseBody(cfg, auth, req.Payload, body)
|
||||
if identityState.promptCacheKey != "" {
|
||||
headers.Set("Conversation_id", identityState.promptCacheKey)
|
||||
}
|
||||
ctx := contextWithGinHeaders(map[string]string{
|
||||
"X-Codex-Turn-Metadata": `{"prompt_cache_key":"cache-ws-1"}`,
|
||||
"X-Client-Request-Id": "client-request-1",
|
||||
})
|
||||
headers = applyCodexWebsocketHeaders(ctx, headers, auth, "oauth-token", cfg)
|
||||
applyCodexIdentityConfuseHeaders(headers, identityState)
|
||||
|
||||
expectedPromptCacheKey := codexIdentityConfuseUUID("auth-ws-1", "prompt-cache", "cache-ws-1")
|
||||
if gotKey := gjson.GetBytes(body, "prompt_cache_key").String(); gotKey != expectedPromptCacheKey {
|
||||
t.Fatalf("prompt_cache_key = %q, want %q", gotKey, expectedPromptCacheKey)
|
||||
}
|
||||
if gotSession := headerValueCaseInsensitive(headers, "session_id"); gotSession != "" {
|
||||
t.Fatalf("session_id = %q, want empty", gotSession)
|
||||
}
|
||||
if gotRequestID := headers.Get("X-Client-Request-Id"); gotRequestID != expectedPromptCacheKey {
|
||||
t.Fatalf("X-Client-Request-Id = %q, want %q", gotRequestID, expectedPromptCacheKey)
|
||||
}
|
||||
if gotThreadID := headers.Get("Thread-Id"); gotThreadID != expectedPromptCacheKey {
|
||||
t.Fatalf("Thread-Id = %q, want %q", gotThreadID, expectedPromptCacheKey)
|
||||
}
|
||||
if gotWindowID := headers.Get("X-Codex-Window-Id"); gotWindowID != expectedPromptCacheKey+":0" {
|
||||
t.Fatalf("X-Codex-Window-Id = %q, want %q", gotWindowID, expectedPromptCacheKey+":0")
|
||||
}
|
||||
if gotMetadata := headers.Get("X-Codex-Turn-Metadata"); gotMetadata != `{"prompt_cache_key":"`+expectedPromptCacheKey+`"}` {
|
||||
t.Fatalf("X-Codex-Turn-Metadata = %s", gotMetadata)
|
||||
}
|
||||
expectedInstallationID := codexIdentityConfuseUUID("auth-ws-1", "installation", "install-ws-1")
|
||||
if gotInstallationID := gjson.GetBytes(body, "client_metadata.x-codex-installation-id").String(); gotInstallationID != expectedInstallationID {
|
||||
t.Fatalf("installation id = %q, want %q", gotInstallationID, expectedInstallationID)
|
||||
}
|
||||
}
|
||||
|
||||
func TestCodexIdentityConfuseResponsePayloadHidesUpstreamAndRestoresClient(t *testing.T) {
|
||||
state := codexIdentityConfuseState{
|
||||
originalPromptCacheKey: "cache-ws-1",
|
||||
promptCacheKey: codexIdentityConfuseUUID("auth-ws-1", "prompt-cache", "cache-ws-1"),
|
||||
}
|
||||
rawPayload := []byte(`{"type":"response.completed","response":{"prompt_cache_key":"cache-ws-1"},"prompt_cache_key":"cache-ws-1"}`)
|
||||
|
||||
upstreamPayload := applyCodexIdentityConfuseResponsePayload(rawPayload, state)
|
||||
if bytes.Contains(upstreamPayload, []byte(`cache-ws-1`)) {
|
||||
t.Fatalf("upstream payload still contains original prompt_cache_key: %s", string(upstreamPayload))
|
||||
}
|
||||
if !bytes.Contains(upstreamPayload, []byte(state.promptCacheKey)) {
|
||||
t.Fatalf("upstream payload missing confused prompt_cache_key: %s", string(upstreamPayload))
|
||||
}
|
||||
|
||||
clientPayload := applyCodexIdentityExposeResponsePayload(upstreamPayload, state)
|
||||
if bytes.Contains(clientPayload, []byte(state.promptCacheKey)) {
|
||||
t.Fatalf("client payload still contains confused prompt_cache_key: %s", string(clientPayload))
|
||||
}
|
||||
if !bytes.Contains(clientPayload, []byte(`cache-ws-1`)) {
|
||||
t.Fatalf("client payload missing original prompt_cache_key: %s", string(clientPayload))
|
||||
}
|
||||
|
||||
rawSSE := []byte(`data: {"type":"response.completed","response":{"prompt_cache_key":"cache-ws-1"}}`)
|
||||
upstreamSSE := applyCodexIdentityConfuseResponsePayload(rawSSE, state)
|
||||
if bytes.Contains(upstreamSSE, []byte(`cache-ws-1`)) {
|
||||
t.Fatalf("upstream SSE still contains original prompt_cache_key: %s", string(upstreamSSE))
|
||||
}
|
||||
clientSSE := applyCodexIdentityExposeResponsePayload(upstreamSSE, state)
|
||||
if !bytes.Contains(clientSSE, []byte(`cache-ws-1`)) || bytes.Contains(clientSSE, []byte(state.promptCacheKey)) {
|
||||
t.Fatalf("client SSE prompt_cache_key was not restored: %s", string(clientSSE))
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplyCodexWebsocketHeadersUsesCanonicalAccountHeader(t *testing.T) {
|
||||
auth := &cliproxyauth.Auth{Provider: "codex", Metadata: map[string]any{"account_id": "acct-1"}}
|
||||
|
||||
|
||||
Reference in New Issue
Block a user