Initialize Clash Web source and deployment documentation
This commit is contained in:
commit
8e0fa00a73
66 files changed
+8532
No files matched your search
@@ -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")
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user