http_upstream_tls_local_test.go 3.2 KB

1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677
  1. package repository
  2. import (
  3. "crypto/tls"
  4. "crypto/x509"
  5. "io"
  6. "net/http"
  7. "net/http/httptest"
  8. "reflect"
  9. "strings"
  10. "sync/atomic"
  11. "testing"
  12. "github.com/Wei-Shaw/sub2api/internal/config"
  13. "github.com/Wei-Shaw/sub2api/internal/pkg/tlsfingerprint"
  14. "github.com/Wei-Shaw/sub2api/internal/service"
  15. "github.com/stretchr/testify/require"
  16. )
  17. func TestMode1TLSPoolAlwaysSeparatesAccountsProxyAndProfile(t *testing.T) {
  18. cfg := &config.Config{}
  19. cfg.Gateway.ConnectionPoolIsolation = config.ConnectionPoolIsolationProxy
  20. svc := NewHTTPUpstream(cfg).(*httpUpstreamService)
  21. p := tlsfingerprint.BuiltinProfile("nodejs24")
  22. get := func(account int64, proxy string, p *tlsfingerprint.Profile) *upstreamClientEntry {
  23. entry, err := svc.getClientEntryWithTLS(proxy, account, 2, p, service.HTTPUpstreamProfileOpenAI, false, false)
  24. require.NoError(t, err)
  25. t.Cleanup(entry.client.CloseIdleConnections)
  26. return entry
  27. }
  28. one := get(1, "", p)
  29. require.Same(t, one, get(1, "", p.Clone()), "stable profile should reuse account pool")
  30. require.NotSame(t, one, get(2, "", p), "global proxy mode must not share fingerprint pools across accounts")
  31. require.NotSame(t, one, get(1, "http://127.0.0.1:1080", p), "proxy update must select a new pool")
  32. changed := p.Clone()
  33. changed.CipherSuites[0], changed.CipherSuites[1] = changed.CipherSuites[1], changed.CipherSuites[0]
  34. require.NotSame(t, one, get(1, "", changed), "same-name template edit must select a new pool")
  35. }
  36. func TestMode1DoWithTLSActuallyUsesClientHello(t *testing.T) {
  37. var handshakes atomic.Int64
  38. p := tlsfingerprint.BuiltinProfile("nodejs24")
  39. target := httptest.NewUnstartedServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
  40. _, _ = io.Copy(w, r.Body)
  41. }))
  42. target.TLS = &tls.Config{GetConfigForClient: func(h *tls.ClientHelloInfo) (*tls.Config, error) {
  43. handshakes.Add(1)
  44. if !reflect.DeepEqual(h.CipherSuites, p.CipherSuites) {
  45. t.Errorf("actual upstream ClientHello differs: got %v want %v", h.CipherSuites, p.CipherSuites)
  46. }
  47. return nil, nil
  48. }}
  49. target.StartTLS()
  50. t.Cleanup(target.Close)
  51. roots := x509.NewCertPool()
  52. roots.AddCert(target.Certificate())
  53. svc := NewHTTPUpstream(nil).(*httpUpstreamService)
  54. entry, err := svc.getClientEntryWithTLS("", 19, 2, p, service.HTTPUpstreamProfileDefault, false, false)
  55. require.NoError(t, err)
  56. // Inject only the local CA into the already selected production transport.
  57. // DoWithTLS still resolves its own account/profile cache entry and executes
  58. // the complete request path, with certificate verification enabled.
  59. transport := entry.client.Transport.(*http.Transport)
  60. require.NoError(t, tlsfingerprint.ConfigureTransport(transport, p, nil, tlsfingerprint.TransportOptions{RootCAs: roots}))
  61. t.Cleanup(transport.CloseIdleConnections)
  62. const payload = `{"reasoning":{"effort":"high"},"tools":[{"type":"function","name":"test"}],"input":"complete"}`
  63. req, err := http.NewRequest(http.MethodPost, target.URL, strings.NewReader(payload))
  64. require.NoError(t, err)
  65. resp, err := svc.DoWithTLS(req, "", 19, 2, p)
  66. require.NoError(t, err)
  67. body, err := io.ReadAll(resp.Body)
  68. require.NoError(t, err)
  69. require.NoError(t, resp.Body.Close())
  70. require.Equal(t, payload, string(body))
  71. require.Equal(t, int64(1), handshakes.Load())
  72. require.Zero(t, atomic.LoadInt64(&entry.inFlight))
  73. }