// 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 } // 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"` } // 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"` } // 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"` 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)), } 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, } // 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, "", " ") }