Files
creator-hub/cmd/docker-gateway/proxy_test.go
T

253 lines
8.3 KiB
Go

package main
import (
"bufio"
"encoding/binary"
"fmt"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strconv"
"strings"
"testing"
"time"
)
func TestMemoryProxyUsesSOCKS5Credentials(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
done := make(chan error, 1)
go func() {
connection, err := listener.Accept()
if err != nil {
done <- err
return
}
defer connection.Close()
greeting := make([]byte, 3)
if _, err := io.ReadFull(connection, greeting); err != nil {
done <- err
return
}
_, _ = connection.Write([]byte{5, 2})
authHeader := make([]byte, 2)
_, _ = io.ReadFull(connection, authHeader)
username := make([]byte, int(authHeader[1]))
_, _ = io.ReadFull(connection, username)
var passwordLength [1]byte
_, _ = io.ReadFull(connection, passwordLength[:])
password := make([]byte, int(passwordLength[0]))
_, _ = io.ReadFull(connection, password)
if string(username) != "operator" || string(password) != "ephemeral" {
done <- io.ErrUnexpectedEOF
return
}
_, _ = connection.Write([]byte{1, 0})
requestHeader := make([]byte, 5)
_, _ = io.ReadFull(connection, requestHeader)
host := make([]byte, int(requestHeader[4]))
_, _ = io.ReadFull(connection, host)
port := make([]byte, 2)
_, _ = io.ReadFull(connection, port)
if string(host) != "example.com" || binary.BigEndian.Uint16(port) != 443 {
done <- io.ErrUnexpectedEOF
return
}
if _, err = connection.Write([]byte{5, 0, 0, 1, 127, 0, 0, 1, 0, 0}); err != nil {
done <- err
return
}
var tunneled [1]byte
_, err = io.ReadFull(connection, tunneled[:])
if err == nil && tunneled[0] != 'x' {
err = io.ErrUnexpectedEOF
}
done <- err
}()
host, portText, _ := net.SplitHostPort(listener.Addr().String())
port, _ := net.LookupPort("tcp", portText)
registry := newMemoryProxyRegistry()
proxyURL, cleanup, err := registry.configure("account-a", 1, "127.0.0.1", 0, gatewayProxyExit{
Protocol: "socks5", Host: host, Port: port, Username: "operator", Password: "ephemeral",
})
if err != nil {
t.Fatal(err)
}
defer cleanup()
parsed, _ := url.Parse(proxyURL)
connection, err := net.Dial("tcp", strings.Replace(parsed.Host, browserProxyHost, "127.0.0.1", 1))
if err != nil {
t.Fatal(err)
}
if _, err := fmt.Fprint(connection, "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n"); err != nil {
t.Fatal(err)
}
response, err := http.ReadResponse(bufio.NewReader(connection), &http.Request{Method: http.MethodConnect})
if err != nil || response.StatusCode != http.StatusOK {
t.Fatalf("memory proxy CONNECT failed: response=%v err=%v", response, err)
}
if _, err := connection.Write([]byte{'x'}); err != nil {
t.Fatal(err)
}
_ = connection.Close()
if err := <-done; err != nil {
t.Fatal(err)
}
}
func TestMemoryProxyUsesAbsoluteFormForHTTPUpstream(t *testing.T) {
upstream := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
if request.Method == http.MethodConnect {
http.Error(response, "CONNECT forbidden", http.StatusMethodNotAllowed)
return
}
if !request.URL.IsAbs() || request.URL.String() != "http://example.com/plain" {
t.Fatalf("expected absolute-form request, got %q", request.URL.String())
}
if request.Header.Get("Proxy-Authorization") == "" {
t.Fatal("upstream proxy credentials were not applied")
}
_, _ = response.Write([]byte("forwarded"))
}))
defer upstream.Close()
address, _ := url.Parse(upstream.URL)
port, _ := strconv.Atoi(address.Port())
registry := newMemoryProxyRegistry()
proxyURL, cleanup, err := registry.configure("account-a", 1, "127.0.0.1", 0, gatewayProxyExit{
Protocol: "http", Host: address.Hostname(), Port: port, Username: "operator", Password: "ephemeral",
})
if err != nil {
t.Fatal(err)
}
defer cleanup()
proxyAddress := strings.Replace(strings.TrimPrefix(proxyURL, "http://"), browserProxyHost, "127.0.0.1", 1)
client := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(&url.URL{Scheme: "http", Host: proxyAddress})}}
response, err := client.Get("http://example.com/plain")
if err != nil {
t.Fatal(err)
}
defer response.Body.Close()
body, _ := io.ReadAll(response.Body)
if response.StatusCode != http.StatusOK || string(body) != "forwarded" {
t.Fatalf("plain HTTP was not forwarded: status=%d body=%s", response.StatusCode, body)
}
}
func TestMemoryProxyCleanupClosesHijackedTunnel(t *testing.T) {
listener, err := net.Listen("tcp", "127.0.0.1:0")
if err != nil {
t.Fatal(err)
}
defer listener.Close()
upstreamClosed := make(chan error, 1)
go func() {
connection, err := listener.Accept()
if err != nil {
upstreamClosed <- err
return
}
defer connection.Close()
request, err := http.ReadRequest(bufio.NewReader(connection))
if err == nil && request.Method == http.MethodConnect {
_, err = fmt.Fprint(connection, "HTTP/1.1 200 Connection Established\r\n\r\n")
}
if err == nil {
var data [1]byte
_, err = connection.Read(data[:])
}
upstreamClosed <- err
}()
host, portText, _ := net.SplitHostPort(listener.Addr().String())
port, _ := strconv.Atoi(portText)
registry := newMemoryProxyRegistry()
proxyURL, cleanup, err := registry.configure("account-a", 1, "127.0.0.1", 0,
gatewayProxyExit{Protocol: "http", Host: host, Port: port})
if err != nil {
t.Fatal(err)
}
defer cleanup()
proxyAddress := strings.Replace(strings.TrimPrefix(proxyURL, "http://"), browserProxyHost, "127.0.0.1", 1)
client, err := net.Dial("tcp", proxyAddress)
if err != nil {
t.Fatal(err)
}
defer client.Close()
if _, err = fmt.Fprint(client, "CONNECT example.com:443 HTTP/1.1\r\nHost: example.com:443\r\n\r\n"); err != nil {
t.Fatal(err)
}
if response, err := http.ReadResponse(bufio.NewReader(client), &http.Request{Method: http.MethodConnect}); err != nil || response.StatusCode != http.StatusOK {
t.Fatalf("open CONNECT tunnel: response=%v err=%v", response, err)
}
cleanup()
_ = client.SetReadDeadline(time.Now().Add(time.Second))
if _, err = client.Read(make([]byte, 1)); err == nil {
t.Fatal("proxy cleanup left the client tunnel open")
}
select {
case err = <-upstreamClosed:
if err == nil {
t.Fatal("proxy cleanup left the upstream tunnel open")
}
case <-time.After(time.Second):
t.Fatal("proxy cleanup did not close the upstream tunnel")
}
}
func TestMemoryProxyRejectsCrossAliasAddress(t *testing.T) {
registry := newMemoryProxyRegistry()
proxyURL, cleanup, err := registry.configure("account-a", 1, "127.0.0.1", 0, gatewayProxyExit{Protocol: "http", Host: "127.0.0.1", Port: 1})
if err != nil {
t.Fatal(err)
}
defer cleanup()
parsed, _ := url.Parse(proxyURL)
if connection, err := net.DialTimeout("tcp", net.JoinHostPort("127.0.0.2", parsed.Port()), 100*time.Millisecond); err == nil {
_ = connection.Close()
t.Fatal("another tenant address could reach account-a proxy")
}
}
func TestMemoryProxyRemoveRequiresMatchingGeneration(t *testing.T) {
registry := newMemoryProxyRegistry()
proxyURL, cleanup, err := registry.configure("account-a", 2, "127.0.0.1", 0, gatewayProxyExit{Protocol: "http", Host: "127.0.0.1", Port: 1})
if err != nil {
t.Fatal(err)
}
defer cleanup()
if !registry.bind("account-a", 2, proxyURL, "container-c2") {
t.Fatal("bind proxy generation")
}
proxy := registry.proxies["account-a"]
if registry.remove("account-a", 2, "") || registry.remove("account-a", 2, "container-c1") || registry.proxies["account-a"] != proxy {
t.Fatal("stale generation removed the current proxy")
}
}
func TestMemoryProxyReplacesStaleGenerationWithoutOldCleanup(t *testing.T) {
registry := newMemoryProxyRegistry()
exit := gatewayProxyExit{Protocol: "http", Host: "127.0.0.1", Port: 1}
oldURL, oldCleanup, err := registry.configure("account-a", 1, "127.0.0.1", 0, exit, "network-n1")
if err != nil {
t.Fatal(err)
}
oldPort := registry.proxies["account-a"].listener.Addr().(*net.TCPAddr).Port
newURL, newCleanup, err := registry.configure("account-a", 2, "127.0.0.1", oldPort, exit, "network-n2")
if err != nil {
t.Fatal(err)
}
defer newCleanup()
if newURL != oldURL || registry.proxies["account-a"].bindingVersion != 2 {
t.Fatalf("stale proxy was not replaced: old=%q new=%q proxy=%#v", oldURL, newURL, registry.proxies["account-a"])
}
oldCleanup()
if registry.proxies["account-a"].bindingVersion != 2 {
t.Fatal("old cleanup removed the replacement proxy")
}
}