嗯很好抓到了 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
}
}
这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。
V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。
V2EX is a community of developers, designers and creative people.