Quellcode durchsuchen

Merge pull request #3968 from fatedier/dev

bump version
fatedier vor 1 Jahr
Ursprung
Commit
d689f0fc53
61 geänderte Dateien mit 1247 neuen und 1496 gelöschten Zeilen
  1. 6 5
      .github/workflows/stale.yml
  2. 2 0
      .gitignore
  3. 9 1
      Release.md
  4. 0 0
      assets/frpc/static/index-1c7ed8b0.js
  5. 0 0
      assets/frpc/static/index-1e2a7ce0.css
  6. 4 0
      assets/frpc/static/index-bLBhaJo8.js
  7. 0 0
      assets/frpc/static/index-iuf46MlF.css
  8. 2 3
      assets/frpc/static/index.html
  9. 0 0
      assets/frps/static/index-1e0c7400.css
  10. 4 0
      assets/frps/static/index-1gecbKzv.js
  11. 0 0
      assets/frps/static/index-Lf6B06jY.css
  12. 0 0
      assets/frps/static/index-c322b7dd.js
  13. 2 3
      assets/frps/static/index.html
  14. 2 2
      client/connector.go
  15. 1 0
      client/control.go
  16. 1 0
      cmd/frpc/sub/root.go
  17. 1 0
      cmd/frps/root.go
  18. 2 2
      conf/frps_full_example.toml
  19. 4 4
      go.mod
  20. 9 9
      go.sum
  21. 5 1
      hack/run-e2e.sh
  22. 10 0
      pkg/config/flags.go
  23. 1 1
      pkg/config/v1/proxy.go
  24. 8 4
      pkg/metrics/mem/server.go
  25. 1 0
      pkg/metrics/mem/types.go
  26. 2 0
      pkg/ssh/server.go
  27. 1 1
      pkg/util/version/version.go
  28. 6 0
      pkg/util/vhost/router.go
  29. 28 0
      server/dashboard_api.go
  30. 1 1
      test/e2e/framework/framework.go
  31. 3 3
      test/e2e/framework/process.go
  32. 7 7
      test/e2e/legacy/basic/server.go
  33. 1 0
      test/e2e/pkg/port/port.go
  34. 2 2
      test/e2e/v1/basic/cmd.go
  35. 9 9
      test/e2e/v1/basic/server.go
  36. 5 5
      test/e2e/v1/features/ssh_tunnel.go
  37. 1 0
      web/frpc/auto-imports.d.ts
  38. 1 3
      web/frpc/components.d.ts
  39. 18 18
      web/frpc/package.json
  40. 0 8
      web/frpc/tsconfig.config.json
  41. 21 12
      web/frpc/tsconfig.json
  42. 10 0
      web/frpc/tsconfig.node.json
  43. 0 0
      web/frpc/vite.config.mts
  44. 459 634
      web/frpc/yarn.lock
  45. 5 1
      web/frps/auto-imports.d.ts
  46. 7 5
      web/frps/components.d.ts
  47. 17 17
      web/frps/package.json
  48. 2 1
      web/frps/src/components/ProxiesHTTP.vue
  49. 2 1
      web/frps/src/components/ProxiesHTTPS.vue
  50. 2 1
      web/frps/src/components/ProxiesSTCP.vue
  51. 2 1
      web/frps/src/components/ProxiesSUDP.vue
  52. 2 1
      web/frps/src/components/ProxiesTCP.vue
  53. 2 2
      web/frps/src/components/ProxiesUDP.vue
  54. 59 0
      web/frps/src/components/ProxyView.vue
  55. 1 4
      web/frps/src/components/ServerOverview.vue
  56. 7 5
      web/frps/src/utils/proxy.ts
  57. 0 8
      web/frps/tsconfig.config.json
  58. 21 12
      web/frps/tsconfig.json
  59. 10 0
      web/frps/tsconfig.node.json
  60. 0 0
      web/frps/vite.config.mts
  61. 459 699
      web/frps/yarn.lock

+ 6 - 5
.github/workflows/stale.yml

@@ -16,19 +16,20 @@ jobs:
     permissions:
       issues: write  # for actions/stale to close stale issues
       pull-requests: write  # for actions/stale to close stale PRs
+      actions: write
     runs-on: ubuntu-latest
     steps:
-    - uses: actions/stale@v8
+    - uses: actions/stale@v9
       with:
-        repo-token: ${{ secrets.GITHUB_TOKEN }}
-        stale-issue-message: 'Issues go stale after 30d of inactivity. Stale issues rot after an additional 7d of inactivity and eventually close.'
-        stale-pr-message: "PRs go stale after 30d of inactivity. Stale PRs rot after an additional 7d of inactivity and eventually close."
+        stale-issue-message: 'Issues go stale after 21d of inactivity. Stale issues rot after an additional 7d of inactivity and eventually close.'
+        stale-pr-message: "PRs go stale after 21d of inactivity. Stale PRs rot after an additional 7d of inactivity and eventually close."
         stale-issue-label: 'lifecycle/stale'
         exempt-issue-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
         stale-pr-label: 'lifecycle/stale'
         exempt-pr-labels: 'bug,doc,enhancement,future,proposal,question,testing,todo,easy,help wanted,assigned'
-        days-before-stale: 30
+        days-before-stale: 21
         days-before-close: 7
         debug-only: ${{ github.event.inputs.debug-only }}
         exempt-all-pr-milestones: true
         exempt-all-pr-assignees: true
+        operations-per-run: 200

+ 2 - 0
.gitignore

@@ -34,6 +34,8 @@ dist/
 .idea/
 .vscode/
 .autogen_ssh_key
+client.crt
+client.key
 
 # Cache
 *.swp

+ 9 - 1
Release.md

@@ -1,3 +1,11 @@
+### 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`.
+
+### Features
+
+* The `Refresh` and `ClearOfflineProxies` buttons have been added to the Dashboard of frps.
+
 ### Fixes
 
-* frpc has a certain chance to panic when login: close of closed channel.
+* The host/domain matching in the routing rules has been changed to be case-insensitive.

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
assets/frpc/static/index-1c7ed8b0.js


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
assets/frpc/static/index-1e2a7ce0.css


Datei-Diff unterdrückt, da er zu groß ist
+ 4 - 0
assets/frpc/static/index-bLBhaJo8.js


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
assets/frpc/static/index-iuf46MlF.css


+ 2 - 3
assets/frpc/static/index.html

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

Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
assets/frps/static/index-1e0c7400.css


Datei-Diff unterdrückt, da er zu groß ist
+ 4 - 0
assets/frps/static/index-1gecbKzv.js


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
assets/frps/static/index-Lf6B06jY.css


Datei-Diff unterdrückt, da er zu groß ist
+ 0 - 0
assets/frps/static/index-c322b7dd.js


+ 2 - 3
assets/frps/static/index.html

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

+ 2 - 2
client/connector.go

@@ -35,7 +35,7 @@ import (
 	"github.com/fatedier/frp/pkg/util/xlog"
 )
 
-// Connector is a interface for establishing connections to the server.
+// Connector is an interface for establishing connections to the server.
 type Connector interface {
 	Open() error
 	Connect() (net.Conn, error)
@@ -59,7 +59,7 @@ func NewConnector(ctx context.Context, cfg *v1.ClientCommonConfig) Connector {
 	}
 }
 
-// Open opens a underlying connection to the server.
+// Open opens an underlying connection to the server.
 // The underlying connection is either a TCP connection or a QUIC connection.
 // After the underlying connection is established, you can call Connect() to get a stream.
 // If TCPMux isn't enabled, the underlying connection is nil, you will get a new real TCP connection every time you call Connect().

+ 1 - 0
client/control.go

@@ -133,6 +133,7 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
 	}
 	if err = ctl.sessionCtx.AuthSetter.SetNewWorkConn(m); err != nil {
 		xl.Warn("error during NewWorkConn authentication: %v", err)
+		workConn.Close()
 		return
 	}
 	if err = msg.WriteMsg(workConn, m); err != nil {

+ 1 - 0
cmd/frpc/sub/root.go

@@ -97,6 +97,7 @@ func runMultipleClients(cfgDir string) error {
 }
 
 func Execute() {
+	rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
 	if err := rootCmd.Execute(); err != nil {
 		os.Exit(1)
 	}

+ 1 - 0
cmd/frps/root.go

@@ -92,6 +92,7 @@ var rootCmd = &cobra.Command{
 }
 
 func Execute() {
+	rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
 	if err := rootCmd.Execute(); err != nil {
 		os.Exit(1)
 	}

+ 2 - 2
conf/frps_full_example.toml

@@ -41,7 +41,7 @@ transport.maxPoolCount = 5
 # transport.tcpKeepalive = 7200
 
 # transport.tls.force specifies whether to only accept TLS-encrypted connections. By default, the value is false.
-tls.force = false
+transport.tls.force = false
 
 # transport.tls.certFile = "server.crt"
 # transport.tls.keyFile = "server.key"
@@ -129,7 +129,7 @@ allowPorts = [
 maxPortsPerClient = 0
 
 # If subDomainHost is not empty, you can set subdomain when type is http or https in frpc's configure file
-# When subdomain is est, the host used by routing is test.frps.com
+# When subdomain is test, the host used by routing is test.frps.com
 subDomainHost = "frps.com"
 
 # custom 404 page for HTTP requests

+ 4 - 4
go.mod

@@ -18,13 +18,13 @@ require (
 	github.com/pion/stun v0.6.1
 	github.com/pires/go-proxyproto v0.7.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/samber/lo v1.38.1
 	github.com/spf13/cobra v1.8.0
 	github.com/spf13/pflag v1.0.5
 	github.com/stretchr/testify v1.8.4
-	golang.org/x/crypto v0.15.0
+	golang.org/x/crypto v0.17.0
 	golang.org/x/net v0.17.0
 	golang.org/x/oauth2 v0.10.0
 	golang.org/x/sync v0.3.0
@@ -39,7 +39,7 @@ require (
 	github.com/beorn7/perks v1.0.1 // indirect
 	github.com/cespare/xxhash/v2 v2.2.0 // indirect
 	github.com/davecgh/go-spew v1.1.1 // indirect
-	github.com/go-jose/go-jose/v3 v3.0.0 // indirect
+	github.com/go-jose/go-jose/v3 v3.0.1 // indirect
 	github.com/go-logr/logr v1.2.4 // indirect
 	github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
 	github.com/golang/mock v1.6.0 // indirect
@@ -67,7 +67,7 @@ require (
 	github.com/tjfoc/gmsm v1.4.1 // indirect
 	golang.org/x/exp v0.0.0-20221205204356-47842c84f3db // indirect
 	golang.org/x/mod v0.10.0 // indirect
-	golang.org/x/sys v0.14.0 // indirect
+	golang.org/x/sys v0.15.0 // indirect
 	golang.org/x/text v0.14.0 // indirect
 	golang.org/x/tools v0.9.3 // indirect
 	google.golang.org/appengine v1.6.7 // indirect

+ 9 - 9
go.sum

@@ -32,8 +32,8 @@ github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible h1:
 github.com/fatedier/kcp-go v2.0.4-0.20190803094908-fe8645b0a904+incompatible/go.mod h1:YpCOaxj7vvMThhIQ9AfTOPW2sfztQR5WDfs7AflSy4s=
 github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d h1:ynk1ra0RUqDWQfvFi5KtMiSobkVQ3cNc0ODb8CfIETo=
 github.com/fatedier/yamux v0.0.0-20230628132301-7aca4898904d/go.mod h1:CtWFDAQgb7dxtzFs4tWbplKIe2jSi3+5vKbgIO0SLnQ=
-github.com/go-jose/go-jose/v3 v3.0.0 h1:s6rrhirfEP/CGIoc6p+PZAeogN2SxKav6Wp7+dyMWVo=
-github.com/go-jose/go-jose/v3 v3.0.0/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
+github.com/go-jose/go-jose/v3 v3.0.1 h1:pWmKFVtt+Jl0vBZTIpz/eAKwsm6LkIxDVVbFHKkchhA=
+github.com/go-jose/go-jose/v3 v3.0.1/go.mod h1:RNkWWRld676jZEYoV3+XK8L2ZnNSvIsxFMht0mSX+u8=
 github.com/go-logr/logr v1.2.4 h1:g01GSCwiDw2xSZfjJ2/T9M+S6pFdcNtFYsp+Y43HYDQ=
 github.com/go-logr/logr v1.2.4/go.mod h1:jdQByPbusPIv2/zmleS9BjJVeZ6kBagPoEUsqbVz/1A=
 github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
@@ -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/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/quic-go v0.37.4 h1:ke8B73yMCWGq9MfrCCAw0Uzdm7GaViC3i39dsIdDlH4=
-github.com/quic-go/quic-go v0.37.4/go.mod h1:YsbH1r4mSHPJcLF4k4zruUkLBqctEMBDR6VPvcYjIsU=
+github.com/quic-go/quic-go v0.37.7 h1:AgKsQLZ1+YCwZd2GYhBUsJDYZwEkA5gENtAjb+MxONU=
+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/go.mod h1:Qu3q5wi1jTQD6B6HsP6szie/S4w1QUQ8pq22pz9iL8g=
 github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
@@ -157,8 +157,8 @@ golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPh
 golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
 golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
 golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE=
-golang.org/x/crypto v0.15.0 h1:frVn1TEaCEaZcn3Tmd7Y2b5KKPaZ+I32Q2OA3kYp5TA=
-golang.org/x/crypto v0.15.0/go.mod h1:4ChreQoLWfG3xLDer1WdlH5NdlQ3+mwnQq1YTKY+72g=
+golang.org/x/crypto v0.17.0 h1:r8bRNjWL3GshPW3gkd+RpvzWrZAwPS49OmTGZ/uhM4k=
+golang.org/x/crypto v0.17.0/go.mod h1:gCAAfMLgwOJRpTjQ2zCCt2OcSfYMTeZVSRtQlPC7Nq4=
 golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
 golang.org/x/exp v0.0.0-20221205204356-47842c84f3db h1:D/cFflL63o2KSLJIwjlcIt8PR064j/xsmdEJL/YvY/o=
 golang.org/x/exp v0.0.0-20221205204356-47842c84f3db/go.mod h1:CxIveKay+FTh1D0yPZemJVgC/95VzuuOLq5Qi4xnoYc=
@@ -210,13 +210,13 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
 golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
 golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
-golang.org/x/sys v0.14.0 h1:Vz7Qs629MkJkGyHxUlRHizWJRG2j8fbQKjELVSNhy7Q=
-golang.org/x/sys v0.14.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+golang.org/x/sys v0.15.0 h1:h48lPFYpsTvQJZF4EKyI4aLHaev3CxivZmv7yZig9pc=
+golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
 golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
 golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
 golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
 golang.org/x/term v0.7.0/go.mod h1:P32HKFT3hSsZrRxla30E9HqToFYAQPCMs/zFMBUFqPY=
-golang.org/x/term v0.14.0 h1:LGK9IlZ8T9jvdy6cTdfKUCltatMFOehAQo9SRC46UQ8=
+golang.org/x/term v0.15.0 h1:y/Oo/a/q3IXu26lQgl04j/gjuBDOBlx7X6Om1j2CPW4=
 golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
 golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
 golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=

+ 5 - 1
hack/run-e2e.sh

@@ -26,5 +26,9 @@ frpsPath=${ROOT}/bin/frps
 if [ "${FRPS_PATH}" ]; then
     frpsPath="${FRPS_PATH}"
 fi
+concurrency="16"
+if [ "${CONCURRENCY}" ]; then
+    concurrency="${CONCURRENCY}"
+fi
 
-ginkgo -nodes=8 --poll-progress-after=60s ${ROOT}/test/e2e -- -frpc-path=${frpcPath} -frps-path=${frpsPath} -log-level=${logLevel} -debug=${debug}
+ginkgo -nodes=${concurrency} --poll-progress-after=60s ${ROOT}/test/e2e -- -frpc-path=${frpcPath} -frps-path=${frpsPath} -log-level=${logLevel} -debug=${debug}

+ 10 - 0
pkg/config/flags.go

@@ -17,14 +17,24 @@ package config
 import (
 	"fmt"
 	"strconv"
+	"strings"
 
 	"github.com/spf13/cobra"
+	"github.com/spf13/pflag"
 
 	"github.com/fatedier/frp/pkg/config/types"
 	v1 "github.com/fatedier/frp/pkg/config/v1"
 	"github.com/fatedier/frp/pkg/config/v1/validation"
 )
 
+// WordSepNormalizeFunc changes all flags that contain "_" separators
+func WordSepNormalizeFunc(f *pflag.FlagSet, name string) pflag.NormalizedName {
+	if strings.Contains(name, "_") {
+		return pflag.NormalizedName(strings.ReplaceAll(name, "_", "-"))
+	}
+	return pflag.NormalizedName(name)
+}
+
 type RegisterFlagOption func(*registerFlagOptions)
 
 type registerFlagOptions struct {

+ 1 - 1
pkg/config/v1/proxy.go

@@ -195,7 +195,7 @@ type ProxyConfigurer interface {
 	// MarshalToMsg marshals this config into a msg.NewProxy message. This
 	// function will be called on the frpc side.
 	MarshalToMsg(*msg.NewProxy)
-	// UnmarshalFromMsg unmarshals a msg.NewProxy message into this config.
+	// UnmarshalFromMsg unmarshal a msg.NewProxy message into this config.
 	// This function will be called on the frps side.
 	UnmarshalFromMsg(*msg.NewProxy)
 }

+ 8 - 4
pkg/metrics/mem/server.go

@@ -61,23 +61,23 @@ func (m *serverMetrics) run() {
 		for {
 			time.Sleep(12 * time.Hour)
 			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))
 		}
 	}()
 }
 
-func (m *serverMetrics) clearUselessInfo() (int, int) {
+func (m *serverMetrics) clearUselessInfo(continuousOfflineDuration time.Duration) (int, int) {
 	count := 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()
 	defer m.mu.Unlock()
 	total = len(m.info.ProxyStatistics)
 	for name, data := range m.info.ProxyStatistics {
 		if !data.LastCloseTime.IsZero() &&
 			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)
 			count++
 			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
 }
 
+func (m *serverMetrics) ClearOfflineProxies() (int, int) {
+	return m.clearUselessInfo(0)
+}
+
 func (m *serverMetrics) NewClient() {
 	m.info.ClientCounts.Inc(1)
 }

+ 1 - 0
pkg/metrics/mem/types.go

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

+ 2 - 0
pkg/ssh/server.go

@@ -254,6 +254,8 @@ func (s *TunnelServer) parseClientAndProxyConfigurer(_ *tcpipForward, extraPaylo
 		Short: "ssh v0@{address} [command]",
 		Run:   func(*cobra.Command, []string) {},
 	}
+	cmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
+
 	args := strings.Split(extraPayload, " ")
 	if len(args) < 1 {
 		return nil, nil, helpMessage, fmt.Errorf("invalid extra payload")

+ 1 - 1
pkg/util/version/version.go

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

+ 6 - 0
pkg/util/vhost/router.go

@@ -33,6 +33,8 @@ func NewRouters() *Routers {
 }
 
 func (r *Routers) Add(domain, location, httpUser string, payload interface{}) error {
+	domain = strings.ToLower(domain)
+
 	r.mutex.Lock()
 	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) {
+	domain = strings.ToLower(domain)
+
 	r.mutex.Lock()
 	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) {
+	host = strings.ToLower(host)
+
 	r.mutex.RLock()
 	defer r.mutex.RUnlock()
 

+ 28 - 0
server/dashboard_api.go

@@ -17,6 +17,7 @@ package server
 import (
 	"encoding/json"
 	"net/http"
+	"sort"
 
 	"github.com/gorilla/mux"
 	"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}/{name}", svr.apiProxyByTypeAndName).Methods("GET")
 	subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
+	subRouter.HandleFunc("/api/proxies", svr.deleteProxies).Methods("DELETE")
 
 	// view
 	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.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)
 	res.Msg = string(buf)
@@ -376,3 +381,26 @@ func (svr *Service) apiProxyTraffic(w http.ResponseWriter, r *http.Request) {
 	buf, _ := json.Marshal(&trafficResp)
 	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)
+}

+ 1 - 1
test/e2e/framework/framework.go

@@ -67,7 +67,7 @@ func NewDefaultFramework() *Framework {
 		TotalParallelNode: suiteConfig.ParallelTotal,
 		CurrentNodeIndex:  suiteConfig.ParallelProcess,
 		FromPortIndex:     10000,
-		ToPortIndex:       60000,
+		ToPortIndex:       30000,
 	}
 	return NewFramework(options)
 }

+ 3 - 3
test/e2e/framework/process.go

@@ -42,7 +42,7 @@ func (f *Framework) RunProcesses(serverTemplates []string, clientTemplates []str
 		ExpectNoError(err)
 		time.Sleep(500 * time.Millisecond)
 	}
-	time.Sleep(1 * time.Second)
+	time.Sleep(2 * time.Second)
 
 	currentClientProcesses := make([]*process.Process, 0, len(clientTemplates))
 	for i := range clientTemplates {
@@ -76,7 +76,7 @@ func (f *Framework) RunFrps(args ...string) (*process.Process, string, error) {
 		return p, p.StdOutput(), err
 	}
 	// sleep for a while to get std output
-	time.Sleep(time.Second)
+	time.Sleep(2 * time.Second)
 	return p, p.StdOutput(), nil
 }
 
@@ -87,7 +87,7 @@ func (f *Framework) RunFrpc(args ...string) (*process.Process, string, error) {
 	if err != nil {
 		return p, p.StdOutput(), err
 	}
-	time.Sleep(time.Second)
+	time.Sleep(2 * time.Second)
 	return p, p.StdOutput(), nil
 }
 

+ 7 - 7
test/e2e/legacy/basic/server.go

@@ -22,11 +22,11 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 		clientConf := consts.LegacyDefaultClientConfig
 
 		serverConf += `
-			allow_ports = 20000-25000,25002,30000-50000
+			allow_ports = 10000-11000,11002,12000-13000
 		`
 
-		tcpPortName := port.GenName("TCP", port.WithRangePorts(20000, 25000))
-		udpPortName := port.GenName("UDP", port.WithRangePorts(30000, 50000))
+		tcpPortName := port.GenName("TCP", port.WithRangePorts(10000, 11000))
+		udpPortName := port.GenName("UDP", port.WithRangePorts(12000, 13000))
 		clientConf += fmt.Sprintf(`
 			[tcp-allowded-in-range]
 			type = tcp
@@ -37,7 +37,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 			[tcp-port-not-allowed]
 			type = tcp
 			local_port = {{ .%s }}
-			remote_port = 25001
+			remote_port = 11001
 			`, framework.TCPEchoServerPort)
 		clientConf += fmt.Sprintf(`
 			[tcp-port-unavailable]
@@ -55,7 +55,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 			[udp-port-not-allowed]
 			type = udp
 			local_port = {{ .%s }}
-			remote_port = 25003
+			remote_port = 11003
 			`, framework.UDPEchoServerPort)
 
 		f.RunProcesses([]string{serverConf}, []string{clientConf})
@@ -65,7 +65,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 		framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
 
 		// Not Allowed
-		framework.NewRequestExpect(f).Port(25001).ExpectError(true).Ensure()
+		framework.NewRequestExpect(f).Port(11001).ExpectError(true).Ensure()
 
 		// Unavailable, already bind by frps
 		framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
@@ -76,7 +76,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 
 		// Not Allowed
 		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
-			r.UDP().Port(25003)
+			r.UDP().Port(11003)
 		}).ExpectError(true).Ensure()
 	})
 

+ 1 - 0
test/e2e/pkg/port/port.go

@@ -79,6 +79,7 @@ func (pa *Allocator) GetByName(portName string) int {
 		udpConn.Close()
 
 		pa.used.Insert(port)
+		pa.reserved.Delete(port)
 		return port
 	}
 	return 0

+ 2 - 2
test/e2e/v1/basic/cmd.go

@@ -90,12 +90,12 @@ var _ = ginkgo.Describe("[Feature: Cmd]", func() {
 		ginkgo.It("HTTP", func() {
 			serverPort := f.AllocPort()
 			vhostHTTPPort := f.AllocPort()
-			_, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort), "--vhost_http_port", strconv.Itoa(vhostHTTPPort))
+			_, _, err := f.RunFrps("-t", "123", "-p", strconv.Itoa(serverPort), "--vhost-http-port", strconv.Itoa(vhostHTTPPort))
 			framework.ExpectNoError(err)
 
 			_, _, err = f.RunFrpc("http", "-s", "127.0.0.1", "-P", strconv.Itoa(serverPort), "-t", "123", "-u", "test",
 				"-n", "udp_test", "-l", strconv.Itoa(f.PortByName(framework.HTTPSimpleServerPort)),
-				"--custom_domain", "test.example.com")
+				"--custom-domain", "test.example.com")
 			framework.ExpectNoError(err)
 
 			framework.NewRequestExpect(f).Port(vhostHTTPPort).

+ 9 - 9
test/e2e/v1/basic/server.go

@@ -23,14 +23,14 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 
 		serverConf += `
 		allowPorts = [
-		  { start = 20000, end = 25000 },
-		  { single = 25002 },
-		  { start = 30000, end = 50000 },
+		  { start = 10000, end = 11000 },
+		  { single = 11002 },
+		  { start = 12000, end = 13000 },
 		]
 		`
 
-		tcpPortName := port.GenName("TCP", port.WithRangePorts(20000, 25000))
-		udpPortName := port.GenName("UDP", port.WithRangePorts(30000, 50000))
+		tcpPortName := port.GenName("TCP", port.WithRangePorts(10000, 11000))
+		udpPortName := port.GenName("UDP", port.WithRangePorts(12000, 13000))
 		clientConf += fmt.Sprintf(`
 			[[proxies]]
 			name = "tcp-allowded-in-range"
@@ -43,7 +43,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 			name = "tcp-port-not-allowed"
 			type = "tcp"
 			localPort = {{ .%s }}
-			remotePort = 25001
+			remotePort = 11001
 			`, framework.TCPEchoServerPort)
 		clientConf += fmt.Sprintf(`
 			[[proxies]]
@@ -64,7 +64,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 			name = "udp-port-not-allowed"
 			type = "udp"
 			localPort = {{ .%s }}
-			remotePort = 25003
+			remotePort = 11003
 			`, framework.UDPEchoServerPort)
 
 		f.RunProcesses([]string{serverConf}, []string{clientConf})
@@ -74,7 +74,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 		framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
 
 		// Not Allowed
-		framework.NewRequestExpect(f).Port(25001).ExpectError(true).Ensure()
+		framework.NewRequestExpect(f).Port(11001).ExpectError(true).Ensure()
 
 		// Unavailable, already bind by frps
 		framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
@@ -85,7 +85,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
 
 		// Not Allowed
 		framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
-			r.UDP().Port(25003)
+			r.UDP().Port(11003)
 		}).ExpectError(true).Ensure()
 	})
 

+ 5 - 5
test/e2e/v1/features/ssh_tunnel.go

@@ -32,7 +32,7 @@ var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() {
 		tc := ssh.NewTunnelClient(
 			fmt.Sprintf("127.0.0.1:%d", localPort),
 			fmt.Sprintf("127.0.0.1:%d", sshPort),
-			fmt.Sprintf("tcp --remote_port %d", remotePort),
+			fmt.Sprintf("tcp --remote-port %d", remotePort),
 		)
 		framework.ExpectNoError(tc.Start())
 		defer tc.Close()
@@ -55,7 +55,7 @@ var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() {
 		tc := ssh.NewTunnelClient(
 			fmt.Sprintf("127.0.0.1:%d", localPort),
 			fmt.Sprintf("127.0.0.1:%d", sshPort),
-			"http --custom_domain test.example.com",
+			"http --custom-domain test.example.com",
 		)
 		framework.ExpectNoError(tc.Start())
 		defer tc.Close()
@@ -83,7 +83,7 @@ var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() {
 		tc := ssh.NewTunnelClient(
 			fmt.Sprintf("127.0.0.1:%d", localPort),
 			fmt.Sprintf("127.0.0.1:%d", sshPort),
-			fmt.Sprintf("https --custom_domain %s", testDomain),
+			fmt.Sprintf("https --custom-domain %s", testDomain),
 		)
 		framework.ExpectNoError(tc.Start())
 		defer tc.Close()
@@ -125,7 +125,7 @@ var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() {
 		tc := ssh.NewTunnelClient(
 			fmt.Sprintf("127.0.0.1:%d", localPort),
 			fmt.Sprintf("127.0.0.1:%d", sshPort),
-			fmt.Sprintf("tcpmux --mux=httpconnect --custom_domain %s", testDomain),
+			fmt.Sprintf("tcpmux --mux=httpconnect --custom-domain %s", testDomain),
 		)
 		framework.ExpectNoError(tc.Start())
 		defer tc.Close()
@@ -179,7 +179,7 @@ var _ = ginkgo.Describe("[Feature: SSH Tunnel]", func() {
 		tc := ssh.NewTunnelClient(
 			fmt.Sprintf("127.0.0.1:%d", localPort),
 			fmt.Sprintf("127.0.0.1:%d", sshPort),
-			"stcp -n stcp-test --sk=abcdefg --allow_users=\"*\"",
+			"stcp -n stcp-test --sk=abcdefg --allow-users=\"*\"",
 		)
 		framework.ExpectNoError(tc.Start())
 		defer tc.Close()

+ 1 - 0
web/frpc/auto-imports.d.ts

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

+ 1 - 3
web/frpc/components.d.ts

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

+ 18 - 18
web/frpc/package.json

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

+ 0 - 8
web/frpc/tsconfig.config.json

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

+ 21 - 12
web/frpc/tsconfig.json

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

+ 10 - 0
web/frpc/tsconfig.node.json

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

+ 0 - 0
web/frpc/vite.config.ts → web/frpc/vite.config.mts


Datei-Diff unterdrückt, da er zu groß ist
+ 459 - 634
web/frpc/yarn.lock


+ 5 - 1
web/frps/auto-imports.d.ts

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

+ 7 - 5
web/frps/components.d.ts

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

+ 17 - 17
web/frps/package.json

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

+ 2 - 1
web/frps/src/components/ProxiesHTTP.vue

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

+ 2 - 1
web/frps/src/components/ProxiesHTTPS.vue

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

+ 2 - 1
web/frps/src/components/ProxiesSTCP.vue

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

+ 2 - 1
web/frps/src/components/ProxiesSUDP.vue

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

+ 2 - 1
web/frps/src/components/ProxiesTCP.vue

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

+ 2 - 2
web/frps/src/components/ProxiesUDP.vue

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

+ 59 - 0
web/frps/src/components/ProxyView.vue

@@ -1,5 +1,28 @@
 <template>
   <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
       :data="proxies"
       :default-sort="{ prop: 'name', order: 'ascending' }"
@@ -67,6 +90,7 @@
 import * as Humanize from 'humanize-plus'
 import type { TableColumnCtx } from 'element-plus'
 import type { BaseProxy } from '../utils/proxy.js'
+import { ElMessage } from 'element-plus'
 import ProxyViewExpand from './ProxyViewExpand.vue'
 
 defineProps<{
@@ -74,6 +98,8 @@ defineProps<{
   proxyType: string
 }>()
 
+const emit = defineEmits(['refresh'])
+
 const formatTrafficIn = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
   return Humanize.fileSize(row.trafficIn)
 }
@@ -81,4 +107,37 @@ const formatTrafficIn = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
 const formatTrafficOut = (row: BaseProxy, _: TableColumnCtx<BaseProxy>) => {
   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>
+
+<style>
+.el-page-header__title {
+  font-size: 20px;
+}
+</style>

+ 1 - 4
web/frps/src/components/ServerOverview.vue

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

+ 7 - 5
web/frps/src/utils/proxy.ts

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

+ 0 - 8
web/frps/tsconfig.config.json

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

+ 21 - 12
web/frps/tsconfig.json

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

+ 10 - 0
web/frps/tsconfig.node.json

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

+ 0 - 0
web/frps/vite.config.ts → web/frps/vite.config.mts


Datei-Diff unterdrückt, da er zu groß ist
+ 459 - 699
web/frps/yarn.lock


Einige Dateien werden nicht angezeigt, da zu viele Dateien in diesem Diff geändert wurden.