// 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): proxy-level encryption/compression/limit. UseEncryption bool UseCompression bool BandwidthLimit string 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) // where bandwidthLimit is defined. type frpcProxyTransport struct { 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"` // 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"` // 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"` 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, 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, 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} } 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 .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, "", " ") }