mirror of
https://gitcode.com/JianFeeeee/ModelRouter.git
synced 2026-11-07 09:47:23 +00:00
问题:登录返回的 view_url 是 http://127.0.0.1:60100/vnc.html,WebUI 直接把它 嵌进 iframe。远程运维打开必白屏 —— 127.0.0.1 在客户端指向客户端自己,且 websockify/x11vnc 只绑回环,换服务器 IP 也连不上(实测 LAN IP 访问 000)。 ★ 教训:只验证"服务端截图有画面"就宣布 VNC 可用是不充分的。四层链路里 ①X 渲染 ②noVNC 客户端渲染 一直是好的,白屏真因在第④层「客户端如何拿到地址」。 内核新增通用「插件前缀代理」能力(不是为 channels 写的专用反向代理): - plugin.http 支持 prefix = true:声明前缀路由,承接所有子路径 - handler 返回 { proxy = "http://127.0.0.1:PORT" },内核每次请求都问一遍 ⇒ 会话停止/过期后插件不再给目标,旧地址自然失效(撤销点唯一) - 鉴权与角色门禁复用现有链路,无旁路 - statusRecorder 增加 Unwrap(),让 Hijack 穿透访问日志中间件(否则 WebSocket 升级失败) - 目标只允许带显式端口的**字面回环 IP**(用户拍板方案 A):拒绝 https、localhost 主机名、无端口、带 userinfo/path/query;理由是主机名可能被 hosts/DNS 改写 - 拒绝跨站 Origin(WebSocket 带环境 Cookie,否则第三方网页可借管理员会话开隧道) - 不向插件后端转发 Authorization/Cookie/api_key channels 接入: - 新增 channels/browser/proxy/:session(prefix=true) - 会话代号 = seq-随机8位,防猜到活跃会话 - view_url 改为网关同源路径,noVNC 的 WebSocket 走同前缀 /websockify - 会话过期即回收浏览器栈并返回 404 判据(每条咬真实外部效应): - 静态资源转发 - 真实 HTTP 101 升级 + 双向数据回传(只断言静态转发不够 —— 升级是白屏真考点) - 网关凭据不泄漏(在服务端断言请求头) - 非法目标全拒(公网/localhost/https/无端口/带 path、query、userinfo) - 跨站 Origin 403 - 未鉴权 401、过期会话 404、非子路径 404 回归:go test -tags luajit ./... 9 包全绿
83 lines
2.5 KiB
Go
83 lines
2.5 KiB
Go
package gateway
|
|
|
|
import (
|
|
"bufio"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
// Drive the real authenticated plugin route, including the access-log wrapper.
|
|
// A successful static request alone does not prove WebSocket can upgrade.
|
|
func TestPluginPrefixProxyHTTPAndUpgrade(t *testing.T) {
|
|
backend := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
if r.Header.Get("Authorization") != "" || r.Header.Get("Cookie") != "" || r.URL.Query().Get("api_key") != "" {
|
|
t.Error("gateway credentials leaked to plugin backend")
|
|
}
|
|
if r.URL.Path == "/websockify" {
|
|
c, b, err := w.(http.Hijacker).Hijack()
|
|
if err != nil {
|
|
t.Error(err)
|
|
return
|
|
}
|
|
defer c.Close()
|
|
fmt.Fprint(b, "HTTP/1.1 101 Switching Protocols\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n")
|
|
b.Flush()
|
|
io.Copy(c, c)
|
|
return
|
|
}
|
|
fmt.Fprint(w, r.URL.Path+"?"+r.URL.RawQuery)
|
|
}))
|
|
defer backend.Close()
|
|
g, _ := gatewayWithSpy(t)
|
|
src := fmt.Sprintf(`local p={name="relay",http={{path="relay/:session",prefix=true,method="GET",roles={"admin"},handler="proxy"}}}
|
|
function p.proxy(req)
|
|
if req.params.session ~= "live" then return {status=404,body="expired"} end
|
|
return {proxy=%q}
|
|
end
|
|
return p`, backend.URL)
|
|
if err := g.core.Plugins().LoadSource("relay", src); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
server := httptest.NewServer(g.Handler())
|
|
defer server.Close()
|
|
req, _ := http.NewRequest("GET", server.URL+"/api/plugins/http/relay/live/app/main.js?x=1&api_key=sk-test", nil)
|
|
req.Header.Set("Authorization", "Bearer sk-test")
|
|
req.AddCookie(&http.Cookie{Name: "gw_key", Value: "sk-test"})
|
|
resp, err := http.DefaultClient.Do(req)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
body, _ := io.ReadAll(resp.Body)
|
|
resp.Body.Close()
|
|
if resp.StatusCode != 200 || string(body) != "/app/main.js?x=1" {
|
|
t.Fatalf("HTTP proxy: %d %s", resp.StatusCode, body)
|
|
}
|
|
c, err := net.Dial("tcp", strings.TrimPrefix(server.URL, "http://"))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
defer c.Close()
|
|
fmt.Fprintf(c, "GET /api/plugins/http/relay/live/websockify HTTP/1.1\r\nHost: %s\r\nAuthorization: Bearer sk-test\r\nConnection: Upgrade\r\nUpgrade: websocket\r\n\r\n", strings.TrimPrefix(server.URL, "http://"))
|
|
b := bufio.NewReader(c)
|
|
reply, err := http.ReadResponse(b, nil)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if reply.StatusCode != 101 {
|
|
t.Fatalf("upgrade failed: %s", reply.Status)
|
|
}
|
|
c.Write([]byte("ping"))
|
|
got := make([]byte, 4)
|
|
if _, err = io.ReadFull(b, got); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
if string(got) != "ping" {
|
|
t.Fatalf("tunnel payload=%q", got)
|
|
}
|
|
}
|