Compare commits
10 Commits
cef71fb949
...
2cd3052da1
@@ -8,6 +8,7 @@ _obj
|
||||
_test
|
||||
|
||||
*.exe
|
||||
*.msi
|
||||
*.test
|
||||
*.prof
|
||||
|
||||
@@ -18,7 +19,13 @@ release/
|
||||
test/bin/
|
||||
vendor/
|
||||
lastversion/
|
||||
.cache/
|
||||
dist/
|
||||
|
||||
# Generated at runtime
|
||||
admin.db
|
||||
/frps
|
||||
/frpc
|
||||
.idea/
|
||||
.vscode/
|
||||
.autogen_ssh_key
|
||||
|
||||
@@ -1,39 +1,68 @@
|
||||
# AGENTS.md
|
||||
|
||||
## Development Commands
|
||||
## Project
|
||||
|
||||
### 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
|
||||
`github.com/fatedier/frp` — fast reverse proxy. Go 1.25. Two binaries:
|
||||
- **frps** (server): `cmd/frps/main.go` → `server/`
|
||||
- **frpc** (client): `cmd/frpc/main.go` → `client/`
|
||||
|
||||
### 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
|
||||
## Build & Dev Commands
|
||||
|
||||
### 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)
|
||||
| Command | What |
|
||||
|---------|------|
|
||||
| `make build` | Build both binaries to `bin/` |
|
||||
| `make frps` / `make frpc` | Single binary |
|
||||
| `make all` | `fmt → web → build` |
|
||||
| `make web` | Build web dashboards (`web/frps/`, `web/frpc/`) |
|
||||
| `make clean` | Remove `bin/`, `lastversion/`, `.cache/`, `.compat/` |
|
||||
|
||||
### Assets
|
||||
- `make web` - Build web dashboards (frps and frpc)
|
||||
Build tags: `frps`, `frpc`, and `noweb` (auto-added when web assets missing).
|
||||
CGO disabled, `-trimpath`, stripped symbols.
|
||||
|
||||
### Cleanup
|
||||
- `make clean` - Remove built binaries and temporary files
|
||||
## Test
|
||||
|
||||
## Testing
|
||||
| Command | What |
|
||||
|---------|------|
|
||||
| `make test` / `make gotest` | Unit tests with `--cover` per package |
|
||||
| `make e2e` | E2E via ginkgo (parallel, 16 nodes) |
|
||||
| `make e2e-trace` | Same with `LOG_LEVEL=trace` |
|
||||
| `make alltest` | `vet → gotest → e2e` (CI pipeline) |
|
||||
|
||||
- E2E tests using Ginkgo/Gomega framework
|
||||
- Mock servers in `/test/e2e/mock/`
|
||||
- Run: `make e2e` or `make alltest`
|
||||
- E2E uses Ginkgo/Gomega. Requires `ginkgo` binary (auto-installed by `hack/run-e2e.sh`).
|
||||
- Binary paths: `FRPC_PATH` / `FRPS_PATH` env vars (default `bin/frpc`, `bin/frps`).
|
||||
- E2E mock servers in `test/e2e/mock/`.
|
||||
- Compatibility e2e: `make e2e-compatibility`, `make e2e-compatibility-smoke`, `make e2e-compatibility-floor`. Controls: `FRP_COMPAT_BASELINE_COUNT` (default 8), `FRP_COMPAT_FLOOR_VERSION` (default 0.61.0).
|
||||
- CI: CircleCI runs `make alltest` after building web assets.
|
||||
|
||||
## Agent Runbooks
|
||||
## Code Quality
|
||||
|
||||
Operational procedures for agents are in `doc/agents/`:
|
||||
- `doc/agents/release.md` - Release process
|
||||
| Command | What |
|
||||
|---------|------|
|
||||
| `make fmt` | `go fmt ./...` |
|
||||
| `make fmt-more` | `gofumpt` |
|
||||
| `make gci` | Import organizer: standard → default → `github.com/fatedier/frp/` |
|
||||
| `make vet` | `go vet` with `noweb` build tag |
|
||||
| `golangci-lint run` | Full lint (v2 config, `.golangci.yml`) |
|
||||
|
||||
## Architecture
|
||||
|
||||
- **Config**: TOML, YAML, or JSON. INI format is **deprecated** (v0.52.0+). Default: `./frpc.ini`.
|
||||
- **DB-first config**: frps stores config in SQLite by default. Config file is optional (`-c` flag bypasses DB).
|
||||
- **First-run flow**: No config file + no DB config → auto-saves defaults (bind 0.0.0.0:7000, web 0.0.0.0:7500) and redirects admin UI to setup wizard.
|
||||
- **Flags**: `--config` / `-c`, `--strict_config` (default true), `--allow-unsafe`.
|
||||
- **Feature gates**: Enabled via `featureGates = { VirtualNet = true }`.
|
||||
- **Store**: `store.path` enables dynamic proxy CRUD at runtime (persisted to disk, takes precedence over static config on name conflict).
|
||||
- **Config aggregation**: Static file + includes + store. `start` acts as global allowlist (not recommended for new configs; prefer per-proxy `enabled`).
|
||||
- **frpc subcommands**: `frpc reload`, `frpc verify`, `frpc status`, `frpc tcp` (direct CLI proxy creation).
|
||||
- **Multi-client mode**: `--config_dir` runs one frpc per config file (testing only, not stable).
|
||||
- **Admin DB**: SQLite via `ent` ORM at `pkg/db/`. Schema: User, FrpcClient, Proxy, ServerConfig. Auto-creates `admin.db` and seeds default admin user (`admin`/`admin123`).
|
||||
- **Admin UI**: htmx-based dashboard at `/admin/` on frps web server port. Login required, session cookie auth. Pages: Dashboard, Clients, Proxies, Settings.
|
||||
- **Server Config via UI**: All frps settings editable from Settings page. First-run redirects to setup wizard. Save triggers restart prompt.
|
||||
|
||||
## Release
|
||||
|
||||
See `doc/agents/release.md` for the full process. Key steps:
|
||||
- Bump version in `pkg/util/version/version.go`
|
||||
- `make e2e` + compatibility tests
|
||||
- Merge `dev` → `master` with merge commit (not squash)
|
||||
- Tag `vX.Y.Z` and trigger `goreleaser` workflow manually via `gh`
|
||||
|
||||
@@ -1,23 +1,26 @@
|
||||
export PATH := $(PATH):`go env GOPATH`/bin
|
||||
export GO111MODULE=on
|
||||
LDFLAGS := -s -w
|
||||
NOWEB_TAG = $(shell [ ! -d web/frps/dist ] || [ ! -d web/frpc/dist ] && echo ',noweb')
|
||||
NOWEB_TAG = $(shell [ ! -d web/kanhole/dist ] || [ ! -d web/kanholec/dist ] && echo ',noweb')
|
||||
KANHOLE_VERSION ?= 0.1.0
|
||||
FRP_COMPAT_BASELINE_COUNT ?= 8
|
||||
FRP_COMPAT_FLOOR_VERSION ?= 0.61.0
|
||||
|
||||
.PHONY: web frps-web frpc-web frps frpc
|
||||
.PHONY: web kanhole-web kanholec-web kanhole kanholec kanholec-gui e2e-compatibility-smoke e2e-compatibility e2e-compatibility-floor
|
||||
|
||||
all: env fmt web build
|
||||
|
||||
build: frps frpc
|
||||
build: kanhole kanholec
|
||||
|
||||
env:
|
||||
@go version
|
||||
|
||||
web: frps-web frpc-web
|
||||
web: kanhole-web kanholec-web
|
||||
|
||||
frps-web:
|
||||
kanhole-web:
|
||||
$(MAKE) -C web/frps build
|
||||
|
||||
frpc-web:
|
||||
kanholec-web:
|
||||
$(MAKE) -C web/frpc build
|
||||
|
||||
fmt:
|
||||
@@ -27,16 +30,30 @@ fmt-more:
|
||||
gofumpt -l -w .
|
||||
|
||||
gci:
|
||||
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
|
||||
gci write -s standard -s default -s "prefix(kanhole/)" ./
|
||||
|
||||
vet:
|
||||
go vet -tags "$(NOWEB_TAG)" ./...
|
||||
|
||||
frps:
|
||||
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "frps$(NOWEB_TAG)" -o bin/frps ./cmd/frps
|
||||
kanhole:
|
||||
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "kanhole$(NOWEB_TAG)" -o bin/kanhole ./cmd/kanhole
|
||||
|
||||
frpc:
|
||||
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "frpc$(NOWEB_TAG)" -o bin/frpc ./cmd/frpc
|
||||
kanholec:
|
||||
env CGO_ENABLED=0 go build -trimpath -ldflags "$(LDFLAGS)" -tags "kanholec$(NOWEB_TAG)" -o bin/kanholec ./cmd/kanholec
|
||||
|
||||
kanholec-windows:
|
||||
env CGO_ENABLED=0 GOOS=windows GOARCH=amd64 go build -trimpath -ldflags "$(LDFLAGS)" -tags "kanholec$(NOWEB_TAG)" -o bin/kanholec-windows-amd64.exe ./cmd/kanholec
|
||||
|
||||
kanholec-windows-arm64:
|
||||
env CGO_ENABLED=0 GOOS=windows GOARCH=arm64 go build -trimpath -ldflags "$(LDFLAGS)" -tags "kanholec$(NOWEB_TAG)" -o bin/kanholec-windows-arm64.exe ./cmd/kanholec
|
||||
|
||||
kanholec-windows-msi: kanholec-windows
|
||||
wixl -o bin/kanholec-$(KANHOLE_VERSION).msi packaging/windows/kanholec.wxs 2>/dev/null \
|
||||
|| { echo "wixl failed. Try: apt install msitools"; \
|
||||
echo "Or on Windows: candle kanholec.wxs -o kanholec.wixobj && light kanholec.wixobj -o kanholec.msi"; \
|
||||
exit 1; }
|
||||
|
||||
.PHONY: kanholec-windows kanholec-windows-arm64 kanholec-windows-msi
|
||||
|
||||
test: gotest
|
||||
|
||||
@@ -53,23 +70,20 @@ e2e:
|
||||
e2e-trace:
|
||||
DEBUG=true LOG_LEVEL=trace ./hack/run-e2e.sh
|
||||
|
||||
e2e-compatibility-last-frpc:
|
||||
if [ ! -d "./lastversion" ]; then \
|
||||
TARGET_DIRNAME=lastversion ./hack/download.sh; \
|
||||
fi
|
||||
FRPC_PATH="`pwd`/lastversion/frpc" ./hack/run-e2e.sh
|
||||
rm -r ./lastversion
|
||||
e2e-compatibility-smoke: build
|
||||
FRP_COMPAT_BASELINE_COUNT=1 ./hack/run-e2e-compatibility.sh
|
||||
|
||||
e2e-compatibility-last-frps:
|
||||
if [ ! -d "./lastversion" ]; then \
|
||||
TARGET_DIRNAME=lastversion ./hack/download.sh; \
|
||||
fi
|
||||
FRPS_PATH="`pwd`/lastversion/frps" ./hack/run-e2e.sh
|
||||
rm -r ./lastversion
|
||||
e2e-compatibility: build
|
||||
FRP_COMPAT_BASELINE_COUNT="$(FRP_COMPAT_BASELINE_COUNT)" ./hack/run-e2e-compatibility.sh
|
||||
|
||||
e2e-compatibility-floor: build
|
||||
FRP_COMPAT_BASELINE_VERSIONS="$(FRP_COMPAT_FLOOR_VERSION)" ./hack/run-e2e-compatibility.sh
|
||||
|
||||
alltest: vet gotest e2e
|
||||
|
||||
clean:
|
||||
rm -f ./bin/frpc
|
||||
rm -f ./bin/frps
|
||||
rm -f ./bin/kanhole
|
||||
rm -f ./bin/kanholec
|
||||
rm -rf ./lastversion
|
||||
rm -rf ./.cache
|
||||
rm -rf ./.compat
|
||||
|
||||
@@ -13,6 +13,14 @@ frp is an open source project with its ongoing development made possible entirel
|
||||
|
||||
<h3 align="center">Gold Sponsors</h3>
|
||||
<!--gold sponsors start-->
|
||||
<p align="center">
|
||||
<a href="https://jb.gg/frp" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||
<br>
|
||||
<b>The complete IDE crafted for professional Go developers</b>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||
@@ -32,24 +40,6 @@ If you're looking for a meeting recording API, consider checking out [Recall.ai]
|
||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||
<br>
|
||||
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||
<br>
|
||||
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://jb.gg/frp" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||
<br>
|
||||
<b>The complete IDE crafted for professional Go developers</b>
|
||||
</a>
|
||||
</p>
|
||||
<!--gold sponsors end-->
|
||||
|
||||
## What is frp?
|
||||
|
||||
+8
-18
@@ -15,6 +15,14 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
||||
|
||||
<h3 align="center">Gold Sponsors</h3>
|
||||
<!--gold sponsors start-->
|
||||
<p align="center">
|
||||
<a href="https://jb.gg/frp" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||
<br>
|
||||
<b>The complete IDE crafted for professional Go developers</b>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||
@@ -34,24 +42,6 @@ If you're looking for a meeting recording API, consider checking out [Recall.ai]
|
||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||
|
||||
</div>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||
<br>
|
||||
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||
<br>
|
||||
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<p align="center">
|
||||
<a href="https://jb.gg/frp" target="_blank">
|
||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||
<br>
|
||||
<b>The complete IDE crafted for professional Go developers</b>
|
||||
</a>
|
||||
</p>
|
||||
<!--gold sponsors end-->
|
||||
|
||||
## 为什么使用 frp ?
|
||||
|
||||
+8
-16
@@ -1,18 +1,10 @@
|
||||
## Compatibility Policy
|
||||
|
||||
Starting with v0.69.0, each minor release is supported until there are nine newer minor releases. For example, v0.69.0 will be supported until v0.78.0 is released. Within this window, frpc v0.69.0 is guaranteed to work with any frps from v0.61.0 to v0.77.0, and vice versa. Patch releases within the same minor are always compatible. Versions outside the support window may continue to work on a best-effort basis, but compatibility is no longer guaranteed.
|
||||
|
||||
For mixed-version deployments, upgrade frps first, then upgrade frpc. This keeps the server side ready for newer client-side protocol behavior before clients start using it.
|
||||
|
||||
## Notes
|
||||
|
||||
This release introduces wire protocol v2 as a transition path for future frpc/frps protocol changes. The existing wire protocol is difficult to extend without compatibility risk, and upcoming changes, including replacing deprecated stream encryption methods, require a versioned protocol.
|
||||
|
||||
**The default value of `transport.wireProtocol` remains `v1` in this release.** Users can keep the default for now. To test v2 early, upgrade both frpc and frps to versions that support it, then set `transport.wireProtocol = "v2"` in frpc. A v2-enabled frpc cannot connect to an older frps.
|
||||
|
||||
v1 will be deprecated when v2 becomes the default in a future release. It will continue to be supported until v0.78.0 is released, and may be removed in v0.78.0 or later.
|
||||
|
||||
## Features
|
||||
|
||||
* Added `transport.wireProtocol` for frpc to select the internal message protocol used between frpc and frps. Supported values are `v1` and `v2`.
|
||||
* Added client protocol visibility in the frps dashboard and `/api/clients` API. Online clients now report their negotiated protocol as `v1` or `v2`.
|
||||
* When `transport.wireProtocol = "v2"` is enabled, ordinary UDP proxy work connection payloads now use wire protocol v2 message framing. This keeps UDP message payloads aligned with the negotiated frpc/frps wire protocol.
|
||||
|
||||
## Compatibility Notes
|
||||
|
||||
* The default/empty `transport.wireProtocol` and `transport.wireProtocol = "v1"` continue to use the legacy message codec for ordinary UDP proxy payloads.
|
||||
* Raw stream proxy paths such as TCP, HTTP, and STCP remain unframed and are not affected by the UDP payload framing change.
|
||||
* SUDP and XTCP keep their existing legacy behavior in this release and will be considered separately in a future phase.
|
||||
* `transport.wireProtocol = "v2"` requires both frpc and frps to use versions that support the same wire v2 semantics. Mixing a newer peer that sends v2-framed ordinary UDP payloads with an older v2-capable peer that still expects the legacy UDP payload codec can break ordinary UDP proxy traffic.
|
||||
|
||||
@@ -17,10 +17,10 @@ package client
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
adminapi "github.com/fatedier/frp/client/http"
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
adminapi "kanhole/client/http"
|
||||
"kanhole/client/proxy"
|
||||
httppkg "kanhole/pkg/util/http"
|
||||
netpkg "kanhole/pkg/util/net"
|
||||
)
|
||||
|
||||
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
||||
|
||||
@@ -6,13 +6,13 @@ import (
|
||||
"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"
|
||||
"kanhole/client/configmgmt"
|
||||
"kanhole/client/proxy"
|
||||
"kanhole/pkg/config"
|
||||
"kanhole/pkg/config/source"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/config/v1/validation"
|
||||
"kanhole/pkg/util/log"
|
||||
)
|
||||
|
||||
type serviceConfigManager struct {
|
||||
|
||||
@@ -5,9 +5,9 @@ import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
|
||||
"github.com/fatedier/frp/client/configmgmt"
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"kanhole/client/configmgmt"
|
||||
"kanhole/pkg/config/source"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func newTestRawTCPProxyConfig(name string) *v1.TCPProxyConfig {
|
||||
|
||||
@@ -4,8 +4,8 @@ import (
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"kanhole/client/proxy"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
+6
-6
@@ -28,12 +28,12 @@ import (
|
||||
quic "github.com/quic-go/quic-go"
|
||||
"github.com/samber/lo"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/wire"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/proto/wire"
|
||||
"kanhole/pkg/transport"
|
||||
netpkg "kanhole/pkg/util/net"
|
||||
"kanhole/pkg/util/xlog"
|
||||
)
|
||||
|
||||
// Connector is an interface for establishing connections to the server.
|
||||
|
||||
+10
-10
@@ -20,16 +20,16 @@ import (
|
||||
"sync/atomic"
|
||||
"time"
|
||||
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"github.com/fatedier/frp/client/visitor"
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/naming"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/util/wait"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
"kanhole/client/proxy"
|
||||
"kanhole/client/visitor"
|
||||
"kanhole/pkg/auth"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/naming"
|
||||
"kanhole/pkg/transport"
|
||||
"kanhole/pkg/util/wait"
|
||||
"kanhole/pkg/util/xlog"
|
||||
"kanhole/pkg/vnet"
|
||||
)
|
||||
|
||||
type SessionContext struct {
|
||||
|
||||
+62
-14
@@ -26,13 +26,13 @@ import (
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/wire"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/version"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
"kanhole/pkg/auth"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/proto/wire"
|
||||
netpkg "kanhole/pkg/util/net"
|
||||
"kanhole/pkg/util/version"
|
||||
"kanhole/pkg/vnet"
|
||||
)
|
||||
|
||||
type controlSessionDialer struct {
|
||||
@@ -74,17 +74,18 @@ func (d *controlSessionDialer) Dial(previousRunID string) (*SessionContext, erro
|
||||
return nil, err
|
||||
}
|
||||
|
||||
loginRespMsg, err := d.exchangeLogin(conn, loginMsg)
|
||||
loginResult, err := d.exchangeLogin(conn, loginMsg)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
loginRespMsg := loginResult.resp
|
||||
if loginRespMsg.Error != "" {
|
||||
return nil, errors.New(loginRespMsg.Error)
|
||||
}
|
||||
|
||||
var controlRW io.ReadWriter = conn
|
||||
if d.clientSpec == nil || d.clientSpec.Type != "ssh-tunnel" {
|
||||
controlRW, err = netpkg.NewCryptoReadWriter(conn, d.auth.EncryptionKey())
|
||||
controlRW, err = d.newControlReadWriter(conn, loginResult.crypto)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("create control crypto read writer: %w", err)
|
||||
}
|
||||
@@ -125,9 +126,16 @@ func (d *controlSessionDialer) buildLoginMsg(previousRunID string) (*msg.Login,
|
||||
return loginMsg, nil
|
||||
}
|
||||
|
||||
func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) (*msg.LoginResp, error) {
|
||||
type loginExchangeResult struct {
|
||||
resp *msg.LoginResp
|
||||
crypto *wire.CryptoContext
|
||||
}
|
||||
|
||||
func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login) (*loginExchangeResult, error) {
|
||||
rw := msg.NewV1ReadWriter(conn)
|
||||
var wireConn *wire.Conn
|
||||
var clientHello wire.ClientHello
|
||||
var clientHelloPayload []byte
|
||||
|
||||
if d.common.Transport.WireProtocol == wire.ProtocolV2 {
|
||||
if err := wire.WriteMagic(conn); err != nil {
|
||||
@@ -136,14 +144,23 @@ func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login)
|
||||
|
||||
wireConn = wire.NewConn(conn)
|
||||
rw = msg.NewV2ReadWriterWithConn(wireConn)
|
||||
hello := wire.DefaultClientHello(wire.BootstrapInfo{
|
||||
var err error
|
||||
clientHello, err = wire.NewClientHello(wire.BootstrapInfo{
|
||||
Transport: d.common.Transport.Protocol,
|
||||
TLS: lo.FromPtr(d.common.Transport.TLS.Enable) || d.common.Transport.Protocol == "wss" || d.common.Transport.Protocol == "quic",
|
||||
TCPMux: lo.FromPtr(d.common.Transport.TCPMux),
|
||||
})
|
||||
if err := wireConn.WriteJSONFrame(wire.FrameTypeClientHello, hello); err != nil {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientHelloFrame, err := wire.NewJSONFrame(wire.FrameTypeClientHello, clientHello)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if err := wireConn.WriteFrame(clientHelloFrame); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
clientHelloPayload = clientHelloFrame.Payload
|
||||
}
|
||||
if err := rw.WriteMsg(loginMsg); err != nil {
|
||||
return nil, err
|
||||
@@ -154,19 +171,50 @@ func (d *controlSessionDialer) exchangeLogin(conn net.Conn, loginMsg *msg.Login)
|
||||
_ = conn.SetReadDeadline(time.Time{})
|
||||
}()
|
||||
|
||||
var cryptoContext *wire.CryptoContext
|
||||
if wireConn != nil {
|
||||
serverHelloFrame, err := wireConn.ReadFrame()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if serverHelloFrame.Type != wire.FrameTypeServerHello {
|
||||
return nil, fmt.Errorf("unexpected frame type %d, want %d", serverHelloFrame.Type, wire.FrameTypeServerHello)
|
||||
}
|
||||
var serverHello wire.ServerHello
|
||||
if err := wireConn.ReadJSONFrame(wire.FrameTypeServerHello, &serverHello); err != nil {
|
||||
if err := wireConn.UnmarshalFrame(serverHelloFrame, &serverHello); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if serverHello.Error != "" {
|
||||
return nil, errors.New(serverHello.Error)
|
||||
}
|
||||
cryptoContext, err = wire.NewClientCryptoContext(clientHelloPayload, serverHelloFrame.Payload)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
var loginRespMsg msg.LoginResp
|
||||
if err := rw.ReadMsgInto(&loginRespMsg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &loginRespMsg, nil
|
||||
return &loginExchangeResult{
|
||||
resp: &loginRespMsg,
|
||||
crypto: cryptoContext,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (d *controlSessionDialer) newControlReadWriter(conn net.Conn, cryptoContext *wire.CryptoContext) (io.ReadWriter, error) {
|
||||
if d.common.Transport.WireProtocol == wire.ProtocolV2 {
|
||||
if cryptoContext == nil {
|
||||
return nil, errors.New("missing v2 crypto negotiation")
|
||||
}
|
||||
return netpkg.NewAEADCryptoReadWriter(
|
||||
conn,
|
||||
d.auth.EncryptionKey(),
|
||||
netpkg.AEADCryptoRoleClient,
|
||||
cryptoContext.Algorithm,
|
||||
cryptoContext.TranscriptHash,
|
||||
)
|
||||
}
|
||||
return netpkg.NewCryptoReadWriter(conn, d.auth.EncryptionKey())
|
||||
}
|
||||
|
||||
@@ -25,10 +25,11 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/wire"
|
||||
"kanhole/pkg/auth"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/proto/wire"
|
||||
netpkg "kanhole/pkg/util/net"
|
||||
)
|
||||
|
||||
type testConnector struct {
|
||||
@@ -140,8 +141,17 @@ func TestControlSessionDialerDialV2(t *testing.T) {
|
||||
}
|
||||
|
||||
wireConn := wire.NewConn(serverRaw)
|
||||
clientHelloFrame, err := wireConn.ReadFrame()
|
||||
if err != nil {
|
||||
serverErrCh <- err
|
||||
return
|
||||
}
|
||||
if clientHelloFrame.Type != wire.FrameTypeClientHello {
|
||||
serverErrCh <- fmt.Errorf("unexpected frame type %d, want %d", clientHelloFrame.Type, wire.FrameTypeClientHello)
|
||||
return
|
||||
}
|
||||
var hello wire.ClientHello
|
||||
if err := wireConn.ReadJSONFrame(wire.FrameTypeClientHello, &hello); err != nil {
|
||||
if err := wireConn.UnmarshalFrame(clientHelloFrame, &hello); err != nil {
|
||||
serverErrCh <- err
|
||||
return
|
||||
}
|
||||
@@ -160,11 +170,52 @@ func TestControlSessionDialerDialV2(t *testing.T) {
|
||||
serverErrCh <- fmt.Errorf("unexpected user: %s", loginMsg.User)
|
||||
return
|
||||
}
|
||||
if err := wireConn.WriteJSONFrame(wire.FrameTypeServerHello, wire.DefaultServerHello()); err != nil {
|
||||
serverHello, err := wire.NewServerHello(hello)
|
||||
if err != nil {
|
||||
serverErrCh <- err
|
||||
return
|
||||
}
|
||||
serverErrCh <- rw.WriteMsg(&msg.LoginResp{RunID: "run-v2"})
|
||||
serverHelloFrame, err := wire.NewJSONFrame(wire.FrameTypeServerHello, serverHello)
|
||||
if err != nil {
|
||||
serverErrCh <- err
|
||||
return
|
||||
}
|
||||
cryptoContext := wire.NewCryptoContext(
|
||||
serverHello.Selected.Crypto.Algorithm,
|
||||
clientHelloFrame.Payload,
|
||||
serverHelloFrame.Payload,
|
||||
)
|
||||
if err := wireConn.WriteFrame(serverHelloFrame); err != nil {
|
||||
serverErrCh <- err
|
||||
return
|
||||
}
|
||||
if err := rw.WriteMsg(&msg.LoginResp{RunID: "run-v2"}); err != nil {
|
||||
serverErrCh <- err
|
||||
return
|
||||
}
|
||||
|
||||
controlRW, err := netpkg.NewAEADCryptoReadWriter(
|
||||
serverRaw,
|
||||
[]byte("token"),
|
||||
netpkg.AEADCryptoRoleServer,
|
||||
cryptoContext.Algorithm,
|
||||
cryptoContext.TranscriptHash,
|
||||
)
|
||||
if err != nil {
|
||||
serverErrCh <- err
|
||||
return
|
||||
}
|
||||
controlMsgRW := msg.NewReadWriter(controlRW, wire.ProtocolV2)
|
||||
var ping msg.Ping
|
||||
if err := controlMsgRW.ReadMsgInto(&ping); err != nil {
|
||||
serverErrCh <- err
|
||||
return
|
||||
}
|
||||
if ping.PrivilegeKey != "v2-ping" || ping.Timestamp != 12345 {
|
||||
serverErrCh <- fmt.Errorf("unexpected ping: %+v", ping)
|
||||
return
|
||||
}
|
||||
serverErrCh <- nil
|
||||
}()
|
||||
|
||||
dialer := newTestControlSessionDialer(t, wire.ProtocolV2, connector, nil)
|
||||
@@ -177,6 +228,7 @@ func TestControlSessionDialerDialV2(t *testing.T) {
|
||||
require.NotNil(t, sessionCtx.Conn)
|
||||
require.NotNil(t, sessionCtx.Connector)
|
||||
require.False(t, connector.closed.Load())
|
||||
require.NoError(t, sessionCtx.Conn.WriteMsg(&msg.Ping{PrivilegeKey: "v2-ping", Timestamp: 12345}))
|
||||
require.NoError(t, <-serverErrCh)
|
||||
}
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ package event
|
||||
import (
|
||||
"errors"
|
||||
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"kanhole/pkg/msg"
|
||||
)
|
||||
|
||||
var ErrPayloadType = errors.New("error payload type")
|
||||
|
||||
@@ -0,0 +1,254 @@
|
||||
//go:build kanholec_gui
|
||||
|
||||
package gui
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"fyne.io/fyne/v2"
|
||||
"fyne.io/fyne/v2/app"
|
||||
"fyne.io/fyne/v2/container"
|
||||
"fyne.io/fyne/v2/dialog"
|
||||
"fyne.io/fyne/v2/layout"
|
||||
"fyne.io/fyne/v2/theme"
|
||||
"fyne.io/fyne/v2/widget"
|
||||
|
||||
"kanhole/pkg/config"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/util/log"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
fyneApp fyne.App
|
||||
window fyne.Window
|
||||
|
||||
serverURL *widget.Entry
|
||||
authToken *widget.Entry
|
||||
clientKey *widget.Entry
|
||||
statusLabel *widget.Label
|
||||
configView *widget.Entry
|
||||
proxyList *widget.List
|
||||
startBtn *widget.Button
|
||||
stopBtn *widget.Button
|
||||
connectBtn *widget.Button
|
||||
|
||||
mu sync.Mutex
|
||||
running bool
|
||||
configData string
|
||||
}
|
||||
|
||||
func New() *App {
|
||||
a := &App{}
|
||||
a.fyneApp = app.New()
|
||||
a.window = a.fyneApp.NewWindow("kanholec GUI")
|
||||
a.window.Resize(fyne.NewSize(850, 620))
|
||||
a.window.SetMaster()
|
||||
a.setupUI()
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *App) saveSettings() {
|
||||
p := a.fyneApp.Preferences()
|
||||
p.SetString("server_url", a.serverURL.Text)
|
||||
}
|
||||
|
||||
func (a *App) loadSettings() {
|
||||
p := a.fyneApp.Preferences()
|
||||
if url := p.StringWithFallback("server_url", "http://localhost:7500"); url != "" {
|
||||
a.serverURL.SetText(url)
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) setupUI() {
|
||||
a.serverURL = widget.NewEntry()
|
||||
a.serverURL.SetText("http://localhost:7500")
|
||||
a.serverURL.PlaceHolder = "http://frps-server:7500"
|
||||
|
||||
a.authToken = widget.NewEntry()
|
||||
a.authToken.PlaceHolder = "One-time token"
|
||||
|
||||
a.clientKey = widget.NewEntry()
|
||||
a.clientKey.PlaceHolder = "Client key (from server)"
|
||||
|
||||
a.statusLabel = widget.NewLabel("Not connected")
|
||||
a.statusLabel.Importance = widget.MediumImportance
|
||||
|
||||
a.configView = widget.NewMultiLineEntry()
|
||||
a.configView.SetMinRowsVisible(8)
|
||||
a.configView.Disable()
|
||||
|
||||
a.proxyList = widget.NewList(
|
||||
func() int { return 0 },
|
||||
func() fyne.CanvasObject { return widget.NewLabel("") },
|
||||
func(id widget.ListItemID, o fyne.CanvasObject) {},
|
||||
)
|
||||
|
||||
a.connectBtn = widget.NewButtonWithIcon("Connect", theme.NavigateNextIcon(), a.onConnect)
|
||||
a.startBtn = widget.NewButtonWithIcon("Start", theme.MediaPlayIcon(), a.onStart)
|
||||
a.startBtn.Disable()
|
||||
a.stopBtn = widget.NewButtonWithIcon("Stop", theme.MediaStopIcon(), a.onStop)
|
||||
a.stopBtn.Disable()
|
||||
|
||||
// Settings form
|
||||
settingsForm := widget.NewForm(
|
||||
widget.NewFormItem("Server URL", a.serverURL),
|
||||
)
|
||||
settingsForm.OnSubmit = func() { a.saveSettings() }
|
||||
settingsBtn := widget.NewButtonWithIcon("Save Settings", theme.DocumentSaveIcon(), a.saveSettings)
|
||||
|
||||
settingsTab := container.NewVBox(
|
||||
widget.NewLabelWithStyle("Connection Settings", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
settingsForm,
|
||||
settingsBtn,
|
||||
layout.NewSpacer(),
|
||||
)
|
||||
|
||||
// Auth form
|
||||
authForm := widget.NewForm(
|
||||
widget.NewFormItem("Server URL", a.serverURL),
|
||||
widget.NewFormItem("One-Time Token", a.authToken),
|
||||
widget.NewFormItem("Client Key", a.clientKey),
|
||||
)
|
||||
|
||||
statusLine := container.NewHBox(
|
||||
widget.NewLabelWithStyle("Status:", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
a.statusLabel,
|
||||
layout.NewSpacer(),
|
||||
a.connectBtn,
|
||||
)
|
||||
|
||||
controls := container.NewHBox(
|
||||
a.startBtn,
|
||||
a.stopBtn,
|
||||
)
|
||||
|
||||
connectTab := container.NewVBox(
|
||||
widget.NewLabelWithStyle("frp Client GUI", fyne.TextAlignCenter, fyne.TextStyle{Bold: true}),
|
||||
authForm,
|
||||
statusLine,
|
||||
controls,
|
||||
container.NewBorder(
|
||||
widget.NewLabelWithStyle("Proxies", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
nil, nil, nil,
|
||||
a.proxyList,
|
||||
),
|
||||
)
|
||||
|
||||
// Config tab
|
||||
configTab := container.NewBorder(
|
||||
widget.NewLabelWithStyle("Configuration (read-only)", fyne.TextAlignLeading, fyne.TextStyle{Bold: true}),
|
||||
nil, nil, nil,
|
||||
a.configView,
|
||||
)
|
||||
|
||||
tabs := container.NewAppTabs(
|
||||
container.NewTabItemWithIcon("Connect", theme.ComputerIcon(), connectTab),
|
||||
container.NewTabItemWithIcon("Config", theme.DocumentIcon(), configTab),
|
||||
container.NewTabItemWithIcon("Settings", theme.SettingsIcon(), settingsTab),
|
||||
)
|
||||
tabs.SelectIndex(0)
|
||||
|
||||
a.window.SetContent(tabs)
|
||||
a.loadSettings()
|
||||
}
|
||||
|
||||
func (a *App) onConnect() {
|
||||
url := strings.TrimRight(a.serverURL.Text, "/")
|
||||
token := a.authToken.Text
|
||||
key := a.clientKey.Text
|
||||
a.saveSettings()
|
||||
|
||||
if token == "" && key == "" {
|
||||
a.statusLabel.SetText("Error: provide token or client key")
|
||||
return
|
||||
}
|
||||
|
||||
if token != "" {
|
||||
apiURL := url + "/admin/api/client/auth"
|
||||
body := fmt.Sprintf(`{"token":"%s"}`, token)
|
||||
resp, err := http.Post(apiURL, "application/json", strings.NewReader(body))
|
||||
if err != nil {
|
||||
a.statusLabel.SetText(fmt.Sprintf("Error: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
a.statusLabel.SetText(fmt.Sprintf("Auth failed (%d): %s", resp.StatusCode, string(respBody)))
|
||||
return
|
||||
}
|
||||
|
||||
configData, _ := io.ReadAll(resp.Body)
|
||||
a.configData = string(configData)
|
||||
a.configView.SetText(a.configData)
|
||||
a.statusLabel.SetText("Connected (token auth)")
|
||||
a.startBtn.Enable()
|
||||
a.connectBtn.SetText("Reconnect")
|
||||
} else if key != "" {
|
||||
apiURL := fmt.Sprintf("%s/admin/api/frpc/proxy-config/%s", url, key)
|
||||
resp, err := http.Get(apiURL)
|
||||
if err != nil {
|
||||
a.statusLabel.SetText(fmt.Sprintf("Error: %v", err))
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
a.statusLabel.SetText(fmt.Sprintf("Config fetch failed (%d)", resp.StatusCode))
|
||||
return
|
||||
}
|
||||
|
||||
configData, _ := io.ReadAll(resp.Body)
|
||||
a.configData = string(configData)
|
||||
a.configView.SetText(a.configData)
|
||||
a.statusLabel.SetText("Connected (key auth)")
|
||||
a.startBtn.Enable()
|
||||
a.connectBtn.SetText("Reconnect")
|
||||
}
|
||||
}
|
||||
|
||||
func (a *App) onStart() {
|
||||
if a.configData == "" {
|
||||
a.statusLabel.SetText("Error: no config loaded")
|
||||
return
|
||||
}
|
||||
|
||||
var cfg v1.ClientConfig
|
||||
if err := config.LoadConfigure([]byte(a.configData), &cfg, false, "toml"); err != nil {
|
||||
dialog.ShowError(fmt.Errorf("invalid config: %w", err), a.window)
|
||||
return
|
||||
}
|
||||
|
||||
a.mu.Lock()
|
||||
a.running = true
|
||||
a.mu.Unlock()
|
||||
|
||||
a.startBtn.Disable()
|
||||
a.stopBtn.Enable()
|
||||
a.statusLabel.SetText("Running")
|
||||
}
|
||||
|
||||
func (a *App) onStop() {
|
||||
a.mu.Lock()
|
||||
a.running = false
|
||||
a.mu.Unlock()
|
||||
|
||||
a.startBtn.Enable()
|
||||
a.stopBtn.Disable()
|
||||
a.statusLabel.SetText("Stopped")
|
||||
}
|
||||
|
||||
func (a *App) Run() {
|
||||
a.window.ShowAndRun()
|
||||
}
|
||||
|
||||
func Run() {
|
||||
gui := New()
|
||||
log.Infof("starting frpc GUI")
|
||||
gui.Run()
|
||||
}
|
||||
@@ -24,8 +24,8 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/util/xlog"
|
||||
)
|
||||
|
||||
var ErrHealthCheckType = errors.New("error health check type")
|
||||
|
||||
@@ -24,11 +24,11 @@ import (
|
||||
"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"
|
||||
"kanhole/client/configmgmt"
|
||||
"kanhole/client/http/model"
|
||||
"kanhole/client/proxy"
|
||||
httppkg "kanhole/pkg/util/http"
|
||||
"kanhole/pkg/util/jsonx"
|
||||
)
|
||||
|
||||
// Controller handles HTTP API requests for frpc.
|
||||
|
||||
@@ -12,11 +12,11 @@ import (
|
||||
|
||||
"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"
|
||||
"kanhole/client/configmgmt"
|
||||
"kanhole/client/http/model"
|
||||
"kanhole/client/proxy"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
httppkg "kanhole/pkg/util/http"
|
||||
)
|
||||
|
||||
type fakeConfigManager struct {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
type ProxyDefinition struct {
|
||||
|
||||
@@ -4,7 +4,7 @@ import (
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
type VisitorDefinition struct {
|
||||
|
||||
@@ -17,7 +17,7 @@ package proxy
|
||||
import (
|
||||
"reflect"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
@@ -28,15 +28,15 @@ import (
|
||||
libnet "github.com/fatedier/golib/net"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
plugin "github.com/fatedier/frp/pkg/plugin/client"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/util/limit"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
"kanhole/pkg/config/types"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
plugin "kanhole/pkg/plugin/client"
|
||||
"kanhole/pkg/transport"
|
||||
"kanhole/pkg/util/limit"
|
||||
netpkg "kanhole/pkg/util/net"
|
||||
"kanhole/pkg/util/xlog"
|
||||
"kanhole/pkg/vnet"
|
||||
)
|
||||
|
||||
var proxyFactoryRegistry = map[reflect.Type]func(*BaseProxy, v1.ProxyConfigurer) Proxy{}
|
||||
|
||||
@@ -23,12 +23,12 @@ import (
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/client/event"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
"kanhole/client/event"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/transport"
|
||||
"kanhole/pkg/util/xlog"
|
||||
"kanhole/pkg/vnet"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
|
||||
@@ -25,14 +25,14 @@ import (
|
||||
|
||||
"github.com/fatedier/golib/errors"
|
||||
|
||||
"github.com/fatedier/frp/client/event"
|
||||
"github.com/fatedier/frp/client/health"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/naming"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
"kanhole/client/event"
|
||||
"kanhole/client/health"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/naming"
|
||||
"kanhole/pkg/transport"
|
||||
"kanhole/pkg/util/xlog"
|
||||
"kanhole/pkg/vnet"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -25,10 +25,10 @@ import (
|
||||
|
||||
"github.com/fatedier/golib/errors"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/udp"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/proto/udp"
|
||||
netpkg "kanhole/pkg/util/net"
|
||||
)
|
||||
|
||||
func init() {
|
||||
|
||||
+12
-10
@@ -24,10 +24,10 @@ import (
|
||||
|
||||
"github.com/fatedier/golib/errors"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/udp"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/proto/udp"
|
||||
netpkg "kanhole/pkg/util/net"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -99,15 +99,17 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||
|
||||
pxy.mu.Lock()
|
||||
pxy.workConn = netpkg.WrapReadWriteCloserToConn(remote, conn)
|
||||
// Plain UDP payload follows the configured wire protocol for message framing.
|
||||
payloadRW := msg.NewReadWriter(pxy.workConn, pxy.clientCfg.Transport.WireProtocol)
|
||||
pxy.readCh = make(chan *msg.UDPPacket, 1024)
|
||||
pxy.sendCh = make(chan msg.Message, 1024)
|
||||
pxy.closed = false
|
||||
pxy.mu.Unlock()
|
||||
|
||||
workConnReaderFn := func(conn net.Conn, readCh chan *msg.UDPPacket) {
|
||||
workConnReaderFn := func(rw msg.ReadWriter, readCh chan *msg.UDPPacket) {
|
||||
for {
|
||||
var udpMsg msg.UDPPacket
|
||||
if errRet := msg.ReadMsgInto(conn, &udpMsg); errRet != nil {
|
||||
if errRet := rw.ReadMsgInto(&udpMsg); errRet != nil {
|
||||
xl.Warnf("read from workConn for udp error: %v", errRet)
|
||||
return
|
||||
}
|
||||
@@ -120,7 +122,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||
}
|
||||
}
|
||||
}
|
||||
workConnSenderFn := func(conn net.Conn, sendCh chan msg.Message) {
|
||||
workConnSenderFn := func(rw msg.ReadWriter, sendCh chan msg.Message) {
|
||||
defer func() {
|
||||
xl.Infof("writer goroutine for udp work connection closed")
|
||||
}()
|
||||
@@ -132,7 +134,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||
case *msg.Ping:
|
||||
xl.Tracef("send ping message to udp workConn")
|
||||
}
|
||||
if errRet = msg.WriteMsg(conn, rawMsg); errRet != nil {
|
||||
if errRet = rw.WriteMsg(rawMsg); errRet != nil {
|
||||
xl.Errorf("udp work write error: %v", errRet)
|
||||
return
|
||||
}
|
||||
@@ -151,8 +153,8 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||
}
|
||||
}
|
||||
|
||||
go workConnSenderFn(pxy.workConn, pxy.sendCh)
|
||||
go workConnReaderFn(pxy.workConn, pxy.readCh)
|
||||
go workConnSenderFn(payloadRW, pxy.sendCh)
|
||||
go workConnReaderFn(payloadRW, pxy.readCh)
|
||||
go heartbeatFn(pxy.sendCh)
|
||||
|
||||
// Call Forwarder with proxy protocol version (empty string means no proxy protocol)
|
||||
|
||||
+16
-8
@@ -25,12 +25,12 @@ import (
|
||||
fmux "github.com/hashicorp/yamux"
|
||||
"github.com/quic-go/quic-go"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/naming"
|
||||
"github.com/fatedier/frp/pkg/nathole"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/naming"
|
||||
"kanhole/pkg/nathole"
|
||||
"kanhole/pkg/transport"
|
||||
netpkg "kanhole/pkg/util/net"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -57,8 +57,7 @@ func NewXTCPProxy(baseProxy *BaseProxy, cfg v1.ProxyConfigurer) Proxy {
|
||||
func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkConn) {
|
||||
xl := pxy.xl
|
||||
defer conn.Close()
|
||||
var natHoleSidMsg msg.NatHoleSid
|
||||
err := msg.ReadMsgInto(conn, &natHoleSidMsg)
|
||||
natHoleSidMsg, err := readNatHoleSid(conn, pxy.clientCfg.Transport.WireProtocol)
|
||||
if err != nil {
|
||||
xl.Errorf("xtcp read from workConn error: %v", err)
|
||||
return
|
||||
@@ -131,6 +130,15 @@ func (pxy *XTCPProxy) InWorkConn(conn net.Conn, startWorkConnMsg *msg.StartWorkC
|
||||
pxy.listenByQUIC(listenConn, raddr, startWorkConnMsg)
|
||||
}
|
||||
|
||||
func readNatHoleSid(conn net.Conn, wireProtocol string) (*msg.NatHoleSid, error) {
|
||||
workMsgConn := msg.NewConn(conn, msg.NewReadWriter(conn, wireProtocol))
|
||||
var natHoleSidMsg msg.NatHoleSid
|
||||
if err := workMsgConn.ReadMsgInto(&natHoleSidMsg); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return &natHoleSidMsg, nil
|
||||
}
|
||||
|
||||
func (pxy *XTCPProxy) listenByKCP(listenConn *net.UDPConn, raddr *net.UDPAddr, startWorkConnMsg *msg.StartWorkConn) {
|
||||
xl := pxy.xl
|
||||
listenConn.Close()
|
||||
|
||||
@@ -0,0 +1,66 @@
|
||||
// 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.
|
||||
|
||||
//go:build !frps
|
||||
|
||||
package proxy
|
||||
|
||||
import (
|
||||
"net"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/proto/wire"
|
||||
)
|
||||
|
||||
func TestReadNatHoleSidUsesSelectedWireProtocol(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
wireProtocol string
|
||||
}{
|
||||
{name: "v2", wireProtocol: wire.ProtocolV2},
|
||||
{name: "v1", wireProtocol: wire.ProtocolV1},
|
||||
{name: "default", wireProtocol: ""},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
client, server := net.Pipe()
|
||||
defer client.Close()
|
||||
defer server.Close()
|
||||
setPipeDeadline(t, client, server)
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
writer := msg.NewConn(server, msg.NewReadWriter(server, tc.wireProtocol))
|
||||
errCh <- writer.WriteMsg(&msg.NatHoleSid{Sid: "sid"})
|
||||
}()
|
||||
|
||||
out, err := readNatHoleSid(client, tc.wireProtocol)
|
||||
require.NoError(t, err)
|
||||
require.Equal(t, "sid", out.Sid)
|
||||
require.NoError(t, <-errCh)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func setPipeDeadline(t *testing.T, conns ...net.Conn) {
|
||||
t.Helper()
|
||||
|
||||
deadline := time.Now().Add(time.Second)
|
||||
for _, conn := range conns {
|
||||
require.NoError(t, conn.SetDeadline(deadline))
|
||||
}
|
||||
}
|
||||
+93
-13
@@ -18,6 +18,7 @@ import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
@@ -27,19 +28,19 @@ import (
|
||||
"github.com/fatedier/golib/crypto"
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/client/proxy"
|
||||
"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"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/wait"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
"kanhole/client/proxy"
|
||||
"kanhole/pkg/auth"
|
||||
"kanhole/pkg/config"
|
||||
"kanhole/pkg/config/source"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/policy/security"
|
||||
httppkg "kanhole/pkg/util/http"
|
||||
"kanhole/pkg/util/log"
|
||||
netpkg "kanhole/pkg/util/net"
|
||||
"kanhole/pkg/util/wait"
|
||||
"kanhole/pkg/util/xlog"
|
||||
"kanhole/pkg/vnet"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -146,6 +147,9 @@ type Service struct {
|
||||
// string if no configuration file was used.
|
||||
configFilePath string
|
||||
|
||||
// configURL is the URL to fetch configuration from on startup and periodically.
|
||||
configURL string
|
||||
|
||||
// service context
|
||||
ctx context.Context
|
||||
// call cancel to stop service
|
||||
@@ -199,6 +203,7 @@ func NewService(options ServiceOptions) (*Service, error) {
|
||||
common: options.Common,
|
||||
reloadCommon: options.Common,
|
||||
configFilePath: options.ConfigFilePath,
|
||||
configURL: options.Common.ConfigURL,
|
||||
unsafeFeatures: options.UnsafeFeatures,
|
||||
proxyCfgs: proxyCfgs,
|
||||
visitorCfgs: visitorCfgs,
|
||||
@@ -265,6 +270,10 @@ func (svr *Service) Run(ctx context.Context) error {
|
||||
|
||||
go svr.keepControllerWorking()
|
||||
|
||||
if svr.configURL != "" {
|
||||
go svr.pollConfigURL()
|
||||
}
|
||||
|
||||
<-svr.ctx.Done()
|
||||
svr.stop()
|
||||
return nil
|
||||
@@ -513,3 +522,74 @@ func (svr *Service) reloadConfigFromSourcesLocked() error {
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (svr *Service) pollConfigURL() {
|
||||
url := svr.configURL
|
||||
if url == "" {
|
||||
return
|
||||
}
|
||||
|
||||
log.Infof("starting config URL poller: %s", url)
|
||||
lastBody := ""
|
||||
|
||||
ticker := time.NewTicker(30 * time.Second)
|
||||
defer ticker.Stop()
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-svr.ctx.Done():
|
||||
return
|
||||
case <-ticker.C:
|
||||
}
|
||||
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
log.Warnf("failed to fetch config from %s: %v", url, err)
|
||||
continue
|
||||
}
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
resp.Body.Close()
|
||||
if err != nil {
|
||||
log.Warnf("failed to read config from %s: %v", url, err)
|
||||
continue
|
||||
}
|
||||
|
||||
bodyStr := string(body)
|
||||
if bodyStr == lastBody {
|
||||
continue
|
||||
}
|
||||
|
||||
log.Infof("config changed, reloading from %s", url)
|
||||
|
||||
allCfg := v1.ClientConfig{}
|
||||
if err := config.LoadConfigure(body, &allCfg, false, "toml"); err != nil {
|
||||
log.Warnf("failed to parse config from %s: %v", url, err)
|
||||
continue
|
||||
}
|
||||
|
||||
proxyCfgs := make([]v1.ProxyConfigurer, 0)
|
||||
for _, c := range allCfg.Proxies {
|
||||
proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
|
||||
}
|
||||
visitorCfgs := make([]v1.VisitorConfigurer, 0)
|
||||
for _, c := range allCfg.Visitors {
|
||||
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
|
||||
}
|
||||
|
||||
// Update common config for new proxies
|
||||
if allCfg.ClientCommonConfig.Log.To != "" {
|
||||
svr.cfgMu.Lock()
|
||||
svr.reloadCommon = &allCfg.ClientCommonConfig
|
||||
svr.cfgMu.Unlock()
|
||||
}
|
||||
|
||||
if err := svr.UpdateConfigSource(&allCfg.ClientCommonConfig, proxyCfgs, visitorCfgs); err != nil {
|
||||
log.Warnf("failed to apply config from %s: %v", url, err)
|
||||
continue
|
||||
}
|
||||
|
||||
lastBody = bodyStr
|
||||
log.Infof("config reloaded successfully from %s", url)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -11,8 +11,8 @@ import (
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/source"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"kanhole/pkg/config/source"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
type failingConnector struct {
|
||||
|
||||
@@ -20,8 +20,8 @@ import (
|
||||
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/util/xlog"
|
||||
)
|
||||
|
||||
type STCPVisitor struct {
|
||||
|
||||
@@ -23,11 +23,11 @@ import (
|
||||
|
||||
"github.com/fatedier/golib/errors"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/proto/udp"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/proto/udp"
|
||||
netpkg "kanhole/pkg/util/net"
|
||||
"kanhole/pkg/util/xlog"
|
||||
)
|
||||
|
||||
type SUDPVisitor struct {
|
||||
|
||||
@@ -24,15 +24,15 @@ import (
|
||||
|
||||
libio "github.com/fatedier/golib/io"
|
||||
|
||||
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"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
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/vnet"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/naming"
|
||||
plugin "kanhole/pkg/plugin/visitor"
|
||||
"kanhole/pkg/transport"
|
||||
netpkg "kanhole/pkg/util/net"
|
||||
"kanhole/pkg/util/util"
|
||||
"kanhole/pkg/util/xlog"
|
||||
"kanhole/pkg/vnet"
|
||||
)
|
||||
|
||||
// Helper wraps some functions for visitor to use.
|
||||
|
||||
@@ -24,11 +24,11 @@ import (
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
"github.com/fatedier/frp/pkg/vnet"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/transport"
|
||||
"kanhole/pkg/util/xlog"
|
||||
"kanhole/pkg/vnet"
|
||||
)
|
||||
|
||||
type Manager struct {
|
||||
|
||||
@@ -29,14 +29,14 @@ import (
|
||||
quic "github.com/quic-go/quic-go"
|
||||
"golang.org/x/time/rate"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/naming"
|
||||
"github.com/fatedier/frp/pkg/nathole"
|
||||
"github.com/fatedier/frp/pkg/transport"
|
||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"github.com/fatedier/frp/pkg/util/xlog"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/naming"
|
||||
"kanhole/pkg/nathole"
|
||||
"kanhole/pkg/transport"
|
||||
netpkg "kanhole/pkg/util/net"
|
||||
"kanhole/pkg/util/util"
|
||||
"kanhole/pkg/util/xlog"
|
||||
)
|
||||
|
||||
var ErrNoTunnelSession = errors.New("no tunnel session")
|
||||
|
||||
@@ -15,9 +15,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
_ "github.com/fatedier/frp/pkg/metrics"
|
||||
"github.com/fatedier/frp/pkg/util/system"
|
||||
_ "github.com/fatedier/frp/web/frps"
|
||||
_ "kanhole/pkg/metrics"
|
||||
"kanhole/pkg/util/system"
|
||||
_ "kanhole/web/frps"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -1,17 +1,3 @@
|
||||
// Copyright 2018 fatedier, fatedier@gmail.com
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
package main
|
||||
|
||||
import (
|
||||
@@ -22,13 +8,14 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"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"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
"github.com/fatedier/frp/pkg/util/version"
|
||||
"github.com/fatedier/frp/server"
|
||||
"kanhole/pkg/config"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/config/v1/validation"
|
||||
"kanhole/pkg/db"
|
||||
"kanhole/pkg/policy/security"
|
||||
"kanhole/pkg/util/log"
|
||||
"kanhole/pkg/util/version"
|
||||
"kanhole/server"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -41,8 +28,8 @@ var (
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
|
||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of kanhole")
|
||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of kanhole")
|
||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
|
||||
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ServerUnsafeFeatures, ", ")))
|
||||
@@ -51,14 +38,20 @@ func init() {
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "frps",
|
||||
Short: "frps is the server of frp (https://github.com/fatedier/frp)",
|
||||
Use: "kanhole",
|
||||
Short: "kanhole is the server component of kanhole",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if showVersion {
|
||||
fmt.Println(version.Full())
|
||||
return nil
|
||||
}
|
||||
|
||||
if err := db.Init("sqlite", "admin.db"); err != nil {
|
||||
fmt.Printf("failed to initialize admin database: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
var (
|
||||
svrCfg *v1.ServerConfig
|
||||
isLegacyFormat bool
|
||||
@@ -71,15 +64,14 @@ var rootCmd = &cobra.Command{
|
||||
os.Exit(1)
|
||||
}
|
||||
if isLegacyFormat {
|
||||
fmt.Printf("WARNING: ini format is deprecated and the support will be removed in the future, " +
|
||||
"please use yaml/json/toml format instead!\n")
|
||||
fmt.Printf("WARNING: ini format is deprecated, please use yaml/json/toml instead!\n")
|
||||
}
|
||||
} else {
|
||||
if err := serverCfg.Complete(); err != nil {
|
||||
fmt.Printf("failed to complete server config: %v\n", err)
|
||||
svrCfg, err = loadConfigFromDB()
|
||||
if err != nil {
|
||||
fmt.Printf("failed to load server config: %v\n", err)
|
||||
os.Exit(1)
|
||||
}
|
||||
svrCfg = &serverCfg
|
||||
}
|
||||
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
@@ -108,20 +100,49 @@ func Execute() {
|
||||
}
|
||||
}
|
||||
|
||||
func loadConfigFromDB() (*v1.ServerConfig, error) {
|
||||
hasConfig, err := db.HasServerConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check server config in db: %w", err)
|
||||
}
|
||||
|
||||
if hasConfig {
|
||||
cfg, err := db.LoadServerConfig()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to load server config from db: %w", err)
|
||||
}
|
||||
if err := cfg.Complete(); err != nil {
|
||||
return nil, fmt.Errorf("failed to complete server config: %w", err)
|
||||
}
|
||||
log.Infof("kanhole uses database configuration")
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
cfg := db.DefaultServerConfig()
|
||||
if err := cfg.Complete(); err != nil {
|
||||
return nil, fmt.Errorf("failed to complete server config: %w", err)
|
||||
}
|
||||
if err := db.SaveServerConfig(cfg); err != nil {
|
||||
log.Warnf("failed to save default config to db: %v", err)
|
||||
}
|
||||
log.Infof("kanhole started with default configuration (first run)")
|
||||
return cfg, nil
|
||||
}
|
||||
|
||||
func runServer(cfg *v1.ServerConfig) (err error) {
|
||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||
|
||||
if cfgFile != "" {
|
||||
log.Infof("frps uses config file: %s", cfgFile)
|
||||
log.Infof("kanhole uses config file: %s", cfgFile)
|
||||
} else {
|
||||
log.Infof("frps uses command line arguments for config")
|
||||
log.Infof("kanhole uses database configuration")
|
||||
}
|
||||
|
||||
svr, err := server.NewService(cfg)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
log.Infof("frps started successfully")
|
||||
log.Infof("kanhole started successfully")
|
||||
svr.Run(context.Background())
|
||||
return
|
||||
}
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
"kanhole/pkg/config"
|
||||
"kanhole/pkg/config/v1/validation"
|
||||
"kanhole/pkg/policy/security"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -34,7 +34,7 @@ var verifyCmd = &cobra.Command{
|
||||
Short: "Verify that the configures is valid",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cfgFile == "" {
|
||||
fmt.Println("frps: the configuration file is not specified")
|
||||
fmt.Println("kanhole: the configuration file is not specified")
|
||||
return nil
|
||||
}
|
||||
svrCfg, _, err := config.LoadServerConfig(cfgFile, strictConfigMode)
|
||||
@@ -53,7 +53,7 @@ var verifyCmd = &cobra.Command{
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
fmt.Printf("frps: the configuration file %s syntax is ok\n", cfgFile)
|
||||
fmt.Printf("kanhole: the configuration file %s syntax is ok\n", cfgFile)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
@@ -15,9 +15,9 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"github.com/fatedier/frp/cmd/frpc/sub"
|
||||
"github.com/fatedier/frp/pkg/util/system"
|
||||
_ "github.com/fatedier/frp/web/frpc"
|
||||
"kanhole/cmd/kanholec/sub"
|
||||
"kanhole/pkg/util/system"
|
||||
_ "kanhole/web/frpc"
|
||||
)
|
||||
|
||||
func main() {
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The frp Authors
|
||||
// Copyright 2023 The kanhole Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -24,9 +24,9 @@ import (
|
||||
"github.com/rodaine/table"
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
clientsdk "github.com/fatedier/frp/pkg/sdk/client"
|
||||
"kanhole/pkg/config"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
clientsdk "kanhole/pkg/sdk/client"
|
||||
)
|
||||
|
||||
var adminAPITimeout = 30 * time.Second
|
||||
@@ -37,9 +37,9 @@ func init() {
|
||||
description string
|
||||
handler func(*v1.ClientCommonConfig) error
|
||||
}{
|
||||
{"reload", "Hot-Reload frpc configuration", ReloadHandler},
|
||||
{"reload", "Hot-Reload kanholec configuration", ReloadHandler},
|
||||
{"status", "Overview of all proxies status", StatusHandler},
|
||||
{"stop", "Stop the running frpc", StopHandler},
|
||||
{"stop", "Stop the running kanholec", StopHandler},
|
||||
}
|
||||
|
||||
for _, cmdConfig := range commands {
|
||||
@@ -0,0 +1,183 @@
|
||||
package sub
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"kanhole/pkg/util/log"
|
||||
)
|
||||
|
||||
var (
|
||||
authServer string
|
||||
authOutput string
|
||||
)
|
||||
|
||||
func init() {
|
||||
authCmd := &cobra.Command{
|
||||
Use: "auth",
|
||||
Short: "Authenticate kanholec with a kanhole server",
|
||||
Long: `Authenticate this kanholec instance with a kanhole server.
|
||||
|
||||
One-time token:
|
||||
kanholec auth token <token> --server http://server:7500
|
||||
|
||||
Interactive login:
|
||||
kanholec auth login --server http://server:7500 --client-name myclient
|
||||
`,
|
||||
}
|
||||
|
||||
tokenCmd := &cobra.Command{
|
||||
Use: "token <token>",
|
||||
Short: "Authenticate using a one-time token",
|
||||
Args: cobra.ExactArgs(1),
|
||||
RunE: runAuthToken,
|
||||
}
|
||||
tokenCmd.Flags().StringVarP(&authServer, "server", "s", "http://localhost:7500", "kanhole server admin URL")
|
||||
tokenCmd.Flags().StringVarP(&authOutput, "output", "o", "", "output config file path (default: ./kanholec-<client-name>.toml)")
|
||||
|
||||
loginCmd := &cobra.Command{
|
||||
Use: "login",
|
||||
Short: "Authenticate using admin credentials",
|
||||
RunE: runAuthLogin,
|
||||
}
|
||||
loginCmd.Flags().StringVarP(&authServer, "server", "s", "http://localhost:7500", "kanhole server admin URL")
|
||||
loginCmd.Flags().StringVarP(&authOutput, "output", "o", "", "output config file path (default: ./kanholec-<client-name>.toml)")
|
||||
loginCmd.Flags().String("username", "", "admin username (prompts if empty)")
|
||||
loginCmd.Flags().String("password", "", "admin password (prompts if empty)")
|
||||
loginCmd.Flags().String("client-name", "", "client name (fetches list if empty)")
|
||||
|
||||
rootCmd.AddCommand(authCmd)
|
||||
authCmd.AddCommand(tokenCmd)
|
||||
authCmd.AddCommand(loginCmd)
|
||||
}
|
||||
|
||||
func runAuthToken(cmd *cobra.Command, args []string) error {
|
||||
token := args[0]
|
||||
|
||||
url := authServer + "/admin/api/client/auth"
|
||||
body := map[string]string{"token": token}
|
||||
data, _ := json.Marshal(body)
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to server: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("auth failed (HTTP %d): %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
configData, _ := io.ReadAll(resp.Body)
|
||||
return saveConfig(configData)
|
||||
}
|
||||
|
||||
func runAuthLogin(cmd *cobra.Command, args []string) error {
|
||||
username, _ := cmd.Flags().GetString("username")
|
||||
password, _ := cmd.Flags().GetString("password")
|
||||
clientName, _ := cmd.Flags().GetString("client-name")
|
||||
|
||||
if username == "" {
|
||||
fmt.Print("Admin username: ")
|
||||
fmt.Scanln(&username)
|
||||
}
|
||||
if password == "" {
|
||||
fmt.Print("Admin password: ")
|
||||
bytePassword, err := readPassword()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
password = string(bytePassword)
|
||||
fmt.Println()
|
||||
}
|
||||
|
||||
url := authServer + "/admin/api/client/auth"
|
||||
body := map[string]string{
|
||||
"username": username,
|
||||
"password": password,
|
||||
"client_name": clientName,
|
||||
}
|
||||
data, _ := json.Marshal(body)
|
||||
|
||||
resp, err := http.Post(url, "application/json", bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to server: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("auth failed (HTTP %d): %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
|
||||
// Check if server returned a client list
|
||||
contentType := resp.Header.Get("Content-Type")
|
||||
if contentType == "application/json" || len(contentType) == 0 {
|
||||
var result struct {
|
||||
Clients []map[string]any `json:"clients"`
|
||||
RequiresClientName bool `json:"requires_client_name"`
|
||||
}
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
if err := json.Unmarshal(respBody, &result); err == nil && result.RequiresClientName {
|
||||
fmt.Println("Available clients:")
|
||||
for i, c := range result.Clients {
|
||||
fmt.Printf(" %d. %s\n", i+1, c["name"])
|
||||
}
|
||||
fmt.Print("Enter client name: ")
|
||||
var name string
|
||||
fmt.Scanln(&name)
|
||||
|
||||
body["client_name"] = name
|
||||
data, _ = json.Marshal(body)
|
||||
resp, err = http.Post(url, "application/json", bytes.NewReader(data))
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to connect to server: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
if resp.StatusCode != http.StatusOK {
|
||||
respBody, _ := io.ReadAll(resp.Body)
|
||||
return fmt.Errorf("auth failed (HTTP %d): %s", resp.StatusCode, string(respBody))
|
||||
}
|
||||
configData, _ := io.ReadAll(resp.Body)
|
||||
return saveConfig(configData)
|
||||
}
|
||||
configData := respBody
|
||||
return saveConfig(configData)
|
||||
}
|
||||
|
||||
configData, _ := io.ReadAll(resp.Body)
|
||||
return saveConfig(configData)
|
||||
}
|
||||
|
||||
func saveConfig(data []byte) error {
|
||||
outputPath := authOutput
|
||||
if outputPath == "" {
|
||||
// Try to extract client name from config
|
||||
outputPath = "kanholec.toml"
|
||||
}
|
||||
|
||||
if err := os.MkdirAll(filepath.Dir(outputPath), 0755); err != nil {
|
||||
return fmt.Errorf("failed to create output directory: %w", err)
|
||||
}
|
||||
|
||||
if err := os.WriteFile(outputPath, data, 0644); err != nil {
|
||||
return fmt.Errorf("failed to write config: %w", err)
|
||||
}
|
||||
|
||||
log.Infof("config saved to %s", outputPath)
|
||||
fmt.Printf("Config saved to %s\n", outputPath)
|
||||
fmt.Printf("Run: kanholec -c %s\n", outputPath)
|
||||
return nil
|
||||
}
|
||||
|
||||
func readPassword() ([]byte, error) {
|
||||
return io.ReadAll(os.Stdin)
|
||||
}
|
||||
@@ -0,0 +1,21 @@
|
||||
//go:build kanholec_gui
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"kanhole/client/gui"
|
||||
)
|
||||
|
||||
func init() {
|
||||
guiCmd := &cobra.Command{
|
||||
Use: "gui",
|
||||
Short: "Start the kanholec graphical user interface",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
gui.Run()
|
||||
return nil
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(guiCmd)
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
//go:build !kanholec_gui
|
||||
|
||||
package sub
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
)
|
||||
|
||||
func init() {
|
||||
guiCmd := &cobra.Command{
|
||||
Use: "gui",
|
||||
Short: "Start the kanholec graphical user interface",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
fmt.Println("kanholec GUI is not available in this build.")
|
||||
fmt.Println("To build with GUI support, install OpenGL and X11 dev libraries, then:")
|
||||
fmt.Println(" CGO_ENABLED=1 go build -tags kanholec_gui -o kanholec ./cmd/kanholec")
|
||||
return nil
|
||||
},
|
||||
}
|
||||
rootCmd.AddCommand(guiCmd)
|
||||
}
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The frp Authors
|
||||
// Copyright 2023 The kanhole Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/nathole"
|
||||
"kanhole/pkg/config"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/nathole"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2023 The frp Authors
|
||||
// Copyright 2023 The kanhole Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -21,11 +21,11 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"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/policy/security"
|
||||
"kanhole/pkg/config"
|
||||
"kanhole/pkg/config/source"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/config/v1/validation"
|
||||
"kanhole/pkg/policy/security"
|
||||
)
|
||||
|
||||
var proxyTypes = []v1.ProxyType{
|
||||
@@ -73,7 +73,7 @@ func init() {
|
||||
func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: name,
|
||||
Short: fmt.Sprintf("Run frpc with a single %s proxy", name),
|
||||
Short: fmt.Sprintf("Run kanholec with a single %s proxy", name),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := clientCfg.Complete(); err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -106,7 +106,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
||||
func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.ClientCommonConfig) *cobra.Command {
|
||||
return &cobra.Command{
|
||||
Use: "visitor",
|
||||
Short: fmt.Sprintf("Run frpc with a single %s visitor", name),
|
||||
Short: fmt.Sprintf("Run kanholec with a single %s visitor", name),
|
||||
Run: func(cmd *cobra.Command, args []string) {
|
||||
if err := clientCfg.Complete(); err != nil {
|
||||
fmt.Println(err)
|
||||
@@ -17,7 +17,9 @@ package sub
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"path/filepath"
|
||||
@@ -28,15 +30,15 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/client"
|
||||
"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/policy/featuregate"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
"github.com/fatedier/frp/pkg/util/log"
|
||||
"github.com/fatedier/frp/pkg/util/version"
|
||||
"kanhole/client"
|
||||
"kanhole/pkg/config"
|
||||
"kanhole/pkg/config/source"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/config/v1/validation"
|
||||
"kanhole/pkg/policy/featuregate"
|
||||
"kanhole/pkg/policy/security"
|
||||
"kanhole/pkg/util/log"
|
||||
"kanhole/pkg/util/version"
|
||||
)
|
||||
|
||||
var (
|
||||
@@ -45,21 +47,23 @@ var (
|
||||
showVersion bool
|
||||
strictConfigMode bool
|
||||
allowUnsafe []string
|
||||
serverConfigURL string
|
||||
)
|
||||
|
||||
func init() {
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./frpc.ini", "config file of frpc")
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
|
||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "./kanholec.toml", "config file of kanholec")
|
||||
rootCmd.PersistentFlags().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one kanholec service for each file in config directory")
|
||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of kanholec")
|
||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
|
||||
rootCmd.PersistentFlags().StringVarP(&serverConfigURL, "server-config", "", "", "fetch config from frps server URL (auto-reloads on change)")
|
||||
|
||||
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
|
||||
}
|
||||
|
||||
var rootCmd = &cobra.Command{
|
||||
Use: "frpc",
|
||||
Short: "frpc is the client of frp (https://github.com/fatedier/frp)",
|
||||
Use: "kanholec",
|
||||
Short: "kanholec is the client component of kanhole",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if showVersion {
|
||||
fmt.Println(version.Full())
|
||||
@@ -68,13 +72,23 @@ var rootCmd = &cobra.Command{
|
||||
|
||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||
|
||||
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
|
||||
// If cfgDir is not empty, run multiple kanholec service for each config file in cfgDir.
|
||||
// Note that it's only designed for testing. It's not guaranteed to be stable.
|
||||
if cfgDir != "" {
|
||||
_ = runMultipleClients(cfgDir, unsafeFeatures)
|
||||
return nil
|
||||
}
|
||||
|
||||
// If server-config is set, use it instead of local config file.
|
||||
if serverConfigURL != "" {
|
||||
err := runClientWithServerConfig(serverConfigURL, unsafeFeatures)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
os.Exit(1)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Do not show command usage here.
|
||||
err := runClient(cfgFile, unsafeFeatures)
|
||||
if err != nil {
|
||||
@@ -97,7 +111,7 @@ func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures)
|
||||
defer wg.Done()
|
||||
err := runClient(path, unsafeFeatures)
|
||||
if err != nil {
|
||||
fmt.Printf("frpc service error for config file [%s]\n", path)
|
||||
fmt.Printf("kanholec service error for config file [%s]\n", path)
|
||||
}
|
||||
}()
|
||||
return nil
|
||||
@@ -120,6 +134,37 @@ func handleTermSignal(svr *client.Service) {
|
||||
svr.GracefulClose(500 * time.Millisecond)
|
||||
}
|
||||
|
||||
func runClientWithServerConfig(url string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||
log.Infof("fetching config from server: %s", url)
|
||||
resp, err := http.Get(url)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to fetch config from server: %w", err)
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to read config from server: %w", err)
|
||||
}
|
||||
|
||||
allCfg := v1.ClientConfig{}
|
||||
if err := config.LoadConfigure(body, &allCfg, strictConfigMode, "toml"); err != nil {
|
||||
return fmt.Errorf("failed to parse config from server: %w", err)
|
||||
}
|
||||
|
||||
result := &config.ClientConfigLoadResult{
|
||||
Common: &allCfg.ClientCommonConfig,
|
||||
Proxies: make([]v1.ProxyConfigurer, 0),
|
||||
}
|
||||
for _, c := range allCfg.Proxies {
|
||||
result.Proxies = append(result.Proxies, c.ProxyConfigurer)
|
||||
}
|
||||
|
||||
result.Common.ConfigURL = url
|
||||
|
||||
return runClientWithAggregator(result, unsafeFeatures, "")
|
||||
}
|
||||
|
||||
func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) error {
|
||||
// Load configuration
|
||||
result, err := config.LoadClientConfigResult(cfgFilePath, strictConfigMode)
|
||||
@@ -198,8 +243,8 @@ func startServiceWithAggregator(
|
||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||
|
||||
if cfgFile != "" {
|
||||
log.Infof("start frpc service for config file [%s] with aggregated configuration", cfgFile)
|
||||
defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
|
||||
log.Infof("start kanholec service for config file [%s] with aggregated configuration", cfgFile)
|
||||
defer log.Infof("kanholec service for config file [%s] stopped", cfgFile)
|
||||
}
|
||||
svr, err := client.NewService(client.ServiceOptions{
|
||||
Common: cfg,
|
||||
@@ -1,4 +1,4 @@
|
||||
// Copyright 2021 The frp Authors
|
||||
// Copyright 2021 The kanhole Authors
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
@@ -20,9 +20,9 @@ import (
|
||||
|
||||
"github.com/spf13/cobra"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
"kanhole/pkg/config"
|
||||
"kanhole/pkg/config/v1/validation"
|
||||
"kanhole/pkg/policy/security"
|
||||
)
|
||||
|
||||
func init() {
|
||||
@@ -34,7 +34,7 @@ var verifyCmd = &cobra.Command{
|
||||
Short: "Verify that the configures is valid",
|
||||
RunE: func(cmd *cobra.Command, args []string) error {
|
||||
if cfgFile == "" {
|
||||
fmt.Println("frpc: the configuration file is not specified")
|
||||
fmt.Println("kanholec: the configuration file is not specified")
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -53,7 +53,7 @@ var verifyCmd = &cobra.Command{
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Printf("frpc: the configuration file %s syntax is ok\n", cfgFile)
|
||||
fmt.Printf("kanholec: the configuration file %s syntax is ok\n", cfgFile)
|
||||
return nil
|
||||
},
|
||||
}
|
||||
+47
-3
@@ -33,7 +33,51 @@ git commit -m "bump version to vX.Y.Z"
|
||||
git push origin dev
|
||||
```
|
||||
|
||||
## 3. Merge dev → master
|
||||
## 3. Pre-release Validation
|
||||
|
||||
Run the standard e2e suite locally:
|
||||
|
||||
```bash
|
||||
make e2e
|
||||
```
|
||||
|
||||
For releases that touch compatibility-sensitive areas such as login, control
|
||||
connections, work connections, visitors, transport, or wire protocol handling,
|
||||
also run the manual compatibility e2e suite:
|
||||
|
||||
```bash
|
||||
make e2e-compatibility
|
||||
make e2e-compatibility-floor
|
||||
```
|
||||
|
||||
`make e2e-compatibility` builds the current `frps` and `frpc`, resolves the
|
||||
recent stable release baselines from GitHub, downloads or reuses their binaries,
|
||||
and tests current binaries against those historical releases. The default number
|
||||
of recent baselines is controlled by `FRP_COMPAT_BASELINE_COUNT` in the
|
||||
`Makefile`.
|
||||
|
||||
Downloaded release binaries are cached under:
|
||||
|
||||
```text
|
||||
.cache/e2e-compat/<version>/<os>_<arch>/
|
||||
```
|
||||
|
||||
For a release validation run that must be exactly reproducible, pass an explicit
|
||||
baseline matrix instead of using the floating recent-release list:
|
||||
|
||||
```bash
|
||||
FRP_COMPAT_BASELINE_VERSIONS="0.X.0 0.Y.0" make e2e-compatibility
|
||||
```
|
||||
|
||||
Use `make e2e-compatibility-smoke` for a quick single-baseline check while
|
||||
iterating locally. If GitHub release metadata requests are rate-limited, set
|
||||
`GITHUB_TOKEN` or use `FRP_COMPAT_BASELINE_VERSIONS`.
|
||||
|
||||
The compatibility floor is a support-policy decision, not a value that should
|
||||
change every release. Update `FRP_COMPAT_FLOOR_VERSION` only when the declared
|
||||
compatibility window changes.
|
||||
|
||||
## 4. Merge dev → master
|
||||
|
||||
Create a PR from `dev` to `master`:
|
||||
|
||||
@@ -43,7 +87,7 @@ gh pr create --base master --head dev --title "bump version"
|
||||
|
||||
Wait for CI to pass, then merge using **merge commit** (not squash).
|
||||
|
||||
## 4. Tag the Release
|
||||
## 5. Tag the Release
|
||||
|
||||
```bash
|
||||
git checkout master
|
||||
@@ -52,7 +96,7 @@ git tag -a vX.Y.Z -m "bump version"
|
||||
git push origin vX.Y.Z
|
||||
```
|
||||
|
||||
## 5. Trigger GoReleaser
|
||||
## 6. Trigger GoReleaser
|
||||
|
||||
Manually trigger the `goreleaser` workflow in GitHub Actions:
|
||||
|
||||
|
||||
@@ -1,11 +1,13 @@
|
||||
module github.com/fatedier/frp
|
||||
module kanhole
|
||||
|
||||
go 1.25.0
|
||||
|
||||
require (
|
||||
entgo.io/ent v0.14.6
|
||||
fyne.io/fyne/v2 v2.7.4
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5
|
||||
github.com/coreos/go-oidc/v3 v3.14.1
|
||||
github.com/fatedier/golib v0.6.0
|
||||
github.com/fatedier/golib v0.7.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/mux v1.8.1
|
||||
github.com/gorilla/websocket v1.5.0
|
||||
@@ -26,31 +28,66 @@ require (
|
||||
github.com/tidwall/gjson v1.17.1
|
||||
github.com/vishvananda/netlink v1.3.0
|
||||
github.com/xtaci/kcp-go/v5 v5.6.13
|
||||
golang.org/x/crypto v0.49.0
|
||||
golang.org/x/net v0.52.0
|
||||
golang.org/x/crypto v0.52.0
|
||||
golang.org/x/net v0.54.0
|
||||
golang.org/x/oauth2 v0.28.0
|
||||
golang.org/x/sync v0.20.0
|
||||
golang.org/x/sys v0.45.0
|
||||
golang.org/x/time v0.10.0
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173
|
||||
gopkg.in/ini.v1 v1.67.0
|
||||
k8s.io/apimachinery v0.28.8
|
||||
k8s.io/client-go v0.28.8
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2
|
||||
modernc.org/sqlite v1.51.0
|
||||
)
|
||||
|
||||
require (
|
||||
ariga.io/atlas v0.36.2-0.20250730182955-2c6300d0a3e1 // indirect
|
||||
fyne.io/systray v1.12.1 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/BurntSushi/toml v1.5.0 // indirect
|
||||
github.com/agext/levenshtein v1.2.3 // indirect
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bmatcuk/doublestar v1.3.4 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||
github.com/clipperhouse/stringish v0.1.1 // indirect
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 // indirect
|
||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/fredbi/uri v1.1.1 // indirect
|
||||
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||
github.com/fyne-io/gl-js v0.2.0 // indirect
|
||||
github.com/fyne-io/glfw-js v0.3.0 // indirect
|
||||
github.com/fyne-io/image v0.1.1 // indirect
|
||||
github.com/fyne-io/oksvg v0.2.0 // indirect
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 // indirect
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a // indirect
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||
github.com/go-logr/logr v1.4.2 // indirect
|
||||
github.com/go-openapi/inflect v0.19.0 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/go-text/render v0.2.1 // indirect
|
||||
github.com/go-text/typesetting v0.3.4 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-cmp v0.7.0 // indirect
|
||||
github.com/google/pprof v0.0.0-20250403155104-27863c87afa6 // indirect
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 // indirect
|
||||
github.com/hack-pad/safejs v0.1.0 // indirect
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade // indirect
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/klauspost/reedsolomon v1.12.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-runewidth v0.0.19 // indirect
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 // indirect
|
||||
github.com/ncruces/go-strftime v1.0.0 // indirect
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 // indirect
|
||||
github.com/pion/dtls/v3 v3.0.10 // indirect
|
||||
github.com/pion/logging v0.2.4 // indirect
|
||||
github.com/pion/transport/v4 v4.0.1 // indirect
|
||||
@@ -59,6 +96,10 @@ require (
|
||||
github.com/prometheus/client_model v0.5.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
github.com/rymdport/portal v0.4.2 // indirect
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c // indirect
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef // indirect
|
||||
github.com/templexxx/cpu v0.1.1 // indirect
|
||||
github.com/templexxx/xorsimd v0.4.3 // indirect
|
||||
github.com/tidwall/match v1.1.1 // indirect
|
||||
@@ -66,16 +107,21 @@ require (
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/wlynxg/anet v0.0.5 // indirect
|
||||
github.com/yuin/goldmark v1.7.8 // indirect
|
||||
github.com/zclconf/go-cty v1.14.4 // indirect
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 // indirect
|
||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||
golang.org/x/mod v0.33.0 // indirect
|
||||
golang.org/x/sys v0.42.0 // indirect
|
||||
golang.org/x/text v0.35.0 // indirect
|
||||
golang.org/x/tools v0.42.0 // indirect
|
||||
golang.org/x/image v0.24.0 // indirect
|
||||
golang.org/x/mod v0.35.0 // indirect
|
||||
golang.org/x/text v0.37.0 // indirect
|
||||
golang.org/x/tools v0.44.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
google.golang.org/protobuf v1.36.5 // indirect
|
||||
gopkg.in/yaml.v2 v2.4.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 // indirect
|
||||
modernc.org/libc v1.72.3 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd // indirect
|
||||
sigs.k8s.io/yaml v1.3.0 // indirect
|
||||
)
|
||||
|
||||
@@ -1,15 +1,37 @@
|
||||
ariga.io/atlas v0.36.2-0.20250730182955-2c6300d0a3e1 h1:NPPfBaVZgz4LKBCIc0FbMogCjvXN+yGf7CZwotOwJo8=
|
||||
ariga.io/atlas v0.36.2-0.20250730182955-2c6300d0a3e1/go.mod h1:Ex5l1xHsnWQUc3wYnrJ9gD7RUEzG76P7ZRQp8wNr0wc=
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
entgo.io/ent v0.14.6 h1:/f2696BpwuWAEEG6PVGWflg6+Inrpq4pRWuNlWz/Skk=
|
||||
entgo.io/ent v0.14.6/go.mod h1:z46QBUdGC+BATwsedbDuREfSS0oSCV+csdEYlL4p73s=
|
||||
fyne.io/fyne/v2 v2.7.4 h1:OVCI5mT+Onb2kA4wlmGA5pLCqKik9f4NDb5jiR1OMTc=
|
||||
fyne.io/fyne/v2 v2.7.4/go.mod h1:ZD1mmhBY75mSa97IXl3MPlICd1uNHfCXYh5hKIlVOII=
|
||||
fyne.io/systray v1.12.1 h1:ygBD6aZXwiOmZoY5N+ukbH9pih0Kq6fYgVeMYbr5skQ=
|
||||
fyne.io/systray v1.12.1/go.mod h1:RVwqP9nYMo7h5zViCBHri2FgjXF7H2cub7MAq4NSoLs=
|
||||
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg=
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0 h1:Shsta01QNfFxHCfpW6YH2STWB0MudeXXEWMr20OEh60=
|
||||
github.com/DATA-DOG/go-sqlmock v1.5.0/go.mod h1:f/Ixk793poVmq4qj/V1dPUg2JEAKC73Q5eFN3EC/SaM=
|
||||
github.com/agext/levenshtein v1.2.3 h1:YB2fHEn0UJagG8T1rrWknE3ZQzWM06O8AMAatNn7lmo=
|
||||
github.com/agext/levenshtein v1.2.3/go.mod h1:JEDfjyjHDjOF/1e4FlBE/PkbqA9OfWu2ki2W0IB5558=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0 h1:uYvfpb3DyLSCGWnctWKGj857c6ew1u1fNQOlOtuGxQY=
|
||||
github.com/apparentlymart/go-textseg/v15 v15.0.0/go.mod h1:K8XmNZdhEBkdlyDdvbmmsvpAG721bKi0joRfFdHIWJ4=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
||||
github.com/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||
github.com/bmatcuk/doublestar v1.3.4 h1:gPypJ5xD31uhX6Tf54sDPUOBXTqKH4c9aPY66CyQrS0=
|
||||
github.com/bmatcuk/doublestar v1.3.4/go.mod h1:wiQtGV+rzVYxB7WIlirSN++5HPtPlXEo9MEoZQC/PmE=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.2.0 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/clipperhouse/stringish v0.1.1 h1:+NSqMOr3GR6k1FdRhhnXrLfztGzuG+VuFDfatpWHKCs=
|
||||
github.com/clipperhouse/stringish v0.1.1/go.mod h1:v/WhFtE1q0ovMta2+m+UbpZ+2/HEXNWYXQgCt4hdOzA=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0 h1:SNdx9DVUqMoBuBoW3iLOj4FQv3dN5mDtuqwuhIGpJy4=
|
||||
github.com/clipperhouse/uax29/v2 v2.3.0/go.mod h1:Wn1g7MK6OoeDT0vL+Q0SQLDz/KpfsVRgg6W7ihQeh4g=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||
github.com/coreos/go-oidc/v3 v3.14.1/go.mod h1:HaZ3szPaZ0e4r6ebqvsLWlk2Tn+aejfmrfah6hnSYEU=
|
||||
@@ -17,19 +39,51 @@ github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46t
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/fatedier/golib v0.6.0 h1:/mgBZZbkbMhIEZoXf7nV8knpUDzas/b+2ruYKxx1lww=
|
||||
github.com/fatedier/golib v0.6.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
|
||||
github.com/fatedier/golib v0.7.0 h1:tMDF9ObcwVt59VUHroJOzHQjVFPLymZVMpGm9WAVwhY=
|
||||
github.com/fatedier/golib v0.7.0/go.mod h1:ArUGvPg2cOw/py2RAuBt46nNZH2VQ5Z70p109MAZpJw=
|
||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMBUuongRWC0rBqN6gd01Dzu+D21NE=
|
||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
||||
github.com/felixge/fgprof v0.9.3 h1:VvyZxILNuCiUCSXtPtYmmtGvb65nqXh2QFWc0Wpf2/g=
|
||||
github.com/felixge/fgprof v0.9.3/go.mod h1:RdbpDgzqYVh/T9fPELJyV7EYJuHB55UTEULNun8eiPw=
|
||||
github.com/fredbi/uri v1.1.1 h1:xZHJC08GZNIUhbP5ImTHnt5Ya0T8FI2VAwI/37kh2Ko=
|
||||
github.com/fredbi/uri v1.1.1/go.mod h1:4+DZQ5zBjEwQCDmXW5JdIjz0PUA+yJbvtBv+u+adr5o=
|
||||
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||
github.com/fyne-io/gl-js v0.2.0 h1:+EXMLVEa18EfkXBVKhifYB6OGs3HwKO3lUElA0LlAjs=
|
||||
github.com/fyne-io/gl-js v0.2.0/go.mod h1:ZcepK8vmOYLu96JoxbCKJy2ybr+g1pTnaBDdl7c3ajI=
|
||||
github.com/fyne-io/glfw-js v0.3.0 h1:d8k2+Y7l+zy2pc7wlGRyPfTgZoqDf3AI4G+2zOWhWUk=
|
||||
github.com/fyne-io/glfw-js v0.3.0/go.mod h1:Ri6te7rdZtBgBpxLW19uBpp3Dl6K9K/bRaYdJ22G8Jk=
|
||||
github.com/fyne-io/image v0.1.1 h1:WH0z4H7qfvNUw5l4p3bC1q70sa5+YWVt6HCj7y4VNyA=
|
||||
github.com/fyne-io/image v0.1.1/go.mod h1:xrfYBh6yspc+KjkgdZU/ifUC9sPA5Iv7WYUBzQKK7JM=
|
||||
github.com/fyne-io/oksvg v0.2.0 h1:mxcGU2dx6nwjJsSA9PCYZDuoAcsZ/OuJlvg/Q9Njfo8=
|
||||
github.com/fyne-io/oksvg v0.2.0/go.mod h1:dJ9oEkPiWhnTFNCmRgEze+YNprJF7YRbpjgpWS4kzoI=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71 h1:5BVwOaUSBTlVZowGO6VZGw2H/zl9nrd3eCZfYV+NfQA=
|
||||
github.com/go-gl/gl v0.0.0-20231021071112-07e5d0ea2e71/go.mod h1:9YTyiznxEY1fVinfM7RvRcjRHbw2xLBJ3AAGIT0I4Nw=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a h1:vxnBhFDDT+xzxf1jTJKMKZw3H0swfWk9RpWbBbDK5+0=
|
||||
github.com/go-gl/glfw/v3.3/glfw v0.0.0-20240506104042-037f3cc74f2a/go.mod h1:tQ2UAYgL5IevRw8kRxooKSPJfGvJ9fJQFa0TUsXzTg8=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
||||
github.com/go-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-openapi/inflect v0.19.0 h1:9jCH9scKIbHeV9m12SmPilScz6krDxKRasNNSNPXu/4=
|
||||
github.com/go-openapi/inflect v0.19.0/go.mod h1:lHpZVlpIQqLyKwJ4N+YSc9hchQy/i12fJykb83CRBH4=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/go-test/deep v1.0.3 h1:ZrJSEWsXzPOxaZnFteGEfooLba+ju3FYIbOrS+rQd68=
|
||||
github.com/go-test/deep v1.0.3/go.mod h1:wGDj63lr65AM2AQyKZd/NYHGb0R+1RLqB8NKt3aSFNA=
|
||||
github.com/go-text/render v0.2.1 h1:qwHhxqGUjjg4L0XyJWj7M7bpY75NZM+kBpv2Yfw5mcg=
|
||||
github.com/go-text/render v0.2.1/go.mod h1:HCCAq8MUlm/WRcXshBb4K/n+IkjeXQ1c2Ba+yICSm0A=
|
||||
github.com/go-text/typesetting v0.3.4 h1:YYurUOtEb9kGSOz4uE3k4OpBGsp1dDL8+fjCeaFamAU=
|
||||
github.com/go-text/typesetting v0.3.4/go.mod h1:4qZCQphq4KSgGTAeI0uMEkVbROgfah8BuyF5LRYr7XY=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3 h1:drBZzMgdYPbmyXqOto4YhhJGrFIQCX94FpR4MzTCsos=
|
||||
github.com/go-text/typesetting-utils v0.0.0-20260223113751-2d88ac90dae3/go.mod h1:3/62I4La/HBRX9TcTpBj4eipLiwzf+vhI+7whTc9V7o=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
@@ -60,8 +114,20 @@ github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY=
|
||||
github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ=
|
||||
github.com/gorilla/websocket v1.5.0 h1:PPwGk2jz7EePpoHN/+ClbZu8SPxiqlu12wZP/3sWmnc=
|
||||
github.com/gorilla/websocket v1.5.0/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2 h1:DTqeJJYc1usa45Q5r52t01KhvlSN02+Oq+tQbSBI91A=
|
||||
github.com/hack-pad/go-indexeddb v0.3.2/go.mod h1:QvfTevpDVlkfomY498LhstjwbPW6QC4VC/lxYb0Kom0=
|
||||
github.com/hack-pad/safejs v0.1.0 h1:qPS6vjreAqh2amUqj4WNG1zIw7qlRQJ9K10eDKMCnE8=
|
||||
github.com/hack-pad/safejs v0.1.0/go.mod h1:HdS+bKF1NrE72VoXZeWzxFOVQVUSqZJAG0xNCnb+Tio=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl/v2 v2.18.1 h1:6nxnOJFku1EuSawSD81fuviYUV8DxFr3fp2dUi3ZYSo=
|
||||
github.com/hashicorp/hcl/v2 v2.18.1/go.mod h1:ThLC89FV4p9MPW804KVbe/cEXoQ8NZEh+JtMeeGErHE=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade h1:FmusiCI1wHw+XQbvL9M+1r/C3SPqKrmBaIOYwVfQoDE=
|
||||
github.com/jeandeaual/go-locale v0.0.0-20250612000132-0ef82f21eade/go.mod h1:ZDXo8KHryOWSIqnsb/CiDq7hQUYryCgdVnxbj8tDG7o=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25 h1:YLvr1eE6cdCqjOe972w/cYF+FjW34v27+9Vo5106B4M=
|
||||
github.com/jsummers/gobmp v0.0.0-20230614200233-a9de23ed2e25/go.mod h1:kLgvv7o6UM+0QSf0QjAse3wReFDsb9qbZJdfexWlrQw=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
|
||||
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/reedsolomon v1.12.0 h1:I5FEp3xSwVCcEh3F5A7dofEfhXdF/bWhQWPH+XwBFno=
|
||||
@@ -70,8 +136,23 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||
github.com/kylelemons/godebug v1.1.0 h1:RPNrshWIDI6G2gRW9EHilWtl7Z6Sb1BR0xunSBf0SNc=
|
||||
github.com/kylelemons/godebug v1.1.0/go.mod h1:9/0rRGxNHcop5bhtWyNeEfOS8JIWk580+fNqagV/RAw=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||
github.com/mattn/go-runewidth v0.0.19 h1:v++JhqYnZuu5jSKrk9RbgF5v4CGUjqRfBm05byFGLdw=
|
||||
github.com/mattn/go-runewidth v0.0.19/go.mod h1:XBkDxAl56ILZc9knddidhrOlY5R/pDhgLpndooCuJAs=
|
||||
github.com/mattn/go-sqlite3 v1.14.28 h1:ThEiQrnbtumT+QMknw63Befp/ce/nUPgBPMlRFEum7A=
|
||||
github.com/mattn/go-sqlite3 v1.14.28/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1 h1:TLuKupo69TCn6TQSyGxwI1EblZZEsQ0vMlAFQflz0v0=
|
||||
github.com/mitchellh/go-wordwrap v1.0.1/go.mod h1:R62XHJLzvMFRBbcrT7m7WgmE1eOyTSsCt+hzestvNj0=
|
||||
github.com/ncruces/go-strftime v1.0.0 h1:HMFp8mLCTPp341M/ZnA4qaf7ZlsbTc+miZjCLOFAw7w=
|
||||
github.com/ncruces/go-strftime v1.0.0/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1 h1:IxtPxYsR9Gp60cGXjfuR/llTqV8aYMsC472zD0D1vHk=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.5.1/go.mod h1:DrhgsSDZxoAfvVrBVLXoxZn/pN5TXqaDbq7ju94viiQ=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
||||
github.com/onsi/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||
@@ -90,6 +171,8 @@ github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwy
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pkg/profile v1.7.0 h1:hnbDkaNWPCLMO9wGLdBFTIZvzDrDfBM2072E1S9gJkA=
|
||||
github.com/pkg/profile v1.7.0/go.mod h1:8Uer0jas47ZQMJ7VD+OHknK4YDY07LPUC6dEvqDjvNo=
|
||||
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/prashantv/gostub v1.1.0 h1:BTyx3RfQjRHnUWaGF9oQos79AlQ5k8WNktv7VGvVH4g=
|
||||
@@ -105,21 +188,30 @@ github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
|
||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
||||
github.com/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||
github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
|
||||
github.com/rodaine/table v1.2.0/go.mod h1:wejb/q/Yd4T/SVmBSRMr7GCq3KlcZp3gyNYdLSBhkaE=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/rymdport/portal v0.4.2 h1:7jKRSemwlTyVHHrTGgQg7gmNPJs88xkbKcIL3NlcmSU=
|
||||
github.com/rymdport/portal v0.4.2/go.mod h1:kFF4jslnJ8pD5uCi17brj/ODlfIidOxlgUDTO5ncnC4=
|
||||
github.com/samber/lo v1.47.0 h1:z7RynLwP5nbyRscyvcD043DWYoOcYRv3mV8lBeqOCLc=
|
||||
github.com/samber/lo v1.47.0/go.mod h1:RmDH9Ct32Qy3gduHQuKJ3gW1fMHAnE/fAzQuf6He5cU=
|
||||
github.com/sergi/go-diff v1.3.1 h1:xkr+Oxo4BOQKmkn/B9eMK0g5Kg/983T9DqqPHwYqD+8=
|
||||
github.com/sergi/go-diff v1.3.1/go.mod h1:aMJSSKb2lpPvRNec0+w3fl7LP9IOFzdc9Pa4NFbPK1I=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c h1:km8GpoQut05eY3GiYWEedbTT0qnSxrCjsVbb7yKY1KE=
|
||||
github.com/srwiley/oksvg v0.0.0-20221011165216-be6e8873101c/go.mod h1:cNQ3dwVJtS5Hmnjxy6AgTPd0Inb3pW05ftPSX7NZO7Q=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef h1:Ch6Q+AZUxDBCVqdkI8FSpFyZDtCVBc2VmejdNrm5rRQ=
|
||||
github.com/srwiley/rasterx v0.0.0-20220730225603-2ab79fcdd4ef/go.mod h1:nXTWP6+gD5+LUJ8krVhhoeHjvHTutPxMYl5SvkcnJNE=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -152,6 +244,12 @@ github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4H
|
||||
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
|
||||
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
|
||||
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37/go.mod h1:HpMP7DB2CyokmAh4lp0EQnnWhmycP/TvwBGzvuie+H0=
|
||||
github.com/yuin/goldmark v1.7.8 h1:iERMLn0/QJeHFhxSt3p6PeN9mGnvIKSpG9YYorDMnic=
|
||||
github.com/yuin/goldmark v1.7.8/go.mod h1:uzxRWxtg69N339t3louHJ7+O03ezfj6PlliRlaOzY1E=
|
||||
github.com/zclconf/go-cty v1.14.4 h1:uXXczd9QDGsgu0i/QFR/hzI5NYCHLf6NQw/atrbnhq8=
|
||||
github.com/zclconf/go-cty v1.14.4/go.mod h1:VvMs5i0vgZdhYawQNq5kePSpLAoz8u1xvZgrPIxfnZE=
|
||||
github.com/zclconf/go-cty-yaml v1.1.0 h1:nP+jp0qPHv2IhUVqmQSzjvqAWcObN0KBkUl2rWBdig0=
|
||||
github.com/zclconf/go-cty-yaml v1.1.0/go.mod h1:9YLUH4g7lOhVWqUbctnVlZ5KLpg7JAprQNgxSZ1Gyxs=
|
||||
go.uber.org/automaxprocs v1.6.0 h1:O3y2/QNTOdbF+e/dpXNNW7Rx2hZ4sTIPyybbxyNqTUs=
|
||||
go.uber.org/automaxprocs v1.6.0/go.mod h1:ifeIMSnPZuznNm6jmdzmU3/bfk01Fe2fotchwEFJ8r8=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
@@ -159,22 +257,24 @@ go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.49.0 h1:+Ng2ULVvLHnJ/ZFEq4KdcDd/cfjrrjjNSXNzxg0Y4U4=
|
||||
golang.org/x/crypto v0.49.0/go.mod h1:ErX4dUh2UM+CFYiXZRTcMpEcN8b/1gxEuv3nODoYtCA=
|
||||
golang.org/x/crypto v0.52.0 h1:RMs7fP2rXdep0CftQlK8Uf+kibLm7qkCcradZWYz988=
|
||||
golang.org/x/crypto v0.52.0/go.mod h1:1QgfPxDqh0T2M/elOJtp9RvuR95kVjir0e6/BvEmGbc=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/image v0.24.0 h1:AN7zRgVsbvmTfNyqIbbOraYL8mSwcKncEj8ofjgzcMQ=
|
||||
golang.org/x/image v0.24.0/go.mod h1:4b/ITuLfqYq1hqZcjofwctIhi7sZh2WaCjvsBNjjya8=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||
golang.org/x/mod v0.35.0 h1:Ww1D637e6Pg+Zb2KrWfHQUnH2dQRLBQyAtpr/haaJeM=
|
||||
golang.org/x/mod v0.35.0/go.mod h1:+GwiRhIInF8wPm+4AoT6L0FA1QWAad3OMdTRx4tFYlU=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.52.0 h1:He/TN1l0e4mmR3QqHMT2Xab3Aj3L9qjbhRm78/6jrW0=
|
||||
golang.org/x/net v0.52.0/go.mod h1:R1MAz7uMZxVMualyPXb+VaqGSa3LIaUqk0eEt3w36Sw=
|
||||
golang.org/x/net v0.54.0 h1:2zJIZAxAHV/OHCDTCOHAYehQzLfSXuf/5SoL/Dv6w/w=
|
||||
golang.org/x/net v0.54.0/go.mod h1:Sj4oj8jK6XmHpBZU/zWHw3BV3abl4Kvi+Ut7cQcY+cQ=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/oauth2 v0.28.0 h1:CrgCKl8PPAVtLnU3c+EDw6x11699EWlsDeWNWKdIOkc=
|
||||
golang.org/x/oauth2 v0.28.0/go.mod h1:onh5ek6nERTohokkhCD/y2cV4Do3fxFHFuAejCkRWT8=
|
||||
@@ -189,15 +289,16 @@ golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7w
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.42.0 h1:omrd2nAlyT5ESRdCLYdm3+fMfNFE/+Rf4bDIQImRJeo=
|
||||
golang.org/x/sys v0.42.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.41.0 h1:QCgPso/Q3RTJx2Th4bDLqML4W6iJiaXFq2/ftQF13YU=
|
||||
golang.org/x/term v0.41.0/go.mod h1:3pfBgksrReYfZ5lvYM0kSO0LIkAl4Yl2bXOkKP7Ec2A=
|
||||
golang.org/x/sys v0.45.0 h1:dO4czNzziLiiXplLQgBCEpCvXQ3dnkn0SdaZSYdQ+FY=
|
||||
golang.org/x/sys v0.45.0/go.mod h1:4GL1E5IUh+htKOUEOaiffhrAeqysfVGipDYzABqnCmw=
|
||||
golang.org/x/term v0.43.0 h1:S4RLU2sB31O/NCl+zFN9Aru9A/Cq2aqKpTZJ6B+DwT4=
|
||||
golang.org/x/term v0.43.0/go.mod h1:lrhlHNdQJHO+1qVYiHfFKVuVioJIheAc3fBSMFYEIsk=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||
golang.org/x/text v0.37.0 h1:Cqjiwd9eSg8e0QAkyCaQTNHFIIzWtidPahFWR83rTrc=
|
||||
golang.org/x/text v0.37.0/go.mod h1:a5sjxXGs9hsn/AJVwuElvCAo9v8QYLzvavO5z2PiM38=
|
||||
golang.org/x/time v0.10.0 h1:3usCWA8tQn0L8+hFJQNgzpWbd89begxN66o1Ojdn5L4=
|
||||
golang.org/x/time v0.10.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
@@ -205,8 +306,8 @@ golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||
golang.org/x/tools v0.44.0 h1:UP4ajHPIcuMjT1GqzDWRlalUEoY+uzoZKnhOjbIPD2c=
|
||||
golang.org/x/tools v0.44.0/go.mod h1:KA0AfVErSdxRZIsOVipbv3rQhVXTnlU6UhKxHd1seDI=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
@@ -248,6 +349,34 @@ k8s.io/client-go v0.28.8 h1:TE59Tjd87WKvS2FPBTfIKLFX0nQJ4SSHsnDo5IHjgOw=
|
||||
k8s.io/client-go v0.28.8/go.mod h1:uDVQ/rPzWpWIy40c6lZ4mUwaEvRWGnpoqSO4FM65P3o=
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2 h1:qY1Ad8PODbnymg2pRbkyMT/ylpTrCM8P2RJ0yroCyIk=
|
||||
k8s.io/utils v0.0.0-20230406110748-d93618cff8a2/go.mod h1:OLgZIPagt7ERELqWJFomSt595RzquPNLL48iOWgYOg0=
|
||||
modernc.org/cc/v4 v4.28.2 h1:3tQ0lf2ADtoby2EtSP+J7IE2SHwEJdP8ioR59wx7XpY=
|
||||
modernc.org/cc/v4 v4.28.2/go.mod h1:OnovgIhbbMXMu1aISnJ0wvVD1KnW+cAUJkIrAWh+kVI=
|
||||
modernc.org/ccgo/v4 v4.34.0 h1:yRLPFZieg532OT4rp4JFNIVcquwalMX26G95WQDqwCQ=
|
||||
modernc.org/ccgo/v4 v4.34.0/go.mod h1:AS5WYMyBakQ+fhsHhtP8mWB82KTGPkNNJDGfGQCe0/A=
|
||||
modernc.org/fileutil v1.4.0 h1:j6ZzNTftVS054gi281TyLjHPp6CPHr2KCxEXjEbD6SM=
|
||||
modernc.org/fileutil v1.4.0/go.mod h1:EqdKFDxiByqxLk8ozOxObDSfcVOv/54xDs/DUHdvCUU=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/gc/v3 v3.1.2 h1:ZtDCnhonXSZexk/AYsegNRV1lJGgaNZJuKjJSWKyEqo=
|
||||
modernc.org/gc/v3 v3.1.2/go.mod h1:HFK/6AGESC7Ex+EZJhJ2Gni6cTaYpSMmU/cT9RmlfYY=
|
||||
modernc.org/goabi0 v0.2.0 h1:HvEowk7LxcPd0eq6mVOAEMai46V+i7Jrj13t4AzuNks=
|
||||
modernc.org/goabi0 v0.2.0/go.mod h1:CEFRnnJhKvWT1c1JTI3Avm+tgOWbkOu5oPA8eH8LnMI=
|
||||
modernc.org/libc v1.72.3 h1:ZnDF4tXn4NBXFutMMQC4vtbTFSXhhKzR73fv0beZEAU=
|
||||
modernc.org/libc v1.72.3/go.mod h1:dn0dZNnnn1clLyvRxLxYExxiKRZIRENOfqQ8XEeg4Qs=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.2.0 h1:tGyef5ApycA7FSEOMraay9SaTk5zmbx7Tu+cJs4QKZg=
|
||||
modernc.org/opt v0.2.0/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.51.0 h1:aH/MMSoayAIhozZ7uJbVTT9QO/VhzBf0J9tymmmuC/U=
|
||||
modernc.org/sqlite v1.51.0/go.mod h1:tcNzv5p84E0skkmJn038y+hWJbLQXQqEnQfeh5r2JLM=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd h1:EDPBXCAspyGV4jQlpZSudPeMmr1bNJefnuqLsRAsHZo=
|
||||
sigs.k8s.io/json v0.0.0-20221116044647-bc3834ca7abd/go.mod h1:B8JuhiUyNFVKdsE8h686QcCxMaH6HrOAZj4vswFpcB0=
|
||||
sigs.k8s.io/yaml v1.3.0 h1:a2VclLzOGrwOHDiV8EfBGhvjHvP46CtW5j6POvhYGGo=
|
||||
|
||||
Executable
+162
@@ -0,0 +1,162 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -eu
|
||||
|
||||
SCRIPT=$(readlink -f "$0")
|
||||
ROOT=$(unset CDPATH && cd "$(dirname "$SCRIPT")/.." && pwd)
|
||||
|
||||
if ! command -v ginkgo >/dev/null 2>&1; then
|
||||
echo "ginkgo not found, try to install..."
|
||||
go install github.com/onsi/ginkgo/v2/ginkgo@v2.23.4
|
||||
fi
|
||||
|
||||
debug=false
|
||||
if [ "x${DEBUG:-}" = "xtrue" ]; then
|
||||
debug=true
|
||||
fi
|
||||
logLevel=debug
|
||||
if [ "${LOG_LEVEL:-}" ]; then
|
||||
logLevel="${LOG_LEVEL}"
|
||||
fi
|
||||
|
||||
currentFrpsPath=${CURRENT_FRPS_PATH:-${ROOT}/bin/frps}
|
||||
currentFrpcPath=${CURRENT_FRPC_PATH:-${ROOT}/bin/frpc}
|
||||
baselineCount=${FRP_COMPAT_BASELINE_COUNT:-8}
|
||||
targetOS=${TARGET_OS:-$(go env GOOS)}
|
||||
targetArch=${TARGET_ARCH:-$(go env GOARCH)}
|
||||
targetPlatform="${targetOS}_${targetArch}"
|
||||
cacheRoot=${FRP_COMPAT_CACHE_DIR:-${ROOT}/.cache/e2e-compat}
|
||||
|
||||
check_file() {
|
||||
if [ ! -f "$2" ]; then
|
||||
echo "$1 not found: $2"
|
||||
exit 1
|
||||
fi
|
||||
}
|
||||
|
||||
check_file "current frps" "${currentFrpsPath}"
|
||||
check_file "current frpc" "${currentFrpcPath}"
|
||||
|
||||
run_current_current=true
|
||||
|
||||
run_compatibility() {
|
||||
baselineVersion=$1
|
||||
baselineFrpsPath=$2
|
||||
baselineFrpcPath=$3
|
||||
|
||||
check_file "baseline frps" "${baselineFrpsPath}"
|
||||
check_file "baseline frpc" "${baselineFrpcPath}"
|
||||
|
||||
echo "Running compatibility e2e with baseline ${baselineVersion}"
|
||||
ginkgo -nodes=1 --poll-progress-after=60s "${ROOT}/test/e2e/compatibility" -- \
|
||||
-current-frps-path="${currentFrpsPath}" \
|
||||
-current-frpc-path="${currentFrpcPath}" \
|
||||
-baseline-frps-path="${baselineFrpsPath}" \
|
||||
-baseline-frpc-path="${baselineFrpcPath}" \
|
||||
-baseline-version="${baselineVersion}" \
|
||||
-run-current-current="${run_current_current}" \
|
||||
-log-level="${logLevel}" \
|
||||
-debug="${debug}"
|
||||
run_current_current=false
|
||||
}
|
||||
|
||||
github_api_curl() {
|
||||
if [ "${GITHUB_TOKEN:-}" ]; then
|
||||
curl -fsSL \
|
||||
-H "Accept: application/vnd.github+json" \
|
||||
-H "Authorization: Bearer ${GITHUB_TOKEN}" \
|
||||
"$1"
|
||||
else
|
||||
curl -fsSL "$1"
|
||||
fi
|
||||
}
|
||||
|
||||
resolve_versions() {
|
||||
if [ "${FRP_COMPAT_BASELINE_VERSIONS:-}" ]; then
|
||||
printf "%s\n" "${FRP_COMPAT_BASELINE_VERSIONS}"
|
||||
return
|
||||
fi
|
||||
|
||||
case "${baselineCount}" in
|
||||
'' | *[!0-9]*)
|
||||
echo "FRP_COMPAT_BASELINE_COUNT must be a positive integer: ${baselineCount}" >&2
|
||||
exit 1
|
||||
;;
|
||||
esac
|
||||
if [ "${baselineCount}" -eq 0 ]; then
|
||||
echo "FRP_COMPAT_BASELINE_COUNT must be greater than 0" >&2
|
||||
exit 1
|
||||
fi
|
||||
if [ "${baselineCount}" -gt 100 ]; then
|
||||
echo "FRP_COMPAT_BASELINE_COUNT must be less than or equal to 100" >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
releaseURL="https://api.github.com/repos/fatedier/frp/releases?per_page=100"
|
||||
resolvedVersions=""
|
||||
if releases=$(github_api_curl "${releaseURL}" 2>/dev/null); then
|
||||
resolvedVersions=$(printf "%s\n" "${releases}" |
|
||||
sed -n 's/.*"tag_name":[[:space:]]*"v\([0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*\)".*/\1/p' |
|
||||
awk '!seen[$0]++' |
|
||||
head -n "${baselineCount}" |
|
||||
tr '\n' ' ' |
|
||||
sed 's/[[:space:]]*$//')
|
||||
else
|
||||
echo "Failed to fetch release metadata from GitHub API, falling back to GitHub releases page." >&2
|
||||
fi
|
||||
|
||||
if [ -z "${resolvedVersions}" ]; then
|
||||
releasesPageURL="https://github.com/fatedier/frp/releases"
|
||||
if ! releases=$(curl -fsSL "${releasesPageURL}"); then
|
||||
echo "Failed to fetch release metadata from GitHub: ${releasesPageURL}" >&2
|
||||
echo "Set FRP_COMPAT_BASELINE_VERSIONS to run with explicit baseline versions." >&2
|
||||
exit 1
|
||||
fi
|
||||
resolvedVersions=$(printf "%s\n" "${releases}" |
|
||||
grep -o 'releases/tag/v[0-9][0-9]*\.[0-9][0-9]*\.[0-9][0-9]*"' |
|
||||
sed 's#.*/v##; s/"$//' |
|
||||
awk '!seen[$0]++' |
|
||||
head -n "${baselineCount}" |
|
||||
tr '\n' ' ' |
|
||||
sed 's/[[:space:]]*$//')
|
||||
fi
|
||||
|
||||
set -- ${resolvedVersions}
|
||||
if [ "$#" -lt "${baselineCount}" ]; then
|
||||
echo "Only resolved $# stable release versions from GitHub, expected ${baselineCount}." >&2
|
||||
echo "Set FRP_COMPAT_BASELINE_VERSIONS to run with explicit baseline versions." >&2
|
||||
exit 1
|
||||
fi
|
||||
|
||||
printf "%s\n" "${resolvedVersions}"
|
||||
}
|
||||
|
||||
if [ "${BASELINE_FRPS_PATH:-}" ] || [ "${BASELINE_FRPC_PATH:-}" ]; then
|
||||
if [ -z "${BASELINE_FRPS_PATH:-}" ] || [ -z "${BASELINE_FRPC_PATH:-}" ]; then
|
||||
echo "BASELINE_FRPS_PATH and BASELINE_FRPC_PATH must be set together"
|
||||
exit 1
|
||||
fi
|
||||
run_compatibility "${FRP_COMPAT_BASELINE_VERSION:-custom}" "${BASELINE_FRPS_PATH}" "${BASELINE_FRPC_PATH}"
|
||||
exit 0
|
||||
fi
|
||||
|
||||
versions=$(resolve_versions)
|
||||
echo "Compatibility baseline versions: ${versions}"
|
||||
|
||||
mkdir -p "${cacheRoot}"
|
||||
for version in ${versions}; do
|
||||
baselineDir="${cacheRoot}/${version}/${targetPlatform}"
|
||||
if [ ! -f "${baselineDir}/frps" ] || [ ! -f "${baselineDir}/frpc" ]; then
|
||||
tmpDir="${cacheRoot}/.download-${version}-${targetPlatform}"
|
||||
rm -rf "${tmpDir}"
|
||||
(
|
||||
cd "${cacheRoot}"
|
||||
FRP_VERSION="${version}" TARGET_DIRNAME="$(basename "${tmpDir}")" "${ROOT}/hack/download.sh"
|
||||
)
|
||||
mkdir -p "$(dirname "${baselineDir}")"
|
||||
rm -rf "${baselineDir}"
|
||||
mv "${tmpDir}" "${baselineDir}"
|
||||
fi
|
||||
|
||||
run_compatibility "${version}" "${baselineDir}/frps" "${baselineDir}/frpc"
|
||||
done
|
||||
@@ -0,0 +1,309 @@
|
||||
<#
|
||||
.SYNOPSIS
|
||||
kanholec Windows Setup Wizard (Interactive PowerShell Installer)
|
||||
.DESCRIPTION
|
||||
Interactive installer for kanholec with EULA acceptance, directory selection,
|
||||
and optional features (PATH, shortcuts, desktop icon).
|
||||
.PARAMETER Unattended
|
||||
Run in silent mode with defaults.
|
||||
.PARAMETER InstallDir
|
||||
Installation directory (default: $env:ProgramFiles\kanholec).
|
||||
.EXAMPLE
|
||||
.\install.ps1
|
||||
.\install.ps1 -Unattended
|
||||
#>
|
||||
|
||||
param(
|
||||
[switch]$Unattended,
|
||||
[string]$InstallDir = "$env:ProgramFiles\kanholec"
|
||||
)
|
||||
|
||||
$ErrorActionPreference = "Stop"
|
||||
$host.UI.RawUI.WindowTitle = "kanholec Setup Wizard"
|
||||
$env:KANHOLEC_VERSION = "0.62.0"
|
||||
|
||||
function Write-Banner {
|
||||
Clear-Host
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host " kanholec - kanhole Client v$env:KANHOLEC_VERSION" -ForegroundColor Cyan
|
||||
Write-Host " Setup Wizard" -ForegroundColor Cyan
|
||||
Write-Host "========================================" -ForegroundColor Cyan
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
function Show-License {
|
||||
param([string]$Path)
|
||||
Write-Banner
|
||||
Write-Host "License Agreement" -ForegroundColor Yellow
|
||||
Write-Host "==================" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
if (Test-Path $Path) {
|
||||
Get-Content $Path | ForEach-Object { Write-Host $_ }
|
||||
} else {
|
||||
Write-Host "kanholec is licensed under the Apache License, Version 2.0."
|
||||
Write-Host "See http://www.apache.org/licenses/LICENSE-2.0"
|
||||
}
|
||||
Write-Host ""
|
||||
if (-not $Unattended) {
|
||||
$choice = $host.UI.PromptForChoice(
|
||||
"License Agreement",
|
||||
"Do you accept the terms?",
|
||||
[System.Management.Automation.Host.ChoiceDescription[]]@(
|
||||
@{Label="&Agree"; Help="I accept the agreement"},
|
||||
@{Label="&Decline"; Help="I do not accept the agreement"}
|
||||
),
|
||||
1
|
||||
)
|
||||
if ($choice -ne 0) {
|
||||
Write-Host "Setup cancelled. You must accept the license to install." -ForegroundColor Red
|
||||
exit 1
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
function Select-Directory {
|
||||
param([string]$DefaultPath)
|
||||
Write-Banner
|
||||
Write-Host "Installation Directory" -ForegroundColor Yellow
|
||||
Write-Host "======================" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
Write-Host "Default: $DefaultPath" -ForegroundColor Gray
|
||||
Write-Host ""
|
||||
|
||||
if (-not $Unattended) {
|
||||
$input = Read-Host "Press Enter to accept, or type a custom path"
|
||||
if ($input -ne "") {
|
||||
return $input
|
||||
}
|
||||
}
|
||||
return $DefaultPath
|
||||
}
|
||||
|
||||
function Select-Components {
|
||||
Write-Banner
|
||||
Write-Host "Select Components" -ForegroundColor Yellow
|
||||
Write-Host "=================" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
$components = @{
|
||||
"Main" = @{Desc="kanholec binary and config"; Default=$true}
|
||||
"Path" = @{Desc="Add to system PATH"; Default=$true}
|
||||
"StartMenu" = @{Desc="Start Menu shortcuts"; Default=$true}
|
||||
"Desktop" = @{Desc="Desktop shortcut"; Default=$false}
|
||||
}
|
||||
|
||||
$selected = @{}
|
||||
$i = 1
|
||||
$choices = @()
|
||||
|
||||
foreach ($key in $components.Keys) {
|
||||
$c = $components[$key]
|
||||
$default = if ($c.Default) { "Y" } else { "N" }
|
||||
$displayDefault = if ($c.Default) { "Yes" } else { "No" }
|
||||
Write-Host " $i. $($c.Desc) [$displayDefault]" -ForegroundColor Gray
|
||||
$choices += @{Key=$key; Default=$default}
|
||||
$i++
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
if (-not $Unattended) {
|
||||
Write-Host "Press Enter for defaults, or type numbers to toggle (e.g. 1,3):" -ForegroundColor Yellow
|
||||
$input = Read-Host
|
||||
if ($input -ne "") {
|
||||
$toggleIndices = $input -split ',' | ForEach-Object { $_.Trim() } | Where-Object { $_ -ne "" }
|
||||
for ($j = 0; $j -lt $choices.Count; $j++) {
|
||||
$idx = $j + 1
|
||||
$isToggled = [string]$idx -in $toggleIndices
|
||||
$c = $components[$choices[$j].Key]
|
||||
$default = $c.Default
|
||||
$selected[$choices[$j].Key] = if ($isToggled) { -not $default } else { $default }
|
||||
}
|
||||
} else {
|
||||
foreach ($ch in $choices) { $selected[$ch.Key] = $components[$ch.Key].Default }
|
||||
}
|
||||
} else {
|
||||
foreach ($ch in $choices) { $selected[$ch.Key] = $components[$ch.Key].Default }
|
||||
}
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Selected components:" -ForegroundColor Green
|
||||
foreach ($key in $selected.Keys) {
|
||||
$mark = if ($selected[$key]) { "[X]" } else { "[ ]" }
|
||||
Write-Host " $mark $($components[$key].Desc)" -ForegroundColor $(if ($selected[$key]) { "Green" } else { "DarkGray" })
|
||||
}
|
||||
Start-Sleep 1
|
||||
|
||||
return $selected
|
||||
}
|
||||
|
||||
function Install-Frpc {
|
||||
param(
|
||||
[string]$InstallDir,
|
||||
[hashtable]$Components,
|
||||
[string]$BinarySource
|
||||
)
|
||||
|
||||
Write-Banner
|
||||
Write-Host "Installing kanholec..." -ForegroundColor Yellow
|
||||
Write-Host "==================" -ForegroundColor Yellow
|
||||
Write-Host ""
|
||||
|
||||
# Create directories
|
||||
Write-Host " Creating directories..." -NoNewline
|
||||
New-Item -Path $InstallDir -ItemType Directory -Force | Out-Null
|
||||
$configDir = "$env:ProgramData\kanholec"
|
||||
New-Item -Path $configDir -ItemType Directory -Force | Out-Null
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
|
||||
# Copy binary
|
||||
Write-Host " Copying kanholec.exe..." -NoNewline
|
||||
if ($BinarySource -and (Test-Path $BinarySource)) {
|
||||
Copy-Item -Path $BinarySource -Destination "$InstallDir\kanholec.exe" -Force
|
||||
} else {
|
||||
# Download from GitHub
|
||||
$url = "https://github.com/fatedier/kanhole/releases/download/v$env:KANHOLEC_VERSION/kanhole_${env:KANHOLEC_VERSION}_windows_amd64.zip"
|
||||
$zipPath = "$env:TEMP\kanholec.zip"
|
||||
Write-Host ""
|
||||
Write-Host " Downloading from GitHub..." -NoNewline
|
||||
Invoke-WebRequest -Uri $url -OutFile $zipPath -UseBasicParsing
|
||||
Expand-Archive -Path $zipPath -DestinationPath "$env:TEMP\kanholec" -Force
|
||||
$exePath = Get-ChildItem -Path "$env:TEMP\kanholec" -Recurse -Filter "kanholec.exe" | Select-Object -First 1 -ExpandProperty FullName
|
||||
if (-not $exePath) { throw "kanholec.exe not found in archive" }
|
||||
Copy-Item -Path $exePath -Destination "$InstallDir\kanholec.exe" -Force
|
||||
}
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
|
||||
# Config file
|
||||
Write-Host " Creating config..." -NoNewline
|
||||
$configFile = "$configDir\kanholec.toml"
|
||||
if (-not (Test-Path $configFile)) {
|
||||
@"
|
||||
# kanholec configuration
|
||||
# Edit this file to configure your kanhole client.
|
||||
|
||||
serverAddr = "127.0.0.1"
|
||||
serverPort = 7000
|
||||
auth.token = ""
|
||||
"@ | Out-File -FilePath $configFile -Encoding utf8
|
||||
}
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
|
||||
# PATH
|
||||
if ($Components["Path"]) {
|
||||
Write-Host " Adding to PATH..." -NoNewline
|
||||
$currentPath = [Environment]::GetEnvironmentVariable("PATH", "Machine")
|
||||
if ($currentPath -notlike "*$InstallDir*") {
|
||||
$newPath = "$currentPath;$InstallDir"
|
||||
[Environment]::SetEnvironmentVariable("PATH", $newPath, "Machine")
|
||||
$env:PATH = "$env:PATH;$InstallDir"
|
||||
}
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Start Menu
|
||||
if ($Components["StartMenu"]) {
|
||||
Write-Host " Creating Start Menu shortcuts..." -NoNewline
|
||||
$startMenu = "$([Environment]::GetFolderPath('CommonStartMenu'))\Programs\kanholec"
|
||||
New-Item -Path $startMenu -ItemType Directory -Force | Out-Null
|
||||
$wshell = New-Object -ComObject WScript.Shell
|
||||
$shortcut = $wshell.CreateShortcut("$startMenu\kanholec.lnk")
|
||||
$shortcut.TargetPath = "$InstallDir\kanholec.exe"
|
||||
$shortcut.Save()
|
||||
$unlink = $wshell.CreateShortcut("$startMenu\Uninstall kanholec.lnk")
|
||||
$unlink.TargetPath = "$InstallDir\uninstall.ps1"
|
||||
$unlink.Save()
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Desktop
|
||||
if ($Components["Desktop"]) {
|
||||
Write-Host " Creating desktop shortcut..." -NoNewline
|
||||
$desktop = [Environment]::GetFolderPath('Desktop')
|
||||
$wshell = New-Object -ComObject WScript.Shell
|
||||
$shortcut = $wshell.CreateShortcut("$desktop\kanholec.lnk")
|
||||
$shortcut.TargetPath = "$InstallDir\kanholec.exe"
|
||||
$shortcut.Save()
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
}
|
||||
|
||||
# Write uninstall script
|
||||
Write-Host " Creating uninstaller..." -NoNewline
|
||||
@"
|
||||
param([switch]`$Silent)
|
||||
`$InstallDir = "$InstallDir"
|
||||
if (-not `$Silent) {
|
||||
`$choice = `$host.UI.PromptForChoice("Uninstall kanholec", "Remove kanholec from this computer?", @(@{Label="&Yes"; Help=""}, @{Label="&No"; Help=""}), 1)
|
||||
if (`$choice -ne 0) { exit }
|
||||
}
|
||||
# Remove files
|
||||
Remove-Item -Path "`$InstallDir\kanholec.exe" -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path "`$InstallDir\uninstall.ps1" -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path "`$InstallDir" -Force -ErrorAction SilentlyContinue
|
||||
# Remove shortcuts
|
||||
Remove-Item -Path "$([Environment]::GetFolderPath('CommonStartMenu'))\Programs\kanholec" -Recurse -Force -ErrorAction SilentlyContinue
|
||||
Remove-Item -Path "$([Environment]::GetFolderPath('Desktop'))\kanholec.lnk" -Force -ErrorAction SilentlyContinue
|
||||
Write-Host "kanholec has been uninstalled."
|
||||
"@ | Out-File -FilePath "$InstallDir\uninstall.ps1" -Encoding utf8
|
||||
Write-Host " OK" -ForegroundColor Green
|
||||
|
||||
Write-Host ""
|
||||
Write-Host "Installation complete!" -ForegroundColor Green
|
||||
Write-Host ""
|
||||
Write-Host "Quick start:" -ForegroundColor Yellow
|
||||
Write-Host " kanholec --help"
|
||||
Write-Host " kanholec --server-config http://your-server:7500/admin/api/kanholec/proxy-config/YOUR_KEY"
|
||||
Write-Host " kanholec -c $configFile"
|
||||
Write-Host ""
|
||||
Write-Host "Auth with provisioning token:" -ForegroundColor Yellow
|
||||
Write-Host " kanholec auth login --server http://your-server:7500 --client-name myclient"
|
||||
Write-Host ""
|
||||
}
|
||||
|
||||
# ===== Main =====
|
||||
|
||||
$scriptDir = Split-Path -Parent $PSCommandPath
|
||||
$binarySource = Join-Path $scriptDir "..\bin\kanholec-windows-amd64.exe"
|
||||
if (-not (Test-Path $binarySource)) {
|
||||
$binarySource = Join-Path $scriptDir "kanholec-windows-amd64.exe"
|
||||
}
|
||||
if (-not (Test-Path $binarySource)) {
|
||||
$binarySource = $null
|
||||
}
|
||||
|
||||
# Check admin rights
|
||||
$isAdmin = ([Security.Principal.WindowsPrincipal][Security.Principal.WindowsIdentity]::GetCurrent()).IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
|
||||
if (-not $isAdmin) {
|
||||
Write-Host "This installer requires administrator privileges." -ForegroundColor Red
|
||||
Write-Host "Please run PowerShell as Administrator and try again." -ForegroundColor Yellow
|
||||
if (-not $Unattended) {
|
||||
Start-Sleep 2
|
||||
# Self-elevate
|
||||
$script = "-NoProfile -ExecutionPolicy Bypass -File `"$PSCommandPath`""
|
||||
Start-Process powershell -Verb RunAs -ArgumentList $script
|
||||
}
|
||||
exit 1
|
||||
}
|
||||
|
||||
if ($Unattended) {
|
||||
Install-Frpc -InstallDir $InstallDir -Components @{
|
||||
Main=$true; Path=$true; StartMenu=$true; Desktop=$false
|
||||
} -BinarySource $binarySource
|
||||
} else {
|
||||
Show-License -Path (Join-Path $scriptDir "license.txt")
|
||||
$dir = Select-Directory -DefaultPath $InstallDir
|
||||
$components = Select-Components
|
||||
Install-Frpc -InstallDir $dir -Components $components -BinarySource $binarySource
|
||||
Write-Host ""
|
||||
$runNow = $host.UI.PromptForChoice(
|
||||
"Setup Complete",
|
||||
"Run kanholec now?",
|
||||
[System.Management.Automation.Host.ChoiceDescription[]]@(
|
||||
@{Label="&Yes"; Help="Run kanholec"},
|
||||
@{Label="&No"; Help="Close"}
|
||||
),
|
||||
0
|
||||
)
|
||||
if ($runNow -eq 0) {
|
||||
Start-Process "$dir\kanholec.exe"
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,10 @@
|
||||
# kanholec configuration
|
||||
# Generated by kanholec installer
|
||||
# Edit this file to configure your kanhole client.
|
||||
|
||||
serverAddr = "127.0.0.1"
|
||||
serverPort = 7000
|
||||
auth.token = ""
|
||||
|
||||
# Uncomment and set the URL to auto-fetch config from kanholes:
|
||||
# configURL = "http://your-server:7500/admin/api/kanholec/proxy-config/YOUR_KEY"
|
||||
@@ -0,0 +1,148 @@
|
||||
; kanholec Windows Installer (NSIS)
|
||||
; Build: makensis kanholec.nsi
|
||||
; Requires: NSIS 3.x (https://nsis.sourceforge.io)
|
||||
|
||||
!define PRODUCT_NAME "kanholec"
|
||||
!define PRODUCT_VERSION "0.62.0"
|
||||
!define PRODUCT_PUBLISHER "kanhole Contributors"
|
||||
!define PRODUCT_WEB_SITE "https://github.com/kanhole/kanhole"
|
||||
!define PRODUCT_DIR "$PROGRAMFILES64\${PRODUCT_NAME}"
|
||||
!define PRODUCT_UNINSTALL_KEY "Software\Microsoft\Windows\CurrentVersion\Uninstall\${PRODUCT_NAME}"
|
||||
|
||||
Name "${PRODUCT_NAME} ${PRODUCT_VERSION}"
|
||||
OutFile "..\..\bin\kanholec-${PRODUCT_VERSION}-setup.exe"
|
||||
InstallDir "${PRODUCT_DIR}"
|
||||
InstallDirRegKey HKLM "${PRODUCT_UNINSTALL_KEY}" "InstallLocation"
|
||||
RequestExecutionLevel admin
|
||||
|
||||
SetCompressor lzma
|
||||
|
||||
; Pages (setup wizard)
|
||||
Page license
|
||||
Page components
|
||||
Page directory
|
||||
Page instfiles
|
||||
Page custom setFinishPage
|
||||
|
||||
; Uninstaller pages
|
||||
UninstPage uninstConfirm
|
||||
UninstPage instfiles
|
||||
|
||||
; License data
|
||||
LicenseData "..\..\packaging\windows\license.txt"
|
||||
|
||||
; Languages
|
||||
!insertmacro MUI_LANGUAGE "English"
|
||||
|
||||
; Include modern UI
|
||||
!include "MUI2.nsh"
|
||||
!include "FileFunc.nsh"
|
||||
!include "WinMessages.nsh"
|
||||
!include "LogicLib.nsh"
|
||||
|
||||
Var FinishPage
|
||||
Var RunNowCheckbox
|
||||
Var ShowReadmeCheckbox
|
||||
Var RunNowState
|
||||
Var ReadmeState
|
||||
|
||||
Function setFinishPage
|
||||
nsDialogs::Create 1018
|
||||
Pop $FinishPage
|
||||
|
||||
${NSD_CreateLabel} 0 0 100% 24 "Setup Complete"
|
||||
Pop $0
|
||||
|
||||
${NSD_CreateCheckbox} 0 30 100% 12 "Run kanholec now"
|
||||
Pop $RunNowCheckbox
|
||||
${NSD_Check} $RunNowCheckbox
|
||||
|
||||
${NSD_CreateCheckbox} 0 46 100% 12 "Open config directory"
|
||||
Pop $ShowReadmeCheckbox
|
||||
|
||||
nsDialogs::Show
|
||||
FunctionEnd
|
||||
|
||||
Function .onGUIEnd
|
||||
${NSD_GetState} $RunNowCheckbox $RunNowState
|
||||
${NSD_GetState} $ShowReadmeCheckbox $ReadmeState
|
||||
|
||||
${If} $RunNowState == ${BST_CHECKED}
|
||||
ExecShell "open" "$INSTDIR\kanholec.exe"
|
||||
${EndIf}
|
||||
|
||||
${If} $ReadmeState == ${BST_CHECKED}
|
||||
ExecShell "open" "$PROGRAMDATA\kanholec"
|
||||
${EndIf}
|
||||
FunctionEnd
|
||||
|
||||
Section "kanholec (required)" SEC_KANHOLEC
|
||||
SectionIn RO
|
||||
SetOutPath "$INSTDIR"
|
||||
File "..\..\bin\kanholec-windows-amd64.exe"
|
||||
Rename "$INSTDIR\kanholec-windows-amd64.exe" "$INSTDIR\kanholec.exe"
|
||||
|
||||
; Config directory
|
||||
CreateDirectory "$PROGRAMDATA\kanholec"
|
||||
SetOutPath "$PROGRAMDATA\kanholec"
|
||||
File "/oname=kanholec.toml" "..\..\packaging\windows\kanholec.default.toml"
|
||||
|
||||
WriteUninstaller "$INSTDIR\uninstall.exe"
|
||||
|
||||
; Registry for uninstaller
|
||||
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "DisplayName" "${PRODUCT_NAME}"
|
||||
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "UninstallString" "$INSTDIR\uninstall.exe"
|
||||
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "DisplayVersion" "${PRODUCT_VERSION}"
|
||||
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "Publisher" "${PRODUCT_PUBLISHER}"
|
||||
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "URLInfoAbout" "${PRODUCT_WEB_SITE}"
|
||||
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "DisplayIcon" "$INSTDIR\kanholec.exe,0"
|
||||
WriteRegStr HKLM "${PRODUCT_UNINSTALL_KEY}" "InstallLocation" "$INSTDIR"
|
||||
WriteRegDWORD HKLM "${PRODUCT_UNINSTALL_KEY}" "NoModify" 1
|
||||
WriteRegDWORD HKLM "${PRODUCT_UNINSTALL_KEY}" "NoRepair" 1
|
||||
SectionEnd
|
||||
|
||||
Section "Start Menu Shortcuts" SEC_SHORTCUTS
|
||||
CreateDirectory "$SMPROGRAMS\${PRODUCT_NAME}"
|
||||
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\kanholec.lnk" "$INSTDIR\kanholec.exe" "" "$INSTDIR\kanholec.exe" 0
|
||||
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Config Directory.lnk" "$PROGRAMDATA\kanholec"
|
||||
CreateShortCut "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk" "$INSTDIR\uninstall.exe"
|
||||
SectionEnd
|
||||
|
||||
Section "Add to PATH" SEC_PATH
|
||||
EnVar::AddValue "PATH" "$INSTDIR"
|
||||
Pop $0
|
||||
SectionEnd
|
||||
|
||||
Section "Desktop Shortcut" SEC_DESKTOP
|
||||
CreateShortCut "$DESKTOP\kanholec.lnk" "$INSTDIR\kanholec.exe" "" "$INSTDIR\kanholec.exe" 0
|
||||
SectionEnd
|
||||
|
||||
; Descriptions
|
||||
LangString DESC_SEC_KANHOLEC ${LANG_ENGLISH} "kanholec binary and default configuration."
|
||||
LangString DESC_SEC_SHORTCUTS ${LANG_ENGLISH} "Start menu shortcuts for kanholec."
|
||||
LangString DESC_SEC_PATH ${LANG_ENGLISH} "Add kanholec installation directory to system PATH."
|
||||
LangString DESC_SEC_DESKTOP ${LANG_ENGLISH} "Create a desktop shortcut for kanholec."
|
||||
|
||||
!insertmacro MUI_FUNCTION_DESCRIPTION_BEGIN
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_KANHOLEC} $(DESC_SEC_KANHOLEC)
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_SHORTCUTS} $(DESC_SEC_SHORTCUTS)
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_PATH} $(DESC_SEC_PATH)
|
||||
!insertmacro MUI_DESCRIPTION_TEXT ${SEC_DESKTOP} $(DESC_SEC_DESKTOP)
|
||||
!insertmacro MUI_FUNCTION_DESCRIPTION_END
|
||||
|
||||
Section "Uninstall"
|
||||
Delete "$INSTDIR\kanholec.exe"
|
||||
Delete "$INSTDIR\uninstall.exe"
|
||||
RMDir "$INSTDIR"
|
||||
|
||||
Delete "$SMPROGRAMS\${PRODUCT_NAME}\kanholec.lnk"
|
||||
Delete "$SMPROGRAMS\${PRODUCT_NAME}\Config Directory.lnk"
|
||||
Delete "$SMPROGRAMS\${PRODUCT_NAME}\Uninstall.lnk"
|
||||
RMDir "$SMPROGRAMS\${PRODUCT_NAME}"
|
||||
|
||||
Delete "$DESKTOP\kanholec.lnk"
|
||||
|
||||
EnVar::DeleteValue "PATH" "$INSTDIR"
|
||||
|
||||
DeleteRegKey HKLM "${PRODUCT_UNINSTALL_KEY}"
|
||||
SectionEnd
|
||||
@@ -0,0 +1,47 @@
|
||||
<?xml version="1.0" encoding="utf-8"?>
|
||||
<Wix xmlns="http://wixtoolset.org/schemas/v3/wxs">
|
||||
<Product
|
||||
Name="kanholec"
|
||||
Id="*"
|
||||
UpgradeCode="A1B2C3D4-E5F6-7890-ABCD-EF1234567890"
|
||||
Language="1033"
|
||||
Codepage="1252"
|
||||
Version="0.62.0"
|
||||
Manufacturer="kanhole Contributors">
|
||||
|
||||
<Package
|
||||
InstallerVersion="200"
|
||||
Compressed="yes"
|
||||
InstallScope="perMachine"
|
||||
Description="kanhole Client"
|
||||
Comments="kanholec is the client component of kanhole - a fast reverse proxy." />
|
||||
|
||||
<Media Id="1" Cabinet="kanholec.cab" EmbedCab="yes" />
|
||||
|
||||
<Directory Id="TARGETDIR" Name="SourceDir">
|
||||
<Directory Id="ProgramFiles64Folder">
|
||||
<Directory Id="INSTALLDIR" Name="kanholec">
|
||||
<Component Id="FrpcBinary" Guid="E1B2C3D4-E5F6-7890-ABCD-EF1234567894">
|
||||
<File Id="FrpcExe" Name="kanholec.exe" DiskId="1" Source="bin/kanholec-windows-amd64.exe" KeyPath="yes" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<Directory Id="ProgramMenuFolder">
|
||||
<Directory Id="ApplicationProgramsFolder" Name="kanholec">
|
||||
<Component Id="StartMenuShortcuts" Guid="A2B2C3D4-E5F6-7890-ABCD-EF1234567892">
|
||||
<Shortcut Id="FrpcShortcut" Name="kanholec" Target="[INSTALLDIR]kanholec.exe" WorkingDirectory="INSTALLDIR" Description="kanhole client" />
|
||||
<Shortcut Id="UninstallShortcut" Name="Uninstall kanholec" Target="[System64Folder]msiexec.exe" Arguments="/x [ProductCode]" />
|
||||
<RemoveFolder Id="RemoveApplicationProgramsFolder" On="uninstall" />
|
||||
<RegistryValue Root="HKCU" Key="Software\kanhole\kanholec" Name="installed" Type="integer" Value="1" />
|
||||
</Component>
|
||||
</Directory>
|
||||
</Directory>
|
||||
</Directory>
|
||||
|
||||
<Feature Id="Main" Title="kanholec" Description="kanhole client binary" Level="1">
|
||||
<ComponentRef Id="FrpcBinary" />
|
||||
<ComponentRef Id="StartMenuShortcuts" />
|
||||
</Feature>
|
||||
</Product>
|
||||
</Wix>
|
||||
@@ -0,0 +1,33 @@
|
||||
{\rtf1\ansi\ansicpg1252\deff0\deflang1033\deftab720{\fonttbl{\f0\fnil\fcharset0 Arial;}{\f1\fmodern\fcharset0 Courier New;}}
|
||||
{\colortbl\red0\green0\blue0;\red255\green255\blue255;}
|
||||
\viewkind4\uc1\pard\qc\b\fs28 kanholec\par
|
||||
\b0\fs20 Version 0.1.0\par
|
||||
\pard\sa200\sl240\slmult1\fs20\par
|
||||
\pard\sa200\sl240\slmult1\b\fs24 License Agreement\b0\fs20\par
|
||||
\pard\sa200\sl240\slmult1\par
|
||||
\pard\sa200\sl240\slmult1\fs20 This is a legal agreement between you (the "Licensee") and the kanhole project contributors ("Licensor"). By installing or using kanholec, you agree to the following terms and conditions.\par
|
||||
\pard\sa200\sl240\slmult1\par
|
||||
\pard\sa200\sl240\slmult1\b 1. Grant of License\b0\par
|
||||
\pard\sa200\sl240\slmult1 The kanholec software is 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:\par
|
||||
\pard\sa200\sl240\slmult1 http://www.apache.org/licenses/LICENSE-2.0\par
|
||||
\pard\sa200\sl240\slmult1\par
|
||||
\pard\sa200\sl240\slmult1\b 2. Distribution\b0\par
|
||||
\pard\sa200\sl240\slmult1 You may reproduce and distribute copies of the work or derivative works thereof in any medium, with or without modifications, and in source or object form, provided that you meet the following conditions:\par
|
||||
\pard\sa200\sl240\slmult1 - You must give any other recipients of the work or derivative works a copy of this License\par
|
||||
\pard\sa200\sl240\slmult1 - You must cause any modified files to carry prominent notices stating that you changed the files\par
|
||||
\pard\sa200\sl240\slmult1 - You must retain all copyright, patent, trademark, and attribution notices\par
|
||||
\pard\sa200\sl240\slmult1\par
|
||||
\pard\sa200\sl240\slmult1\b 3. Disclaimer of Warranty\b0\par
|
||||
\pard\sa200\sl240\slmult1 Unless required by applicable law or agreed to in writing, the software is provided on an "AS IS" basis, WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.\par
|
||||
\pard\sa200\sl240\slmult1\par
|
||||
\pard\sa200\sl240\slmult1\b 4. Limitation of Liability\b0\par
|
||||
\pard\sa200\sl240\slmult1 In no event and under no legal theory shall the authors or copyright holders be liable for any damages arising from the use of the software.\par
|
||||
\pard\sa200\sl240\slmult1\par
|
||||
\pard\sa200\sl240\slmult1\b 5. Termination\b0\par
|
||||
\pard\sa200\sl240\slmult1 This license automatically terminates if you violate any of its terms. Upon termination, you must destroy all copies of the software.\par
|
||||
\pard\sa200\sl240\slmult1\par
|
||||
\pard\sa200\sl240\slmult1\b 6. Governing Law\b0\par
|
||||
\pard\sa200\sl240\slmult1 This license shall be governed by the laws applicable to the Licensor's jurisdiction.\par
|
||||
\pard\sa200\sl240\slmult1\par
|
||||
\pard\sa200\sl240\slmult1 By clicking "I Agree" or installing the software, you acknowledge that you have read this agreement, understand it, and agree to be bound by its terms.\par
|
||||
}
|
||||
@@ -0,0 +1,34 @@
|
||||
kanholec - kanhole Client (version 0.1.0)
|
||||
============================================
|
||||
Copyright (c) kanhole project contributors.
|
||||
Licensed under the Apache License, Version 2.0.
|
||||
|
||||
TERMS AND CONDITIONS
|
||||
--------------------
|
||||
|
||||
1. Grant of License
|
||||
frpc is licensed under the Apache License, Version 2.0 (the "License").
|
||||
You may not use this file except in compliance with the License.
|
||||
A copy of the License is available at:
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
2. Distribution
|
||||
You may reproduce and distribute copies of the work or derivative works
|
||||
in any medium, with or without modifications, and in source or object
|
||||
form, provided that you meet the following conditions:
|
||||
- You must give any other recipients a copy of this License
|
||||
- You must cause any modified files to carry prominent notices
|
||||
- You must retain all copyright, patent, trademark notices
|
||||
|
||||
3. Disclaimer of Warranty
|
||||
Unless required by applicable law, the software is provided "AS IS",
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, express or implied.
|
||||
|
||||
4. Limitation of Liability
|
||||
In no event shall the authors be liable for any damages arising from
|
||||
the use of this software.
|
||||
|
||||
5. Termination
|
||||
This license automatically terminates if you violate its terms.
|
||||
|
||||
By installing this software, you accept these terms.
|
||||
+2
-2
@@ -18,8 +18,8 @@ import (
|
||||
"context"
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
)
|
||||
|
||||
type Setter interface {
|
||||
|
||||
+3
-3
@@ -29,9 +29,9 @@ import (
|
||||
"golang.org/x/oauth2"
|
||||
"golang.org/x/oauth2/clientcredentials"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/config/v1/validation"
|
||||
"kanhole/pkg/msg"
|
||||
)
|
||||
|
||||
// createOIDCHTTPClient creates an HTTP client with custom TLS and proxy configuration for OIDC token requests
|
||||
|
||||
@@ -12,9 +12,9 @@ import (
|
||||
"github.com/coreos/go-oidc/v3/oidc"
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
"github.com/fatedier/frp/pkg/auth"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"kanhole/pkg/auth"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
)
|
||||
|
||||
type mockTokenVerifier struct{}
|
||||
|
||||
+1
-1
@@ -15,7 +15,7 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"kanhole/pkg/msg"
|
||||
)
|
||||
|
||||
var AlwaysPassVerifier = &alwaysPass{}
|
||||
|
||||
+3
-3
@@ -19,9 +19,9 @@ import (
|
||||
"slices"
|
||||
"time"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/util/util"
|
||||
)
|
||||
|
||||
type TokenAuthSetterVerifier struct {
|
||||
|
||||
+6
-6
@@ -22,9 +22,9 @@ import (
|
||||
"github.com/spf13/cobra"
|
||||
"github.com/spf13/pflag"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"kanhole/pkg/config/types"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/config/v1/validation"
|
||||
)
|
||||
|
||||
// WordSepNormalizeFunc changes all flags that contain "_" separators
|
||||
@@ -154,8 +154,8 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi
|
||||
}
|
||||
|
||||
if !options.sshMode {
|
||||
cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "frp server's address")
|
||||
cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "frp server's port")
|
||||
cmd.PersistentFlags().StringVarP(&c.ServerAddr, "server_addr", "s", "127.0.0.1", "kanhole server's address")
|
||||
cmd.PersistentFlags().IntVarP(&c.ServerPort, "server_port", "P", 7000, "kanhole server's port")
|
||||
cmd.PersistentFlags().StringVarP(&c.Transport.Protocol, "protocol", "p", "tcp",
|
||||
fmt.Sprintf("optional values are %v", validation.SupportedTransportProtocols))
|
||||
cmd.PersistentFlags().StringVarP(&c.Log.Level, "log_level", "", "info", "log level")
|
||||
@@ -248,7 +248,7 @@ func RegisterServerConfigFlags(cmd *cobra.Command, c *v1.ServerConfig, opts ...R
|
||||
cmd.PersistentFlags().StringVarP(&c.SubDomainHost, "subdomain_host", "", "", "subdomain host")
|
||||
cmd.PersistentFlags().VarP(&PortsRangeSliceFlag{V: &c.AllowPorts}, "allow_ports", "", "allow ports")
|
||||
cmd.PersistentFlags().Int64VarP(&c.MaxPortsPerClient, "max_ports_per_client", "", 0, "max ports per client")
|
||||
cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "frps tls only")
|
||||
cmd.PersistentFlags().BoolVarP(&c.Transport.TLS.Force, "tls_only", "", false, "kanhole tls only")
|
||||
|
||||
webServerTLS := v1.TLSConfig{}
|
||||
cmd.PersistentFlags().StringVarP(&webServerTLS.CertFile, "dashboard_tls_cert_file", "", "", "dashboard tls cert file")
|
||||
|
||||
@@ -23,8 +23,8 @@ import (
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
legacyauth "github.com/fatedier/frp/pkg/auth/legacy"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
legacyauth "kanhole/pkg/auth/legacy"
|
||||
"kanhole/pkg/util/util"
|
||||
)
|
||||
|
||||
// ClientCommonConf is the configuration parsed from ini.
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"kanhole/pkg/config/types"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func Convert_ClientCommonConf_To_v1(conf *ClientCommonConf) *v1.ClientCommonConfig {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
"kanhole/pkg/config/types"
|
||||
)
|
||||
|
||||
type ProxyType string
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
"gopkg.in/ini.v1"
|
||||
|
||||
legacyauth "github.com/fatedier/frp/pkg/auth/legacy"
|
||||
legacyauth "kanhole/pkg/auth/legacy"
|
||||
)
|
||||
|
||||
type HTTPPluginOptions struct {
|
||||
@@ -131,7 +131,7 @@ type ServerCommonConf struct {
|
||||
|
||||
// SubDomainHost specifies the domain that will be attached to sub-domains
|
||||
// requested by the client when using Vhost proxying. For example, if this
|
||||
// value is set to "frps.com" and the client requested the subdomain
|
||||
// value is set to "kanhole.com" and the client requested the subdomain
|
||||
// "test", the resulting URL would be "test.frps.com". By default, this
|
||||
// value is "".
|
||||
SubDomainHost string `ini:"subdomain_host" json:"subdomain_host"`
|
||||
|
||||
@@ -46,7 +46,7 @@ func GetValues() *Values {
|
||||
}
|
||||
|
||||
func RenderContent(in []byte) (out []byte, err error) {
|
||||
tmpl, errRet := template.New("frp").Parse(string(in))
|
||||
tmpl, errRet := template.New("kanhole").Parse(string(in))
|
||||
if errRet != nil {
|
||||
err = errRet
|
||||
return
|
||||
|
||||
+7
-7
@@ -30,12 +30,12 @@ import (
|
||||
"k8s.io/apimachinery/pkg/util/sets"
|
||||
"k8s.io/apimachinery/pkg/util/yaml"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/legacy"
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"kanhole/pkg/config/legacy"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/config/v1/validation"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/util/jsonx"
|
||||
"kanhole/pkg/util/util"
|
||||
)
|
||||
|
||||
var glbEnvs map[string]string
|
||||
@@ -82,7 +82,7 @@ func DetectLegacyINIFormatFromFile(path string) bool {
|
||||
}
|
||||
|
||||
func RenderWithTemplate(in []byte, values *Values) ([]byte, error) {
|
||||
tmpl, err := template.New("frp").Funcs(template.FuncMap{
|
||||
tmpl, err := template.New("kanhole").Funcs(template.FuncMap{
|
||||
"parseNumberRange": parseNumberRange,
|
||||
"parseNumberRangePair": parseNumberRangePair,
|
||||
}).Parse(string(in))
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
const tomlServerContent = `
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
"slices"
|
||||
"sync"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
type Aggregator struct {
|
||||
|
||||
@@ -20,7 +20,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
// mockProxy creates a TCP proxy config for testing
|
||||
|
||||
@@ -17,7 +17,7 @@ package source
|
||||
import (
|
||||
"sync"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
// baseSource provides shared state and behavior for Source implementations.
|
||||
|
||||
@@ -5,7 +5,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func TestBaseSourceLoadReturnsClonedConfigurers(t *testing.T) {
|
||||
|
||||
@@ -17,7 +17,7 @@ package source
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func cloneConfigurers(
|
||||
|
||||
@@ -14,11 +14,7 @@
|
||||
|
||||
package source
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
)
|
||||
import v1 "kanhole/pkg/config/v1"
|
||||
|
||||
// ConfigSource implements Source for in-memory configuration.
|
||||
// All operations are thread-safe.
|
||||
@@ -39,23 +35,17 @@ func (s *ConfigSource) ReplaceAll(proxies []v1.ProxyConfigurer, visitors []v1.Vi
|
||||
|
||||
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")
|
||||
name, err := validateProxyName(p)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
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")
|
||||
name, err := validateVisitorName(v)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
nextVisitors[name] = v
|
||||
}
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func TestNewConfigSource(t *testing.T) {
|
||||
|
||||
@@ -15,7 +15,7 @@
|
||||
package source
|
||||
|
||||
import (
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
// Source is the interface for configuration sources.
|
||||
|
||||
+102
-120
@@ -20,8 +20,8 @@ import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/util/jsonx"
|
||||
)
|
||||
|
||||
type StoreSourceConfig struct {
|
||||
@@ -43,6 +43,11 @@ var (
|
||||
ErrNotFound = errors.New("not found")
|
||||
)
|
||||
|
||||
const (
|
||||
storeKindProxy = "proxy"
|
||||
storeKindVisitor = "visitor"
|
||||
)
|
||||
|
||||
func NewStoreSource(cfg StoreSourceConfig) (*StoreSource, error) {
|
||||
if cfg.Path == "" {
|
||||
return nil, fmt.Errorf("path is required")
|
||||
@@ -172,79 +177,111 @@ func (s *StoreSource) saveToFileUnlocked() error {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *StoreSource) AddProxy(proxy v1.ProxyConfigurer) error {
|
||||
if proxy == nil {
|
||||
return fmt.Errorf("proxy cannot be nil")
|
||||
func (s *StoreSource) persistOrRollbackUnlocked(rollback func()) error {
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
rollback()
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
// Store map selectors return the target map for generic helpers.
|
||||
func proxyStoreEntries(s *StoreSource) map[string]v1.ProxyConfigurer {
|
||||
return s.proxies
|
||||
}
|
||||
|
||||
func visitorStoreEntries(s *StoreSource) map[string]v1.VisitorConfigurer {
|
||||
return s.visitors
|
||||
}
|
||||
|
||||
// Store entry helpers share mutation, persistence, and rollback for proxy and visitor maps.
|
||||
// T is intentionally limited by callers to v1.ProxyConfigurer or v1.VisitorConfigurer.
|
||||
func addStoreEntry[T any](
|
||||
s *StoreSource,
|
||||
entriesFn func(*StoreSource) map[string]T,
|
||||
kind string,
|
||||
name string,
|
||||
value T,
|
||||
) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
entries := entriesFn(s)
|
||||
if _, exists := entries[name]; exists {
|
||||
return fmt.Errorf("%w: %s %q", ErrAlreadyExists, kind, name)
|
||||
}
|
||||
|
||||
name := proxy.GetBaseConfig().Name
|
||||
entries[name] = value
|
||||
return s.persistOrRollbackUnlocked(func() {
|
||||
delete(entries, name)
|
||||
})
|
||||
}
|
||||
|
||||
func updateStoreEntry[T any](
|
||||
s *StoreSource,
|
||||
entriesFn func(*StoreSource) map[string]T,
|
||||
kind string,
|
||||
name string,
|
||||
value T,
|
||||
) error {
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
entries := entriesFn(s)
|
||||
old, exists := entries[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("%w: %s %q", ErrNotFound, kind, name)
|
||||
}
|
||||
|
||||
entries[name] = value
|
||||
return s.persistOrRollbackUnlocked(func() {
|
||||
entries[name] = old
|
||||
})
|
||||
}
|
||||
|
||||
func removeStoreEntry[T any](
|
||||
s *StoreSource,
|
||||
entriesFn func(*StoreSource) map[string]T,
|
||||
kind string,
|
||||
name string,
|
||||
) error {
|
||||
if name == "" {
|
||||
return fmt.Errorf("proxy name cannot be empty")
|
||||
return fmt.Errorf("%s name cannot be empty", kind)
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
defer s.mu.Unlock()
|
||||
|
||||
if _, exists := s.proxies[name]; exists {
|
||||
return fmt.Errorf("%w: proxy %q", ErrAlreadyExists, name)
|
||||
entries := entriesFn(s)
|
||||
old, exists := entries[name]
|
||||
if !exists {
|
||||
return fmt.Errorf("%w: %s %q", ErrNotFound, kind, name)
|
||||
}
|
||||
|
||||
s.proxies[name] = proxy
|
||||
delete(entries, name)
|
||||
return s.persistOrRollbackUnlocked(func() {
|
||||
entries[name] = old
|
||||
})
|
||||
}
|
||||
|
||||
if err := s.saveToFileUnlocked(); err != nil {
|
||||
delete(s.proxies, name)
|
||||
return fmt.Errorf("failed to persist: %w", err)
|
||||
func (s *StoreSource) AddProxy(proxy v1.ProxyConfigurer) error {
|
||||
name, err := validateProxyName(proxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return nil
|
||||
return addStoreEntry(s, proxyStoreEntries, storeKindProxy, name, proxy)
|
||||
}
|
||||
|
||||
func (s *StoreSource) UpdateProxy(proxy v1.ProxyConfigurer) error {
|
||||
if proxy == nil {
|
||||
return fmt.Errorf("proxy cannot be nil")
|
||||
name, err := validateProxyName(proxy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
return updateStoreEntry(s, proxyStoreEntries, storeKindProxy, name, proxy)
|
||||
}
|
||||
|
||||
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
|
||||
return removeStoreEntry(s, proxyStoreEntries, storeKindProxy, name)
|
||||
}
|
||||
|
||||
func (s *StoreSource) GetProxy(name string) v1.ProxyConfigurer {
|
||||
@@ -259,78 +296,23 @@ func (s *StoreSource) GetProxy(name string) v1.ProxyConfigurer {
|
||||
}
|
||||
|
||||
func (s *StoreSource) AddVisitor(visitor v1.VisitorConfigurer) error {
|
||||
if visitor == nil {
|
||||
return fmt.Errorf("visitor cannot be nil")
|
||||
name, err := validateVisitorName(visitor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
return addStoreEntry(s, visitorStoreEntries, storeKindVisitor, name, visitor)
|
||||
}
|
||||
|
||||
func (s *StoreSource) UpdateVisitor(visitor v1.VisitorConfigurer) error {
|
||||
if visitor == nil {
|
||||
return fmt.Errorf("visitor cannot be nil")
|
||||
name, err := validateVisitorName(visitor)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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
|
||||
return updateStoreEntry(s, visitorStoreEntries, storeKindVisitor, name, visitor)
|
||||
}
|
||||
|
||||
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
|
||||
return removeStoreEntry(s, visitorStoreEntries, storeKindVisitor, name)
|
||||
}
|
||||
|
||||
func (s *StoreSource) GetVisitor(name string) v1.VisitorConfigurer {
|
||||
|
||||
@@ -17,12 +17,13 @@ package source
|
||||
import (
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/util/jsonx"
|
||||
)
|
||||
|
||||
func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||
@@ -59,6 +60,101 @@ func TestStoreSource_AddProxyAndVisitor_DoesNotApplyRuntimeDefaults(t *testing.T
|
||||
require.Empty(gotVisitor.(*v1.XTCPVisitorConfig).Protocol)
|
||||
}
|
||||
|
||||
func TestStoreSource_UpdateAndRemoveProxyAndVisitor(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
storeSource := newTestStoreSource(t)
|
||||
|
||||
proxyCfg := mockProxy("proxy1")
|
||||
visitorCfg := mockVisitor("visitor1")
|
||||
|
||||
require.NoError(storeSource.AddProxy(proxyCfg))
|
||||
require.NoError(storeSource.AddVisitor(visitorCfg))
|
||||
require.ErrorIs(storeSource.AddProxy(proxyCfg), ErrAlreadyExists)
|
||||
require.ErrorIs(storeSource.AddVisitor(visitorCfg), ErrAlreadyExists)
|
||||
require.ErrorContains(storeSource.RemoveProxy(""), "proxy name cannot be empty")
|
||||
require.ErrorContains(storeSource.RemoveVisitor(""), "visitor name cannot be empty")
|
||||
|
||||
updatedProxy := mockProxy("proxy1").(*v1.TCPProxyConfig)
|
||||
updatedProxy.RemotePort = 19090
|
||||
require.NoError(storeSource.UpdateProxy(updatedProxy))
|
||||
require.Equal(19090, storeSource.GetProxy("proxy1").(*v1.TCPProxyConfig).RemotePort)
|
||||
|
||||
updatedVisitor := mockVisitor("visitor1").(*v1.STCPVisitorConfig)
|
||||
updatedVisitor.ServerName = "updated-server"
|
||||
require.NoError(storeSource.UpdateVisitor(updatedVisitor))
|
||||
require.Equal("updated-server", storeSource.GetVisitor("visitor1").(*v1.STCPVisitorConfig).ServerName)
|
||||
|
||||
require.NoError(storeSource.RemoveProxy("proxy1"))
|
||||
require.Nil(storeSource.GetProxy("proxy1"))
|
||||
require.ErrorIs(storeSource.RemoveProxy("proxy1"), ErrNotFound)
|
||||
|
||||
require.NoError(storeSource.RemoveVisitor("visitor1"))
|
||||
require.Nil(storeSource.GetVisitor("visitor1"))
|
||||
require.ErrorIs(storeSource.RemoveVisitor("visitor1"), ErrNotFound)
|
||||
|
||||
require.ErrorIs(storeSource.UpdateProxy(updatedProxy), ErrNotFound)
|
||||
require.ErrorIs(storeSource.UpdateVisitor(updatedVisitor), ErrNotFound)
|
||||
}
|
||||
|
||||
func TestStoreSource_MutationRollsBackOnPersistFailure(t *testing.T) {
|
||||
if runtime.GOOS == "windows" {
|
||||
t.Skip("chmod does not make directories unwritable on Windows")
|
||||
}
|
||||
if os.Getuid() == 0 {
|
||||
t.Skip("chmod does not block writes for uid 0")
|
||||
}
|
||||
|
||||
require := require.New(t)
|
||||
|
||||
dir := t.TempDir()
|
||||
path := filepath.Join(dir, "store.json")
|
||||
storeSource, err := NewStoreSource(StoreSourceConfig{Path: path})
|
||||
require.NoError(err)
|
||||
|
||||
proxyCfg := mockProxy("proxy1")
|
||||
visitorCfg := mockVisitor("visitor1")
|
||||
originalRemotePort := proxyCfg.(*v1.TCPProxyConfig).RemotePort
|
||||
originalServerName := visitorCfg.(*v1.STCPVisitorConfig).ServerName
|
||||
require.NoError(storeSource.AddProxy(proxyCfg))
|
||||
require.NoError(storeSource.AddVisitor(visitorCfg))
|
||||
|
||||
require.NoError(os.Chmod(dir, 0o500))
|
||||
t.Cleanup(func() {
|
||||
_ = os.Chmod(dir, 0o700)
|
||||
})
|
||||
|
||||
requirePersistError := func(err error) {
|
||||
t.Helper()
|
||||
require.Error(err)
|
||||
require.ErrorContains(err, "failed to persist")
|
||||
require.NotErrorIs(err, ErrAlreadyExists)
|
||||
require.NotErrorIs(err, ErrNotFound)
|
||||
}
|
||||
|
||||
requirePersistError(storeSource.AddProxy(mockProxy("proxy2")))
|
||||
require.Nil(storeSource.GetProxy("proxy2"))
|
||||
|
||||
updatedProxy := mockProxy("proxy1").(*v1.TCPProxyConfig)
|
||||
updatedProxy.RemotePort = 19090
|
||||
requirePersistError(storeSource.UpdateProxy(updatedProxy))
|
||||
require.Equal(originalRemotePort, storeSource.GetProxy("proxy1").(*v1.TCPProxyConfig).RemotePort)
|
||||
|
||||
requirePersistError(storeSource.RemoveProxy("proxy1"))
|
||||
require.NotNil(storeSource.GetProxy("proxy1"))
|
||||
|
||||
requirePersistError(storeSource.AddVisitor(mockVisitor("visitor2")))
|
||||
require.Nil(storeSource.GetVisitor("visitor2"))
|
||||
|
||||
updatedVisitor := mockVisitor("visitor1").(*v1.STCPVisitorConfig)
|
||||
updatedVisitor.ServerName = "updated-server"
|
||||
requirePersistError(storeSource.UpdateVisitor(updatedVisitor))
|
||||
require.Equal(originalServerName, storeSource.GetVisitor("visitor1").(*v1.STCPVisitorConfig).ServerName)
|
||||
|
||||
requirePersistError(storeSource.RemoveVisitor("visitor1"))
|
||||
require.NotNil(storeSource.GetVisitor("visitor1"))
|
||||
}
|
||||
|
||||
func TestStoreSource_LoadFromFile_DoesNotApplyRuntimeDefaults(t *testing.T) {
|
||||
require := require.New(t)
|
||||
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func validateProxyName(proxy v1.ProxyConfigurer) (string, 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")
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
|
||||
func validateVisitorName(visitor v1.VisitorConfigurer) (string, 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")
|
||||
}
|
||||
return name, nil
|
||||
}
|
||||
@@ -17,7 +17,7 @@ package config
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"kanhole/pkg/util/util"
|
||||
)
|
||||
|
||||
type NumberPair struct {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"kanhole/pkg/util/util"
|
||||
)
|
||||
|
||||
type ClientConfig struct {
|
||||
@@ -78,8 +78,13 @@ type ClientCommonConfig struct {
|
||||
// Include other config files for proxies.
|
||||
IncludeConfigFiles []string `json:"includes,omitempty"`
|
||||
|
||||
// Store config enables the built-in store source (not configurable via sources list).
|
||||
// Store config enables the built-in store source.
|
||||
Store StoreConfig `json:"store,omitempty"`
|
||||
|
||||
// ConfigURL fetches the full client configuration from a remote URL.
|
||||
// When set, frpc fetches this URL on startup and periodically thereafter,
|
||||
// automatically reloading when the configuration changes.
|
||||
ConfigURL string `json:"configURL,omitempty"`
|
||||
}
|
||||
|
||||
func (c *ClientCommonConfig) Complete() error {
|
||||
|
||||
@@ -17,7 +17,7 @@ package v1
|
||||
import (
|
||||
"maps"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"kanhole/pkg/util/util"
|
||||
)
|
||||
|
||||
type AuthScope string
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"fmt"
|
||||
"reflect"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
"kanhole/pkg/util/jsonx"
|
||||
)
|
||||
|
||||
type DecodeOptions struct {
|
||||
|
||||
@@ -19,10 +19,10 @@ import (
|
||||
"reflect"
|
||||
"slices"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
"github.com/fatedier/frp/pkg/msg"
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"kanhole/pkg/config/types"
|
||||
"kanhole/pkg/msg"
|
||||
"kanhole/pkg/util/jsonx"
|
||||
"kanhole/pkg/util/util"
|
||||
)
|
||||
|
||||
type ProxyTransport struct {
|
||||
|
||||
@@ -19,8 +19,8 @@ import (
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/pkg/util/jsonx"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"kanhole/pkg/util/jsonx"
|
||||
"kanhole/pkg/util/util"
|
||||
)
|
||||
|
||||
const (
|
||||
|
||||
@@ -17,8 +17,8 @@ package v1
|
||||
import (
|
||||
"github.com/samber/lo"
|
||||
|
||||
"github.com/fatedier/frp/pkg/config/types"
|
||||
"github.com/fatedier/frp/pkg/util/util"
|
||||
"kanhole/pkg/config/types"
|
||||
"kanhole/pkg/util/util"
|
||||
)
|
||||
|
||||
type ServerConfig struct {
|
||||
@@ -60,7 +60,7 @@ type ServerConfig struct {
|
||||
TCPMuxPassthrough bool `json:"tcpmuxPassthrough,omitempty"`
|
||||
// SubDomainHost specifies the domain that will be attached to sub-domains
|
||||
// requested by the client when using Vhost proxying. For example, if this
|
||||
// value is set to "frps.com" and the client requested the subdomain
|
||||
// value is set to "kanhole.com" and the client requested the subdomain
|
||||
// "test", the resulting URL would be "test.frps.com".
|
||||
SubDomainHost string `json:"subDomainHost,omitempty"`
|
||||
// Custom404Page specifies a path to a custom 404 page to display. If this
|
||||
|
||||
@@ -0,0 +1,43 @@
|
||||
// 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 validation
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/policy/security"
|
||||
)
|
||||
|
||||
func (v *ConfigValidator) validateAuthTokenSource(token string, tokenSource *v1.ValueSource) error {
|
||||
var errs error
|
||||
// Preserve the previous client/server validation order for joined errors.
|
||||
if token != "" && tokenSource != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
|
||||
}
|
||||
if tokenSource == nil {
|
||||
return errs
|
||||
}
|
||||
|
||||
if tokenSource.Type == "exec" {
|
||||
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
}
|
||||
if err := tokenSource.Validate(); err != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||
}
|
||||
return errs
|
||||
}
|
||||
@@ -0,0 +1,228 @@
|
||||
// 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 validation
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/policy/security"
|
||||
)
|
||||
|
||||
const (
|
||||
tokenSourceConflictErr = "cannot specify both auth.token and auth.tokenSource"
|
||||
tokenSourceExecErr = "unsafe feature \"TokenSourceExec\" is not enabled. To enable it, ensure it is allowed in the configuration or command line flags"
|
||||
invalidFileSourceErr = "invalid auth.tokenSource: file configuration is required when type is 'file'"
|
||||
unsupportedSourceErr = "invalid auth.tokenSource: unsupported value source type: env (only 'file' and 'exec' are supported)"
|
||||
)
|
||||
|
||||
func TestValidateAuthTokenSource(t *testing.T) {
|
||||
for _, tc := range authTokenSourceTestCases() {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
validator := newAuthTokenSourceValidator(tc.unsafeAllowed)
|
||||
err := validator.validateAuthTokenSource(tc.token, tc.tokenSource())
|
||||
requireValidationErrors(t, err, tc.wantErrs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateClientAuthTokenSource(t *testing.T) {
|
||||
for _, tc := range authTokenSourceTestCases() {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
auth := v1.AuthClientConfig{
|
||||
Method: v1.AuthMethodToken,
|
||||
Token: tc.token,
|
||||
TokenSource: tc.tokenSource(),
|
||||
}
|
||||
validator := newAuthTokenSourceValidator(tc.unsafeAllowed)
|
||||
_, err := validator.ValidateClientCommonConfig(validClientConfigWithAuth(auth))
|
||||
requireValidationErrors(t, err, tc.wantErrs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestValidateServerAuthTokenSource(t *testing.T) {
|
||||
for _, tc := range authTokenSourceTestCases() {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
auth := v1.AuthServerConfig{
|
||||
Method: v1.AuthMethodToken,
|
||||
Token: tc.token,
|
||||
TokenSource: tc.tokenSource(),
|
||||
}
|
||||
validator := newAuthTokenSourceValidator(tc.unsafeAllowed)
|
||||
_, err := validator.ValidateServerConfig(validServerConfigWithAuth(auth))
|
||||
requireValidationErrors(t, err, tc.wantErrs)
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
type authTokenSourceTestCase struct {
|
||||
name string
|
||||
token string
|
||||
tokenSource func() *v1.ValueSource
|
||||
unsafeAllowed bool
|
||||
wantErrs []string
|
||||
}
|
||||
|
||||
func authTokenSourceTestCases() []authTokenSourceTestCase {
|
||||
return []authTokenSourceTestCase{
|
||||
{
|
||||
name: "empty token config",
|
||||
tokenSource: nilTokenSource,
|
||||
},
|
||||
{
|
||||
name: "valid file tokenSource",
|
||||
tokenSource: validFileTokenSource,
|
||||
},
|
||||
{
|
||||
name: "literal token without tokenSource",
|
||||
token: "token",
|
||||
tokenSource: nilTokenSource,
|
||||
},
|
||||
{
|
||||
name: "literal token conflicts with file tokenSource",
|
||||
token: "token",
|
||||
tokenSource: validFileTokenSource,
|
||||
wantErrs: []string{tokenSourceConflictErr},
|
||||
},
|
||||
{
|
||||
name: "exec tokenSource requires unsafe feature",
|
||||
tokenSource: validExecTokenSource,
|
||||
wantErrs: []string{tokenSourceExecErr},
|
||||
},
|
||||
{
|
||||
name: "exec tokenSource with unsafe feature allowed",
|
||||
tokenSource: validExecTokenSource,
|
||||
unsafeAllowed: true,
|
||||
},
|
||||
{
|
||||
name: "literal token conflicts with exec tokenSource and unsafe feature disabled",
|
||||
token: "token",
|
||||
tokenSource: validExecTokenSource,
|
||||
wantErrs: []string{
|
||||
tokenSourceConflictErr,
|
||||
tokenSourceExecErr,
|
||||
},
|
||||
},
|
||||
{
|
||||
name: "literal token conflicts with exec tokenSource and unsafe feature allowed",
|
||||
token: "token",
|
||||
tokenSource: validExecTokenSource,
|
||||
unsafeAllowed: true,
|
||||
wantErrs: []string{tokenSourceConflictErr},
|
||||
},
|
||||
{
|
||||
name: "invalid file tokenSource is wrapped",
|
||||
tokenSource: invalidFileTokenSource,
|
||||
wantErrs: []string{invalidFileSourceErr},
|
||||
},
|
||||
{
|
||||
name: "unsupported tokenSource type is wrapped",
|
||||
tokenSource: unsupportedTokenSource,
|
||||
wantErrs: []string{unsupportedSourceErr},
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func newAuthTokenSourceValidator(unsafeAllowed bool) *ConfigValidator {
|
||||
if !unsafeAllowed {
|
||||
return NewConfigValidator(nil)
|
||||
}
|
||||
return NewConfigValidator(security.NewUnsafeFeatures([]string{security.TokenSourceExec}))
|
||||
}
|
||||
|
||||
func requireValidationErrors(t *testing.T, err error, wantErrs []string) {
|
||||
t.Helper()
|
||||
if len(wantErrs) == 0 {
|
||||
require.NoError(t, err)
|
||||
return
|
||||
}
|
||||
require.Error(t, err)
|
||||
// Client/server validators may wrap joined errors in another join layer; compare leaf errors.
|
||||
gotErrs := unwrapValidationErrors(err)
|
||||
require.Len(t, gotErrs, len(wantErrs))
|
||||
for i, wantErr := range wantErrs {
|
||||
require.EqualError(t, gotErrs[i], wantErr)
|
||||
}
|
||||
}
|
||||
|
||||
func unwrapValidationErrors(err error) []error {
|
||||
type joinedError interface {
|
||||
Unwrap() []error
|
||||
}
|
||||
joined, ok := err.(joinedError)
|
||||
if !ok {
|
||||
return []error{err}
|
||||
}
|
||||
|
||||
var errs []error
|
||||
for _, err := range joined.Unwrap() {
|
||||
errs = append(errs, unwrapValidationErrors(err)...)
|
||||
}
|
||||
return errs
|
||||
}
|
||||
|
||||
// nilTokenSource keeps the shared table shape uniform for cases without a tokenSource.
|
||||
func nilTokenSource() *v1.ValueSource {
|
||||
return nil
|
||||
}
|
||||
|
||||
func validFileTokenSource() *v1.ValueSource {
|
||||
return &v1.ValueSource{
|
||||
Type: "file",
|
||||
File: &v1.FileSource{Path: "token.txt"},
|
||||
}
|
||||
}
|
||||
|
||||
func validExecTokenSource() *v1.ValueSource {
|
||||
return &v1.ValueSource{
|
||||
Type: "exec",
|
||||
Exec: &v1.ExecSource{Command: "print-token"},
|
||||
}
|
||||
}
|
||||
|
||||
func invalidFileTokenSource() *v1.ValueSource {
|
||||
return &v1.ValueSource{
|
||||
Type: "file",
|
||||
}
|
||||
}
|
||||
|
||||
func unsupportedTokenSource() *v1.ValueSource {
|
||||
return &v1.ValueSource{Type: "env"}
|
||||
}
|
||||
|
||||
func validClientConfigWithAuth(auth v1.AuthClientConfig) *v1.ClientCommonConfig {
|
||||
return &v1.ClientCommonConfig{
|
||||
Auth: auth,
|
||||
Log: v1.LogConfig{
|
||||
Level: "info",
|
||||
},
|
||||
Transport: v1.ClientTransportConfig{
|
||||
Protocol: "tcp",
|
||||
WireProtocol: "v1",
|
||||
},
|
||||
}
|
||||
}
|
||||
|
||||
func validServerConfigWithAuth(auth v1.AuthServerConfig) *v1.ServerConfig {
|
||||
return &v1.ServerConfig{
|
||||
Auth: auth,
|
||||
Log: v1.LogConfig{
|
||||
Level: "info",
|
||||
},
|
||||
}
|
||||
}
|
||||
@@ -22,9 +22,9 @@ import (
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/policy/featuregate"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
"kanhole/pkg/policy/featuregate"
|
||||
"kanhole/pkg/policy/security"
|
||||
)
|
||||
|
||||
func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
||||
@@ -68,22 +68,7 @@ func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, e
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
|
||||
}
|
||||
|
||||
// Validate token/tokenSource mutual exclusivity
|
||||
if c.Token != "" && c.TokenSource != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
|
||||
}
|
||||
|
||||
// Validate tokenSource if specified
|
||||
if c.TokenSource != nil {
|
||||
if c.TokenSource.Type == "exec" {
|
||||
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
}
|
||||
if err := c.TokenSource.Validate(); err != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||
}
|
||||
}
|
||||
errs = AppendError(errs, v.validateAuthTokenSource(c.Token, c.TokenSource))
|
||||
|
||||
if err := v.validateOIDCConfig(&c.OIDC); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
|
||||
@@ -18,7 +18,7 @@ import (
|
||||
"fmt"
|
||||
"slices"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func validateWebServerConfig(c *v1.WebServerConfig) error {
|
||||
|
||||
@@ -19,7 +19,7 @@ import (
|
||||
"net/url"
|
||||
"strings"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func ValidateOIDCClientCredentialsConfig(c *v1.AuthOIDCClientConfig) error {
|
||||
|
||||
@@ -21,7 +21,7 @@ import (
|
||||
|
||||
"github.com/stretchr/testify/require"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func TestValidateOIDCClientCredentialsConfig(t *testing.T) {
|
||||
|
||||
@@ -17,7 +17,7 @@ package validation
|
||||
import (
|
||||
"errors"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func ValidateClientPluginOptions(c v1.ClientPluginOptions) error {
|
||||
|
||||
@@ -22,7 +22,7 @@ import (
|
||||
|
||||
"k8s.io/apimachinery/pkg/util/validation"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func validateProxyBaseConfigForClient(c *v1.ProxyBaseConfig) error {
|
||||
|
||||
@@ -20,8 +20,7 @@ import (
|
||||
|
||||
"github.com/samber/lo"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
)
|
||||
|
||||
func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
||||
@@ -36,22 +35,7 @@ func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, err
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth additional scopes, optional values are %v", SupportedAuthAdditionalScopes))
|
||||
}
|
||||
|
||||
// Validate token/tokenSource mutual exclusivity
|
||||
if c.Auth.Token != "" && c.Auth.TokenSource != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.token and auth.tokenSource"))
|
||||
}
|
||||
|
||||
// Validate tokenSource if specified
|
||||
if c.Auth.TokenSource != nil {
|
||||
if c.Auth.TokenSource.Type == "exec" {
|
||||
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
}
|
||||
}
|
||||
if err := c.Auth.TokenSource.Validate(); err != nil {
|
||||
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||
}
|
||||
}
|
||||
errs = AppendError(errs, v.validateAuthTokenSource(c.Auth.Token, c.Auth.TokenSource))
|
||||
|
||||
if err := validateLogConfig(&c.Log); err != nil {
|
||||
errs = AppendError(errs, err)
|
||||
|
||||
@@ -17,8 +17,8 @@ package validation
|
||||
import (
|
||||
"errors"
|
||||
|
||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||
splugin "github.com/fatedier/frp/pkg/plugin/server"
|
||||
v1 "kanhole/pkg/config/v1"
|
||||
splugin "kanhole/pkg/plugin/server"
|
||||
)
|
||||
|
||||
var (
|
||||
|
||||
@@ -3,7 +3,7 @@ package validation
|
||||
import (
|
||||
"fmt"
|
||||
|
||||
"github.com/fatedier/frp/pkg/policy/security"
|
||||
"kanhole/pkg/policy/security"
|
||||
)
|
||||
|
||||
// ConfigValidator holds the context dependencies for configuration validation.
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user