收起左侧

论飞牛统一网关转发层,解决code-server中WebSocket连接失败问题

0
回复
22
查看
[ 复制链接 ]

前言

为了飞牛上的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)
	}
}

完结。。

收藏
送赞
分享
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则