嗯很好抓到了 V2EX 的一个 404 的 bug ,把 webui 配置一个域名,然后往工作群一发,前端和 app 自己玩去。
package main import ( "bufio" "bytes" "crypto/tls" "fmt" "io" "log" "net" "net/http" "strings" ) var whistleAddr = "127.0.0.1:8899" // Whistle 监听的代理端口 func main() { http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { targetHost := r.Header.Get("X-Target-Host") if targetHost == "" { http.Error(w, "Missing X-Target-Host header", http.StatusBadRequest) return } if strings.HasSuffix(targetHost, ":443") { handleTLSProxy(w, r, targetHost) return } if strings.ToLower(r.Header.Get("Upgrade")) == "websocket" { // ws:// 直接 HTTP 请求升级 handleWebSocketProxy(w, r, targetHost) return } // 普通 HTTP 请求 handleHTTP(w, r, targetHost) }) log.Println("Proxy wrapper (for Whistle) listening on :8082") log.Fatal( http.ListenAndServe(":8082", nil)) } func handleHTTP(w http.ResponseWriter, r *http.Request, targetHost string) { // 建立到 whistle 的连接 fmt.Println("Handling HTTP request for target:", targetHost) conn, err := net.Dial("tcp", whistleAddr) if err != nil { http.Error(w, "Failed to connect to whistle: "+err.Error(), http.StatusBadGateway) return } defer conn.Close() // 构造代理协议的完整 URL fullURL := fmt.Sprintf("http://%s%s", targetHost, r.URL.RequestURI()) // 创建新的请求 req, err := http.NewRequest(r.Method, fullURL, r.Body) if err != nil { http.Error(w, "Failed to create proxy request: "+err.Error(), http.StatusInternalServerError) return } req.Header = r.Header.Clone() // 写入代理请求 if err := req.Write(conn); err != nil { http.Error(w, "Failed to write proxy request to whistle: "+err.Error(), http.StatusBadGateway) return } // 从 whistle 读取响应 resp, err := http.ReadResponse(bufio.NewReader(conn), req) if err != nil { http.Error(w, "Failed to read response from whistle: "+err.Error(), http.StatusBadGateway) return } defer resp.Body.Close() // 转发响应头和状态码 for k, v := range resp.Header { for _, vv := range v { w.Header().Add(k, vv) } } w.WriteHeader(resp.StatusCode) // 转发响应体 io.Copy(w, resp.Body) } func handleWebSocketProxy(w http.ResponseWriter, r *http.Request, targetHost string) { hj, ok := w.( http.Hijacker) if !ok { http.Error(w, "Hijack not supported", http.StatusInternalServerError) return } clientConn, _, err := hj.Hijack() if err != nil { http.Error(w, "Hijack failed: "+err.Error(), http.StatusInternalServerError) return } backendConn, err := net.Dial("tcp", whistleAddr) if err != nil { clientConn.Close() return } // 构造完整的 websocket 请求(代理格式) var buf bytes.Buffer fmt.Fprintf(&buf, "GET http://%s%s HTTP/1.1\r\n", targetHost, r.URL.RequestURI()) fmt.Fprintf(&buf, "Host: %s\r\n", targetHost) fmt.Fprintf(&buf, "Connection: Upgrade\r\n") fmt.Fprintf(&buf, "Upgrade: websocket\r\n") for key, values := range r.Header { if strings.EqualFold(key, "Host") || strings.EqualFold(key, "Connection") || strings.EqualFold(key, "Upgrade") { continue } for _, v := range values { fmt.Fprintf(&buf, "%s: %s\r\n", key, v) } } buf.WriteString("\r\n") // 直接转发请求内容 _, err = backendConn.Write(buf.Bytes()) if err != nil { clientConn.Close() backendConn.Close() return } // 直接把返回内容透传回客户端(不要使用 http.ReadResponse 解析,否则后续帧会丢) go io.Copy(backendConn, clientConn) io.Copy(clientConn, backendConn) } func handleTLSProxy(w http.ResponseWriter, r *http.Request, targetHost string) { hj, ok := w.( http.Hijacker) if !ok { http.Error(w, "Hijacking not supported", http.StatusInternalServerError) return } clientConn, _, err := hj.Hijack() if err != nil { http.Error(w, "Hijack failed: "+err.Error(), http.StatusInternalServerError) return } // 中间件连接 whistle ,发送 CONNECT 建立隧道 backendConn, err := net.Dial("tcp", whistleAddr) if err != nil { clientConn.Close() return } // 发起 CONNECT 请求 connectReq := fmt.Sprintf("CONNECT %s HTTP/1.1\r\nHost: %s\r\n\r\n", targetHost, targetHost) _, err = backendConn.Write([]byte(connectReq)) if err != nil { http.Error(w, "backendConn CONNECT failed: "+err.Error(), http.StatusInternalServerError) clientConn.Close() backendConn.Close() return } // 读取 CONNECT 响应 resp, err := http.ReadResponse(bufio.NewReader(backendConn), r) if err != nil || resp.StatusCode != 200 { clientConn.Write([]byte("HTTP/1.1 502 Bad Gateway\r\n\r\n")) clientConn.Close() backendConn.Close() return } // 2. TLS 握手 tlsConn := tls.Client(backendConn, &tls.Config{ ServerName: strings.Split(targetHost, ":")[0], // SNI 用于证书校验 } if err := tlsConn.Handshake(); err != nil { clientConn.Close() backendConn.Close() return } // 构造完整的 websocket 请求(代理格式) var buf bytes.Buffer fmt.Fprintf(&buf, "%s %s HTTP/1.1\r\n", r.Method, r.URL.RequestURI()) fmt.Fprintf(&buf, "Host: %s\r\n", targetHost) for key, values := range r.Header { if strings.EqualFold(key, "Host") { continue } for _, v := range values { fmt.Fprintf(&buf, "%s: %s\r\n", key, v) } } buf.WriteString("\r\n") // 直接转发请求内容 _, err = tlsConn.Write(buf.Bytes()) if err != nil { clientConn.Close() tlsConn.Close() return } // 3. 开始转发数据( WebSocket over TLS ) go io.Copy(tlsConn, clientConn) io.Copy(clientConn, tlsConn) }
交叉编译命令:GOOS=linux GOARCH=amd64 go build -o go_middle main.go
编译命令需要根据自己服务器进行调整我这里是 centos7 注意:go_middle 和 Whistle 要配置在一台机器上,如果需要配置到不同的服务器请修改代码中的whistleAddr
启动命令: w2 start
自己有域名可以这样配置: w2 start -H 0.0.0.0 -l w2.xxx.com
# go_middle 的地址 upstream go_middle{server 192.168.31.148:8082} server{ server_name wchat.dsmai.com localtion / { proxy_pass http://go_middle # 内网真实服务器的地址 proxy_set_header X-Target-Host 192.168.31.1:80 } }
![]() | 1 NouveauNom 114 天前 一直开着内存会不会占用很高 |
2 GenServer OP @NouveauNom 测试服基本没问题,因为只是针对某些具体域名。反正挺好用的也不用他们自己安装证书配置代理 |
![]() | 3 lululau 114 天前 通常来说 Nginx 和后端之间是 Plain HTTP ,所以这个事情完全可以通过 tcpdump | wireshark 来完成: ssh my.server "tcpdump -i any -w - -U port 18080" | wireshark -k -i - |
5 relife 114 天前 挺好 |
6 timzaak 113 天前 写了个类似效果,不过是走 mitm 路数。https://github.com/timzaak/log-http-proxy |
7 lizhien 106 天前 可以直接 gost |