openai_ws_mode1_tls_test.go 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476
  1. package service
  2. import (
  3. "context"
  4. "crypto/tls"
  5. "crypto/x509"
  6. "io"
  7. "log"
  8. "net"
  9. "net/http"
  10. "net/http/httptest"
  11. "strings"
  12. "sync/atomic"
  13. "testing"
  14. "time"
  15. "github.com/Wei-Shaw/sub2api/internal/config"
  16. "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
  17. coderws "github.com/coder/websocket"
  18. "github.com/gin-gonic/gin"
  19. "github.com/stretchr/testify/require"
  20. )
  21. type mode1WSCapturedHello struct {
  22. ciphers []uint16
  23. curves []tls.CurveID
  24. protocols []string
  25. }
  26. // This endpoint performs a real TLS handshake and HTTP 101 upgrade. It only
  27. // binds loopback and echoes the exact application frames received over WSS.
  28. func newMode1WSTestEndpoint(t *testing.T) (*httptest.Server, <-chan mode1WSCapturedHello, *atomic.Int32) {
  29. t.Helper()
  30. hellos := make(chan mode1WSCapturedHello, 32)
  31. upgrades := &atomic.Int32{}
  32. server := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  33. conn, err := coderws.Accept(w, r, nil)
  34. if err != nil {
  35. return
  36. }
  37. upgrades.Add(1)
  38. defer conn.CloseNow()
  39. for {
  40. kind, payload, err := conn.Read(r.Context())
  41. if err != nil {
  42. return
  43. }
  44. if err := conn.Write(r.Context(), kind, payload); err != nil {
  45. return
  46. }
  47. }
  48. }))
  49. server.Config.ErrorLog = log.New(io.Discard, "", 0)
  50. server.TLS = &tls.Config{GetConfigForClient: func(hello *tls.ClientHelloInfo) (*tls.Config, error) {
  51. hellos <- mode1WSCapturedHello{
  52. ciphers: append([]uint16(nil), hello.CipherSuites...),
  53. curves: append([]tls.CurveID(nil), hello.SupportedCurves...),
  54. protocols: append([]string(nil), hello.SupportedProtos...),
  55. }
  56. return nil, nil
  57. }}
  58. server.StartTLS()
  59. t.Cleanup(server.Close)
  60. return server, hellos, upgrades
  61. }
  62. func mode1WSTestDialer(server *httptest.Server) *coderOpenAIWSClientDialer {
  63. roots := x509.NewCertPool()
  64. roots.AddCert(server.Certificate())
  65. return &coderOpenAIWSClientDialer{tlsRootCAs: roots, proxyClients: make(map[string]*openAIWSProxyClientEntry)}
  66. }
  67. func TestMode1WSSActualHandshakeAndFramePreservation(t *testing.T) {
  68. server, hellos, upgrades := newMode1WSTestEndpoint(t)
  69. dialer := mode1WSTestDialer(server)
  70. profile := tlsfingerprint.BuiltinProfile("nodejs24")
  71. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  72. defer cancel()
  73. conn, _, _, err := dialer.Dial(withOpenAIWSTLSProfile(ctx, 41, profile), "wss"+strings.TrimPrefix(server.URL, "https"), nil, "")
  74. require.NoError(t, err)
  75. defer conn.Close()
  76. select {
  77. case hello := <-hellos:
  78. require.Equal(t, profile.CipherSuites, hello.ciphers, "real ClientHello must use the configured cipher list and order")
  79. require.Equal(t, []tls.CurveID{tls.X25519, tls.CurveP256, tls.CurveP384}, hello.curves)
  80. require.Equal(t, []string{"http/1.1"}, hello.protocols)
  81. case <-ctx.Done():
  82. t.Fatal("no ClientHello captured")
  83. }
  84. frameConn := conn.(*coderOpenAIWSClientConn)
  85. for _, payload := range []string{
  86. `{"type":"response.create","model":"gpt-5.1","reasoning":{"effort":"high"},"input":[{"role":"user","content":"one"}],"tools":[{"type":"function","name":"lookup","parameters":{"type":"object"}}]}`,
  87. `{"type":"response.create","previous_response_id":"response-local","input":[{"type":"function_call_output","call_id":"call-local","output":"{\"answer\":42}"}]}`,
  88. } {
  89. require.NoError(t, frameConn.WriteFrame(ctx, coderws.MessageText, []byte(payload)))
  90. kind, response, err := frameConn.ReadFrame(ctx)
  91. require.NoError(t, err)
  92. require.Equal(t, coderws.MessageText, kind)
  93. require.Equal(t, payload, string(response), "TLS transport must not mutate application frames")
  94. }
  95. require.EqualValues(t, 1, upgrades.Load())
  96. }
  97. func TestMode1WSSTransportCacheIsolationAndProfileRotation(t *testing.T) {
  98. server, _, _ := newMode1WSTestEndpoint(t)
  99. dialer := mode1WSTestDialer(server)
  100. profile := tlsfingerprint.BuiltinProfile("nodejs24")
  101. cfg := openAIWSTLSConfig{accountID: 41, profile: profile}
  102. first, err := dialer.fingerprintHTTPClient(cfg, server.URL, "")
  103. require.NoError(t, err)
  104. same, err := dialer.fingerprintHTTPClient(cfg, server.URL, "")
  105. require.NoError(t, err)
  106. require.Same(t, first, same)
  107. otherAccount, err := dialer.fingerprintHTTPClient(openAIWSTLSConfig{accountID: 42, profile: profile}, server.URL, "")
  108. require.NoError(t, err)
  109. require.NotSame(t, first, otherAccount, "accounts must never share their transport cache")
  110. otherTarget, err := dialer.fingerprintHTTPClient(cfg, server.URL+"/other", "")
  111. require.NoError(t, err)
  112. require.NotSame(t, first, otherTarget)
  113. otherProxy, err := dialer.fingerprintHTTPClient(cfg, server.URL, "http://127.0.0.1:1")
  114. require.NoError(t, err)
  115. require.NotSame(t, first, otherProxy)
  116. changed := profile.Clone()
  117. changed.CipherSuites[0], changed.CipherSuites[1] = changed.CipherSuites[1], changed.CipherSuites[0]
  118. rotated, err := dialer.fingerprintHTTPClient(openAIWSTLSConfig{accountID: 41, profile: changed}, server.URL, "")
  119. require.NoError(t, err)
  120. require.NotSame(t, first, rotated, "same profile name with different contents must use a new transport")
  121. }
  122. func TestMode1WSSFailsClosedOnInvalidTLSAndCertificate(t *testing.T) {
  123. server, _, upgrades := newMode1WSTestEndpoint(t)
  124. target := "wss" + strings.TrimPrefix(server.URL, "https")
  125. t.Run("untrusted certificate", func(t *testing.T) {
  126. dialer := newDefaultOpenAIWSClientDialer()
  127. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  128. defer cancel()
  129. conn, _, _, err := dialer.Dial(withOpenAIWSTLSProfile(ctx, 1, tlsfingerprint.BuiltinProfile("nodejs24")), target, nil, "")
  130. require.Error(t, err)
  131. require.Nil(t, conn)
  132. require.Contains(t, err.Error(), "certificate")
  133. })
  134. t.Run("invalid ALPN cannot fall back to ordinary TLS", func(t *testing.T) {
  135. dialer := mode1WSTestDialer(server)
  136. profile := tlsfingerprint.BuiltinProfile("nodejs24")
  137. profile.ALPNProtocols = []string{"h2"}
  138. conn, _, _, err := dialer.Dial(withOpenAIWSTLSProfile(context.Background(), 1, profile), target, nil, "")
  139. require.Error(t, err)
  140. require.Nil(t, conn)
  141. })
  142. t.Run("plaintext rejected", func(t *testing.T) {
  143. dialer := mode1WSTestDialer(server)
  144. conn, _, _, err := dialer.Dial(withOpenAIWSTLSProfile(context.Background(), 1, tlsfingerprint.BuiltinProfile("nodejs24")), "ws://127.0.0.1:1", nil, "")
  145. require.ErrorContains(t, err, "requires a valid wss target")
  146. require.Nil(t, conn)
  147. })
  148. require.Zero(t, upgrades.Load(), "no failure may result in an unprotected successful upgrade")
  149. }
  150. func TestMode1WSConnectionPoolTransportCompatibility(t *testing.T) {
  151. server, hellos, _ := newMode1WSTestEndpoint(t)
  152. cfg := &config.Config{}
  153. cfg.Gateway.TLSFingerprint.Enabled = true
  154. cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 1
  155. cfg.Gateway.OpenAIWS.MaxIdlePerAccount = 1
  156. pool := newOpenAIWSConnPool(cfg)
  157. defer pool.Close()
  158. pool.setClientDialerForTest(mode1WSTestDialer(server))
  159. profile := tlsfingerprint.BuiltinProfile("nodejs24")
  160. account := &Account{ID: 41, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Concurrency: 1}
  161. req := openAIWSAcquireRequest{Account: account, WSURL: "wss" + strings.TrimPrefix(server.URL, "https"), tlsProfile: profile}
  162. ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
  163. defer cancel()
  164. first, err := pool.acquire(ctx, cloneOpenAIWSAcquireRequest(req), 0)
  165. require.NoError(t, err)
  166. firstID := first.ConnID()
  167. first.Release()
  168. reused, err := pool.acquire(ctx, cloneOpenAIWSAcquireRequest(req), 0)
  169. require.NoError(t, err)
  170. require.Equal(t, firstID, reused.ConnID())
  171. require.True(t, reused.Reused())
  172. reused.Release()
  173. changed := profile.Clone()
  174. changed.CipherSuites[0], changed.CipherSuites[1] = changed.CipherSuites[1], changed.CipherSuites[0]
  175. req.tlsProfile = changed
  176. rotated, err := pool.acquire(ctx, cloneOpenAIWSAcquireRequest(req), 0)
  177. require.NoError(t, err)
  178. require.NotEqual(t, firstID, rotated.ConnID())
  179. require.False(t, rotated.Reused())
  180. rotated.Release()
  181. req.WSURL += "/other"
  182. otherTarget, err := pool.acquire(ctx, cloneOpenAIWSAcquireRequest(req), 0)
  183. require.NoError(t, err)
  184. require.NotEqual(t, rotated.ConnID(), otherTarget.ConnID())
  185. otherTarget.Release()
  186. otherAccount := *account
  187. otherAccount.ID = 42
  188. req.Account = &otherAccount
  189. other, err := pool.acquire(ctx, cloneOpenAIWSAcquireRequest(req), 0)
  190. require.NoError(t, err)
  191. require.NotEqual(t, otherTarget.ConnID(), other.ConnID())
  192. other.Release()
  193. for _, expected := range [][]uint16{profile.CipherSuites, changed.CipherSuites, changed.CipherSuites, changed.CipherSuites} {
  194. select {
  195. case hello := <-hellos:
  196. require.Equal(t, expected, hello.ciphers)
  197. case <-ctx.Done():
  198. t.Fatal("missing handshake after account/target/profile change")
  199. }
  200. }
  201. }
  202. func mode1WSAccount(id int64) *Account {
  203. return &Account{ID: id, Platform: PlatformOpenAI, Type: AccountTypeOAuth, Concurrency: 2, Extra: map[string]any{
  204. "anti_degrade": map[string]any{"enabled": true, "mode": "mode1", "policy_version": 2, "max_concurrency": 2},
  205. "codex_fingerprint_mode": "device", "codex_fingerprint_seed": testCodexFingerprintSeed,
  206. "enable_tls_fingerprint": true, "tls_fingerprint_builtin": "nodejs24",
  207. }}
  208. }
  209. func TestMode1WSAcquireUsesAccountTLSAndKeepsSessionsIsolated(t *testing.T) {
  210. server, hellos, upgrades := newMode1WSTestEndpoint(t)
  211. cfg := &config.Config{}
  212. cfg.Gateway.TLSFingerprint.Enabled = true
  213. cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 2
  214. cfg.Gateway.OpenAIWS.MaxIdlePerAccount = 2
  215. pool := newOpenAIWSConnPool(cfg)
  216. defer pool.Close()
  217. pool.setClientDialerForTest(mode1WSTestDialer(server))
  218. req := openAIWSAcquireRequest{
  219. Account: mode1WSAccount(43), WSURL: "wss" + strings.TrimPrefix(server.URL, "https"),
  220. Headers: http.Header{"Session_id": {"session-a"}, "Thread-Id": {"thread-a"}},
  221. }
  222. ctx, cancel := context.WithTimeout(context.Background(), 8*time.Second)
  223. defer cancel()
  224. first, err := pool.Acquire(ctx, req)
  225. require.NoError(t, err)
  226. requireMode1WSEcho(t, ctx, first)
  227. first.Release()
  228. req.Headers = http.Header{"Session_id": {"session-b"}, "Thread-Id": {"thread-b"}}
  229. second, err := pool.Acquire(ctx, req)
  230. require.NoError(t, err)
  231. requireMode1WSEcho(t, ctx, second)
  232. require.NotEqual(t, first.ConnID(), second.ConnID(), "different conversations must not reuse one upstream WSS session")
  233. second.Release()
  234. for i := 0; i < 2; i++ {
  235. select {
  236. case hello := <-hellos:
  237. require.Equal(t, tlsfingerprint.BuiltinProfile("nodejs24").CipherSuites, hello.ciphers)
  238. case <-ctx.Done():
  239. t.Fatal("account policy did not reach a real TLS dial")
  240. }
  241. }
  242. require.EqualValues(t, 2, upgrades.Load())
  243. // A globally disabled template must attempt standard TLS rather than fail
  244. // locally. The standard client correctly rejects this private test CA.
  245. cfg.Gateway.TLSFingerprint.Enabled = false
  246. lease, err := pool.Acquire(ctx, req)
  247. require.ErrorContains(t, err, "certificate")
  248. require.Nil(t, lease)
  249. cfg.Gateway.TLSFingerprint.Enabled = true
  250. req.Account.Extra["enable_tls_fingerprint"] = false
  251. lease, err = pool.Acquire(ctx, req)
  252. require.ErrorContains(t, err, "certificate")
  253. require.Nil(t, lease)
  254. require.EqualValues(t, 2, upgrades.Load())
  255. }
  256. // A successful dial can return as soon as Accept flushes HTTP 101, before the
  257. // server increments its upgrade counter. A real frame round trip establishes
  258. // that server-side barrier without sleeps or weakening the connection check.
  259. func requireMode1WSEcho(t *testing.T, ctx context.Context, lease *openAIWSConnLease) {
  260. t.Helper()
  261. require.NoError(t, lease.WriteJSONContext(ctx, map[string]any{"type": "test.echo", "probe": "ready"}))
  262. body, err := lease.ReadMessageContext(ctx)
  263. require.NoError(t, err)
  264. require.Contains(t, string(body), `"probe":"ready"`)
  265. }
  266. func TestMode1WSRuntimeConcurrencyCapCannotBeBypassed(t *testing.T) {
  267. for _, router := range []bool{false, true} {
  268. for _, dynamic := range []bool{false, true} {
  269. cfg := &config.Config{}
  270. cfg.Gateway.OpenAIWS.MaxConnsPerAccount = 100
  271. cfg.Gateway.OpenAIWS.ModeRouterV2Enabled = router
  272. cfg.Gateway.OpenAIWS.DynamicMaxConnsByAccountConcurrencyEnabled = dynamic
  273. cfg.Gateway.OpenAIWS.OAuthMaxConnsFactor = 4
  274. pool := newOpenAIWSConnPool(cfg)
  275. account := mode1WSAccount(44)
  276. account.Concurrency = 50
  277. require.Equal(t, 50, pool.effectiveMaxConnsByAccount(account))
  278. account.Concurrency = 0
  279. require.Equal(t, AntiDegradeConcurrencyCap, pool.effectiveMaxConnsByAccount(account))
  280. account.Concurrency = 1
  281. require.Equal(t, 1, pool.effectiveMaxConnsByAccount(account))
  282. pool.Close()
  283. }
  284. }
  285. }
  286. type mode1WSLocalTargetDialer struct {
  287. inner *coderOpenAIWSClientDialer
  288. target string
  289. observed chan openAIWSTLSConfig
  290. headers chan http.Header
  291. }
  292. func (d *mode1WSLocalTargetDialer) Dial(ctx context.Context, _ string, h http.Header, proxy string) (openAIWSClientConn, int, http.Header, error) {
  293. if cfg, ok := ctx.Value(openAIWSTLSContextKey{}).(openAIWSTLSConfig); ok {
  294. d.observed <- cfg
  295. }
  296. if d.headers != nil {
  297. d.headers <- h.Clone()
  298. }
  299. // Test-only retargeting: no production/upstream host is ever contacted.
  300. return d.inner.Dial(ctx, d.target, h, proxy)
  301. }
  302. func TestMode1WSPassthroughEntryReachesRealTLS(t *testing.T) {
  303. gin.SetMode(gin.TestMode)
  304. upstream, hellos, _ := newMode1WSTestEndpoint(t)
  305. dialer := &mode1WSLocalTargetDialer{
  306. inner: mode1WSTestDialer(upstream), target: "wss" + strings.TrimPrefix(upstream.URL, "https"),
  307. observed: make(chan openAIWSTLSConfig, 1), headers: make(chan http.Header, 1),
  308. }
  309. cfg := passthroughLifecycleConfig()
  310. cfg.Gateway.TLSFingerprint.Enabled = true
  311. cfg.Gateway.OpenAIWS.OAuthEnabled = true
  312. account := mode1WSAccount(45)
  313. account.Credentials = map[string]any{"chatgpt_account_id": "00000000-0000-4000-8000-000000000045"}
  314. account.Extra["openai_oauth_responses_websockets_v2_mode"] = OpenAIWSIngressModePassthrough
  315. svc := newPassthroughLifecycleService(cfg, newStagedPassthroughConn())
  316. svc.openaiWSPassthroughDialer = dialer
  317. ctx, cancel := context.WithTimeout(context.Background(), 6*time.Second)
  318. defer cancel()
  319. server, serverErr := startPassthroughLifecycleServer(t, ctx, svc, account)
  320. defer server.Close()
  321. client := dialPassthroughLifecycleClientWithPayload(t, server, `{"type":"response.create","model":"gpt-5.1","input":[{"role":"user","content":"local-only"}],"reasoning":{"effort":"high"}}`)
  322. defer client.CloseNow()
  323. select {
  324. case observed := <-dialer.observed:
  325. require.Equal(t, account.ID, observed.accountID)
  326. require.Equal(t, tlsfingerprint.BuiltinProfile("nodejs24").CacheKey(), observed.profile.CacheKey())
  327. case err := <-serverErr:
  328. t.Fatalf("passthrough failed before TLS: %v", err)
  329. case <-ctx.Done():
  330. t.Fatal("passthrough did not dial")
  331. }
  332. actualWSHeaders := <-dialer.headers
  333. httpContext, _ := gin.CreateTestContext(httptest.NewRecorder())
  334. httpContext.Request = httptest.NewRequest(http.MethodPost, "/v1/responses", nil)
  335. httpRequest, err := svc.buildUpstreamRequest(ctx, httpContext, account, []byte(`{"model":"gpt-5.4","input":"local-only"}`), "sk-test", false, "", false)
  336. require.NoError(t, err)
  337. require.NotEmpty(t, actualWSHeaders.Get("x-codex-installation-id"))
  338. require.Equal(t, httpRequest.Header.Get("x-codex-installation-id"), actualWSHeaders.Get("x-codex-installation-id"), "native WSS and HTTP must derive the same device from the persistent account seed")
  339. select {
  340. case hello := <-hellos:
  341. require.Equal(t, tlsfingerprint.BuiltinProfile("nodejs24").CipherSuites, hello.ciphers)
  342. case <-ctx.Done():
  343. t.Fatal("passthrough did not perform TLS")
  344. }
  345. _, payload, err := client.Read(ctx)
  346. require.NoError(t, err)
  347. require.Contains(t, string(payload), `"effort":"high"`)
  348. require.Contains(t, string(payload), `"local-only"`)
  349. require.NoError(t, client.Close(coderws.StatusNormalClosure, "done"))
  350. select {
  351. case <-serverErr:
  352. case <-ctx.Done():
  353. t.Fatal("passthrough did not close")
  354. }
  355. }
  356. func TestMode1WSIngressRejectsLossyPolicyBeforeDial(t *testing.T) {
  357. gin.SetMode(gin.TestMode)
  358. for _, mode := range []string{OpenAIWSIngressModePassthrough, OpenAIWSIngressModeCtxPool} {
  359. t.Run(mode, func(t *testing.T) {
  360. upstream, _, upgrades := newMode1WSTestEndpoint(t)
  361. dialer := &mode1WSLocalTargetDialer{
  362. inner: mode1WSTestDialer(upstream), target: "wss" + strings.TrimPrefix(upstream.URL, "https"),
  363. observed: make(chan openAIWSTLSConfig, 4),
  364. }
  365. cfg := passthroughLifecycleConfig()
  366. cfg.Gateway.TLSFingerprint.Enabled = true
  367. cfg.Gateway.OpenAIWS.OAuthEnabled = true
  368. account := mode1WSAccount(46)
  369. account.Credentials = map[string]any{"chatgpt_account_id": "00000000-0000-4000-8000-000000000046"}
  370. account.Extra["openai_oauth_responses_websockets_v2_mode"] = mode
  371. svc := newPassthroughLifecycleService(cfg, newStagedPassthroughConn())
  372. svc.openaiWSPassthroughDialer = dialer
  373. svc.getOpenAIWSConnPool().setClientDialerForTest(dialer)
  374. defer svc.openaiWSPool.Close()
  375. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  376. defer cancel()
  377. server, serverErr := startPassthroughLifecycleServerWithHooks(t, ctx, svc, account, func(*gin.Context) *OpenAIWSIngressHooks {
  378. return &OpenAIWSIngressHooks{MaxReasoningEffort: "low", MaxReasoningEffortOverLimit: "downgrade"}
  379. })
  380. defer server.Close()
  381. client := dialPassthroughLifecycleClientWithPayload(t, server, `{"type":"response.create","model":"gpt-5.4","input":[{"role":"user","content":"local-only"}],"reasoning":{"effort":"high"}}`)
  382. defer client.CloseNow()
  383. select {
  384. case err := <-serverErr:
  385. require.ErrorContains(t, err, "reasoning")
  386. case <-ctx.Done():
  387. t.Fatal("lossy policy was not rejected")
  388. }
  389. require.Zero(t, upgrades.Load())
  390. require.Empty(t, dialer.observed, "lossy request must not reach any dial path")
  391. })
  392. }
  393. }
  394. func TestMode1WSSOverLocalHTTPAndHTTPSProxy(t *testing.T) {
  395. for _, useTLS := range []bool{false, true} {
  396. name := "http"
  397. if useTLS {
  398. name = "https"
  399. }
  400. t.Run(name, func(t *testing.T) {
  401. upstream, hellos, _ := newMode1WSTestEndpoint(t)
  402. targetAddr := strings.TrimPrefix(upstream.URL, "https://")
  403. proxy := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  404. if r.Method != http.MethodConnect || r.Host != targetAddr {
  405. w.WriteHeader(http.StatusForbidden)
  406. return
  407. }
  408. peer, err := (&net.Dialer{}).DialContext(r.Context(), "tcp", targetAddr)
  409. if err != nil {
  410. w.WriteHeader(http.StatusBadGateway)
  411. return
  412. }
  413. client, rw, err := w.(http.Hijacker).Hijack()
  414. if err != nil {
  415. peer.Close()
  416. return
  417. }
  418. defer client.Close()
  419. defer peer.Close()
  420. if _, err := client.Write([]byte("HTTP/1.1 200 Connection Established\r\n\r\n")); err != nil {
  421. return
  422. }
  423. done := make(chan struct{})
  424. go func() { _, _ = io.Copy(peer, rw); _ = peer.Close(); close(done) }()
  425. _, _ = io.Copy(client, peer)
  426. _ = client.Close()
  427. <-done
  428. }))
  429. if useTLS {
  430. proxy.StartTLS()
  431. } else {
  432. proxy.Start()
  433. }
  434. defer proxy.Close()
  435. dialer := mode1WSTestDialer(upstream)
  436. if useTLS {
  437. dialer.tlsProxyRootCAs = x509.NewCertPool()
  438. dialer.tlsProxyRootCAs.AddCert(proxy.Certificate())
  439. }
  440. ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
  441. defer cancel()
  442. profile := tlsfingerprint.BuiltinProfile("nodejs24")
  443. conn, _, _, err := dialer.Dial(withOpenAIWSTLSProfile(ctx, 47, profile), "wss://"+targetAddr, nil, proxy.URL)
  444. require.NoError(t, err)
  445. defer conn.Close()
  446. require.NoError(t, conn.WriteJSON(ctx, map[string]any{"type": "response.create", "input": "proxy-local-only"}))
  447. payload, err := conn.ReadMessage(ctx)
  448. require.NoError(t, err)
  449. require.Contains(t, string(payload), "proxy-local-only")
  450. select {
  451. case hello := <-hellos:
  452. require.Equal(t, profile.CipherSuites, hello.ciphers, "the destination must see uTLS through the CONNECT tunnel")
  453. case <-ctx.Done():
  454. t.Fatal("no proxied ClientHello")
  455. }
  456. })
  457. }
  458. }