Files
webui4frpc/internal/render/render.go

266 lines
9.3 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): 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 <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, "", " ")
}