From 0376a5f39dddaf38df2755f293cdd103d1dc2c29 Mon Sep 17 00:00:00 2001 From: huyinsong Date: Sat, 31 May 2025 10:22:44 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BF=AE=E5=A4=8D=E5=AE=A2=E6=88=B7=E7=AB=AF?= =?UTF-8?q?=E6=A0=B8=E5=BF=83=E9=97=AE=E9=A2=98:=20HTTP=E4=BB=A3=E7=90=86?= =?UTF-8?q?=E8=B7=AF=E7=94=B1=E9=80=BB=E8=BE=91=E3=80=81SOCKS5=E7=BB=9F?= =?UTF-8?q?=E8=AE=A1=E5=8A=9F=E8=83=BD=E3=80=81CONNECT=E8=AF=B7=E6=B1=82?= =?UTF-8?q?=E5=A4=84=E7=90=86=E4=BC=98=E5=8C=96=EF=BC=8C=E6=B5=8F=E8=A7=88?= =?UTF-8?q?=E5=99=A8=E8=AE=BF=E9=97=AE=E5=92=8C=E7=BB=9F=E8=AE=A1=E7=9B=91?= =?UTF-8?q?=E6=8E=A7=E7=8E=B0=E5=B7=B2=E6=AD=A3=E5=B8=B8=E5=B7=A5=E4=BD=9C?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- configs/client.yaml | 63 +++++- internal/client/client.go | 417 +++++++++++++++++++++++++++++++++++++- internal/proxy/socks5.go | 25 +++ 3 files changed, 499 insertions(+), 6 deletions(-) diff --git a/configs/client.yaml b/configs/client.yaml index 226cb15..ca5140f 100644 --- a/configs/client.yaml +++ b/configs/client.yaml @@ -3,15 +3,15 @@ serviceType: client # SOCKS5 服务器设置 server: - address: 127.0.0.1 + address: 3.133.130.202 port: 1080 username: admin - password: secure_password_123 + password: secure123 # 代理模式设置 proxy: mode: global # http, global, transparent - localPort: 8080 + localPort: 9090 # 全局代理设置 globalProxy: @@ -60,62 +60,117 @@ globalProxy: # 强制代理域名列表 (必须经过代理) forceDomains: # Google 服务 + - "google.com" - "*.google.com" + - "googlepai.com" - "*.googlepai.com" + - "googleapis.com" - "*.googleapis.com" + - "googleusercontent.com" - "*.googleusercontent.com" + - "googlevideo.com" - "*.googlevideo.com" + - "gstatic.com" - "*.gstatic.com" + - "gmail.com" - "*.gmail.com" + - "youtube.com" - "*.youtube.com" + - "youtu.be" - "*.youtu.be" + - "ytimg.com" - "*.ytimg.com" # 社交媒体 + - "facebook.com" - "*.facebook.com" + - "fbcdn.net" - "*.fbcdn.net" + - "instagram.com" - "*.instagram.com" + - "twitter.com" - "*.twitter.com" + - "twimg.com" - "*.twimg.com" + - "t.co" - "*.t.co" + - "linkedin.com" - "*.linkedin.com" + - "pinterest.com" - "*.pinterest.com" + - "reddit.com" - "*.reddit.com" + - "snapchat.com" - "*.snapchat.com" + - "discord.com" - "*.discord.com" + - "telegram.org" - "*.telegram.org" + - "whatsapp.com" - "*.whatsapp.com" # 技术开发 + - "github.com" - "*.github.com" + - "githubusercontent.com" - "*.githubusercontent.com" + - "github.io" - "*.github.io" + - "stackoverflow.com" - "*.stackoverflow.com" + - "stackexchange.com" - "*.stackexchange.com" + - "medium.com" - "*.medium.com" + - "dev.to" - "*.dev.to" + - "npmjs.com" - "*.npmjs.com" + - "pypi.org" - "*.pypi.org" + - "docker.com" - "*.docker.com" + - "hub.docker.com" - "*.hub.docker.com" + - "docker.io" + - "*.docker.io" + - "registry-1.docker.io" + - "auth.docker.io" + - "registry.docker.io" + - "index.docker.io" # 新闻媒体 + - "nytimes.com" - "*.nytimes.com" + - "washingtonpost.com" - "*.washingtonpost.com" + - "wsj.com" - "*.wsj.com" + - "reuters.com" - "*.reuters.com" + - "bbc.com" - "*.bbc.com" + - "cnn.com" - "*.cnn.com" + - "bloomberg.com" - "*.bloomberg.com" # 其他服务 + - "dropbox.com" - "*.dropbox.com" + - "onedrive.com" - "*.onedrive.com" + - "zoom.us" - "*.zoom.us" + - "spotify.com" - "*.spotify.com" + - "netflix.com" - "*.netflix.com" + - "hulu.com" - "*.hulu.com" + - "twitch.tv" - "*.twitch.tv" + - "steam.community" - "*.steam.community" # 透明代理设置 (实验性功能) @@ -197,7 +252,7 @@ timeout: 30s # Web管理界面 (可选) webUI: - enabled: false + enabled: true port: 8081 username: "admin" password: "wormhole123" diff --git a/internal/client/client.go b/internal/client/client.go index b4c4508..abb8f00 100644 --- a/internal/client/client.go +++ b/internal/client/client.go @@ -1,8 +1,12 @@ package client import ( + "bufio" "context" + "encoding/json" "fmt" + "io" + "net" "net/http" "os" "os/signal" @@ -10,6 +14,7 @@ import ( "time" "github.com/azoic/wormhole-client/internal/config" + "github.com/azoic/wormhole-client/internal/gui" "github.com/azoic/wormhole-client/internal/proxy" "github.com/azoic/wormhole-client/internal/routing" "github.com/azoic/wormhole-client/internal/system" @@ -24,6 +29,7 @@ type Client struct { dnsProxy *dns.DNSProxy systemProxyMgr *system.SystemProxyManager routeMatcher *routing.RouteMatcher + guiServer *gui.GUIServer httpServer *http.Server ctx context.Context cancel context.CancelFunc @@ -78,6 +84,9 @@ func (c *Client) Start(configPath string) error { } } + // 创建 GUI 服务器 + c.guiServer = gui.NewGUIServer(cfg, c.socks5Proxy, c.systemProxyMgr, c.routeMatcher, c.mode) + // 设置信号处理 c.setupSignalHandler() @@ -97,12 +106,13 @@ func (c *Client) startHTTPProxy() error { logger.Info("🌐 Starting HTTP proxy mode...") logger.Info("💡 Setting up HTTP proxy on port %d", c.config.Proxy.LocalPort) - c.httpServer = c.socks5Proxy.CreateHTTPProxy(c.config.Proxy.LocalPort) + c.httpServer = c.createHTTPServerWithGUI(c.config.Proxy.LocalPort) logger.Info("✅ HTTP proxy started on :%d", c.config.Proxy.LocalPort) logger.Info("💡 Configure your browser to use HTTP proxy: 127.0.0.1:%d", c.config.Proxy.LocalPort) logger.Info("📊 Statistics available at: http://127.0.0.1:%d/stats", c.config.Proxy.LocalPort) logger.Info("💚 Health check available at: http://127.0.0.1:%d/health", c.config.Proxy.LocalPort) + logger.Info("🖥️ Web GUI available at: http://127.0.0.1:%d/gui", c.config.Proxy.LocalPort) return c.httpServer.ListenAndServe() } @@ -113,7 +123,7 @@ func (c *Client) startGlobalProxy() error { logger.Info("⚠️ Requires administrator privileges") // 启动HTTP代理服务器 - c.httpServer = c.socks5Proxy.CreateHTTPProxy(c.config.Proxy.LocalPort) + c.httpServer = c.createHTTPServerWithGUI(c.config.Proxy.LocalPort) errChan := make(chan error, 1) go func() { @@ -171,6 +181,7 @@ func (c *Client) startGlobalProxy() error { } logger.Info("📊 Statistics: http://%s/stats", httpProxy) logger.Info("💚 Health check: http://%s/health", httpProxy) + logger.Info("🖥️ Web GUI: http://%s/gui", httpProxy) logger.Info("🛑 Press Ctrl+C to stop") // 检查是否有错误 @@ -196,6 +207,408 @@ func (c *Client) startTransparentProxy() error { return fmt.Errorf("transparent proxy mode not implemented") } +// createHTTPServerWithGUI 创建带有 GUI 的 HTTP 服务器 +func (c *Client) createHTTPServerWithGUI(localPort int) *http.Server { + // 创建ServeMux来处理不同的路径 + mux := http.NewServeMux() + + // 代理处理器 + proxyHandler := &httpProxyHandler{ + socks5Proxy: c.socks5Proxy, + routeMatcher: c.routeMatcher, + } + + // API 路由 + mux.HandleFunc("/stats", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + stats := c.socks5Proxy.GetStats() + w.Header().Set("Content-Type", "application/json") + w.Header().Set("Access-Control-Allow-Origin", "*") + + if err := json.NewEncoder(w).Encode(stats); err != nil { + logger.Error("Failed to encode stats: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + }) + + mux.HandleFunc("/health", func(w http.ResponseWriter, r *http.Request) { + if r.Method != http.MethodGet { + http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) + return + } + + stats := c.socks5Proxy.GetStats() + health := map[string]interface{}{ + "status": "healthy", + "uptime": stats.Uptime.String(), + "active_connections": stats.ActiveConnections, + "success_rate": stats.GetSuccessRate(), + } + + w.Header().Set("Content-Type", "application/json") + if err := json.NewEncoder(w).Encode(health); err != nil { + logger.Error("Failed to encode health: %v", err) + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + }) + + // GUI 路由 + if c.guiServer != nil { + c.guiServer.RegisterRoutes(mux) + } + + // 创建一个包装器来处理CONNECT请求 + handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + // 对于CONNECT请求,直接使用代理处理器 + if r.Method == http.MethodConnect { + proxyHandler.ServeHTTP(w, r) + return + } + + // 对于其他请求,使用ServeMux + mux.ServeHTTP(w, r) + }) + + server := &http.Server{ + Addr: fmt.Sprintf(":%d", localPort), + Handler: handler, + ReadTimeout: 30 * time.Second, + WriteTimeout: 30 * time.Second, + IdleTimeout: 120 * time.Second, + MaxHeaderBytes: 1 << 20, // 1MB + } + + return server +} + +// httpProxyHandler HTTP代理处理器 +type httpProxyHandler struct { + socks5Proxy *proxy.SOCKS5Proxy + routeMatcher *routing.RouteMatcher +} + +func (h *httpProxyHandler) ServeHTTP(w http.ResponseWriter, r *http.Request) { + // 添加调试日志 + logger.Info("Received request: Method=%s, URL.Path=%s, Host=%s", r.Method, r.URL.Path, r.Host) + + // 只对非CONNECT请求检查API和GUI路径 + if r.Method != http.MethodConnect && isAPIOrGUIPath(r.URL.Path) { + logger.Info("Request blocked by isAPIOrGUIPath: %s", r.URL.Path) + http.NotFound(w, r) + return + } + + // 统计连接 - 增加连接计数 + h.socks5Proxy.IncrementConnections() + + // 使用defer确保在请求结束时减少活跃连接数 + defer h.socks5Proxy.DecrementActiveConnections() + + // 调用代理处理逻辑 + h.handleProxyRequest(w, r) +} + +// handleProxyRequest 处理代理请求 +func (h *httpProxyHandler) handleProxyRequest(w http.ResponseWriter, r *http.Request) { + if r.Method == http.MethodConnect { + h.handleHTTPSProxy(w, r) + } else { + h.handleHTTPProxy(w, r) + } +} + +// handleHTTPSProxy 处理HTTPS代理请求 (CONNECT方法) +func (h *httpProxyHandler) handleHTTPSProxy(w http.ResponseWriter, r *http.Request) { + // 解析目标主机 + host, port, err := net.SplitHostPort(r.Host) + if err != nil { + // 如果没有端口,默认使用443 + host = r.Host + port = "443" + } + + // 使用路由匹配器判断是否需要代理 + if h.routeMatcher != nil { + matchResult := h.routeMatcher.Match(host) + + switch matchResult { + case routing.MatchBypass: + // 直连 + logger.Debug("HTTPS %s: Direct connection (bypass)", host) + h.handleDirectHTTPSProxy(w, r, host, port) + return + case routing.MatchProxy: + // 强制代理 + logger.Debug("HTTPS %s: Using SOCKS5 proxy (force)", host) + case routing.MatchAuto: + // 自动决定,默认使用代理 + logger.Debug("HTTPS %s: Using SOCKS5 proxy (auto)", host) + } + } + + // 通过SOCKS5代理连接 + targetHost := net.JoinHostPort(host, port) + destConn, err := h.socks5Proxy.DialTCP(targetHost) + if err != nil { + logger.Error("Failed to connect via SOCKS5 to %s: %v", targetHost, err) + h.socks5Proxy.IncrementFailedRequests() + http.Error(w, "Bad Gateway", http.StatusBadGateway) + return + } + defer destConn.Close() + + // 发送200 Connection established响应 + w.WriteHeader(http.StatusOK) + + hijacker, ok := w.(http.Hijacker) + if !ok { + logger.Error("Hijacking not supported") + h.socks5Proxy.IncrementFailedRequests() + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + clientConn, _, err := hijacker.Hijack() + if err != nil { + logger.Error("Failed to hijack connection: %v", err) + h.socks5Proxy.IncrementFailedRequests() + return + } + defer clientConn.Close() + + // 记录成功建立连接 + h.socks5Proxy.IncrementSuccessfulRequests() + + // 双向数据转发 + go func() { + defer destConn.Close() + defer clientConn.Close() + written, err := io.Copy(destConn, clientConn) + if err == nil && written > 0 { + h.socks5Proxy.AddBytesTransferred(written, 0) + } + }() + + written, err := io.Copy(clientConn, destConn) + if err == nil && written > 0 { + h.socks5Proxy.AddBytesTransferred(0, written) + } +} + +// handleDirectHTTPSProxy 直接连接处理HTTPS代理 +func (h *httpProxyHandler) handleDirectHTTPSProxy(w http.ResponseWriter, r *http.Request, host, port string) { + // 直接连接到目标服务器 + targetAddr := net.JoinHostPort(host, port) + destConn, err := net.DialTimeout("tcp", targetAddr, 10*time.Second) + if err != nil { + logger.Error("Failed to connect directly to %s: %v", targetAddr, err) + h.socks5Proxy.IncrementFailedRequests() + http.Error(w, "Bad Gateway", http.StatusBadGateway) + return + } + defer destConn.Close() + + // 发送200 Connection established响应 + w.WriteHeader(http.StatusOK) + + hijacker, ok := w.(http.Hijacker) + if !ok { + logger.Error("Hijacking not supported") + h.socks5Proxy.IncrementFailedRequests() + http.Error(w, "Internal Server Error", http.StatusInternalServerError) + return + } + + clientConn, _, err := hijacker.Hijack() + if err != nil { + logger.Error("Failed to hijack connection: %v", err) + h.socks5Proxy.IncrementFailedRequests() + return + } + defer clientConn.Close() + + // 记录成功建立连接 + h.socks5Proxy.IncrementSuccessfulRequests() + + // 双向数据转发 + go func() { + defer destConn.Close() + defer clientConn.Close() + written, err := io.Copy(destConn, clientConn) + if err == nil && written > 0 { + h.socks5Proxy.AddBytesTransferred(written, 0) + } + }() + + written, err := io.Copy(clientConn, destConn) + if err == nil && written > 0 { + h.socks5Proxy.AddBytesTransferred(0, written) + } +} + +// handleHTTPProxy 处理HTTP代理请求 +func (h *httpProxyHandler) handleHTTPProxy(w http.ResponseWriter, r *http.Request) { + // 确保URL包含Host + if r.URL.Host == "" { + r.URL.Host = r.Host + } + if r.URL.Scheme == "" { + r.URL.Scheme = "http" + } + + // 解析目标主机 + host, port, err := net.SplitHostPort(r.Host) + if err != nil { + // 如果没有端口,根据协议添加默认端口 + host = r.Host + if r.URL.Scheme == "https" { + port = "443" + } else { + port = "80" + } + } + + // 使用路由匹配器判断是否需要代理 + if h.routeMatcher != nil { + matchResult := h.routeMatcher.Match(host) + + switch matchResult { + case routing.MatchBypass: + // 直连 + logger.Debug("HTTP %s: Direct connection (bypass)", host) + h.handleDirectHTTPProxy(w, r) + return + case routing.MatchProxy: + // 强制代理 + logger.Debug("HTTP %s: Using SOCKS5 proxy (force)", host) + case routing.MatchAuto: + // 自动决定,默认使用代理 + logger.Debug("HTTP %s: Using SOCKS5 proxy (auto)", host) + } + } + + // 通过SOCKS5代理连接 + targetHost := net.JoinHostPort(host, port) + destConn, err := h.socks5Proxy.DialTCP(targetHost) + if err != nil { + logger.Error("Failed to connect via SOCKS5 to %s: %v", targetHost, err) + h.socks5Proxy.IncrementFailedRequests() + http.Error(w, "Bad Gateway", http.StatusBadGateway) + return + } + defer destConn.Close() + + // 发送HTTP请求 + if err := r.Write(destConn); err != nil { + logger.Error("Failed to write request to SOCKS5 connection: %v", err) + h.socks5Proxy.IncrementFailedRequests() + http.Error(w, "Bad Gateway", http.StatusBadGateway) + return + } + + // 读取并转发响应 + resp, err := http.ReadResponse(bufio.NewReader(destConn), r) + if err != nil { + logger.Error("Failed to read response from SOCKS5 connection: %v", err) + h.socks5Proxy.IncrementFailedRequests() + http.Error(w, "Bad Gateway", http.StatusBadGateway) + return + } + defer resp.Body.Close() + + // 记录成功请求 + h.socks5Proxy.IncrementSuccessfulRequests() + + // 复制响应头 + for key, values := range resp.Header { + for _, value := range values { + w.Header().Add(key, value) + } + } + + // 设置状态码 + w.WriteHeader(resp.StatusCode) + + // 复制响应体并统计字节数 + written, err := io.Copy(w, resp.Body) + if err == nil && written > 0 { + h.socks5Proxy.AddBytesTransferred(0, written) + } +} + +// handleDirectHTTPProxy 直接连接处理HTTP代理 +func (h *httpProxyHandler) handleDirectHTTPProxy(w http.ResponseWriter, r *http.Request) { + // 创建直接连接的HTTP客户端 + client := &http.Client{ + Timeout: 30 * time.Second, + Transport: &http.Transport{ + DialContext: (&net.Dialer{ + Timeout: 10 * time.Second, + KeepAlive: 30 * time.Second, + }).DialContext, + }, + } + + // 创建新的请求 + newReq, err := http.NewRequest(r.Method, r.URL.String(), r.Body) + if err != nil { + logger.Error("Failed to create direct request: %v", err) + http.Error(w, "Bad Request", http.StatusBadRequest) + return + } + + // 复制请求头 + for key, values := range r.Header { + for _, value := range values { + newReq.Header.Add(key, value) + } + } + + // 发送请求 + resp, err := client.Do(newReq) + if err != nil { + logger.Error("Failed to make direct request: %v", err) + http.Error(w, "Bad Gateway", http.StatusBadGateway) + return + } + defer resp.Body.Close() + + // 复制响应头 + for key, values := range resp.Header { + for _, value := range values { + w.Header().Add(key, value) + } + } + + // 设置状态码 + w.WriteHeader(resp.StatusCode) + + // 复制响应体 + if _, err := io.Copy(w, resp.Body); err != nil { + logger.Error("Failed to copy response body: %v", err) + return + } +} + +// isAPIOrGUIPath 检查是否为API或GUI路径 +func isAPIOrGUIPath(path string) bool { + apiPaths := []string{"/stats", "/health", "/api/", "/gui", "/static/"} + + for _, apiPath := range apiPaths { + if path == apiPath || (apiPath[len(apiPath)-1] == '/' && len(path) > len(apiPath) && path[:len(apiPath)] == apiPath) { + return true + } + } + + return false +} + func (c *Client) setupSignalHandler() { sigChan := make(chan os.Signal, 1) signal.Notify(sigChan, os.Interrupt, syscall.SIGTERM) diff --git a/internal/proxy/socks5.go b/internal/proxy/socks5.go index e8a5d41..7989914 100644 --- a/internal/proxy/socks5.go +++ b/internal/proxy/socks5.go @@ -278,6 +278,31 @@ func (p *SOCKS5Proxy) GetStats() ProxyStatsSnapshot { return p.stats.GetStats() } +// IncrementConnections 增加连接计数 +func (p *SOCKS5Proxy) IncrementConnections() { + p.stats.IncrementConnections() +} + +// DecrementActiveConnections 减少活跃连接计数 +func (p *SOCKS5Proxy) DecrementActiveConnections() { + p.stats.DecrementActiveConnections() +} + +// IncrementSuccessfulRequests 增加成功请求计数 +func (p *SOCKS5Proxy) IncrementSuccessfulRequests() { + p.stats.IncrementSuccessfulRequests() +} + +// IncrementFailedRequests 增加失败请求计数 +func (p *SOCKS5Proxy) IncrementFailedRequests() { + p.stats.IncrementFailedRequests() +} + +// AddBytesTransferred 添加传输字节数 +func (p *SOCKS5Proxy) AddBytesTransferred(sent, received int64) { + p.stats.AddBytesTransferred(sent, received) +} + // DialTCP 通过SOCKS5连接到目标地址 func (p *SOCKS5Proxy) DialTCP(address string) (net.Conn, error) { return p.DialTCPWithContext(context.Background(), address)