feat: M2 HTTP/HTTPS 完善

- store: Local 增加 customDomains/subdomain/locations/hostHeaderRewrite/httpHeaders/basicAuth;Remote 增加 vhostHttpPort;migrate() 补列;CRUD 读写(JSOm 编解码)

- render: http/https proxy 输出 customDomains/subdomain/locations/hostHeaderRewrite/httpHeaders/basicAuth

- main: renderRemote 透传 M2 字段(customDomains 逗号拆分 splitCSV)

- web: LocalNode 折叠 HTTP 路由高级区;RemoteNode vhostHTTPPort;StatusView 显示 HTTP 访问端口

- 测试: TestRenderHTTPAdvanced / TestRenderHTTPSUsesCustomDomains;端到端 PUT canvas→config 验证通过
This commit is contained in:
2026-08-17 12:59:12 +08:00
parent 07f25ea067
commit 58906c5a81
11 changed files with 323 additions and 116 deletions

View File

@ -27,6 +27,15 @@ type Local struct {
PoolCount int `json:"poolCount,omitempty"`
Metadatas map[string]string `json:"metadatas,omitempty"`
Annotations map[string]string `json:"annotations,omitempty"`
// M2 HTTP/HTTPS routing (http/https only).
CustomDomains string `json:"customDomains,omitempty"` // comma-separated
SubDomain string `json:"subdomain,omitempty"`
Locations []string `json:"locations,omitempty"` // path routing
HostHeaderRewrite string `json:"hostHeaderRewrite,omitempty"`
HTTPHeaders map[string]string `json:"httpHeaders,omitempty"`
BasicAuthUser string `json:"basicAuthUser,omitempty"`
BasicAuthPassword string `json:"basicAuthPassword,omitempty"`
}
// Remote is a remote server node on the canvas.
@ -43,6 +52,10 @@ type Remote struct {
TransportTLS bool `json:"transportTls,omitempty"`
TransportPool int `json:"transportPool,omitempty"`
TransportTLSServerName string `json:"transportTlsServerName,omitempty"`
// M2: frps vhostHTTPPort (optional, displayed on the status page as the
// HTTP access port for http/https services).
VhostHTTPPort int `json:"vhostHttpPort,omitempty"`
}
// Link connects one local to one remote.
@ -148,12 +161,20 @@ func (s *Store) migrate() error {
"pool_count INTEGER NOT NULL DEFAULT 0",
"metadatas TEXT NOT NULL DEFAULT ''",
"annotations TEXT NOT NULL DEFAULT ''",
"custom_domains TEXT NOT NULL DEFAULT ''",
"subdomain TEXT NOT NULL DEFAULT ''",
"locations TEXT NOT NULL DEFAULT ''",
"host_header_rewrite TEXT NOT NULL DEFAULT ''",
"http_headers TEXT NOT NULL DEFAULT ''",
"basic_auth_user TEXT NOT NULL DEFAULT ''",
"basic_auth_password TEXT NOT NULL DEFAULT ''",
},
"remotes": {
"transport_protocol TEXT NOT NULL DEFAULT ''",
"transport_tls INTEGER NOT NULL DEFAULT 0",
"transport_pool INTEGER NOT NULL DEFAULT 0",
"transport_tls_server_name TEXT NOT NULL DEFAULT ''",
"vhost_http_port INTEGER NOT NULL DEFAULT 0",
},
}
for table, cols := range tables {
@ -191,7 +212,7 @@ func (s *Store) Close() error { return s.db.Close() }
// ---- Locals ----
func (s *Store) ListLocals() ([]Local, error) {
rows, err := s.db.Query("SELECT name, ip, port, protocol, use_encryption, use_compression, bandwidth_limit, pool_count, metadatas, annotations FROM locals ORDER BY name")
rows, err := s.db.Query("SELECT name, ip, port, protocol, use_encryption, use_compression, bandwidth_limit, pool_count, metadatas, annotations, custom_domains, subdomain, locations, host_header_rewrite, http_headers, basic_auth_user, basic_auth_password FROM locals ORDER BY name")
if err != nil {
return nil, err
}
@ -200,14 +221,16 @@ func (s *Store) ListLocals() ([]Local, error) {
for rows.Next() {
var l Local
var enc, comp int
var metaRaw, annoRaw string
if err := rows.Scan(&l.Name, &l.IP, &l.Port, &l.Protocol, &enc, &comp, &l.BandwidthLimit, &l.PoolCount, &metaRaw, &annoRaw); err != nil {
var metaRaw, annoRaw, hdrRaw, locRaw string
if err := rows.Scan(&l.Name, &l.IP, &l.Port, &l.Protocol, &enc, &comp, &l.BandwidthLimit, &l.PoolCount, &metaRaw, &annoRaw, &l.CustomDomains, &l.SubDomain, &locRaw, &l.HostHeaderRewrite, &hdrRaw, &l.BasicAuthUser, &l.BasicAuthPassword); err != nil {
return nil, err
}
l.UseEncryption = enc != 0
l.UseCompression = comp != 0
l.Metadatas = decodeMap(metaRaw)
l.Annotations = decodeMap(annoRaw)
l.Locations = decodeSlice(locRaw)
l.HTTPHeaders = decodeMap(hdrRaw)
out = append(out, l)
}
return out, rows.Err()
@ -216,27 +239,53 @@ func (s *Store) ListLocals() ([]Local, error) {
func (s *Store) GetLocal(name string) (Local, bool) {
var l Local
var enc, comp int
var metaRaw, annoRaw string
row := s.db.QueryRow("SELECT name, ip, port, protocol, use_encryption, use_compression, bandwidth_limit, pool_count, metadatas, annotations FROM locals WHERE name = ?", name)
if err := row.Scan(&l.Name, &l.IP, &l.Port, &l.Protocol, &enc, &comp, &l.BandwidthLimit, &l.PoolCount, &metaRaw, &annoRaw); err != nil {
var metaRaw, annoRaw, hdrRaw, locRaw string
row := s.db.QueryRow("SELECT name, ip, port, protocol, use_encryption, use_compression, bandwidth_limit, pool_count, metadatas, annotations, custom_domains, subdomain, locations, host_header_rewrite, http_headers, basic_auth_user, basic_auth_password FROM locals WHERE name = ?", name)
if err := row.Scan(&l.Name, &l.IP, &l.Port, &l.Protocol, &enc, &comp, &l.BandwidthLimit, &l.PoolCount, &metaRaw, &annoRaw, &l.CustomDomains, &l.SubDomain, &locRaw, &l.HostHeaderRewrite, &hdrRaw, &l.BasicAuthUser, &l.BasicAuthPassword); err != nil {
return Local{}, false
}
l.UseEncryption = enc != 0
l.UseCompression = comp != 0
l.Metadatas = decodeMap(metaRaw)
l.Annotations = decodeMap(annoRaw)
l.Locations = decodeSlice(locRaw)
l.HTTPHeaders = decodeMap(hdrRaw)
return l, true
}
func (s *Store) UpsertLocal(l Local) error {
_, err := s.db.Exec(
"INSERT INTO locals(name, ip, port, protocol, use_encryption, use_compression, bandwidth_limit, pool_count, metadatas, annotations) VALUES(?,?,?,?,?,?,?,?,?,?) "+
"ON CONFLICT(name) DO UPDATE SET ip=excluded.ip, port=excluded.port, protocol=excluded.protocol, use_encryption=excluded.use_encryption, use_compression=excluded.use_compression, bandwidth_limit=excluded.bandwidth_limit, pool_count=excluded.pool_count, metadatas=excluded.metadatas, annotations=excluded.annotations",
l.Name, l.IP, l.Port, l.Protocol, boolToInt(l.UseEncryption), boolToInt(l.UseCompression), l.BandwidthLimit, l.PoolCount, encodeMap(l.Metadatas), encodeMap(l.Annotations),
"INSERT INTO locals(name, ip, port, protocol, use_encryption, use_compression, bandwidth_limit, pool_count, metadatas, annotations, custom_domains, subdomain, locations, host_header_rewrite, http_headers, basic_auth_user, basic_auth_password) VALUES(?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?,?) "+
"ON CONFLICT(name) DO UPDATE SET ip=excluded.ip, port=excluded.port, protocol=excluded.protocol, use_encryption=excluded.use_encryption, use_compression=excluded.use_compression, bandwidth_limit=excluded.bandwidth_limit, pool_count=excluded.pool_count, metadatas=excluded.metadatas, annotations=excluded.annotations, custom_domains=excluded.custom_domains, subdomain=excluded.subdomain, locations=excluded.locations, host_header_rewrite=excluded.host_header_rewrite, http_headers=excluded.http_headers, basic_auth_user=excluded.basic_auth_user, basic_auth_password=excluded.basic_auth_password",
l.Name, l.IP, l.Port, l.Protocol, boolToInt(l.UseEncryption), boolToInt(l.UseCompression), l.BandwidthLimit, l.PoolCount, encodeMap(l.Metadatas), encodeMap(l.Annotations), l.CustomDomains, l.SubDomain, encodeSlice(l.Locations), l.HostHeaderRewrite, encodeMap(l.HTTPHeaders), l.BasicAuthUser, l.BasicAuthPassword,
)
return err
}
// encodeSlice serializes an optional []string as JSON (empty -> "").
func encodeSlice(s []string) string {
if len(s) == 0 {
return ""
}
b, err := json.Marshal(s)
if err != nil {
return ""
}
return string(b)
}
// decodeSlice reads a JSON []string column; empty/invalid -> nil.
func decodeSlice(raw string) []string {
if raw == "" {
return nil
}
var s []string
if err := json.Unmarshal([]byte(raw), &s); err != nil {
return nil
}
return s
}
// encodeMap serializes an optional JSON map so nil stays an empty string.
func encodeMap(m map[string]string) string {
if len(m) == 0 {
@ -269,7 +318,7 @@ func (s *Store) DeleteLocal(name string) error {
// ---- Remotes ----
func (s *Store) ListRemotes() ([]Remote, error) {
rows, err := s.db.Query("SELECT name, ip, port, token, url, enabled, transport_protocol, transport_tls, transport_pool, transport_tls_server_name FROM remotes ORDER BY name")
rows, err := s.db.Query("SELECT name, ip, port, token, url, enabled, transport_protocol, transport_tls, transport_pool, transport_tls_server_name, vhost_http_port FROM remotes ORDER BY name")
if err != nil {
return nil, err
}
@ -278,7 +327,7 @@ func (s *Store) ListRemotes() ([]Remote, error) {
for rows.Next() {
var r Remote
var en, tls int
if err := rows.Scan(&r.Name, &r.IP, &r.Port, &r.Token, &r.URL, &en, &r.TransportProtocol, &tls, &r.TransportPool, &r.TransportTLSServerName); err != nil {
if err := rows.Scan(&r.Name, &r.IP, &r.Port, &r.Token, &r.URL, &en, &r.TransportProtocol, &tls, &r.TransportPool, &r.TransportTLSServerName, &r.VhostHTTPPort); err != nil {
return nil, err
}
r.Enabled = en != 0
@ -291,8 +340,8 @@ func (s *Store) ListRemotes() ([]Remote, error) {
func (s *Store) GetRemote(name string) (Remote, bool) {
var r Remote
var en, tls int
row := s.db.QueryRow("SELECT name, ip, port, token, url, enabled, transport_protocol, transport_tls, transport_pool, transport_tls_server_name FROM remotes WHERE name = ?", name)
if err := row.Scan(&r.Name, &r.IP, &r.Port, &r.Token, &r.URL, &en, &r.TransportProtocol, &tls, &r.TransportPool, &r.TransportTLSServerName); err != nil {
row := s.db.QueryRow("SELECT name, ip, port, token, url, enabled, transport_protocol, transport_tls, transport_pool, transport_tls_server_name, vhost_http_port FROM remotes WHERE name = ?", name)
if err := row.Scan(&r.Name, &r.IP, &r.Port, &r.Token, &r.URL, &en, &r.TransportProtocol, &tls, &r.TransportPool, &r.TransportTLSServerName, &r.VhostHTTPPort); err != nil {
return Remote{}, false
}
r.Enabled = en != 0
@ -302,9 +351,9 @@ func (s *Store) GetRemote(name string) (Remote, bool) {
func (s *Store) UpsertRemote(r Remote) error {
_, err := s.db.Exec(
"INSERT INTO remotes(name, ip, port, token, url, enabled, transport_protocol, transport_tls, transport_pool, transport_tls_server_name) VALUES(?,?,?,?,?,?,?,?,?,?) "+
"ON CONFLICT(name) DO UPDATE SET ip=excluded.ip, port=excluded.port, token=excluded.token, url=excluded.url, enabled=excluded.enabled, transport_protocol=excluded.transport_protocol, transport_tls=excluded.transport_tls, transport_pool=excluded.transport_pool, transport_tls_server_name=excluded.transport_tls_server_name",
r.Name, r.IP, r.Port, r.Token, r.URL, boolToInt(r.Enabled), r.TransportProtocol, boolToInt(r.TransportTLS), r.TransportPool, r.TransportTLSServerName,
"INSERT INTO remotes(name, ip, port, token, url, enabled, transport_protocol, transport_tls, transport_pool, transport_tls_server_name, vhost_http_port) VALUES(?,?,?,?,?,?,?,?,?,?,?) "+
"ON CONFLICT(name) DO UPDATE SET ip=excluded.ip, port=excluded.port, token=excluded.token, url=excluded.url, enabled=excluded.enabled, transport_protocol=excluded.transport_protocol, transport_tls=excluded.transport_tls, transport_pool=excluded.transport_pool, transport_tls_server_name=excluded.transport_tls_server_name, vhost_http_port=excluded.vhost_http_port",
r.Name, r.IP, r.Port, r.Token, r.URL, boolToInt(r.Enabled), r.TransportProtocol, boolToInt(r.TransportTLS), r.TransportPool, r.TransportTLSServerName, r.VhostHTTPPort,
)
return err
}