Compare commits

...

5 Commits

Author SHA1 Message Date
217f1fb2fe
Merge b57bba48a9a35fdbea1a620260a7cb5a71a65354 into e8ace492a5d9a2c3f9d9a2d3037b2e0ead058f62 2024-02-01 00:10:10 -07:00
fatedier
e8ace492a5
bump version to 0.54.0 (#3967) 2024-02-01 12:52:25 +08:00
fatedier
1c8bc0bfa8
make the host/domain matching case-insensitive (#3966) 2024-02-01 11:53:52 +08:00
fatedier
b31c67d7c0
web: support to clear offline proxies data on dashboard (#3963) 2024-02-01 10:54:57 +08:00
b57bba48a9
Update README.md for TLS configuration
Modified the configuration in the tls configuration to better connect with the context.
Added precautions in the certificate configuration below
2024-01-21 21:19:27 +08:00
44 changed files with 2022 additions and 2941 deletions

View File

@ -644,8 +644,8 @@ To **enforce** `frps` to only accept TLS connections - configure `transport.tls.
```toml ```toml
transport.tls.enable = true transport.tls.enable = true
transport.tls.certFile = "certificate.crt" transport.tls.certFile = "client.crt"
transport.tls.keyFile = "certificate.key" transport.tls.keyFile = "client.key"
transport.tls.trustedCaFile = "ca.crt" transport.tls.trustedCaFile = "ca.crt"
``` ```
@ -653,8 +653,8 @@ transport.tls.trustedCaFile = "ca.crt"
```toml ```toml
transport.tls.force = true transport.tls.force = true
transport.tls.certFile = "certificate.crt" transport.tls.certFile = "server.crt"
transport.tls.keyFile = "certificate.key" transport.tls.keyFile = "server.key"
transport.tls.trustedCaFile = "ca.crt" transport.tls.trustedCaFile = "ca.crt"
``` ```
@ -714,6 +714,11 @@ openssl x509 -req -days 365 -sha256 \
-extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1,DNS:example.server.com") \ -extfile <(printf "subjectAltName=DNS:localhost,IP:127.0.0.1,DNS:example.server.com") \
-out server.crt -out server.crt
``` ```
**Note: The above IP must be changed to your server IP, otherwise an error message will be displayed:**
```
tls: failed to verify certificate: x509: certificate is valid for 127.0.0.1, not your_ IP
```
* build frpc certificates * build frpc certificates
``` ```

View File

@ -1,3 +1,11 @@
### Deprecation Notices ### Deprecation Notices
* Using an underscore in a flag name is deprecated and has been replaced by a hyphen. The underscore format will remain compatible for some time, until it is completely removed in a future version. For example, `--remote_port` is replaced with `--remote-port`. * Using an underscore in a flag name is deprecated and has been replaced by a hyphen. The underscore format will remain compatible for some time, until it is completely removed in a future version. For example, `--remote_port` is replaced with `--remote-port`.
### Features
* The `Refresh` and `ClearOfflineProxies` buttons have been added to the Dashboard of frps.
### Fixes
* The host/domain matching in the routing rules has been changed to be case-insensitive.

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,13 +4,12 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>frp client admin UI</title> <title>frp client admin UI</title>
<script type="module" crossorigin src="./index-1c7ed8b0.js"></script> <script type="module" crossorigin src="./index-bLBhaJo8.js"></script>
<link rel="stylesheet" href="./index-1e2a7ce0.css"> <link rel="stylesheet" crossorigin href="./index-iuf46MlF.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
</body> </body>
</html> </html>

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

File diff suppressed because one or more lines are too long

View File

@ -4,13 +4,12 @@
<head> <head>
<meta charset="utf-8"> <meta charset="utf-8">
<title>frps dashboard</title> <title>frps dashboard</title>
<script type="module" crossorigin src="./index-c322b7dd.js"></script> <script type="module" crossorigin src="./index-1gecbKzv.js"></script>
<link rel="stylesheet" href="./index-1e0c7400.css"> <link rel="stylesheet" crossorigin href="./index-Lf6B06jY.css">
</head> </head>
<body> <body>
<div id="app"></div> <div id="app"></div>
</body> </body>
</html> </html>

2
go.mod
View File

@ -18,7 +18,7 @@ require (
github.com/pion/stun v0.6.1 github.com/pion/stun v0.6.1
github.com/pires/go-proxyproto v0.7.0 github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.16.0 github.com/prometheus/client_golang v1.16.0
github.com/quic-go/quic-go v0.37.4 github.com/quic-go/quic-go v0.37.7
github.com/rodaine/table v1.1.0 github.com/rodaine/table v1.1.0
github.com/samber/lo v1.38.1 github.com/samber/lo v1.38.1
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0

4
go.sum
View File

@ -119,8 +119,8 @@ github.com/prometheus/procfs v0.10.1 h1:kYK1Va/YMlutzCGazswoHKo//tZVlFpKYh+Pymzi
github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM= github.com/prometheus/procfs v0.10.1/go.mod h1:nwNm2aOCAYw8uTR/9bWRREkZFxAUcWzPHWJq+XBB/FM=
github.com/quic-go/qtls-go1-20 v0.3.1 h1:O4BLOM3hwfVF3AcktIylQXyl7Yi2iBNVy5QsV+ySxbg= github.com/quic-go/qtls-go1-20 v0.3.1 h1:O4BLOM3hwfVF3AcktIylQXyl7Yi2iBNVy5QsV+ySxbg=
github.com/quic-go/qtls-go1-20 v0.3.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= github.com/quic-go/qtls-go1-20 v0.3.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.37.4 h1:ke8B73yMCWGq9MfrCCAw0Uzdm7GaViC3i39dsIdDlH4= github.com/quic-go/quic-go v0.37.7 h1:AgKsQLZ1+YCwZd2GYhBUsJDYZwEkA5gENtAjb+MxONU=
github.com/quic-go/quic-go v0.37.4/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU= github.com/quic-go/quic-go v0.37.7/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU=
github.com/rodaine/table v1.1.0 h1:/fUlCSdjamMY8VifdQRIu3VWZXYLY7QHFkVorS8NTr4= github.com/rodaine/table v1.1.0 h1:/fUlCSdjamMY8VifdQRIu3VWZXYLY7QHFkVorS8NTr4=
github.com/rodaine/table v1.1.0/go.mod h1:Qu3q5wi1jTQD6B6HsP6szie/S4w1QUQ8pq22pz9iL8g= github.com/rodaine/table v1.1.0/go.mod h1:Qu3q5wi1jTQD6B6HsP6szie/S4w1QUQ8pq22pz9iL8g=
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M= github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=

View File

@ -61,23 +61,23 @@ func (m *serverMetrics) run() {
for { for {
time.Sleep(12 * time.Hour) time.Sleep(12 * time.Hour)
start := time.Now() start := time.Now()
count, total := m.clearUselessInfo() count, total := m.clearUselessInfo(time.Duration(7*24) * time.Hour)
log.Debug("clear useless proxy statistics data count %d/%d, cost %v", count, total, time.Since(start)) log.Debug("clear useless proxy statistics data count %d/%d, cost %v", count, total, time.Since(start))
} }
}() }()
} }
func (m *serverMetrics) clearUselessInfo() (int, int) { func (m *serverMetrics) clearUselessInfo(continuousOfflineDuration time.Duration) (int, int) {
count := 0 count := 0
total := 0 total := 0
// To check if there are proxies that closed than 7 days and drop them. // To check if there are any proxies that have been closed for more than continuousOfflineDuration and remove them.
m.mu.Lock() m.mu.Lock()
defer m.mu.Unlock() defer m.mu.Unlock()
total = len(m.info.ProxyStatistics) total = len(m.info.ProxyStatistics)
for name, data := range m.info.ProxyStatistics { for name, data := range m.info.ProxyStatistics {
if !data.LastCloseTime.IsZero() && if !data.LastCloseTime.IsZero() &&
data.LastStartTime.Before(data.LastCloseTime) && data.LastStartTime.Before(data.LastCloseTime) &&
time.Since(data.LastCloseTime) > time.Duration(7*24)*time.Hour { time.Since(data.LastCloseTime) > continuousOfflineDuration {
delete(m.info.ProxyStatistics, name) delete(m.info.ProxyStatistics, name)
count++ count++
log.Trace("clear proxy [%s]'s statistics data, lastCloseTime: [%s]", name, data.LastCloseTime.String()) log.Trace("clear proxy [%s]'s statistics data, lastCloseTime: [%s]", name, data.LastCloseTime.String())
@ -86,6 +86,10 @@ func (m *serverMetrics) clearUselessInfo() (int, int) {
return count, total return count, total
} }
func (m *serverMetrics) ClearOfflineProxies() (int, int) {
return m.clearUselessInfo(0)
}
func (m *serverMetrics) NewClient() { func (m *serverMetrics) NewClient() {
m.info.ClientCounts.Inc(1) m.info.ClientCounts.Inc(1)
} }

View File

@ -79,4 +79,5 @@ type Collector interface {
GetProxiesByType(proxyType string) []*ProxyStats GetProxiesByType(proxyType string) []*ProxyStats
GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats
GetProxyTraffic(name string) *ProxyTrafficInfo GetProxyTraffic(name string) *ProxyTrafficInfo
ClearOfflineProxies() (int, int)
} }

View File

@ -19,7 +19,7 @@ import (
"strings" "strings"
) )
var version = "0.53.2" var version = "0.54.0"
func Full() string { func Full() string {
return version return version

View File

@ -33,6 +33,8 @@ func NewRouters() *Routers {
} }
func (r *Routers) Add(domain, location, httpUser string, payload interface{}) error { func (r *Routers) Add(domain, location, httpUser string, payload interface{}) error {
domain = strings.ToLower(domain)
r.mutex.Lock() r.mutex.Lock()
defer r.mutex.Unlock() defer r.mutex.Unlock()
@ -64,6 +66,8 @@ func (r *Routers) Add(domain, location, httpUser string, payload interface{}) er
} }
func (r *Routers) Del(domain, location, httpUser string) { func (r *Routers) Del(domain, location, httpUser string) {
domain = strings.ToLower(domain)
r.mutex.Lock() r.mutex.Lock()
defer r.mutex.Unlock() defer r.mutex.Unlock()
@ -86,6 +90,8 @@ func (r *Routers) Del(domain, location, httpUser string) {
} }
func (r *Routers) Get(host, path, httpUser string) (vr *Router, exist bool) { func (r *Routers) Get(host, path, httpUser string) (vr *Router, exist bool) {
host = strings.ToLower(host)
r.mutex.RLock() r.mutex.RLock()
defer r.mutex.RUnlock() defer r.mutex.RUnlock()

View File

@ -17,6 +17,7 @@ package server
import ( import (
"encoding/json" "encoding/json"
"net/http" "net/http"
"sort"
"github.com/gorilla/mux" "github.com/gorilla/mux"
"github.com/prometheus/client_golang/prometheus/promhttp" "github.com/prometheus/client_golang/prometheus/promhttp"
@ -53,6 +54,7 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET") subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET")
subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET") subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET")
subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET") subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
subRouter.HandleFunc("/api/proxies", svr.deleteProxies).Methods("DELETE")
// view // view
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET") subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
@ -226,6 +228,9 @@ func (svr *Service) apiProxyByType(w http.ResponseWriter, r *http.Request) {
proxyInfoResp := GetProxyInfoResp{} proxyInfoResp := GetProxyInfoResp{}
proxyInfoResp.Proxies = svr.getProxyStatsByType(proxyType) proxyInfoResp.Proxies = svr.getProxyStatsByType(proxyType)
sort.Slice(proxyInfoResp.Proxies, func(i, j int) bool {
return proxyInfoResp.Proxies[i].Name < proxyInfoResp.Proxies[j].Name
})
buf, _ := json.Marshal(&proxyInfoResp) buf, _ := json.Marshal(&proxyInfoResp)
res.Msg = string(buf) res.Msg = string(buf)
@ -376,3 +381,26 @@ func (svr *Service) apiProxyTraffic(w http.ResponseWriter, r *http.Request) {
buf, _ := json.Marshal(&trafficResp) buf, _ := json.Marshal(&trafficResp)
res.Msg = string(buf) res.Msg = string(buf)
} }
// DELETE /api/proxies?status=offline
func (svr *Service) deleteProxies(w http.ResponseWriter, r *http.Request) {
res := GeneralResponse{Code: 200}
log.Info("Http request: [%s]", r.URL.Path)
defer func() {
log.Info("Http response [%s]: code [%d]", r.URL.Path, res.Code)
w.WriteHeader(res.Code)
if len(res.Msg) > 0 {
_, _ = w.Write([]byte(res.Msg))
}
}()
status := r.URL.Query().Get("status")
if status != "offline" {
res.Code = 400
res.Msg = "status only support offline"
return
}
cleared, total := mem.StatsCollector.ClearOfflineProxies()
log.Info("cleared [%d] offline proxies, total [%d] proxies", cleared, total)
}

View File

@ -1,6 +1,7 @@
/* eslint-disable */ /* eslint-disable */
/* prettier-ignore */ /* prettier-ignore */
// @ts-nocheck // @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import // Generated by unplugin-auto-import
export {} export {}
declare global { declare global {

View File

@ -3,11 +3,9 @@
// @ts-nocheck // @ts-nocheck
// Generated by unplugin-vue-components // Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {} export {}
declare module '@vue/runtime-core' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ClientConfigure: typeof import('./src/components/ClientConfigure.vue')['default'] ClientConfigure: typeof import('./src/components/ClientConfigure.vue')['default']
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']

View File

@ -1,6 +1,6 @@
{ {
"name": "-frpc-dashboard", "name": "frpc-dashboard",
"version": "0.0.0", "version": "0.0.1",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite",
@ -11,25 +11,25 @@
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore" "lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"element-plus": "^2.3.3", "element-plus": "^2.5.3",
"vue": "^3.2.47", "vue": "^3.4.15",
"vue-router": "^4.1.6" "vue-router": "^4.2.5"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.1.4", "@rushstack/eslint-patch": "^1.7.2",
"@types/node": "^18.11.12", "@types/node": "^18.11.12",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^11.0.0", "@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.5.1",
"eslint": "^8.22.0", "eslint": "^8.56.0",
"eslint-plugin-vue": "^9.3.0", "eslint-plugin-vue": "^9.21.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.7.1", "prettier": "^3.2.4",
"typescript": "~4.7.4", "typescript": "~5.3.3",
"unplugin-auto-import": "^0.14.3", "unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.24.0", "unplugin-vue-components": "^0.26.0",
"vite": "^4.0.0", "vite": "^5.0.12",
"vue-tsc": "^1.0.12" "vue-tsc": "^1.8.27"
} }
} }

View File

@ -1,8 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

View File

@ -1,16 +1,25 @@
{ {
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "target": "ES2020",
"paths": { "useDefineForClassFields": true,
"@/*": ["./src/*"] "module": "ESNext",
} "lib": ["ES2020", "DOM", "DOM.Iterable"],
}, "skipLibCheck": true,
"references": [ /* Bundler mode */
{ "moduleResolution": "bundler",
"path": "./tsconfig.config.json" "allowImportingTsExtensions": true,
} "resolveJsonModule": true,
] "isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

File diff suppressed because it is too large Load Diff

View File

@ -1,4 +1,8 @@
// Generated by 'unplugin-auto-import' /* eslint-disable */
/* prettier-ignore */
// @ts-nocheck
// noinspection JSUnusedGlobalSymbols
// Generated by unplugin-auto-import
export {} export {}
declare global { declare global {

View File

@ -1,11 +1,11 @@
// generated by unplugin-vue-components /* eslint-disable */
// We suggest you to commit this file into source control /* prettier-ignore */
// @ts-nocheck
// Generated by unplugin-vue-components
// Read more: https://github.com/vuejs/core/pull/3399 // Read more: https://github.com/vuejs/core/pull/3399
import '@vue/runtime-core'
export {} export {}
declare module '@vue/runtime-core' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
@ -13,6 +13,8 @@ declare module '@vue/runtime-core' {
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import('element-plus/es')['ElFormItem']
ElMenu: typeof import('element-plus/es')['ElMenu'] ElMenu: typeof import('element-plus/es')['ElMenu']
ElMenuItem: typeof import('element-plus/es')['ElMenuItem'] ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
ElPageHeader: typeof import('element-plus/es')['ElPageHeader']
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
ElPopover: typeof import('element-plus/es')['ElPopover'] ElPopover: typeof import('element-plus/es')['ElPopover']
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']
ElSubMenu: typeof import('element-plus/es')['ElSubMenu'] ElSubMenu: typeof import('element-plus/es')['ElSubMenu']

View File

@ -12,27 +12,27 @@
}, },
"dependencies": { "dependencies": {
"@types/humanize-plus": "^1.8.0", "@types/humanize-plus": "^1.8.0",
"echarts": "^5.4.1", "echarts": "^5.4.3",
"element-plus": "^2.3.3", "element-plus": "^2.5.3",
"humanize-plus": "^1.8.2", "humanize-plus": "^1.8.2",
"vue": "^3.2.47", "vue": "^3.4.15",
"vue-router": "^4.1.6" "vue-router": "^4.2.5"
}, },
"devDependencies": { "devDependencies": {
"@rushstack/eslint-patch": "^1.1.4", "@rushstack/eslint-patch": "^1.7.2",
"@types/node": "^18.11.12", "@types/node": "^18.11.12",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^5.0.3",
"@vue/eslint-config-prettier": "^7.0.0", "@vue/eslint-config-prettier": "^9.0.0",
"@vue/eslint-config-typescript": "^11.0.0", "@vue/eslint-config-typescript": "^12.0.0",
"@vue/tsconfig": "^0.1.3", "@vue/tsconfig": "^0.5.1",
"eslint": "^8.22.0", "eslint": "^8.56.0",
"eslint-plugin-vue": "^9.3.0", "eslint-plugin-vue": "^9.21.0",
"npm-run-all": "^4.1.5", "npm-run-all": "^4.1.5",
"prettier": "^2.7.1", "prettier": "^3.2.4",
"typescript": "~4.7.4", "typescript": "~5.3.3",
"unplugin-auto-import": "^0.13.0", "unplugin-auto-import": "^0.17.5",
"unplugin-vue-components": "^0.23.0", "unplugin-vue-components": "^0.26.0",
"vite": "^4.0.4", "vite": "^5.0.12",
"vue-tsc": "^1.0.12" "vue-tsc": "^1.8.27"
} }
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<ProxyView :proxies="proxies" proxyType="http" /> <ProxyView :proxies="proxies" proxyType="http" @refresh="fetchData"/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -27,6 +27,7 @@ const fetchData = () => {
return res.json() return res.json()
}) })
.then((json) => { .then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) { for (let proxyStats of json.proxies) {
proxies.value.push( proxies.value.push(
new HTTPProxy(proxyStats, vhostHTTPPort, subdomainHost) new HTTPProxy(proxyStats, vhostHTTPPort, subdomainHost)

View File

@ -1,5 +1,5 @@
<template> <template>
<ProxyView :proxies="proxies" proxyType="https" /> <ProxyView :proxies="proxies" proxyType="https" @refresh="fetchData"/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -27,6 +27,7 @@ const fetchData = () => {
return res.json() return res.json()
}) })
.then((json) => { .then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) { for (let proxyStats of json.proxies) {
proxies.value.push( proxies.value.push(
new HTTPSProxy(proxyStats, vhostHTTPSPort, subdomainHost) new HTTPSProxy(proxyStats, vhostHTTPSPort, subdomainHost)

View File

@ -1,5 +1,5 @@
<template> <template>
<ProxyView :proxies="proxies" proxyType="stcp" /> <ProxyView :proxies="proxies" proxyType="stcp" @refresh="fetchData"/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -15,6 +15,7 @@ const fetchData = () => {
return res.json() return res.json()
}) })
.then((json) => { .then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) { for (let proxyStats of json.proxies) {
proxies.value.push(new STCPProxy(proxyStats)) proxies.value.push(new STCPProxy(proxyStats))
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<ProxyView :proxies="proxies" proxyType="sudp" /> <ProxyView :proxies="proxies" proxyType="sudp" @refresh="fetchData"/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -15,6 +15,7 @@ const fetchData = () => {
return res.json() return res.json()
}) })
.then((json) => { .then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) { for (let proxyStats of json.proxies) {
proxies.value.push(new SUDPProxy(proxyStats)) proxies.value.push(new SUDPProxy(proxyStats))
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<ProxyView :proxies="proxies" proxyType="tcp" /> <ProxyView :proxies="proxies" proxyType="tcp" @refresh="fetchData" />
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -15,6 +15,7 @@ const fetchData = () => {
return res.json() return res.json()
}) })
.then((json) => { .then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) { for (let proxyStats of json.proxies) {
proxies.value.push(new TCPProxy(proxyStats)) proxies.value.push(new TCPProxy(proxyStats))
} }

View File

@ -1,5 +1,5 @@
<template> <template>
<ProxyView :proxies="proxies" proxyType="udp" /> <ProxyView :proxies="proxies" proxyType="udp" @refresh="fetchData"/>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
@ -15,12 +15,12 @@ const fetchData = () => {
return res.json() return res.json()
}) })
.then((json) => { .then((json) => {
proxies.value = []
for (let proxyStats of json.proxies) { for (let proxyStats of json.proxies) {
proxies.value.push(new UDPProxy(proxyStats)) proxies.value.push(new UDPProxy(proxyStats))
} }
}) })
} }
fetchData() fetchData()
</script> </script>

View File

@ -1,5 +1,28 @@
<template> <template>
<div> <div>
<el-page-header
:icon="null"
style="width: 100%; margin-left: 30px; margin-bottom: 20px"
>
<template #title>
<span>{{ proxyType }}</span>
</template>
<template #content> </template>
<template #extra>
<div class="flex items-center" style="margin-right: 30px">
<el-popconfirm
title="Are you sure to clear all data of offline proxies?"
@confirm="clearOfflineProxies"
>
<template #reference>
<el-button>ClearOfflineProxies</el-button>
</template>
</el-popconfirm>
<el-button @click="$emit('refresh')">Refresh</el-button>
</div>
</template>
</el-page-header>
<el-table <el-table
:data="proxies" :data="proxies"
:default-sort="{ prop: 'name', order: 'ascending' }" :default-sort="{ prop: 'name', order: 'ascending' }"
@ -67,6 +90,7 @@
import * as Humanize from 'humanize-plus' import * as Humanize from 'humanize-plus'
import type { TableColumnCtx } from 'element-plus' import type { TableColumnCtx } from 'element-plus'
import type { BaseProxy } from '../utils/proxy.js' import type { BaseProxy } from '../utils/proxy.js'
import { ElMessage } from 'element-plus'
import ProxyViewExpand from './ProxyViewExpand.vue' import ProxyViewExpand from './ProxyViewExpand.vue'
defineProps<{ defineProps<{
@ -74,6 +98,8 @@ defineProps<{
proxyType: string proxyType: string
}>() }>()
const emit = defineEmits(['refresh'])
const formatTrafficIn = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => { const formatTrafficIn = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
return Humanize.fileSize(row.trafficIn) return Humanize.fileSize(row.trafficIn)
} }
@ -81,4 +107,37 @@ const formatTrafficIn = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
const formatTrafficOut = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => { const formatTrafficOut = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
return Humanize.fileSize(row.trafficOut) return Humanize.fileSize(row.trafficOut)
} }
const clearOfflineProxies = () => {
fetch('/api/proxies?status=offline', {
method: 'DELETE',
credentials: 'include',
})
.then((res) => {
if (res.ok) {
ElMessage({
message: 'Successfully cleared offline proxies',
type: 'success',
})
emit('refresh')
} else {
ElMessage({
message: 'Failed to clear offline proxies: ' + res.status + ' ' + res.statusText,
type: 'warning',
})
}
})
.catch((err) => {
ElMessage({
message: 'Failed to clear offline proxies: ' + err.message,
type: 'warning',
})
})
}
</script> </script>
<style>
.el-page-header__title {
font-size: 20px;
}
</style>

View File

@ -17,10 +17,7 @@
<el-form-item label="KCP Bind Port" v-if="data.kcpBindPort != 0"> <el-form-item label="KCP Bind Port" v-if="data.kcpBindPort != 0">
<span>{{ data.kcpBindPort }}</span> <span>{{ data.kcpBindPort }}</span>
</el-form-item> </el-form-item>
<el-form-item <el-form-item label="QUIC Bind Port" v-if="data.quicBindPort != 0">
label="QUIC Bind Port"
v-if="data.quicBindPort != 0"
>
<span>{{ data.quicBindPort }}</span> <span>{{ data.quicBindPort }}</span>
</el-form-item> </el-form-item>
<el-form-item label="Http Port" v-if="data.vhostHTTPPort != 0"> <el-form-item label="Http Port" v-if="data.vhostHTTPPort != 0">

View File

@ -23,8 +23,10 @@ class BaseProxy {
this.type = '' this.type = ''
this.encryption = false this.encryption = false
this.compression = false this.compression = false
this.encryption = (proxyStats.conf?.transport?.useEncryption) || this.encryption; this.encryption =
this.compression = (proxyStats.conf?.transport?.useCompression) || this.compression; proxyStats.conf?.transport?.useEncryption || this.encryption
this.compression =
proxyStats.conf?.transport?.useCompression || this.compression
this.conns = proxyStats.curConns this.conns = proxyStats.curConns
this.trafficIn = proxyStats.todayTrafficIn this.trafficIn = proxyStats.todayTrafficIn
this.trafficOut = proxyStats.todayTrafficOut this.trafficOut = proxyStats.todayTrafficOut
@ -76,12 +78,12 @@ class HTTPProxy extends BaseProxy {
this.type = 'http' this.type = 'http'
this.port = port this.port = port
if (proxyStats.conf) { if (proxyStats.conf) {
this.customDomains = proxyStats.conf.customDomains || this.customDomains; this.customDomains = proxyStats.conf.customDomains || this.customDomains
this.hostHeaderRewrite = proxyStats.conf.hostHeaderRewrite this.hostHeaderRewrite = proxyStats.conf.hostHeaderRewrite
this.locations = proxyStats.conf.locations this.locations = proxyStats.conf.locations
if (proxyStats.conf.subdomain) { if (proxyStats.conf.subdomain) {
this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}` this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}`
} }
} }
} }
} }
@ -92,7 +94,7 @@ class HTTPSProxy extends BaseProxy {
this.type = 'https' this.type = 'https'
this.port = port this.port = port
if (proxyStats.conf != null) { if (proxyStats.conf != null) {
this.customDomains = proxyStats.conf.customDomains || this.customDomains; this.customDomains = proxyStats.conf.customDomains || this.customDomains
if (proxyStats.conf.subdomain) { if (proxyStats.conf.subdomain) {
this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}` this.subdomain = `${proxyStats.conf.subdomain}.${subdomainHost}`
} }

View File

@ -1,8 +0,0 @@
{
"extends": "@vue/tsconfig/tsconfig.node.json",
"include": ["vite.config.*", "vitest.config.*", "cypress.config.*", "playwright.config.*"],
"compilerOptions": {
"composite": true,
"types": ["node"]
}
}

View File

@ -1,16 +1,25 @@
{ {
"extends": "@vue/tsconfig/tsconfig.web.json",
"include": ["env.d.ts", "src/**/*", "src/**/*.vue"],
"compilerOptions": { "compilerOptions": {
"baseUrl": ".", "target": "ES2020",
"paths": { "useDefineForClassFields": true,
"@/*": ["./src/*"] "module": "ESNext",
} "lib": ["ES2020", "DOM", "DOM.Iterable"],
}, "skipLibCheck": true,
"references": [ /* Bundler mode */
{ "moduleResolution": "bundler",
"path": "./tsconfig.config.json" "allowImportingTsExtensions": true,
} "resolveJsonModule": true,
] "isolatedModules": true,
"noEmit": true,
"jsx": "preserve",
/* Linting */
"strict": true,
"noUnusedLocals": true,
"noUnusedParameters": true,
"noFallthroughCasesInSwitch": true
},
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }]
} }

View File

@ -0,0 +1,10 @@
{
"compilerOptions": {
"composite": true,
"skipLibCheck": true,
"module": "ESNext",
"moduleResolution": "bundler",
"allowSyntheticDefaultImports": true
},
"include": ["vite.config.ts"]
}

File diff suppressed because it is too large Load Diff