收起左侧

统一网关版code-server

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

实现

为了实现在飞牛上敲写代码,我将docker容器加上了iframe加载,然后变成了用systemctl来挂载code-server服务,最后想用飞牛统一网关,但是code-server默认运行在/根目录,而飞牛的统一网关要加/app/xxx的前缀,解决办法就是加中间层,最开始用的是node.js写了个中间层,结果是websocket连接不上,后面用go来手写很意外的成功了,然后后想手写优化,优化了一下就出来之前的问题,如下图所示:

image.png

后面,反复修改终于确认了问题根源,之前的nodejs版本就是这个问题解决不了,在代理给 Unix Socket 时,请求头里的 Host 可能变成了 unix 或者与 Origin 的协议不匹配。code-server 看到 Origin 和 Host 不一致,触发了安全防御机制,直接掐断了连接(导致 1006 错误)。而手动拼接字符串粗糙代码,手动复制了 Sec-WebSocket-* 和 Cookie,完全漏掉了浏览器的 Origin 请求头,code-server 发现没有 Origin 头,以为是某种非浏览器的本地客户端发起的请求,直接跳过了安全校验,所以放行了就连接成功了,所以修复起来就非常简单:要么让 Host 和 Origin 完美匹配,要么直接把 Origin 头删掉来绕过校验,这下终于code-server用上了统一网关了,fpk下载地址:https://github.com/ctllo-bit/coder

go中间层

这个golang转发层是最核心的,最初以为只要改写路径就可以了,结果是websocket连不上,才知道websocket也要转发,转发了还是1006 的错误,才又知道有个CORS的跨域与主机头安全校验的问题,最终都解决了,就写出了下面的http/websocket统一代理层,代码如下所示:

package main

import (
	"context"
	"flag"
	"log"
	"net"
	"net/http"
	"net/http/httputil"
	"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()

	// 不再使用 httputil.NewSingleHostReverseProxy,直接初始化 ReverseProxy 结构体
	backend := &httputil.ReverseProxy{
		// ====== 使用 Rewrite 替代 Director ======
		Rewrite: func(pr *httputil.ProxyRequest) {
			pr.SetXForwarded() // 自动处理 X-Forwarded-* 头

			// 处理路径前缀剥离
			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
			// 将外部真实的 Host 传递给后端
			pr.Out.Host = pr.In.Host
			// 删除 Origin 头,绕过 code-server 严格的同源检测
			pr.Out.Header.Del("Origin")
		},
		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,
		},
		ModifyResponse: func(r *http.Response) error {
			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)
			if strings.Contains(err.Error(), "connection refused") || strings.Contains(err.Error(), "no such file") {
				http.Error(w, "code-server is not running or the socket does not exist", http.StatusServiceUnavailable)
			} else {
				http.Error(w, "Bad Gateway", http.StatusBadGateway)
			}
		},
	}

	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)
	}
	if err := os.Chmod(*proxySocket, 0666); err != nil {
		log.Printf("warning: failed to set socket permissions: %v", err)
	}

	// 优雅停机逻辑
	server := &http.Server{
		Handler: backend, // 直接将 backend 作为 Server 的 Handler
	}

	go func() {
		sigCh := make(chan os.Signal, 1)
		signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
		<-sigCh
		log.Println("shutting down proxy...")

		// 给活动连接 5 秒钟时间完成关闭
		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)

	// 使用 server.Serve 替代 http.Serve
	if err := server.Serve(listener); err != nil && err != http.ErrServerClosed {
		log.Fatalf("server error: %v", err)
	}
	os.RemoveAll(*proxySocket)
}

go程序编译:

GOOS=linux GOARCH=amd64 go build -o coder-proxy-linux-amd64 main.go

GOOS=linux GOARCH=arm64 go build -o coder-proxy-linux-arm64 main.go
收藏
送赞
分享

本帖子中包含更多资源

您需要 登录 才可以下载或查看,没有账号?立即注册

x
您需要登录后才可以回帖 登录 | 立即注册

本版积分规则