mirror of
https://github.com/router-for-me/CLIProxyAPI.git
synced 2026-09-11 14:47:58 +08:00
Stop refreshing Home-owned OAuth credentials after upstream 401s. Preserve marked upstream response bodies for direct responses, usage records, request logs, and websocket handshake failures.
1316 lines
47 KiB
Go
1316 lines
47 KiB
Go
package live
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
"github.com/gorilla/websocket"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/internal/config"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/auth"
|
|
"github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executionregistry"
|
|
coreexecutor "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/executor"
|
|
coreusage "github.com/router-for-me/CLIProxyAPI/v7/sdk/cliproxy/usage"
|
|
)
|
|
|
|
type apiKeyFirstSelector struct{}
|
|
|
|
func (*apiKeyFirstSelector) Pick(_ context.Context, _ string, _ string, _ coreexecutor.Options, auths []*auth.Auth) (*auth.Auth, error) {
|
|
for _, candidate := range auths {
|
|
if candidate.AuthKind() == auth.AuthKindAPIKey {
|
|
return candidate, nil
|
|
}
|
|
}
|
|
if len(auths) == 0 {
|
|
return nil, nil
|
|
}
|
|
return auths[0], nil
|
|
}
|
|
|
|
type captureExecutor struct {
|
|
request *http.Request
|
|
body []byte
|
|
selectedAuth *auth.Auth
|
|
responseBody io.ReadCloser
|
|
statusCode int
|
|
statuses []int
|
|
httpCalls atomic.Int32
|
|
refreshCalls atomic.Int32
|
|
beforeReturn func()
|
|
}
|
|
|
|
func (*captureExecutor) Identifier() string { return "codex" }
|
|
|
|
func (*captureExecutor) Execute(context.Context, *auth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
|
|
return coreexecutor.Response{}, nil
|
|
}
|
|
|
|
func (*captureExecutor) ExecuteStream(context.Context, *auth.Auth, coreexecutor.Request, coreexecutor.Options) (*coreexecutor.StreamResult, error) {
|
|
return nil, nil
|
|
}
|
|
|
|
func (e *captureExecutor) Refresh(_ context.Context, credential *auth.Auth) (*auth.Auth, error) {
|
|
e.refreshCalls.Add(1)
|
|
updated := credential.Clone()
|
|
if updated.Metadata == nil {
|
|
updated.Metadata = make(map[string]any)
|
|
}
|
|
updated.Metadata["access_token"] = "refreshed-home-live-token"
|
|
return updated, nil
|
|
}
|
|
|
|
func (*captureExecutor) CountTokens(context.Context, *auth.Auth, coreexecutor.Request, coreexecutor.Options) (coreexecutor.Response, error) {
|
|
return coreexecutor.Response{}, nil
|
|
}
|
|
|
|
func (*captureExecutor) PrepareRequest(req *http.Request, credential *auth.Auth) error {
|
|
token, _ := credential.Metadata["access_token"].(string)
|
|
req.Header.Set("Authorization", "Bearer "+token)
|
|
return nil
|
|
}
|
|
|
|
func (e *captureExecutor) HttpRequest(_ context.Context, credential *auth.Auth, req *http.Request) (*http.Response, error) {
|
|
e.request = req.Clone(req.Context())
|
|
e.selectedAuth = credential.Clone()
|
|
httpCall := int(e.httpCalls.Add(1))
|
|
body, errRead := io.ReadAll(req.Body)
|
|
if errRead != nil {
|
|
return nil, errRead
|
|
}
|
|
e.body = body
|
|
statusCode := e.statusCode
|
|
if httpCall <= len(e.statuses) && e.statuses[httpCall-1] > 0 {
|
|
statusCode = e.statuses[httpCall-1]
|
|
}
|
|
if statusCode == 0 {
|
|
statusCode = http.StatusCreated
|
|
}
|
|
responseBody := e.responseBody
|
|
if statusCode == http.StatusUnauthorized && httpCall < len(e.statuses) {
|
|
responseBody = io.NopCloser(strings.NewReader("unauthorized"))
|
|
}
|
|
if e.beforeReturn != nil {
|
|
e.beforeReturn()
|
|
}
|
|
return &http.Response{
|
|
StatusCode: statusCode,
|
|
Header: http.Header{
|
|
"Connection": []string{"X-Connection-Secret"},
|
|
"Content-Type": []string{"application/sdp"},
|
|
"Location": []string{"/v1/live/call-123"},
|
|
"Set-Cookie": []string{"session=secret"},
|
|
"X-Connection-Secret": []string{"secret"},
|
|
"X-Live-Session": []string{"live-session-123"},
|
|
},
|
|
Body: responseBody,
|
|
}, nil
|
|
}
|
|
|
|
type homeDispatcher struct {
|
|
model string
|
|
authID string
|
|
}
|
|
|
|
type homeUnauthorizedUsageCapture struct {
|
|
authID string
|
|
records chan coreusage.Record
|
|
}
|
|
|
|
func (p *homeUnauthorizedUsageCapture) HandleUsage(_ context.Context, record coreusage.Record) {
|
|
if p == nil || record.ExecutorType != "home-result" || record.AuthID != p.authID {
|
|
return
|
|
}
|
|
select {
|
|
case p.records <- record:
|
|
default:
|
|
}
|
|
}
|
|
|
|
func (p *homeUnauthorizedUsageCapture) wait(t *testing.T) coreusage.Record {
|
|
t.Helper()
|
|
select {
|
|
case record := <-p.records:
|
|
return record
|
|
case <-time.After(time.Second):
|
|
t.Fatal("timed out waiting for Home unauthorized usage record")
|
|
return coreusage.Record{}
|
|
}
|
|
}
|
|
|
|
type noopHomeUnauthorizedUsagePlugin struct{}
|
|
|
|
func (noopHomeUnauthorizedUsagePlugin) HandleUsage(context.Context, coreusage.Record) {}
|
|
|
|
func registerHomeUnauthorizedUsageCapture(t *testing.T, name, authID string) *homeUnauthorizedUsageCapture {
|
|
t.Helper()
|
|
capture := &homeUnauthorizedUsageCapture{authID: authID, records: make(chan coreusage.Record, 1)}
|
|
coreusage.RegisterNamedPlugin(name, capture)
|
|
t.Cleanup(func() {
|
|
coreusage.RegisterNamedPlugin(name, noopHomeUnauthorizedUsagePlugin{})
|
|
})
|
|
return capture
|
|
}
|
|
|
|
func (*homeDispatcher) HeartbeatOK() bool { return true }
|
|
|
|
func (d *homeDispatcher) RPopAuth(_ context.Context, model string, _ string, _ http.Header, _ int) ([]byte, error) {
|
|
d.model = model
|
|
authID := d.authID
|
|
if authID == "" {
|
|
authID = "home-codex-live"
|
|
}
|
|
return json.Marshal(map[string]any{
|
|
"model": model,
|
|
"provider": "codex",
|
|
"auth_index": authID,
|
|
"auth": map[string]any{
|
|
"id": authID,
|
|
"provider": "codex",
|
|
"status": "active",
|
|
"metadata": map[string]any{"access_token": "home-live-token"},
|
|
},
|
|
"concurrency": map[string]any{
|
|
"accounted": true,
|
|
"credential_id": authID,
|
|
"model": model,
|
|
},
|
|
})
|
|
}
|
|
|
|
func (*homeDispatcher) AbortAmbiguousDispatch() {}
|
|
|
|
type failingHTTPWriter struct {
|
|
header http.Header
|
|
status int
|
|
}
|
|
|
|
func (w *failingHTTPWriter) Header() http.Header {
|
|
return w.header
|
|
}
|
|
|
|
func (*failingHTTPWriter) Write([]byte) (int, error) {
|
|
return 0, errors.New("downstream write failed")
|
|
}
|
|
|
|
func (w *failingHTTPWriter) WriteHeader(statusCode int) {
|
|
w.status = statusCode
|
|
}
|
|
|
|
type trackedResponseBody struct {
|
|
io.Reader
|
|
closed atomic.Bool
|
|
}
|
|
|
|
func (b *trackedResponseBody) Close() error {
|
|
b.closed.Store(true)
|
|
return nil
|
|
}
|
|
|
|
type errorResponseBody struct {
|
|
payload []byte
|
|
read atomic.Bool
|
|
closed atomic.Bool
|
|
}
|
|
|
|
func (b *errorResponseBody) Read(p []byte) (int, error) {
|
|
if !b.read.CompareAndSwap(false, true) {
|
|
return 0, io.EOF
|
|
}
|
|
return copy(p, b.payload), io.ErrUnexpectedEOF
|
|
}
|
|
|
|
func (b *errorResponseBody) Close() error {
|
|
b.closed.Store(true)
|
|
return nil
|
|
}
|
|
|
|
func TestReadLimitedBodyPreservesPayloadOnReadError(t *testing.T) {
|
|
const upstreamError = `{"error":{"message":"access token expired"}}`
|
|
payload, errRead := readLimitedBody(&errorResponseBody{payload: []byte(upstreamError)})
|
|
if !errors.Is(errRead, io.ErrUnexpectedEOF) || string(payload) != upstreamError {
|
|
t.Fatalf("readLimitedBody() = %q, %v; want partial payload and unexpected EOF", payload, errRead)
|
|
}
|
|
}
|
|
|
|
type fakeMediaRelay struct {
|
|
clientOffer string
|
|
route mediaSessionRoute
|
|
upstreamOffer string
|
|
session *fakeMediaSession
|
|
err error
|
|
}
|
|
|
|
func (r *fakeMediaRelay) NewSession(_ context.Context, clientOffer string, route mediaSessionRoute) (mediaRelaySession, string, error) {
|
|
r.clientOffer = clientOffer
|
|
r.route = route
|
|
return r.session, r.upstreamOffer, r.err
|
|
}
|
|
|
|
type fakeMediaSession struct {
|
|
upstreamAnswer string
|
|
callIDAtAccept string
|
|
downstreamSDP string
|
|
closeHandler func(string)
|
|
callID string
|
|
closeReason string
|
|
closed atomic.Bool
|
|
err error
|
|
}
|
|
|
|
func (s *fakeMediaSession) AcceptUpstreamAnswer(_ context.Context, answer string) (string, error) {
|
|
s.upstreamAnswer = answer
|
|
s.callIDAtAccept = s.callID
|
|
return s.downstreamSDP, s.err
|
|
}
|
|
|
|
func (s *fakeMediaSession) SetCallID(callID string) {
|
|
s.callID = callID
|
|
}
|
|
|
|
func (s *fakeMediaSession) SetCloseHandler(handler func(string)) {
|
|
s.closeHandler = handler
|
|
}
|
|
|
|
func (s *fakeMediaSession) Close() error {
|
|
return s.CloseWithReason("closed")
|
|
}
|
|
|
|
func (s *fakeMediaSession) CloseWithReason(reason string) error {
|
|
s.closeReason = reason
|
|
s.closed.Store(true)
|
|
return nil
|
|
}
|
|
|
|
func registerCredential(t *testing.T, manager *auth.Manager, credential *auth.Auth) {
|
|
t.Helper()
|
|
if _, errRegister := manager.Register(context.Background(), credential); errRegister != nil {
|
|
t.Fatalf("register %s: %v", credential.ID, errRegister)
|
|
}
|
|
}
|
|
|
|
func multipartBody(boundary, sdp, session string) string {
|
|
body := "--" + boundary + "\r\n" +
|
|
"Content-Disposition: form-data; name=\"sdp\"\r\n" +
|
|
"Content-Type: application/sdp\r\n\r\n" +
|
|
sdp + "\r\n"
|
|
if session != "" {
|
|
body += "--" + boundary + "\r\n" +
|
|
"Content-Disposition: form-data; name=\"session\"\r\n" +
|
|
"Content-Type: application/json\r\n\r\n" +
|
|
session + "\r\n"
|
|
}
|
|
return body + "--" + boundary + "--\r\n"
|
|
}
|
|
|
|
func TestHandlerRewritesLiveCallAndSchedulesOAuth(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
manager := auth.NewManager(nil, &apiKeyFirstSelector{}, nil)
|
|
responseBody := &trackedResponseBody{Reader: strings.NewReader("v=0\r\na=ice-lite\r\n")}
|
|
executor := &captureExecutor{responseBody: responseBody}
|
|
manager.RegisterExecutor(executor)
|
|
registerCredential(t, manager, &auth.Auth{
|
|
ID: "codex-api-key",
|
|
Provider: "codex",
|
|
Status: auth.StatusActive,
|
|
Attributes: map[string]string{auth.AttributeAPIKey: "must-not-be-used"},
|
|
})
|
|
registerCredential(t, manager, &auth.Auth{
|
|
ID: "codex-oauth",
|
|
Provider: "codex",
|
|
Status: auth.StatusActive,
|
|
Metadata: map[string]any{
|
|
"access_token": "oauth-token",
|
|
"account_id": "account-123",
|
|
},
|
|
})
|
|
|
|
handler := NewHandler(manager, nil)
|
|
router := gin.New()
|
|
router.POST("/v1/live", handler.Handle)
|
|
|
|
const boundary = "codex-realtime-call-boundary"
|
|
body := multipartBody(boundary, "v=0\r\na=setup:actpass", `{"model":"gpt-live-1-codex"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/live", strings.NewReader(body))
|
|
req.Header.Set("Authorization", "Bearer downstream-api-key")
|
|
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
|
|
req.Header.Set("Originator", "Codex Desktop")
|
|
req.Header.Set("Thread-Id", "thread-123")
|
|
req.Header.Set("Session-Id", "session-123")
|
|
req.Header.Set("OpenAI-Alpha", "quicksilver=v2")
|
|
req.Header.Set("X-Oai-Attestation", "attestation-token")
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, req)
|
|
|
|
if recorder.Code != http.StatusCreated {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusCreated, recorder.Body.String())
|
|
}
|
|
if executor.request == nil || executor.selectedAuth == nil {
|
|
t.Fatal("Codex executor did not receive a live request")
|
|
}
|
|
if executor.selectedAuth.ID != "codex-oauth" {
|
|
t.Fatalf("selected auth = %q, want codex-oauth", executor.selectedAuth.ID)
|
|
}
|
|
if got := executor.request.URL.String(); got != upstreamCallURL {
|
|
t.Fatalf("upstream URL = %q, want %q", got, upstreamCallURL)
|
|
}
|
|
var upstreamPayload struct {
|
|
SDP string `json:"sdp"`
|
|
Session map[string]any `json:"session"`
|
|
}
|
|
if errUnmarshal := json.Unmarshal(executor.body, &upstreamPayload); errUnmarshal != nil {
|
|
t.Fatalf("unmarshal upstream body: %v; body=%s", errUnmarshal, executor.body)
|
|
}
|
|
if upstreamPayload.SDP != "v=0\r\na=setup:actpass" {
|
|
t.Fatalf("upstream sdp = %q", upstreamPayload.SDP)
|
|
}
|
|
if got := upstreamPayload.Session["model"]; got != "gpt-live-1-codex" {
|
|
t.Fatalf("upstream session model = %#v", got)
|
|
}
|
|
if got := executor.request.Header.Get("Content-Type"); got != "application/json" {
|
|
t.Fatalf("Content-Type = %q, want application/json", got)
|
|
}
|
|
if got := executor.request.Header.Get("Authorization"); got != "Bearer oauth-token" {
|
|
t.Fatalf("Authorization = %q, want OAuth token", got)
|
|
}
|
|
if got := executor.request.Header.Get("Chatgpt-Account-Id"); got != "account-123" {
|
|
t.Fatalf("Chatgpt-Account-Id = %q, want account-123", got)
|
|
}
|
|
for header, want := range map[string]string{
|
|
"OpenAI-Alpha": "quicksilver=v2",
|
|
"Originator": "Codex Desktop",
|
|
"Session-Id": "session-123",
|
|
"Thread-Id": "thread-123",
|
|
"X-Oai-Attestation": "attestation-token",
|
|
} {
|
|
if got := executor.request.Header.Get(header); got != want {
|
|
t.Errorf("%s = %q, want %q", header, got, want)
|
|
}
|
|
}
|
|
if got := recorder.Body.String(); got != "v=0\r\na=ice-lite\r\n" {
|
|
t.Fatalf("response body = %q", got)
|
|
}
|
|
if got := recorder.Header().Get("Location"); got != "/v1/live/call-123" {
|
|
t.Fatalf("Location = %q, want live call location", got)
|
|
}
|
|
for _, blocked := range []string{"Connection", "Set-Cookie", "X-Connection-Secret", "X-Live-Session"} {
|
|
if got := recorder.Header().Get(blocked); got != "" {
|
|
t.Errorf("blocked response header %s leaked as %q", blocked, got)
|
|
}
|
|
}
|
|
if !responseBody.closed.Load() {
|
|
t.Fatal("upstream response body was not closed")
|
|
}
|
|
stored, ok := handler.sessions.peek("call-123")
|
|
if !ok || stored.authID != "codex-oauth" || stored.model != "gpt-live-1-codex" {
|
|
t.Fatalf("stored live session = %#v, ok=%t", stored, ok)
|
|
}
|
|
}
|
|
|
|
func TestMediaCredentialNameUsesSafeIdentity(t *testing.T) {
|
|
for name, testCase := range map[string]struct {
|
|
selected *auth.Auth
|
|
index string
|
|
want string
|
|
}{
|
|
"label": {
|
|
selected: &auth.Auth{Label: "Voice credential", FileName: "/auths/codex-user.json", ID: "secret-id"},
|
|
index: "auth-index",
|
|
want: "Voice credential",
|
|
},
|
|
"file basename": {
|
|
selected: &auth.Auth{FileName: "/auths/codex-user.json", ID: "secret-id"},
|
|
index: "auth-index",
|
|
want: "codex-user.json",
|
|
},
|
|
"opaque index": {
|
|
selected: &auth.Auth{ID: "secret-id"},
|
|
index: "auth-index",
|
|
want: "auth-index",
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
if got := mediaCredentialName(testCase.selected, testCase.index); got != testCase.want {
|
|
t.Fatalf("mediaCredentialName() = %q, want %q", got, testCase.want)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestProxyURLForAuthPrefersCredentialOverride(t *testing.T) {
|
|
cfg := &config.Config{}
|
|
cfg.ProxyURL = "http://global.example:8080"
|
|
if got := proxyURLForAuth(cfg, &auth.Auth{ProxyURL: "socks5://credential.example:1080"}); got != "socks5://credential.example:1080" {
|
|
t.Fatalf("effective proxy URL = %q, want credential override", got)
|
|
}
|
|
if got := proxyURLForAuth(cfg, &auth.Auth{}); got != "http://global.example:8080" {
|
|
t.Fatalf("effective proxy URL = %q, want global fallback", got)
|
|
}
|
|
if got := proxyURLForAuth(cfg, &auth.Auth{ProxyURL: "direct"}); got != "direct" {
|
|
t.Fatalf("effective proxy URL = %q, want explicit direct override", got)
|
|
}
|
|
}
|
|
|
|
func TestHandlerRelaysWebRTCMediaSDP(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
manager := auth.NewManager(nil, nil, nil)
|
|
executor := &captureExecutor{
|
|
responseBody: &trackedResponseBody{Reader: strings.NewReader("v=0\r\no=upstream-answer\r\n")},
|
|
}
|
|
manager.RegisterExecutor(executor)
|
|
registerCredential(t, manager, &auth.Auth{
|
|
ID: "codex-oauth",
|
|
Provider: "codex",
|
|
Status: auth.StatusActive,
|
|
Label: "Voice credential",
|
|
ProxyURL: "socks5://credential-proxy.example:1080",
|
|
Metadata: map[string]any{"access_token": "oauth-token"},
|
|
})
|
|
mediaSession := &fakeMediaSession{downstreamSDP: "v=0\r\no=downstream-answer\r\n"}
|
|
mediaRelay := &fakeMediaRelay{
|
|
upstreamOffer: "v=0\r\no=gateway-offer\r\n",
|
|
session: mediaSession,
|
|
}
|
|
runtimeConfig := &config.Config{}
|
|
runtimeConfig.ProxyURL = "http://global-proxy.example:8080"
|
|
handler := NewHandler(manager, runtimeConfig)
|
|
handler.mediaRelay = mediaRelay
|
|
router := gin.New()
|
|
router.POST("/v1/live", handler.Handle)
|
|
|
|
const boundary = "media-relay-boundary"
|
|
body := multipartBody(boundary, "v=0\r\no=desktop-offer\r\n", `{"model":"gpt-live-1-codex"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/live", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, req)
|
|
|
|
if recorder.Code != http.StatusCreated {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusCreated, recorder.Body.String())
|
|
}
|
|
if mediaRelay.clientOffer != "v=0\r\no=desktop-offer\r\n" {
|
|
t.Fatalf("media client offer = %q", mediaRelay.clientOffer)
|
|
}
|
|
if mediaRelay.route.proxyURL != "socks5://credential-proxy.example:1080" {
|
|
t.Fatalf("media proxy URL = %q, want credential override", mediaRelay.route.proxyURL)
|
|
}
|
|
if mediaRelay.route.credential != "Voice credential" || mediaRelay.route.authIndex == "" {
|
|
t.Fatalf("media credential route = %#v", mediaRelay.route)
|
|
}
|
|
var upstreamPayload struct {
|
|
SDP string `json:"sdp"`
|
|
}
|
|
if errUnmarshal := json.Unmarshal(executor.body, &upstreamPayload); errUnmarshal != nil {
|
|
t.Fatalf("unmarshal upstream body: %v", errUnmarshal)
|
|
}
|
|
if upstreamPayload.SDP != mediaRelay.upstreamOffer {
|
|
t.Fatalf("upstream SDP = %q, want gateway offer", upstreamPayload.SDP)
|
|
}
|
|
if mediaSession.upstreamAnswer != "v=0\r\no=upstream-answer\r\n" {
|
|
t.Fatalf("accepted upstream answer = %q", mediaSession.upstreamAnswer)
|
|
}
|
|
if mediaSession.callID != "call-123" {
|
|
t.Fatalf("media call ID = %q, want call-123", mediaSession.callID)
|
|
}
|
|
if mediaSession.callIDAtAccept != "call-123" {
|
|
t.Fatalf("media call ID at answer acceptance = %q, want call-123", mediaSession.callIDAtAccept)
|
|
}
|
|
if got := recorder.Body.String(); got != mediaSession.downstreamSDP {
|
|
t.Fatalf("downstream SDP = %q, want %q", got, mediaSession.downstreamSDP)
|
|
}
|
|
if got := recorder.Header().Get("Content-Type"); got != "application/sdp" {
|
|
t.Fatalf("Content-Type = %q, want application/sdp", got)
|
|
}
|
|
if mediaSession.closed.Load() {
|
|
t.Fatal("retained media session was closed before session completion")
|
|
}
|
|
if mediaSession.closeHandler == nil {
|
|
t.Fatal("media session close handler was not installed")
|
|
}
|
|
mediaSession.closeHandler("test_closed")
|
|
if !mediaSession.closed.Load() {
|
|
t.Fatal("completed media session was not closed")
|
|
}
|
|
if _, ok := handler.sessions.peek("call-123"); ok {
|
|
t.Fatal("completed media session remained stored")
|
|
}
|
|
}
|
|
|
|
func TestHandlerClosesUnretainedMediaSession(t *testing.T) {
|
|
for name, testCase := range map[string]struct {
|
|
upstreamStatus int
|
|
answerError error
|
|
wantStatus int
|
|
}{
|
|
"upstream rejection": {
|
|
upstreamStatus: http.StatusUnauthorized,
|
|
wantStatus: http.StatusUnauthorized,
|
|
},
|
|
"invalid upstream answer": {
|
|
upstreamStatus: http.StatusCreated,
|
|
answerError: errors.New("invalid answer"),
|
|
wantStatus: http.StatusBadGateway,
|
|
},
|
|
} {
|
|
t.Run(name, func(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
manager := auth.NewManager(nil, nil, nil)
|
|
executor := &captureExecutor{
|
|
responseBody: &trackedResponseBody{Reader: strings.NewReader("v=0\r\no=upstream-answer\r\n")},
|
|
statusCode: testCase.upstreamStatus,
|
|
}
|
|
manager.RegisterExecutor(executor)
|
|
registerCredential(t, manager, &auth.Auth{
|
|
ID: "codex-oauth",
|
|
Provider: "codex",
|
|
Status: auth.StatusActive,
|
|
Metadata: map[string]any{"access_token": "oauth-token"},
|
|
})
|
|
mediaSession := &fakeMediaSession{
|
|
downstreamSDP: "v=0\r\no=downstream-answer\r\n",
|
|
err: testCase.answerError,
|
|
}
|
|
handler := NewHandler(manager, nil)
|
|
handler.mediaRelay = &fakeMediaRelay{
|
|
upstreamOffer: "v=0\r\no=gateway-offer\r\n",
|
|
session: mediaSession,
|
|
}
|
|
router := gin.New()
|
|
router.POST("/v1/live", handler.Handle)
|
|
|
|
const boundary = "media-error-boundary"
|
|
body := multipartBody(boundary, "v=0\r\no=desktop-offer\r\n", `{"model":"gpt-live-1-codex"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/live", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, req)
|
|
|
|
if recorder.Code != testCase.wantStatus {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, testCase.wantStatus, recorder.Body.String())
|
|
}
|
|
if !mediaSession.closed.Load() {
|
|
t.Fatal("failed request retained its media session")
|
|
}
|
|
if mediaSession.closeReason != "request_not_retained" {
|
|
t.Fatalf("media close reason = %q, want request_not_retained", mediaSession.closeReason)
|
|
}
|
|
if _, ok := handler.sessions.peek("call-123"); ok {
|
|
t.Fatal("failed request stored its media session")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandlerReleasesHomeSelectionWhenMediaSetupFails(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
manager := auth.NewManager(nil, nil, nil)
|
|
manager.SetConfig(&config.Config{Home: config.HomeConfig{Enabled: true}})
|
|
registry := executionregistry.New()
|
|
manager.PublishHomeDispatch(&homeDispatcher{}, registry, 1)
|
|
manager.RegisterExecutor(&captureExecutor{})
|
|
handler := NewHandler(manager, nil)
|
|
handler.mediaRelay = &fakeMediaRelay{err: errors.New("media setup failed")}
|
|
router := gin.New()
|
|
router.POST("/v1/live", handler.Handle)
|
|
|
|
const boundary = "home-media-error-boundary"
|
|
body := multipartBody(boundary, "v=0\r\no=desktop-offer\r\n", `{"model":"gpt-live-1-codex"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/live", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, req)
|
|
|
|
if recorder.Code != http.StatusBadGateway {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusBadGateway, recorder.Body.String())
|
|
}
|
|
if got := len(registry.FreezeInFlight(time.Now()).Executions); got != 0 {
|
|
t.Fatalf("active Home executions = %d, want 0", got)
|
|
}
|
|
if errDrain := registry.Drain(context.Background()); errDrain != nil {
|
|
t.Fatalf("Drain() error = %v", errDrain)
|
|
}
|
|
}
|
|
|
|
func TestHandlerClosesMediaWhenResponseWriteFails(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
manager := auth.NewManager(nil, nil, nil)
|
|
manager.RegisterExecutor(&captureExecutor{
|
|
responseBody: &trackedResponseBody{Reader: strings.NewReader("v=0\r\no=upstream-answer\r\n")},
|
|
})
|
|
registerCredential(t, manager, &auth.Auth{
|
|
ID: "codex-oauth",
|
|
Provider: "codex",
|
|
Status: auth.StatusActive,
|
|
Metadata: map[string]any{"access_token": "oauth-token"},
|
|
})
|
|
mediaSession := &fakeMediaSession{downstreamSDP: "v=0\r\no=downstream-answer\r\n"}
|
|
handler := NewHandler(manager, nil)
|
|
handler.mediaRelay = &fakeMediaRelay{
|
|
upstreamOffer: "v=0\r\no=gateway-offer\r\n",
|
|
session: mediaSession,
|
|
}
|
|
router := gin.New()
|
|
router.POST("/v1/live", handler.Handle)
|
|
|
|
const boundary = "response-write-error-boundary"
|
|
body := multipartBody(boundary, "v=0\r\no=desktop-offer\r\n", `{"model":"gpt-live-1-codex"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/live", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
|
|
writer := &failingHTTPWriter{header: make(http.Header)}
|
|
router.ServeHTTP(writer, req)
|
|
|
|
if writer.status != http.StatusCreated {
|
|
t.Fatalf("status = %d, want %d", writer.status, http.StatusCreated)
|
|
}
|
|
if !mediaSession.closed.Load() {
|
|
t.Fatal("response write failure retained its media session")
|
|
}
|
|
if mediaSession.closeReason != "response_write_failed" {
|
|
t.Fatalf("media close reason = %q, want response_write_failed", mediaSession.closeReason)
|
|
}
|
|
if _, ok := handler.sessions.peek("call-123"); ok {
|
|
t.Fatal("response write failure retained a stored session")
|
|
}
|
|
}
|
|
|
|
func TestHandlerForwardsUnauthorizedHomeResponseWithoutRefresh(t *testing.T) {
|
|
const upstreamError = `{"error":{"message":"access token expired"}}`
|
|
gin.SetMode(gin.TestMode)
|
|
manager := auth.NewManager(nil, nil, nil)
|
|
manager.SetConfig(&config.Config{Home: config.HomeConfig{Enabled: true}})
|
|
registry := executionregistry.New()
|
|
manager.PublishHomeDispatch(&homeDispatcher{}, registry, 1)
|
|
executor := &captureExecutor{
|
|
statuses: []int{http.StatusUnauthorized},
|
|
responseBody: &trackedResponseBody{Reader: strings.NewReader(upstreamError)},
|
|
}
|
|
manager.RegisterExecutor(executor)
|
|
handler := NewHandler(manager, nil)
|
|
router := gin.New()
|
|
router.POST("/v1/live", handler.Handle)
|
|
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/live", strings.NewReader(`{"model":"gpt-live-1-codex","sdp":"v=0"}`))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, req)
|
|
|
|
if recorder.Code != http.StatusUnauthorized {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusUnauthorized, recorder.Body.String())
|
|
}
|
|
if got := recorder.Body.String(); got != upstreamError {
|
|
t.Fatalf("body = %q, want original upstream error %q", got, upstreamError)
|
|
}
|
|
if executor.refreshCalls.Load() != 0 || executor.httpCalls.Load() != 1 {
|
|
t.Fatalf("refresh/http calls = %d/%d, want 0/1", executor.refreshCalls.Load(), executor.httpCalls.Load())
|
|
}
|
|
if got := executor.request.Header.Get("Authorization"); got != "Bearer home-live-token" {
|
|
t.Fatalf("Authorization = %q, want original Home token", got)
|
|
}
|
|
if errDrain := registry.Drain(context.Background()); errDrain != nil {
|
|
t.Fatalf("Drain() error = %v", errDrain)
|
|
}
|
|
}
|
|
|
|
func TestHandlerReportsUnauthorizedBeforeEarlyReturn(t *testing.T) {
|
|
const upstreamError = `{"error":{"message":"access token expired"}}`
|
|
tests := []struct {
|
|
name string
|
|
responseBody func() io.ReadCloser
|
|
beforeReturn func(*executionregistry.Registry)
|
|
wantStatus int
|
|
wantFailBody string
|
|
}{
|
|
{
|
|
name: "response bind failure",
|
|
responseBody: func() io.ReadCloser {
|
|
return &trackedResponseBody{Reader: strings.NewReader(upstreamError)}
|
|
},
|
|
beforeReturn: func(registry *executionregistry.Registry) {
|
|
_ = registry.Close()
|
|
},
|
|
wantStatus: http.StatusServiceUnavailable,
|
|
wantFailBody: "upstream unauthorized",
|
|
},
|
|
{
|
|
name: "response read failure",
|
|
responseBody: func() io.ReadCloser {
|
|
return &errorResponseBody{payload: []byte(upstreamError)}
|
|
},
|
|
wantStatus: http.StatusBadGateway,
|
|
wantFailBody: upstreamError,
|
|
},
|
|
}
|
|
|
|
for _, test := range tests {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
authID := "home-codex-live-" + strings.ReplaceAll(test.name, " ", "-")
|
|
runtimeConfig := &config.Config{
|
|
Home: config.HomeConfig{Enabled: true},
|
|
SDKConfig: config.SDKConfig{RequestLog: true},
|
|
}
|
|
manager := auth.NewManager(nil, nil, nil)
|
|
manager.SetConfig(runtimeConfig)
|
|
registry := executionregistry.New()
|
|
manager.PublishHomeDispatch(&homeDispatcher{authID: authID}, registry, 1)
|
|
executor := &captureExecutor{
|
|
statuses: []int{http.StatusUnauthorized},
|
|
responseBody: test.responseBody(),
|
|
}
|
|
if test.beforeReturn != nil {
|
|
executor.beforeReturn = func() { test.beforeReturn(registry) }
|
|
}
|
|
manager.RegisterExecutor(executor)
|
|
usageCapture := registerHomeUnauthorizedUsageCapture(t, t.Name(), authID)
|
|
handler := NewHandler(manager, runtimeConfig)
|
|
router := gin.New()
|
|
var apiResponse []byte
|
|
router.Use(func(c *gin.Context) {
|
|
c.Next()
|
|
if raw, exists := c.Get("API_RESPONSE"); exists {
|
|
apiResponse, _ = raw.([]byte)
|
|
}
|
|
})
|
|
router.POST("/v1/live", handler.Handle)
|
|
|
|
request := httptest.NewRequest(http.MethodPost, "/v1/live", strings.NewReader(`{"model":"gpt-live-1-codex","sdp":"v=0"}`))
|
|
request.Header.Set("Content-Type", "application/json")
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, request)
|
|
|
|
if recorder.Code != test.wantStatus {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, test.wantStatus, recorder.Body.String())
|
|
}
|
|
record := usageCapture.wait(t)
|
|
if record.Fail.StatusCode != http.StatusUnauthorized || record.Fail.Body != test.wantFailBody {
|
|
t.Fatalf("Home unauthorized failure = status %d body %q, want status 401 body %q", record.Fail.StatusCode, record.Fail.Body, test.wantFailBody)
|
|
}
|
|
if test.name == "response read failure" && (!strings.Contains(string(apiResponse), upstreamError) || !strings.Contains(string(apiResponse), io.ErrUnexpectedEOF.Error())) {
|
|
t.Fatalf("API_RESPONSE = %q, want upstream body and read error", apiResponse)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandlerUsesLiveModelForHomeDispatch(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
manager := auth.NewManager(nil, nil, nil)
|
|
manager.SetConfig(&config.Config{Home: config.HomeConfig{Enabled: true}})
|
|
dispatcher := &homeDispatcher{}
|
|
registry := executionregistry.New()
|
|
manager.PublishHomeDispatch(dispatcher, registry, 1)
|
|
responseBody := &trackedResponseBody{Reader: strings.NewReader("v=0\r\n")}
|
|
executor := &captureExecutor{responseBody: responseBody}
|
|
manager.RegisterExecutor(executor)
|
|
|
|
handler := NewHandler(manager, nil)
|
|
router := gin.New()
|
|
router.POST("/v1/live", handler.Handle)
|
|
|
|
const boundary = "home-live-boundary"
|
|
body := multipartBody(boundary, "v=0", `{"model":"future-live-model"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/live", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, req)
|
|
|
|
if recorder.Code != http.StatusCreated {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusCreated, recorder.Body.String())
|
|
}
|
|
if dispatcher.model != "future-live-model" {
|
|
t.Fatalf("Home dispatch model = %q, want future-live-model", dispatcher.model)
|
|
}
|
|
if executor.selectedAuth == nil || executor.selectedAuth.ID != "home-codex-live" {
|
|
t.Fatalf("selected Home auth = %#v", executor.selectedAuth)
|
|
}
|
|
if !responseBody.closed.Load() {
|
|
t.Fatal("Home upstream response body was not closed")
|
|
}
|
|
stored, ok := handler.sessions.peek("call-123")
|
|
if !ok || stored.homeSelection == nil || !stored.homeSelection.Retained() || !stored.homeSelection.Active() {
|
|
t.Fatalf("stored Home live session = %#v, ok=%t", stored, ok)
|
|
}
|
|
if errDrain := registry.Drain(context.Background()); errDrain != nil {
|
|
t.Fatalf("Drain() error = %v", errDrain)
|
|
}
|
|
if stored.homeSelection.Active() {
|
|
t.Fatal("Home live selection remained active after drain")
|
|
}
|
|
}
|
|
|
|
func TestHomeLiveSessionExpiryReleasesSelection(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
manager := auth.NewManager(nil, nil, nil)
|
|
manager.SetConfig(&config.Config{Home: config.HomeConfig{Enabled: true}})
|
|
registry := executionregistry.New()
|
|
manager.PublishHomeDispatch(&homeDispatcher{}, registry, 1)
|
|
manager.RegisterExecutor(&captureExecutor{
|
|
responseBody: &trackedResponseBody{Reader: strings.NewReader("v=0\r\n")},
|
|
})
|
|
|
|
handler := NewHandler(manager, nil)
|
|
handler.sessions.lifetime = 20 * time.Millisecond
|
|
router := gin.New()
|
|
router.POST("/v1/live", handler.Handle)
|
|
|
|
const boundary = "expiring-home-live-boundary"
|
|
body := multipartBody(boundary, "v=0", `{"model":"gpt-live-1-codex"}`)
|
|
req := httptest.NewRequest(http.MethodPost, "/v1/live", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "multipart/form-data; boundary="+boundary)
|
|
recorder := httptest.NewRecorder()
|
|
router.ServeHTTP(recorder, req)
|
|
if recorder.Code != http.StatusCreated {
|
|
t.Fatalf("status = %d, want %d; body=%s", recorder.Code, http.StatusCreated, recorder.Body.String())
|
|
}
|
|
stored, ok := handler.sessions.peek("call-123")
|
|
if !ok || stored.homeSelection == nil || !stored.homeSelection.Active() {
|
|
t.Fatalf("stored Home live session = %#v, ok=%t", stored, ok)
|
|
}
|
|
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
_, stillStored := handler.sessions.peek("call-123")
|
|
if !stillStored && !stored.homeSelection.Active() {
|
|
if errDrain := registry.Drain(context.Background()); errDrain != nil {
|
|
t.Fatalf("Drain() error = %v", errDrain)
|
|
}
|
|
return
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
t.Fatal("expired Home live session remained active")
|
|
}
|
|
|
|
func TestHandleSidebandPinsAuthAndRelaysBidirectionally(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
|
|
upstreamHeaders := make(chan http.Header, 1)
|
|
upstreamServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
upgrader := websocket.Upgrader{CheckOrigin: func(*http.Request) bool { return true }}
|
|
conn, errUpgrade := upgrader.Upgrade(writer, request, nil)
|
|
if errUpgrade != nil {
|
|
return
|
|
}
|
|
defer func() { _ = conn.Close() }()
|
|
upstreamHeaders <- request.Header.Clone()
|
|
messageType, payload, errRead := conn.ReadMessage()
|
|
if errRead != nil {
|
|
return
|
|
}
|
|
_ = conn.WriteMessage(messageType, append([]byte("echo:"), payload...))
|
|
}))
|
|
defer upstreamServer.Close()
|
|
|
|
manager := auth.NewManager(nil, nil, nil)
|
|
executor := &captureExecutor{}
|
|
manager.RegisterExecutor(executor)
|
|
registerCredential(t, manager, &auth.Auth{
|
|
ID: "other-oauth",
|
|
Provider: "codex",
|
|
Status: auth.StatusActive,
|
|
Metadata: map[string]any{"access_token": "other-token", "account_id": "other-account"},
|
|
})
|
|
registerCredential(t, manager, &auth.Auth{
|
|
ID: "pinned-oauth",
|
|
Provider: "codex",
|
|
Status: auth.StatusActive,
|
|
Metadata: map[string]any{"access_token": "pinned-token", "account_id": "pinned-account"},
|
|
})
|
|
|
|
handler := NewHandler(manager, nil)
|
|
handler.sidebandAPIBaseURL = "ws" + strings.TrimPrefix(upstreamServer.URL, "http") + "/v1"
|
|
handler.sessions.put("call-sideband", liveSession{authID: "pinned-oauth", model: defaultLiveModel})
|
|
router := gin.New()
|
|
router.GET("/v1/live/:call_id", handler.HandleSideband)
|
|
downstreamServer := httptest.NewServer(router)
|
|
defer downstreamServer.Close()
|
|
|
|
wsURL := "ws" + strings.TrimPrefix(downstreamServer.URL, "http") + "/v1/live/call-sideband"
|
|
headers := http.Header{
|
|
"OpenAI-Alpha": []string{"quicksilver=v2"},
|
|
"X-Oai-Attestation": []string{"attestation-token"},
|
|
}
|
|
client, response, errDial := websocket.DefaultDialer.Dial(wsURL, headers)
|
|
if errDial != nil {
|
|
if response != nil && response.Body != nil {
|
|
_ = response.Body.Close()
|
|
}
|
|
t.Fatalf("dial downstream sideband: %v", errDial)
|
|
}
|
|
if response != nil && response.Body != nil {
|
|
_ = response.Body.Close()
|
|
}
|
|
defer func() { _ = client.Close() }()
|
|
if errWrite := client.WriteMessage(websocket.TextMessage, []byte("ping")); errWrite != nil {
|
|
t.Fatalf("write sideband message: %v", errWrite)
|
|
}
|
|
_, payload, errRead := client.ReadMessage()
|
|
if errRead != nil {
|
|
t.Fatalf("read sideband message: %v", errRead)
|
|
}
|
|
if got := string(payload); got != "echo:ping" {
|
|
t.Fatalf("sideband payload = %q, want echo:ping", got)
|
|
}
|
|
|
|
select {
|
|
case captured := <-upstreamHeaders:
|
|
if got := captured.Get("Authorization"); got != "Bearer pinned-token" {
|
|
t.Fatalf("upstream Authorization = %q, want pinned OAuth token", got)
|
|
}
|
|
if got := captured.Get("Chatgpt-Account-Id"); got != "pinned-account" {
|
|
t.Fatalf("upstream Chatgpt-Account-Id = %q, want pinned-account", got)
|
|
}
|
|
if got := captured.Get("OpenAI-Alpha"); got != "quicksilver=v2" {
|
|
t.Fatalf("upstream OpenAI-Alpha = %q", got)
|
|
}
|
|
case <-time.After(time.Second):
|
|
t.Fatal("upstream sideband headers were not captured")
|
|
}
|
|
}
|
|
|
|
func TestHandleSidebandForwardsUnauthorizedHomeHandshakeWithoutRefresh(t *testing.T) {
|
|
gin.SetMode(gin.TestMode)
|
|
for _, tc := range []struct {
|
|
name string
|
|
upstreamBody string
|
|
}{
|
|
{name: "response body", upstreamBody: `{"error":{"message":"access token expired"}}`},
|
|
{name: "empty response body"},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
var upstreamCalls atomic.Int32
|
|
upstreamHeaders := make(chan http.Header, 1)
|
|
upstreamServer := httptest.NewServer(http.HandlerFunc(func(writer http.ResponseWriter, request *http.Request) {
|
|
upstreamCalls.Add(1)
|
|
upstreamHeaders <- request.Header.Clone()
|
|
writer.Header().Set("Content-Type", "application/json")
|
|
writer.WriteHeader(http.StatusUnauthorized)
|
|
_, _ = writer.Write([]byte(tc.upstreamBody))
|
|
}))
|
|
defer upstreamServer.Close()
|
|
|
|
manager := auth.NewManager(nil, nil, nil)
|
|
manager.SetConfig(&config.Config{Home: config.HomeConfig{Enabled: true}})
|
|
registry := executionregistry.New()
|
|
manager.PublishHomeDispatch(&homeDispatcher{}, registry, 1)
|
|
executor := &captureExecutor{}
|
|
manager.RegisterExecutor(executor)
|
|
selection, errSelect := manager.SelectHomeAuthByKind(context.Background(), "codex", defaultLiveModel, auth.AuthKindOAuth, coreexecutor.Options{})
|
|
if errSelect != nil {
|
|
t.Fatalf("SelectHomeAuthByKind() error = %v", errSelect)
|
|
}
|
|
selection.Retain()
|
|
defer selection.End("test_complete")
|
|
|
|
handler := NewHandler(manager, nil)
|
|
handler.sidebandAPIBaseURL = "ws" + strings.TrimPrefix(upstreamServer.URL, "http") + "/v1"
|
|
handler.sessions.put("call-home-refresh", liveSession{authID: "home-codex-live", model: defaultLiveModel, homeSelection: selection})
|
|
router := gin.New()
|
|
router.GET("/v1/live/:call_id", handler.HandleSideband)
|
|
downstreamServer := httptest.NewServer(router)
|
|
defer downstreamServer.Close()
|
|
|
|
wsURL := "ws" + strings.TrimPrefix(downstreamServer.URL, "http") + "/v1/live/call-home-refresh"
|
|
client, response, errDial := websocket.DefaultDialer.Dial(wsURL, nil)
|
|
if client != nil {
|
|
_ = client.Close()
|
|
}
|
|
if errDial == nil || response == nil {
|
|
t.Fatalf("dial downstream sideband = response %#v err %v, want rejected handshake", response, errDial)
|
|
}
|
|
defer func() { _ = response.Body.Close() }()
|
|
responseBody, errRead := io.ReadAll(response.Body)
|
|
if errRead != nil {
|
|
t.Fatalf("read downstream rejection: %v", errRead)
|
|
}
|
|
if response.StatusCode != http.StatusUnauthorized || string(responseBody) != tc.upstreamBody {
|
|
t.Fatalf("downstream rejection = status %d body %q, want original upstream 401 body %q", response.StatusCode, responseBody, tc.upstreamBody)
|
|
}
|
|
if executor.refreshCalls.Load() != 0 || upstreamCalls.Load() != 1 {
|
|
t.Fatalf("refresh/upstream calls = %d/%d, want 0/1", executor.refreshCalls.Load(), upstreamCalls.Load())
|
|
}
|
|
if got := (<-upstreamHeaders).Get("Authorization"); got != "Bearer home-live-token" {
|
|
t.Fatalf("upstream Authorization = %q, want original Home token", got)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestHandleSidebandDialErrorPreservesBodyReturnedWithReadError(t *testing.T) {
|
|
const upstreamError = `{"error":{"message":"access token expired"}}`
|
|
gin.SetMode(gin.TestMode)
|
|
recorder := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(recorder)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/v1/live/call-123", nil)
|
|
ctx := context.WithValue(c.Request.Context(), "gin", c)
|
|
response := &http.Response{
|
|
StatusCode: http.StatusUnauthorized,
|
|
Header: http.Header{"Content-Type": []string{"application/json"}},
|
|
Body: &errorResponseBody{payload: []byte(upstreamError)},
|
|
}
|
|
|
|
responseBody := handleSidebandDialError(c, ctx, &config.Config{SDKConfig: config.SDKConfig{RequestLog: true}}, response, errors.New("websocket: bad handshake"))
|
|
if recorder.Code != http.StatusUnauthorized || recorder.Body.String() != upstreamError || string(responseBody) != upstreamError {
|
|
t.Fatalf("forwarded response = status %d body %q returned %q, want original upstream 401", recorder.Code, recorder.Body.String(), responseBody)
|
|
}
|
|
rawTimeline, okTimeline := c.Get("API_WEBSOCKET_TIMELINE")
|
|
timeline, _ := rawTimeline.([]byte)
|
|
if !okTimeline || !strings.Contains(string(timeline), upstreamError) {
|
|
t.Fatalf("API_WEBSOCKET_TIMELINE = %q, want original upstream error", timeline)
|
|
}
|
|
}
|
|
|
|
func TestHandleSidebandDialErrorDoesNotForwardNonUnauthorizedBody(t *testing.T) {
|
|
const upstreamError = `<html>proxy-01.internal authentication failed</html>`
|
|
gin.SetMode(gin.TestMode)
|
|
recorder := httptest.NewRecorder()
|
|
c, _ := gin.CreateTestContext(recorder)
|
|
c.Request = httptest.NewRequest(http.MethodGet, "/v1/live/call-123", nil)
|
|
ctx := context.WithValue(c.Request.Context(), "gin", c)
|
|
response := &http.Response{
|
|
StatusCode: http.StatusBadGateway,
|
|
Header: http.Header{"Content-Type": []string{"text/html"}},
|
|
Body: io.NopCloser(strings.NewReader(upstreamError)),
|
|
}
|
|
|
|
responseBody := handleSidebandDialError(c, ctx, &config.Config{SDKConfig: config.SDKConfig{RequestLog: true}}, response, errors.New("websocket: bad handshake"))
|
|
if recorder.Code != http.StatusBadGateway || len(responseBody) != 0 {
|
|
t.Fatalf("response = status %d returned %q, want generic 502", recorder.Code, responseBody)
|
|
}
|
|
if strings.Contains(recorder.Body.String(), upstreamError) || !strings.Contains(recorder.Body.String(), "Codex live sideband upstream unavailable") {
|
|
t.Fatalf("downstream body = %q, want generic error without proxy detail", recorder.Body.String())
|
|
}
|
|
rawTimeline, okTimeline := c.Get("API_WEBSOCKET_TIMELINE")
|
|
timeline, _ := rawTimeline.([]byte)
|
|
if !okTimeline || !strings.Contains(string(timeline), upstreamError) {
|
|
t.Fatalf("API_WEBSOCKET_TIMELINE = %q, want original upstream error", timeline)
|
|
}
|
|
}
|
|
|
|
func TestPrepareCallRequestRewritesMultipart(t *testing.T) {
|
|
const boundary = "live-model-boundary"
|
|
body := multipartBody(boundary, "v=0-offer", `{"model":"future-live-model","instructions":"hi"}`)
|
|
|
|
encoded, contentType, model, errPrepare := prepareCallRequest([]byte(body), "multipart/form-data; boundary="+boundary)
|
|
if errPrepare != nil {
|
|
t.Fatalf("prepareCallRequest() error = %v", errPrepare)
|
|
}
|
|
if contentType != "application/json" {
|
|
t.Fatalf("content type = %q, want application/json", contentType)
|
|
}
|
|
if model != "future-live-model" {
|
|
t.Fatalf("model = %q, want future-live-model", model)
|
|
}
|
|
var payload struct {
|
|
SDP string `json:"sdp"`
|
|
Session map[string]any `json:"session"`
|
|
}
|
|
if errUnmarshal := json.Unmarshal(encoded, &payload); errUnmarshal != nil {
|
|
t.Fatalf("unmarshal encoded body: %v", errUnmarshal)
|
|
}
|
|
if payload.SDP != "v=0-offer" || payload.Session["instructions"] != "hi" {
|
|
t.Fatalf("encoded payload = %#v", payload)
|
|
}
|
|
}
|
|
|
|
func TestPrepareCallRequestPreservesRawSDPWhenRelayDisabled(t *testing.T) {
|
|
body := []byte("v=0\r\no=raw-offer\r\n")
|
|
prepared, contentType, model, errPrepare := prepareCallRequest(body, "application/sdp")
|
|
if errPrepare != nil {
|
|
t.Fatalf("prepareCallRequest() error = %v", errPrepare)
|
|
}
|
|
if string(prepared) != string(body) {
|
|
t.Fatalf("prepared SDP = %q, want original body", prepared)
|
|
}
|
|
if contentType != "application/sdp" {
|
|
t.Fatalf("content type = %q, want application/sdp", contentType)
|
|
}
|
|
if model != defaultLiveModel {
|
|
t.Fatalf("model = %q, want %q", model, defaultLiveModel)
|
|
}
|
|
}
|
|
|
|
func TestMediaRelayWrapsRawSDPForCodexBackend(t *testing.T) {
|
|
body := []byte("v=0\r\no=raw-offer\r\n")
|
|
clientOffer, errSDP := callRequestSDP(body, "application/sdp")
|
|
if errSDP != nil {
|
|
t.Fatalf("callRequestSDP() error = %v", errSDP)
|
|
}
|
|
if clientOffer != string(body) {
|
|
t.Fatalf("client offer = %q, want original body", clientOffer)
|
|
}
|
|
prepared, contentType, errReplace := replaceCallRequestSDP(body, "application/sdp", "v=0\r\no=gateway-offer\r\n")
|
|
if errReplace != nil {
|
|
t.Fatalf("replaceCallRequestSDP() error = %v", errReplace)
|
|
}
|
|
if contentType != "application/json" {
|
|
t.Fatalf("content type = %q, want application/json", contentType)
|
|
}
|
|
var payload struct {
|
|
SDP string `json:"sdp"`
|
|
}
|
|
if errUnmarshal := json.Unmarshal(prepared, &payload); errUnmarshal != nil {
|
|
t.Fatalf("unmarshal prepared request: %v", errUnmarshal)
|
|
}
|
|
if payload.SDP != "v=0\r\no=gateway-offer\r\n" {
|
|
t.Fatalf("upstream SDP = %q", payload.SDP)
|
|
}
|
|
}
|
|
|
|
func TestHandlerUpdatesMediaRelayConfig(t *testing.T) {
|
|
handler := NewHandler(nil, nil)
|
|
if relay, errRelay := handler.currentMediaRelay(); relay != nil || errRelay != nil {
|
|
t.Fatalf("initial media relay = %#v, error = %v", relay, errRelay)
|
|
}
|
|
enabled := &config.Config{Codex: config.CodexConfig{LiveMediaRelay: config.CodexLiveMediaRelayConfig{
|
|
Enabled: true,
|
|
MaxSessions: 1,
|
|
DisablePrivateRemoteIPs: false,
|
|
}}}
|
|
if errUpdate := handler.UpdateConfig(enabled); errUpdate != nil {
|
|
t.Fatalf("enable media relay: %v", errUpdate)
|
|
}
|
|
enabledRelay, errRelay := handler.currentMediaRelay()
|
|
if enabledRelay == nil || errRelay != nil {
|
|
t.Fatalf("enabled media relay = %#v, error = %v", enabledRelay, errRelay)
|
|
}
|
|
unchanged := *enabled
|
|
unchanged.Debug = true
|
|
unchanged.ProxyURL = "http://new-proxy.example"
|
|
if errUpdate := handler.UpdateConfig(&unchanged); errUpdate != nil {
|
|
t.Fatalf("apply unrelated config change: %v", errUpdate)
|
|
}
|
|
unchangedRelay, errRelay := handler.currentMediaRelay()
|
|
if unchangedRelay != enabledRelay || errRelay != nil {
|
|
t.Fatalf("unrelated config change rebuilt media relay: before=%#v after=%#v error=%v", enabledRelay, unchangedRelay, errRelay)
|
|
}
|
|
if current := handler.currentConfig(); current == nil || current.ProxyURL != "http://new-proxy.example" {
|
|
t.Fatalf("runtime config was not updated: %#v", current)
|
|
}
|
|
changed := *enabled
|
|
changed.Codex.LiveMediaRelay.MaxSessions = 2
|
|
if errUpdate := handler.UpdateConfig(&changed); errUpdate != nil {
|
|
t.Fatalf("reload media relay: %v", errUpdate)
|
|
}
|
|
changedRelay, errRelay := handler.currentMediaRelay()
|
|
if changedRelay == nil || changedRelay == enabledRelay || errRelay != nil {
|
|
t.Fatalf("changed media relay = %#v, previous=%#v error=%v", changedRelay, enabledRelay, errRelay)
|
|
}
|
|
if errUpdate := handler.UpdateConfig(&config.Config{}); errUpdate != nil {
|
|
t.Fatalf("disable media relay: %v", errUpdate)
|
|
}
|
|
if relay, errRelay := handler.currentMediaRelay(); relay != nil || errRelay != nil {
|
|
t.Fatalf("disabled media relay = %#v, error = %v", relay, errRelay)
|
|
}
|
|
}
|
|
|
|
func TestPrepareCallRequestRejectsInvalidMultipart(t *testing.T) {
|
|
const boundary = "invalid-live-boundary"
|
|
body := "--" + boundary + "\r\n" +
|
|
"Content-Disposition: form-data; name=\"session\"\r\n\r\n" +
|
|
`{"model":"gpt-live-1-codex"}` + "\r\n" +
|
|
"--" + boundary + "--\r\n"
|
|
|
|
if _, _, _, errPrepare := prepareCallRequest([]byte(body), "multipart/form-data; boundary="+boundary); errPrepare == nil {
|
|
t.Fatal("prepareCallRequest() accepted multipart body without sdp")
|
|
}
|
|
}
|
|
|
|
func TestHeadersForLoggingRedactsAttestation(t *testing.T) {
|
|
source := http.Header{
|
|
"Authorization": []string{"Bearer oauth-token"},
|
|
"X-Oai-Attestation": []string{"attestation-token"},
|
|
}
|
|
|
|
got := headersForLogging(source)
|
|
if value := got.Get("X-Oai-Attestation"); value != "[REDACTED]" {
|
|
t.Fatalf("logged X-Oai-Attestation = %q, want redacted", value)
|
|
}
|
|
if value := source.Get("X-Oai-Attestation"); value != "attestation-token" {
|
|
t.Fatalf("source X-Oai-Attestation changed to %q", value)
|
|
}
|
|
}
|
|
|
|
func TestSessionStoreClaimsAndExpiresSessions(t *testing.T) {
|
|
store := newSessionStore()
|
|
store.lifetime = 20 * time.Millisecond
|
|
store.put("call-claim", liveSession{authID: "auth-1", model: defaultLiveModel})
|
|
|
|
session, claim := store.claim("call-claim")
|
|
if claim != sessionClaimAcquired {
|
|
t.Fatalf("first claim = %v, want acquired", claim)
|
|
}
|
|
if _, duplicateClaim := store.claim("call-claim"); duplicateClaim != sessionClaimBusy {
|
|
t.Fatalf("duplicate claim = %v, want busy", duplicateClaim)
|
|
}
|
|
store.release(session)
|
|
if _, retryClaim := store.claim("call-claim"); retryClaim != sessionClaimAcquired {
|
|
t.Fatalf("retry claim = %v, want acquired", retryClaim)
|
|
}
|
|
store.release(session)
|
|
|
|
deadline := time.Now().Add(time.Second)
|
|
for time.Now().Before(deadline) {
|
|
if _, ok := store.peek("call-claim"); !ok {
|
|
return
|
|
}
|
|
time.Sleep(time.Millisecond)
|
|
}
|
|
t.Fatal("released live session did not expire")
|
|
}
|
|
|
|
func TestSessionStoreCloseAllReleasesMediaAndResources(t *testing.T) {
|
|
store := newSessionStore()
|
|
mediaSession := &fakeMediaSession{}
|
|
stored := store.put("call-close-all", liveSession{media: mediaSession})
|
|
var resourceClosed atomic.Bool
|
|
stored.resources.add(func() error {
|
|
resourceClosed.Store(true)
|
|
return nil
|
|
})
|
|
|
|
store.closeAll("test_shutdown")
|
|
|
|
if !mediaSession.closed.Load() {
|
|
t.Fatal("closeAll() did not close the media session")
|
|
}
|
|
if !resourceClosed.Load() {
|
|
t.Fatal("closeAll() did not close session resources")
|
|
}
|
|
if _, ok := store.peek("call-close-all"); ok {
|
|
t.Fatal("closeAll() retained a session")
|
|
}
|
|
}
|
|
|
|
func TestSidebandURLShapes(t *testing.T) {
|
|
if got := buildSidebandURL(defaultSidebandAPIBaseURL, sidebandFrameless, "rtc_1"); got != "wss://api.openai.com/v1/live/rtc_1" {
|
|
t.Fatalf("Frameless sideband URL = %q", got)
|
|
}
|
|
if got := buildSidebandURL(defaultSidebandAPIBaseURL, sidebandRealtimeCalls, "rtc_1"); got != "wss://api.openai.com/v1/realtime/calls/rtc_1" {
|
|
t.Fatalf("Realtime calls sideband URL = %q", got)
|
|
}
|
|
if got := buildSidebandURL(defaultSidebandAPIBaseURL, sidebandRealtimeQuery, "rtc_2"); got != "wss://api.openai.com/v1/realtime?intent=quicksilver&call_id=rtc_2" {
|
|
t.Fatalf("Realtime query sideband URL = %q", got)
|
|
}
|
|
for location, want := range map[string]string{
|
|
"/v1/live/rtc_1": "rtc_1",
|
|
"/v1/realtime/calls/rtc_2": "rtc_2",
|
|
"/v1/realtime?intent=quicksilver&call_id=rtc_3": "rtc_3",
|
|
} {
|
|
if got := callIDFromLocation(location); got != want {
|
|
t.Errorf("callIDFromLocation(%q) = %q, want %q", location, got, want)
|
|
}
|
|
}
|
|
}
|