fix(render,store): frpc user 支持 + 修正 useEncryption/useCompression 渲染位置

- store.Remote 新增 user 字段(DB 列名 usr,避开保留字;migrate() 自动加列),
  渲染为 frpc 全局 user。面板型 frps(如 ChhChongGou 实例)强制要求 user 登录,
  缺失时服务端返回 panel authentication unavailable: panel HTTP status 403
- ★ 修复渲染 bug:useEncryption/useCompression/bandwidthLimit 必须内嵌在 proxy 的
  transport 对象(frpc ProxyTransport)。此前渲染在 proxy 顶层,frpc 默认
  --strict-config 直接拒载:unknown field "useEncryption"。任何启用加密/压缩
  的转发都无法启动。poolCount 无 proxy 级表达(官方 ProxyTransport 无此字段),
  保留在 store 但不再渲染
- 前端:RemoteNode 画布节点与 StatusView 表单加 user 输入(画布此前会静默
  清掉未声明的字段),带『面板型 frps 必填』提示
- 测试:user 往返(含迁移路径)、渲染含/不含 user、transport 内嵌与顶层
  非法键回归守卫
This commit is contained in:
JianFeeeee
2026-10-12 19:16:28 +08:00
parent b29cd0d186
commit d83e5d8e70
7 changed files with 214 additions and 36 deletions

View File

@ -74,14 +74,31 @@ func TestRenderTransportAdvanced(t *testing.T) {
t.Fatalf("transport.poolCount = %d, want 3", cfg.Transport.PoolCount)
}
p := cfg.Proxies[0]
if !p.UseEncryption || !p.UseCompression || p.PoolCount != 2 {
t.Fatalf("proxy advanced fields = %+v", p)
// useEncryption/useCompression/bandwidthLimit belong to the proxy's nested
// `transport` object. Asserting them on the proxy top level is what let the
// old renderer ship a config frpc rejects outright (`unknown field
// "useEncryption"` under --strict-config).
if p.Transport == nil {
t.Fatalf("proxy transport section missing: %+v", p)
}
if !p.Transport.UseEncryption || !p.Transport.UseCompression || p.Transport.BandwidthLimit != "1MB" {
t.Fatalf("proxy transport = %+v", p.Transport)
}
if p.Metadatas["env"] != "prod" || p.Annotations["owner"] != "ops" {
t.Fatalf("proxy metadatas/annotations = %+v", p)
}
if p.Transport == nil || p.Transport.BandwidthLimit != "1MB" {
t.Fatalf("proxy transport.bandwidthLimit = %+v", p.Transport)
// Regression guard: the top level must carry no encryption/pool keys, since
// frpc runs with --strict-config and rejects unknown fields.
var raw struct {
Proxies []map[string]any `json:"proxies"`
}
if err := json.Unmarshal(data, &raw); err != nil {
t.Fatal(err)
}
for _, bad := range []string{"useEncryption", "useCompression", "poolCount"} {
if _, ok := raw.Proxies[0][bad]; ok {
t.Fatalf("proxy top level must not carry %q: %s", bad, data)
}
}
}
@ -100,8 +117,8 @@ func TestRenderTransportOmittedWhenEmpty(t *testing.T) {
if cfg.Transport.Protocol != "" || cfg.Transport.TLS != nil || cfg.Transport.PoolCount != 0 {
t.Fatalf("transport should be empty, got %+v", cfg.Transport)
}
if cfg.Proxies[0].UseEncryption || cfg.Proxies[0].Transport != nil {
t.Fatalf("proxy advanced should be empty, got %+v", cfg.Proxies[0])
if cfg.Proxies[0].Transport != nil {
t.Fatalf("proxy transport should be empty when no proxy-level knobs are set, got %+v", cfg.Proxies[0].Transport)
}
}
@ -264,3 +281,47 @@ func TestRenderAdminWebServerOmitted(t *testing.T) {
t.Fatalf("webServer should be omitted, got %+v", cfg.WebServer)
}
}
// A panel-managed frps refuses logins without `user`, so the rendered config
// must carry it verbatim when set.
func TestRenderEmitsUserWhenSet(t *testing.T) {
remote := store.Remote{Name: "srv", IP: "1.2.3.4", Port: 7000, Token: "tok", User: "e8f52adfaf0e5c95"}
data, err := Render(remote, []Proxy{
{Name: "club", Type: "tcp", LocalIP: "127.0.0.1", LocalPort: 80, RemotePort: 62962},
})
if err != nil {
t.Fatal(err)
}
var cfg Config
if err := json.Unmarshal(data, &cfg); err != nil {
t.Fatal(err)
}
if cfg.User != "e8f52adfaf0e5c95" {
t.Fatalf("user = %q, want e8f52adfaf0e5c95", cfg.User)
}
// The key must be present on the wire, not merely in the parsed struct:
// an omitempty regression would silently break panel-type frps again.
var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
t.Fatal(err)
}
if _, ok := raw["user"]; !ok {
t.Fatalf("rendered JSON has no user key: %s", data)
}
}
// Plain frps rejects unknown fields, so an empty user must be omitted entirely.
func TestRenderOmitsUserWhenEmpty(t *testing.T) {
remote := store.Remote{Name: "srv", IP: "1.2.3.4", Port: 7000}
data, err := Render(remote, nil)
if err != nil {
t.Fatal(err)
}
var raw map[string]any
if err := json.Unmarshal(data, &raw); err != nil {
t.Fatal(err)
}
if _, ok := raw["user"]; ok {
t.Fatalf("empty user must be omitted, got: %s", data)
}
}