Files
creator-hub/cmd/control-plane/network_test.go
T

246 lines
7.5 KiB
Go

package main
import (
"bufio"
"context"
"encoding/base64"
"encoding/binary"
"encoding/json"
"io"
"net"
"net/http"
"net/http/httptest"
"net/url"
"strings"
"testing"
"time"
"git.ipao.vip/rogee/creator-hub/internal/hub"
)
func TestHTTPNetworkExitProbeUsesStoredBasicAuth(t *testing.T) {
proxy := httptest.NewServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
want := "Basic " + base64.StdEncoding.EncodeToString([]byte("operator:plain-password"))
if request.Header.Get("Proxy-Authorization") != want {
response.WriteHeader(http.StatusProxyAuthRequired)
return
}
_, _ = response.Write([]byte(`{"ip":"203.0.113.10","region":"Shanghai"}`))
}))
defer proxy.Close()
exit := networkExitForURL(t, "http", proxy.URL)
exit.Username, exit.Password = "operator", "plain-password"
observation, reason := (httpNetworkExitProbe{endpoint: "http://observation.test/json", client: &http.Client{Timeout: time.Second}}).Check(context.Background(), exit)
if reason != "" || observation.PublicIP != "203.0.113.10" || observation.Region != "Shanghai" {
t.Fatalf("authenticated HTTP probe failed: observation=%#v reason=%q", observation, reason)
}
}
func TestHTTPSNetworkExitProbeUsesStoredBasicAuth(t *testing.T) {
proxy := httptest.NewTLSServer(http.HandlerFunc(func(response http.ResponseWriter, request *http.Request) {
want := "Basic " + base64.StdEncoding.EncodeToString([]byte("operator:plain-password"))
if request.Header.Get("Proxy-Authorization") != want {
response.WriteHeader(http.StatusProxyAuthRequired)
return
}
_, _ = response.Write([]byte(`{"ip":"203.0.113.12","region":"Shenzhen"}`))
}))
defer proxy.Close()
exit := networkExitForURL(t, "https", proxy.URL)
exit.Username, exit.Password = "operator", "plain-password"
client := proxy.Client()
client.Timeout = time.Second
observation, reason := (httpNetworkExitProbe{endpoint: "http://observation.test/json", client: client}).Check(context.Background(), exit)
if reason != "" || observation.PublicIP != "203.0.113.12" || observation.Region != "Shenzhen" {
t.Fatalf("authenticated HTTPS probe failed: observation=%#v reason=%q", observation, reason)
}
}
func TestSOCKS5NetworkExitProbeUsesStoredCredentials(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() { done <- serveAuthenticatedSOCKS5(listener, "operator", "plain-password") }()
host, portText, _ := net.SplitHostPort(listener.Addr().String())
exit := hub.NetworkExitAccess{NetworkExit: hub.NetworkExit{
Protocol: "socks5", Host: host, Port: mustPort(t, portText), Username: "operator", Password: "plain-password",
}}
observation, reason := (httpNetworkExitProbe{endpoint: "http://observation.test/json", client: &http.Client{Timeout: time.Second}}).Check(context.Background(), exit)
if reason != "" || observation.PublicIP != "203.0.113.11" || observation.Region != "Beijing" {
t.Fatalf("authenticated SOCKS5 probe failed: observation=%#v reason=%q", observation, reason)
}
if err := <-done; err != nil {
t.Fatal(err)
}
}
func networkExitForURL(t *testing.T, protocol, rawURL string) hub.NetworkExitAccess {
t.Helper()
parsed, err := url.Parse(rawURL)
if err != nil {
t.Fatal(err)
}
host, portText, err := net.SplitHostPort(parsed.Host)
if err != nil {
t.Fatal(err)
}
return hub.NetworkExitAccess{NetworkExit: hub.NetworkExit{Protocol: protocol, Host: host, Port: mustPort(t, portText)}}
}
func mustPort(t *testing.T, value string) int {
t.Helper()
port, err := net.LookupPort("tcp", value)
if err != nil {
t.Fatal(err)
}
return port
}
func serveAuthenticatedSOCKS5(listener net.Listener, username, password string) error {
connection, err := listener.Accept()
if err != nil {
return err
}
defer connection.Close()
reader := bufio.NewReader(connection)
greeting := make([]byte, 2)
if _, err := io.ReadFull(reader, greeting); err != nil {
return err
}
methods := make([]byte, int(greeting[1]))
if _, err := io.ReadFull(reader, methods); err != nil {
return err
}
if _, err := connection.Write([]byte{5, 2}); err != nil {
return err
}
authHeader := make([]byte, 2)
if _, err := io.ReadFull(reader, authHeader); err != nil {
return err
}
user := make([]byte, int(authHeader[1]))
if _, err := io.ReadFull(reader, user); err != nil {
return err
}
passwordLength, err := reader.ReadByte()
if err != nil {
return err
}
secret := make([]byte, int(passwordLength))
if _, err := io.ReadFull(reader, secret); err != nil {
return err
}
if string(user) != username || string(secret) != password {
return io.ErrUnexpectedEOF
}
if _, err := connection.Write([]byte{1, 0}); err != nil {
return err
}
requestHeader := make([]byte, 4)
if _, err := io.ReadFull(reader, requestHeader); err != nil {
return err
}
addressLength := 4
switch requestHeader[3] {
case 3:
length, err := reader.ReadByte()
if err != nil {
return err
}
addressLength = int(length)
case 4:
addressLength = 16
}
if _, err := io.CopyN(io.Discard, reader, int64(addressLength+2)); err != nil {
return err
}
if _, err := connection.Write([]byte{5, 0, 0, 1, 127, 0, 0, 1, 0, 0}); err != nil {
return err
}
if _, err := http.ReadRequest(reader); err != nil {
return err
}
_, err = io.WriteString(connection, "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 40\r\nConnection: close\r\n\r\n{\"ip\":\"203.0.113.11\",\"region\":\"Beijing\"}")
return err
}
func TestSOCKS4DialerUsesBoundProxy(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()
header := make([]byte, 8)
if _, err := io.ReadFull(connection, header); err != nil {
done <- err
return
}
user := make([]byte, 0, 16)
for {
var value [1]byte
if _, err := io.ReadFull(connection, value[:]); err != nil {
done <- err
return
}
if value[0] == 0 {
break
}
user = append(user, value[0])
}
if header[0] != 4 || header[1] != 1 || binary.BigEndian.Uint16(header[2:4]) != 443 ||
net.IP(header[4:8]).String() != "203.0.113.1" || string(user) != "operator" {
done <- io.ErrUnexpectedEOF
return
}
_, err = connection.Write([]byte{0, 90, 0, 0, 0, 0, 0, 0})
done <- err
}()
connection, err := socks4DialContext(listener.Addr().String(), "operator")(context.Background(), "tcp", "203.0.113.1:443")
if err != nil {
t.Fatal(err)
}
_ = connection.Close()
if err := <-done; err != nil {
t.Fatal(err)
}
}
func TestGatewayNetworkExitUsesStoredPlainCredentials(t *testing.T) {
exit := hub.NetworkExitAccess{NetworkExit: hub.NetworkExit{
Protocol: "socks5", Host: "proxy.example", Port: 1080,
Username: "operator", Password: "plain-password",
}}
gatewayExit := gatewayNetworkExitFor(exit)
if gatewayExit.Username != "operator" || gatewayExit.Password != "plain-password" || gatewayExit.Host != "proxy.example" {
t.Fatalf("stored credential was not copied into the gateway payload: %#v", gatewayExit)
}
encoded := string(mustJSON(t, exit.NetworkExit))
if !strings.Contains(encoded, `"username":"operator"`) || !strings.Contains(encoded, `"password":"plain-password"`) {
t.Fatalf("network exit API model must expose stored credentials: %s", encoded)
}
}
func mustJSON(t *testing.T, value any) []byte {
t.Helper()
encoded, err := json.Marshal(value)
if err != nil {
t.Fatal(err)
}
return encoded
}