|
|
|
@ -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) |
|
|
|
|