mirror of
https://github.com/fatedier/frp.git
synced 2026-03-13 13:29:14 +08:00
Compare commits
5 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5f575b8442 | ||
|
|
a1348cdf00 | ||
|
|
2f5e1f7945 | ||
|
|
22ae8166d3 | ||
|
|
af6bc6369d |
@@ -2,7 +2,7 @@ version: 2
|
|||||||
jobs:
|
jobs:
|
||||||
go-version-latest:
|
go-version-latest:
|
||||||
docker:
|
docker:
|
||||||
- image: cimg/go:1.25-node
|
- image: cimg/go:1.24-node
|
||||||
resource_class: large
|
resource_class: large
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
|
|||||||
10
.github/workflows/golangci-lint.yml
vendored
10
.github/workflows/golangci-lint.yml
vendored
@@ -17,19 +17,19 @@ jobs:
|
|||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- uses: actions/setup-go@v5
|
- uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.24'
|
||||||
cache: false
|
cache: false
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
- name: Build web assets (frps)
|
- name: Build web assets (frps)
|
||||||
run: make build
|
run: make install build
|
||||||
working-directory: web/frps
|
working-directory: web/frps
|
||||||
- name: Build web assets (frpc)
|
- name: Build web assets (frpc)
|
||||||
run: make build
|
run: make install build
|
||||||
working-directory: web/frpc
|
working-directory: web/frpc
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v9
|
uses: golangci/golangci-lint-action@v8
|
||||||
with:
|
with:
|
||||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||||
version: v2.10
|
version: v2.3
|
||||||
|
|||||||
6
.github/workflows/goreleaser.yml
vendored
6
.github/workflows/goreleaser.yml
vendored
@@ -15,15 +15,15 @@ jobs:
|
|||||||
- name: Set up Go
|
- name: Set up Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.25'
|
go-version: '1.24'
|
||||||
- uses: actions/setup-node@v4
|
- uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '22'
|
||||||
- name: Build web assets (frps)
|
- name: Build web assets (frps)
|
||||||
run: make build
|
run: make install build
|
||||||
working-directory: web/frps
|
working-directory: web/frps
|
||||||
- name: Build web assets (frpc)
|
- name: Build web assets (frpc)
|
||||||
run: make build
|
run: make install build
|
||||||
working-directory: web/frpc
|
working-directory: web/frpc
|
||||||
- name: Make All
|
- name: Make All
|
||||||
run: |
|
run: |
|
||||||
|
|||||||
15
.gitignore
vendored
15
.gitignore
vendored
@@ -7,6 +7,18 @@
|
|||||||
_obj
|
_obj
|
||||||
_test
|
_test
|
||||||
|
|
||||||
|
# Architecture specific extensions/prefixes
|
||||||
|
*.[568vq]
|
||||||
|
[568vq].out
|
||||||
|
|
||||||
|
*.cgo1.go
|
||||||
|
*.cgo2.c
|
||||||
|
_cgo_defun.c
|
||||||
|
_cgo_gotypes.go
|
||||||
|
_cgo_export.*
|
||||||
|
|
||||||
|
_testmain.go
|
||||||
|
|
||||||
*.exe
|
*.exe
|
||||||
*.test
|
*.test
|
||||||
*.prof
|
*.prof
|
||||||
@@ -29,5 +41,4 @@ client.key
|
|||||||
*.swp
|
*.swp
|
||||||
|
|
||||||
# AI
|
# AI
|
||||||
.claude/
|
CLAUDE.md
|
||||||
.sisyphus/
|
|
||||||
|
|||||||
@@ -18,7 +18,6 @@ linters:
|
|||||||
- lll
|
- lll
|
||||||
- makezero
|
- makezero
|
||||||
- misspell
|
- misspell
|
||||||
- modernize
|
|
||||||
- prealloc
|
- prealloc
|
||||||
- predeclared
|
- predeclared
|
||||||
- revive
|
- revive
|
||||||
@@ -34,7 +33,13 @@ linters:
|
|||||||
disabled-checks:
|
disabled-checks:
|
||||||
- exitAfterDefer
|
- exitAfterDefer
|
||||||
gosec:
|
gosec:
|
||||||
excludes: ["G115", "G117", "G204", "G401", "G402", "G404", "G501", "G703", "G704", "G705"]
|
excludes:
|
||||||
|
- G401
|
||||||
|
- G402
|
||||||
|
- G404
|
||||||
|
- G501
|
||||||
|
- G115
|
||||||
|
- G204
|
||||||
severity: low
|
severity: low
|
||||||
confidence: low
|
confidence: low
|
||||||
govet:
|
govet:
|
||||||
@@ -48,9 +53,6 @@ linters:
|
|||||||
ignore-rules:
|
ignore-rules:
|
||||||
- cancelled
|
- cancelled
|
||||||
- marshalled
|
- marshalled
|
||||||
modernize:
|
|
||||||
disable:
|
|
||||||
- omitzero
|
|
||||||
unparam:
|
unparam:
|
||||||
check-exported: false
|
check-exported: false
|
||||||
exclusions:
|
exclusions:
|
||||||
@@ -75,9 +77,6 @@ linters:
|
|||||||
- linters:
|
- linters:
|
||||||
- revive
|
- revive
|
||||||
text: "avoid meaningless package names"
|
text: "avoid meaningless package names"
|
||||||
- linters:
|
|
||||||
- revive
|
|
||||||
text: "Go standard library package names"
|
|
||||||
- linters:
|
- linters:
|
||||||
- unparam
|
- unparam
|
||||||
text: is always false
|
text: is always false
|
||||||
|
|||||||
34
AGENTS.md
34
AGENTS.md
@@ -1,34 +0,0 @@
|
|||||||
# AGENTS.md
|
|
||||||
|
|
||||||
## Development Commands
|
|
||||||
|
|
||||||
### Build
|
|
||||||
- `make build` - Build both frps and frpc binaries
|
|
||||||
- `make frps` - Build server binary only
|
|
||||||
- `make frpc` - Build client binary only
|
|
||||||
- `make all` - Build everything with formatting
|
|
||||||
|
|
||||||
### Testing
|
|
||||||
- `make test` - Run unit tests
|
|
||||||
- `make e2e` - Run end-to-end tests
|
|
||||||
- `make e2e-trace` - Run e2e tests with trace logging
|
|
||||||
- `make alltest` - Run all tests including vet, unit tests, and e2e
|
|
||||||
|
|
||||||
### Code Quality
|
|
||||||
- `make fmt` - Run go fmt
|
|
||||||
- `make fmt-more` - Run gofumpt for more strict formatting
|
|
||||||
- `make gci` - Run gci import organizer
|
|
||||||
- `make vet` - Run go vet
|
|
||||||
- `golangci-lint run` - Run comprehensive linting (configured in .golangci.yml)
|
|
||||||
|
|
||||||
### Assets
|
|
||||||
- `make web` - Build web dashboards (frps and frpc)
|
|
||||||
|
|
||||||
### Cleanup
|
|
||||||
- `make clean` - Remove built binaries and temporary files
|
|
||||||
|
|
||||||
## Testing
|
|
||||||
|
|
||||||
- E2E tests using Ginkgo/Gomega framework
|
|
||||||
- Mock servers in `/test/e2e/mock/`
|
|
||||||
- Run: `make e2e` or `make alltest`
|
|
||||||
21
Makefile
21
Makefile
@@ -1,7 +1,6 @@
|
|||||||
export PATH := $(PATH):`go env GOPATH`/bin
|
export PATH := $(PATH):`go env GOPATH`/bin
|
||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
LDFLAGS := -s -w
|
LDFLAGS := -s -w
|
||||||
NOWEB_TAG = $(shell [ ! -d web/frps/dist ] || [ ! -d web/frpc/dist ] && echo ',noweb')
|
|
||||||
|
|
||||||
.PHONY: web frps-web frpc-web frps frpc
|
.PHONY: web frps-web frpc-web frps frpc
|
||||||
|
|
||||||
@@ -29,23 +28,23 @@ 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:
|
vet: web
|
||||||
go vet -tags "$(NOWEB_TAG)" ./...
|
go vet ./...
|
||||||
|
|
||||||
frps:
|
frps:
|
||||||
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "frps$(NOWEB_TAG)" -o bin/frps ./cmd/frps
|
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frps -o bin/frps ./cmd/frps
|
||||||
|
|
||||||
frpc:
|
frpc:
|
||||||
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "frpc$(NOWEB_TAG)" -o bin/frpc ./cmd/frpc
|
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags frpc -o bin/frpc ./cmd/frpc
|
||||||
|
|
||||||
test: gotest
|
test: gotest
|
||||||
|
|
||||||
gotest:
|
gotest: web
|
||||||
go test -tags "$(NOWEB_TAG)" -v --cover ./assets/...
|
go test -v --cover ./assets/...
|
||||||
go test -tags "$(NOWEB_TAG)" -v --cover ./cmd/...
|
go test -v --cover ./cmd/...
|
||||||
go test -tags "$(NOWEB_TAG)" -v --cover ./client/...
|
go test -v --cover ./client/...
|
||||||
go test -tags "$(NOWEB_TAG)" -v --cover ./server/...
|
go test -v --cover ./server/...
|
||||||
go test -tags "$(NOWEB_TAG)" -v --cover ./pkg/...
|
go test -v --cover ./pkg/...
|
||||||
|
|
||||||
e2e:
|
e2e:
|
||||||
./hack/run-e2e.sh
|
./hack/run-e2e.sh
|
||||||
|
|||||||
27
README.md
27
README.md
@@ -13,16 +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-->
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
## Recall.ai - API for meeting recordings
|
|
||||||
|
|
||||||
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
|
||||||
|
|
||||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
<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">
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
@@ -50,6 +40,15 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and
|
|||||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
## Recall.ai - API for meeting recordings
|
||||||
|
|
||||||
|
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
||||||
|
|
||||||
|
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||||
|
|
||||||
|
</div>
|
||||||
<!--gold sponsors end-->
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
## What is frp?
|
## What is frp?
|
||||||
@@ -801,14 +800,6 @@ Then run command `frpc reload -c ./frpc.toml` and wait for about 10 seconds to l
|
|||||||
|
|
||||||
**Note that global client parameters won't be modified except 'start'.**
|
**Note that global client parameters won't be modified except 'start'.**
|
||||||
|
|
||||||
`start` is a global allowlist evaluated after all sources are merged (config file/include/store).
|
|
||||||
If `start` is non-empty, any proxy or visitor not listed there will not be started, including
|
|
||||||
entries created via Store API.
|
|
||||||
|
|
||||||
`start` is kept mainly for compatibility and is generally not recommended for new configurations.
|
|
||||||
Prefer per-proxy/per-visitor `enabled`, and keep `start` empty unless you explicitly want this
|
|
||||||
global allowlist behavior.
|
|
||||||
|
|
||||||
You can run command `frpc verify -c ./frpc.toml` before reloading to check if there are config errors.
|
You can run command `frpc verify -c ./frpc.toml` before reloading to check if there are config errors.
|
||||||
|
|
||||||
### Get proxy status from client
|
### Get proxy status from client
|
||||||
|
|||||||
19
README_zh.md
19
README_zh.md
@@ -15,16 +15,6 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
|||||||
|
|
||||||
<h3 align="center">Gold Sponsors</h3>
|
<h3 align="center">Gold Sponsors</h3>
|
||||||
<!--gold sponsors start-->
|
<!--gold sponsors start-->
|
||||||
<div align="center">
|
|
||||||
|
|
||||||
## Recall.ai - API for meeting recordings
|
|
||||||
|
|
||||||
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
|
||||||
|
|
||||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
|
||||||
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
<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">
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
@@ -52,6 +42,15 @@ an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and
|
|||||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
<div align="center">
|
||||||
|
|
||||||
|
## Recall.ai - API for meeting recordings
|
||||||
|
|
||||||
|
If you're looking for a meeting recording API, consider checking out [Recall.ai](https://www.recall.ai/?utm_source=github&utm_medium=sponsorship&utm_campaign=fatedier-frp),
|
||||||
|
|
||||||
|
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||||
|
|
||||||
|
</div>
|
||||||
<!--gold sponsors end-->
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
## 为什么使用 frp ?
|
## 为什么使用 frp ?
|
||||||
|
|||||||
@@ -1,9 +1,8 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* Added a built-in `store` capability for frpc, including persisted store source (`[store] path = "..."`), Store CRUD admin APIs (`/api/store/proxies*`, `/api/store/visitors*`) with runtime reload, and Store management pages in the frpc web dashboard.
|
* 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.
|
||||||
|
* Redesigned the frp web dashboard with a modern UI, dark mode support, and improved navigation.
|
||||||
|
|
||||||
## Improvements
|
## Fixes
|
||||||
|
|
||||||
* Kept proxy/visitor names as raw config names during completion; moved user-prefix handling to explicit wire-level naming logic.
|
* Fixed UDP proxy protocol sending header on every packet instead of only the first packet of each session.
|
||||||
* Added `noweb` build tag to allow compiling without frontend assets. `make build` now auto-detects missing `web/*/dist` directories and skips embedding, so a fresh clone can build without running `make web` first. The dashboard gracefully returns 404 when assets are not embedded.
|
|
||||||
* Improved config parsing errors: for `.toml` files, syntax errors now return immediately with parser position details (line/column when available) instead of falling through to YAML/JSON parsing, and TOML type mismatches report field-level errors without misleading line numbers.
|
|
||||||
|
|||||||
@@ -29,23 +29,14 @@ var (
|
|||||||
prefixPath string
|
prefixPath string
|
||||||
)
|
)
|
||||||
|
|
||||||
type emptyFS struct{}
|
|
||||||
|
|
||||||
func (emptyFS) Open(name string) (http.File, error) {
|
|
||||||
return nil, &fs.PathError{Op: "open", Path: name, Err: fs.ErrNotExist}
|
|
||||||
}
|
|
||||||
|
|
||||||
// if path is empty, load assets in memory
|
// if path is empty, load assets in memory
|
||||||
// or set FileSystem using disk files
|
// or set FileSystem using disk files
|
||||||
func Load(path string) {
|
func Load(path string) {
|
||||||
prefixPath = path
|
prefixPath = path
|
||||||
switch {
|
if prefixPath != "" {
|
||||||
case prefixPath != "":
|
|
||||||
FileSystem = http.Dir(prefixPath)
|
FileSystem = http.Dir(prefixPath)
|
||||||
case content != nil:
|
} else {
|
||||||
FileSystem = http.FS(content)
|
FileSystem = http.FS(content)
|
||||||
default:
|
|
||||||
FileSystem = emptyFS{}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,7 @@ package client
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
adminapi "github.com/fatedier/frp/client/http"
|
"github.com/fatedier/frp/client/api"
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
@@ -38,20 +38,6 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
|
|||||||
subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet)
|
subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet)
|
||||||
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet)
|
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet)
|
||||||
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut)
|
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut)
|
||||||
|
|
||||||
if svr.storeSource != nil {
|
|
||||||
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreProxies)).Methods(http.MethodGet)
|
|
||||||
subRouter.HandleFunc("/api/store/proxies", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreProxy)).Methods(http.MethodPost)
|
|
||||||
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreProxy)).Methods(http.MethodGet)
|
|
||||||
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreProxy)).Methods(http.MethodPut)
|
|
||||||
subRouter.HandleFunc("/api/store/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreProxy)).Methods(http.MethodDelete)
|
|
||||||
subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.ListStoreVisitors)).Methods(http.MethodGet)
|
|
||||||
subRouter.HandleFunc("/api/store/visitors", httppkg.MakeHTTPHandlerFunc(apiController.CreateStoreVisitor)).Methods(http.MethodPost)
|
|
||||||
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.GetStoreVisitor)).Methods(http.MethodGet)
|
|
||||||
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.UpdateStoreVisitor)).Methods(http.MethodPut)
|
|
||||||
subRouter.HandleFunc("/api/store/visitors/{name}", httppkg.MakeHTTPHandlerFunc(apiController.DeleteStoreVisitor)).Methods(http.MethodDelete)
|
|
||||||
}
|
|
||||||
|
|
||||||
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))),
|
||||||
@@ -65,11 +51,14 @@ func healthz(w http.ResponseWriter, _ *http.Request) {
|
|||||||
w.WriteHeader(http.StatusOK)
|
w.WriteHeader(http.StatusOK)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAPIController(svr *Service) *adminapi.Controller {
|
func newAPIController(svr *Service) *api.Controller {
|
||||||
manager := newServiceConfigManager(svr)
|
return api.NewController(api.ControllerParams{
|
||||||
return adminapi.NewController(adminapi.ControllerParams{
|
GetProxyStatus: svr.getAllProxyStatus,
|
||||||
ServerAddr: svr.common.ServerAddr,
|
ServerAddr: svr.common.ServerAddr,
|
||||||
Manager: manager,
|
ConfigFilePath: svr.configFilePath,
|
||||||
|
UnsafeFeatures: svr.unsafeFeatures,
|
||||||
|
UpdateConfig: svr.UpdateAllConfigurer,
|
||||||
|
GracefulClose: svr.GracefulClose,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
189
client/api/controller.go
Normal file
189
client/api/controller.go
Normal file
@@ -0,0 +1,189 @@
|
|||||||
|
// 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
|
||||||
|
}
|
||||||
@@ -12,9 +12,7 @@
|
|||||||
// See the License for the specific language governing permissions and
|
// See the License for the specific language governing permissions and
|
||||||
// limitations under the License.
|
// limitations under the License.
|
||||||
|
|
||||||
package model
|
package api
|
||||||
|
|
||||||
const SourceStore = "store"
|
|
||||||
|
|
||||||
// StatusResp is the response for GET /api/status
|
// StatusResp is the response for GET /api/status
|
||||||
type StatusResp map[string][]ProxyStatusResp
|
type StatusResp map[string][]ProxyStatusResp
|
||||||
@@ -28,15 +26,4 @@ type ProxyStatusResp struct {
|
|||||||
LocalAddr string `json:"local_addr"`
|
LocalAddr string `json:"local_addr"`
|
||||||
Plugin string `json:"plugin"`
|
Plugin string `json:"plugin"`
|
||||||
RemoteAddr string `json:"remote_addr"`
|
RemoteAddr string `json:"remote_addr"`
|
||||||
Source string `json:"source,omitempty"` // "store" or "config"
|
|
||||||
}
|
|
||||||
|
|
||||||
// ProxyListResp is the response for GET /api/store/proxies
|
|
||||||
type ProxyListResp struct {
|
|
||||||
Proxies []ProxyDefinition `json:"proxies"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// VisitorListResp is the response for GET /api/store/visitors
|
|
||||||
type VisitorListResp struct {
|
|
||||||
Visitors []VisitorDefinition `json:"visitors"`
|
|
||||||
}
|
}
|
||||||
@@ -1,422 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/configmgmt"
|
|
||||||
"github.com/fatedier/frp/client/proxy"
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
|
||||||
"github.com/fatedier/frp/pkg/config/source"
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type serviceConfigManager struct {
|
|
||||||
svr *Service
|
|
||||||
}
|
|
||||||
|
|
||||||
func newServiceConfigManager(svr *Service) configmgmt.ConfigManager {
|
|
||||||
return &serviceConfigManager{svr: svr}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) ReloadFromFile(strict bool) error {
|
|
||||||
if m.svr.configFilePath == "" {
|
|
||||||
return fmt.Errorf("%w: frpc has no config file path", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
|
|
||||||
result, err := config.LoadClientConfigResult(m.svr.configFilePath, strict)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyCfgsForValidation, visitorCfgsForValidation := config.FilterClientConfigurers(
|
|
||||||
result.Common,
|
|
||||||
result.Proxies,
|
|
||||||
result.Visitors,
|
|
||||||
)
|
|
||||||
proxyCfgsForValidation = config.CompleteProxyConfigurers(proxyCfgsForValidation)
|
|
||||||
visitorCfgsForValidation = config.CompleteVisitorConfigurers(visitorCfgsForValidation)
|
|
||||||
|
|
||||||
if _, err := validation.ValidateAllClientConfig(result.Common, proxyCfgsForValidation, visitorCfgsForValidation, m.svr.unsafeFeatures); err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.svr.UpdateConfigSource(result.Common, result.Proxies, result.Visitors); err != nil {
|
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrApplyConfig, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("success reload conf")
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) ReadConfigFile() (string, error) {
|
|
||||||
if m.svr.configFilePath == "" {
|
|
||||||
return "", fmt.Errorf("%w: frpc has no config file path", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := os.ReadFile(m.svr.configFilePath)
|
|
||||||
if err != nil {
|
|
||||||
return "", fmt.Errorf("%w: %v", configmgmt.ErrInvalidArgument, err)
|
|
||||||
}
|
|
||||||
return string(content), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) WriteConfigFile(content []byte) error {
|
|
||||||
if len(content) == 0 {
|
|
||||||
return fmt.Errorf("%w: body can't be empty", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(m.svr.configFilePath, content, 0o600); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) GetProxyStatus() []*proxy.WorkingStatus {
|
|
||||||
return m.svr.getAllProxyStatus()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) IsStoreProxyEnabled(name string) bool {
|
|
||||||
if name == "" {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
m.svr.reloadMu.Lock()
|
|
||||||
storeSource := m.svr.storeSource
|
|
||||||
m.svr.reloadMu.Unlock()
|
|
||||||
|
|
||||||
if storeSource == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := storeSource.GetProxy(name)
|
|
||||||
if cfg == nil {
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
enabled := cfg.GetBaseConfig().Enabled
|
|
||||||
return enabled == nil || *enabled
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) StoreEnabled() bool {
|
|
||||||
m.svr.reloadMu.Lock()
|
|
||||||
storeSource := m.svr.storeSource
|
|
||||||
m.svr.reloadMu.Unlock()
|
|
||||||
return storeSource != nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
|
|
||||||
storeSource, err := m.storeSourceOrError()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return storeSource.GetAllProxies()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) {
|
|
||||||
if name == "" {
|
|
||||||
return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
|
|
||||||
storeSource, err := m.storeSourceOrError()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := storeSource.GetProxy(name)
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, fmt.Errorf("%w: proxy %q", configmgmt.ErrNotFound, name)
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
|
||||||
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
name := cfg.GetBaseConfig().Name
|
|
||||||
persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
|
||||||
if err := storeSource.AddProxy(cfg); err != nil {
|
|
||||||
if errors.Is(err, source.ErrAlreadyExists) {
|
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
log.Infof("store: created proxy %q", name)
|
|
||||||
return persisted, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
|
||||||
if name == "" {
|
|
||||||
return nil, fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, fmt.Errorf("%w: invalid proxy config: type is required", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
bodyName := cfg.GetBaseConfig().Name
|
|
||||||
if bodyName != name {
|
|
||||||
return nil, fmt.Errorf("%w: proxy name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
if err := m.validateStoreProxyConfigurer(cfg); err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
persisted, err := m.withStoreProxyMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
|
||||||
if err := storeSource.UpdateProxy(cfg); err != nil {
|
|
||||||
if errors.Is(err, source.ErrNotFound) {
|
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("store: updated proxy %q", name)
|
|
||||||
return persisted, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) DeleteStoreProxy(name string) error {
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("%w: proxy name is required", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
|
||||||
if err := storeSource.RemoveProxy(name); err != nil {
|
|
||||||
if errors.Is(err, source.ErrNotFound) {
|
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("store: deleted proxy %q", name)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) {
|
|
||||||
storeSource, err := m.storeSourceOrError()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return storeSource.GetAllVisitors()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) {
|
|
||||||
if name == "" {
|
|
||||||
return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
|
|
||||||
storeSource, err := m.storeSourceOrError()
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := storeSource.GetVisitor(name)
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, fmt.Errorf("%w: visitor %q", configmgmt.ErrNotFound, name)
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
|
||||||
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
name := cfg.GetBaseConfig().Name
|
|
||||||
persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
|
||||||
if err := storeSource.AddVisitor(cfg); err != nil {
|
|
||||||
if errors.Is(err, source.ErrAlreadyExists) {
|
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrConflict, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("store: created visitor %q", name)
|
|
||||||
return persisted, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
|
||||||
if name == "" {
|
|
||||||
return nil, fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, fmt.Errorf("%w: invalid visitor config: type is required", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
bodyName := cfg.GetBaseConfig().Name
|
|
||||||
if bodyName != name {
|
|
||||||
return nil, fmt.Errorf("%w: visitor name in URL must match name in body", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
if err := m.validateStoreVisitorConfigurer(cfg); err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: validation error: %v", configmgmt.ErrInvalidArgument, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
persisted, err := m.withStoreVisitorMutationAndReload(name, func(storeSource *source.StoreSource) error {
|
|
||||||
if err := storeSource.UpdateVisitor(cfg); err != nil {
|
|
||||||
if errors.Is(err, source.ErrNotFound) {
|
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("store: updated visitor %q", name)
|
|
||||||
return persisted, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) DeleteStoreVisitor(name string) error {
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("%w: visitor name is required", configmgmt.ErrInvalidArgument)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.withStoreMutationAndReload(func(storeSource *source.StoreSource) error {
|
|
||||||
if err := storeSource.RemoveVisitor(name); err != nil {
|
|
||||||
if errors.Is(err, source.ErrNotFound) {
|
|
||||||
return fmt.Errorf("%w: %v", configmgmt.ErrNotFound, err)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("store: deleted visitor %q", name)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) GracefulClose(d time.Duration) {
|
|
||||||
m.svr.GracefulClose(d)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) storeSourceOrError() (*source.StoreSource, error) {
|
|
||||||
m.svr.reloadMu.Lock()
|
|
||||||
storeSource := m.svr.storeSource
|
|
||||||
m.svr.reloadMu.Unlock()
|
|
||||||
|
|
||||||
if storeSource == nil {
|
|
||||||
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
|
||||||
}
|
|
||||||
return storeSource, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) withStoreMutationAndReload(
|
|
||||||
fn func(storeSource *source.StoreSource) error,
|
|
||||||
) error {
|
|
||||||
m.svr.reloadMu.Lock()
|
|
||||||
defer m.svr.reloadMu.Unlock()
|
|
||||||
|
|
||||||
storeSource := m.svr.storeSource
|
|
||||||
if storeSource == nil {
|
|
||||||
return fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fn(storeSource); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
|
||||||
return fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) withStoreProxyMutationAndReload(
|
|
||||||
name string,
|
|
||||||
fn func(storeSource *source.StoreSource) error,
|
|
||||||
) (v1.ProxyConfigurer, error) {
|
|
||||||
m.svr.reloadMu.Lock()
|
|
||||||
defer m.svr.reloadMu.Unlock()
|
|
||||||
|
|
||||||
storeSource := m.svr.storeSource
|
|
||||||
if storeSource == nil {
|
|
||||||
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fn(storeSource); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
persisted := storeSource.GetProxy(name)
|
|
||||||
if persisted == nil {
|
|
||||||
return nil, fmt.Errorf("%w: proxy %q not found in store after mutation", configmgmt.ErrApplyConfig, name)
|
|
||||||
}
|
|
||||||
return persisted.Clone(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) withStoreVisitorMutationAndReload(
|
|
||||||
name string,
|
|
||||||
fn func(storeSource *source.StoreSource) error,
|
|
||||||
) (v1.VisitorConfigurer, error) {
|
|
||||||
m.svr.reloadMu.Lock()
|
|
||||||
defer m.svr.reloadMu.Unlock()
|
|
||||||
|
|
||||||
storeSource := m.svr.storeSource
|
|
||||||
if storeSource == nil {
|
|
||||||
return nil, fmt.Errorf("%w: store API is disabled", configmgmt.ErrStoreDisabled)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := fn(storeSource); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
if err := m.svr.reloadConfigFromSourcesLocked(); err != nil {
|
|
||||||
return nil, fmt.Errorf("%w: failed to apply config: %v", configmgmt.ErrApplyConfig, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
persisted := storeSource.GetVisitor(name)
|
|
||||||
if persisted == nil {
|
|
||||||
return nil, fmt.Errorf("%w: visitor %q not found in store after mutation", configmgmt.ErrApplyConfig, name)
|
|
||||||
}
|
|
||||||
return persisted.Clone(), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) validateStoreProxyConfigurer(cfg v1.ProxyConfigurer) error {
|
|
||||||
if cfg == nil {
|
|
||||||
return fmt.Errorf("invalid proxy config")
|
|
||||||
}
|
|
||||||
runtimeCfg := cfg.Clone()
|
|
||||||
if runtimeCfg == nil {
|
|
||||||
return fmt.Errorf("invalid proxy config")
|
|
||||||
}
|
|
||||||
runtimeCfg.Complete()
|
|
||||||
return validation.ValidateProxyConfigurerForClient(runtimeCfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serviceConfigManager) validateStoreVisitorConfigurer(cfg v1.VisitorConfigurer) error {
|
|
||||||
if cfg == nil {
|
|
||||||
return fmt.Errorf("invalid visitor config")
|
|
||||||
}
|
|
||||||
runtimeCfg := cfg.Clone()
|
|
||||||
if runtimeCfg == nil {
|
|
||||||
return fmt.Errorf("invalid visitor config")
|
|
||||||
}
|
|
||||||
runtimeCfg.Complete()
|
|
||||||
return validation.ValidateVisitorConfigurer(runtimeCfg)
|
|
||||||
}
|
|
||||||
@@ -1,137 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/configmgmt"
|
|
||||||
"github.com/fatedier/frp/pkg/config/source"
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func newTestRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
|
||||||
return &v1.TCPProxyConfig{
|
|
||||||
ProxyBaseConfig: v1.ProxyBaseConfig{
|
|
||||||
Name: name,
|
|
||||||
Type: "tcp",
|
|
||||||
ProxyBackend: v1.ProxyBackend{
|
|
||||||
LocalPort: 10080,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServiceConfigManagerCreateStoreProxyConflict(t *testing.T) {
|
|
||||||
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
|
||||||
Path: filepath.Join(t.TempDir(), "store.json"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("new store source: %v", err)
|
|
||||||
}
|
|
||||||
if err := storeSource.AddProxy(newTestRawTCPProxyConfig("p1")); err != nil {
|
|
||||||
t.Fatalf("seed proxy: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
agg := source.NewAggregator(source.NewConfigSource())
|
|
||||||
agg.SetStoreSource(storeSource)
|
|
||||||
|
|
||||||
mgr := &serviceConfigManager{
|
|
||||||
svr: &Service{
|
|
||||||
aggregator: agg,
|
|
||||||
configSource: agg.ConfigSource(),
|
|
||||||
storeSource: storeSource,
|
|
||||||
reloadCommon: &v1.ClientCommonConfig{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected conflict error")
|
|
||||||
}
|
|
||||||
if !errors.Is(err, configmgmt.ErrConflict) {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServiceConfigManagerCreateStoreProxyKeepsStoreOnReloadFailure(t *testing.T) {
|
|
||||||
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
|
||||||
Path: filepath.Join(t.TempDir(), "store.json"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("new store source: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
mgr := &serviceConfigManager{
|
|
||||||
svr: &Service{
|
|
||||||
storeSource: storeSource,
|
|
||||||
reloadCommon: &v1.ClientCommonConfig{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err = mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected apply config error")
|
|
||||||
}
|
|
||||||
if !errors.Is(err, configmgmt.ErrApplyConfig) {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
if storeSource.GetProxy("p1") == nil {
|
|
||||||
t.Fatal("proxy should remain in store after reload failure")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServiceConfigManagerCreateStoreProxyStoreDisabled(t *testing.T) {
|
|
||||||
mgr := &serviceConfigManager{
|
|
||||||
svr: &Service{
|
|
||||||
reloadCommon: &v1.ClientCommonConfig{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("p1"))
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected store disabled error")
|
|
||||||
}
|
|
||||||
if !errors.Is(err, configmgmt.ErrStoreDisabled) {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestServiceConfigManagerCreateStoreProxyDoesNotPersistRuntimeDefaults(t *testing.T) {
|
|
||||||
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
|
||||||
Path: filepath.Join(t.TempDir(), "store.json"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("new store source: %v", err)
|
|
||||||
}
|
|
||||||
agg := source.NewAggregator(source.NewConfigSource())
|
|
||||||
agg.SetStoreSource(storeSource)
|
|
||||||
|
|
||||||
mgr := &serviceConfigManager{
|
|
||||||
svr: &Service{
|
|
||||||
aggregator: agg,
|
|
||||||
configSource: agg.ConfigSource(),
|
|
||||||
storeSource: storeSource,
|
|
||||||
reloadCommon: &v1.ClientCommonConfig{},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
persisted, err := mgr.CreateStoreProxy(newTestRawTCPProxyConfig("raw-proxy"))
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create store proxy: %v", err)
|
|
||||||
}
|
|
||||||
if persisted == nil {
|
|
||||||
t.Fatal("expected persisted proxy to be returned")
|
|
||||||
}
|
|
||||||
|
|
||||||
got := storeSource.GetProxy("raw-proxy")
|
|
||||||
if got == nil {
|
|
||||||
t.Fatal("proxy not found in store")
|
|
||||||
}
|
|
||||||
if got.GetBaseConfig().LocalIP != "" {
|
|
||||||
t.Fatalf("localIP was persisted with runtime default: %q", got.GetBaseConfig().LocalIP)
|
|
||||||
}
|
|
||||||
if got.GetBaseConfig().Transport.BandwidthLimitMode != "" {
|
|
||||||
t.Fatalf("bandwidthLimitMode was persisted with runtime default: %q", got.GetBaseConfig().Transport.BandwidthLimitMode)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,42 +0,0 @@
|
|||||||
package configmgmt
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/proxy"
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrInvalidArgument = errors.New("invalid argument")
|
|
||||||
ErrNotFound = errors.New("not found")
|
|
||||||
ErrConflict = errors.New("conflict")
|
|
||||||
ErrStoreDisabled = errors.New("store disabled")
|
|
||||||
ErrApplyConfig = errors.New("apply config failed")
|
|
||||||
)
|
|
||||||
|
|
||||||
type ConfigManager interface {
|
|
||||||
ReloadFromFile(strict bool) error
|
|
||||||
|
|
||||||
ReadConfigFile() (string, error)
|
|
||||||
WriteConfigFile(content []byte) error
|
|
||||||
|
|
||||||
GetProxyStatus() []*proxy.WorkingStatus
|
|
||||||
IsStoreProxyEnabled(name string) bool
|
|
||||||
StoreEnabled() bool
|
|
||||||
|
|
||||||
ListStoreProxies() ([]v1.ProxyConfigurer, error)
|
|
||||||
GetStoreProxy(name string) (v1.ProxyConfigurer, error)
|
|
||||||
CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
|
||||||
UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
|
||||||
DeleteStoreProxy(name string) error
|
|
||||||
|
|
||||||
ListStoreVisitors() ([]v1.VisitorConfigurer, error)
|
|
||||||
GetStoreVisitor(name string) (v1.VisitorConfigurer, error)
|
|
||||||
CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
|
||||||
UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
|
||||||
DeleteStoreVisitor(name string) error
|
|
||||||
|
|
||||||
GracefulClose(d time.Duration)
|
|
||||||
}
|
|
||||||
@@ -25,7 +25,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/naming"
|
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
"github.com/fatedier/frp/pkg/util/wait"
|
"github.com/fatedier/frp/pkg/util/wait"
|
||||||
@@ -157,8 +156,6 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
startMsg.ProxyName = naming.StripUserPrefix(ctl.sessionCtx.Common.User, startMsg.ProxyName)
|
|
||||||
|
|
||||||
// dispatch this work connection to related proxy
|
// dispatch this work connection to related proxy
|
||||||
ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg)
|
ctl.pm.HandleWorkConn(startMsg.ProxyName, workConn, &startMsg)
|
||||||
}
|
}
|
||||||
@@ -168,12 +165,11 @@ func (ctl *Control) handleNewProxyResp(m msg.Message) {
|
|||||||
inMsg := m.(*msg.NewProxyResp)
|
inMsg := m.(*msg.NewProxyResp)
|
||||||
// Server will return NewProxyResp message to each NewProxy message.
|
// Server will return NewProxyResp message to each NewProxy message.
|
||||||
// Start a new proxy handler if no error got
|
// Start a new proxy handler if no error got
|
||||||
proxyName := naming.StripUserPrefix(ctl.sessionCtx.Common.User, inMsg.ProxyName)
|
err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)
|
||||||
err := ctl.pm.StartProxy(proxyName, inMsg.RemoteAddr, inMsg.Error)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("[%s] start error: %v", proxyName, err)
|
xl.Warnf("[%s] start error: %v", inMsg.ProxyName, err)
|
||||||
} else {
|
} else {
|
||||||
xl.Infof("[%s] start proxy success", proxyName)
|
xl.Infof("[%s] start proxy success", inMsg.ProxyName)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,395 +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 (
|
|
||||||
"cmp"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/configmgmt"
|
|
||||||
"github.com/fatedier/frp/client/http/model"
|
|
||||||
"github.com/fatedier/frp/client/proxy"
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
|
||||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Controller handles HTTP API requests for frpc.
|
|
||||||
type Controller struct {
|
|
||||||
serverAddr string
|
|
||||||
manager configmgmt.ConfigManager
|
|
||||||
}
|
|
||||||
|
|
||||||
// ControllerParams contains parameters for creating an APIController.
|
|
||||||
type ControllerParams struct {
|
|
||||||
ServerAddr string
|
|
||||||
Manager configmgmt.ConfigManager
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewController(params ControllerParams) *Controller {
|
|
||||||
return &Controller{
|
|
||||||
serverAddr: params.ServerAddr,
|
|
||||||
manager: params.Manager,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) toHTTPError(err error) error {
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
code := http.StatusInternalServerError
|
|
||||||
switch {
|
|
||||||
case errors.Is(err, configmgmt.ErrInvalidArgument):
|
|
||||||
code = http.StatusBadRequest
|
|
||||||
case errors.Is(err, configmgmt.ErrNotFound), errors.Is(err, configmgmt.ErrStoreDisabled):
|
|
||||||
code = http.StatusNotFound
|
|
||||||
case errors.Is(err, configmgmt.ErrConflict):
|
|
||||||
code = http.StatusConflict
|
|
||||||
}
|
|
||||||
return httppkg.NewError(code, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
// 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)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.manager.ReloadFromFile(strictConfigMode); err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop handles POST /api/stop
|
|
||||||
func (c *Controller) Stop(ctx *httppkg.Context) (any, error) {
|
|
||||||
go c.manager.GracefulClose(100 * time.Millisecond)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status handles GET /api/status
|
|
||||||
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
|
||||||
res := make(model.StatusResp)
|
|
||||||
ps := c.manager.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 model.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) {
|
|
||||||
content, err := c.manager.ReadConfigFile()
|
|
||||||
if err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
return 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 := c.manager.WriteConfigFile(body); err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) model.ProxyStatusResp {
|
|
||||||
psr := model.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
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.manager.IsStoreProxyEnabled(status.Name) {
|
|
||||||
psr.Source = model.SourceStore
|
|
||||||
}
|
|
||||||
return psr
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) ListStoreProxies(ctx *httppkg.Context) (any, error) {
|
|
||||||
proxies, err := c.manager.ListStoreProxies()
|
|
||||||
if err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := model.ProxyListResp{Proxies: make([]model.ProxyDefinition, 0, len(proxies))}
|
|
||||||
for _, p := range proxies {
|
|
||||||
payload, err := model.ProxyDefinitionFromConfigurer(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
resp.Proxies = append(resp.Proxies, payload)
|
|
||||||
}
|
|
||||||
slices.SortFunc(resp.Proxies, func(a, b model.ProxyDefinition) int {
|
|
||||||
return cmp.Compare(a.Name, b.Name)
|
|
||||||
})
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) GetStoreProxy(ctx *httppkg.Context) (any, error) {
|
|
||||||
name := ctx.Param("name")
|
|
||||||
if name == "" {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
p, err := c.manager.GetStoreProxy(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, err := model.ProxyDefinitionFromConfigurer(p)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) CreateStoreProxy(ctx *httppkg.Context) (any, error) {
|
|
||||||
body, err := ctx.Body()
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload model.ProxyDefinition
|
|
||||||
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := payload.Validate("", false); err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
cfg, err := payload.ToConfigurer()
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
created, err := c.manager.CreateStoreProxy(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := model.ProxyDefinitionFromConfigurer(created)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) UpdateStoreProxy(ctx *httppkg.Context) (any, error) {
|
|
||||||
name := ctx.Param("name")
|
|
||||||
if name == "" {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ctx.Body()
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload model.ProxyDefinition
|
|
||||||
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := payload.Validate(name, true); err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
cfg, err := payload.ToConfigurer()
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
updated, err := c.manager.UpdateStoreProxy(name, cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := model.ProxyDefinitionFromConfigurer(updated)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) DeleteStoreProxy(ctx *httppkg.Context) (any, error) {
|
|
||||||
name := ctx.Param("name")
|
|
||||||
if name == "" {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.manager.DeleteStoreProxy(name); err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) ListStoreVisitors(ctx *httppkg.Context) (any, error) {
|
|
||||||
visitors, err := c.manager.ListStoreVisitors()
|
|
||||||
if err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := model.VisitorListResp{Visitors: make([]model.VisitorDefinition, 0, len(visitors))}
|
|
||||||
for _, v := range visitors {
|
|
||||||
payload, err := model.VisitorDefinitionFromConfigurer(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
resp.Visitors = append(resp.Visitors, payload)
|
|
||||||
}
|
|
||||||
slices.SortFunc(resp.Visitors, func(a, b model.VisitorDefinition) int {
|
|
||||||
return cmp.Compare(a.Name, b.Name)
|
|
||||||
})
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) GetStoreVisitor(ctx *httppkg.Context) (any, error) {
|
|
||||||
name := ctx.Param("name")
|
|
||||||
if name == "" {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, err := c.manager.GetStoreVisitor(name)
|
|
||||||
if err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, err := model.VisitorDefinitionFromConfigurer(v)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) CreateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
|
||||||
body, err := ctx.Body()
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload model.VisitorDefinition
|
|
||||||
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := payload.Validate("", false); err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
cfg, err := payload.ToConfigurer()
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
created, err := c.manager.CreateStoreVisitor(cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := model.VisitorDefinitionFromConfigurer(created)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) UpdateStoreVisitor(ctx *httppkg.Context) (any, error) {
|
|
||||||
name := ctx.Param("name")
|
|
||||||
if name == "" {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
body, err := ctx.Body()
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read body error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
var payload model.VisitorDefinition
|
|
||||||
if err := jsonx.Unmarshal(body, &payload); err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("parse JSON error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := payload.Validate(name, true); err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
cfg, err := payload.ToConfigurer()
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
updated, err := c.manager.UpdateStoreVisitor(name, cfg)
|
|
||||||
if err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
resp, err := model.VisitorDefinitionFromConfigurer(updated)
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
return resp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) DeleteStoreVisitor(ctx *httppkg.Context) (any, error) {
|
|
||||||
name := ctx.Param("name")
|
|
||||||
if name == "" {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "visitor name is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.manager.DeleteStoreVisitor(name); err != nil {
|
|
||||||
return nil, c.toHTTPError(err)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
@@ -1,531 +0,0 @@
|
|||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"testing"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/configmgmt"
|
|
||||||
"github.com/fatedier/frp/client/http/model"
|
|
||||||
"github.com/fatedier/frp/client/proxy"
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
|
||||||
)
|
|
||||||
|
|
||||||
type fakeConfigManager struct {
|
|
||||||
reloadFromFileFn func(strict bool) error
|
|
||||||
readConfigFileFn func() (string, error)
|
|
||||||
writeConfigFileFn func(content []byte) error
|
|
||||||
getProxyStatusFn func() []*proxy.WorkingStatus
|
|
||||||
isStoreProxyEnabledFn func(name string) bool
|
|
||||||
storeEnabledFn func() bool
|
|
||||||
|
|
||||||
listStoreProxiesFn func() ([]v1.ProxyConfigurer, error)
|
|
||||||
getStoreProxyFn func(name string) (v1.ProxyConfigurer, error)
|
|
||||||
createStoreProxyFn func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
|
||||||
updateStoreProxyFn func(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error)
|
|
||||||
deleteStoreProxyFn func(name string) error
|
|
||||||
listStoreVisitorsFn func() ([]v1.VisitorConfigurer, error)
|
|
||||||
getStoreVisitorFn func(name string) (v1.VisitorConfigurer, error)
|
|
||||||
createStoreVisitFn func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
|
||||||
updateStoreVisitFn func(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error)
|
|
||||||
deleteStoreVisitFn func(name string) error
|
|
||||||
gracefulCloseFn func(d time.Duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) ReloadFromFile(strict bool) error {
|
|
||||||
if m.reloadFromFileFn != nil {
|
|
||||||
return m.reloadFromFileFn(strict)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) ReadConfigFile() (string, error) {
|
|
||||||
if m.readConfigFileFn != nil {
|
|
||||||
return m.readConfigFileFn()
|
|
||||||
}
|
|
||||||
return "", nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) WriteConfigFile(content []byte) error {
|
|
||||||
if m.writeConfigFileFn != nil {
|
|
||||||
return m.writeConfigFileFn(content)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) GetProxyStatus() []*proxy.WorkingStatus {
|
|
||||||
if m.getProxyStatusFn != nil {
|
|
||||||
return m.getProxyStatusFn()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) IsStoreProxyEnabled(name string) bool {
|
|
||||||
if m.isStoreProxyEnabledFn != nil {
|
|
||||||
return m.isStoreProxyEnabledFn(name)
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) StoreEnabled() bool {
|
|
||||||
if m.storeEnabledFn != nil {
|
|
||||||
return m.storeEnabledFn()
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) ListStoreProxies() ([]v1.ProxyConfigurer, error) {
|
|
||||||
if m.listStoreProxiesFn != nil {
|
|
||||||
return m.listStoreProxiesFn()
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) GetStoreProxy(name string) (v1.ProxyConfigurer, error) {
|
|
||||||
if m.getStoreProxyFn != nil {
|
|
||||||
return m.getStoreProxyFn(name)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) CreateStoreProxy(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
|
||||||
if m.createStoreProxyFn != nil {
|
|
||||||
return m.createStoreProxyFn(cfg)
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) UpdateStoreProxy(name string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
|
||||||
if m.updateStoreProxyFn != nil {
|
|
||||||
return m.updateStoreProxyFn(name, cfg)
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) DeleteStoreProxy(name string) error {
|
|
||||||
if m.deleteStoreProxyFn != nil {
|
|
||||||
return m.deleteStoreProxyFn(name)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) ListStoreVisitors() ([]v1.VisitorConfigurer, error) {
|
|
||||||
if m.listStoreVisitorsFn != nil {
|
|
||||||
return m.listStoreVisitorsFn()
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) GetStoreVisitor(name string) (v1.VisitorConfigurer, error) {
|
|
||||||
if m.getStoreVisitorFn != nil {
|
|
||||||
return m.getStoreVisitorFn(name)
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) CreateStoreVisitor(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
|
||||||
if m.createStoreVisitFn != nil {
|
|
||||||
return m.createStoreVisitFn(cfg)
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) UpdateStoreVisitor(name string, cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
|
||||||
if m.updateStoreVisitFn != nil {
|
|
||||||
return m.updateStoreVisitFn(name, cfg)
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) DeleteStoreVisitor(name string) error {
|
|
||||||
if m.deleteStoreVisitFn != nil {
|
|
||||||
return m.deleteStoreVisitFn(name)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *fakeConfigManager) GracefulClose(d time.Duration) {
|
|
||||||
if m.gracefulCloseFn != nil {
|
|
||||||
m.gracefulCloseFn(d)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func newRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
|
||||||
return &v1.TCPProxyConfig{
|
|
||||||
ProxyBaseConfig: v1.ProxyBaseConfig{
|
|
||||||
Name: name,
|
|
||||||
Type: "tcp",
|
|
||||||
ProxyBackend: v1.ProxyBackend{
|
|
||||||
LocalPort: 10080,
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildProxyStatusRespStoreSourceEnabled(t *testing.T) {
|
|
||||||
status := &proxy.WorkingStatus{
|
|
||||||
Name: "shared-proxy",
|
|
||||||
Type: "tcp",
|
|
||||||
Phase: proxy.ProxyPhaseRunning,
|
|
||||||
RemoteAddr: ":8080",
|
|
||||||
Cfg: newRawTCPProxyConfig("shared-proxy"),
|
|
||||||
}
|
|
||||||
|
|
||||||
controller := &Controller{
|
|
||||||
serverAddr: "127.0.0.1",
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
isStoreProxyEnabledFn: func(name string) bool {
|
|
||||||
return name == "shared-proxy"
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
resp := controller.buildProxyStatusResp(status)
|
|
||||||
if resp.Source != "store" {
|
|
||||||
t.Fatalf("unexpected source: %q", resp.Source)
|
|
||||||
}
|
|
||||||
if resp.RemoteAddr != "127.0.0.1:8080" {
|
|
||||||
t.Fatalf("unexpected remote addr: %q", resp.RemoteAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReloadErrorMapping(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
err error
|
|
||||||
expectedCode int
|
|
||||||
}{
|
|
||||||
{name: "invalid arg", err: fmtError(configmgmt.ErrInvalidArgument, "bad cfg"), expectedCode: http.StatusBadRequest},
|
|
||||||
{name: "apply fail", err: fmtError(configmgmt.ErrApplyConfig, "reload failed"), expectedCode: http.StatusInternalServerError},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{reloadFromFileFn: func(bool) error { return tc.err }},
|
|
||||||
}
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/reload", nil))
|
|
||||||
_, err := controller.Reload(ctx)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error")
|
|
||||||
}
|
|
||||||
assertHTTPCode(t, err, tc.expectedCode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreProxyErrorMapping(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
err error
|
|
||||||
expectedCode int
|
|
||||||
}{
|
|
||||||
{name: "not found", err: fmtError(configmgmt.ErrNotFound, "not found"), expectedCode: http.StatusNotFound},
|
|
||||||
{name: "conflict", err: fmtError(configmgmt.ErrConflict, "exists"), expectedCode: http.StatusConflict},
|
|
||||||
{name: "internal", err: errors.New("persist failed"), expectedCode: http.StatusInternalServerError},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tc := range tests {
|
|
||||||
t.Run(tc.name, func(t *testing.T) {
|
|
||||||
body := []byte(`{"name":"shared-proxy","type":"tcp","tcp":{"localPort":10080}}`)
|
|
||||||
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(body))
|
|
||||||
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
updateStoreProxyFn: func(_ string, _ v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
|
||||||
return nil, tc.err
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := controller.UpdateStoreProxy(ctx)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error")
|
|
||||||
}
|
|
||||||
assertHTTPCode(t, err, tc.expectedCode)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreVisitorErrorMapping(t *testing.T) {
|
|
||||||
body := []byte(`{"name":"shared-visitor","type":"xtcp","xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret"}}`)
|
|
||||||
req := httptest.NewRequest(http.MethodDelete, "/api/store/visitors/shared-visitor", bytes.NewReader(body))
|
|
||||||
req = mux.SetURLVars(req, map[string]string{"name": "shared-visitor"})
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
deleteStoreVisitFn: func(string) error {
|
|
||||||
return fmtError(configmgmt.ErrStoreDisabled, "disabled")
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
_, err := controller.DeleteStoreVisitor(ctx)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error")
|
|
||||||
}
|
|
||||||
assertHTTPCode(t, err, http.StatusNotFound)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateStoreProxyIgnoresUnknownFields(t *testing.T) {
|
|
||||||
var gotName string
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
|
||||||
gotName = cfg.GetBaseConfig().Name
|
|
||||||
return cfg, nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
body := []byte(`{"name":"raw-proxy","type":"tcp","unexpected":"value","tcp":{"localPort":10080,"unknownInBlock":"value"}}`)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
resp, err := controller.CreateStoreProxy(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create store proxy: %v", err)
|
|
||||||
}
|
|
||||||
if gotName != "raw-proxy" {
|
|
||||||
t.Fatalf("unexpected proxy name: %q", gotName)
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, ok := resp.(model.ProxyDefinition)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("unexpected response type: %T", resp)
|
|
||||||
}
|
|
||||||
if payload.Type != "tcp" || payload.TCP == nil {
|
|
||||||
t.Fatalf("unexpected payload: %#v", payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateStoreVisitorIgnoresUnknownFields(t *testing.T) {
|
|
||||||
var gotName string
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
|
||||||
gotName = cfg.GetBaseConfig().Name
|
|
||||||
return cfg, nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
body := []byte(`{
|
|
||||||
"name":"raw-visitor","type":"xtcp","unexpected":"value",
|
|
||||||
"xtcp":{"serverName":"server","bindPort":10081,"secretKey":"secret","unknownInBlock":"value"}
|
|
||||||
}`)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
resp, err := controller.CreateStoreVisitor(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create store visitor: %v", err)
|
|
||||||
}
|
|
||||||
if gotName != "raw-visitor" {
|
|
||||||
t.Fatalf("unexpected visitor name: %q", gotName)
|
|
||||||
}
|
|
||||||
|
|
||||||
payload, ok := resp.(model.VisitorDefinition)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("unexpected response type: %T", resp)
|
|
||||||
}
|
|
||||||
if payload.Type != "xtcp" || payload.XTCP == nil {
|
|
||||||
t.Fatalf("unexpected payload: %#v", payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateStoreProxyPluginUnknownFieldsAreIgnored(t *testing.T) {
|
|
||||||
var gotPluginType string
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
createStoreProxyFn: func(cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
|
||||||
gotPluginType = cfg.GetBaseConfig().Plugin.Type
|
|
||||||
return cfg, nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
body := []byte(`{"name":"plugin-proxy","type":"tcp","tcp":{"plugin":{"type":"http2https","localAddr":"127.0.0.1:8080","unknownInPlugin":"value"}}}`)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/store/proxies", bytes.NewReader(body))
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
resp, err := controller.CreateStoreProxy(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create store proxy: %v", err)
|
|
||||||
}
|
|
||||||
if gotPluginType != "http2https" {
|
|
||||||
t.Fatalf("unexpected plugin type: %q", gotPluginType)
|
|
||||||
}
|
|
||||||
payload, ok := resp.(model.ProxyDefinition)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("unexpected response type: %T", resp)
|
|
||||||
}
|
|
||||||
if payload.TCP == nil {
|
|
||||||
t.Fatalf("unexpected response payload: %#v", payload)
|
|
||||||
}
|
|
||||||
pluginType := payload.TCP.Plugin.Type
|
|
||||||
|
|
||||||
if pluginType != "http2https" {
|
|
||||||
t.Fatalf("unexpected plugin type in response payload: %q", pluginType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCreateStoreVisitorPluginUnknownFieldsAreIgnored(t *testing.T) {
|
|
||||||
var gotPluginType string
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
createStoreVisitFn: func(cfg v1.VisitorConfigurer) (v1.VisitorConfigurer, error) {
|
|
||||||
gotPluginType = cfg.GetBaseConfig().Plugin.Type
|
|
||||||
return cfg, nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
body := []byte(`{
|
|
||||||
"name":"plugin-visitor","type":"stcp",
|
|
||||||
"stcp":{"serverName":"server","bindPort":10081,"plugin":{"type":"virtual_net","destinationIP":"10.0.0.1","unknownInPlugin":"value"}}
|
|
||||||
}`)
|
|
||||||
req := httptest.NewRequest(http.MethodPost, "/api/store/visitors", bytes.NewReader(body))
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
resp, err := controller.CreateStoreVisitor(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("create store visitor: %v", err)
|
|
||||||
}
|
|
||||||
if gotPluginType != "virtual_net" {
|
|
||||||
t.Fatalf("unexpected plugin type: %q", gotPluginType)
|
|
||||||
}
|
|
||||||
payload, ok := resp.(model.VisitorDefinition)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("unexpected response type: %T", resp)
|
|
||||||
}
|
|
||||||
if payload.STCP == nil {
|
|
||||||
t.Fatalf("unexpected response payload: %#v", payload)
|
|
||||||
}
|
|
||||||
pluginType := payload.STCP.Plugin.Type
|
|
||||||
|
|
||||||
if pluginType != "virtual_net" {
|
|
||||||
t.Fatalf("unexpected plugin type in response payload: %q", pluginType)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateStoreProxyRejectsMismatchedTypeBlock(t *testing.T) {
|
|
||||||
controller := &Controller{manager: &fakeConfigManager{}}
|
|
||||||
body := []byte(`{"name":"p1","type":"tcp","udp":{"localPort":10080}}`)
|
|
||||||
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body))
|
|
||||||
req = mux.SetURLVars(req, map[string]string{"name": "p1"})
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
_, err := controller.UpdateStoreProxy(ctx)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error")
|
|
||||||
}
|
|
||||||
assertHTTPCode(t, err, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateStoreProxyRejectsNameMismatch(t *testing.T) {
|
|
||||||
controller := &Controller{manager: &fakeConfigManager{}}
|
|
||||||
body := []byte(`{"name":"p2","type":"tcp","tcp":{"localPort":10080}}`)
|
|
||||||
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/p1", bytes.NewReader(body))
|
|
||||||
req = mux.SetURLVars(req, map[string]string{"name": "p1"})
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
_, err := controller.UpdateStoreProxy(ctx)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error")
|
|
||||||
}
|
|
||||||
assertHTTPCode(t, err, http.StatusBadRequest)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestListStoreProxiesReturnsSortedPayload(t *testing.T) {
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
listStoreProxiesFn: func() ([]v1.ProxyConfigurer, error) {
|
|
||||||
b := newRawTCPProxyConfig("b")
|
|
||||||
a := newRawTCPProxyConfig("a")
|
|
||||||
return []v1.ProxyConfigurer{b, a}, nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), httptest.NewRequest(http.MethodGet, "/api/store/proxies", nil))
|
|
||||||
|
|
||||||
resp, err := controller.ListStoreProxies(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("list store proxies: %v", err)
|
|
||||||
}
|
|
||||||
out, ok := resp.(model.ProxyListResp)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("unexpected response type: %T", resp)
|
|
||||||
}
|
|
||||||
if len(out.Proxies) != 2 {
|
|
||||||
t.Fatalf("unexpected proxy count: %d", len(out.Proxies))
|
|
||||||
}
|
|
||||||
if out.Proxies[0].Name != "a" || out.Proxies[1].Name != "b" {
|
|
||||||
t.Fatalf("proxies are not sorted by name: %#v", out.Proxies)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func fmtError(sentinel error, msg string) error {
|
|
||||||
return fmt.Errorf("%w: %s", sentinel, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
func assertHTTPCode(t *testing.T, err error, expected int) {
|
|
||||||
t.Helper()
|
|
||||||
var httpErr *httppkg.Error
|
|
||||||
if !errors.As(err, &httpErr) {
|
|
||||||
t.Fatalf("unexpected error type: %T", err)
|
|
||||||
}
|
|
||||||
if httpErr.Code != expected {
|
|
||||||
t.Fatalf("unexpected status code: got %d, want %d", httpErr.Code, expected)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateStoreProxyReturnsTypedPayload(t *testing.T) {
|
|
||||||
controller := &Controller{
|
|
||||||
manager: &fakeConfigManager{
|
|
||||||
updateStoreProxyFn: func(_ string, cfg v1.ProxyConfigurer) (v1.ProxyConfigurer, error) {
|
|
||||||
return cfg, nil
|
|
||||||
},
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
body := map[string]any{
|
|
||||||
"name": "shared-proxy",
|
|
||||||
"type": "tcp",
|
|
||||||
"tcp": map[string]any{
|
|
||||||
"localPort": 10080,
|
|
||||||
"remotePort": 7000,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
data, err := json.Marshal(body)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("marshal request: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodPut, "/api/store/proxies/shared-proxy", bytes.NewReader(data))
|
|
||||||
req = mux.SetURLVars(req, map[string]string{"name": "shared-proxy"})
|
|
||||||
ctx := httppkg.NewContext(httptest.NewRecorder(), req)
|
|
||||||
|
|
||||||
resp, err := controller.UpdateStoreProxy(ctx)
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("update store proxy: %v", err)
|
|
||||||
}
|
|
||||||
payload, ok := resp.(model.ProxyDefinition)
|
|
||||||
if !ok {
|
|
||||||
t.Fatalf("unexpected response type: %T", resp)
|
|
||||||
}
|
|
||||||
if payload.TCP == nil || payload.TCP.RemotePort != 7000 {
|
|
||||||
t.Fatalf("unexpected response payload: %#v", payload)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,148 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ProxyDefinition struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
|
|
||||||
TCP *v1.TCPProxyConfig `json:"tcp,omitempty"`
|
|
||||||
UDP *v1.UDPProxyConfig `json:"udp,omitempty"`
|
|
||||||
HTTP *v1.HTTPProxyConfig `json:"http,omitempty"`
|
|
||||||
HTTPS *v1.HTTPSProxyConfig `json:"https,omitempty"`
|
|
||||||
TCPMux *v1.TCPMuxProxyConfig `json:"tcpmux,omitempty"`
|
|
||||||
STCP *v1.STCPProxyConfig `json:"stcp,omitempty"`
|
|
||||||
SUDP *v1.SUDPProxyConfig `json:"sudp,omitempty"`
|
|
||||||
XTCP *v1.XTCPProxyConfig `json:"xtcp,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProxyDefinition) Validate(pathName string, isUpdate bool) error {
|
|
||||||
if strings.TrimSpace(p.Name) == "" {
|
|
||||||
return fmt.Errorf("proxy name is required")
|
|
||||||
}
|
|
||||||
if !IsProxyType(p.Type) {
|
|
||||||
return fmt.Errorf("invalid proxy type: %s", p.Type)
|
|
||||||
}
|
|
||||||
if isUpdate && pathName != "" && pathName != p.Name {
|
|
||||||
return fmt.Errorf("proxy name in URL must match name in body")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, blockType, blockCount := p.activeBlock()
|
|
||||||
if blockCount != 1 {
|
|
||||||
return fmt.Errorf("exactly one proxy type block is required")
|
|
||||||
}
|
|
||||||
if blockType != p.Type {
|
|
||||||
return fmt.Errorf("proxy type block %q does not match type %q", blockType, p.Type)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProxyDefinition) ToConfigurer() (v1.ProxyConfigurer, error) {
|
|
||||||
block, _, _ := p.activeBlock()
|
|
||||||
if block == nil {
|
|
||||||
return nil, fmt.Errorf("exactly one proxy type block is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := block
|
|
||||||
cfg.GetBaseConfig().Name = p.Name
|
|
||||||
cfg.GetBaseConfig().Type = p.Type
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func ProxyDefinitionFromConfigurer(cfg v1.ProxyConfigurer) (ProxyDefinition, error) {
|
|
||||||
if cfg == nil {
|
|
||||||
return ProxyDefinition{}, fmt.Errorf("proxy config is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
base := cfg.GetBaseConfig()
|
|
||||||
payload := ProxyDefinition{
|
|
||||||
Name: base.Name,
|
|
||||||
Type: base.Type,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch c := cfg.(type) {
|
|
||||||
case *v1.TCPProxyConfig:
|
|
||||||
payload.TCP = c
|
|
||||||
case *v1.UDPProxyConfig:
|
|
||||||
payload.UDP = c
|
|
||||||
case *v1.HTTPProxyConfig:
|
|
||||||
payload.HTTP = c
|
|
||||||
case *v1.HTTPSProxyConfig:
|
|
||||||
payload.HTTPS = c
|
|
||||||
case *v1.TCPMuxProxyConfig:
|
|
||||||
payload.TCPMux = c
|
|
||||||
case *v1.STCPProxyConfig:
|
|
||||||
payload.STCP = c
|
|
||||||
case *v1.SUDPProxyConfig:
|
|
||||||
payload.SUDP = c
|
|
||||||
case *v1.XTCPProxyConfig:
|
|
||||||
payload.XTCP = c
|
|
||||||
default:
|
|
||||||
return ProxyDefinition{}, fmt.Errorf("unsupported proxy configurer type %T", cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *ProxyDefinition) activeBlock() (v1.ProxyConfigurer, string, int) {
|
|
||||||
count := 0
|
|
||||||
var block v1.ProxyConfigurer
|
|
||||||
var blockType string
|
|
||||||
|
|
||||||
if p.TCP != nil {
|
|
||||||
count++
|
|
||||||
block = p.TCP
|
|
||||||
blockType = "tcp"
|
|
||||||
}
|
|
||||||
if p.UDP != nil {
|
|
||||||
count++
|
|
||||||
block = p.UDP
|
|
||||||
blockType = "udp"
|
|
||||||
}
|
|
||||||
if p.HTTP != nil {
|
|
||||||
count++
|
|
||||||
block = p.HTTP
|
|
||||||
blockType = "http"
|
|
||||||
}
|
|
||||||
if p.HTTPS != nil {
|
|
||||||
count++
|
|
||||||
block = p.HTTPS
|
|
||||||
blockType = "https"
|
|
||||||
}
|
|
||||||
if p.TCPMux != nil {
|
|
||||||
count++
|
|
||||||
block = p.TCPMux
|
|
||||||
blockType = "tcpmux"
|
|
||||||
}
|
|
||||||
if p.STCP != nil {
|
|
||||||
count++
|
|
||||||
block = p.STCP
|
|
||||||
blockType = "stcp"
|
|
||||||
}
|
|
||||||
if p.SUDP != nil {
|
|
||||||
count++
|
|
||||||
block = p.SUDP
|
|
||||||
blockType = "sudp"
|
|
||||||
}
|
|
||||||
if p.XTCP != nil {
|
|
||||||
count++
|
|
||||||
block = p.XTCP
|
|
||||||
blockType = "xtcp"
|
|
||||||
}
|
|
||||||
|
|
||||||
return block, blockType, count
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsProxyType(typ string) bool {
|
|
||||||
switch typ {
|
|
||||||
case "tcp", "udp", "http", "https", "tcpmux", "stcp", "sudp", "xtcp":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,107 +0,0 @@
|
|||||||
package model
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
type VisitorDefinition struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
|
|
||||||
STCP *v1.STCPVisitorConfig `json:"stcp,omitempty"`
|
|
||||||
SUDP *v1.SUDPVisitorConfig `json:"sudp,omitempty"`
|
|
||||||
XTCP *v1.XTCPVisitorConfig `json:"xtcp,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *VisitorDefinition) Validate(pathName string, isUpdate bool) error {
|
|
||||||
if strings.TrimSpace(p.Name) == "" {
|
|
||||||
return fmt.Errorf("visitor name is required")
|
|
||||||
}
|
|
||||||
if !IsVisitorType(p.Type) {
|
|
||||||
return fmt.Errorf("invalid visitor type: %s", p.Type)
|
|
||||||
}
|
|
||||||
if isUpdate && pathName != "" && pathName != p.Name {
|
|
||||||
return fmt.Errorf("visitor name in URL must match name in body")
|
|
||||||
}
|
|
||||||
|
|
||||||
_, blockType, blockCount := p.activeBlock()
|
|
||||||
if blockCount != 1 {
|
|
||||||
return fmt.Errorf("exactly one visitor type block is required")
|
|
||||||
}
|
|
||||||
if blockType != p.Type {
|
|
||||||
return fmt.Errorf("visitor type block %q does not match type %q", blockType, p.Type)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *VisitorDefinition) ToConfigurer() (v1.VisitorConfigurer, error) {
|
|
||||||
block, _, _ := p.activeBlock()
|
|
||||||
if block == nil {
|
|
||||||
return nil, fmt.Errorf("exactly one visitor type block is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := block
|
|
||||||
cfg.GetBaseConfig().Name = p.Name
|
|
||||||
cfg.GetBaseConfig().Type = p.Type
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func VisitorDefinitionFromConfigurer(cfg v1.VisitorConfigurer) (VisitorDefinition, error) {
|
|
||||||
if cfg == nil {
|
|
||||||
return VisitorDefinition{}, fmt.Errorf("visitor config is nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
base := cfg.GetBaseConfig()
|
|
||||||
payload := VisitorDefinition{
|
|
||||||
Name: base.Name,
|
|
||||||
Type: base.Type,
|
|
||||||
}
|
|
||||||
|
|
||||||
switch c := cfg.(type) {
|
|
||||||
case *v1.STCPVisitorConfig:
|
|
||||||
payload.STCP = c
|
|
||||||
case *v1.SUDPVisitorConfig:
|
|
||||||
payload.SUDP = c
|
|
||||||
case *v1.XTCPVisitorConfig:
|
|
||||||
payload.XTCP = c
|
|
||||||
default:
|
|
||||||
return VisitorDefinition{}, fmt.Errorf("unsupported visitor configurer type %T", cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return payload, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *VisitorDefinition) activeBlock() (v1.VisitorConfigurer, string, int) {
|
|
||||||
count := 0
|
|
||||||
var block v1.VisitorConfigurer
|
|
||||||
var blockType string
|
|
||||||
|
|
||||||
if p.STCP != nil {
|
|
||||||
count++
|
|
||||||
block = p.STCP
|
|
||||||
blockType = "stcp"
|
|
||||||
}
|
|
||||||
if p.SUDP != nil {
|
|
||||||
count++
|
|
||||||
block = p.SUDP
|
|
||||||
blockType = "sudp"
|
|
||||||
}
|
|
||||||
if p.XTCP != nil {
|
|
||||||
count++
|
|
||||||
block = p.XTCP
|
|
||||||
blockType = "xtcp"
|
|
||||||
}
|
|
||||||
return block, blockType, count
|
|
||||||
}
|
|
||||||
|
|
||||||
func IsVisitorType(typ string) bool {
|
|
||||||
switch typ {
|
|
||||||
case "stcp", "sudp", "xtcp":
|
|
||||||
return true
|
|
||||||
default:
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -16,7 +16,6 @@ package proxy
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
@@ -123,33 +122,6 @@ func (pxy *BaseProxy) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// wrapWorkConn applies rate limiting, encryption, and compression
|
|
||||||
// to a work connection based on the proxy's transport configuration.
|
|
||||||
// The returned recycle function should be called when the stream is no longer in use
|
|
||||||
// to return compression resources to the pool. It is safe to not call recycle,
|
|
||||||
// in which case resources will be garbage collected normally.
|
|
||||||
func (pxy *BaseProxy) wrapWorkConn(conn net.Conn, encKey []byte) (io.ReadWriteCloser, func(), error) {
|
|
||||||
var rwc io.ReadWriteCloser = conn
|
|
||||||
if pxy.limiter != nil {
|
|
||||||
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
|
||||||
return conn.Close()
|
|
||||||
})
|
|
||||||
}
|
|
||||||
if pxy.baseCfg.Transport.UseEncryption {
|
|
||||||
var err error
|
|
||||||
rwc, err = libio.WithEncryption(rwc, encKey)
|
|
||||||
if err != nil {
|
|
||||||
conn.Close()
|
|
||||||
return nil, nil, fmt.Errorf("create encryption stream error: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
var recycleFn func()
|
|
||||||
if pxy.baseCfg.Transport.UseCompression {
|
|
||||||
rwc, recycleFn = libio.WithCompressionFromPool(rwc)
|
|
||||||
}
|
|
||||||
return rwc, recycleFn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
func (pxy *BaseProxy) SetInWorkConnCallback(cb func(*v1.ProxyBaseConfig, net.Conn, *msg.StartWorkConn) bool) {
|
||||||
pxy.inWorkConnCallback = cb
|
pxy.inWorkConnCallback = cb
|
||||||
}
|
}
|
||||||
@@ -167,14 +139,30 @@ func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
|||||||
func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) {
|
func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWorkConn, encKey []byte) {
|
||||||
xl := pxy.xl
|
xl := pxy.xl
|
||||||
baseCfg := pxy.baseCfg
|
baseCfg := pxy.baseCfg
|
||||||
|
var (
|
||||||
|
remote io.ReadWriteCloser
|
||||||
|
err error
|
||||||
|
)
|
||||||
|
remote = workConn
|
||||||
|
if pxy.limiter != nil {
|
||||||
|
remote = libio.WrapReadWriteCloser(limit.NewReader(workConn, pxy.limiter), limit.NewWriter(workConn, pxy.limiter), func() error {
|
||||||
|
return workConn.Close()
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t",
|
xl.Tracef("handle tcp work connection, useEncryption: %t, useCompression: %t",
|
||||||
baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
|
baseCfg.Transport.UseEncryption, baseCfg.Transport.UseCompression)
|
||||||
|
if baseCfg.Transport.UseEncryption {
|
||||||
remote, recycleFn, err := pxy.wrapWorkConn(workConn, encKey)
|
remote, err = libio.WithEncryption(remote, encKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("wrap work connection: %v", err)
|
workConn.Close()
|
||||||
return
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var compressionResourceRecycleFn func()
|
||||||
|
if baseCfg.Transport.UseCompression {
|
||||||
|
remote, compressionResourceRecycleFn = libio.WithCompressionFromPool(remote)
|
||||||
}
|
}
|
||||||
|
|
||||||
// check if we need to send proxy protocol info
|
// check if we need to send proxy protocol info
|
||||||
@@ -190,6 +178,7 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
}
|
}
|
||||||
|
|
||||||
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
|
if baseCfg.Transport.ProxyProtocolVersion != "" && m.SrcAddr != "" && m.SrcPort != 0 {
|
||||||
|
// Use the common proxy protocol builder function
|
||||||
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
|
header := netpkg.BuildProxyProtocolHeaderStruct(connInfo.SrcAddr, connInfo.DstAddr, baseCfg.Transport.ProxyProtocolVersion)
|
||||||
connInfo.ProxyProtocolHeader = header
|
connInfo.ProxyProtocolHeader = header
|
||||||
}
|
}
|
||||||
@@ -198,18 +187,12 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
|
|
||||||
if pxy.proxyPlugin != nil {
|
if pxy.proxyPlugin != nil {
|
||||||
// if plugin is set, let plugin handle connection first
|
// if plugin is set, let plugin handle connection first
|
||||||
// Don't recycle compression resources here because plugins may
|
|
||||||
// retain the connection after Handle returns.
|
|
||||||
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
|
xl.Debugf("handle by plugin: %s", pxy.proxyPlugin.Name())
|
||||||
pxy.proxyPlugin.Handle(pxy.ctx, &connInfo)
|
pxy.proxyPlugin.Handle(pxy.ctx, &connInfo)
|
||||||
xl.Debugf("handle by plugin finished")
|
xl.Debugf("handle by plugin finished")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if recycleFn != nil {
|
|
||||||
defer recycleFn()
|
|
||||||
}
|
|
||||||
|
|
||||||
localConn, err := libnet.Dial(
|
localConn, err := libnet.Dial(
|
||||||
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
|
net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort)),
|
||||||
libnet.WithTimeout(10*time.Second),
|
libnet.WithTimeout(10*time.Second),
|
||||||
@@ -226,7 +209,6 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
if connInfo.ProxyProtocolHeader != nil {
|
if connInfo.ProxyProtocolHeader != nil {
|
||||||
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
localConn.Close()
|
|
||||||
xl.Errorf("write proxy protocol header to local conn error: %v", err)
|
xl.Errorf("write proxy protocol header to local conn error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -237,4 +219,7 @@ func (pxy *BaseProxy) HandleTCPWorkConnection(workConn net.Conn, m *msg.StartWor
|
|||||||
if len(errs) > 0 {
|
if len(errs) > 0 {
|
||||||
xl.Tracef("join connections errors: %v", errs)
|
xl.Tracef("join connections errors: %v", errs)
|
||||||
}
|
}
|
||||||
|
if compressionResourceRecycleFn != nil {
|
||||||
|
compressionResourceRecycleFn()
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -118,9 +118,9 @@ func (pm *Manager) HandleEvent(payload any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
|
func (pm *Manager) GetAllProxyStatus() []*WorkingStatus {
|
||||||
|
ps := make([]*WorkingStatus, 0)
|
||||||
pm.mu.RLock()
|
pm.mu.RLock()
|
||||||
defer pm.mu.RUnlock()
|
defer pm.mu.RUnlock()
|
||||||
ps := make([]*WorkingStatus, 0, len(pm.proxies))
|
|
||||||
for _, pxy := range pm.proxies {
|
for _, pxy := range pm.proxies {
|
||||||
ps = append(ps, pxy.GetStatus())
|
ps = append(ps, pxy.GetStatus())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -29,7 +29,6 @@ import (
|
|||||||
"github.com/fatedier/frp/client/health"
|
"github.com/fatedier/frp/client/health"
|
||||||
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/naming"
|
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
"github.com/fatedier/frp/pkg/vnet"
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
@@ -87,8 +86,6 @@ type Wrapper struct {
|
|||||||
|
|
||||||
xl *xlog.Logger
|
xl *xlog.Logger
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
|
|
||||||
wireName string
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewWrapper(
|
func NewWrapper(
|
||||||
@@ -116,7 +113,6 @@ func NewWrapper(
|
|||||||
vnetController: vnetController,
|
vnetController: vnetController,
|
||||||
xl: xl,
|
xl: xl,
|
||||||
ctx: xlog.NewContext(ctx, xl),
|
ctx: xlog.NewContext(ctx, xl),
|
||||||
wireName: naming.AddUserPrefix(clientCfg.User, baseInfo.Name),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 {
|
if baseInfo.HealthCheck.Type != "" && baseInfo.LocalPort > 0 {
|
||||||
@@ -186,7 +182,7 @@ func (pw *Wrapper) Stop() {
|
|||||||
func (pw *Wrapper) close() {
|
func (pw *Wrapper) close() {
|
||||||
_ = pw.handler(&event.CloseProxyPayload{
|
_ = pw.handler(&event.CloseProxyPayload{
|
||||||
CloseProxyMsg: &msg.CloseProxy{
|
CloseProxyMsg: &msg.CloseProxy{
|
||||||
ProxyName: pw.wireName,
|
ProxyName: pw.Name,
|
||||||
},
|
},
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -212,7 +208,6 @@ func (pw *Wrapper) checkWorker() {
|
|||||||
|
|
||||||
var newProxyMsg msg.NewProxy
|
var newProxyMsg msg.NewProxy
|
||||||
pw.Cfg.MarshalToMsg(&newProxyMsg)
|
pw.Cfg.MarshalToMsg(&newProxyMsg)
|
||||||
newProxyMsg.ProxyName = pw.wireName
|
|
||||||
pw.lastSendStartMsg = now
|
pw.lastSendStartMsg = now
|
||||||
_ = pw.handler(&event.StartProxyPayload{
|
_ = pw.handler(&event.StartProxyPayload{
|
||||||
NewProxyMsg: &newProxyMsg,
|
NewProxyMsg: &newProxyMsg,
|
||||||
|
|||||||
@@ -17,6 +17,7 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
@@ -24,15 +25,17 @@ import (
|
|||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/golib/errors"
|
"github.com/fatedier/golib/errors"
|
||||||
|
libio "github.com/fatedier/golib/io"
|
||||||
|
|
||||||
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/proto/udp"
|
"github.com/fatedier/frp/pkg/proto/udp"
|
||||||
|
"github.com/fatedier/frp/pkg/util/limit"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterProxyFactory(reflect.TypeFor[*v1.SUDPProxyConfig](), NewSUDPProxy)
|
RegisterProxyFactory(reflect.TypeOf(&v1.SUDPProxyConfig{}), NewSUDPProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
type SUDPProxy struct {
|
type SUDPProxy struct {
|
||||||
@@ -80,13 +83,27 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
xl := pxy.xl
|
xl := pxy.xl
|
||||||
xl.Infof("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
|
xl.Infof("incoming a new work connection for sudp proxy, %s", conn.RemoteAddr().String())
|
||||||
|
|
||||||
remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey)
|
var rwc io.ReadWriteCloser = conn
|
||||||
if err != nil {
|
var err error
|
||||||
xl.Errorf("wrap work connection: %v", err)
|
if pxy.limiter != nil {
|
||||||
return
|
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
||||||
|
return conn.Close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
|
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pxy.cfg.Transport.UseCompression {
|
||||||
|
rwc = libio.WithCompression(rwc)
|
||||||
|
}
|
||||||
|
conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
|
||||||
|
|
||||||
workConn := netpkg.WrapReadWriteCloserToConn(remote, conn)
|
workConn := conn
|
||||||
readCh := make(chan *msg.UDPPacket, 1024)
|
readCh := make(chan *msg.UDPPacket, 1024)
|
||||||
sendCh := make(chan msg.Message, 1024)
|
sendCh := make(chan msg.Message, 1024)
|
||||||
isClose := false
|
isClose := false
|
||||||
|
|||||||
@@ -17,21 +17,24 @@
|
|||||||
package proxy
|
package proxy
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"strconv"
|
"strconv"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/golib/errors"
|
"github.com/fatedier/golib/errors"
|
||||||
|
libio "github.com/fatedier/golib/io"
|
||||||
|
|
||||||
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/proto/udp"
|
"github.com/fatedier/frp/pkg/proto/udp"
|
||||||
|
"github.com/fatedier/frp/pkg/util/limit"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterProxyFactory(reflect.TypeFor[*v1.UDPProxyConfig](), NewUDPProxy)
|
RegisterProxyFactory(reflect.TypeOf(&v1.UDPProxyConfig{}), NewUDPProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
type UDPProxy struct {
|
type UDPProxy struct {
|
||||||
@@ -91,14 +94,28 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
// close resources related with old workConn
|
// close resources related with old workConn
|
||||||
pxy.Close()
|
pxy.Close()
|
||||||
|
|
||||||
remote, _, err := pxy.wrapWorkConn(conn, pxy.encryptionKey)
|
var rwc io.ReadWriteCloser = conn
|
||||||
if err != nil {
|
var err error
|
||||||
xl.Errorf("wrap work connection: %v", err)
|
if pxy.limiter != nil {
|
||||||
return
|
rwc = libio.WrapReadWriteCloser(limit.NewReader(conn, pxy.limiter), limit.NewWriter(conn, pxy.limiter), func() error {
|
||||||
|
return conn.Close()
|
||||||
|
})
|
||||||
}
|
}
|
||||||
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
|
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
||||||
|
if err != nil {
|
||||||
|
conn.Close()
|
||||||
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if pxy.cfg.Transport.UseCompression {
|
||||||
|
rwc = libio.WithCompression(rwc)
|
||||||
|
}
|
||||||
|
conn = netpkg.WrapReadWriteCloserToConn(rwc, conn)
|
||||||
|
|
||||||
pxy.mu.Lock()
|
pxy.mu.Lock()
|
||||||
pxy.workConn = netpkg.WrapReadWriteCloserToConn(remote, conn)
|
pxy.workConn = conn
|
||||||
pxy.readCh = make(chan *msg.UDPPacket, 1024)
|
pxy.readCh = make(chan *msg.UDPPacket, 1024)
|
||||||
pxy.sendCh = make(chan msg.Message, 1024)
|
pxy.sendCh = make(chan msg.Message, 1024)
|
||||||
pxy.closed = false
|
pxy.closed = false
|
||||||
@@ -112,7 +129,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
if errRet := errors.PanicToError(func() {
|
if errRet := errors.PanicToError(func() {
|
||||||
xl.Tracef("get udp package from workConn, len: %d", len(udpMsg.Content))
|
xl.Tracef("get udp package from workConn: %s", udpMsg.Content)
|
||||||
readCh <- &udpMsg
|
readCh <- &udpMsg
|
||||||
}); errRet != nil {
|
}); errRet != nil {
|
||||||
xl.Infof("reader goroutine for udp work connection closed: %v", errRet)
|
xl.Infof("reader goroutine for udp work connection closed: %v", errRet)
|
||||||
@@ -128,7 +145,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
for rawMsg := range sendCh {
|
for rawMsg := range sendCh {
|
||||||
switch m := rawMsg.(type) {
|
switch m := rawMsg.(type) {
|
||||||
case *msg.UDPPacket:
|
case *msg.UDPPacket:
|
||||||
xl.Tracef("send udp package to workConn, len: %d", len(m.Content))
|
xl.Tracef("send udp package to workConn: %s", m.Content)
|
||||||
case *msg.Ping:
|
case *msg.Ping:
|
||||||
xl.Tracef("send ping message to udp workConn")
|
xl.Tracef("send ping message to udp workConn")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,14 +27,13 @@ import (
|
|||||||
|
|
||||||
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/naming"
|
|
||||||
"github.com/fatedier/frp/pkg/nathole"
|
"github.com/fatedier/frp/pkg/nathole"
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
RegisterProxyFactory(reflect.TypeFor[*v1.XTCPProxyConfig](), NewXTCPProxy)
|
RegisterProxyFactory(reflect.TypeOf(&v1.XTCPProxyConfig{}), NewXTCPProxy)
|
||||||
}
|
}
|
||||||
|
|
||||||
type XTCPProxy struct {
|
type XTCPProxy struct {
|
||||||
@@ -86,7 +85,7 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
|
|||||||
transactionID := nathole.NewTransactionID()
|
transactionID := nathole.NewTransactionID()
|
||||||
natHoleClientMsg := &msg.NatHoleClient{
|
natHoleClientMsg := &msg.NatHoleClient{
|
||||||
TransactionID: transactionID,
|
TransactionID: transactionID,
|
||||||
ProxyName: naming.AddUserPrefix(pxy.clientCfg.User, pxy.cfg.Name),
|
ProxyName: pxy.cfg.Name,
|
||||||
Sid: natHoleSidMsg.Sid,
|
Sid: natHoleSidMsg.Sid,
|
||||||
MappedAddrs: prepareResult.Addrs,
|
MappedAddrs: prepareResult.Addrs,
|
||||||
AssistedAddrs: prepareResult.AssistedAddrs,
|
AssistedAddrs: prepareResult.AssistedAddrs,
|
||||||
|
|||||||
@@ -29,8 +29,6 @@ import (
|
|||||||
|
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
"github.com/fatedier/frp/pkg/auth"
|
"github.com/fatedier/frp/pkg/auth"
|
||||||
"github.com/fatedier/frp/pkg/config"
|
|
||||||
"github.com/fatedier/frp/pkg/config/source"
|
|
||||||
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"
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
@@ -63,11 +61,9 @@ func (e cancelErr) Error() string {
|
|||||||
|
|
||||||
// ServiceOptions contains options for creating a new client service.
|
// ServiceOptions contains options for creating a new client service.
|
||||||
type ServiceOptions struct {
|
type ServiceOptions struct {
|
||||||
Common *v1.ClientCommonConfig
|
Common *v1.ClientCommonConfig
|
||||||
|
ProxyCfgs []v1.ProxyConfigurer
|
||||||
// ConfigSourceAggregator manages internal config and optional store sources.
|
VisitorCfgs []v1.VisitorConfigurer
|
||||||
// It is required for creating a Service.
|
|
||||||
ConfigSourceAggregator *source.Aggregator
|
|
||||||
|
|
||||||
UnsafeFeatures *security.UnsafeFeatures
|
UnsafeFeatures *security.UnsafeFeatures
|
||||||
|
|
||||||
@@ -123,23 +119,11 @@ type Service struct {
|
|||||||
|
|
||||||
vnetController *vnet.Controller
|
vnetController *vnet.Controller
|
||||||
|
|
||||||
cfgMu sync.RWMutex
|
cfgMu sync.RWMutex
|
||||||
// reloadMu serializes reload transactions to keep reloadCommon and applied
|
common *v1.ClientCommonConfig
|
||||||
// config in sync across concurrent API operations.
|
proxyCfgs []v1.ProxyConfigurer
|
||||||
reloadMu sync.Mutex
|
visitorCfgs []v1.VisitorConfigurer
|
||||||
common *v1.ClientCommonConfig
|
clientSpec *msg.ClientSpec
|
||||||
// reloadCommon is used for filtering/defaulting during config-source reloads.
|
|
||||||
// It can be updated by /api/reload without mutating startup-only common behavior.
|
|
||||||
reloadCommon *v1.ClientCommonConfig
|
|
||||||
proxyCfgs []v1.ProxyConfigurer
|
|
||||||
visitorCfgs []v1.VisitorConfigurer
|
|
||||||
clientSpec *msg.ClientSpec
|
|
||||||
|
|
||||||
// aggregator manages multiple configuration sources.
|
|
||||||
// When set, the service watches for config changes and reloads automatically.
|
|
||||||
aggregator *source.Aggregator
|
|
||||||
configSource *source.ConfigSource
|
|
||||||
storeSource *source.StoreSource
|
|
||||||
|
|
||||||
unsafeFeatures *security.UnsafeFeatures
|
unsafeFeatures *security.UnsafeFeatures
|
||||||
|
|
||||||
@@ -176,39 +160,19 @@ func NewService(options ServiceOptions) (*Service, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if options.ConfigSourceAggregator == nil {
|
|
||||||
return nil, fmt.Errorf("config source aggregator is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
configSource := options.ConfigSourceAggregator.ConfigSource()
|
|
||||||
storeSource := options.ConfigSourceAggregator.StoreSource()
|
|
||||||
|
|
||||||
proxyCfgs, visitorCfgs, loadErr := options.ConfigSourceAggregator.Load()
|
|
||||||
if loadErr != nil {
|
|
||||||
return nil, fmt.Errorf("failed to load config from aggregator: %w", loadErr)
|
|
||||||
}
|
|
||||||
proxyCfgs, visitorCfgs = config.FilterClientConfigurers(options.Common, proxyCfgs, visitorCfgs)
|
|
||||||
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
|
||||||
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
|
||||||
|
|
||||||
s := &Service{
|
s := &Service{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
auth: authRuntime,
|
auth: authRuntime,
|
||||||
webServer: webServer,
|
webServer: webServer,
|
||||||
common: options.Common,
|
common: options.Common,
|
||||||
reloadCommon: options.Common,
|
|
||||||
configFilePath: options.ConfigFilePath,
|
configFilePath: options.ConfigFilePath,
|
||||||
unsafeFeatures: options.UnsafeFeatures,
|
unsafeFeatures: options.UnsafeFeatures,
|
||||||
proxyCfgs: proxyCfgs,
|
proxyCfgs: options.ProxyCfgs,
|
||||||
visitorCfgs: visitorCfgs,
|
visitorCfgs: options.VisitorCfgs,
|
||||||
clientSpec: options.ClientSpec,
|
clientSpec: options.ClientSpec,
|
||||||
aggregator: options.ConfigSourceAggregator,
|
|
||||||
configSource: configSource,
|
|
||||||
storeSource: storeSource,
|
|
||||||
connectorCreator: options.ConnectorCreator,
|
connectorCreator: options.ConnectorCreator,
|
||||||
handleWorkConnCb: options.HandleWorkConnCb,
|
handleWorkConnCb: options.HandleWorkConnCb,
|
||||||
}
|
}
|
||||||
|
|
||||||
if webServer != nil {
|
if webServer != nil {
|
||||||
webServer.RouteRegister(s.registerRouteHandlers)
|
webServer.RouteRegister(s.registerRouteHandlers)
|
||||||
}
|
}
|
||||||
@@ -439,35 +403,6 @@ func (svr *Service) UpdateAllConfigurer(proxyCfgs []v1.ProxyConfigurer, visitorC
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Service) UpdateConfigSource(
|
|
||||||
common *v1.ClientCommonConfig,
|
|
||||||
proxyCfgs []v1.ProxyConfigurer,
|
|
||||||
visitorCfgs []v1.VisitorConfigurer,
|
|
||||||
) error {
|
|
||||||
svr.reloadMu.Lock()
|
|
||||||
defer svr.reloadMu.Unlock()
|
|
||||||
|
|
||||||
cfgSource := svr.configSource
|
|
||||||
if cfgSource == nil {
|
|
||||||
return fmt.Errorf("config source is not available")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := cfgSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// Non-atomic update semantics: source has been updated at this point.
|
|
||||||
// Even if reload fails below, keep this common config for subsequent reloads.
|
|
||||||
svr.cfgMu.Lock()
|
|
||||||
svr.reloadCommon = common
|
|
||||||
svr.cfgMu.Unlock()
|
|
||||||
|
|
||||||
if err := svr.reloadConfigFromSourcesLocked(); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svr *Service) Close() {
|
func (svr *Service) Close() {
|
||||||
svr.GracefulClose(time.Duration(0))
|
svr.GracefulClose(time.Duration(0))
|
||||||
}
|
}
|
||||||
@@ -478,15 +413,6 @@ func (svr *Service) GracefulClose(d time.Duration) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Service) stop() {
|
func (svr *Service) stop() {
|
||||||
// Coordinate shutdown with reload/update paths that read source pointers.
|
|
||||||
svr.reloadMu.Lock()
|
|
||||||
if svr.aggregator != nil {
|
|
||||||
svr.aggregator = nil
|
|
||||||
}
|
|
||||||
svr.configSource = nil
|
|
||||||
svr.storeSource = nil
|
|
||||||
svr.reloadMu.Unlock()
|
|
||||||
|
|
||||||
svr.ctlMu.Lock()
|
svr.ctlMu.Lock()
|
||||||
defer svr.ctlMu.Unlock()
|
defer svr.ctlMu.Unlock()
|
||||||
if svr.ctl != nil {
|
if svr.ctl != nil {
|
||||||
@@ -527,35 +453,3 @@ type statusExporterImpl struct {
|
|||||||
func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
func (s *statusExporterImpl) GetProxyStatus(name string) (*proxy.WorkingStatus, bool) {
|
||||||
return s.getProxyStatusFunc(name)
|
return s.getProxyStatusFunc(name)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Service) reloadConfigFromSources() error {
|
|
||||||
svr.reloadMu.Lock()
|
|
||||||
defer svr.reloadMu.Unlock()
|
|
||||||
return svr.reloadConfigFromSourcesLocked()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (svr *Service) reloadConfigFromSourcesLocked() error {
|
|
||||||
aggregator := svr.aggregator
|
|
||||||
if aggregator == nil {
|
|
||||||
return errors.New("config aggregator is not initialized")
|
|
||||||
}
|
|
||||||
|
|
||||||
svr.cfgMu.RLock()
|
|
||||||
reloadCommon := svr.reloadCommon
|
|
||||||
svr.cfgMu.RUnlock()
|
|
||||||
|
|
||||||
proxies, visitors, err := aggregator.Load()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("reload config from sources failed: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxies, visitors = config.FilterClientConfigurers(reloadCommon, proxies, visitors)
|
|
||||||
proxies = config.CompleteProxyConfigurers(proxies)
|
|
||||||
visitors = config.CompleteVisitorConfigurers(visitors)
|
|
||||||
|
|
||||||
// Atomically replace the entire configuration
|
|
||||||
if err := svr.UpdateAllConfigurer(proxies, visitors); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,140 +0,0 @@
|
|||||||
package client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/source"
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestUpdateConfigSourceRollsBackReloadCommonOnReplaceAllFailure(t *testing.T) {
|
|
||||||
prevCommon := &v1.ClientCommonConfig{User: "old-user"}
|
|
||||||
newCommon := &v1.ClientCommonConfig{User: "new-user"}
|
|
||||||
|
|
||||||
svr := &Service{
|
|
||||||
configSource: source.NewConfigSource(),
|
|
||||||
reloadCommon: prevCommon,
|
|
||||||
}
|
|
||||||
|
|
||||||
invalidProxy := &v1.TCPProxyConfig{}
|
|
||||||
err := svr.UpdateConfigSource(newCommon, []v1.ProxyConfigurer{invalidProxy}, nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error, got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(err.Error(), "proxy name cannot be empty") {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if svr.reloadCommon != prevCommon {
|
|
||||||
t.Fatalf("reloadCommon should roll back on ReplaceAll failure")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestUpdateConfigSourceKeepsReloadCommonOnReloadFailure(t *testing.T) {
|
|
||||||
prevCommon := &v1.ClientCommonConfig{User: "old-user"}
|
|
||||||
newCommon := &v1.ClientCommonConfig{User: "new-user"}
|
|
||||||
|
|
||||||
svr := &Service{
|
|
||||||
// Keep configSource valid so ReplaceAll succeeds first.
|
|
||||||
configSource: source.NewConfigSource(),
|
|
||||||
reloadCommon: prevCommon,
|
|
||||||
// Keep aggregator nil to force reload failure.
|
|
||||||
aggregator: nil,
|
|
||||||
}
|
|
||||||
|
|
||||||
validProxy := &v1.TCPProxyConfig{
|
|
||||||
ProxyBaseConfig: v1.ProxyBaseConfig{
|
|
||||||
Name: "p1",
|
|
||||||
Type: "tcp",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
err := svr.UpdateConfigSource(newCommon, []v1.ProxyConfigurer{validProxy}, nil)
|
|
||||||
if err == nil {
|
|
||||||
t.Fatal("expected error, got nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
if !strings.Contains(err.Error(), "config aggregator is not initialized") {
|
|
||||||
t.Fatalf("unexpected error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if svr.reloadCommon != newCommon {
|
|
||||||
t.Fatalf("reloadCommon should keep new value on reload failure")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestReloadConfigFromSourcesDoesNotMutateStoreConfigs(t *testing.T) {
|
|
||||||
storeSource, err := source.NewStoreSource(source.StoreSourceConfig{
|
|
||||||
Path: filepath.Join(t.TempDir(), "store.json"),
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("new store source: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyCfg := &v1.TCPProxyConfig{
|
|
||||||
ProxyBaseConfig: v1.ProxyBaseConfig{
|
|
||||||
Name: "store-proxy",
|
|
||||||
Type: "tcp",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
visitorCfg := &v1.STCPVisitorConfig{
|
|
||||||
VisitorBaseConfig: v1.VisitorBaseConfig{
|
|
||||||
Name: "store-visitor",
|
|
||||||
Type: "stcp",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
if err := storeSource.AddProxy(proxyCfg); err != nil {
|
|
||||||
t.Fatalf("add proxy to store: %v", err)
|
|
||||||
}
|
|
||||||
if err := storeSource.AddVisitor(visitorCfg); err != nil {
|
|
||||||
t.Fatalf("add visitor to store: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
agg := source.NewAggregator(source.NewConfigSource())
|
|
||||||
agg.SetStoreSource(storeSource)
|
|
||||||
svr := &Service{
|
|
||||||
aggregator: agg,
|
|
||||||
configSource: agg.ConfigSource(),
|
|
||||||
storeSource: storeSource,
|
|
||||||
reloadCommon: &v1.ClientCommonConfig{},
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := svr.reloadConfigFromSources(); err != nil {
|
|
||||||
t.Fatalf("reload config from sources: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
gotProxy := storeSource.GetProxy("store-proxy")
|
|
||||||
if gotProxy == nil {
|
|
||||||
t.Fatalf("proxy not found in store")
|
|
||||||
}
|
|
||||||
if gotProxy.GetBaseConfig().LocalIP != "" {
|
|
||||||
t.Fatalf("store proxy localIP should stay empty, got %q", gotProxy.GetBaseConfig().LocalIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
gotVisitor := storeSource.GetVisitor("store-visitor")
|
|
||||||
if gotVisitor == nil {
|
|
||||||
t.Fatalf("visitor not found in store")
|
|
||||||
}
|
|
||||||
if gotVisitor.GetBaseConfig().BindAddr != "" {
|
|
||||||
t.Fatalf("store visitor bindAddr should stay empty, got %q", gotVisitor.GetBaseConfig().BindAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
svr.cfgMu.RLock()
|
|
||||||
defer svr.cfgMu.RUnlock()
|
|
||||||
|
|
||||||
if len(svr.proxyCfgs) != 1 {
|
|
||||||
t.Fatalf("expected 1 runtime proxy, got %d", len(svr.proxyCfgs))
|
|
||||||
}
|
|
||||||
if svr.proxyCfgs[0].GetBaseConfig().LocalIP != "127.0.0.1" {
|
|
||||||
t.Fatalf("runtime proxy localIP should be defaulted, got %q", svr.proxyCfgs[0].GetBaseConfig().LocalIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(svr.visitorCfgs) != 1 {
|
|
||||||
t.Fatalf("expected 1 runtime visitor, got %d", len(svr.visitorCfgs))
|
|
||||||
}
|
|
||||||
if svr.visitorCfgs[0].GetBaseConfig().BindAddr != "127.0.0.1" {
|
|
||||||
t.Fatalf("runtime visitor bindAddr should be defaulted, got %q", svr.visitorCfgs[0].GetBaseConfig().BindAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -15,12 +15,17 @@
|
|||||||
package visitor
|
package visitor
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
libio "github.com/fatedier/golib/io"
|
libio "github.com/fatedier/golib/io"
|
||||||
|
|
||||||
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/util/util"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -36,10 +41,10 @@ func (sv *STCPVisitor) Run() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go sv.acceptLoop(sv.l, "stcp local", sv.handleConn)
|
go sv.worker()
|
||||||
}
|
}
|
||||||
|
|
||||||
go sv.acceptLoop(sv.internalLn, "stcp internal", sv.handleConn)
|
go sv.internalConnWorker()
|
||||||
|
|
||||||
if sv.plugin != nil {
|
if sv.plugin != nil {
|
||||||
sv.plugin.Start()
|
sv.plugin.Start()
|
||||||
@@ -51,10 +56,35 @@ func (sv *STCPVisitor) Close() {
|
|||||||
sv.BaseVisitor.Close()
|
sv.BaseVisitor.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sv *STCPVisitor) worker() {
|
||||||
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
for {
|
||||||
|
conn, err := sv.l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("stcp local listener closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go sv.handleConn(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *STCPVisitor) internalConnWorker() {
|
||||||
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
for {
|
||||||
|
conn, err := sv.internalLn.Accept()
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("stcp internal listener closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go sv.handleConn(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
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
|
var tunnelErr error
|
||||||
defer func() {
|
defer func() {
|
||||||
|
// If there was an error and connection supports CloseWithError, use it
|
||||||
if tunnelErr != nil {
|
if tunnelErr != nil {
|
||||||
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
if eConn, ok := userConn.(interface{ CloseWithError(error) error }); ok {
|
||||||
_ = eConn.CloseWithError(tunnelErr)
|
_ = eConn.CloseWithError(tunnelErr)
|
||||||
@@ -65,21 +95,61 @@ func (sv *STCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
xl.Debugf("get a new stcp user connection")
|
xl.Debugf("get a new stcp user connection")
|
||||||
visitorConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig())
|
visitorConn, err := sv.helper.ConnectServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("dialRawVisitorConn error: %v", err)
|
|
||||||
tunnelErr = err
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer visitorConn.Close()
|
defer visitorConn.Close()
|
||||||
|
|
||||||
remote, recycleFn, err := wrapVisitorConn(visitorConn, sv.cfg.GetBaseConfig())
|
now := time.Now().Unix()
|
||||||
|
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||||
|
RunID: sv.helper.RunID(),
|
||||||
|
ProxyName: sv.cfg.ServerName,
|
||||||
|
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||||
|
Timestamp: now,
|
||||||
|
UseEncryption: sv.cfg.Transport.UseEncryption,
|
||||||
|
UseCompression: sv.cfg.Transport.UseCompression,
|
||||||
|
}
|
||||||
|
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("wrapVisitorConn error: %v", err)
|
xl.Warnf("send newVisitorConnMsg to server error: %v", err)
|
||||||
tunnelErr = err
|
tunnelErr = err
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
defer recycleFn()
|
|
||||||
|
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
||||||
|
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("get newVisitorConnRespMsg error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
if newVisitorConnRespMsg.Error != "" {
|
||||||
|
xl.Warnf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||||
|
tunnelErr = fmt.Errorf("%s", newVisitorConnRespMsg.Error)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
var remote io.ReadWriteCloser
|
||||||
|
remote = visitorConn
|
||||||
|
if sv.cfg.Transport.UseEncryption {
|
||||||
|
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
tunnelErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if sv.cfg.Transport.UseCompression {
|
||||||
|
var recycleFn func()
|
||||||
|
remote, recycleFn = libio.WithCompressionFromPool(remote)
|
||||||
|
defer recycleFn()
|
||||||
|
}
|
||||||
|
|
||||||
libio.Join(userConn, remote)
|
libio.Join(userConn, remote)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -16,17 +16,20 @@ package visitor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"strconv"
|
"strconv"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/golib/errors"
|
"github.com/fatedier/golib/errors"
|
||||||
|
libio "github.com/fatedier/golib/io"
|
||||||
|
|
||||||
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/proto/udp"
|
"github.com/fatedier/frp/pkg/proto/udp"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -72,7 +75,6 @@ func (sv *SUDPVisitor) dispatcher() {
|
|||||||
|
|
||||||
var (
|
var (
|
||||||
visitorConn net.Conn
|
visitorConn net.Conn
|
||||||
recycleFn func()
|
|
||||||
err error
|
err error
|
||||||
|
|
||||||
firstPacket *msg.UDPPacket
|
firstPacket *msg.UDPPacket
|
||||||
@@ -90,17 +92,14 @@ func (sv *SUDPVisitor) dispatcher() {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
visitorConn, recycleFn, err = sv.getNewVisitorConn()
|
visitorConn, err = sv.getNewVisitorConn()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err)
|
xl.Warnf("newVisitorConn to frps error: %v, try to reconnect", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
// visitorConn always be closed when worker done.
|
// visitorConn always be closed when worker done.
|
||||||
func() {
|
sv.worker(visitorConn, firstPacket)
|
||||||
defer recycleFn()
|
|
||||||
sv.worker(visitorConn, firstPacket)
|
|
||||||
}()
|
|
||||||
|
|
||||||
select {
|
select {
|
||||||
case <-sv.checkCloseCh:
|
case <-sv.checkCloseCh:
|
||||||
@@ -147,7 +146,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
|||||||
case *msg.UDPPacket:
|
case *msg.UDPPacket:
|
||||||
if errRet := errors.PanicToError(func() {
|
if errRet := errors.PanicToError(func() {
|
||||||
sv.readCh <- m
|
sv.readCh <- m
|
||||||
xl.Tracef("frpc visitor get udp packet from workConn, len: %d", len(m.Content))
|
xl.Tracef("frpc visitor get udp packet from workConn: %s", m.Content)
|
||||||
}); errRet != nil {
|
}); errRet != nil {
|
||||||
xl.Infof("reader goroutine for udp work connection closed")
|
xl.Infof("reader goroutine for udp work connection closed")
|
||||||
return
|
return
|
||||||
@@ -169,7 +168,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
|||||||
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
xl.Tracef("send udp package to workConn, len: %d", len(firstPacket.Content))
|
xl.Tracef("send udp package to workConn: %s", firstPacket.Content)
|
||||||
}
|
}
|
||||||
|
|
||||||
for {
|
for {
|
||||||
@@ -184,7 +183,7 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
|||||||
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
xl.Warnf("sender goroutine for udp work connection closed: %v", errRet)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
xl.Tracef("send udp package to workConn, len: %d", len(udpMsg.Content))
|
xl.Tracef("send udp package to workConn: %s", udpMsg.Content)
|
||||||
case <-closeCh:
|
case <-closeCh:
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -198,17 +197,52 @@ func (sv *SUDPVisitor) worker(workConn net.Conn, firstPacket *msg.UDPPacket) {
|
|||||||
xl.Infof("sudp worker is closed")
|
xl.Infof("sudp worker is closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, func(), error) {
|
func (sv *SUDPVisitor) getNewVisitorConn() (net.Conn, error) {
|
||||||
rawConn, err := sv.dialRawVisitorConn(sv.cfg.GetBaseConfig())
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
visitorConn, err := sv.helper.ConnectServer()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, func() {}, err
|
return nil, fmt.Errorf("frpc connect frps error: %v", err)
|
||||||
}
|
}
|
||||||
rwc, recycleFn, err := wrapVisitorConn(rawConn, sv.cfg.GetBaseConfig())
|
|
||||||
|
now := time.Now().Unix()
|
||||||
|
newVisitorConnMsg := &msg.NewVisitorConn{
|
||||||
|
RunID: sv.helper.RunID(),
|
||||||
|
ProxyName: sv.cfg.ServerName,
|
||||||
|
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||||
|
Timestamp: now,
|
||||||
|
UseEncryption: sv.cfg.Transport.UseEncryption,
|
||||||
|
UseCompression: sv.cfg.Transport.UseCompression,
|
||||||
|
}
|
||||||
|
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
rawConn.Close()
|
return nil, fmt.Errorf("frpc send newVisitorConnMsg to frps error: %v", err)
|
||||||
return nil, func() {}, err
|
|
||||||
}
|
}
|
||||||
return netpkg.WrapReadWriteCloserToConn(rwc, rawConn), recycleFn, nil
|
|
||||||
|
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
||||||
|
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
||||||
|
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
||||||
|
if err != nil {
|
||||||
|
return nil, fmt.Errorf("frpc read newVisitorConnRespMsg error: %v", err)
|
||||||
|
}
|
||||||
|
_ = visitorConn.SetReadDeadline(time.Time{})
|
||||||
|
|
||||||
|
if newVisitorConnRespMsg.Error != "" {
|
||||||
|
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
||||||
|
}
|
||||||
|
|
||||||
|
var remote io.ReadWriteCloser
|
||||||
|
remote = visitorConn
|
||||||
|
if sv.cfg.Transport.UseEncryption {
|
||||||
|
remote, err = libio.WithEncryption(remote, []byte(sv.cfg.SecretKey))
|
||||||
|
if err != nil {
|
||||||
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sv.cfg.Transport.UseCompression {
|
||||||
|
remote = libio.WithCompression(remote)
|
||||||
|
}
|
||||||
|
return netpkg.WrapReadWriteCloserToConn(remote, visitorConn), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (sv *SUDPVisitor) Close() {
|
func (sv *SUDPVisitor) Close() {
|
||||||
|
|||||||
@@ -16,21 +16,13 @@ package visitor
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
|
||||||
|
|
||||||
libio "github.com/fatedier/golib/io"
|
|
||||||
|
|
||||||
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/naming"
|
|
||||||
plugin "github.com/fatedier/frp/pkg/plugin/visitor"
|
plugin "github.com/fatedier/frp/pkg/plugin/visitor"
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
"github.com/fatedier/frp/pkg/vnet"
|
"github.com/fatedier/frp/pkg/vnet"
|
||||||
)
|
)
|
||||||
@@ -127,18 +119,6 @@ func (v *BaseVisitor) AcceptConn(conn net.Conn) error {
|
|||||||
return v.internalLn.PutConn(conn)
|
return v.internalLn.PutConn(conn)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *BaseVisitor) acceptLoop(l net.Listener, name string, handleConn func(net.Conn)) {
|
|
||||||
xl := xlog.FromContextSafe(v.ctx)
|
|
||||||
for {
|
|
||||||
conn, err := l.Accept()
|
|
||||||
if err != nil {
|
|
||||||
xl.Warnf("%s listener closed", name)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
go handleConn(conn)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (v *BaseVisitor) Close() {
|
func (v *BaseVisitor) Close() {
|
||||||
if v.l != nil {
|
if v.l != nil {
|
||||||
v.l.Close()
|
v.l.Close()
|
||||||
@@ -150,57 +130,3 @@ func (v *BaseVisitor) Close() {
|
|||||||
v.plugin.Close()
|
v.plugin.Close()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *BaseVisitor) dialRawVisitorConn(cfg *v1.VisitorBaseConfig) (net.Conn, error) {
|
|
||||||
visitorConn, err := v.helper.ConnectServer()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("connect to server error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
now := time.Now().Unix()
|
|
||||||
targetProxyName := naming.BuildTargetServerProxyName(v.clientCfg.User, cfg.ServerUser, cfg.ServerName)
|
|
||||||
newVisitorConnMsg := &msg.NewVisitorConn{
|
|
||||||
RunID: v.helper.RunID(),
|
|
||||||
ProxyName: targetProxyName,
|
|
||||||
SignKey: util.GetAuthKey(cfg.SecretKey, now),
|
|
||||||
Timestamp: now,
|
|
||||||
UseEncryption: cfg.Transport.UseEncryption,
|
|
||||||
UseCompression: cfg.Transport.UseCompression,
|
|
||||||
}
|
|
||||||
err = msg.WriteMsg(visitorConn, newVisitorConnMsg)
|
|
||||||
if err != nil {
|
|
||||||
visitorConn.Close()
|
|
||||||
return nil, fmt.Errorf("send newVisitorConnMsg to server error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var newVisitorConnRespMsg msg.NewVisitorConnResp
|
|
||||||
_ = visitorConn.SetReadDeadline(time.Now().Add(10 * time.Second))
|
|
||||||
err = msg.ReadMsgInto(visitorConn, &newVisitorConnRespMsg)
|
|
||||||
if err != nil {
|
|
||||||
visitorConn.Close()
|
|
||||||
return nil, fmt.Errorf("read newVisitorConnRespMsg error: %v", err)
|
|
||||||
}
|
|
||||||
_ = visitorConn.SetReadDeadline(time.Time{})
|
|
||||||
|
|
||||||
if newVisitorConnRespMsg.Error != "" {
|
|
||||||
visitorConn.Close()
|
|
||||||
return nil, fmt.Errorf("start new visitor connection error: %s", newVisitorConnRespMsg.Error)
|
|
||||||
}
|
|
||||||
return visitorConn, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrapVisitorConn(conn io.ReadWriteCloser, cfg *v1.VisitorBaseConfig) (io.ReadWriteCloser, func(), error) {
|
|
||||||
rwc := conn
|
|
||||||
if cfg.Transport.UseEncryption {
|
|
||||||
var err error
|
|
||||||
rwc, err = libio.WithEncryption(rwc, []byte(cfg.SecretKey))
|
|
||||||
if err != nil {
|
|
||||||
return nil, func() {}, fmt.Errorf("create encryption stream error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
recycleFn := func() {}
|
|
||||||
if cfg.Transport.UseCompression {
|
|
||||||
rwc, recycleFn = libio.WithCompressionFromPool(rwc)
|
|
||||||
}
|
|
||||||
return rwc, recycleFn, nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -31,7 +31,6 @@ import (
|
|||||||
|
|
||||||
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/naming"
|
|
||||||
"github.com/fatedier/frp/pkg/nathole"
|
"github.com/fatedier/frp/pkg/nathole"
|
||||||
"github.com/fatedier/frp/pkg/transport"
|
"github.com/fatedier/frp/pkg/transport"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
@@ -65,10 +64,10 @@ func (sv *XTCPVisitor) Run() (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
go sv.acceptLoop(sv.l, "xtcp local", sv.handleConn)
|
go sv.worker()
|
||||||
}
|
}
|
||||||
|
|
||||||
go sv.acceptLoop(sv.internalLn, "xtcp internal", sv.handleConn)
|
go sv.internalConnWorker()
|
||||||
go sv.processTunnelStartEvents()
|
go sv.processTunnelStartEvents()
|
||||||
if sv.cfg.KeepTunnelOpen {
|
if sv.cfg.KeepTunnelOpen {
|
||||||
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
|
sv.retryLimiter = rate.NewLimiter(rate.Every(time.Hour/time.Duration(sv.cfg.MaxRetriesAnHour)), sv.cfg.MaxRetriesAnHour)
|
||||||
@@ -93,6 +92,30 @@ func (sv *XTCPVisitor) Close() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (sv *XTCPVisitor) worker() {
|
||||||
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
for {
|
||||||
|
conn, err := sv.l.Accept()
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("xtcp local listener closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go sv.handleConn(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (sv *XTCPVisitor) internalConnWorker() {
|
||||||
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
|
for {
|
||||||
|
conn, err := sv.internalLn.Accept()
|
||||||
|
if err != nil {
|
||||||
|
xl.Warnf("xtcp internal listener closed")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
go sv.handleConn(conn)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func (sv *XTCPVisitor) processTunnelStartEvents() {
|
func (sv *XTCPVisitor) processTunnelStartEvents() {
|
||||||
for {
|
for {
|
||||||
select {
|
select {
|
||||||
@@ -182,14 +205,20 @@ func (sv *XTCPVisitor) handleConn(userConn net.Conn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
muxConnRWCloser, recycleFn, err := wrapVisitorConn(tunnelConn, sv.cfg.GetBaseConfig())
|
var muxConnRWCloser io.ReadWriteCloser = tunnelConn
|
||||||
if err != nil {
|
if sv.cfg.Transport.UseEncryption {
|
||||||
xl.Errorf("%v", err)
|
muxConnRWCloser, err = libio.WithEncryption(muxConnRWCloser, []byte(sv.cfg.SecretKey))
|
||||||
tunnelConn.Close()
|
if err != nil {
|
||||||
tunnelErr = err
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
return
|
tunnelErr = err
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sv.cfg.Transport.UseCompression {
|
||||||
|
var recycleFn func()
|
||||||
|
muxConnRWCloser, recycleFn = libio.WithCompressionFromPool(muxConnRWCloser)
|
||||||
|
defer recycleFn()
|
||||||
}
|
}
|
||||||
defer recycleFn()
|
|
||||||
|
|
||||||
_, _, errs := libio.Join(userConn, muxConnRWCloser)
|
_, _, errs := libio.Join(userConn, muxConnRWCloser)
|
||||||
xl.Debugf("join connections closed")
|
xl.Debugf("join connections closed")
|
||||||
@@ -251,9 +280,8 @@ func (sv *XTCPVisitor) getTunnelConn(ctx context.Context) (net.Conn, error) {
|
|||||||
// 4. Create a tunnel session using an underlying UDP connection.
|
// 4. Create a tunnel session using an underlying UDP connection.
|
||||||
func (sv *XTCPVisitor) makeNatHole() {
|
func (sv *XTCPVisitor) makeNatHole() {
|
||||||
xl := xlog.FromContextSafe(sv.ctx)
|
xl := xlog.FromContextSafe(sv.ctx)
|
||||||
targetProxyName := naming.BuildTargetServerProxyName(sv.clientCfg.User, sv.cfg.ServerUser, sv.cfg.ServerName)
|
|
||||||
xl.Tracef("makeNatHole start")
|
xl.Tracef("makeNatHole start")
|
||||||
if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), targetProxyName, 5*time.Second); err != nil {
|
if err := nathole.PreCheck(sv.ctx, sv.helper.MsgTransporter(), sv.cfg.ServerName, 5*time.Second); err != nil {
|
||||||
xl.Warnf("nathole precheck error: %v", err)
|
xl.Warnf("nathole precheck error: %v", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -282,7 +310,7 @@ func (sv *XTCPVisitor) makeNatHole() {
|
|||||||
transactionID := nathole.NewTransactionID()
|
transactionID := nathole.NewTransactionID()
|
||||||
natHoleVisitorMsg := &msg.NatHoleVisitor{
|
natHoleVisitorMsg := &msg.NatHoleVisitor{
|
||||||
TransactionID: transactionID,
|
TransactionID: transactionID,
|
||||||
ProxyName: targetProxyName,
|
ProxyName: sv.cfg.ServerName,
|
||||||
Protocol: sv.cfg.Protocol,
|
Protocol: sv.cfg.Protocol,
|
||||||
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
SignKey: util.GetAuthKey(sv.cfg.SecretKey, now),
|
||||||
Timestamp: now,
|
Timestamp: now,
|
||||||
@@ -343,7 +371,6 @@ func (ks *KCPTunnelSession) Init(listenConn *net.UDPConn, raddr *net.UDPAddr) er
|
|||||||
}
|
}
|
||||||
remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
|
remote, err := netpkg.NewKCPConnFromUDP(lConn, true, raddr.String())
|
||||||
if err != nil {
|
if err != nil {
|
||||||
lConn.Close()
|
|
||||||
return fmt.Errorf("create kcp connection from udp connection error: %v", err)
|
return fmt.Errorf("create kcp connection from udp connection error: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -47,7 +47,7 @@ var natholeDiscoveryCmd = &cobra.Command{
|
|||||||
Use: "discover",
|
Use: "discover",
|
||||||
Short: "Discover nathole information from stun server",
|
Short: "Discover nathole information from stun server",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
// ignore error here, because we can use command line parameters
|
// ignore error here, because we can use command line pameters
|
||||||
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
cfg = &v1.ClientCommonConfig{}
|
cfg = &v1.ClientCommonConfig{}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
"github.com/fatedier/frp/pkg/config/source"
|
|
||||||
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/policy/security"
|
||||||
@@ -87,14 +86,13 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Complete(clientCfg.User)
|
||||||
c.GetBaseConfig().Type = name
|
c.GetBaseConfig().Type = name
|
||||||
c.Complete()
|
if err := validation.ValidateProxyConfigurerForClient(c); err != nil {
|
||||||
proxyCfg := c
|
|
||||||
if err := validation.ValidateProxyConfigurerForClient(proxyCfg); err != nil {
|
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err := startService(clientCfg, []v1.ProxyConfigurer{proxyCfg}, nil, unsafeFeatures, "")
|
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -119,14 +117,13 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
c.Complete(clientCfg)
|
||||||
c.GetBaseConfig().Type = name
|
c.GetBaseConfig().Type = name
|
||||||
c.Complete()
|
if err := validation.ValidateVisitorConfigurer(c); err != nil {
|
||||||
visitorCfg := c
|
|
||||||
if err := validation.ValidateVisitorConfigurer(visitorCfg); err != nil {
|
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{visitorCfg}, unsafeFeatures, "")
|
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -134,18 +131,3 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
},
|
},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func startService(
|
|
||||||
cfg *v1.ClientCommonConfig,
|
|
||||||
proxyCfgs []v1.ProxyConfigurer,
|
|
||||||
visitorCfgs []v1.VisitorConfigurer,
|
|
||||||
unsafeFeatures *security.UnsafeFeatures,
|
|
||||||
cfgFile string,
|
|
||||||
) error {
|
|
||||||
configSource := source.NewConfigSource()
|
|
||||||
if err := configSource.ReplaceAll(proxyCfgs, visitorCfgs); err != nil {
|
|
||||||
return fmt.Errorf("failed to set config source: %w", err)
|
|
||||||
}
|
|
||||||
aggregator := source.NewAggregator(configSource)
|
|
||||||
return startServiceWithAggregator(cfg, aggregator, unsafeFeatures, cfgFile)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import (
|
|||||||
|
|
||||||
"github.com/fatedier/frp/client"
|
"github.com/fatedier/frp/client"
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
"github.com/fatedier/frp/pkg/config/source"
|
|
||||||
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/policy/featuregate"
|
||||||
@@ -121,64 +120,22 @@ func handleTermSignal(svr *client.Service) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||||
// Load configuration
|
cfg, proxyCfgs, visitorCfgs, isLegacyFormat, err := config.LoadClientConfig(cfgFilePath, strictConfigMode)
|
||||||
result, err := config.LoadClientConfigResult(cfgFilePath, strictConfigMode)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if result.IsLegacyFormat {
|
if isLegacyFormat {
|
||||||
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
|
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
|
||||||
"please use yaml/json/toml format instead!\n")
|
"please use yaml/json/toml format instead!\n")
|
||||||
}
|
}
|
||||||
|
|
||||||
if len(result.Common.FeatureGates) > 0 {
|
if len(cfg.FeatureGates) > 0 {
|
||||||
if err := featuregate.SetFromMap(result.Common.FeatureGates); err != nil {
|
if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return runClientWithAggregator(result, unsafeFeatures, cfgFilePath)
|
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
||||||
}
|
|
||||||
|
|
||||||
// runClientWithAggregator runs the client using the internal source aggregator.
|
|
||||||
func runClientWithAggregator(result *config.ClientConfigLoadResult, unsafeFeatures *security.UnsafeFeatures, cfgFilePath string) error {
|
|
||||||
configSource := source.NewConfigSource()
|
|
||||||
if err := configSource.ReplaceAll(result.Proxies, result.Visitors); err != nil {
|
|
||||||
return fmt.Errorf("failed to set config source: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var storeSource *source.StoreSource
|
|
||||||
|
|
||||||
if result.Common.Store.IsEnabled() {
|
|
||||||
storePath := result.Common.Store.Path
|
|
||||||
if storePath != "" && cfgFilePath != "" && !filepath.IsAbs(storePath) {
|
|
||||||
storePath = filepath.Join(filepath.Dir(cfgFilePath), storePath)
|
|
||||||
}
|
|
||||||
|
|
||||||
s, err := source.NewStoreSource(source.StoreSourceConfig{
|
|
||||||
Path: storePath,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create store source: %w", err)
|
|
||||||
}
|
|
||||||
storeSource = s
|
|
||||||
}
|
|
||||||
|
|
||||||
aggregator := source.NewAggregator(configSource)
|
|
||||||
if storeSource != nil {
|
|
||||||
aggregator.SetStoreSource(storeSource)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyCfgs, visitorCfgs, err := aggregator.Load()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to load config from sources: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyCfgs, visitorCfgs = config.FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs)
|
|
||||||
proxyCfgs = config.CompleteProxyConfigurers(proxyCfgs)
|
|
||||||
visitorCfgs = config.CompleteVisitorConfigurers(visitorCfgs)
|
|
||||||
|
|
||||||
warning, err := validation.ValidateAllClientConfig(result.Common, proxyCfgs, visitorCfgs, unsafeFeatures)
|
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
@@ -186,32 +143,35 @@ func runClientWithAggregator(result *config.ClientConfigLoadResult, unsafeFeatur
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
return startServiceWithAggregator(result.Common, aggregator, unsafeFeatures, cfgFilePath)
|
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath)
|
||||||
}
|
}
|
||||||
|
|
||||||
func startServiceWithAggregator(
|
func startService(
|
||||||
cfg *v1.ClientCommonConfig,
|
cfg *v1.ClientCommonConfig,
|
||||||
aggregator *source.Aggregator,
|
proxyCfgs []v1.ProxyConfigurer,
|
||||||
|
visitorCfgs []v1.VisitorConfigurer,
|
||||||
unsafeFeatures *security.UnsafeFeatures,
|
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)
|
||||||
|
|
||||||
if cfgFile != "" {
|
if cfgFile != "" {
|
||||||
log.Infof("start frpc service for config file [%s] with aggregated configuration", cfgFile)
|
log.Infof("start frpc service for config file [%s]", cfgFile)
|
||||||
defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
|
defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
|
||||||
}
|
}
|
||||||
svr, err := client.NewService(client.ServiceOptions{
|
svr, err := client.NewService(client.ServiceOptions{
|
||||||
Common: cfg,
|
Common: cfg,
|
||||||
ConfigSourceAggregator: aggregator,
|
ProxyCfgs: proxyCfgs,
|
||||||
UnsafeFeatures: unsafeFeatures,
|
VisitorCfgs: visitorCfgs,
|
||||||
ConfigFilePath: cfgFile,
|
UnsafeFeatures: unsafeFeatures,
|
||||||
|
ConfigFilePath: cfgFile,
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
|
shouldGracefulClose := cfg.Transport.Protocol == "kcp" || cfg.Transport.Protocol == "quic"
|
||||||
|
// Capture the exit signal if we use kcp or quic.
|
||||||
if shouldGracefulClose {
|
if shouldGracefulClose {
|
||||||
go handleTermSignal(svr)
|
go handleTermSignal(svr)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -143,9 +143,6 @@ transport.tls.enable = true
|
|||||||
|
|
||||||
# Proxy names you want to start.
|
# Proxy names you want to start.
|
||||||
# Default is empty, means all proxies.
|
# Default is empty, means all proxies.
|
||||||
# This list is a global allowlist after config + store are merged, so entries
|
|
||||||
# created via Store API are also filtered by this list.
|
|
||||||
# If start is non-empty, any proxy/visitor not listed here will not be started.
|
|
||||||
# start = ["ssh", "dns"]
|
# start = ["ssh", "dns"]
|
||||||
|
|
||||||
# Alternative to 'start': You can control each proxy individually using the 'enabled' field.
|
# Alternative to 'start': You can control each proxy individually using the 'enabled' field.
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ COPY web/frpc/ ./
|
|||||||
RUN npm install
|
RUN npm install
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM golang:1.25 AS building
|
FROM golang:1.24 AS building
|
||||||
|
|
||||||
COPY . /building
|
COPY . /building
|
||||||
COPY --from=web-builder /web/frpc/dist /building/web/frpc/dist
|
COPY --from=web-builder /web/frpc/dist /building/web/frpc/dist
|
||||||
|
|||||||
@@ -5,7 +5,7 @@ COPY web/frps/ ./
|
|||||||
RUN npm install
|
RUN npm install
|
||||||
RUN npm run build
|
RUN npm run build
|
||||||
|
|
||||||
FROM golang:1.25 AS building
|
FROM golang:1.24 AS building
|
||||||
|
|
||||||
COPY . /building
|
COPY . /building
|
||||||
COPY --from=web-builder /web/frps/dist /building/web/frps/dist
|
COPY --from=web-builder /web/frps/dist /building/web/frps/dist
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -1,6 +1,6 @@
|
|||||||
module github.com/fatedier/frp
|
module github.com/fatedier/frp
|
||||||
|
|
||||||
go 1.25.0
|
go 1.24.0
|
||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||||
|
|||||||
@@ -23,7 +23,6 @@ import (
|
|||||||
"net/url"
|
"net/url"
|
||||||
"os"
|
"os"
|
||||||
"slices"
|
"slices"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/coreos/go-oidc/v3/oidc"
|
"github.com/coreos/go-oidc/v3/oidc"
|
||||||
"golang.org/x/oauth2"
|
"golang.org/x/oauth2"
|
||||||
@@ -75,23 +74,11 @@ func createOIDCHTTPClient(trustedCAFile string, insecureSkipVerify bool, proxyUR
|
|||||||
return &http.Client{Transport: transport}, nil
|
return &http.Client{Transport: transport}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// nonCachingTokenSource wraps a clientcredentials.Config to fetch a fresh
|
|
||||||
// token on every call. This is used as a fallback when the OIDC provider
|
|
||||||
// does not return expires_in, which would cause a caching TokenSource to
|
|
||||||
// hold onto a stale token forever.
|
|
||||||
type nonCachingTokenSource struct {
|
|
||||||
cfg *clientcredentials.Config
|
|
||||||
ctx context.Context
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *nonCachingTokenSource) Token() (*oauth2.Token, error) {
|
|
||||||
return s.cfg.Token(s.ctx)
|
|
||||||
}
|
|
||||||
|
|
||||||
type OidcAuthProvider struct {
|
type OidcAuthProvider struct {
|
||||||
additionalAuthScopes []v1.AuthScope
|
additionalAuthScopes []v1.AuthScope
|
||||||
|
|
||||||
tokenSource oauth2.TokenSource
|
tokenGenerator *clientcredentials.Config
|
||||||
|
httpClient *http.Client
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) {
|
func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClientConfig) (*OidcAuthProvider, error) {
|
||||||
@@ -112,44 +99,30 @@ func NewOidcAuthSetter(additionalAuthScopes []v1.AuthScope, cfg v1.AuthOIDCClien
|
|||||||
EndpointParams: eps,
|
EndpointParams: eps,
|
||||||
}
|
}
|
||||||
|
|
||||||
// Build the context that TokenSource will use for all future HTTP requests.
|
// Create custom HTTP client if needed
|
||||||
// context.Background() is appropriate here because the token source is
|
var httpClient *http.Client
|
||||||
// long-lived and outlives any single request.
|
|
||||||
ctx := context.Background()
|
|
||||||
if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" {
|
if cfg.TrustedCaFile != "" || cfg.InsecureSkipVerify || cfg.ProxyURL != "" {
|
||||||
httpClient, err := createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL)
|
var err error
|
||||||
|
httpClient, err = createOIDCHTTPClient(cfg.TrustedCaFile, cfg.InsecureSkipVerify, cfg.ProxyURL)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err)
|
return nil, fmt.Errorf("failed to create OIDC HTTP client: %w", err)
|
||||||
}
|
}
|
||||||
ctx = context.WithValue(ctx, oauth2.HTTPClient, httpClient)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create a persistent TokenSource that caches the token and refreshes
|
|
||||||
// it before expiry. This avoids making a new HTTP request to the OIDC
|
|
||||||
// provider on every heartbeat/ping.
|
|
||||||
tokenSource := tokenGenerator.TokenSource(ctx)
|
|
||||||
|
|
||||||
// Fetch the initial token to check if the provider returns an expiry.
|
|
||||||
// If Expiry is the zero value (provider omitted expires_in), the cached
|
|
||||||
// TokenSource would treat the token as valid forever and never refresh it,
|
|
||||||
// even after the JWT's exp claim passes. In that case, fall back to
|
|
||||||
// fetching a fresh token on every request.
|
|
||||||
initialToken, err := tokenSource.Token()
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to obtain initial OIDC token: %w", err)
|
|
||||||
}
|
|
||||||
if initialToken.Expiry.IsZero() {
|
|
||||||
tokenSource = &nonCachingTokenSource{cfg: tokenGenerator, ctx: ctx}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return &OidcAuthProvider{
|
return &OidcAuthProvider{
|
||||||
additionalAuthScopes: additionalAuthScopes,
|
additionalAuthScopes: additionalAuthScopes,
|
||||||
tokenSource: tokenSource,
|
tokenGenerator: tokenGenerator,
|
||||||
|
httpClient: httpClient,
|
||||||
}, nil
|
}, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
|
func (auth *OidcAuthProvider) generateAccessToken() (accessToken string, err error) {
|
||||||
tokenObj, err := auth.tokenSource.Token()
|
ctx := context.Background()
|
||||||
|
if auth.httpClient != nil {
|
||||||
|
ctx = context.WithValue(ctx, oauth2.HTTPClient, auth.httpClient)
|
||||||
|
}
|
||||||
|
|
||||||
|
tokenObj, err := auth.tokenGenerator.Token(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
|
return "", fmt.Errorf("couldn't generate OIDC token for login: %v", err)
|
||||||
}
|
}
|
||||||
@@ -232,8 +205,7 @@ type OidcAuthConsumer struct {
|
|||||||
additionalAuthScopes []v1.AuthScope
|
additionalAuthScopes []v1.AuthScope
|
||||||
|
|
||||||
verifier TokenVerifier
|
verifier TokenVerifier
|
||||||
mu sync.RWMutex
|
subjectsFromLogin []string
|
||||||
subjectsFromLogin map[string]struct{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
|
func NewTokenVerifier(cfg v1.AuthOIDCServerConfig) TokenVerifier {
|
||||||
@@ -254,7 +226,7 @@ func NewOidcAuthVerifier(additionalAuthScopes []v1.AuthScope, verifier TokenVeri
|
|||||||
return &OidcAuthConsumer{
|
return &OidcAuthConsumer{
|
||||||
additionalAuthScopes: additionalAuthScopes,
|
additionalAuthScopes: additionalAuthScopes,
|
||||||
verifier: verifier,
|
verifier: verifier,
|
||||||
subjectsFromLogin: make(map[string]struct{}),
|
subjectsFromLogin: []string{},
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -263,9 +235,9 @@ func (auth *OidcAuthConsumer) VerifyLogin(loginMsg *msg.Login) (err error) {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid OIDC token in login: %v", err)
|
return fmt.Errorf("invalid OIDC token in login: %v", err)
|
||||||
}
|
}
|
||||||
auth.mu.Lock()
|
if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
|
||||||
auth.subjectsFromLogin[token.Subject] = struct{}{}
|
auth.subjectsFromLogin = append(auth.subjectsFromLogin, token.Subject)
|
||||||
auth.mu.Unlock()
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -274,13 +246,11 @@ func (auth *OidcAuthConsumer) verifyPostLoginToken(privilegeKey string) (err err
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return fmt.Errorf("invalid OIDC token in ping: %v", err)
|
return fmt.Errorf("invalid OIDC token in ping: %v", err)
|
||||||
}
|
}
|
||||||
auth.mu.RLock()
|
if !slices.Contains(auth.subjectsFromLogin, token.Subject) {
|
||||||
_, ok := auth.subjectsFromLogin[token.Subject]
|
|
||||||
auth.mu.RUnlock()
|
|
||||||
if !ok {
|
|
||||||
return fmt.Errorf("received different OIDC subject in login and ping. "+
|
return fmt.Errorf("received different OIDC subject in login and ping. "+
|
||||||
|
"original subjects: %s, "+
|
||||||
"new subject: %s",
|
"new subject: %s",
|
||||||
token.Subject)
|
auth.subjectsFromLogin, token.Subject)
|
||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,10 +2,6 @@ package auth_test
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
"net/http/httptest"
|
|
||||||
"sync/atomic"
|
|
||||||
"testing"
|
"testing"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -66,90 +62,3 @@ func TestPingAfterLoginWithDifferentSubjectFails(t *testing.T) {
|
|||||||
r.Error(err)
|
r.Error(err)
|
||||||
r.Contains(err.Error(), "received different OIDC subject in login and ping")
|
r.Contains(err.Error(), "received different OIDC subject in login and ping")
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestOidcAuthProviderFallsBackWhenNoExpiry(t *testing.T) {
|
|
||||||
r := require.New(t)
|
|
||||||
|
|
||||||
var requestCount atomic.Int32
|
|
||||||
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
requestCount.Add(1)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response
|
|
||||||
"access_token": "fresh-test-token",
|
|
||||||
"token_type": "Bearer",
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
defer tokenServer.Close()
|
|
||||||
|
|
||||||
provider, err := auth.NewOidcAuthSetter(
|
|
||||||
[]v1.AuthScope{v1.AuthScopeHeartBeats},
|
|
||||||
v1.AuthOIDCClientConfig{
|
|
||||||
ClientID: "test-client",
|
|
||||||
ClientSecret: "test-secret",
|
|
||||||
TokenEndpointURL: tokenServer.URL,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
r.NoError(err)
|
|
||||||
|
|
||||||
// Constructor fetches the initial token (1 request).
|
|
||||||
// Each subsequent call should also fetch a fresh token since there is no expiry.
|
|
||||||
loginMsg := &msg.Login{}
|
|
||||||
err = provider.SetLogin(loginMsg)
|
|
||||||
r.NoError(err)
|
|
||||||
r.Equal("fresh-test-token", loginMsg.PrivilegeKey)
|
|
||||||
|
|
||||||
for range 3 {
|
|
||||||
pingMsg := &msg.Ping{}
|
|
||||||
err = provider.SetPing(pingMsg)
|
|
||||||
r.NoError(err)
|
|
||||||
r.Equal("fresh-test-token", pingMsg.PrivilegeKey)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 1 initial (constructor) + 1 login + 3 pings = 5 requests
|
|
||||||
r.Equal(int32(5), requestCount.Load(), "each call should fetch a fresh token when expires_in is missing")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestOidcAuthProviderCachesToken(t *testing.T) {
|
|
||||||
r := require.New(t)
|
|
||||||
|
|
||||||
var requestCount atomic.Int32
|
|
||||||
tokenServer := httptest.NewServer(http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
requestCount.Add(1)
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
_ = json.NewEncoder(w).Encode(map[string]any{ //nolint:gosec // test-only dummy token response
|
|
||||||
"access_token": "cached-test-token",
|
|
||||||
"token_type": "Bearer",
|
|
||||||
"expires_in": 3600,
|
|
||||||
})
|
|
||||||
}))
|
|
||||||
defer tokenServer.Close()
|
|
||||||
|
|
||||||
provider, err := auth.NewOidcAuthSetter(
|
|
||||||
[]v1.AuthScope{v1.AuthScopeHeartBeats},
|
|
||||||
v1.AuthOIDCClientConfig{
|
|
||||||
ClientID: "test-client",
|
|
||||||
ClientSecret: "test-secret",
|
|
||||||
TokenEndpointURL: tokenServer.URL,
|
|
||||||
},
|
|
||||||
)
|
|
||||||
r.NoError(err)
|
|
||||||
|
|
||||||
// Constructor eagerly fetches the initial token (1 request).
|
|
||||||
r.Equal(int32(1), requestCount.Load())
|
|
||||||
|
|
||||||
// SetLogin should reuse the cached token
|
|
||||||
loginMsg := &msg.Login{}
|
|
||||||
err = provider.SetLogin(loginMsg)
|
|
||||||
r.NoError(err)
|
|
||||||
r.Equal("cached-test-token", loginMsg.PrivilegeKey)
|
|
||||||
r.Equal(int32(1), requestCount.Load())
|
|
||||||
|
|
||||||
// Subsequent calls should also reuse the cached token
|
|
||||||
for range 5 {
|
|
||||||
pingMsg := &msg.Ping{}
|
|
||||||
err = provider.SetPing(pingMsg)
|
|
||||||
r.NoError(err)
|
|
||||||
r.Equal("cached-test-token", pingMsg.PrivilegeKey)
|
|
||||||
}
|
|
||||||
r.Equal(int32(1), requestCount.Load(), "token endpoint should only be called once; cached token should be reused")
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -171,14 +171,15 @@ func Convert_ServerCommonConf_To_v1(conf *ServerCommonConf) *v1.ServerConfig {
|
|||||||
func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations {
|
func transformHeadersFromPluginParams(params map[string]string) v1.HeaderOperations {
|
||||||
out := v1.HeaderOperations{}
|
out := v1.HeaderOperations{}
|
||||||
for k, v := range params {
|
for k, v := range params {
|
||||||
k, ok := strings.CutPrefix(k, "plugin_header_")
|
if !strings.HasPrefix(k, "plugin_header_") {
|
||||||
if !ok || k == "" {
|
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
if out.Set == nil {
|
if k = strings.TrimPrefix(k, "plugin_header_"); k != "" {
|
||||||
out.Set = make(map[string]string)
|
if out.Set == nil {
|
||||||
|
out.Set = make(map[string]string)
|
||||||
|
}
|
||||||
|
out.Set[k] = v
|
||||||
}
|
}
|
||||||
out.Set[k] = v
|
|
||||||
}
|
}
|
||||||
return out
|
return out
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -39,14 +39,14 @@ const (
|
|||||||
// Proxy
|
// Proxy
|
||||||
var (
|
var (
|
||||||
proxyConfTypeMap = map[ProxyType]reflect.Type{
|
proxyConfTypeMap = map[ProxyType]reflect.Type{
|
||||||
ProxyTypeTCP: reflect.TypeFor[TCPProxyConf](),
|
ProxyTypeTCP: reflect.TypeOf(TCPProxyConf{}),
|
||||||
ProxyTypeUDP: reflect.TypeFor[UDPProxyConf](),
|
ProxyTypeUDP: reflect.TypeOf(UDPProxyConf{}),
|
||||||
ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConf](),
|
ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConf{}),
|
||||||
ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConf](),
|
ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConf{}),
|
||||||
ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConf](),
|
ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConf{}),
|
||||||
ProxyTypeSTCP: reflect.TypeFor[STCPProxyConf](),
|
ProxyTypeSTCP: reflect.TypeOf(STCPProxyConf{}),
|
||||||
ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConf](),
|
ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConf{}),
|
||||||
ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConf](),
|
ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConf{}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -22,8 +22,8 @@ func GetMapWithoutPrefix(set map[string]string, prefix string) map[string]string
|
|||||||
m := make(map[string]string)
|
m := make(map[string]string)
|
||||||
|
|
||||||
for key, value := range set {
|
for key, value := range set {
|
||||||
if trimmed, ok := strings.CutPrefix(key, prefix); ok {
|
if strings.HasPrefix(key, prefix) {
|
||||||
m[trimmed] = value
|
m[strings.TrimPrefix(key, prefix)] = value
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -32,9 +32,9 @@ const (
|
|||||||
// Visitor
|
// Visitor
|
||||||
var (
|
var (
|
||||||
visitorConfTypeMap = map[VisitorType]reflect.Type{
|
visitorConfTypeMap = map[VisitorType]reflect.Type{
|
||||||
VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConf](),
|
VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConf{}),
|
||||||
VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConf](),
|
VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConf{}),
|
||||||
VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConf](),
|
VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConf{}),
|
||||||
}
|
}
|
||||||
)
|
)
|
||||||
|
|
||||||
|
|||||||
@@ -17,7 +17,6 @@ package config
|
|||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"errors"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -34,7 +33,6 @@ import (
|
|||||||
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/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -110,21 +108,7 @@ func LoadConfigureFromFile(path string, c any, strict bool) error {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return LoadConfigure(content, c, strict, detectFormatFromPath(path))
|
return LoadConfigure(content, c, strict)
|
||||||
}
|
|
||||||
|
|
||||||
// detectFormatFromPath returns a format hint based on the file extension.
|
|
||||||
func detectFormatFromPath(path string) string {
|
|
||||||
switch strings.ToLower(filepath.Ext(path)) {
|
|
||||||
case ".toml":
|
|
||||||
return "toml"
|
|
||||||
case ".yaml", ".yml":
|
|
||||||
return "yaml"
|
|
||||||
case ".json":
|
|
||||||
return "json"
|
|
||||||
default:
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
|
// parseYAMLWithDotFieldsHandling parses YAML with dot-prefixed fields handling
|
||||||
@@ -145,136 +129,48 @@ func parseYAMLWithDotFieldsHandling(content []byte, target any) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Convert to JSON and decode with strict validation
|
// Convert to JSON and decode with strict validation
|
||||||
jsonBytes, err := jsonx.Marshal(temp)
|
jsonBytes, err := json.Marshal(temp)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
return decodeJSONContent(jsonBytes, target, true)
|
decoder := json.NewDecoder(bytes.NewReader(jsonBytes))
|
||||||
}
|
decoder.DisallowUnknownFields()
|
||||||
|
return decoder.Decode(target)
|
||||||
func decodeJSONContent(content []byte, target any, strict bool) error {
|
|
||||||
if clientCfg, ok := target.(*v1.ClientConfig); ok {
|
|
||||||
decoded, err := v1.DecodeClientConfigJSON(content, v1.DecodeOptions{
|
|
||||||
DisallowUnknownFields: strict,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
*clientCfg = decoded
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return jsonx.UnmarshalWithOptions(content, target, jsonx.DecodeOptions{
|
|
||||||
RejectUnknownMembers: strict,
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// LoadConfigure loads configuration from bytes and unmarshal into c.
|
// LoadConfigure loads configuration from bytes and unmarshal into c.
|
||||||
// Now it supports json, yaml and toml format.
|
// Now it supports json, yaml and toml format.
|
||||||
// An optional format hint (e.g. "toml", "yaml", "json") can be provided
|
func LoadConfigure(b []byte, c any, strict bool) error {
|
||||||
// to enable better error messages with line number information.
|
v1.DisallowUnknownFieldsMu.Lock()
|
||||||
func LoadConfigure(b []byte, c any, strict bool, formats ...string) error {
|
defer v1.DisallowUnknownFieldsMu.Unlock()
|
||||||
format := ""
|
v1.DisallowUnknownFields = strict
|
||||||
if len(formats) > 0 {
|
|
||||||
format = formats[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
originalBytes := b
|
|
||||||
parsedFromTOML := false
|
|
||||||
|
|
||||||
var tomlObj any
|
var tomlObj any
|
||||||
tomlErr := toml.Unmarshal(b, &tomlObj)
|
// Try to unmarshal as TOML first; swallow errors from that (assume it's not valid TOML).
|
||||||
if tomlErr == nil {
|
if err := toml.Unmarshal(b, &tomlObj); err == nil {
|
||||||
parsedFromTOML = true
|
b, err = json.Marshal(&tomlObj)
|
||||||
var err error
|
|
||||||
b, err = jsonx.Marshal(&tomlObj)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
} else if format == "toml" {
|
|
||||||
// File is known to be TOML but has syntax errors.
|
|
||||||
return formatTOMLError(tomlErr)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly.
|
// If the buffer smells like JSON (first non-whitespace character is '{'), unmarshal as JSON directly.
|
||||||
if yaml.IsJSONBuffer(b) {
|
if yaml.IsJSONBuffer(b) {
|
||||||
if err := decodeJSONContent(b, c, strict); err != nil {
|
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
||||||
return enhanceDecodeError(err, originalBytes, !parsedFromTOML)
|
if strict {
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
}
|
}
|
||||||
return nil
|
return decoder.Decode(c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Handle YAML content
|
// Handle YAML content
|
||||||
if strict {
|
if strict {
|
||||||
// In strict mode, always use our custom handler to support YAML merge
|
// In strict mode, always use our custom handler to support YAML merge
|
||||||
if err := parseYAMLWithDotFieldsHandling(b, c); err != nil {
|
return parseYAMLWithDotFieldsHandling(b, c)
|
||||||
return enhanceDecodeError(err, originalBytes, !parsedFromTOML)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
// Non-strict mode, parse normally
|
// Non-strict mode, parse normally
|
||||||
return yaml.Unmarshal(b, c)
|
return yaml.Unmarshal(b, c)
|
||||||
}
|
}
|
||||||
|
|
||||||
// formatTOMLError extracts line/column information from TOML decode errors.
|
|
||||||
func formatTOMLError(err error) error {
|
|
||||||
var decErr *toml.DecodeError
|
|
||||||
if errors.As(err, &decErr) {
|
|
||||||
row, col := decErr.Position()
|
|
||||||
return fmt.Errorf("toml: line %d, column %d: %s", row, col, decErr.Error())
|
|
||||||
}
|
|
||||||
var strictErr *toml.StrictMissingError
|
|
||||||
if errors.As(err, &strictErr) {
|
|
||||||
return strictErr
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// enhanceDecodeError tries to add field path and line number information to JSON/YAML decode errors.
|
|
||||||
func enhanceDecodeError(err error, originalContent []byte, includeLine bool) error {
|
|
||||||
var typeErr *json.UnmarshalTypeError
|
|
||||||
if errors.As(err, &typeErr) && typeErr.Field != "" {
|
|
||||||
if includeLine {
|
|
||||||
line := findFieldLineInContent(originalContent, typeErr.Field)
|
|
||||||
if line > 0 {
|
|
||||||
return fmt.Errorf("line %d: field \"%s\": cannot unmarshal %s into %s", line, typeErr.Field, typeErr.Value, typeErr.Type)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return fmt.Errorf("field \"%s\": cannot unmarshal %s into %s", typeErr.Field, typeErr.Value, typeErr.Type)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// findFieldLineInContent searches the original config content for a field name
|
|
||||||
// and returns the 1-indexed line number where it appears, or 0 if not found.
|
|
||||||
func findFieldLineInContent(content []byte, fieldPath string) int {
|
|
||||||
if fieldPath == "" {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
// Use the last component of the field path (e.g. "proxies" from "proxies" or
|
|
||||||
// "protocol" from "transport.protocol").
|
|
||||||
parts := strings.Split(fieldPath, ".")
|
|
||||||
searchKey := parts[len(parts)-1]
|
|
||||||
|
|
||||||
lines := bytes.Split(content, []byte("\n"))
|
|
||||||
for i, line := range lines {
|
|
||||||
trimmed := bytes.TrimSpace(line)
|
|
||||||
// Match TOML key assignments like: key = ...
|
|
||||||
if bytes.HasPrefix(trimmed, []byte(searchKey)) {
|
|
||||||
rest := bytes.TrimSpace(trimmed[len(searchKey):])
|
|
||||||
if len(rest) > 0 && rest[0] == '=' {
|
|
||||||
return i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
// Match TOML table array headers like: [[proxies]]
|
|
||||||
if bytes.Contains(trimmed, []byte("[["+searchKey+"]]")) {
|
|
||||||
return i + 1
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
|
func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.ProxyConfigurer, error) {
|
||||||
m.ProxyType = util.EmptyOr(m.ProxyType, string(v1.ProxyTypeTCP))
|
m.ProxyType = util.EmptyOr(m.ProxyType, string(v1.ProxyTypeTCP))
|
||||||
|
|
||||||
@@ -284,7 +180,7 @@ func NewProxyConfigurerFromMsg(m *msg.NewProxy, serverCfg *v1.ServerConfig) (v1.
|
|||||||
}
|
}
|
||||||
|
|
||||||
configurer.UnmarshalFromMsg(m)
|
configurer.UnmarshalFromMsg(m)
|
||||||
configurer.Complete()
|
configurer.Complete("")
|
||||||
|
|
||||||
if err := validation.ValidateProxyConfigurerForServer(configurer, serverCfg); err != nil {
|
if err := validation.ValidateProxyConfigurerForServer(configurer, serverCfg); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -323,132 +219,60 @@ func LoadServerConfig(path string, strict bool) (*v1.ServerConfig, bool, error)
|
|||||||
return svrCfg, isLegacyFormat, nil
|
return svrCfg, isLegacyFormat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientConfigLoadResult contains the result of loading a client configuration file.
|
|
||||||
type ClientConfigLoadResult struct {
|
|
||||||
// Common contains the common client configuration.
|
|
||||||
Common *v1.ClientCommonConfig
|
|
||||||
|
|
||||||
// Proxies contains proxy configurations from inline [[proxies]] and includeConfigFiles.
|
|
||||||
// These are NOT completed (user prefix not added).
|
|
||||||
Proxies []v1.ProxyConfigurer
|
|
||||||
|
|
||||||
// Visitors contains visitor configurations from inline [[visitors]] and includeConfigFiles.
|
|
||||||
// These are NOT completed.
|
|
||||||
Visitors []v1.VisitorConfigurer
|
|
||||||
|
|
||||||
// IsLegacyFormat indicates whether the config file is in legacy INI format.
|
|
||||||
IsLegacyFormat bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoadClientConfigResult loads and parses a client configuration file.
|
|
||||||
// It returns the raw configuration without completing proxies/visitors.
|
|
||||||
// The caller should call Complete on the configs manually for legacy behavior.
|
|
||||||
func LoadClientConfigResult(path string, strict bool) (*ClientConfigLoadResult, error) {
|
|
||||||
result := &ClientConfigLoadResult{
|
|
||||||
Proxies: make([]v1.ProxyConfigurer, 0),
|
|
||||||
Visitors: make([]v1.VisitorConfigurer, 0),
|
|
||||||
}
|
|
||||||
|
|
||||||
if DetectLegacyINIFormatFromFile(path) {
|
|
||||||
legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result.Common = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
|
|
||||||
for _, c := range legacyProxyCfgs {
|
|
||||||
result.Proxies = append(result.Proxies, legacy.Convert_ProxyConf_To_v1(c))
|
|
||||||
}
|
|
||||||
for _, c := range legacyVisitorCfgs {
|
|
||||||
result.Visitors = append(result.Visitors, legacy.Convert_VisitorConf_To_v1(c))
|
|
||||||
}
|
|
||||||
result.IsLegacyFormat = true
|
|
||||||
} else {
|
|
||||||
allCfg := v1.ClientConfig{}
|
|
||||||
if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result.Common = &allCfg.ClientCommonConfig
|
|
||||||
for _, c := range allCfg.Proxies {
|
|
||||||
result.Proxies = append(result.Proxies, c.ProxyConfigurer)
|
|
||||||
}
|
|
||||||
for _, c := range allCfg.Visitors {
|
|
||||||
result.Visitors = append(result.Visitors, c.VisitorConfigurer)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load additional config from includes.
|
|
||||||
// legacy ini format already handle this in ParseClientConfig.
|
|
||||||
if len(result.Common.IncludeConfigFiles) > 0 && !result.IsLegacyFormat {
|
|
||||||
extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(result.Common.IncludeConfigFiles, result.IsLegacyFormat, strict)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
result.Proxies = append(result.Proxies, extProxyCfgs...)
|
|
||||||
result.Visitors = append(result.Visitors, extVisitorCfgs...)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Complete the common config
|
|
||||||
if result.Common != nil {
|
|
||||||
if err := result.Common.Complete(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func LoadClientConfig(path string, strict bool) (
|
func LoadClientConfig(path string, strict bool) (
|
||||||
*v1.ClientCommonConfig,
|
*v1.ClientCommonConfig,
|
||||||
[]v1.ProxyConfigurer,
|
[]v1.ProxyConfigurer,
|
||||||
[]v1.VisitorConfigurer,
|
[]v1.VisitorConfigurer,
|
||||||
bool, error,
|
bool, error,
|
||||||
) {
|
) {
|
||||||
result, err := LoadClientConfigResult(path, strict)
|
var (
|
||||||
if err != nil {
|
cliCfg *v1.ClientCommonConfig
|
||||||
return nil, nil, nil, result != nil && result.IsLegacyFormat, err
|
proxyCfgs = make([]v1.ProxyConfigurer, 0)
|
||||||
|
visitorCfgs = make([]v1.VisitorConfigurer, 0)
|
||||||
|
isLegacyFormat bool
|
||||||
|
)
|
||||||
|
|
||||||
|
if DetectLegacyINIFormatFromFile(path) {
|
||||||
|
legacyCommon, legacyProxyCfgs, legacyVisitorCfgs, err := legacy.ParseClientConfig(path)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, nil, true, err
|
||||||
|
}
|
||||||
|
cliCfg = legacy.Convert_ClientCommonConf_To_v1(&legacyCommon)
|
||||||
|
for _, c := range legacyProxyCfgs {
|
||||||
|
proxyCfgs = append(proxyCfgs, legacy.Convert_ProxyConf_To_v1(c))
|
||||||
|
}
|
||||||
|
for _, c := range legacyVisitorCfgs {
|
||||||
|
visitorCfgs = append(visitorCfgs, legacy.Convert_VisitorConf_To_v1(c))
|
||||||
|
}
|
||||||
|
isLegacyFormat = true
|
||||||
|
} else {
|
||||||
|
allCfg := v1.ClientConfig{}
|
||||||
|
if err := LoadConfigureFromFile(path, &allCfg, strict); err != nil {
|
||||||
|
return nil, nil, nil, false, err
|
||||||
|
}
|
||||||
|
cliCfg = &allCfg.ClientCommonConfig
|
||||||
|
for _, c := range allCfg.Proxies {
|
||||||
|
proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
|
||||||
|
}
|
||||||
|
for _, c := range allCfg.Visitors {
|
||||||
|
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyCfgs := result.Proxies
|
// Load additional config from includes.
|
||||||
visitorCfgs := result.Visitors
|
// legacy ini format already handle this in ParseClientConfig.
|
||||||
|
if len(cliCfg.IncludeConfigFiles) > 0 && !isLegacyFormat {
|
||||||
proxyCfgs, visitorCfgs = FilterClientConfigurers(result.Common, proxyCfgs, visitorCfgs)
|
extProxyCfgs, extVisitorCfgs, err := LoadAdditionalClientConfigs(cliCfg.IncludeConfigFiles, isLegacyFormat, strict)
|
||||||
proxyCfgs = CompleteProxyConfigurers(proxyCfgs)
|
if err != nil {
|
||||||
visitorCfgs = CompleteVisitorConfigurers(visitorCfgs)
|
return nil, nil, nil, isLegacyFormat, err
|
||||||
return result.Common, proxyCfgs, visitorCfgs, result.IsLegacyFormat, nil
|
}
|
||||||
}
|
proxyCfgs = append(proxyCfgs, extProxyCfgs...)
|
||||||
|
visitorCfgs = append(visitorCfgs, extVisitorCfgs...)
|
||||||
func CompleteProxyConfigurers(proxies []v1.ProxyConfigurer) []v1.ProxyConfigurer {
|
|
||||||
proxyCfgs := proxies
|
|
||||||
for _, c := range proxyCfgs {
|
|
||||||
c.Complete()
|
|
||||||
}
|
|
||||||
return proxyCfgs
|
|
||||||
}
|
|
||||||
|
|
||||||
func CompleteVisitorConfigurers(visitors []v1.VisitorConfigurer) []v1.VisitorConfigurer {
|
|
||||||
visitorCfgs := visitors
|
|
||||||
for _, c := range visitorCfgs {
|
|
||||||
c.Complete()
|
|
||||||
}
|
|
||||||
return visitorCfgs
|
|
||||||
}
|
|
||||||
|
|
||||||
func FilterClientConfigurers(
|
|
||||||
common *v1.ClientCommonConfig,
|
|
||||||
proxies []v1.ProxyConfigurer,
|
|
||||||
visitors []v1.VisitorConfigurer,
|
|
||||||
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) {
|
|
||||||
if common == nil {
|
|
||||||
common = &v1.ClientCommonConfig{}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
proxyCfgs := proxies
|
// Filter by start
|
||||||
visitorCfgs := visitors
|
if len(cliCfg.Start) > 0 {
|
||||||
|
startSet := sets.New(cliCfg.Start...)
|
||||||
// Filter by start across merged configurers from all sources.
|
|
||||||
// For example, store entries are also filtered by this set.
|
|
||||||
if len(common.Start) > 0 {
|
|
||||||
startSet := sets.New(common.Start...)
|
|
||||||
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
proxyCfgs = lo.Filter(proxyCfgs, func(c v1.ProxyConfigurer, _ int) bool {
|
||||||
return startSet.Has(c.GetBaseConfig().Name)
|
return startSet.Has(c.GetBaseConfig().Name)
|
||||||
})
|
})
|
||||||
@@ -467,7 +291,19 @@ func FilterClientConfigurers(
|
|||||||
enabled := c.GetBaseConfig().Enabled
|
enabled := c.GetBaseConfig().Enabled
|
||||||
return enabled == nil || *enabled
|
return enabled == nil || *enabled
|
||||||
})
|
})
|
||||||
return proxyCfgs, visitorCfgs
|
|
||||||
|
if cliCfg != nil {
|
||||||
|
if err := cliCfg.Complete(); err != nil {
|
||||||
|
return nil, nil, nil, isLegacyFormat, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, c := range proxyCfgs {
|
||||||
|
c.Complete(cliCfg.User)
|
||||||
|
}
|
||||||
|
for _, c := range visitorCfgs {
|
||||||
|
c.Complete(cliCfg)
|
||||||
|
}
|
||||||
|
return cliCfg, proxyCfgs, visitorCfgs, isLegacyFormat, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
func LoadAdditionalClientConfigs(paths []string, isLegacyFormat bool, strict bool) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"strings"
|
"strings"
|
||||||
"testing"
|
"testing"
|
||||||
@@ -189,31 +188,6 @@ unixPath = "/tmp/uds.sock"
|
|||||||
require.Error(err)
|
require.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestLoadClientConfigStrictMode_UnknownPluginField(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
content := `
|
|
||||||
serverPort = 7000
|
|
||||||
|
|
||||||
[[proxies]]
|
|
||||||
name = "test"
|
|
||||||
type = "tcp"
|
|
||||||
localPort = 6000
|
|
||||||
[proxies.plugin]
|
|
||||||
type = "http2https"
|
|
||||||
localAddr = "127.0.0.1:8080"
|
|
||||||
unknownInPlugin = "value"
|
|
||||||
`
|
|
||||||
|
|
||||||
clientCfg := v1.ClientConfig{}
|
|
||||||
|
|
||||||
err := LoadConfigure([]byte(content), &clientCfg, false)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
err = LoadConfigure([]byte(content), &clientCfg, true)
|
|
||||||
require.ErrorContains(err, "unknownInPlugin")
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
|
// TestYAMLMergeInStrictMode tests that YAML merge functionality works
|
||||||
// even in strict mode by properly handling dot-prefixed fields
|
// even in strict mode by properly handling dot-prefixed fields
|
||||||
func TestYAMLMergeInStrictMode(t *testing.T) {
|
func TestYAMLMergeInStrictMode(t *testing.T) {
|
||||||
@@ -299,169 +273,6 @@ proxies:
|
|||||||
require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type)
|
require.Equal("stcp", clientCfg.Proxies[0].ProxyConfigurer.GetBaseConfig().Type)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestFilterClientConfigurers_PreserveRawNamesAndNoMutation(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
enabled := true
|
|
||||||
proxyCfg := &v1.TCPProxyConfig{}
|
|
||||||
proxyCfg.Name = "proxy-raw"
|
|
||||||
proxyCfg.Type = "tcp"
|
|
||||||
proxyCfg.LocalPort = 10080
|
|
||||||
proxyCfg.Enabled = &enabled
|
|
||||||
|
|
||||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
|
||||||
visitorCfg.Name = "visitor-raw"
|
|
||||||
visitorCfg.Type = "xtcp"
|
|
||||||
visitorCfg.ServerName = "server-raw"
|
|
||||||
visitorCfg.FallbackTo = "fallback-raw"
|
|
||||||
visitorCfg.SecretKey = "secret"
|
|
||||||
visitorCfg.BindPort = 10081
|
|
||||||
visitorCfg.Enabled = &enabled
|
|
||||||
|
|
||||||
common := &v1.ClientCommonConfig{
|
|
||||||
User: "alice",
|
|
||||||
}
|
|
||||||
|
|
||||||
proxies, visitors := FilterClientConfigurers(common, []v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg})
|
|
||||||
require.Len(proxies, 1)
|
|
||||||
require.Len(visitors, 1)
|
|
||||||
|
|
||||||
p := proxies[0].GetBaseConfig()
|
|
||||||
require.Equal("proxy-raw", p.Name)
|
|
||||||
require.Empty(p.LocalIP)
|
|
||||||
|
|
||||||
v := visitors[0].GetBaseConfig()
|
|
||||||
require.Equal("visitor-raw", v.Name)
|
|
||||||
require.Equal("server-raw", v.ServerName)
|
|
||||||
require.Empty(v.BindAddr)
|
|
||||||
|
|
||||||
xtcp := visitors[0].(*v1.XTCPVisitorConfig)
|
|
||||||
require.Equal("fallback-raw", xtcp.FallbackTo)
|
|
||||||
require.Empty(xtcp.Protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteProxyConfigurers_PreserveRawNames(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
enabled := true
|
|
||||||
proxyCfg := &v1.TCPProxyConfig{}
|
|
||||||
proxyCfg.Name = "proxy-raw"
|
|
||||||
proxyCfg.Type = "tcp"
|
|
||||||
proxyCfg.LocalPort = 10080
|
|
||||||
proxyCfg.Enabled = &enabled
|
|
||||||
|
|
||||||
proxies := CompleteProxyConfigurers([]v1.ProxyConfigurer{proxyCfg})
|
|
||||||
require.Len(proxies, 1)
|
|
||||||
|
|
||||||
p := proxies[0].GetBaseConfig()
|
|
||||||
require.Equal("proxy-raw", p.Name)
|
|
||||||
require.Equal("127.0.0.1", p.LocalIP)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteVisitorConfigurers_PreserveRawNames(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
enabled := true
|
|
||||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
|
||||||
visitorCfg.Name = "visitor-raw"
|
|
||||||
visitorCfg.Type = "xtcp"
|
|
||||||
visitorCfg.ServerName = "server-raw"
|
|
||||||
visitorCfg.FallbackTo = "fallback-raw"
|
|
||||||
visitorCfg.SecretKey = "secret"
|
|
||||||
visitorCfg.BindPort = 10081
|
|
||||||
visitorCfg.Enabled = &enabled
|
|
||||||
|
|
||||||
visitors := CompleteVisitorConfigurers([]v1.VisitorConfigurer{visitorCfg})
|
|
||||||
require.Len(visitors, 1)
|
|
||||||
|
|
||||||
v := visitors[0].GetBaseConfig()
|
|
||||||
require.Equal("visitor-raw", v.Name)
|
|
||||||
require.Equal("server-raw", v.ServerName)
|
|
||||||
require.Equal("127.0.0.1", v.BindAddr)
|
|
||||||
|
|
||||||
xtcp := visitors[0].(*v1.XTCPVisitorConfig)
|
|
||||||
require.Equal("fallback-raw", xtcp.FallbackTo)
|
|
||||||
require.Equal("quic", xtcp.Protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteProxyConfigurers_Idempotent(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
proxyCfg := &v1.TCPProxyConfig{}
|
|
||||||
proxyCfg.Name = "proxy"
|
|
||||||
proxyCfg.Type = "tcp"
|
|
||||||
proxyCfg.LocalPort = 10080
|
|
||||||
|
|
||||||
proxies := CompleteProxyConfigurers([]v1.ProxyConfigurer{proxyCfg})
|
|
||||||
firstProxyJSON, err := json.Marshal(proxies[0])
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
proxies = CompleteProxyConfigurers(proxies)
|
|
||||||
secondProxyJSON, err := json.Marshal(proxies[0])
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
require.Equal(string(firstProxyJSON), string(secondProxyJSON))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestCompleteVisitorConfigurers_Idempotent(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
|
||||||
visitorCfg.Name = "visitor"
|
|
||||||
visitorCfg.Type = "xtcp"
|
|
||||||
visitorCfg.ServerName = "server"
|
|
||||||
visitorCfg.SecretKey = "secret"
|
|
||||||
visitorCfg.BindPort = 10081
|
|
||||||
|
|
||||||
visitors := CompleteVisitorConfigurers([]v1.VisitorConfigurer{visitorCfg})
|
|
||||||
firstVisitorJSON, err := json.Marshal(visitors[0])
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
visitors = CompleteVisitorConfigurers(visitors)
|
|
||||||
secondVisitorJSON, err := json.Marshal(visitors[0])
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
require.Equal(string(firstVisitorJSON), string(secondVisitorJSON))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFilterClientConfigurers_FilterByStartAndEnabled(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
enabled := true
|
|
||||||
disabled := false
|
|
||||||
|
|
||||||
proxyKeep := &v1.TCPProxyConfig{}
|
|
||||||
proxyKeep.Name = "keep"
|
|
||||||
proxyKeep.Type = "tcp"
|
|
||||||
proxyKeep.LocalPort = 10080
|
|
||||||
proxyKeep.Enabled = &enabled
|
|
||||||
|
|
||||||
proxyDropByStart := &v1.TCPProxyConfig{}
|
|
||||||
proxyDropByStart.Name = "drop-by-start"
|
|
||||||
proxyDropByStart.Type = "tcp"
|
|
||||||
proxyDropByStart.LocalPort = 10081
|
|
||||||
proxyDropByStart.Enabled = &enabled
|
|
||||||
|
|
||||||
proxyDropByEnabled := &v1.TCPProxyConfig{}
|
|
||||||
proxyDropByEnabled.Name = "drop-by-enabled"
|
|
||||||
proxyDropByEnabled.Type = "tcp"
|
|
||||||
proxyDropByEnabled.LocalPort = 10082
|
|
||||||
proxyDropByEnabled.Enabled = &disabled
|
|
||||||
|
|
||||||
common := &v1.ClientCommonConfig{
|
|
||||||
Start: []string{"keep"},
|
|
||||||
}
|
|
||||||
|
|
||||||
proxies, visitors := FilterClientConfigurers(common, []v1.ProxyConfigurer{
|
|
||||||
proxyKeep,
|
|
||||||
proxyDropByStart,
|
|
||||||
proxyDropByEnabled,
|
|
||||||
}, nil)
|
|
||||||
require.Len(visitors, 0)
|
|
||||||
require.Len(proxies, 1)
|
|
||||||
require.Equal("keep", proxies[0].GetBaseConfig().Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types
|
// TestYAMLEdgeCases tests edge cases for YAML parsing, including non-map types
|
||||||
func TestYAMLEdgeCases(t *testing.T) {
|
func TestYAMLEdgeCases(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
@@ -495,111 +306,3 @@ serverPort: 7000
|
|||||||
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
||||||
require.Equal(7000, clientCfg.ServerPort)
|
require.Equal(7000, clientCfg.ServerPort)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestTOMLSyntaxErrorWithPosition(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
// TOML with syntax error (unclosed table array header)
|
|
||||||
content := `serverAddr = "127.0.0.1"
|
|
||||||
serverPort = 7000
|
|
||||||
|
|
||||||
[[proxies]
|
|
||||||
name = "test"
|
|
||||||
`
|
|
||||||
|
|
||||||
clientCfg := v1.ClientConfig{}
|
|
||||||
err := LoadConfigure([]byte(content), &clientCfg, false, "toml")
|
|
||||||
require.Error(err)
|
|
||||||
require.Contains(err.Error(), "toml")
|
|
||||||
require.Contains(err.Error(), "line")
|
|
||||||
require.Contains(err.Error(), "column")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestTOMLTypeMismatchErrorWithFieldInfo(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
// TOML with wrong type: proxies should be a table array, not a string
|
|
||||||
content := `serverAddr = "127.0.0.1"
|
|
||||||
serverPort = 7000
|
|
||||||
proxies = "this should be a table array"
|
|
||||||
`
|
|
||||||
|
|
||||||
clientCfg := v1.ClientConfig{}
|
|
||||||
err := LoadConfigure([]byte(content), &clientCfg, false, "toml")
|
|
||||||
require.Error(err)
|
|
||||||
// The error should contain field info
|
|
||||||
errMsg := err.Error()
|
|
||||||
require.Contains(errMsg, "proxies")
|
|
||||||
require.NotContains(errMsg, "line")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFindFieldLineInContent(t *testing.T) {
|
|
||||||
content := []byte(`serverAddr = "127.0.0.1"
|
|
||||||
serverPort = 7000
|
|
||||||
|
|
||||||
[[proxies]]
|
|
||||||
name = "test"
|
|
||||||
type = "tcp"
|
|
||||||
remotePort = 6000
|
|
||||||
`)
|
|
||||||
|
|
||||||
tests := []struct {
|
|
||||||
fieldPath string
|
|
||||||
wantLine int
|
|
||||||
}{
|
|
||||||
{"serverAddr", 1},
|
|
||||||
{"serverPort", 2},
|
|
||||||
{"name", 5},
|
|
||||||
{"type", 6},
|
|
||||||
{"remotePort", 7},
|
|
||||||
{"nonexistent", 0},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.fieldPath, func(t *testing.T) {
|
|
||||||
got := findFieldLineInContent(content, tt.fieldPath)
|
|
||||||
require.Equal(t, tt.wantLine, got)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestFormatDetection(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
path string
|
|
||||||
format string
|
|
||||||
}{
|
|
||||||
{"config.toml", "toml"},
|
|
||||||
{"config.TOML", "toml"},
|
|
||||||
{"config.yaml", "yaml"},
|
|
||||||
{"config.yml", "yaml"},
|
|
||||||
{"config.json", "json"},
|
|
||||||
{"config.ini", ""},
|
|
||||||
{"config", ""},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.path, func(t *testing.T) {
|
|
||||||
require.Equal(t, tt.format, detectFormatFromPath(tt.path))
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestValidTOMLStillWorks(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
// Valid TOML with format hint should work fine
|
|
||||||
content := `serverAddr = "127.0.0.1"
|
|
||||||
serverPort = 7000
|
|
||||||
|
|
||||||
[[proxies]]
|
|
||||||
name = "test"
|
|
||||||
type = "tcp"
|
|
||||||
remotePort = 6000
|
|
||||||
`
|
|
||||||
clientCfg := v1.ClientConfig{}
|
|
||||||
err := LoadConfigure([]byte(content), &clientCfg, false, "toml")
|
|
||||||
require.NoError(err)
|
|
||||||
require.Equal("127.0.0.1", clientCfg.ServerAddr)
|
|
||||||
require.Equal(7000, clientCfg.ServerPort)
|
|
||||||
require.Len(clientCfg.Proxies, 1)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
// Copyright 2026 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 source
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"maps"
|
|
||||||
"slices"
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Aggregator struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
|
|
||||||
configSource *ConfigSource
|
|
||||||
storeSource *StoreSource
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAggregator(configSource *ConfigSource) *Aggregator {
|
|
||||||
if configSource == nil {
|
|
||||||
configSource = NewConfigSource()
|
|
||||||
}
|
|
||||||
return &Aggregator{
|
|
||||||
configSource: configSource,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Aggregator) SetStoreSource(storeSource *StoreSource) {
|
|
||||||
a.mu.Lock()
|
|
||||||
defer a.mu.Unlock()
|
|
||||||
|
|
||||||
a.storeSource = storeSource
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Aggregator) ConfigSource() *ConfigSource {
|
|
||||||
return a.configSource
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Aggregator) StoreSource() *StoreSource {
|
|
||||||
return a.storeSource
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Aggregator) getSourcesLocked() []Source {
|
|
||||||
sources := make([]Source, 0, 2)
|
|
||||||
if a.configSource != nil {
|
|
||||||
sources = append(sources, a.configSource)
|
|
||||||
}
|
|
||||||
if a.storeSource != nil {
|
|
||||||
sources = append(sources, a.storeSource)
|
|
||||||
}
|
|
||||||
return sources
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Aggregator) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
|
||||||
a.mu.RLock()
|
|
||||||
entries := a.getSourcesLocked()
|
|
||||||
a.mu.RUnlock()
|
|
||||||
|
|
||||||
if len(entries) == 0 {
|
|
||||||
return nil, nil, errors.New("no sources configured")
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyMap := make(map[string]v1.ProxyConfigurer)
|
|
||||||
visitorMap := make(map[string]v1.VisitorConfigurer)
|
|
||||||
|
|
||||||
for _, src := range entries {
|
|
||||||
proxies, visitors, err := src.Load()
|
|
||||||
if err != nil {
|
|
||||||
return nil, nil, fmt.Errorf("load source: %w", err)
|
|
||||||
}
|
|
||||||
for _, p := range proxies {
|
|
||||||
proxyMap[p.GetBaseConfig().Name] = p
|
|
||||||
}
|
|
||||||
for _, v := range visitors {
|
|
||||||
visitorMap[v.GetBaseConfig().Name] = v
|
|
||||||
}
|
|
||||||
}
|
|
||||||
proxies, visitors := a.mapsToSortedSlices(proxyMap, visitorMap)
|
|
||||||
return proxies, visitors, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *Aggregator) mapsToSortedSlices(
|
|
||||||
proxyMap map[string]v1.ProxyConfigurer,
|
|
||||||
visitorMap map[string]v1.VisitorConfigurer,
|
|
||||||
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer) {
|
|
||||||
proxies := slices.SortedFunc(maps.Values(proxyMap), func(x, y v1.ProxyConfigurer) int {
|
|
||||||
return cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name)
|
|
||||||
})
|
|
||||||
visitors := slices.SortedFunc(maps.Values(visitorMap), func(x, y v1.VisitorConfigurer) int {
|
|
||||||
return cmp.Compare(x.GetBaseConfig().Name, y.GetBaseConfig().Name)
|
|
||||||
})
|
|
||||||
return proxies, visitors
|
|
||||||
}
|
|
||||||
@@ -1,238 +0,0 @@
|
|||||||
// Copyright 2026 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 source
|
|
||||||
|
|
||||||
import (
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// mockProxy creates a TCP proxy config for testing
|
|
||||||
func mockProxy(name string) v1.ProxyConfigurer {
|
|
||||||
cfg := &v1.TCPProxyConfig{}
|
|
||||||
cfg.Name = name
|
|
||||||
cfg.Type = "tcp"
|
|
||||||
cfg.LocalPort = 8080
|
|
||||||
cfg.RemotePort = 9090
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
// mockVisitor creates a STCP visitor config for testing
|
|
||||||
func mockVisitor(name string) v1.VisitorConfigurer {
|
|
||||||
cfg := &v1.STCPVisitorConfig{}
|
|
||||||
cfg.Name = name
|
|
||||||
cfg.Type = "stcp"
|
|
||||||
cfg.ServerName = "test-server"
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestStoreSource(t *testing.T) *StoreSource {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "store.json")
|
|
||||||
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
|
||||||
require.NoError(t, err)
|
|
||||||
return storeSource
|
|
||||||
}
|
|
||||||
|
|
||||||
func newTestAggregator(t *testing.T, storeSource *StoreSource) *Aggregator {
|
|
||||||
t.Helper()
|
|
||||||
|
|
||||||
configSource := NewConfigSource()
|
|
||||||
agg := NewAggregator(configSource)
|
|
||||||
if storeSource != nil {
|
|
||||||
agg.SetStoreSource(storeSource)
|
|
||||||
}
|
|
||||||
return agg
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewAggregator_CreatesConfigSourceWhenNil(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
agg := NewAggregator(nil)
|
|
||||||
require.NotNil(agg)
|
|
||||||
require.NotNil(agg.ConfigSource())
|
|
||||||
require.Nil(agg.StoreSource())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewAggregator_WithoutStore(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
configSource := NewConfigSource()
|
|
||||||
agg := NewAggregator(configSource)
|
|
||||||
require.NotNil(agg)
|
|
||||||
require.Same(configSource, agg.ConfigSource())
|
|
||||||
require.Nil(agg.StoreSource())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestNewAggregator_WithStore(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
storeSource := newTestStoreSource(t)
|
|
||||||
configSource := NewConfigSource()
|
|
||||||
agg := NewAggregator(configSource)
|
|
||||||
agg.SetStoreSource(storeSource)
|
|
||||||
|
|
||||||
require.Same(configSource, agg.ConfigSource())
|
|
||||||
require.Same(storeSource, agg.StoreSource())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAggregator_SetStoreSource_Overwrite(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
agg := newTestAggregator(t, nil)
|
|
||||||
first := newTestStoreSource(t)
|
|
||||||
second := newTestStoreSource(t)
|
|
||||||
|
|
||||||
agg.SetStoreSource(first)
|
|
||||||
require.Same(first, agg.StoreSource())
|
|
||||||
|
|
||||||
agg.SetStoreSource(second)
|
|
||||||
require.Same(second, agg.StoreSource())
|
|
||||||
|
|
||||||
agg.SetStoreSource(nil)
|
|
||||||
require.Nil(agg.StoreSource())
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAggregator_MergeBySourceOrder(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
storeSource := newTestStoreSource(t)
|
|
||||||
agg := newTestAggregator(t, storeSource)
|
|
||||||
|
|
||||||
configSource := agg.ConfigSource()
|
|
||||||
|
|
||||||
configShared := mockProxy("shared").(*v1.TCPProxyConfig)
|
|
||||||
configShared.LocalPort = 1111
|
|
||||||
configOnly := mockProxy("only-in-config").(*v1.TCPProxyConfig)
|
|
||||||
configOnly.LocalPort = 1112
|
|
||||||
|
|
||||||
err := configSource.ReplaceAll([]v1.ProxyConfigurer{configShared, configOnly}, nil)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
storeShared := mockProxy("shared").(*v1.TCPProxyConfig)
|
|
||||||
storeShared.LocalPort = 2222
|
|
||||||
storeOnly := mockProxy("only-in-store").(*v1.TCPProxyConfig)
|
|
||||||
storeOnly.LocalPort = 2223
|
|
||||||
err = storeSource.AddProxy(storeShared)
|
|
||||||
require.NoError(err)
|
|
||||||
err = storeSource.AddProxy(storeOnly)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
proxies, visitors, err := agg.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(visitors, 0)
|
|
||||||
require.Len(proxies, 3)
|
|
||||||
|
|
||||||
var sharedProxy *v1.TCPProxyConfig
|
|
||||||
for _, p := range proxies {
|
|
||||||
if p.GetBaseConfig().Name == "shared" {
|
|
||||||
sharedProxy = p.(*v1.TCPProxyConfig)
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
require.NotNil(sharedProxy)
|
|
||||||
require.Equal(2222, sharedProxy.LocalPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAggregator_DisabledEntryIsSourceLocalFilter(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
storeSource := newTestStoreSource(t)
|
|
||||||
agg := newTestAggregator(t, storeSource)
|
|
||||||
configSource := agg.ConfigSource()
|
|
||||||
|
|
||||||
lowProxy := mockProxy("shared-proxy").(*v1.TCPProxyConfig)
|
|
||||||
lowProxy.LocalPort = 1111
|
|
||||||
err := configSource.ReplaceAll([]v1.ProxyConfigurer{lowProxy}, nil)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
disabled := false
|
|
||||||
highProxy := mockProxy("shared-proxy").(*v1.TCPProxyConfig)
|
|
||||||
highProxy.LocalPort = 2222
|
|
||||||
highProxy.Enabled = &disabled
|
|
||||||
err = storeSource.AddProxy(highProxy)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
proxies, visitors, err := agg.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(proxies, 1)
|
|
||||||
require.Len(visitors, 0)
|
|
||||||
|
|
||||||
proxy := proxies[0].(*v1.TCPProxyConfig)
|
|
||||||
require.Equal("shared-proxy", proxy.Name)
|
|
||||||
require.Equal(1111, proxy.LocalPort)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAggregator_VisitorMerge(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
storeSource := newTestStoreSource(t)
|
|
||||||
agg := newTestAggregator(t, storeSource)
|
|
||||||
|
|
||||||
err := agg.ConfigSource().ReplaceAll(nil, []v1.VisitorConfigurer{mockVisitor("visitor1")})
|
|
||||||
require.NoError(err)
|
|
||||||
err = storeSource.AddVisitor(mockVisitor("visitor2"))
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
_, visitors, err := agg.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(visitors, 2)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAggregator_Load_ReturnsSortedByName(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
agg := newTestAggregator(t, nil)
|
|
||||||
err := agg.ConfigSource().ReplaceAll(
|
|
||||||
[]v1.ProxyConfigurer{mockProxy("charlie"), mockProxy("alice"), mockProxy("bob")},
|
|
||||||
[]v1.VisitorConfigurer{mockVisitor("zulu"), mockVisitor("alpha")},
|
|
||||||
)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
proxies, visitors, err := agg.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(proxies, 3)
|
|
||||||
require.Equal("alice", proxies[0].GetBaseConfig().Name)
|
|
||||||
require.Equal("bob", proxies[1].GetBaseConfig().Name)
|
|
||||||
require.Equal("charlie", proxies[2].GetBaseConfig().Name)
|
|
||||||
require.Len(visitors, 2)
|
|
||||||
require.Equal("alpha", visitors[0].GetBaseConfig().Name)
|
|
||||||
require.Equal("zulu", visitors[1].GetBaseConfig().Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestAggregator_Load_ReturnsDefensiveCopies(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
agg := newTestAggregator(t, nil)
|
|
||||||
err := agg.ConfigSource().ReplaceAll([]v1.ProxyConfigurer{mockProxy("ssh")}, nil)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
proxies, _, err := agg.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(proxies, 1)
|
|
||||||
require.Equal("ssh", proxies[0].GetBaseConfig().Name)
|
|
||||||
|
|
||||||
proxies[0].GetBaseConfig().Name = "alice.ssh"
|
|
||||||
|
|
||||||
proxies2, _, err := agg.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(proxies2, 1)
|
|
||||||
require.Equal("ssh", proxies2[0].GetBaseConfig().Name)
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// Copyright 2026 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 source
|
|
||||||
|
|
||||||
import (
|
|
||||||
"sync"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// baseSource provides shared state and behavior for Source implementations.
|
|
||||||
// It manages proxy/visitor storage.
|
|
||||||
// Concrete types (ConfigSource, StoreSource) embed this struct.
|
|
||||||
type baseSource struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
|
|
||||||
proxies map[string]v1.ProxyConfigurer
|
|
||||||
visitors map[string]v1.VisitorConfigurer
|
|
||||||
}
|
|
||||||
|
|
||||||
func newBaseSource() baseSource {
|
|
||||||
return baseSource{
|
|
||||||
proxies: make(map[string]v1.ProxyConfigurer),
|
|
||||||
visitors: make(map[string]v1.VisitorConfigurer),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load returns all enabled proxy and visitor configurations.
|
|
||||||
// Configurations with Enabled explicitly set to false are filtered out.
|
|
||||||
func (s *baseSource) Load() ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
proxies := make([]v1.ProxyConfigurer, 0, len(s.proxies))
|
|
||||||
for _, p := range s.proxies {
|
|
||||||
// Filter out disabled proxies (nil or true means enabled)
|
|
||||||
if enabled := p.GetBaseConfig().Enabled; enabled != nil && !*enabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
proxies = append(proxies, p)
|
|
||||||
}
|
|
||||||
|
|
||||||
visitors := make([]v1.VisitorConfigurer, 0, len(s.visitors))
|
|
||||||
for _, v := range s.visitors {
|
|
||||||
// Filter out disabled visitors (nil or true means enabled)
|
|
||||||
if enabled := v.GetBaseConfig().Enabled; enabled != nil && !*enabled {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
visitors = append(visitors, v)
|
|
||||||
}
|
|
||||||
|
|
||||||
return cloneConfigurers(proxies, visitors)
|
|
||||||
}
|
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
package source
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestBaseSourceLoadReturnsClonedConfigurers(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
src := NewConfigSource()
|
|
||||||
|
|
||||||
proxyCfg := &v1.TCPProxyConfig{
|
|
||||||
ProxyBaseConfig: v1.ProxyBaseConfig{
|
|
||||||
Name: "proxy1",
|
|
||||||
Type: "tcp",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
visitorCfg := &v1.STCPVisitorConfig{
|
|
||||||
VisitorBaseConfig: v1.VisitorBaseConfig{
|
|
||||||
Name: "visitor1",
|
|
||||||
Type: "stcp",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
err := src.ReplaceAll([]v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
firstProxies, firstVisitors, err := src.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(firstProxies, 1)
|
|
||||||
require.Len(firstVisitors, 1)
|
|
||||||
|
|
||||||
// Mutate loaded objects as runtime completion would do.
|
|
||||||
firstProxies[0].Complete()
|
|
||||||
firstVisitors[0].Complete()
|
|
||||||
|
|
||||||
secondProxies, secondVisitors, err := src.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(secondProxies, 1)
|
|
||||||
require.Len(secondVisitors, 1)
|
|
||||||
|
|
||||||
require.Empty(secondProxies[0].GetBaseConfig().LocalIP)
|
|
||||||
require.Empty(secondVisitors[0].GetBaseConfig().BindAddr)
|
|
||||||
}
|
|
||||||
@@ -1,43 +0,0 @@
|
|||||||
// Copyright 2026 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 source
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func cloneConfigurers(
|
|
||||||
proxies []v1.ProxyConfigurer,
|
|
||||||
visitors []v1.VisitorConfigurer,
|
|
||||||
) ([]v1.ProxyConfigurer, []v1.VisitorConfigurer, error) {
|
|
||||||
clonedProxies := make([]v1.ProxyConfigurer, 0, len(proxies))
|
|
||||||
clonedVisitors := make([]v1.VisitorConfigurer, 0, len(visitors))
|
|
||||||
|
|
||||||
for _, cfg := range proxies {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, nil, fmt.Errorf("proxy cannot be nil")
|
|
||||||
}
|
|
||||||
clonedProxies = append(clonedProxies, cfg.Clone())
|
|
||||||
}
|
|
||||||
for _, cfg := range visitors {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, nil, fmt.Errorf("visitor cannot be nil")
|
|
||||||
}
|
|
||||||
clonedVisitors = append(clonedVisitors, cfg.Clone())
|
|
||||||
}
|
|
||||||
return clonedProxies, clonedVisitors, nil
|
|
||||||
}
|
|
||||||
@@ -1,65 +0,0 @@
|
|||||||
// Copyright 2026 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 source
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConfigSource implements Source for in-memory configuration.
|
|
||||||
// All operations are thread-safe.
|
|
||||||
type ConfigSource struct {
|
|
||||||
baseSource
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConfigSource() *ConfigSource {
|
|
||||||
return &ConfigSource{
|
|
||||||
baseSource: newBaseSource(),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ReplaceAll replaces all proxy and visitor configurations atomically.
|
|
||||||
func (s *ConfigSource) ReplaceAll(proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer) error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
nextProxies := make(map[string]v1.ProxyConfigurer, len(proxies))
|
|
||||||
for _, p := range proxies {
|
|
||||||
if p == nil {
|
|
||||||
return fmt.Errorf("proxy cannot be nil")
|
|
||||||
}
|
|
||||||
name := p.GetBaseConfig().Name
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("proxy name cannot be empty")
|
|
||||||
}
|
|
||||||
nextProxies[name] = p
|
|
||||||
}
|
|
||||||
nextVisitors := make(map[string]v1.VisitorConfigurer, len(visitors))
|
|
||||||
for _, v := range visitors {
|
|
||||||
if v == nil {
|
|
||||||
return fmt.Errorf("visitor cannot be nil")
|
|
||||||
}
|
|
||||||
name := v.GetBaseConfig().Name
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("visitor name cannot be empty")
|
|
||||||
}
|
|
||||||
nextVisitors[name] = v
|
|
||||||
}
|
|
||||||
s.proxies = nextProxies
|
|
||||||
s.visitors = nextVisitors
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,173 +0,0 @@
|
|||||||
// Copyright 2026 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 source
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestNewConfigSource(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
src := NewConfigSource()
|
|
||||||
require.NotNil(src)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigSource_ReplaceAll(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
src := NewConfigSource()
|
|
||||||
|
|
||||||
err := src.ReplaceAll(
|
|
||||||
[]v1.ProxyConfigurer{mockProxy("proxy1"), mockProxy("proxy2")},
|
|
||||||
[]v1.VisitorConfigurer{mockVisitor("visitor1")},
|
|
||||||
)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
proxies, visitors, err := src.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(proxies, 2)
|
|
||||||
require.Len(visitors, 1)
|
|
||||||
|
|
||||||
// ReplaceAll again should replace everything
|
|
||||||
err = src.ReplaceAll(
|
|
||||||
[]v1.ProxyConfigurer{mockProxy("proxy3")},
|
|
||||||
nil,
|
|
||||||
)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
proxies, visitors, err = src.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(proxies, 1)
|
|
||||||
require.Len(visitors, 0)
|
|
||||||
require.Equal("proxy3", proxies[0].GetBaseConfig().Name)
|
|
||||||
|
|
||||||
// ReplaceAll with nil proxy should fail
|
|
||||||
err = src.ReplaceAll([]v1.ProxyConfigurer{nil}, nil)
|
|
||||||
require.Error(err)
|
|
||||||
|
|
||||||
// ReplaceAll with empty name proxy should fail
|
|
||||||
err = src.ReplaceAll([]v1.ProxyConfigurer{&v1.TCPProxyConfig{}}, nil)
|
|
||||||
require.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigSource_Load(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
src := NewConfigSource()
|
|
||||||
|
|
||||||
err := src.ReplaceAll(
|
|
||||||
[]v1.ProxyConfigurer{mockProxy("proxy1"), mockProxy("proxy2")},
|
|
||||||
[]v1.VisitorConfigurer{mockVisitor("visitor1")},
|
|
||||||
)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
proxies, visitors, err := src.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(proxies, 2)
|
|
||||||
require.Len(visitors, 1)
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestConfigSource_Load_FiltersDisabled verifies that Load() filters out
|
|
||||||
// proxies and visitors with Enabled explicitly set to false.
|
|
||||||
func TestConfigSource_Load_FiltersDisabled(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
src := NewConfigSource()
|
|
||||||
|
|
||||||
disabled := false
|
|
||||||
enabled := true
|
|
||||||
|
|
||||||
// Create enabled proxy (nil Enabled = enabled by default)
|
|
||||||
enabledProxy := mockProxy("enabled-proxy")
|
|
||||||
|
|
||||||
// Create disabled proxy
|
|
||||||
disabledProxy := &v1.TCPProxyConfig{}
|
|
||||||
disabledProxy.Name = "disabled-proxy"
|
|
||||||
disabledProxy.Type = "tcp"
|
|
||||||
disabledProxy.Enabled = &disabled
|
|
||||||
|
|
||||||
// Create explicitly enabled proxy
|
|
||||||
explicitEnabledProxy := &v1.TCPProxyConfig{}
|
|
||||||
explicitEnabledProxy.Name = "explicit-enabled-proxy"
|
|
||||||
explicitEnabledProxy.Type = "tcp"
|
|
||||||
explicitEnabledProxy.Enabled = &enabled
|
|
||||||
|
|
||||||
// Create enabled visitor (nil Enabled = enabled by default)
|
|
||||||
enabledVisitor := mockVisitor("enabled-visitor")
|
|
||||||
|
|
||||||
// Create disabled visitor
|
|
||||||
disabledVisitor := &v1.STCPVisitorConfig{}
|
|
||||||
disabledVisitor.Name = "disabled-visitor"
|
|
||||||
disabledVisitor.Type = "stcp"
|
|
||||||
disabledVisitor.Enabled = &disabled
|
|
||||||
|
|
||||||
err := src.ReplaceAll(
|
|
||||||
[]v1.ProxyConfigurer{enabledProxy, disabledProxy, explicitEnabledProxy},
|
|
||||||
[]v1.VisitorConfigurer{enabledVisitor, disabledVisitor},
|
|
||||||
)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
// Load should filter out disabled configs
|
|
||||||
proxies, visitors, err := src.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(proxies, 2, "Should have 2 enabled proxies")
|
|
||||||
require.Len(visitors, 1, "Should have 1 enabled visitor")
|
|
||||||
|
|
||||||
// Verify the correct proxies are returned
|
|
||||||
proxyNames := make([]string, 0, len(proxies))
|
|
||||||
for _, p := range proxies {
|
|
||||||
proxyNames = append(proxyNames, p.GetBaseConfig().Name)
|
|
||||||
}
|
|
||||||
require.Contains(proxyNames, "enabled-proxy")
|
|
||||||
require.Contains(proxyNames, "explicit-enabled-proxy")
|
|
||||||
require.NotContains(proxyNames, "disabled-proxy")
|
|
||||||
|
|
||||||
// Verify the correct visitor is returned
|
|
||||||
require.Equal("enabled-visitor", visitors[0].GetBaseConfig().Name)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestConfigSource_ReplaceAll_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
src := NewConfigSource()
|
|
||||||
|
|
||||||
proxyCfg := &v1.TCPProxyConfig{}
|
|
||||||
proxyCfg.Name = "proxy1"
|
|
||||||
proxyCfg.Type = "tcp"
|
|
||||||
proxyCfg.LocalPort = 10080
|
|
||||||
|
|
||||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
|
||||||
visitorCfg.Name = "visitor1"
|
|
||||||
visitorCfg.Type = "xtcp"
|
|
||||||
visitorCfg.ServerName = "server1"
|
|
||||||
visitorCfg.SecretKey = "secret"
|
|
||||||
visitorCfg.BindPort = 10081
|
|
||||||
|
|
||||||
err := src.ReplaceAll([]v1.ProxyConfigurer{proxyCfg}, []v1.VisitorConfigurer{visitorCfg})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
proxies, visitors, err := src.Load()
|
|
||||||
require.NoError(err)
|
|
||||||
require.Len(proxies, 1)
|
|
||||||
require.Len(visitors, 1)
|
|
||||||
require.Empty(proxies[0].GetBaseConfig().LocalIP)
|
|
||||||
require.Empty(visitors[0].GetBaseConfig().BindAddr)
|
|
||||||
require.Empty(visitors[0].(*v1.XTCPVisitorConfig).Protocol)
|
|
||||||
}
|
|
||||||
@@ -1,37 +0,0 @@
|
|||||||
// Copyright 2026 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 source
|
|
||||||
|
|
||||||
import (
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Source is the interface for configuration sources.
|
|
||||||
// A Source provides proxy and visitor configurations from various backends.
|
|
||||||
// Aggregator currently uses the built-in config source as base and an optional
|
|
||||||
// store source as higher-priority overlay.
|
|
||||||
type Source interface {
|
|
||||||
// Load loads the proxy and visitor configurations from this source.
|
|
||||||
// Returns the loaded configurations and any error encountered.
|
|
||||||
// A disabled entry in one source is source-local filtering, not a cross-source
|
|
||||||
// tombstone for entries from lower-priority sources.
|
|
||||||
//
|
|
||||||
// Error handling contract with Aggregator:
|
|
||||||
// - When err is nil, returned slices are consumed.
|
|
||||||
// - When err is non-nil, Aggregator aborts the merge and returns the error.
|
|
||||||
// - To publish best-effort or partial results, return those results with
|
|
||||||
// err set to nil.
|
|
||||||
Load() (proxies []v1.ProxyConfigurer, visitors []v1.VisitorConfigurer, err error)
|
|
||||||
}
|
|
||||||
@@ -1,367 +0,0 @@
|
|||||||
// Copyright 2026 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 source
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
|
||||||
)
|
|
||||||
|
|
||||||
type StoreSourceConfig struct {
|
|
||||||
Path string `json:"path"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type storeData struct {
|
|
||||||
Proxies []v1.TypedProxyConfig `json:"proxies,omitempty"`
|
|
||||||
Visitors []v1.TypedVisitorConfig `json:"visitors,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type StoreSource struct {
|
|
||||||
baseSource
|
|
||||||
config StoreSourceConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
var (
|
|
||||||
ErrAlreadyExists = errors.New("already exists")
|
|
||||||
ErrNotFound = errors.New("not found")
|
|
||||||
)
|
|
||||||
|
|
||||||
func NewStoreSource(cfg StoreSourceConfig) (*StoreSource, error) {
|
|
||||||
if cfg.Path == "" {
|
|
||||||
return nil, fmt.Errorf("path is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
s := &StoreSource{
|
|
||||||
baseSource: newBaseSource(),
|
|
||||||
config: cfg,
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := s.loadFromFile(); err != nil {
|
|
||||||
if !os.IsNotExist(err) {
|
|
||||||
return nil, fmt.Errorf("failed to load existing data: %w", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return s, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) loadFromFile() error {
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
return s.loadFromFileUnlocked()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) loadFromFileUnlocked() error {
|
|
||||||
data, err := os.ReadFile(s.config.Path)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
type rawStoreData struct {
|
|
||||||
Proxies []jsonx.RawMessage `json:"proxies,omitempty"`
|
|
||||||
Visitors []jsonx.RawMessage `json:"visitors,omitempty"`
|
|
||||||
}
|
|
||||||
stored := rawStoreData{}
|
|
||||||
if err := jsonx.Unmarshal(data, &stored); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse JSON: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.proxies = make(map[string]v1.ProxyConfigurer)
|
|
||||||
s.visitors = make(map[string]v1.VisitorConfigurer)
|
|
||||||
|
|
||||||
for i, proxyData := range stored.Proxies {
|
|
||||||
proxyCfg, err := v1.DecodeProxyConfigurerJSON(proxyData, v1.DecodeOptions{
|
|
||||||
DisallowUnknownFields: false,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to decode proxy at index %d: %w", i, err)
|
|
||||||
}
|
|
||||||
name := proxyCfg.GetBaseConfig().Name
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("proxy name cannot be empty")
|
|
||||||
}
|
|
||||||
s.proxies[name] = proxyCfg
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, visitorData := range stored.Visitors {
|
|
||||||
visitorCfg, err := v1.DecodeVisitorConfigurerJSON(visitorData, v1.DecodeOptions{
|
|
||||||
DisallowUnknownFields: false,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to decode visitor at index %d: %w", i, err)
|
|
||||||
}
|
|
||||||
name := visitorCfg.GetBaseConfig().Name
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("visitor name cannot be empty")
|
|
||||||
}
|
|
||||||
s.visitors[name] = visitorCfg
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) saveToFileUnlocked() error {
|
|
||||||
stored := storeData{
|
|
||||||
Proxies: make([]v1.TypedProxyConfig, 0, len(s.proxies)),
|
|
||||||
Visitors: make([]v1.TypedVisitorConfig, 0, len(s.visitors)),
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, p := range s.proxies {
|
|
||||||
stored.Proxies = append(stored.Proxies, v1.TypedProxyConfig{ProxyConfigurer: p})
|
|
||||||
}
|
|
||||||
for _, v := range s.visitors {
|
|
||||||
stored.Visitors = append(stored.Visitors, v1.TypedVisitorConfig{VisitorConfigurer: v})
|
|
||||||
}
|
|
||||||
|
|
||||||
data, err := jsonx.MarshalIndent(stored, "", " ")
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to marshal JSON: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
dir := filepath.Dir(s.config.Path)
|
|
||||||
if err := os.MkdirAll(dir, 0o755); err != nil {
|
|
||||||
return fmt.Errorf("failed to create directory: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tmpPath := s.config.Path + ".tmp"
|
|
||||||
|
|
||||||
f, err := os.OpenFile(tmpPath, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, 0o600)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to create temp file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := f.Write(data); err != nil {
|
|
||||||
f.Close()
|
|
||||||
os.Remove(tmpPath)
|
|
||||||
return fmt.Errorf("failed to write temp file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := f.Sync(); err != nil {
|
|
||||||
f.Close()
|
|
||||||
os.Remove(tmpPath)
|
|
||||||
return fmt.Errorf("failed to sync temp file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := f.Close(); err != nil {
|
|
||||||
os.Remove(tmpPath)
|
|
||||||
return fmt.Errorf("failed to close temp file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.Rename(tmpPath, s.config.Path); err != nil {
|
|
||||||
os.Remove(tmpPath)
|
|
||||||
return fmt.Errorf("failed to rename temp file: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) AddProxy(proxy v1.ProxyConfigurer) error {
|
|
||||||
if proxy == nil {
|
|
||||||
return fmt.Errorf("proxy cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
name := proxy.GetBaseConfig().Name
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("proxy name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
if _, exists := s.proxies[name]; exists {
|
|
||||||
return fmt.Errorf("%w: proxy %q", ErrAlreadyExists, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.proxies[name] = proxy
|
|
||||||
|
|
||||||
if err := s.saveToFileUnlocked(); err != nil {
|
|
||||||
delete(s.proxies, name)
|
|
||||||
return fmt.Errorf("failed to persist: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) UpdateProxy(proxy v1.ProxyConfigurer) error {
|
|
||||||
if proxy == nil {
|
|
||||||
return fmt.Errorf("proxy cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
name := proxy.GetBaseConfig().Name
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("proxy name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
oldProxy, exists := s.proxies[name]
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("%w: proxy %q", ErrNotFound, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.proxies[name] = proxy
|
|
||||||
|
|
||||||
if err := s.saveToFileUnlocked(); err != nil {
|
|
||||||
s.proxies[name] = oldProxy
|
|
||||||
return fmt.Errorf("failed to persist: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) RemoveProxy(name string) error {
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("proxy name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
oldProxy, exists := s.proxies[name]
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("%w: proxy %q", ErrNotFound, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(s.proxies, name)
|
|
||||||
|
|
||||||
if err := s.saveToFileUnlocked(); err != nil {
|
|
||||||
s.proxies[name] = oldProxy
|
|
||||||
return fmt.Errorf("failed to persist: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) GetProxy(name string) v1.ProxyConfigurer {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
p, exists := s.proxies[name]
|
|
||||||
if !exists {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return p
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) AddVisitor(visitor v1.VisitorConfigurer) error {
|
|
||||||
if visitor == nil {
|
|
||||||
return fmt.Errorf("visitor cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
name := visitor.GetBaseConfig().Name
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("visitor name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
if _, exists := s.visitors[name]; exists {
|
|
||||||
return fmt.Errorf("%w: visitor %q", ErrAlreadyExists, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.visitors[name] = visitor
|
|
||||||
|
|
||||||
if err := s.saveToFileUnlocked(); err != nil {
|
|
||||||
delete(s.visitors, name)
|
|
||||||
return fmt.Errorf("failed to persist: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) UpdateVisitor(visitor v1.VisitorConfigurer) error {
|
|
||||||
if visitor == nil {
|
|
||||||
return fmt.Errorf("visitor cannot be nil")
|
|
||||||
}
|
|
||||||
|
|
||||||
name := visitor.GetBaseConfig().Name
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("visitor name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
oldVisitor, exists := s.visitors[name]
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("%w: visitor %q", ErrNotFound, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
s.visitors[name] = visitor
|
|
||||||
|
|
||||||
if err := s.saveToFileUnlocked(); err != nil {
|
|
||||||
s.visitors[name] = oldVisitor
|
|
||||||
return fmt.Errorf("failed to persist: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) RemoveVisitor(name string) error {
|
|
||||||
if name == "" {
|
|
||||||
return fmt.Errorf("visitor name cannot be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
s.mu.Lock()
|
|
||||||
defer s.mu.Unlock()
|
|
||||||
|
|
||||||
oldVisitor, exists := s.visitors[name]
|
|
||||||
if !exists {
|
|
||||||
return fmt.Errorf("%w: visitor %q", ErrNotFound, name)
|
|
||||||
}
|
|
||||||
|
|
||||||
delete(s.visitors, name)
|
|
||||||
|
|
||||||
if err := s.saveToFileUnlocked(); err != nil {
|
|
||||||
s.visitors[name] = oldVisitor
|
|
||||||
return fmt.Errorf("failed to persist: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) GetVisitor(name string) v1.VisitorConfigurer {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
v, exists := s.visitors[name]
|
|
||||||
if !exists {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) GetAllProxies() ([]v1.ProxyConfigurer, error) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
result := make([]v1.ProxyConfigurer, 0, len(s.proxies))
|
|
||||||
for _, p := range s.proxies {
|
|
||||||
result = append(result, p)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *StoreSource) GetAllVisitors() ([]v1.VisitorConfigurer, error) {
|
|
||||||
s.mu.RLock()
|
|
||||||
defer s.mu.RUnlock()
|
|
||||||
|
|
||||||
result := make([]v1.VisitorConfigurer, 0, len(s.visitors))
|
|
||||||
for _, v := range s.visitors {
|
|
||||||
result = append(result, v)
|
|
||||||
}
|
|
||||||
return result, nil
|
|
||||||
}
|
|
||||||
@@ -1,121 +0,0 @@
|
|||||||
// Copyright 2026 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 source
|
|
||||||
|
|
||||||
import (
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "store.json")
|
|
||||||
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
proxyCfg := &v1.TCPProxyConfig{}
|
|
||||||
proxyCfg.Name = "proxy1"
|
|
||||||
proxyCfg.Type = "tcp"
|
|
||||||
proxyCfg.LocalPort = 10080
|
|
||||||
|
|
||||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
|
||||||
visitorCfg.Name = "visitor1"
|
|
||||||
visitorCfg.Type = "xtcp"
|
|
||||||
visitorCfg.ServerName = "server1"
|
|
||||||
visitorCfg.SecretKey = "secret"
|
|
||||||
visitorCfg.BindPort = 10081
|
|
||||||
|
|
||||||
err = storeSource.AddProxy(proxyCfg)
|
|
||||||
require.NoError(err)
|
|
||||||
err = storeSource.AddVisitor(visitorCfg)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
gotProxy := storeSource.GetProxy("proxy1")
|
|
||||||
require.NotNil(gotProxy)
|
|
||||||
require.Empty(gotProxy.GetBaseConfig().LocalIP)
|
|
||||||
|
|
||||||
gotVisitor := storeSource.GetVisitor("visitor1")
|
|
||||||
require.NotNil(gotVisitor)
|
|
||||||
require.Empty(gotVisitor.GetBaseConfig().BindAddr)
|
|
||||||
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "store.json")
|
|
||||||
|
|
||||||
proxyCfg := &v1.TCPProxyConfig{}
|
|
||||||
proxyCfg.Name = "proxy1"
|
|
||||||
proxyCfg.Type = "tcp"
|
|
||||||
proxyCfg.LocalPort = 10080
|
|
||||||
|
|
||||||
visitorCfg := &v1.XTCPVisitorConfig{}
|
|
||||||
visitorCfg.Name = "visitor1"
|
|
||||||
visitorCfg.Type = "xtcp"
|
|
||||||
visitorCfg.ServerName = "server1"
|
|
||||||
visitorCfg.SecretKey = "secret"
|
|
||||||
visitorCfg.BindPort = 10081
|
|
||||||
|
|
||||||
stored := storeData{
|
|
||||||
Proxies: []v1.TypedProxyConfig{{ProxyConfigurer: proxyCfg}},
|
|
||||||
Visitors: []v1.TypedVisitorConfig{{VisitorConfigurer: visitorCfg}},
|
|
||||||
}
|
|
||||||
data, err := jsonx.Marshal(stored)
|
|
||||||
require.NoError(err)
|
|
||||||
err = os.WriteFile(path, data, 0o600)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
gotProxy := storeSource.GetProxy("proxy1")
|
|
||||||
require.NotNil(gotProxy)
|
|
||||||
require.Empty(gotProxy.GetBaseConfig().LocalIP)
|
|
||||||
|
|
||||||
gotVisitor := storeSource.GetVisitor("visitor1")
|
|
||||||
require.NotNil(gotVisitor)
|
|
||||||
require.Empty(gotVisitor.GetBaseConfig().BindAddr)
|
|
||||||
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStoreSource_LoadFromFile_UnknownFieldsAreIgnored(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
path := filepath.Join(t.TempDir(), "store.json")
|
|
||||||
raw := []byte(`{
|
|
||||||
"proxies": [
|
|
||||||
{"name":"proxy1","type":"tcp","localPort":10080,"unexpected":"value"}
|
|
||||||
],
|
|
||||||
"visitors": [
|
|
||||||
{"name":"visitor1","type":"xtcp","serverName":"server1","secretKey":"secret","bindPort":10081,"unexpected":"value"}
|
|
||||||
]
|
|
||||||
}`)
|
|
||||||
err := os.WriteFile(path, raw, 0o600)
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
require.NotNil(storeSource.GetProxy("proxy1"))
|
|
||||||
require.NotNil(storeSource.GetVisitor("visitor1"))
|
|
||||||
}
|
|
||||||
@@ -38,7 +38,7 @@ func parseNumberRangePair(firstRangeStr, secondRangeStr string) ([]NumberPair, e
|
|||||||
return nil, fmt.Errorf("first and second range numbers are not in pairs")
|
return nil, fmt.Errorf("first and second range numbers are not in pairs")
|
||||||
}
|
}
|
||||||
pairs := make([]NumberPair, 0, len(firstRangeNumbers))
|
pairs := make([]NumberPair, 0, len(firstRangeNumbers))
|
||||||
for i := range firstRangeNumbers {
|
for i := 0; i < len(firstRangeNumbers); i++ {
|
||||||
pairs = append(pairs, NumberPair{
|
pairs = append(pairs, NumberPair{
|
||||||
First: firstRangeNumbers[i],
|
First: firstRangeNumbers[i],
|
||||||
Second: secondRangeNumbers[i],
|
Second: secondRangeNumbers[i],
|
||||||
|
|||||||
@@ -70,18 +70,24 @@ func (q *BandwidthQuantity) UnmarshalString(s string) error {
|
|||||||
f float64
|
f float64
|
||||||
err error
|
err error
|
||||||
)
|
)
|
||||||
if fstr, ok := strings.CutSuffix(s, "MB"); ok {
|
switch {
|
||||||
|
case strings.HasSuffix(s, "MB"):
|
||||||
base = MB
|
base = MB
|
||||||
|
fstr := strings.TrimSuffix(s, "MB")
|
||||||
f, err = strconv.ParseFloat(fstr, 64)
|
f, err = strconv.ParseFloat(fstr, 64)
|
||||||
} else if fstr, ok := strings.CutSuffix(s, "KB"); ok {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case strings.HasSuffix(s, "KB"):
|
||||||
base = KB
|
base = KB
|
||||||
|
fstr := strings.TrimSuffix(s, "KB")
|
||||||
f, err = strconv.ParseFloat(fstr, 64)
|
f, err = strconv.ParseFloat(fstr, 64)
|
||||||
} else {
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
return errors.New("unit not support")
|
return errors.New("unit not support")
|
||||||
}
|
}
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
q.s = s
|
q.s = s
|
||||||
q.i = int64(f * float64(base))
|
q.i = int64(f * float64(base))
|
||||||
@@ -137,8 +143,8 @@ func (p PortsRangeSlice) String() string {
|
|||||||
func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) {
|
func NewPortsRangeSliceFromString(str string) ([]PortsRange, error) {
|
||||||
str = strings.TrimSpace(str)
|
str = strings.TrimSpace(str)
|
||||||
out := []PortsRange{}
|
out := []PortsRange{}
|
||||||
numRanges := strings.SplitSeq(str, ",")
|
numRanges := strings.Split(str, ",")
|
||||||
for numRangeStr := range numRanges {
|
for _, numRangeStr := range numRanges {
|
||||||
// 1000-2000 or 2001
|
// 1000-2000 or 2001
|
||||||
numArray := strings.Split(numRangeStr, "-")
|
numArray := strings.Split(numRangeStr, "-")
|
||||||
// length: only 1 or 2 is correct
|
// length: only 1 or 2 is correct
|
||||||
|
|||||||
@@ -39,31 +39,6 @@ func TestBandwidthQuantity(t *testing.T) {
|
|||||||
require.Equal(`{"b":"1KB","int":5}`, string(buf))
|
require.Equal(`{"b":"1KB","int":5}`, string(buf))
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestBandwidthQuantity_MB(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
var w Wrap
|
|
||||||
err := json.Unmarshal([]byte(`{"b":"2MB","int":1}`), &w)
|
|
||||||
require.NoError(err)
|
|
||||||
require.EqualValues(2*MB, w.B.Bytes())
|
|
||||||
|
|
||||||
buf, err := json.Marshal(&w)
|
|
||||||
require.NoError(err)
|
|
||||||
require.Equal(`{"b":"2MB","int":1}`, string(buf))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBandwidthQuantity_InvalidUnit(t *testing.T) {
|
|
||||||
var w Wrap
|
|
||||||
err := json.Unmarshal([]byte(`{"b":"1GB","int":1}`), &w)
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBandwidthQuantity_InvalidNumber(t *testing.T) {
|
|
||||||
var w Wrap
|
|
||||||
err := json.Unmarshal([]byte(`{"b":"abcKB","int":1}`), &w)
|
|
||||||
require.Error(t, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestPortsRangeSlice2String(t *testing.T) {
|
func TestPortsRangeSlice2String(t *testing.T) {
|
||||||
require := require.New(t)
|
require := require.New(t)
|
||||||
|
|
||||||
|
|||||||
@@ -77,9 +77,6 @@ type ClientCommonConfig struct {
|
|||||||
|
|
||||||
// Include other config files for proxies.
|
// Include other config files for proxies.
|
||||||
IncludeConfigFiles []string `json:"includes,omitempty"`
|
IncludeConfigFiles []string `json:"includes,omitempty"`
|
||||||
|
|
||||||
// Store config enables the built-in store source (not configurable via sources list).
|
|
||||||
Store StoreConfig `json:"store,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ClientCommonConfig) Complete() error {
|
func (c *ClientCommonConfig) Complete() error {
|
||||||
|
|||||||
@@ -1,109 +0,0 @@
|
|||||||
package v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestProxyCloneDeepCopy(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
enabled := true
|
|
||||||
pluginHTTP2 := true
|
|
||||||
cfg := &HTTPProxyConfig{
|
|
||||||
ProxyBaseConfig: ProxyBaseConfig{
|
|
||||||
Name: "p1",
|
|
||||||
Type: "http",
|
|
||||||
Enabled: &enabled,
|
|
||||||
Annotations: map[string]string{"a": "1"},
|
|
||||||
Metadatas: map[string]string{"m": "1"},
|
|
||||||
HealthCheck: HealthCheckConfig{
|
|
||||||
Type: "http",
|
|
||||||
HTTPHeaders: []HTTPHeader{
|
|
||||||
{Name: "X-Test", Value: "v1"},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
ProxyBackend: ProxyBackend{
|
|
||||||
Plugin: TypedClientPluginOptions{
|
|
||||||
Type: PluginHTTPS2HTTP,
|
|
||||||
ClientPluginOptions: &HTTPS2HTTPPluginOptions{
|
|
||||||
Type: PluginHTTPS2HTTP,
|
|
||||||
EnableHTTP2: &pluginHTTP2,
|
|
||||||
RequestHeaders: HeaderOperations{Set: map[string]string{"k": "v"}},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
DomainConfig: DomainConfig{
|
|
||||||
CustomDomains: []string{"a.example.com"},
|
|
||||||
SubDomain: "a",
|
|
||||||
},
|
|
||||||
Locations: []string{"/api"},
|
|
||||||
RequestHeaders: HeaderOperations{Set: map[string]string{"h1": "v1"}},
|
|
||||||
ResponseHeaders: HeaderOperations{Set: map[string]string{"h2": "v2"}},
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned := cfg.Clone().(*HTTPProxyConfig)
|
|
||||||
|
|
||||||
*cloned.Enabled = false
|
|
||||||
cloned.Annotations["a"] = "changed"
|
|
||||||
cloned.Metadatas["m"] = "changed"
|
|
||||||
cloned.HealthCheck.HTTPHeaders[0].Value = "changed"
|
|
||||||
cloned.CustomDomains[0] = "b.example.com"
|
|
||||||
cloned.Locations[0] = "/new"
|
|
||||||
cloned.RequestHeaders.Set["h1"] = "changed"
|
|
||||||
cloned.ResponseHeaders.Set["h2"] = "changed"
|
|
||||||
clientPlugin := cloned.Plugin.ClientPluginOptions.(*HTTPS2HTTPPluginOptions)
|
|
||||||
*clientPlugin.EnableHTTP2 = false
|
|
||||||
clientPlugin.RequestHeaders.Set["k"] = "changed"
|
|
||||||
|
|
||||||
require.True(*cfg.Enabled)
|
|
||||||
require.Equal("1", cfg.Annotations["a"])
|
|
||||||
require.Equal("1", cfg.Metadatas["m"])
|
|
||||||
require.Equal("v1", cfg.HealthCheck.HTTPHeaders[0].Value)
|
|
||||||
require.Equal("a.example.com", cfg.CustomDomains[0])
|
|
||||||
require.Equal("/api", cfg.Locations[0])
|
|
||||||
require.Equal("v1", cfg.RequestHeaders.Set["h1"])
|
|
||||||
require.Equal("v2", cfg.ResponseHeaders.Set["h2"])
|
|
||||||
|
|
||||||
origPlugin := cfg.Plugin.ClientPluginOptions.(*HTTPS2HTTPPluginOptions)
|
|
||||||
require.True(*origPlugin.EnableHTTP2)
|
|
||||||
require.Equal("v", origPlugin.RequestHeaders.Set["k"])
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestVisitorCloneDeepCopy(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
enabled := true
|
|
||||||
cfg := &XTCPVisitorConfig{
|
|
||||||
VisitorBaseConfig: VisitorBaseConfig{
|
|
||||||
Name: "v1",
|
|
||||||
Type: "xtcp",
|
|
||||||
Enabled: &enabled,
|
|
||||||
ServerName: "server",
|
|
||||||
BindPort: 7000,
|
|
||||||
Plugin: TypedVisitorPluginOptions{
|
|
||||||
Type: VisitorPluginVirtualNet,
|
|
||||||
VisitorPluginOptions: &VirtualNetVisitorPluginOptions{
|
|
||||||
Type: VisitorPluginVirtualNet,
|
|
||||||
DestinationIP: "10.0.0.1",
|
|
||||||
},
|
|
||||||
},
|
|
||||||
},
|
|
||||||
NatTraversal: &NatTraversalConfig{
|
|
||||||
DisableAssistedAddrs: true,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
cloned := cfg.Clone().(*XTCPVisitorConfig)
|
|
||||||
*cloned.Enabled = false
|
|
||||||
cloned.NatTraversal.DisableAssistedAddrs = false
|
|
||||||
visitorPlugin := cloned.Plugin.VisitorPluginOptions.(*VirtualNetVisitorPluginOptions)
|
|
||||||
visitorPlugin.DestinationIP = "10.0.0.2"
|
|
||||||
|
|
||||||
require.True(*cfg.Enabled)
|
|
||||||
require.True(cfg.NatTraversal.DisableAssistedAddrs)
|
|
||||||
origPlugin := cfg.Plugin.VisitorPluginOptions.(*VirtualNetVisitorPluginOptions)
|
|
||||||
require.Equal("10.0.0.1", origPlugin.DestinationIP)
|
|
||||||
}
|
|
||||||
@@ -15,11 +15,23 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"sync"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// TODO(fatedier): Due to the current implementation issue of the go json library, the UnmarshalJSON method
|
||||||
|
// of a custom struct cannot access the DisallowUnknownFields parameter of the parent decoder.
|
||||||
|
// Here, a global variable is temporarily used to control whether unknown fields are allowed.
|
||||||
|
// Once the v2 version is implemented by the community, we can switch to a standardized approach.
|
||||||
|
//
|
||||||
|
// https://github.com/golang/go/issues/41144
|
||||||
|
// https://github.com/golang/go/discussions/63397
|
||||||
|
var (
|
||||||
|
DisallowUnknownFields = false
|
||||||
|
DisallowUnknownFieldsMu sync.Mutex
|
||||||
|
)
|
||||||
|
|
||||||
type AuthScope string
|
type AuthScope string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -92,14 +104,6 @@ type NatTraversalConfig struct {
|
|||||||
DisableAssistedAddrs bool `json:"disableAssistedAddrs,omitempty"`
|
DisableAssistedAddrs bool `json:"disableAssistedAddrs,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *NatTraversalConfig) Clone() *NatTraversalConfig {
|
|
||||||
if c == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *c
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
type LogConfig struct {
|
type LogConfig struct {
|
||||||
// This is destination where frp should write the logs.
|
// This is destination where frp should write the logs.
|
||||||
// If "console" is used, logs will be printed to stdout, otherwise,
|
// If "console" is used, logs will be printed to stdout, otherwise,
|
||||||
@@ -134,12 +138,6 @@ type HeaderOperations struct {
|
|||||||
Set map[string]string `json:"set,omitempty"`
|
Set map[string]string `json:"set,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o HeaderOperations) Clone() HeaderOperations {
|
|
||||||
return HeaderOperations{
|
|
||||||
Set: maps.Clone(o.Set),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPHeader struct {
|
type HTTPHeader struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Value string `json:"value"`
|
Value string `json:"value"`
|
||||||
|
|||||||
@@ -1,195 +0,0 @@
|
|||||||
// Copyright 2026 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 v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"errors"
|
|
||||||
"fmt"
|
|
||||||
"reflect"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DecodeOptions struct {
|
|
||||||
DisallowUnknownFields bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func decodeJSONWithOptions(b []byte, out any, options DecodeOptions) error {
|
|
||||||
return jsonx.UnmarshalWithOptions(b, out, jsonx.DecodeOptions{
|
|
||||||
RejectUnknownMembers: options.DisallowUnknownFields,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func isJSONNull(b []byte) bool {
|
|
||||||
return len(b) == 0 || string(b) == "null"
|
|
||||||
}
|
|
||||||
|
|
||||||
type typedEnvelope struct {
|
|
||||||
Type string `json:"type"`
|
|
||||||
Plugin jsonx.RawMessage `json:"plugin,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeProxyConfigurerJSON(b []byte, options DecodeOptions) (ProxyConfigurer, error) {
|
|
||||||
if isJSONNull(b) {
|
|
||||||
return nil, errors.New("type is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
var env typedEnvelope
|
|
||||||
if err := jsonx.Unmarshal(b, &env); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
configurer := NewProxyConfigurerByType(ProxyType(env.Type))
|
|
||||||
if configurer == nil {
|
|
||||||
return nil, fmt.Errorf("unknown proxy type: %s", env.Type)
|
|
||||||
}
|
|
||||||
if err := decodeJSONWithOptions(b, configurer, options); err != nil {
|
|
||||||
return nil, fmt.Errorf("unmarshal ProxyConfig error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) {
|
|
||||||
plugin, err := DecodeClientPluginOptionsJSON(env.Plugin, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unmarshal proxy plugin error: %v", err)
|
|
||||||
}
|
|
||||||
configurer.GetBaseConfig().Plugin = plugin
|
|
||||||
}
|
|
||||||
return configurer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeVisitorConfigurerJSON(b []byte, options DecodeOptions) (VisitorConfigurer, error) {
|
|
||||||
if isJSONNull(b) {
|
|
||||||
return nil, errors.New("type is required")
|
|
||||||
}
|
|
||||||
|
|
||||||
var env typedEnvelope
|
|
||||||
if err := jsonx.Unmarshal(b, &env); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
configurer := NewVisitorConfigurerByType(VisitorType(env.Type))
|
|
||||||
if configurer == nil {
|
|
||||||
return nil, fmt.Errorf("unknown visitor type: %s", env.Type)
|
|
||||||
}
|
|
||||||
if err := decodeJSONWithOptions(b, configurer, options); err != nil {
|
|
||||||
return nil, fmt.Errorf("unmarshal VisitorConfig error: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(env.Plugin) > 0 && !isJSONNull(env.Plugin) {
|
|
||||||
plugin, err := DecodeVisitorPluginOptionsJSON(env.Plugin, options)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("unmarshal visitor plugin error: %v", err)
|
|
||||||
}
|
|
||||||
configurer.GetBaseConfig().Plugin = plugin
|
|
||||||
}
|
|
||||||
return configurer, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeClientPluginOptionsJSON(b []byte, options DecodeOptions) (TypedClientPluginOptions, error) {
|
|
||||||
if isJSONNull(b) {
|
|
||||||
return TypedClientPluginOptions{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var env typedEnvelope
|
|
||||||
if err := jsonx.Unmarshal(b, &env); err != nil {
|
|
||||||
return TypedClientPluginOptions{}, err
|
|
||||||
}
|
|
||||||
if env.Type == "" {
|
|
||||||
return TypedClientPluginOptions{}, errors.New("plugin type is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := clientPluginOptionsTypeMap[env.Type]
|
|
||||||
if !ok {
|
|
||||||
return TypedClientPluginOptions{}, fmt.Errorf("unknown plugin type: %s", env.Type)
|
|
||||||
}
|
|
||||||
optionsStruct := reflect.New(v).Interface().(ClientPluginOptions)
|
|
||||||
if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {
|
|
||||||
return TypedClientPluginOptions{}, fmt.Errorf("unmarshal ClientPluginOptions error: %v", err)
|
|
||||||
}
|
|
||||||
return TypedClientPluginOptions{
|
|
||||||
Type: env.Type,
|
|
||||||
ClientPluginOptions: optionsStruct,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeVisitorPluginOptionsJSON(b []byte, options DecodeOptions) (TypedVisitorPluginOptions, error) {
|
|
||||||
if isJSONNull(b) {
|
|
||||||
return TypedVisitorPluginOptions{}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
var env typedEnvelope
|
|
||||||
if err := jsonx.Unmarshal(b, &env); err != nil {
|
|
||||||
return TypedVisitorPluginOptions{}, err
|
|
||||||
}
|
|
||||||
if env.Type == "" {
|
|
||||||
return TypedVisitorPluginOptions{}, errors.New("visitor plugin type is empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
v, ok := visitorPluginOptionsTypeMap[env.Type]
|
|
||||||
if !ok {
|
|
||||||
return TypedVisitorPluginOptions{}, fmt.Errorf("unknown visitor plugin type: %s", env.Type)
|
|
||||||
}
|
|
||||||
optionsStruct := reflect.New(v).Interface().(VisitorPluginOptions)
|
|
||||||
if err := decodeJSONWithOptions(b, optionsStruct, options); err != nil {
|
|
||||||
return TypedVisitorPluginOptions{}, fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err)
|
|
||||||
}
|
|
||||||
return TypedVisitorPluginOptions{
|
|
||||||
Type: env.Type,
|
|
||||||
VisitorPluginOptions: optionsStruct,
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func DecodeClientConfigJSON(b []byte, options DecodeOptions) (ClientConfig, error) {
|
|
||||||
type rawClientConfig struct {
|
|
||||||
ClientCommonConfig
|
|
||||||
Proxies []jsonx.RawMessage `json:"proxies,omitempty"`
|
|
||||||
Visitors []jsonx.RawMessage `json:"visitors,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
raw := rawClientConfig{}
|
|
||||||
if err := decodeJSONWithOptions(b, &raw, options); err != nil {
|
|
||||||
return ClientConfig{}, err
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := ClientConfig{
|
|
||||||
ClientCommonConfig: raw.ClientCommonConfig,
|
|
||||||
Proxies: make([]TypedProxyConfig, 0, len(raw.Proxies)),
|
|
||||||
Visitors: make([]TypedVisitorConfig, 0, len(raw.Visitors)),
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, proxyData := range raw.Proxies {
|
|
||||||
proxyCfg, err := DecodeProxyConfigurerJSON(proxyData, options)
|
|
||||||
if err != nil {
|
|
||||||
return ClientConfig{}, fmt.Errorf("decode proxy at index %d: %w", i, err)
|
|
||||||
}
|
|
||||||
cfg.Proxies = append(cfg.Proxies, TypedProxyConfig{
|
|
||||||
Type: proxyCfg.GetBaseConfig().Type,
|
|
||||||
ProxyConfigurer: proxyCfg,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
for i, visitorData := range raw.Visitors {
|
|
||||||
visitorCfg, err := DecodeVisitorConfigurerJSON(visitorData, options)
|
|
||||||
if err != nil {
|
|
||||||
return ClientConfig{}, fmt.Errorf("decode visitor at index %d: %w", i, err)
|
|
||||||
}
|
|
||||||
cfg.Visitors = append(cfg.Visitors, TypedVisitorConfig{
|
|
||||||
Type: visitorCfg.GetBaseConfig().Type,
|
|
||||||
VisitorConfigurer: visitorCfg,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
@@ -1,86 +0,0 @@
|
|||||||
// Copyright 2026 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 v1
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestDecodeProxyConfigurerJSON_StrictPluginUnknownFields(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
data := []byte(`{
|
|
||||||
"name":"p1",
|
|
||||||
"type":"tcp",
|
|
||||||
"localPort":10080,
|
|
||||||
"plugin":{
|
|
||||||
"type":"http2https",
|
|
||||||
"localAddr":"127.0.0.1:8080",
|
|
||||||
"unknownInPlugin":"value"
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
_, err := DecodeProxyConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: false})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
_, err = DecodeProxyConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: true})
|
|
||||||
require.ErrorContains(err, "unknownInPlugin")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDecodeVisitorConfigurerJSON_StrictPluginUnknownFields(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
data := []byte(`{
|
|
||||||
"name":"v1",
|
|
||||||
"type":"stcp",
|
|
||||||
"serverName":"server",
|
|
||||||
"bindPort":10081,
|
|
||||||
"plugin":{
|
|
||||||
"type":"virtual_net",
|
|
||||||
"destinationIP":"10.0.0.1",
|
|
||||||
"unknownInPlugin":"value"
|
|
||||||
}
|
|
||||||
}`)
|
|
||||||
|
|
||||||
_, err := DecodeVisitorConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: false})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
_, err = DecodeVisitorConfigurerJSON(data, DecodeOptions{DisallowUnknownFields: true})
|
|
||||||
require.ErrorContains(err, "unknownInPlugin")
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestDecodeClientConfigJSON_StrictUnknownProxyField(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
data := []byte(`{
|
|
||||||
"serverPort":7000,
|
|
||||||
"proxies":[
|
|
||||||
{
|
|
||||||
"name":"p1",
|
|
||||||
"type":"tcp",
|
|
||||||
"localPort":10080,
|
|
||||||
"unknownField":"value"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
}`)
|
|
||||||
|
|
||||||
_, err := DecodeClientConfigJSON(data, DecodeOptions{DisallowUnknownFields: false})
|
|
||||||
require.NoError(err)
|
|
||||||
|
|
||||||
_, err = DecodeClientConfigJSON(data, DecodeOptions{DisallowUnknownFields: true})
|
|
||||||
require.ErrorContains(err, "unknownField")
|
|
||||||
}
|
|
||||||
@@ -15,13 +15,16 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"maps"
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
|
||||||
|
"github.com/samber/lo"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/types"
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
"github.com/fatedier/frp/pkg/msg"
|
"github.com/fatedier/frp/pkg/msg"
|
||||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,23 +102,11 @@ type HealthCheckConfig struct {
|
|||||||
HTTPHeaders []HTTPHeader `json:"httpHeaders,omitempty"`
|
HTTPHeaders []HTTPHeader `json:"httpHeaders,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c HealthCheckConfig) Clone() HealthCheckConfig {
|
|
||||||
out := c
|
|
||||||
out.HTTPHeaders = slices.Clone(c.HTTPHeaders)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
type DomainConfig struct {
|
type DomainConfig struct {
|
||||||
CustomDomains []string `json:"customDomains,omitempty"`
|
CustomDomains []string `json:"customDomains,omitempty"`
|
||||||
SubDomain string `json:"subdomain,omitempty"`
|
SubDomain string `json:"subdomain,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c DomainConfig) Clone() DomainConfig {
|
|
||||||
out := c
|
|
||||||
out.CustomDomains = slices.Clone(c.CustomDomains)
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProxyBaseConfig struct {
|
type ProxyBaseConfig struct {
|
||||||
Name string `json:"name"`
|
Name string `json:"name"`
|
||||||
Type string `json:"type"`
|
Type string `json:"type"`
|
||||||
@@ -131,27 +122,12 @@ type ProxyBaseConfig struct {
|
|||||||
ProxyBackend
|
ProxyBackend
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c ProxyBaseConfig) Clone() ProxyBaseConfig {
|
|
||||||
out := c
|
|
||||||
out.Enabled = util.ClonePtr(c.Enabled)
|
|
||||||
out.Annotations = maps.Clone(c.Annotations)
|
|
||||||
out.Metadatas = maps.Clone(c.Metadatas)
|
|
||||||
out.HealthCheck = c.HealthCheck.Clone()
|
|
||||||
out.ProxyBackend = c.ProxyBackend.Clone()
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c ProxyBackend) Clone() ProxyBackend {
|
|
||||||
out := c
|
|
||||||
out.Plugin = c.Plugin.Clone()
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ProxyBaseConfig) GetBaseConfig() *ProxyBaseConfig {
|
func (c *ProxyBaseConfig) GetBaseConfig() *ProxyBaseConfig {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *ProxyBaseConfig) Complete() {
|
func (c *ProxyBaseConfig) Complete(namePrefix string) {
|
||||||
|
c.Name = lo.Ternary(namePrefix == "", "", namePrefix+".") + c.Name
|
||||||
c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1")
|
c.LocalIP = util.EmptyOr(c.LocalIP, "127.0.0.1")
|
||||||
c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient)
|
c.Transport.BandwidthLimitMode = util.EmptyOr(c.Transport.BandwidthLimitMode, types.BandwidthLimitModeClient)
|
||||||
|
|
||||||
@@ -199,24 +175,40 @@ type TypedProxyConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
|
func (c *TypedProxyConfig) UnmarshalJSON(b []byte) error {
|
||||||
configurer, err := DecodeProxyConfigurerJSON(b, DecodeOptions{})
|
if len(b) == 4 && string(b) == "null" {
|
||||||
if err != nil {
|
return errors.New("type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
typeStruct := struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}{}
|
||||||
|
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Type = configurer.GetBaseConfig().Type
|
c.Type = typeStruct.Type
|
||||||
|
configurer := NewProxyConfigurerByType(ProxyType(typeStruct.Type))
|
||||||
|
if configurer == nil {
|
||||||
|
return fmt.Errorf("unknown proxy type: %s", typeStruct.Type)
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
||||||
|
if DisallowUnknownFields {
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
}
|
||||||
|
if err := decoder.Decode(configurer); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal ProxyConfig error: %v", err)
|
||||||
|
}
|
||||||
c.ProxyConfigurer = configurer
|
c.ProxyConfigurer = configurer
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
|
func (c *TypedProxyConfig) MarshalJSON() ([]byte, error) {
|
||||||
return jsonx.Marshal(c.ProxyConfigurer)
|
return json.Marshal(c.ProxyConfigurer)
|
||||||
}
|
}
|
||||||
|
|
||||||
type ProxyConfigurer interface {
|
type ProxyConfigurer interface {
|
||||||
Complete()
|
Complete(namePrefix string)
|
||||||
GetBaseConfig() *ProxyBaseConfig
|
GetBaseConfig() *ProxyBaseConfig
|
||||||
Clone() ProxyConfigurer
|
|
||||||
// MarshalToMsg marshals this config into a msg.NewProxy message. This
|
// MarshalToMsg marshals this config into a msg.NewProxy message. This
|
||||||
// function will be called on the frpc side.
|
// function will be called on the frpc side.
|
||||||
MarshalToMsg(*msg.NewProxy)
|
MarshalToMsg(*msg.NewProxy)
|
||||||
@@ -239,14 +231,14 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var proxyConfigTypeMap = map[ProxyType]reflect.Type{
|
var proxyConfigTypeMap = map[ProxyType]reflect.Type{
|
||||||
ProxyTypeTCP: reflect.TypeFor[TCPProxyConfig](),
|
ProxyTypeTCP: reflect.TypeOf(TCPProxyConfig{}),
|
||||||
ProxyTypeUDP: reflect.TypeFor[UDPProxyConfig](),
|
ProxyTypeUDP: reflect.TypeOf(UDPProxyConfig{}),
|
||||||
ProxyTypeHTTP: reflect.TypeFor[HTTPProxyConfig](),
|
ProxyTypeHTTP: reflect.TypeOf(HTTPProxyConfig{}),
|
||||||
ProxyTypeHTTPS: reflect.TypeFor[HTTPSProxyConfig](),
|
ProxyTypeHTTPS: reflect.TypeOf(HTTPSProxyConfig{}),
|
||||||
ProxyTypeTCPMUX: reflect.TypeFor[TCPMuxProxyConfig](),
|
ProxyTypeTCPMUX: reflect.TypeOf(TCPMuxProxyConfig{}),
|
||||||
ProxyTypeSTCP: reflect.TypeFor[STCPProxyConfig](),
|
ProxyTypeSTCP: reflect.TypeOf(STCPProxyConfig{}),
|
||||||
ProxyTypeXTCP: reflect.TypeFor[XTCPProxyConfig](),
|
ProxyTypeXTCP: reflect.TypeOf(XTCPProxyConfig{}),
|
||||||
ProxyTypeSUDP: reflect.TypeFor[SUDPProxyConfig](),
|
ProxyTypeSUDP: reflect.TypeOf(SUDPProxyConfig{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {
|
func NewProxyConfigurerByType(proxyType ProxyType) ProxyConfigurer {
|
||||||
@@ -279,12 +271,6 @@ func (c *TCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.RemotePort = m.RemotePort
|
c.RemotePort = m.RemotePort
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TCPProxyConfig) Clone() ProxyConfigurer {
|
|
||||||
out := *c
|
|
||||||
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ ProxyConfigurer = &UDPProxyConfig{}
|
var _ ProxyConfigurer = &UDPProxyConfig{}
|
||||||
|
|
||||||
type UDPProxyConfig struct {
|
type UDPProxyConfig struct {
|
||||||
@@ -305,12 +291,6 @@ func (c *UDPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.RemotePort = m.RemotePort
|
c.RemotePort = m.RemotePort
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *UDPProxyConfig) Clone() ProxyConfigurer {
|
|
||||||
out := *c
|
|
||||||
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ ProxyConfigurer = &HTTPProxyConfig{}
|
var _ ProxyConfigurer = &HTTPProxyConfig{}
|
||||||
|
|
||||||
type HTTPProxyConfig struct {
|
type HTTPProxyConfig struct {
|
||||||
@@ -354,16 +334,6 @@ func (c *HTTPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.RouteByHTTPUser = m.RouteByHTTPUser
|
c.RouteByHTTPUser = m.RouteByHTTPUser
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *HTTPProxyConfig) Clone() ProxyConfigurer {
|
|
||||||
out := *c
|
|
||||||
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
|
||||||
out.DomainConfig = c.DomainConfig.Clone()
|
|
||||||
out.Locations = slices.Clone(c.Locations)
|
|
||||||
out.RequestHeaders = c.RequestHeaders.Clone()
|
|
||||||
out.ResponseHeaders = c.ResponseHeaders.Clone()
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ ProxyConfigurer = &HTTPSProxyConfig{}
|
var _ ProxyConfigurer = &HTTPSProxyConfig{}
|
||||||
|
|
||||||
type HTTPSProxyConfig struct {
|
type HTTPSProxyConfig struct {
|
||||||
@@ -385,13 +355,6 @@ func (c *HTTPSProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.SubDomain = m.SubDomain
|
c.SubDomain = m.SubDomain
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *HTTPSProxyConfig) Clone() ProxyConfigurer {
|
|
||||||
out := *c
|
|
||||||
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
|
||||||
out.DomainConfig = c.DomainConfig.Clone()
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCPMultiplexerType string
|
type TCPMultiplexerType string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -432,13 +395,6 @@ func (c *TCPMuxProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.RouteByHTTPUser = m.RouteByHTTPUser
|
c.RouteByHTTPUser = m.RouteByHTTPUser
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TCPMuxProxyConfig) Clone() ProxyConfigurer {
|
|
||||||
out := *c
|
|
||||||
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
|
||||||
out.DomainConfig = c.DomainConfig.Clone()
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ ProxyConfigurer = &STCPProxyConfig{}
|
var _ ProxyConfigurer = &STCPProxyConfig{}
|
||||||
|
|
||||||
type STCPProxyConfig struct {
|
type STCPProxyConfig struct {
|
||||||
@@ -462,13 +418,6 @@ func (c *STCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.AllowUsers = m.AllowUsers
|
c.AllowUsers = m.AllowUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STCPProxyConfig) Clone() ProxyConfigurer {
|
|
||||||
out := *c
|
|
||||||
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
|
||||||
out.AllowUsers = slices.Clone(c.AllowUsers)
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ ProxyConfigurer = &XTCPProxyConfig{}
|
var _ ProxyConfigurer = &XTCPProxyConfig{}
|
||||||
|
|
||||||
type XTCPProxyConfig struct {
|
type XTCPProxyConfig struct {
|
||||||
@@ -495,14 +444,6 @@ func (c *XTCPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.AllowUsers = m.AllowUsers
|
c.AllowUsers = m.AllowUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *XTCPProxyConfig) Clone() ProxyConfigurer {
|
|
||||||
out := *c
|
|
||||||
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
|
||||||
out.AllowUsers = slices.Clone(c.AllowUsers)
|
|
||||||
out.NatTraversal = c.NatTraversal.Clone()
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ ProxyConfigurer = &SUDPProxyConfig{}
|
var _ ProxyConfigurer = &SUDPProxyConfig{}
|
||||||
|
|
||||||
type SUDPProxyConfig struct {
|
type SUDPProxyConfig struct {
|
||||||
@@ -525,10 +466,3 @@ func (c *SUDPProxyConfig) UnmarshalFromMsg(m *msg.NewProxy) {
|
|||||||
c.Secretkey = m.Sk
|
c.Secretkey = m.Sk
|
||||||
c.AllowUsers = m.AllowUsers
|
c.AllowUsers = m.AllowUsers
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SUDPProxyConfig) Clone() ProxyConfigurer {
|
|
||||||
out := *c
|
|
||||||
out.ProxyBaseConfig = c.ProxyBaseConfig.Clone()
|
|
||||||
out.AllowUsers = slices.Clone(c.AllowUsers)
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -15,11 +15,14 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -37,21 +40,20 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var clientPluginOptionsTypeMap = map[string]reflect.Type{
|
var clientPluginOptionsTypeMap = map[string]reflect.Type{
|
||||||
PluginHTTP2HTTPS: reflect.TypeFor[HTTP2HTTPSPluginOptions](),
|
PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
|
||||||
PluginHTTPProxy: reflect.TypeFor[HTTPProxyPluginOptions](),
|
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
|
||||||
PluginHTTPS2HTTP: reflect.TypeFor[HTTPS2HTTPPluginOptions](),
|
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
|
||||||
PluginHTTPS2HTTPS: reflect.TypeFor[HTTPS2HTTPSPluginOptions](),
|
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
|
||||||
PluginHTTP2HTTP: reflect.TypeFor[HTTP2HTTPPluginOptions](),
|
PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
|
||||||
PluginSocks5: reflect.TypeFor[Socks5PluginOptions](),
|
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
|
||||||
PluginStaticFile: reflect.TypeFor[StaticFilePluginOptions](),
|
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
|
||||||
PluginUnixDomainSocket: reflect.TypeFor[UnixDomainSocketPluginOptions](),
|
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
|
||||||
PluginTLS2Raw: reflect.TypeFor[TLS2RawPluginOptions](),
|
PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
|
||||||
PluginVirtualNet: reflect.TypeFor[VirtualNetPluginOptions](),
|
PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientPluginOptions interface {
|
type ClientPluginOptions interface {
|
||||||
Complete()
|
Complete()
|
||||||
Clone() ClientPluginOptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TypedClientPluginOptions struct {
|
type TypedClientPluginOptions struct {
|
||||||
@@ -59,25 +61,43 @@ type TypedClientPluginOptions struct {
|
|||||||
ClientPluginOptions
|
ClientPluginOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c TypedClientPluginOptions) Clone() TypedClientPluginOptions {
|
|
||||||
out := c
|
|
||||||
if c.ClientPluginOptions != nil {
|
|
||||||
out.ClientPluginOptions = c.ClientPluginOptions.Clone()
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
|
func (c *TypedClientPluginOptions) UnmarshalJSON(b []byte) error {
|
||||||
decoded, err := DecodeClientPluginOptionsJSON(b, DecodeOptions{})
|
if len(b) == 4 && string(b) == "null" {
|
||||||
if err != nil {
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
typeStruct := struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}{}
|
||||||
|
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
*c = decoded
|
|
||||||
|
c.Type = typeStruct.Type
|
||||||
|
if c.Type == "" {
|
||||||
|
return errors.New("plugin type is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := clientPluginOptionsTypeMap[typeStruct.Type]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown plugin type: %s", typeStruct.Type)
|
||||||
|
}
|
||||||
|
options := reflect.New(v).Interface().(ClientPluginOptions)
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
||||||
|
if DisallowUnknownFields {
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := decoder.Decode(options); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal ClientPluginOptions error: %v", err)
|
||||||
|
}
|
||||||
|
c.ClientPluginOptions = options
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
|
func (c *TypedClientPluginOptions) MarshalJSON() ([]byte, error) {
|
||||||
return jsonx.Marshal(c.ClientPluginOptions)
|
return json.Marshal(c.ClientPluginOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
type HTTP2HTTPSPluginOptions struct {
|
type HTTP2HTTPSPluginOptions struct {
|
||||||
@@ -89,15 +109,6 @@ type HTTP2HTTPSPluginOptions struct {
|
|||||||
|
|
||||||
func (o *HTTP2HTTPSPluginOptions) Complete() {}
|
func (o *HTTP2HTTPSPluginOptions) Complete() {}
|
||||||
|
|
||||||
func (o *HTTP2HTTPSPluginOptions) Clone() ClientPluginOptions {
|
|
||||||
if o == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *o
|
|
||||||
out.RequestHeaders = o.RequestHeaders.Clone()
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPProxyPluginOptions struct {
|
type HTTPProxyPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
HTTPUser string `json:"httpUser,omitempty"`
|
HTTPUser string `json:"httpUser,omitempty"`
|
||||||
@@ -106,14 +117,6 @@ type HTTPProxyPluginOptions struct {
|
|||||||
|
|
||||||
func (o *HTTPProxyPluginOptions) Complete() {}
|
func (o *HTTPProxyPluginOptions) Complete() {}
|
||||||
|
|
||||||
func (o *HTTPProxyPluginOptions) Clone() ClientPluginOptions {
|
|
||||||
if o == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *o
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPS2HTTPPluginOptions struct {
|
type HTTPS2HTTPPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
LocalAddr string `json:"localAddr,omitempty"`
|
LocalAddr string `json:"localAddr,omitempty"`
|
||||||
@@ -128,16 +131,6 @@ func (o *HTTPS2HTTPPluginOptions) Complete() {
|
|||||||
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
|
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *HTTPS2HTTPPluginOptions) Clone() ClientPluginOptions {
|
|
||||||
if o == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *o
|
|
||||||
out.RequestHeaders = o.RequestHeaders.Clone()
|
|
||||||
out.EnableHTTP2 = util.ClonePtr(o.EnableHTTP2)
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPS2HTTPSPluginOptions struct {
|
type HTTPS2HTTPSPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
LocalAddr string `json:"localAddr,omitempty"`
|
LocalAddr string `json:"localAddr,omitempty"`
|
||||||
@@ -152,16 +145,6 @@ func (o *HTTPS2HTTPSPluginOptions) Complete() {
|
|||||||
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
|
o.EnableHTTP2 = util.EmptyOr(o.EnableHTTP2, lo.ToPtr(true))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *HTTPS2HTTPSPluginOptions) Clone() ClientPluginOptions {
|
|
||||||
if o == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *o
|
|
||||||
out.RequestHeaders = o.RequestHeaders.Clone()
|
|
||||||
out.EnableHTTP2 = util.ClonePtr(o.EnableHTTP2)
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTP2HTTPPluginOptions struct {
|
type HTTP2HTTPPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
LocalAddr string `json:"localAddr,omitempty"`
|
LocalAddr string `json:"localAddr,omitempty"`
|
||||||
@@ -171,15 +154,6 @@ type HTTP2HTTPPluginOptions struct {
|
|||||||
|
|
||||||
func (o *HTTP2HTTPPluginOptions) Complete() {}
|
func (o *HTTP2HTTPPluginOptions) Complete() {}
|
||||||
|
|
||||||
func (o *HTTP2HTTPPluginOptions) Clone() ClientPluginOptions {
|
|
||||||
if o == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *o
|
|
||||||
out.RequestHeaders = o.RequestHeaders.Clone()
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
type Socks5PluginOptions struct {
|
type Socks5PluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
Username string `json:"username,omitempty"`
|
Username string `json:"username,omitempty"`
|
||||||
@@ -188,14 +162,6 @@ type Socks5PluginOptions struct {
|
|||||||
|
|
||||||
func (o *Socks5PluginOptions) Complete() {}
|
func (o *Socks5PluginOptions) Complete() {}
|
||||||
|
|
||||||
func (o *Socks5PluginOptions) Clone() ClientPluginOptions {
|
|
||||||
if o == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *o
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
type StaticFilePluginOptions struct {
|
type StaticFilePluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
LocalPath string `json:"localPath,omitempty"`
|
LocalPath string `json:"localPath,omitempty"`
|
||||||
@@ -206,14 +172,6 @@ type StaticFilePluginOptions struct {
|
|||||||
|
|
||||||
func (o *StaticFilePluginOptions) Complete() {}
|
func (o *StaticFilePluginOptions) Complete() {}
|
||||||
|
|
||||||
func (o *StaticFilePluginOptions) Clone() ClientPluginOptions {
|
|
||||||
if o == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *o
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
type UnixDomainSocketPluginOptions struct {
|
type UnixDomainSocketPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
UnixPath string `json:"unixPath,omitempty"`
|
UnixPath string `json:"unixPath,omitempty"`
|
||||||
@@ -221,14 +179,6 @@ type UnixDomainSocketPluginOptions struct {
|
|||||||
|
|
||||||
func (o *UnixDomainSocketPluginOptions) Complete() {}
|
func (o *UnixDomainSocketPluginOptions) Complete() {}
|
||||||
|
|
||||||
func (o *UnixDomainSocketPluginOptions) Clone() ClientPluginOptions {
|
|
||||||
if o == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *o
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
type TLS2RawPluginOptions struct {
|
type TLS2RawPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
LocalAddr string `json:"localAddr,omitempty"`
|
LocalAddr string `json:"localAddr,omitempty"`
|
||||||
@@ -238,24 +188,8 @@ type TLS2RawPluginOptions struct {
|
|||||||
|
|
||||||
func (o *TLS2RawPluginOptions) Complete() {}
|
func (o *TLS2RawPluginOptions) Complete() {}
|
||||||
|
|
||||||
func (o *TLS2RawPluginOptions) Clone() ClientPluginOptions {
|
|
||||||
if o == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *o
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
type VirtualNetPluginOptions struct {
|
type VirtualNetPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *VirtualNetPluginOptions) Complete() {}
|
func (o *VirtualNetPluginOptions) Complete() {}
|
||||||
|
|
||||||
func (o *VirtualNetPluginOptions) Clone() ClientPluginOptions {
|
|
||||||
if o == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *o
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,26 +0,0 @@
|
|||||||
// Copyright 2026 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 v1
|
|
||||||
|
|
||||||
// StoreConfig configures the built-in store source.
|
|
||||||
type StoreConfig struct {
|
|
||||||
// Path is the store file path.
|
|
||||||
Path string `json:"path,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsEnabled returns true if the store is configured with a valid path.
|
|
||||||
func (c *StoreConfig) IsEnabled() bool {
|
|
||||||
return c.Path != ""
|
|
||||||
}
|
|
||||||
@@ -81,7 +81,7 @@ func validateDomainConfigForClient(c *v1.DomainConfig) error {
|
|||||||
func validateDomainConfigForServer(c *v1.DomainConfig, s *v1.ServerConfig) error {
|
func validateDomainConfigForServer(c *v1.DomainConfig, s *v1.ServerConfig) error {
|
||||||
for _, domain := range c.CustomDomains {
|
for _, domain := range c.CustomDomains {
|
||||||
if s.SubDomainHost != "" && len(strings.Split(s.SubDomainHost, ".")) < len(strings.Split(domain, ".")) {
|
if s.SubDomainHost != "" && len(strings.Split(s.SubDomainHost, ".")) < len(strings.Split(domain, ".")) {
|
||||||
if strings.HasSuffix(domain, "."+s.SubDomainHost) {
|
if strings.Contains(domain, s.SubDomainHost) {
|
||||||
return fmt.Errorf("custom domain [%s] should not belong to subdomain host [%s]", domain, s.SubDomainHost)
|
return fmt.Errorf("custom domain [%s] should not belong to subdomain host [%s]", domain, s.SubDomainHost)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,14 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/util"
|
"github.com/fatedier/frp/pkg/util/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -47,27 +52,31 @@ type VisitorBaseConfig struct {
|
|||||||
Plugin TypedVisitorPluginOptions `json:"plugin,omitempty"`
|
Plugin TypedVisitorPluginOptions `json:"plugin,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c VisitorBaseConfig) Clone() VisitorBaseConfig {
|
|
||||||
out := c
|
|
||||||
out.Enabled = util.ClonePtr(c.Enabled)
|
|
||||||
out.Plugin = c.Plugin.Clone()
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig {
|
func (c *VisitorBaseConfig) GetBaseConfig() *VisitorBaseConfig {
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *VisitorBaseConfig) Complete() {
|
func (c *VisitorBaseConfig) Complete(g *ClientCommonConfig) {
|
||||||
if c.BindAddr == "" {
|
if c.BindAddr == "" {
|
||||||
c.BindAddr = "127.0.0.1"
|
c.BindAddr = "127.0.0.1"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
namePrefix := ""
|
||||||
|
if g.User != "" {
|
||||||
|
namePrefix = g.User + "."
|
||||||
|
}
|
||||||
|
c.Name = namePrefix + c.Name
|
||||||
|
|
||||||
|
if c.ServerUser != "" {
|
||||||
|
c.ServerName = c.ServerUser + "." + c.ServerName
|
||||||
|
} else {
|
||||||
|
c.ServerName = namePrefix + c.ServerName
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
type VisitorConfigurer interface {
|
type VisitorConfigurer interface {
|
||||||
Complete()
|
Complete(*ClientCommonConfig)
|
||||||
GetBaseConfig() *VisitorBaseConfig
|
GetBaseConfig() *VisitorBaseConfig
|
||||||
Clone() VisitorConfigurer
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type VisitorType string
|
type VisitorType string
|
||||||
@@ -79,9 +88,9 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var visitorConfigTypeMap = map[VisitorType]reflect.Type{
|
var visitorConfigTypeMap = map[VisitorType]reflect.Type{
|
||||||
VisitorTypeSTCP: reflect.TypeFor[STCPVisitorConfig](),
|
VisitorTypeSTCP: reflect.TypeOf(STCPVisitorConfig{}),
|
||||||
VisitorTypeXTCP: reflect.TypeFor[XTCPVisitorConfig](),
|
VisitorTypeXTCP: reflect.TypeOf(XTCPVisitorConfig{}),
|
||||||
VisitorTypeSUDP: reflect.TypeFor[SUDPVisitorConfig](),
|
VisitorTypeSUDP: reflect.TypeOf(SUDPVisitorConfig{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
type TypedVisitorConfig struct {
|
type TypedVisitorConfig struct {
|
||||||
@@ -90,18 +99,35 @@ type TypedVisitorConfig struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
|
func (c *TypedVisitorConfig) UnmarshalJSON(b []byte) error {
|
||||||
configurer, err := DecodeVisitorConfigurerJSON(b, DecodeOptions{})
|
if len(b) == 4 && string(b) == "null" {
|
||||||
if err != nil {
|
return errors.New("type is required")
|
||||||
|
}
|
||||||
|
|
||||||
|
typeStruct := struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}{}
|
||||||
|
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
c.Type = configurer.GetBaseConfig().Type
|
c.Type = typeStruct.Type
|
||||||
|
configurer := NewVisitorConfigurerByType(VisitorType(typeStruct.Type))
|
||||||
|
if configurer == nil {
|
||||||
|
return fmt.Errorf("unknown visitor type: %s", typeStruct.Type)
|
||||||
|
}
|
||||||
|
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
||||||
|
if DisallowUnknownFields {
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
}
|
||||||
|
if err := decoder.Decode(configurer); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal VisitorConfig error: %v", err)
|
||||||
|
}
|
||||||
c.VisitorConfigurer = configurer
|
c.VisitorConfigurer = configurer
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) {
|
func (c *TypedVisitorConfig) MarshalJSON() ([]byte, error) {
|
||||||
return jsonx.Marshal(c.VisitorConfigurer)
|
return json.Marshal(c.VisitorConfigurer)
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
|
func NewVisitorConfigurerByType(t VisitorType) VisitorConfigurer {
|
||||||
@@ -120,24 +146,12 @@ type STCPVisitorConfig struct {
|
|||||||
VisitorBaseConfig
|
VisitorBaseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *STCPVisitorConfig) Clone() VisitorConfigurer {
|
|
||||||
out := *c
|
|
||||||
out.VisitorBaseConfig = c.VisitorBaseConfig.Clone()
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ VisitorConfigurer = &SUDPVisitorConfig{}
|
var _ VisitorConfigurer = &SUDPVisitorConfig{}
|
||||||
|
|
||||||
type SUDPVisitorConfig struct {
|
type SUDPVisitorConfig struct {
|
||||||
VisitorBaseConfig
|
VisitorBaseConfig
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *SUDPVisitorConfig) Clone() VisitorConfigurer {
|
|
||||||
out := *c
|
|
||||||
out.VisitorBaseConfig = c.VisitorBaseConfig.Clone()
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|
||||||
var _ VisitorConfigurer = &XTCPVisitorConfig{}
|
var _ VisitorConfigurer = &XTCPVisitorConfig{}
|
||||||
|
|
||||||
type XTCPVisitorConfig struct {
|
type XTCPVisitorConfig struct {
|
||||||
@@ -154,18 +168,15 @@ type XTCPVisitorConfig struct {
|
|||||||
NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"`
|
NatTraversal *NatTraversalConfig `json:"natTraversal,omitempty"`
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *XTCPVisitorConfig) Complete() {
|
func (c *XTCPVisitorConfig) Complete(g *ClientCommonConfig) {
|
||||||
c.VisitorBaseConfig.Complete()
|
c.VisitorBaseConfig.Complete(g)
|
||||||
|
|
||||||
c.Protocol = util.EmptyOr(c.Protocol, "quic")
|
c.Protocol = util.EmptyOr(c.Protocol, "quic")
|
||||||
c.MaxRetriesAnHour = util.EmptyOr(c.MaxRetriesAnHour, 8)
|
c.MaxRetriesAnHour = util.EmptyOr(c.MaxRetriesAnHour, 8)
|
||||||
c.MinRetryInterval = util.EmptyOr(c.MinRetryInterval, 90)
|
c.MinRetryInterval = util.EmptyOr(c.MinRetryInterval, 90)
|
||||||
c.FallbackTimeoutMs = util.EmptyOr(c.FallbackTimeoutMs, 1000)
|
c.FallbackTimeoutMs = util.EmptyOr(c.FallbackTimeoutMs, 1000)
|
||||||
}
|
|
||||||
|
|
||||||
func (c *XTCPVisitorConfig) Clone() VisitorConfigurer {
|
if c.FallbackTo != "" {
|
||||||
out := *c
|
c.FallbackTo = lo.Ternary(g.User == "", "", g.User+".") + c.FallbackTo
|
||||||
out.VisitorBaseConfig = c.VisitorBaseConfig.Clone()
|
}
|
||||||
out.NatTraversal = c.NatTraversal.Clone()
|
|
||||||
return &out
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,11 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
"reflect"
|
"reflect"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -25,12 +27,11 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
var visitorPluginOptionsTypeMap = map[string]reflect.Type{
|
var visitorPluginOptionsTypeMap = map[string]reflect.Type{
|
||||||
VisitorPluginVirtualNet: reflect.TypeFor[VirtualNetVisitorPluginOptions](),
|
VisitorPluginVirtualNet: reflect.TypeOf(VirtualNetVisitorPluginOptions{}),
|
||||||
}
|
}
|
||||||
|
|
||||||
type VisitorPluginOptions interface {
|
type VisitorPluginOptions interface {
|
||||||
Complete()
|
Complete()
|
||||||
Clone() VisitorPluginOptions
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type TypedVisitorPluginOptions struct {
|
type TypedVisitorPluginOptions struct {
|
||||||
@@ -38,25 +39,43 @@ type TypedVisitorPluginOptions struct {
|
|||||||
VisitorPluginOptions
|
VisitorPluginOptions
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c TypedVisitorPluginOptions) Clone() TypedVisitorPluginOptions {
|
|
||||||
out := c
|
|
||||||
if c.VisitorPluginOptions != nil {
|
|
||||||
out.VisitorPluginOptions = c.VisitorPluginOptions.Clone()
|
|
||||||
}
|
|
||||||
return out
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error {
|
func (c *TypedVisitorPluginOptions) UnmarshalJSON(b []byte) error {
|
||||||
decoded, err := DecodeVisitorPluginOptionsJSON(b, DecodeOptions{})
|
if len(b) == 4 && string(b) == "null" {
|
||||||
if err != nil {
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
typeStruct := struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
}{}
|
||||||
|
if err := json.Unmarshal(b, &typeStruct); err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
*c = decoded
|
|
||||||
|
c.Type = typeStruct.Type
|
||||||
|
if c.Type == "" {
|
||||||
|
return errors.New("visitor plugin type is empty")
|
||||||
|
}
|
||||||
|
|
||||||
|
v, ok := visitorPluginOptionsTypeMap[typeStruct.Type]
|
||||||
|
if !ok {
|
||||||
|
return fmt.Errorf("unknown visitor plugin type: %s", typeStruct.Type)
|
||||||
|
}
|
||||||
|
options := reflect.New(v).Interface().(VisitorPluginOptions)
|
||||||
|
|
||||||
|
decoder := json.NewDecoder(bytes.NewBuffer(b))
|
||||||
|
if DisallowUnknownFields {
|
||||||
|
decoder.DisallowUnknownFields()
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := decoder.Decode(options); err != nil {
|
||||||
|
return fmt.Errorf("unmarshal VisitorPluginOptions error: %v", err)
|
||||||
|
}
|
||||||
|
c.VisitorPluginOptions = options
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) {
|
func (c *TypedVisitorPluginOptions) MarshalJSON() ([]byte, error) {
|
||||||
return jsonx.Marshal(c.VisitorPluginOptions)
|
return json.Marshal(c.VisitorPluginOptions)
|
||||||
}
|
}
|
||||||
|
|
||||||
type VirtualNetVisitorPluginOptions struct {
|
type VirtualNetVisitorPluginOptions struct {
|
||||||
@@ -65,11 +84,3 @@ type VirtualNetVisitorPluginOptions struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (o *VirtualNetVisitorPluginOptions) Complete() {}
|
func (o *VirtualNetVisitorPluginOptions) Complete() {}
|
||||||
|
|
||||||
func (o *VirtualNetVisitorPluginOptions) Clone() VisitorPluginOptions {
|
|
||||||
if o == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *o
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -143,6 +143,7 @@ func (m *serverMetrics) OpenConnection(name string, _ string) {
|
|||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if ok {
|
if ok {
|
||||||
proxyStats.CurConns.Inc(1)
|
proxyStats.CurConns.Inc(1)
|
||||||
|
m.info.ProxyStatistics[name] = proxyStats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -154,6 +155,7 @@ func (m *serverMetrics) CloseConnection(name string, _ string) {
|
|||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if ok {
|
if ok {
|
||||||
proxyStats.CurConns.Dec(1)
|
proxyStats.CurConns.Dec(1)
|
||||||
|
m.info.ProxyStatistics[name] = proxyStats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,6 +168,7 @@ func (m *serverMetrics) AddTrafficIn(name string, _ string, trafficBytes int64)
|
|||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if ok {
|
if ok {
|
||||||
proxyStats.TrafficIn.Inc(trafficBytes)
|
proxyStats.TrafficIn.Inc(trafficBytes)
|
||||||
|
m.info.ProxyStatistics[name] = proxyStats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -178,6 +181,7 @@ func (m *serverMetrics) AddTrafficOut(name string, _ string, trafficBytes int64)
|
|||||||
proxyStats, ok := m.info.ProxyStatistics[name]
|
proxyStats, ok := m.info.ProxyStatistics[name]
|
||||||
if ok {
|
if ok {
|
||||||
proxyStats.TrafficOut.Inc(trafficBytes)
|
proxyStats.TrafficOut.Inc(trafficBytes)
|
||||||
|
m.info.ProxyStatistics[name] = proxyStats
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -199,25 +203,6 @@ func (m *serverMetrics) GetServer() *ServerStats {
|
|||||||
return s
|
return s
|
||||||
}
|
}
|
||||||
|
|
||||||
func toProxyStats(name string, proxyStats *ProxyStatistics) *ProxyStats {
|
|
||||||
ps := &ProxyStats{
|
|
||||||
Name: name,
|
|
||||||
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() {
|
|
||||||
ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
|
||||||
}
|
|
||||||
if !proxyStats.LastCloseTime.IsZero() {
|
|
||||||
ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
|
||||||
}
|
|
||||||
return ps
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
||||||
res := make([]*ProxyStats, 0)
|
res := make([]*ProxyStats, 0)
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
@@ -227,7 +212,23 @@ func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
|||||||
if proxyStats.ProxyType != proxyType {
|
if proxyStats.ProxyType != proxyType {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
res = append(res, toProxyStats(name, proxyStats))
|
|
||||||
|
ps := &ProxyStats{
|
||||||
|
Name: name,
|
||||||
|
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() {
|
||||||
|
ps.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
||||||
|
}
|
||||||
|
if !proxyStats.LastCloseTime.IsZero() {
|
||||||
|
ps.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
||||||
|
}
|
||||||
|
res = append(res, ps)
|
||||||
}
|
}
|
||||||
return res
|
return res
|
||||||
}
|
}
|
||||||
@@ -236,9 +237,31 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
|
|||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|
||||||
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
for name, proxyStats := range m.info.ProxyStatistics {
|
||||||
if ok && proxyStats.ProxyType == proxyType {
|
if proxyStats.ProxyType != proxyType {
|
||||||
res = toProxyStats(proxyName, proxyStats)
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
if name != proxyName {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
res = &ProxyStats{
|
||||||
|
Name: name,
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
break
|
||||||
}
|
}
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -249,7 +272,21 @@ func (m *serverMetrics) GetProxyByName(proxyName string) (res *ProxyStats) {
|
|||||||
|
|
||||||
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
||||||
if ok {
|
if ok {
|
||||||
res = toProxyStats(proxyName, proxyStats)
|
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
|
return
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -61,7 +61,7 @@ var msgTypeMap = map[byte]any{
|
|||||||
TypeNatHoleReport: NatHoleReport{},
|
TypeNatHoleReport: NatHoleReport{},
|
||||||
}
|
}
|
||||||
|
|
||||||
var TypeNameNatHoleResp = reflect.TypeFor[NatHoleResp]().Name()
|
var TypeNameNatHoleResp = reflect.TypeOf(&NatHoleResp{}).Elem().Name()
|
||||||
|
|
||||||
type ClientSpec struct {
|
type ClientSpec struct {
|
||||||
// Due to the support of VirtualClient, frps needs to know the client type in order to
|
// Due to the support of VirtualClient, frps needs to know the client type in order to
|
||||||
@@ -184,7 +184,7 @@ type Pong struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type UDPPacket struct {
|
type UDPPacket struct {
|
||||||
Content []byte `json:"c,omitempty"`
|
Content string `json:"c,omitempty"`
|
||||||
LocalAddr *net.UDPAddr `json:"l,omitempty"`
|
LocalAddr *net.UDPAddr `json:"l,omitempty"`
|
||||||
RemoteAddr *net.UDPAddr `json:"r,omitempty"`
|
RemoteAddr *net.UDPAddr `json:"r,omitempty"`
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,32 +0,0 @@
|
|||||||
package naming
|
|
||||||
|
|
||||||
import "strings"
|
|
||||||
|
|
||||||
// AddUserPrefix builds the wire-level proxy name for frps by prefixing user.
|
|
||||||
func AddUserPrefix(user, name string) string {
|
|
||||||
if user == "" {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
return user + "." + name
|
|
||||||
}
|
|
||||||
|
|
||||||
// StripUserPrefix converts a wire-level proxy name to an internal raw name.
|
|
||||||
// It strips only one exact "{user}." prefix.
|
|
||||||
func StripUserPrefix(user, name string) string {
|
|
||||||
if user == "" {
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
if trimmed, ok := strings.CutPrefix(name, user+"."); ok {
|
|
||||||
return trimmed
|
|
||||||
}
|
|
||||||
return name
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildTargetServerProxyName resolves visitor target proxy name for wire-level
|
|
||||||
// protocol messages. serverUser overrides local user when set.
|
|
||||||
func BuildTargetServerProxyName(localUser, serverUser, serverName string) string {
|
|
||||||
if serverUser != "" {
|
|
||||||
return AddUserPrefix(serverUser, serverName)
|
|
||||||
}
|
|
||||||
return AddUserPrefix(localUser, serverName)
|
|
||||||
}
|
|
||||||
@@ -1,27 +0,0 @@
|
|||||||
package naming
|
|
||||||
|
|
||||||
import (
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"github.com/stretchr/testify/require"
|
|
||||||
)
|
|
||||||
|
|
||||||
func TestAddUserPrefix(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
require.Equal("test", AddUserPrefix("", "test"))
|
|
||||||
require.Equal("alice.test", AddUserPrefix("alice", "test"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestStripUserPrefix(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
require.Equal("test", StripUserPrefix("", "test"))
|
|
||||||
require.Equal("test", StripUserPrefix("alice", "alice.test"))
|
|
||||||
require.Equal("alice.test", StripUserPrefix("alice", "alice.alice.test"))
|
|
||||||
require.Equal("bob.test", StripUserPrefix("alice", "bob.test"))
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestBuildTargetServerProxyName(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
require.Equal("alice.test", BuildTargetServerProxyName("alice", "", "test"))
|
|
||||||
require.Equal("bob.test", BuildTargetServerProxyName("alice", "bob", "test"))
|
|
||||||
}
|
|
||||||
@@ -151,7 +151,7 @@ func getBehaviorScoresByMode(mode int, defaultScore int) []*BehaviorScore {
|
|||||||
func getBehaviorScoresByMode2(mode int, senderScore, receiverScore int) []*BehaviorScore {
|
func getBehaviorScoresByMode2(mode int, senderScore, receiverScore int) []*BehaviorScore {
|
||||||
behaviors := getBehaviorByMode(mode)
|
behaviors := getBehaviorByMode(mode)
|
||||||
scores := make([]*BehaviorScore, 0, len(behaviors))
|
scores := make([]*BehaviorScore, 0, len(behaviors))
|
||||||
for i := range behaviors {
|
for i := 0; i < len(behaviors); i++ {
|
||||||
score := receiverScore
|
score := receiverScore
|
||||||
if behaviors[i].A.Role == DetectRoleSender {
|
if behaviors[i].A.Role == DetectRoleSender {
|
||||||
score = senderScore
|
score = senderScore
|
||||||
|
|||||||
@@ -70,8 +70,12 @@ func ClassifyNATFeature(addresses []string, localIPs []string) (*NatFeature, err
|
|||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
|
||||||
portMax = max(portMax, portNum)
|
if portNum > portMax {
|
||||||
portMin = min(portMin, portNum)
|
portMax = portNum
|
||||||
|
}
|
||||||
|
if portNum < portMin {
|
||||||
|
portMin = portNum
|
||||||
|
}
|
||||||
if baseIP != ip {
|
if baseIP != ip {
|
||||||
ipChanged = true
|
ipChanged = true
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -152,9 +152,7 @@ func (c *Controller) GenSid() string {
|
|||||||
|
|
||||||
func (c *Controller) HandleVisitor(m *msg.NatHoleVisitor, transporter transport.MessageTransporter, visitorUser string) {
|
func (c *Controller) HandleVisitor(m *msg.NatHoleVisitor, transporter transport.MessageTransporter, visitorUser string) {
|
||||||
if m.PreCheck {
|
if m.PreCheck {
|
||||||
c.mu.RLock()
|
|
||||||
cfg, ok := c.clientCfgs[m.ProxyName]
|
cfg, ok := c.clientCfgs[m.ProxyName]
|
||||||
c.mu.RUnlock()
|
|
||||||
if !ok {
|
if !ok {
|
||||||
_ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, fmt.Sprintf("xtcp server for [%s] doesn't exist", m.ProxyName)))
|
_ = transporter.Send(c.GenNatHoleResponse(m.TransactionID, nil, fmt.Sprintf("xtcp server for [%s] doesn't exist", m.ProxyName)))
|
||||||
return
|
return
|
||||||
@@ -377,7 +375,7 @@ func getRangePorts(addrs []string, difference, maxNumber int) []msg.PortsRange {
|
|||||||
if !isLast {
|
if !isLast {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
ports := make([]msg.PortsRange, 0, 1)
|
var ports []msg.PortsRange
|
||||||
_, portStr, err := net.SplitHostPort(addr)
|
_, portStr, err := net.SplitHostPort(addr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil
|
return nil
|
||||||
|
|||||||
@@ -298,13 +298,11 @@ func waitDetectMessage(
|
|||||||
n, raddr, err := conn.ReadFromUDP(buf)
|
n, raddr, err := conn.ReadFromUDP(buf)
|
||||||
_ = conn.SetReadDeadline(time.Time{})
|
_ = conn.SetReadDeadline(time.Time{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
pool.PutBuf(buf)
|
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
xl.Debugf("get udp message local %s, from %s", conn.LocalAddr(), raddr)
|
xl.Debugf("get udp message local %s, from %s", conn.LocalAddr(), raddr)
|
||||||
var m msg.NatHoleSid
|
var m msg.NatHoleSid
|
||||||
if err := DecodeMessageInto(buf[:n], key, &m); err != nil {
|
if err := DecodeMessageInto(buf[:n], key, &m); err != nil {
|
||||||
pool.PutBuf(buf)
|
|
||||||
xl.Warnf("decode sid message error: %v", err)
|
xl.Warnf("decode sid message error: %v", err)
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
@@ -410,7 +408,7 @@ func sendSidMessageToRandomPorts(
|
|||||||
xl := xlog.FromContextSafe(ctx)
|
xl := xlog.FromContextSafe(ctx)
|
||||||
used := sets.New[int]()
|
used := sets.New[int]()
|
||||||
getUnusedPort := func() int {
|
getUnusedPort := func() int {
|
||||||
for range 10 {
|
for i := 0; i < 10; i++ {
|
||||||
port := rand.IntN(65535-1024) + 1024
|
port := rand.IntN(65535-1024) + 1024
|
||||||
if !used.Has(port) {
|
if !used.Has(port) {
|
||||||
used.Insert(port)
|
used.Insert(port)
|
||||||
@@ -420,7 +418,7 @@ func sendSidMessageToRandomPorts(
|
|||||||
return 0
|
return 0
|
||||||
}
|
}
|
||||||
|
|
||||||
for range count {
|
for i := 0; i < count; i++ {
|
||||||
select {
|
select {
|
||||||
case <-ctx.Done():
|
case <-ctx.Done():
|
||||||
return
|
return
|
||||||
|
|||||||
@@ -21,7 +21,6 @@ import (
|
|||||||
stdlog "log"
|
stdlog "log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatedier/golib/pool"
|
"github.com/fatedier/golib/pool"
|
||||||
|
|
||||||
@@ -69,7 +68,7 @@ func NewHTTP2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin
|
|||||||
|
|
||||||
p.s = &http.Server{
|
p.s = &http.Server{
|
||||||
Handler: rp,
|
Handler: rp,
|
||||||
ReadHeaderTimeout: 60 * time.Second,
|
ReadHeaderTimeout: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
stdlog "log"
|
stdlog "log"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httputil"
|
"net/http/httputil"
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatedier/golib/pool"
|
"github.com/fatedier/golib/pool"
|
||||||
|
|
||||||
@@ -78,7 +77,7 @@ func NewHTTP2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugi
|
|||||||
|
|
||||||
p.s = &http.Server{
|
p.s = &http.Server{
|
||||||
Handler: rp,
|
Handler: rp,
|
||||||
ReadHeaderTimeout: 60 * time.Second,
|
ReadHeaderTimeout: 0,
|
||||||
}
|
}
|
||||||
|
|
||||||
go func() {
|
go func() {
|
||||||
|
|||||||
@@ -62,13 +62,11 @@ func (p *TLS2RawPlugin) Handle(ctx context.Context, connInfo *ConnectionInfo) {
|
|||||||
|
|
||||||
if err := tlsConn.Handshake(); err != nil {
|
if err := tlsConn.Handshake(); err != nil {
|
||||||
xl.Warnf("tls handshake error: %v", err)
|
xl.Warnf("tls handshake error: %v", err)
|
||||||
tlsConn.Close()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
rawConn, err := net.Dial("tcp", p.opts.LocalAddr)
|
rawConn, err := net.Dial("tcp", p.opts.LocalAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("dial to local addr error: %v", err)
|
xl.Warnf("dial to local addr error: %v", err)
|
||||||
tlsConn.Close()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -54,13 +54,10 @@ func (uds *UnixDomainSocketPlugin) Handle(ctx context.Context, connInfo *Connect
|
|||||||
localConn, err := net.DialUnix("unix", nil, uds.UnixAddr)
|
localConn, err := net.DialUnix("unix", nil, uds.UnixAddr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err)
|
xl.Warnf("dial to uds %s error: %v", uds.UnixAddr, err)
|
||||||
connInfo.Conn.Close()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
if connInfo.ProxyProtocolHeader != nil {
|
if connInfo.ProxyProtocolHeader != nil {
|
||||||
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
if _, err := connInfo.ProxyProtocolHeader.WriteTo(localConn); err != nil {
|
||||||
localConn.Close()
|
|
||||||
connInfo.Conn.Close()
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -24,7 +24,6 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"net/url"
|
"net/url"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
@@ -65,7 +64,12 @@ func (p *httpPlugin) Name() string {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (p *httpPlugin) IsSupport(op string) bool {
|
func (p *httpPlugin) IsSupport(op string) bool {
|
||||||
return slices.Contains(p.options.Ops, op)
|
for _, v := range p.options.Ops {
|
||||||
|
if v == op {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
}
|
}
|
||||||
|
|
||||||
func (p *httpPlugin) Handle(ctx context.Context, op string, content any) (*Response, any, error) {
|
func (p *httpPlugin) Handle(ctx context.Context, op string, content any) (*Response, any, error) {
|
||||||
|
|||||||
@@ -153,7 +153,10 @@ func (p *VirtualNetPlugin) run() {
|
|||||||
|
|
||||||
// Exponential backoff: 60s, 120s, 240s, 300s (capped)
|
// Exponential backoff: 60s, 120s, 240s, 300s (capped)
|
||||||
baseDelay := 60 * time.Second
|
baseDelay := 60 * time.Second
|
||||||
reconnectDelay = min(baseDelay*time.Duration(1<<uint(p.consecutiveErrors-1)), 300*time.Second)
|
reconnectDelay = baseDelay * time.Duration(1<<uint(p.consecutiveErrors-1))
|
||||||
|
if reconnectDelay > 300*time.Second {
|
||||||
|
reconnectDelay = 300 * time.Second
|
||||||
|
}
|
||||||
} else {
|
} else {
|
||||||
// Reset consecutive errors on successful connection
|
// Reset consecutive errors on successful connection
|
||||||
if p.consecutiveErrors > 0 {
|
if p.consecutiveErrors > 0 {
|
||||||
|
|||||||
@@ -16,7 +16,6 @@ package featuregate
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"maps"
|
|
||||||
"sort"
|
"sort"
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
"sync"
|
||||||
@@ -93,7 +92,10 @@ type featureGate struct {
|
|||||||
|
|
||||||
// NewFeatureGate creates a new feature gate with the default features
|
// NewFeatureGate creates a new feature gate with the default features
|
||||||
func NewFeatureGate() MutableFeatureGate {
|
func NewFeatureGate() MutableFeatureGate {
|
||||||
known := maps.Clone(defaultFeatures)
|
known := map[Feature]FeatureSpec{}
|
||||||
|
for k, v := range defaultFeatures {
|
||||||
|
known[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
f := &featureGate{}
|
f := &featureGate{}
|
||||||
f.known.Store(known)
|
f.known.Store(known)
|
||||||
@@ -107,8 +109,14 @@ func (f *featureGate) SetFromMap(m map[string]bool) error {
|
|||||||
defer f.lock.Unlock()
|
defer f.lock.Unlock()
|
||||||
|
|
||||||
// Copy existing state
|
// Copy existing state
|
||||||
known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec))
|
known := map[Feature]FeatureSpec{}
|
||||||
enabled := maps.Clone(f.enabled.Load().(map[Feature]bool))
|
for k, v := range f.known.Load().(map[Feature]FeatureSpec) {
|
||||||
|
known[k] = v
|
||||||
|
}
|
||||||
|
enabled := map[Feature]bool{}
|
||||||
|
for k, v := range f.enabled.Load().(map[Feature]bool) {
|
||||||
|
enabled[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
// Apply the new settings
|
// Apply the new settings
|
||||||
for k, v := range m {
|
for k, v := range m {
|
||||||
@@ -139,7 +147,10 @@ func (f *featureGate) Add(features map[Feature]FeatureSpec) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Copy existing state
|
// Copy existing state
|
||||||
known := maps.Clone(f.known.Load().(map[Feature]FeatureSpec))
|
known := map[Feature]FeatureSpec{}
|
||||||
|
for k, v := range f.known.Load().(map[Feature]FeatureSpec) {
|
||||||
|
known[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
// Add new features
|
// Add new features
|
||||||
for name, spec := range features {
|
for name, spec := range features {
|
||||||
@@ -160,9 +171,8 @@ func (f *featureGate) Add(features map[Feature]FeatureSpec) error {
|
|||||||
|
|
||||||
// String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,..."
|
// String returns a string containing all enabled feature gates, formatted as "key1=value1,key2=value2,..."
|
||||||
func (f *featureGate) String() string {
|
func (f *featureGate) String() string {
|
||||||
enabled := f.enabled.Load().(map[Feature]bool)
|
pairs := []string{}
|
||||||
pairs := make([]string, 0, len(enabled))
|
for k, v := range f.enabled.Load().(map[Feature]bool) {
|
||||||
for k, v := range enabled {
|
|
||||||
pairs = append(pairs, fmt.Sprintf("%s=%t", k, v))
|
pairs = append(pairs, fmt.Sprintf("%s=%t", k, v))
|
||||||
}
|
}
|
||||||
sort.Strings(pairs)
|
sort.Strings(pairs)
|
||||||
|
|||||||
@@ -15,6 +15,7 @@
|
|||||||
package udp
|
package udp
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"encoding/base64"
|
||||||
"net"
|
"net"
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
@@ -27,17 +28,16 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket {
|
func NewUDPPacket(buf []byte, laddr, raddr *net.UDPAddr) *msg.UDPPacket {
|
||||||
content := make([]byte, len(buf))
|
|
||||||
copy(content, buf)
|
|
||||||
return &msg.UDPPacket{
|
return &msg.UDPPacket{
|
||||||
Content: content,
|
Content: base64.StdEncoding.EncodeToString(buf),
|
||||||
LocalAddr: laddr,
|
LocalAddr: laddr,
|
||||||
RemoteAddr: raddr,
|
RemoteAddr: raddr,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetContent(m *msg.UDPPacket) (buf []byte, err error) {
|
func GetContent(m *msg.UDPPacket) (buf []byte, err error) {
|
||||||
return m.Content, nil
|
buf, err = base64.StdEncoding.DecodeString(m.Content)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh chan<- *msg.UDPPacket, bufSize int) {
|
func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh chan<- *msg.UDPPacket, bufSize int) {
|
||||||
@@ -60,7 +60,7 @@ func ForwardUserConn(udpConn *net.UDPConn, readCh <-chan *msg.UDPPacket, sendCh
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
// NewUDPPacket copies buf[:n], so the read buffer can be reused
|
// buf[:n] will be encoded to string, so the bytes can be reused
|
||||||
udpMsg := NewUDPPacket(buf[:n], nil, remoteAddr)
|
udpMsg := NewUDPPacket(buf[:n], nil, remoteAddr)
|
||||||
|
|
||||||
select {
|
select {
|
||||||
@@ -85,7 +85,6 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
|
|||||||
}()
|
}()
|
||||||
|
|
||||||
buf := pool.GetBuf(bufSize)
|
buf := pool.GetBuf(bufSize)
|
||||||
defer pool.PutBuf(buf)
|
|
||||||
for {
|
for {
|
||||||
_ = udpConn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
_ = udpConn.SetReadDeadline(time.Now().Add(30 * time.Second))
|
||||||
n, _, err := udpConn.ReadFromUDP(buf)
|
n, _, err := udpConn.ReadFromUDP(buf)
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/http/model"
|
"github.com/fatedier/frp/client/api"
|
||||||
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) (*model.ProxyStatusResp, error) {
|
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.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) (*model.ProxyS
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allStatus := make(model.StatusResp)
|
allStatus := make(api.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) (*model.ProxyS
|
|||||||
return nil, fmt.Errorf("no proxy status found")
|
return nil, fmt.Errorf("no proxy status found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetAllProxyStatus(ctx context.Context) (model.StatusResp, error) {
|
func (c *Client) GetAllProxyStatus(ctx context.Context) (api.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) (model.StatusResp, error
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allStatus := make(model.StatusResp)
|
allStatus := make(api.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))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -112,7 +112,7 @@ func (s *TunnelServer) Run() error {
|
|||||||
if sshConn.Permissions != nil {
|
if sshConn.Permissions != nil {
|
||||||
clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
|
clientCfg.User = util.EmptyOr(sshConn.Permissions.Extensions["user"], clientCfg.User)
|
||||||
}
|
}
|
||||||
pc.Complete()
|
pc.Complete(clientCfg.User)
|
||||||
|
|
||||||
vc, err := virtual.NewClient(virtual.ClientOptions{
|
vc, err := virtual.NewClient(virtual.ClientOptions{
|
||||||
Common: clientCfg,
|
Common: clientCfg,
|
||||||
|
|||||||
@@ -20,7 +20,6 @@ import (
|
|||||||
"crypto/tls"
|
"crypto/tls"
|
||||||
"crypto/x509"
|
"crypto/x509"
|
||||||
"encoding/pem"
|
"encoding/pem"
|
||||||
"fmt"
|
|
||||||
"math/big"
|
"math/big"
|
||||||
"os"
|
"os"
|
||||||
"time"
|
"time"
|
||||||
@@ -86,9 +85,7 @@ func newCertPool(caPath string) (*x509.CertPool, error) {
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
if !pool.AppendCertsFromPEM(caCrt) {
|
pool.AppendCertsFromPEM(caCrt)
|
||||||
return nil, fmt.Errorf("failed to parse CA certificate from file %q: no valid PEM certificates found", caPath)
|
|
||||||
}
|
|
||||||
|
|
||||||
return pool, nil
|
return pool, nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -89,11 +89,11 @@ func ParseBasicAuth(auth string) (username, password string, ok bool) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
cs := string(c)
|
cs := string(c)
|
||||||
before, after, found := strings.Cut(cs, ":")
|
s := strings.IndexByte(cs, ':')
|
||||||
if !found {
|
if s < 0 {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
return before, after, true
|
return cs[:s], cs[s+1:], true
|
||||||
}
|
}
|
||||||
|
|
||||||
func BasicAuth(username, passwd string) string {
|
func BasicAuth(username, passwd string) string {
|
||||||
|
|||||||
@@ -1,45 +0,0 @@
|
|||||||
// Copyright 2026 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 jsonx
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"encoding/json"
|
|
||||||
)
|
|
||||||
|
|
||||||
type DecodeOptions struct {
|
|
||||||
RejectUnknownMembers bool
|
|
||||||
}
|
|
||||||
|
|
||||||
func Marshal(v any) ([]byte, error) {
|
|
||||||
return json.Marshal(v)
|
|
||||||
}
|
|
||||||
|
|
||||||
func MarshalIndent(v any, prefix, indent string) ([]byte, error) {
|
|
||||||
return json.MarshalIndent(v, prefix, indent)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Unmarshal(data []byte, out any) error {
|
|
||||||
return json.Unmarshal(data, out)
|
|
||||||
}
|
|
||||||
|
|
||||||
func UnmarshalWithOptions(data []byte, out any, options DecodeOptions) error {
|
|
||||||
if !options.RejectUnknownMembers {
|
|
||||||
return json.Unmarshal(data, out)
|
|
||||||
}
|
|
||||||
decoder := json.NewDecoder(bytes.NewReader(data))
|
|
||||||
decoder.DisallowUnknownFields()
|
|
||||||
return decoder.Decode(out)
|
|
||||||
}
|
|
||||||
@@ -1,36 +0,0 @@
|
|||||||
// Copyright 2026 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 jsonx
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
// RawMessage stores a raw encoded JSON value.
|
|
||||||
// It is equivalent to encoding/json.RawMessage behavior.
|
|
||||||
type RawMessage []byte
|
|
||||||
|
|
||||||
func (m RawMessage) MarshalJSON() ([]byte, error) {
|
|
||||||
if m == nil {
|
|
||||||
return []byte("null"), nil
|
|
||||||
}
|
|
||||||
return m, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *RawMessage) UnmarshalJSON(data []byte) error {
|
|
||||||
if m == nil {
|
|
||||||
return fmt.Errorf("jsonx.RawMessage: UnmarshalJSON on nil pointer")
|
|
||||||
}
|
|
||||||
*m = append((*m)[:0], data...)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -86,7 +86,11 @@ func (c *FakeUDPConn) Read(b []byte) (n int, err error) {
|
|||||||
c.lastActive = time.Now()
|
c.lastActive = time.Now()
|
||||||
c.mu.Unlock()
|
c.mu.Unlock()
|
||||||
|
|
||||||
n = min(len(b), len(content))
|
if len(b) < len(content) {
|
||||||
|
n = len(b)
|
||||||
|
} else {
|
||||||
|
n = len(content)
|
||||||
|
}
|
||||||
copy(b, content)
|
copy(b, content)
|
||||||
return n, nil
|
return n, nil
|
||||||
}
|
}
|
||||||
@@ -164,15 +168,11 @@ func ListenUDP(bindAddr string, bindPort int) (l *UDPListener, err error) {
|
|||||||
return l, err
|
return l, err
|
||||||
}
|
}
|
||||||
readConn, err := net.ListenUDP("udp", udpAddr)
|
readConn, err := net.ListenUDP("udp", udpAddr)
|
||||||
if err != nil {
|
|
||||||
return l, err
|
|
||||||
}
|
|
||||||
|
|
||||||
l = &UDPListener{
|
l = &UDPListener{
|
||||||
addr: udpAddr,
|
addr: udpAddr,
|
||||||
acceptCh: make(chan net.Conn),
|
acceptCh: make(chan net.Conn),
|
||||||
writeCh: make(chan *UDPPacket, 1000),
|
writeCh: make(chan *UDPPacket, 1000),
|
||||||
readConn: readConn,
|
|
||||||
fakeConns: make(map[string]*FakeUDPConn),
|
fakeConns: make(map[string]*FakeUDPConn),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -26,7 +26,6 @@ type WebsocketListener struct {
|
|||||||
// ln: tcp listener for websocket connections
|
// ln: tcp listener for websocket connections
|
||||||
func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
|
func NewWebsocketListener(ln net.Listener) (wl *WebsocketListener) {
|
||||||
wl = &WebsocketListener{
|
wl = &WebsocketListener{
|
||||||
ln: ln,
|
|
||||||
acceptCh: make(chan net.Conn),
|
acceptCh: make(chan net.Conn),
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -68,8 +68,8 @@ func ParseRangeNumbers(rangeStr string) (numbers []int64, err error) {
|
|||||||
rangeStr = strings.TrimSpace(rangeStr)
|
rangeStr = strings.TrimSpace(rangeStr)
|
||||||
numbers = make([]int64, 0)
|
numbers = make([]int64, 0)
|
||||||
// e.g. 1000-2000,2001,2002,3000-4000
|
// e.g. 1000-2000,2001,2002,3000-4000
|
||||||
numRanges := strings.SplitSeq(rangeStr, ",")
|
numRanges := strings.Split(rangeStr, ",")
|
||||||
for numRangeStr := range numRanges {
|
for _, numRangeStr := range numRanges {
|
||||||
// 1000-2000 or 2001
|
// 1000-2000 or 2001
|
||||||
numArray := strings.Split(numRangeStr, "-")
|
numArray := strings.Split(numRangeStr, "-")
|
||||||
// length: only 1 or 2 is correct
|
// length: only 1 or 2 is correct
|
||||||
@@ -134,12 +134,3 @@ func RandomSleep(duration time.Duration, minRatio, maxRatio float64) time.Durati
|
|||||||
func ConstantTimeEqString(a, b string) bool {
|
func ConstantTimeEqString(a, b string) bool {
|
||||||
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
return subtle.ConstantTimeCompare([]byte(a), []byte(b)) == 1
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClonePtr returns a pointer to a copied value. If v is nil, it returns nil.
|
|
||||||
func ClonePtr[T any](v *T) *T {
|
|
||||||
if v == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
out := *v
|
|
||||||
return &out
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -41,16 +41,3 @@ func TestParseRangeNumbers(t *testing.T) {
|
|||||||
_, err = ParseRangeNumbers("3-a")
|
_, err = ParseRangeNumbers("3-a")
|
||||||
require.Error(err)
|
require.Error(err)
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestClonePtr(t *testing.T) {
|
|
||||||
require := require.New(t)
|
|
||||||
|
|
||||||
var nilInt *int
|
|
||||||
require.Nil(ClonePtr(nilInt))
|
|
||||||
|
|
||||||
v := 42
|
|
||||||
cloned := ClonePtr(&v)
|
|
||||||
require.NotNil(cloned)
|
|
||||||
require.Equal(v, *cloned)
|
|
||||||
require.NotSame(&v, cloned)
|
|
||||||
}
|
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user