mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-10-31 14:27:23 +00:00
- 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 内嵌与顶层 非法键回归守卫
283 lines
10 KiB
Go
283 lines
10 KiB
Go
// Package render generates frpc worker configuration (JSON) from canvas data.
|
|
package render
|
|
|
|
import (
|
|
"encoding/json"
|
|
"fmt"
|
|
|
|
"webui4frpc/internal/store"
|
|
)
|
|
|
|
// Proxy is a single proxy to render inside a worker config. It carries enough
|
|
// info to build any supported frpc proxy type.
|
|
type Proxy struct {
|
|
// Name is the display name (the local service name).
|
|
Name string
|
|
// Type is the proxy type: tcp | udp | http | https.
|
|
Type string
|
|
// LocalIP and LocalPort are the backend address.
|
|
LocalIP string
|
|
LocalPort int
|
|
// RemotePort is the exposed port on the frps server (tcp/udp only).
|
|
RemotePort int
|
|
// SubDomain is used by http/https proxies when set.
|
|
SubDomain string
|
|
// CustomDomains is used by http/https proxies when set.
|
|
CustomDomains []string
|
|
|
|
// 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 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
|
|
HostHeaderRewrite string // rewrite Host header
|
|
HTTPHeaders map[string]string // additional request headers
|
|
BasicAuthUser string
|
|
BasicAuthPassword string
|
|
|
|
// M3 Load balancing + health check.
|
|
// LBGroup/LBGroupKey put this proxy into a frps load-balancing group;
|
|
// all members with the same group are balanced across.
|
|
LBGroup string
|
|
LBGroupKey string
|
|
|
|
// HealthCheck* enable frpc's built-in health monitor. Empty type = off.
|
|
HealthCheckType string // "" | tcp | http
|
|
HealthCheckPath string // http only; path to GET on LocalIP:LocalPort
|
|
HealthCheckTimeout int // seconds, default 3
|
|
HealthCheckMaxFailed int // default 1
|
|
HealthCheckInterval int // seconds, default 10
|
|
}
|
|
|
|
// frpcAuth mirrors frpc's auth section.
|
|
type frpcAuth struct {
|
|
Method string `json:"method"`
|
|
Token string `json:"token,omitempty"`
|
|
}
|
|
|
|
// frpcTransport is the transport section used by frpc.
|
|
type frpcTransport struct {
|
|
Protocol string `json:"protocol,omitempty"`
|
|
TLS *frpcTLS `json:"tls,omitempty"`
|
|
PoolCount int `json:"poolCount,omitempty"`
|
|
}
|
|
|
|
// frpcTLS mirrors frpc's transport.tls section.
|
|
type frpcTLS struct {
|
|
Enable bool `json:"enable,omitempty"`
|
|
ServerName string `json:"serverName,omitempty"`
|
|
}
|
|
|
|
// 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"`
|
|
}
|
|
|
|
// frpcBasicAuth mirrors frpc's HTTP basicAuth section.
|
|
type frpcBasicAuth struct {
|
|
User string `json:"user,omitempty"`
|
|
Password string `json:"password,omitempty"`
|
|
}
|
|
|
|
// frpcProxy is the serialized proxy object. Different types use different
|
|
// fields; empty ones are omitted so the JSON is accepted by frpc.
|
|
type frpcProxy struct {
|
|
Name string `json:"name"`
|
|
Type string `json:"type"`
|
|
LocalIP string `json:"localIP,omitempty"`
|
|
LocalPort int `json:"localPort"`
|
|
RemotePort int `json:"remotePort,omitempty"`
|
|
CustomDomains []string `json:"customDomains,omitempty"`
|
|
SubDomain string `json:"subdomain,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"`
|
|
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
|
|
HTTPHeaders map[string]string `json:"httpHeaders,omitempty"`
|
|
HTTPBasicAuth *frpcBasicAuth `json:"basicAuth,omitempty"`
|
|
|
|
// M3 load balancer group + health check.
|
|
LoadBalancer *frpcLoadBalancer `json:"loadBalancer,omitempty"`
|
|
HealthCheck *frpcHealthCheck `json:"healthCheck,omitempty"`
|
|
}
|
|
|
|
// frpcWebServer is the frpc admin API (webServer) section. Enabled when
|
|
// AdminPort > 0; used by the manager to query true per-proxy status.
|
|
type frpcWebServer struct {
|
|
Addr string `json:"addr,omitempty"`
|
|
Port int `json:"port,omitempty"`
|
|
User string `json:"user,omitempty"`
|
|
Password string `json:"password,omitempty"`
|
|
}
|
|
|
|
// frpcLoadBalancer mirrors frpc's loadBalancer section (group load balancing).
|
|
type frpcLoadBalancer struct {
|
|
Group string `json:"group"`
|
|
GroupKey string `json:"groupKey,omitempty"`
|
|
}
|
|
|
|
// frpcHealthCheck mirrors frpc's healthCheck section.
|
|
type frpcHealthCheck struct {
|
|
Type string `json:"type"`
|
|
TimeoutSeconds int `json:"timeoutSeconds,omitempty"`
|
|
MaxFailed int `json:"maxFailed,omitempty"`
|
|
IntervalSeconds int `json:"intervalSeconds"`
|
|
Path string `json:"path,omitempty"`
|
|
HTTPHeaders []frpcHTTPHeader `json:"httpHeaders,omitempty"`
|
|
}
|
|
|
|
// frpcHTTPHeader is a single extra header for an http health check.
|
|
type frpcHTTPHeader struct {
|
|
Name string `json:"name"`
|
|
Value string `json:"value"`
|
|
}
|
|
|
|
// Config is a complete frpc configuration document.
|
|
type Config struct {
|
|
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"`
|
|
LoginFailExit bool `json:"loginFailExit"`
|
|
Proxies []frpcProxy `json:"proxies"`
|
|
}
|
|
|
|
// Render builds the worker config JSON for a remote server from its setting
|
|
// plus the list of proxies to forward.
|
|
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)),
|
|
}
|
|
// M3: optional local frpc admin API (webServer) so the manager can query
|
|
// true per-proxy status. Only emitted when a port is configured.
|
|
if remote.AdminPort > 0 {
|
|
addr := remote.AdminAddr
|
|
if addr == "" {
|
|
addr = "127.0.0.1"
|
|
}
|
|
cfg.WebServer = &frpcWebServer{
|
|
Addr: addr, Port: remote.AdminPort,
|
|
User: remote.AdminUser, Password: remote.AdminPassword,
|
|
}
|
|
}
|
|
if remote.TransportProtocol != "" || remote.TransportTLS || remote.TransportPool > 0 || remote.TransportTLSServerName != "" {
|
|
cfg.Transport = frpcTransport{
|
|
Protocol: remote.TransportProtocol,
|
|
PoolCount: remote.TransportPool,
|
|
}
|
|
if remote.TransportTLS || remote.TransportTLSServerName != "" {
|
|
cfg.Transport.TLS = &frpcTLS{
|
|
Enable: remote.TransportTLS,
|
|
ServerName: remote.TransportTLSServerName,
|
|
}
|
|
}
|
|
}
|
|
|
|
nameCounts := make(map[string]int)
|
|
for _, p := range proxies {
|
|
frpcP := frpcProxy{
|
|
Name: p.Name,
|
|
Type: p.Type,
|
|
LocalIP: p.LocalIP,
|
|
LocalPort: p.LocalPort,
|
|
SubDomain: p.SubDomain,
|
|
Metadatas: p.Metadatas,
|
|
Annotations: p.Annotations,
|
|
Locations: p.Locations,
|
|
HostHeaderRewrite: p.HostHeaderRewrite,
|
|
HTTPHeaders: p.HTTPHeaders,
|
|
}
|
|
// 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}
|
|
}
|
|
// M3: load balancing group.
|
|
if p.LBGroup != "" {
|
|
frpcP.LoadBalancer = &frpcLoadBalancer{Group: p.LBGroup, GroupKey: p.LBGroupKey}
|
|
}
|
|
// M3: health check. Type "" means off; frpc defaults apply when a
|
|
// specific knob is left zero.
|
|
if p.HealthCheckType != "" {
|
|
hc := &frpcHealthCheck{
|
|
Type: p.HealthCheckType,
|
|
TimeoutSeconds: p.HealthCheckTimeout,
|
|
MaxFailed: p.HealthCheckMaxFailed,
|
|
IntervalSeconds: p.HealthCheckInterval,
|
|
Path: p.HealthCheckPath,
|
|
}
|
|
// The HTTP health probe needs its own headers (distinct from the
|
|
// proxy's request headers): map the proxy's HTTPHeaders through.
|
|
if len(p.HTTPHeaders) > 0 {
|
|
hh := make([]frpcHTTPHeader, 0, len(p.HTTPHeaders))
|
|
for name, val := range p.HTTPHeaders {
|
|
hh = append(hh, frpcHTTPHeader{Name: name, Value: val})
|
|
}
|
|
hc.HTTPHeaders = hh
|
|
}
|
|
frpcP.HealthCheck = hc
|
|
}
|
|
// http/https proxies use customDomains instead of remotePort. The
|
|
// default domain is <name>.local; frps matches it by Host header.
|
|
if p.Type == "http" || p.Type == "https" {
|
|
frpcP.RemotePort = 0
|
|
if frpcP.SubDomain == "" && len(frpcP.CustomDomains) == 0 {
|
|
frpcP.CustomDomains = []string{p.Name + ".local"}
|
|
}
|
|
} else {
|
|
frpcP.RemotePort = p.RemotePort
|
|
}
|
|
if len(p.CustomDomains) > 0 {
|
|
frpcP.CustomDomains = p.CustomDomains
|
|
}
|
|
// Duplicate service name -> suffix with the remote port so frpc accepts
|
|
// multiple proxies for the same local service.
|
|
nameCounts[frpcP.Name]++
|
|
if nameCounts[frpcP.Name] > 1 {
|
|
frpcP.Name = fmt.Sprintf("%s-%d", frpcP.Name, frpcP.RemotePort)
|
|
}
|
|
cfg.Proxies = append(cfg.Proxies, frpcP)
|
|
}
|
|
return json.MarshalIndent(cfg, "", " ")
|
|
}
|