mirror of
https://github.com/fatedier/frp.git
synced 2026-07-20 20:39:17 +08:00
Compare commits
1 Commits
v0.67.0
...
feb98d3cda
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
feb98d3cda |
@@ -6,14 +6,6 @@ jobs:
|
|||||||
resource_class: large
|
resource_class: large
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run:
|
|
||||||
name: Build web assets (frps)
|
|
||||||
command: make install build
|
|
||||||
working_directory: web/frps
|
|
||||||
- run:
|
|
||||||
name: Build web assets (frpc)
|
|
||||||
command: make install build
|
|
||||||
working_directory: web/frpc
|
|
||||||
- run: make
|
- run: make
|
||||||
- run: make alltest
|
- run: make alltest
|
||||||
|
|
||||||
|
|||||||
9
.github/workflows/golangci-lint.yml
vendored
9
.github/workflows/golangci-lint.yml
vendored
@@ -19,15 +19,6 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version: '1.24'
|
||||||
cache: false
|
cache: false
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
- name: Build web assets (frps)
|
|
||||||
run: make install build
|
|
||||||
working-directory: web/frps
|
|
||||||
- name: Build web assets (frpc)
|
|
||||||
run: make install build
|
|
||||||
working-directory: web/frpc
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v8
|
uses: golangci/golangci-lint-action@v8
|
||||||
with:
|
with:
|
||||||
|
|||||||
12
.github/workflows/goreleaser.yml
vendored
12
.github/workflows/goreleaser.yml
vendored
@@ -16,21 +16,13 @@ jobs:
|
|||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version: '1.24'
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
- name: Build web assets (frps)
|
|
||||||
run: make install build
|
|
||||||
working-directory: web/frps
|
|
||||||
- name: Build web assets (frpc)
|
|
||||||
run: make install build
|
|
||||||
working-directory: web/frpc
|
|
||||||
- name: Make All
|
- name: Make All
|
||||||
run: |
|
run: |
|
||||||
./package.sh
|
./package.sh
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v5
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --clean --release-notes=./Release.md
|
args: release --clean --release-notes=./Release.md
|
||||||
|
|||||||
@@ -39,7 +39,6 @@ linters:
|
|||||||
- G404
|
- G404
|
||||||
- G501
|
- G501
|
||||||
- G115
|
- G115
|
||||||
- G204
|
|
||||||
severity: low
|
severity: low
|
||||||
confidence: low
|
confidence: low
|
||||||
govet:
|
govet:
|
||||||
|
|||||||
21
Makefile
21
Makefile
@@ -2,22 +2,19 @@ export PATH := $(PATH):`go env GOPATH`/bin
|
|||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
LDFLAGS := -s -w
|
LDFLAGS := -s -w
|
||||||
|
|
||||||
.PHONY: web frps-web frpc-web frps frpc
|
all: env fmt build
|
||||||
|
|
||||||
all: env fmt web build
|
|
||||||
|
|
||||||
build: frps frpc
|
build: frps frpc
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@go version
|
@go version
|
||||||
|
|
||||||
web: frps-web frpc-web
|
# compile assets into binary file
|
||||||
|
file:
|
||||||
frps-web:
|
rm -rf ./assets/frps/static/*
|
||||||
$(MAKE) -C web/frps build
|
rm -rf ./assets/frpc/static/*
|
||||||
|
cp -rf ./web/frps/dist/* ./assets/frps/static
|
||||||
frpc-web:
|
cp -rf ./web/frpc/dist/* ./assets/frpc/static
|
||||||
$(MAKE) -C web/frpc build
|
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
@@ -28,7 +25,7 @@ fmt-more:
|
|||||||
gci:
|
gci:
|
||||||
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
|
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
|
||||||
|
|
||||||
vet: web
|
vet:
|
||||||
go vet ./...
|
go vet ./...
|
||||||
|
|
||||||
frps:
|
frps:
|
||||||
@@ -39,7 +36,7 @@ frpc:
|
|||||||
|
|
||||||
test: gotest
|
test: gotest
|
||||||
|
|
||||||
gotest: web
|
gotest:
|
||||||
go test -v --cover ./assets/...
|
go test -v --cover ./assets/...
|
||||||
go test -v --cover ./cmd/...
|
go test -v --cover ./cmd/...
|
||||||
go test -v --cover ./client/...
|
go test -v --cover ./client/...
|
||||||
|
|||||||
68
README.md
68
README.md
@@ -13,33 +13,6 @@ 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://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
|
||||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
|
||||||
<br>
|
|
||||||
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
|
||||||
<br>
|
|
||||||
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://jb.gg/frp" target="_blank">
|
|
||||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
|
||||||
<br>
|
|
||||||
<b>The complete IDE crafted for professional Go developers</b>
|
|
||||||
</a>
|
|
||||||
</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>
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
## Recall.ai - API for meeting recordings
|
## Recall.ai - API for meeting recordings
|
||||||
@@ -49,6 +22,47 @@ If you're looking for a meeting recording API, consider checking out [Recall.ai]
|
|||||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||||
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
|
<br>
|
||||||
|
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||||
|
<br>
|
||||||
|
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://go.warp.dev/frp" target="_blank">
|
||||||
|
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
|
||||||
|
<br>
|
||||||
|
<b>Warp, built for collaborating with AI Agents</b>
|
||||||
|
<br>
|
||||||
|
<sub>Available for macOS, Linux and Windows</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://jb.gg/frp" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||||
|
<br>
|
||||||
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/daytonaio/daytona" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
|
||||||
|
<br>
|
||||||
|
<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
|
||||||
|
</a>
|
||||||
|
</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?
|
||||||
|
|||||||
74
README_zh.md
74
README_zh.md
@@ -15,33 +15,6 @@ 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://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
|
||||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
|
||||||
<br>
|
|
||||||
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
|
||||||
<br>
|
|
||||||
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://jb.gg/frp" target="_blank">
|
|
||||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
|
||||||
<br>
|
|
||||||
<b>The complete IDE crafted for professional Go developers</b>
|
|
||||||
</a>
|
|
||||||
</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>
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
## Recall.ai - API for meeting recordings
|
## Recall.ai - API for meeting recordings
|
||||||
@@ -51,6 +24,47 @@ If you're looking for a meeting recording API, consider checking out [Recall.ai]
|
|||||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||||
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
|
<br>
|
||||||
|
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||||
|
<br>
|
||||||
|
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://go.warp.dev/frp" target="_blank">
|
||||||
|
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
|
||||||
|
<br>
|
||||||
|
<b>Warp, built for collaborating with AI Agents</b>
|
||||||
|
<br>
|
||||||
|
<sub>Available for macOS, Linux and Windows</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://jb.gg/frp" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||||
|
<br>
|
||||||
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/daytonaio/daytona" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
|
||||||
|
<br>
|
||||||
|
<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
|
||||||
|
</a>
|
||||||
|
</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 ?
|
||||||
@@ -123,3 +137,9 @@ frp 是一个免费且开源的项目,我们欢迎任何人为其开发和进
|
|||||||
国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。
|
国内用户可以通过 [爱发电](https://afdian.com/a/fatedier) 赞助我们。
|
||||||
|
|
||||||
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
|
企业赞助者可以将贵公司的 Logo 以及链接放置在项目 README 文件中。
|
||||||
|
|
||||||
|
### 知识星球
|
||||||
|
|
||||||
|
如果您想了解更多 frp 相关技术以及更新详解,或者寻求任何 frp 使用方面的帮助,都可以通过微信扫描下方的二维码付费加入知识星球的官方社群:
|
||||||
|
|
||||||
|

|
||||||
|
|||||||
@@ -1,8 +1,3 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* frpc now supports a `clientID` option to uniquely identify client instances. The server dashboard displays all connected clients with their online/offline status, connection history, and metadata, making it easier to monitor and manage multiple frpc deployments.
|
* HTTPS proxies now support load balancing groups. Multiple HTTPS proxies can be configured with the same `loadBalancer.group` and `loadBalancer.groupKey` to share the same custom domain and distribute traffic across multiple backend services, similar to the existing TCP and HTTP load balancing capabilities.
|
||||||
* Redesigned the frp web dashboard with a modern UI, dark mode support, and improved navigation.
|
|
||||||
|
|
||||||
## Fixes
|
|
||||||
|
|
||||||
* Fixed UDP proxy protocol sending header on every packet instead of only the first packet of each session.
|
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func Load(path string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Register(fileSystem fs.FS) {
|
func Register(fileSystem fs.FS) {
|
||||||
subFs, err := fs.Sub(fileSystem, "dist")
|
subFs, err := fs.Sub(fileSystem, "static")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
content = subFs
|
content = subFs
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"github.com/fatedier/frp/assets"
|
"github.com/fatedier/frp/assets"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dist
|
//go:embed static/*
|
||||||
var EmbedFS embed.FS
|
var content embed.FS
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
assets.Register(EmbedFS)
|
assets.Register(content)
|
||||||
}
|
}
|
||||||
BIN
assets/frpc/static/favicon.ico
Normal file
BIN
assets/frpc/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
42
assets/frpc/static/index-bLBhaJo8.js
Normal file
42
assets/frpc/static/index-bLBhaJo8.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/frpc/static/index-iuf46MlF.css
Normal file
1
assets/frpc/static/index-iuf46MlF.css
Normal file
File diff suppressed because one or more lines are too long
15
assets/frpc/static/index.html
Normal file
15
assets/frpc/static/index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>frp client admin UI</title>
|
||||||
|
<script type="module" crossorigin src="./index-bLBhaJo8.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="./index-iuf46MlF.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
14
assets/frps/embed.go
Normal file
14
assets/frps/embed.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package frpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/assets"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed static/*
|
||||||
|
var content embed.FS
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
assets.Register(content)
|
||||||
|
}
|
||||||
BIN
assets/frps/static/favicon.ico
Normal file
BIN
assets/frps/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
84
assets/frps/static/index-82-40HIG.js
Normal file
84
assets/frps/static/index-82-40HIG.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/frps/static/index-rzPDshRD.css
Normal file
1
assets/frps/static/index-rzPDshRD.css
Normal file
File diff suppressed because one or more lines are too long
15
assets/frps/static/index.html
Normal file
15
assets/frps/static/index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>frps dashboard</title>
|
||||||
|
<script type="module" crossorigin src="./index-82-40HIG.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="./index-rzPDshRD.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -15,29 +15,44 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/api"
|
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GeneralResponse struct {
|
||||||
|
Code int
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
||||||
apiController := newAPIController(svr)
|
helper.Router.HandleFunc("/healthz", svr.healthz)
|
||||||
|
|
||||||
// Healthz endpoint without auth
|
|
||||||
helper.Router.HandleFunc("/healthz", healthz)
|
|
||||||
|
|
||||||
// API routes and static files with auth
|
|
||||||
subRouter := helper.Router.NewRoute().Subrouter()
|
subRouter := helper.Router.NewRoute().Subrouter()
|
||||||
subRouter.Use(helper.AuthMiddleware)
|
|
||||||
subRouter.Use(httppkg.NewRequestLogger)
|
subRouter.Use(helper.AuthMiddleware.Middleware)
|
||||||
subRouter.HandleFunc("/api/reload", httppkg.MakeHTTPHandlerFunc(apiController.Reload)).Methods(http.MethodGet)
|
|
||||||
subRouter.HandleFunc("/api/stop", httppkg.MakeHTTPHandlerFunc(apiController.Stop)).Methods(http.MethodPost)
|
// api, see admin_api.go
|
||||||
subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet)
|
subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
|
||||||
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet)
|
subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
|
||||||
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut)
|
subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
|
||||||
|
|
||||||
|
// view
|
||||||
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
||||||
subRouter.PathPrefix("/static/").Handler(
|
subRouter.PathPrefix("/static/").Handler(
|
||||||
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
||||||
@@ -47,28 +62,201 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func healthz(w http.ResponseWriter, _ *http.Request) {
|
// /healthz
|
||||||
w.WriteHeader(http.StatusOK)
|
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAPIController(svr *Service) *api.Controller {
|
// GET /api/reload
|
||||||
return api.NewController(api.ControllerParams{
|
func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
|
||||||
GetProxyStatus: svr.getAllProxyStatus,
|
res := GeneralResponse{Code: 200}
|
||||||
ServerAddr: svr.common.ServerAddr,
|
strictConfigMode := false
|
||||||
ConfigFilePath: svr.configFilePath,
|
strictStr := r.URL.Query().Get("strictConfig")
|
||||||
UnsafeFeatures: svr.unsafeFeatures,
|
if strictStr != "" {
|
||||||
UpdateConfig: svr.UpdateAllConfigurer,
|
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
||||||
GracefulClose: svr.GracefulClose,
|
}
|
||||||
})
|
|
||||||
|
log.Infof("api request [/api/reload]")
|
||||||
|
defer func() {
|
||||||
|
log.Infof("api response [/api/reload], code [%d]", res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.configFilePath, strictConfigMode)
|
||||||
|
if err != nil {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = err.Error()
|
||||||
|
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs); err != nil {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = err.Error()
|
||||||
|
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := svr.UpdateAllConfigurer(proxyCfgs, visitorCfgs); err != nil {
|
||||||
|
res.Code = 500
|
||||||
|
res.Msg = err.Error()
|
||||||
|
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Infof("success reload conf")
|
||||||
}
|
}
|
||||||
|
|
||||||
// getAllProxyStatus returns all proxy statuses.
|
// POST /api/stop
|
||||||
func (svr *Service) getAllProxyStatus() []*proxy.WorkingStatus {
|
func (svr *Service) apiStop(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
|
||||||
|
log.Infof("api request [/api/stop]")
|
||||||
|
defer func() {
|
||||||
|
log.Infof("api response [/api/stop], code [%d]", res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go svr.GracefulClose(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusResp map[string][]ProxyStatusResp
|
||||||
|
|
||||||
|
type ProxyStatusResp struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Err string `json:"err"`
|
||||||
|
LocalAddr string `json:"local_addr"`
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxyStatusResp {
|
||||||
|
psr := ProxyStatusResp{
|
||||||
|
Name: status.Name,
|
||||||
|
Type: status.Type,
|
||||||
|
Status: status.Phase,
|
||||||
|
Err: status.Err,
|
||||||
|
}
|
||||||
|
baseCfg := status.Cfg.GetBaseConfig()
|
||||||
|
if baseCfg.LocalPort != 0 {
|
||||||
|
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
||||||
|
}
|
||||||
|
psr.Plugin = baseCfg.Plugin.Type
|
||||||
|
|
||||||
|
if status.Err == "" {
|
||||||
|
psr.RemoteAddr = status.RemoteAddr
|
||||||
|
if slices.Contains([]string{"tcp", "udp"}, status.Type) {
|
||||||
|
psr.RemoteAddr = serverAddr + psr.RemoteAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return psr
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/status
|
||||||
|
func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
var (
|
||||||
|
buf []byte
|
||||||
|
res StatusResp = make(map[string][]ProxyStatusResp)
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Infof("http request [/api/status]")
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [/api/status]")
|
||||||
|
buf, _ = json.Marshal(&res)
|
||||||
|
_, _ = w.Write(buf)
|
||||||
|
}()
|
||||||
|
|
||||||
svr.ctlMu.RLock()
|
svr.ctlMu.RLock()
|
||||||
ctl := svr.ctl
|
ctl := svr.ctl
|
||||||
svr.ctlMu.RUnlock()
|
svr.ctlMu.RUnlock()
|
||||||
if ctl == nil {
|
if ctl == nil {
|
||||||
return nil
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := ctl.pm.GetAllProxyStatus()
|
||||||
|
for _, status := range ps {
|
||||||
|
res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.common.ServerAddr))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arrs := range res {
|
||||||
|
if len(arrs) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/config
|
||||||
|
func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
|
||||||
|
log.Infof("http get request [/api/config]")
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http get response [/api/config], code [%d]", res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if svr.configFilePath == "" {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = "frpc has no config file path"
|
||||||
|
log.Warnf("%s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(svr.configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = err.Error()
|
||||||
|
log.Warnf("load frpc config file error: %s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.Msg = string(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/config
|
||||||
|
func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
|
||||||
|
log.Infof("http put request [/api/config]")
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http put response [/api/config], code [%d]", res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// get new config content
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = fmt.Sprintf("read request body error: %v", err)
|
||||||
|
log.Warnf("%s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(body) == 0 {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = "body can't be empty"
|
||||||
|
log.Warnf("%s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(svr.configFilePath, body, 0o600); err != nil {
|
||||||
|
res.Code = 500
|
||||||
|
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
|
||||||
|
log.Warnf("%s", res.Msg)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return ctl.pm.GetAllProxyStatus()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/proxy"
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Controller handles HTTP API requests for frpc.
|
|
||||||
type Controller struct {
|
|
||||||
// getProxyStatus returns the current proxy status.
|
|
||||||
// Returns nil if the control connection is not established.
|
|
||||||
getProxyStatus func() []*proxy.WorkingStatus
|
|
||||||
|
|
||||||
// serverAddr is the frps server address for display.
|
|
||||||
serverAddr string
|
|
||||||
|
|
||||||
// configFilePath is the path to the configuration file.
|
|
||||||
configFilePath string
|
|
||||||
|
|
||||||
// unsafeFeatures is used for validation.
|
|
||||||
unsafeFeatures *security.UnsafeFeatures
|
|
||||||
|
|
||||||
// updateConfig updates proxy and visitor configurations.
|
|
||||||
updateConfig func(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error
|
|
||||||
|
|
||||||
// gracefulClose gracefully stops the service.
|
|
||||||
gracefulClose func(d time.Duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ControllerParams contains parameters for creating an APIController.
|
|
||||||
type ControllerParams struct {
|
|
||||||
GetProxyStatus func() []*proxy.WorkingStatus
|
|
||||||
ServerAddr string
|
|
||||||
ConfigFilePath string
|
|
||||||
UnsafeFeatures *security.UnsafeFeatures
|
|
||||||
UpdateConfig func(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error
|
|
||||||
GracefulClose func(d time.Duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewController creates a new Controller.
|
|
||||||
func NewController(params ControllerParams) *Controller {
|
|
||||||
return &Controller{
|
|
||||||
getProxyStatus: params.GetProxyStatus,
|
|
||||||
serverAddr: params.ServerAddr,
|
|
||||||
configFilePath: params.ConfigFilePath,
|
|
||||||
unsafeFeatures: params.UnsafeFeatures,
|
|
||||||
updateConfig: params.UpdateConfig,
|
|
||||||
gracefulClose: params.GracefulClose,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload handles GET /api/reload
|
|
||||||
func (c *Controller) Reload(ctx *httppkg.Context) (any, error) {
|
|
||||||
strictConfigMode := false
|
|
||||||
strictStr := ctx.Query("strictConfig")
|
|
||||||
if strictStr != "" {
|
|
||||||
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(c.configFilePath, strictConfigMode)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, c.unsafeFeatures); err != nil {
|
|
||||||
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.updateConfig(proxyCfgs, visitorCfgs); err != nil {
|
|
||||||
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("success reload conf")
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop handles POST /api/stop
|
|
||||||
func (c *Controller) Stop(ctx *httppkg.Context) (any, error) {
|
|
||||||
go c.gracefulClose(100 * time.Millisecond)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status handles GET /api/status
|
|
||||||
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
|
||||||
res := make(StatusResp)
|
|
||||||
ps := c.getProxyStatus()
|
|
||||||
if ps == nil {
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, status := range ps {
|
|
||||||
res[status.Type] = append(res[status.Type], c.buildProxyStatusResp(status))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, arrs := range res {
|
|
||||||
if len(arrs) <= 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
|
||||||
return cmp.Compare(a.Name, b.Name)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConfig handles GET /api/config
|
|
||||||
func (c *Controller) GetConfig(ctx *httppkg.Context) (any, error) {
|
|
||||||
if c.configFilePath == "" {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "frpc has no config file path")
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := os.ReadFile(c.configFilePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("load frpc config file error: %s", err.Error())
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
return string(content), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutConfig handles PUT /api/config
|
|
||||||
func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) {
|
|
||||||
body, err := ctx.Body()
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read request body error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(body) == 0 {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "body can't be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(c.configFilePath, body, 0o600); err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, fmt.Sprintf("write content to frpc config file error: %v", err))
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildProxyStatusResp creates a ProxyStatusResp from proxy.WorkingStatus
|
|
||||||
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStatusResp {
|
|
||||||
psr := ProxyStatusResp{
|
|
||||||
Name: status.Name,
|
|
||||||
Type: status.Type,
|
|
||||||
Status: status.Phase,
|
|
||||||
Err: status.Err,
|
|
||||||
}
|
|
||||||
baseCfg := status.Cfg.GetBaseConfig()
|
|
||||||
if baseCfg.LocalPort != 0 {
|
|
||||||
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
|
||||||
}
|
|
||||||
psr.Plugin = baseCfg.Plugin.Type
|
|
||||||
|
|
||||||
if status.Err == "" {
|
|
||||||
psr.RemoteAddr = status.RemoteAddr
|
|
||||||
if slices.Contains([]string{"tcp", "udp"}, status.Type) {
|
|
||||||
psr.RemoteAddr = c.serverAddr + psr.RemoteAddr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return psr
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
// StatusResp is the response for GET /api/status
|
|
||||||
type StatusResp map[string][]ProxyStatusResp
|
|
||||||
|
|
||||||
// ProxyStatusResp contains proxy status information
|
|
||||||
type ProxyStatusResp struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Err string `json:"err"`
|
|
||||||
LocalAddr string `json:"local_addr"`
|
|
||||||
Plugin string `json:"plugin"`
|
|
||||||
RemoteAddr string `json:"remote_addr"`
|
|
||||||
}
|
|
||||||
@@ -43,8 +43,8 @@ type SessionContext struct {
|
|||||||
Conn net.Conn
|
Conn net.Conn
|
||||||
// Indicates whether the connection is encrypted.
|
// Indicates whether the connection is encrypted.
|
||||||
ConnEncrypted bool
|
ConnEncrypted bool
|
||||||
// Auth runtime used for login, heartbeats, and encryption.
|
// Sets authentication based on selected method
|
||||||
Auth *auth.ClientAuth
|
AuthSetter auth.Setter
|
||||||
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
|
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
|
||||||
Connector Connector
|
Connector Connector
|
||||||
// Virtual net controller
|
// Virtual net controller
|
||||||
@@ -91,7 +91,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
|||||||
ctl.lastPong.Store(time.Now())
|
ctl.lastPong.Store(time.Now())
|
||||||
|
|
||||||
if sessionCtx.ConnEncrypted {
|
if sessionCtx.ConnEncrypted {
|
||||||
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.Auth.EncryptionKey())
|
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, []byte(sessionCtx.Common.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -100,9 +100,9 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
|||||||
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
ctl.msgDispatcher = msg.NewDispatcher(sessionCtx.Conn)
|
||||||
}
|
}
|
||||||
ctl.registerMsgHandlers()
|
ctl.registerMsgHandlers()
|
||||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
|
||||||
|
|
||||||
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, sessionCtx.Auth.EncryptionKey(), ctl.msgTransporter, sessionCtx.VnetController)
|
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter, sessionCtx.VnetController)
|
||||||
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
|
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
|
||||||
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
|
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
|
||||||
return ctl, nil
|
return ctl, nil
|
||||||
@@ -133,7 +133,7 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
|
|||||||
m := &msg.NewWorkConn{
|
m := &msg.NewWorkConn{
|
||||||
RunID: ctl.sessionCtx.RunID,
|
RunID: ctl.sessionCtx.RunID,
|
||||||
}
|
}
|
||||||
if err = ctl.sessionCtx.Auth.Setter.SetNewWorkConn(m); err != nil {
|
if err = ctl.sessionCtx.AuthSetter.SetNewWorkConn(m); err != nil {
|
||||||
xl.Warnf("error during NewWorkConn authentication: %v", err)
|
xl.Warnf("error during NewWorkConn authentication: %v", err)
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
return
|
return
|
||||||
@@ -243,7 +243,7 @@ func (ctl *Control) heartbeatWorker() {
|
|||||||
sendHeartBeat := func() (bool, error) {
|
sendHeartBeat := func() (bool, error) {
|
||||||
xl.Debugf("send heartbeat to server")
|
xl.Debugf("send heartbeat to server")
|
||||||
pingMsg := &msg.Ping{}
|
pingMsg := &msg.Ping{}
|
||||||
if err := ctl.sessionCtx.Auth.Setter.SetPing(pingMsg); err != nil {
|
if err := ctl.sessionCtx.AuthSetter.SetPing(pingMsg); err != nil {
|
||||||
xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
|
xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -57,7 +57,6 @@ func NewProxy(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
pxyConf v1.ProxyConfigurer,
|
pxyConf v1.ProxyConfigurer,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
encryptionKey []byte,
|
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
vnetController *vnet.Controller,
|
vnetController *vnet.Controller,
|
||||||
) (pxy Proxy) {
|
) (pxy Proxy) {
|
||||||
@@ -70,7 +69,6 @@ func NewProxy(
|
|||||||
baseProxy := BaseProxy{
|
baseProxy := BaseProxy{
|
||||||
baseCfg: pxyConf.GetBaseConfig(),
|
baseCfg: pxyConf.GetBaseConfig(),
|
||||||
clientCfg: clientCfg,
|
clientCfg: clientCfg,
|
||||||
encryptionKey: encryptionKey,
|
|
||||||
limiter: limiter,
|
limiter: limiter,
|
||||||
msgTransporter: msgTransporter,
|
msgTransporter: msgTransporter,
|
||||||
vnetController: vnetController,
|
vnetController: vnetController,
|
||||||
@@ -88,7 +86,6 @@ func NewProxy(
|
|||||||
type BaseProxy struct {
|
type BaseProxy struct {
|
||||||
baseCfg *v1.ProxyBaseConfig
|
baseCfg *v1.ProxyBaseConfig
|
||||||
clientCfg *v1.ClientCommonConfig
|
clientCfg *v1.ClientCommonConfig
|
||||||
encryptionKey []byte
|
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
vnetController *vnet.Controller
|
vnetController *vnet.Controller
|
||||||
limiter *rate.Limiter
|
limiter *rate.Limiter
|
||||||
@@ -132,7 +129,7 @@ func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pxy.HandleTCPWorkConnection(conn, m, pxy.encryptionKey)
|
pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common handler for tcp work connections.
|
// Common handler for tcp work connections.
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ type Manager struct {
|
|||||||
closed bool
|
closed bool
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
encryptionKey []byte
|
clientCfg *v1.ClientCommonConfig
|
||||||
clientCfg *v1.ClientCommonConfig
|
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
@@ -49,7 +48,6 @@ type Manager struct {
|
|||||||
func NewManager(
|
func NewManager(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
encryptionKey []byte,
|
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
vnetController *vnet.Controller,
|
vnetController *vnet.Controller,
|
||||||
) *Manager {
|
) *Manager {
|
||||||
@@ -58,7 +56,6 @@ func NewManager(
|
|||||||
msgTransporter: msgTransporter,
|
msgTransporter: msgTransporter,
|
||||||
vnetController: vnetController,
|
vnetController: vnetController,
|
||||||
closed: false,
|
closed: false,
|
||||||
encryptionKey: encryptionKey,
|
|
||||||
clientCfg: clientCfg,
|
clientCfg: clientCfg,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
}
|
}
|
||||||
@@ -166,7 +163,7 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
|
|||||||
for _, cfg := range proxyCfgs {
|
for _, cfg := range proxyCfgs {
|
||||||
name := cfg.GetBaseConfig().Name
|
name := cfg.GetBaseConfig().Name
|
||||||
if _, ok := pm.proxies[name]; !ok {
|
if _, ok := pm.proxies[name]; !ok {
|
||||||
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.encryptionKey, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
|
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
|
||||||
if pm.inWorkConnCallback != nil {
|
if pm.inWorkConnCallback != nil {
|
||||||
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
|
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ func NewWrapper(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
cfg v1.ProxyConfigurer,
|
cfg v1.ProxyConfigurer,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
encryptionKey []byte,
|
|
||||||
eventHandler event.Handler,
|
eventHandler event.Handler,
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
vnetController *vnet.Controller,
|
vnetController *vnet.Controller,
|
||||||
@@ -123,7 +122,7 @@ func NewWrapper(
|
|||||||
xl.Tracef("enable health check monitor")
|
xl.Tracef("enable health check monitor")
|
||||||
}
|
}
|
||||||
|
|
||||||
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, encryptionKey, pw.msgTransporter, pw.vnetController)
|
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter, pw.vnetController)
|
||||||
return pw
|
return pw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/auth"
|
"github.com/fatedier/frp/pkg/auth"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
@@ -65,8 +64,6 @@ type ServiceOptions struct {
|
|||||||
ProxyCfgs []v1.ProxyConfigurer
|
ProxyCfgs []v1.ProxyConfigurer
|
||||||
VisitorCfgs []v1.VisitorConfigurer
|
VisitorCfgs []v1.VisitorConfigurer
|
||||||
|
|
||||||
UnsafeFeatures *security.UnsafeFeatures
|
|
||||||
|
|
||||||
// ConfigFilePath is the path to the configuration file used to initialize.
|
// ConfigFilePath is the path to the configuration file used to initialize.
|
||||||
// If it is empty, it means that the configuration file is not used for initialization.
|
// If it is empty, it means that the configuration file is not used for initialization.
|
||||||
// It may be initialized using command line parameters or called directly.
|
// It may be initialized using command line parameters or called directly.
|
||||||
@@ -111,8 +108,8 @@ type Service struct {
|
|||||||
// Uniq id got from frps, it will be attached to loginMsg.
|
// Uniq id got from frps, it will be attached to loginMsg.
|
||||||
runID string
|
runID string
|
||||||
|
|
||||||
// Auth runtime and encryption materials
|
// Sets authentication based on selected method
|
||||||
auth *auth.ClientAuth
|
authSetter auth.Setter
|
||||||
|
|
||||||
// web server for admin UI and apis
|
// web server for admin UI and apis
|
||||||
webServer *httppkg.Server
|
webServer *httppkg.Server
|
||||||
@@ -125,8 +122,6 @@ type Service struct {
|
|||||||
visitorCfgs []v1.VisitorConfigurer
|
visitorCfgs []v1.VisitorConfigurer
|
||||||
clientSpec *msg.ClientSpec
|
clientSpec *msg.ClientSpec
|
||||||
|
|
||||||
unsafeFeatures *security.UnsafeFeatures
|
|
||||||
|
|
||||||
// The configuration file used to initialize this client, or an empty
|
// The configuration file used to initialize this client, or an empty
|
||||||
// string if no configuration file was used.
|
// string if no configuration file was used.
|
||||||
configFilePath string
|
configFilePath string
|
||||||
@@ -155,18 +150,17 @@ func NewService(options ServiceOptions) (*Service, error) {
|
|||||||
webServer = ws
|
webServer = ws
|
||||||
}
|
}
|
||||||
|
|
||||||
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
|
authSetter, err := auth.NewAuthSetter(options.Common.Auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Service{
|
s := &Service{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
auth: authRuntime,
|
authSetter: authSetter,
|
||||||
webServer: webServer,
|
webServer: webServer,
|
||||||
common: options.Common,
|
common: options.Common,
|
||||||
configFilePath: options.ConfigFilePath,
|
configFilePath: options.ConfigFilePath,
|
||||||
unsafeFeatures: options.UnsafeFeatures,
|
|
||||||
proxyCfgs: options.ProxyCfgs,
|
proxyCfgs: options.ProxyCfgs,
|
||||||
visitorCfgs: options.VisitorCfgs,
|
visitorCfgs: options.VisitorCfgs,
|
||||||
clientSpec: options.ClientSpec,
|
clientSpec: options.ClientSpec,
|
||||||
@@ -281,15 +275,11 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hostname, _ := os.Hostname()
|
|
||||||
|
|
||||||
loginMsg := &msg.Login{
|
loginMsg := &msg.Login{
|
||||||
Arch: runtime.GOARCH,
|
Arch: runtime.GOARCH,
|
||||||
Os: runtime.GOOS,
|
Os: runtime.GOOS,
|
||||||
Hostname: hostname,
|
|
||||||
PoolCount: svr.common.Transport.PoolCount,
|
PoolCount: svr.common.Transport.PoolCount,
|
||||||
User: svr.common.User,
|
User: svr.common.User,
|
||||||
ClientID: svr.common.ClientID,
|
|
||||||
Version: version.Full(),
|
Version: version.Full(),
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
RunID: svr.runID,
|
RunID: svr.runID,
|
||||||
@@ -300,7 +290,7 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add auth
|
// Add auth
|
||||||
if err = svr.auth.Setter.SetLogin(loginMsg); err != nil {
|
if err = svr.authSetter.SetLogin(loginMsg); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -354,7 +344,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
|
|||||||
RunID: svr.runID,
|
RunID: svr.runID,
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
ConnEncrypted: connEncrypted,
|
ConnEncrypted: connEncrypted,
|
||||||
Auth: svr.auth,
|
AuthSetter: svr.authSetter,
|
||||||
Connector: connector,
|
Connector: connector,
|
||||||
VnetController: svr.vnetController,
|
VnetController: svr.vnetController,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package visitor
|
package visitor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -82,22 +81,11 @@ func (sv *STCPVisitor) internalConnWorker() {
|
|||||||
|
|
||||||
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
var tunnelErr error
|
defer userConn.Close()
|
||||||
defer func() {
|
|
||||||
// If there was an error and connection supports CloseWithError, use it
|
|
||||||
if tunnelErr != nil {
|
|
||||||
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
|
||||||
_ = eConn.CloseWithError(tunnelErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
userConn.Close()
|
|
||||||
}()
|
|
||||||
|
|
||||||
xl.Debugf("get a new stcp user connection")
|
xl.Debugf("get a new stcp user connection")
|
||||||
visitorConn, err := sv.helper.ConnectServer()
|
visitorConn, err := sv.helper.ConnectServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
tunnelErr = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer visitorConn.Close()
|
defer visitorConn.Close()
|
||||||
@@ -114,7 +102,6 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
||||||
tunnelErr = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -123,14 +110,12 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
||||||
tunnelErr = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
if newVisitorConnRespMsg.Error != "" {
|
if newVisitorConnRespMsg.Error != "" {
|
||||||
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||||
tunnelErr = fmt.Errorf("%s", newVisitorConnRespMsg.Error)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -140,7 +125,6 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
tunnelErr = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -71,7 +71,7 @@ func NewVisitor(
|
|||||||
Name: cfg.GetBaseConfig().Name,
|
Name: cfg.GetBaseConfig().Name,
|
||||||
Ctx: ctx,
|
Ctx: ctx,
|
||||||
VnetController: helper.VNetController(),
|
VnetController: helper.VNetController(),
|
||||||
SendConnToVisitor: func(conn net.Conn) {
|
HandleConn: func(conn net.Conn) {
|
||||||
_ = baseVisitor.AcceptConn(conn)
|
_ = baseVisitor.AcceptConn(conn)
|
||||||
},
|
},
|
||||||
},
|
},
|
||||||
|
|||||||
@@ -162,16 +162,8 @@ func (sv *XTCPVisitor) keepTunnelOpenWorker() {
|
|||||||
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
isConnTransferred := false
|
isConnTransferred := false
|
||||||
var tunnelErr error
|
|
||||||
defer func() {
|
defer func() {
|
||||||
if !isConnTransferred {
|
if !isConnTransferred {
|
||||||
// If there was an error and connection supports CloseWithError, use it
|
|
||||||
if tunnelErr != nil {
|
|
||||||
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
|
||||||
_ = eConn.CloseWithError(tunnelErr)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
userConn.Close()
|
userConn.Close()
|
||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
@@ -189,8 +181,6 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
tunnelConn, err := sv.openTunnel(ctx)
|
tunnelConn, err := sv.openTunnel(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("open tunnel error: %v", err)
|
xl.Errorf("open tunnel error: %v", err)
|
||||||
tunnelErr = err
|
|
||||||
|
|
||||||
// no fallback, just return
|
// no fallback, just return
|
||||||
if sv.cfg.FallbackTo == "" {
|
if sv.cfg.FallbackTo == "" {
|
||||||
return
|
return
|
||||||
@@ -210,7 +200,6 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
tunnelErr = err
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
_ "github.com/fatedier/frp/assets/frpc"
|
||||||
"github.com/fatedier/frp/cmd/frpc/sub"
|
"github.com/fatedier/frp/cmd/frpc/sub"
|
||||||
"github.com/fatedier/frp/pkg/util/system"
|
"github.com/fatedier/frp/pkg/util/system"
|
||||||
_ "github.com/fatedier/frp/web/frpc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var proxyTypes = []v1.ProxyType{
|
var proxyTypes = []v1.ProxyType{
|
||||||
@@ -78,10 +77,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
|
||||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
|
||||||
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -92,7 +88,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "")
|
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -110,9 +106,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
if _, err := validation.ValidateClientCommonConfig(clientCfg); err != nil {
|
||||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
|
||||||
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -123,7 +117,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "")
|
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"syscall"
|
"syscall"
|
||||||
"time"
|
"time"
|
||||||
@@ -32,8 +31,7 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/policy/featuregate"
|
"github.com/fatedier/frp/pkg/featuregate"
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
)
|
)
|
||||||
@@ -43,7 +41,6 @@ var (
|
|||||||
cfgDir string
|
cfgDir string
|
||||||
showVersion bool
|
showVersion bool
|
||||||
strictConfigMode bool
|
strictConfigMode bool
|
||||||
allowUnsafe []string
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -51,9 +48,6 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
|
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
|
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
|
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
|
||||||
|
|
||||||
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
|
||||||
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
|
|
||||||
}
|
}
|
||||||
|
|
||||||
var rootCmd = &cobra.Command{
|
var rootCmd = &cobra.Command{
|
||||||
@@ -65,17 +59,15 @@ var rootCmd = &cobra.Command{
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
|
||||||
|
|
||||||
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
|
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
|
||||||
// Note that it's only designed for testing. It's not guaranteed to be stable.
|
// Note that it's only designed for testing. It's not guaranteed to be stable.
|
||||||
if cfgDir != "" {
|
if cfgDir != "" {
|
||||||
_ = runMultipleClients(cfgDir, unsafeFeatures)
|
_ = runMultipleClients(cfgDir)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Do not show command usage here.
|
// Do not show command usage here.
|
||||||
err := runClient(cfgFile, unsafeFeatures)
|
err := runClient(cfgFile)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -84,7 +76,7 @@ var rootCmd = &cobra.Command{
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures) error {
|
func runMultipleClients(cfgDir string) error {
|
||||||
var wg sync.WaitGroup
|
var wg sync.WaitGroup
|
||||||
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
|
err := filepath.WalkDir(cfgDir, func(path string, d fs.DirEntry, err error) error {
|
||||||
if err != nil || d.IsDir() {
|
if err != nil || d.IsDir() {
|
||||||
@@ -94,7 +86,7 @@ func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures)
|
|||||||
time.Sleep(time.Millisecond)
|
time.Sleep(time.Millisecond)
|
||||||
go func() {
|
go func() {
|
||||||
defer wg.Done()
|
defer wg.Done()
|
||||||
err := runClient(path, unsafeFeatures)
|
err := runClient(path)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Printf("frpc service error for config file [%s]\n", path)
|
fmt.Printf("frpc service error for config file [%s]\n", path)
|
||||||
}
|
}
|
||||||
@@ -119,7 +111,7 @@ func handleTermSignal(svr *client.Service) {
|
|||||||
svr.GracefulClose(500 * time.Millisecond)
|
svr.GracefulClose(500 * time.Millisecond)
|
||||||
}
|
}
|
||||||
|
|
||||||
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
func runClient(cfgFilePath string) error {
|
||||||
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
|
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -135,22 +127,20 @@ func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) erro
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs)
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return startService(cfg, proxyCfgs, visitorCfgs, cfgFilePath)
|
||||||
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startService(
|
func startService(
|
||||||
cfg *v1.ClientCommonConfig,
|
cfg *v1.ClientCommonConfig,
|
||||||
proxyCfgs []v1.ProxyConfigurer,
|
proxyCfgs []v1.ProxyConfigurer,
|
||||||
visitorCfgs []v1.VisitorConfigurer,
|
visitorCfgs []v1.VisitorConfigurer,
|
||||||
unsafeFeatures *security.UnsafeFeatures,
|
|
||||||
cfgFile string,
|
cfgFile string,
|
||||||
) error {
|
) error {
|
||||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||||
@@ -163,7 +153,6 @@ func startService(
|
|||||||
Common: cfg,
|
Common: cfg,
|
||||||
ProxyCfgs: proxyCfgs,
|
ProxyCfgs: proxyCfgs,
|
||||||
VisitorCfgs: visitorCfgs,
|
VisitorCfgs: visitorCfgs,
|
||||||
UnsafeFeatures: unsafeFeatures,
|
|
||||||
ConfigFilePath: cfgFile,
|
ConfigFilePath: cfgFile,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -43,8 +42,7 @@ var verifyCmd = &cobra.Command{
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs)
|
||||||
warning, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
_ "github.com/fatedier/frp/assets/frps"
|
||||||
_ "github.com/fatedier/frp/pkg/metrics"
|
_ "github.com/fatedier/frp/pkg/metrics"
|
||||||
"github.com/fatedier/frp/pkg/util/system"
|
"github.com/fatedier/frp/pkg/util/system"
|
||||||
_ "github.com/fatedier/frp/web/frps"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -18,14 +18,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
"github.com/fatedier/frp/server"
|
"github.com/fatedier/frp/server"
|
||||||
@@ -35,7 +33,6 @@ var (
|
|||||||
cfgFile string
|
cfgFile string
|
||||||
showVersion bool
|
showVersion bool
|
||||||
strictConfigMode bool
|
strictConfigMode bool
|
||||||
allowUnsafe []string
|
|
||||||
|
|
||||||
serverCfg v1.ServerConfig
|
serverCfg v1.ServerConfig
|
||||||
)
|
)
|
||||||
@@ -44,8 +41,6 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
|
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
|
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
|
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
|
||||||
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
|
||||||
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ServerUnsafeFeatures, ", ")))
|
|
||||||
|
|
||||||
config.RegisterServerConfigFlags(rootCmd, &serverCfg)
|
config.RegisterServerConfigFlags(rootCmd, &serverCfg)
|
||||||
}
|
}
|
||||||
@@ -82,9 +77,7 @@ var rootCmd = &cobra.Command{
|
|||||||
svrCfg = &serverCfg
|
svrCfg = &serverCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
warning, err := validation.ValidateServerConfig(svrCfg)
|
||||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
|
||||||
warning, err := validator.ValidateServerConfig(svrCfg)
|
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -43,9 +42,7 @@ var verifyCmd = &cobra.Command{
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
warning, err := validation.ValidateServerConfig(svrCfg)
|
||||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
|
||||||
warning, err := validator.ValidateServerConfig(svrCfg)
|
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
||||||
|
|
||||||
# Optional unique identifier for this frpc instance.
|
|
||||||
clientID = "your_client_id"
|
|
||||||
# your proxy name will be changed to {user}.{proxy}
|
# your proxy name will be changed to {user}.{proxy}
|
||||||
user = "your_name"
|
user = "your_name"
|
||||||
|
|
||||||
@@ -145,11 +143,6 @@ transport.tls.enable = true
|
|||||||
# Default is empty, means all proxies.
|
# Default is empty, means all proxies.
|
||||||
# start = ["ssh", "dns"]
|
# start = ["ssh", "dns"]
|
||||||
|
|
||||||
# Alternative to 'start': You can control each proxy individually using the 'enabled' field.
|
|
||||||
# Set 'enabled = false' in a proxy configuration to disable it.
|
|
||||||
# If 'enabled' is not set or set to true, the proxy is enabled by default.
|
|
||||||
# The 'enabled' field provides more granular control and is recommended over 'start'.
|
|
||||||
|
|
||||||
# Specify udp packet size, unit is byte. If not set, the default value is 1500.
|
# Specify udp packet size, unit is byte. If not set, the default value is 1500.
|
||||||
# This parameter should be same between client and server.
|
# This parameter should be same between client and server.
|
||||||
# It affects the udp and sudp proxy.
|
# It affects the udp and sudp proxy.
|
||||||
@@ -176,8 +169,6 @@ metadatas.var2 = "123"
|
|||||||
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
# If global user is not empty, it will be changed to {user}.{proxy} such as 'your_name.ssh'
|
||||||
name = "ssh"
|
name = "ssh"
|
||||||
type = "tcp"
|
type = "tcp"
|
||||||
# Enable or disable this proxy. true or omit this field to enable, false to disable.
|
|
||||||
# enabled = true
|
|
||||||
localIP = "127.0.0.1"
|
localIP = "127.0.0.1"
|
||||||
localPort = 22
|
localPort = 22
|
||||||
# Limit bandwidth for this proxy, unit is KB and MB
|
# Limit bandwidth for this proxy, unit is KB and MB
|
||||||
@@ -262,8 +253,6 @@ healthCheck.httpHeaders=[
|
|||||||
[[proxies]]
|
[[proxies]]
|
||||||
name = "web02"
|
name = "web02"
|
||||||
type = "https"
|
type = "https"
|
||||||
# Disable this proxy by setting enabled to false
|
|
||||||
# enabled = false
|
|
||||||
localIP = "127.0.0.1"
|
localIP = "127.0.0.1"
|
||||||
localPort = 8000
|
localPort = 8000
|
||||||
subdomain = "web02"
|
subdomain = "web02"
|
||||||
|
|||||||
BIN
doc/pic/zsxq.jpg
Normal file
BIN
doc/pic/zsxq.jpg
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 12 KiB |
@@ -1,17 +1,9 @@
|
|||||||
FROM node:22 AS web-builder
|
|
||||||
|
|
||||||
WORKDIR /web/frpc
|
|
||||||
COPY web/frpc/ ./
|
|
||||||
RUN npm install
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
FROM golang:1.24 AS building
|
FROM golang:1.24 AS building
|
||||||
|
|
||||||
COPY . /building
|
COPY . /building
|
||||||
COPY --from=web-builder /web/frpc/dist /building/web/frpc/dist
|
|
||||||
WORKDIR /building
|
WORKDIR /building
|
||||||
|
|
||||||
RUN env CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -tags frpc -o bin/frpc ./cmd/frpc
|
RUN make frpc
|
||||||
|
|
||||||
FROM alpine:3
|
FROM alpine:3
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
FROM node:22 AS web-builder
|
|
||||||
|
|
||||||
WORKDIR /web/frps
|
|
||||||
COPY web/frps/ ./
|
|
||||||
RUN npm install
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
FROM golang:1.24 AS building
|
FROM golang:1.24 AS building
|
||||||
|
|
||||||
COPY . /building
|
COPY . /building
|
||||||
COPY --from=web-builder /web/frps/dist /building/web/frps/dist
|
|
||||||
WORKDIR /building
|
WORKDIR /building
|
||||||
|
|
||||||
RUN env CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -tags frps -o bin/frps ./cmd/frps
|
RUN make frps
|
||||||
|
|
||||||
FROM alpine:3
|
FROM alpine:3
|
||||||
|
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
@@ -28,51 +27,14 @@ type Setter interface {
|
|||||||
SetNewWorkConn(*msg.NewWorkConn) error
|
SetNewWorkConn(*msg.NewWorkConn) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientAuth struct {
|
|
||||||
Setter Setter
|
|
||||||
key []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ClientAuth) EncryptionKey() []byte {
|
|
||||||
return a.key
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildClientAuth resolves any dynamic auth values and returns a prepared auth runtime.
|
|
||||||
// Caller must run validation before calling this function.
|
|
||||||
func BuildClientAuth(cfg *v1.AuthClientConfig) (*ClientAuth, error) {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, fmt.Errorf("auth config is nil")
|
|
||||||
}
|
|
||||||
resolved := *cfg
|
|
||||||
if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil {
|
|
||||||
token, err := resolved.TokenSource.Resolve(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
|
||||||
}
|
|
||||||
resolved.Token = token
|
|
||||||
}
|
|
||||||
setter, err := NewAuthSetter(resolved)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ClientAuth{
|
|
||||||
Setter: setter,
|
|
||||||
key: []byte(resolved.Token),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) {
|
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) {
|
||||||
switch cfg.Method {
|
switch cfg.Method {
|
||||||
case v1.AuthMethodToken:
|
case v1.AuthMethodToken:
|
||||||
authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
|
authProvider = NewTokenAuth(cfg.AdditionalScopes, cfg.Token)
|
||||||
case v1.AuthMethodOIDC:
|
case v1.AuthMethodOIDC:
|
||||||
if cfg.OIDC.TokenSource != nil {
|
authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
|
||||||
authProvider = NewOidcTokenSourceAuthSetter(cfg.AdditionalScopes, cfg.OIDC.TokenSource)
|
if err != nil {
|
||||||
} else {
|
return nil, err
|
||||||
authProvider, err = NewOidcAuthSetter(cfg.AdditionalScopes, cfg.OIDC)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
default:
|
default:
|
||||||
return nil, fmt.Errorf("unsupported auth method: %s", cfg.Method)
|
return nil, fmt.Errorf("unsupported auth method: %s", cfg.Method)
|
||||||
@@ -86,35 +48,6 @@ type Verifier interface {
|
|||||||
VerifyNewWorkConn(*msg.NewWorkConn) error
|
VerifyNewWorkConn(*msg.NewWorkConn) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerAuth struct {
|
|
||||||
Verifier Verifier
|
|
||||||
key []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ServerAuth) EncryptionKey() []byte {
|
|
||||||
return a.key
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildServerAuth resolves any dynamic auth values and returns a prepared auth runtime.
|
|
||||||
// Caller must run validation before calling this function.
|
|
||||||
func BuildServerAuth(cfg *v1.AuthServerConfig) (*ServerAuth, error) {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, fmt.Errorf("auth config is nil")
|
|
||||||
}
|
|
||||||
resolved := *cfg
|
|
||||||
if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil {
|
|
||||||
token, err := resolved.TokenSource.Resolve(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
|
||||||
}
|
|
||||||
resolved.Token = token
|
|
||||||
}
|
|
||||||
return &ServerAuth{
|
|
||||||
Verifier: NewAuthVerifier(resolved),
|
|
||||||
key: []byte(resolved.Token),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
|
func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
|
||||||
switch cfg.Method {
|
switch cfg.Method {
|
||||||
case v1.AuthMethodToken:
|
case v1.AuthMethodToken:
|
||||||
|
|||||||
@@ -152,51 +152,6 @@ func (auth *OidcAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (e
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
type OidcTokenSourceAuthProvider struct {
|
|
||||||
additionalAuthScopes []v1.AuthScope
|
|
||||||
|
|
||||||
valueSource *v1.ValueSource
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewOidcTokenSourceAuthSetter(additionalAuthScopes []v1.AuthScope, valueSource *v1.ValueSource) *OidcTokenSourceAuthProvider {
|
|
||||||
return &OidcTokenSourceAuthProvider{
|
|
||||||
additionalAuthScopes: additionalAuthScopes,
|
|
||||||
valueSource: valueSource,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OidcTokenSourceAuthProvider) generateAccessToken() (accessToken string, err error) {
|
|
||||||
ctx := context.Background()
|
|
||||||
accessToken, err = auth.valueSource.Resolve(ctx)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("couldn't acquire OIDC token for login: %v", err)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OidcTokenSourceAuthProvider) SetLogin(loginMsg *msg.Login) (err error) {
|
|
||||||
loginMsg.PrivilegeKey, err = auth.generateAccessToken()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OidcTokenSourceAuthProvider) SetPing(pingMsg *msg.Ping) (err error) {
|
|
||||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeHeartBeats) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
pingMsg.PrivilegeKey, err = auth.generateAccessToken()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (auth *OidcTokenSourceAuthProvider) SetNewWorkConn(newWorkConnMsg *msg.NewWorkConn) (err error) {
|
|
||||||
if !slices.Contains(auth.additionalAuthScopes, v1.AuthScopeNewWorkConns) {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
newWorkConnMsg.PrivilegeKey, err = auth.generateAccessToken()
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type TokenVerifier interface {
|
type TokenVerifier interface {
|
||||||
Verify(context.Context, string) (*oidc.IDToken, error)
|
Verify(context.Context, string) (*oidc.IDToken, error)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -167,7 +167,6 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi
|
|||||||
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
||||||
}
|
}
|
||||||
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
||||||
cmd.PersistentFlags().StringVar(&c.ClientID, "client-id", "", "unique identifier for this frpc instance")
|
|
||||||
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
|
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -281,17 +281,6 @@ func LoadClientConfig(path string, strict bool) (
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Filter by enabled field in each proxy
|
|
||||||
// nil or true means enabled, false means disabled
|
|
||||||
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
|
||||||
enabled := c.GetBaseConfig().Enabled
|
|
||||||
return enabled == nil || *enabled
|
|
||||||
})
|
|
||||||
visitorCfgs = lo.Filter(visitorCfgs, func(c v1.VisitorConfigurer, _ int) bool {
|
|
||||||
enabled := c.GetBaseConfig().Enabled
|
|
||||||
return enabled == nil || *enabled
|
|
||||||
})
|
|
||||||
|
|
||||||
if cliCfg != nil {
|
if cliCfg != nil {
|
||||||
if err := cliCfg.Complete(); err != nil {
|
if err := cliCfg.Complete(); err != nil {
|
||||||
return nil, nil, nil, isLegacyFormat, err
|
return nil, nil, nil, isLegacyFormat, err
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@@ -37,8 +39,6 @@ type ClientCommonConfig struct {
|
|||||||
// clients. If this value is not "", proxy names will automatically be
|
// clients. If this value is not "", proxy names will automatically be
|
||||||
// changed to "{user}.{proxy_name}".
|
// changed to "{user}.{proxy_name}".
|
||||||
User string `json:"user,omitempty"`
|
User string `json:"user,omitempty"`
|
||||||
// ClientID uniquely identifies this frpc instance.
|
|
||||||
ClientID string `json:"clientID,omitempty"`
|
|
||||||
|
|
||||||
// ServerAddr specifies the address of the server to connect to. By
|
// ServerAddr specifies the address of the server to connect to. By
|
||||||
// default, this value is "0.0.0.0".
|
// default, this value is "0.0.0.0".
|
||||||
@@ -198,6 +198,17 @@ type AuthClientConfig struct {
|
|||||||
|
|
||||||
func (c *AuthClientConfig) Complete() error {
|
func (c *AuthClientConfig) Complete() error {
|
||||||
c.Method = util.EmptyOr(c.Method, "token")
|
c.Method = util.EmptyOr(c.Method, "token")
|
||||||
|
|
||||||
|
// Resolve tokenSource during configuration loading
|
||||||
|
if c.Method == AuthMethodToken && c.TokenSource != nil {
|
||||||
|
token, err := c.TokenSource.Resolve(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
||||||
|
}
|
||||||
|
// Move the resolved token to the Token field and clear TokenSource
|
||||||
|
c.Token = token
|
||||||
|
c.TokenSource = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -228,10 +239,6 @@ type AuthOIDCClientConfig struct {
|
|||||||
// Supports http, https, socks5, and socks5h proxy protocols.
|
// Supports http, https, socks5, and socks5h proxy protocols.
|
||||||
// If empty, no proxy is used for OIDC connections.
|
// If empty, no proxy is used for OIDC connections.
|
||||||
ProxyURL string `json:"proxyURL,omitempty"`
|
ProxyURL string `json:"proxyURL,omitempty"`
|
||||||
|
|
||||||
// TokenSource specifies a custom dynamic source for the authorization token.
|
|
||||||
// This is mutually exclusive with every other field of this structure.
|
|
||||||
TokenSource *ValueSource `json:"tokenSource,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type VirtualNetConfig struct {
|
type VirtualNetConfig struct {
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@@ -36,9 +38,68 @@ func TestClientConfigComplete(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthClientConfig_Complete(t *testing.T) {
|
func TestAuthClientConfig_Complete(t *testing.T) {
|
||||||
require := require.New(t)
|
// Create a temporary file for testing
|
||||||
cfg := &AuthClientConfig{}
|
tmpDir := t.TempDir()
|
||||||
err := cfg.Complete()
|
testFile := filepath.Join(tmpDir, "test_token")
|
||||||
require.NoError(err)
|
testContent := "client-token-value"
|
||||||
require.EqualValues("token", cfg.Method)
|
err := os.WriteFile(testFile, []byte(testContent), 0o600)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config AuthClientConfig
|
||||||
|
expectToken string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "tokenSource resolved to token",
|
||||||
|
config: AuthClientConfig{
|
||||||
|
Method: AuthMethodToken,
|
||||||
|
TokenSource: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: &FileSource{
|
||||||
|
Path: testFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectToken: testContent,
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "direct token unchanged",
|
||||||
|
config: AuthClientConfig{
|
||||||
|
Method: AuthMethodToken,
|
||||||
|
Token: "direct-token",
|
||||||
|
},
|
||||||
|
expectToken: "direct-token",
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid tokenSource should panic",
|
||||||
|
config: AuthClientConfig{
|
||||||
|
Method: AuthMethodToken,
|
||||||
|
TokenSource: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: &FileSource{
|
||||||
|
Path: "/non/existent/file",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.expectPanic {
|
||||||
|
err := tt.config.Complete()
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
err := tt.config.Complete()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.expectToken, tt.config.Token)
|
||||||
|
require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -108,11 +108,8 @@ type DomainConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type ProxyBaseConfig struct {
|
type ProxyBaseConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
// Enabled controls whether this proxy is enabled. nil or true means enabled, false means disabled.
|
|
||||||
// This allows individual control over each proxy, complementing the global "start" field.
|
|
||||||
Enabled *bool `json:"enabled,omitempty"`
|
|
||||||
Annotations map[string]string `json:"annotations,omitempty"`
|
Annotations map[string]string `json:"annotations,omitempty"`
|
||||||
Transport ProxyTransport `json:"transport,omitempty"`
|
Transport ProxyTransport `json:"transport,omitempty"`
|
||||||
// metadata info for each proxy
|
// metadata info for each proxy
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/types"
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
@@ -135,6 +138,17 @@ type AuthServerConfig struct {
|
|||||||
|
|
||||||
func (c *AuthServerConfig) Complete() error {
|
func (c *AuthServerConfig) Complete() error {
|
||||||
c.Method = util.EmptyOr(c.Method, "token")
|
c.Method = util.EmptyOr(c.Method, "token")
|
||||||
|
|
||||||
|
// Resolve tokenSource during configuration loading
|
||||||
|
if c.Method == AuthMethodToken && c.TokenSource != nil {
|
||||||
|
token, err := c.TokenSource.Resolve(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
||||||
|
}
|
||||||
|
// Move the resolved token to the Token field and clear TokenSource
|
||||||
|
c.Token = token
|
||||||
|
c.TokenSource = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@@ -33,9 +35,68 @@ func TestServerConfigComplete(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthServerConfig_Complete(t *testing.T) {
|
func TestAuthServerConfig_Complete(t *testing.T) {
|
||||||
require := require.New(t)
|
// Create a temporary file for testing
|
||||||
cfg := &AuthServerConfig{}
|
tmpDir := t.TempDir()
|
||||||
err := cfg.Complete()
|
testFile := filepath.Join(tmpDir, "test_token")
|
||||||
require.NoError(err)
|
testContent := "file-token-value"
|
||||||
require.EqualValues("token", cfg.Method)
|
err := os.WriteFile(testFile, []byte(testContent), 0o600)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config AuthServerConfig
|
||||||
|
expectToken string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "tokenSource resolved to token",
|
||||||
|
config: AuthServerConfig{
|
||||||
|
Method: AuthMethodToken,
|
||||||
|
TokenSource: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: &FileSource{
|
||||||
|
Path: testFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectToken: testContent,
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "direct token unchanged",
|
||||||
|
config: AuthServerConfig{
|
||||||
|
Method: AuthMethodToken,
|
||||||
|
Token: "direct-token",
|
||||||
|
},
|
||||||
|
expectToken: "direct-token",
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid tokenSource should panic",
|
||||||
|
config: AuthServerConfig{
|
||||||
|
Method: AuthMethodToken,
|
||||||
|
TokenSource: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: &FileSource{
|
||||||
|
Path: "/non/existent/file",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.expectPanic {
|
||||||
|
err := tt.config.Complete()
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
err := tt.config.Complete()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.expectToken, tt.config.Token)
|
||||||
|
require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -23,109 +23,55 @@ import (
|
|||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/policy/featuregate"
|
"github.com/fatedier/frp/pkg/featuregate"
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
func ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
||||||
var (
|
var (
|
||||||
warnings Warning
|
warnings Warning
|
||||||
errs error
|
errs error
|
||||||
)
|
)
|
||||||
|
// validate feature gates
|
||||||
validators := []func() (Warning, error){
|
|
||||||
func() (Warning, error) { return validateFeatureGates(c) },
|
|
||||||
func() (Warning, error) { return v.validateAuthConfig(&c.Auth) },
|
|
||||||
func() (Warning, error) { return nil, validateLogConfig(&c.Log) },
|
|
||||||
func() (Warning, error) { return nil, validateWebServerConfig(&c.WebServer) },
|
|
||||||
func() (Warning, error) { return validateTransportConfig(&c.Transport) },
|
|
||||||
func() (Warning, error) { return validateIncludeFiles(c.IncludeConfigFiles) },
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, validator := range validators {
|
|
||||||
w, err := validator()
|
|
||||||
warnings = AppendError(warnings, w)
|
|
||||||
errs = AppendError(errs, err)
|
|
||||||
}
|
|
||||||
return warnings, errs
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateFeatureGates(c *v1.ClientCommonConfig) (Warning, error) {
|
|
||||||
if c.VirtualNet.Address != "" {
|
if c.VirtualNet.Address != "" {
|
||||||
if !featuregate.Enabled(featuregate.VirtualNet) {
|
if !featuregate.Enabled(featuregate.VirtualNet) {
|
||||||
return nil, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag")
|
return warnings, fmt.Errorf("VirtualNet feature is not enabled; enable it by setting the appropriate feature gate flag")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, error) {
|
if !slices.Contains(SupportedAuthMethods, c.Auth.Method) {
|
||||||
var errs error
|
|
||||||
if !slices.Contains(SupportedAuthMethods, c.Method) {
|
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
|
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
|
||||||
}
|
}
|
||||||
if !lo.Every(SupportedAuthAdditionalScopes, c.AdditionalScopes) {
|
if !lo.Every(SupportedAuthAdditionalScopes, c.Auth.AdditionalScopes) {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
|
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate token/tokenSource mutual exclusivity
|
// Validate token/tokenSource mutual exclusivity
|
||||||
if c.Token != "" && c.TokenSource != nil {
|
if c.Auth.Token != "" && c.Auth.TokenSource != nil {
|
||||||
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
|
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate tokenSource if specified
|
// Validate tokenSource if specified
|
||||||
if c.TokenSource != nil {
|
if c.Auth.TokenSource != nil {
|
||||||
if c.TokenSource.Type == "exec" {
|
if err := c.Auth.TokenSource.Validate(); err != nil {
|
||||||
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
|
||||||
errs = AppendError(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := c.TokenSource.Validate(); err != nil {
|
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := v.validateOIDCConfig(&c.OIDC); err != nil {
|
if err := validateLogConfig(&c.Log); err != nil {
|
||||||
errs = AppendError(errs, err)
|
errs = AppendError(errs, err)
|
||||||
}
|
}
|
||||||
return nil, errs
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *ConfigValidator) validateOIDCConfig(c *v1.AuthOIDCClientConfig) error {
|
if err := validateWebServerConfig(&c.WebServer); err != nil {
|
||||||
if c.TokenSource == nil {
|
errs = AppendError(errs, err)
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
var errs error
|
|
||||||
// Validate oidc.tokenSource mutual exclusivity with other fields of oidc
|
|
||||||
if c.ClientID != "" || c.ClientSecret != "" || c.Audience != "" ||
|
|
||||||
c.Scope != "" || c.TokenEndpointURL != "" || len(c.AdditionalEndpointParams) > 0 ||
|
|
||||||
c.TrustedCaFile != "" || c.InsecureSkipVerify || c.ProxyURL != "" {
|
|
||||||
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc"))
|
|
||||||
}
|
|
||||||
if c.TokenSource.Type == "exec" {
|
|
||||||
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
|
||||||
errs = AppendError(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := c.TokenSource.Validate(); err != nil {
|
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth.oidc.tokenSource: %v", err))
|
|
||||||
}
|
|
||||||
return errs
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) {
|
if c.Transport.HeartbeatTimeout > 0 && c.Transport.HeartbeatInterval > 0 {
|
||||||
var (
|
if c.Transport.HeartbeatTimeout < c.Transport.HeartbeatInterval {
|
||||||
warnings Warning
|
|
||||||
errs error
|
|
||||||
)
|
|
||||||
|
|
||||||
if c.HeartbeatTimeout > 0 && c.HeartbeatInterval > 0 {
|
|
||||||
if c.HeartbeatTimeout < c.HeartbeatInterval {
|
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval"))
|
errs = AppendError(errs, fmt.Errorf("invalid transport.heartbeatTimeout, heartbeat timeout should not less than heartbeat interval"))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if !lo.FromPtr(c.TLS.Enable) {
|
if !lo.FromPtr(c.Transport.TLS.Enable) {
|
||||||
checkTLSConfig := func(name string, value string) Warning {
|
checkTLSConfig := func(name string, value string) Warning {
|
||||||
if value != "" {
|
if value != "" {
|
||||||
return fmt.Errorf("%s is invalid when transport.tls.enable is false", name)
|
return fmt.Errorf("%s is invalid when transport.tls.enable is false", name)
|
||||||
@@ -133,20 +79,16 @@ func validateTransportConfig(c *v1.ClientTransportConfig) (Warning, error) {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.TLS.CertFile))
|
warnings = AppendError(warnings, checkTLSConfig("transport.tls.certFile", c.Transport.TLS.CertFile))
|
||||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.TLS.KeyFile))
|
warnings = AppendError(warnings, checkTLSConfig("transport.tls.keyFile", c.Transport.TLS.KeyFile))
|
||||||
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.TLS.TrustedCaFile))
|
warnings = AppendError(warnings, checkTLSConfig("transport.tls.trustedCaFile", c.Transport.TLS.TrustedCaFile))
|
||||||
}
|
}
|
||||||
|
|
||||||
if !slices.Contains(SupportedTransportProtocols, c.Protocol) {
|
if !slices.Contains(SupportedTransportProtocols, c.Transport.Protocol) {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))
|
errs = AppendError(errs, fmt.Errorf("invalid transport.protocol, optional values are %v", SupportedTransportProtocols))
|
||||||
}
|
}
|
||||||
return warnings, errs
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateIncludeFiles(files []string) (Warning, error) {
|
for _, f := range c.IncludeConfigFiles {
|
||||||
var errs error
|
|
||||||
for _, f := range files {
|
|
||||||
absDir, err := filepath.Abs(filepath.Dir(f))
|
absDir, err := filepath.Abs(filepath.Dir(f))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err))
|
errs = AppendError(errs, fmt.Errorf("include: parse directory of %s failed: %v", f, err))
|
||||||
@@ -156,19 +98,13 @@ func validateIncludeFiles(files []string) (Warning, error) {
|
|||||||
errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f))
|
errs = AppendError(errs, fmt.Errorf("include: directory of %s not exist", f))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return nil, errs
|
return warnings, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func ValidateAllClientConfig(
|
func ValidateAllClientConfig(c *v1.ClientCommonConfig, proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) (Warning, error) {
|
||||||
c *v1.ClientCommonConfig,
|
|
||||||
proxyCfgs []v1.ProxyConfigurer,
|
|
||||||
visitorCfgs []v1.VisitorConfigurer,
|
|
||||||
unsafeFeatures *security.UnsafeFeatures,
|
|
||||||
) (Warning, error) {
|
|
||||||
validator := NewConfigValidator(unsafeFeatures)
|
|
||||||
var warnings Warning
|
var warnings Warning
|
||||||
if c != nil {
|
if c != nil {
|
||||||
warning, err := validator.ValidateClientCommonConfig(c)
|
warning, err := ValidateClientCommonConfig(c)
|
||||||
warnings = AppendError(warnings, warning)
|
warnings = AppendError(warnings, warning)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return warnings, err
|
return warnings, err
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ import (
|
|||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
||||||
var (
|
var (
|
||||||
warnings Warning
|
warnings Warning
|
||||||
errs error
|
errs error
|
||||||
@@ -43,11 +42,6 @@ func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, err
|
|||||||
|
|
||||||
// Validate tokenSource if specified
|
// Validate tokenSource if specified
|
||||||
if c.Auth.TokenSource != nil {
|
if c.Auth.TokenSource != nil {
|
||||||
if c.Auth.TokenSource.Type == "exec" {
|
|
||||||
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
|
||||||
errs = AppendError(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := c.Auth.TokenSource.Validate(); err != nil {
|
if err := c.Auth.TokenSource.Validate(); err != nil {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
package validation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConfigValidator holds the context dependencies for configuration validation.
|
|
||||||
type ConfigValidator struct {
|
|
||||||
unsafeFeatures *security.UnsafeFeatures
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConfigValidator creates a new ConfigValidator instance.
|
|
||||||
func NewConfigValidator(unsafeFeatures *security.UnsafeFeatures) *ConfigValidator {
|
|
||||||
return &ConfigValidator{
|
|
||||||
unsafeFeatures: unsafeFeatures,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateUnsafeFeature checks if a specific unsafe feature is enabled.
|
|
||||||
func (v *ConfigValidator) ValidateUnsafeFeature(feature string) error {
|
|
||||||
if !v.unsafeFeatures.IsEnabled(feature) {
|
|
||||||
return fmt.Errorf("unsafe feature %q is not enabled. "+
|
|
||||||
"To enable it, ensure it is allowed in the configuration or command line flags", feature)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -19,7 +19,6 @@ import (
|
|||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"os/exec"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -28,7 +27,6 @@ import (
|
|||||||
type ValueSource struct {
|
type ValueSource struct {
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
File *FileSource `json:"file,omitempty"`
|
File *FileSource `json:"file,omitempty"`
|
||||||
Exec *ExecSource `json:"exec,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// FileSource specifies how to load a value from a file.
|
// FileSource specifies how to load a value from a file.
|
||||||
@@ -36,18 +34,6 @@ type FileSource struct {
|
|||||||
Path string `json:"path"`
|
Path string `json:"path"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// ExecSource specifies how to get a value from another program launched as subprocess.
|
|
||||||
type ExecSource struct {
|
|
||||||
Command string `json:"command"`
|
|
||||||
Args []string `json:"args,omitempty"`
|
|
||||||
Env []ExecEnvVar `json:"env,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ExecEnvVar struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Value string `json:"value"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Validate validates the ValueSource configuration.
|
// Validate validates the ValueSource configuration.
|
||||||
func (v *ValueSource) Validate() error {
|
func (v *ValueSource) Validate() error {
|
||||||
if v == nil {
|
if v == nil {
|
||||||
@@ -60,13 +46,8 @@ func (v *ValueSource) Validate() error {
|
|||||||
return errors.New("file configuration is required when type is 'file'")
|
return errors.New("file configuration is required when type is 'file'")
|
||||||
}
|
}
|
||||||
return v.File.Validate()
|
return v.File.Validate()
|
||||||
case "exec":
|
|
||||||
if v.Exec == nil {
|
|
||||||
return errors.New("exec configuration is required when type is 'exec'")
|
|
||||||
}
|
|
||||||
return v.Exec.Validate()
|
|
||||||
default:
|
default:
|
||||||
return fmt.Errorf("unsupported value source type: %s (only 'file' and 'exec' are supported)", v.Type)
|
return fmt.Errorf("unsupported value source type: %s (only 'file' is supported)", v.Type)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -79,8 +60,6 @@ func (v *ValueSource) Resolve(ctx context.Context) (string, error) {
|
|||||||
switch v.Type {
|
switch v.Type {
|
||||||
case "file":
|
case "file":
|
||||||
return v.File.Resolve(ctx)
|
return v.File.Resolve(ctx)
|
||||||
case "exec":
|
|
||||||
return v.Exec.Resolve(ctx)
|
|
||||||
default:
|
default:
|
||||||
return "", fmt.Errorf("unsupported value source type: %s", v.Type)
|
return "", fmt.Errorf("unsupported value source type: %s", v.Type)
|
||||||
}
|
}
|
||||||
@@ -112,47 +91,3 @@ func (f *FileSource) Resolve(_ context.Context) (string, error) {
|
|||||||
// Trim whitespace, which is important for file-based tokens
|
// Trim whitespace, which is important for file-based tokens
|
||||||
return strings.TrimSpace(string(content)), nil
|
return strings.TrimSpace(string(content)), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Validate validates the ExecSource configuration.
|
|
||||||
func (e *ExecSource) Validate() error {
|
|
||||||
if e == nil {
|
|
||||||
return errors.New("execSource cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if e.Command == "" {
|
|
||||||
return errors.New("exec command cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, env := range e.Env {
|
|
||||||
if env.Name == "" {
|
|
||||||
return errors.New("exec env name cannot be empty")
|
|
||||||
}
|
|
||||||
if strings.Contains(env.Name, "=") {
|
|
||||||
return errors.New("exec env name cannot contain '='")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Resolve reads and returns the content captured from stdout of launched subprocess.
|
|
||||||
func (e *ExecSource) Resolve(ctx context.Context) (string, error) {
|
|
||||||
if err := e.Validate(); err != nil {
|
|
||||||
return "", err
|
|
||||||
}
|
|
||||||
|
|
||||||
cmd := exec.CommandContext(ctx, e.Command, e.Args...)
|
|
||||||
if len(e.Env) != 0 {
|
|
||||||
cmd.Env = os.Environ()
|
|
||||||
for _, env := range e.Env {
|
|
||||||
cmd.Env = append(cmd.Env, env.Name+"="+env.Value)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := cmd.Output()
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("failed to execute command %v: %v", e.Command, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Trim whitespace, which is important for exec-based tokens
|
|
||||||
return strings.TrimSpace(string(content)), nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -32,11 +32,8 @@ type VisitorTransport struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type VisitorBaseConfig struct {
|
type VisitorBaseConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
// Enabled controls whether this visitor is enabled. nil or true means enabled, false means disabled.
|
|
||||||
// This allows individual control over each visitor, complementing the global "start" field.
|
|
||||||
Enabled *bool `json:"enabled,omitempty"`
|
|
||||||
Transport VisitorTransport `json:"transport,omitempty"`
|
Transport VisitorTransport `json:"transport,omitempty"`
|
||||||
SecretKey string `json:"secretKey,omitempty"`
|
SecretKey string `json:"secretKey,omitempty"`
|
||||||
// if the server user is not set, it defaults to the current user
|
// if the server user is not set, it defaults to the current user
|
||||||
|
|||||||
@@ -56,9 +56,9 @@ func (m *serverMetrics) CloseClient() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) NewProxy(name string, proxyType string, user string, clientID string) {
|
func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
||||||
for _, v := range m.ms {
|
for _, v := range m.ms {
|
||||||
v.NewProxy(name, proxyType, user, clientID)
|
v.NewProxy(name, proxyType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ func (m *serverMetrics) CloseClient() {
|
|||||||
m.info.ClientCounts.Dec(1)
|
m.info.ClientCounts.Dec(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) NewProxy(name string, proxyType string, user string, clientID string) {
|
func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
counter, ok := m.info.ProxyTypeCounts[proxyType]
|
counter, ok := m.info.ProxyTypeCounts[proxyType]
|
||||||
@@ -119,8 +119,6 @@ func (m *serverMetrics) NewProxy(name string, proxyType string, user string, cli
|
|||||||
}
|
}
|
||||||
m.info.ProxyStatistics[name] = proxyStats
|
m.info.ProxyStatistics[name] = proxyStats
|
||||||
}
|
}
|
||||||
proxyStats.User = user
|
|
||||||
proxyStats.ClientID = clientID
|
|
||||||
proxyStats.LastStartTime = time.Now()
|
proxyStats.LastStartTime = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,8 +214,6 @@ func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
|||||||
ps := &ProxyStats{
|
ps := &ProxyStats{
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: proxyStats.ProxyType,
|
Type: proxyStats.ProxyType,
|
||||||
User: proxyStats.User,
|
|
||||||
ClientID: proxyStats.ClientID,
|
|
||||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
||||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
||||||
CurConns: int64(proxyStats.CurConns.Count()),
|
CurConns: int64(proxyStats.CurConns.Count()),
|
||||||
@@ -249,8 +245,6 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
|
|||||||
res = &ProxyStats{
|
res = &ProxyStats{
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: proxyStats.ProxyType,
|
Type: proxyStats.ProxyType,
|
||||||
User: proxyStats.User,
|
|
||||||
ClientID: proxyStats.ClientID,
|
|
||||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
||||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
||||||
CurConns: int64(proxyStats.CurConns.Count()),
|
CurConns: int64(proxyStats.CurConns.Count()),
|
||||||
@@ -266,31 +260,6 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) GetProxyByName(proxyName string) (res *ProxyStats) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
|
||||||
if ok {
|
|
||||||
res = &ProxyStats{
|
|
||||||
Name: proxyName,
|
|
||||||
Type: proxyStats.ProxyType,
|
|
||||||
User: proxyStats.User,
|
|
||||||
ClientID: proxyStats.ClientID,
|
|
||||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
|
||||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
|
||||||
CurConns: int64(proxyStats.CurConns.Count()),
|
|
||||||
}
|
|
||||||
if !proxyStats.LastStartTime.IsZero() {
|
|
||||||
res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
|
||||||
}
|
|
||||||
if !proxyStats.LastCloseTime.IsZero() {
|
|
||||||
res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serverMetrics) GetProxyTraffic(name string) (res *ProxyTrafficInfo) {
|
func (m *serverMetrics) GetProxyTraffic(name string) (res *ProxyTrafficInfo) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ type ServerStats struct {
|
|||||||
type ProxyStats struct {
|
type ProxyStats struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
User string
|
|
||||||
ClientID string
|
|
||||||
TodayTrafficIn int64
|
TodayTrafficIn int64
|
||||||
TodayTrafficOut int64
|
TodayTrafficOut int64
|
||||||
LastStartTime string
|
LastStartTime string
|
||||||
@@ -53,8 +51,6 @@ type ProxyTrafficInfo struct {
|
|||||||
type ProxyStatistics struct {
|
type ProxyStatistics struct {
|
||||||
Name string
|
Name string
|
||||||
ProxyType string
|
ProxyType string
|
||||||
User string
|
|
||||||
ClientID string
|
|
||||||
TrafficIn metric.DateCounter
|
TrafficIn metric.DateCounter
|
||||||
TrafficOut metric.DateCounter
|
TrafficOut metric.DateCounter
|
||||||
CurConns metric.Counter
|
CurConns metric.Counter
|
||||||
@@ -82,7 +78,6 @@ type Collector interface {
|
|||||||
GetServer() *ServerStats
|
GetServer() *ServerStats
|
||||||
GetProxiesByType(proxyType string) []*ProxyStats
|
GetProxiesByType(proxyType string) []*ProxyStats
|
||||||
GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats
|
GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats
|
||||||
GetProxyByName(proxyName string) *ProxyStats
|
|
||||||
GetProxyTraffic(name string) *ProxyTrafficInfo
|
GetProxyTraffic(name string) *ProxyTrafficInfo
|
||||||
ClearOfflineProxies() (int, int)
|
ClearOfflineProxies() (int, int)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func (m *serverMetrics) CloseClient() {
|
|||||||
m.clientCount.Dec()
|
m.clientCount.Dec()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) NewProxy(name string, proxyType string, _ string, _ string) {
|
func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
||||||
m.proxyCount.WithLabelValues(proxyType).Inc()
|
m.proxyCount.WithLabelValues(proxyType).Inc()
|
||||||
m.proxyCountDetailed.WithLabelValues(proxyType, name).Inc()
|
m.proxyCountDetailed.WithLabelValues(proxyType, name).Inc()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -86,6 +86,10 @@ func (d *Dispatcher) Send(m Message) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (d *Dispatcher) SendChannel() chan Message {
|
||||||
|
return d.sendCh
|
||||||
|
}
|
||||||
|
|
||||||
func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) {
|
func (d *Dispatcher) RegisterHandler(msg Message, handler func(Message)) {
|
||||||
d.msgHandlers[reflect.TypeOf(msg)] = handler
|
d.msgHandlers[reflect.TypeOf(msg)] = handler
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ type Login struct {
|
|||||||
PrivilegeKey string `json:"privilege_key,omitempty"`
|
PrivilegeKey string `json:"privilege_key,omitempty"`
|
||||||
Timestamp int64 `json:"timestamp,omitempty"`
|
Timestamp int64 `json:"timestamp,omitempty"`
|
||||||
RunID string `json:"run_id,omitempty"`
|
RunID string `json:"run_id,omitempty"`
|
||||||
ClientID string `json:"client_id,omitempty"`
|
|
||||||
Metas map[string]string `json:"metas,omitempty"`
|
Metas map[string]string `json:"metas,omitempty"`
|
||||||
|
|
||||||
// Currently only effective for VirtualClient.
|
// Currently only effective for VirtualClient.
|
||||||
|
|||||||
@@ -23,20 +23,11 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/vnet"
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
|
|
||||||
// PluginContext provides the necessary context and callbacks for visitor plugins.
|
|
||||||
type PluginContext struct {
|
type PluginContext struct {
|
||||||
// Name is the unique identifier for this visitor, used for logging and routing.
|
Name string
|
||||||
Name string
|
Ctx context.Context
|
||||||
|
|
||||||
// Ctx manages the plugin's lifecycle and carries the logger for structured logging.
|
|
||||||
Ctx context.Context
|
|
||||||
|
|
||||||
// VnetController manages TUN device routing. May be nil if virtual networking is disabled.
|
|
||||||
VnetController *vnet.Controller
|
VnetController *vnet.Controller
|
||||||
|
HandleConn func(net.Conn)
|
||||||
// SendConnToVisitor sends a connection to the visitor's internal processing queue.
|
|
||||||
// Does not return error; failures are handled by closing the connection.
|
|
||||||
SendConnToVisitor func(net.Conn)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// Creators is used for create plugins to handle connections.
|
// Creators is used for create plugins to handle connections.
|
||||||
|
|||||||
@@ -42,8 +42,6 @@ type VirtualNetPlugin struct {
|
|||||||
controllerConn net.Conn
|
controllerConn net.Conn
|
||||||
closeSignal chan struct{}
|
closeSignal chan struct{}
|
||||||
|
|
||||||
consecutiveErrors int // Tracks consecutive connection errors for exponential backoff
|
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
cancel context.CancelFunc
|
cancel context.CancelFunc
|
||||||
}
|
}
|
||||||
@@ -100,6 +98,7 @@ func (p *VirtualNetPlugin) Start() {
|
|||||||
|
|
||||||
func (p *VirtualNetPlugin) run() {
|
func (p *VirtualNetPlugin) run() {
|
||||||
xl := xlog.FromContextSafe(p.ctx)
|
xl := xlog.FromContextSafe(p.ctx)
|
||||||
|
reconnectDelay := 10 * time.Second
|
||||||
|
|
||||||
for {
|
for {
|
||||||
currentCloseSignal := make(chan struct{})
|
currentCloseSignal := make(chan struct{})
|
||||||
@@ -122,10 +121,7 @@ func (p *VirtualNetPlugin) run() {
|
|||||||
p.controllerConn = controllerConn
|
p.controllerConn = controllerConn
|
||||||
p.mu.Unlock()
|
p.mu.Unlock()
|
||||||
|
|
||||||
// Wrap with CloseNotifyConn which supports both close notification and error recording
|
pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func() {
|
||||||
var closeErr error
|
|
||||||
pluginNotifyConn := netutil.WrapCloseNotifyConn(pluginConn, func(err error) {
|
|
||||||
closeErr = err
|
|
||||||
close(currentCloseSignal) // Signal the run loop on close.
|
close(currentCloseSignal) // Signal the run loop on close.
|
||||||
})
|
})
|
||||||
|
|
||||||
@@ -133,9 +129,9 @@ func (p *VirtualNetPlugin) run() {
|
|||||||
p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn)
|
p.pluginCtx.VnetController.RegisterClientRoute(p.ctx, p.pluginCtx.Name, p.routes, controllerConn)
|
||||||
xl.Infof("successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name)
|
xl.Infof("successfully registered client route for visitor [%s]. Starting connection handler with CloseNotifyConn.", p.pluginCtx.Name)
|
||||||
|
|
||||||
// Pass the CloseNotifyConn to the visitor for handling.
|
// Pass the CloseNotifyConn to HandleConn.
|
||||||
// The visitor can call CloseWithError to record the failure reason.
|
// HandleConn is responsible for calling Close() on pluginNotifyConn.
|
||||||
p.pluginCtx.SendConnToVisitor(pluginNotifyConn)
|
p.pluginCtx.HandleConn(pluginNotifyConn)
|
||||||
|
|
||||||
// Wait for context cancellation or connection close.
|
// Wait for context cancellation or connection close.
|
||||||
select {
|
select {
|
||||||
@@ -144,32 +140,8 @@ func (p *VirtualNetPlugin) run() {
|
|||||||
p.cleanupControllerConn(xl)
|
p.cleanupControllerConn(xl)
|
||||||
return
|
return
|
||||||
case <-currentCloseSignal:
|
case <-currentCloseSignal:
|
||||||
// Determine reconnect delay based on error with exponential backoff
|
xl.Infof("detected connection closed via CloseNotifyConn for visitor [%s].", p.pluginCtx.Name)
|
||||||
var reconnectDelay time.Duration
|
// HandleConn closed the plugin side. Close the controller side.
|
||||||
if closeErr != nil {
|
|
||||||
p.consecutiveErrors++
|
|
||||||
xl.Warnf("connection closed with error for visitor [%s] (consecutive errors: %d): %v",
|
|
||||||
p.pluginCtx.Name, p.consecutiveErrors, closeErr)
|
|
||||||
|
|
||||||
// Exponential backoff: 60s, 120s, 240s, 300s (capped)
|
|
||||||
baseDelay := 60 * time.Second
|
|
||||||
reconnectDelay = baseDelay * time.Duration(1<<uint(p.consecutiveErrors-1))
|
|
||||||
if reconnectDelay > 300*time.Second {
|
|
||||||
reconnectDelay = 300 * time.Second
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
// Reset consecutive errors on successful connection
|
|
||||||
if p.consecutiveErrors > 0 {
|
|
||||||
xl.Infof("connection closed normally for visitor [%s], resetting error counter (was %d)",
|
|
||||||
p.pluginCtx.Name, p.consecutiveErrors)
|
|
||||||
p.consecutiveErrors = 0
|
|
||||||
} else {
|
|
||||||
xl.Infof("connection closed normally for visitor [%s]", p.pluginCtx.Name)
|
|
||||||
}
|
|
||||||
reconnectDelay = 10 * time.Second
|
|
||||||
}
|
|
||||||
|
|
||||||
// The visitor closed the plugin side. Close the controller side.
|
|
||||||
p.cleanupControllerConn(xl)
|
p.cleanupControllerConn(xl)
|
||||||
|
|
||||||
xl.Infof("waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name)
|
xl.Infof("waiting %v before attempting reconnection for visitor [%s]...", reconnectDelay, p.pluginCtx.Name)
|
||||||
@@ -212,7 +184,7 @@ func (p *VirtualNetPlugin) Close() error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Explicitly close the controller side of the pipe.
|
// Explicitly close the controller side of the pipe.
|
||||||
// This ensures the pipe is broken even if the run loop is stuck or the visitor hasn't closed its end.
|
// This ensures the pipe is broken even if the run loop is stuck or HandleConn hasn't closed its end.
|
||||||
p.cleanupControllerConn(xl)
|
p.cleanupControllerConn(xl)
|
||||||
xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name)
|
xl.Infof("finished cleaning up connections during close for visitor [%s]", p.pluginCtx.Name)
|
||||||
|
|
||||||
|
|||||||
@@ -1,34 +0,0 @@
|
|||||||
package security
|
|
||||||
|
|
||||||
const (
|
|
||||||
TokenSourceExec = "TokenSourceExec"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ClientUnsafeFeatures = []string{
|
|
||||||
TokenSourceExec,
|
|
||||||
}
|
|
||||||
|
|
||||||
ServerUnsafeFeatures = []string{
|
|
||||||
TokenSourceExec,
|
|
||||||
}
|
|
||||||
)
|
|
||||||
|
|
||||||
type UnsafeFeatures struct {
|
|
||||||
features map[string]bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewUnsafeFeatures(allowed []string) *UnsafeFeatures {
|
|
||||||
features := make(map[string]bool)
|
|
||||||
for _, f := range allowed {
|
|
||||||
features[f] = true
|
|
||||||
}
|
|
||||||
return &UnsafeFeatures{features: features}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (u *UnsafeFeatures) IsEnabled(feature string) bool {
|
|
||||||
if u == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
return u.features[feature]
|
|
||||||
}
|
|
||||||
@@ -124,8 +124,8 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
|
|||||||
}
|
}
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
||||||
// Add proxy protocol header if configured (only for the first packet of a new connection)
|
// Add proxy protocol header if configured
|
||||||
if !ok && proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil {
|
if proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil {
|
||||||
ppBuf, err := netpkg.BuildProxyProtocolHeader(udpMsg.RemoteAddr, dstAddr, proxyProtocolVersion)
|
ppBuf, err := netpkg.BuildProxyProtocolHeader(udpMsg.RemoteAddr, dstAddr, proxyProtocolVersion)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Prepend proxy protocol header to the UDP payload
|
// Prepend proxy protocol header to the UDP payload
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/api"
|
"github.com/fatedier/frp/client"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ func (c *Client) SetAuth(user, pwd string) {
|
|||||||
c.authPwd = pwd
|
c.authPwd = pwd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxyStatusResp, error) {
|
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.ProxyStatusResp, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -41,7 +41,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxySta
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allStatus := make(api.StatusResp)
|
allStatus := make(client.StatusResp)
|
||||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxySta
|
|||||||
return nil, fmt.Errorf("no proxy status found")
|
return nil, fmt.Errorf("no proxy status found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error) {
|
func (c *Client) GetAllProxyStatus(ctx context.Context) (client.StatusResp, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -64,7 +64,7 @@ func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allStatus := make(api.StatusResp)
|
allStatus := make(client.StatusResp)
|
||||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -35,19 +35,15 @@ type MessageTransporter interface {
|
|||||||
DispatchWithType(m msg.Message, msgType, laneKey string) bool
|
DispatchWithType(m msg.Message, msgType, laneKey string) bool
|
||||||
}
|
}
|
||||||
|
|
||||||
type MessageSender interface {
|
func NewMessageTransporter(sendCh chan msg.Message) MessageTransporter {
|
||||||
Send(msg.Message) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewMessageTransporter(sender MessageSender) MessageTransporter {
|
|
||||||
return &transporterImpl{
|
return &transporterImpl{
|
||||||
sender: sender,
|
sendCh: sendCh,
|
||||||
registry: make(map[string]map[string]chan msg.Message),
|
registry: make(map[string]map[string]chan msg.Message),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type transporterImpl struct {
|
type transporterImpl struct {
|
||||||
sender MessageSender
|
sendCh chan msg.Message
|
||||||
|
|
||||||
// First key is message type and second key is lane key.
|
// First key is message type and second key is lane key.
|
||||||
// Dispatch will dispatch message to related channel by its message type
|
// Dispatch will dispatch message to related channel by its message type
|
||||||
@@ -57,7 +53,9 @@ type transporterImpl struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (impl *transporterImpl) Send(m msg.Message) error {
|
func (impl *transporterImpl) Send(m msg.Message) error {
|
||||||
return impl.sender.Send(m)
|
return errors.PanicToError(func() {
|
||||||
|
impl.sendCh <- m
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func (impl *transporterImpl) Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error) {
|
func (impl *transporterImpl) Do(ctx context.Context, req msg.Message, laneKey, recvMsgType string) (msg.Message, error) {
|
||||||
|
|||||||
@@ -1,57 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Context struct {
|
|
||||||
Req *http.Request
|
|
||||||
Resp http.ResponseWriter
|
|
||||||
vars map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewContext(w http.ResponseWriter, r *http.Request) *Context {
|
|
||||||
return &Context{
|
|
||||||
Req: r,
|
|
||||||
Resp: w,
|
|
||||||
vars: mux.Vars(r),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) Param(key string) string {
|
|
||||||
return c.vars[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) Query(key string) string {
|
|
||||||
return c.Req.URL.Query().Get(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) BindJSON(obj any) error {
|
|
||||||
body, err := io.ReadAll(c.Req.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return json.Unmarshal(body, obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) Body() ([]byte, error) {
|
|
||||||
return io.ReadAll(c.Req.Body)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package http
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
type Error struct {
|
|
||||||
Code int
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Error) Error() string {
|
|
||||||
return e.Err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewError(code int, msg string) *Error {
|
|
||||||
return &Error{
|
|
||||||
Code: code,
|
|
||||||
Err: fmt.Errorf("%s", msg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GeneralResponse struct {
|
|
||||||
Code int
|
|
||||||
Msg string
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIHandler is a handler function that returns a response object or an error.
|
|
||||||
type APIHandler func(ctx *Context) (any, error)
|
|
||||||
|
|
||||||
// MakeHTTPHandlerFunc turns a normal APIHandler into a http.HandlerFunc.
|
|
||||||
func MakeHTTPHandlerFunc(handler APIHandler) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := NewContext(w, r)
|
|
||||||
res, err := handler(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("http response [%s]: error: %v", r.URL.Path, err)
|
|
||||||
code := http.StatusInternalServerError
|
|
||||||
if e, ok := err.(*Error); ok {
|
|
||||||
code = e.Code
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(code)
|
|
||||||
_ = json.NewEncoder(w).Encode(GeneralResponse{Code: code, Msg: err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if res == nil {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := res.(type) {
|
|
||||||
case []byte:
|
|
||||||
_, _ = w.Write(v)
|
|
||||||
case string:
|
|
||||||
_, _ = w.Write([]byte(v))
|
|
||||||
default:
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_ = json.NewEncoder(w).Encode(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type responseWriter struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
code int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rw *responseWriter) WriteHeader(code int) {
|
|
||||||
rw.code = code
|
|
||||||
rw.ResponseWriter.WriteHeader(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRequestLogger(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Infof("http request: [%s]", r.URL.Path)
|
|
||||||
rw := &responseWriter{ResponseWriter: w, code: http.StatusOK}
|
|
||||||
next.ServeHTTP(rw, r)
|
|
||||||
log.Infof("http response [%s]: code [%d]", r.URL.Path, rw.code)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -135,11 +135,11 @@ type CloseNotifyConn struct {
|
|||||||
// 1 means closed
|
// 1 means closed
|
||||||
closeFlag int32
|
closeFlag int32
|
||||||
|
|
||||||
closeFn func(error)
|
closeFn func()
|
||||||
}
|
}
|
||||||
|
|
||||||
// closeFn will be only called once with the error (nil if Close() was called, non-nil if CloseWithError() was called)
|
// closeFn will be only called once
|
||||||
func WrapCloseNotifyConn(c net.Conn, closeFn func(error)) *CloseNotifyConn {
|
func WrapCloseNotifyConn(c net.Conn, closeFn func()) net.Conn {
|
||||||
return &CloseNotifyConn{
|
return &CloseNotifyConn{
|
||||||
Conn: c,
|
Conn: c,
|
||||||
closeFn: closeFn,
|
closeFn: closeFn,
|
||||||
@@ -151,25 +151,12 @@ func (cc *CloseNotifyConn) Close() (err error) {
|
|||||||
if pflag == 0 {
|
if pflag == 0 {
|
||||||
err = cc.Conn.Close()
|
err = cc.Conn.Close()
|
||||||
if cc.closeFn != nil {
|
if cc.closeFn != nil {
|
||||||
cc.closeFn(nil)
|
cc.closeFn()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloseWithError closes the connection and passes the error to the close callback.
|
|
||||||
func (cc *CloseNotifyConn) CloseWithError(err error) error {
|
|
||||||
pflag := atomic.SwapInt32(&cc.closeFlag, 1)
|
|
||||||
if pflag == 0 {
|
|
||||||
closeErr := cc.Conn.Close()
|
|
||||||
if cc.closeFn != nil {
|
|
||||||
cc.closeFn(err)
|
|
||||||
}
|
|
||||||
return closeErr
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
type StatsConn struct {
|
type StatsConn struct {
|
||||||
net.Conn
|
net.Conn
|
||||||
|
|
||||||
|
|||||||
@@ -32,7 +32,7 @@ func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
|
|||||||
muxer := http.NewServeMux()
|
muxer := http.NewServeMux()
|
||||||
muxer.Handle(FrpWebsocketPath, websocket.Handler(func(c *websocket.Conn) {
|
muxer.Handle(FrpWebsocketPath, websocket.Handler(func(c *websocket.Conn) {
|
||||||
notifyCh := make(chan struct{})
|
notifyCh := make(chan struct{})
|
||||||
conn := WrapCloseNotifyConn(c, func(_ error) {
|
conn := WrapCloseNotifyConn(c, func() {
|
||||||
close(notifyCh)
|
close(notifyCh)
|
||||||
})
|
})
|
||||||
wl.acceptCh <- conn
|
wl.acceptCh <- conn
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package version
|
package version
|
||||||
|
|
||||||
var version = "0.67.0"
|
var version = "0.65.0"
|
||||||
|
|
||||||
func Full() string {
|
func Full() string {
|
||||||
return version
|
return version
|
||||||
|
|||||||
@@ -1,424 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/types"
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
"github.com/fatedier/frp/pkg/metrics/mem"
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
|
||||||
"github.com/fatedier/frp/server/proxy"
|
|
||||||
"github.com/fatedier/frp/server/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Controller struct {
|
|
||||||
// dependencies
|
|
||||||
serverCfg *v1.ServerConfig
|
|
||||||
clientRegistry *registry.ClientRegistry
|
|
||||||
pxyManager ProxyManager
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProxyManager interface {
|
|
||||||
GetByName(name string) (proxy.Proxy, bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewController(
|
|
||||||
serverCfg *v1.ServerConfig,
|
|
||||||
clientRegistry *registry.ClientRegistry,
|
|
||||||
pxyManager ProxyManager,
|
|
||||||
) *Controller {
|
|
||||||
return &Controller{
|
|
||||||
serverCfg: serverCfg,
|
|
||||||
clientRegistry: clientRegistry,
|
|
||||||
pxyManager: pxyManager,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/serverinfo
|
|
||||||
func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) {
|
|
||||||
serverStats := mem.StatsCollector.GetServer()
|
|
||||||
svrResp := ServerInfoResp{
|
|
||||||
Version: version.Full(),
|
|
||||||
BindPort: c.serverCfg.BindPort,
|
|
||||||
VhostHTTPPort: c.serverCfg.VhostHTTPPort,
|
|
||||||
VhostHTTPSPort: c.serverCfg.VhostHTTPSPort,
|
|
||||||
TCPMuxHTTPConnectPort: c.serverCfg.TCPMuxHTTPConnectPort,
|
|
||||||
KCPBindPort: c.serverCfg.KCPBindPort,
|
|
||||||
QUICBindPort: c.serverCfg.QUICBindPort,
|
|
||||||
SubdomainHost: c.serverCfg.SubDomainHost,
|
|
||||||
MaxPoolCount: c.serverCfg.Transport.MaxPoolCount,
|
|
||||||
MaxPortsPerClient: c.serverCfg.MaxPortsPerClient,
|
|
||||||
HeartBeatTimeout: c.serverCfg.Transport.HeartbeatTimeout,
|
|
||||||
AllowPortsStr: types.PortsRangeSlice(c.serverCfg.AllowPorts).String(),
|
|
||||||
TLSForce: c.serverCfg.Transport.TLS.Force,
|
|
||||||
|
|
||||||
TotalTrafficIn: serverStats.TotalTrafficIn,
|
|
||||||
TotalTrafficOut: serverStats.TotalTrafficOut,
|
|
||||||
CurConns: serverStats.CurConns,
|
|
||||||
ClientCounts: serverStats.ClientCounts,
|
|
||||||
ProxyTypeCounts: serverStats.ProxyTypeCounts,
|
|
||||||
}
|
|
||||||
// For API that returns struct, we can just return it.
|
|
||||||
// But current GeneralResponse.Msg in legacy code expects a JSON string.
|
|
||||||
// Since MakeHTTPHandlerFunc handles struct by encoding to JSON, we can return svrResp directly?
|
|
||||||
// The original code wraps it in GeneralResponse{Msg: string(json)}.
|
|
||||||
// If we return svrResp, the response body will be the JSON of svrResp.
|
|
||||||
// We should check if the frontend expects { "code": 200, "msg": "{...}" } or just {...}.
|
|
||||||
// Looking at previous code:
|
|
||||||
// res := GeneralResponse{Code: 200}
|
|
||||||
// buf, _ := json.Marshal(&svrResp)
|
|
||||||
// res.Msg = string(buf)
|
|
||||||
// Response body: {"code": 200, "msg": "{\"version\":...}"}
|
|
||||||
// Wait, is it double encoded JSON? Yes it seems so!
|
|
||||||
// Let's check dashboard_api.go original code again.
|
|
||||||
// Yes: res.Msg = string(buf).
|
|
||||||
// So the frontend expects { "code": 200, "msg": "JSON_STRING" }.
|
|
||||||
// This is kind of ugly, but we must preserve compatibility.
|
|
||||||
|
|
||||||
return svrResp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/clients
|
|
||||||
func (c *Controller) APIClientList(ctx *httppkg.Context) (any, error) {
|
|
||||||
if c.clientRegistry == nil {
|
|
||||||
return nil, fmt.Errorf("client registry unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
userFilter := ctx.Query("user")
|
|
||||||
clientIDFilter := ctx.Query("clientId")
|
|
||||||
runIDFilter := ctx.Query("runId")
|
|
||||||
statusFilter := strings.ToLower(ctx.Query("status"))
|
|
||||||
|
|
||||||
records := c.clientRegistry.List()
|
|
||||||
items := make([]ClientInfoResp, 0, len(records))
|
|
||||||
for _, info := range records {
|
|
||||||
if userFilter != "" && info.User != userFilter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if clientIDFilter != "" && info.ClientID() != clientIDFilter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if runIDFilter != "" && info.RunID != runIDFilter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !matchStatusFilter(info.Online, statusFilter) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
items = append(items, buildClientInfoResp(info))
|
|
||||||
}
|
|
||||||
|
|
||||||
slices.SortFunc(items, func(a, b ClientInfoResp) int {
|
|
||||||
if v := cmp.Compare(a.User, b.User); v != 0 {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
if v := cmp.Compare(a.ClientID, b.ClientID); v != 0 {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return cmp.Compare(a.Key, b.Key)
|
|
||||||
})
|
|
||||||
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/clients/{key}
|
|
||||||
func (c *Controller) APIClientDetail(ctx *httppkg.Context) (any, error) {
|
|
||||||
key := ctx.Param("key")
|
|
||||||
if key == "" {
|
|
||||||
return nil, fmt.Errorf("missing client key")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.clientRegistry == nil {
|
|
||||||
return nil, fmt.Errorf("client registry unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
info, ok := c.clientRegistry.GetByKey(key)
|
|
||||||
if !ok {
|
|
||||||
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("client %s not found", key))
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildClientInfoResp(info), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/proxy/:type
|
|
||||||
func (c *Controller) APIProxyByType(ctx *httppkg.Context) (any, error) {
|
|
||||||
proxyType := ctx.Param("type")
|
|
||||||
|
|
||||||
proxyInfoResp := GetProxyInfoResp{}
|
|
||||||
proxyInfoResp.Proxies = c.getProxyStatsByType(proxyType)
|
|
||||||
slices.SortFunc(proxyInfoResp.Proxies, func(a, b *ProxyStatsInfo) int {
|
|
||||||
return cmp.Compare(a.Name, b.Name)
|
|
||||||
})
|
|
||||||
|
|
||||||
return proxyInfoResp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/proxy/:type/:name
|
|
||||||
func (c *Controller) APIProxyByTypeAndName(ctx *httppkg.Context) (any, error) {
|
|
||||||
proxyType := ctx.Param("type")
|
|
||||||
name := ctx.Param("name")
|
|
||||||
|
|
||||||
proxyStatsResp, code, msg := c.getProxyStatsByTypeAndName(proxyType, name)
|
|
||||||
if code != 200 {
|
|
||||||
return nil, httppkg.NewError(code, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxyStatsResp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/traffic/:name
|
|
||||||
func (c *Controller) APIProxyTraffic(ctx *httppkg.Context) (any, error) {
|
|
||||||
name := ctx.Param("name")
|
|
||||||
|
|
||||||
trafficResp := GetProxyTrafficResp{}
|
|
||||||
trafficResp.Name = name
|
|
||||||
proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name)
|
|
||||||
|
|
||||||
if proxyTrafficInfo == nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found")
|
|
||||||
}
|
|
||||||
trafficResp.TrafficIn = proxyTrafficInfo.TrafficIn
|
|
||||||
trafficResp.TrafficOut = proxyTrafficInfo.TrafficOut
|
|
||||||
|
|
||||||
return trafficResp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/proxies/:name
|
|
||||||
func (c *Controller) APIProxyByName(ctx *httppkg.Context) (any, error) {
|
|
||||||
name := ctx.Param("name")
|
|
||||||
|
|
||||||
ps := mem.StatsCollector.GetProxyByName(name)
|
|
||||||
if ps == nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found")
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyInfo := GetProxyStatsResp{
|
|
||||||
Name: ps.Name,
|
|
||||||
User: ps.User,
|
|
||||||
ClientID: ps.ClientID,
|
|
||||||
TodayTrafficIn: ps.TodayTrafficIn,
|
|
||||||
TodayTrafficOut: ps.TodayTrafficOut,
|
|
||||||
CurConns: ps.CurConns,
|
|
||||||
LastStartTime: ps.LastStartTime,
|
|
||||||
LastCloseTime: ps.LastCloseTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
if pxy, ok := c.pxyManager.GetByName(name); ok {
|
|
||||||
content, err := json.Marshal(pxy.GetConfigurer())
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("marshal proxy [%s] conf info error: %v", name, err)
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "parse conf error")
|
|
||||||
}
|
|
||||||
proxyInfo.Conf = getConfByType(ps.Type)
|
|
||||||
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
|
||||||
log.Warnf("unmarshal proxy [%s] conf info error: %v", name, err)
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "parse conf error")
|
|
||||||
}
|
|
||||||
proxyInfo.Status = "online"
|
|
||||||
c.fillProxyClientInfo(&proxyClientInfo{
|
|
||||||
clientVersion: &proxyInfo.ClientVersion,
|
|
||||||
}, pxy)
|
|
||||||
} else {
|
|
||||||
proxyInfo.Status = "offline"
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxyInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE /api/proxies?status=offline
|
|
||||||
func (c *Controller) DeleteProxies(ctx *httppkg.Context) (any, error) {
|
|
||||||
status := ctx.Query("status")
|
|
||||||
if status != "offline" {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "status only support offline")
|
|
||||||
}
|
|
||||||
cleared, total := mem.StatsCollector.ClearOfflineProxies()
|
|
||||||
log.Infof("cleared [%d] offline proxies, total [%d] proxies", cleared, total)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) {
|
|
||||||
proxyStats := mem.StatsCollector.GetProxiesByType(proxyType)
|
|
||||||
proxyInfos = make([]*ProxyStatsInfo, 0, len(proxyStats))
|
|
||||||
for _, ps := range proxyStats {
|
|
||||||
proxyInfo := &ProxyStatsInfo{
|
|
||||||
User: ps.User,
|
|
||||||
ClientID: ps.ClientID,
|
|
||||||
}
|
|
||||||
if pxy, ok := c.pxyManager.GetByName(ps.Name); ok {
|
|
||||||
content, err := json.Marshal(pxy.GetConfigurer())
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
proxyInfo.Conf = getConfByType(ps.Type)
|
|
||||||
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
|
||||||
log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
proxyInfo.Status = "online"
|
|
||||||
c.fillProxyClientInfo(&proxyClientInfo{
|
|
||||||
clientVersion: &proxyInfo.ClientVersion,
|
|
||||||
}, pxy)
|
|
||||||
} else {
|
|
||||||
proxyInfo.Status = "offline"
|
|
||||||
}
|
|
||||||
proxyInfo.Name = ps.Name
|
|
||||||
proxyInfo.TodayTrafficIn = ps.TodayTrafficIn
|
|
||||||
proxyInfo.TodayTrafficOut = ps.TodayTrafficOut
|
|
||||||
proxyInfo.CurConns = ps.CurConns
|
|
||||||
proxyInfo.LastStartTime = ps.LastStartTime
|
|
||||||
proxyInfo.LastCloseTime = ps.LastCloseTime
|
|
||||||
proxyInfos = append(proxyInfos, proxyInfo)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp, code int, msg string) {
|
|
||||||
proxyInfo.Name = proxyName
|
|
||||||
ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName)
|
|
||||||
if ps == nil {
|
|
||||||
code = 404
|
|
||||||
msg = "no proxy info found"
|
|
||||||
} else {
|
|
||||||
proxyInfo.User = ps.User
|
|
||||||
proxyInfo.ClientID = ps.ClientID
|
|
||||||
if pxy, ok := c.pxyManager.GetByName(proxyName); ok {
|
|
||||||
content, err := json.Marshal(pxy.GetConfigurer())
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
||||||
code = 400
|
|
||||||
msg = "parse conf error"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
proxyInfo.Conf = getConfByType(ps.Type)
|
|
||||||
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
|
||||||
log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
||||||
code = 400
|
|
||||||
msg = "parse conf error"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
proxyInfo.Status = "online"
|
|
||||||
} else {
|
|
||||||
proxyInfo.Status = "offline"
|
|
||||||
}
|
|
||||||
proxyInfo.TodayTrafficIn = ps.TodayTrafficIn
|
|
||||||
proxyInfo.TodayTrafficOut = ps.TodayTrafficOut
|
|
||||||
proxyInfo.CurConns = ps.CurConns
|
|
||||||
proxyInfo.LastStartTime = ps.LastStartTime
|
|
||||||
proxyInfo.LastCloseTime = ps.LastCloseTime
|
|
||||||
code = 200
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildClientInfoResp(info registry.ClientInfo) ClientInfoResp {
|
|
||||||
resp := ClientInfoResp{
|
|
||||||
Key: info.Key,
|
|
||||||
User: info.User,
|
|
||||||
ClientID: info.ClientID(),
|
|
||||||
RunID: info.RunID,
|
|
||||||
Hostname: info.Hostname,
|
|
||||||
ClientIP: info.IP,
|
|
||||||
FirstConnectedAt: toUnix(info.FirstConnectedAt),
|
|
||||||
LastConnectedAt: toUnix(info.LastConnectedAt),
|
|
||||||
Online: info.Online,
|
|
||||||
}
|
|
||||||
if !info.DisconnectedAt.IsZero() {
|
|
||||||
resp.DisconnectedAt = info.DisconnectedAt.Unix()
|
|
||||||
}
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
type proxyClientInfo struct {
|
|
||||||
user *string
|
|
||||||
clientID *string
|
|
||||||
clientVersion *string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) fillProxyClientInfo(proxyInfo *proxyClientInfo, pxy proxy.Proxy) {
|
|
||||||
loginMsg := pxy.GetLoginMsg()
|
|
||||||
if loginMsg == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if proxyInfo.user != nil {
|
|
||||||
*proxyInfo.user = loginMsg.User
|
|
||||||
}
|
|
||||||
if proxyInfo.clientVersion != nil {
|
|
||||||
*proxyInfo.clientVersion = loginMsg.Version
|
|
||||||
}
|
|
||||||
if info, ok := c.clientRegistry.GetByRunID(loginMsg.RunID); ok {
|
|
||||||
if proxyInfo.clientID != nil {
|
|
||||||
*proxyInfo.clientID = info.ClientID()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if proxyInfo.clientID != nil {
|
|
||||||
*proxyInfo.clientID = loginMsg.ClientID
|
|
||||||
if *proxyInfo.clientID == "" {
|
|
||||||
*proxyInfo.clientID = loginMsg.RunID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toUnix(t time.Time) int64 {
|
|
||||||
if t.IsZero() {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return t.Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchStatusFilter(online bool, filter string) bool {
|
|
||||||
switch strings.ToLower(filter) {
|
|
||||||
case "", "all":
|
|
||||||
return true
|
|
||||||
case "online":
|
|
||||||
return online
|
|
||||||
case "offline":
|
|
||||||
return !online
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getConfByType(proxyType string) any {
|
|
||||||
switch v1.ProxyType(proxyType) {
|
|
||||||
case v1.ProxyTypeTCP:
|
|
||||||
return &TCPOutConf{}
|
|
||||||
case v1.ProxyTypeTCPMUX:
|
|
||||||
return &TCPMuxOutConf{}
|
|
||||||
case v1.ProxyTypeUDP:
|
|
||||||
return &UDPOutConf{}
|
|
||||||
case v1.ProxyTypeHTTP:
|
|
||||||
return &HTTPOutConf{}
|
|
||||||
case v1.ProxyTypeHTTPS:
|
|
||||||
return &HTTPSOutConf{}
|
|
||||||
case v1.ProxyTypeSTCP:
|
|
||||||
return &STCPOutConf{}
|
|
||||||
case v1.ProxyTypeXTCP:
|
|
||||||
return &XTCPOutConf{}
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ServerInfoResp struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
BindPort int `json:"bindPort"`
|
|
||||||
VhostHTTPPort int `json:"vhostHTTPPort"`
|
|
||||||
VhostHTTPSPort int `json:"vhostHTTPSPort"`
|
|
||||||
TCPMuxHTTPConnectPort int `json:"tcpmuxHTTPConnectPort"`
|
|
||||||
KCPBindPort int `json:"kcpBindPort"`
|
|
||||||
QUICBindPort int `json:"quicBindPort"`
|
|
||||||
SubdomainHost string `json:"subdomainHost"`
|
|
||||||
MaxPoolCount int64 `json:"maxPoolCount"`
|
|
||||||
MaxPortsPerClient int64 `json:"maxPortsPerClient"`
|
|
||||||
HeartBeatTimeout int64 `json:"heartbeatTimeout"`
|
|
||||||
AllowPortsStr string `json:"allowPortsStr,omitempty"`
|
|
||||||
TLSForce bool `json:"tlsForce,omitempty"`
|
|
||||||
|
|
||||||
TotalTrafficIn int64 `json:"totalTrafficIn"`
|
|
||||||
TotalTrafficOut int64 `json:"totalTrafficOut"`
|
|
||||||
CurConns int64 `json:"curConns"`
|
|
||||||
ClientCounts int64 `json:"clientCounts"`
|
|
||||||
ProxyTypeCounts map[string]int64 `json:"proxyTypeCount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientInfoResp struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
User string `json:"user"`
|
|
||||||
ClientID string `json:"clientID"`
|
|
||||||
RunID string `json:"runID"`
|
|
||||||
Hostname string `json:"hostname"`
|
|
||||||
ClientIP string `json:"clientIP,omitempty"`
|
|
||||||
FirstConnectedAt int64 `json:"firstConnectedAt"`
|
|
||||||
LastConnectedAt int64 `json:"lastConnectedAt"`
|
|
||||||
DisconnectedAt int64 `json:"disconnectedAt,omitempty"`
|
|
||||||
Online bool `json:"online"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseOutConf struct {
|
|
||||||
v1.ProxyBaseConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCPOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
RemotePort int `json:"remotePort"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCPMuxOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
v1.DomainConfig
|
|
||||||
Multiplexer string `json:"multiplexer"`
|
|
||||||
RouteByHTTPUser string `json:"routeByHTTPUser"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UDPOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
RemotePort int `json:"remotePort"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
v1.DomainConfig
|
|
||||||
Locations []string `json:"locations"`
|
|
||||||
HostHeaderRewrite string `json:"hostHeaderRewrite"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPSOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
v1.DomainConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type STCPOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
}
|
|
||||||
|
|
||||||
type XTCPOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get proxy info.
|
|
||||||
type ProxyStatsInfo struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Conf any `json:"conf"`
|
|
||||||
User string `json:"user,omitempty"`
|
|
||||||
ClientID string `json:"clientID,omitempty"`
|
|
||||||
ClientVersion string `json:"clientVersion,omitempty"`
|
|
||||||
TodayTrafficIn int64 `json:"todayTrafficIn"`
|
|
||||||
TodayTrafficOut int64 `json:"todayTrafficOut"`
|
|
||||||
CurConns int64 `json:"curConns"`
|
|
||||||
LastStartTime string `json:"lastStartTime"`
|
|
||||||
LastCloseTime string `json:"lastCloseTime"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetProxyInfoResp struct {
|
|
||||||
Proxies []*ProxyStatsInfo `json:"proxies"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get proxy info by name.
|
|
||||||
type GetProxyStatsResp struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Conf any `json:"conf"`
|
|
||||||
User string `json:"user,omitempty"`
|
|
||||||
ClientID string `json:"clientID,omitempty"`
|
|
||||||
ClientVersion string `json:"clientVersion,omitempty"`
|
|
||||||
TodayTrafficIn int64 `json:"todayTrafficIn"`
|
|
||||||
TodayTrafficOut int64 `json:"todayTrafficOut"`
|
|
||||||
CurConns int64 `json:"curConns"`
|
|
||||||
LastStartTime string `json:"lastStartTime"`
|
|
||||||
LastCloseTime string `json:"lastCloseTime"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/traffic/:name
|
|
||||||
type GetProxyTrafficResp struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
TrafficIn []int64 `json:"trafficIn"`
|
|
||||||
TrafficOut []int64 `json:"trafficOut"`
|
|
||||||
}
|
|
||||||
@@ -40,7 +40,6 @@ import (
|
|||||||
"github.com/fatedier/frp/server/controller"
|
"github.com/fatedier/frp/server/controller"
|
||||||
"github.com/fatedier/frp/server/metrics"
|
"github.com/fatedier/frp/server/metrics"
|
||||||
"github.com/fatedier/frp/server/proxy"
|
"github.com/fatedier/frp/server/proxy"
|
||||||
"github.com/fatedier/frp/server/registry"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ControlManager struct {
|
type ControlManager struct {
|
||||||
@@ -107,8 +106,6 @@ type Control struct {
|
|||||||
|
|
||||||
// verifies authentication based on selected method
|
// verifies authentication based on selected method
|
||||||
authVerifier auth.Verifier
|
authVerifier auth.Verifier
|
||||||
// key used for connection encryption
|
|
||||||
encryptionKey []byte
|
|
||||||
|
|
||||||
// other components can use this to communicate with client
|
// other components can use this to communicate with client
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
@@ -148,8 +145,6 @@ type Control struct {
|
|||||||
// Server configuration information
|
// Server configuration information
|
||||||
serverCfg *v1.ServerConfig
|
serverCfg *v1.ServerConfig
|
||||||
|
|
||||||
clientRegistry *registry.ClientRegistry
|
|
||||||
|
|
||||||
xl *xlog.Logger
|
xl *xlog.Logger
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
doneCh chan struct{}
|
doneCh chan struct{}
|
||||||
@@ -162,7 +157,6 @@ func NewControl(
|
|||||||
pxyManager *proxy.Manager,
|
pxyManager *proxy.Manager,
|
||||||
pluginManager *plugin.Manager,
|
pluginManager *plugin.Manager,
|
||||||
authVerifier auth.Verifier,
|
authVerifier auth.Verifier,
|
||||||
encryptionKey []byte,
|
|
||||||
ctlConn net.Conn,
|
ctlConn net.Conn,
|
||||||
ctlConnEncrypted bool,
|
ctlConnEncrypted bool,
|
||||||
loginMsg *msg.Login,
|
loginMsg *msg.Login,
|
||||||
@@ -177,7 +171,6 @@ func NewControl(
|
|||||||
pxyManager: pxyManager,
|
pxyManager: pxyManager,
|
||||||
pluginManager: pluginManager,
|
pluginManager: pluginManager,
|
||||||
authVerifier: authVerifier,
|
authVerifier: authVerifier,
|
||||||
encryptionKey: encryptionKey,
|
|
||||||
conn: ctlConn,
|
conn: ctlConn,
|
||||||
loginMsg: loginMsg,
|
loginMsg: loginMsg,
|
||||||
workConnCh: make(chan net.Conn, poolCount+10),
|
workConnCh: make(chan net.Conn, poolCount+10),
|
||||||
@@ -193,7 +186,7 @@ func NewControl(
|
|||||||
ctl.lastPing.Store(time.Now())
|
ctl.lastPing.Store(time.Now())
|
||||||
|
|
||||||
if ctlConnEncrypted {
|
if ctlConnEncrypted {
|
||||||
cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, ctl.encryptionKey)
|
cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -202,7 +195,7 @@ func NewControl(
|
|||||||
ctl.msgDispatcher = msg.NewDispatcher(ctl.conn)
|
ctl.msgDispatcher = msg.NewDispatcher(ctl.conn)
|
||||||
}
|
}
|
||||||
ctl.registerMsgHandlers()
|
ctl.registerMsgHandlers()
|
||||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher.SendChannel())
|
||||||
return ctl, nil
|
return ctl, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -361,7 +354,6 @@ func (ctl *Control) worker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
metrics.Server.CloseClient()
|
metrics.Server.CloseClient()
|
||||||
ctl.clientRegistry.MarkOfflineByRunID(ctl.runID)
|
|
||||||
xl.Infof("client exit success")
|
xl.Infof("client exit success")
|
||||||
close(ctl.doneCh)
|
close(ctl.doneCh)
|
||||||
}
|
}
|
||||||
@@ -405,11 +397,7 @@ func (ctl *Control) handleNewProxy(m msg.Message) {
|
|||||||
} else {
|
} else {
|
||||||
resp.RemoteAddr = remoteAddr
|
resp.RemoteAddr = remoteAddr
|
||||||
xl.Infof("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType)
|
xl.Infof("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType)
|
||||||
clientID := ctl.loginMsg.ClientID
|
metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType)
|
||||||
if clientID == "" {
|
|
||||||
clientID = ctl.loginMsg.RunID
|
|
||||||
}
|
|
||||||
metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType, ctl.loginMsg.User, clientID)
|
|
||||||
}
|
}
|
||||||
_ = ctl.msgDispatcher.Send(resp)
|
_ = ctl.msgDispatcher.Send(resp)
|
||||||
}
|
}
|
||||||
@@ -490,7 +478,6 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
|
|||||||
GetWorkConnFn: ctl.GetWorkConn,
|
GetWorkConnFn: ctl.GetWorkConn,
|
||||||
Configurer: pxyConf,
|
Configurer: pxyConf,
|
||||||
ServerCfg: ctl.serverCfg,
|
ServerCfg: ctl.serverCfg,
|
||||||
EncryptionKey: ctl.encryptionKey,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return remoteAddr, err
|
return remoteAddr, err
|
||||||
|
|||||||
406
server/dashboard_api.go
Normal file
406
server/dashboard_api.go
Normal file
@@ -0,0 +1,406 @@
|
|||||||
|
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/metrics/mem"
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GeneralResponse struct {
|
||||||
|
Code int
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
||||||
|
helper.Router.HandleFunc("/healthz", svr.healthz)
|
||||||
|
subRouter := helper.Router.NewRoute().Subrouter()
|
||||||
|
|
||||||
|
subRouter.Use(helper.AuthMiddleware.Middleware)
|
||||||
|
|
||||||
|
// metrics
|
||||||
|
if svr.cfg.EnablePrometheus {
|
||||||
|
subRouter.Handle("/metrics", promhttp.Handler())
|
||||||
|
}
|
||||||
|
|
||||||
|
// apis
|
||||||
|
subRouter.HandleFunc("/api/serverinfo", svr.apiServerInfo).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxies", svr.deleteProxies).Methods("DELETE")
|
||||||
|
|
||||||
|
// view
|
||||||
|
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
||||||
|
subRouter.PathPrefix("/static/").Handler(
|
||||||
|
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
||||||
|
).Methods("GET")
|
||||||
|
|
||||||
|
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverInfoResp struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
BindPort int `json:"bindPort"`
|
||||||
|
VhostHTTPPort int `json:"vhostHTTPPort"`
|
||||||
|
VhostHTTPSPort int `json:"vhostHTTPSPort"`
|
||||||
|
TCPMuxHTTPConnectPort int `json:"tcpmuxHTTPConnectPort"`
|
||||||
|
KCPBindPort int `json:"kcpBindPort"`
|
||||||
|
QUICBindPort int `json:"quicBindPort"`
|
||||||
|
SubdomainHost string `json:"subdomainHost"`
|
||||||
|
MaxPoolCount int64 `json:"maxPoolCount"`
|
||||||
|
MaxPortsPerClient int64 `json:"maxPortsPerClient"`
|
||||||
|
HeartBeatTimeout int64 `json:"heartbeatTimeout"`
|
||||||
|
AllowPortsStr string `json:"allowPortsStr,omitempty"`
|
||||||
|
TLSForce bool `json:"tlsForce,omitempty"`
|
||||||
|
|
||||||
|
TotalTrafficIn int64 `json:"totalTrafficIn"`
|
||||||
|
TotalTrafficOut int64 `json:"totalTrafficOut"`
|
||||||
|
CurConns int64 `json:"curConns"`
|
||||||
|
ClientCounts int64 `json:"clientCounts"`
|
||||||
|
ProxyTypeCounts map[string]int64 `json:"proxyTypeCount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// /healthz
|
||||||
|
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/serverinfo
|
||||||
|
func (svr *Service) apiServerInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
serverStats := mem.StatsCollector.GetServer()
|
||||||
|
svrResp := serverInfoResp{
|
||||||
|
Version: version.Full(),
|
||||||
|
BindPort: svr.cfg.BindPort,
|
||||||
|
VhostHTTPPort: svr.cfg.VhostHTTPPort,
|
||||||
|
VhostHTTPSPort: svr.cfg.VhostHTTPSPort,
|
||||||
|
TCPMuxHTTPConnectPort: svr.cfg.TCPMuxHTTPConnectPort,
|
||||||
|
KCPBindPort: svr.cfg.KCPBindPort,
|
||||||
|
QUICBindPort: svr.cfg.QUICBindPort,
|
||||||
|
SubdomainHost: svr.cfg.SubDomainHost,
|
||||||
|
MaxPoolCount: svr.cfg.Transport.MaxPoolCount,
|
||||||
|
MaxPortsPerClient: svr.cfg.MaxPortsPerClient,
|
||||||
|
HeartBeatTimeout: svr.cfg.Transport.HeartbeatTimeout,
|
||||||
|
AllowPortsStr: types.PortsRangeSlice(svr.cfg.AllowPorts).String(),
|
||||||
|
TLSForce: svr.cfg.Transport.TLS.Force,
|
||||||
|
|
||||||
|
TotalTrafficIn: serverStats.TotalTrafficIn,
|
||||||
|
TotalTrafficOut: serverStats.TotalTrafficOut,
|
||||||
|
CurConns: serverStats.CurConns,
|
||||||
|
ClientCounts: serverStats.ClientCounts,
|
||||||
|
ProxyTypeCounts: serverStats.ProxyTypeCounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, _ := json.Marshal(&svrResp)
|
||||||
|
res.Msg = string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseOutConf struct {
|
||||||
|
v1.ProxyBaseConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCPOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
RemotePort int `json:"remotePort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCPMuxOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
v1.DomainConfig
|
||||||
|
Multiplexer string `json:"multiplexer"`
|
||||||
|
RouteByHTTPUser string `json:"routeByHTTPUser"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UDPOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
RemotePort int `json:"remotePort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
v1.DomainConfig
|
||||||
|
Locations []string `json:"locations"`
|
||||||
|
HostHeaderRewrite string `json:"hostHeaderRewrite"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPSOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
v1.DomainConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type STCPOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
}
|
||||||
|
|
||||||
|
type XTCPOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfByType(proxyType string) any {
|
||||||
|
switch v1.ProxyType(proxyType) {
|
||||||
|
case v1.ProxyTypeTCP:
|
||||||
|
return &TCPOutConf{}
|
||||||
|
case v1.ProxyTypeTCPMUX:
|
||||||
|
return &TCPMuxOutConf{}
|
||||||
|
case v1.ProxyTypeUDP:
|
||||||
|
return &UDPOutConf{}
|
||||||
|
case v1.ProxyTypeHTTP:
|
||||||
|
return &HTTPOutConf{}
|
||||||
|
case v1.ProxyTypeHTTPS:
|
||||||
|
return &HTTPSOutConf{}
|
||||||
|
case v1.ProxyTypeSTCP:
|
||||||
|
return &STCPOutConf{}
|
||||||
|
case v1.ProxyTypeXTCP:
|
||||||
|
return &XTCPOutConf{}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get proxy info.
|
||||||
|
type ProxyStatsInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Conf any `json:"conf"`
|
||||||
|
ClientVersion string `json:"clientVersion,omitempty"`
|
||||||
|
TodayTrafficIn int64 `json:"todayTrafficIn"`
|
||||||
|
TodayTrafficOut int64 `json:"todayTrafficOut"`
|
||||||
|
CurConns int64 `json:"curConns"`
|
||||||
|
LastStartTime string `json:"lastStartTime"`
|
||||||
|
LastCloseTime string `json:"lastCloseTime"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetProxyInfoResp struct {
|
||||||
|
Proxies []*ProxyStatsInfo `json:"proxies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/proxy/:type
|
||||||
|
func (svr *Service) apiProxyByType(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
params := mux.Vars(r)
|
||||||
|
proxyType := params["type"]
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
|
||||||
|
proxyInfoResp := GetProxyInfoResp{}
|
||||||
|
proxyInfoResp.Proxies = svr.getProxyStatsByType(proxyType)
|
||||||
|
slices.SortFunc(proxyInfoResp.Proxies, func(a, b *ProxyStatsInfo) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
buf, _ := json.Marshal(&proxyInfoResp)
|
||||||
|
res.Msg = string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) {
|
||||||
|
proxyStats := mem.StatsCollector.GetProxiesByType(proxyType)
|
||||||
|
proxyInfos = make([]*ProxyStatsInfo, 0, len(proxyStats))
|
||||||
|
for _, ps := range proxyStats {
|
||||||
|
proxyInfo := &ProxyStatsInfo{}
|
||||||
|
if pxy, ok := svr.pxyManager.GetByName(ps.Name); ok {
|
||||||
|
content, err := json.Marshal(pxy.GetConfigurer())
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
proxyInfo.Conf = getConfByType(ps.Type)
|
||||||
|
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
||||||
|
log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
proxyInfo.Status = "online"
|
||||||
|
if pxy.GetLoginMsg() != nil {
|
||||||
|
proxyInfo.ClientVersion = pxy.GetLoginMsg().Version
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
proxyInfo.Status = "offline"
|
||||||
|
}
|
||||||
|
proxyInfo.Name = ps.Name
|
||||||
|
proxyInfo.TodayTrafficIn = ps.TodayTrafficIn
|
||||||
|
proxyInfo.TodayTrafficOut = ps.TodayTrafficOut
|
||||||
|
proxyInfo.CurConns = ps.CurConns
|
||||||
|
proxyInfo.LastStartTime = ps.LastStartTime
|
||||||
|
proxyInfo.LastCloseTime = ps.LastCloseTime
|
||||||
|
proxyInfos = append(proxyInfos, proxyInfo)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get proxy info by name.
|
||||||
|
type GetProxyStatsResp struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Conf any `json:"conf"`
|
||||||
|
TodayTrafficIn int64 `json:"todayTrafficIn"`
|
||||||
|
TodayTrafficOut int64 `json:"todayTrafficOut"`
|
||||||
|
CurConns int64 `json:"curConns"`
|
||||||
|
LastStartTime string `json:"lastStartTime"`
|
||||||
|
LastCloseTime string `json:"lastCloseTime"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/proxy/:type/:name
|
||||||
|
func (svr *Service) apiProxyByTypeAndName(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
params := mux.Vars(r)
|
||||||
|
proxyType := params["type"]
|
||||||
|
name := params["name"]
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
|
||||||
|
var proxyStatsResp GetProxyStatsResp
|
||||||
|
proxyStatsResp, res.Code, res.Msg = svr.getProxyStatsByTypeAndName(proxyType, name)
|
||||||
|
if res.Code != 200 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, _ := json.Marshal(&proxyStatsResp)
|
||||||
|
res.Msg = string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp, code int, msg string) {
|
||||||
|
proxyInfo.Name = proxyName
|
||||||
|
ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName)
|
||||||
|
if ps == nil {
|
||||||
|
code = 404
|
||||||
|
msg = "no proxy info found"
|
||||||
|
} else {
|
||||||
|
if pxy, ok := svr.pxyManager.GetByName(proxyName); ok {
|
||||||
|
content, err := json.Marshal(pxy.GetConfigurer())
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err)
|
||||||
|
code = 400
|
||||||
|
msg = "parse conf error"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proxyInfo.Conf = getConfByType(ps.Type)
|
||||||
|
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
||||||
|
log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
|
||||||
|
code = 400
|
||||||
|
msg = "parse conf error"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proxyInfo.Status = "online"
|
||||||
|
} else {
|
||||||
|
proxyInfo.Status = "offline"
|
||||||
|
}
|
||||||
|
proxyInfo.TodayTrafficIn = ps.TodayTrafficIn
|
||||||
|
proxyInfo.TodayTrafficOut = ps.TodayTrafficOut
|
||||||
|
proxyInfo.CurConns = ps.CurConns
|
||||||
|
proxyInfo.LastStartTime = ps.LastStartTime
|
||||||
|
proxyInfo.LastCloseTime = ps.LastCloseTime
|
||||||
|
code = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/traffic/:name
|
||||||
|
type GetProxyTrafficResp struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
TrafficIn []int64 `json:"trafficIn"`
|
||||||
|
TrafficOut []int64 `json:"trafficOut"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) apiProxyTraffic(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
params := mux.Vars(r)
|
||||||
|
name := params["name"]
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
|
||||||
|
trafficResp := GetProxyTrafficResp{}
|
||||||
|
trafficResp.Name = name
|
||||||
|
proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name)
|
||||||
|
|
||||||
|
if proxyTrafficInfo == nil {
|
||||||
|
res.Code = 404
|
||||||
|
res.Msg = "no proxy info found"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trafficResp.TrafficIn = proxyTrafficInfo.TrafficIn
|
||||||
|
trafficResp.TrafficOut = proxyTrafficInfo.TrafficOut
|
||||||
|
|
||||||
|
buf, _ := json.Marshal(&trafficResp)
|
||||||
|
res.Msg = string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/proxies?status=offline
|
||||||
|
func (svr *Service) deleteProxies(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
status := r.URL.Query().Get("status")
|
||||||
|
if status != "offline" {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = "status only support offline"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cleared, total := mem.StatsCollector.ClearOfflineProxies()
|
||||||
|
log.Infof("cleared [%d] offline proxies, total [%d] proxies", cleared, total)
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
type ServerMetrics interface {
|
type ServerMetrics interface {
|
||||||
NewClient()
|
NewClient()
|
||||||
CloseClient()
|
CloseClient()
|
||||||
NewProxy(name string, proxyType string, user string, clientID string)
|
NewProxy(name string, proxyType string)
|
||||||
CloseProxy(name string, proxyType string)
|
CloseProxy(name string, proxyType string)
|
||||||
OpenConnection(name string, proxyType string)
|
OpenConnection(name string, proxyType string)
|
||||||
CloseConnection(name string, proxyType string)
|
CloseConnection(name string, proxyType string)
|
||||||
@@ -27,11 +27,11 @@ func Register(m ServerMetrics) {
|
|||||||
|
|
||||||
type noopServerMetrics struct{}
|
type noopServerMetrics struct{}
|
||||||
|
|
||||||
func (noopServerMetrics) NewClient() {}
|
func (noopServerMetrics) NewClient() {}
|
||||||
func (noopServerMetrics) CloseClient() {}
|
func (noopServerMetrics) CloseClient() {}
|
||||||
func (noopServerMetrics) NewProxy(string, string, string, string) {}
|
func (noopServerMetrics) NewProxy(string, string) {}
|
||||||
func (noopServerMetrics) CloseProxy(string, string) {}
|
func (noopServerMetrics) CloseProxy(string, string) {}
|
||||||
func (noopServerMetrics) OpenConnection(string, string) {}
|
func (noopServerMetrics) OpenConnection(string, string) {}
|
||||||
func (noopServerMetrics) CloseConnection(string, string) {}
|
func (noopServerMetrics) CloseConnection(string, string) {}
|
||||||
func (noopServerMetrics) AddTrafficIn(string, string, int64) {}
|
func (noopServerMetrics) AddTrafficIn(string, string, int64) {}
|
||||||
func (noopServerMetrics) AddTrafficOut(string, string, int64) {}
|
func (noopServerMetrics) AddTrafficOut(string, string, int64) {}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
|
|||||||
|
|
||||||
var rwc io.ReadWriteCloser = tmpConn
|
var rwc io.ReadWriteCloser = tmpConn
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ type BaseProxy struct {
|
|||||||
poolCount int
|
poolCount int
|
||||||
getWorkConnFn GetWorkConnFn
|
getWorkConnFn GetWorkConnFn
|
||||||
serverCfg *v1.ServerConfig
|
serverCfg *v1.ServerConfig
|
||||||
encryptionKey []byte
|
|
||||||
limiter *rate.Limiter
|
limiter *rate.Limiter
|
||||||
userInfo plugin.UserInfo
|
userInfo plugin.UserInfo
|
||||||
loginMsg *msg.Login
|
loginMsg *msg.Login
|
||||||
@@ -214,6 +213,7 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
|
|||||||
xl := xlog.FromContextSafe(pxy.Context())
|
xl := xlog.FromContextSafe(pxy.Context())
|
||||||
defer userConn.Close()
|
defer userConn.Close()
|
||||||
|
|
||||||
|
serverCfg := pxy.serverCfg
|
||||||
cfg := pxy.configurer.GetBaseConfig()
|
cfg := pxy.configurer.GetBaseConfig()
|
||||||
// server plugin hook
|
// server plugin hook
|
||||||
rc := pxy.GetResourceController()
|
rc := pxy.GetResourceController()
|
||||||
@@ -240,7 +240,7 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
|
|||||||
xl.Tracef("handler user tcp connection, use_encryption: %t, use_compression: %t",
|
xl.Tracef("handler user tcp connection, use_encryption: %t, use_compression: %t",
|
||||||
cfg.Transport.UseEncryption, cfg.Transport.UseCompression)
|
cfg.Transport.UseEncryption, cfg.Transport.UseCompression)
|
||||||
if cfg.Transport.UseEncryption {
|
if cfg.Transport.UseEncryption {
|
||||||
local, err = libio.WithEncryption(local, pxy.encryptionKey)
|
local, err = libio.WithEncryption(local, []byte(serverCfg.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
return
|
return
|
||||||
@@ -279,7 +279,6 @@ type Options struct {
|
|||||||
GetWorkConnFn GetWorkConnFn
|
GetWorkConnFn GetWorkConnFn
|
||||||
Configurer v1.ProxyConfigurer
|
Configurer v1.ProxyConfigurer
|
||||||
ServerCfg *v1.ServerConfig
|
ServerCfg *v1.ServerConfig
|
||||||
EncryptionKey []byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) {
|
func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) {
|
||||||
@@ -299,7 +298,6 @@ func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) {
|
|||||||
poolCount: options.PoolCount,
|
poolCount: options.PoolCount,
|
||||||
getWorkConnFn: options.GetWorkConnFn,
|
getWorkConnFn: options.GetWorkConnFn,
|
||||||
serverCfg: options.ServerCfg,
|
serverCfg: options.ServerCfg,
|
||||||
encryptionKey: options.EncryptionKey,
|
|
||||||
limiter: limiter,
|
limiter: limiter,
|
||||||
xl: xl,
|
xl: xl,
|
||||||
ctx: xlog.NewContext(ctx, xl),
|
ctx: xlog.NewContext(ctx, xl),
|
||||||
|
|||||||
@@ -205,7 +205,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
|
|||||||
|
|
||||||
var rwc io.ReadWriteCloser = workConn
|
var rwc io.ReadWriteCloser = workConn
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ClientInfo captures metadata about a connected frpc instance.
|
|
||||||
type ClientInfo struct {
|
|
||||||
Key string
|
|
||||||
User string
|
|
||||||
RawClientID string
|
|
||||||
RunID string
|
|
||||||
Hostname string
|
|
||||||
IP string
|
|
||||||
FirstConnectedAt time.Time
|
|
||||||
LastConnectedAt time.Time
|
|
||||||
DisconnectedAt time.Time
|
|
||||||
Online bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientRegistry keeps track of active clients keyed by "{user}.{clientID}" (runID fallback when raw clientID is empty).
|
|
||||||
// Entries without an explicit raw clientID are removed on disconnect to avoid stale offline records.
|
|
||||||
type ClientRegistry struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
clients map[string]*ClientInfo
|
|
||||||
runIndex map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClientRegistry() *ClientRegistry {
|
|
||||||
return &ClientRegistry{
|
|
||||||
clients: make(map[string]*ClientInfo),
|
|
||||||
runIndex: make(map[string]string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register stores/updates metadata for a client and returns the registry key plus whether it conflicts with an online client.
|
|
||||||
func (cr *ClientRegistry) Register(user, rawClientID, runID, hostname, remoteAddr string) (key string, conflict bool) {
|
|
||||||
if runID == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
effectiveID := rawClientID
|
|
||||||
if effectiveID == "" {
|
|
||||||
effectiveID = runID
|
|
||||||
}
|
|
||||||
key = cr.composeClientKey(user, effectiveID)
|
|
||||||
enforceUnique := rawClientID != ""
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
cr.mu.Lock()
|
|
||||||
defer cr.mu.Unlock()
|
|
||||||
|
|
||||||
info, exists := cr.clients[key]
|
|
||||||
if enforceUnique && exists && info.Online && info.RunID != "" && info.RunID != runID {
|
|
||||||
return key, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
info = &ClientInfo{
|
|
||||||
Key: key,
|
|
||||||
User: user,
|
|
||||||
FirstConnectedAt: now,
|
|
||||||
}
|
|
||||||
cr.clients[key] = info
|
|
||||||
} else if info.RunID != "" {
|
|
||||||
delete(cr.runIndex, info.RunID)
|
|
||||||
}
|
|
||||||
|
|
||||||
info.RawClientID = rawClientID
|
|
||||||
info.RunID = runID
|
|
||||||
info.Hostname = hostname
|
|
||||||
info.IP = remoteAddr
|
|
||||||
if info.FirstConnectedAt.IsZero() {
|
|
||||||
info.FirstConnectedAt = now
|
|
||||||
}
|
|
||||||
info.LastConnectedAt = now
|
|
||||||
info.DisconnectedAt = time.Time{}
|
|
||||||
info.Online = true
|
|
||||||
|
|
||||||
cr.runIndex[runID] = key
|
|
||||||
return key, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkOfflineByRunID marks the client as offline when the corresponding control disconnects.
|
|
||||||
func (cr *ClientRegistry) MarkOfflineByRunID(runID string) {
|
|
||||||
cr.mu.Lock()
|
|
||||||
defer cr.mu.Unlock()
|
|
||||||
|
|
||||||
key, ok := cr.runIndex[runID]
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if info, ok := cr.clients[key]; ok && info.RunID == runID {
|
|
||||||
if info.RawClientID == "" {
|
|
||||||
delete(cr.clients, key)
|
|
||||||
} else {
|
|
||||||
info.RunID = ""
|
|
||||||
info.Online = false
|
|
||||||
now := time.Now()
|
|
||||||
info.DisconnectedAt = now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete(cr.runIndex, runID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// List returns a snapshot of all known clients.
|
|
||||||
func (cr *ClientRegistry) List() []ClientInfo {
|
|
||||||
cr.mu.RLock()
|
|
||||||
defer cr.mu.RUnlock()
|
|
||||||
|
|
||||||
result := make([]ClientInfo, 0, len(cr.clients))
|
|
||||||
for _, info := range cr.clients {
|
|
||||||
result = append(result, *info)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByKey retrieves a client by its composite key ({user}.{clientID} with runID fallback).
|
|
||||||
func (cr *ClientRegistry) GetByKey(key string) (ClientInfo, bool) {
|
|
||||||
cr.mu.RLock()
|
|
||||||
defer cr.mu.RUnlock()
|
|
||||||
|
|
||||||
info, ok := cr.clients[key]
|
|
||||||
if !ok {
|
|
||||||
return ClientInfo{}, false
|
|
||||||
}
|
|
||||||
return *info, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientID returns the resolved client identifier for external use.
|
|
||||||
func (info ClientInfo) ClientID() string {
|
|
||||||
if info.RawClientID != "" {
|
|
||||||
return info.RawClientID
|
|
||||||
}
|
|
||||||
return info.RunID
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByRunID retrieves a client by its run ID.
|
|
||||||
func (cr *ClientRegistry) GetByRunID(runID string) (ClientInfo, bool) {
|
|
||||||
cr.mu.RLock()
|
|
||||||
defer cr.mu.RUnlock()
|
|
||||||
|
|
||||||
key, ok := cr.runIndex[runID]
|
|
||||||
if !ok {
|
|
||||||
return ClientInfo{}, false
|
|
||||||
}
|
|
||||||
info, ok := cr.clients[key]
|
|
||||||
if !ok {
|
|
||||||
return ClientInfo{}, false
|
|
||||||
}
|
|
||||||
return *info, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cr *ClientRegistry) composeClientKey(user, id string) string {
|
|
||||||
switch {
|
|
||||||
case user == "":
|
|
||||||
return id
|
|
||||||
case id == "":
|
|
||||||
return user
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%s.%s", user, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,6 @@ import (
|
|||||||
"github.com/fatedier/golib/crypto"
|
"github.com/fatedier/golib/crypto"
|
||||||
"github.com/fatedier/golib/net/mux"
|
"github.com/fatedier/golib/net/mux"
|
||||||
fmux "github.com/hashicorp/yamux"
|
fmux "github.com/hashicorp/yamux"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
quic "github.com/quic-go/quic-go"
|
quic "github.com/quic-go/quic-go"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
@@ -48,13 +47,11 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
"github.com/fatedier/frp/pkg/util/vhost"
|
"github.com/fatedier/frp/pkg/util/vhost"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
"github.com/fatedier/frp/server/api"
|
|
||||||
"github.com/fatedier/frp/server/controller"
|
"github.com/fatedier/frp/server/controller"
|
||||||
"github.com/fatedier/frp/server/group"
|
"github.com/fatedier/frp/server/group"
|
||||||
"github.com/fatedier/frp/server/metrics"
|
"github.com/fatedier/frp/server/metrics"
|
||||||
"github.com/fatedier/frp/server/ports"
|
"github.com/fatedier/frp/server/ports"
|
||||||
"github.com/fatedier/frp/server/proxy"
|
"github.com/fatedier/frp/server/proxy"
|
||||||
"github.com/fatedier/frp/server/registry"
|
|
||||||
"github.com/fatedier/frp/server/visitor"
|
"github.com/fatedier/frp/server/visitor"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,9 +96,6 @@ type Service struct {
|
|||||||
// Manage all controllers
|
// Manage all controllers
|
||||||
ctlManager *ControlManager
|
ctlManager *ControlManager
|
||||||
|
|
||||||
// Track logical clients keyed by user.clientID (runID fallback when raw clientID is empty).
|
|
||||||
clientRegistry *registry.ClientRegistry
|
|
||||||
|
|
||||||
// Manage all proxies
|
// Manage all proxies
|
||||||
pxyManager *proxy.Manager
|
pxyManager *proxy.Manager
|
||||||
|
|
||||||
@@ -119,8 +113,8 @@ type Service struct {
|
|||||||
|
|
||||||
sshTunnelGateway *ssh.Gateway
|
sshTunnelGateway *ssh.Gateway
|
||||||
|
|
||||||
// Auth runtime and encryption materials
|
// Verifies authentication based on selected method
|
||||||
auth *auth.ServerAuth
|
authVerifier auth.Verifier
|
||||||
|
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
|
|
||||||
@@ -155,16 +149,10 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authRuntime, err := auth.BuildServerAuth(&cfg.Auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
svr := &Service{
|
svr := &Service{
|
||||||
ctlManager: NewControlManager(),
|
ctlManager: NewControlManager(),
|
||||||
clientRegistry: registry.NewClientRegistry(),
|
pxyManager: proxy.NewManager(),
|
||||||
pxyManager: proxy.NewManager(),
|
pluginManager: plugin.NewManager(),
|
||||||
pluginManager: plugin.NewManager(),
|
|
||||||
rc: &controller.ResourceController{
|
rc: &controller.ResourceController{
|
||||||
VisitorManager: visitor.NewManager(),
|
VisitorManager: visitor.NewManager(),
|
||||||
TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts),
|
TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts),
|
||||||
@@ -172,7 +160,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
|||||||
},
|
},
|
||||||
sshTunnelListener: netpkg.NewInternalListener(),
|
sshTunnelListener: netpkg.NewInternalListener(),
|
||||||
httpVhostRouter: vhost.NewRouters(),
|
httpVhostRouter: vhost.NewRouters(),
|
||||||
auth: authRuntime,
|
authVerifier: auth.NewAuthVerifier(cfg.Auth),
|
||||||
webServer: webServer,
|
webServer: webServer,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@@ -598,7 +586,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
|
|||||||
ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch)
|
ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch)
|
||||||
|
|
||||||
// Check auth.
|
// Check auth.
|
||||||
authVerifier := svr.auth.Verifier
|
authVerifier := svr.authVerifier
|
||||||
if internal && loginMsg.ClientSpec.AlwaysAuthPass {
|
if internal && loginMsg.ClientSpec.AlwaysAuthPass {
|
||||||
authVerifier = auth.AlwaysPassVerifier
|
authVerifier = auth.AlwaysPassVerifier
|
||||||
}
|
}
|
||||||
@@ -607,29 +595,16 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO(fatedier): use SessionContext
|
// TODO(fatedier): use SessionContext
|
||||||
ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, svr.auth.EncryptionKey(), ctlConn, !internal, loginMsg, svr.cfg)
|
ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, ctlConn, !internal, loginMsg, svr.cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("create new controller error: %v", err)
|
xl.Warnf("create new controller error: %v", err)
|
||||||
// don't return detailed errors to client
|
// don't return detailed errors to client
|
||||||
return fmt.Errorf("unexpected error when creating new controller")
|
return fmt.Errorf("unexpected error when creating new controller")
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
|
if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
|
||||||
oldCtl.WaitClosed()
|
oldCtl.WaitClosed()
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteAddr := ctlConn.RemoteAddr().String()
|
|
||||||
if host, _, err := net.SplitHostPort(remoteAddr); err == nil {
|
|
||||||
remoteAddr = host
|
|
||||||
}
|
|
||||||
_, conflict := svr.clientRegistry.Register(loginMsg.User, loginMsg.ClientID, loginMsg.RunID, loginMsg.Hostname, remoteAddr)
|
|
||||||
if conflict {
|
|
||||||
svr.ctlManager.Del(loginMsg.RunID, ctl)
|
|
||||||
ctl.Close()
|
|
||||||
return fmt.Errorf("client_id [%s] for user [%s] is already online", loginMsg.ClientID, loginMsg.User)
|
|
||||||
}
|
|
||||||
ctl.clientRegistry = svr.clientRegistry
|
|
||||||
|
|
||||||
ctl.Start()
|
ctl.Start()
|
||||||
|
|
||||||
// for statistics
|
// for statistics
|
||||||
@@ -690,42 +665,3 @@ func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVis
|
|||||||
return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey,
|
return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey,
|
||||||
newMsg.UseEncryption, newMsg.UseCompression, visitorUser)
|
newMsg.UseEncryption, newMsg.UseCompression, visitorUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
|
||||||
helper.Router.HandleFunc("/healthz", healthz)
|
|
||||||
subRouter := helper.Router.NewRoute().Subrouter()
|
|
||||||
|
|
||||||
subRouter.Use(helper.AuthMiddleware)
|
|
||||||
subRouter.Use(httppkg.NewRequestLogger)
|
|
||||||
|
|
||||||
// metrics
|
|
||||||
if svr.cfg.EnablePrometheus {
|
|
||||||
subRouter.Handle("/metrics", promhttp.Handler())
|
|
||||||
}
|
|
||||||
|
|
||||||
apiController := api.NewController(svr.cfg, svr.clientRegistry, svr.pxyManager)
|
|
||||||
|
|
||||||
// apis
|
|
||||||
subRouter.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(apiController.APIServerInfo)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByType)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/proxy/{type}/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByTypeAndName)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByName)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/traffic/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyTraffic)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(apiController.APIClientList)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/clients/{key}", httppkg.MakeHTTPHandlerFunc(apiController.APIClientDetail)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/proxies", httppkg.MakeHTTPHandlerFunc(apiController.DeleteProxies)).Methods("DELETE")
|
|
||||||
|
|
||||||
// view
|
|
||||||
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
|
||||||
subRouter.PathPrefix("/static/").Handler(
|
|
||||||
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
|
||||||
).Methods("GET")
|
|
||||||
|
|
||||||
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func healthz(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
w.WriteHeader(200)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,18 +29,6 @@ import (
|
|||||||
var _ = ginkgo.Describe("[Feature: TokenSource]", func() {
|
var _ = ginkgo.Describe("[Feature: TokenSource]", func() {
|
||||||
f := framework.NewDefaultFramework()
|
f := framework.NewDefaultFramework()
|
||||||
|
|
||||||
createExecTokenScript := func(name string) string {
|
|
||||||
scriptPath := filepath.Join(f.TempDirectory, name)
|
|
||||||
scriptContent := `#!/bin/sh
|
|
||||||
printf '%s\n' "$1"
|
|
||||||
`
|
|
||||||
err := os.WriteFile(scriptPath, []byte(scriptContent), 0o600)
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
err = os.Chmod(scriptPath, 0o700)
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
return scriptPath
|
|
||||||
}
|
|
||||||
|
|
||||||
ginkgo.Describe("File-based token loading", func() {
|
ginkgo.Describe("File-based token loading", func() {
|
||||||
ginkgo.It("should work with file tokenSource", func() {
|
ginkgo.It("should work with file tokenSource", func() {
|
||||||
// Create a temporary token file
|
// Create a temporary token file
|
||||||
@@ -226,154 +214,4 @@ auth.tokenSource.file.path = "%s"
|
|||||||
f.RunProcesses([]string{serverConf}, []string{})
|
f.RunProcesses([]string{serverConf}, []string{})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ginkgo.Describe("Exec-based token loading", func() {
|
|
||||||
ginkgo.It("should work with server tokenSource", func() {
|
|
||||||
execValue := "exec-server-value"
|
|
||||||
scriptPath := createExecTokenScript("server_token_exec.sh")
|
|
||||||
|
|
||||||
serverPort := f.AllocPort()
|
|
||||||
remotePort := f.AllocPort()
|
|
||||||
|
|
||||||
serverConf := fmt.Sprintf(`
|
|
||||||
bindAddr = "0.0.0.0"
|
|
||||||
bindPort = %d
|
|
||||||
|
|
||||||
auth.tokenSource.type = "exec"
|
|
||||||
auth.tokenSource.exec.command = %q
|
|
||||||
auth.tokenSource.exec.args = [%q]
|
|
||||||
`, serverPort, scriptPath, execValue)
|
|
||||||
|
|
||||||
clientConf := fmt.Sprintf(`
|
|
||||||
serverAddr = "127.0.0.1"
|
|
||||||
serverPort = %d
|
|
||||||
loginFailExit = false
|
|
||||||
auth.token = %q
|
|
||||||
|
|
||||||
[[proxies]]
|
|
||||||
name = "tcp"
|
|
||||||
type = "tcp"
|
|
||||||
localPort = %d
|
|
||||||
remotePort = %d
|
|
||||||
`, serverPort, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort)
|
|
||||||
|
|
||||||
serverConfigPath := f.GenerateConfigFile(serverConf)
|
|
||||||
clientConfigPath := f.GenerateConfigFile(clientConf)
|
|
||||||
|
|
||||||
_, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec")
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
_, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec")
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("should work with client tokenSource", func() {
|
|
||||||
execValue := "exec-client-value"
|
|
||||||
scriptPath := createExecTokenScript("client_token_exec.sh")
|
|
||||||
|
|
||||||
serverPort := f.AllocPort()
|
|
||||||
remotePort := f.AllocPort()
|
|
||||||
|
|
||||||
serverConf := fmt.Sprintf(`
|
|
||||||
bindAddr = "0.0.0.0"
|
|
||||||
bindPort = %d
|
|
||||||
|
|
||||||
auth.token = %q
|
|
||||||
`, serverPort, execValue)
|
|
||||||
|
|
||||||
clientConf := fmt.Sprintf(`
|
|
||||||
serverAddr = "127.0.0.1"
|
|
||||||
serverPort = %d
|
|
||||||
loginFailExit = false
|
|
||||||
|
|
||||||
auth.tokenSource.type = "exec"
|
|
||||||
auth.tokenSource.exec.command = %q
|
|
||||||
auth.tokenSource.exec.args = [%q]
|
|
||||||
|
|
||||||
[[proxies]]
|
|
||||||
name = "tcp"
|
|
||||||
type = "tcp"
|
|
||||||
localPort = %d
|
|
||||||
remotePort = %d
|
|
||||||
`, serverPort, scriptPath, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort)
|
|
||||||
|
|
||||||
serverConfigPath := f.GenerateConfigFile(serverConf)
|
|
||||||
clientConfigPath := f.GenerateConfigFile(clientConf)
|
|
||||||
|
|
||||||
_, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec")
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
_, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec")
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("should work with both server and client tokenSource", func() {
|
|
||||||
execValue := "exec-shared-value"
|
|
||||||
scriptPath := createExecTokenScript("shared_token_exec.sh")
|
|
||||||
|
|
||||||
serverPort := f.AllocPort()
|
|
||||||
remotePort := f.AllocPort()
|
|
||||||
|
|
||||||
serverConf := fmt.Sprintf(`
|
|
||||||
bindAddr = "0.0.0.0"
|
|
||||||
bindPort = %d
|
|
||||||
|
|
||||||
auth.tokenSource.type = "exec"
|
|
||||||
auth.tokenSource.exec.command = %q
|
|
||||||
auth.tokenSource.exec.args = [%q]
|
|
||||||
`, serverPort, scriptPath, execValue)
|
|
||||||
|
|
||||||
clientConf := fmt.Sprintf(`
|
|
||||||
serverAddr = "127.0.0.1"
|
|
||||||
serverPort = %d
|
|
||||||
loginFailExit = false
|
|
||||||
|
|
||||||
auth.tokenSource.type = "exec"
|
|
||||||
auth.tokenSource.exec.command = %q
|
|
||||||
auth.tokenSource.exec.args = [%q]
|
|
||||||
|
|
||||||
[[proxies]]
|
|
||||||
name = "tcp"
|
|
||||||
type = "tcp"
|
|
||||||
localPort = %d
|
|
||||||
remotePort = %d
|
|
||||||
`, serverPort, scriptPath, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort)
|
|
||||||
|
|
||||||
serverConfigPath := f.GenerateConfigFile(serverConf)
|
|
||||||
clientConfigPath := f.GenerateConfigFile(clientConf)
|
|
||||||
|
|
||||||
_, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec")
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
_, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec")
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("should fail validation without allow-unsafe", func() {
|
|
||||||
execValue := "exec-unsafe-value"
|
|
||||||
scriptPath := createExecTokenScript("unsafe_token_exec.sh")
|
|
||||||
|
|
||||||
serverPort := f.AllocPort()
|
|
||||||
serverConf := fmt.Sprintf(`
|
|
||||||
bindAddr = "0.0.0.0"
|
|
||||||
bindPort = %d
|
|
||||||
|
|
||||||
auth.tokenSource.type = "exec"
|
|
||||||
auth.tokenSource.exec.command = %q
|
|
||||||
auth.tokenSource.exec.args = [%q]
|
|
||||||
`, serverPort, scriptPath, execValue)
|
|
||||||
|
|
||||||
serverConfigPath := f.GenerateConfigFile(serverConf)
|
|
||||||
|
|
||||||
_, output, err := f.RunFrps("verify", "-c", serverConfigPath)
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
framework.ExpectContainSubstring(output, "unsafe feature \"TokenSourceExec\" is not enabled")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,7 +1,4 @@
|
|||||||
.PHONY: dist install build preview lint
|
.PHONY: dist build preview lint
|
||||||
|
|
||||||
install:
|
|
||||||
@npm install
|
|
||||||
|
|
||||||
build:
|
build:
|
||||||
@npm run build
|
@npm run build
|
||||||
|
|||||||
16
web/frpc/components.d.ts
vendored
16
web/frpc/components.d.ts
vendored
@@ -7,22 +7,18 @@ export {}
|
|||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
ClientConfigure: typeof import('./src/components/ClientConfigure.vue')['default']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
ProxyCard: typeof import('./src/components/ProxyCard.vue')['default']
|
Overview: typeof import('./src/components/Overview.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
StatCard: typeof import('./src/components/StatCard.vue')['default']
|
|
||||||
}
|
|
||||||
export interface ComponentCustomProperties {
|
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>frp client</title>
|
<title>frp client admin UI</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
5857
web/frpc/package-lock.json
generated
5857
web/frpc/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -11,30 +11,25 @@
|
|||||||
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
"lint": "eslint . --ext .vue,.js,.jsx,.cjs,.mjs,.ts,.tsx,.cts,.mts --fix --ignore-path .gitignore"
|
||||||
},
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"element-plus": "^2.13.0",
|
"element-plus": "^2.5.3",
|
||||||
"vue": "^3.5.26",
|
"vue": "^3.4.15",
|
||||||
"vue-router": "^4.6.4"
|
"vue-router": "^4.2.5"
|
||||||
},
|
},
|
||||||
"devDependencies": {
|
"devDependencies": {
|
||||||
"@rushstack/eslint-patch": "^1.15.0",
|
"@rushstack/eslint-patch": "^1.7.2",
|
||||||
"@types/node": "24",
|
"@types/node": "^18.11.12",
|
||||||
"@vitejs/plugin-vue": "^6.0.3",
|
"@vitejs/plugin-vue": "^5.0.3",
|
||||||
"@vue/eslint-config-prettier": "^9.0.0",
|
"@vue/eslint-config-prettier": "^9.0.0",
|
||||||
"@vue/eslint-config-typescript": "^12.0.0",
|
"@vue/eslint-config-typescript": "^12.0.0",
|
||||||
"@vue/tsconfig": "^0.8.1",
|
"@vue/tsconfig": "^0.5.1",
|
||||||
"@vueuse/core": "^14.1.0",
|
|
||||||
"eslint": "^8.56.0",
|
"eslint": "^8.56.0",
|
||||||
"eslint-plugin-vue": "^9.33.0",
|
"eslint-plugin-vue": "^9.21.0",
|
||||||
"npm-run-all": "^4.1.5",
|
"npm-run-all": "^4.1.5",
|
||||||
"prettier": "^3.7.4",
|
"prettier": "^3.2.4",
|
||||||
"sass": "^1.97.2",
|
"typescript": "~5.3.3",
|
||||||
"terser": "^5.44.1",
|
|
||||||
"typescript": "^5.9.3",
|
|
||||||
"unplugin-auto-import": "^0.17.5",
|
"unplugin-auto-import": "^0.17.5",
|
||||||
"unplugin-element-plus": "^0.11.2",
|
|
||||||
"unplugin-vue-components": "^0.26.0",
|
"unplugin-vue-components": "^0.26.0",
|
||||||
"vite": "^7.3.0",
|
"vite": "^5.0.12",
|
||||||
"vite-svg-loader": "^5.1.0",
|
"vue-tsc": "^1.8.27"
|
||||||
"vue-tsc": "^3.2.2"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,265 +1,116 @@
|
|||||||
<template>
|
<template>
|
||||||
<div id="app">
|
<div id="app">
|
||||||
<header class="header">
|
<header class="grid-content header-color">
|
||||||
<div class="header-content">
|
<div class="header-content">
|
||||||
<div class="header-top">
|
<div class="brand">
|
||||||
<div class="brand-section">
|
<a href="#">frp client</a>
|
||||||
<div class="logo-wrapper">
|
</div>
|
||||||
<LogoIcon class="logo-icon" />
|
<div class="dark-switch">
|
||||||
</div>
|
<el-switch
|
||||||
<span class="divider">/</span>
|
v-model="darkmodeSwitch"
|
||||||
<span class="brand-name">frp</span>
|
inline-prompt
|
||||||
<span class="badge client-badge">Client</span>
|
active-text="Dark"
|
||||||
<span class="badge" v-if="currentRouteName">{{
|
inactive-text="Light"
|
||||||
currentRouteName
|
@change="toggleDark"
|
||||||
}}</span>
|
style="
|
||||||
</div>
|
--el-switch-on-color: #444452;
|
||||||
|
--el-switch-off-color: #589ef8;
|
||||||
<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>
|
</div>
|
||||||
|
|
||||||
<nav class="nav-bar">
|
|
||||||
<router-link to="/" class="nav-link" active-class="active"
|
|
||||||
>Overview</router-link
|
|
||||||
>
|
|
||||||
<router-link to="/configure" class="nav-link" active-class="active"
|
|
||||||
>Configure</router-link
|
|
||||||
>
|
|
||||||
</nav>
|
|
||||||
</div>
|
</div>
|
||||||
</header>
|
</header>
|
||||||
|
<section>
|
||||||
|
<el-row>
|
||||||
|
<el-col id="side-nav" :xs="24" :md="4">
|
||||||
|
<el-menu
|
||||||
|
default-active="1"
|
||||||
|
mode="vertical"
|
||||||
|
theme="light"
|
||||||
|
router="false"
|
||||||
|
@select="handleSelect"
|
||||||
|
>
|
||||||
|
<el-menu-item index="/">Overview</el-menu-item>
|
||||||
|
<el-menu-item index="/configure">Configure</el-menu-item>
|
||||||
|
<el-menu-item index="">Help</el-menu-item>
|
||||||
|
</el-menu>
|
||||||
|
</el-col>
|
||||||
|
|
||||||
<main id="content">
|
<el-col :xs="24" :md="20">
|
||||||
<router-view></router-view>
|
<div id="content">
|
||||||
</main>
|
<router-view></router-view>
|
||||||
|
</div>
|
||||||
|
</el-col>
|
||||||
|
</el-row>
|
||||||
|
</section>
|
||||||
|
<footer></footer>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed } from 'vue'
|
import { ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useDark, useToggle } from '@vueuse/core'
|
||||||
import { useDark } from '@vueuse/core'
|
|
||||||
import { Moon, Sunny } from '@element-plus/icons-vue'
|
|
||||||
import GitHubIcon from './assets/icons/github.svg?component'
|
|
||||||
import LogoIcon from './assets/icons/logo.svg?component'
|
|
||||||
|
|
||||||
const route = useRoute()
|
|
||||||
const isDark = useDark()
|
const isDark = useDark()
|
||||||
|
const darkmodeSwitch = ref(isDark)
|
||||||
|
const toggleDark = useToggle(isDark)
|
||||||
|
|
||||||
const currentRouteName = computed(() => {
|
const handleSelect = (key: string) => {
|
||||||
if (route.path === '/') return 'Overview'
|
if (key == '') {
|
||||||
if (route.path === '/configure') return 'Configure'
|
window.open('https://github.com/fatedier/frp')
|
||||||
return ''
|
}
|
||||||
})
|
}
|
||||||
</script>
|
</script>
|
||||||
|
|
||||||
<style>
|
<style>
|
||||||
:root {
|
|
||||||
--header-height: 112px;
|
|
||||||
--header-bg: rgba(255, 255, 255, 0.8);
|
|
||||||
--header-border: #eaeaea;
|
|
||||||
--text-primary: #000;
|
|
||||||
--text-secondary: #666;
|
|
||||||
--hover-bg: #f5f5f5;
|
|
||||||
--active-link: #000;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark {
|
|
||||||
--header-bg: rgba(0, 0, 0, 0.8);
|
|
||||||
--header-border: #333;
|
|
||||||
--text-primary: #fff;
|
|
||||||
--text-secondary: #888;
|
|
||||||
--hover-bg: #1a1a1a;
|
|
||||||
--active-link: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
body {
|
body {
|
||||||
margin: 0;
|
margin: 0px;
|
||||||
font-family:
|
font-family: -apple-system, BlinkMacSystemFont, Helvetica Neue, sans-serif;
|
||||||
-apple-system, BlinkMacSystemFont, 'Segoe UI', Roboto, 'Helvetica Neue',
|
|
||||||
Arial, sans-serif;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#app {
|
header {
|
||||||
min-height: 100vh;
|
width: 100%;
|
||||||
display: flex;
|
height: 60px;
|
||||||
flex-direction: column;
|
|
||||||
background-color: var(--el-bg-color-page);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.header {
|
.header-color {
|
||||||
position: sticky;
|
background: #58b7ff;
|
||||||
top: 0;
|
}
|
||||||
z-index: 100;
|
|
||||||
background: var(--header-bg);
|
html.dark .header-color {
|
||||||
backdrop-filter: blur(12px);
|
background: #395c74;
|
||||||
-webkit-backdrop-filter: blur(12px);
|
|
||||||
border-bottom: 1px solid var(--header-border);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
.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;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-section {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 12px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-wrapper {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
}
|
|
||||||
|
|
||||||
.logo-icon {
|
|
||||||
width: 32px;
|
|
||||||
height: 32px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.divider {
|
|
||||||
color: var(--header-border);
|
|
||||||
font-size: 24px;
|
|
||||||
font-weight: 200;
|
|
||||||
}
|
|
||||||
|
|
||||||
.brand-name {
|
|
||||||
font-weight: 600;
|
|
||||||
font-size: 18px;
|
|
||||||
color: var(--text-primary);
|
|
||||||
letter-spacing: -0.5px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge {
|
|
||||||
font-size: 12px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
background: var(--hover-bg);
|
|
||||||
padding: 2px 8px;
|
|
||||||
border-radius: 99px;
|
|
||||||
border: 1px solid var(--header-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.badge.client-badge {
|
|
||||||
background: linear-gradient(135deg, #667eea 0%, #764ba2 100%);
|
|
||||||
color: white;
|
|
||||||
border: none;
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .badge.client-badge {
|
|
||||||
background: linear-gradient(135deg, #818cf8 0%, #a78bfa 100%);
|
|
||||||
}
|
|
||||||
|
|
||||||
.header-controls {
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 16px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.github-link {
|
|
||||||
width: 26px;
|
|
||||||
height: 26px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
justify-content: center;
|
|
||||||
border-radius: 50%;
|
|
||||||
color: var(--text-primary);
|
|
||||||
transition: background 0.2s;
|
|
||||||
background: transparent;
|
|
||||||
border: 1px solid transparent;
|
|
||||||
cursor: pointer;
|
|
||||||
}
|
|
||||||
|
|
||||||
.github-icon {
|
|
||||||
width: 18px;
|
|
||||||
height: 18px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.github-link:hover {
|
|
||||||
background: var(--hover-bg);
|
|
||||||
border-color: var(--header-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-switch {
|
|
||||||
--el-switch-on-color: #2c2c3a;
|
|
||||||
--el-switch-off-color: #f2f2f2;
|
|
||||||
--el-switch-border-color: var(--header-border);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .theme-switch {
|
|
||||||
--el-switch-off-color: #333;
|
|
||||||
}
|
|
||||||
|
|
||||||
.theme-switch .el-switch__core .el-switch__inner .el-icon {
|
|
||||||
color: #909399 !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-bar {
|
|
||||||
height: 48px;
|
|
||||||
display: flex;
|
|
||||||
align-items: center;
|
|
||||||
gap: 24px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link {
|
|
||||||
text-decoration: none;
|
|
||||||
font-size: 14px;
|
|
||||||
color: var(--text-secondary);
|
|
||||||
padding: 8px 0;
|
|
||||||
border-bottom: 2px solid transparent;
|
|
||||||
transition: all 0.2s;
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link:hover {
|
|
||||||
color: var(--text-primary);
|
|
||||||
}
|
|
||||||
|
|
||||||
.nav-link.active {
|
|
||||||
color: var(--active-link);
|
|
||||||
border-bottom-color: var(--active-link);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
#content {
|
||||||
flex: 1;
|
margin-top: 20px;
|
||||||
width: 100%;
|
padding-right: 40px;
|
||||||
padding: 40px;
|
|
||||||
max-width: 1200px;
|
|
||||||
margin: 0 auto;
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
@media (max-width: 768px) {
|
.brand {
|
||||||
.header-content {
|
display: flex;
|
||||||
padding: 0 20px;
|
justify-content: flex-start;
|
||||||
}
|
}
|
||||||
|
|
||||||
#content {
|
.brand a {
|
||||||
padding: 20px;
|
color: #fff;
|
||||||
}
|
background-color: transparent;
|
||||||
|
margin-left: 20px;
|
||||||
|
line-height: 25px;
|
||||||
|
font-size: 25px;
|
||||||
|
padding: 15px 15px;
|
||||||
|
height: 30px;
|
||||||
|
text-decoration: none;
|
||||||
|
}
|
||||||
|
|
||||||
|
.dark-switch {
|
||||||
|
display: flex;
|
||||||
|
justify-content: flex-end;
|
||||||
|
flex-grow: 1;
|
||||||
|
padding-right: 40px;
|
||||||
}
|
}
|
||||||
</style>
|
</style>
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
import { http } from './http'
|
|
||||||
import type { StatusResponse } from '../types/proxy'
|
|
||||||
|
|
||||||
export const getStatus = () => {
|
|
||||||
return http.get<StatusResponse>('/api/status')
|
|
||||||
}
|
|
||||||
|
|
||||||
export const getConfig = () => {
|
|
||||||
return http.get<string>('/api/config')
|
|
||||||
}
|
|
||||||
|
|
||||||
export const putConfig = (content: string) => {
|
|
||||||
return http.put<void>('/api/config', content)
|
|
||||||
}
|
|
||||||
|
|
||||||
export const reloadConfig = () => {
|
|
||||||
return http.get<void>('/api/reload')
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
// http.ts - Base HTTP client
|
|
||||||
|
|
||||||
class HTTPError extends Error {
|
|
||||||
status: number
|
|
||||||
statusText: string
|
|
||||||
|
|
||||||
constructor(status: number, statusText: string, message?: string) {
|
|
||||||
super(message || statusText)
|
|
||||||
this.status = status
|
|
||||||
this.statusText = statusText
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async function request<T>(url: string, options: RequestInit = {}): Promise<T> {
|
|
||||||
const defaultOptions: RequestInit = {
|
|
||||||
credentials: 'include',
|
|
||||||
}
|
|
||||||
|
|
||||||
const response = await fetch(url, { ...defaultOptions, ...options })
|
|
||||||
|
|
||||||
if (!response.ok) {
|
|
||||||
throw new HTTPError(
|
|
||||||
response.status,
|
|
||||||
response.statusText,
|
|
||||||
`HTTP ${response.status}`,
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Handle empty response (e.g. 204 No Content)
|
|
||||||
if (response.status === 204) {
|
|
||||||
return {} as T
|
|
||||||
}
|
|
||||||
|
|
||||||
const contentType = response.headers.get('content-type')
|
|
||||||
if (contentType && contentType.includes('application/json')) {
|
|
||||||
return response.json()
|
|
||||||
}
|
|
||||||
return response.text() as unknown as T
|
|
||||||
}
|
|
||||||
|
|
||||||
export const http = {
|
|
||||||
get: <T>(url: string, options?: RequestInit) =>
|
|
||||||
request<T>(url, { ...options, method: 'GET' }),
|
|
||||||
post: <T>(url: string, body?: any, options?: RequestInit) => {
|
|
||||||
const headers: HeadersInit = { ...options?.headers }
|
|
||||||
let requestBody = body
|
|
||||||
|
|
||||||
if (
|
|
||||||
body &&
|
|
||||||
typeof body === 'object' &&
|
|
||||||
!(body instanceof FormData) &&
|
|
||||||
!(body instanceof Blob)
|
|
||||||
) {
|
|
||||||
if (!('Content-Type' in headers)) {
|
|
||||||
;(headers as any)['Content-Type'] = 'application/json'
|
|
||||||
}
|
|
||||||
requestBody = JSON.stringify(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return request<T>(url, {
|
|
||||||
...options,
|
|
||||||
method: 'POST',
|
|
||||||
headers,
|
|
||||||
body: requestBody,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
put: <T>(url: string, body?: any, options?: RequestInit) => {
|
|
||||||
const headers: HeadersInit = { ...options?.headers }
|
|
||||||
let requestBody = body
|
|
||||||
|
|
||||||
if (
|
|
||||||
body &&
|
|
||||||
typeof body === 'object' &&
|
|
||||||
!(body instanceof FormData) &&
|
|
||||||
!(body instanceof Blob)
|
|
||||||
) {
|
|
||||||
if (!('Content-Type' in headers)) {
|
|
||||||
;(headers as any)['Content-Type'] = 'application/json'
|
|
||||||
}
|
|
||||||
requestBody = JSON.stringify(body)
|
|
||||||
}
|
|
||||||
|
|
||||||
return request<T>(url, {
|
|
||||||
...options,
|
|
||||||
method: 'PUT',
|
|
||||||
headers,
|
|
||||||
body: requestBody,
|
|
||||||
})
|
|
||||||
},
|
|
||||||
delete: <T>(url: string, options?: RequestInit) =>
|
|
||||||
request<T>(url, { ...options, method: 'DELETE' }),
|
|
||||||
}
|
|
||||||
@@ -1,105 +0,0 @@
|
|||||||
/* Modern Base Styles */
|
|
||||||
* {
|
|
||||||
box-sizing: border-box;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Smooth transitions for Element Plus components */
|
|
||||||
.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.08);
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* 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;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Input enhancements */
|
|
||||||
.el-input__wrapper {
|
|
||||||
transition: all 0.2s ease;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-input__wrapper:hover {
|
|
||||||
box-shadow: 0 0 0 1px var(--el-border-color-hover) inset;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Button enhancements */
|
|
||||||
.el-button {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Tag enhancements */
|
|
||||||
.el-tag {
|
|
||||||
font-weight: 500;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Card enhancements */
|
|
||||||
.el-card__header {
|
|
||||||
padding: 16px 20px;
|
|
||||||
border-bottom: 1px solid var(--el-border-color-lighter);
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-card__body {
|
|
||||||
padding: 20px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Table enhancements */
|
|
||||||
.el-table {
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
.el-table th {
|
|
||||||
font-weight: 600;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Empty state */
|
|
||||||
.el-empty__description {
|
|
||||||
margin-top: 16px;
|
|
||||||
font-size: 14px;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Loading state */
|
|
||||||
.el-loading-mask {
|
|
||||||
border-radius: 12px;
|
|
||||||
}
|
|
||||||
@@ -1,180 +0,0 @@
|
|||||||
/* Dark Mode Theme */
|
|
||||||
html.dark {
|
|
||||||
--el-bg-color: #1e1e2e;
|
|
||||||
--el-bg-color-page: #1a1a2e;
|
|
||||||
--el-bg-color-overlay: #27293d;
|
|
||||||
--el-fill-color-blank: #1e1e2e;
|
|
||||||
background-color: #1a1a2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark body {
|
|
||||||
background-color: #1a1a2e;
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode scrollbar */
|
|
||||||
html.dark ::-webkit-scrollbar-track {
|
|
||||||
background: #27293d;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark ::-webkit-scrollbar-thumb {
|
|
||||||
background: #3a3d5c;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark ::-webkit-scrollbar-thumb:hover {
|
|
||||||
background: #4a4d6c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode cards */
|
|
||||||
html.dark .el-card {
|
|
||||||
background-color: #27293d;
|
|
||||||
border-color: #3a3d5c;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-card__header {
|
|
||||||
border-bottom-color: #3a3d5c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode inputs */
|
|
||||||
html.dark .el-input__wrapper {
|
|
||||||
background-color: #27293d;
|
|
||||||
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 {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-input__inner::placeholder {
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode textarea */
|
|
||||||
html.dark .el-textarea__inner {
|
|
||||||
background-color: #1e1e2d;
|
|
||||||
border-color: #3a3d5c;
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-textarea__inner::placeholder {
|
|
||||||
color: #6b7280;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode table */
|
|
||||||
html.dark .el-table {
|
|
||||||
background-color: #27293d;
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-table th.el-table__cell {
|
|
||||||
background-color: #1e1e2e;
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-table tr {
|
|
||||||
background-color: #27293d;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-table--striped .el-table__body tr.el-table__row--striped td.el-table__cell {
|
|
||||||
background-color: #1e1e2e;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-table__row:hover > td.el-table__cell {
|
|
||||||
background-color: #2a2a3c !important;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode tags */
|
|
||||||
html.dark .el-tag--info {
|
|
||||||
background-color: #3a3d5c;
|
|
||||||
border-color: #3a3d5c;
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode buttons */
|
|
||||||
html.dark .el-button--default {
|
|
||||||
background-color: #27293d;
|
|
||||||
border-color: #3a3d5c;
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-button--default:hover {
|
|
||||||
background-color: #2a2a3c;
|
|
||||||
border-color: #4a4d6c;
|
|
||||||
color: #fff;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode select */
|
|
||||||
html.dark .el-select .el-input__wrapper {
|
|
||||||
background-color: #27293d;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-select-dropdown {
|
|
||||||
background-color: #27293d;
|
|
||||||
border-color: #3a3d5c;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-select-dropdown__item {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-select-dropdown__item:hover {
|
|
||||||
background-color: #2a2a3c;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode dialog */
|
|
||||||
html.dark .el-dialog {
|
|
||||||
background-color: #27293d;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-dialog__header {
|
|
||||||
border-bottom-color: #3a3d5c;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-dialog__title {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-dialog__body {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode message box */
|
|
||||||
html.dark .el-message-box {
|
|
||||||
background-color: #27293d;
|
|
||||||
border-color: #3a3d5c;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-message-box__title {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-message-box__message {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode empty */
|
|
||||||
html.dark .el-empty__description {
|
|
||||||
color: #9ca3af;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode loading */
|
|
||||||
html.dark .el-loading-mask {
|
|
||||||
background-color: rgba(30, 30, 46, 0.9);
|
|
||||||
}
|
|
||||||
|
|
||||||
html.dark .el-loading-text {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
|
|
||||||
/* Dark mode tooltip */
|
|
||||||
html.dark .el-tooltip__trigger {
|
|
||||||
color: #e5e7eb;
|
|
||||||
}
|
|
||||||
5
web/frpc/src/assets/dark.css
Normal file
5
web/frpc/src/assets/dark.css
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
html.dark {
|
||||||
|
--el-bg-color: #343432;
|
||||||
|
--el-fill-color-blank: #343432;
|
||||||
|
background-color: #343432;
|
||||||
|
}
|
||||||
@@ -1,3 +0,0 @@
|
|||||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 16 16" fill="currentColor">
|
|
||||||
<path d="M8 0C3.58 0 0 3.58 0 8c0 3.54 2.29 6.53 5.47 7.59.4.07.55-.17.55-.38 0-.19-.01-.82-.01-1.49-2.01.37-2.53-.49-2.69-.94-.09-.23-.48-.94-.82-1.13-.28-.15-.68-.52-.01-.53.63-.01 1.08.58 1.23.82.72 1.21 1.87.87 2.33.66.07-.52.28-.87.51-1.07-1.78-.2-3.64-.89-3.64-3.95 0-.87.31-1.59.82-2.15-.08-.2-.36-1.02.08-2.12 0 0 .67-.21 2.2.82.64-.18 1.32-.27 2-.27.68 0 1.36.09 2 .27 1.53-1.04 2.2-.82 2.2-.82.44 1.1.16 1.92.08 2.12.51.56.82 1.27.82 2.15 0 3.07-1.87 3.75-3.65 3.95.29.25.54.73.54 1.48 0 1.07-.01 1.93-.01 2.2 0 .21.15.46.55.38A8.013 8.013 0 0016 8c0-4.42-3.58-8-8-8z"/>
|
|
||||||
</svg>
|
|
||||||
|
Before Width: | Height: | Size: 671 B |
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user