mirror of
https://gitcode.com/JianFeeeee/webui4frpc.git
synced 2026-09-20 00:47:57 +00:00
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:
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
4
internal/httpapi/dist/index.html
vendored
4
internal/httpapi/dist/index.html
vendored
@ -4,8 +4,8 @@
|
||||
<meta charset="UTF-8" />
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||
<title>webui-frpc</title>
|
||||
<script type="module" crossorigin src="/assets/index-D5XN_p_5.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-B38pSFA7.css">
|
||||
<script type="module" crossorigin src="/assets/index-DHaXL5Wt.js"></script>
|
||||
<link rel="stylesheet" crossorigin href="/assets/index-m13EHiyb.css">
|
||||
</head>
|
||||
<body>
|
||||
<div id="app"></div>
|
||||
|
||||
@ -32,6 +32,13 @@ type Proxy struct {
|
||||
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
|
||||
}
|
||||
|
||||
// frpcAuth mirrors frpc's auth section.
|
||||
@ -53,6 +60,12 @@ type frpcTLS struct {
|
||||
ServerName string `json:"serverName,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 {
|
||||
@ -71,6 +84,12 @@ type frpcProxy struct {
|
||||
PoolCount int `json:"poolCount,omitempty"`
|
||||
Metadatas map[string]string `json:"metadatas,omitempty"`
|
||||
Annotations map[string]string `json:"annotations,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"`
|
||||
}
|
||||
|
||||
// Config is a complete frpc configuration document.
|
||||
@ -120,6 +139,12 @@ func Render(remote store.Remote, proxies []Proxy) ([]byte, error) {
|
||||
PoolCount: p.PoolCount,
|
||||
Metadatas: p.Metadatas,
|
||||
Annotations: p.Annotations,
|
||||
Locations: p.Locations,
|
||||
HostHeaderRewrite: p.HostHeaderRewrite,
|
||||
HTTPHeaders: p.HTTPHeaders,
|
||||
}
|
||||
if p.BasicAuthUser != "" || p.BasicAuthPassword != "" {
|
||||
frpcP.HTTPBasicAuth = &frpcBasicAuth{User: p.BasicAuthUser, Password: p.BasicAuthPassword}
|
||||
}
|
||||
// http/https proxies use customDomains instead of remotePort. The
|
||||
// default domain is <name>.local; frps matches it by Host header.
|
||||
|
||||
@ -101,3 +101,58 @@ func TestRenderTransportOmittedWhenEmpty(t *testing.T) {
|
||||
t.Fatalf("proxy advanced should be empty, got %+v", cfg.Proxies[0])
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderHTTPAdvanced(t *testing.T) {
|
||||
remote := store.Remote{Name: "srv", IP: "1.2.3.4", Port: 7000, Token: "secret"}
|
||||
data, err := Render(remote, []Proxy{
|
||||
{Name: "web", Type: "http", LocalIP: "127.0.0.1", LocalPort: 8080,
|
||||
CustomDomains: []string{"app.example.com"},
|
||||
Locations: []string{"/api", "/admin"},
|
||||
HostHeaderRewrite: "backend.internal",
|
||||
HTTPHeaders: map[string]string{"X-Custom": "val"},
|
||||
BasicAuthUser: "user", BasicAuthPassword: "pass"},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg Config
|
||||
if err := json.Unmarshal(data, &cfg); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
p := cfg.Proxies[0]
|
||||
if p.CustomDomains[0] != "app.example.com" {
|
||||
t.Fatalf("customDomains = %+v", p.CustomDomains)
|
||||
}
|
||||
if len(p.Locations) != 2 || p.Locations[0] != "/api" {
|
||||
t.Fatalf("locations = %+v", p.Locations)
|
||||
}
|
||||
if p.HostHeaderRewrite != "backend.internal" {
|
||||
t.Fatalf("hostHeaderRewrite = %q", p.HostHeaderRewrite)
|
||||
}
|
||||
if p.HTTPHeaders["X-Custom"] != "val" {
|
||||
t.Fatalf("httpHeaders = %+v", p.HTTPHeaders)
|
||||
}
|
||||
if p.HTTPBasicAuth == nil || p.HTTPBasicAuth.User != "user" || p.HTTPBasicAuth.Password != "pass" {
|
||||
t.Fatalf("basicAuth = %+v", p.HTTPBasicAuth)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRenderHTTPSUsesCustomDomains(t *testing.T) {
|
||||
remote := store.Remote{Name: "srv", IP: "1.2.3.4", Port: 7000}
|
||||
data, err := Render(remote, []Proxy{
|
||||
{Name: "secure", Type: "https", LocalIP: "127.0.0.1", LocalPort: 8443,
|
||||
CustomDomains: []string{"secure.example.com"}},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
var cfg Config
|
||||
_ = json.Unmarshal(data, &cfg)
|
||||
p := cfg.Proxies[0]
|
||||
if p.RemotePort != 0 {
|
||||
t.Fatalf("https proxy should not set remotePort, got %d", p.RemotePort)
|
||||
}
|
||||
if len(p.CustomDomains) != 1 || p.CustomDomains[0] != "secure.example.com" {
|
||||
t.Fatalf("customDomains = %+v", p.CustomDomains)
|
||||
}
|
||||
}
|
||||
|
||||
@ -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
|
||||
}
|
||||
|
||||
8
plan.md
8
plan.md
@ -48,10 +48,10 @@
|
||||
|
||||
### M2 HTTP/HTTPS 完善
|
||||
|
||||
- [ ] local http/https 增加可编辑域名字段(customDomains / subdomain)
|
||||
- [ ] locations(路径路由)多配置
|
||||
- [ ] httpHeaderRewrite / hostHeaderRewrite / basicAuth(站点访问认证)
|
||||
- [ ] frps vhostHTTPPort 的显示与状态映射(状态页展示)
|
||||
- [x] local http/https 增加可编辑域名字段(customDomains / subdomain)
|
||||
- [x] locations(路径路由)多配置
|
||||
- [x] httpHeaderRewrite / hostHeaderRewrite / basicAuth(站点访问认证)
|
||||
- [x] frps vhostHTTPPort 的显示与状态映射(状态页展示)
|
||||
|
||||
### M3 负载均衡与健康检查
|
||||
|
||||
|
||||
@ -46,6 +46,15 @@
|
||||
<label>连接池 <input v-model.number="poolCountField" type="number" min="0" /></label>
|
||||
<label>元数据(JSON) <input v-model="metadatasField" placeholder='{"env":"prod"}' /></label>
|
||||
<label>注解(JSON) <input v-model="annotationsField" placeholder='{"owner":"ops"}' /></label>
|
||||
|
||||
<div class="adv-sep">HTTP/HTTPS 路由</div>
|
||||
<label>域名(逗号分隔) <input v-model="customDomainsField" placeholder="app.example.com" /></label>
|
||||
<label>子域 <input v-model="subdomainField" placeholder="app (需 frps subdomain_host)" /></label>
|
||||
<label>路径路由(JSON) <input v-model="locationsField" placeholder='["/api","/admin"]' /></label>
|
||||
<label>Host 改写 <input v-model="hostHeaderRewriteField" placeholder="backend.internal" /></label>
|
||||
<label>请求头(JSON) <input v-model="httpHeadersField" placeholder='{"X-Custom":"val"}' /></label>
|
||||
<label>BasicAuth 用户 <input v-model="basicAuthUserField" placeholder="user" /></label>
|
||||
<label>BasicAuth 密码 <input v-model="basicAuthPasswordField" type="password" placeholder="pass" /></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -114,6 +123,44 @@ const annotationsField = computed({
|
||||
emit('update:data', { ...props.data, annotations: parsed })
|
||||
},
|
||||
})
|
||||
const customDomainsField = field('customDomains')
|
||||
const subdomainField = field('subdomain')
|
||||
const hostHeaderRewriteField = field('hostHeaderRewrite')
|
||||
const basicAuthUserField = field('basicAuthUser')
|
||||
const basicAuthPasswordField = field('basicAuthPassword')
|
||||
|
||||
// locations is an array shown as JSON in the UI
|
||||
const locationsField = computed({
|
||||
get: () => JSON.stringify(props.data.locations ?? []),
|
||||
set: (v: string) => {
|
||||
let parsed: string[] = []
|
||||
if (v.trim()) {
|
||||
try {
|
||||
const arr = JSON.parse(v)
|
||||
if (Array.isArray(arr)) parsed = arr.map(String)
|
||||
else return
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
emit('update:data', { ...props.data, locations: parsed })
|
||||
},
|
||||
})
|
||||
// httpHeaders is a map shown as JSON
|
||||
const httpHeadersField = computed({
|
||||
get: () => JSON.stringify(props.data.httpHeaders ?? {}),
|
||||
set: (v: string) => {
|
||||
let parsed: Record<string, string> = {}
|
||||
if (v.trim()) {
|
||||
try {
|
||||
parsed = JSON.parse(v)
|
||||
} catch {
|
||||
return
|
||||
}
|
||||
}
|
||||
emit('update:data', { ...props.data, httpHeaders: parsed })
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
<style scoped lang="scss">
|
||||
|
||||
@ -45,6 +45,8 @@
|
||||
</label>
|
||||
<label>TLS SNI <input v-model="transportTlsServerNameField" placeholder="frps.local" /></label>
|
||||
<label>连接池 <input v-model.number="transportPoolField" type="number" min="0" /></label>
|
||||
<label class="adv-sep">HTTP 访问端口 (frps vhostHTTPPort)</label>
|
||||
<label>vhostHTTPPort <input v-model.number="vhostHttpPortField" type="number" min="0" /></label>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@ -84,6 +86,7 @@ const transportProtocolField = field('transportProtocol')
|
||||
const transportTlsField = field('transportTls')
|
||||
const transportTlsServerNameField = field('transportTlsServerName')
|
||||
const transportPoolField = field('transportPool')
|
||||
const vhostHttpPortField = field('vhostHttpPort')
|
||||
const showAdv = ref(false)
|
||||
</script>
|
||||
|
||||
|
||||
140
web/src/types.ts
140
web/src/types.ts
@ -2,109 +2,121 @@
|
||||
|
||||
// CanvasLocal / CanvasRemote are aliases kept for node components migrated
|
||||
// from the original prototype; they equal Local / Remote.
|
||||
export type CanvasLocal = Local
|
||||
export type CanvasRemote = Remote
|
||||
export type CanvasLocal = Local;
|
||||
export type CanvasRemote = Remote;
|
||||
|
||||
// CanvasLink mirrors the store.Link JSON shape used by the canvas editor.
|
||||
export type CanvasLink = Link
|
||||
export type CanvasLink = Link;
|
||||
|
||||
export interface Local {
|
||||
name: string
|
||||
ip: string
|
||||
port: number
|
||||
protocol: string // tcp | udp | http | https
|
||||
name: string;
|
||||
ip: string;
|
||||
port: number;
|
||||
protocol: string; // tcp | udp | http | https
|
||||
|
||||
// M1 advanced transport knobs (optional; empty/zero = frpc defaults)
|
||||
useEncryption?: boolean
|
||||
useCompression?: boolean
|
||||
bandwidthLimit?: string
|
||||
poolCount?: number
|
||||
metadatas?: Record<string, string>
|
||||
annotations?: Record<string, string>
|
||||
useEncryption?: boolean;
|
||||
useCompression?: boolean;
|
||||
bandwidthLimit?: string;
|
||||
poolCount?: number;
|
||||
metadatas?: Record<string, string>;
|
||||
annotations?: Record<string, string>;
|
||||
|
||||
// M2 HTTP/HTTPS routing (http/https only)
|
||||
customDomains?: string; // comma-separated
|
||||
subdomain?: string;
|
||||
locations?: string[];
|
||||
hostHeaderRewrite?: string;
|
||||
httpHeaders?: Record<string, string>;
|
||||
basicAuthUser?: string;
|
||||
basicAuthPassword?: string;
|
||||
}
|
||||
|
||||
export interface Remote {
|
||||
name: string
|
||||
ip: string
|
||||
port: number // frps connect port
|
||||
token?: string
|
||||
url?: string
|
||||
enabled: boolean
|
||||
name: string;
|
||||
ip: string;
|
||||
port: number; // frps connect port
|
||||
token?: string;
|
||||
url?: string;
|
||||
enabled: boolean;
|
||||
|
||||
// M1 transport section
|
||||
transportProtocol?: string // tcp | quic | kcp | websocket
|
||||
transportTls?: boolean
|
||||
transportPool?: number
|
||||
transportTlsServerName?: string
|
||||
transportProtocol?: string; // tcp | quic | kcp | websocket
|
||||
transportTls?: boolean;
|
||||
transportPool?: number;
|
||||
transportTlsServerName?: string;
|
||||
|
||||
// M2: frps vhostHTTPPort
|
||||
vhostHttpPort?: number;
|
||||
}
|
||||
|
||||
export interface Link {
|
||||
id?: number
|
||||
local: string
|
||||
remote: string
|
||||
remotePort: number
|
||||
offsetX?: number
|
||||
offsetY?: number
|
||||
id?: number;
|
||||
local: string;
|
||||
remote: string;
|
||||
remotePort: number;
|
||||
offsetX?: number;
|
||||
offsetY?: number;
|
||||
}
|
||||
|
||||
export interface CanvasData {
|
||||
locals: Local[]
|
||||
remotes: Remote[]
|
||||
links: Link[]
|
||||
locals: Local[];
|
||||
remotes: Remote[];
|
||||
links: Link[];
|
||||
}
|
||||
|
||||
export interface Settings {
|
||||
autoStartProfiles: boolean
|
||||
restartOnExit: boolean
|
||||
restartIntervalSeconds: number
|
||||
binaryPath?: string
|
||||
autoStartProfiles: boolean;
|
||||
restartOnExit: boolean;
|
||||
restartIntervalSeconds: number;
|
||||
binaryPath?: string;
|
||||
}
|
||||
|
||||
export interface ProcessStatus {
|
||||
state: string
|
||||
pid?: number
|
||||
startTime?: number
|
||||
restartCount: number
|
||||
exitCode?: number
|
||||
err?: string
|
||||
state: string;
|
||||
pid?: number;
|
||||
startTime?: number;
|
||||
restartCount: number;
|
||||
exitCode?: number;
|
||||
err?: string;
|
||||
}
|
||||
|
||||
export interface StatusProfile {
|
||||
name: string
|
||||
enabled: boolean
|
||||
process: ProcessStatus
|
||||
hasProcess: boolean
|
||||
forwards: { service: string; remotePort: number; localPort?: number }[]
|
||||
name: string;
|
||||
enabled: boolean;
|
||||
process: ProcessStatus;
|
||||
hasProcess: boolean;
|
||||
forwards: { service: string; remotePort: number; localPort?: number }[];
|
||||
}
|
||||
|
||||
export interface StatusResp {
|
||||
version: string
|
||||
workDir: string
|
||||
settings: Settings
|
||||
services: Local[]
|
||||
remotes: Remote[]
|
||||
binaryPath: string
|
||||
profiles: StatusProfile[]
|
||||
localStatus: LocalStatus[]
|
||||
version: string;
|
||||
workDir: string;
|
||||
settings: Settings;
|
||||
services: Local[];
|
||||
remotes: Remote[];
|
||||
binaryPath: string;
|
||||
profiles: StatusProfile[];
|
||||
localStatus: LocalStatus[];
|
||||
}
|
||||
|
||||
export interface LocalTargetStatus {
|
||||
remote: string
|
||||
remotePort: number
|
||||
workerState: string
|
||||
remote: string;
|
||||
remotePort: number;
|
||||
workerState: string;
|
||||
}
|
||||
|
||||
export interface LocalStatus {
|
||||
local: Local
|
||||
targets: LocalTargetStatus[]
|
||||
local: Local;
|
||||
targets: LocalTargetStatus[];
|
||||
}
|
||||
|
||||
export interface BinaryStatus {
|
||||
binaryPath: string
|
||||
version?: string
|
||||
binaryPath: string;
|
||||
version?: string;
|
||||
}
|
||||
|
||||
export interface InstallResult {
|
||||
path: string
|
||||
version: string
|
||||
path: string;
|
||||
version: string;
|
||||
}
|
||||
|
||||
@ -33,6 +33,9 @@
|
||||
<div class="ns-meta">
|
||||
<span v-if="p.process.pid">pid {{ p.process.pid }}</span>
|
||||
<span class="ns-err" v-else-if="p.process.err">{{ p.process.err }}</span>
|
||||
<span v-if="remoteVhostPort(p.name)" class="ns-vhost">
|
||||
HTTP 访问端口 :{{ remoteVhostPort(p.name) }}
|
||||
</span>
|
||||
</div>
|
||||
<div class="ns-forwards" v-if="p.forwards.length">
|
||||
<span v-for="f in p.forwards" :key="f.service" class="ns-fwd">
|
||||
@ -218,6 +221,10 @@ const workerStateLabel = (s?: string) => {
|
||||
|
||||
const workerHealthy = (s?: string) => s === 'running'
|
||||
|
||||
// remoteVhostPort returns the frps vhostHTTPPort for a remote (0 if unset).
|
||||
const remoteVhostPort = (name: string): number =>
|
||||
status.value?.remotes.find((r) => r.name === name)?.vhostHttpPort || 0
|
||||
|
||||
onMounted(() => {
|
||||
loadStatus()
|
||||
timer = window.setInterval(loadStatus, 5000)
|
||||
@ -402,6 +409,15 @@ onBeforeUnmount(() => {
|
||||
color: $color-danger;
|
||||
}
|
||||
|
||||
.ns-vhost {
|
||||
margin-left: 8px;
|
||||
background: #ecf5ff;
|
||||
color: #409eff;
|
||||
border-radius: 6px;
|
||||
padding: 1px 8px;
|
||||
font-size: 12px;
|
||||
}
|
||||
|
||||
.ns-forwards {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
|
||||
Reference in New Issue
Block a user