收起左侧

第三方应用接入飞牛统一网关(golang中间层)

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

前言

飞牛官方出了统一网关,那时候我就想将它用于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的密码,不登录的情况下是用不了的,如下图所示:

image.png

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

image.png

image.png

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

收藏
送赞
分享

本帖子中包含更多资源

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

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

本版积分规则