前言
为了飞牛上的code-server也能用上统一网关,我真的是想尽了办法,各种的尝试还学会http请求,现在终于是有一点点明悟,现在以如何用飞牛统一网关如何用go来代理一个简单的html为示例,采用的是端口输出,只保留核心逻辑:
package main
import (
"log"
"net"
"net/http"
"os"
)
const (
rootDir = "/var/apps/helloworld/target/www"
socketPath = "/var/apps/helloworld/target/app.sock"
gatewayPrefix = "/app/helloworld"
)
func main() {
// 1. 清理旧 socket
os.Remove(socketPath)
// 2. 监听 Unix Socket
listener, err := net.Listen("unix", socketPath)
if err != nil {
log.Fatalf("监听失败: %v", err)
}
defer listener.Close()
// 3. 构造静态文件处理器:剥掉前缀,再交给文件服务器
handler := http.StripPrefix(gatewayPrefix, http.FileServer(http.Dir(rootDir)))
// 4. 启动服务(阻塞)
log.Fatal(http.Serve(listener, handler))
}
可以看出,即就是请求一个html也要去除前缀,不然后它就是找不到报404错误,其实跟cgi方案也差不多,它同样也是要进行路径前缀分离的,刚开始不明白就天天加载不出来。
过程
后面才是麻烦,就是code-server一直卡在了状态码1006,一直就是说websocket连接不上,那是什么导致websocket连接不上的原因,没有找到,现在我找到了它的根本原因了,那就code-server的Host/Origin 校验,这是一个安全校验机制,坑也就是坑在这里,只要它们两个不一致就直接状态码1006,而反复调查这个Origin是正常的,它输出的是http://192.168.xx.x:5666及https://192.168.xx.x:5667,而这个Host就坑了,它输出结果没有带端口号,可能被统一网关给吞了,一个多了个端口及一个没有端口,就对不上了就连接不上了原因就是这么简单。
其中,X-Forwarded-Proto与X-Forwarded-Host是code-server必需的,X-Forwarded-For是可选用于记录原始客户端的 IP 地址,最重要的是**X-Forwarded-Host**这个请求头,用于传递客户端请求的原始域名和端口,它没有了端口,导致Host也没有端口就对不上了,而X-Forwarded-Proto这个请求头,用于告知 code-server 原始请求是 HTTP 还是 HTTPS,如果缺失或配置错误,可能导致语法高亮失效,go函数来处理pr.SetxForwarded()这些请求头,传入都没有端口输出的X-Forwarded-Host也就没有端口,所以重点来了,解决办法有两种:第一种就是将Origin的端口给删除了,第二种将Origin设置给X-Forwarded-Host请求头,下面是参考案例:
解决code-server中WebSocket连接关闭问题(状态码1006)
解决code-server在Nginx反向代理后语法高亮失效的问题
第一种,代码如下:
backend := &httputil.ReverseProxy{
Rewrite: func(pr *httputil.ProxyRequest) {
// 处理路径前缀剥离
path := pr.In.URL.Path
if strings.HasPrefix(path, *prefix) {
path = strings.TrimPrefix(path, *prefix)
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
}
// 设置目标 Scheme 和 Host
pr.Out.URL.Scheme = "http"
pr.Out.URL.Host = "unix"
pr.Out.URL.Path = path
// 自动处理 X-Forwarded-* 头
pr.SetXForwarded()
// 当 Origin 不为空时:触发了 WebSocket 握手或 API 提交
if origin := pr.In.Header.Get("Origin"); origin != "" {
if u, err := url.Parse(origin); err == nil && u.Host != "" {
u.Host = pr.In.Host
pr.Out.Header.Set("Origin", u.String())
}
}
},
第二种,代码如下:
backend := &httputil.ReverseProxy{
Rewrite: func(pr *httputil.ProxyRequest) {
// 处理路径前缀剥离
path := pr.In.URL.Path
if strings.HasPrefix(path, *prefix) {
path = strings.TrimPrefix(path, *prefix)
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
}
// 设置目标 Scheme 和 Host
pr.Out.URL.Scheme = "http"
pr.Out.URL.Host = "unix"
pr.Out.URL.Path = path
// 自动处理 X-Forwarded-* 头
pr.SetXForwarded()
// 当 Origin 不为空时:触发了 WebSocket 握手或 API 提交
if origin := pr.In.Header.Get("Origin"); origin != "" {
u, err := url.Parse(origin)
if err == nil {
pr.Out.Header.Set("X-Forwarded-Host", u.Host)
}
}
},
解决
到目前为止,学习了http请求的东西,结合上面的理论与实践终于是明白了它1006的原因,就是因为一个请求头的问题,对于小白来说太难了,不过也好现在code-srver也可以用上飞牛的统一网关了,最后给出完整代码供大家参考:
package main
import (
"context"
"errors"
"flag"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
var (
codeServerSocket = flag.String("socket", "/var/apps/coder/var/code-server.sock", "upstream code-server unix socket")
proxySocket = flag.String("proxy-socket", "/var/apps/coder/target/coder-proxy.sock", "this proxy's own unix socket")
prefix = flag.String("prefix", "/app/coder", "URL prefix to strip before forwarding")
)
func main() {
flag.Parse()
backend := &httputil.ReverseProxy{
Rewrite: func(pr *httputil.ProxyRequest) {
// 处理路径前缀剥离
path := pr.In.URL.Path
if strings.HasPrefix(path, *prefix) {
path = strings.TrimPrefix(path, *prefix)
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
}
// 设置目标 Scheme 和 Host
pr.Out.URL.Scheme = "http"
pr.Out.URL.Host = "unix"
pr.Out.URL.Path = path
// 自动处理 X-Forwarded-* 头
pr.SetXForwarded()
// 当 Origin 不为空时:触发了 WebSocket 握手或 API 提交
if origin := pr.In.Header.Get("Origin"); origin != "" {
u, err := url.Parse(origin)
if err == nil {
pr.Out.Header.Set("X-Forwarded-Host", u.Host)
}
}
},
Transport: &http.Transport{
DialContext: func(ctx context.Context, network, addr string) (net.Conn, error) {
var d net.Dialer
return d.DialContext(ctx, "unix", *codeServerSocket)
},
MaxIdleConns: 100,
IdleConnTimeout: 90 * time.Second,
ExpectContinueTimeout: 1 * time.Second, // 优化对 100-continue 响应的处理
},
ModifyResponse: func(r *http.Response) error {
// 修改重定向 Header 加上 Prefix
if loc := r.Header.Get("Location"); strings.HasPrefix(loc, "/") && !strings.HasPrefix(loc, *prefix) {
r.Header.Set("Location", *prefix+loc)
}
return nil
},
ErrorHandler: func(w http.ResponseWriter, r *http.Request, err error) {
log.Printf("proxy error: %s %s -> %v", r.Method, r.URL.Path, err)
// 使用 errors.Is 精确匹配底层系统错误,替代脆弱的字符串匹配
if errors.Is(err, syscall.ECONNREFUSED) || errors.Is(err, os.ErrNotExist) {
http.Error(w, "code-server is not running or the socket does not exist", http.StatusServiceUnavailable)
return
}
http.Error(w, "Bad Gateway", http.StatusBadGateway)
},
}
// 启动前清理残留旧 Socket
if err := os.RemoveAll(*proxySocket); err != nil {
log.Fatalf("failed to remove old proxy socket: %v", err)
}
listener, err := net.Listen("unix", *proxySocket)
if err != nil {
log.Fatalf("failed to listen on proxy socket %s: %v", *proxySocket, err)
}
// 利用 defer 确保退出或 panic 时都能安全清理 Socket
defer os.RemoveAll(*proxySocket)
if err := os.Chmod(*proxySocket, 0666); err != nil {
log.Printf("warning: failed to set socket permissions: %v", err)
}
server := &http.Server{
Handler: backend,
// 设置读取请求头的超时时间,防止 Slowloris 攻击。
// 注意:千万不要设置 ReadTimeout/WriteTimeout,否则会切断 code-server 的 WebSocket。
ReadHeaderTimeout: 5 * time.Second,
}
// 优雅停机逻辑
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("shutting down proxy...")
ctx, cancel := context.WithTimeout(context.Background(), 5*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Printf("proxy shutdown error: %v", err)
}
}()
log.Printf("coder proxy listening on %s", *proxySocket)
log.Printf("upstream: unix://%s prefix=%s", *codeServerSocket, *prefix)
if err := server.Serve(listener); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("server error: %v", err)
}
}
完结。。