diff --git a/server/api_router.go b/server/api_router.go index 8becb239..22bf81ea 100644 --- a/server/api_router.go +++ b/server/api_router.go @@ -49,6 +49,7 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) subRouter.HandleFunc("/api/proxies", httppkg.MakeHTTPHandlerFunc(apiController.DeleteProxies)).Methods("DELETE") subRouter.HandleFunc("/api/v2/users", httppkg.MakeHTTPHandlerFuncV2(apiController.APIV2UserList)).Methods("GET") + subRouter.HandleFunc("/api/v2/system/info", httppkg.MakeHTTPHandlerFuncV2(apiController.APIV2SystemInfo)).Methods("GET") subRouter.HandleFunc("/api/v2/clients", httppkg.MakeHTTPHandlerFuncV2(apiController.APIV2ClientList)).Methods("GET") v2EncodedPathRouter := subRouter.NewRoute().Subrouter() v2EncodedPathRouter.UseEncodedPath() diff --git a/server/http/controller.go b/server/http/controller.go index 65f6c00f..c1c25725 100644 --- a/server/http/controller.go +++ b/server/http/controller.go @@ -58,8 +58,12 @@ func NewController( // /api/serverinfo func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) { + return c.buildServerInfoResp(), nil +} + +func (c *Controller) buildServerInfoResp() model.ServerInfoResp { serverStats := mem.StatsCollector.GetServer() - svrResp := model.ServerInfoResp{ + return model.ServerInfoResp{ Version: version.Full(), BindPort: c.serverCfg.BindPort, VhostHTTPPort: c.serverCfg.VhostHTTPPort, @@ -80,8 +84,6 @@ func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) { ClientCounts: serverStats.ClientCounts, ProxyTypeCounts: serverStats.ProxyTypeCounts, } - - return svrResp, nil } // /api/clients diff --git a/server/http/controller_v2.go b/server/http/controller_v2.go index 52dcd248..e18154c6 100644 --- a/server/http/controller_v2.go +++ b/server/http/controller_v2.go @@ -48,6 +48,40 @@ var apiV2ProxyTypes = []string{ string(v1.ProxyTypeSUDP), } +// /api/v2/system/info +func (c *Controller) APIV2SystemInfo(ctx *httppkg.Context) (any, error) { + info := c.buildServerInfoResp() + proxyTypeCounts := info.ProxyTypeCounts + if proxyTypeCounts == nil { + proxyTypeCounts = map[string]int64{} + } + + return model.V2SystemInfoResp{ + Version: info.Version, + Config: model.V2SystemInfoConfigResp{ + BindPort: info.BindPort, + VhostHTTPPort: info.VhostHTTPPort, + VhostHTTPSPort: info.VhostHTTPSPort, + TCPMuxHTTPConnectPort: info.TCPMuxHTTPConnectPort, + KCPBindPort: info.KCPBindPort, + QUICBindPort: info.QUICBindPort, + SubdomainHost: info.SubdomainHost, + MaxPoolCount: info.MaxPoolCount, + MaxPortsPerClient: info.MaxPortsPerClient, + HeartbeatTimeout: info.HeartBeatTimeout, + AllowPortsStr: info.AllowPortsStr, + TLSForce: info.TLSForce, + }, + Status: model.V2SystemInfoStatusResp{ + TotalTrafficIn: info.TotalTrafficIn, + TotalTrafficOut: info.TotalTrafficOut, + CurConns: info.CurConns, + ClientCounts: info.ClientCounts, + ProxyTypeCounts: proxyTypeCounts, + }, + }, nil +} + // /api/v2/users func (c *Controller) APIV2UserList(ctx *httppkg.Context) (any, error) { page, pageSize, err := parseV2PageParams(ctx) diff --git a/server/http/controller_v2_test.go b/server/http/controller_v2_test.go index 44b5a6e9..e28cc8dc 100644 --- a/server/http/controller_v2_test.go +++ b/server/http/controller_v2_test.go @@ -25,6 +25,7 @@ import ( "github.com/gorilla/mux" + "github.com/fatedier/frp/pkg/config/types" v1 "github.com/fatedier/frp/pkg/config/v1" "github.com/fatedier/frp/pkg/metrics/mem" httppkg "github.com/fatedier/frp/pkg/util/http" @@ -40,10 +41,14 @@ type v2EnvelopeForTest[T any] struct { } type fakeStatsCollector struct { + server *mem.ServerStats proxies map[string]*mem.ProxyStats } func (f *fakeStatsCollector) GetServer() *mem.ServerStats { + if f.server != nil { + return f.server + } return &mem.ServerStats{ProxyTypeCounts: map[string]int64{}} } @@ -77,6 +82,108 @@ func (f *fakeStatsCollector) ClearOfflineProxies() (int, int) { return 0, len(f.proxies) } +func TestAPIV2SystemInfoEnvelope(t *testing.T) { + oldStatsCollector := mem.StatsCollector + mem.StatsCollector = &fakeStatsCollector{ + server: &mem.ServerStats{ + TotalTrafficIn: 1024, + TotalTrafficOut: 2048, + CurConns: 3, + ClientCounts: 4, + ProxyTypeCounts: map[string]int64{ + "tcp": 2, + "http": 1, + }, + }, + proxies: map[string]*mem.ProxyStats{}, + } + t.Cleanup(func() { + mem.StatsCollector = oldStatsCollector + }) + + controller := NewController(&v1.ServerConfig{ + BindPort: 7000, + VhostHTTPPort: 8080, + VhostHTTPSPort: 8443, + TCPMuxHTTPConnectPort: 9000, + KCPBindPort: 7001, + QUICBindPort: 7002, + SubDomainHost: "example.com", + MaxPortsPerClient: 8, + AllowPorts: []types.PortsRange{ + {Start: 1000, End: 1002}, + {Single: 2000}, + }, + Transport: v1.ServerTransportConfig{ + MaxPoolCount: 5, + HeartbeatTimeout: 90, + TLS: v1.TLSServerConfig{ + Force: true, + }, + }, + }, registry.NewClientRegistry(), serverproxy.NewManager()) + router := newV2TestRouter(controller) + + resp := performRequest(router, "/api/v2/system/info") + if resp.Code != http.StatusOK { + t.Fatalf("status mismatch, want %d got %d", http.StatusOK, resp.Code) + } + + rawResp := decodeResponse[v2EnvelopeForTest[map[string]json.RawMessage]](t, resp) + if rawResp.Code != http.StatusOK || rawResp.Msg != "success" { + t.Fatalf("envelope mismatch: %#v", rawResp) + } + assertRawJSONKeys(t, rawResp.Data, "config", "status", "version") + assertRawJSONKeysFromMessage(t, rawResp.Data["config"], + "allowPortsStr", + "bindPort", + "heartbeatTimeout", + "kcpBindPort", + "maxPoolCount", + "maxPortsPerClient", + "quicBindPort", + "subdomainHost", + "tcpmuxHTTPConnectPort", + "tlsForce", + "vhostHTTPPort", + "vhostHTTPSPort", + ) + assertRawJSONKeysFromMessage(t, rawResp.Data["status"], + "clientCounts", + "curConns", + "proxyTypeCount", + "totalTrafficIn", + "totalTrafficOut", + ) + + systemResp := decodeResponse[v2EnvelopeForTest[model.V2SystemInfoResp]](t, resp) + if systemResp.Data.Version == "" { + t.Fatal("version should be set at top level") + } + if systemResp.Data.Config.BindPort != 7000 || + systemResp.Data.Config.VhostHTTPPort != 8080 || + systemResp.Data.Config.VhostHTTPSPort != 8443 || + systemResp.Data.Config.TCPMuxHTTPConnectPort != 9000 || + systemResp.Data.Config.KCPBindPort != 7001 || + systemResp.Data.Config.QUICBindPort != 7002 || + systemResp.Data.Config.SubdomainHost != "example.com" || + systemResp.Data.Config.MaxPoolCount != 5 || + systemResp.Data.Config.MaxPortsPerClient != 8 || + systemResp.Data.Config.HeartbeatTimeout != 90 || + systemResp.Data.Config.AllowPortsStr != "1000-1002,2000" || + !systemResp.Data.Config.TLSForce { + t.Fatalf("config mismatch: %#v", systemResp.Data.Config) + } + if systemResp.Data.Status.TotalTrafficIn != 1024 || + systemResp.Data.Status.TotalTrafficOut != 2048 || + systemResp.Data.Status.CurConns != 3 || + systemResp.Data.Status.ClientCounts != 4 || + systemResp.Data.Status.ProxyTypeCounts["tcp"] != 2 || + systemResp.Data.Status.ProxyTypeCounts["http"] != 1 { + t.Fatalf("status mismatch: %#v", systemResp.Data.Status) + } +} + func TestAPIV2ClientListEnvelopePaginationAndFilters(t *testing.T) { controller := newV2TestController(t) router := newV2TestRouter(controller) @@ -322,7 +429,26 @@ func TestLegacyAPIResponsesRemainBare(t *testing.T) { controller := newV2TestController(t) router := newV2TestRouter(controller) - resp := performRequest(router, "/api/clients") + resp := performRequest(router, "/api/serverinfo") + var serverInfo model.ServerInfoResp + if err := json.Unmarshal(resp.Body.Bytes(), &serverInfo); err != nil { + t.Fatalf("legacy serverinfo should be a bare object: %v, body: %s", err, resp.Body.String()) + } + if serverInfo.Version == "" { + t.Fatal("legacy serverinfo version should be set") + } + var serverInfoRaw map[string]json.RawMessage + if err := json.Unmarshal(resp.Body.Bytes(), &serverInfoRaw); err != nil { + t.Fatalf("unmarshal legacy serverinfo object failed: %v", err) + } + if _, ok := serverInfoRaw["data"]; ok { + t.Fatalf("legacy serverinfo should not use v2 envelope: %s", resp.Body.String()) + } + if _, ok := serverInfoRaw["config"]; ok { + t.Fatalf("legacy serverinfo should stay flat, got config in: %s", resp.Body.String()) + } + + resp = performRequest(router, "/api/clients") var clients []model.ClientInfoResp if err := json.Unmarshal(resp.Body.Bytes(), &clients); err != nil { t.Fatalf("legacy clients should be a bare array: %v, body: %s", err, resp.Body.String()) @@ -400,12 +526,14 @@ func newV2TestController(t *testing.T) *Controller { func newV2TestRouter(controller *Controller) *mux.Router { router := mux.NewRouter() router.HandleFunc("/api/v2/users", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2UserList)).Methods(http.MethodGet) + router.HandleFunc("/api/v2/system/info", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2SystemInfo)).Methods(http.MethodGet) router.HandleFunc("/api/v2/clients", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2ClientList)).Methods(http.MethodGet) encodedPathRouter := router.NewRoute().Subrouter() encodedPathRouter.UseEncodedPath() encodedPathRouter.HandleFunc("/api/v2/clients/{key}", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2ClientDetail)).Methods(http.MethodGet) router.HandleFunc("/api/v2/proxies", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2ProxyList)).Methods(http.MethodGet) router.HandleFunc("/api/v2/proxies/{name}", httppkg.MakeHTTPHandlerFuncV2(controller.APIV2ProxyDetail)).Methods(http.MethodGet) + router.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(controller.APIServerInfo)).Methods(http.MethodGet) router.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(controller.APIClientList)).Methods(http.MethodGet) router.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(controller.APIProxyByType)).Methods(http.MethodGet) return router @@ -427,3 +555,26 @@ func decodeResponse[T any](t *testing.T, resp *httptest.ResponseRecorder) T { } return out } + +func assertRawJSONKeys(t *testing.T, raw map[string]json.RawMessage, want ...string) { + t.Helper() + + if len(raw) != len(want) { + t.Fatalf("json keys mismatch, want %v got %v", want, raw) + } + for _, key := range want { + if _, ok := raw[key]; !ok { + t.Fatalf("json key %q missing from %v", key, raw) + } + } +} + +func assertRawJSONKeysFromMessage(t *testing.T, raw json.RawMessage, want ...string) { + t.Helper() + + var out map[string]json.RawMessage + if err := json.Unmarshal(raw, &out); err != nil { + t.Fatalf("unmarshal raw json object failed: %v, body: %s", err, string(raw)) + } + assertRawJSONKeys(t, out, want...) +} diff --git a/server/http/model/v2.go b/server/http/model/v2.go index fd394a63..51ba8e2c 100644 --- a/server/http/model/v2.go +++ b/server/http/model/v2.go @@ -21,6 +21,35 @@ type V2PageResp[T any] struct { Items []T `json:"items"` } +type V2SystemInfoResp struct { + Version string `json:"version"` + Config V2SystemInfoConfigResp `json:"config"` + Status V2SystemInfoStatusResp `json:"status"` +} + +type V2SystemInfoConfigResp struct { + BindPort int `json:"bindPort"` + VhostHTTPPort int `json:"vhostHTTPPort"` + VhostHTTPSPort int `json:"vhostHTTPSPort"` + TCPMuxHTTPConnectPort int `json:"tcpmuxHTTPConnectPort"` + KCPBindPort int `json:"kcpBindPort"` + QUICBindPort int `json:"quicBindPort"` + SubdomainHost string `json:"subdomainHost"` + MaxPoolCount int64 `json:"maxPoolCount"` + MaxPortsPerClient int64 `json:"maxPortsPerClient"` + HeartbeatTimeout int64 `json:"heartbeatTimeout"` + AllowPortsStr string `json:"allowPortsStr"` + TLSForce bool `json:"tlsForce"` +} + +type V2SystemInfoStatusResp struct { + TotalTrafficIn int64 `json:"totalTrafficIn"` + TotalTrafficOut int64 `json:"totalTrafficOut"` + CurConns int64 `json:"curConns"` + ClientCounts int64 `json:"clientCounts"` + ProxyTypeCounts map[string]int64 `json:"proxyTypeCount"` +} + type V2UserResp struct { User string `json:"user"` ClientCount int `json:"clientCount"` diff --git a/web/frps/src/api/server.ts b/web/frps/src/api/server.ts index f46f21d3..f6621ca8 100644 --- a/web/frps/src/api/server.ts +++ b/web/frps/src/api/server.ts @@ -2,5 +2,5 @@ import { http } from './http' import type { ServerInfo } from '../types/server' export const getServerInfo = () => { - return http.get('../api/serverinfo') + return http.getV2('../api/v2/system/info') } diff --git a/web/frps/src/types/server.ts b/web/frps/src/types/server.ts index ada31cc5..b4796e90 100644 --- a/web/frps/src/types/server.ts +++ b/web/frps/src/types/server.ts @@ -1,5 +1,10 @@ export interface ServerInfo { version: string + config: ServerInfoConfig + status: ServerInfoStatus +} + +export interface ServerInfoConfig { bindPort: number vhostHTTPPort: number vhostHTTPSPort: number @@ -12,8 +17,9 @@ export interface ServerInfo { heartbeatTimeout: number allowPortsStr: string tlsForce: boolean +} - // Stats +export interface ServerInfoStatus { totalTrafficIn: number totalTrafficOut: number curConns: number diff --git a/web/frps/src/views/ClientDetail.vue b/web/frps/src/views/ClientDetail.vue index 7fbe865f..17a50468 100644 --- a/web/frps/src/views/ClientDetail.vue +++ b/web/frps/src/views/ClientDetail.vue @@ -161,6 +161,7 @@ import { import { getServerInfo } from '../api/server' import ProxyCard from '../components/ProxyCard.vue' import type { ProxyStatsInfo } from '../types/proxy' +import type { ServerInfo } from '../types/server' const route = useRoute() const router = useRouter() @@ -183,16 +184,9 @@ const total = ref(0) let requestSeq = 0 let searchDebounceTimer: number | null = null -type ServerInfoLite = { - vhostHTTPPort: number - vhostHTTPSPort: number - tcpmuxHTTPConnectPort: number - subdomainHost: string -} +let serverInfoPromise: Promise | null = null -let serverInfoPromise: Promise | null = null - -const fetchServerInfo = (): Promise => { +const fetchServerInfo = (): Promise => { if (!serverInfoPromise) { serverInfoPromise = getServerInfo().catch((err) => { serverInfoPromise = null @@ -232,25 +226,33 @@ const convertProxy = async ( } if (type === 'http') { const info = await fetchServerInfo() - if (info && info.vhostHTTPPort) { - return new HTTPProxy(proxy, info.vhostHTTPPort, info.subdomainHost) + if (info && info.config.vhostHTTPPort) { + return new HTTPProxy( + proxy, + info.config.vhostHTTPPort, + info.config.subdomainHost, + ) } return null } if (type === 'https') { const info = await fetchServerInfo() - if (info && info.vhostHTTPSPort) { - return new HTTPSProxy(proxy, info.vhostHTTPSPort, info.subdomainHost) + if (info && info.config.vhostHTTPSPort) { + return new HTTPSProxy( + proxy, + info.config.vhostHTTPSPort, + info.config.subdomainHost, + ) } return null } if (type === 'tcpmux') { const info = await fetchServerInfo() - if (info && info.tcpmuxHTTPConnectPort) { + if (info && info.config.tcpmuxHTTPConnectPort) { return new TCPMuxProxy( proxy, - info.tcpmuxHTTPConnectPort, - info.subdomainHost, + info.config.tcpmuxHTTPConnectPort, + info.config.subdomainHost, ) } return null diff --git a/web/frps/src/views/Proxies.vue b/web/frps/src/views/Proxies.vue index 0571088d..734c85a5 100644 --- a/web/frps/src/views/Proxies.vue +++ b/web/frps/src/views/Proxies.vue @@ -104,6 +104,7 @@ import { } from '../api/proxy' import { getServerInfo } from '../api/server' import type { ProxyStatsInfo } from '../types/proxy' +import type { ServerInfo } from '../types/server' const route = useRoute() const router = useRouter() @@ -133,15 +134,9 @@ let searchDebounceTimer: number | null = null // Server info cache - cache the Promise itself so concurrent first calls // from Promise.all (convertProxies) don't kick off multiple HTTP requests. -type ServerInfoLite = { - vhostHTTPPort: number - vhostHTTPSPort: number - tcpmuxHTTPConnectPort: number - subdomainHost: string -} -let serverInfoPromise: Promise | null = null +let serverInfoPromise: Promise | null = null -const fetchServerInfo = (): Promise => { +const fetchServerInfo = (): Promise => { if (!serverInfoPromise) { serverInfoPromise = getServerInfo().catch((err) => { // Allow retry after failure @@ -164,25 +159,33 @@ const convertProxy = async ( } if (type === 'http') { const info = await fetchServerInfo() - if (info && info.vhostHTTPPort) { - return new HTTPProxy(proxy, info.vhostHTTPPort, info.subdomainHost) + if (info && info.config.vhostHTTPPort) { + return new HTTPProxy( + proxy, + info.config.vhostHTTPPort, + info.config.subdomainHost, + ) } return null } if (type === 'https') { const info = await fetchServerInfo() - if (info && info.vhostHTTPSPort) { - return new HTTPSProxy(proxy, info.vhostHTTPSPort, info.subdomainHost) + if (info && info.config.vhostHTTPSPort) { + return new HTTPSProxy( + proxy, + info.config.vhostHTTPSPort, + info.config.subdomainHost, + ) } return null } if (type === 'tcpmux') { const info = await fetchServerInfo() - if (info && info.tcpmuxHTTPConnectPort) { + if (info && info.config.tcpmuxHTTPConnectPort) { return new TCPMuxProxy( proxy, - info.tcpmuxHTTPConnectPort, - info.subdomainHost, + info.config.tcpmuxHTTPConnectPort, + info.config.subdomainHost, ) } return null diff --git a/web/frps/src/views/ProxyDetail.vue b/web/frps/src/views/ProxyDetail.vue index 07359aae..b4487dd2 100644 --- a/web/frps/src/views/ProxyDetail.vue +++ b/web/frps/src/views/ProxyDetail.vue @@ -254,6 +254,7 @@ import { SUDPProxy, } from '../utils/proxy' import Traffic from '../components/Traffic.vue' +import type { ServerInfo } from '../types/server' const route = useRoute() const router = useRouter() @@ -275,12 +276,7 @@ const goBack = () => { } } -let serverInfo: { - vhostHTTPPort: number - vhostHTTPSPort: number - tcpmuxHTTPConnectPort: number - subdomainHost: string -} | null = null +let serverInfo: ServerInfo | null = null const clientLink = computed(() => { if (!proxy.value) return '' @@ -367,25 +363,30 @@ const fetchProxy = async () => { try { const data = await getProxyByNameV2(name) const info = await fetchServerInfo() + const config = info.config const type = data.type || data.conf?.type || '' if (type === 'tcp') { proxy.value = new TCPProxy(data) } else if (type === 'udp') { proxy.value = new UDPProxy(data) - } else if (type === 'http' && info?.vhostHTTPPort) { - proxy.value = new HTTPProxy(data, info.vhostHTTPPort, info.subdomainHost) - } else if (type === 'https' && info?.vhostHTTPSPort) { + } else if (type === 'http' && config.vhostHTTPPort) { + proxy.value = new HTTPProxy( + data, + config.vhostHTTPPort, + config.subdomainHost, + ) + } else if (type === 'https' && config.vhostHTTPSPort) { proxy.value = new HTTPSProxy( data, - info.vhostHTTPSPort, - info.subdomainHost, + config.vhostHTTPSPort, + config.subdomainHost, ) - } else if (type === 'tcpmux' && info?.tcpmuxHTTPConnectPort) { + } else if (type === 'tcpmux' && config.tcpmuxHTTPConnectPort) { proxy.value = new TCPMuxProxy( data, - info.tcpmuxHTTPConnectPort, - info.subdomainHost, + config.tcpmuxHTTPConnectPort, + config.subdomainHost, ) } else if (type === 'stcp') { proxy.value = new STCPProxy(data) diff --git a/web/frps/src/views/ServerOverview.vue b/web/frps/src/views/ServerOverview.vue index ed3c4a64..5e38e1e5 100644 --- a/web/frps/src/views/ServerOverview.vue +++ b/web/frps/src/views/ServerOverview.vue @@ -4,7 +4,7 @@ @@ -54,7 +54,7 @@
Inbound
- {{ formatFileSize(data.totalTrafficIn) }} + {{ formatFileSize(data.status.totalTrafficIn) }}
@@ -66,7 +66,7 @@
Outbound
- {{ formatFileSize(data.totalTrafficOut) }} + {{ formatFileSize(data.status.totalTrafficOut) }}
@@ -83,7 +83,7 @@
Bind Port - {{ data.bindPort }} + {{ data.config.bindPort }}
-
+
KCP Port - {{ data.kcpBindPort }} + {{ data.config.kcpBindPort }}
-
+
QUIC Port - {{ data.quicBindPort }} + {{ data.config.quicBindPort }}
-
+
HTTP Port - {{ data.vhostHTTPPort }} + {{ data.config.vhostHTTPPort }}
-
+
HTTPS Port - {{ data.vhostHTTPSPort }} + {{ data.config.vhostHTTPSPort }}
-
+
TCPMux Port - {{ data.tcpmuxHTTPConnectPort }} + {{ data.config.tcpmuxHTTPConnectPort }}
-
+
Subdomain Host - {{ data.subdomainHost }} + {{ data.config.subdomainHost }}
Max Pool Count - {{ data.maxPoolCount }} + {{ data.config.maxPoolCount }}
Max Ports/Client - {{ data.maxPortsPerClient }} + {{ maxPortsPerClientLabel }}
-
+
Allow Ports - {{ data.allowPortsStr }} + {{ data.config.allowPortsStr }}
-
+
TLS Force Enabled
Heartbeat Timeout - {{ data.heartbeatTimeout }}s + {{ data.config.heartbeatTimeout }}s
@@ -167,69 +167,59 @@ import { formatFileSize } from '../utils/format' import { Download, Upload } from '@element-plus/icons-vue' import StatCard from '../components/StatCard.vue' import { getServerInfo } from '../api/server' +import type { ServerInfo } from '../types/server' -const data = ref({ +const data = ref({ version: '', - bindPort: 0, - kcpBindPort: 0, - quicBindPort: 0, - vhostHTTPPort: 0, - vhostHTTPSPort: 0, - tcpmuxHTTPConnectPort: 0, - subdomainHost: '', - maxPoolCount: 0, - maxPortsPerClient: '', - allowPortsStr: '', - tlsForce: false, - heartbeatTimeout: 0, - clientCounts: 0, - curConns: 0, - proxyCounts: 0, - totalTrafficIn: 0, - totalTrafficOut: 0, - proxyTypeCounts: {} as Record, + config: { + bindPort: 0, + kcpBindPort: 0, + quicBindPort: 0, + vhostHTTPPort: 0, + vhostHTTPSPort: 0, + tcpmuxHTTPConnectPort: 0, + subdomainHost: '', + maxPoolCount: 0, + maxPortsPerClient: 0, + allowPortsStr: '', + tlsForce: false, + heartbeatTimeout: 0, + }, + status: { + clientCounts: 0, + curConns: 0, + totalTrafficIn: 0, + totalTrafficOut: 0, + proxyTypeCount: {}, + }, }) const hasActiveProxies = computed(() => { - return Object.values(data.value.proxyTypeCounts).some((c) => c > 0) + return Object.values(data.value.status.proxyTypeCount).some((c) => c > 0) +}) + +const proxyCounts = computed(() => { + return Object.values(data.value.status.proxyTypeCount).reduce( + (sum, count) => sum + (count || 0), + 0, + ) +}) + +const maxPortsPerClientLabel = computed(() => { + const value = data.value.config.maxPortsPerClient + return value === 0 ? 'no limit' : String(value) }) const formatTrafficTotal = () => { - const total = data.value.totalTrafficIn + data.value.totalTrafficOut + const total = + data.value.status.totalTrafficIn + data.value.status.totalTrafficOut return formatFileSize(total) } const fetchData = async () => { try { const json = await getServerInfo() - data.value.version = json.version - data.value.bindPort = json.bindPort - data.value.kcpBindPort = json.kcpBindPort - data.value.quicBindPort = json.quicBindPort - data.value.vhostHTTPPort = json.vhostHTTPPort - data.value.vhostHTTPSPort = json.vhostHTTPSPort - data.value.tcpmuxHTTPConnectPort = json.tcpmuxHTTPConnectPort - data.value.subdomainHost = json.subdomainHost - data.value.maxPoolCount = json.maxPoolCount - data.value.maxPortsPerClient = String(json.maxPortsPerClient) - if (data.value.maxPortsPerClient == '0') { - data.value.maxPortsPerClient = 'no limit' - } - data.value.allowPortsStr = json.allowPortsStr - data.value.tlsForce = json.tlsForce - data.value.heartbeatTimeout = json.heartbeatTimeout - data.value.clientCounts = json.clientCounts - data.value.curConns = json.curConns - data.value.totalTrafficIn = json.totalTrafficIn - data.value.totalTrafficOut = json.totalTrafficOut - data.value.proxyTypeCounts = json.proxyTypeCount || {} - - data.value.proxyCounts = 0 - if (json.proxyTypeCount != null) { - Object.values(json.proxyTypeCount).forEach((count: any) => { - data.value.proxyCounts += count || 0 - }) - } + data.value = json } catch { ElMessage({ showClose: true,