前言
飞牛官方出了统一网关,那时候我就想将它用于code-server,这样就可以在飞牛上随时随地编写代码了,最开始是HyperPu_ter、Win11React的模仿,最开始用的是官方的docker-server,加一个自己写的html的ifame页面,后面自己写了一个docker应用,又解决iframe复制粘贴问题,其中最麻烦就是各种网络地址的问题,这个解决了好久,然后又是尝试写了一个原生应用,出现命令行权限不能用问题,用root可以但是权限太高,所以后面用systemctl来运行刚好,然后就遇到了是棘手的问题了,code-server用统一网关,就会报GET http://192.168.x.xxx:5666/app/coder 404(Not Found)的错误,我那是解决了三四天都没有解决,那时尝试用node.js写中间层,解决了一些问题但还是不能用,也就是文章:统一网关版Vsocde的Docker版本。
那时的我其实是已经放弃这方案了,code-serv默认认为自己运行在/根目录下,前面加了个app/coder它就是加载不出来的,用openvscode来代替的,但这个openvscode也不好用,还没docker的code-server好用,而当时想的是又不是不能用,为什么一定要用这统一网关呢,就很长时间没有想过去弄这个的,转机是移植了pvz的wasm版本,然后用了go语言来做存档服务端,然后就有了灵感,为什么不用go来做中间层,事实证明这行的通,而且很好用。
开发
飞牛的统一网关,就是用Unix Socket文件给到nginx,而code-serv默认认为自己运行在/根目录下,加一个中间层相当于加一个翻译,其中的核心职责只有两件事:
1、路径转换(/app/coder/* ↔ /*)
2、Unix Socket 转发(浏览器请求 → code-server)以及 WebSocket 的代理。
设计实际上是一个两层代理:
Browser(浏览器)
**
Nginx
**
coder-proxy.sock (-proxy-socket)
**
coder-proxy
**
code-server.sock (-socket)
**
code-server
整体设计是清晰且合理的。
用到的go代码:
package main
import (
"bufio"
"context"
"flag"
"fmt"
"io"
"log"
"net"
"net/http"
"net/http/httputil"
"net/url"
"os"
"os/signal"
"strings"
"syscall"
"time"
)
func main() {
codeServerSocket := flag.String("socket", "/var/apps/coder/target/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")
flag.Parse()
overrideEnv(flag.CommandLine)
backend := httputil.NewSingleHostReverseProxy(&url.URL{Scheme: "http", Host: "unix"})
backend.Director = func(r *http.Request) {
if strings.HasPrefix(r.URL.Path, *prefix) {
r.URL.Path = strings.TrimPrefix(r.URL.Path, *prefix)
if !strings.HasPrefix(r.URL.Path, "/") {
r.URL.Path = "/" + r.URL.Path
}
}
r.URL.Scheme = "http"
r.URL.Host = "unix"
}
backend.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,
}
backend.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
}
backend.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)
}
}
handler := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
if r.Header.Get("Upgrade") == "websocket" || strings.Contains(strings.ToLower(r.Header.Get("Connection")), "upgrade") {
proxyWebSocket(w, r, *codeServerSocket, *prefix)
return
}
backend.ServeHTTP(w, r)
})
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, 0777); err != nil {
log.Printf("warning: failed to set socket permissions: %v", err)
}
go func() {
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
<-sigCh
log.Println("shutting down proxy...")
listener.Close()
}()
log.Printf("coder proxy listening on %s", *proxySocket)
log.Printf("upstream: unix://%s prefix=%s", *codeServerSocket, *prefix)
if err := http.Serve(listener, handler); err != nil && !strings.Contains(err.Error(), "use of closed network connection") {
log.Fatalf("server error: %v", err)
}
os.RemoveAll(*proxySocket)
}
func overrideEnv(fs *flag.FlagSet) {
fs.VisitAll(func(f *flag.Flag) {
envKey := "CODER_" + strings.ToUpper(strings.ReplaceAll(f.Name, "-", "_"))
if v, ok := os.LookupEnv(envKey); ok {
f.Value.Set(v)
}
})
}
func proxyWebSocket(w http.ResponseWriter, r *http.Request, codeServerSocket, prefix string) {
path := r.URL.Path
if strings.HasPrefix(path, prefix) {
path = strings.TrimPrefix(path, prefix)
if !strings.HasPrefix(path, "/") {
path = "/" + path
}
}
conn, err := net.DialTimeout("unix", codeServerSocket, 10*time.Second)
if err != nil {
http.Error(w, fmt.Sprintf("cannot reach code-server socket: %v", err), http.StatusBadGateway)
return
}
defer conn.Close()
upgradeReq := fmt.Sprintf("GET %s HTTP/1.1\r\n", path)
upgradeReq += "Host: unix\r\n"
upgradeReq += "Upgrade: websocket\r\n"
upgradeReq += "Connection: Upgrade\r\n"
if secKey := r.Header.Get("Sec-WebSocket-Key"); secKey != "" {
upgradeReq += fmt.Sprintf("Sec-WebSocket-Key: %s\r\n", secKey)
}
if secVer := r.Header.Get("Sec-WebSocket-Version"); secVer != "" {
upgradeReq += fmt.Sprintf("Sec-WebSocket-Version: %s\r\n", secVer)
}
if secProto := r.Header.Get("Sec-WebSocket-Protocol"); secProto != "" {
upgradeReq += fmt.Sprintf("Sec-WebSocket-Protocol: %s\r\n", secProto)
}
if secExt := r.Header.Get("Sec-WebSocket-Extensions"); secExt != "" {
upgradeReq += fmt.Sprintf("Sec-WebSocket-Extensions: %s\r\n", secExt)
}
if cookie := r.Header.Get("Cookie"); cookie != "" {
upgradeReq += fmt.Sprintf("Cookie: %s\r\n", cookie)
}
upgradeReq += "\r\n"
if _, err := conn.Write([]byte(upgradeReq)); err != nil {
log.Printf("websocket send upgrade: %v", err)
return
}
br := bufio.NewReader(conn)
resp, err := http.ReadResponse(br, nil)
if err != nil {
log.Printf("websocket read response: %v", err)
http.Error(w, "upstream error", http.StatusBadGateway)
return
}
defer resp.Body.Close()
hj, ok := w.(http.Hijacker)
if !ok {
http.Error(w, "hijacking not supported", http.StatusInternalServerError)
return
}
clientConn, _, err := hj.Hijack()
if err != nil {
log.Printf("websocket hijack: %v", err)
return
}
defer clientConn.Close()
resp.Write(clientConn)
done := make(chan struct{}, 2)
go func() {
io.Copy(conn, clientConn)
done <- struct{}{}
}()
go func() {
io.Copy(clientConn, conn)
done <- struct{}{}
}()
<-done
}
其中这go代理中间层,有三个参数可以指定,不指定就是默认值:-socket 上游(Upstream)code-server 的 Unix Socket,告诉 proxy要代理那个;-proxy-socket 自己监听的 Unix Socket,指定生成sock 文件在那个目录;-prefix 路径的配置,也就是URL 前缀。
整个启动流程,最终就是:
飞牛 App
**
▼
start.sh
**
**──── systemctl start code-server
**
**──── 等待 code-server.sock 出现
**
**──── 启动 coder-proxy
**
▼
coder-proxy.sock
**
▼
飞牛 nginx
使用
这样就可以将/app/coder路径转换成/路径,将Unix Socket 进转发,其中这入口要这样写,代码如下:
{
".url": {
"coder.Application": {
"title": "XXX",
"icon": "images/code_icon_{0}.png",
"type": "url",
"protocol": "http",
"gatewaySocket": "coder-proxy.sock",
"gatewayPrefix": "/app/coder",
"url": "/app/coder/",
"allUsers": true
}
}
}
注意这个"url": "/app/coder/",最后是/app/coder/的,因为HTTP 协议认为 /app/coder 和 /app/coder/ 是两个不同的资源,这个go中间层访问目录缺少尾部 / 时,不会自动重定向到带 / 的 URL,所以要写成/app/coder/去除/app/coder,只留下/来使用,这样子不用修改飞牛官方的nginx,也不用修改code-server的源代码,加个golang中间层就可以让很多第三方的应用用上飞牛的统一网关,fpk可以在https://github.com/ctllo-bit/coder/releases/tag/1.0下载,http/https/fn connect都可以,去除了code-server的密码,不登录的情况下是用不了的,如下图所示:

登陆了就可以了,如下所示:


和官方应用一样同用5666、5667端口,基本和桌面端使用一致,走到那用到那,最终这想法还是实现了,完结。。