246 lines
7.5 KiB
Go
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
|
|
}
|