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,183 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"net/url"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"golang.org/x/net/proxy"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
func freePort(t *testing.T) int {
|
||||
t.Helper()
|
||||
listener, err := net.Listen("tcp", "127.0.0.1:0")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
port := listener.Addr().(*net.TCPAddr).Port
|
||||
listener.Close()
|
||||
return port
|
||||
}
|
||||
|
||||
// This test starts a real core with TUN disabled and routes test traffic through local HTTP proxy nodes.
|
||||
// It never modifies the development machine's routes, forwarding, DNS, or DHCP.
|
||||
func TestRealMihomoProxyAndNodeSelection(t *testing.T) {
|
||||
binary := os.Getenv("CLASHWEB_TEST_MIHOMO_BIN")
|
||||
rulesDir := os.Getenv("CLASHWEB_TEST_RULES_DIR")
|
||||
if binary == "" || rulesDir == "" {
|
||||
t.Skip("set CLASHWEB_TEST_MIHOMO_BIN and CLASHWEB_TEST_RULES_DIR to run real-core integration")
|
||||
}
|
||||
binary, _ = filepath.Abs(binary)
|
||||
dir := t.TempDir()
|
||||
os.MkdirAll(filepath.Join(dir, "rules"), 0750)
|
||||
for _, file := range []string{"cn-domain.mrs", "cn-ip.mrs"} {
|
||||
data, err := os.ReadFile(filepath.Join(rulesDir, file))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
os.WriteFile(filepath.Join(dir, "rules", file), data, 0640)
|
||||
}
|
||||
makeProxy := func(label string) *httptest.Server {
|
||||
return httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodConnect {
|
||||
fmt.Fprint(w, label)
|
||||
return
|
||||
}
|
||||
connection, buffer, err := w.(http.Hijacker).Hijack()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
defer connection.Close()
|
||||
io.WriteString(buffer, "HTTP/1.1 200 Connection Established\r\n\r\n")
|
||||
buffer.Flush()
|
||||
request, err := http.ReadRequest(buffer.Reader)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
request.Body.Close()
|
||||
time.Sleep(30 * time.Millisecond)
|
||||
fmt.Fprintf(buffer, "HTTP/1.1 200 OK\r\nContent-Length: %d\r\nConnection: close\r\n\r\n%s", len(label), label)
|
||||
buffer.Flush()
|
||||
}))
|
||||
}
|
||||
nodeA, nodeB := makeProxy("node-a"), makeProxy("node-b")
|
||||
defer nodeA.Close()
|
||||
defer nodeB.Close()
|
||||
makeNode := func(name string, server *httptest.Server) map[string]any {
|
||||
u, _ := url.Parse(server.URL)
|
||||
_, p, _ := net.SplitHostPort(u.Host)
|
||||
port, _ := strconv.Atoi(p)
|
||||
return map[string]any{"name": name, "type": "http", "server": "127.0.0.1", "port": port}
|
||||
}
|
||||
s := defaultState()
|
||||
s.Settings.SelectedNode = "node-A"
|
||||
s.Settings.MixedPort = freePort(t)
|
||||
s.Settings.DNSPort = freePort(t)
|
||||
s.Subscriptions = []Subscription{{Enabled: true, Nodes: []map[string]any{makeNode("node-A", nodeA), makeNode("node-B", nodeB)}}}
|
||||
data, _ := compileConfig(s, "integration-secret")
|
||||
var config map[string]any
|
||||
yaml.Unmarshal(data, &config)
|
||||
controller := freePort(t)
|
||||
config["external-controller"] = fmt.Sprintf("127.0.0.1:%d", controller)
|
||||
data, _ = yaml.Marshal(config)
|
||||
core := Core{Binary: binary, Dir: dir, URL: fmt.Sprintf("http://127.0.0.1:%d", controller), Secret: "integration-secret", HTTP: &http.Client{Timeout: 3 * time.Second}}
|
||||
if err := core.Validate(context.Background(), data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := atomicWrite(filepath.Join(dir, "config.yaml"), data); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
command := exec.Command(binary, "-d", dir, "-f", filepath.Join(dir, "config.yaml"))
|
||||
var output strings.Builder
|
||||
command.Stdout = &output
|
||||
command.Stderr = &output
|
||||
if err := command.Start(); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer func() {
|
||||
command.Process.Signal(os.Interrupt)
|
||||
command.Wait()
|
||||
if t.Failed() {
|
||||
t.Log(output.String())
|
||||
}
|
||||
}()
|
||||
ready := false
|
||||
for i := 0; i < 50; i++ {
|
||||
var version map[string]any
|
||||
if core.Request(context.Background(), "GET", "/version", nil, &version) == nil {
|
||||
ready = true
|
||||
break
|
||||
}
|
||||
time.Sleep(100 * time.Millisecond)
|
||||
}
|
||||
if !ready {
|
||||
t.Fatal("core did not start")
|
||||
}
|
||||
var latency struct {
|
||||
Delay int `json:"delay"`
|
||||
}
|
||||
if err := core.Request(context.Background(), "GET", "/proxies/node-A/delay?url="+url.QueryEscape("http://198.51.100.42/health")+"&timeout=2000", nil, &latency); err != nil || latency.Delay <= 0 {
|
||||
t.Fatalf("node latency check failed: delay=%d err=%v", latency.Delay, err)
|
||||
}
|
||||
u, _ := url.Parse(fmt.Sprintf("http://127.0.0.1:%d", s.Settings.MixedPort))
|
||||
client := &http.Client{Transport: &http.Transport{Proxy: http.ProxyURL(u)}, Timeout: 5 * time.Second}
|
||||
get := func(target, want string) {
|
||||
t.Helper()
|
||||
resp, err := client.Get(target)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
if string(b) != want {
|
||||
t.Fatalf("got %q, want %q", b, want)
|
||||
}
|
||||
}
|
||||
get("http://198.51.100.42/test", "node-a")
|
||||
if err := core.Request(context.Background(), "PUT", "/proxies/"+ProxyGroup, map[string]string{"name": "node-B"}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
get("http://198.51.100.42/test", "node-b")
|
||||
direct := makeProxy("direct-lan")
|
||||
defer direct.Close()
|
||||
get(direct.URL, "direct-lan")
|
||||
dialer, err := proxy.SOCKS5("tcp", fmt.Sprintf("127.0.0.1:%d", s.Settings.MixedPort), nil, proxy.Direct)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
socksClient := &http.Client{Transport: &http.Transport{DialContext: func(ctx context.Context, network, address string) (net.Conn, error) {
|
||||
return dialer.(proxy.ContextDialer).DialContext(ctx, network, address)
|
||||
}}, Timeout: 5 * time.Second}
|
||||
resp, err := socksClient.Get("http://198.51.100.42/socks")
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b, _ := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if string(b) != "node-b" {
|
||||
t.Fatal("SOCKS selection not applied", string(b))
|
||||
}
|
||||
if err := core.Request(context.Background(), "PUT", "/proxies/"+ProxyGroup, map[string]string{"name": "REJECT"}, nil); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
resp, err = client.Get("http://198.51.100.42/reject")
|
||||
if err == nil {
|
||||
resp.Body.Close()
|
||||
if resp.StatusCode < 400 {
|
||||
t.Fatal("REJECT fell back to a different node")
|
||||
}
|
||||
}
|
||||
get(direct.URL, "direct-lan")
|
||||
}
|
||||
Reference in new issue
Block a user