Initialize Clash Web source and deployment documentation

This commit is contained in:
lhc committed 2026-10-05 17:32:00 +08:00
commit 8e0fa00a73
66 files changed
+8532

No files matched your search

+180
View File
@@ -0,0 +1,180 @@
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")
}
}