Compare commits

...
23 Commits
Author SHA1 Message Date
fatedierandGitHub 4ec8de973f Merge pull request #5287 from fatedier/dev
bump version to v0.68.1
2026-04-14 01:28:33 +08:00
fatedierandGitHub e8dfd6efcc web/frpc: use static imports for proxy and visitor route components (#5286) 2026-04-14 01:18:31 +08:00
fatedierandGitHub a9a4416ecf vhost/http: fix auth bypass when routeByHTTPUser is used with proxy-style requests (#5285) 2026-04-14 00:58:19 +08:00
fatedierandGitHub d667be7a0a update architecture pic (#5270) 2026-03-30 03:22:35 +08:00
fatedierandGitHub 31c3deb4f7 deps: bump golib to v0.6.0 (#5269) 2026-03-30 00:29:17 +08:00
fatedierandGitHub 31e271939b test/e2e: allocate dynamic ports outside whitelist ranges in server whitelist test (#5268) 2026-03-30 00:21:30 +08:00
fatedierandGitHub 061c141756 update README.md (#5267) 2026-03-29 23:53:17 +08:00
cuiandGitHub 98ee1adb13 client: close TCP connection when fmux session creation fails (#5262)
If realConnect() succeeds but fmux.Client returns an error, the
connection was not assigned to the connector and would leak.
2026-03-29 23:16:00 +08:00
fatedierandGitHub 76abeff881 ci: remove security vulnerability scan workflow (#5266) 2026-03-29 22:55:52 +08:00
Oleksandr RedkoandGitHub c694b1f6a9 bump pion/stun to v3 to fix vulnerability (#5245) 2026-03-29 22:45:23 +08:00
fatedierandGitHub 5bfcea3d0c merge dev to master (#5254)
* ci: bump github actions to latest major versions (#5251)

* docker: copy shared web directory for npm workspace builds
2026-03-20 15:54:26 +08:00
fatedier 5ed02275da docker: copy shared web directory for npm workspace builds 2026-03-20 14:39:35 +08:00
fatedier 60c4f5d4bd ci: bump github actions to latest major versions (#5251) 2026-03-20 14:39:00 +08:00
fatedierandGitHub 0a1b4ab21f Merge pull request #5249 from fatedier/dev
bump version
2026-03-20 13:56:28 +08:00
fatedierandGitHub d20e384bf1 doc: add agent runbooks directory with release process (#5248)
* web: remove redundant SCSS and CSS files superseded by shared

* doc: add agent runbooks directory with release process
2026-03-20 13:52:12 +08:00
fatedierandGitHub c95dc9d88a web: remove redundant SCSS and CSS files superseded by shared (#5247) 2026-03-20 13:08:18 +08:00
fatedierandGitHub 38a71a6803 web/frps: redesign frps dashboard with sidebar nav, responsive layout, and shared component workspace (#5246) 2026-03-20 03:33:44 +08:00
fatedierandGitHub 6cdef90113 web: bump dev dependencies in frpc and frps package-lock.json (#5239) 2026-03-16 23:22:42 +08:00
fatedierandGitHub 5f575b8442 Merge pull request #5147 from fatedier/dev
bump version
2026-01-31 14:01:40 +08:00
fatedierandGitHub a1348cdf00 bump version (#5112) 2026-01-04 14:54:13 +08:00
fatedierandGitHub 2f5e1f7945 Merge pull request #4999 from fatedier/dev
bump version
2025-09-25 20:23:42 +08:00
fatedierandGitHub 22ae8166d3 Merge pull request #4925 from fatedier/dev
bump version
2025-08-10 23:26:32 +08:00
fatedierandGitHub af6bc6369d Merge pull request #4849 from fatedier/dev
bump version
2025-06-25 11:51:19 +08:00
67 changed files with 1920 additions and 8912 deletions
+7 -7
View File
@@ -19,15 +19,15 @@ jobs:
steps: steps:
# environment # environment
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: '0' fetch-depth: '0'
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v4
- name: Set up Docker Buildx - name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v4
# get image tag name # get image tag name
- name: Get Image Tag Name - name: Get Image Tag Name
@@ -38,13 +38,13 @@ jobs:
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
fi fi
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
username: ${{ secrets.DOCKERHUB_USERNAME }} username: ${{ secrets.DOCKERHUB_USERNAME }}
password: ${{ secrets.DOCKERHUB_PASSWORD }} password: ${{ secrets.DOCKERHUB_PASSWORD }}
- name: Login to the GPR - name: Login to the GPR
uses: docker/login-action@v3 uses: docker/login-action@v4
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.repository_owner }} username: ${{ github.repository_owner }}
@@ -61,7 +61,7 @@ jobs:
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
- name: Build and push frpc - name: Build and push frpc
uses: docker/build-push-action@v5 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: ./dockerfiles/Dockerfile-for-frpc file: ./dockerfiles/Dockerfile-for-frpc
@@ -72,7 +72,7 @@ jobs:
${{ env.TAG_FRPC_GPR }} ${{ env.TAG_FRPC_GPR }}
- name: Build and push frps - name: Build and push frps
uses: docker/build-push-action@v5 uses: docker/build-push-action@v7
with: with:
context: . context: .
file: ./dockerfiles/Dockerfile-for-frps file: ./dockerfiles/Dockerfile-for-frps
+3 -3
View File
@@ -14,12 +14,12 @@ jobs:
name: lint name: lint
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v6
- uses: actions/setup-go@v5 - uses: actions/setup-go@v6
with: with:
go-version: '1.25' go-version: '1.25'
cache: false cache: false
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with: with:
node-version: '22' node-version: '22'
- name: Build web assets (frps) - name: Build web assets (frps)
+4 -4
View File
@@ -8,15 +8,15 @@ jobs:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v6
with: with:
fetch-depth: 0 fetch-depth: 0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v5 uses: actions/setup-go@v6
with: with:
go-version: '1.25' go-version: '1.25'
- uses: actions/setup-node@v4 - uses: actions/setup-node@v6
with: with:
node-version: '22' node-version: '22'
- name: Build web assets (frps) - name: Build web assets (frps)
@@ -30,7 +30,7 @@ jobs:
./package.sh ./package.sh
- name: Run GoReleaser - name: Run GoReleaser
uses: goreleaser/goreleaser-action@v6 uses: goreleaser/goreleaser-action@v7
with: with:
version: latest version: latest
args: release --clean --release-notes=./Release.md args: release --clean --release-notes=./Release.md
+1 -1
View File
@@ -19,7 +19,7 @@ jobs:
actions: write actions: write
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- uses: actions/stale@v9 - uses: actions/stale@v10
with: with:
stale-issue-message: 'Issues go stale after 14d of inactivity. Stale issues rot after an additional 3d of inactivity and eventually close.' stale-issue-message: 'Issues go stale after 14d of inactivity. Stale issues rot after an additional 3d of inactivity and eventually close.'
stale-pr-message: "PRs go stale after 14d of inactivity. Stale PRs rot after an additional 3d of inactivity and eventually close." stale-pr-message: "PRs go stale after 14d of inactivity. Stale PRs rot after an additional 3d of inactivity and eventually close."
+2
View File
@@ -25,6 +25,8 @@ dist/
client.crt client.crt
client.key client.key
node_modules/
# Cache # Cache
*.swp *.swp
+2
View File
@@ -90,6 +90,7 @@ linters:
- third_party$ - third_party$
- builtin$ - builtin$
- examples$ - examples$
- node_modules
formatters: formatters:
enable: enable:
- gci - gci
@@ -112,6 +113,7 @@ formatters:
- third_party$ - third_party$
- builtin$ - builtin$
- examples$ - examples$
- node_modules
issues: issues:
max-issues-per-linter: 0 max-issues-per-linter: 0
max-same-issues: 0 max-same-issues: 0
+5
View File
@@ -32,3 +32,8 @@
- E2E tests using Ginkgo/Gomega framework - E2E tests using Ginkgo/Gomega framework
- Mock servers in `/test/e2e/mock/` - Mock servers in `/test/e2e/mock/`
- Run: `make e2e` or `make alltest` - Run: `make e2e` or `make alltest`
## Agent Runbooks
Operational procedures for agents are in `doc/agents/`:
- `doc/agents/release.md` - Release process
+28 -12
View File
@@ -13,6 +13,16 @@ frp is an open source project with its ongoing development made possible entirel
<h3 align="center">Gold Sponsors</h3> <h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start--> <!--gold sponsors start-->
<p align="center">
<a href="https://github.com/beclab/Olares" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
<br>
<b>The sovereign cloud that puts you in control</b>
<br>
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
</a>
</p>
<div align="center"> <div align="center">
## Recall.ai - API for meeting recordings ## Recall.ai - API for meeting recordings
@@ -40,16 +50,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and
<b>The complete IDE crafted for professional Go developers</b> <b>The complete IDE crafted for professional Go developers</b>
</a> </a>
</p> </p>
<p align="center">
<a href="https://github.com/beclab/Olares" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
<br>
<b>The sovereign cloud that puts you in control</b>
<br>
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
</a>
</p>
<!--gold sponsors end--> <!--gold sponsors end-->
## What is frp? ## What is frp?
@@ -81,6 +81,7 @@ frp also offers a P2P connect mode.
* [Split Configures Into Different Files](#split-configures-into-different-files) * [Split Configures Into Different Files](#split-configures-into-different-files)
* [Server Dashboard](#server-dashboard) * [Server Dashboard](#server-dashboard)
* [Client Admin UI](#client-admin-ui) * [Client Admin UI](#client-admin-ui)
* [Dynamic Proxy Management (Store)](#dynamic-proxy-management-store)
* [Monitor](#monitor) * [Monitor](#monitor)
* [Prometheus](#prometheus) * [Prometheus](#prometheus)
* [Authenticating the Client](#authenticating-the-client) * [Authenticating the Client](#authenticating-the-client)
@@ -149,7 +150,9 @@ We sincerely appreciate your support for frp.
## Architecture ## Architecture
![architecture](/doc/pic/architecture.png) <p align="center">
<img src="/doc/pic/architecture.jpg" alt="architecture" width="760">
</p>
## Example Usage ## Example Usage
@@ -593,7 +596,7 @@ Then visit `https://[serverAddr]:7500` to see the dashboard in secure HTTPS conn
### Client Admin UI ### Client Admin UI
The Client Admin UI helps you check and manage frpc's configuration. The Client Admin UI helps you check and manage frpc's configuration and proxies.
Configure an address for admin UI to enable this feature: Configure an address for admin UI to enable this feature:
@@ -606,6 +609,19 @@ webServer.password = "admin"
Then visit `http://127.0.0.1:7400` to see admin UI, with username and password both being `admin`. Then visit `http://127.0.0.1:7400` to see admin UI, with username and password both being `admin`.
#### Dynamic Proxy Management (Store)
You can dynamically create, update, and delete proxies and visitors at runtime through the Web UI or API, without restarting frpc.
To enable this feature, configure `store.path` to specify a file for persisting the configurations:
```toml
[store]
path = "./db.json"
```
Proxies and visitors managed through the Store are saved to disk and automatically restored on frpc restart. They work alongside proxies defined in the configuration file — Store entries take precedence when names conflict.
### Monitor ### Monitor
When web server is enabled, frps will save monitor data in cache for 7 days. It will be cleared after process restart. When web server is enabled, frps will save monitor data in cache for 7 days. It will be cleared after process restart.
+10 -10
View File
@@ -15,6 +15,16 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
<h3 align="center">Gold Sponsors</h3> <h3 align="center">Gold Sponsors</h3>
<!--gold sponsors start--> <!--gold sponsors start-->
<p align="center">
<a href="https://github.com/beclab/Olares" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
<br>
<b>The sovereign cloud that puts you in control</b>
<br>
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
</a>
</p>
<div align="center"> <div align="center">
## Recall.ai - API for meeting recordings ## Recall.ai - API for meeting recordings
@@ -42,16 +52,6 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and
<b>The complete IDE crafted for professional Go developers</b> <b>The complete IDE crafted for professional Go developers</b>
</a> </a>
</p> </p>
<p align="center">
<a href="https://github.com/beclab/Olares" target="_blank">
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
<br>
<b>The sovereign cloud that puts you in control</b>
<br>
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
</a>
</p>
<!--gold sponsors end--> <!--gold sponsors end-->
## 为什么使用 frp ## 为什么使用 frp
+2 -9
View File
@@ -1,10 +1,3 @@
## Features ## Fixes
* Added a built-in `store` capability for frpc, including persisted store source (`[store] path = "..."`), Store CRUD admin APIs (`/api/store/proxies*`, `/api/store/visitors*`) with runtime reload, and Store management pages in the frpc web dashboard. * Fixed a configuration-dependent authentication bypass in `type = "http"` proxies when `routeByHTTPUser` is used together with `httpUser` / `httpPassword`. This affected proxy-style requests. Proxy-style authentication failures now return `407 Proxy Authentication Required`.
## Improvements
* Kept proxy/visitor names as raw config names during completion; moved user-prefix handling to explicit wire-level naming logic.
* Added `noweb` build tag to allow compiling without frontend assets. `make build` now auto-detects missing `web/*/dist` directories and skips embedding, so a fresh clone can build without running `make web` first. The dashboard gracefully returns 404 when assets are not embedded.
* Improved config parsing errors: for `.toml` files, syntax errors now return immediately with parser position details (line/column when available) instead of falling through to YAML/JSON parsing, and TOML type mismatches report field-level errors without misleading line numbers.
* OIDC auth now caches the access token and refreshes it before expiry, avoiding a new token request on every heartbeat. Falls back to per-request fetch when the provider omits `expires_in`.
+1
View File
@@ -119,6 +119,7 @@ func (c *defaultConnectorImpl) Open() error {
fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024 fmuxCfg.MaxStreamWindowSize = 6 * 1024 * 1024
session, err := fmux.Client(conn, fmuxCfg) session, err := fmux.Client(conn, fmuxCfg)
if err != nil { if err != nil {
conn.Close()
return err return err
} }
c.muxSession = session c.muxSession = session
+80
View File
@@ -0,0 +1,80 @@
# Release Process
## 1. Update Release Notes
Edit `Release.md` in the project root with the changes for this version:
```markdown
## Features
* ...
## Improvements
* ...
## Fixes
* ...
```
This file is used by GoReleaser as the GitHub Release body.
## 2. Bump Version
Update the version string in `pkg/util/version/version.go`:
```go
var version = "0.X.0"
```
Commit and push to `dev`:
```bash
git add pkg/util/version/version.go Release.md
git commit -m "bump version to vX.Y.Z"
git push origin dev
```
## 3. Merge dev → master
Create a PR from `dev` to `master`:
```bash
gh pr create --base master --head dev --title "bump version"
```
Wait for CI to pass, then merge using **merge commit** (not squash).
## 4. Tag the Release
```bash
git checkout master
git pull origin master
git tag -a vX.Y.Z -m "bump version"
git push origin vX.Y.Z
```
## 5. Trigger GoReleaser
Manually trigger the `goreleaser` workflow in GitHub Actions:
```bash
gh workflow run goreleaser --ref master
```
GoReleaser will:
1. Run `package.sh` to cross-compile all platforms and create archives
2. Create a GitHub Release with all packages, using `Release.md` as release notes
## Key Files
| File | Purpose |
|------|---------|
| `pkg/util/version/version.go` | Version string |
| `Release.md` | Release notes (read by GoReleaser) |
| `.goreleaser.yml` | GoReleaser config |
| `package.sh` | Cross-compile and packaging script |
| `.github/workflows/goreleaser.yml` | GitHub Actions workflow (manual trigger) |
## Versioning
- Minor release: `v0.X.0`
- Patch release: `v0.X.Y` (e.g., `v0.62.1`)
Binary file not shown.

After

Width:  |  Height:  |  Size: 84 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 10 KiB

+5 -2
View File
@@ -1,8 +1,11 @@
FROM node:22 AS web-builder FROM node:22 AS web-builder
WORKDIR /web/frpc COPY web/package.json /web/package.json
COPY web/frpc/ ./ COPY web/shared/ /web/shared/
COPY web/frpc/ /web/frpc/
WORKDIR /web
RUN npm install RUN npm install
WORKDIR /web/frpc
RUN npm run build RUN npm run build
FROM golang:1.25 AS building FROM golang:1.25 AS building
+5 -2
View File
@@ -1,8 +1,11 @@
FROM node:22 AS web-builder FROM node:22 AS web-builder
WORKDIR /web/frps COPY web/package.json /web/package.json
COPY web/frps/ ./ COPY web/shared/ /web/shared/
COPY web/frps/ /web/frps/
WORKDIR /web
RUN npm install RUN npm install
WORKDIR /web/frps
RUN npm run build RUN npm run build
FROM golang:1.25 AS building FROM golang:1.25 AS building
+16 -16
View File
@@ -5,7 +5,7 @@ go 1.25.0
require ( require (
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
github.com/coreos/go-oidc/v3 v3.14.1 github.com/coreos/go-oidc/v3 v3.14.1
github.com/fatedier/golib v0.5.1 github.com/fatedier/golib v0.6.0
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/gorilla/mux v1.8.1 github.com/gorilla/mux v1.8.1
github.com/gorilla/websocket v1.5.0 github.com/gorilla/websocket v1.5.0
@@ -13,7 +13,7 @@ require (
github.com/onsi/ginkgo/v2 v2.23.4 github.com/onsi/ginkgo/v2 v2.23.4
github.com/onsi/gomega v1.36.3 github.com/onsi/gomega v1.36.3
github.com/pelletier/go-toml/v2 v2.2.0 github.com/pelletier/go-toml/v2 v2.2.0
github.com/pion/stun/v2 v2.0.0 github.com/pion/stun/v3 v3.1.1
github.com/pires/go-proxyproto v0.7.0 github.com/pires/go-proxyproto v0.7.0
github.com/prometheus/client_golang v1.19.1 github.com/prometheus/client_golang v1.19.1
github.com/quic-go/quic-go v0.55.0 github.com/quic-go/quic-go v0.55.0
@@ -22,15 +22,15 @@ require (
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8
github.com/spf13/cobra v1.8.0 github.com/spf13/cobra v1.8.0
github.com/spf13/pflag v1.0.5 github.com/spf13/pflag v1.0.5
github.com/stretchr/testify v1.10.0 github.com/stretchr/testify v1.11.1
github.com/tidwall/gjson v1.17.1 github.com/tidwall/gjson v1.17.1
github.com/vishvananda/netlink v1.3.0 github.com/vishvananda/netlink v1.3.0
github.com/xtaci/kcp-go/v5 v5.6.13 github.com/xtaci/kcp-go/v5 v5.6.13
golang.org/x/crypto v0.41.0 golang.org/x/crypto v0.49.0
golang.org/x/net v0.43.0 golang.org/x/net v0.52.0
golang.org/x/oauth2 v0.28.0 golang.org/x/oauth2 v0.28.0
golang.org/x/sync v0.16.0 golang.org/x/sync v0.20.0
golang.org/x/time v0.5.0 golang.org/x/time v0.10.0
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
gopkg.in/ini.v1 v1.67.0 gopkg.in/ini.v1 v1.67.0
k8s.io/apimachinery v0.28.8 k8s.io/apimachinery v0.28.8
@@ -38,7 +38,7 @@ require (
) )
require ( require (
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect github.com/Azure/go-ntlmssp v0.1.0 // indirect
github.com/beorn7/perks v1.0.1 // indirect github.com/beorn7/perks v1.0.1 // indirect
github.com/cespare/xxhash/v2 v2.2.0 // indirect github.com/cespare/xxhash/v2 v2.2.0 // indirect
github.com/davecgh/go-spew v1.1.1 // indirect github.com/davecgh/go-spew v1.1.1 // indirect
@@ -51,10 +51,9 @@ require (
github.com/inconshreveable/mousetrap v1.1.0 // indirect github.com/inconshreveable/mousetrap v1.1.0 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/klauspost/reedsolomon v1.12.0 // indirect github.com/klauspost/reedsolomon v1.12.0 // indirect
github.com/pion/dtls/v2 v2.2.7 // indirect github.com/pion/dtls/v3 v3.0.10 // indirect
github.com/pion/logging v0.2.2 // indirect github.com/pion/logging v0.2.4 // indirect
github.com/pion/transport/v2 v2.2.1 // indirect github.com/pion/transport/v4 v4.0.1 // indirect
github.com/pion/transport/v3 v3.0.1 // indirect
github.com/pkg/errors v0.9.1 // indirect github.com/pkg/errors v0.9.1 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect
github.com/prometheus/client_model v0.5.0 // indirect github.com/prometheus/client_model v0.5.0 // indirect
@@ -66,11 +65,12 @@ require (
github.com/tidwall/pretty v1.2.0 // indirect github.com/tidwall/pretty v1.2.0 // indirect
github.com/tjfoc/gmsm v1.4.1 // indirect github.com/tjfoc/gmsm v1.4.1 // indirect
github.com/vishvananda/netns v0.0.4 // indirect github.com/vishvananda/netns v0.0.4 // indirect
github.com/wlynxg/anet v0.0.5 // indirect
go.uber.org/automaxprocs v1.6.0 // indirect go.uber.org/automaxprocs v1.6.0 // indirect
golang.org/x/mod v0.27.0 // indirect golang.org/x/mod v0.33.0 // indirect
golang.org/x/sys v0.35.0 // indirect golang.org/x/sys v0.42.0 // indirect
golang.org/x/text v0.28.0 // indirect golang.org/x/text v0.35.0 // indirect
golang.org/x/tools v0.36.0 // indirect golang.org/x/tools v0.42.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
google.golang.org/protobuf v1.36.5 // indirect google.golang.org/protobuf v1.36.5 // indirect
gopkg.in/yaml.v2 v2.4.0 // indirect gopkg.in/yaml.v2 v2.4.0 // indirect
+34 -71
View File
@@ -1,6 +1,6 @@
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw= cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8= github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU= github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs= github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
@@ -20,8 +20,8 @@ github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSs
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4= github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98= github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c= github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
github.com/fatedier/golib v0.5.1 h1:hcKAnaw5mdI/1KWRGejxR+i1Hn/NvbY5UsMKDr7o13M= github.com/fatedier/golib v0.6.0 h1:/mgBZZbkbMhIEZoXf7nV8knpUDzas/b+2ruYKxx1lww=
github.com/fatedier/golib v0.5.1/go.mod h1:W6kIYkIFxHsTzbgqg5piCxIiDo4LzwgTY6R5W8l9NFQ= github.com/fatedier/golib v0.6.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE= github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34= github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE= github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
@@ -78,16 +78,14 @@ github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0= github.com/onsi/gomega v1.36.3/go.mod h1:8D9+Txp43QWKhM24yyOBEdpkzN8FvJyAwecBgsU4KU0=
github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo= github.com/pelletier/go-toml/v2 v2.2.0 h1:QLgLl2yMN7N+ruc31VynXs1vhMZa7CeHHejIeBAsoHo=
github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs= github.com/pelletier/go-toml/v2 v2.2.0/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v3 v3.0.10 h1:k9ekkq1kaZoxnNEbyLKI8DI37j/Nbk1HWmMuywpQJgg=
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s= github.com/pion/dtls/v3 v3.0.10/go.mod h1:YEmmBYIoBsY3jmG56dsziTv/Lca9y4Om83370CXfqJ8=
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY= github.com/pion/logging v0.2.4 h1:tTew+7cmQ+Mc1pTBLKH2puKsOvhm32dROumOZ655zB8=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= github.com/pion/logging v0.2.4/go.mod h1:DffhXTKYdNZU+KtJ5pyQDjvOAh/GsNSyv1lbkFbe3so=
github.com/pion/stun/v2 v2.0.0 h1:A5+wXKLAypxQri59+tmQKVs7+l6mMM+3d+eER9ifRU0= github.com/pion/stun/v3 v3.1.1 h1:CkQxveJ4xGQjulGSROXbXq94TAWu8gIX2dT+ePhUkqw=
github.com/pion/stun/v2 v2.0.0/go.mod h1:22qRSh08fSEttYUmJZGlriq9+03jtVmXNODgLccj8GQ= github.com/pion/stun/v3 v3.1.1/go.mod h1:qC1DfmcCTQjl9PBaMa5wSn3x9IPmKxSdcCsxBcDBndM=
github.com/pion/transport/v2 v2.2.1 h1:7qYnCBlpgSJNYMbLCKuSY9KbQdBFoETvPNETv0y4N7c= github.com/pion/transport/v4 v4.0.1 h1:sdROELU6BZ63Ab7FrOLn13M6YdJLY20wldXW2Cu2k8o=
github.com/pion/transport/v2 v2.2.1/go.mod h1:cXXWavvCnFF6McHTft3DWS9iic2Mftcz1Aq29pGcU5g= github.com/pion/transport/v4 v4.0.1/go.mod h1:nEuEA4AD5lPdcIegQDpVLgNoDGreqM/YqmEx3ovP4jM=
github.com/pion/transport/v3 v3.0.1 h1:gDTlPJwROfSfz6QfSi0ZmeCSkFcnWWiiR9ES0ouANiM=
github.com/pion/transport/v3 v3.0.1/go.mod h1:UY7kiITrlMv7/IKgd5eTUcaahZx5oUN3l9SzK5f5xE0=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
@@ -128,11 +126,10 @@ github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpE
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA= github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.3/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA= github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY= github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI= github.com/templexxx/cpu v0.1.1 h1:isxHaxBXpYFWnk2DReuKkigaZyrjs2+9ypIdGP4h+HI=
github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk= github.com/templexxx/cpu v0.1.1/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU= github.com/templexxx/xorsimd v0.4.3 h1:9AQTFHd7Bhk3dIT7Al2XeBX5DWOvsUPZCuhyAtNbHjU=
@@ -149,11 +146,12 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs= github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8= github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
github.com/wlynxg/anet v0.0.5 h1:J3VJGi1gvo0JwZ/P1/Yc/8p63SoW98B5dHkYDmpgvvU=
github.com/wlynxg/anet v0.0.5/go.mod h1:eay5PRQr7fIVAMbTbchTnO9gG65Hg/uYGdc7mguHxoA=
github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk= github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM= github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0= github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs= go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8= go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko= go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
@@ -161,89 +159,54 @@ go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= 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.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
golang.org/x/crypto v0.8.0/go.mod h1:mRqEX+O9/h5TFCrQhkgjo2yKi0yYA+9ecGkdQoHrywE= golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98yw=
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE= golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU= golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc= golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs= golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.9.0/go.mod h1:d48xBJpPfHeWQsugry2m+kC02ZBRGRgulfHnEXEuWns=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.14.0/go.mod h1:PpSgVXXLK0OxS0F31C1/tv6XNguvCrnXIDrFMspZIUI=
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc= golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8= golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI= golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
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.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.11.0/go.mod h1:zC9APTIj3jG3FdV/Ons+XE1riIZXG4aZ4GTHiPZJPIU=
golang.org/x/term v0.34.0 h1:O/2T7POpk0ZZ7MAzMeWFSg6S5IpWd/RXDlM9hgM3DR4=
golang.org/x/term v0.34.0/go.mod h1:5jC53AEywhIVebHgPVeg0mj8OD3VO9OzclacVrqpaAw=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8= golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
golang.org/x/text v0.12.0/go.mod h1:TvPlkZtksWOMsz7fbANvkp4WM8x/WCo/om8BMLbz+aE= golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY= golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs= golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q= golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI= golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
+1 -1
View File
@@ -19,7 +19,7 @@ import (
"net" "net"
"time" "time"
"github.com/pion/stun/v2" "github.com/pion/stun/v3"
) )
var responseTimeout = 3 * time.Second var responseTimeout = 3 * time.Second
+1 -1
View File
@@ -21,7 +21,7 @@ import (
"strconv" "strconv"
"github.com/fatedier/golib/crypto" "github.com/fatedier/golib/crypto"
"github.com/pion/stun/v2" "github.com/pion/stun/v3"
"github.com/fatedier/frp/pkg/msg" "github.com/fatedier/frp/pkg/msg"
) )
+1 -1
View File
@@ -14,7 +14,7 @@
package version package version
var version = "0.68.0" var version = "0.68.1"
func Full() string { func Full() string {
return version return version
+41 -22
View File
@@ -187,16 +187,25 @@ func (rp *HTTPReverseProxy) CreateConnection(reqRouteInfo *RequestRouteInfo, byE
return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, host, reqRouteInfo.URL, reqRouteInfo.HTTPUser) return nil, fmt.Errorf("%v: %s %s %s", ErrNoRouteFound, host, reqRouteInfo.URL, reqRouteInfo.HTTPUser)
} }
func (rp *HTTPReverseProxy) CheckAuth(domain, location, routeByHTTPUser, user, passwd string) bool { func checkRouteAuthByRequest(req *http.Request, rc *RouteConfig) bool {
vr, ok := rp.getVhost(domain, location, routeByHTTPUser) if rc == nil {
if ok { return true
checkUser := vr.payload.(*RouteConfig).Username }
checkPasswd := vr.payload.(*RouteConfig).Password if rc.Username == "" && rc.Password == "" {
if (checkUser != "" || checkPasswd != "") && (checkUser != user || checkPasswd != passwd) { return true
}
if req.URL.Host != "" {
proxyAuth := req.Header.Get("Proxy-Authorization")
if proxyAuth == "" {
return false return false
} }
user, passwd, ok := httppkg.ParseBasicAuth(proxyAuth)
return ok && user == rc.Username && passwd == rc.Password
} }
return true
user, passwd, ok := req.BasicAuth()
return ok && user == rc.Username && passwd == rc.Password
} }
// getVhost tries to get vhost router by route policy. // getVhost tries to get vhost router by route policy.
@@ -266,18 +275,25 @@ func (rp *HTTPReverseProxy) connectHandler(rw http.ResponseWriter, req *http.Req
go libio.Join(remote, client) go libio.Join(remote, client)
} }
func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request { func getRequestRouteUser(req *http.Request) string {
user := ""
// If url host isn't empty, it's a proxy request. Get http user from Proxy-Authorization header.
if req.URL.Host != "" { if req.URL.Host != "" {
proxyAuth := req.Header.Get("Proxy-Authorization") proxyAuth := req.Header.Get("Proxy-Authorization")
if proxyAuth != "" { if proxyAuth == "" {
user, _, _ = httppkg.ParseBasicAuth(proxyAuth) // Preserve legacy proxy-mode routing when clients send only Authorization,
// so requests still hit the matched route and return 407 instead of 404.
// Auth validation intentionally does not share this fallback.
user, _, _ := req.BasicAuth()
return user
} }
user, _, _ := httppkg.ParseBasicAuth(proxyAuth)
return user
} }
if user == "" { user, _, _ := req.BasicAuth()
user, _, _ = req.BasicAuth() return user
} }
func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Request {
user := getRequestRouteUser(req)
reqRouteInfo := &RequestRouteInfo{ reqRouteInfo := &RequestRouteInfo{
URL: req.URL.Path, URL: req.URL.Path,
@@ -297,16 +313,19 @@ func (rp *HTTPReverseProxy) injectRequestInfoToCtx(req *http.Request) *http.Requ
} }
func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) { func (rp *HTTPReverseProxy) ServeHTTP(rw http.ResponseWriter, req *http.Request) {
domain, _ := httppkg.CanonicalHost(req.Host) newreq := rp.injectRequestInfoToCtx(req)
location := req.URL.Path rc := newreq.Context().Value(RouteConfigKey).(*RouteConfig)
user, passwd, _ := req.BasicAuth() if !checkRouteAuthByRequest(req, rc) {
if !rp.CheckAuth(domain, location, user, user, passwd) { if req.URL.Host != "" {
rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`) rw.Header().Set("Proxy-Authenticate", `Basic realm="Restricted"`)
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized) http.Error(rw, http.StatusText(http.StatusProxyAuthRequired), http.StatusProxyAuthRequired)
} else {
rw.Header().Set("WWW-Authenticate", `Basic realm="Restricted"`)
http.Error(rw, http.StatusText(http.StatusUnauthorized), http.StatusUnauthorized)
}
return return
} }
newreq := rp.injectRequestInfoToCtx(req)
if req.Method == http.MethodConnect { if req.Method == http.MethodConnect {
rp.connectHandler(rw, newreq) rp.connectHandler(rw, newreq)
} else { } else {
+102
View File
@@ -0,0 +1,102 @@
package vhost
import (
"net/http/httptest"
"testing"
"github.com/stretchr/testify/require"
httppkg "github.com/fatedier/frp/pkg/util/http"
)
func TestCheckRouteAuthByRequest(t *testing.T) {
rc := &RouteConfig{
Username: "alice",
Password: "secret",
}
t.Run("accepts nil route config", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
require.True(t, checkRouteAuthByRequest(req, nil))
})
t.Run("accepts route without credentials", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
require.True(t, checkRouteAuthByRequest(req, &RouteConfig{}))
})
t.Run("accepts authorization header", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.SetBasicAuth("alice", "secret")
require.True(t, checkRouteAuthByRequest(req, rc))
})
t.Run("accepts proxy authorization header", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "secret"))
require.True(t, checkRouteAuthByRequest(req, rc))
})
t.Run("rejects authorization fallback for proxy request", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.SetBasicAuth("alice", "secret")
require.False(t, checkRouteAuthByRequest(req, rc))
})
t.Run("rejects wrong proxy authorization even when authorization matches", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.SetBasicAuth("alice", "secret")
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "wrong"))
require.False(t, checkRouteAuthByRequest(req, rc))
})
t.Run("rejects when neither header matches", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.SetBasicAuth("alice", "wrong")
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "wrong"))
require.False(t, checkRouteAuthByRequest(req, rc))
})
t.Run("rejects proxy authorization on direct request", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("alice", "secret"))
require.False(t, checkRouteAuthByRequest(req, rc))
})
}
func TestGetRequestRouteUser(t *testing.T) {
t.Run("proxy request uses proxy authorization username", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.Host = "target.example.com"
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("proxy-user", "proxy-pass"))
req.SetBasicAuth("direct-user", "direct-pass")
require.Equal(t, "proxy-user", getRequestRouteUser(req))
})
t.Run("connect request keeps proxy authorization routing", func(t *testing.T) {
req := httptest.NewRequest("CONNECT", "http://target.example.com:443", nil)
req.Host = "target.example.com:443"
req.Header.Set("Proxy-Authorization", httppkg.BasicAuth("proxy-user", "proxy-pass"))
req.SetBasicAuth("direct-user", "direct-pass")
require.Equal(t, "proxy-user", getRequestRouteUser(req))
})
t.Run("direct request uses authorization username", func(t *testing.T) {
req := httptest.NewRequest("GET", "/", nil)
req.Host = "example.com"
req.SetBasicAuth("direct-user", "direct-pass")
require.Equal(t, "direct-user", getRequestRouteUser(req))
})
t.Run("proxy request does not fall back when proxy authorization is invalid", func(t *testing.T) {
req := httptest.NewRequest("GET", "http://target.example.com/", nil)
req.Host = "target.example.com"
req.Header.Set("Proxy-Authorization", "Basic !!!")
req.SetBasicAuth("direct-user", "direct-pass")
require.Empty(t, getRequestRouteUser(req))
})
}
+25
View File
@@ -241,6 +241,31 @@ func (f *Framework) AllocPort() int {
return port return port
} }
func (f *Framework) AllocPortExcludingRanges(ranges ...[2]int) int {
for range 1000 {
port := f.portAllocator.Get()
ExpectTrue(port > 0, "alloc port failed")
inExcludedRange := false
for _, portRange := range ranges {
if port >= portRange[0] && port <= portRange[1] {
inExcludedRange = true
break
}
}
if inExcludedRange {
f.portAllocator.Release(port)
continue
}
f.allocatedPorts = append(f.allocatedPorts, port)
return port
}
Failf("alloc port outside excluded ranges failed")
return 0
}
func (f *Framework) ReleasePort(port int) { func (f *Framework) ReleasePort(port int) {
f.portAllocator.Release(port) f.portAllocator.Release(port)
} }
+8 -6
View File
@@ -20,6 +20,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
ginkgo.It("Ports Whitelist", func() { ginkgo.It("Ports Whitelist", func() {
serverConf := consts.LegacyDefaultServerConfig serverConf := consts.LegacyDefaultServerConfig
clientConf := consts.LegacyDefaultClientConfig clientConf := consts.LegacyDefaultClientConfig
tcpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000})
udpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000})
serverConf += ` serverConf += `
allow_ports = 10000-11000,11002,12000-13000 allow_ports = 10000-11000,11002,12000-13000
@@ -37,8 +39,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
[tcp-port-not-allowed] [tcp-port-not-allowed]
type = tcp type = tcp
local_port = {{ .%s }} local_port = {{ .%s }}
remote_port = 11001 remote_port = %d
`, framework.TCPEchoServerPort) `, framework.TCPEchoServerPort, tcpPortNotAllowed)
clientConf += fmt.Sprintf(` clientConf += fmt.Sprintf(`
[tcp-port-unavailable] [tcp-port-unavailable]
type = tcp type = tcp
@@ -55,8 +57,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
[udp-port-not-allowed] [udp-port-not-allowed]
type = udp type = udp
local_port = {{ .%s }} local_port = {{ .%s }}
remote_port = 11003 remote_port = %d
`, framework.UDPEchoServerPort) `, framework.UDPEchoServerPort, udpPortNotAllowed)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses(serverConf, []string{clientConf})
@@ -65,7 +67,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
framework.NewRequestExpect(f).PortName(tcpPortName).Ensure() framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
// Not Allowed // Not Allowed
framework.NewRequestExpect(f).Port(11001).ExpectError(true).Ensure() framework.NewRequestExpect(f).Port(tcpPortNotAllowed).ExpectError(true).Ensure()
// Unavailable, already bind by frps // Unavailable, already bind by frps
framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure() framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
@@ -76,7 +78,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
// Not Allowed // Not Allowed
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
r.UDP().Port(11003) r.UDP().Port(udpPortNotAllowed)
}).ExpectError(true).Ensure() }).ExpectError(true).Ensure()
}) })
+73
View File
@@ -144,6 +144,79 @@ var _ = ginkgo.Describe("[Feature: HTTP]", func() {
Ensure() Ensure()
}) })
ginkgo.It("HTTP proxy mode uses proxy auth consistently", func() {
vhostHTTPPort := f.AllocPort()
serverConf := getDefaultServerConf(vhostHTTPPort)
backendPort := f.AllocPort()
f.RunServer("", newHTTPServer(backendPort, "PRIVATE"))
clientConf := consts.DefaultClientConfig
clientConf += fmt.Sprintf(`
[[proxies]]
name = "protected"
type = "http"
localPort = %d
customDomains = ["normal.example.com"]
routeByHTTPUser = "alice"
httpUser = "alice"
httpPassword = "secret"
`, backendPort)
f.RunProcesses(serverConf, []string{clientConf})
proxyURLWithAuth := func(username, password string) string {
if username == "" {
return fmt.Sprintf("http://127.0.0.1:%d", vhostHTTPPort)
}
return fmt.Sprintf("http://%s:%s@127.0.0.1:%d", username, password, vhostHTTPPort)
}
framework.NewRequestExpect(f).Explain("direct no auth").Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com")
}).
Ensure(framework.ExpectResponseCode(http.StatusNotFound))
framework.NewRequestExpect(f).Explain("direct correct auth").Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com").HTTPAuth("alice", "secret")
}).
ExpectResp([]byte("PRIVATE")).
Ensure()
framework.NewRequestExpect(f).Explain("direct wrong auth").Port(vhostHTTPPort).
RequestModify(func(r *request.Request) {
r.HTTP().HTTPHost("normal.example.com").HTTPAuth("alice", "wrong")
}).
Ensure(framework.ExpectResponseCode(http.StatusUnauthorized))
framework.NewRequestExpect(f).Explain("proxy correct proxy auth").
RequestModify(func(r *request.Request) {
r.HTTP().Addr("normal.example.com").Proxy(proxyURLWithAuth("alice", "secret"))
}).
ExpectResp([]byte("PRIVATE")).
Ensure()
framework.NewRequestExpect(f).Explain("proxy wrong proxy auth").
RequestModify(func(r *request.Request) {
r.HTTP().Addr("normal.example.com").Proxy(proxyURLWithAuth("alice", "wrong"))
}).
Ensure(framework.ExpectResponseCode(http.StatusProxyAuthRequired))
framework.NewRequestExpect(f).Explain("proxy request ignores authorization header").
RequestModify(func(r *request.Request) {
r.HTTP().Addr("normal.example.com").Proxy(proxyURLWithAuth("", "")).HTTPAuth("alice", "secret")
}).
Ensure(framework.ExpectResponseCode(http.StatusProxyAuthRequired))
framework.NewRequestExpect(f).Explain("proxy wrong proxy auth with correct authorization").
RequestModify(func(r *request.Request) {
r.HTTP().Addr("normal.example.com").Proxy(proxyURLWithAuth("alice", "wrong")).HTTPAuth("alice", "secret")
}).
Ensure(framework.ExpectResponseCode(http.StatusProxyAuthRequired))
})
ginkgo.It("HTTP Basic Auth", func() { ginkgo.It("HTTP Basic Auth", func() {
vhostHTTPPort := f.AllocPort() vhostHTTPPort := f.AllocPort()
serverConf := getDefaultServerConf(vhostHTTPPort) serverConf := getDefaultServerConf(vhostHTTPPort)
+8 -6
View File
@@ -20,6 +20,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
ginkgo.It("Ports Whitelist", func() { ginkgo.It("Ports Whitelist", func() {
serverConf := consts.DefaultServerConfig serverConf := consts.DefaultServerConfig
clientConf := consts.DefaultClientConfig clientConf := consts.DefaultClientConfig
tcpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000})
udpPortNotAllowed := f.AllocPortExcludingRanges([2]int{10000, 11000}, [2]int{11002, 11002}, [2]int{12000, 13000})
serverConf += ` serverConf += `
allowPorts = [ allowPorts = [
@@ -43,8 +45,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
name = "tcp-port-not-allowed" name = "tcp-port-not-allowed"
type = "tcp" type = "tcp"
localPort = {{ .%s }} localPort = {{ .%s }}
remotePort = 11001 remotePort = %d
`, framework.TCPEchoServerPort) `, framework.TCPEchoServerPort, tcpPortNotAllowed)
clientConf += fmt.Sprintf(` clientConf += fmt.Sprintf(`
[[proxies]] [[proxies]]
name = "tcp-port-unavailable" name = "tcp-port-unavailable"
@@ -64,8 +66,8 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
name = "udp-port-not-allowed" name = "udp-port-not-allowed"
type = "udp" type = "udp"
localPort = {{ .%s }} localPort = {{ .%s }}
remotePort = 11003 remotePort = %d
`, framework.UDPEchoServerPort) `, framework.UDPEchoServerPort, udpPortNotAllowed)
f.RunProcesses(serverConf, []string{clientConf}) f.RunProcesses(serverConf, []string{clientConf})
@@ -74,7 +76,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
framework.NewRequestExpect(f).PortName(tcpPortName).Ensure() framework.NewRequestExpect(f).PortName(tcpPortName).Ensure()
// Not Allowed // Not Allowed
framework.NewRequestExpect(f).Port(11001).ExpectError(true).Ensure() framework.NewRequestExpect(f).Port(tcpPortNotAllowed).ExpectError(true).Ensure()
// Unavailable, already bind by frps // Unavailable, already bind by frps
framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure() framework.NewRequestExpect(f).PortName(consts.PortServerName).ExpectError(true).Ensure()
@@ -85,7 +87,7 @@ var _ = ginkgo.Describe("[Feature: Server Manager]", func() {
// Not Allowed // Not Allowed
framework.NewRequestExpect(f).RequestModify(func(r *request.Request) { framework.NewRequestExpect(f).RequestModify(func(r *request.Request) {
r.UDP().Port(11003) r.UDP().Port(udpPortNotAllowed)
}).ExpectError(true).Ensure() }).ExpectError(true).Ensure()
}) })
+1 -1
View File
@@ -1,7 +1,7 @@
.PHONY: dist install build preview lint .PHONY: dist install build preview lint
install: install:
@npm install @cd .. && npm install
build: install build: install
@npm run build @npm run build
-6
View File
@@ -7,11 +7,8 @@ export {}
declare module 'vue' { declare module 'vue' {
export interface GlobalComponents { export interface GlobalComponents {
ActionButton: typeof import('./src/components/ActionButton.vue')['default']
BaseDialog: typeof import('./src/components/BaseDialog.vue')['default']
ConfigField: typeof import('./src/components/ConfigField.vue')['default'] ConfigField: typeof import('./src/components/ConfigField.vue')['default']
ConfigSection: typeof import('./src/components/ConfigSection.vue')['default'] ConfigSection: typeof import('./src/components/ConfigSection.vue')['default']
ConfirmDialog: typeof import('./src/components/ConfirmDialog.vue')['default']
ElDialog: typeof import('element-plus/es')['ElDialog'] ElDialog: typeof import('element-plus/es')['ElDialog']
ElForm: typeof import('element-plus/es')['ElForm'] ElForm: typeof import('element-plus/es')['ElForm']
ElFormItem: typeof import('element-plus/es')['ElFormItem'] ElFormItem: typeof import('element-plus/es')['ElFormItem']
@@ -21,10 +18,7 @@ declare module 'vue' {
ElRadio: typeof import('element-plus/es')['ElRadio'] ElRadio: typeof import('element-plus/es')['ElRadio']
ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup'] ElRadioGroup: typeof import('element-plus/es')['ElRadioGroup']
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import('element-plus/es')['ElSwitch']
FilterDropdown: typeof import('./src/components/FilterDropdown.vue')['default']
KeyValueEditor: typeof import('./src/components/KeyValueEditor.vue')['default'] KeyValueEditor: typeof import('./src/components/KeyValueEditor.vue')['default']
PopoverMenu: typeof import('./src/components/PopoverMenu.vue')['default']
PopoverMenuItem: typeof import('./src/components/PopoverMenuItem.vue')['default']
ProxyAuthSection: typeof import('./src/components/proxy-form/ProxyAuthSection.vue')['default'] ProxyAuthSection: typeof import('./src/components/proxy-form/ProxyAuthSection.vue')['default']
ProxyBackendSection: typeof import('./src/components/proxy-form/ProxyBackendSection.vue')['default'] ProxyBackendSection: typeof import('./src/components/proxy-form/ProxyBackendSection.vue')['default']
ProxyBaseSection: typeof import('./src/components/proxy-form/ProxyBaseSection.vue')['default'] ProxyBaseSection: typeof import('./src/components/proxy-form/ProxyBaseSection.vue')['default']
+1 -1
View File
@@ -1,4 +1,4 @@
@use './mixins' as *; @use '@shared/css/mixins' as *;
/* Shared form layout styles for proxy/visitor form sections */ /* Shared form layout styles for proxy/visitor form sections */
.field-row { .field-row {
+2 -2
View File
@@ -115,8 +115,8 @@
import { computed } from 'vue' import { computed } from 'vue'
import KeyValueEditor from './KeyValueEditor.vue' import KeyValueEditor from './KeyValueEditor.vue'
import StringListEditor from './StringListEditor.vue' import StringListEditor from './StringListEditor.vue'
import PopoverMenu from './PopoverMenu.vue' import PopoverMenu from '@shared/components/PopoverMenu.vue'
import PopoverMenuItem from './PopoverMenuItem.vue' import PopoverMenuItem from '@shared/components/PopoverMenuItem.vue'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
+3 -3
View File
@@ -53,9 +53,9 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { MoreFilled, Edit, Delete, Open, TurnOff } from '@element-plus/icons-vue' import { MoreFilled, Edit, Delete, Open, TurnOff } from '@element-plus/icons-vue'
import ActionButton from './ActionButton.vue' import ActionButton from '@shared/components/ActionButton.vue'
import PopoverMenu from './PopoverMenu.vue' import PopoverMenu from '@shared/components/PopoverMenu.vue'
import PopoverMenuItem from './PopoverMenuItem.vue' import PopoverMenuItem from '@shared/components/PopoverMenuItem.vue'
import type { ProxyStatus } from '../types' import type { ProxyStatus } from '../types'
interface Props { interface Props {
+8 -4
View File
@@ -1,8 +1,12 @@
import { createRouter, createWebHashHistory } from 'vue-router' import { createRouter, createWebHashHistory } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import ClientConfigure from '../views/ClientConfigure.vue' import ClientConfigure from '../views/ClientConfigure.vue'
import ProxyDetail from '../views/ProxyDetail.vue'
import ProxyEdit from '../views/ProxyEdit.vue' import ProxyEdit from '../views/ProxyEdit.vue'
import ProxyList from '../views/ProxyList.vue'
import VisitorDetail from '../views/VisitorDetail.vue'
import VisitorEdit from '../views/VisitorEdit.vue' import VisitorEdit from '../views/VisitorEdit.vue'
import VisitorList from '../views/VisitorList.vue'
import { useProxyStore } from '../stores/proxy' import { useProxyStore } from '../stores/proxy'
const router = createRouter({ const router = createRouter({
@@ -15,12 +19,12 @@ const router = createRouter({
{ {
path: '/proxies', path: '/proxies',
name: 'ProxyList', name: 'ProxyList',
component: () => import('../views/ProxyList.vue'), component: ProxyList,
}, },
{ {
path: '/proxies/detail/:name', path: '/proxies/detail/:name',
name: 'ProxyDetail', name: 'ProxyDetail',
component: () => import('../views/ProxyDetail.vue'), component: ProxyDetail,
}, },
{ {
path: '/proxies/create', path: '/proxies/create',
@@ -37,12 +41,12 @@ const router = createRouter({
{ {
path: '/visitors', path: '/visitors',
name: 'VisitorList', name: 'VisitorList',
component: () => import('../views/VisitorList.vue'), component: VisitorList,
}, },
{ {
path: '/visitors/detail/:name', path: '/visitors/detail/:name',
name: 'VisitorDetail', name: 'VisitorDetail',
component: () => import('../views/VisitorDetail.vue'), component: VisitorDetail,
}, },
{ {
path: '/visitors/create', path: '/visitors/create',
+5 -2
View File
@@ -41,6 +41,7 @@ serverPort = 7000"
message="This operation will update your frpc configuration and reload it. Do you want to continue?" message="This operation will update your frpc configuration and reload it. Do you want to continue?"
confirm-text="Update" confirm-text="Update"
:loading="uploading" :loading="uploading"
:is-mobile="isMobile"
@confirm="doUpload" @confirm="doUpload"
/> />
</div> </div>
@@ -51,9 +52,11 @@ import { ref } from 'vue'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Link } from '@element-plus/icons-vue' import { Link } from '@element-plus/icons-vue'
import { useClientStore } from '../stores/client' import { useClientStore } from '../stores/client'
import ActionButton from '../components/ActionButton.vue' import ActionButton from '@shared/components/ActionButton.vue'
import ConfirmDialog from '../components/ConfirmDialog.vue' import ConfirmDialog from '@shared/components/ConfirmDialog.vue'
import { useResponsive } from '../composables/useResponsive'
const { isMobile } = useResponsive()
const clientStore = useClientStore() const clientStore = useClientStore()
const configContent = ref('') const configContent = ref('')
+1 -1
View File
@@ -69,7 +69,7 @@ import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Warning } from '@element-plus/icons-vue' import { Warning } from '@element-plus/icons-vue'
import ActionButton from '../components/ActionButton.vue' import ActionButton from '@shared/components/ActionButton.vue'
import ProxyFormLayout from '../components/proxy-form/ProxyFormLayout.vue' import ProxyFormLayout from '../components/proxy-form/ProxyFormLayout.vue'
import { getProxyConfig, getStoreProxy } from '../api/frpc' import { getProxyConfig, getStoreProxy } from '../api/frpc'
import { useProxyStore } from '../stores/proxy' import { useProxyStore } from '../stores/proxy'
+5 -2
View File
@@ -31,6 +31,7 @@
v-model="leaveDialogVisible" v-model="leaveDialogVisible"
title="Unsaved Changes" title="Unsaved Changes"
message="You have unsaved changes. Are you sure you want to leave?" message="You have unsaved changes. Are you sure you want to leave?"
:is-mobile="isMobile"
@confirm="handleLeaveConfirm" @confirm="handleLeaveConfirm"
@cancel="handleLeaveCancel" @cancel="handleLeaveCancel"
/> />
@@ -50,10 +51,12 @@ import {
} from '../types' } from '../types'
import { getStoreProxy } from '../api/frpc' import { getStoreProxy } from '../api/frpc'
import { useProxyStore } from '../stores/proxy' import { useProxyStore } from '../stores/proxy'
import ActionButton from '../components/ActionButton.vue' import ActionButton from '@shared/components/ActionButton.vue'
import ConfirmDialog from '../components/ConfirmDialog.vue' import ConfirmDialog from '@shared/components/ConfirmDialog.vue'
import ProxyFormLayout from '../components/proxy-form/ProxyFormLayout.vue' import ProxyFormLayout from '../components/proxy-form/ProxyFormLayout.vue'
import { useResponsive } from '../composables/useResponsive'
const { isMobile } = useResponsive()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const proxyStore = useProxyStore() const proxyStore = useProxyStore()
+7 -6
View File
@@ -30,8 +30,8 @@
<el-input v-model="searchText" placeholder="Search..." clearable class="search-input"> <el-input v-model="searchText" placeholder="Search..." clearable class="search-input">
<template #prefix><el-icon><Search /></el-icon></template> <template #prefix><el-icon><Search /></el-icon></template>
</el-input> </el-input>
<FilterDropdown v-model="sourceFilter" label="Source" :options="sourceOptions" :min-width="140" /> <FilterDropdown v-model="sourceFilter" label="Source" :options="sourceOptions" :min-width="140" :is-mobile="isMobile" />
<FilterDropdown v-model="typeFilter" label="Type" :options="typeOptions" :min-width="140" /> <FilterDropdown v-model="typeFilter" label="Type" :options="typeOptions" :min-width="140" :is-mobile="isMobile" />
</div> </div>
</template> </template>
@@ -41,7 +41,7 @@
<el-input v-model="storeSearch" placeholder="Search..." clearable class="search-input"> <el-input v-model="storeSearch" placeholder="Search..." clearable class="search-input">
<template #prefix><el-icon><Search /></el-icon></template> <template #prefix><el-icon><Search /></el-icon></template>
</el-input> </el-input>
<FilterDropdown v-model="storeTypeFilter" label="Type" :options="storeTypeOptions" :min-width="140" /> <FilterDropdown v-model="storeTypeFilter" label="Type" :options="storeTypeOptions" :min-width="140" :is-mobile="isMobile" />
</div> </div>
</template> </template>
</div> </div>
@@ -100,6 +100,7 @@ path = "./frpc_store.json"</pre>
confirm-text="Delete" confirm-text="Delete"
danger danger
:loading="deleteDialog.loading" :loading="deleteDialog.loading"
:is-mobile="isMobile"
@confirm="doDelete" @confirm="doDelete"
/> />
</div> </div>
@@ -110,11 +111,11 @@ import { ref, computed, reactive, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Search, Refresh } from '@element-plus/icons-vue' import { Search, Refresh } from '@element-plus/icons-vue'
import ActionButton from '../components/ActionButton.vue' import ActionButton from '@shared/components/ActionButton.vue'
import StatusPills from '../components/StatusPills.vue' import StatusPills from '../components/StatusPills.vue'
import FilterDropdown from '../components/FilterDropdown.vue' import FilterDropdown from '@shared/components/FilterDropdown.vue'
import ProxyCard from '../components/ProxyCard.vue' import ProxyCard from '../components/ProxyCard.vue'
import ConfirmDialog from '../components/ConfirmDialog.vue' import ConfirmDialog from '@shared/components/ConfirmDialog.vue'
import { useProxyStore } from '../stores/proxy' import { useProxyStore } from '../stores/proxy'
import { useResponsive } from '../composables/useResponsive' import { useResponsive } from '../composables/useResponsive'
import type { ProxyStatus } from '../types' import type { ProxyStatus } from '../types'
+1 -1
View File
@@ -48,7 +48,7 @@
import { ref, computed, onMounted } from 'vue' import { ref, computed, onMounted } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import ActionButton from '../components/ActionButton.vue' import ActionButton from '@shared/components/ActionButton.vue'
import VisitorFormLayout from '../components/visitor-form/VisitorFormLayout.vue' import VisitorFormLayout from '../components/visitor-form/VisitorFormLayout.vue'
import { getVisitorConfig, getStoreVisitor } from '../api/frpc' import { getVisitorConfig, getStoreVisitor } from '../api/frpc'
import type { VisitorDefinition, VisitorFormData } from '../types' import type { VisitorDefinition, VisitorFormData } from '../types'
+5 -2
View File
@@ -30,6 +30,7 @@
v-model="leaveDialogVisible" v-model="leaveDialogVisible"
title="Unsaved Changes" title="Unsaved Changes"
message="You have unsaved changes. Are you sure you want to leave?" message="You have unsaved changes. Are you sure you want to leave?"
:is-mobile="isMobile"
@confirm="handleLeaveConfirm" @confirm="handleLeaveConfirm"
@cancel="handleLeaveCancel" @cancel="handleLeaveCancel"
/> />
@@ -40,9 +41,10 @@
import { ref, computed, onMounted, watch, nextTick } from 'vue' import { ref, computed, onMounted, watch, nextTick } from 'vue'
import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router' import { useRoute, useRouter, onBeforeRouteLeave } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import ActionButton from '../components/ActionButton.vue' import ActionButton from '@shared/components/ActionButton.vue'
import ConfirmDialog from '../components/ConfirmDialog.vue' import ConfirmDialog from '@shared/components/ConfirmDialog.vue'
import VisitorFormLayout from '../components/visitor-form/VisitorFormLayout.vue' import VisitorFormLayout from '../components/visitor-form/VisitorFormLayout.vue'
import { useResponsive } from '../composables/useResponsive'
import type { FormInstance, FormRules } from 'element-plus' import type { FormInstance, FormRules } from 'element-plus'
import { import {
type VisitorFormData, type VisitorFormData,
@@ -53,6 +55,7 @@ import {
import { getStoreVisitor } from '../api/frpc' import { getStoreVisitor } from '../api/frpc'
import { useVisitorStore } from '../stores/visitor' import { useVisitorStore } from '../stores/visitor'
const { isMobile } = useResponsive()
const route = useRoute() const route = useRoute()
const router = useRouter() const router = useRouter()
const visitorStore = useVisitorStore() const visitorStore = useVisitorStore()
+9 -7
View File
@@ -32,7 +32,7 @@ path = "./frpc_store.json"</pre>
<el-input v-model="searchText" placeholder="Search..." clearable class="search-input"> <el-input v-model="searchText" placeholder="Search..." clearable class="search-input">
<template #prefix><el-icon><Search /></el-icon></template> <template #prefix><el-icon><Search /></el-icon></template>
</el-input> </el-input>
<FilterDropdown v-model="typeFilter" label="Type" :options="typeOptions" :min-width="140" /> <FilterDropdown v-model="typeFilter" label="Type" :options="typeOptions" :min-width="140" :is-mobile="isMobile" />
</div> </div>
<div v-if="filteredVisitors.length > 0" class="visitor-list"> <div v-if="filteredVisitors.length > 0" class="visitor-list">
@@ -74,7 +74,7 @@ path = "./frpc_store.json"</pre>
<ConfirmDialog v-model="deleteDialog.visible" title="Delete Visitor" <ConfirmDialog v-model="deleteDialog.visible" title="Delete Visitor"
:message="deleteDialog.message" confirm-text="Delete" danger :message="deleteDialog.message" confirm-text="Delete" danger
:loading="deleteDialog.loading" @confirm="doDelete" /> :loading="deleteDialog.loading" :is-mobile="isMobile" @confirm="doDelete" />
</div> </div>
</template> </template>
@@ -83,14 +83,16 @@ import { ref, computed, reactive, onMounted } from 'vue'
import { useRouter } from 'vue-router' import { useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Search, Refresh, MoreFilled, Edit, Delete } from '@element-plus/icons-vue' import { Search, Refresh, MoreFilled, Edit, Delete } from '@element-plus/icons-vue'
import ActionButton from '../components/ActionButton.vue' import ActionButton from '@shared/components/ActionButton.vue'
import FilterDropdown from '../components/FilterDropdown.vue' import FilterDropdown from '@shared/components/FilterDropdown.vue'
import PopoverMenu from '../components/PopoverMenu.vue' import PopoverMenu from '@shared/components/PopoverMenu.vue'
import PopoverMenuItem from '../components/PopoverMenuItem.vue' import PopoverMenuItem from '@shared/components/PopoverMenuItem.vue'
import ConfirmDialog from '../components/ConfirmDialog.vue' import ConfirmDialog from '@shared/components/ConfirmDialog.vue'
import { useVisitorStore } from '../stores/visitor' import { useVisitorStore } from '../stores/visitor'
import { useResponsive } from '../composables/useResponsive'
import type { VisitorDefinition } from '../types' import type { VisitorDefinition } from '../types'
const { isMobile } = useResponsive()
const router = useRouter() const router = useRouter()
const visitorStore = useVisitorStore() const visitorStore = useVisitorStore()
+6 -2
View File
@@ -18,8 +18,12 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"],
"@shared/*": ["../shared/*"]
}
}, },
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "../shared/**/*.ts", "../shared/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }
+7 -1
View File
@@ -25,13 +25,19 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
'@shared': fileURLToPath(new URL('../shared', import.meta.url)),
}, },
dedupe: ['vue', 'element-plus', '@element-plus/icons-vue'],
modules: [
fileURLToPath(new URL('../node_modules', import.meta.url)),
'node_modules',
],
}, },
css: { css: {
preprocessorOptions: { preprocessorOptions: {
scss: { scss: {
api: 'modern', api: 'modern',
additionalData: `@use "@/assets/css/_index.scss" as *;`, additionalData: `@use "@shared/css/_index.scss" as *;`,
}, },
}, },
}, },
+1 -1
View File
@@ -1,7 +1,7 @@
.PHONY: dist install build preview lint .PHONY: dist install build preview lint
install: install:
@npm install @cd .. && npm install
build: install build: install
@npm run build @npm run build
+2 -3
View File
@@ -11,13 +11,12 @@ declare module 'vue' {
ElButton: typeof import('element-plus/es')['ElButton'] ElButton: typeof import('element-plus/es')['ElButton']
ElCard: typeof import('element-plus/es')['ElCard'] ElCard: typeof import('element-plus/es')['ElCard']
ElCol: typeof import('element-plus/es')['ElCol'] ElCol: typeof import('element-plus/es')['ElCol']
ElDialog: typeof import('element-plus/es')['ElDialog']
ElEmpty: typeof import('element-plus/es')['ElEmpty'] ElEmpty: typeof import('element-plus/es')['ElEmpty']
ElIcon: typeof import('element-plus/es')['ElIcon'] ElIcon: typeof import('element-plus/es')['ElIcon']
ElInput: typeof import('element-plus/es')['ElInput'] ElInput: typeof import('element-plus/es')['ElInput']
ElOption: typeof import('element-plus/es')['ElOption'] ElPopover: typeof import('element-plus/es')['ElPopover']
ElPopconfirm: typeof import('element-plus/es')['ElPopconfirm']
ElRow: typeof import('element-plus/es')['ElRow'] ElRow: typeof import('element-plus/es')['ElRow']
ElSelect: typeof import('element-plus/es')['ElSelect']
ElSwitch: typeof import('element-plus/es')['ElSwitch'] ElSwitch: typeof import('element-plus/es')['ElSwitch']
ElTag: typeof import('element-plus/es')['ElTag'] ElTag: typeof import('element-plus/es')['ElTag']
ElTooltip: typeof import('element-plus/es')['ElTooltip'] ElTooltip: typeof import('element-plus/es')['ElTooltip']
-7646
View File
File diff suppressed because it is too large Load Diff
+367 -120
View File
@@ -2,135 +2,195 @@
<div id="app"> <div id="app">
<header class="header"> <header class="header">
<div class="header-content"> <div class="header-content">
<div class="header-top"> <div class="brand-section">
<div class="brand-section"> <button
<div class="logo-wrapper"> v-if="isMobile"
<LogoIcon class="logo-icon" /> class="hamburger-btn"
</div> @click="toggleSidebar"
<span class="divider">/</span> aria-label="Toggle menu"
<span class="brand-name">frp</span> >
<span class="badge server-badge">Server</span> <span class="hamburger-icon">&#9776;</span>
<span class="badge" v-if="currentRouteName">{{ </button>
currentRouteName <div class="logo-wrapper">
}}</span> <LogoIcon class="logo-icon" />
</div>
<div class="header-controls">
<a
class="github-link"
href="https://github.com/fatedier/frp"
target="_blank"
aria-label="GitHub"
>
<GitHubIcon class="github-icon" />
</a>
<el-switch
v-model="isDark"
inline-prompt
:active-icon="Moon"
:inactive-icon="Sunny"
class="theme-switch"
/>
</div> </div>
<span class="divider">/</span>
<span class="brand-name">frp</span>
<span class="badge server-badge">Server</span>
</div> </div>
<nav class="nav-bar"> <div class="header-controls">
<router-link to="/" class="nav-link" active-class="active" <a
>Overview</router-link class="github-link"
href="https://github.com/fatedier/frp"
target="_blank"
aria-label="GitHub"
> >
<router-link to="/clients" class="nav-link" active-class="active" <GitHubIcon class="github-icon" />
>Clients</router-link </a>
> <el-switch
<router-link v-model="isDark"
to="/proxies" inline-prompt
class="nav-link" :active-icon="Moon"
:class="{ active: route.path.startsWith('/proxies') }" :inactive-icon="Sunny"
>Proxies</router-link class="theme-switch"
> />
</nav> </div>
</div> </div>
</header> </header>
<main id="content"> <div class="layout">
<router-view></router-view> <!-- Mobile overlay -->
</main> <div
v-if="isMobile && sidebarOpen"
class="sidebar-overlay"
@click="closeSidebar"
/>
<aside
class="sidebar"
:class="{ 'mobile-open': isMobile && sidebarOpen }"
>
<nav class="sidebar-nav">
<router-link
to="/"
class="sidebar-link"
:class="{ active: route.path === '/' }"
@click="closeSidebar"
>
Overview
</router-link>
<router-link
to="/clients"
class="sidebar-link"
:class="{ active: route.path.startsWith('/clients') }"
@click="closeSidebar"
>
Clients
</router-link>
<router-link
to="/proxies"
class="sidebar-link"
:class="{
active:
route.path.startsWith('/proxies') ||
route.path.startsWith('/proxy'),
}"
@click="closeSidebar"
>
Proxies
</router-link>
</nav>
</aside>
<main id="content">
<router-view></router-view>
</main>
</div>
</div> </div>
</template> </template>
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { ref, watch } from 'vue'
import { useRoute } from 'vue-router' import { useRoute } from 'vue-router'
import { useDark } from '@vueuse/core' import { useDark } from '@vueuse/core'
import { Moon, Sunny } from '@element-plus/icons-vue' import { Moon, Sunny } from '@element-plus/icons-vue'
import GitHubIcon from './assets/icons/github.svg?component' import GitHubIcon from './assets/icons/github.svg?component'
import LogoIcon from './assets/icons/logo.svg?component' import LogoIcon from './assets/icons/logo.svg?component'
import { useResponsive } from './composables/useResponsive'
const route = useRoute() const route = useRoute()
const isDark = useDark() const isDark = useDark()
const { isMobile } = useResponsive()
const currentRouteName = computed(() => { const sidebarOpen = ref(false)
if (route.path === '/') return 'Overview'
if (route.path.startsWith('/clients')) return 'Clients' const toggleSidebar = () => {
if (route.path.startsWith('/proxies')) return 'Proxies' sidebarOpen.value = !sidebarOpen.value
return '' }
})
const closeSidebar = () => {
sidebarOpen.value = false
}
// Auto-close sidebar on route change
watch(
() => route.path,
() => {
if (isMobile.value) {
closeSidebar()
}
},
)
</script> </script>
<style> <style>
:root { :root {
--header-height: 112px; --header-height: 50px;
--header-bg: rgba(255, 255, 255, 0.8); --sidebar-width: 200px;
--header-border: #eaeaea; --header-bg: #ffffff;
--text-primary: #000; --header-border: #e4e7ed;
--text-secondary: #666; --sidebar-bg: #ffffff;
--hover-bg: #f5f5f5; --text-primary: #303133;
--active-link: #000; --text-secondary: #606266;
--text-muted: #909399;
--hover-bg: #efefef;
--content-bg: #f9f9f9;
} }
html.dark { html.dark {
--header-bg: rgba(0, 0, 0, 0.8); --header-bg: #1e1e2e;
--header-border: #333; --header-border: #3a3d5c;
--text-primary: #fff; --sidebar-bg: #1e1e2e;
--text-secondary: #888; --text-primary: #e5e7eb;
--hover-bg: #1a1a1a; --text-secondary: #b0b0b0;
--active-link: #fff; --text-muted: #888888;
--hover-bg: #2a2a3e;
--content-bg: #181825;
} }
body { body {
margin: 0; margin: 0;
font-family: font-family:
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue', ui-sans-serif, -apple-system, system-ui, Segoe UI, Helvetica, Arial,
Arial, sans-serif; sans-serif;
}
*,
:after,
:before {
box-sizing: border-box;
-webkit-tap-highlight-color: transparent;
}
html,
body {
height: 100%;
overflow: hidden;
} }
#app { #app {
min-height: 100vh; height: 100vh;
height: 100dvh;
display: flex; display: flex;
flex-direction: column; flex-direction: column;
background-color: var(--el-bg-color-page); background-color: var(--content-bg);
} }
/* Header */
.header { .header {
position: sticky; flex-shrink: 0;
top: 0;
z-index: 100;
background: var(--header-bg); background: var(--header-bg);
backdrop-filter: blur(12px);
-webkit-backdrop-filter: blur(12px);
border-bottom: 1px solid var(--header-border); border-bottom: 1px solid var(--header-border);
height: var(--header-height);
} }
.header-content { .header-content {
max-width: 1200px;
margin: 0 auto;
padding: 0 40px;
}
.header-top {
height: 64px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: space-between; justify-content: space-between;
height: 100%;
padding: 0 20px;
} }
.brand-section { .brand-section {
@@ -145,13 +205,13 @@ body {
} }
.logo-icon { .logo-icon {
width: 32px; width: 28px;
height: 32px; height: 28px;
} }
.divider { .divider {
color: var(--header-border); color: var(--header-border);
font-size: 24px; font-size: 22px;
font-weight: 200; font-weight: 200;
} }
@@ -163,12 +223,12 @@ body {
} }
.badge { .badge {
font-size: 12px; font-size: 11px;
color: var(--text-secondary); font-weight: 500;
color: var(--text-muted);
background: var(--hover-bg); background: var(--hover-bg);
padding: 2px 8px; padding: 2px 8px;
border-radius: 99px; border-radius: 4px;
border: 1px solid var(--header-border);
} }
.badge.server-badge { .badge.server-badge {
@@ -189,17 +249,20 @@ html.dark .badge.server-badge {
} }
.github-link { .github-link {
width: 26px;
height: 26px;
display: flex; display: flex;
align-items: center; align-items: center;
justify-content: center; justify-content: center;
border-radius: 50%; width: 28px;
height: 28px;
border-radius: 6px;
color: var(--text-secondary);
transition: all 0.15s ease;
text-decoration: none;
}
.github-link:hover {
background: var(--hover-bg);
color: var(--text-primary); color: var(--text-primary);
transition: background 0.2s;
background: transparent;
border: 1px solid transparent;
cursor: pointer;
} }
.github-icon { .github-icon {
@@ -207,11 +270,6 @@ html.dark .badge.server-badge {
height: 18px; height: 18px;
} }
.github-link:hover {
background: var(--hover-bg);
border-color: var(--header-border);
}
.theme-switch { .theme-switch {
--el-switch-on-color: #2c2c3a; --el-switch-on-color: #2c2c3a;
--el-switch-off-color: #f2f2f2; --el-switch-off-color: #f2f2f2;
@@ -226,46 +284,235 @@ html.dark .theme-switch {
color: #909399 !important; color: #909399 !important;
} }
.nav-bar { /* Layout */
height: 48px; .layout {
flex: 1;
display: flex;
overflow: hidden;
}
/* Sidebar */
.sidebar {
width: var(--sidebar-width);
flex-shrink: 0;
background: var(--sidebar-bg);
border-right: 1px solid var(--header-border);
padding: 16px 12px;
overflow-y: auto;
display: flex;
flex-direction: column;
}
.sidebar-nav {
display: flex;
flex-direction: column;
gap: 2px;
}
.sidebar-link {
display: block;
text-decoration: none;
font-size: 15px;
color: var(--text-secondary);
padding: 10px 12px;
border-radius: 6px;
transition: all 0.15s ease;
}
.sidebar-link:hover {
color: var(--text-primary);
background: var(--hover-bg);
}
.sidebar-link.active {
color: var(--text-primary);
background: var(--hover-bg);
font-weight: 500;
}
/* Hamburger button (mobile only) */
.hamburger-btn {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 24px; justify-content: center;
width: 36px;
height: 36px;
border: none;
border-radius: 6px;
background: transparent;
cursor: pointer;
padding: 0;
transition: background 0.15s ease;
} }
.nav-link { .hamburger-btn:hover {
text-decoration: none; background: var(--hover-bg);
font-size: 14px;
color: var(--text-secondary);
padding: 8px 0;
border-bottom: 2px solid transparent;
transition: all 0.2s;
} }
.nav-link:hover { .hamburger-icon {
font-size: 20px;
line-height: 1;
color: var(--text-primary); color: var(--text-primary);
} }
.nav-link.active { /* Mobile overlay */
color: var(--active-link); .sidebar-overlay {
border-bottom-color: var(--active-link); position: fixed;
inset: 0;
background: rgba(0, 0, 0, 0.5);
z-index: 99;
} }
/* Content */
#content { #content {
flex: 1; flex: 1;
width: 100%; min-width: 0;
overflow-y: auto;
padding: 40px; padding: 40px;
max-width: 1200px;
margin: 0 auto;
box-sizing: border-box;
} }
@media (max-width: 768px) { #content > * {
max-width: 1024px;
margin: 0 auto;
}
/* Common page styles */
.page-title {
font-size: 20px;
font-weight: 600;
color: var(--color-text-primary, var(--text-primary));
margin: 0;
}
.page-subtitle {
font-size: 14px;
color: var(--color-text-muted, var(--text-muted));
margin: 8px 0 0;
}
/* Element Plus global overrides */
.el-button {
font-weight: 500;
}
.el-tag {
font-weight: 500;
}
.el-switch {
--el-switch-on-color: #606266;
--el-switch-off-color: #dcdfe6;
}
html.dark .el-switch {
--el-switch-on-color: #b0b0b0;
--el-switch-off-color: #3a3d5c;
}
.el-form-item {
margin-bottom: 16px;
}
.el-loading-mask {
border-radius: 8px;
}
/* Select overrides */
.el-select__wrapper {
border-radius: 8px !important;
box-shadow: 0 0 0 1px var(--color-border-light, #e4e7ed) inset !important;
transition: all 0.15s ease;
}
.el-select__wrapper:hover {
box-shadow: 0 0 0 1px var(--color-border, #dcdfe6) inset !important;
}
.el-select__wrapper.is-focused {
box-shadow: 0 0 0 1px var(--color-border, #dcdfe6) inset !important;
}
.el-select-dropdown {
border-radius: 12px !important;
border: 1px solid var(--color-border-light, #e4e7ed) !important;
box-shadow:
0 10px 25px -5px rgba(0, 0, 0, 0.1),
0 8px 10px -6px rgba(0, 0, 0, 0.1) !important;
padding: 4px !important;
}
.el-select-dropdown__item {
border-radius: 6px;
margin: 2px 0;
transition: background 0.15s ease;
}
.el-select-dropdown__item.is-selected {
color: var(--color-text-primary, var(--text-primary));
font-weight: 500;
}
/* Input overrides */
.el-input__wrapper {
border-radius: 8px !important;
box-shadow: 0 0 0 1px var(--color-border-light, #e4e7ed) inset !important;
transition: all 0.15s ease;
}
.el-input__wrapper:hover {
box-shadow: 0 0 0 1px var(--color-border, #dcdfe6) inset !important;
}
.el-input__wrapper.is-focus {
box-shadow: 0 0 0 1px var(--color-border, #dcdfe6) inset !important;
}
/* Card overrides */
.el-card {
border-radius: 12px;
border: 1px solid var(--color-border-light, #e4e7ed);
transition: all 0.2s ease;
}
/* Scrollbar */
::-webkit-scrollbar {
width: 6px;
height: 6px;
}
::-webkit-scrollbar-track {
background: transparent;
}
::-webkit-scrollbar-thumb {
background: #d1d1d1;
border-radius: 3px;
}
/* Mobile */
@media (max-width: 767px) {
.header-content { .header-content {
padding: 0 20px; padding: 0 16px;
}
.sidebar {
position: fixed;
top: var(--header-height);
left: 0;
bottom: 0;
z-index: 100;
background: var(--sidebar-bg);
transform: translateX(-100%);
transition: transform 0.25s cubic-bezier(0.4, 0, 0.2, 1);
border-right: 1px solid var(--header-border);
}
.sidebar.mobile-open {
transform: translateX(0);
} }
#content { #content {
width: 100%;
padding: 20px; padding: 20px;
} }
} }
-89
View File
@@ -1,89 +0,0 @@
.el-form-item span {
margin-left: 15px;
}
.proxy-table-expand {
font-size: 0;
}
.proxy-table-expand .el-form-item__label{
width: 90px;
color: #99a9bf;
}
.proxy-table-expand .el-form-item {
margin-right: 0;
margin-bottom: 0;
width: 50%;
}
.el-table .el-table__expanded-cell {
padding: 20px 50px;
}
/* Modern styles */
* {
box-sizing: border-box;
}
/* Smooth transitions */
.el-button,
.el-card,
.el-input,
.el-select,
.el-tag {
transition: all 0.3s ease;
}
/* Card hover effects */
.el-card:hover {
box-shadow: 0 8px 16px rgba(0, 0, 0, 0.1);
}
/* Better scrollbar */
::-webkit-scrollbar {
width: 8px;
height: 8px;
}
::-webkit-scrollbar-track {
background: #f1f1f1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb {
background: #c1c1c1;
border-radius: 4px;
}
::-webkit-scrollbar-thumb:hover {
background: #a8a8a8;
}
/* Page headers */
.el-page-header {
padding: 16px 0;
}
.el-page-header__title {
font-size: 20px;
font-weight: 500;
}
/* Better form layouts */
.el-form-item {
margin-bottom: 18px;
}
/* Responsive adjustments */
@media (max-width: 768px) {
.el-row {
margin-left: 0 !important;
margin-right: 0 !important;
}
.el-col {
padding-left: 10px !important;
padding-right: 10px !important;
}
}
+169 -15
View File
@@ -1,46 +1,153 @@
/* Dark mode styles */
html.dark { html.dark {
--el-bg-color: #1e1e2e; --el-bg-color: #1e1e2e;
--el-bg-color-page: #181825;
--el-bg-color-overlay: #27293d;
--el-fill-color-blank: #1e1e2e; --el-fill-color-blank: #1e1e2e;
--el-border-color: #3a3d5c;
--el-border-color-light: #313348;
--el-border-color-lighter: #2a2a3e;
--el-text-color-primary: #e5e7eb;
--el-text-color-secondary: #888888;
--el-text-color-placeholder: #afafaf;
background-color: #1e1e2e; background-color: #1e1e2e;
color-scheme: dark;
} }
html.dark body { /* Scrollbar */
background-color: #1e1e2e; html.dark ::-webkit-scrollbar {
color: #e5e7eb; width: 6px;
height: 6px;
} }
/* Dark mode scrollbar */
html.dark ::-webkit-scrollbar-track { html.dark ::-webkit-scrollbar-track {
background: #27293d; background: #27293d;
} }
html.dark ::-webkit-scrollbar-thumb { html.dark ::-webkit-scrollbar-thumb {
background: #3a3d5c; background: #3a3d5c;
border-radius: 3px;
} }
html.dark ::-webkit-scrollbar-thumb:hover { html.dark ::-webkit-scrollbar-thumb:hover {
background: #4a4d6c; background: #4a4d6c;
} }
/* Dark mode cards */ /* Form */
html.dark .el-card { html.dark .el-form-item__label {
background-color: #27293d; color: #e5e7eb;
border-color: #3a3d5c;
} }
/* Dark mode inputs */ /* Input */
html.dark .el-input__wrapper { html.dark .el-input__wrapper {
background-color: #27293d; background: var(--color-bg-input);
border-color: #3a3d5c; box-shadow: 0 0 0 1px #3a3d5c inset;
}
html.dark .el-input__wrapper:hover {
box-shadow: 0 0 0 1px #4a4d6c inset;
}
html.dark .el-input__wrapper.is-focus {
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
} }
html.dark .el-input__inner { html.dark .el-input__inner {
color: #e5e7eb; color: #e5e7eb;
} }
/* Dark mode table */ html.dark .el-input__inner::placeholder {
color: #afafaf;
}
html.dark .el-textarea__inner {
background: var(--color-bg-input);
box-shadow: 0 0 0 1px #3a3d5c inset;
color: #e5e7eb;
}
html.dark .el-textarea__inner:hover {
box-shadow: 0 0 0 1px #4a4d6c inset;
}
html.dark .el-textarea__inner:focus {
box-shadow: 0 0 0 1px var(--el-color-primary) inset;
}
/* Select */
html.dark .el-select__wrapper {
background: var(--color-bg-input);
box-shadow: 0 0 0 1px #3a3d5c inset;
}
html.dark .el-select__wrapper:hover {
box-shadow: 0 0 0 1px #4a4d6c inset;
}
html.dark .el-select__selected-item {
color: #e5e7eb;
}
html.dark .el-select__placeholder {
color: #afafaf;
}
html.dark .el-select-dropdown {
background: #27293d;
border-color: #3a3d5c;
}
html.dark .el-select-dropdown__item {
color: #e5e7eb;
}
html.dark .el-select-dropdown__item:hover {
background: #2a2a3e;
}
html.dark .el-select-dropdown__item.is-selected {
color: var(--el-color-primary);
}
html.dark .el-select-dropdown__item.is-disabled {
color: #666666;
}
/* Tag */
html.dark .el-tag--info {
background: #27293d;
border-color: #3a3d5c;
color: #b0b0b0;
}
/* Button */
html.dark .el-button--default {
background: #27293d;
border-color: #3a3d5c;
color: #e5e7eb;
}
html.dark .el-button--default:hover {
background: #2a2a3e;
border-color: #4a4d6c;
color: #e5e7eb;
}
/* Card */
html.dark .el-card {
background: #1e1e2e;
border-color: #3a3d5c;
color: #b0b0b0;
}
html.dark .el-card__header {
border-bottom-color: #3a3d5c;
color: #e5e7eb;
}
/* Table */
html.dark .el-table { html.dark .el-table {
background-color: #27293d; background-color: #1e1e2e;
color: #e5e7eb; color: #e5e7eb;
} }
@@ -50,9 +157,56 @@ html.dark .el-table th {
} }
html.dark .el-table tr { html.dark .el-table tr {
background-color: #27293d; background-color: #1e1e2e;
} }
html.dark .el-table--striped .el-table__body tr.el-table__row--striped td { html.dark .el-table--striped .el-table__body tr.el-table__row--striped td {
background-color: #1e1e2e; background-color: #181825;
}
/* Dialog */
html.dark .el-dialog {
background: #1e1e2e;
}
html.dark .el-dialog__title {
color: #e5e7eb;
}
/* Message */
html.dark .el-message {
background: #27293d;
border-color: #3a3d5c;
}
html.dark .el-message--success {
background: #1e3d2e;
border-color: #3d6b4f;
}
html.dark .el-message--warning {
background: #3d3020;
border-color: #6b5020;
}
html.dark .el-message--error {
background: #3d2027;
border-color: #5c2d2d;
}
/* Loading */
html.dark .el-loading-mask {
background-color: rgba(30, 30, 46, 0.9);
}
/* Overlay */
html.dark .el-overlay {
background-color: rgba(0, 0, 0, 0.6);
}
/* Tooltip */
html.dark .el-tooltip__popper {
background: #27293d !important;
border-color: #3a3d5c !important;
color: #e5e7eb !important;
} }
+109
View File
@@ -0,0 +1,109 @@
:root {
/* Text colors */
--color-text-primary: #303133;
--color-text-secondary: #606266;
--color-text-muted: #909399;
--color-text-light: #c0c4cc;
--color-text-placeholder: #a8abb2;
/* Background colors */
--color-bg-primary: #ffffff;
--color-bg-secondary: #f9f9f9;
--color-bg-tertiary: #fafafa;
--color-bg-surface: #ffffff;
--color-bg-muted: #f4f4f5;
--color-bg-input: #ffffff;
--color-bg-hover: #efefef;
--color-bg-active: #eaeaea;
/* Border colors */
--color-border: #dcdfe6;
--color-border-light: #e4e7ed;
--color-border-lighter: #ebeef5;
--color-border-extra-light: #f2f6fc;
/* Status colors */
--color-primary: #409eff;
--color-primary-light: #ecf5ff;
--color-success: #67c23a;
--color-warning: #e6a23c;
--color-danger: #f56c6c;
--color-danger-dark: #c45656;
--color-danger-light: #fef0f0;
--color-info: #909399;
/* Element Plus mapping */
--el-color-primary: var(--color-primary);
--el-color-success: var(--color-success);
--el-color-warning: var(--color-warning);
--el-color-danger: var(--color-danger);
--el-color-info: var(--color-info);
--el-text-color-primary: var(--color-text-primary);
--el-text-color-regular: var(--color-text-secondary);
--el-text-color-secondary: var(--color-text-muted);
--el-text-color-placeholder: var(--color-text-placeholder);
--el-bg-color: var(--color-bg-primary);
--el-bg-color-page: var(--color-bg-secondary);
--el-bg-color-overlay: var(--color-bg-primary);
--el-border-color: var(--color-border);
--el-border-color-light: var(--color-border-light);
--el-border-color-lighter: var(--color-border-lighter);
--el-border-color-extra-light: var(--color-border-extra-light);
--el-fill-color-blank: var(--color-bg-primary);
--el-fill-color-light: var(--color-bg-tertiary);
--el-fill-color: var(--color-bg-tertiary);
--el-fill-color-dark: var(--color-bg-hover);
--el-fill-color-darker: var(--color-bg-active);
/* Input */
--el-input-bg-color: var(--color-bg-input);
--el-input-border-color: var(--color-border);
--el-input-hover-border-color: var(--color-border-light);
/* Dialog */
--el-dialog-bg-color: var(--color-bg-primary);
--el-overlay-color: rgba(0, 0, 0, 0.5);
}
html.dark {
/* Text colors */
--color-text-primary: #e5e7eb;
--color-text-secondary: #b0b0b0;
--color-text-muted: #888888;
--color-text-light: #666666;
--color-text-placeholder: #afafaf;
/* Background colors */
--color-bg-primary: #1e1e2e;
--color-bg-secondary: #181825;
--color-bg-tertiary: #27293d;
--color-bg-surface: #27293d;
--color-bg-muted: #27293d;
--color-bg-input: #27293d;
--color-bg-hover: #2a2a3e;
--color-bg-active: #353550;
/* Border colors */
--color-border: #3a3d5c;
--color-border-light: #313348;
--color-border-lighter: #2a2a3e;
--color-border-extra-light: #222233;
/* Status colors */
--color-primary: #409eff;
--color-danger: #f87171;
--color-danger-dark: #f87171;
--color-danger-light: #3d2027;
--color-info: #888888;
/* Dark overrides */
--el-text-color-regular: var(--color-text-primary);
--el-overlay-color: rgba(0, 0, 0, 0.7);
background-color: #181825;
color-scheme: dark;
}
@@ -0,0 +1,8 @@
import { useBreakpoints } from '@vueuse/core'
const breakpoints = useBreakpoints({ mobile: 0, desktop: 768 })
export function useResponsive() {
const isMobile = breakpoints.smaller('desktop') // < 768px
return { isMobile }
}
+1 -1
View File
@@ -3,7 +3,7 @@ import 'element-plus/theme-chalk/dark/css-vars.css'
import App from './App.vue' import App from './App.vue'
import router from './router' import router from './router'
import './assets/css/custom.css' import './assets/css/var.css'
import './assets/css/dark.css' import './assets/css/dark.css'
const app = createApp(App) const app = createApp(App)
+71 -68
View File
@@ -8,23 +8,13 @@
</div> </div>
<div class="actions-section"> <div class="actions-section">
<el-button :icon="Refresh" class="action-btn" @click="fetchData" <ActionButton variant="outline" size="small" @click="fetchData">
>Refresh</el-button Refresh
> </ActionButton>
<el-popconfirm <ActionButton variant="outline" size="small" danger @click="showClearDialog = true">
title="Clear all offline proxies?" Clear Offline
width="220" </ActionButton>
confirm-button-text="Clear"
cancel-button-text="Cancel"
@confirm="clearOfflineProxies"
>
<template #reference>
<el-button :icon="Delete" class="action-btn" type="danger" plain
>Clear Offline</el-button
>
</template>
</el-popconfirm>
</div> </div>
</div> </div>
@@ -38,28 +28,35 @@
class="main-search" class="main-search"
/> />
<el-select <PopoverMenu
:model-value="selectedClientKey" :model-value="selectedClientKey"
placeholder="All Clients" :width="220"
clearable placement="bottom-end"
selectable
filterable filterable
class="client-select" filter-placeholder="Search clients..."
@change="onClientFilterChange" :display-value="selectedClientLabel"
clearable
class="client-filter"
@update:model-value="onClientFilterChange($event as string)"
> >
<el-option label="All Clients" value="" /> <template #default="{ filterText }">
<el-option <PopoverMenuItem value="">All Clients</PopoverMenuItem>
v-if="clientIDFilter && !selectedClientInList" <PopoverMenuItem
:label="`${userFilter ? userFilter + '.' : ''}${clientIDFilter} (not found)`" v-if="clientIDFilter && !selectedClientInList"
:value="selectedClientKey" :value="selectedClientKey"
style="color: var(--el-color-warning); font-style: italic" >
/> {{ userFilter ? userFilter + '.' : '' }}{{ clientIDFilter }} (not found)
<el-option </PopoverMenuItem>
v-for="client in clientOptions" <PopoverMenuItem
:key="client.key" v-for="client in filteredClientOptions(filterText)"
:label="client.label" :key="client.key"
:value="client.key" :value="client.key"
/> >
</el-select> {{ client.label }}
</PopoverMenuItem>
</template>
</PopoverMenu>
</div> </div>
<div class="type-tabs"> <div class="type-tabs">
@@ -88,6 +85,15 @@
<el-empty description="No proxies found" /> <el-empty description="No proxies found" />
</div> </div>
</div> </div>
<ConfirmDialog
v-model="showClearDialog"
title="Clear Offline"
message="Are you sure you want to clear all offline proxies?"
confirm-text="Clear"
danger
@confirm="handleClearConfirm"
/>
</div> </div>
</template> </template>
@@ -95,7 +101,9 @@
import { ref, computed, watch } from 'vue' import { ref, computed, watch } from 'vue'
import { useRoute, useRouter } from 'vue-router' import { useRoute, useRouter } from 'vue-router'
import { ElMessage } from 'element-plus' import { ElMessage } from 'element-plus'
import { Search, Refresh, Delete } from '@element-plus/icons-vue' import { Search } from '@element-plus/icons-vue'
import ActionButton from '@shared/components/ActionButton.vue'
import ConfirmDialog from '@shared/components/ConfirmDialog.vue'
import { import {
BaseProxy, BaseProxy,
TCPProxy, TCPProxy,
@@ -107,6 +115,8 @@ import {
SUDPProxy, SUDPProxy,
} from '../utils/proxy' } from '../utils/proxy'
import ProxyCard from '../components/ProxyCard.vue' import ProxyCard from '../components/ProxyCard.vue'
import PopoverMenu from '@shared/components/PopoverMenu.vue'
import PopoverMenuItem from '@shared/components/PopoverMenuItem.vue'
import { import {
getProxiesByType, getProxiesByType,
clearOfflineProxies as apiClearOfflineProxies, clearOfflineProxies as apiClearOfflineProxies,
@@ -133,6 +143,7 @@ const proxies = ref<BaseProxy[]>([])
const clients = ref<Client[]>([]) const clients = ref<Client[]>([])
const loading = ref(false) const loading = ref(false)
const searchText = ref('') const searchText = ref('')
const showClearDialog = ref(false)
const clientIDFilter = ref((route.query.clientID as string) || '') const clientIDFilter = ref((route.query.clientID as string) || '')
const userFilter = ref((route.query.user as string) || '') const userFilter = ref((route.query.user as string) || '')
@@ -157,6 +168,20 @@ const selectedClientKey = computed(() => {
return client?.key || `${userFilter.value}:${clientIDFilter.value}` return client?.key || `${userFilter.value}:${clientIDFilter.value}`
}) })
const selectedClientLabel = computed(() => {
if (!clientIDFilter.value) return 'All Clients'
const client = clientOptions.value.find(
(c) => c.clientID === clientIDFilter.value && c.user === userFilter.value,
)
return client?.label || `${userFilter.value ? userFilter.value + '.' : ''}${clientIDFilter.value}`
})
const filteredClientOptions = (filterText: string) => {
if (!filterText) return clientOptions.value
const search = filterText.toLowerCase()
return clientOptions.value.filter((c) => c.label.toLowerCase().includes(search))
}
// Check if the filtered client exists in the client list // Check if the filtered client exists in the client list
const selectedClientInList = computed(() => { const selectedClientInList = computed(() => {
if (!clientIDFilter.value) return true if (!clientIDFilter.value) return true
@@ -275,6 +300,11 @@ const fetchData = async () => {
} }
} }
const handleClearConfirm = async () => {
showClearDialog.value = false
await clearOfflineProxies()
}
const clearOfflineProxies = async () => { const clearOfflineProxies = async () => {
try { try {
await apiClearOfflineProxies() await apiClearOfflineProxies()
@@ -357,12 +387,6 @@ fetchClients()
gap: 12px; gap: 12px;
} }
.action-btn {
border-radius: 8px;
padding: 8px 16px;
height: 36px;
font-weight: 500;
}
.filter-section { .filter-section {
display: flex; display: flex;
@@ -382,37 +406,16 @@ fetchClients()
flex: 1; flex: 1;
} }
.main-search,
.client-select {
height: 44px;
}
.main-search :deep(.el-input__wrapper), .main-search :deep(.el-input__wrapper),
.client-select :deep(.el-input__wrapper) { .client-filter :deep(.el-input__wrapper) {
border-radius: 12px; height: 32px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04); border-radius: 8px;
padding: 0 16px;
height: 100%;
border: 1px solid var(--el-border-color);
} }
.main-search :deep(.el-input__wrapper) { .client-filter {
font-size: 15px;
}
.client-select {
width: 240px; width: 240px;
} }
.client-select :deep(.el-select__wrapper) {
border-radius: 12px;
box-shadow: 0 1px 2px rgba(0, 0, 0, 0.04);
padding: 0 12px;
height: 44px;
min-height: 44px;
border: 1px solid var(--el-border-color);
}
.type-tabs { .type-tabs {
display: flex; display: flex;
gap: 8px; gap: 8px;
@@ -462,7 +465,7 @@ fetchClients()
flex-direction: column; flex-direction: column;
} }
.client-select { .client-filter {
width: 100%; width: 100%;
} }
} }
+74 -240
View File
@@ -51,98 +51,41 @@
<router-link <router-link
v-if="proxy.clientID" v-if="proxy.clientID"
:to="clientLink" :to="clientLink"
class="client-link" class="meta-link"
> >
<el-icon><Monitor /></el-icon> <el-icon><Monitor /></el-icon>
<span <span>{{
>Client: proxy.user
{{ ? `${proxy.user}.${proxy.clientID}`
proxy.user : proxy.clientID
? `${proxy.user}.${proxy.clientID}` }}</span>
: proxy.clientID
}}</span
>
</router-link> </router-link>
<span v-if="proxy.lastStartTime" class="meta-text">
<span class="meta-sep">·</span>
Last Started {{ proxy.lastStartTime }}
</span>
<span v-if="proxy.lastCloseTime" class="meta-text">
<span class="meta-sep">·</span>
Last Closed {{ proxy.lastCloseTime }}
</span>
</div> </div>
</div> </div>
</div> </div>
</div> </div>
<!-- Stats Cards --> <!-- Stats Bar -->
<div class="stats-grid"> <div class="stats-bar">
<div v-if="proxy.port" class="stat-card"> <div v-if="proxy.port" class="stats-item">
<div class="stat-header"> <span class="stats-label">Port</span>
<span class="stat-label">Port</span> <span class="stats-value">{{ proxy.port }}</span>
<div class="stat-icon port">
<el-icon><Connection /></el-icon>
</div>
</div>
<div class="stat-value">{{ proxy.port }}</div>
</div> </div>
<div class="stat-card"> <div class="stats-item">
<div class="stat-header"> <span class="stats-label">Connections</span>
<span class="stat-label">Connections</span> <span class="stats-value">{{ proxy.conns }}</span>
<div class="stat-icon connections">
<el-icon><DataLine /></el-icon>
</div>
</div>
<div class="stat-value">{{ proxy.conns }}</div>
</div> </div>
<div class="stat-card"> <div class="stats-item">
<div class="stat-header"> <span class="stats-label">Traffic</span>
<span class="stat-label">Traffic In</span> <span class="stats-value"> {{ formatTrafficValue(proxy.trafficIn) }} <small>{{ formatTrafficUnit(proxy.trafficIn) }}</small> / {{ formatTrafficValue(proxy.trafficOut) }} <small>{{ formatTrafficUnit(proxy.trafficOut) }}</small></span>
<div class="stat-icon traffic-in">
<el-icon><Bottom /></el-icon>
</div>
</div>
<div class="stat-value">
<span class="value-number">{{
formatTrafficValue(proxy.trafficIn)
}}</span>
<span class="value-unit">{{
formatTrafficUnit(proxy.trafficIn)
}}</span>
</div>
</div>
<div class="stat-card">
<div class="stat-header">
<span class="stat-label">Traffic Out</span>
<div class="stat-icon traffic-out">
<el-icon><Top /></el-icon>
</div>
</div>
<div class="stat-value">
<span class="value-number">{{
formatTrafficValue(proxy.trafficOut)
}}</span>
<span class="value-unit">{{
formatTrafficUnit(proxy.trafficOut)
}}</span>
</div>
</div>
</div>
<!-- Status Timeline -->
<div class="timeline-card">
<div class="timeline-header">
<el-icon><DataLine /></el-icon>
<h2>Status Timeline</h2>
</div>
<div class="timeline-body">
<div class="timeline-grid">
<div class="timeline-item">
<span class="timeline-label">Last Start Time</span>
<span class="timeline-value">{{
proxy.lastStartTime || '-'
}}</span>
</div>
<div class="timeline-item">
<span class="timeline-label">Last Close Time</span>
<span class="timeline-value">{{
proxy.lastCloseTime || '-'
}}</span>
</div>
</div>
</div> </div>
</div> </div>
@@ -288,9 +231,6 @@ import {
ArrowLeft, ArrowLeft,
Monitor, Monitor,
Connection, Connection,
DataLine,
Bottom,
Top,
Link, Link,
Lock, Lock,
Promotion, Promotion,
@@ -593,177 +533,72 @@ html.dark .status-badge.online {
.header-meta { .header-meta {
display: flex; display: flex;
align-items: center; align-items: center;
gap: 16px; flex-wrap: wrap;
gap: 4px;
font-size: 13px;
color: var(--text-secondary);
} }
.client-link { .meta-link {
display: inline-flex; display: inline-flex;
align-items: center; align-items: center;
gap: 6px; gap: 4px;
font-size: 14px;
color: var(--text-secondary); color: var(--text-secondary);
text-decoration: none; text-decoration: none;
transition: color 0.2s; transition: color 0.2s;
} }
.client-link:hover { .meta-link:hover {
color: var(--el-color-primary); color: var(--el-color-primary);
} }
/* Stats Grid */ .meta-text {
.stats-grid { color: var(--text-muted);
display: grid;
grid-template-columns: repeat(4, 1fr);
gap: 16px;
margin-bottom: 24px;
} }
.stat-card { .meta-sep {
margin: 0 4px;
}
/* Stats Bar */
.stats-bar {
display: flex;
background: var(--el-bg-color); background: var(--el-bg-color);
border: 1px solid var(--header-border); border: 1px solid var(--header-border);
border-radius: 12px;
padding: 20px;
}
.stat-header {
display: flex;
justify-content: space-between;
align-items: flex-start;
margin-bottom: 12px;
}
.stat-label {
font-size: 13px;
color: var(--text-secondary);
font-weight: 500;
}
.stat-icon {
width: 36px;
height: 36px;
border-radius: 10px; border-radius: 10px;
display: flex; margin-bottom: 20px;
align-items: center;
justify-content: center;
font-size: 18px;
} }
.stat-icon.port { .stats-item {
background: rgba(139, 92, 246, 0.1); flex: 1;
color: #8b5cf6;
}
.stat-icon.connections {
background: rgba(168, 85, 247, 0.1);
color: #a855f7;
}
.stat-icon.traffic-in {
background: rgba(59, 130, 246, 0.1);
color: #3b82f6;
}
.stat-icon.traffic-out {
background: rgba(34, 197, 94, 0.1);
color: #22c55e;
}
html.dark .stat-icon.port {
background: rgba(139, 92, 246, 0.15);
}
html.dark .stat-icon.connections {
background: rgba(168, 85, 247, 0.15);
}
html.dark .stat-icon.traffic-in {
background: rgba(59, 130, 246, 0.15);
}
html.dark .stat-icon.traffic-out {
background: rgba(34, 197, 94, 0.15);
}
.stat-value {
display: flex;
align-items: baseline;
gap: 6px;
}
.value-number {
font-size: 28px;
font-weight: 500;
color: var(--text-primary);
line-height: 1;
}
.stat-value:not(:has(.value-number)) {
font-size: 28px;
font-weight: 500;
color: var(--text-primary);
}
.value-unit {
font-size: 14px;
font-weight: 500;
color: var(--text-secondary);
}
/* Timeline Card */
.timeline-card {
background: var(--el-bg-color);
border: 1px solid var(--header-border);
border-radius: 12px;
margin-bottom: 16px;
}
.timeline-header {
display: flex;
align-items: center;
gap: 8px;
padding: 16px 20px;
color: var(--text-secondary);
}
.timeline-header h2 {
font-size: 15px;
font-weight: 500;
color: var(--text-primary);
margin: 0;
}
.timeline-body {
padding: 20px;
padding-top: 0;
}
.timeline-grid {
display: grid;
grid-template-columns: repeat(2, 1fr);
gap: 24px;
background: var(--el-fill-color-light);
border-radius: 10px;
padding: 20px 24px;
}
.timeline-item {
display: flex; display: flex;
flex-direction: column; flex-direction: column;
gap: 8px; gap: 4px;
padding: 16px 20px;
} }
.timeline-label { .stats-item + .stats-item {
font-size: 13px; border-left: 1px solid var(--header-border);
}
.stats-label {
font-size: 12px;
color: var(--text-secondary); color: var(--text-secondary);
font-weight: 500;
} }
.timeline-value { .stats-value {
font-size: 15px; font-size: 18px;
font-weight: 500; font-weight: 600;
color: var(--text-primary); color: var(--text-primary);
} }
.stats-value small {
font-size: 12px;
font-weight: 500;
color: var(--text-secondary);
}
/* Card Base */ /* Card Base */
.traffic-card { .traffic-card {
background: var(--el-bg-color); background: var(--el-bg-color);
@@ -956,16 +791,22 @@ html.dark .config-item-icon.route {
} }
/* Responsive */ /* Responsive */
@media (max-width: 1024px) {
.stats-grid {
grid-template-columns: repeat(2, 1fr);
}
}
@media (max-width: 768px) { @media (max-width: 768px) {
.config-grid { .config-grid {
grid-template-columns: 1fr; grid-template-columns: 1fr;
} }
.stats-bar {
flex-wrap: wrap;
}
.stats-item {
flex: 1 1 40%;
}
.stats-item:nth-child(n+3) {
border-top: 1px solid var(--header-border);
}
} }
@media (max-width: 640px) { @media (max-width: 640px) {
@@ -974,12 +815,5 @@ html.dark .config-item-icon.route {
gap: 16px; gap: 16px;
} }
.stats-grid {
grid-template-columns: 1fr;
}
.timeline-grid {
grid-template-columns: 1fr;
}
} }
</style> </style>
+6 -2
View File
@@ -18,8 +18,12 @@
"strict": true, "strict": true,
"noUnusedLocals": true, "noUnusedLocals": true,
"noUnusedParameters": true, "noUnusedParameters": true,
"noFallthroughCasesInSwitch": true "noFallthroughCasesInSwitch": true,
"paths": {
"@/*": ["./src/*"],
"@shared/*": ["../shared/*"]
}
}, },
"include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue"], "include": ["src/**/*.ts", "src/**/*.tsx", "src/**/*.vue", "../shared/**/*.ts", "../shared/**/*.vue"],
"references": [{ "path": "./tsconfig.node.json" }] "references": [{ "path": "./tsconfig.node.json" }]
} }
+14
View File
@@ -25,6 +25,20 @@ export default defineConfig({
resolve: { resolve: {
alias: { alias: {
'@': fileURLToPath(new URL('./src', import.meta.url)), '@': fileURLToPath(new URL('./src', import.meta.url)),
'@shared': fileURLToPath(new URL('../shared', import.meta.url)),
},
dedupe: ['vue', 'element-plus', '@element-plus/icons-vue'],
modules: [
fileURLToPath(new URL('../node_modules', import.meta.url)),
'node_modules',
],
},
css: {
preprocessorOptions: {
scss: {
api: 'modern',
additionalData: `@use "@shared/css/_index.scss" as *;`,
},
}, },
}, },
build: { build: {
File diff suppressed because it is too large Load Diff
+5
View File
@@ -0,0 +1,5 @@
{
"name": "frp-web",
"private": true,
"workspaces": ["shared", "frpc", "frps"]
}
@@ -21,7 +21,6 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import { useResponsive } from '../composables/useResponsive'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -33,6 +32,7 @@ const props = withDefaults(
closeOnPressEscape?: boolean closeOnPressEscape?: boolean
appendToBody?: boolean appendToBody?: boolean
top?: string top?: string
isMobile?: boolean
}>(), }>(),
{ {
width: '480px', width: '480px',
@@ -41,6 +41,7 @@ const props = withDefaults(
closeOnPressEscape: true, closeOnPressEscape: true,
appendToBody: false, appendToBody: false,
top: '15vh', top: '15vh',
isMobile: false,
}, },
) )
@@ -53,15 +54,13 @@ const visible = computed({
set: (value) => emit('update:modelValue', value), set: (value) => emit('update:modelValue', value),
}) })
const { isMobile } = useResponsive()
const dialogWidth = computed(() => { const dialogWidth = computed(() => {
if (isMobile.value) return '100%' if (props.isMobile) return '100%'
return props.width return props.width
}) })
const dialogTop = computed(() => { const dialogTop = computed(() => {
if (isMobile.value) return '0' if (props.isMobile) return '0'
return props.top return props.top
}) })
</script> </script>
@@ -5,6 +5,7 @@
width="400px" width="400px"
:close-on-click-modal="false" :close-on-click-modal="false"
:append-to-body="true" :append-to-body="true"
:is-mobile="isMobile"
> >
<p class="confirm-message">{{ message }}</p> <p class="confirm-message">{{ message }}</p>
<template #footer> <template #footer>
@@ -27,7 +28,7 @@
<script setup lang="ts"> <script setup lang="ts">
import { computed } from 'vue' import { computed } from 'vue'
import BaseDialog from './BaseDialog.vue' import BaseDialog from './BaseDialog.vue'
import ActionButton from './ActionButton.vue' import ActionButton from '@shared/components/ActionButton.vue'
const props = withDefaults( const props = withDefaults(
defineProps<{ defineProps<{
@@ -38,12 +39,14 @@ const props = withDefaults(
cancelText?: string cancelText?: string
danger?: boolean danger?: boolean
loading?: boolean loading?: boolean
isMobile?: boolean
}>(), }>(),
{ {
confirmText: 'Confirm', confirmText: 'Confirm',
cancelText: 'Cancel', cancelText: 'Cancel',
danger: false, danger: false,
loading: false, loading: false,
isMobile: false,
}, },
) )
@@ -30,9 +30,6 @@ import { computed } from 'vue'
import { ArrowDown } from '@element-plus/icons-vue' import { ArrowDown } from '@element-plus/icons-vue'
import PopoverMenu from './PopoverMenu.vue' import PopoverMenu from './PopoverMenu.vue'
import PopoverMenuItem from './PopoverMenuItem.vue' import PopoverMenuItem from './PopoverMenuItem.vue'
import { useResponsive } from '../composables/useResponsive'
const { isMobile } = useResponsive()
interface Props { interface Props {
modelValue: string modelValue: string
@@ -41,6 +38,7 @@ interface Props {
allLabel?: string allLabel?: string
width?: number width?: number
minWidth?: number minWidth?: number
isMobile?: boolean
} }
const props = withDefaults(defineProps<Props>(), { const props = withDefaults(defineProps<Props>(), {
+4
View File
@@ -0,0 +1,4 @@
{
"name": "frp-shared",
"private": true
}