2026-02-12 20:47:05 +05:30
|
|
|
package proxy
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"crypto/tls"
|
|
|
|
|
"net/http"
|
|
|
|
|
"net/http/httptest"
|
2026-03-10 10:35:28 +05:30
|
|
|
"net/url"
|
2026-02-12 20:47:05 +05:30
|
|
|
"testing"
|
|
|
|
|
"time"
|
|
|
|
|
|
|
|
|
|
"github.com/stretchr/testify/assert"
|
2026-03-11 14:40:27 +05:30
|
|
|
"github.com/stretchr/testify/require"
|
2026-02-12 20:47:05 +05:30
|
|
|
)
|
|
|
|
|
|
|
|
|
|
func TestNewProxyServerSecuresUpstreamTLSConfig(t *testing.T) {
|
|
|
|
|
server, err := NewProxyServer(&ProxyConfig{
|
|
|
|
|
ListenAddr: "127.0.0.1:0",
|
|
|
|
|
EnableMITM: false,
|
|
|
|
|
ConnectTimeout: 30 * time.Second,
|
|
|
|
|
RequestTimeout: 5 * time.Minute,
|
|
|
|
|
})
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
internalProxy, ok := server.(*proxyServer)
|
|
|
|
|
assert.True(t, ok)
|
|
|
|
|
assert.NotNil(t, internalProxy.proxy.Tr)
|
|
|
|
|
assert.NotNil(t, internalProxy.proxy.Tr.TLSClientConfig)
|
|
|
|
|
assert.False(t, internalProxy.proxy.Tr.TLSClientConfig.InsecureSkipVerify, "upstream TLS verification must stay enabled")
|
|
|
|
|
assert.GreaterOrEqual(t, internalProxy.proxy.Tr.TLSClientConfig.MinVersion, uint16(tls.VersionTLS12), "minimum TLS version should be 1.2+")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-11 14:40:27 +05:30
|
|
|
func TestNewProxyServerUpstreamTransportEnablesHTTP2(t *testing.T) {
|
|
|
|
|
server, err := NewProxyServer(&ProxyConfig{
|
|
|
|
|
ListenAddr: "127.0.0.1:0",
|
|
|
|
|
EnableMITM: false,
|
|
|
|
|
ConnectTimeout: 30 * time.Second,
|
|
|
|
|
RequestTimeout: 5 * time.Minute,
|
|
|
|
|
})
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
internalProxy, ok := server.(*proxyServer)
|
|
|
|
|
assert.True(t, ok)
|
|
|
|
|
|
|
|
|
|
tr := internalProxy.proxy.Tr
|
|
|
|
|
assert.True(t, tr.ForceAttemptHTTP2, "HTTP/2 must be enabled to allow upstream connection multiplexing")
|
|
|
|
|
assert.Equal(t, 100, tr.MaxConnsPerHost, "MaxConnsPerHost should cap concurrent connections per upstream host")
|
|
|
|
|
assert.Equal(t, 50, tr.MaxIdleConnsPerHost, "MaxIdleConnsPerHost should be raised from default of 2 for connection reuse")
|
|
|
|
|
assert.Equal(t, 200, tr.MaxIdleConns, "MaxIdleConns should accommodate multiple upstream registries")
|
|
|
|
|
}
|
|
|
|
|
|
2026-03-10 10:35:28 +05:30
|
|
|
func TestNormalizeRequestURL(t *testing.T) {
|
|
|
|
|
tests := []struct {
|
|
|
|
|
name string
|
|
|
|
|
inputURL string
|
|
|
|
|
expectedURL string
|
|
|
|
|
}{
|
|
|
|
|
{
|
|
|
|
|
name: "malformed URL with http embedded in https",
|
|
|
|
|
inputURL: "https://registry.npmjs.org:443http://registry.npmjs.org:443/-/npm/v1/security/advisories/bulk",
|
|
|
|
|
expectedURL: "https://registry.npmjs.org:443/-/npm/v1/security/advisories/bulk",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "malformed URL with http embedded for github packages",
|
|
|
|
|
inputURL: "https://npm.pkg.github.com:443http://npm.pkg.github.com:443/download/some_package/0.2.7-rc2/abc123",
|
|
|
|
|
expectedURL: "https://npm.pkg.github.com:443/download/some_package/0.2.7-rc2/abc123",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "normal https URL is unchanged",
|
|
|
|
|
inputURL: "https://registry.npmjs.org/-/npm/v1/security/advisories/bulk",
|
|
|
|
|
expectedURL: "https://registry.npmjs.org/-/npm/v1/security/advisories/bulk",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "normal http URL is unchanged",
|
|
|
|
|
inputURL: "http://registry.npmjs.org/-/npm/v1/security/advisories/bulk",
|
|
|
|
|
expectedURL: "http://registry.npmjs.org/-/npm/v1/security/advisories/bulk",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "URL with scoped package encoding",
|
|
|
|
|
inputURL: "https://npm.pkg.github.com:443http://npm.pkg.github.com:443/@scope%2fpackage",
|
|
|
|
|
expectedURL: "https://npm.pkg.github.com:443/@scope/package",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "URL with http in query parameter is unchanged",
|
|
|
|
|
inputURL: "https://example.com/api?redirect=http://foo.com",
|
|
|
|
|
expectedURL: "https://example.com/api?redirect=http://foo.com",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "URL with https in query parameter is unchanged",
|
|
|
|
|
inputURL: "https://example.com/api?url=https://bar.com/path",
|
|
|
|
|
expectedURL: "https://example.com/api?url=https://bar.com/path",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "URL with http in path segment is unchanged",
|
|
|
|
|
inputURL: "https://example.com/proxy/http://target.com/resource",
|
|
|
|
|
expectedURL: "https://example.com/proxy/http://target.com/resource",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "URL with http in fragment is unchanged",
|
|
|
|
|
inputURL: "https://example.com/docs#http://ref.com",
|
|
|
|
|
expectedURL: "https://example.com/docs#http://ref.com",
|
|
|
|
|
},
|
|
|
|
|
{
|
|
|
|
|
name: "malformed URL with query string preserved",
|
|
|
|
|
inputURL: "https://registry.npmjs.org:443http://registry.npmjs.org:443/-/npm/v1/security/advisories/bulk?foo=bar",
|
|
|
|
|
expectedURL: "https://registry.npmjs.org:443/-/npm/v1/security/advisories/bulk?foo=bar",
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
for _, tt := range tests {
|
|
|
|
|
t.Run(tt.name, func(t *testing.T) {
|
|
|
|
|
parsed, err := url.Parse(tt.inputURL)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
req := &http.Request{URL: parsed}
|
|
|
|
|
normalizeRequestURL(req)
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, tt.expectedURL, req.URL.String())
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func TestNormalizeRequestURLNilSafety(t *testing.T) {
|
|
|
|
|
// Should not panic
|
|
|
|
|
normalizeRequestURL(nil)
|
|
|
|
|
normalizeRequestURL(&http.Request{})
|
|
|
|
|
normalizeRequestURL(&http.Request{URL: &url.URL{}})
|
|
|
|
|
}
|
|
|
|
|
|
2026-02-12 20:47:05 +05:30
|
|
|
func TestNewProxyServerRejectsUntrustedUpstreamCertByDefault(t *testing.T) {
|
|
|
|
|
target := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, _ *http.Request) {
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
}))
|
|
|
|
|
defer target.Close()
|
|
|
|
|
|
|
|
|
|
server, err := NewProxyServer(&ProxyConfig{
|
|
|
|
|
ListenAddr: "127.0.0.1:0",
|
|
|
|
|
EnableMITM: false,
|
|
|
|
|
ConnectTimeout: 30 * time.Second,
|
|
|
|
|
RequestTimeout: 5 * time.Minute,
|
|
|
|
|
})
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
internalProxy, ok := server.(*proxyServer)
|
|
|
|
|
assert.True(t, ok)
|
|
|
|
|
|
|
|
|
|
req, err := http.NewRequest(http.MethodGet, target.URL, nil)
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
resp, err := internalProxy.proxy.Tr.RoundTrip(req)
|
|
|
|
|
assert.Error(t, err, "untrusted upstream certificate should fail verification")
|
|
|
|
|
assert.Nil(t, resp)
|
|
|
|
|
}
|
2026-03-11 14:40:27 +05:30
|
|
|
|
|
|
|
|
func TestResponseProtoNormalisedToHTTP11(t *testing.T) {
|
|
|
|
|
// When the upstream transport negotiates HTTP/2, responses arrive with
|
|
|
|
|
// Proto "HTTP/2.0". goproxy writes MITM responses via resp.Write() which
|
|
|
|
|
// serialises the status line verbatim. An HTTP/1.1 client rejects the
|
|
|
|
|
// "HTTP/2.0 200 OK" status line and resets the connection.
|
|
|
|
|
// The OnResponse handler must normalise the proto back to HTTP/1.1.
|
|
|
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
|
|
|
w.WriteHeader(http.StatusOK)
|
|
|
|
|
}))
|
|
|
|
|
defer upstream.Close()
|
|
|
|
|
|
|
|
|
|
server, err := NewProxyServer(&ProxyConfig{
|
|
|
|
|
ListenAddr: "127.0.0.1:0",
|
|
|
|
|
EnableMITM: false,
|
|
|
|
|
ConnectTimeout: 5 * time.Second,
|
|
|
|
|
RequestTimeout: 5 * time.Second,
|
|
|
|
|
})
|
|
|
|
|
assert.NoError(t, err)
|
|
|
|
|
|
|
|
|
|
ps, ok := server.(*proxyServer)
|
|
|
|
|
assert.True(t, ok)
|
|
|
|
|
|
|
|
|
|
// Simulate an HTTP/2 response going through the OnResponse handler by
|
|
|
|
|
// sending a request through the proxy to the upstream test server.
|
|
|
|
|
assert.NoError(t, ps.Start())
|
|
|
|
|
defer func() {
|
|
|
|
|
_ = ps.Stop(t.Context())
|
|
|
|
|
}()
|
|
|
|
|
|
|
|
|
|
client := &http.Client{
|
|
|
|
|
Transport: &http.Transport{
|
|
|
|
|
Proxy: http.ProxyURL(&url.URL{
|
|
|
|
|
Scheme: "http",
|
|
|
|
|
Host: ps.Address(),
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
resp, err := client.Get(upstream.URL)
|
|
|
|
|
require.NoError(t, err)
|
|
|
|
|
defer func() { _ = resp.Body.Close() }()
|
|
|
|
|
|
|
|
|
|
assert.Equal(t, 1, resp.ProtoMajor, "response ProtoMajor should be 1 (HTTP/1.1)")
|
|
|
|
|
assert.Equal(t, 1, resp.ProtoMinor, "response ProtoMinor should be 1 (HTTP/1.1)")
|
|
|
|
|
}
|