69b561f944
Adds 12 new HTTP handler test scenarios: HTTP connector (no-auth, auth, TLS upstream), CONNECT tunnel (no-sniffing, bypass-403), probeResist (host, web, file, knock), idleTimeout, and UDP relay over HTTP. Introduces RunGostContainerWithFiles helper for mounting extra files into test containers.
606 lines
22 KiB
Go
606 lines
22 KiB
Go
package e2e
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"io"
|
|
"os"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/stretchr/testify/suite"
|
|
"github.com/testcontainers/testcontainers-go"
|
|
)
|
|
|
|
type HTTPSuite struct {
|
|
suite.Suite
|
|
ctx context.Context
|
|
echoC testcontainers.Container
|
|
echoIP string
|
|
}
|
|
|
|
func (s *HTTPSuite) SetupSuite() {
|
|
s.ctx = context.Background()
|
|
|
|
s.T().Logf("start tcp echo container...")
|
|
echoC, err := RunEchoContainer(s.ctx, SharedNetworkName)
|
|
s.Require().NoError(err)
|
|
s.echoC = echoC
|
|
|
|
echoIP, err := echoC.ContainerIP(s.ctx)
|
|
s.Require().NoError(err)
|
|
s.echoIP = echoIP
|
|
}
|
|
|
|
// sendRaw sends a raw HTTP request via netcat and returns the response.
|
|
// Uses base64 to avoid shell quoting issues with CRLF bytes.
|
|
func (s *HTTPSuite) sendRaw(gostC testcontainers.Container, host, port, data string) string {
|
|
encoded := base64.StdEncoding.EncodeToString([]byte(data))
|
|
cmd := []string{"sh", "-c",
|
|
fmt.Sprintf("echo %s | base64 -d | nc -w 5 %s %s", encoded, host, port)}
|
|
_, out, _ := gostC.Exec(s.ctx, cmd)
|
|
b, _ := io.ReadAll(out)
|
|
return string(b)
|
|
}
|
|
|
|
func (s *HTTPSuite) TearDownSuite() {
|
|
if s.echoC != nil {
|
|
s.echoC.Terminate(s.ctx)
|
|
}
|
|
}
|
|
|
|
// TestHTTPProxy verifies basic HTTP forward proxy (no auth, no metadata).
|
|
// Covers: handler type http, listener type tcp, basic GET via proxy.
|
|
func (s *HTTPSuite) TestHTTPProxy() {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
// curl -x uses HTTP forward proxy (GET with absolute URL)
|
|
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
code, out, err := gostC.Exec(s.ctx, cmd)
|
|
s.Require().NoError(err)
|
|
|
|
body, err := io.ReadAll(out)
|
|
s.Require().NoError(err)
|
|
if code != 0 || !strings.Contains(string(body), "hello-gost") {
|
|
DumpLogs(s.T(), s.ctx, "http-proxy logs", gostC)
|
|
}
|
|
s.Require().Equal(0, code)
|
|
s.Require().Contains(string(body), "hello-gost")
|
|
}
|
|
|
|
// TestHTTPProxyAuth verifies proxy authentication and the authBasicRealm
|
|
// metadata parameter.
|
|
//
|
|
// Covers:
|
|
// - auther: basic auth on HTTP proxy
|
|
// - authBasicRealm: custom realm string in 407 Proxy-Authenticate header
|
|
func (s *HTTPSuite) TestHTTPProxyAuth() {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_auth.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
s.T().Run("no-auth-407", func(t *testing.T) {
|
|
cmd := []string{"curl", "-v", "-s", "-D", "-", "-o", "/dev/null",
|
|
"-x", "http://127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
_, out, _ := gostC.Exec(s.ctx, cmd)
|
|
body, _ := io.ReadAll(out)
|
|
output := string(body)
|
|
s.Assert().Contains(output, "407",
|
|
"no auth should return 407")
|
|
s.Assert().Contains(output, "gost-e2e-realm",
|
|
"authBasicRealm should appear in 407 Proxy-Authenticate")
|
|
})
|
|
|
|
s.T().Run("wrong-auth-407", func(t *testing.T) {
|
|
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
|
|
"-x", "http://wrong:pass@127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
_, out, _ := gostC.Exec(s.ctx, cmd)
|
|
body, _ := io.ReadAll(out)
|
|
s.Assert().Contains(string(body), "407",
|
|
"wrong password should return 407")
|
|
})
|
|
|
|
s.T().Run("with-auth-success", func(t *testing.T) {
|
|
cmd := []string{"curl", "-v", "-s", "-x", "http://user:pass@127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
code, out, err := gostC.Exec(s.ctx, cmd)
|
|
s.Require().NoError(err)
|
|
|
|
body, err := io.ReadAll(out)
|
|
s.Require().NoError(err)
|
|
if code != 0 || !strings.Contains(string(body), "hello-gost") {
|
|
DumpLogs(s.T(), s.ctx, "http-proxy-auth logs", gostC)
|
|
}
|
|
s.Require().Equal(0, code)
|
|
s.Require().Contains(string(body), "hello-gost")
|
|
})
|
|
}
|
|
|
|
// TestHTTPProxyMetadata verifies:
|
|
// - probeResist: code:404 — unauthorised clients see 404 instead of 407
|
|
// - keepalive: config parsing (applied at Init time)
|
|
// - compression: config parsing (applied at Init time)
|
|
func (s *HTTPSuite) TestHTTPProxyMetadata() {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_metadata.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
s.T().Run("probe-resist-404", func(t *testing.T) {
|
|
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
|
|
"-x", "http://127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
_, out, _ := gostC.Exec(s.ctx, cmd)
|
|
body, _ := io.ReadAll(out)
|
|
output := string(body)
|
|
s.Assert().NotContains(output, "407",
|
|
"probeResist should hide 407 status")
|
|
s.Assert().Contains(output, "404",
|
|
"probeResist should return configured decoy code")
|
|
})
|
|
|
|
s.T().Run("with-auth-success", func(t *testing.T) {
|
|
cmd := []string{"curl", "-v", "-s", "-x", "http://user:pass@127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
code, out, err := gostC.Exec(s.ctx, cmd)
|
|
s.Require().NoError(err)
|
|
|
|
body, err := io.ReadAll(out)
|
|
s.Require().NoError(err)
|
|
if code != 0 || !strings.Contains(string(body), "hello-gost") {
|
|
DumpLogs(s.T(), s.ctx, "http-metadata logs", gostC)
|
|
}
|
|
s.Require().Equal(0, code)
|
|
s.Require().Contains(string(body), "hello-gost")
|
|
})
|
|
}
|
|
|
|
// TestHTTPProxyHeaders verifies:
|
|
// - header: custom response headers (X-Proxy-Info, X-Custom) on
|
|
// proxy-originated error responses. The header metadata is set on
|
|
// the skeleton response used for error paths after authentication.
|
|
// - proxyAgent: custom Proxy-Agent header in proxy responses.
|
|
//
|
|
// Uses a bypass matcher (0.0.0.0/0) that blocks all traffic after auth
|
|
// succeeds → requests hit 403 Forbidden path which carries custom headers.
|
|
func (s *HTTPSuite) TestHTTPProxyHeaders() {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_headers.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
// Auth succeeds, but target is bypassed → 403 Forbidden
|
|
// The 403 response is built from the skeleton resp which
|
|
// carries h.md.header (custom headers) and h.md.proxyAgent.
|
|
s.T().Log("bypass: expect 403 with custom headers")
|
|
cmd := []string{"curl", "-v", "-s", "-D", "-", "-o", "/dev/null",
|
|
"-x", "http://user:pass@127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
_, out, _ := gostC.Exec(s.ctx, cmd)
|
|
headers, _ := io.ReadAll(out)
|
|
output := string(headers)
|
|
|
|
// Bypass → 403 Forbidden
|
|
s.Assert().Contains(output, "403",
|
|
"bypass should return 403")
|
|
// header: custom X-Proxy-Info header on error response
|
|
s.Assert().Contains(output, "X-Proxy-Info: gost-e2e",
|
|
"custom header should appear in 403 response")
|
|
// header: custom X-Custom header on error response
|
|
s.Assert().Contains(output, "X-Custom: test-value",
|
|
"custom header should appear in 403 response")
|
|
// proxyAgent: custom Proxy-Agent header on error response
|
|
s.Assert().Contains(output, "Proxy-Agent: gost-e2e/1.0",
|
|
"proxyAgent should appear in 403 response")
|
|
}
|
|
|
|
// TestHTTPConnect verifies HTTP CONNECT tunnel:
|
|
// - sniffing-enabled: sniffing:true + sniffing.timeout:2s
|
|
// - no-sniffing: plain CONNECT (default, no metadata)
|
|
// - bypass-403: CONNECT blocked by (0.0.0.0/0) bypass
|
|
//
|
|
// Uses curl --proxytunnel (-p) to force CONNECT method instead of GET.
|
|
func (s *HTTPSuite) TestHTTPConnect() {
|
|
s.T().Run("sniffing-enabled", func(t *testing.T) {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_connect.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
cmd := []string{"curl", "-v", "-s", "-p", "-x", "http://127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
code, out, err := gostC.Exec(s.ctx, cmd)
|
|
s.Require().NoError(err)
|
|
|
|
body, err := io.ReadAll(out)
|
|
s.Require().NoError(err)
|
|
if code != 0 || !strings.Contains(string(body), "hello-gost") {
|
|
DumpLogs(s.T(), s.ctx, "http-connect sniffing logs", gostC)
|
|
}
|
|
s.Require().Equal(0, code)
|
|
s.Require().Contains(string(body), "hello-gost")
|
|
})
|
|
|
|
s.T().Run("no-sniffing", func(t *testing.T) {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
cmd := []string{"curl", "-v", "-s", "-p", "-x", "http://127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
code, out, err := gostC.Exec(s.ctx, cmd)
|
|
s.Require().NoError(err)
|
|
|
|
body, err := io.ReadAll(out)
|
|
s.Require().NoError(err)
|
|
if code != 0 || !strings.Contains(string(body), "hello-gost") {
|
|
DumpLogs(s.T(), s.ctx, "http-connect no-sniffing logs", gostC)
|
|
}
|
|
s.Require().Equal(0, code)
|
|
s.Require().Contains(string(body), "hello-gost")
|
|
})
|
|
|
|
s.T().Run("bypass-403", func(t *testing.T) {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_connect_bypass.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
cmd := []string{"curl", "-v", "-s", "-D", "-", "-o", "/dev/null",
|
|
"-p", "-x", "http://user:pass@127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
_, out, _ := gostC.Exec(s.ctx, cmd)
|
|
body, _ := io.ReadAll(out)
|
|
output := string(body)
|
|
s.Assert().Contains(output, "403",
|
|
"CONNECT bypass should return 403")
|
|
})
|
|
}
|
|
|
|
// TestHTTPProxyDirectRequest verifies that a non-proxy-form HTTP request
|
|
// (raw GET / sent to the proxy port) is rejected with 400 Bad Request.
|
|
// A raw request with empty Host header has no valid scheme to infer,
|
|
// so the handler returns 400.
|
|
func (s *HTTPSuite) TestHTTPProxyDirectRequest() {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
|
|
"GET / HTTP/1.0\r\nHost: bad!\r\n\r\n")
|
|
s.Assert().Contains(resp, "400",
|
|
"non-proxy GET should return 400")
|
|
}
|
|
|
|
// TestHTTPProxyPOST verifies that POST method forwarding works through
|
|
// the HTTP forward proxy. The echo server only handles GET, so the proxy
|
|
// should forward the POST request and return whatever the upstream sends
|
|
// back (501 Not Implemented from the echo server — not a proxy error).
|
|
// 501 is the real upstream response, proving the proxy forwarded it.
|
|
func (s *HTTPSuite) TestHTTPProxyPOST() {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
cmd := []string{"curl", "-v", "-s", "-o", "/dev/null", "-w", "%{http_code}",
|
|
"-X", "POST", "-x", "http://127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678/", s.echoIP)}
|
|
_, out, _ := gostC.Exec(s.ctx, cmd)
|
|
body, _ := io.ReadAll(out)
|
|
output := string(body)
|
|
// The echo server's BaseHTTPRequestHandler defaults to 501 for POST.
|
|
// If we get 501 through the proxy, it means the method was forwarded.
|
|
s.Assert().Contains(output, "501",
|
|
"POST through proxy should be forwarded, echo server returns 501")
|
|
}
|
|
|
|
// TestHTTPConnectUnreachable verifies that CONNECT to an unreachable target
|
|
// returns 503 Service Unavailable.
|
|
func (s *HTTPSuite) TestHTTPConnectUnreachable() {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_connect.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
// Send a raw CONNECT request to a port where nothing is listening.
|
|
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
|
|
fmt.Sprintf("CONNECT %s:1 HTTP/1.0\r\nHost: %s:1\r\n\r\n", s.echoIP, s.echoIP))
|
|
s.Assert().Contains(resp, "503",
|
|
"CONNECT to unreachable target should return 503")
|
|
}
|
|
|
|
// TestHTTPConnector verifies HTTP connector type: a gost client uses an HTTP
|
|
// connector to forward through an upstream HTTP proxy (no auth), reaching the
|
|
// target via the proxy chain. This covers the server+client two-container
|
|
// pattern where the client connects to the server via connector type: http.
|
|
func (s *HTTPSuite) TestHTTPConnector() {
|
|
// Start upstream HTTP proxy server (no auth).
|
|
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
|
|
"testdata/http/server.yaml", []string{"http-server"}, []string{"8080/tcp"})
|
|
s.Require().NoError(err)
|
|
defer serverC.Terminate(s.ctx)
|
|
|
|
// Render client config that chains through the upstream server.
|
|
rendered, err := RenderConfig("testdata/http/client_connector.yaml",
|
|
ConfigData{ServerAddr: "http-server:8080"})
|
|
s.Require().NoError(err)
|
|
defer os.Remove(rendered)
|
|
|
|
// Start client with the rendered config.
|
|
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
rendered, "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer clientC.Terminate(s.ctx)
|
|
|
|
// Request through client proxy → HTTP connector → upstream HTTP server → target.
|
|
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
code, out, err := clientC.Exec(s.ctx, cmd)
|
|
s.Require().NoError(err)
|
|
|
|
body, err := io.ReadAll(out)
|
|
s.Require().NoError(err)
|
|
if code != 0 || !strings.Contains(string(body), "hello-gost") {
|
|
DumpLogs(s.T(), s.ctx, "http-connector client logs", clientC)
|
|
DumpLogs(s.T(), s.ctx, "http-connector server logs", serverC)
|
|
}
|
|
s.Require().Equal(0, code)
|
|
s.Require().Contains(string(body), "hello-gost")
|
|
}
|
|
|
|
// TestHTTPConnectorAuth verifies HTTP connector with authentication:
|
|
// no-auth → 407, correct auth → success.
|
|
func (s *HTTPSuite) TestHTTPConnectorAuth() {
|
|
// Start upstream HTTP proxy with auth and network alias.
|
|
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_auth.yaml", []string{"http-server"}, []string{"8080/tcp"})
|
|
s.Require().NoError(err)
|
|
defer serverC.Terminate(s.ctx)
|
|
|
|
s.T().Run("no-auth-407", func(t *testing.T) {
|
|
// Send raw request directly to server container (no auth) → 407.
|
|
resp := s.sendRaw(serverC, "127.0.0.1", "8080",
|
|
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
|
|
s.Assert().Contains(resp, "407",
|
|
"no auth via upstream should return 407")
|
|
})
|
|
|
|
s.T().Run("correct-auth-success", func(t *testing.T) {
|
|
rendered, err := RenderConfig("testdata/http/client_connector_auth.yaml",
|
|
ConfigData{ServerAddr: "http-server:8080"})
|
|
s.Require().NoError(err)
|
|
defer os.Remove(rendered)
|
|
|
|
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
rendered, "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer clientC.Terminate(s.ctx)
|
|
|
|
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
code, out, err := clientC.Exec(s.ctx, cmd)
|
|
s.Require().NoError(err)
|
|
|
|
body, err := io.ReadAll(out)
|
|
s.Require().NoError(err)
|
|
if code != 0 || !strings.Contains(string(body), "hello-gost") {
|
|
DumpLogs(s.T(), s.ctx, "http-connector-auth client logs", clientC)
|
|
DumpLogs(s.T(), s.ctx, "http-connector-auth server logs", serverC)
|
|
}
|
|
s.Require().Equal(0, code)
|
|
s.Require().Contains(string(body), "hello-gost")
|
|
})
|
|
}
|
|
|
|
// TestHTTPProbeResistHost verifies probeResist host type.
|
|
// When auth fails, the handler pipes the raw request to
|
|
// the decoy host (tcp-echo:5678). The TCP echo server echoes
|
|
// back the request bytes rather than returning a 407.
|
|
func (s *HTTPSuite) TestHTTPProbeResistHost() {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_proberesist_host.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
// Without auth, probeResist host pipes the raw request to tcp-echo:5678.
|
|
// The HTTP echo server returns 200 OK with "hello-gost", proving the
|
|
// request reached the decoy host rather than getting a 407 response.
|
|
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
|
|
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
|
|
s.Assert().NotContains(resp, "407",
|
|
"probeResist host should hide 407 response")
|
|
s.Assert().Contains(resp, "hello-gost",
|
|
"probeResist host should pipe request to decoy host")
|
|
}
|
|
|
|
// TestHTTPProbeResistKnock verifies probeResist knock mechanism.
|
|
// Clients that connect with a recognized knock Host header
|
|
// get a normal 407 auth challenge instead of the decoy response.
|
|
//
|
|
// Uses server_proberesist_knock.yaml:
|
|
// - probeResist: code:404 (decoy 404 for unknown hosts)
|
|
// - knock: secret.example.com (known hosts get normal 407)
|
|
func (s *HTTPSuite) TestHTTPProbeResistKnock() {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_proberesist_knock.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
s.T().Run("unknown-host-decoy", func(t *testing.T) {
|
|
// URL hostname doesn't match knock list → probeResist fires → 404
|
|
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
|
|
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
|
|
s.Assert().Contains(resp, "404",
|
|
"unknown host should get decoy 404")
|
|
s.Assert().NotContains(resp, "407",
|
|
"unknown host should NOT get 407")
|
|
})
|
|
|
|
s.T().Run("knock-host-407", func(t *testing.T) {
|
|
// URL hostname matches knock list → normal 407 auth challenge.
|
|
// Note: knock checks req.URL.Hostname(), not the Host header.
|
|
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
|
|
"GET http://secret.example.com:5678/ HTTP/1.0\r\nHost: secret.example.com\r\n\r\n")
|
|
s.Assert().Contains(resp, "407",
|
|
"knock host should get normal 407")
|
|
})
|
|
}
|
|
|
|
// TestHTTPConnectorTLS verifies HTTP connector using TLS dialer
|
|
// to upstream HTTPS proxy (listener type: tls).
|
|
// The TLS listener auto-generates a self-signed cert;
|
|
// the TLS dialer defaults to InsecureSkipVerify=true, so
|
|
// handshake succeeds without explicit cert configuration.
|
|
func (s *HTTPSuite) TestHTTPConnectorTLS() {
|
|
serverC, err := RunGostContainerWithOptions(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_tls.yaml", []string{"tls-server"}, []string{"8443/tcp"})
|
|
s.Require().NoError(err)
|
|
defer serverC.Terminate(s.ctx)
|
|
|
|
rendered, err := RenderConfig("testdata/http/client_connector_tls.yaml",
|
|
ConfigData{ServerAddr: "tls-server:8443"})
|
|
s.Require().NoError(err)
|
|
defer os.Remove(rendered)
|
|
|
|
clientC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
rendered, "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer clientC.Terminate(s.ctx)
|
|
|
|
cmd := []string{"curl", "-v", "-s", "-x", "http://127.0.0.1:8080",
|
|
fmt.Sprintf("http://%s:5678", s.echoIP)}
|
|
code, out, err := clientC.Exec(s.ctx, cmd)
|
|
s.Require().NoError(err)
|
|
|
|
body, err := io.ReadAll(out)
|
|
s.Require().NoError(err)
|
|
if code != 0 || !strings.Contains(string(body), "hello-gost") {
|
|
DumpLogs(s.T(), s.ctx, "http-connector-tls client logs", clientC)
|
|
DumpLogs(s.T(), s.ctx, "http-connector-tls server logs", serverC)
|
|
}
|
|
s.Require().Equal(0, code)
|
|
s.Require().Contains(string(body), "hello-gost")
|
|
}
|
|
|
|
// TestHTTPProbeResistWeb verifies probeResist web type.
|
|
// On auth failure, the handler fetches the decoy URL
|
|
// (tcp-echo:5678) and returns its response body.
|
|
func (s *HTTPSuite) TestHTTPProbeResistWeb() {
|
|
gostC, err := RunGostContainerWithPorts(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_proberesist_web.yaml", "8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
// Without auth, probeResist web fetches http://tcp-echo:5678
|
|
// and returns the echo server's response ("hello-gost").
|
|
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
|
|
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
|
|
s.Assert().NotContains(resp, "407",
|
|
"probeResist web should hide 407 response")
|
|
s.Assert().Contains(resp, "hello-gost",
|
|
"probeResist web should return decoy response body")
|
|
}
|
|
|
|
// TestHTTPProbeResistFile verifies probeResist file type.
|
|
// On auth failure, the handler reads a local file and
|
|
// returns its contents as the HTTP response body.
|
|
func (s *HTTPSuite) TestHTTPProbeResistFile() {
|
|
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_proberesist_file.yaml",
|
|
[]testcontainers.ContainerFile{
|
|
{HostFilePath: "testdata/http/decoy.html", ContainerFilePath: "/tmp/decoy.html", FileMode: 0644},
|
|
},
|
|
"8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
// Without auth, probeResist file reads /tmp/decoy.html and
|
|
// returns "decoy-response" rather than a 407.
|
|
resp := s.sendRaw(gostC, "127.0.0.1", "8080",
|
|
"GET http://127.0.0.1:5678/ HTTP/1.0\r\nHost: 127.0.0.1\r\n\r\n")
|
|
s.Assert().NotContains(resp, "407",
|
|
"probeResist file should hide 407 response")
|
|
s.Assert().Contains(resp, "decoy-response",
|
|
"probeResist file should return decoy file content")
|
|
}
|
|
|
|
// TestHTTPIdleTimeout verifies idleTimeout on CONNECT tunnels.
|
|
// After the configured idle timeout, the pipe between client
|
|
// and target should close.
|
|
func (s *HTTPSuite) TestHTTPIdleTimeout() {
|
|
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_idle_timeout.yaml",
|
|
[]testcontainers.ContainerFile{
|
|
{HostFilePath: "scripts/http_idle_timeout.py", ContainerFilePath: "/scripts/http_idle_timeout.py", FileMode: 0644},
|
|
},
|
|
"8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
// The Python script:
|
|
// 1. Opens a CONNECT tunnel to tcp-echo:5678
|
|
// 2. Sends and receives data (confirm tunnel is alive)
|
|
// 3. Waits > idleTimeout (3s + 2s buffer)
|
|
// 4. Sends more data — expects connection to be closed
|
|
code, out, err := gostC.Exec(s.ctx, []string{
|
|
"python3", "/scripts/http_idle_timeout.py",
|
|
"127.0.0.1", "8080", "3",
|
|
})
|
|
output, _ := io.ReadAll(out)
|
|
s.T().Logf("idle timeout output:\n%s", string(output))
|
|
if code != 0 {
|
|
DumpLogs(s.T(), s.ctx, "http-idle-timeout logs", gostC)
|
|
}
|
|
s.Require().Equal(0, code, "idle timeout test script should exit 0")
|
|
}
|
|
|
|
// TestHTTPUDPRelay verifies UDP relay over HTTP. Uses
|
|
// X-Gost-Protocol: udp in the CONNECT request to establish
|
|
// a SOCKS5 UDP tunnel through the HTTP handler.
|
|
func (s *HTTPSuite) TestHTTPUDPRelay() {
|
|
// Start UDP echo container on the shared network.
|
|
udpC, err := RunUDPEchoContainer(s.ctx, SharedNetworkName)
|
|
s.Require().NoError(err)
|
|
defer udpC.Terminate(s.ctx)
|
|
|
|
gostC, err := RunGostContainerWithFiles(s.ctx, SharedNetworkName,
|
|
"testdata/http/server_udp.yaml",
|
|
[]testcontainers.ContainerFile{
|
|
{HostFilePath: "scripts/http_udp_relay.py", ContainerFilePath: "/scripts/http_udp_relay.py", FileMode: 0644},
|
|
},
|
|
"8080/tcp")
|
|
s.Require().NoError(err)
|
|
defer gostC.Terminate(s.ctx)
|
|
|
|
// The Python script:
|
|
// 1. Connects to gost HTTP proxy
|
|
// 2. Sends CONNECT with X-Gost-Protocol: udp
|
|
// 3. After 200 OK, sends a SOCKS5 UDP frame targeting udp-echo:5679
|
|
// 4. Reads back the echoed frame
|
|
code, out, err := gostC.Exec(s.ctx, []string{
|
|
"python3", "/scripts/http_udp_relay.py",
|
|
"127.0.0.1", "8080",
|
|
})
|
|
output, _ := io.ReadAll(out)
|
|
s.T().Logf("udp relay output:\n%s", string(output))
|
|
if code != 0 {
|
|
DumpLogs(s.T(), s.ctx, "http-udp logs", gostC)
|
|
}
|
|
s.Require().Equal(0, code, "udp relay test script should exit 0")
|
|
}
|
|
|
|
func TestHTTPSuite(t *testing.T) {
|
|
suite.Run(t, new(HTTPSuite))
|
|
}
|