mirror of
https://github.com/GopeedLab/gopeed.git
synced 2026-11-04 15:49:49 +00:00
316 lines
9.2 KiB
Go
316 lines
9.2 KiB
Go
package proxy
|
|
|
|
import (
|
|
"bufio"
|
|
"context"
|
|
"crypto/tls"
|
|
"errors"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
|
|
"github.com/armon/go-socks5"
|
|
)
|
|
|
|
func TestRejectedProxyRequests(t *testing.T) {
|
|
secret := "upstream-password"
|
|
for _, tc := range []struct {
|
|
name, method, target string
|
|
status int
|
|
}{
|
|
{"relative URL", "GET", "/relative", 400},
|
|
{"unsupported URL", "GET", "ftp://example.com/file", 400},
|
|
{"invalid CONNECT", "CONNECT", "example.com", 400},
|
|
{"HTTP selection failure", "GET", "http://example.com/", 502},
|
|
{"CONNECT selection failure", "CONNECT", "example.com:443", 502},
|
|
} {
|
|
t.Run(tc.name, func(t *testing.T) {
|
|
s := &Server{selectProxy: func(*http.Request) (*url.URL, error) { return nil, errors.New(secret) }}
|
|
s.transport = &http.Transport{Proxy: s.selectProxy}
|
|
defer s.transport.CloseIdleConnections()
|
|
response := httptest.NewRecorder()
|
|
s.ServeHTTP(response, httptest.NewRequest(tc.method, tc.target, nil))
|
|
if response.Code != tc.status || strings.Contains(response.Body.String(), secret) {
|
|
t.Fatalf("unexpected proxy rejection: %d %q", response.Code, response.Body.String())
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestTunnelRejectsUnsafeUpstreams(t *testing.T) {
|
|
rejected := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
http.Error(w, "authentication required", http.StatusProxyAuthRequired)
|
|
}))
|
|
defer rejected.Close()
|
|
secure := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
t.Error("untrusted TLS proxy received a request")
|
|
}))
|
|
defer secure.Close()
|
|
for _, upstream := range []string{"ftp://127.0.0.1", rejected.URL, secure.URL} {
|
|
t.Run(upstream, func(t *testing.T) {
|
|
selected, err := url.Parse(upstream)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
ctx, cancel := context.WithTimeout(context.Background(), 3*time.Second)
|
|
defer cancel()
|
|
conn, err := dialTunnel(ctx, "example.com:443", selected)
|
|
if conn != nil {
|
|
conn.Close()
|
|
}
|
|
if err == nil {
|
|
t.Fatal("unsafe upstream unexpectedly accepted")
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestRoutesAndAuthentication(t *testing.T) {
|
|
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Proxy-Authorization") != "" {
|
|
t.Error("proxy credentials leaked to origin")
|
|
}
|
|
w.Write([]byte("origin"))
|
|
}))
|
|
defer origin.Close()
|
|
secure := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("secure")) }))
|
|
defer secure.Close()
|
|
upstream, err := New(func(*http.Request) (*url.URL, error) { return nil, nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer upstream.Close()
|
|
var authenticated atomic.Int32
|
|
authProxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Proxy-Authorization") != "Basic dXNlcjpwYXNz" {
|
|
http.Error(w, "authentication required", 407)
|
|
return
|
|
}
|
|
authenticated.Add(1)
|
|
upstream.ServeHTTP(w, r)
|
|
}))
|
|
defer authProxy.Close()
|
|
socks, err := socks5.New(&socks5.Config{Credentials: socks5.StaticCredentials{"user": "pass"}})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer listener.Close()
|
|
go socks.Serve(listener)
|
|
for _, route := range []string{"", authProxy.URL, "socks5://" + listener.Addr().String()} {
|
|
t.Run(route, func(t *testing.T) {
|
|
var selected *url.URL
|
|
if route != "" {
|
|
selected, _ = url.Parse(route)
|
|
selected.User = url.UserPassword("user", "pass")
|
|
}
|
|
local, err := New(func(r *http.Request) (*url.URL, error) { return selected, nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer local.Close()
|
|
proxyURL, _ := url.Parse(local.URL())
|
|
transport := &http.Transport{Proxy: http.ProxyURL(proxyURL), TLSClientConfig: &tls.Config{InsecureSkipVerify: true}}
|
|
defer transport.CloseIdleConnections()
|
|
client := &http.Client{Transport: transport, Timeout: 5 * time.Second}
|
|
for _, test := range []struct{ url, want string }{{origin.URL, "origin"}, {secure.URL, "secure"}} {
|
|
response, err := client.Get(test.url)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := io.ReadAll(response.Body)
|
|
response.Body.Close()
|
|
if string(body) != test.want {
|
|
t.Fatalf("got status %d, body %q", response.StatusCode, body)
|
|
}
|
|
}
|
|
})
|
|
}
|
|
if authenticated.Load() != 2 {
|
|
t.Fatalf("expected authenticated HTTP and CONNECT, got %d", authenticated.Load())
|
|
}
|
|
}
|
|
|
|
func TestRoutingChangesAndFailureDoesNotBypassProxy(t *testing.T) {
|
|
var hits atomic.Int32
|
|
origin := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { hits.Add(1); w.Write([]byte("direct")) }))
|
|
defer origin.Close()
|
|
var useProxy atomic.Bool
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.Write([]byte("proxied")) }))
|
|
upstreamURL, _ := url.Parse(upstream.URL)
|
|
local, err := New(func(*http.Request) (*url.URL, error) {
|
|
if useProxy.Load() {
|
|
return upstreamURL, nil
|
|
}
|
|
return nil, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer local.Close()
|
|
endpoint, _ := url.Parse(local.URL())
|
|
transport := &http.Transport{Proxy: http.ProxyURL(endpoint)}
|
|
defer transport.CloseIdleConnections()
|
|
client := &http.Client{Transport: transport, Timeout: 3 * time.Second}
|
|
for _, want := range []string{"direct", "proxied", "upstream request failed\n"} {
|
|
resp, err := client.Get(origin.URL)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if string(body) != want {
|
|
t.Fatalf("got %q want %q", body, want)
|
|
}
|
|
if want == "direct" {
|
|
useProxy.Store(true)
|
|
} else if want == "proxied" {
|
|
upstream.Close()
|
|
}
|
|
}
|
|
if hits.Load() != 1 {
|
|
t.Fatal("failed proxy must not fall back to direct")
|
|
}
|
|
}
|
|
|
|
func TestCloseTerminatesTunnels(t *testing.T) {
|
|
echo, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer echo.Close()
|
|
go func() {
|
|
c, e := echo.Accept()
|
|
if e == nil {
|
|
defer c.Close()
|
|
io.Copy(c, c)
|
|
}
|
|
}()
|
|
s, err := New(func(*http.Request) (*url.URL, error) { return nil, nil })
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
endpoint, _ := url.Parse(s.URL())
|
|
c, err := dialTunnel(context.Background(), echo.Addr().String(), endpoint)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer c.Close()
|
|
c.SetDeadline(time.Now().Add(3 * time.Second))
|
|
c.Write([]byte("hello\n"))
|
|
line, err := bufio.NewReader(c).ReadString('\n')
|
|
if err != nil || line != "hello\n" {
|
|
t.Fatalf("echo %q %v", line, err)
|
|
}
|
|
s.Close()
|
|
if _, err := c.Read(make([]byte, 1)); err == nil {
|
|
t.Fatal("tunnel stayed open")
|
|
}
|
|
}
|
|
|
|
func TestSOCKSIngressPreservesRemoteDNS(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "CONNECT" || r.Host != "unresolved.invalid:80" {
|
|
t.Errorf("lost destination: %s %s", r.Method, r.Host)
|
|
}
|
|
c, b, err := w.(http.Hijacker).Hijack()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.Close()
|
|
b.WriteString("HTTP/1.1 200 Connection Established\r\n\r\n")
|
|
b.Flush()
|
|
request, err := http.ReadRequest(b.Reader)
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
request.Body.Close()
|
|
b.WriteString("HTTP/1.1 200 OK\r\nContent-Length: 7\r\nConnection: close\r\n\r\nproxied")
|
|
b.Flush()
|
|
}))
|
|
defer upstream.Close()
|
|
target, _ := url.Parse(upstream.URL)
|
|
local, err := New(func(r *http.Request) (*url.URL, error) {
|
|
if r.URL.Host != "unresolved.invalid:80" {
|
|
t.Errorf("selector lost hostname: %s", r.URL)
|
|
}
|
|
return target, nil
|
|
})
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer local.Close()
|
|
endpoint, _ := url.Parse(local.SOCKSURL())
|
|
transport := &http.Transport{Proxy: http.ProxyURL(endpoint)}
|
|
defer transport.CloseIdleConnections()
|
|
client := &http.Client{Transport: transport, Timeout: 5 * time.Second}
|
|
response, err := client.Get("http://unresolved.invalid/")
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer response.Body.Close()
|
|
body, _ := io.ReadAll(response.Body)
|
|
if string(body) != "proxied" {
|
|
t.Fatalf("unexpected body %q", body)
|
|
}
|
|
}
|
|
|
|
func TestHTTPUpgradeIsForwarded(t *testing.T) {
|
|
upstream := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Upgrade") != "fixture" {
|
|
http.Error(w, "upgrade lost", 400)
|
|
return
|
|
}
|
|
c, b, err := w.(http.Hijacker).Hijack()
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer c.Close()
|
|
b.WriteString("HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: fixture\r\n\r\n")
|
|
b.Flush()
|
|
line, err := b.ReadString('\n')
|
|
if err != nil {
|
|
return
|
|
}
|
|
b.WriteString(line)
|
|
b.Flush()
|
|
}))
|
|
defer upstream.Close()
|
|
local, err := New(nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer local.Close()
|
|
endpoint, _ := url.Parse(local.URL())
|
|
c, err := net.Dial("tcp", endpoint.Host)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer c.Close()
|
|
c.SetDeadline(time.Now().Add(3 * time.Second))
|
|
io.WriteString(c, "GET "+upstream.URL+"/ HTTP/1.1\r\nHost: fixture\r\nConnection: Upgrade\r\nUpgrade: fixture\r\n\r\n")
|
|
reader := bufio.NewReader(c)
|
|
response, err := http.ReadResponse(reader, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if response.StatusCode != 101 {
|
|
t.Fatalf("upgrade failed: %s", response.Status)
|
|
}
|
|
io.WriteString(c, "hello\n")
|
|
line, err := reader.ReadString('\n')
|
|
if err != nil || line != "hello\n" {
|
|
t.Fatalf("upgraded echo %q %v", line, err)
|
|
}
|
|
}
|