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 }