feat: add system info API v2 (#5394)
golangci-lint / lint (push) Has been cancelled

This commit is contained in:
fatedier
2026-07-07 02:00:44 +08:00
committed by GitHub
parent 7fe152e3aa
commit 5876beceac
11 changed files with 344 additions and 125 deletions
+1
View File
@@ -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()
+5 -3
View File
@@ -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
+34
View File
@@ -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)
+152 -1
View File
@@ -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...)
}
+29
View File
@@ -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"`
+1 -1
View File
@@ -2,5 +2,5 @@ import { http } from './http'
import type { ServerInfo } from '../types/server'
export const getServerInfo = () => {
return http.get<ServerInfo>('../api/serverinfo')
return http.getV2<ServerInfo>('../api/v2/system/info')
}
+7 -1
View File
@@ -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
+18 -16
View File
@@ -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<ServerInfo> | null = null
let serverInfoPromise: Promise<ServerInfoLite> | null = null
const fetchServerInfo = (): Promise<ServerInfoLite> => {
const fetchServerInfo = (): Promise<ServerInfo> => {
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
+18 -15
View File
@@ -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<ServerInfoLite> | null = null
let serverInfoPromise: Promise<ServerInfo> | null = null
const fetchServerInfo = (): Promise<ServerInfoLite> => {
const fetchServerInfo = (): Promise<ServerInfo> => {
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
+15 -14
View File
@@ -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)
+64 -74
View File
@@ -4,7 +4,7 @@
<el-col :xs="24" :sm="12" :lg="6">
<StatCard
label="Clients"
:value="data.clientCounts"
:value="data.status.clientCounts"
type="clients"
subtitle="Connected clients"
to="/clients"
@@ -13,7 +13,7 @@
<el-col :xs="24" :sm="12" :lg="6">
<StatCard
label="Proxies"
:value="data.proxyCounts"
:value="proxyCounts"
type="proxies"
subtitle="Active proxies"
to="/proxies/tcp"
@@ -22,7 +22,7 @@
<el-col :xs="24" :sm="12" :lg="6">
<StatCard
label="Connections"
:value="data.curConns"
:value="data.status.curConns"
type="connections"
subtitle="Current connections"
/>
@@ -54,7 +54,7 @@
<div class="traffic-info">
<div class="label">Inbound</div>
<div class="value">
{{ formatFileSize(data.totalTrafficIn) }}
{{ formatFileSize(data.status.totalTrafficIn) }}
</div>
</div>
</div>
@@ -66,7 +66,7 @@
<div class="traffic-info">
<div class="label">Outbound</div>
<div class="value">
{{ formatFileSize(data.totalTrafficOut) }}
{{ formatFileSize(data.status.totalTrafficOut) }}
</div>
</div>
</div>
@@ -83,7 +83,7 @@
</template>
<div class="proxy-types-grid">
<div
v-for="(count, type) in data.proxyTypeCounts"
v-for="(count, type) in data.status.proxyTypeCount"
:key="type"
class="proxy-type-item"
v-show="count > 0"
@@ -109,51 +109,51 @@
<div class="config-grid">
<div class="config-item">
<span class="config-label">Bind Port</span>
<span class="config-value">{{ data.bindPort }}</span>
<span class="config-value">{{ data.config.bindPort }}</span>
</div>
<div class="config-item" v-if="data.kcpBindPort != 0">
<div class="config-item" v-if="data.config.kcpBindPort != 0">
<span class="config-label">KCP Port</span>
<span class="config-value">{{ data.kcpBindPort }}</span>
<span class="config-value">{{ data.config.kcpBindPort }}</span>
</div>
<div class="config-item" v-if="data.quicBindPort != 0">
<div class="config-item" v-if="data.config.quicBindPort != 0">
<span class="config-label">QUIC Port</span>
<span class="config-value">{{ data.quicBindPort }}</span>
<span class="config-value">{{ data.config.quicBindPort }}</span>
</div>
<div class="config-item" v-if="data.vhostHTTPPort != 0">
<div class="config-item" v-if="data.config.vhostHTTPPort != 0">
<span class="config-label">HTTP Port</span>
<span class="config-value">{{ data.vhostHTTPPort }}</span>
<span class="config-value">{{ data.config.vhostHTTPPort }}</span>
</div>
<div class="config-item" v-if="data.vhostHTTPSPort != 0">
<div class="config-item" v-if="data.config.vhostHTTPSPort != 0">
<span class="config-label">HTTPS Port</span>
<span class="config-value">{{ data.vhostHTTPSPort }}</span>
<span class="config-value">{{ data.config.vhostHTTPSPort }}</span>
</div>
<div class="config-item" v-if="data.tcpmuxHTTPConnectPort != 0">
<div class="config-item" v-if="data.config.tcpmuxHTTPConnectPort != 0">
<span class="config-label">TCPMux Port</span>
<span class="config-value">{{ data.tcpmuxHTTPConnectPort }}</span>
<span class="config-value">{{ data.config.tcpmuxHTTPConnectPort }}</span>
</div>
<div class="config-item" v-if="data.subdomainHost != ''">
<div class="config-item" v-if="data.config.subdomainHost != ''">
<span class="config-label">Subdomain Host</span>
<span class="config-value">{{ data.subdomainHost }}</span>
<span class="config-value">{{ data.config.subdomainHost }}</span>
</div>
<div class="config-item">
<span class="config-label">Max Pool Count</span>
<span class="config-value">{{ data.maxPoolCount }}</span>
<span class="config-value">{{ data.config.maxPoolCount }}</span>
</div>
<div class="config-item">
<span class="config-label">Max Ports/Client</span>
<span class="config-value">{{ data.maxPortsPerClient }}</span>
<span class="config-value">{{ maxPortsPerClientLabel }}</span>
</div>
<div class="config-item" v-if="data.allowPortsStr != ''">
<div class="config-item" v-if="data.config.allowPortsStr != ''">
<span class="config-label">Allow Ports</span>
<span class="config-value">{{ data.allowPortsStr }}</span>
<span class="config-value">{{ data.config.allowPortsStr }}</span>
</div>
<div class="config-item" v-if="data.tlsForce">
<div class="config-item" v-if="data.config.tlsForce">
<span class="config-label">TLS Force</span>
<el-tag size="small" type="warning">Enabled</el-tag>
</div>
<div class="config-item">
<span class="config-label">Heartbeat Timeout</span>
<span class="config-value">{{ data.heartbeatTimeout }}s</span>
<span class="config-value">{{ data.config.heartbeatTimeout }}s</span>
</div>
</div>
</el-card>
@@ -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<ServerInfo>({
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<string, number>,
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,