mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-10-31 14:27:23 +00:00
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:
@ -25,13 +25,19 @@ type Proxy struct {
|
||||
// CustomDomains is used by http/https proxies when set.
|
||||
CustomDomains []string
|
||||
|
||||
// Advanced transport knobs (M1): proxy-level encryption/compression/limit.
|
||||
// Advanced transport knobs (M1). These are rendered INSIDE the proxy's
|
||||
// `transport` object (frpc's ProxyTransport), not at the proxy top level.
|
||||
UseEncryption bool
|
||||
UseCompression bool
|
||||
BandwidthLimit string
|
||||
PoolCount int
|
||||
Metadatas map[string]string
|
||||
Annotations map[string]string
|
||||
// PoolCount has no per-proxy representation in frpc (ProxyTransport has no
|
||||
// poolCount field; only the global `transport.poolCount` exists), so this
|
||||
// value is deliberately not rendered — use the remote's TransportPool for
|
||||
// connection pooling. Kept here so existing canvases keep loading.
|
||||
PoolCount int
|
||||
|
||||
Metadatas map[string]string
|
||||
Annotations map[string]string
|
||||
|
||||
// M2 HTTP/HTTPS routing (http/https only).
|
||||
Locations []string // path routing
|
||||
@ -73,9 +79,15 @@ type frpcTLS struct {
|
||||
ServerName string `json:"serverName,omitempty"`
|
||||
}
|
||||
|
||||
// frpcProxyTransport is the per-proxy transport section (frpc >= 0.52)
|
||||
// where bandwidthLimit is defined.
|
||||
// frpcProxyTransport is the per-proxy transport section (frpc >= 0.52).
|
||||
//
|
||||
// useEncryption/useCompression/bandwidthLimit all live HERE, not at the proxy's
|
||||
// top level: frpc's ProxyTransport struct owns them, and frpc runs with
|
||||
// --strict-config by default, so a top-level spelling fails the whole config
|
||||
// with `unmarshal ProxyConfig error: json: unknown field "useEncryption"`.
|
||||
type frpcProxyTransport struct {
|
||||
UseEncryption bool `json:"useEncryption,omitempty"`
|
||||
UseCompression bool `json:"useCompression,omitempty"`
|
||||
BandwidthLimit string `json:"bandwidthLimit,omitempty"`
|
||||
}
|
||||
|
||||
@ -96,13 +108,9 @@ type frpcProxy struct {
|
||||
CustomDomains []string `json:"customDomains,omitempty"`
|
||||
SubDomain string `json:"subdomain,omitempty"`
|
||||
|
||||
// M1 advanced transport fields. bandwidthLimit lives in transport.
|
||||
UseEncryption bool `json:"useEncryption,omitempty"`
|
||||
UseCompression bool `json:"useCompression,omitempty"`
|
||||
PoolCount int `json:"poolCount,omitempty"`
|
||||
Metadatas map[string]string `json:"metadatas,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
Transport *frpcProxyTransport `json:"transport,omitempty"`
|
||||
Metadatas map[string]string `json:"metadatas,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,omitempty"`
|
||||
Transport *frpcProxyTransport `json:"transport,omitempty"`
|
||||
|
||||
// M2 HTTP/HTTPS routing.
|
||||
Locations []string `json:"locations,omitempty"`
|
||||
@ -148,8 +156,12 @@ type frpcHTTPHeader struct {
|
||||
|
||||
// Config is a complete frpc configuration document.
|
||||
type Config struct {
|
||||
ServerAddr string `json:"serverAddr"`
|
||||
ServerPort int `json:"serverPort"`
|
||||
ServerAddr string `json:"serverAddr"`
|
||||
ServerPort int `json:"serverPort"`
|
||||
// User is frpc's global `user` field. frps prefixes every proxy name with it
|
||||
// (`<user>.<name>`), and panel-managed frps requires it to admit the login.
|
||||
// Omitted when empty so plain frps sees an unchanged config.
|
||||
User string `json:"user,omitempty"`
|
||||
Auth frpcAuth `json:"auth"`
|
||||
Transport frpcTransport `json:"transport,omitempty"`
|
||||
WebServer *frpcWebServer `json:"webServer,omitempty"`
|
||||
@ -163,6 +175,7 @@ func Render(remote store.Remote, proxies []Proxy) ([]byte, error) {
|
||||
cfg := Config{
|
||||
ServerAddr: remote.IP,
|
||||
ServerPort: remote.Port,
|
||||
User: remote.User,
|
||||
Auth: frpcAuth{Method: "token", Token: remote.Token},
|
||||
LoginFailExit: false,
|
||||
Proxies: make([]frpcProxy, 0, len(proxies)),
|
||||
@ -200,17 +213,21 @@ func Render(remote store.Remote, proxies []Proxy) ([]byte, error) {
|
||||
LocalIP: p.LocalIP,
|
||||
LocalPort: p.LocalPort,
|
||||
SubDomain: p.SubDomain,
|
||||
UseEncryption: p.UseEncryption,
|
||||
UseCompression: p.UseCompression,
|
||||
PoolCount: p.PoolCount,
|
||||
Metadatas: p.Metadatas,
|
||||
Annotations: p.Annotations,
|
||||
Locations: p.Locations,
|
||||
HostHeaderRewrite: p.HostHeaderRewrite,
|
||||
HTTPHeaders: p.HTTPHeaders,
|
||||
}
|
||||
if p.BandwidthLimit != "" {
|
||||
frpcP.Transport = &frpcProxyTransport{BandwidthLimit: p.BandwidthLimit}
|
||||
// useEncryption/useCompression/bandwidthLimit are all members of frpc's
|
||||
// per-proxy `transport` object — emitting them at the proxy top level
|
||||
// makes frpc reject the entire config under --strict-config.
|
||||
if p.UseEncryption || p.UseCompression || p.BandwidthLimit != "" {
|
||||
frpcP.Transport = &frpcProxyTransport{
|
||||
UseEncryption: p.UseEncryption,
|
||||
UseCompression: p.UseCompression,
|
||||
BandwidthLimit: p.BandwidthLimit,
|
||||
}
|
||||
}
|
||||
if p.BasicAuthUser != "" || p.BasicAuthPassword != "" {
|
||||
frpcP.HTTPBasicAuth = &frpcBasicAuth{User: p.BasicAuthUser, Password: p.BasicAuthPassword}
|
||||
|
||||
@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user