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, "", "Welcome", "reachable"}, {"header challenge", 403, "challenge", "verify", "challenge"}, {"body challenge", 200, "", "Just a moment 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") } }