181 lines
6.9 KiB
Go
181 lines
6.9 KiB
Go
package app
|
|
|
|
import (
|
|
"context"
|
|
"crypto/x509"
|
|
"encoding/json"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"sync/atomic"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
func TestConnectivityResponses(t *testing.T) {
|
|
for _, test := range []struct {
|
|
name string
|
|
code int
|
|
header, body, status string
|
|
}{
|
|
{"success", 200, "", "<html>Welcome</html>", "reachable"},
|
|
{"header challenge", 403, "challenge", "verify", "challenge"},
|
|
{"body challenge", 200, "", "<title>Just a moment</title> Cloudflare cf-chl-test", "challenge"},
|
|
{"denied", 403, "", "denied", "restricted"},
|
|
{"region", 451, "", "region", "restricted"},
|
|
{"limited", 429, "", "rate limited", "restricted"},
|
|
{"server failure", 503, "", "unavailable", "http_error"},
|
|
} {
|
|
t.Run(test.name, func(t *testing.T) {
|
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "GET" {
|
|
t.Error("not a GET probe")
|
|
}
|
|
w.Header().Set("Cf-Mitigated", test.header)
|
|
w.WriteHeader(test.code)
|
|
io.WriteString(w, test.body)
|
|
}))
|
|
defer server.Close()
|
|
result := probeConnectivity(context.Background(), server.Client(), connectivitySite{ID: "fixture", URL: server.URL})
|
|
if result.Status != test.status || result.HTTPStatus != test.code || !result.TLSVerified || result.CheckedAt.IsZero() {
|
|
t.Fatalf("unexpected result: %+v", result)
|
|
}
|
|
})
|
|
}
|
|
}
|
|
|
|
func TestConnectivityUsesExplicitProxyAndVerifiesTLS(t *testing.T) {
|
|
origin := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { io.WriteString(w, "ok") }))
|
|
defer origin.Close()
|
|
var connects atomic.Int32
|
|
proxy := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Method != "CONNECT" {
|
|
t.Error("expected CONNECT")
|
|
w.WriteHeader(400)
|
|
return
|
|
}
|
|
connects.Add(1)
|
|
upstream, err := net.Dial("tcp", r.Host)
|
|
if err != nil {
|
|
w.WriteHeader(502)
|
|
return
|
|
}
|
|
connection, buffered, err := w.(http.Hijacker).Hijack()
|
|
if err != nil {
|
|
upstream.Close()
|
|
return
|
|
}
|
|
defer connection.Close()
|
|
defer upstream.Close()
|
|
buffered.WriteString("HTTP/1.1 200 Connection Established\r\n\r\n")
|
|
buffered.Flush()
|
|
go io.Copy(upstream, buffered)
|
|
io.Copy(connection, upstream)
|
|
}))
|
|
defer proxy.Close()
|
|
u, _ := url.Parse(proxy.URL)
|
|
port, _ := strconv.Atoi(u.Port())
|
|
t.Setenv("HTTP_PROXY", "http://127.0.0.1:1")
|
|
t.Setenv("HTTPS_PROXY", "http://127.0.0.1:1")
|
|
t.Setenv("NO_PROXY", "*")
|
|
client := connectivityClient(Settings{MixedPort: port})
|
|
defer client.CloseIdleConnections()
|
|
transport := client.Transport.(*http.Transport)
|
|
transport.TLSClientConfig = origin.Client().Transport.(*http.Transport).TLSClientConfig.Clone()
|
|
result := probeConnectivity(context.Background(), client, connectivitySite{ID: "fixture", URL: origin.URL})
|
|
if connects.Load() != 1 || result.Status != "reachable" || !result.TLSVerified {
|
|
t.Fatalf("proxy/TLS regression: %+v, connects=%d", result, connects.Load())
|
|
}
|
|
client.CloseIdleConnections()
|
|
transport.TLSClientConfig.RootCAs = x509.NewCertPool()
|
|
result = probeConnectivity(context.Background(), client, connectivitySite{ID: "fixture", URL: origin.URL})
|
|
if result.Status != "failed" || result.TLSVerified || !strings.Contains(result.Detail, "证书") {
|
|
t.Fatalf("untrusted TLS accepted: %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestConnectivityTimeoutCancellationAndRedirects(t *testing.T) {
|
|
server := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { <-r.Context().Done() }))
|
|
defer server.Close()
|
|
client := server.Client()
|
|
client.Timeout = 30 * time.Millisecond
|
|
result := probeConnectivity(context.Background(), client, connectivitySite{URL: server.URL})
|
|
if result.Status != "failed" || !strings.Contains(result.Detail, "超时") {
|
|
t.Fatalf("timeout: %+v", result)
|
|
}
|
|
ctx, cancel := context.WithCancel(context.Background())
|
|
cancel()
|
|
result = probeConnectivity(ctx, client, connectivitySite{URL: server.URL})
|
|
if !strings.Contains(result.Detail, "取消") {
|
|
t.Fatalf("cancel: %+v", result)
|
|
}
|
|
redirectClient := connectivityClient(Settings{MixedPort: 7890})
|
|
defer redirectClient.CloseIdleConnections()
|
|
original, _ := http.NewRequest("GET", "https://chatgpt.com/", nil)
|
|
for _, target := range []string{"http://chatgpt.com/", "https://127.0.0.1/", "https://evilchatgpt.com/", "https://chatgpt.com.evil.example/", "https://user:pass@chatgpt.com/"} {
|
|
req, _ := http.NewRequest("GET", target, nil)
|
|
if redirectClient.CheckRedirect(req, []*http.Request{original}) == nil {
|
|
t.Fatalf("unsafe redirect accepted: %s", target)
|
|
}
|
|
}
|
|
req, _ := http.NewRequest("GET", "https://auth.openai.com/", nil)
|
|
if err := redirectClient.CheckRedirect(req, []*http.Request{original}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if err := redirectClient.CheckRedirect(req, []*http.Request{original, original, original, original, original}); err == nil {
|
|
t.Fatal("unbounded redirects")
|
|
}
|
|
redirectServer := httptest.NewTLSServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { http.Redirect(w, r, "http://127.0.0.1/private", 302) }))
|
|
defer redirectServer.Close()
|
|
client = redirectServer.Client()
|
|
client.CheckRedirect = redirectClient.CheckRedirect
|
|
result = probeConnectivity(context.Background(), client, connectivitySite{URL: redirectServer.URL})
|
|
if result.Status != "http_error" || result.HTTPStatus != 302 || !result.TLSVerified {
|
|
t.Fatalf("redirect response discarded: %+v", result)
|
|
}
|
|
}
|
|
|
|
func TestConnectivityAPIAuthCatalogAndLimits(t *testing.T) {
|
|
a := newTestApp(t)
|
|
path := "/api/v1/system/connectivity"
|
|
if w := request(t, a, "GET", path, nil, nil, ""); w.Code != 401 {
|
|
t.Fatal("unauthenticated test access")
|
|
}
|
|
cookie, csrf := loginTest(t, a)
|
|
w := request(t, a, "GET", path, nil, cookie, "")
|
|
var sites []connectivitySite
|
|
if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &sites) != nil || len(sites) != 9 {
|
|
t.Fatal("catalog missing")
|
|
}
|
|
if w := request(t, a, "POST", path+"/test", map[string]string{"siteId": "google"}, cookie, ""); w.Code != 403 {
|
|
t.Fatal("CSRF missing")
|
|
}
|
|
if w := request(t, a, "POST", path+"/test", map[string]string{"siteId": "https://127.0.0.1/"}, cookie, csrf); w.Code != 400 {
|
|
t.Fatal("arbitrary target allowed")
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
a.connectivitySlots <- struct{}{}
|
|
}
|
|
if w := request(t, a, "POST", path+"/test", map[string]string{"siteId": "google"}, cookie, csrf); w.Code != 429 {
|
|
t.Fatal("concurrency limit missing")
|
|
}
|
|
for i := 0; i < 3; i++ {
|
|
<-a.connectivitySlots
|
|
}
|
|
core := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { w.WriteHeader(503) }))
|
|
defer core.Close()
|
|
a.core.URL = core.URL
|
|
w = request(t, a, "POST", path+"/test", map[string]string{"siteId": "google"}, cookie, csrf)
|
|
var result connectivityResult
|
|
if w.Code != 200 || json.Unmarshal(w.Body.Bytes(), &result) != nil || result.Status != "failed" || !strings.Contains(result.Detail, "内核") {
|
|
t.Fatal("offline core not explained", w.Body)
|
|
}
|
|
if len(a.connectivitySlots) != 0 {
|
|
t.Fatal("slot leaked")
|
|
}
|
|
}
|