forked from Mxmilu666/frp
Compare commits
6 Commits
dev
...
a0ae9879c0
| Author | SHA1 | Date | |
|---|---|---|---|
| a0ae9879c0 | |||
|
9072177b79
|
|||
| e4f1ec648f | |||
|
a76ecfe76c
|
|||
|
58126df910
|
|||
|
bc1e44268a
|
@@ -6,14 +6,6 @@ jobs:
|
|||||||
resource_class: large
|
resource_class: large
|
||||||
steps:
|
steps:
|
||||||
- checkout
|
- checkout
|
||||||
- run:
|
|
||||||
name: Build web assets (frps)
|
|
||||||
command: make install build
|
|
||||||
working_directory: web/frps
|
|
||||||
- run:
|
|
||||||
name: Build web assets (frpc)
|
|
||||||
command: make install build
|
|
||||||
working_directory: web/frpc
|
|
||||||
- run: make
|
- run: make
|
||||||
- run: make alltest
|
- run: make alltest
|
||||||
|
|
||||||
|
|||||||
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +0,0 @@
|
|||||||
* text=auto eol=lf
|
|
||||||
50
.gitea/workflows/build-all.yaml
Normal file
50
.gitea/workflows/build-all.yaml
Normal file
@@ -0,0 +1,50 @@
|
|||||||
|
# 由于成本问题,现已全面转向 Github Actions 构建
|
||||||
|
name: Build FRP Binaries
|
||||||
|
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- '**'
|
||||||
|
tags:
|
||||||
|
- '**'
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Manual build tag'
|
||||||
|
required: false
|
||||||
|
default: 'manual'
|
||||||
|
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
name: Build and Package FRP
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- name: Checkout source
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: 0
|
||||||
|
|
||||||
|
- name: Set up Go
|
||||||
|
uses: actions/setup-go@v5
|
||||||
|
with:
|
||||||
|
go-version: '1.22'
|
||||||
|
|
||||||
|
- name: Set up dependencies
|
||||||
|
run: |
|
||||||
|
sudo apt-get update -y
|
||||||
|
sudo apt-get install -y zip make gcc g++ upx
|
||||||
|
|
||||||
|
- name: Run build script
|
||||||
|
run: |
|
||||||
|
chmod +x ./package.sh
|
||||||
|
./package.sh
|
||||||
|
|
||||||
|
- name: Upload build artifacts
|
||||||
|
uses: actions/upload-artifact@v3
|
||||||
|
with:
|
||||||
|
name: frp-packages
|
||||||
|
path: release/packages
|
||||||
102
.github/workflows/build-all.yaml
vendored
102
.github/workflows/build-all.yaml
vendored
@@ -4,8 +4,9 @@ on:
|
|||||||
push:
|
push:
|
||||||
branches:
|
branches:
|
||||||
- '**'
|
- '**'
|
||||||
|
tags:
|
||||||
|
- '**'
|
||||||
workflow_dispatch:
|
workflow_dispatch:
|
||||||
workflow_call:
|
|
||||||
|
|
||||||
permissions:
|
permissions:
|
||||||
contents: read
|
contents: read
|
||||||
@@ -18,23 +19,16 @@ jobs:
|
|||||||
strategy:
|
strategy:
|
||||||
matrix:
|
matrix:
|
||||||
goos: [linux, windows, darwin, freebsd, openbsd, android]
|
goos: [linux, windows, darwin, freebsd, openbsd, android]
|
||||||
goarch: [amd64, 386, arm, arm64]
|
goarch: [amd64, arm, arm64]
|
||||||
exclude:
|
exclude:
|
||||||
- goos: darwin
|
- goos: darwin
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: darwin
|
|
||||||
goarch: 386
|
|
||||||
- goos: freebsd
|
- goos: freebsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: openbsd
|
- goos: openbsd
|
||||||
goarch: arm
|
goarch: arm
|
||||||
- goos: android
|
- goos: android
|
||||||
goarch: amd64
|
goarch: amd64
|
||||||
- goos: android
|
|
||||||
goarch: 386
|
|
||||||
# 排除 Android ARM 32位,在单独的 job 中处理
|
|
||||||
- goos: android
|
|
||||||
goarch: arm
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout source
|
- name: Checkout source
|
||||||
@@ -94,96 +88,6 @@ jobs:
|
|||||||
uses: actions/upload-artifact@v4
|
uses: actions/upload-artifact@v4
|
||||||
with:
|
with:
|
||||||
name: LoliaFrp_${{ matrix.goos }}_${{ matrix.goarch }}
|
name: LoliaFrp_${{ matrix.goos }}_${{ matrix.goarch }}
|
||||||
path: |
|
|
||||||
release/packages/frp_*
|
|
||||||
retention-days: 7
|
|
||||||
|
|
||||||
build-android-arm:
|
|
||||||
name: Build FRP android-arm
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout source
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Go
|
|
||||||
uses: actions/setup-go@v5
|
|
||||||
with:
|
|
||||||
go-version: '1.22'
|
|
||||||
|
|
||||||
- name: Install dependencies and Android NDK
|
|
||||||
run: |
|
|
||||||
sudo apt-get update -y
|
|
||||||
sudo apt-get install -y zip tar make gcc g++ upx wget unzip
|
|
||||||
|
|
||||||
# 下载并安装 Android NDK
|
|
||||||
echo "Downloading Android NDK..."
|
|
||||||
wget -q https://dl.google.com/android/repository/android-ndk-r26c-linux.zip
|
|
||||||
echo "Extracting Android NDK..."
|
|
||||||
unzip -q android-ndk-r26c-linux.zip
|
|
||||||
echo "NDK installed at: $PWD/android-ndk-r26c"
|
|
||||||
|
|
||||||
- name: Build FRP for android-arm
|
|
||||||
run: |
|
|
||||||
mkdir -p release/packages
|
|
||||||
mkdir -p bin
|
|
||||||
|
|
||||||
echo "Building for android-arm with CGO"
|
|
||||||
|
|
||||||
# 首先构建一次获取版本号
|
|
||||||
CGO_ENABLED=0 make
|
|
||||||
version=$(./bin/frps --version)
|
|
||||||
echo "Detected version: $version"
|
|
||||||
|
|
||||||
# 清理之前的构建
|
|
||||||
rm -rf ./bin/*
|
|
||||||
|
|
||||||
# 设置 Android ARM 交叉编译环境
|
|
||||||
export GOOS=android
|
|
||||||
export GOARCH=arm
|
|
||||||
export GOARM=7
|
|
||||||
export CGO_ENABLED=1
|
|
||||||
export CC=$PWD/android-ndk-r26c/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang
|
|
||||||
export CXX=$PWD/android-ndk-r26c/toolchains/llvm/prebuilt/linux-x86_64/bin/armv7a-linux-androideabi21-clang++
|
|
||||||
|
|
||||||
echo "Environment:"
|
|
||||||
echo "GOOS=$GOOS"
|
|
||||||
echo "GOARCH=$GOARCH"
|
|
||||||
echo "GOARM=$GOARM"
|
|
||||||
echo "CGO_ENABLED=$CGO_ENABLED"
|
|
||||||
echo "CC=$CC"
|
|
||||||
|
|
||||||
# 直接使用 go build 命令,不通过 Makefile,防止 CGO_ENABLED 被覆盖
|
|
||||||
echo "Building frps..."
|
|
||||||
go build -trimpath -ldflags "-s -w" -tags frps -o bin/frps ./cmd/frps
|
|
||||||
|
|
||||||
echo "Building frpc..."
|
|
||||||
go build -trimpath -ldflags "-s -w" -tags frpc -o bin/frpc ./cmd/frpc
|
|
||||||
|
|
||||||
# 验证文件已生成
|
|
||||||
ls -lh ./bin/
|
|
||||||
file ./bin/frpc
|
|
||||||
file ./bin/frps
|
|
||||||
|
|
||||||
out_dir="release/packages/frp_${version}_android_arm"
|
|
||||||
mkdir -p "$out_dir"
|
|
||||||
|
|
||||||
mv ./bin/frpc "$out_dir/frpc"
|
|
||||||
mv ./bin/frps "$out_dir/frps"
|
|
||||||
|
|
||||||
cp LICENSE "$out_dir"
|
|
||||||
cp -f conf/frpc.toml "$out_dir"
|
|
||||||
cp -f conf/frps.toml "$out_dir"
|
|
||||||
|
|
||||||
echo "Build completed for android-arm"
|
|
||||||
ls -lh "$out_dir"
|
|
||||||
|
|
||||||
- name: Upload artifact
|
|
||||||
uses: actions/upload-artifact@v4
|
|
||||||
with:
|
|
||||||
name: LoliaFrp_android_arm
|
|
||||||
path: |
|
path: |
|
||||||
release/packages/frp_*
|
release/packages/frp_*
|
||||||
retention-days: 7
|
retention-days: 7
|
||||||
83
.github/workflows/build-and-push-image.yml
vendored
Normal file
83
.github/workflows/build-and-push-image.yml
vendored
Normal file
@@ -0,0 +1,83 @@
|
|||||||
|
name: Build Image and Publish to Dockerhub & GPR
|
||||||
|
|
||||||
|
on:
|
||||||
|
release:
|
||||||
|
types: [ published ]
|
||||||
|
workflow_dispatch:
|
||||||
|
inputs:
|
||||||
|
tag:
|
||||||
|
description: 'Image tag'
|
||||||
|
required: true
|
||||||
|
default: 'test'
|
||||||
|
permissions:
|
||||||
|
contents: read
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
image:
|
||||||
|
name: Build Image from Dockerfile and binaries
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
# environment
|
||||||
|
- name: Checkout
|
||||||
|
uses: actions/checkout@v4
|
||||||
|
with:
|
||||||
|
fetch-depth: '0'
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
# get image tag name
|
||||||
|
- name: Get Image Tag Name
|
||||||
|
run: |
|
||||||
|
if [ x${{ github.event.inputs.tag }} == x"" ]; then
|
||||||
|
echo "TAG_NAME=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
else
|
||||||
|
echo "TAG_NAME=${{ github.event.inputs.tag }}" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
- name: Login to DockerHub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKERHUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKERHUB_PASSWORD }}
|
||||||
|
|
||||||
|
- name: Login to the GPR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GPR_TOKEN }}
|
||||||
|
|
||||||
|
# prepare image tags
|
||||||
|
- name: Prepare Image Tags
|
||||||
|
run: |
|
||||||
|
echo "DOCKERFILE_FRPC_PATH=dockerfiles/Dockerfile-for-frpc" >> $GITHUB_ENV
|
||||||
|
echo "DOCKERFILE_FRPS_PATH=dockerfiles/Dockerfile-for-frps" >> $GITHUB_ENV
|
||||||
|
echo "TAG_FRPC=fatedier/frpc:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||||
|
echo "TAG_FRPS=fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||||
|
echo "TAG_FRPC_GPR=ghcr.io/fatedier/frpc:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||||
|
echo "TAG_FRPS_GPR=ghcr.io/fatedier/frps:${{ env.TAG_NAME }}" >> $GITHUB_ENV
|
||||||
|
|
||||||
|
- name: Build and push frpc
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./dockerfiles/Dockerfile-for-frpc
|
||||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.TAG_FRPC }}
|
||||||
|
${{ env.TAG_FRPC_GPR }}
|
||||||
|
|
||||||
|
- name: Build and push frps
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: ./dockerfiles/Dockerfile-for-frps
|
||||||
|
platforms: linux/amd64,linux/arm/v7,linux/arm64,linux/ppc64le,linux/s390x
|
||||||
|
push: true
|
||||||
|
tags: |
|
||||||
|
${{ env.TAG_FRPS }}
|
||||||
|
${{ env.TAG_FRPS_GPR }}
|
||||||
82
.github/workflows/docker-build.yml
vendored
82
.github/workflows/docker-build.yml
vendored
@@ -1,82 +0,0 @@
|
|||||||
name: Docker Build and Push
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
tags:
|
|
||||||
- 'v*'
|
|
||||||
pull_request:
|
|
||||||
branches:
|
|
||||||
- main
|
|
||||||
- dev
|
|
||||||
workflow_dispatch:
|
|
||||||
|
|
||||||
env:
|
|
||||||
REGISTRY: ghcr.io
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
build:
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
packages: write
|
|
||||||
id-token: write
|
|
||||||
|
|
||||||
strategy:
|
|
||||||
matrix:
|
|
||||||
include:
|
|
||||||
- name: frps
|
|
||||||
dockerfile: dockerfiles/Dockerfile-for-frps
|
|
||||||
image_name: ghcr.io/${{ github.repository_owner }}/loliacli-frps
|
|
||||||
- name: frpc
|
|
||||||
dockerfile: dockerfiles/Dockerfile-for-frpc
|
|
||||||
image_name: ghcr.io/${{ github.repository_owner }}/loliacli-frpc
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout code
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Set up Docker Buildx
|
|
||||||
uses: docker/setup-buildx-action@v3
|
|
||||||
|
|
||||||
- name: Log in to GitHub Container Registry
|
|
||||||
uses: docker/login-action@v3
|
|
||||||
with:
|
|
||||||
registry: ${{ env.REGISTRY }}
|
|
||||||
username: ${{ github.actor }}
|
|
||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
|
|
||||||
- name: Extract metadata
|
|
||||||
id: meta
|
|
||||||
uses: docker/metadata-action@v5
|
|
||||||
with:
|
|
||||||
images: ${{ matrix.image_name }}
|
|
||||||
tags: |
|
|
||||||
type=ref,event=branch
|
|
||||||
type=ref,event=pr
|
|
||||||
type=semver,pattern={{version}}
|
|
||||||
type=semver,pattern={{major}}.{{minor}}
|
|
||||||
type=semver,pattern={{major}}
|
|
||||||
type=raw,value=latest,enable={{is_default_branch}}
|
|
||||||
|
|
||||||
- name: Build and push Docker image
|
|
||||||
uses: docker/build-push-action@v5
|
|
||||||
with:
|
|
||||||
context: .
|
|
||||||
file: ${{ matrix.dockerfile }}
|
|
||||||
push: ${{ github.event_name != 'pull_request' }}
|
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
|
||||||
cache-from: type=gha
|
|
||||||
cache-to: type=gha,mode=max
|
|
||||||
platforms: linux/amd64,linux/arm64
|
|
||||||
|
|
||||||
- name: Generate image digest
|
|
||||||
if: github.event_name != 'pull_request'
|
|
||||||
run: |
|
|
||||||
echo "Image pushed to: ${{ matrix.image_name }}"
|
|
||||||
echo "Tags: ${{ steps.meta.outputs.tags }}"
|
|
||||||
11
.github/workflows/golangci-lint.yml
vendored
11
.github/workflows/golangci-lint.yml
vendored
@@ -19,17 +19,8 @@ jobs:
|
|||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version: '1.24'
|
||||||
cache: false
|
cache: false
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
- name: Build web assets (frps)
|
|
||||||
run: make build
|
|
||||||
working-directory: web/frps
|
|
||||||
- name: Build web assets (frpc)
|
|
||||||
run: make build
|
|
||||||
working-directory: web/frpc
|
|
||||||
- name: golangci-lint
|
- name: golangci-lint
|
||||||
uses: golangci/golangci-lint-action@v8
|
uses: golangci/golangci-lint-action@v8
|
||||||
with:
|
with:
|
||||||
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
# Optional: version of golangci-lint to use in form of v1.2 or v1.2.3 or `latest` to use the latest version
|
||||||
version: v2.3
|
version: v2.3
|
||||||
|
|||||||
12
.github/workflows/goreleaser.yml
vendored
12
.github/workflows/goreleaser.yml
vendored
@@ -16,21 +16,13 @@ jobs:
|
|||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
go-version: '1.24'
|
go-version: '1.24'
|
||||||
- uses: actions/setup-node@v4
|
|
||||||
with:
|
|
||||||
node-version: '22'
|
|
||||||
- name: Build web assets (frps)
|
|
||||||
run: make build
|
|
||||||
working-directory: web/frps
|
|
||||||
- name: Build web assets (frpc)
|
|
||||||
run: make build
|
|
||||||
working-directory: web/frpc
|
|
||||||
- name: Make All
|
- name: Make All
|
||||||
run: |
|
run: |
|
||||||
./package.sh
|
./package.sh
|
||||||
|
|
||||||
- name: Run GoReleaser
|
- name: Run GoReleaser
|
||||||
uses: goreleaser/goreleaser-action@v6
|
uses: goreleaser/goreleaser-action@v5
|
||||||
with:
|
with:
|
||||||
version: latest
|
version: latest
|
||||||
args: release --clean --release-notes=./Release.md
|
args: release --clean --release-notes=./Release.md
|
||||||
|
|||||||
129
.github/workflows/release.yaml
vendored
129
.github/workflows/release.yaml
vendored
@@ -1,129 +0,0 @@
|
|||||||
name: Release FRP Binaries
|
|
||||||
|
|
||||||
on:
|
|
||||||
push:
|
|
||||||
tags:
|
|
||||||
- "v*"
|
|
||||||
workflow_dispatch:
|
|
||||||
inputs:
|
|
||||||
tag:
|
|
||||||
description: "Tag to release (e.g., v1.0.0)"
|
|
||||||
required: true
|
|
||||||
type: string
|
|
||||||
|
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
jobs:
|
|
||||||
# 调用 build-all workflow
|
|
||||||
build:
|
|
||||||
uses: ./.github/workflows/build-all.yaml
|
|
||||||
permissions:
|
|
||||||
contents: read
|
|
||||||
|
|
||||||
# 创建 release
|
|
||||||
release:
|
|
||||||
name: Create Release
|
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
|
||||||
|
|
||||||
steps:
|
|
||||||
- name: Checkout source
|
|
||||||
uses: actions/checkout@v4
|
|
||||||
with:
|
|
||||||
fetch-depth: 0
|
|
||||||
|
|
||||||
- name: Get tag name
|
|
||||||
id: tag
|
|
||||||
run: |
|
|
||||||
if [ "${{ github.event_name }}" = "workflow_dispatch" ]; then
|
|
||||||
echo "tag=${{ github.event.inputs.tag }}" >> $GITHUB_OUTPUT
|
|
||||||
else
|
|
||||||
echo "tag=${GITHUB_REF#refs/tags/}" >> $GITHUB_OUTPUT
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Download all artifacts
|
|
||||||
uses: actions/download-artifact@v4
|
|
||||||
with:
|
|
||||||
path: artifacts
|
|
||||||
|
|
||||||
- name: Display artifact structure
|
|
||||||
run: |
|
|
||||||
echo "Artifact structure:"
|
|
||||||
ls -R artifacts/
|
|
||||||
|
|
||||||
- name: Organize release files
|
|
||||||
run: |
|
|
||||||
mkdir -p release_files
|
|
||||||
|
|
||||||
# 查找并复制所有压缩包
|
|
||||||
find artifacts -type f \( -name "*.zip" -o -name "*.tar.gz" \) -exec cp {} release_files/ \;
|
|
||||||
|
|
||||||
# 如果没有压缩包,尝试查找二进制文件并打包
|
|
||||||
if [ -z "$(ls -A release_files/)" ]; then
|
|
||||||
echo "No archives found, looking for directories to package..."
|
|
||||||
for dir in artifacts/*/; do
|
|
||||||
if [ -d "$dir" ]; then
|
|
||||||
artifact_name=$(basename "$dir")
|
|
||||||
echo "Packaging $artifact_name"
|
|
||||||
|
|
||||||
# 检查是否是 Windows 构建
|
|
||||||
if echo "$artifact_name" | grep -q "windows"; then
|
|
||||||
(cd "$dir" && zip -r "../../release_files/${artifact_name}.zip" .)
|
|
||||||
else
|
|
||||||
tar -czf "release_files/${artifact_name}.tar.gz" -C "$dir" .
|
|
||||||
fi
|
|
||||||
fi
|
|
||||||
done
|
|
||||||
fi
|
|
||||||
|
|
||||||
echo "Files in release_files:"
|
|
||||||
ls -lh release_files/
|
|
||||||
|
|
||||||
- name: Generate checksums
|
|
||||||
run: |
|
|
||||||
cd release_files
|
|
||||||
if [ -n "$(ls -A .)" ]; then
|
|
||||||
sha256sum * > sha256sum.txt
|
|
||||||
cat sha256sum.txt
|
|
||||||
else
|
|
||||||
echo "No files to generate checksums for!"
|
|
||||||
exit 1
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Debug - Check tags and commits
|
|
||||||
run: |
|
|
||||||
echo "Current tag: ${{ steps.tag.outputs.tag }}"
|
|
||||||
PREV_TAG=$(git describe --tags --abbrev=0 ${{ steps.tag.outputs.tag }}^ 2>/dev/null || echo "none")
|
|
||||||
echo "Previous tag: $PREV_TAG"
|
|
||||||
|
|
||||||
echo ""
|
|
||||||
echo "Commits between tags:"
|
|
||||||
if [ "$PREV_TAG" != "none" ]; then
|
|
||||||
git log --oneline $PREV_TAG..${{ steps.tag.outputs.tag }}
|
|
||||||
else
|
|
||||||
echo "First tag, showing all commits:"
|
|
||||||
git log --oneline ${{ steps.tag.outputs.tag }}
|
|
||||||
fi
|
|
||||||
|
|
||||||
- name: Build Changelog
|
|
||||||
id: changelog
|
|
||||||
uses: requarks/changelog-action@v1
|
|
||||||
with:
|
|
||||||
token: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
tag: ${{ steps.tag.outputs.tag }}
|
|
||||||
writeToFile: false
|
|
||||||
includeInvalidCommits: true
|
|
||||||
useGitmojis: false
|
|
||||||
- name: Create Release
|
|
||||||
uses: softprops/action-gh-release@v1
|
|
||||||
with:
|
|
||||||
tag_name: ${{ steps.tag.outputs.tag }}
|
|
||||||
name: Release ${{ steps.tag.outputs.tag }}
|
|
||||||
body: ${{ steps.changelog.outputs.changes }}
|
|
||||||
draft: false
|
|
||||||
prerelease: false
|
|
||||||
files: |
|
|
||||||
release_files/*
|
|
||||||
env:
|
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
|
||||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -42,6 +42,3 @@ client.key
|
|||||||
|
|
||||||
# AI
|
# AI
|
||||||
CLAUDE.md
|
CLAUDE.md
|
||||||
|
|
||||||
# TLS
|
|
||||||
.autotls-cache
|
|
||||||
21
Makefile
21
Makefile
@@ -2,22 +2,19 @@ export PATH := $(PATH):`go env GOPATH`/bin
|
|||||||
export GO111MODULE=on
|
export GO111MODULE=on
|
||||||
LDFLAGS := -s -w
|
LDFLAGS := -s -w
|
||||||
|
|
||||||
.PHONY: web frps-web frpc-web frps frpc
|
all: env fmt build
|
||||||
|
|
||||||
all: env fmt web build
|
|
||||||
|
|
||||||
build: frps frpc
|
build: frps frpc
|
||||||
|
|
||||||
env:
|
env:
|
||||||
@go version
|
@go version
|
||||||
|
|
||||||
web: frps-web frpc-web
|
# compile assets into binary file
|
||||||
|
file:
|
||||||
frps-web:
|
rm -rf ./assets/frps/static/*
|
||||||
$(MAKE) -C web/frps build
|
rm -rf ./assets/frpc/static/*
|
||||||
|
cp -rf ./web/frps/dist/* ./assets/frps/static
|
||||||
frpc-web:
|
cp -rf ./web/frpc/dist/* ./assets/frpc/static
|
||||||
$(MAKE) -C web/frpc build
|
|
||||||
|
|
||||||
fmt:
|
fmt:
|
||||||
go fmt ./...
|
go fmt ./...
|
||||||
@@ -28,7 +25,7 @@ fmt-more:
|
|||||||
gci:
|
gci:
|
||||||
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
|
gci write -s standard -s default -s "prefix(github.com/fatedier/frp/)" ./
|
||||||
|
|
||||||
vet: web
|
vet:
|
||||||
go vet ./...
|
go vet ./...
|
||||||
|
|
||||||
frps:
|
frps:
|
||||||
@@ -39,7 +36,7 @@ frpc:
|
|||||||
|
|
||||||
test: gotest
|
test: gotest
|
||||||
|
|
||||||
gotest: web
|
gotest:
|
||||||
go test -v --cover ./assets/...
|
go test -v --cover ./assets/...
|
||||||
go test -v --cover ./cmd/...
|
go test -v --cover ./cmd/...
|
||||||
go test -v --cover ./client/...
|
go test -v --cover ./client/...
|
||||||
|
|||||||
51
README.md
51
README.md
@@ -13,24 +13,6 @@ frp is an open source project with its ongoing development made possible entirel
|
|||||||
|
|
||||||
<h3 align="center">Gold Sponsors</h3>
|
<h3 align="center">Gold Sponsors</h3>
|
||||||
<!--gold sponsors start-->
|
<!--gold sponsors start-->
|
||||||
<p align="center">
|
|
||||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
|
||||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
|
||||||
<br>
|
|
||||||
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
|
||||||
<br>
|
|
||||||
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://jb.gg/frp" target="_blank">
|
|
||||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
|
||||||
<br>
|
|
||||||
<b>The complete IDE crafted for professional Go developers</b>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
<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">
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||||
@@ -40,6 +22,7 @@ frp is an open source project with its ongoing development made possible entirel
|
|||||||
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
<sub>An open source, self-hosted alternative to public clouds, built for data ownership and privacy</sub>
|
||||||
</a>
|
</a>
|
||||||
</p>
|
</p>
|
||||||
|
|
||||||
<div align="center">
|
<div align="center">
|
||||||
|
|
||||||
## Recall.ai - API for meeting recordings
|
## Recall.ai - API for meeting recordings
|
||||||
@@ -49,6 +32,38 @@ If you're looking for a meeting recording API, consider checking out [Recall.ai]
|
|||||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||||
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
|
<br>
|
||||||
|
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||||
|
<br>
|
||||||
|
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://go.warp.dev/frp" target="_blank">
|
||||||
|
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
|
||||||
|
<br>
|
||||||
|
<b>Warp, built for collaborating with AI Agents</b>
|
||||||
|
<br>
|
||||||
|
<sub>Available for macOS, Linux and Windows</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://jb.gg/frp" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||||
|
<br>
|
||||||
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/daytonaio/daytona" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
|
||||||
|
<br>
|
||||||
|
<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
<!--gold sponsors end-->
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
## What is frp?
|
## What is frp?
|
||||||
|
|||||||
50
README_zh.md
50
README_zh.md
@@ -15,24 +15,6 @@ frp 是一个完全开源的项目,我们的开发工作完全依靠赞助者
|
|||||||
|
|
||||||
<h3 align="center">Gold Sponsors</h3>
|
<h3 align="center">Gold Sponsors</h3>
|
||||||
<!--gold sponsors start-->
|
<!--gold sponsors start-->
|
||||||
<p align="center">
|
|
||||||
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
|
||||||
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
|
||||||
<br>
|
|
||||||
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
|
||||||
<br>
|
|
||||||
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
|
||||||
<a href="https://jb.gg/frp" target="_blank">
|
|
||||||
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
|
||||||
<br>
|
|
||||||
<b>The complete IDE crafted for professional Go developers</b>
|
|
||||||
</a>
|
|
||||||
</p>
|
|
||||||
|
|
||||||
<p align="center">
|
<p align="center">
|
||||||
<a href="https://github.com/beclab/Olares" target="_blank">
|
<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">
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_olares.jpeg">
|
||||||
@@ -51,6 +33,38 @@ If you're looking for a meeting recording API, consider checking out [Recall.ai]
|
|||||||
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
an API that records Zoom, Google Meet, Microsoft Teams, in-person meetings, and more.
|
||||||
|
|
||||||
</div>
|
</div>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://requestly.com/?utm_source=github&utm_medium=partnered&utm_campaign=frp" target="_blank">
|
||||||
|
<img width="480px" src="https://github.com/user-attachments/assets/24670320-997d-4d62-9bca-955c59fe883d">
|
||||||
|
<br>
|
||||||
|
<b>Requestly - Free & Open-Source alternative to Postman</b>
|
||||||
|
<br>
|
||||||
|
<sub>All-in-one platform to Test, Mock and Intercept APIs.</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://go.warp.dev/frp" target="_blank">
|
||||||
|
<img width="360px" src="https://raw.githubusercontent.com/warpdotdev/brand-assets/refs/heads/main/Github/Sponsor/Warp-Github-LG-01.png">
|
||||||
|
<br>
|
||||||
|
<b>Warp, built for collaborating with AI Agents</b>
|
||||||
|
<br>
|
||||||
|
<sub>Available for macOS, Linux and Windows</sub>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://jb.gg/frp" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_jetbrains.jpg">
|
||||||
|
<br>
|
||||||
|
<b>The complete IDE crafted for professional Go developers</b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
|
<p align="center">
|
||||||
|
<a href="https://github.com/daytonaio/daytona" target="_blank">
|
||||||
|
<img width="420px" src="https://raw.githubusercontent.com/fatedier/frp/dev/doc/pic/sponsor_daytona.png">
|
||||||
|
<br>
|
||||||
|
<b>Secure and Elastic Infrastructure for Running Your AI-Generated Code</b>
|
||||||
|
</a>
|
||||||
|
</p>
|
||||||
<!--gold sponsors end-->
|
<!--gold sponsors end-->
|
||||||
|
|
||||||
## 为什么使用 frp ?
|
## 为什么使用 frp ?
|
||||||
|
|||||||
10
Release.md
10
Release.md
@@ -1,8 +1,12 @@
|
|||||||
## Features
|
## Features
|
||||||
|
|
||||||
* frpc now supports a `clientID` option to uniquely identify client instances. The server dashboard displays all connected clients with their online/offline status, connection history, and metadata, making it easier to monitor and manage multiple frpc deployments.
|
* HTTPS proxies now support load balancing groups. Multiple HTTPS proxies can be configured with the same `loadBalancer.group` and `loadBalancer.groupKey` to share the same custom domain and distribute traffic across multiple backend services, similar to the existing TCP and HTTP load balancing capabilities.
|
||||||
* Redesigned the frp web dashboard with a modern UI, dark mode support, and improved navigation.
|
* Individual frpc proxies and visitors now accept an `enabled` flag (defaults to true), letting you disable specific entries without relying on the global `start` list—disabled blocks are skipped when client configs load.
|
||||||
|
|
||||||
|
## Improvements
|
||||||
|
|
||||||
|
* **VirtualNet**: Implemented intelligent reconnection with exponential backoff. When connection errors occur repeatedly, the reconnect interval increases from 60s to 300s (max), reducing unnecessary reconnection attempts. Normal disconnections still reconnect quickly at 10s intervals.
|
||||||
|
|
||||||
## Fixes
|
## Fixes
|
||||||
|
|
||||||
* Fixed UDP proxy protocol sending header on every packet instead of only the first packet of each session.
|
* Fix deadlock issue when TCP connection is closed. Previously, sending messages could block forever if the connection handler had already stopped.
|
||||||
|
|||||||
@@ -41,7 +41,7 @@ func Load(path string) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Register(fileSystem fs.FS) {
|
func Register(fileSystem fs.FS) {
|
||||||
subFs, err := fs.Sub(fileSystem, "dist")
|
subFs, err := fs.Sub(fileSystem, "static")
|
||||||
if err == nil {
|
if err == nil {
|
||||||
content = subFs
|
content = subFs
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -6,9 +6,9 @@ import (
|
|||||||
"github.com/fatedier/frp/assets"
|
"github.com/fatedier/frp/assets"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed dist
|
//go:embed static/*
|
||||||
var EmbedFS embed.FS
|
var content embed.FS
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
assets.Register(EmbedFS)
|
assets.Register(content)
|
||||||
}
|
}
|
||||||
BIN
assets/frpc/static/favicon.ico
Normal file
BIN
assets/frpc/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
42
assets/frpc/static/index-bLBhaJo8.js
Normal file
42
assets/frpc/static/index-bLBhaJo8.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/frpc/static/index-iuf46MlF.css
Normal file
1
assets/frpc/static/index-iuf46MlF.css
Normal file
File diff suppressed because one or more lines are too long
15
assets/frpc/static/index.html
Normal file
15
assets/frpc/static/index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>frp client admin UI</title>
|
||||||
|
<script type="module" crossorigin src="./index-bLBhaJo8.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="./index-iuf46MlF.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
14
assets/frps/embed.go
Normal file
14
assets/frps/embed.go
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
package frpc
|
||||||
|
|
||||||
|
import (
|
||||||
|
"embed"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/assets"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed static/*
|
||||||
|
var content embed.FS
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
assets.Register(content)
|
||||||
|
}
|
||||||
BIN
assets/frps/static/favicon.ico
Normal file
BIN
assets/frps/static/favicon.ico
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 9.4 KiB |
84
assets/frps/static/index-82-40HIG.js
Normal file
84
assets/frps/static/index-82-40HIG.js
Normal file
File diff suppressed because one or more lines are too long
1
assets/frps/static/index-rzPDshRD.css
Normal file
1
assets/frps/static/index-rzPDshRD.css
Normal file
File diff suppressed because one or more lines are too long
15
assets/frps/static/index.html
Normal file
15
assets/frps/static/index.html
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en" class="dark">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="utf-8">
|
||||||
|
<title>frps dashboard</title>
|
||||||
|
<script type="module" crossorigin src="./index-82-40HIG.js"></script>
|
||||||
|
<link rel="stylesheet" crossorigin href="./index-rzPDshRD.css">
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
@@ -15,29 +15,44 @@
|
|||||||
package client
|
package client
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"cmp"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
"net/http"
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"slices"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/api"
|
|
||||||
"github.com/fatedier/frp/client/proxy"
|
"github.com/fatedier/frp/client/proxy"
|
||||||
|
"github.com/fatedier/frp/pkg/config"
|
||||||
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
type GeneralResponse struct {
|
||||||
|
Code int
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
||||||
apiController := newAPIController(svr)
|
helper.Router.HandleFunc("/healthz", svr.healthz)
|
||||||
|
|
||||||
// Healthz endpoint without auth
|
|
||||||
helper.Router.HandleFunc("/healthz", healthz)
|
|
||||||
|
|
||||||
// API routes and static files with auth
|
|
||||||
subRouter := helper.Router.NewRoute().Subrouter()
|
subRouter := helper.Router.NewRoute().Subrouter()
|
||||||
subRouter.Use(helper.AuthMiddleware)
|
|
||||||
subRouter.Use(httppkg.NewRequestLogger)
|
subRouter.Use(helper.AuthMiddleware.Middleware)
|
||||||
subRouter.HandleFunc("/api/reload", httppkg.MakeHTTPHandlerFunc(apiController.Reload)).Methods(http.MethodGet)
|
|
||||||
subRouter.HandleFunc("/api/stop", httppkg.MakeHTTPHandlerFunc(apiController.Stop)).Methods(http.MethodPost)
|
// api, see admin_api.go
|
||||||
subRouter.HandleFunc("/api/status", httppkg.MakeHTTPHandlerFunc(apiController.Status)).Methods(http.MethodGet)
|
subRouter.HandleFunc("/api/reload", svr.apiReload).Methods("GET")
|
||||||
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.GetConfig)).Methods(http.MethodGet)
|
subRouter.HandleFunc("/api/stop", svr.apiStop).Methods("POST")
|
||||||
subRouter.HandleFunc("/api/config", httppkg.MakeHTTPHandlerFunc(apiController.PutConfig)).Methods(http.MethodPut)
|
subRouter.HandleFunc("/api/status", svr.apiStatus).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/config", svr.apiGetConfig).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/config", svr.apiPutConfig).Methods("PUT")
|
||||||
|
|
||||||
|
// view
|
||||||
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
||||||
subRouter.PathPrefix("/static/").Handler(
|
subRouter.PathPrefix("/static/").Handler(
|
||||||
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
||||||
@@ -47,28 +62,201 @@ func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper)
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
func healthz(w http.ResponseWriter, _ *http.Request) {
|
// /healthz
|
||||||
w.WriteHeader(http.StatusOK)
|
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(200)
|
||||||
}
|
}
|
||||||
|
|
||||||
func newAPIController(svr *Service) *api.Controller {
|
// GET /api/reload
|
||||||
return api.NewController(api.ControllerParams{
|
func (svr *Service) apiReload(w http.ResponseWriter, r *http.Request) {
|
||||||
GetProxyStatus: svr.getAllProxyStatus,
|
res := GeneralResponse{Code: 200}
|
||||||
ServerAddr: svr.common.ServerAddr,
|
strictConfigMode := false
|
||||||
ConfigFilePath: svr.configFilePath,
|
strictStr := r.URL.Query().Get("strictConfig")
|
||||||
UnsafeFeatures: svr.unsafeFeatures,
|
if strictStr != "" {
|
||||||
UpdateConfig: svr.UpdateAllConfigurer,
|
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
||||||
GracefulClose: svr.GracefulClose,
|
}
|
||||||
})
|
|
||||||
|
log.Infof("api request [/api/reload]")
|
||||||
|
defer func() {
|
||||||
|
log.Infof("api response [/api/reload], code [%d]", res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(svr.configFilePath, strictConfigMode)
|
||||||
|
if err != nil {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = err.Error()
|
||||||
|
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, svr.unsafeFeatures); err != nil {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = err.Error()
|
||||||
|
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := svr.UpdateAllConfigurer(proxyCfgs, visitorCfgs); err != nil {
|
||||||
|
res.Code = 500
|
||||||
|
res.Msg = err.Error()
|
||||||
|
log.Warnf("reload frpc proxy config error: %s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
log.Infof("success reload conf")
|
||||||
}
|
}
|
||||||
|
|
||||||
// getAllProxyStatus returns all proxy statuses.
|
// POST /api/stop
|
||||||
func (svr *Service) getAllProxyStatus() []*proxy.WorkingStatus {
|
func (svr *Service) apiStop(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
|
||||||
|
log.Infof("api request [/api/stop]")
|
||||||
|
defer func() {
|
||||||
|
log.Infof("api response [/api/stop], code [%d]", res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
go svr.GracefulClose(100 * time.Millisecond)
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatusResp map[string][]ProxyStatusResp
|
||||||
|
|
||||||
|
type ProxyStatusResp struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Type string `json:"type"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
Err string `json:"err"`
|
||||||
|
LocalAddr string `json:"local_addr"`
|
||||||
|
Plugin string `json:"plugin"`
|
||||||
|
RemoteAddr string `json:"remote_addr"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewProxyStatusResp(status *proxy.WorkingStatus, serverAddr string) ProxyStatusResp {
|
||||||
|
psr := ProxyStatusResp{
|
||||||
|
Name: status.Name,
|
||||||
|
Type: status.Type,
|
||||||
|
Status: status.Phase,
|
||||||
|
Err: status.Err,
|
||||||
|
}
|
||||||
|
baseCfg := status.Cfg.GetBaseConfig()
|
||||||
|
if baseCfg.LocalPort != 0 {
|
||||||
|
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
||||||
|
}
|
||||||
|
psr.Plugin = baseCfg.Plugin.Type
|
||||||
|
|
||||||
|
if status.Err == "" {
|
||||||
|
psr.RemoteAddr = status.RemoteAddr
|
||||||
|
if slices.Contains([]string{"tcp", "udp"}, status.Type) {
|
||||||
|
psr.RemoteAddr = serverAddr + psr.RemoteAddr
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return psr
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/status
|
||||||
|
func (svr *Service) apiStatus(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
var (
|
||||||
|
buf []byte
|
||||||
|
res StatusResp = make(map[string][]ProxyStatusResp)
|
||||||
|
)
|
||||||
|
|
||||||
|
log.Infof("http request [/api/status]")
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [/api/status]")
|
||||||
|
buf, _ = json.Marshal(&res)
|
||||||
|
_, _ = w.Write(buf)
|
||||||
|
}()
|
||||||
|
|
||||||
svr.ctlMu.RLock()
|
svr.ctlMu.RLock()
|
||||||
ctl := svr.ctl
|
ctl := svr.ctl
|
||||||
svr.ctlMu.RUnlock()
|
svr.ctlMu.RUnlock()
|
||||||
if ctl == nil {
|
if ctl == nil {
|
||||||
return nil
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
ps := ctl.pm.GetAllProxyStatus()
|
||||||
|
for _, status := range ps {
|
||||||
|
res[status.Type] = append(res[status.Type], NewProxyStatusResp(status, svr.common.ServerAddr))
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, arrs := range res {
|
||||||
|
if len(arrs) <= 1 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/config
|
||||||
|
func (svr *Service) apiGetConfig(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
|
||||||
|
log.Infof("http get request [/api/config]")
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http get response [/api/config], code [%d]", res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if svr.configFilePath == "" {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = "frpc has no config file path"
|
||||||
|
log.Warnf("%s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
content, err := os.ReadFile(svr.configFilePath)
|
||||||
|
if err != nil {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = err.Error()
|
||||||
|
log.Warnf("load frpc config file error: %s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
res.Msg = string(content)
|
||||||
|
}
|
||||||
|
|
||||||
|
// PUT /api/config
|
||||||
|
func (svr *Service) apiPutConfig(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
|
||||||
|
log.Infof("http put request [/api/config]")
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http put response [/api/config], code [%d]", res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// get new config content
|
||||||
|
body, err := io.ReadAll(r.Body)
|
||||||
|
if err != nil {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = fmt.Sprintf("read request body error: %v", err)
|
||||||
|
log.Warnf("%s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if len(body) == 0 {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = "body can't be empty"
|
||||||
|
log.Warnf("%s", res.Msg)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := os.WriteFile(svr.configFilePath, body, 0o600); err != nil {
|
||||||
|
res.Code = 500
|
||||||
|
res.Msg = fmt.Sprintf("write content to frpc config file error: %v", err)
|
||||||
|
log.Warnf("%s", res.Msg)
|
||||||
|
return
|
||||||
}
|
}
|
||||||
return ctl.pm.GetAllProxyStatus()
|
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,189 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"fmt"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"os"
|
|
||||||
"slices"
|
|
||||||
"strconv"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/proxy"
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
// Controller handles HTTP API requests for frpc.
|
|
||||||
type Controller struct {
|
|
||||||
// getProxyStatus returns the current proxy status.
|
|
||||||
// Returns nil if the control connection is not established.
|
|
||||||
getProxyStatus func() []*proxy.WorkingStatus
|
|
||||||
|
|
||||||
// serverAddr is the frps server address for display.
|
|
||||||
serverAddr string
|
|
||||||
|
|
||||||
// configFilePath is the path to the configuration file.
|
|
||||||
configFilePath string
|
|
||||||
|
|
||||||
// unsafeFeatures is used for validation.
|
|
||||||
unsafeFeatures *security.UnsafeFeatures
|
|
||||||
|
|
||||||
// updateConfig updates proxy and visitor configurations.
|
|
||||||
updateConfig func(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error
|
|
||||||
|
|
||||||
// gracefulClose gracefully stops the service.
|
|
||||||
gracefulClose func(d time.Duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// ControllerParams contains parameters for creating an APIController.
|
|
||||||
type ControllerParams struct {
|
|
||||||
GetProxyStatus func() []*proxy.WorkingStatus
|
|
||||||
ServerAddr string
|
|
||||||
ConfigFilePath string
|
|
||||||
UnsafeFeatures *security.UnsafeFeatures
|
|
||||||
UpdateConfig func(proxyCfgs []v1.ProxyConfigurer, visitorCfgs []v1.VisitorConfigurer) error
|
|
||||||
GracefulClose func(d time.Duration)
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewController creates a new Controller.
|
|
||||||
func NewController(params ControllerParams) *Controller {
|
|
||||||
return &Controller{
|
|
||||||
getProxyStatus: params.GetProxyStatus,
|
|
||||||
serverAddr: params.ServerAddr,
|
|
||||||
configFilePath: params.ConfigFilePath,
|
|
||||||
unsafeFeatures: params.UnsafeFeatures,
|
|
||||||
updateConfig: params.UpdateConfig,
|
|
||||||
gracefulClose: params.GracefulClose,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Reload handles GET /api/reload
|
|
||||||
func (c *Controller) Reload(ctx *httppkg.Context) (any, error) {
|
|
||||||
strictConfigMode := false
|
|
||||||
strictStr := ctx.Query("strictConfig")
|
|
||||||
if strictStr != "" {
|
|
||||||
strictConfigMode, _ = strconv.ParseBool(strictStr)
|
|
||||||
}
|
|
||||||
|
|
||||||
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(c.configFilePath, strictConfigMode)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if _, err := validation.ValidateAllClientConfig(cliCfg, proxyCfgs, visitorCfgs, c.unsafeFeatures); err != nil {
|
|
||||||
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.updateConfig(proxyCfgs, visitorCfgs); err != nil {
|
|
||||||
log.Warnf("reload frpc proxy config error: %s", err.Error())
|
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
log.Infof("success reload conf")
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Stop handles POST /api/stop
|
|
||||||
func (c *Controller) Stop(ctx *httppkg.Context) (any, error) {
|
|
||||||
go c.gracefulClose(100 * time.Millisecond)
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Status handles GET /api/status
|
|
||||||
func (c *Controller) Status(ctx *httppkg.Context) (any, error) {
|
|
||||||
res := make(StatusResp)
|
|
||||||
ps := c.getProxyStatus()
|
|
||||||
if ps == nil {
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, status := range ps {
|
|
||||||
res[status.Type] = append(res[status.Type], c.buildProxyStatusResp(status))
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, arrs := range res {
|
|
||||||
if len(arrs) <= 1 {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
slices.SortFunc(arrs, func(a, b ProxyStatusResp) int {
|
|
||||||
return cmp.Compare(a.Name, b.Name)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
return res, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConfig handles GET /api/config
|
|
||||||
func (c *Controller) GetConfig(ctx *httppkg.Context) (any, error) {
|
|
||||||
if c.configFilePath == "" {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "frpc has no config file path")
|
|
||||||
}
|
|
||||||
|
|
||||||
content, err := os.ReadFile(c.configFilePath)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("load frpc config file error: %s", err.Error())
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, err.Error())
|
|
||||||
}
|
|
||||||
return string(content), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// PutConfig handles PUT /api/config
|
|
||||||
func (c *Controller) PutConfig(ctx *httppkg.Context) (any, error) {
|
|
||||||
body, err := ctx.Body()
|
|
||||||
if err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, fmt.Sprintf("read request body error: %v", err))
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(body) == 0 {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "body can't be empty")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.WriteFile(c.configFilePath, body, 0o600); err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusInternalServerError, fmt.Sprintf("write content to frpc config file error: %v", err))
|
|
||||||
}
|
|
||||||
return nil, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// buildProxyStatusResp creates a ProxyStatusResp from proxy.WorkingStatus
|
|
||||||
func (c *Controller) buildProxyStatusResp(status *proxy.WorkingStatus) ProxyStatusResp {
|
|
||||||
psr := ProxyStatusResp{
|
|
||||||
Name: status.Name,
|
|
||||||
Type: status.Type,
|
|
||||||
Status: status.Phase,
|
|
||||||
Err: status.Err,
|
|
||||||
}
|
|
||||||
baseCfg := status.Cfg.GetBaseConfig()
|
|
||||||
if baseCfg.LocalPort != 0 {
|
|
||||||
psr.LocalAddr = net.JoinHostPort(baseCfg.LocalIP, strconv.Itoa(baseCfg.LocalPort))
|
|
||||||
}
|
|
||||||
psr.Plugin = baseCfg.Plugin.Type
|
|
||||||
|
|
||||||
if status.Err == "" {
|
|
||||||
psr.RemoteAddr = status.RemoteAddr
|
|
||||||
if slices.Contains([]string{"tcp", "udp"}, status.Type) {
|
|
||||||
psr.RemoteAddr = c.serverAddr + psr.RemoteAddr
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return psr
|
|
||||||
}
|
|
||||||
@@ -1,29 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
// StatusResp is the response for GET /api/status
|
|
||||||
type StatusResp map[string][]ProxyStatusResp
|
|
||||||
|
|
||||||
// ProxyStatusResp contains proxy status information
|
|
||||||
type ProxyStatusResp struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Type string `json:"type"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
Err string `json:"err"`
|
|
||||||
LocalAddr string `json:"local_addr"`
|
|
||||||
Plugin string `json:"plugin"`
|
|
||||||
RemoteAddr string `json:"remote_addr"`
|
|
||||||
}
|
|
||||||
@@ -16,9 +16,7 @@ package client
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
|
||||||
"net"
|
"net"
|
||||||
"strings"
|
|
||||||
"sync/atomic"
|
"sync/atomic"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -45,8 +43,8 @@ type SessionContext struct {
|
|||||||
Conn net.Conn
|
Conn net.Conn
|
||||||
// Indicates whether the connection is encrypted.
|
// Indicates whether the connection is encrypted.
|
||||||
ConnEncrypted bool
|
ConnEncrypted bool
|
||||||
// Auth runtime used for login, heartbeats, and encryption.
|
// Sets authentication based on selected method
|
||||||
Auth *auth.ClientAuth
|
AuthSetter auth.Setter
|
||||||
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
|
// Connector is used to create new connections, which could be real TCP connections or virtual streams.
|
||||||
Connector Connector
|
Connector Connector
|
||||||
// Virtual net controller
|
// Virtual net controller
|
||||||
@@ -93,7 +91,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
|||||||
ctl.lastPong.Store(time.Now())
|
ctl.lastPong.Store(time.Now())
|
||||||
|
|
||||||
if sessionCtx.ConnEncrypted {
|
if sessionCtx.ConnEncrypted {
|
||||||
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, sessionCtx.Auth.EncryptionKey())
|
cryptoRW, err := netpkg.NewCryptoReadWriter(sessionCtx.Conn, []byte(sessionCtx.Common.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -104,7 +102,7 @@ func NewControl(ctx context.Context, sessionCtx *SessionContext) (*Control, erro
|
|||||||
ctl.registerMsgHandlers()
|
ctl.registerMsgHandlers()
|
||||||
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
ctl.msgTransporter = transport.NewMessageTransporter(ctl.msgDispatcher)
|
||||||
|
|
||||||
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, sessionCtx.Auth.EncryptionKey(), ctl.msgTransporter, sessionCtx.VnetController)
|
ctl.pm = proxy.NewManager(ctl.ctx, sessionCtx.Common, ctl.msgTransporter, sessionCtx.VnetController)
|
||||||
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
|
ctl.vm = visitor.NewManager(ctl.ctx, sessionCtx.RunID, sessionCtx.Common,
|
||||||
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
|
ctl.connectServer, ctl.msgTransporter, sessionCtx.VnetController)
|
||||||
return ctl, nil
|
return ctl, nil
|
||||||
@@ -135,7 +133,7 @@ func (ctl *Control) handleReqWorkConn(_ msg.Message) {
|
|||||||
m := &msg.NewWorkConn{
|
m := &msg.NewWorkConn{
|
||||||
RunID: ctl.sessionCtx.RunID,
|
RunID: ctl.sessionCtx.RunID,
|
||||||
}
|
}
|
||||||
if err = ctl.sessionCtx.Auth.Setter.SetNewWorkConn(m); err != nil {
|
if err = ctl.sessionCtx.AuthSetter.SetNewWorkConn(m); err != nil {
|
||||||
xl.Warnf("error during NewWorkConn authentication: %v", err)
|
xl.Warnf("error during NewWorkConn authentication: %v", err)
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
return
|
return
|
||||||
@@ -169,44 +167,9 @@ func (ctl *Control) handleNewProxyResp(m msg.Message) {
|
|||||||
// Start a new proxy handler if no error got
|
// Start a new proxy handler if no error got
|
||||||
err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)
|
err := ctl.pm.StartProxy(inMsg.ProxyName, inMsg.RemoteAddr, inMsg.Error)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("[%s] 启动失败: %v", inMsg.ProxyName, err)
|
xl.Warnf("[%s] start error: %v", inMsg.ProxyName, err)
|
||||||
} else {
|
} else {
|
||||||
xl.Infof("[%s] 成功启动隧道", inMsg.ProxyName)
|
xl.Infof("[%s] start proxy success", inMsg.ProxyName)
|
||||||
if inMsg.RemoteAddr != "" {
|
|
||||||
// Get proxy type to format access message
|
|
||||||
if status, ok := ctl.pm.GetProxyStatus(inMsg.ProxyName); ok {
|
|
||||||
proxyType := status.Type
|
|
||||||
remoteAddr := inMsg.RemoteAddr
|
|
||||||
var accessMsg string
|
|
||||||
|
|
||||||
switch proxyType {
|
|
||||||
case "tcp", "udp", "stcp", "xtcp", "sudp", "tcpmux":
|
|
||||||
// If remoteAddr only contains port (e.g., ":8080"), prepend server address
|
|
||||||
if strings.HasPrefix(remoteAddr, ":") {
|
|
||||||
serverAddr := ctl.sessionCtx.Common.ServerAddr
|
|
||||||
remoteAddr = serverAddr + remoteAddr
|
|
||||||
}
|
|
||||||
accessMsg = fmt.Sprintf("您可通过 %s 访问您的服务", remoteAddr)
|
|
||||||
case "http", "https":
|
|
||||||
// Format as URL with protocol
|
|
||||||
protocol := proxyType
|
|
||||||
addr := remoteAddr
|
|
||||||
// Remove standard ports for cleaner URL
|
|
||||||
if proxyType == "http" && strings.HasSuffix(addr, ":80") {
|
|
||||||
addr = strings.TrimSuffix(addr, ":80")
|
|
||||||
} else if proxyType == "https" && strings.HasSuffix(addr, ":443") {
|
|
||||||
addr = strings.TrimSuffix(addr, ":443")
|
|
||||||
}
|
|
||||||
accessMsg = fmt.Sprintf("您可通过 %s://%s 访问您的服务", protocol, addr)
|
|
||||||
default:
|
|
||||||
accessMsg = fmt.Sprintf("您可通过 %s 访问您的服务", remoteAddr)
|
|
||||||
}
|
|
||||||
|
|
||||||
xl.Infof("[%s] %s", inMsg.ProxyName, accessMsg)
|
|
||||||
} else {
|
|
||||||
xl.Infof("[%s] 您可通过 %s 访问您的服务", inMsg.ProxyName, inMsg.RemoteAddr)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,7 +243,7 @@ func (ctl *Control) heartbeatWorker() {
|
|||||||
sendHeartBeat := func() (bool, error) {
|
sendHeartBeat := func() (bool, error) {
|
||||||
xl.Debugf("send heartbeat to server")
|
xl.Debugf("send heartbeat to server")
|
||||||
pingMsg := &msg.Ping{}
|
pingMsg := &msg.Ping{}
|
||||||
if err := ctl.sessionCtx.Auth.Setter.SetPing(pingMsg); err != nil {
|
if err := ctl.sessionCtx.AuthSetter.SetPing(pingMsg); err != nil {
|
||||||
xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
|
xl.Warnf("error during ping authentication: %v, skip sending ping message", err)
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -19,9 +19,7 @@ import (
|
|||||||
"io"
|
"io"
|
||||||
"net"
|
"net"
|
||||||
"reflect"
|
"reflect"
|
||||||
"slices"
|
|
||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
|
||||||
"sync"
|
"sync"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
@@ -59,7 +57,6 @@ func NewProxy(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
pxyConf v1.ProxyConfigurer,
|
pxyConf v1.ProxyConfigurer,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
encryptionKey []byte,
|
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
vnetController *vnet.Controller,
|
vnetController *vnet.Controller,
|
||||||
) (pxy Proxy) {
|
) (pxy Proxy) {
|
||||||
@@ -71,9 +68,7 @@ func NewProxy(
|
|||||||
|
|
||||||
baseProxy := BaseProxy{
|
baseProxy := BaseProxy{
|
||||||
baseCfg: pxyConf.GetBaseConfig(),
|
baseCfg: pxyConf.GetBaseConfig(),
|
||||||
configurer: pxyConf,
|
|
||||||
clientCfg: clientCfg,
|
clientCfg: clientCfg,
|
||||||
encryptionKey: encryptionKey,
|
|
||||||
limiter: limiter,
|
limiter: limiter,
|
||||||
msgTransporter: msgTransporter,
|
msgTransporter: msgTransporter,
|
||||||
vnetController: vnetController,
|
vnetController: vnetController,
|
||||||
@@ -90,9 +85,7 @@ func NewProxy(
|
|||||||
|
|
||||||
type BaseProxy struct {
|
type BaseProxy struct {
|
||||||
baseCfg *v1.ProxyBaseConfig
|
baseCfg *v1.ProxyBaseConfig
|
||||||
configurer v1.ProxyConfigurer
|
|
||||||
clientCfg *v1.ClientCommonConfig
|
clientCfg *v1.ClientCommonConfig
|
||||||
encryptionKey []byte
|
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
vnetController *vnet.Controller
|
vnetController *vnet.Controller
|
||||||
limiter *rate.Limiter
|
limiter *rate.Limiter
|
||||||
@@ -110,7 +103,6 @@ func (pxy *BaseProxy) Run() error {
|
|||||||
if pxy.baseCfg.Plugin.Type != "" {
|
if pxy.baseCfg.Plugin.Type != "" {
|
||||||
p, err := plugin.Create(pxy.baseCfg.Plugin.Type, plugin.PluginContext{
|
p, err := plugin.Create(pxy.baseCfg.Plugin.Type, plugin.PluginContext{
|
||||||
Name: pxy.baseCfg.Name,
|
Name: pxy.baseCfg.Name,
|
||||||
HostAllowList: pxy.getPluginHostAllowList(),
|
|
||||||
VnetController: pxy.vnetController,
|
VnetController: pxy.vnetController,
|
||||||
}, pxy.baseCfg.Plugin.ClientPluginOptions)
|
}, pxy.baseCfg.Plugin.ClientPluginOptions)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -121,39 +113,6 @@ func (pxy *BaseProxy) Run() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pxy *BaseProxy) getPluginHostAllowList() []string {
|
|
||||||
dedupHosts := make([]string, 0)
|
|
||||||
addHost := func(host string) {
|
|
||||||
host = strings.TrimSpace(strings.ToLower(host))
|
|
||||||
if host == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
// autocert.HostWhitelist only supports exact host names.
|
|
||||||
if strings.Contains(host, "*") {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if !slices.Contains(dedupHosts, host) {
|
|
||||||
dedupHosts = append(dedupHosts, host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
switch cfg := pxy.configurer.(type) {
|
|
||||||
case *v1.HTTPProxyConfig:
|
|
||||||
for _, host := range cfg.CustomDomains {
|
|
||||||
addHost(host)
|
|
||||||
}
|
|
||||||
case *v1.HTTPSProxyConfig:
|
|
||||||
for _, host := range cfg.CustomDomains {
|
|
||||||
addHost(host)
|
|
||||||
}
|
|
||||||
case *v1.TCPMuxProxyConfig:
|
|
||||||
for _, host := range cfg.CustomDomains {
|
|
||||||
addHost(host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return dedupHosts
|
|
||||||
}
|
|
||||||
|
|
||||||
func (pxy *BaseProxy) Close() {
|
func (pxy *BaseProxy) Close() {
|
||||||
if pxy.proxyPlugin != nil {
|
if pxy.proxyPlugin != nil {
|
||||||
pxy.proxyPlugin.Close()
|
pxy.proxyPlugin.Close()
|
||||||
@@ -170,7 +129,7 @@ func (pxy *BaseProxy) InWorkConn(conn net.Conn, m *msg.StartWorkConn) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
pxy.HandleTCPWorkConnection(conn, m, pxy.encryptionKey)
|
pxy.HandleTCPWorkConnection(conn, m, []byte(pxy.clientCfg.Auth.Token))
|
||||||
}
|
}
|
||||||
|
|
||||||
// Common handler for tcp work connections.
|
// Common handler for tcp work connections.
|
||||||
|
|||||||
@@ -40,8 +40,7 @@ type Manager struct {
|
|||||||
closed bool
|
closed bool
|
||||||
mu sync.RWMutex
|
mu sync.RWMutex
|
||||||
|
|
||||||
encryptionKey []byte
|
clientCfg *v1.ClientCommonConfig
|
||||||
clientCfg *v1.ClientCommonConfig
|
|
||||||
|
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
}
|
}
|
||||||
@@ -49,7 +48,6 @@ type Manager struct {
|
|||||||
func NewManager(
|
func NewManager(
|
||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
encryptionKey []byte,
|
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
vnetController *vnet.Controller,
|
vnetController *vnet.Controller,
|
||||||
) *Manager {
|
) *Manager {
|
||||||
@@ -58,7 +56,6 @@ func NewManager(
|
|||||||
msgTransporter: msgTransporter,
|
msgTransporter: msgTransporter,
|
||||||
vnetController: vnetController,
|
vnetController: vnetController,
|
||||||
closed: false,
|
closed: false,
|
||||||
encryptionKey: encryptionKey,
|
|
||||||
clientCfg: clientCfg,
|
clientCfg: clientCfg,
|
||||||
ctx: ctx,
|
ctx: ctx,
|
||||||
}
|
}
|
||||||
@@ -159,14 +156,14 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(delPxyNames) > 0 {
|
if len(delPxyNames) > 0 {
|
||||||
xl.Infof("隧道移除: %s", delPxyNames)
|
xl.Infof("proxy removed: %s", delPxyNames)
|
||||||
}
|
}
|
||||||
|
|
||||||
addPxyNames := make([]string, 0)
|
addPxyNames := make([]string, 0)
|
||||||
for _, cfg := range proxyCfgs {
|
for _, cfg := range proxyCfgs {
|
||||||
name := cfg.GetBaseConfig().Name
|
name := cfg.GetBaseConfig().Name
|
||||||
if _, ok := pm.proxies[name]; !ok {
|
if _, ok := pm.proxies[name]; !ok {
|
||||||
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.encryptionKey, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
|
pxy := NewWrapper(pm.ctx, cfg, pm.clientCfg, pm.HandleEvent, pm.msgTransporter, pm.vnetController)
|
||||||
if pm.inWorkConnCallback != nil {
|
if pm.inWorkConnCallback != nil {
|
||||||
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
|
pxy.SetInWorkConnCallback(pm.inWorkConnCallback)
|
||||||
}
|
}
|
||||||
@@ -177,6 +174,6 @@ func (pm *Manager) UpdateAll(proxyCfgs []v1.ProxyConfigurer) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
if len(addPxyNames) > 0 {
|
if len(addPxyNames) > 0 {
|
||||||
xl.Infof("添加隧道: %s", addPxyNames)
|
xl.Infof("proxy added: %s", addPxyNames)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -92,7 +92,6 @@ func NewWrapper(
|
|||||||
ctx context.Context,
|
ctx context.Context,
|
||||||
cfg v1.ProxyConfigurer,
|
cfg v1.ProxyConfigurer,
|
||||||
clientCfg *v1.ClientCommonConfig,
|
clientCfg *v1.ClientCommonConfig,
|
||||||
encryptionKey []byte,
|
|
||||||
eventHandler event.Handler,
|
eventHandler event.Handler,
|
||||||
msgTransporter transport.MessageTransporter,
|
msgTransporter transport.MessageTransporter,
|
||||||
vnetController *vnet.Controller,
|
vnetController *vnet.Controller,
|
||||||
@@ -123,7 +122,7 @@ func NewWrapper(
|
|||||||
xl.Tracef("enable health check monitor")
|
xl.Tracef("enable health check monitor")
|
||||||
}
|
}
|
||||||
|
|
||||||
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, encryptionKey, pw.msgTransporter, pw.vnetController)
|
pw.pxy = NewProxy(pw.ctx, pw.Cfg, clientCfg, pw.msgTransporter, pw.vnetController)
|
||||||
return pw
|
return pw
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -91,7 +91,7 @@ func (pxy *SUDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
|||||||
@@ -90,7 +90,7 @@ func (pxy *UDPProxy) Close() {
|
|||||||
|
|
||||||
func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
||||||
xl := pxy.xl
|
xl := pxy.xl
|
||||||
xl.Infof("收到一条新的 UDP 代理工作连接, %s", conn.RemoteAddr().String())
|
xl.Infof("incoming a new work connection for udp proxy, %s", conn.RemoteAddr().String())
|
||||||
// close resources related with old workConn
|
// close resources related with old workConn
|
||||||
pxy.Close()
|
pxy.Close()
|
||||||
|
|
||||||
@@ -102,7 +102,7 @@ func (pxy *UDPProxy) InWorkConn(conn net.Conn, _ *msg.StartWorkConn) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
rwc, err = libio.WithEncryption(rwc, []byte(pxy.clientCfg.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
conn.Close()
|
conn.Close()
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
|
|||||||
@@ -111,8 +111,8 @@ type Service struct {
|
|||||||
// Uniq id got from frps, it will be attached to loginMsg.
|
// Uniq id got from frps, it will be attached to loginMsg.
|
||||||
runID string
|
runID string
|
||||||
|
|
||||||
// Auth runtime and encryption materials
|
// Sets authentication based on selected method
|
||||||
auth *auth.ClientAuth
|
authSetter auth.Setter
|
||||||
|
|
||||||
// web server for admin UI and apis
|
// web server for admin UI and apis
|
||||||
webServer *httppkg.Server
|
webServer *httppkg.Server
|
||||||
@@ -155,14 +155,14 @@ func NewService(options ServiceOptions) (*Service, error) {
|
|||||||
webServer = ws
|
webServer = ws
|
||||||
}
|
}
|
||||||
|
|
||||||
authRuntime, err := auth.BuildClientAuth(&options.Common.Auth)
|
authSetter, err := auth.NewAuthSetter(options.Common.Auth)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
|
|
||||||
s := &Service{
|
s := &Service{
|
||||||
ctx: context.Background(),
|
ctx: context.Background(),
|
||||||
auth: authRuntime,
|
authSetter: authSetter,
|
||||||
webServer: webServer,
|
webServer: webServer,
|
||||||
common: options.Common,
|
common: options.Common,
|
||||||
configFilePath: options.ConfigFilePath,
|
configFilePath: options.ConfigFilePath,
|
||||||
@@ -219,7 +219,7 @@ func (svr *Service) Run(ctx context.Context) error {
|
|||||||
if svr.ctl == nil {
|
if svr.ctl == nil {
|
||||||
cancelCause := cancelErr{}
|
cancelCause := cancelErr{}
|
||||||
_ = errors.As(context.Cause(svr.ctx), &cancelCause)
|
_ = errors.As(context.Cause(svr.ctx), &cancelCause)
|
||||||
return fmt.Errorf("登录服务器失败: %v. 启用 loginFailExit 后,将不再尝试重试", cancelCause.Err)
|
return fmt.Errorf("login to the server failed: %v. With loginFailExit enabled, no additional retries will be attempted", cancelCause.Err)
|
||||||
}
|
}
|
||||||
|
|
||||||
go svr.keepControllerWorking()
|
go svr.keepControllerWorking()
|
||||||
@@ -281,15 +281,11 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
hostname, _ := os.Hostname()
|
|
||||||
|
|
||||||
loginMsg := &msg.Login{
|
loginMsg := &msg.Login{
|
||||||
Arch: runtime.GOARCH,
|
Arch: runtime.GOARCH,
|
||||||
Os: runtime.GOOS,
|
Os: runtime.GOOS,
|
||||||
Hostname: hostname,
|
|
||||||
PoolCount: svr.common.Transport.PoolCount,
|
PoolCount: svr.common.Transport.PoolCount,
|
||||||
User: svr.common.User,
|
User: svr.common.User,
|
||||||
ClientID: svr.common.ClientID,
|
|
||||||
Version: version.Full(),
|
Version: version.Full(),
|
||||||
Timestamp: time.Now().Unix(),
|
Timestamp: time.Now().Unix(),
|
||||||
RunID: svr.runID,
|
RunID: svr.runID,
|
||||||
@@ -300,7 +296,7 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Add auth
|
// Add auth
|
||||||
if err = svr.auth.Setter.SetLogin(loginMsg); err != nil {
|
if err = svr.authSetter.SetLogin(loginMsg); err != nil {
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -324,7 +320,7 @@ func (svr *Service) login() (conn net.Conn, connector Connector, err error) {
|
|||||||
svr.runID = loginRespMsg.RunID
|
svr.runID = loginRespMsg.RunID
|
||||||
xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID})
|
xl.AddPrefix(xlog.LogPrefix{Name: "runID", Value: svr.runID})
|
||||||
|
|
||||||
xl.Infof("登录服务器成功, 获取 run id [%s]", loginRespMsg.RunID)
|
xl.Infof("login to server success, get run id [%s]", loginRespMsg.RunID)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -332,10 +328,10 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
|
|||||||
xl := xlog.FromContextSafe(svr.ctx)
|
xl := xlog.FromContextSafe(svr.ctx)
|
||||||
|
|
||||||
loginFunc := func() (bool, error) {
|
loginFunc := func() (bool, error) {
|
||||||
xl.Infof("尝试连接到服务器...")
|
xl.Infof("try to connect to server...")
|
||||||
conn, connector, err := svr.login()
|
conn, connector, err := svr.login()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("连接服务器错误: %v", err)
|
xl.Warnf("connect to server error: %v", err)
|
||||||
if firstLoginExit {
|
if firstLoginExit {
|
||||||
svr.cancel(cancelErr{Err: err})
|
svr.cancel(cancelErr{Err: err})
|
||||||
}
|
}
|
||||||
@@ -354,7 +350,7 @@ func (svr *Service) loopLoginUntilSuccess(maxInterval time.Duration, firstLoginE
|
|||||||
RunID: svr.runID,
|
RunID: svr.runID,
|
||||||
Conn: conn,
|
Conn: conn,
|
||||||
ConnEncrypted: connEncrypted,
|
ConnEncrypted: connEncrypted,
|
||||||
Auth: svr.auth,
|
AuthSetter: svr.authSetter,
|
||||||
Connector: connector,
|
Connector: connector,
|
||||||
VnetController: svr.vnetController,
|
VnetController: svr.vnetController,
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
_ "github.com/fatedier/frp/assets/frpc"
|
||||||
"github.com/fatedier/frp/cmd/frpc/sub"
|
"github.com/fatedier/frp/cmd/frpc/sub"
|
||||||
"github.com/fatedier/frp/pkg/util/system"
|
"github.com/fatedier/frp/pkg/util/system"
|
||||||
_ "github.com/fatedier/frp/web/frpc"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -54,11 +54,7 @@ func NewAdminCommand(name, short string, handler func(*v1.ClientCommonConfig) er
|
|||||||
Use: name,
|
Use: name,
|
||||||
Short: short,
|
Short: short,
|
||||||
Run: func(cmd *cobra.Command, args []string) {
|
Run: func(cmd *cobra.Command, args []string) {
|
||||||
if len(cfgFiles) == 0 || cfgFiles[0] == "" {
|
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||||
fmt.Println("frpc: the configuration file is not specified")
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
cfg, _, _, _, err := config.LoadClientConfig(cfgFiles[0], strictConfigMode)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -48,17 +48,8 @@ var natholeDiscoveryCmd = &cobra.Command{
|
|||||||
Short: "Discover nathole information from stun server",
|
Short: "Discover nathole information from stun server",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
// ignore error here, because we can use command line pameters
|
// ignore error here, because we can use command line pameters
|
||||||
var cfg *v1.ClientCommonConfig
|
cfg, _, _, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||||
if len(cfgFiles) > 0 && cfgFiles[0] != "" {
|
if err != nil {
|
||||||
_, _, _, _, err := config.LoadClientConfig(cfgFiles[0], strictConfigMode)
|
|
||||||
if err != nil {
|
|
||||||
cfg = &v1.ClientCommonConfig{}
|
|
||||||
if err := cfg.Complete(); err != nil {
|
|
||||||
fmt.Printf("failed to complete config: %v\n", err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
cfg = &v1.ClientCommonConfig{}
|
cfg = &v1.ClientCommonConfig{}
|
||||||
if err := cfg.Complete(); err != nil {
|
if err := cfg.Complete(); err != nil {
|
||||||
fmt.Printf("failed to complete config: %v\n", err)
|
fmt.Printf("failed to complete config: %v\n", err)
|
||||||
|
|||||||
@@ -80,8 +80,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
|||||||
}
|
}
|
||||||
|
|
||||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil {
|
||||||
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -92,7 +91,7 @@ func NewProxyCommand(name string, c v1.ProxyConfigurer, clientCfg *v1.ClientComm
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "", "", "")
|
err := startService(clientCfg, []v1.ProxyConfigurer{c}, nil, unsafeFeatures, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -111,8 +110,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
if _, err := validation.ValidateClientCommonConfig(clientCfg, unsafeFeatures); err != nil {
|
||||||
if _, err := validator.ValidateClientCommonConfig(clientCfg); err != nil {
|
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
@@ -123,7 +121,7 @@ func NewVisitorCommand(name string, c v1.VisitorConfigurer, clientCfg *v1.Client
|
|||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "", "", "")
|
err := startService(clientCfg, nil, []v1.VisitorConfigurer{c}, unsafeFeatures, "")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
|
|||||||
@@ -16,11 +16,8 @@ package sub
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
"context"
|
||||||
"encoding/base64"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"io/fs"
|
"io/fs"
|
||||||
"net/http"
|
|
||||||
"os"
|
"os"
|
||||||
"os/signal"
|
"os/signal"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
@@ -37,28 +34,24 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/policy/featuregate"
|
"github.com/fatedier/frp/pkg/policy/featuregate"
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
"github.com/fatedier/frp/pkg/util/banner"
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
cfgFiles []string
|
cfgFile string
|
||||||
cfgDir string
|
cfgDir string
|
||||||
showVersion bool
|
showVersion bool
|
||||||
strictConfigMode bool
|
strictConfigMode bool
|
||||||
allowUnsafe []string
|
allowUnsafe []string
|
||||||
authTokens []string
|
|
||||||
|
|
||||||
bannerDisplayed bool
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
rootCmd.PersistentFlags().StringSliceVarP(&cfgFiles, "config", "c", []string{"./frpc.ini"}, "config files of frpc (support multiple files)")
|
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().StringVarP(&cfgDir, "config_dir", "", "", "config directory, run one frpc service for each file in config directory")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
|
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frpc")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
|
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause an errors")
|
||||||
rootCmd.PersistentFlags().StringSliceVarP(&authTokens, "token", "t", []string{}, "authentication tokens in format 'id:token' (LoliaFRP only)")
|
|
||||||
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
||||||
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
|
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ClientUnsafeFeatures, ", ")))
|
||||||
}
|
}
|
||||||
@@ -74,30 +67,15 @@ var rootCmd = &cobra.Command{
|
|||||||
|
|
||||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
||||||
|
|
||||||
// If authTokens is provided, fetch config from API
|
|
||||||
if len(authTokens) > 0 {
|
|
||||||
err := runClientWithTokens(authTokens, unsafeFeatures)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Println(err)
|
|
||||||
os.Exit(1)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
|
// If cfgDir is not empty, run multiple frpc service for each config file in cfgDir.
|
||||||
|
// Note that it's only designed for testing. It's not guaranteed to be stable.
|
||||||
if cfgDir != "" {
|
if cfgDir != "" {
|
||||||
_ = runMultipleClients(cfgDir, unsafeFeatures)
|
_ = runMultipleClients(cfgDir, unsafeFeatures)
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// If multiple config files are specified, run one frpc service for each file
|
|
||||||
if len(cfgFiles) > 1 {
|
|
||||||
runMultipleClientsFromFiles(cfgFiles, unsafeFeatures)
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Do not show command usage here.
|
// Do not show command usage here.
|
||||||
err := runClient(cfgFiles[0], unsafeFeatures)
|
err := runClient(cfgFile, unsafeFeatures)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
@@ -127,29 +105,6 @@ func runMultipleClients(cfgDir string, unsafeFeatures *security.UnsafeFeatures)
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func runMultipleClientsFromFiles(cfgFiles []string, unsafeFeatures *security.UnsafeFeatures) {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
// Display banner first
|
|
||||||
banner.DisplayBanner()
|
|
||||||
bannerDisplayed = true
|
|
||||||
log.Infof("检测到 %d 个配置文件,将启动多个 frpc 服务实例", len(cfgFiles))
|
|
||||||
|
|
||||||
for _, cfgFile := range cfgFiles {
|
|
||||||
wg.Add(1)
|
|
||||||
// Add a small delay to avoid log output mixing
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
go func(path string) {
|
|
||||||
defer wg.Done()
|
|
||||||
err := runClient(path, unsafeFeatures)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("\n配置文件 [%s] 启动失败: %v\n", path, err)
|
|
||||||
}
|
|
||||||
}(cfgFile)
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
}
|
|
||||||
|
|
||||||
func Execute() {
|
func Execute() {
|
||||||
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
|
rootCmd.SetGlobalNormalizationFunc(config.WordSepNormalizeFunc)
|
||||||
if err := rootCmd.Execute(); err != nil {
|
if err := rootCmd.Execute(); err != nil {
|
||||||
@@ -187,8 +142,7 @@ func runClient(cfgFilePath string, unsafeFeatures *security.UnsafeFeatures) erro
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath)
|
||||||
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, cfgFilePath, "", "")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func startService(
|
func startService(
|
||||||
@@ -197,25 +151,12 @@ func startService(
|
|||||||
visitorCfgs []v1.VisitorConfigurer,
|
visitorCfgs []v1.VisitorConfigurer,
|
||||||
unsafeFeatures *security.UnsafeFeatures,
|
unsafeFeatures *security.UnsafeFeatures,
|
||||||
cfgFile string,
|
cfgFile string,
|
||||||
nodeName string,
|
|
||||||
tunnelRemark string,
|
|
||||||
) error {
|
) error {
|
||||||
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
log.InitLogger(cfg.Log.To, cfg.Log.Level, int(cfg.Log.MaxDays), cfg.Log.DisablePrintColor)
|
||||||
|
|
||||||
// Display banner only once before starting the first service
|
|
||||||
if !bannerDisplayed {
|
|
||||||
banner.DisplayBanner()
|
|
||||||
bannerDisplayed = true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Display node information if available
|
|
||||||
if nodeName != "" {
|
|
||||||
log.Info("已获取到配置文件", "隧道名称", tunnelRemark, "使用节点", nodeName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if cfgFile != "" {
|
if cfgFile != "" {
|
||||||
log.Infof("启动 frpc 服务 [%s]", cfgFile)
|
log.Infof("start frpc service for config file [%s]", cfgFile)
|
||||||
defer log.Infof("frpc 服务 [%s] 已停止", cfgFile)
|
defer log.Infof("frpc service for config file [%s] stopped", cfgFile)
|
||||||
}
|
}
|
||||||
svr, err := client.NewService(client.ServiceOptions{
|
svr, err := client.NewService(client.ServiceOptions{
|
||||||
Common: cfg,
|
Common: cfg,
|
||||||
@@ -235,187 +176,3 @@ func startService(
|
|||||||
}
|
}
|
||||||
return svr.Run(context.Background())
|
return svr.Run(context.Background())
|
||||||
}
|
}
|
||||||
|
|
||||||
// APIResponse represents the response from LoliaFRP API
|
|
||||||
type APIResponse struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Msg string `json:"msg"`
|
|
||||||
Data struct {
|
|
||||||
Config string `json:"config"`
|
|
||||||
NodeName string `json:"node_name"`
|
|
||||||
TunnelRemark string `json:"tunnel_remark"`
|
|
||||||
} `json:"data"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// TokenInfo stores parsed id and token from the -t parameter
|
|
||||||
type TokenInfo struct {
|
|
||||||
ID string
|
|
||||||
Token string
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClientWithTokens(tokens []string, unsafeFeatures *security.UnsafeFeatures) error {
|
|
||||||
// Parse all tokens (format: id:token)
|
|
||||||
tokenInfos := make([]TokenInfo, 0, len(tokens))
|
|
||||||
for _, t := range tokens {
|
|
||||||
parts := strings.SplitN(t, ":", 2)
|
|
||||||
if len(parts) != 2 {
|
|
||||||
return fmt.Errorf("invalid token format '%s', expected 'id:token'", t)
|
|
||||||
}
|
|
||||||
tokenInfos = append(tokenInfos, TokenInfo{
|
|
||||||
ID: strings.TrimSpace(parts[0]),
|
|
||||||
Token: strings.TrimSpace(parts[1]),
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// Group tokens by token value (same token can have multiple IDs)
|
|
||||||
tokenToIDs := make(map[string][]string)
|
|
||||||
for _, ti := range tokenInfos {
|
|
||||||
tokenToIDs[ti.Token] = append(tokenToIDs[ti.Token], ti.ID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// If we have multiple different tokens, start one service for each token group
|
|
||||||
if len(tokenToIDs) > 1 {
|
|
||||||
return runMultipleClientsWithTokens(tokenToIDs, unsafeFeatures)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get the single token and all its IDs
|
|
||||||
var token string
|
|
||||||
var ids []string
|
|
||||||
for t, idList := range tokenToIDs {
|
|
||||||
token = t
|
|
||||||
ids = idList
|
|
||||||
break
|
|
||||||
}
|
|
||||||
|
|
||||||
return runClientWithTokenAndIDs(token, ids, unsafeFeatures)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClientWithTokenAndIDs(token string, ids []string, unsafeFeatures *security.UnsafeFeatures) error {
|
|
||||||
// Get API server address from environment variable
|
|
||||||
apiServer := os.Getenv("LOLIA_API")
|
|
||||||
if apiServer == "" {
|
|
||||||
apiServer = "https://api.lolia.link"
|
|
||||||
}
|
|
||||||
|
|
||||||
// Build URL with query parameters
|
|
||||||
url := fmt.Sprintf("%s/api/v1/tunnel/frpc/config?token=%s&id=%s", apiServer, token, strings.Join(ids, ","))
|
|
||||||
// #nosec G107 -- URL is constructed from trusted source (environment variable or hardcoded)
|
|
||||||
resp, err := http.Get(url)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to fetch config from API: %v", err)
|
|
||||||
}
|
|
||||||
defer resp.Body.Close()
|
|
||||||
|
|
||||||
if resp.StatusCode != http.StatusOK {
|
|
||||||
return fmt.Errorf("API returned status code: %d", resp.StatusCode)
|
|
||||||
}
|
|
||||||
|
|
||||||
var apiResp APIResponse
|
|
||||||
if err := json.NewDecoder(resp.Body).Decode(&apiResp); err != nil {
|
|
||||||
return fmt.Errorf("failed to decode API response: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
if apiResp.Code != 200 {
|
|
||||||
return fmt.Errorf("API error: %s", apiResp.Msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Decode base64 config
|
|
||||||
configBytes, err := base64.StdEncoding.DecodeString(apiResp.Data.Config)
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to decode base64 config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Load config directly from bytes
|
|
||||||
return runClientWithConfig(configBytes, unsafeFeatures, apiResp.Data.NodeName, apiResp.Data.TunnelRemark)
|
|
||||||
}
|
|
||||||
|
|
||||||
func runMultipleClientsWithTokens(tokenToIDs map[string][]string, unsafeFeatures *security.UnsafeFeatures) error {
|
|
||||||
var wg sync.WaitGroup
|
|
||||||
|
|
||||||
// Display banner first
|
|
||||||
banner.DisplayBanner()
|
|
||||||
bannerDisplayed = true
|
|
||||||
log.Infof("检测到 %d 个不同的 token,将并行启动多个 frpc 服务实例", len(tokenToIDs))
|
|
||||||
|
|
||||||
index := 0
|
|
||||||
for token, ids := range tokenToIDs {
|
|
||||||
wg.Add(1)
|
|
||||||
currentIndex := index
|
|
||||||
currentToken := token
|
|
||||||
currentIDs := ids
|
|
||||||
totalCount := len(tokenToIDs)
|
|
||||||
|
|
||||||
// Add a small delay to avoid log output mixing
|
|
||||||
time.Sleep(100 * time.Millisecond)
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
defer wg.Done()
|
|
||||||
maskedToken := currentToken
|
|
||||||
if len(maskedToken) > 6 {
|
|
||||||
maskedToken = maskedToken[:3] + "***" + maskedToken[len(maskedToken)-3:]
|
|
||||||
} else {
|
|
||||||
maskedToken = "***"
|
|
||||||
}
|
|
||||||
log.Infof("[%d/%d] 启动 token: %s (IDs: %v)", currentIndex+1, totalCount, maskedToken, currentIDs)
|
|
||||||
err := runClientWithTokenAndIDs(currentToken, currentIDs, unsafeFeatures)
|
|
||||||
if err != nil {
|
|
||||||
fmt.Printf("\nToken [%s] 启动失败: %v\n", maskedToken, err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
index++
|
|
||||||
}
|
|
||||||
wg.Wait()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func runClientWithConfig(configBytes []byte, unsafeFeatures *security.UnsafeFeatures, nodeName, tunnelRemark string) error {
|
|
||||||
// Render template first
|
|
||||||
renderedBytes, err := config.RenderWithTemplate(configBytes, config.GetValues())
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("failed to render template: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
var allCfg v1.ClientConfig
|
|
||||||
if err := config.LoadConfigure(renderedBytes, &allCfg, strictConfigMode); err != nil {
|
|
||||||
return fmt.Errorf("failed to parse config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := &allCfg.ClientCommonConfig
|
|
||||||
proxyCfgs := make([]v1.ProxyConfigurer, 0, len(allCfg.Proxies))
|
|
||||||
for _, c := range allCfg.Proxies {
|
|
||||||
proxyCfgs = append(proxyCfgs, c.ProxyConfigurer)
|
|
||||||
}
|
|
||||||
visitorCfgs := make([]v1.VisitorConfigurer, 0, len(allCfg.Visitors))
|
|
||||||
for _, c := range allCfg.Visitors {
|
|
||||||
visitorCfgs = append(visitorCfgs, c.VisitorConfigurer)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call Complete to fill in default values
|
|
||||||
if err := cfg.Complete(); err != nil {
|
|
||||||
return fmt.Errorf("failed to complete config: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Call Complete for all proxies to add name prefix (e.g., user.tunnel_name)
|
|
||||||
for _, c := range proxyCfgs {
|
|
||||||
c.Complete(cfg.User)
|
|
||||||
}
|
|
||||||
for _, c := range visitorCfgs {
|
|
||||||
c.Complete(cfg)
|
|
||||||
}
|
|
||||||
|
|
||||||
if len(cfg.FeatureGates) > 0 {
|
|
||||||
if err := featuregate.SetFromMap(cfg.FeatureGates); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
warning, err := validation.ValidateAllClientConfig(cfg, proxyCfgs, visitorCfgs, unsafeFeatures)
|
|
||||||
if warning != nil {
|
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
|
||||||
}
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
return startService(cfg, proxyCfgs, visitorCfgs, unsafeFeatures, "", nodeName, tunnelRemark)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -33,12 +33,11 @@ var verifyCmd = &cobra.Command{
|
|||||||
Use: "verify",
|
Use: "verify",
|
||||||
Short: "Verify that the configures is valid",
|
Short: "Verify that the configures is valid",
|
||||||
RunE: func(cmd *cobra.Command, args []string) error {
|
RunE: func(cmd *cobra.Command, args []string) error {
|
||||||
if len(cfgFiles) == 0 || cfgFiles[0] == "" {
|
if cfgFile == "" {
|
||||||
fmt.Println("frpc: the configuration file is not specified")
|
fmt.Println("frpc: the configuration file is not specified")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
cfgFile := cfgFiles[0]
|
|
||||||
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
cliCfg, proxyCfgs, visitorCfgs, _, err := config.LoadClientConfig(cfgFile, strictConfigMode)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println(err)
|
fmt.Println(err)
|
||||||
|
|||||||
@@ -15,9 +15,9 @@
|
|||||||
package main
|
package main
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
_ "github.com/fatedier/frp/assets/frps"
|
||||||
_ "github.com/fatedier/frp/pkg/metrics"
|
_ "github.com/fatedier/frp/pkg/metrics"
|
||||||
"github.com/fatedier/frp/pkg/util/system"
|
"github.com/fatedier/frp/pkg/util/system"
|
||||||
_ "github.com/fatedier/frp/web/frps"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func main() {
|
func main() {
|
||||||
|
|||||||
@@ -18,14 +18,12 @@ import (
|
|||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/spf13/cobra"
|
"github.com/spf13/cobra"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
"github.com/fatedier/frp/server"
|
"github.com/fatedier/frp/server"
|
||||||
@@ -35,7 +33,6 @@ var (
|
|||||||
cfgFile string
|
cfgFile string
|
||||||
showVersion bool
|
showVersion bool
|
||||||
strictConfigMode bool
|
strictConfigMode bool
|
||||||
allowUnsafe []string
|
|
||||||
|
|
||||||
serverCfg v1.ServerConfig
|
serverCfg v1.ServerConfig
|
||||||
)
|
)
|
||||||
@@ -44,8 +41,6 @@ func init() {
|
|||||||
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
|
rootCmd.PersistentFlags().StringVarP(&cfgFile, "config", "c", "", "config file of frps")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
|
rootCmd.PersistentFlags().BoolVarP(&showVersion, "version", "v", false, "version of frps")
|
||||||
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
|
rootCmd.PersistentFlags().BoolVarP(&strictConfigMode, "strict_config", "", true, "strict config parsing mode, unknown fields will cause errors")
|
||||||
rootCmd.PersistentFlags().StringSliceVarP(&allowUnsafe, "allow-unsafe", "", []string{},
|
|
||||||
fmt.Sprintf("allowed unsafe features, one or more of: %s", strings.Join(security.ServerUnsafeFeatures, ", ")))
|
|
||||||
|
|
||||||
config.RegisterServerConfigFlags(rootCmd, &serverCfg)
|
config.RegisterServerConfigFlags(rootCmd, &serverCfg)
|
||||||
}
|
}
|
||||||
@@ -82,9 +77,7 @@ var rootCmd = &cobra.Command{
|
|||||||
svrCfg = &serverCfg
|
svrCfg = &serverCfg
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
warning, err := validation.ValidateServerConfig(svrCfg)
|
||||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
|
||||||
warning, err := validator.ValidateServerConfig(svrCfg)
|
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -22,7 +22,6 @@ import (
|
|||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config"
|
"github.com/fatedier/frp/pkg/config"
|
||||||
"github.com/fatedier/frp/pkg/config/v1/validation"
|
"github.com/fatedier/frp/pkg/config/v1/validation"
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
@@ -43,9 +42,7 @@ var verifyCmd = &cobra.Command{
|
|||||||
os.Exit(1)
|
os.Exit(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
unsafeFeatures := security.NewUnsafeFeatures(allowUnsafe)
|
warning, err := validation.ValidateServerConfig(svrCfg)
|
||||||
validator := validation.NewConfigValidator(unsafeFeatures)
|
|
||||||
warning, err := validator.ValidateServerConfig(svrCfg)
|
|
||||||
if warning != nil {
|
if warning != nil {
|
||||||
fmt.Printf("WARNING: %v\n", warning)
|
fmt.Printf("WARNING: %v\n", warning)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,7 +1,5 @@
|
|||||||
# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
# This configuration file is for reference only. Please do not use this configuration directly to run the program as it may have various issues.
|
||||||
|
|
||||||
# Optional unique identifier for this frpc instance.
|
|
||||||
clientID = "your_client_id"
|
|
||||||
# your proxy name will be changed to {user}.{proxy}
|
# your proxy name will be changed to {user}.{proxy}
|
||||||
user = "your_name"
|
user = "your_name"
|
||||||
|
|
||||||
@@ -329,14 +327,6 @@ type = "https2http"
|
|||||||
localAddr = "127.0.0.1:80"
|
localAddr = "127.0.0.1:80"
|
||||||
crtPath = "./server.crt"
|
crtPath = "./server.crt"
|
||||||
keyPath = "./server.key"
|
keyPath = "./server.key"
|
||||||
# autoTLS can replace crtPath/keyPath and automatically apply/renew certificates.
|
|
||||||
# [proxies.plugin.autoTLS]
|
|
||||||
# enable = true
|
|
||||||
# email = "admin@example.com"
|
|
||||||
# cacheDir = "./.autotls-cache"
|
|
||||||
# hostAllowList is optional. If omitted, frpc will use customDomains automatically.
|
|
||||||
# hostAllowList = ["test.yourdomain.com"]
|
|
||||||
# caDirURL = "https://acme-v02.api.letsencrypt.org/directory"
|
|
||||||
hostHeaderRewrite = "127.0.0.1"
|
hostHeaderRewrite = "127.0.0.1"
|
||||||
requestHeaders.set.x-from-where = "frp"
|
requestHeaders.set.x-from-where = "frp"
|
||||||
|
|
||||||
@@ -349,14 +339,6 @@ type = "https2https"
|
|||||||
localAddr = "127.0.0.1:443"
|
localAddr = "127.0.0.1:443"
|
||||||
crtPath = "./server.crt"
|
crtPath = "./server.crt"
|
||||||
keyPath = "./server.key"
|
keyPath = "./server.key"
|
||||||
# autoTLS can replace crtPath/keyPath and automatically apply/renew certificates.
|
|
||||||
# [proxies.plugin.autoTLS]
|
|
||||||
# enable = true
|
|
||||||
# email = "admin@example.com"
|
|
||||||
# cacheDir = "./.autotls-cache"
|
|
||||||
# hostAllowList is optional. If omitted, frpc will use customDomains automatically.
|
|
||||||
# hostAllowList = ["test.yourdomain.com"]
|
|
||||||
# caDirURL = "https://acme-v02.api.letsencrypt.org/directory"
|
|
||||||
hostHeaderRewrite = "127.0.0.1"
|
hostHeaderRewrite = "127.0.0.1"
|
||||||
requestHeaders.set.x-from-where = "frp"
|
requestHeaders.set.x-from-where = "frp"
|
||||||
|
|
||||||
@@ -370,15 +352,6 @@ localAddr = "127.0.0.1:443"
|
|||||||
hostHeaderRewrite = "127.0.0.1"
|
hostHeaderRewrite = "127.0.0.1"
|
||||||
requestHeaders.set.x-from-where = "frp"
|
requestHeaders.set.x-from-where = "frp"
|
||||||
|
|
||||||
[[proxies]]
|
|
||||||
name = "plugin_http2https_redirect"
|
|
||||||
type = "http"
|
|
||||||
customDomains = ["test.yourdomain.com"]
|
|
||||||
[proxies.plugin]
|
|
||||||
type = "http2https_redirect"
|
|
||||||
# Optional. Defaults to 443. Set this if the HTTPS entry is exposed on a non-standard port.
|
|
||||||
# httpsPort = 443
|
|
||||||
|
|
||||||
[[proxies]]
|
[[proxies]]
|
||||||
name = "plugin_http2http"
|
name = "plugin_http2http"
|
||||||
type = "tcp"
|
type = "tcp"
|
||||||
@@ -398,14 +371,6 @@ type = "tls2raw"
|
|||||||
localAddr = "127.0.0.1:80"
|
localAddr = "127.0.0.1:80"
|
||||||
crtPath = "./server.crt"
|
crtPath = "./server.crt"
|
||||||
keyPath = "./server.key"
|
keyPath = "./server.key"
|
||||||
# autoTLS can replace crtPath/keyPath and automatically apply/renew certificates.
|
|
||||||
# [proxies.plugin.autoTLS]
|
|
||||||
# enable = true
|
|
||||||
# email = "admin@example.com"
|
|
||||||
# cacheDir = "./.autotls-cache"
|
|
||||||
# hostAllowList is optional. If omitted, frpc will use customDomains automatically.
|
|
||||||
# hostAllowList = ["test.yourdomain.com"]
|
|
||||||
# caDirURL = "https://acme-v02.api.letsencrypt.org/directory"
|
|
||||||
|
|
||||||
[[proxies]]
|
[[proxies]]
|
||||||
name = "secret_tcp"
|
name = "secret_tcp"
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
FROM node:22 AS web-builder
|
FROM golang:1.24 AS building
|
||||||
|
|
||||||
WORKDIR /web/frpc
|
|
||||||
COPY web/frpc/ ./
|
|
||||||
RUN npm install
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
FROM golang:1.25 AS building
|
|
||||||
|
|
||||||
COPY . /building
|
COPY . /building
|
||||||
COPY --from=web-builder /web/frpc/dist /building/web/frpc/dist
|
|
||||||
WORKDIR /building
|
WORKDIR /building
|
||||||
|
|
||||||
RUN env CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -tags frpc -o bin/frpc ./cmd/frpc
|
RUN make frpc
|
||||||
|
|
||||||
FROM alpine:3
|
FROM alpine:3
|
||||||
|
|
||||||
|
|||||||
@@ -1,17 +1,9 @@
|
|||||||
FROM node:22 AS web-builder
|
FROM golang:1.24 AS building
|
||||||
|
|
||||||
WORKDIR /web/frps
|
|
||||||
COPY web/frps/ ./
|
|
||||||
RUN npm install
|
|
||||||
RUN npm run build
|
|
||||||
|
|
||||||
FROM golang:1.25 AS building
|
|
||||||
|
|
||||||
COPY . /building
|
COPY . /building
|
||||||
COPY --from=web-builder /web/frps/dist /building/web/frps/dist
|
|
||||||
WORKDIR /building
|
WORKDIR /building
|
||||||
|
|
||||||
RUN env CGO_ENABLED=0 go build -trimpath -ldflags "-s -w" -tags frps -o bin/frps ./cmd/frps
|
RUN make frps
|
||||||
|
|
||||||
FROM alpine:3
|
FROM alpine:3
|
||||||
|
|
||||||
|
|||||||
15
go.mod
15
go.mod
@@ -39,18 +39,10 @@ require (
|
|||||||
|
|
||||||
require (
|
require (
|
||||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 // indirect
|
|
||||||
github.com/beorn7/perks v1.0.1 // indirect
|
github.com/beorn7/perks v1.0.1 // indirect
|
||||||
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
github.com/cespare/xxhash/v2 v2.2.0 // indirect
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc // indirect
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 // indirect
|
|
||||||
github.com/charmbracelet/log v0.4.2 // indirect
|
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 // indirect
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd // indirect
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 // indirect
|
|
||||||
github.com/davecgh/go-spew v1.1.1 // indirect
|
github.com/davecgh/go-spew v1.1.1 // indirect
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
github.com/go-jose/go-jose/v4 v4.0.5 // indirect
|
||||||
github.com/go-logfmt/logfmt v0.6.0 // indirect
|
|
||||||
github.com/go-logr/logr v1.4.2 // indirect
|
github.com/go-logr/logr v1.4.2 // indirect
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||||
github.com/golang/snappy v0.0.4 // indirect
|
github.com/golang/snappy v0.0.4 // indirect
|
||||||
@@ -59,10 +51,6 @@ require (
|
|||||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||||
github.com/klauspost/reedsolomon v1.12.0 // indirect
|
github.com/klauspost/reedsolomon v1.12.0 // indirect
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 // indirect
|
|
||||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
|
||||||
github.com/mattn/go-runewidth v0.0.16 // indirect
|
|
||||||
github.com/muesli/termenv v0.16.0 // indirect
|
|
||||||
github.com/pion/dtls/v2 v2.2.7 // indirect
|
github.com/pion/dtls/v2 v2.2.7 // indirect
|
||||||
github.com/pion/logging v0.2.2 // indirect
|
github.com/pion/logging v0.2.2 // indirect
|
||||||
github.com/pion/transport/v2 v2.2.1 // indirect
|
github.com/pion/transport/v2 v2.2.1 // indirect
|
||||||
@@ -72,16 +60,13 @@ require (
|
|||||||
github.com/prometheus/client_model v0.5.0 // indirect
|
github.com/prometheus/client_model v0.5.0 // indirect
|
||||||
github.com/prometheus/common v0.48.0 // indirect
|
github.com/prometheus/common v0.48.0 // indirect
|
||||||
github.com/prometheus/procfs v0.12.0 // indirect
|
github.com/prometheus/procfs v0.12.0 // indirect
|
||||||
github.com/rivo/uniseg v0.4.7 // indirect
|
|
||||||
github.com/templexxx/cpu v0.1.1 // indirect
|
github.com/templexxx/cpu v0.1.1 // indirect
|
||||||
github.com/templexxx/xorsimd v0.4.3 // indirect
|
github.com/templexxx/xorsimd v0.4.3 // indirect
|
||||||
github.com/tidwall/match v1.1.1 // indirect
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
github.com/tidwall/pretty v1.2.0 // indirect
|
github.com/tidwall/pretty v1.2.0 // indirect
|
||||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||||
github.com/vishvananda/netns v0.0.4 // indirect
|
github.com/vishvananda/netns v0.0.4 // indirect
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e // indirect
|
|
||||||
go.uber.org/automaxprocs v1.6.0 // indirect
|
go.uber.org/automaxprocs v1.6.0 // indirect
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d // indirect
|
|
||||||
golang.org/x/mod v0.27.0 // indirect
|
golang.org/x/mod v0.27.0 // indirect
|
||||||
golang.org/x/sys v0.35.0 // indirect
|
golang.org/x/sys v0.35.0 // indirect
|
||||||
golang.org/x/text v0.28.0 // indirect
|
golang.org/x/text v0.28.0 // indirect
|
||||||
|
|||||||
31
go.sum
31
go.sum
@@ -4,25 +4,11 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzS
|
|||||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5 h1:0CwZNZbxp69SHPdPJAN/hZIm0C4OItdklCFmMRWYpio=
|
||||||
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
github.com/armon/go-socks5 v0.0.0-20160902184237-e75332964ef5/go.mod h1:wHh0iHkYZB8zMSxRWpUBQtwG5a7fFgvEO+odwuTv2gs=
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1 h1:HwpRHbFMcZLEVr42D4p7XBqjyuxQH5SMiErDT4WkJ2k=
|
|
||||||
github.com/aymanbagabas/go-osc52/v2 v2.0.1/go.mod h1:uYgXzlJ7ZpABp8OJ+exZzJJhRNQ2ASbcXHWsFqH8hp8=
|
|
||||||
github.com/beorn7/perks v1.0.1 h1:VlbKKnNfV8bJzeqoa4cOKqO6bYr3WgKZxO8Z16+hsOM=
|
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/beorn7/perks v1.0.1/go.mod h1:G2ZrVWU2WbWT9wwq4/hrbKbnv/1ERSJQ0ibhJ6rlkpw=
|
||||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
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 h1:DC2CZ1Ep5Y4k3ZQ899DldepgrayRUGE6BBZ/cd9Cj44=
|
||||||
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
github.com/cespare/xxhash/v2 v2.2.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc h1:4pZI35227imm7yK2bGPcfpFEmuY1gc2YSTShr4iJBfs=
|
|
||||||
github.com/charmbracelet/colorprofile v0.2.3-0.20250311203215-f60798e515dc/go.mod h1:X4/0JoqgTIPSFcRA/P6INZzIuyqdFY5rm8tb41s9okk=
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0 h1:vYXsiLHVkK7fp74RkV7b2kq9+zDLoEU4MZoFqR/noCY=
|
|
||||||
github.com/charmbracelet/lipgloss v1.1.0/go.mod h1:/6Q8FR2o+kj8rz4Dq0zQc3vYf7X+B0binUUBwA0aL30=
|
|
||||||
github.com/charmbracelet/log v0.4.2 h1:hYt8Qj6a8yLnvR+h7MwsJv/XvmBJXiueUcI3cIxsyig=
|
|
||||||
github.com/charmbracelet/log v0.4.2/go.mod h1:qifHGX/tc7eluv2R6pWIpyHDDrrb/AG71Pf2ysQu5nw=
|
|
||||||
github.com/charmbracelet/x/ansi v0.8.0 h1:9GTq3xq9caJW8ZrBTe0LIe2fvfLR/bYXKTx2llXn7xE=
|
|
||||||
github.com/charmbracelet/x/ansi v0.8.0/go.mod h1:wdYl/ONOLHLIVmQaxbIYEC/cRKOQyjTkowiI4blgS9Q=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd h1:vy0GVL4jeHEwG5YOXDmi86oYw2yuYUGqz6a8sLwg0X8=
|
|
||||||
github.com/charmbracelet/x/cellbuf v0.0.13-0.20250311204145-2c3ea96c31dd/go.mod h1:xe0nKWGd3eJgtqZRaN9RjMtK7xUYchjzPr7q6kcvCCs=
|
|
||||||
github.com/charmbracelet/x/term v0.2.1 h1:AQeHeLZ1OqSXhrAWpYUtZyX1T3zVxfpZuEQMIQaGIAQ=
|
|
||||||
github.com/charmbracelet/x/term v0.2.1/go.mod h1:oQ4enTYFV7QN4m0i9mzHrViD7TQKvNEEkHUMCmsxdUg=
|
|
||||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
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 h1:9ePWwfdwC4QKRlCXsJGou56adA/owXczOzwKdOumLqk=
|
||||||
@@ -40,8 +26,6 @@ github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6 h1:u92UUy6FURPmNsMB
|
|||||||
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
github.com/fatedier/yamux v0.0.0-20250825093530-d0154be01cd6/go.mod h1:c5/tk6G0dSpXGzJN7Wk1OEie8grdSJAmeawId9Zvd34=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
github.com/go-jose/go-jose/v4 v4.0.5 h1:M6T8+mKZl/+fNNuFHvGIzDz7BTLQPIounk/b9dw3AaE=
|
||||||
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
github.com/go-jose/go-jose/v4 v4.0.5/go.mod h1:s3P1lRrkT8igV8D9OjyL4WRyHvjB6a4JSllnOrmmBOA=
|
||||||
github.com/go-logfmt/logfmt v0.6.0 h1:wGYYu3uicYdqXVgoYbvnkrPVXkuLM1p1ifugDMEdRi4=
|
|
||||||
github.com/go-logfmt/logfmt v0.6.0/go.mod h1:WYhtIu8zTZfxdn5+rREduYbwxfcBr/Vr6KEVveWlfTs=
|
|
||||||
github.com/go-logr/logr v1.4.2 h1:6pFjapn8bFcIbiKo3XT4j/BhANplGihG6tvd+8rYgrY=
|
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-logr/logr v1.4.2/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||||
@@ -86,16 +70,8 @@ github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
|||||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0 h1:1nnpGOrhyZZuNyfu1QjKiUICQ74+3FNCN69Aj6K7nkY=
|
|
||||||
github.com/lucasb-eyer/go-colorful v1.2.0/go.mod h1:R4dSotOR9KMtayYi1e77YzuveK+i7ruzyGqttikkLy0=
|
|
||||||
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 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
github.com/mattn/go-runewidth v0.0.15 h1:UNAjwbU9l54TA3KzvqLGxwWjHmMgBUVhBiTjelZgg3U=
|
||||||
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
github.com/mattn/go-runewidth v0.0.15/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
||||||
github.com/mattn/go-runewidth v0.0.16 h1:E5ScNMtiwvlvB5paMFdw9p4kSQzbXFikJ5SQO6TULQc=
|
|
||||||
github.com/mattn/go-runewidth v0.0.16/go.mod h1:Jdepj2loyihRzMpdS35Xk/zdY8IAYHsh153qUoGf23w=
|
|
||||||
github.com/muesli/termenv v0.16.0 h1:S5AlUN9dENB57rsbnkPyfdGuWIlkmzJjbFf0Tf5FWUc=
|
|
||||||
github.com/muesli/termenv v0.16.0/go.mod h1:ZRfOIKPFDYQoDFF4Olj7/QJbW60Ol/kL1pU3VfY/Cnk=
|
|
||||||
github.com/onsi/ginkgo/v2 v2.23.4 h1:ktYTpKJAVZnDT4VjxSbiBenUjmlL/5QkBEocaWXiQus=
|
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/ginkgo/v2 v2.23.4/go.mod h1:Bt66ApGPBFzHyR+JO10Zbt0Gsp4uWxu5mIOTusL46e8=
|
||||||
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
github.com/onsi/gomega v1.36.3 h1:hID7cr8t3Wp26+cYnfcjR6HpJ00fdogN6dqZ1t6IylU=
|
||||||
@@ -133,8 +109,6 @@ github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9M
|
|||||||
github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
|
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/rivo/uniseg v0.2.0 h1:S1pD9weZBuJdFmowNwbpi7BJ8TNftyUImj/0WQi72jY=
|
||||||
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
github.com/rivo/uniseg v0.2.0/go.mod h1:J6wj4VEh+S6ZtnVlnTBMWIodfgj8LQOQFoIToxlJtxc=
|
||||||
github.com/rivo/uniseg v0.4.7 h1:WUdvkW8uEhrYfLC4ZzdpI2ztxP1I582+49Oc5Mq64VQ=
|
|
||||||
github.com/rivo/uniseg v0.4.7/go.mod h1:FN3SvrM+Zdj16jyLfmOkMNblXMcoc8DfTHruCPUcx88=
|
|
||||||
github.com/rodaine/table v1.2.0 h1:38HEnwK4mKSHQJIkavVj+bst1TEY7j9zhLMWu4QJrMA=
|
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/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 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||||
@@ -175,8 +149,6 @@ github.com/vishvananda/netlink v1.3.0 h1:X7l42GfcV4S6E4vHTsw48qbrV+9PVojNfIhZcwQ
|
|||||||
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
github.com/vishvananda/netlink v1.3.0/go.mod h1:i6NetklAujEcC6fK0JPjT8qSwWyO0HLn4UKG+hGqeJs=
|
||||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e h1:JVG44RsyaB9T2KIHavMF/ppJZNG9ZpyihvCd0w101no=
|
|
||||||
github.com/xo/terminfo v0.0.0-20220910002029-abceb7e1c41e/go.mod h1:RbqR21r5mrJuqunuUZ/Dhy/avygyECGrLceyNeo4LiM=
|
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
|
github.com/xtaci/kcp-go/v5 v5.6.13 h1:FEjtz9+D4p8t2x4WjciGt/jsIuhlWjjgPCCWjrVR4Hk=
|
||||||
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
|
github.com/xtaci/kcp-go/v5 v5.6.13/go.mod h1:75S1AKYYzNUSXIv30h+jPKJYZUwqpfvLshu63nCNSOM=
|
||||||
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
|
github.com/xtaci/lossyconn v0.0.0-20200209145036-adba10fffc37 h1:EWU6Pktpas0n8lLQwDsRyZfmkPeRbdgPtW609es+/9E=
|
||||||
@@ -195,8 +167,6 @@ golang.org/x/crypto v0.12.0/go.mod h1:NF0Gs7EO5K4qLn+Ylc+fih8BSTeIjAP05siRnAh98y
|
|||||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d h1:jtJma62tbqLibJ5sFQz8bKtEM8rJBtfilJ2qTU199MI=
|
|
||||||
golang.org/x/exp v0.0.0-20231006140011-7918f672742d/go.mod h1:ldy0pHrwJyGW56pPQzzkH36rKxoZW1tw7ZJpeKx+hdo=
|
|
||||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||||
@@ -239,7 +209,6 @@ golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBc
|
|||||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
|
||||||
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.7.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
|||||||
@@ -18,7 +18,7 @@ rm -rf ./release/packages
|
|||||||
mkdir -p ./release/packages
|
mkdir -p ./release/packages
|
||||||
|
|
||||||
os_all='linux windows darwin freebsd openbsd android'
|
os_all='linux windows darwin freebsd openbsd android'
|
||||||
arch_all='amd64 arm arm64'
|
arch_all='386 amd64 arm arm64 mips64 mips64le mips mipsle riscv64 loong64'
|
||||||
extra_all='_ hf'
|
extra_all='_ hf'
|
||||||
|
|
||||||
cd ./release
|
cd ./release
|
||||||
|
|||||||
@@ -15,7 +15,6 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"context"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
@@ -28,39 +27,6 @@ type Setter interface {
|
|||||||
SetNewWorkConn(*msg.NewWorkConn) error
|
SetNewWorkConn(*msg.NewWorkConn) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientAuth struct {
|
|
||||||
Setter Setter
|
|
||||||
key []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ClientAuth) EncryptionKey() []byte {
|
|
||||||
return a.key
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildClientAuth resolves any dynamic auth values and returns a prepared auth runtime.
|
|
||||||
// Caller must run validation before calling this function.
|
|
||||||
func BuildClientAuth(cfg *v1.AuthClientConfig) (*ClientAuth, error) {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, fmt.Errorf("auth config is nil")
|
|
||||||
}
|
|
||||||
resolved := *cfg
|
|
||||||
if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil {
|
|
||||||
token, err := resolved.TokenSource.Resolve(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
|
||||||
}
|
|
||||||
resolved.Token = token
|
|
||||||
}
|
|
||||||
setter, err := NewAuthSetter(resolved)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
return &ClientAuth{
|
|
||||||
Setter: setter,
|
|
||||||
key: []byte(resolved.Token),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) {
|
func NewAuthSetter(cfg v1.AuthClientConfig) (authProvider Setter, err error) {
|
||||||
switch cfg.Method {
|
switch cfg.Method {
|
||||||
case v1.AuthMethodToken:
|
case v1.AuthMethodToken:
|
||||||
@@ -86,35 +52,6 @@ type Verifier interface {
|
|||||||
VerifyNewWorkConn(*msg.NewWorkConn) error
|
VerifyNewWorkConn(*msg.NewWorkConn) error
|
||||||
}
|
}
|
||||||
|
|
||||||
type ServerAuth struct {
|
|
||||||
Verifier Verifier
|
|
||||||
key []byte
|
|
||||||
}
|
|
||||||
|
|
||||||
func (a *ServerAuth) EncryptionKey() []byte {
|
|
||||||
return a.key
|
|
||||||
}
|
|
||||||
|
|
||||||
// BuildServerAuth resolves any dynamic auth values and returns a prepared auth runtime.
|
|
||||||
// Caller must run validation before calling this function.
|
|
||||||
func BuildServerAuth(cfg *v1.AuthServerConfig) (*ServerAuth, error) {
|
|
||||||
if cfg == nil {
|
|
||||||
return nil, fmt.Errorf("auth config is nil")
|
|
||||||
}
|
|
||||||
resolved := *cfg
|
|
||||||
if resolved.Method == v1.AuthMethodToken && resolved.TokenSource != nil {
|
|
||||||
token, err := resolved.TokenSource.Resolve(context.Background())
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
|
||||||
}
|
|
||||||
resolved.Token = token
|
|
||||||
}
|
|
||||||
return &ServerAuth{
|
|
||||||
Verifier: NewAuthVerifier(resolved),
|
|
||||||
key: []byte(resolved.Token),
|
|
||||||
}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
|
func NewAuthVerifier(cfg v1.AuthServerConfig) (authVerifier Verifier) {
|
||||||
switch cfg.Method {
|
switch cfg.Method {
|
||||||
case v1.AuthMethodToken:
|
case v1.AuthMethodToken:
|
||||||
|
|||||||
@@ -167,7 +167,6 @@ func RegisterClientCommonConfigFlags(cmd *cobra.Command, c *v1.ClientCommonConfi
|
|||||||
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
c.Transport.TLS.Enable = cmd.PersistentFlags().BoolP("tls_enable", "", true, "enable frpc tls")
|
||||||
}
|
}
|
||||||
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
cmd.PersistentFlags().StringVarP(&c.User, "user", "u", "", "user")
|
||||||
cmd.PersistentFlags().StringVar(&c.ClientID, "client-id", "", "unique identifier for this frpc instance")
|
|
||||||
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
|
cmd.PersistentFlags().StringVarP(&c.Auth.Token, "token", "t", "", "auth token")
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@@ -37,8 +39,6 @@ type ClientCommonConfig struct {
|
|||||||
// clients. If this value is not "", proxy names will automatically be
|
// clients. If this value is not "", proxy names will automatically be
|
||||||
// changed to "{user}.{proxy_name}".
|
// changed to "{user}.{proxy_name}".
|
||||||
User string `json:"user,omitempty"`
|
User string `json:"user,omitempty"`
|
||||||
// ClientID uniquely identifies this frpc instance.
|
|
||||||
ClientID string `json:"clientID,omitempty"`
|
|
||||||
|
|
||||||
// ServerAddr specifies the address of the server to connect to. By
|
// ServerAddr specifies the address of the server to connect to. By
|
||||||
// default, this value is "0.0.0.0".
|
// default, this value is "0.0.0.0".
|
||||||
@@ -198,6 +198,17 @@ type AuthClientConfig struct {
|
|||||||
|
|
||||||
func (c *AuthClientConfig) Complete() error {
|
func (c *AuthClientConfig) Complete() error {
|
||||||
c.Method = util.EmptyOr(c.Method, "token")
|
c.Method = util.EmptyOr(c.Method, "token")
|
||||||
|
|
||||||
|
// Resolve tokenSource during configuration loading
|
||||||
|
if c.Method == AuthMethodToken && c.TokenSource != nil {
|
||||||
|
token, err := c.TokenSource.Resolve(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
||||||
|
}
|
||||||
|
// Move the resolved token to the Token field and clear TokenSource
|
||||||
|
c.Token = token
|
||||||
|
c.TokenSource = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@@ -36,9 +38,68 @@ func TestClientConfigComplete(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthClientConfig_Complete(t *testing.T) {
|
func TestAuthClientConfig_Complete(t *testing.T) {
|
||||||
require := require.New(t)
|
// Create a temporary file for testing
|
||||||
cfg := &AuthClientConfig{}
|
tmpDir := t.TempDir()
|
||||||
err := cfg.Complete()
|
testFile := filepath.Join(tmpDir, "test_token")
|
||||||
require.NoError(err)
|
testContent := "client-token-value"
|
||||||
require.EqualValues("token", cfg.Method)
|
err := os.WriteFile(testFile, []byte(testContent), 0o600)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config AuthClientConfig
|
||||||
|
expectToken string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "tokenSource resolved to token",
|
||||||
|
config: AuthClientConfig{
|
||||||
|
Method: AuthMethodToken,
|
||||||
|
TokenSource: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: &FileSource{
|
||||||
|
Path: testFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectToken: testContent,
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "direct token unchanged",
|
||||||
|
config: AuthClientConfig{
|
||||||
|
Method: AuthMethodToken,
|
||||||
|
Token: "direct-token",
|
||||||
|
},
|
||||||
|
expectToken: "direct-token",
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid tokenSource should panic",
|
||||||
|
config: AuthClientConfig{
|
||||||
|
Method: AuthMethodToken,
|
||||||
|
TokenSource: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: &FileSource{
|
||||||
|
Path: "/non/existent/file",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.expectPanic {
|
||||||
|
err := tt.config.Complete()
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
err := tt.config.Complete()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.expectToken, tt.config.Token)
|
||||||
|
require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,31 +27,29 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
const (
|
const (
|
||||||
PluginHTTP2HTTPS = "http2https"
|
PluginHTTP2HTTPS = "http2https"
|
||||||
PluginHTTP2HTTPSRedirect = "http2https_redirect"
|
PluginHTTPProxy = "http_proxy"
|
||||||
PluginHTTPProxy = "http_proxy"
|
PluginHTTPS2HTTP = "https2http"
|
||||||
PluginHTTPS2HTTP = "https2http"
|
PluginHTTPS2HTTPS = "https2https"
|
||||||
PluginHTTPS2HTTPS = "https2https"
|
PluginHTTP2HTTP = "http2http"
|
||||||
PluginHTTP2HTTP = "http2http"
|
PluginSocks5 = "socks5"
|
||||||
PluginSocks5 = "socks5"
|
PluginStaticFile = "static_file"
|
||||||
PluginStaticFile = "static_file"
|
PluginUnixDomainSocket = "unix_domain_socket"
|
||||||
PluginUnixDomainSocket = "unix_domain_socket"
|
PluginTLS2Raw = "tls2raw"
|
||||||
PluginTLS2Raw = "tls2raw"
|
PluginVirtualNet = "virtual_net"
|
||||||
PluginVirtualNet = "virtual_net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var clientPluginOptionsTypeMap = map[string]reflect.Type{
|
var clientPluginOptionsTypeMap = map[string]reflect.Type{
|
||||||
PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
|
PluginHTTP2HTTPS: reflect.TypeOf(HTTP2HTTPSPluginOptions{}),
|
||||||
PluginHTTP2HTTPSRedirect: reflect.TypeOf(HTTP2HTTPSRedirectPluginOptions{}),
|
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
|
||||||
PluginHTTPProxy: reflect.TypeOf(HTTPProxyPluginOptions{}),
|
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
|
||||||
PluginHTTPS2HTTP: reflect.TypeOf(HTTPS2HTTPPluginOptions{}),
|
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
|
||||||
PluginHTTPS2HTTPS: reflect.TypeOf(HTTPS2HTTPSPluginOptions{}),
|
PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
|
||||||
PluginHTTP2HTTP: reflect.TypeOf(HTTP2HTTPPluginOptions{}),
|
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
|
||||||
PluginSocks5: reflect.TypeOf(Socks5PluginOptions{}),
|
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
|
||||||
PluginStaticFile: reflect.TypeOf(StaticFilePluginOptions{}),
|
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
|
||||||
PluginUnixDomainSocket: reflect.TypeOf(UnixDomainSocketPluginOptions{}),
|
PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
|
||||||
PluginTLS2Raw: reflect.TypeOf(TLS2RawPluginOptions{}),
|
PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}),
|
||||||
PluginVirtualNet: reflect.TypeOf(VirtualNetPluginOptions{}),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type ClientPluginOptions interface {
|
type ClientPluginOptions interface {
|
||||||
@@ -111,13 +109,6 @@ type HTTP2HTTPSPluginOptions struct {
|
|||||||
|
|
||||||
func (o *HTTP2HTTPSPluginOptions) Complete() {}
|
func (o *HTTP2HTTPSPluginOptions) Complete() {}
|
||||||
|
|
||||||
type HTTP2HTTPSRedirectPluginOptions struct {
|
|
||||||
Type string `json:"type,omitempty"`
|
|
||||||
HTTPSPort int `json:"httpsPort,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
func (o *HTTP2HTTPSRedirectPluginOptions) Complete() {}
|
|
||||||
|
|
||||||
type HTTPProxyPluginOptions struct {
|
type HTTPProxyPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
HTTPUser string `json:"httpUser,omitempty"`
|
HTTPUser string `json:"httpUser,omitempty"`
|
||||||
@@ -126,18 +117,6 @@ type HTTPProxyPluginOptions struct {
|
|||||||
|
|
||||||
func (o *HTTPProxyPluginOptions) Complete() {}
|
func (o *HTTPProxyPluginOptions) Complete() {}
|
||||||
|
|
||||||
type AutoTLSOptions struct {
|
|
||||||
Enable bool `json:"enable,omitempty"`
|
|
||||||
// Contact email for certificate expiration and important notices.
|
|
||||||
Email string `json:"email,omitempty"`
|
|
||||||
// Directory used to cache ACME account and certificates.
|
|
||||||
CacheDir string `json:"cacheDir,omitempty"`
|
|
||||||
// ACME directory URL, e.g. Let's Encrypt staging/prod endpoint.
|
|
||||||
CADirURL string `json:"caDirURL,omitempty"`
|
|
||||||
// Restrict certificate issuance to the listed domains.
|
|
||||||
HostAllowList []string `json:"hostAllowList,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPS2HTTPPluginOptions struct {
|
type HTTPS2HTTPPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
LocalAddr string `json:"localAddr,omitempty"`
|
LocalAddr string `json:"localAddr,omitempty"`
|
||||||
@@ -146,7 +125,6 @@ type HTTPS2HTTPPluginOptions struct {
|
|||||||
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
|
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
|
||||||
CrtPath string `json:"crtPath,omitempty"`
|
CrtPath string `json:"crtPath,omitempty"`
|
||||||
KeyPath string `json:"keyPath,omitempty"`
|
KeyPath string `json:"keyPath,omitempty"`
|
||||||
AutoTLS *AutoTLSOptions `json:"autoTLS,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *HTTPS2HTTPPluginOptions) Complete() {
|
func (o *HTTPS2HTTPPluginOptions) Complete() {
|
||||||
@@ -161,7 +139,6 @@ type HTTPS2HTTPSPluginOptions struct {
|
|||||||
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
|
EnableHTTP2 *bool `json:"enableHTTP2,omitempty"`
|
||||||
CrtPath string `json:"crtPath,omitempty"`
|
CrtPath string `json:"crtPath,omitempty"`
|
||||||
KeyPath string `json:"keyPath,omitempty"`
|
KeyPath string `json:"keyPath,omitempty"`
|
||||||
AutoTLS *AutoTLSOptions `json:"autoTLS,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *HTTPS2HTTPSPluginOptions) Complete() {
|
func (o *HTTPS2HTTPSPluginOptions) Complete() {
|
||||||
@@ -203,11 +180,10 @@ type UnixDomainSocketPluginOptions struct {
|
|||||||
func (o *UnixDomainSocketPluginOptions) Complete() {}
|
func (o *UnixDomainSocketPluginOptions) Complete() {}
|
||||||
|
|
||||||
type TLS2RawPluginOptions struct {
|
type TLS2RawPluginOptions struct {
|
||||||
Type string `json:"type,omitempty"`
|
Type string `json:"type,omitempty"`
|
||||||
LocalAddr string `json:"localAddr,omitempty"`
|
LocalAddr string `json:"localAddr,omitempty"`
|
||||||
CrtPath string `json:"crtPath,omitempty"`
|
CrtPath string `json:"crtPath,omitempty"`
|
||||||
KeyPath string `json:"keyPath,omitempty"`
|
KeyPath string `json:"keyPath,omitempty"`
|
||||||
AutoTLS *AutoTLSOptions `json:"autoTLS,omitempty"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (o *TLS2RawPluginOptions) Complete() {}
|
func (o *TLS2RawPluginOptions) Complete() {}
|
||||||
|
|||||||
@@ -15,6 +15,9 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"context"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/types"
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
@@ -135,6 +138,17 @@ type AuthServerConfig struct {
|
|||||||
|
|
||||||
func (c *AuthServerConfig) Complete() error {
|
func (c *AuthServerConfig) Complete() error {
|
||||||
c.Method = util.EmptyOr(c.Method, "token")
|
c.Method = util.EmptyOr(c.Method, "token")
|
||||||
|
|
||||||
|
// Resolve tokenSource during configuration loading
|
||||||
|
if c.Method == AuthMethodToken && c.TokenSource != nil {
|
||||||
|
token, err := c.TokenSource.Resolve(context.Background())
|
||||||
|
if err != nil {
|
||||||
|
return fmt.Errorf("failed to resolve auth.tokenSource: %w", err)
|
||||||
|
}
|
||||||
|
// Move the resolved token to the Token field and clear TokenSource
|
||||||
|
c.Token = token
|
||||||
|
c.TokenSource = nil
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -15,6 +15,8 @@
|
|||||||
package v1
|
package v1
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
@@ -33,9 +35,68 @@ func TestServerConfigComplete(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func TestAuthServerConfig_Complete(t *testing.T) {
|
func TestAuthServerConfig_Complete(t *testing.T) {
|
||||||
require := require.New(t)
|
// Create a temporary file for testing
|
||||||
cfg := &AuthServerConfig{}
|
tmpDir := t.TempDir()
|
||||||
err := cfg.Complete()
|
testFile := filepath.Join(tmpDir, "test_token")
|
||||||
require.NoError(err)
|
testContent := "file-token-value"
|
||||||
require.EqualValues("token", cfg.Method)
|
err := os.WriteFile(testFile, []byte(testContent), 0o600)
|
||||||
|
require.NoError(t, err)
|
||||||
|
|
||||||
|
tests := []struct {
|
||||||
|
name string
|
||||||
|
config AuthServerConfig
|
||||||
|
expectToken string
|
||||||
|
expectPanic bool
|
||||||
|
}{
|
||||||
|
{
|
||||||
|
name: "tokenSource resolved to token",
|
||||||
|
config: AuthServerConfig{
|
||||||
|
Method: AuthMethodToken,
|
||||||
|
TokenSource: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: &FileSource{
|
||||||
|
Path: testFile,
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectToken: testContent,
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "direct token unchanged",
|
||||||
|
config: AuthServerConfig{
|
||||||
|
Method: AuthMethodToken,
|
||||||
|
Token: "direct-token",
|
||||||
|
},
|
||||||
|
expectToken: "direct-token",
|
||||||
|
expectPanic: false,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
name: "invalid tokenSource should panic",
|
||||||
|
config: AuthServerConfig{
|
||||||
|
Method: AuthMethodToken,
|
||||||
|
TokenSource: &ValueSource{
|
||||||
|
Type: "file",
|
||||||
|
File: &FileSource{
|
||||||
|
Path: "/non/existent/file",
|
||||||
|
},
|
||||||
|
},
|
||||||
|
},
|
||||||
|
expectPanic: true,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, tt := range tests {
|
||||||
|
t.Run(tt.name, func(t *testing.T) {
|
||||||
|
if tt.expectPanic {
|
||||||
|
err := tt.config.Complete()
|
||||||
|
require.Error(t, err)
|
||||||
|
} else {
|
||||||
|
err := tt.config.Complete()
|
||||||
|
require.NoError(t, err)
|
||||||
|
require.Equal(t, tt.expectToken, tt.config.Token)
|
||||||
|
require.Nil(t, tt.config.TokenSource, "TokenSource should be cleared after resolution")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -27,7 +27,7 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/policy/security"
|
"github.com/fatedier/frp/pkg/policy/security"
|
||||||
)
|
)
|
||||||
|
|
||||||
func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (Warning, error) {
|
func ValidateClientCommonConfig(c *v1.ClientCommonConfig, unsafeFeatures *security.UnsafeFeatures) (Warning, error) {
|
||||||
var (
|
var (
|
||||||
warnings Warning
|
warnings Warning
|
||||||
errs error
|
errs error
|
||||||
@@ -35,15 +35,15 @@ func (v *ConfigValidator) ValidateClientCommonConfig(c *v1.ClientCommonConfig) (
|
|||||||
|
|
||||||
validators := []func() (Warning, error){
|
validators := []func() (Warning, error){
|
||||||
func() (Warning, error) { return validateFeatureGates(c) },
|
func() (Warning, error) { return validateFeatureGates(c) },
|
||||||
func() (Warning, error) { return v.validateAuthConfig(&c.Auth) },
|
func() (Warning, error) { return validateAuthConfig(&c.Auth, unsafeFeatures) },
|
||||||
func() (Warning, error) { return nil, validateLogConfig(&c.Log) },
|
func() (Warning, error) { return nil, validateLogConfig(&c.Log) },
|
||||||
func() (Warning, error) { return nil, validateWebServerConfig(&c.WebServer) },
|
func() (Warning, error) { return nil, validateWebServerConfig(&c.WebServer) },
|
||||||
func() (Warning, error) { return validateTransportConfig(&c.Transport) },
|
func() (Warning, error) { return validateTransportConfig(&c.Transport) },
|
||||||
func() (Warning, error) { return validateIncludeFiles(c.IncludeConfigFiles) },
|
func() (Warning, error) { return validateIncludeFiles(c.IncludeConfigFiles) },
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, validator := range validators {
|
for _, v := range validators {
|
||||||
w, err := validator()
|
w, err := v()
|
||||||
warnings = AppendError(warnings, w)
|
warnings = AppendError(warnings, w)
|
||||||
errs = AppendError(errs, err)
|
errs = AppendError(errs, err)
|
||||||
}
|
}
|
||||||
@@ -59,7 +59,7 @@ func validateFeatureGates(c *v1.ClientCommonConfig) (Warning, error) {
|
|||||||
return nil, nil
|
return nil, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, error) {
|
func validateAuthConfig(c *v1.AuthClientConfig, unsafeFeatures *security.UnsafeFeatures) (Warning, error) {
|
||||||
var errs error
|
var errs error
|
||||||
if !slices.Contains(SupportedAuthMethods, c.Method) {
|
if !slices.Contains(SupportedAuthMethods, c.Method) {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
|
errs = AppendError(errs, fmt.Errorf("invalid auth method, optional values are %v", SupportedAuthMethods))
|
||||||
@@ -76,8 +76,9 @@ func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, e
|
|||||||
// Validate tokenSource if specified
|
// Validate tokenSource if specified
|
||||||
if c.TokenSource != nil {
|
if c.TokenSource != nil {
|
||||||
if c.TokenSource.Type == "exec" {
|
if c.TokenSource.Type == "exec" {
|
||||||
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
if !unsafeFeatures.IsEnabled(security.TokenSourceExec) {
|
||||||
errs = AppendError(errs, err)
|
errs = AppendError(errs, fmt.Errorf("unsafe feature %q is not enabled. "+
|
||||||
|
"To enable it, start frpc with '--allow-unsafe %s'", security.TokenSourceExec, security.TokenSourceExec))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := c.TokenSource.Validate(); err != nil {
|
if err := c.TokenSource.Validate(); err != nil {
|
||||||
@@ -85,13 +86,13 @@ func (v *ConfigValidator) validateAuthConfig(c *v1.AuthClientConfig) (Warning, e
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := v.validateOIDCConfig(&c.OIDC); err != nil {
|
if err := validateOIDCConfig(&c.OIDC, unsafeFeatures); err != nil {
|
||||||
errs = AppendError(errs, err)
|
errs = AppendError(errs, err)
|
||||||
}
|
}
|
||||||
return nil, errs
|
return nil, errs
|
||||||
}
|
}
|
||||||
|
|
||||||
func (v *ConfigValidator) validateOIDCConfig(c *v1.AuthOIDCClientConfig) error {
|
func validateOIDCConfig(c *v1.AuthOIDCClientConfig, unsafeFeatures *security.UnsafeFeatures) error {
|
||||||
if c.TokenSource == nil {
|
if c.TokenSource == nil {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
@@ -103,8 +104,9 @@ func (v *ConfigValidator) validateOIDCConfig(c *v1.AuthOIDCClientConfig) error {
|
|||||||
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc"))
|
errs = AppendError(errs, fmt.Errorf("cannot specify both auth.oidc.tokenSource and any other field of auth.oidc"))
|
||||||
}
|
}
|
||||||
if c.TokenSource.Type == "exec" {
|
if c.TokenSource.Type == "exec" {
|
||||||
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
if !unsafeFeatures.IsEnabled(security.TokenSourceExec) {
|
||||||
errs = AppendError(errs, err)
|
errs = AppendError(errs, fmt.Errorf("unsafe feature %q is not enabled. "+
|
||||||
|
"To enable it, start frpc with '--allow-unsafe %s'", security.TokenSourceExec, security.TokenSourceExec))
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
if err := c.TokenSource.Validate(); err != nil {
|
if err := c.TokenSource.Validate(); err != nil {
|
||||||
@@ -165,10 +167,9 @@ func ValidateAllClientConfig(
|
|||||||
visitorCfgs []v1.VisitorConfigurer,
|
visitorCfgs []v1.VisitorConfigurer,
|
||||||
unsafeFeatures *security.UnsafeFeatures,
|
unsafeFeatures *security.UnsafeFeatures,
|
||||||
) (Warning, error) {
|
) (Warning, error) {
|
||||||
validator := NewConfigValidator(unsafeFeatures)
|
|
||||||
var warnings Warning
|
var warnings Warning
|
||||||
if c != nil {
|
if c != nil {
|
||||||
warning, err := validator.ValidateClientCommonConfig(c)
|
warning, err := ValidateClientCommonConfig(c, unsafeFeatures)
|
||||||
warnings = AppendError(warnings, warning)
|
warnings = AppendError(warnings, warning)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return warnings, err
|
return warnings, err
|
||||||
|
|||||||
@@ -16,8 +16,6 @@ package validation
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
"errors"
|
||||||
"fmt"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
)
|
)
|
||||||
@@ -26,8 +24,6 @@ func ValidateClientPluginOptions(c v1.ClientPluginOptions) error {
|
|||||||
switch v := c.(type) {
|
switch v := c.(type) {
|
||||||
case *v1.HTTP2HTTPSPluginOptions:
|
case *v1.HTTP2HTTPSPluginOptions:
|
||||||
return validateHTTP2HTTPSPluginOptions(v)
|
return validateHTTP2HTTPSPluginOptions(v)
|
||||||
case *v1.HTTP2HTTPSRedirectPluginOptions:
|
|
||||||
return validateHTTP2HTTPSRedirectPluginOptions(v)
|
|
||||||
case *v1.HTTPS2HTTPPluginOptions:
|
case *v1.HTTPS2HTTPPluginOptions:
|
||||||
return validateHTTPS2HTTPPluginOptions(v)
|
return validateHTTPS2HTTPPluginOptions(v)
|
||||||
case *v1.HTTPS2HTTPSPluginOptions:
|
case *v1.HTTPS2HTTPSPluginOptions:
|
||||||
@@ -49,17 +45,10 @@ func validateHTTP2HTTPSPluginOptions(c *v1.HTTP2HTTPSPluginOptions) error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func validateHTTP2HTTPSRedirectPluginOptions(c *v1.HTTP2HTTPSRedirectPluginOptions) error {
|
|
||||||
return ValidatePort(c.HTTPSPort, "httpsPort")
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateHTTPS2HTTPPluginOptions(c *v1.HTTPS2HTTPPluginOptions) error {
|
func validateHTTPS2HTTPPluginOptions(c *v1.HTTPS2HTTPPluginOptions) error {
|
||||||
if c.LocalAddr == "" {
|
if c.LocalAddr == "" {
|
||||||
return errors.New("localAddr is required")
|
return errors.New("localAddr is required")
|
||||||
}
|
}
|
||||||
if err := validateAutoTLSOptions(c.AutoTLS, c.CrtPath, c.KeyPath); err != nil {
|
|
||||||
return fmt.Errorf("invalid autoTLS options: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -67,9 +56,6 @@ func validateHTTPS2HTTPSPluginOptions(c *v1.HTTPS2HTTPSPluginOptions) error {
|
|||||||
if c.LocalAddr == "" {
|
if c.LocalAddr == "" {
|
||||||
return errors.New("localAddr is required")
|
return errors.New("localAddr is required")
|
||||||
}
|
}
|
||||||
if err := validateAutoTLSOptions(c.AutoTLS, c.CrtPath, c.KeyPath); err != nil {
|
|
||||||
return fmt.Errorf("invalid autoTLS options: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -91,29 +77,5 @@ func validateTLS2RawPluginOptions(c *v1.TLS2RawPluginOptions) error {
|
|||||||
if c.LocalAddr == "" {
|
if c.LocalAddr == "" {
|
||||||
return errors.New("localAddr is required")
|
return errors.New("localAddr is required")
|
||||||
}
|
}
|
||||||
if err := validateAutoTLSOptions(c.AutoTLS, c.CrtPath, c.KeyPath); err != nil {
|
|
||||||
return fmt.Errorf("invalid autoTLS options: %w", err)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func validateAutoTLSOptions(c *v1.AutoTLSOptions, crtPath, keyPath string) error {
|
|
||||||
if c == nil || !c.Enable {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
if crtPath != "" || keyPath != "" {
|
|
||||||
return errors.New("crtPath and keyPath must be empty when autoTLS.enable is true")
|
|
||||||
}
|
|
||||||
if strings.TrimSpace(c.CacheDir) == "" {
|
|
||||||
return errors.New("autoTLS.cacheDir is required when autoTLS.enable is true")
|
|
||||||
}
|
|
||||||
if len(c.HostAllowList) > 0 {
|
|
||||||
for _, host := range c.HostAllowList {
|
|
||||||
if strings.TrimSpace(host) == "" {
|
|
||||||
return errors.New("autoTLS.hostAllowList cannot contain empty domain")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -21,10 +21,9 @@ import (
|
|||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
func ValidateServerConfig(c *v1.ServerConfig) (Warning, error) {
|
||||||
var (
|
var (
|
||||||
warnings Warning
|
warnings Warning
|
||||||
errs error
|
errs error
|
||||||
@@ -43,11 +42,6 @@ func (v *ConfigValidator) ValidateServerConfig(c *v1.ServerConfig) (Warning, err
|
|||||||
|
|
||||||
// Validate tokenSource if specified
|
// Validate tokenSource if specified
|
||||||
if c.Auth.TokenSource != nil {
|
if c.Auth.TokenSource != nil {
|
||||||
if c.Auth.TokenSource.Type == "exec" {
|
|
||||||
if err := v.ValidateUnsafeFeature(security.TokenSourceExec); err != nil {
|
|
||||||
errs = AppendError(errs, err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if err := c.Auth.TokenSource.Validate(); err != nil {
|
if err := c.Auth.TokenSource.Validate(); err != nil {
|
||||||
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
errs = AppendError(errs, fmt.Errorf("invalid auth.tokenSource: %v", err))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,28 +0,0 @@
|
|||||||
package validation
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/policy/security"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ConfigValidator holds the context dependencies for configuration validation.
|
|
||||||
type ConfigValidator struct {
|
|
||||||
unsafeFeatures *security.UnsafeFeatures
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewConfigValidator creates a new ConfigValidator instance.
|
|
||||||
func NewConfigValidator(unsafeFeatures *security.UnsafeFeatures) *ConfigValidator {
|
|
||||||
return &ConfigValidator{
|
|
||||||
unsafeFeatures: unsafeFeatures,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// ValidateUnsafeFeature checks if a specific unsafe feature is enabled.
|
|
||||||
func (v *ConfigValidator) ValidateUnsafeFeature(feature string) error {
|
|
||||||
if !v.unsafeFeatures.IsEnabled(feature) {
|
|
||||||
return fmt.Errorf("unsafe feature %q is not enabled. "+
|
|
||||||
"To enable it, ensure it is allowed in the configuration or command line flags", feature)
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -56,9 +56,9 @@ func (m *serverMetrics) CloseClient() {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) NewProxy(name string, proxyType string, user string, clientID string) {
|
func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
||||||
for _, v := range m.ms {
|
for _, v := range m.ms {
|
||||||
v.NewProxy(name, proxyType, user, clientID)
|
v.NewProxy(name, proxyType)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -98,7 +98,7 @@ func (m *serverMetrics) CloseClient() {
|
|||||||
m.info.ClientCounts.Dec(1)
|
m.info.ClientCounts.Dec(1)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) NewProxy(name string, proxyType string, user string, clientID string) {
|
func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
counter, ok := m.info.ProxyTypeCounts[proxyType]
|
counter, ok := m.info.ProxyTypeCounts[proxyType]
|
||||||
@@ -119,8 +119,6 @@ func (m *serverMetrics) NewProxy(name string, proxyType string, user string, cli
|
|||||||
}
|
}
|
||||||
m.info.ProxyStatistics[name] = proxyStats
|
m.info.ProxyStatistics[name] = proxyStats
|
||||||
}
|
}
|
||||||
proxyStats.User = user
|
|
||||||
proxyStats.ClientID = clientID
|
|
||||||
proxyStats.LastStartTime = time.Now()
|
proxyStats.LastStartTime = time.Now()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -217,8 +215,6 @@ func (m *serverMetrics) GetProxiesByType(proxyType string) []*ProxyStats {
|
|||||||
ps := &ProxyStats{
|
ps := &ProxyStats{
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: proxyStats.ProxyType,
|
Type: proxyStats.ProxyType,
|
||||||
User: proxyStats.User,
|
|
||||||
ClientID: proxyStats.ClientID,
|
|
||||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
||||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
||||||
CurConns: int64(proxyStats.CurConns.Count()),
|
CurConns: int64(proxyStats.CurConns.Count()),
|
||||||
@@ -251,8 +247,6 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
|
|||||||
res = &ProxyStats{
|
res = &ProxyStats{
|
||||||
Name: name,
|
Name: name,
|
||||||
Type: proxyStats.ProxyType,
|
Type: proxyStats.ProxyType,
|
||||||
User: proxyStats.User,
|
|
||||||
ClientID: proxyStats.ClientID,
|
|
||||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
||||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
||||||
CurConns: int64(proxyStats.CurConns.Count()),
|
CurConns: int64(proxyStats.CurConns.Count()),
|
||||||
@@ -268,31 +262,6 @@ func (m *serverMetrics) GetProxiesByTypeAndName(proxyType string, proxyName stri
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) GetProxyByName(proxyName string) (res *ProxyStats) {
|
|
||||||
m.mu.Lock()
|
|
||||||
defer m.mu.Unlock()
|
|
||||||
|
|
||||||
proxyStats, ok := m.info.ProxyStatistics[proxyName]
|
|
||||||
if ok {
|
|
||||||
res = &ProxyStats{
|
|
||||||
Name: proxyName,
|
|
||||||
Type: proxyStats.ProxyType,
|
|
||||||
User: proxyStats.User,
|
|
||||||
ClientID: proxyStats.ClientID,
|
|
||||||
TodayTrafficIn: proxyStats.TrafficIn.TodayCount(),
|
|
||||||
TodayTrafficOut: proxyStats.TrafficOut.TodayCount(),
|
|
||||||
CurConns: int64(proxyStats.CurConns.Count()),
|
|
||||||
}
|
|
||||||
if !proxyStats.LastStartTime.IsZero() {
|
|
||||||
res.LastStartTime = proxyStats.LastStartTime.Format("01-02 15:04:05")
|
|
||||||
}
|
|
||||||
if !proxyStats.LastCloseTime.IsZero() {
|
|
||||||
res.LastCloseTime = proxyStats.LastCloseTime.Format("01-02 15:04:05")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (m *serverMetrics) GetProxyTraffic(name string) (res *ProxyTrafficInfo) {
|
func (m *serverMetrics) GetProxyTraffic(name string) (res *ProxyTrafficInfo) {
|
||||||
m.mu.Lock()
|
m.mu.Lock()
|
||||||
defer m.mu.Unlock()
|
defer m.mu.Unlock()
|
||||||
|
|||||||
@@ -35,8 +35,6 @@ type ServerStats struct {
|
|||||||
type ProxyStats struct {
|
type ProxyStats struct {
|
||||||
Name string
|
Name string
|
||||||
Type string
|
Type string
|
||||||
User string
|
|
||||||
ClientID string
|
|
||||||
TodayTrafficIn int64
|
TodayTrafficIn int64
|
||||||
TodayTrafficOut int64
|
TodayTrafficOut int64
|
||||||
LastStartTime string
|
LastStartTime string
|
||||||
@@ -53,8 +51,6 @@ type ProxyTrafficInfo struct {
|
|||||||
type ProxyStatistics struct {
|
type ProxyStatistics struct {
|
||||||
Name string
|
Name string
|
||||||
ProxyType string
|
ProxyType string
|
||||||
User string
|
|
||||||
ClientID string
|
|
||||||
TrafficIn metric.DateCounter
|
TrafficIn metric.DateCounter
|
||||||
TrafficOut metric.DateCounter
|
TrafficOut metric.DateCounter
|
||||||
CurConns metric.Counter
|
CurConns metric.Counter
|
||||||
@@ -82,7 +78,6 @@ type Collector interface {
|
|||||||
GetServer() *ServerStats
|
GetServer() *ServerStats
|
||||||
GetProxiesByType(proxyType string) []*ProxyStats
|
GetProxiesByType(proxyType string) []*ProxyStats
|
||||||
GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats
|
GetProxiesByTypeAndName(proxyType string, proxyName string) *ProxyStats
|
||||||
GetProxyByName(proxyName string) *ProxyStats
|
|
||||||
GetProxyTraffic(name string) *ProxyTrafficInfo
|
GetProxyTraffic(name string) *ProxyTrafficInfo
|
||||||
ClearOfflineProxies() (int, int)
|
ClearOfflineProxies() (int, int)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,7 +30,7 @@ func (m *serverMetrics) CloseClient() {
|
|||||||
m.clientCount.Dec()
|
m.clientCount.Dec()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (m *serverMetrics) NewProxy(name string, proxyType string, _ string, _ string) {
|
func (m *serverMetrics) NewProxy(name string, proxyType string) {
|
||||||
m.proxyCount.WithLabelValues(proxyType).Inc()
|
m.proxyCount.WithLabelValues(proxyType).Inc()
|
||||||
m.proxyCountDetailed.WithLabelValues(proxyType, name).Inc()
|
m.proxyCountDetailed.WithLabelValues(proxyType, name).Inc()
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -82,7 +82,6 @@ type Login struct {
|
|||||||
PrivilegeKey string `json:"privilege_key,omitempty"`
|
PrivilegeKey string `json:"privilege_key,omitempty"`
|
||||||
Timestamp int64 `json:"timestamp,omitempty"`
|
Timestamp int64 `json:"timestamp,omitempty"`
|
||||||
RunID string `json:"run_id,omitempty"`
|
RunID string `json:"run_id,omitempty"`
|
||||||
ClientID string `json:"client_id,omitempty"`
|
|
||||||
Metas map[string]string `json:"metas,omitempty"`
|
Metas map[string]string `json:"metas,omitempty"`
|
||||||
|
|
||||||
// Currently only effective for VirtualClient.
|
// Currently only effective for VirtualClient.
|
||||||
|
|||||||
@@ -220,7 +220,7 @@ func (c *Controller) HandleVisitor(m *msg.NatHoleVisitor, transporter transport.
|
|||||||
// Make hole-punching decisions based on the NAT information of the client and visitor.
|
// Make hole-punching decisions based on the NAT information of the client and visitor.
|
||||||
vResp, cResp, err := c.analysis(session)
|
vResp, cResp, err := c.analysis(session)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
log.Debugf("sid [%s] analysis error: %v", sid, err)
|
log.Debugf("sid [%s] analysis error: %v", err)
|
||||||
vResp = c.GenNatHoleResponse(session.visitorMsg.TransactionID, nil, err.Error())
|
vResp = c.GenNatHoleResponse(session.visitorMsg.TransactionID, nil, err.Error())
|
||||||
cResp = c.GenNatHoleResponse(session.clientMsg.TransactionID, nil, err.Error())
|
cResp = c.GenNatHoleResponse(session.clientMsg.TransactionID, nil, err.Error())
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,212 +0,0 @@
|
|||||||
// Copyright 2026 The LoliaTeam 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 client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"crypto/tls"
|
|
||||||
"crypto/x509"
|
|
||||||
"fmt"
|
|
||||||
"os"
|
|
||||||
"strings"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"golang.org/x/crypto/acme"
|
|
||||||
"golang.org/x/crypto/acme/autocert"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
func buildAutoTLSServerConfigWithHosts(pluginName string, auto *v1.AutoTLSOptions, fallbackHosts []string) (*tls.Config, error) {
|
|
||||||
if auto == nil || !auto.Enable {
|
|
||||||
return nil, fmt.Errorf("插件 %s 未启用 autoTLS", pluginName)
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := os.MkdirAll(auto.CacheDir, 0o700); err != nil {
|
|
||||||
return nil, fmt.Errorf("插件 %s 创建 autoTLS 缓存目录失败: %w", pluginName, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
hostSet := make(map[string]struct{})
|
|
||||||
hosts := make([]string, 0, len(auto.HostAllowList))
|
|
||||||
addHost := func(host string) {
|
|
||||||
host = strings.TrimSpace(strings.ToLower(host))
|
|
||||||
if host == "" {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if strings.Contains(host, "*") {
|
|
||||||
log.Warnf("[autoTLS][%s] 域名 [%s] 含通配符,自动申请不支持,已忽略", pluginName, host)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if _, ok := hostSet[host]; ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
hostSet[host] = struct{}{}
|
|
||||||
hosts = append(hosts, host)
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, host := range auto.HostAllowList {
|
|
||||||
addHost(host)
|
|
||||||
}
|
|
||||||
if len(hosts) == 0 {
|
|
||||||
for _, host := range fallbackHosts {
|
|
||||||
addHost(host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if len(hosts) == 0 {
|
|
||||||
return nil, fmt.Errorf("插件 %s 的 hostAllowList 为空;请设置 autoTLS.hostAllowList 或 customDomains", pluginName)
|
|
||||||
}
|
|
||||||
|
|
||||||
manager := &autocert.Manager{
|
|
||||||
Prompt: autocert.AcceptTOS,
|
|
||||||
Email: strings.TrimSpace(auto.Email),
|
|
||||||
HostPolicy: autocert.HostWhitelist(hosts...),
|
|
||||||
}
|
|
||||||
caDirURL := strings.TrimSpace(auto.CADirURL)
|
|
||||||
if caDirURL != "" {
|
|
||||||
manager.Client = &acme.Client{DirectoryURL: caDirURL}
|
|
||||||
} else {
|
|
||||||
caDirURL = autocert.DefaultACMEDirectory
|
|
||||||
}
|
|
||||||
managedHosts := make(map[string]struct{}, len(hosts))
|
|
||||||
for _, host := range hosts {
|
|
||||||
managedHosts[host] = struct{}{}
|
|
||||||
}
|
|
||||||
var warmupInProgress sync.Map
|
|
||||||
var warmupMissLogged sync.Map
|
|
||||||
manager.Cache = &autoTLSCache{
|
|
||||||
inner: autocert.DirCache(auto.CacheDir),
|
|
||||||
managedHosts: managedHosts,
|
|
||||||
pluginName: pluginName,
|
|
||||||
caDirURL: caDirURL,
|
|
||||||
warmupInProgress: &warmupInProgress,
|
|
||||||
warmupMissLogged: &warmupMissLogged,
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg := manager.TLSConfig()
|
|
||||||
log.Infof("[autoTLS][%s] 已启用 autoTLS,管理域名=%v,缓存目录=%s", pluginName, hosts, auto.CacheDir)
|
|
||||||
|
|
||||||
var readySeen sync.Map
|
|
||||||
|
|
||||||
handleCertReady := func(host string, cert *tls.Certificate) {
|
|
||||||
var (
|
|
||||||
notAfter time.Time
|
|
||||||
hasExpiry bool
|
|
||||||
)
|
|
||||||
if t, ok := getCertificateNotAfter(cert); ok {
|
|
||||||
notAfter = t
|
|
||||||
hasExpiry = true
|
|
||||||
}
|
|
||||||
|
|
||||||
_, readyLogged := readySeen.LoadOrStore(host, struct{}{})
|
|
||||||
if hasExpiry {
|
|
||||||
if !readyLogged {
|
|
||||||
log.Infof("[autoTLS][%s] 域名 [%s] 证书已就绪,过期时间 %s", pluginName, host, notAfter.Format(time.RFC3339))
|
|
||||||
}
|
|
||||||
} else if !readyLogged {
|
|
||||||
log.Infof("[autoTLS][%s] 域名 [%s] 证书已就绪", pluginName, host)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
cfg.GetCertificate = func(hello *tls.ClientHelloInfo) (*tls.Certificate, error) {
|
|
||||||
host := strings.TrimSpace(strings.ToLower(hello.ServerName))
|
|
||||||
if host == "" {
|
|
||||||
host = "<空SNI>"
|
|
||||||
}
|
|
||||||
|
|
||||||
cert, err := manager.GetCertificate(hello)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("[autoTLS][%s] 获取域名 [%s] 证书失败: %v", pluginName, host, err)
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
handleCertReady(host, cert)
|
|
||||||
return cert, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// Warm up certificates in background after startup.
|
|
||||||
for _, host := range hosts {
|
|
||||||
h := host
|
|
||||||
go func() {
|
|
||||||
// Leave time for listener setup and route registration.
|
|
||||||
time.Sleep(1 * time.Second)
|
|
||||||
warmupMissLogged.Delete(h)
|
|
||||||
warmupInProgress.Store(h, struct{}{})
|
|
||||||
cert, err := manager.GetCertificate(&tls.ClientHelloInfo{ServerName: h})
|
|
||||||
warmupInProgress.Delete(h)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("[autoTLS][%s] 域名 [%s] 预申请失败: %v", pluginName, h, err)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
handleCertReady(h, cert)
|
|
||||||
}()
|
|
||||||
}
|
|
||||||
return cfg, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func getCertificateNotAfter(cert *tls.Certificate) (time.Time, bool) {
|
|
||||||
if cert == nil {
|
|
||||||
return time.Time{}, false
|
|
||||||
}
|
|
||||||
if cert.Leaf != nil {
|
|
||||||
return cert.Leaf.NotAfter, true
|
|
||||||
}
|
|
||||||
if len(cert.Certificate) == 0 {
|
|
||||||
return time.Time{}, false
|
|
||||||
}
|
|
||||||
leaf, err := x509.ParseCertificate(cert.Certificate[0])
|
|
||||||
if err != nil {
|
|
||||||
return time.Time{}, false
|
|
||||||
}
|
|
||||||
return leaf.NotAfter, true
|
|
||||||
}
|
|
||||||
|
|
||||||
type autoTLSCache struct {
|
|
||||||
inner autocert.Cache
|
|
||||||
managedHosts map[string]struct{}
|
|
||||||
pluginName string
|
|
||||||
caDirURL string
|
|
||||||
warmupInProgress *sync.Map
|
|
||||||
warmupMissLogged *sync.Map
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *autoTLSCache) Get(ctx context.Context, key string) ([]byte, error) {
|
|
||||||
data, err := c.inner.Get(ctx, key)
|
|
||||||
if err != autocert.ErrCacheMiss {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
host := strings.TrimSuffix(key, "+rsa")
|
|
||||||
if _, ok := c.managedHosts[host]; !ok {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
if _, warming := c.warmupInProgress.Load(host); !warming {
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
if _, loaded := c.warmupMissLogged.LoadOrStore(host, struct{}{}); !loaded {
|
|
||||||
log.Infof("[autoTLS][%s] 开始预申请域名 [%s] 证书,申请方式=TLS-ALPN-01,caDirURL=%s", c.pluginName, host, c.caDirURL)
|
|
||||||
}
|
|
||||||
return data, err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *autoTLSCache) Put(ctx context.Context, key string, data []byte) error {
|
|
||||||
return c.inner.Put(ctx, key, data)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *autoTLSCache) Delete(ctx context.Context, key string) error {
|
|
||||||
return c.inner.Delete(ctx, key)
|
|
||||||
}
|
|
||||||
@@ -1,109 +0,0 @@
|
|||||||
// Copyright 2026 The LoliaTeam 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 client
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"net/http"
|
|
||||||
"net/url"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
netpkg "github.com/fatedier/frp/pkg/util/net"
|
|
||||||
)
|
|
||||||
|
|
||||||
func init() {
|
|
||||||
Register(v1.PluginHTTP2HTTPSRedirect, NewHTTP2HTTPSRedirectPlugin)
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTP2HTTPSRedirectPlugin struct {
|
|
||||||
opts *v1.HTTP2HTTPSRedirectPluginOptions
|
|
||||||
|
|
||||||
l *Listener
|
|
||||||
s *http.Server
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewHTTP2HTTPSRedirectPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
|
||||||
opts := options.(*v1.HTTP2HTTPSRedirectPluginOptions)
|
|
||||||
|
|
||||||
listener := NewProxyListener()
|
|
||||||
p := &HTTP2HTTPSRedirectPlugin{
|
|
||||||
opts: opts,
|
|
||||||
l: listener,
|
|
||||||
}
|
|
||||||
|
|
||||||
p.s = &http.Server{
|
|
||||||
Handler: http.HandlerFunc(func(w http.ResponseWriter, req *http.Request) {
|
|
||||||
http.Redirect(w, req, buildHTTPSRedirectURL(req, opts.HTTPSPort), http.StatusFound)
|
|
||||||
}),
|
|
||||||
ReadHeaderTimeout: 60 * time.Second,
|
|
||||||
}
|
|
||||||
|
|
||||||
go func() {
|
|
||||||
_ = p.s.Serve(listener)
|
|
||||||
}()
|
|
||||||
|
|
||||||
return p, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildHTTPSRedirectURL(req *http.Request, httpsPort int) string {
|
|
||||||
host := strings.TrimSpace(req.Host)
|
|
||||||
if host == "" {
|
|
||||||
host = strings.TrimSpace(req.URL.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
targetHost := host
|
|
||||||
if parsedHost, parsedPort, err := net.SplitHostPort(host); err == nil {
|
|
||||||
targetHost = parsedHost
|
|
||||||
if httpsPort == 0 && parsedPort == "443" {
|
|
||||||
httpsPort = 443
|
|
||||||
}
|
|
||||||
} else if strings.HasPrefix(host, "[") && strings.HasSuffix(host, "]") {
|
|
||||||
targetHost = strings.TrimSuffix(strings.TrimPrefix(host, "["), "]")
|
|
||||||
}
|
|
||||||
|
|
||||||
if httpsPort != 0 && httpsPort != 443 {
|
|
||||||
targetHost = net.JoinHostPort(targetHost, strconv.Itoa(httpsPort))
|
|
||||||
}
|
|
||||||
|
|
||||||
return (&url.URL{
|
|
||||||
Scheme: "https",
|
|
||||||
Host: targetHost,
|
|
||||||
Path: req.URL.Path,
|
|
||||||
RawPath: req.URL.RawPath,
|
|
||||||
RawQuery: req.URL.RawQuery,
|
|
||||||
}).String()
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *HTTP2HTTPSRedirectPlugin) Handle(_ context.Context, connInfo *ConnectionInfo) {
|
|
||||||
wrapConn := netpkg.WrapReadWriteCloserToConn(connInfo.Conn, connInfo.UnderlyingConn)
|
|
||||||
if connInfo.SrcAddr != nil {
|
|
||||||
wrapConn.SetRemoteAddr(connInfo.SrcAddr)
|
|
||||||
}
|
|
||||||
_ = p.l.PutConn(wrapConn)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *HTTP2HTTPSRedirectPlugin) Name() string {
|
|
||||||
return v1.PluginHTTP2HTTPSRedirect
|
|
||||||
}
|
|
||||||
|
|
||||||
func (p *HTTP2HTTPSRedirectPlugin) Close() error {
|
|
||||||
return p.s.Close()
|
|
||||||
}
|
|
||||||
@@ -46,7 +46,7 @@ type HTTPS2HTTPPlugin struct {
|
|||||||
s *http.Server
|
s *http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPS2HTTPPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
func NewHTTPS2HTTPPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
||||||
opts := options.(*v1.HTTPS2HTTPPluginOptions)
|
opts := options.(*v1.HTTPS2HTTPPluginOptions)
|
||||||
listener := NewProxyListener()
|
listener := NewProxyListener()
|
||||||
|
|
||||||
@@ -84,18 +84,9 @@ func NewHTTPS2HTTPPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions
|
|||||||
rp.ServeHTTP(w, r)
|
rp.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|
||||||
var tlsConfig *tls.Config
|
tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
|
||||||
var err error
|
if err != nil {
|
||||||
if p.opts.AutoTLS != nil && p.opts.AutoTLS.Enable {
|
return nil, fmt.Errorf("gen TLS config error: %v", err)
|
||||||
tlsConfig, err = buildAutoTLSServerConfigWithHosts(pluginCtx.Name, p.opts.AutoTLS, pluginCtx.HostAllowList)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("build autoTLS config error: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tlsConfig, err = transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("gen TLS config error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.s = &http.Server{
|
p.s = &http.Server{
|
||||||
|
|||||||
@@ -46,7 +46,7 @@ type HTTPS2HTTPSPlugin struct {
|
|||||||
s *http.Server
|
s *http.Server
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewHTTPS2HTTPSPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
func NewHTTPS2HTTPSPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
||||||
opts := options.(*v1.HTTPS2HTTPSPluginOptions)
|
opts := options.(*v1.HTTPS2HTTPSPluginOptions)
|
||||||
|
|
||||||
listener := NewProxyListener()
|
listener := NewProxyListener()
|
||||||
@@ -90,18 +90,9 @@ func NewHTTPS2HTTPSPlugin(pluginCtx PluginContext, options v1.ClientPluginOption
|
|||||||
rp.ServeHTTP(w, r)
|
rp.ServeHTTP(w, r)
|
||||||
})
|
})
|
||||||
|
|
||||||
var tlsConfig *tls.Config
|
tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
|
||||||
var err error
|
if err != nil {
|
||||||
if p.opts.AutoTLS != nil && p.opts.AutoTLS.Enable {
|
return nil, fmt.Errorf("gen TLS config error: %v", err)
|
||||||
tlsConfig, err = buildAutoTLSServerConfigWithHosts(pluginCtx.Name, p.opts.AutoTLS, pluginCtx.HostAllowList)
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("build autoTLS config error: %v", err)
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tlsConfig, err = transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, fmt.Errorf("gen TLS config error: %v", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
p.s = &http.Server{
|
p.s = &http.Server{
|
||||||
|
|||||||
@@ -30,7 +30,6 @@ import (
|
|||||||
|
|
||||||
type PluginContext struct {
|
type PluginContext struct {
|
||||||
Name string
|
Name string
|
||||||
HostAllowList []string
|
|
||||||
VnetController *vnet.Controller
|
VnetController *vnet.Controller
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -39,25 +39,16 @@ type TLS2RawPlugin struct {
|
|||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewTLS2RawPlugin(pluginCtx PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
func NewTLS2RawPlugin(_ PluginContext, options v1.ClientPluginOptions) (Plugin, error) {
|
||||||
opts := options.(*v1.TLS2RawPluginOptions)
|
opts := options.(*v1.TLS2RawPluginOptions)
|
||||||
|
|
||||||
p := &TLS2RawPlugin{
|
p := &TLS2RawPlugin{
|
||||||
opts: opts,
|
opts: opts,
|
||||||
}
|
}
|
||||||
|
|
||||||
var tlsConfig *tls.Config
|
tlsConfig, err := transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
|
||||||
var err error
|
if err != nil {
|
||||||
if p.opts.AutoTLS != nil && p.opts.AutoTLS.Enable {
|
return nil, err
|
||||||
tlsConfig, err = buildAutoTLSServerConfigWithHosts(pluginCtx.Name, p.opts.AutoTLS, pluginCtx.HostAllowList)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
tlsConfig, err = transport.NewServerTLSConfig(p.opts.CrtPath, p.opts.KeyPath, "")
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
p.tlsConfig = tlsConfig
|
p.tlsConfig = tlsConfig
|
||||||
return p, nil
|
return p, nil
|
||||||
|
|||||||
@@ -124,8 +124,8 @@ func Forwarder(dstAddr *net.UDPAddr, readCh <-chan *msg.UDPPacket, sendCh chan<-
|
|||||||
}
|
}
|
||||||
mu.Unlock()
|
mu.Unlock()
|
||||||
|
|
||||||
// Add proxy protocol header if configured (only for the first packet of a new connection)
|
// Add proxy protocol header if configured
|
||||||
if !ok && proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil {
|
if proxyProtocolVersion != "" && udpMsg.RemoteAddr != nil {
|
||||||
ppBuf, err := netpkg.BuildProxyProtocolHeader(udpMsg.RemoteAddr, dstAddr, proxyProtocolVersion)
|
ppBuf, err := netpkg.BuildProxyProtocolHeader(udpMsg.RemoteAddr, dstAddr, proxyProtocolVersion)
|
||||||
if err == nil {
|
if err == nil {
|
||||||
// Prepend proxy protocol header to the UDP payload
|
// Prepend proxy protocol header to the UDP payload
|
||||||
|
|||||||
@@ -11,7 +11,7 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"github.com/fatedier/frp/client/api"
|
"github.com/fatedier/frp/client"
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -32,7 +32,7 @@ func (c *Client) SetAuth(user, pwd string) {
|
|||||||
c.authPwd = pwd
|
c.authPwd = pwd
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxyStatusResp, error) {
|
func (c *Client) GetProxyStatus(ctx context.Context, name string) (*client.ProxyStatusResp, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -41,7 +41,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxySta
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allStatus := make(api.StatusResp)
|
allStatus := make(client.StatusResp)
|
||||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
||||||
}
|
}
|
||||||
@@ -55,7 +55,7 @@ func (c *Client) GetProxyStatus(ctx context.Context, name string) (*api.ProxySta
|
|||||||
return nil, fmt.Errorf("no proxy status found")
|
return nil, fmt.Errorf("no proxy status found")
|
||||||
}
|
}
|
||||||
|
|
||||||
func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error) {
|
func (c *Client) GetAllProxyStatus(ctx context.Context) (client.StatusResp, error) {
|
||||||
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
req, err := http.NewRequestWithContext(ctx, "GET", "http://"+c.address+"/api/status", nil)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -64,7 +64,7 @@ func (c *Client) GetAllProxyStatus(ctx context.Context) (api.StatusResp, error)
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
allStatus := make(api.StatusResp)
|
allStatus := make(client.StatusResp)
|
||||||
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
if err = json.Unmarshal([]byte(content), &allStatus); err != nil {
|
||||||
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
return nil, fmt.Errorf("unmarshal http response error: %s", strings.TrimSpace(content))
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,18 +0,0 @@
|
|||||||
package banner
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
|
||||||
)
|
|
||||||
|
|
||||||
func DisplayBanner() {
|
|
||||||
fmt.Println(" __ ___ __________ ____ ________ ____")
|
|
||||||
fmt.Println(" / / ____ / (_)___ _/ ____/ __ \\/ __ \\ / ____/ / / _/")
|
|
||||||
fmt.Println(" / / / __ \\/ / / __ `/ /_ / /_/ / /_/ /_____/ / / / / / ")
|
|
||||||
fmt.Println(" / /___/ /_/ / / / /_/ / __/ / _, _/ ____/_____/ /___/ /____/ / ")
|
|
||||||
fmt.Println("/_____/\\____/_/_/\\__,_/_/ /_/ |_/_/ \\____/_____/___/ ")
|
|
||||||
fmt.Println(" ")
|
|
||||||
log.Infof("Nya! %s 启动中", version.Full())
|
|
||||||
}
|
|
||||||
@@ -1,57 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/gorilla/mux"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Context struct {
|
|
||||||
Req *http.Request
|
|
||||||
Resp http.ResponseWriter
|
|
||||||
vars map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewContext(w http.ResponseWriter, r *http.Request) *Context {
|
|
||||||
return &Context{
|
|
||||||
Req: r,
|
|
||||||
Resp: w,
|
|
||||||
vars: mux.Vars(r),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) Param(key string) string {
|
|
||||||
return c.vars[key]
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) Query(key string) string {
|
|
||||||
return c.Req.URL.Query().Get(key)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) BindJSON(obj any) error {
|
|
||||||
body, err := io.ReadAll(c.Req.Body)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return json.Unmarshal(body, obj)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Context) Body() ([]byte, error) {
|
|
||||||
return io.ReadAll(c.Req.Body)
|
|
||||||
}
|
|
||||||
@@ -1,33 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package http
|
|
||||||
|
|
||||||
import "fmt"
|
|
||||||
|
|
||||||
type Error struct {
|
|
||||||
Code int
|
|
||||||
Err error
|
|
||||||
}
|
|
||||||
|
|
||||||
func (e *Error) Error() string {
|
|
||||||
return e.Err.Error()
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewError(code int, msg string) *Error {
|
|
||||||
return &Error{
|
|
||||||
Code: code,
|
|
||||||
Err: fmt.Errorf("%s", msg),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,66 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type GeneralResponse struct {
|
|
||||||
Code int
|
|
||||||
Msg string
|
|
||||||
}
|
|
||||||
|
|
||||||
// APIHandler is a handler function that returns a response object or an error.
|
|
||||||
type APIHandler func(ctx *Context) (any, error)
|
|
||||||
|
|
||||||
// MakeHTTPHandlerFunc turns a normal APIHandler into a http.HandlerFunc.
|
|
||||||
func MakeHTTPHandlerFunc(handler APIHandler) http.HandlerFunc {
|
|
||||||
return func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
ctx := NewContext(w, r)
|
|
||||||
res, err := handler(ctx)
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("http response [%s]: error: %v", r.URL.Path, err)
|
|
||||||
code := http.StatusInternalServerError
|
|
||||||
if e, ok := err.(*Error); ok {
|
|
||||||
code = e.Code
|
|
||||||
}
|
|
||||||
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(code)
|
|
||||||
_ = json.NewEncoder(w).Encode(GeneralResponse{Code: code, Msg: err.Error()})
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
if res == nil {
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
switch v := res.(type) {
|
|
||||||
case []byte:
|
|
||||||
_, _ = w.Write(v)
|
|
||||||
case string:
|
|
||||||
_, _ = w.Write([]byte(v))
|
|
||||||
default:
|
|
||||||
w.Header().Set("Content-Type", "application/json")
|
|
||||||
w.WriteHeader(http.StatusOK)
|
|
||||||
_ = json.NewEncoder(w).Encode(v)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,40 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package http
|
|
||||||
|
|
||||||
import (
|
|
||||||
"net/http"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
)
|
|
||||||
|
|
||||||
type responseWriter struct {
|
|
||||||
http.ResponseWriter
|
|
||||||
code int
|
|
||||||
}
|
|
||||||
|
|
||||||
func (rw *responseWriter) WriteHeader(code int) {
|
|
||||||
rw.code = code
|
|
||||||
rw.ResponseWriter.WriteHeader(code)
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewRequestLogger(next http.Handler) http.Handler {
|
|
||||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
log.Infof("http request: [%s]", r.URL.Path)
|
|
||||||
rw := &responseWriter{ResponseWriter: w, code: http.StatusOK}
|
|
||||||
next.ServeHTTP(rw, r)
|
|
||||||
log.Infof("http response [%s]: code [%d]", r.URL.Path, rw.code)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
@@ -16,18 +16,13 @@ package log
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
"io"
|
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/charmbracelet/lipgloss"
|
"github.com/fatedier/golib/log"
|
||||||
"github.com/charmbracelet/log"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var (
|
||||||
TraceLevel = log.DebugLevel
|
TraceLevel = log.TraceLevel
|
||||||
DebugLevel = log.DebugLevel
|
DebugLevel = log.DebugLevel
|
||||||
InfoLevel = log.InfoLevel
|
InfoLevel = log.InfoLevel
|
||||||
WarnLevel = log.WarnLevel
|
WarnLevel = log.WarnLevel
|
||||||
@@ -37,158 +32,39 @@ var (
|
|||||||
var Logger *log.Logger
|
var Logger *log.Logger
|
||||||
|
|
||||||
func init() {
|
func init() {
|
||||||
Logger = log.NewWithOptions(os.Stderr, log.Options{
|
Logger = log.New(
|
||||||
ReportCaller: true,
|
log.WithCaller(true),
|
||||||
ReportTimestamp: true,
|
log.AddCallerSkip(1),
|
||||||
TimeFormat: time.Kitchen,
|
log.WithLevel(log.InfoLevel),
|
||||||
Prefix: "LoliaFRP-CLI",
|
)
|
||||||
CallerOffset: 1,
|
|
||||||
})
|
|
||||||
// 设置自定义样式以支持 Trace 级别
|
|
||||||
styles := log.DefaultStyles()
|
|
||||||
styles.Levels[TraceLevel] = lipgloss.NewStyle().
|
|
||||||
SetString("TRACE").
|
|
||||||
Bold(true).
|
|
||||||
MaxWidth(5).
|
|
||||||
Foreground(lipgloss.Color("61"))
|
|
||||||
Logger.SetStyles(styles)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func InitLogger(logPath string, levelStr string, maxDays int, disableLogColor bool) {
|
func InitLogger(logPath string, levelStr string, maxDays int, disableLogColor bool) {
|
||||||
var output io.Writer
|
options := []log.Option{}
|
||||||
var err error
|
|
||||||
|
|
||||||
if logPath == "console" {
|
if logPath == "console" {
|
||||||
output = os.Stdout
|
if !disableLogColor {
|
||||||
} else {
|
options = append(options,
|
||||||
// Use rotating file writer
|
log.WithOutput(log.NewConsoleWriter(log.ConsoleConfig{
|
||||||
output, err = NewRotateFileWriter(logPath, maxDays)
|
Colorful: true,
|
||||||
if err != nil {
|
}, os.Stdout)),
|
||||||
// Fallback to console if file creation fails
|
)
|
||||||
output = os.Stdout
|
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
writer := log.NewRotateFileWriter(log.RotateFileConfig{
|
||||||
|
FileName: logPath,
|
||||||
|
Mode: log.RotateFileModeDaily,
|
||||||
|
MaxDays: maxDays,
|
||||||
|
})
|
||||||
|
writer.Init()
|
||||||
|
options = append(options, log.WithOutput(writer))
|
||||||
}
|
}
|
||||||
|
|
||||||
level, err := log.ParseLevel(levelStr)
|
level, err := log.ParseLevel(levelStr)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
level = log.InfoLevel
|
level = log.InfoLevel
|
||||||
}
|
}
|
||||||
|
options = append(options, log.WithLevel(level))
|
||||||
Logger = log.NewWithOptions(output, log.Options{
|
Logger = Logger.WithOptions(options...)
|
||||||
ReportCaller: true,
|
|
||||||
ReportTimestamp: true,
|
|
||||||
TimeFormat: time.Kitchen,
|
|
||||||
Prefix: "LoliaFRP-CLI",
|
|
||||||
CallerOffset: 1,
|
|
||||||
Level: level,
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
// NewRotateFileWriter creates a rotating file writer
|
|
||||||
func NewRotateFileWriter(filePath string, maxDays int) (*RotateFileWriter, error) {
|
|
||||||
w := &RotateFileWriter{
|
|
||||||
filePath: filePath,
|
|
||||||
maxDays: maxDays,
|
|
||||||
lastRotate: time.Now(),
|
|
||||||
currentDate: time.Now().Format("2006-01-02"),
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := w.openFile(); err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
return w, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// RotateFileWriter implements io.Writer with daily rotation
|
|
||||||
type RotateFileWriter struct {
|
|
||||||
filePath string
|
|
||||||
maxDays int
|
|
||||||
file *os.File
|
|
||||||
lastRotate time.Time
|
|
||||||
currentDate string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *RotateFileWriter) openFile() error {
|
|
||||||
var err error
|
|
||||||
w.file, err = os.OpenFile(w.filePath, os.O_CREATE|os.O_WRONLY|os.O_APPEND, 0o644)
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *RotateFileWriter) checkRotate() error {
|
|
||||||
now := time.Now()
|
|
||||||
currentDate := now.Format("2006-01-02")
|
|
||||||
|
|
||||||
if currentDate != w.currentDate {
|
|
||||||
// Close current file
|
|
||||||
if w.file != nil {
|
|
||||||
w.file.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// Rename current file with date suffix
|
|
||||||
oldPath := w.filePath
|
|
||||||
newPath := w.filePath + "." + w.currentDate
|
|
||||||
if _, err := os.Stat(oldPath); err == nil {
|
|
||||||
if err := os.Rename(oldPath, newPath); err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Clean up old log files
|
|
||||||
w.cleanupOldLogs(now)
|
|
||||||
|
|
||||||
// Update current date and open new file
|
|
||||||
w.currentDate = currentDate
|
|
||||||
w.lastRotate = now
|
|
||||||
return w.openFile()
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *RotateFileWriter) cleanupOldLogs(now time.Time) {
|
|
||||||
if w.maxDays <= 0 {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
cutoffDate := now.AddDate(0, 0, -w.maxDays)
|
|
||||||
|
|
||||||
// Find and remove old log files
|
|
||||||
dir := filepath.Dir(w.filePath)
|
|
||||||
base := filepath.Base(w.filePath)
|
|
||||||
|
|
||||||
files, _ := os.ReadDir(dir)
|
|
||||||
for _, f := range files {
|
|
||||||
if f.IsDir() {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
|
|
||||||
name := f.Name()
|
|
||||||
if strings.HasPrefix(name, base+".") {
|
|
||||||
// Extract date from filename (base.YYYY-MM-DD)
|
|
||||||
dateStr := strings.TrimPrefix(name, base+".")
|
|
||||||
if len(dateStr) == 10 {
|
|
||||||
fileDate, err := time.Parse("2006-01-02", dateStr)
|
|
||||||
if err == nil && fileDate.Before(cutoffDate) {
|
|
||||||
os.Remove(filepath.Join(dir, name))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *RotateFileWriter) Write(p []byte) (n int, err error) {
|
|
||||||
if err := w.checkRotate(); err != nil {
|
|
||||||
return 0, err
|
|
||||||
}
|
|
||||||
return w.file.Write(p)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (w *RotateFileWriter) Close() error {
|
|
||||||
if w.file != nil {
|
|
||||||
return w.file.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func Errorf(format string, v ...any) {
|
func Errorf(format string, v ...any) {
|
||||||
@@ -199,10 +75,6 @@ func Warnf(format string, v ...any) {
|
|||||||
Logger.Warnf(format, v...)
|
Logger.Warnf(format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Info(format string, v ...any) {
|
|
||||||
Logger.Info(format, v...)
|
|
||||||
}
|
|
||||||
|
|
||||||
func Infof(format string, v ...any) {
|
func Infof(format string, v ...any) {
|
||||||
Logger.Infof(format, v...)
|
Logger.Infof(format, v...)
|
||||||
}
|
}
|
||||||
@@ -212,12 +84,11 @@ func Debugf(format string, v ...any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func Tracef(format string, v ...any) {
|
func Tracef(format string, v ...any) {
|
||||||
Logger.Logf(TraceLevel, format, v...)
|
Logger.Tracef(format, v...)
|
||||||
}
|
}
|
||||||
|
|
||||||
func Logf(level log.Level, offset int, format string, v ...any) {
|
func Logf(level log.Level, offset int, format string, v ...any) {
|
||||||
// charmbracelet/log doesn't support offset, so we ignore it
|
Logger.Logf(level, offset, format, v...)
|
||||||
Logger.Logf(level, format, v...)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type WriteLogger struct {
|
type WriteLogger struct {
|
||||||
@@ -233,8 +104,6 @@ func NewWriteLogger(level log.Level, offset int) *WriteLogger {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (w *WriteLogger) Write(p []byte) (n int, err error) {
|
func (w *WriteLogger) Write(p []byte) (n int, err error) {
|
||||||
// charmbracelet/log doesn't support offset in Log
|
Logger.Log(w.level, w.offset, string(bytes.TrimRight(p, "\n")))
|
||||||
msg := string(bytes.TrimRight(p, "\n"))
|
|
||||||
Logger.Log(w.level, msg)
|
|
||||||
return len(p), nil
|
return len(p), nil
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -14,7 +14,7 @@
|
|||||||
|
|
||||||
package version
|
package version
|
||||||
|
|
||||||
var version = "LoliaFRP-CLI 0.67.4"
|
var version = "LoliaFRP-0.65.0"
|
||||||
|
|
||||||
func Full() string {
|
func Full() string {
|
||||||
return version
|
return version
|
||||||
|
|||||||
@@ -28,70 +28,23 @@ var NotFoundPagePath = ""
|
|||||||
|
|
||||||
const (
|
const (
|
||||||
NotFound = `<!DOCTYPE html>
|
NotFound = `<!DOCTYPE html>
|
||||||
<html lang="zh-CN">
|
<html>
|
||||||
<head>
|
<head>
|
||||||
<meta charset="UTF-8">
|
<title>Not Found</title>
|
||||||
<meta name="viewport" content="width=device-width, initial-scale=1.0">
|
<style>
|
||||||
<title>404 - 未绑定域名</title>
|
body {
|
||||||
<style>
|
width: 35em;
|
||||||
body {
|
margin: 0 auto;
|
||||||
font-family: -apple-system, sans-serif;
|
font-family: Tahoma, Verdana, Arial, sans-serif;
|
||||||
display: flex;
|
}
|
||||||
align-items: center;
|
</style>
|
||||||
justify-content: center;
|
|
||||||
min-height: 100vh;
|
|
||||||
margin: 0;
|
|
||||||
background: #fff;
|
|
||||||
color: #333;
|
|
||||||
}
|
|
||||||
.container {
|
|
||||||
max-width: 600px;
|
|
||||||
padding: 40px 20px;
|
|
||||||
text-align: center;
|
|
||||||
}
|
|
||||||
h1 {
|
|
||||||
font-size: 32px;
|
|
||||||
font-weight: 600;
|
|
||||||
margin-bottom: 20px;
|
|
||||||
}
|
|
||||||
p {
|
|
||||||
line-height: 1.8;
|
|
||||||
color: #666;
|
|
||||||
margin: 10px 0;
|
|
||||||
}
|
|
||||||
ul {
|
|
||||||
text-align: left;
|
|
||||||
margin: 20px auto;
|
|
||||||
max-width: 400px;
|
|
||||||
}
|
|
||||||
li {
|
|
||||||
margin: 8px 0;
|
|
||||||
color: #666;
|
|
||||||
}
|
|
||||||
a {
|
|
||||||
color: #0066cc;
|
|
||||||
text-decoration: none;
|
|
||||||
}
|
|
||||||
a:hover { text-decoration: underline; }
|
|
||||||
.footer {
|
|
||||||
margin-top: 40px;
|
|
||||||
font-size: 14px;
|
|
||||||
color: #999;
|
|
||||||
}
|
|
||||||
</style>
|
|
||||||
</head>
|
</head>
|
||||||
<body>
|
<body>
|
||||||
<div class="container">
|
<h1>The page you requested was not found.</h1>
|
||||||
<h1>域名未绑定</h1>
|
<p>Sorry, the page you are looking for is currently unavailable.<br/>
|
||||||
<p>这个域名还没有绑定到任何隧道哦 (;д;)</p>
|
Please try again later.</p>
|
||||||
<p><strong>可能是这些原因:</strong></p>
|
<p>The server is powered by <a href="https://github.com/fatedier/frp">frp</a>.</p>
|
||||||
<ul>
|
<p><em>Faithfully yours, frp.</em></p>
|
||||||
<li>域名配置不对,或者没有正确解析</li>
|
|
||||||
<li>隧道可能还没启动,或者已经停止</li>
|
|
||||||
<li>自定义域名忘记在服务端配置了</li>
|
|
||||||
</ul>
|
|
||||||
<div class="footer">由 <a href="https://lolia.link/">LoliaFRP</a> 与捐赠者们用爱发电</div>
|
|
||||||
</div>
|
|
||||||
</body>
|
</body>
|
||||||
</html>
|
</html>
|
||||||
`
|
`
|
||||||
@@ -116,7 +69,7 @@ func getNotFoundPageContent() []byte {
|
|||||||
|
|
||||||
func NotFoundResponse() *http.Response {
|
func NotFoundResponse() *http.Response {
|
||||||
header := make(http.Header)
|
header := make(http.Header)
|
||||||
header.Set("server", version.Full())
|
header.Set("server", "frp/"+version.Full())
|
||||||
header.Set("Content-Type", "text/html")
|
header.Set("Content-Type", "text/html")
|
||||||
|
|
||||||
content := getNotFoundPageContent()
|
content := getNotFoundPageContent()
|
||||||
|
|||||||
@@ -111,5 +111,5 @@ func (l *Logger) Debugf(format string, v ...any) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (l *Logger) Tracef(format string, v ...any) {
|
func (l *Logger) Tracef(format string, v ...any) {
|
||||||
log.Logger.Logf(log.TraceLevel, l.prefixString+format, v...)
|
log.Logger.Tracef(l.prefixString+format, v...)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,449 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"cmp"
|
|
||||||
"encoding/json"
|
|
||||||
"fmt"
|
|
||||||
"net/http"
|
|
||||||
"slices"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"github.com/fatedier/frp/pkg/config/types"
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
"github.com/fatedier/frp/pkg/metrics/mem"
|
|
||||||
httppkg "github.com/fatedier/frp/pkg/util/http"
|
|
||||||
"github.com/fatedier/frp/pkg/util/log"
|
|
||||||
"github.com/fatedier/frp/pkg/util/version"
|
|
||||||
"github.com/fatedier/frp/server/proxy"
|
|
||||||
"github.com/fatedier/frp/server/registry"
|
|
||||||
)
|
|
||||||
|
|
||||||
type Controller struct {
|
|
||||||
// dependencies
|
|
||||||
serverCfg *v1.ServerConfig
|
|
||||||
clientRegistry *registry.ClientRegistry
|
|
||||||
pxyManager ProxyManager
|
|
||||||
ctlManager ControlManager
|
|
||||||
}
|
|
||||||
|
|
||||||
type ProxyManager interface {
|
|
||||||
GetByName(name string) (proxy.Proxy, bool)
|
|
||||||
}
|
|
||||||
|
|
||||||
type ControlManager interface {
|
|
||||||
CloseAllProxyByName(proxyName string) error
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewController(
|
|
||||||
serverCfg *v1.ServerConfig,
|
|
||||||
clientRegistry *registry.ClientRegistry,
|
|
||||||
pxyManager ProxyManager,
|
|
||||||
ctlManager ControlManager,
|
|
||||||
) *Controller {
|
|
||||||
return &Controller{
|
|
||||||
serverCfg: serverCfg,
|
|
||||||
clientRegistry: clientRegistry,
|
|
||||||
pxyManager: pxyManager,
|
|
||||||
ctlManager: ctlManager,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/serverinfo
|
|
||||||
func (c *Controller) APIServerInfo(ctx *httppkg.Context) (any, error) {
|
|
||||||
serverStats := mem.StatsCollector.GetServer()
|
|
||||||
svrResp := ServerInfoResp{
|
|
||||||
Version: version.Full(),
|
|
||||||
BindPort: c.serverCfg.BindPort,
|
|
||||||
VhostHTTPPort: c.serverCfg.VhostHTTPPort,
|
|
||||||
VhostHTTPSPort: c.serverCfg.VhostHTTPSPort,
|
|
||||||
TCPMuxHTTPConnectPort: c.serverCfg.TCPMuxHTTPConnectPort,
|
|
||||||
KCPBindPort: c.serverCfg.KCPBindPort,
|
|
||||||
QUICBindPort: c.serverCfg.QUICBindPort,
|
|
||||||
SubdomainHost: c.serverCfg.SubDomainHost,
|
|
||||||
MaxPoolCount: c.serverCfg.Transport.MaxPoolCount,
|
|
||||||
MaxPortsPerClient: c.serverCfg.MaxPortsPerClient,
|
|
||||||
HeartBeatTimeout: c.serverCfg.Transport.HeartbeatTimeout,
|
|
||||||
AllowPortsStr: types.PortsRangeSlice(c.serverCfg.AllowPorts).String(),
|
|
||||||
TLSForce: c.serverCfg.Transport.TLS.Force,
|
|
||||||
|
|
||||||
TotalTrafficIn: serverStats.TotalTrafficIn,
|
|
||||||
TotalTrafficOut: serverStats.TotalTrafficOut,
|
|
||||||
CurConns: serverStats.CurConns,
|
|
||||||
ClientCounts: serverStats.ClientCounts,
|
|
||||||
ProxyTypeCounts: serverStats.ProxyTypeCounts,
|
|
||||||
}
|
|
||||||
// For API that returns struct, we can just return it.
|
|
||||||
// But current GeneralResponse.Msg in legacy code expects a JSON string.
|
|
||||||
// Since MakeHTTPHandlerFunc handles struct by encoding to JSON, we can return svrResp directly?
|
|
||||||
// The original code wraps it in GeneralResponse{Msg: string(json)}.
|
|
||||||
// If we return svrResp, the response body will be the JSON of svrResp.
|
|
||||||
// We should check if the frontend expects { "code": 200, "msg": "{...}" } or just {...}.
|
|
||||||
// Looking at previous code:
|
|
||||||
// res := GeneralResponse{Code: 200}
|
|
||||||
// buf, _ := json.Marshal(&svrResp)
|
|
||||||
// res.Msg = string(buf)
|
|
||||||
// Response body: {"code": 200, "msg": "{\"version\":...}"}
|
|
||||||
// Wait, is it double encoded JSON? Yes it seems so!
|
|
||||||
// Let's check dashboard_api.go original code again.
|
|
||||||
// Yes: res.Msg = string(buf).
|
|
||||||
// So the frontend expects { "code": 200, "msg": "JSON_STRING" }.
|
|
||||||
// This is kind of ugly, but we must preserve compatibility.
|
|
||||||
|
|
||||||
return svrResp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/clients
|
|
||||||
func (c *Controller) APIClientList(ctx *httppkg.Context) (any, error) {
|
|
||||||
if c.clientRegistry == nil {
|
|
||||||
return nil, fmt.Errorf("client registry unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
userFilter := ctx.Query("user")
|
|
||||||
clientIDFilter := ctx.Query("clientId")
|
|
||||||
runIDFilter := ctx.Query("runId")
|
|
||||||
statusFilter := strings.ToLower(ctx.Query("status"))
|
|
||||||
|
|
||||||
records := c.clientRegistry.List()
|
|
||||||
items := make([]ClientInfoResp, 0, len(records))
|
|
||||||
for _, info := range records {
|
|
||||||
if userFilter != "" && info.User != userFilter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if clientIDFilter != "" && info.ClientID() != clientIDFilter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if runIDFilter != "" && info.RunID != runIDFilter {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
if !matchStatusFilter(info.Online, statusFilter) {
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
items = append(items, buildClientInfoResp(info))
|
|
||||||
}
|
|
||||||
|
|
||||||
slices.SortFunc(items, func(a, b ClientInfoResp) int {
|
|
||||||
if v := cmp.Compare(a.User, b.User); v != 0 {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
if v := cmp.Compare(a.ClientID, b.ClientID); v != 0 {
|
|
||||||
return v
|
|
||||||
}
|
|
||||||
return cmp.Compare(a.Key, b.Key)
|
|
||||||
})
|
|
||||||
|
|
||||||
return items, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/clients/{key}
|
|
||||||
func (c *Controller) APIClientDetail(ctx *httppkg.Context) (any, error) {
|
|
||||||
key := ctx.Param("key")
|
|
||||||
if key == "" {
|
|
||||||
return nil, fmt.Errorf("missing client key")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.clientRegistry == nil {
|
|
||||||
return nil, fmt.Errorf("client registry unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
info, ok := c.clientRegistry.GetByKey(key)
|
|
||||||
if !ok {
|
|
||||||
return nil, httppkg.NewError(http.StatusNotFound, fmt.Sprintf("client %s not found", key))
|
|
||||||
}
|
|
||||||
|
|
||||||
return buildClientInfoResp(info), nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/proxy/:type
|
|
||||||
func (c *Controller) APIProxyByType(ctx *httppkg.Context) (any, error) {
|
|
||||||
proxyType := ctx.Param("type")
|
|
||||||
|
|
||||||
proxyInfoResp := GetProxyInfoResp{}
|
|
||||||
proxyInfoResp.Proxies = c.getProxyStatsByType(proxyType)
|
|
||||||
slices.SortFunc(proxyInfoResp.Proxies, func(a, b *ProxyStatsInfo) int {
|
|
||||||
return cmp.Compare(a.Name, b.Name)
|
|
||||||
})
|
|
||||||
|
|
||||||
return proxyInfoResp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/proxy/:type/:name
|
|
||||||
func (c *Controller) APIProxyByTypeAndName(ctx *httppkg.Context) (any, error) {
|
|
||||||
proxyType := ctx.Param("type")
|
|
||||||
name := ctx.Param("name")
|
|
||||||
|
|
||||||
proxyStatsResp, code, msg := c.getProxyStatsByTypeAndName(proxyType, name)
|
|
||||||
if code != 200 {
|
|
||||||
return nil, httppkg.NewError(code, msg)
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxyStatsResp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/traffic/:name
|
|
||||||
func (c *Controller) APIProxyTraffic(ctx *httppkg.Context) (any, error) {
|
|
||||||
name := ctx.Param("name")
|
|
||||||
|
|
||||||
trafficResp := GetProxyTrafficResp{}
|
|
||||||
trafficResp.Name = name
|
|
||||||
proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name)
|
|
||||||
|
|
||||||
if proxyTrafficInfo == nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found")
|
|
||||||
}
|
|
||||||
trafficResp.TrafficIn = proxyTrafficInfo.TrafficIn
|
|
||||||
trafficResp.TrafficOut = proxyTrafficInfo.TrafficOut
|
|
||||||
|
|
||||||
return trafficResp, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/proxies/:name
|
|
||||||
func (c *Controller) APIProxyByName(ctx *httppkg.Context) (any, error) {
|
|
||||||
name := ctx.Param("name")
|
|
||||||
|
|
||||||
ps := mem.StatsCollector.GetProxyByName(name)
|
|
||||||
if ps == nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusNotFound, "no proxy info found")
|
|
||||||
}
|
|
||||||
|
|
||||||
proxyInfo := GetProxyStatsResp{
|
|
||||||
Name: ps.Name,
|
|
||||||
User: ps.User,
|
|
||||||
ClientID: ps.ClientID,
|
|
||||||
TodayTrafficIn: ps.TodayTrafficIn,
|
|
||||||
TodayTrafficOut: ps.TodayTrafficOut,
|
|
||||||
CurConns: ps.CurConns,
|
|
||||||
LastStartTime: ps.LastStartTime,
|
|
||||||
LastCloseTime: ps.LastCloseTime,
|
|
||||||
}
|
|
||||||
|
|
||||||
if pxy, ok := c.pxyManager.GetByName(name); ok {
|
|
||||||
content, err := json.Marshal(pxy.GetConfigurer())
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("marshal proxy [%s] conf info error: %v", name, err)
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "parse conf error")
|
|
||||||
}
|
|
||||||
proxyInfo.Conf = getConfByType(ps.Type)
|
|
||||||
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
|
||||||
log.Warnf("unmarshal proxy [%s] conf info error: %v", name, err)
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "parse conf error")
|
|
||||||
}
|
|
||||||
proxyInfo.Status = "online"
|
|
||||||
c.fillProxyClientInfo(&proxyClientInfo{
|
|
||||||
clientVersion: &proxyInfo.ClientVersion,
|
|
||||||
}, pxy)
|
|
||||||
} else {
|
|
||||||
proxyInfo.Status = "offline"
|
|
||||||
}
|
|
||||||
|
|
||||||
return proxyInfo, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// POST /api/proxy/:name/close
|
|
||||||
func (c *Controller) APICloseProxyByName(ctx *httppkg.Context) (any, error) {
|
|
||||||
name := ctx.Param("name")
|
|
||||||
if name == "" {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "proxy name required")
|
|
||||||
}
|
|
||||||
|
|
||||||
if c.ctlManager == nil {
|
|
||||||
return nil, fmt.Errorf("control manager unavailable")
|
|
||||||
}
|
|
||||||
|
|
||||||
if err := c.ctlManager.CloseAllProxyByName(name); err != nil {
|
|
||||||
return nil, httppkg.NewError(http.StatusNotFound, err.Error())
|
|
||||||
}
|
|
||||||
|
|
||||||
return httppkg.GeneralResponse{Code: 200, Msg: "ok"}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// DELETE /api/proxies?status=offline
|
|
||||||
func (c *Controller) DeleteProxies(ctx *httppkg.Context) (any, error) {
|
|
||||||
status := ctx.Query("status")
|
|
||||||
if status != "offline" {
|
|
||||||
return nil, httppkg.NewError(http.StatusBadRequest, "status only support offline")
|
|
||||||
}
|
|
||||||
cleared, total := mem.StatsCollector.ClearOfflineProxies()
|
|
||||||
log.Infof("cleared [%d] offline proxies, total [%d] proxies", cleared, total)
|
|
||||||
return httppkg.GeneralResponse{Code: 200, Msg: "success"}, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) {
|
|
||||||
proxyStats := mem.StatsCollector.GetProxiesByType(proxyType)
|
|
||||||
proxyInfos = make([]*ProxyStatsInfo, 0, len(proxyStats))
|
|
||||||
for _, ps := range proxyStats {
|
|
||||||
proxyInfo := &ProxyStatsInfo{
|
|
||||||
User: ps.User,
|
|
||||||
ClientID: ps.ClientID,
|
|
||||||
}
|
|
||||||
if pxy, ok := c.pxyManager.GetByName(ps.Name); ok {
|
|
||||||
content, err := json.Marshal(pxy.GetConfigurer())
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
proxyInfo.Conf = getConfByType(ps.Type)
|
|
||||||
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
|
||||||
log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
||||||
continue
|
|
||||||
}
|
|
||||||
proxyInfo.Status = "online"
|
|
||||||
c.fillProxyClientInfo(&proxyClientInfo{
|
|
||||||
clientVersion: &proxyInfo.ClientVersion,
|
|
||||||
}, pxy)
|
|
||||||
} else {
|
|
||||||
proxyInfo.Status = "offline"
|
|
||||||
}
|
|
||||||
proxyInfo.Name = ps.Name
|
|
||||||
proxyInfo.TodayTrafficIn = ps.TodayTrafficIn
|
|
||||||
proxyInfo.TodayTrafficOut = ps.TodayTrafficOut
|
|
||||||
proxyInfo.CurConns = ps.CurConns
|
|
||||||
proxyInfo.LastStartTime = ps.LastStartTime
|
|
||||||
proxyInfo.LastCloseTime = ps.LastCloseTime
|
|
||||||
proxyInfos = append(proxyInfos, proxyInfo)
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp, code int, msg string) {
|
|
||||||
proxyInfo.Name = proxyName
|
|
||||||
ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName)
|
|
||||||
if ps == nil {
|
|
||||||
code = 404
|
|
||||||
msg = "no proxy info found"
|
|
||||||
} else {
|
|
||||||
proxyInfo.User = ps.User
|
|
||||||
proxyInfo.ClientID = ps.ClientID
|
|
||||||
if pxy, ok := c.pxyManager.GetByName(proxyName); ok {
|
|
||||||
content, err := json.Marshal(pxy.GetConfigurer())
|
|
||||||
if err != nil {
|
|
||||||
log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
||||||
code = 400
|
|
||||||
msg = "parse conf error"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
proxyInfo.Conf = getConfByType(ps.Type)
|
|
||||||
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
|
||||||
log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
|
|
||||||
code = 400
|
|
||||||
msg = "parse conf error"
|
|
||||||
return
|
|
||||||
}
|
|
||||||
proxyInfo.Status = "online"
|
|
||||||
} else {
|
|
||||||
proxyInfo.Status = "offline"
|
|
||||||
}
|
|
||||||
proxyInfo.TodayTrafficIn = ps.TodayTrafficIn
|
|
||||||
proxyInfo.TodayTrafficOut = ps.TodayTrafficOut
|
|
||||||
proxyInfo.CurConns = ps.CurConns
|
|
||||||
proxyInfo.LastStartTime = ps.LastStartTime
|
|
||||||
proxyInfo.LastCloseTime = ps.LastCloseTime
|
|
||||||
code = 200
|
|
||||||
}
|
|
||||||
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func buildClientInfoResp(info registry.ClientInfo) ClientInfoResp {
|
|
||||||
resp := ClientInfoResp{
|
|
||||||
Key: info.Key,
|
|
||||||
User: info.User,
|
|
||||||
ClientID: info.ClientID(),
|
|
||||||
RunID: info.RunID,
|
|
||||||
Hostname: info.Hostname,
|
|
||||||
ClientIP: info.IP,
|
|
||||||
FirstConnectedAt: toUnix(info.FirstConnectedAt),
|
|
||||||
LastConnectedAt: toUnix(info.LastConnectedAt),
|
|
||||||
Online: info.Online,
|
|
||||||
}
|
|
||||||
if !info.DisconnectedAt.IsZero() {
|
|
||||||
resp.DisconnectedAt = info.DisconnectedAt.Unix()
|
|
||||||
}
|
|
||||||
return resp
|
|
||||||
}
|
|
||||||
|
|
||||||
type proxyClientInfo struct {
|
|
||||||
user *string
|
|
||||||
clientID *string
|
|
||||||
clientVersion *string
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Controller) fillProxyClientInfo(proxyInfo *proxyClientInfo, pxy proxy.Proxy) {
|
|
||||||
loginMsg := pxy.GetLoginMsg()
|
|
||||||
if loginMsg == nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if proxyInfo.user != nil {
|
|
||||||
*proxyInfo.user = loginMsg.User
|
|
||||||
}
|
|
||||||
if proxyInfo.clientVersion != nil {
|
|
||||||
*proxyInfo.clientVersion = loginMsg.Version
|
|
||||||
}
|
|
||||||
if info, ok := c.clientRegistry.GetByRunID(loginMsg.RunID); ok {
|
|
||||||
if proxyInfo.clientID != nil {
|
|
||||||
*proxyInfo.clientID = info.ClientID()
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if proxyInfo.clientID != nil {
|
|
||||||
*proxyInfo.clientID = loginMsg.ClientID
|
|
||||||
if *proxyInfo.clientID == "" {
|
|
||||||
*proxyInfo.clientID = loginMsg.RunID
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func toUnix(t time.Time) int64 {
|
|
||||||
if t.IsZero() {
|
|
||||||
return 0
|
|
||||||
}
|
|
||||||
return t.Unix()
|
|
||||||
}
|
|
||||||
|
|
||||||
func matchStatusFilter(online bool, filter string) bool {
|
|
||||||
switch strings.ToLower(filter) {
|
|
||||||
case "", "all":
|
|
||||||
return true
|
|
||||||
case "online":
|
|
||||||
return online
|
|
||||||
case "offline":
|
|
||||||
return !online
|
|
||||||
default:
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func getConfByType(proxyType string) any {
|
|
||||||
switch v1.ProxyType(proxyType) {
|
|
||||||
case v1.ProxyTypeTCP:
|
|
||||||
return &TCPOutConf{}
|
|
||||||
case v1.ProxyTypeTCPMUX:
|
|
||||||
return &TCPMuxOutConf{}
|
|
||||||
case v1.ProxyTypeUDP:
|
|
||||||
return &UDPOutConf{}
|
|
||||||
case v1.ProxyTypeHTTP:
|
|
||||||
return &HTTPOutConf{}
|
|
||||||
case v1.ProxyTypeHTTPS:
|
|
||||||
return &HTTPSOutConf{}
|
|
||||||
case v1.ProxyTypeSTCP:
|
|
||||||
return &STCPOutConf{}
|
|
||||||
case v1.ProxyTypeXTCP:
|
|
||||||
return &XTCPOutConf{}
|
|
||||||
default:
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,136 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
v1 "github.com/fatedier/frp/pkg/config/v1"
|
|
||||||
)
|
|
||||||
|
|
||||||
type ServerInfoResp struct {
|
|
||||||
Version string `json:"version"`
|
|
||||||
BindPort int `json:"bindPort"`
|
|
||||||
VhostHTTPPort int `json:"vhostHTTPPort"`
|
|
||||||
VhostHTTPSPort int `json:"vhostHTTPSPort"`
|
|
||||||
TCPMuxHTTPConnectPort int `json:"tcpmuxHTTPConnectPort"`
|
|
||||||
KCPBindPort int `json:"kcpBindPort"`
|
|
||||||
QUICBindPort int `json:"quicBindPort"`
|
|
||||||
SubdomainHost string `json:"subdomainHost"`
|
|
||||||
MaxPoolCount int64 `json:"maxPoolCount"`
|
|
||||||
MaxPortsPerClient int64 `json:"maxPortsPerClient"`
|
|
||||||
HeartBeatTimeout int64 `json:"heartbeatTimeout"`
|
|
||||||
AllowPortsStr string `json:"allowPortsStr,omitempty"`
|
|
||||||
TLSForce bool `json:"tlsForce,omitempty"`
|
|
||||||
|
|
||||||
TotalTrafficIn int64 `json:"totalTrafficIn"`
|
|
||||||
TotalTrafficOut int64 `json:"totalTrafficOut"`
|
|
||||||
CurConns int64 `json:"curConns"`
|
|
||||||
ClientCounts int64 `json:"clientCounts"`
|
|
||||||
ProxyTypeCounts map[string]int64 `json:"proxyTypeCount"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type ClientInfoResp struct {
|
|
||||||
Key string `json:"key"`
|
|
||||||
User string `json:"user"`
|
|
||||||
ClientID string `json:"clientID"`
|
|
||||||
RunID string `json:"runID"`
|
|
||||||
Hostname string `json:"hostname"`
|
|
||||||
ClientIP string `json:"clientIP,omitempty"`
|
|
||||||
FirstConnectedAt int64 `json:"firstConnectedAt"`
|
|
||||||
LastConnectedAt int64 `json:"lastConnectedAt"`
|
|
||||||
DisconnectedAt int64 `json:"disconnectedAt,omitempty"`
|
|
||||||
Online bool `json:"online"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type BaseOutConf struct {
|
|
||||||
v1.ProxyBaseConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCPOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
RemotePort int `json:"remotePort"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type TCPMuxOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
v1.DomainConfig
|
|
||||||
Multiplexer string `json:"multiplexer"`
|
|
||||||
RouteByHTTPUser string `json:"routeByHTTPUser"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type UDPOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
RemotePort int `json:"remotePort"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
v1.DomainConfig
|
|
||||||
Locations []string `json:"locations"`
|
|
||||||
HostHeaderRewrite string `json:"hostHeaderRewrite"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type HTTPSOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
v1.DomainConfig
|
|
||||||
}
|
|
||||||
|
|
||||||
type STCPOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
}
|
|
||||||
|
|
||||||
type XTCPOutConf struct {
|
|
||||||
BaseOutConf
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get proxy info.
|
|
||||||
type ProxyStatsInfo struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Conf any `json:"conf"`
|
|
||||||
User string `json:"user,omitempty"`
|
|
||||||
ClientID string `json:"clientID,omitempty"`
|
|
||||||
ClientVersion string `json:"clientVersion,omitempty"`
|
|
||||||
TodayTrafficIn int64 `json:"todayTrafficIn"`
|
|
||||||
TodayTrafficOut int64 `json:"todayTrafficOut"`
|
|
||||||
CurConns int64 `json:"curConns"`
|
|
||||||
LastStartTime string `json:"lastStartTime"`
|
|
||||||
LastCloseTime string `json:"lastCloseTime"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
type GetProxyInfoResp struct {
|
|
||||||
Proxies []*ProxyStatsInfo `json:"proxies"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// Get proxy info by name.
|
|
||||||
type GetProxyStatsResp struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
Conf any `json:"conf"`
|
|
||||||
User string `json:"user,omitempty"`
|
|
||||||
ClientID string `json:"clientID,omitempty"`
|
|
||||||
ClientVersion string `json:"clientVersion,omitempty"`
|
|
||||||
TodayTrafficIn int64 `json:"todayTrafficIn"`
|
|
||||||
TodayTrafficOut int64 `json:"todayTrafficOut"`
|
|
||||||
CurConns int64 `json:"curConns"`
|
|
||||||
LastStartTime string `json:"lastStartTime"`
|
|
||||||
LastCloseTime string `json:"lastCloseTime"`
|
|
||||||
Status string `json:"status"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// /api/traffic/:name
|
|
||||||
type GetProxyTrafficResp struct {
|
|
||||||
Name string `json:"name"`
|
|
||||||
TrafficIn []int64 `json:"trafficIn"`
|
|
||||||
TrafficOut []int64 `json:"trafficOut"`
|
|
||||||
}
|
|
||||||
@@ -40,7 +40,6 @@ import (
|
|||||||
"github.com/fatedier/frp/server/controller"
|
"github.com/fatedier/frp/server/controller"
|
||||||
"github.com/fatedier/frp/server/metrics"
|
"github.com/fatedier/frp/server/metrics"
|
||||||
"github.com/fatedier/frp/server/proxy"
|
"github.com/fatedier/frp/server/proxy"
|
||||||
"github.com/fatedier/frp/server/registry"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
type ControlManager struct {
|
type ControlManager struct {
|
||||||
@@ -95,28 +94,8 @@ func (cm *ControlManager) Close() error {
|
|||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// CloseAllProxyByName Finds the tunnel name and closes all tunnels on the same connection.
|
|
||||||
func (cm *ControlManager) CloseAllProxyByName(proxyName string) error {
|
|
||||||
cm.mu.RLock()
|
|
||||||
var target *Control
|
|
||||||
for _, ctl := range cm.ctlsByRunID {
|
|
||||||
ctl.mu.RLock()
|
|
||||||
_, ok := ctl.proxies[proxyName]
|
|
||||||
ctl.mu.RUnlock()
|
|
||||||
if ok {
|
|
||||||
target = ctl
|
|
||||||
break
|
|
||||||
}
|
|
||||||
}
|
|
||||||
cm.mu.RUnlock()
|
|
||||||
if target == nil {
|
|
||||||
return fmt.Errorf("no proxy found with name [%s]", proxyName)
|
|
||||||
}
|
|
||||||
return target.Close()
|
|
||||||
}
|
|
||||||
|
|
||||||
// KickByProxyName finds the Control that manages the given proxy (tunnel) name and closes
|
// KickByProxyName finds the Control that manages the given proxy (tunnel) name and closes
|
||||||
// Bug: The client does not display the kickout message.
|
// the entire control connection (disconnects the frpc). Returns an error if no such proxy is found.
|
||||||
func (cm *ControlManager) KickByProxyName(proxyName string) error {
|
func (cm *ControlManager) KickByProxyName(proxyName string) error {
|
||||||
cm.mu.RLock()
|
cm.mu.RLock()
|
||||||
var target *Control
|
var target *Control
|
||||||
@@ -134,9 +113,6 @@ func (cm *ControlManager) KickByProxyName(proxyName string) error {
|
|||||||
if target == nil {
|
if target == nil {
|
||||||
return fmt.Errorf("no proxy found with name [%s]", proxyName)
|
return fmt.Errorf("no proxy found with name [%s]", proxyName)
|
||||||
}
|
}
|
||||||
|
|
||||||
xl := target.xl
|
|
||||||
xl.Infof("kick client with proxy [%s] by server administrator request", proxyName)
|
|
||||||
return target.Close()
|
return target.Close()
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,8 +128,6 @@ type Control struct {
|
|||||||
|
|
||||||
// verifies authentication based on selected method
|
// verifies authentication based on selected method
|
||||||
authVerifier auth.Verifier
|
authVerifier auth.Verifier
|
||||||
// key used for connection encryption
|
|
||||||
encryptionKey []byte
|
|
||||||
|
|
||||||
// other components can use this to communicate with client
|
// other components can use this to communicate with client
|
||||||
msgTransporter transport.MessageTransporter
|
msgTransporter transport.MessageTransporter
|
||||||
@@ -193,8 +167,6 @@ type Control struct {
|
|||||||
// Server configuration information
|
// Server configuration information
|
||||||
serverCfg *v1.ServerConfig
|
serverCfg *v1.ServerConfig
|
||||||
|
|
||||||
clientRegistry *registry.ClientRegistry
|
|
||||||
|
|
||||||
xl *xlog.Logger
|
xl *xlog.Logger
|
||||||
ctx context.Context
|
ctx context.Context
|
||||||
doneCh chan struct{}
|
doneCh chan struct{}
|
||||||
@@ -207,7 +179,6 @@ func NewControl(
|
|||||||
pxyManager *proxy.Manager,
|
pxyManager *proxy.Manager,
|
||||||
pluginManager *plugin.Manager,
|
pluginManager *plugin.Manager,
|
||||||
authVerifier auth.Verifier,
|
authVerifier auth.Verifier,
|
||||||
encryptionKey []byte,
|
|
||||||
ctlConn net.Conn,
|
ctlConn net.Conn,
|
||||||
ctlConnEncrypted bool,
|
ctlConnEncrypted bool,
|
||||||
loginMsg *msg.Login,
|
loginMsg *msg.Login,
|
||||||
@@ -222,7 +193,6 @@ func NewControl(
|
|||||||
pxyManager: pxyManager,
|
pxyManager: pxyManager,
|
||||||
pluginManager: pluginManager,
|
pluginManager: pluginManager,
|
||||||
authVerifier: authVerifier,
|
authVerifier: authVerifier,
|
||||||
encryptionKey: encryptionKey,
|
|
||||||
conn: ctlConn,
|
conn: ctlConn,
|
||||||
loginMsg: loginMsg,
|
loginMsg: loginMsg,
|
||||||
workConnCh: make(chan net.Conn, poolCount+10),
|
workConnCh: make(chan net.Conn, poolCount+10),
|
||||||
@@ -238,7 +208,7 @@ func NewControl(
|
|||||||
ctl.lastPing.Store(time.Now())
|
ctl.lastPing.Store(time.Now())
|
||||||
|
|
||||||
if ctlConnEncrypted {
|
if ctlConnEncrypted {
|
||||||
cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, ctl.encryptionKey)
|
cryptoRW, err := netpkg.NewCryptoReadWriter(ctl.conn, []byte(ctl.serverCfg.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -406,7 +376,6 @@ func (ctl *Control) worker() {
|
|||||||
}
|
}
|
||||||
|
|
||||||
metrics.Server.CloseClient()
|
metrics.Server.CloseClient()
|
||||||
ctl.clientRegistry.MarkOfflineByRunID(ctl.runID)
|
|
||||||
xl.Infof("client exit success")
|
xl.Infof("client exit success")
|
||||||
close(ctl.doneCh)
|
close(ctl.doneCh)
|
||||||
}
|
}
|
||||||
@@ -450,11 +419,7 @@ func (ctl *Control) handleNewProxy(m msg.Message) {
|
|||||||
} else {
|
} else {
|
||||||
resp.RemoteAddr = remoteAddr
|
resp.RemoteAddr = remoteAddr
|
||||||
xl.Infof("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType)
|
xl.Infof("new proxy [%s] type [%s] success", inMsg.ProxyName, inMsg.ProxyType)
|
||||||
clientID := ctl.loginMsg.ClientID
|
metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType)
|
||||||
if clientID == "" {
|
|
||||||
clientID = ctl.loginMsg.RunID
|
|
||||||
}
|
|
||||||
metrics.Server.NewProxy(inMsg.ProxyName, inMsg.ProxyType, ctl.loginMsg.User, clientID)
|
|
||||||
}
|
}
|
||||||
_ = ctl.msgDispatcher.Send(resp)
|
_ = ctl.msgDispatcher.Send(resp)
|
||||||
}
|
}
|
||||||
@@ -535,7 +500,6 @@ func (ctl *Control) RegisterProxy(pxyMsg *msg.NewProxy) (remoteAddr string, err
|
|||||||
GetWorkConnFn: ctl.GetWorkConn,
|
GetWorkConnFn: ctl.GetWorkConn,
|
||||||
Configurer: pxyConf,
|
Configurer: pxyConf,
|
||||||
ServerCfg: ctl.serverCfg,
|
ServerCfg: ctl.serverCfg,
|
||||||
EncryptionKey: ctl.encryptionKey,
|
|
||||||
})
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return remoteAddr, err
|
return remoteAddr, err
|
||||||
|
|||||||
463
server/dashboard_api.go
Normal file
463
server/dashboard_api.go
Normal file
@@ -0,0 +1,463 @@
|
|||||||
|
// Copyright 2017 fatedier, fatedier@gmail.com
|
||||||
|
//
|
||||||
|
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||||
|
// you may not use this file except in compliance with the License.
|
||||||
|
// You may obtain a copy of the License at
|
||||||
|
//
|
||||||
|
// http://www.apache.org/licenses/LICENSE-2.0
|
||||||
|
//
|
||||||
|
// Unless required by applicable law or agreed to in writing, software
|
||||||
|
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||||
|
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||||
|
// See the License for the specific language governing permissions and
|
||||||
|
// limitations under the License.
|
||||||
|
|
||||||
|
package server
|
||||||
|
|
||||||
|
import (
|
||||||
|
"cmp"
|
||||||
|
"encoding/json"
|
||||||
|
"net/http"
|
||||||
|
"slices"
|
||||||
|
|
||||||
|
"github.com/gorilla/mux"
|
||||||
|
"github.com/prometheus/client_golang/prometheus/promhttp"
|
||||||
|
|
||||||
|
"github.com/fatedier/frp/pkg/config/types"
|
||||||
|
v1 "github.com/fatedier/frp/pkg/config/v1"
|
||||||
|
"github.com/fatedier/frp/pkg/metrics/mem"
|
||||||
|
httppkg "github.com/fatedier/frp/pkg/util/http"
|
||||||
|
"github.com/fatedier/frp/pkg/util/log"
|
||||||
|
netpkg "github.com/fatedier/frp/pkg/util/net"
|
||||||
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
|
)
|
||||||
|
|
||||||
|
type GeneralResponse struct {
|
||||||
|
Code int
|
||||||
|
Msg string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
||||||
|
helper.Router.HandleFunc("/healthz", svr.healthz)
|
||||||
|
subRouter := helper.Router.NewRoute().Subrouter()
|
||||||
|
|
||||||
|
subRouter.Use(helper.AuthMiddleware.Middleware)
|
||||||
|
|
||||||
|
// metrics
|
||||||
|
if svr.cfg.EnablePrometheus {
|
||||||
|
subRouter.Handle("/metrics", promhttp.Handler())
|
||||||
|
}
|
||||||
|
|
||||||
|
// apis
|
||||||
|
subRouter.HandleFunc("/api/serverinfo", svr.apiServerInfo).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxy/{type}", svr.apiProxyByType).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxy/{type}/{name}", svr.apiProxyByTypeAndName).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxy/{name}/kick", svr.apiKickProxyByName).Methods("POST")
|
||||||
|
subRouter.HandleFunc("/api/traffic/{name}", svr.apiProxyTraffic).Methods("GET")
|
||||||
|
subRouter.HandleFunc("/api/proxies", svr.deleteProxies).Methods("DELETE")
|
||||||
|
subRouter.HandleFunc("/api/proxies", svr.apiProxiesAll).Methods("GET")
|
||||||
|
|
||||||
|
// view
|
||||||
|
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
||||||
|
subRouter.PathPrefix("/static/").Handler(
|
||||||
|
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
||||||
|
).Methods("GET")
|
||||||
|
|
||||||
|
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
||||||
|
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
type serverInfoResp struct {
|
||||||
|
Version string `json:"version"`
|
||||||
|
BindPort int `json:"bindPort"`
|
||||||
|
VhostHTTPPort int `json:"vhostHTTPPort"`
|
||||||
|
VhostHTTPSPort int `json:"vhostHTTPSPort"`
|
||||||
|
TCPMuxHTTPConnectPort int `json:"tcpmuxHTTPConnectPort"`
|
||||||
|
KCPBindPort int `json:"kcpBindPort"`
|
||||||
|
QUICBindPort int `json:"quicBindPort"`
|
||||||
|
SubdomainHost string `json:"subdomainHost"`
|
||||||
|
MaxPoolCount int64 `json:"maxPoolCount"`
|
||||||
|
MaxPortsPerClient int64 `json:"maxPortsPerClient"`
|
||||||
|
HeartBeatTimeout int64 `json:"heartbeatTimeout"`
|
||||||
|
AllowPortsStr string `json:"allowPortsStr,omitempty"`
|
||||||
|
TLSForce bool `json:"tlsForce,omitempty"`
|
||||||
|
|
||||||
|
TotalTrafficIn int64 `json:"totalTrafficIn"`
|
||||||
|
TotalTrafficOut int64 `json:"totalTrafficOut"`
|
||||||
|
CurConns int64 `json:"curConns"`
|
||||||
|
ClientCounts int64 `json:"clientCounts"`
|
||||||
|
ProxyTypeCounts map[string]int64 `json:"proxyTypeCount"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// /healthz
|
||||||
|
func (svr *Service) healthz(w http.ResponseWriter, _ *http.Request) {
|
||||||
|
w.WriteHeader(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/serverinfo
|
||||||
|
func (svr *Service) apiServerInfo(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
serverStats := mem.StatsCollector.GetServer()
|
||||||
|
svrResp := serverInfoResp{
|
||||||
|
Version: version.Full(),
|
||||||
|
BindPort: svr.cfg.BindPort,
|
||||||
|
VhostHTTPPort: svr.cfg.VhostHTTPPort,
|
||||||
|
VhostHTTPSPort: svr.cfg.VhostHTTPSPort,
|
||||||
|
TCPMuxHTTPConnectPort: svr.cfg.TCPMuxHTTPConnectPort,
|
||||||
|
KCPBindPort: svr.cfg.KCPBindPort,
|
||||||
|
QUICBindPort: svr.cfg.QUICBindPort,
|
||||||
|
SubdomainHost: svr.cfg.SubDomainHost,
|
||||||
|
MaxPoolCount: svr.cfg.Transport.MaxPoolCount,
|
||||||
|
MaxPortsPerClient: svr.cfg.MaxPortsPerClient,
|
||||||
|
HeartBeatTimeout: svr.cfg.Transport.HeartbeatTimeout,
|
||||||
|
AllowPortsStr: types.PortsRangeSlice(svr.cfg.AllowPorts).String(),
|
||||||
|
TLSForce: svr.cfg.Transport.TLS.Force,
|
||||||
|
|
||||||
|
TotalTrafficIn: serverStats.TotalTrafficIn,
|
||||||
|
TotalTrafficOut: serverStats.TotalTrafficOut,
|
||||||
|
CurConns: serverStats.CurConns,
|
||||||
|
ClientCounts: serverStats.ClientCounts,
|
||||||
|
ProxyTypeCounts: serverStats.ProxyTypeCounts,
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, _ := json.Marshal(&svrResp)
|
||||||
|
res.Msg = string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
type BaseOutConf struct {
|
||||||
|
v1.ProxyBaseConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCPOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
RemotePort int `json:"remotePort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type TCPMuxOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
v1.DomainConfig
|
||||||
|
Multiplexer string `json:"multiplexer"`
|
||||||
|
RouteByHTTPUser string `json:"routeByHTTPUser"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type UDPOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
RemotePort int `json:"remotePort"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
v1.DomainConfig
|
||||||
|
Locations []string `json:"locations"`
|
||||||
|
HostHeaderRewrite string `json:"hostHeaderRewrite"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type HTTPSOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
v1.DomainConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
type STCPOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
}
|
||||||
|
|
||||||
|
type XTCPOutConf struct {
|
||||||
|
BaseOutConf
|
||||||
|
}
|
||||||
|
|
||||||
|
func getConfByType(proxyType string) any {
|
||||||
|
switch v1.ProxyType(proxyType) {
|
||||||
|
case v1.ProxyTypeTCP:
|
||||||
|
return &TCPOutConf{}
|
||||||
|
case v1.ProxyTypeTCPMUX:
|
||||||
|
return &TCPMuxOutConf{}
|
||||||
|
case v1.ProxyTypeUDP:
|
||||||
|
return &UDPOutConf{}
|
||||||
|
case v1.ProxyTypeHTTP:
|
||||||
|
return &HTTPOutConf{}
|
||||||
|
case v1.ProxyTypeHTTPS:
|
||||||
|
return &HTTPSOutConf{}
|
||||||
|
case v1.ProxyTypeSTCP:
|
||||||
|
return &STCPOutConf{}
|
||||||
|
case v1.ProxyTypeXTCP:
|
||||||
|
return &XTCPOutConf{}
|
||||||
|
default:
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get proxy info.
|
||||||
|
type ProxyStatsInfo struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Conf any `json:"conf"`
|
||||||
|
ClientVersion string `json:"clientVersion,omitempty"`
|
||||||
|
TodayTrafficIn int64 `json:"todayTrafficIn"`
|
||||||
|
TodayTrafficOut int64 `json:"todayTrafficOut"`
|
||||||
|
CurConns int64 `json:"curConns"`
|
||||||
|
LastStartTime string `json:"lastStartTime"`
|
||||||
|
LastCloseTime string `json:"lastCloseTime"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type GetProxyInfoResp struct {
|
||||||
|
Proxies []*ProxyStatsInfo `json:"proxies"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// GET /api/proxies
|
||||||
|
// Return all proxies across types (tcp, udp, http, https, stcp, xtcp, tcpmux)
|
||||||
|
func (svr *Service) apiProxiesAll(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
|
||||||
|
proxyInfoResp := GetProxyInfoResp{}
|
||||||
|
proxyInfoResp.Proxies = svr.getProxyStatsByType("all")
|
||||||
|
slices.SortFunc(proxyInfoResp.Proxies, func(a, b *ProxyStatsInfo) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
buf, _ := json.Marshal(&proxyInfoResp)
|
||||||
|
res.Msg = string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/proxy/:type
|
||||||
|
func (svr *Service) apiProxyByType(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
params := mux.Vars(r)
|
||||||
|
proxyType := params["type"]
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
|
||||||
|
proxyInfoResp := GetProxyInfoResp{}
|
||||||
|
proxyInfoResp.Proxies = svr.getProxyStatsByType(proxyType)
|
||||||
|
slices.SortFunc(proxyInfoResp.Proxies, func(a, b *ProxyStatsInfo) int {
|
||||||
|
return cmp.Compare(a.Name, b.Name)
|
||||||
|
})
|
||||||
|
|
||||||
|
buf, _ := json.Marshal(&proxyInfoResp)
|
||||||
|
res.Msg = string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) getProxyStatsByType(proxyType string) (proxyInfos []*ProxyStatsInfo) {
|
||||||
|
// mem.StatsCollector now supports proxyType=="all" or "" to return all proxies
|
||||||
|
proxyStats := mem.StatsCollector.GetProxiesByType(proxyType)
|
||||||
|
proxyInfos = make([]*ProxyStatsInfo, 0, len(proxyStats))
|
||||||
|
for _, ps := range proxyStats {
|
||||||
|
proxyInfo := &ProxyStatsInfo{}
|
||||||
|
if pxy, ok := svr.pxyManager.GetByName(ps.Name); ok {
|
||||||
|
content, err := json.Marshal(pxy.GetConfigurer())
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
proxyInfo.Conf = getConfByType(ps.Type)
|
||||||
|
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
||||||
|
log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
proxyInfo.Status = "online"
|
||||||
|
if pxy.GetLoginMsg() != nil {
|
||||||
|
proxyInfo.ClientVersion = pxy.GetLoginMsg().Version
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
proxyInfo.Status = "offline"
|
||||||
|
}
|
||||||
|
proxyInfo.Name = ps.Name
|
||||||
|
proxyInfo.TodayTrafficIn = ps.TodayTrafficIn
|
||||||
|
proxyInfo.TodayTrafficOut = ps.TodayTrafficOut
|
||||||
|
proxyInfo.CurConns = ps.CurConns
|
||||||
|
proxyInfo.LastStartTime = ps.LastStartTime
|
||||||
|
proxyInfo.LastCloseTime = ps.LastCloseTime
|
||||||
|
proxyInfos = append(proxyInfos, proxyInfo)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// Get proxy info by name.
|
||||||
|
type GetProxyStatsResp struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
Conf any `json:"conf"`
|
||||||
|
TodayTrafficIn int64 `json:"todayTrafficIn"`
|
||||||
|
TodayTrafficOut int64 `json:"todayTrafficOut"`
|
||||||
|
CurConns int64 `json:"curConns"`
|
||||||
|
LastStartTime string `json:"lastStartTime"`
|
||||||
|
LastCloseTime string `json:"lastCloseTime"`
|
||||||
|
Status string `json:"status"`
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/proxy/:type/:name
|
||||||
|
func (svr *Service) apiProxyByTypeAndName(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
params := mux.Vars(r)
|
||||||
|
proxyType := params["type"]
|
||||||
|
name := params["name"]
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
|
||||||
|
var proxyStatsResp GetProxyStatsResp
|
||||||
|
proxyStatsResp, res.Code, res.Msg = svr.getProxyStatsByTypeAndName(proxyType, name)
|
||||||
|
if res.Code != 200 {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
buf, _ := json.Marshal(&proxyStatsResp)
|
||||||
|
res.Msg = string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// POST /api/proxy/:name/kick
|
||||||
|
// Kick the client (frpc) that owns the proxy with given name.
|
||||||
|
func (svr *Service) apiKickProxyByName(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
params := mux.Vars(r)
|
||||||
|
name := params["name"]
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
|
||||||
|
if name == "" {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = "proxy name required"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := svr.ctlManager.KickByProxyName(name); err != nil {
|
||||||
|
res.Code = 404
|
||||||
|
res.Msg = err.Error()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
res.Msg = "ok"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) getProxyStatsByTypeAndName(proxyType string, proxyName string) (proxyInfo GetProxyStatsResp, code int, msg string) {
|
||||||
|
proxyInfo.Name = proxyName
|
||||||
|
ps := mem.StatsCollector.GetProxiesByTypeAndName(proxyType, proxyName)
|
||||||
|
if ps == nil {
|
||||||
|
code = 404
|
||||||
|
msg = "no proxy info found"
|
||||||
|
} else {
|
||||||
|
if pxy, ok := svr.pxyManager.GetByName(proxyName); ok {
|
||||||
|
content, err := json.Marshal(pxy.GetConfigurer())
|
||||||
|
if err != nil {
|
||||||
|
log.Warnf("marshal proxy [%s] conf info error: %v", ps.Name, err)
|
||||||
|
code = 400
|
||||||
|
msg = "parse conf error"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proxyInfo.Conf = getConfByType(ps.Type)
|
||||||
|
if err = json.Unmarshal(content, &proxyInfo.Conf); err != nil {
|
||||||
|
log.Warnf("unmarshal proxy [%s] conf info error: %v", ps.Name, err)
|
||||||
|
code = 400
|
||||||
|
msg = "parse conf error"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
proxyInfo.Status = "online"
|
||||||
|
} else {
|
||||||
|
proxyInfo.Status = "offline"
|
||||||
|
}
|
||||||
|
proxyInfo.TodayTrafficIn = ps.TodayTrafficIn
|
||||||
|
proxyInfo.TodayTrafficOut = ps.TodayTrafficOut
|
||||||
|
proxyInfo.CurConns = ps.CurConns
|
||||||
|
proxyInfo.LastStartTime = ps.LastStartTime
|
||||||
|
proxyInfo.LastCloseTime = ps.LastCloseTime
|
||||||
|
code = 200
|
||||||
|
}
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// /api/traffic/:name
|
||||||
|
type GetProxyTrafficResp struct {
|
||||||
|
Name string `json:"name"`
|
||||||
|
TrafficIn []int64 `json:"trafficIn"`
|
||||||
|
TrafficOut []int64 `json:"trafficOut"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (svr *Service) apiProxyTraffic(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
params := mux.Vars(r)
|
||||||
|
name := params["name"]
|
||||||
|
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
|
||||||
|
trafficResp := GetProxyTrafficResp{}
|
||||||
|
trafficResp.Name = name
|
||||||
|
proxyTrafficInfo := mem.StatsCollector.GetProxyTraffic(name)
|
||||||
|
|
||||||
|
if proxyTrafficInfo == nil {
|
||||||
|
res.Code = 404
|
||||||
|
res.Msg = "no proxy info found"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
trafficResp.TrafficIn = proxyTrafficInfo.TrafficIn
|
||||||
|
trafficResp.TrafficOut = proxyTrafficInfo.TrafficOut
|
||||||
|
|
||||||
|
buf, _ := json.Marshal(&trafficResp)
|
||||||
|
res.Msg = string(buf)
|
||||||
|
}
|
||||||
|
|
||||||
|
// DELETE /api/proxies?status=offline
|
||||||
|
func (svr *Service) deleteProxies(w http.ResponseWriter, r *http.Request) {
|
||||||
|
res := GeneralResponse{Code: 200}
|
||||||
|
|
||||||
|
log.Infof("http request: [%s]", r.URL.Path)
|
||||||
|
defer func() {
|
||||||
|
log.Infof("http response [%s]: code [%d]", r.URL.Path, res.Code)
|
||||||
|
w.WriteHeader(res.Code)
|
||||||
|
if len(res.Msg) > 0 {
|
||||||
|
_, _ = w.Write([]byte(res.Msg))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
status := r.URL.Query().Get("status")
|
||||||
|
if status != "offline" {
|
||||||
|
res.Code = 400
|
||||||
|
res.Msg = "status only support offline"
|
||||||
|
return
|
||||||
|
}
|
||||||
|
cleared, total := mem.StatsCollector.ClearOfflineProxies()
|
||||||
|
log.Infof("cleared [%d] offline proxies, total [%d] proxies", cleared, total)
|
||||||
|
}
|
||||||
@@ -7,7 +7,7 @@ import (
|
|||||||
type ServerMetrics interface {
|
type ServerMetrics interface {
|
||||||
NewClient()
|
NewClient()
|
||||||
CloseClient()
|
CloseClient()
|
||||||
NewProxy(name string, proxyType string, user string, clientID string)
|
NewProxy(name string, proxyType string)
|
||||||
CloseProxy(name string, proxyType string)
|
CloseProxy(name string, proxyType string)
|
||||||
OpenConnection(name string, proxyType string)
|
OpenConnection(name string, proxyType string)
|
||||||
CloseConnection(name string, proxyType string)
|
CloseConnection(name string, proxyType string)
|
||||||
@@ -27,11 +27,11 @@ func Register(m ServerMetrics) {
|
|||||||
|
|
||||||
type noopServerMetrics struct{}
|
type noopServerMetrics struct{}
|
||||||
|
|
||||||
func (noopServerMetrics) NewClient() {}
|
func (noopServerMetrics) NewClient() {}
|
||||||
func (noopServerMetrics) CloseClient() {}
|
func (noopServerMetrics) CloseClient() {}
|
||||||
func (noopServerMetrics) NewProxy(string, string, string, string) {}
|
func (noopServerMetrics) NewProxy(string, string) {}
|
||||||
func (noopServerMetrics) CloseProxy(string, string) {}
|
func (noopServerMetrics) CloseProxy(string, string) {}
|
||||||
func (noopServerMetrics) OpenConnection(string, string) {}
|
func (noopServerMetrics) OpenConnection(string, string) {}
|
||||||
func (noopServerMetrics) CloseConnection(string, string) {}
|
func (noopServerMetrics) CloseConnection(string, string) {}
|
||||||
func (noopServerMetrics) AddTrafficIn(string, string, int64) {}
|
func (noopServerMetrics) AddTrafficIn(string, string, int64) {}
|
||||||
func (noopServerMetrics) AddTrafficOut(string, string, int64) {}
|
func (noopServerMetrics) AddTrafficOut(string, string, int64) {}
|
||||||
|
|||||||
@@ -165,7 +165,7 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
|
|||||||
|
|
||||||
var rwc io.ReadWriteCloser = tmpConn
|
var rwc io.ReadWriteCloser = tmpConn
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
return
|
return
|
||||||
@@ -181,26 +181,18 @@ func (pxy *HTTPProxy) GetRealConn(remoteAddr string) (workConn net.Conn, err err
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
name := pxy.GetName()
|
|
||||||
proxyType := pxy.GetConfigurer().GetBaseConfig().Type
|
|
||||||
rwc = wrapCountingReadWriteCloser(rwc, func(bytes int64) {
|
|
||||||
metrics.Server.AddTrafficOut(name, proxyType, bytes)
|
|
||||||
}, func(bytes int64) {
|
|
||||||
metrics.Server.AddTrafficIn(name, proxyType, bytes)
|
|
||||||
})
|
|
||||||
|
|
||||||
workConn = netpkg.WrapReadWriteCloserToConn(rwc, tmpConn)
|
workConn = netpkg.WrapReadWriteCloserToConn(rwc, tmpConn)
|
||||||
workConn = netpkg.WrapCloseNotifyConn(workConn, func(error) {
|
workConn = netpkg.WrapStatsConn(workConn, pxy.updateStatsAfterClosedConn)
|
||||||
pxy.updateStatsAfterClosedConn()
|
metrics.Server.OpenConnection(pxy.GetName(), pxy.GetConfigurer().GetBaseConfig().Type)
|
||||||
})
|
|
||||||
metrics.Server.OpenConnection(name, proxyType)
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pxy *HTTPProxy) updateStatsAfterClosedConn() {
|
func (pxy *HTTPProxy) updateStatsAfterClosedConn(totalRead, totalWrite int64) {
|
||||||
name := pxy.GetName()
|
name := pxy.GetName()
|
||||||
proxyType := pxy.GetConfigurer().GetBaseConfig().Type
|
proxyType := pxy.GetConfigurer().GetBaseConfig().Type
|
||||||
metrics.Server.CloseConnection(name, proxyType)
|
metrics.Server.CloseConnection(name, proxyType)
|
||||||
|
metrics.Server.AddTrafficIn(name, proxyType, totalWrite)
|
||||||
|
metrics.Server.AddTrafficOut(name, proxyType, totalRead)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (pxy *HTTPProxy) Close() {
|
func (pxy *HTTPProxy) Close() {
|
||||||
|
|||||||
@@ -68,7 +68,6 @@ type BaseProxy struct {
|
|||||||
poolCount int
|
poolCount int
|
||||||
getWorkConnFn GetWorkConnFn
|
getWorkConnFn GetWorkConnFn
|
||||||
serverCfg *v1.ServerConfig
|
serverCfg *v1.ServerConfig
|
||||||
encryptionKey []byte
|
|
||||||
limiter *rate.Limiter
|
limiter *rate.Limiter
|
||||||
userInfo plugin.UserInfo
|
userInfo plugin.UserInfo
|
||||||
loginMsg *msg.Login
|
loginMsg *msg.Login
|
||||||
@@ -214,6 +213,7 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
|
|||||||
xl := xlog.FromContextSafe(pxy.Context())
|
xl := xlog.FromContextSafe(pxy.Context())
|
||||||
defer userConn.Close()
|
defer userConn.Close()
|
||||||
|
|
||||||
|
serverCfg := pxy.serverCfg
|
||||||
cfg := pxy.configurer.GetBaseConfig()
|
cfg := pxy.configurer.GetBaseConfig()
|
||||||
// server plugin hook
|
// server plugin hook
|
||||||
rc := pxy.GetResourceController()
|
rc := pxy.GetResourceController()
|
||||||
@@ -240,7 +240,7 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
|
|||||||
xl.Tracef("handler user tcp connection, use_encryption: %t, use_compression: %t",
|
xl.Tracef("handler user tcp connection, use_encryption: %t, use_compression: %t",
|
||||||
cfg.Transport.UseEncryption, cfg.Transport.UseCompression)
|
cfg.Transport.UseEncryption, cfg.Transport.UseCompression)
|
||||||
if cfg.Transport.UseEncryption {
|
if cfg.Transport.UseEncryption {
|
||||||
local, err = libio.WithEncryption(local, pxy.encryptionKey)
|
local, err = libio.WithEncryption(local, []byte(serverCfg.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
return
|
return
|
||||||
@@ -263,18 +263,11 @@ func (pxy *BaseProxy) handleUserTCPConnection(userConn net.Conn) {
|
|||||||
|
|
||||||
name := pxy.GetName()
|
name := pxy.GetName()
|
||||||
proxyType := cfg.Type
|
proxyType := cfg.Type
|
||||||
local = wrapCountingReadWriteCloser(local, nil, func(bytes int64) {
|
|
||||||
metrics.Server.AddTrafficIn(name, proxyType, bytes)
|
|
||||||
})
|
|
||||||
userConn = netpkg.WrapReadWriteCloserToConn(
|
|
||||||
wrapCountingReadWriteCloser(userConn, nil, func(bytes int64) {
|
|
||||||
metrics.Server.AddTrafficOut(name, proxyType, bytes)
|
|
||||||
}),
|
|
||||||
userConn,
|
|
||||||
)
|
|
||||||
metrics.Server.OpenConnection(name, proxyType)
|
metrics.Server.OpenConnection(name, proxyType)
|
||||||
_, _, _ = libio.Join(local, userConn)
|
inCount, outCount, _ := libio.Join(local, userConn)
|
||||||
metrics.Server.CloseConnection(name, proxyType)
|
metrics.Server.CloseConnection(name, proxyType)
|
||||||
|
metrics.Server.AddTrafficIn(name, proxyType, inCount)
|
||||||
|
metrics.Server.AddTrafficOut(name, proxyType, outCount)
|
||||||
xl.Debugf("join connections closed")
|
xl.Debugf("join connections closed")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -286,7 +279,6 @@ type Options struct {
|
|||||||
GetWorkConnFn GetWorkConnFn
|
GetWorkConnFn GetWorkConnFn
|
||||||
Configurer v1.ProxyConfigurer
|
Configurer v1.ProxyConfigurer
|
||||||
ServerCfg *v1.ServerConfig
|
ServerCfg *v1.ServerConfig
|
||||||
EncryptionKey []byte
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) {
|
func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) {
|
||||||
@@ -306,7 +298,6 @@ func NewProxy(ctx context.Context, options *Options) (pxy Proxy, err error) {
|
|||||||
poolCount: options.PoolCount,
|
poolCount: options.PoolCount,
|
||||||
getWorkConnFn: options.GetWorkConnFn,
|
getWorkConnFn: options.GetWorkConnFn,
|
||||||
serverCfg: options.ServerCfg,
|
serverCfg: options.ServerCfg,
|
||||||
encryptionKey: options.EncryptionKey,
|
|
||||||
limiter: limiter,
|
limiter: limiter,
|
||||||
xl: xl,
|
xl: xl,
|
||||||
ctx: xlog.NewContext(ctx, xl),
|
ctx: xlog.NewContext(ctx, xl),
|
||||||
|
|||||||
@@ -1,36 +0,0 @@
|
|||||||
package proxy
|
|
||||||
|
|
||||||
import "io"
|
|
||||||
|
|
||||||
type countingReadWriteCloser struct {
|
|
||||||
io.ReadWriteCloser
|
|
||||||
onRead func(int64)
|
|
||||||
onWrite func(int64)
|
|
||||||
}
|
|
||||||
|
|
||||||
func wrapCountingReadWriteCloser(rwc io.ReadWriteCloser, onRead, onWrite func(int64)) io.ReadWriteCloser {
|
|
||||||
if onRead == nil && onWrite == nil {
|
|
||||||
return rwc
|
|
||||||
}
|
|
||||||
return &countingReadWriteCloser{
|
|
||||||
ReadWriteCloser: rwc,
|
|
||||||
onRead: onRead,
|
|
||||||
onWrite: onWrite,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *countingReadWriteCloser) Read(p []byte) (n int, err error) {
|
|
||||||
n, err = c.ReadWriteCloser.Read(p)
|
|
||||||
if n > 0 && c.onRead != nil {
|
|
||||||
c.onRead(int64(n))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *countingReadWriteCloser) Write(p []byte) (n int, err error) {
|
|
||||||
n, err = c.ReadWriteCloser.Write(p)
|
|
||||||
if n > 0 && c.onWrite != nil {
|
|
||||||
c.onWrite(int64(n))
|
|
||||||
}
|
|
||||||
return
|
|
||||||
}
|
|
||||||
@@ -205,7 +205,7 @@ func (pxy *UDPProxy) Run() (remoteAddr string, err error) {
|
|||||||
|
|
||||||
var rwc io.ReadWriteCloser = workConn
|
var rwc io.ReadWriteCloser = workConn
|
||||||
if pxy.cfg.Transport.UseEncryption {
|
if pxy.cfg.Transport.UseEncryption {
|
||||||
rwc, err = libio.WithEncryption(rwc, pxy.encryptionKey)
|
rwc, err = libio.WithEncryption(rwc, []byte(pxy.serverCfg.Auth.Token))
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Errorf("create encryption stream error: %v", err)
|
xl.Errorf("create encryption stream error: %v", err)
|
||||||
workConn.Close()
|
workConn.Close()
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
// Copyright 2025 The frp Authors
|
|
||||||
//
|
|
||||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
|
||||||
// you may not use this file except in compliance with the License.
|
|
||||||
// You may obtain a copy of the License at
|
|
||||||
//
|
|
||||||
// http://www.apache.org/licenses/LICENSE-2.0
|
|
||||||
//
|
|
||||||
// Unless required by applicable law or agreed to in writing, software
|
|
||||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
|
||||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
||||||
// See the License for the specific language governing permissions and
|
|
||||||
// limitations under the License.
|
|
||||||
|
|
||||||
package registry
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
)
|
|
||||||
|
|
||||||
// ClientInfo captures metadata about a connected frpc instance.
|
|
||||||
type ClientInfo struct {
|
|
||||||
Key string
|
|
||||||
User string
|
|
||||||
RawClientID string
|
|
||||||
RunID string
|
|
||||||
Hostname string
|
|
||||||
IP string
|
|
||||||
FirstConnectedAt time.Time
|
|
||||||
LastConnectedAt time.Time
|
|
||||||
DisconnectedAt time.Time
|
|
||||||
Online bool
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientRegistry keeps track of active clients keyed by "{user}.{clientID}" (runID fallback when raw clientID is empty).
|
|
||||||
// Entries without an explicit raw clientID are removed on disconnect to avoid stale offline records.
|
|
||||||
type ClientRegistry struct {
|
|
||||||
mu sync.RWMutex
|
|
||||||
clients map[string]*ClientInfo
|
|
||||||
runIndex map[string]string
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewClientRegistry() *ClientRegistry {
|
|
||||||
return &ClientRegistry{
|
|
||||||
clients: make(map[string]*ClientInfo),
|
|
||||||
runIndex: make(map[string]string),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Register stores/updates metadata for a client and returns the registry key plus whether it conflicts with an online client.
|
|
||||||
func (cr *ClientRegistry) Register(user, rawClientID, runID, hostname, remoteAddr string) (key string, conflict bool) {
|
|
||||||
if runID == "" {
|
|
||||||
return "", false
|
|
||||||
}
|
|
||||||
|
|
||||||
effectiveID := rawClientID
|
|
||||||
if effectiveID == "" {
|
|
||||||
effectiveID = runID
|
|
||||||
}
|
|
||||||
key = cr.composeClientKey(user, effectiveID)
|
|
||||||
enforceUnique := rawClientID != ""
|
|
||||||
|
|
||||||
now := time.Now()
|
|
||||||
cr.mu.Lock()
|
|
||||||
defer cr.mu.Unlock()
|
|
||||||
|
|
||||||
info, exists := cr.clients[key]
|
|
||||||
if enforceUnique && exists && info.Online && info.RunID != "" && info.RunID != runID {
|
|
||||||
return key, true
|
|
||||||
}
|
|
||||||
|
|
||||||
if !exists {
|
|
||||||
info = &ClientInfo{
|
|
||||||
Key: key,
|
|
||||||
User: user,
|
|
||||||
FirstConnectedAt: now,
|
|
||||||
}
|
|
||||||
cr.clients[key] = info
|
|
||||||
} else if info.RunID != "" {
|
|
||||||
delete(cr.runIndex, info.RunID)
|
|
||||||
}
|
|
||||||
|
|
||||||
info.RawClientID = rawClientID
|
|
||||||
info.RunID = runID
|
|
||||||
info.Hostname = hostname
|
|
||||||
info.IP = remoteAddr
|
|
||||||
if info.FirstConnectedAt.IsZero() {
|
|
||||||
info.FirstConnectedAt = now
|
|
||||||
}
|
|
||||||
info.LastConnectedAt = now
|
|
||||||
info.DisconnectedAt = time.Time{}
|
|
||||||
info.Online = true
|
|
||||||
|
|
||||||
cr.runIndex[runID] = key
|
|
||||||
return key, false
|
|
||||||
}
|
|
||||||
|
|
||||||
// MarkOfflineByRunID marks the client as offline when the corresponding control disconnects.
|
|
||||||
func (cr *ClientRegistry) MarkOfflineByRunID(runID string) {
|
|
||||||
cr.mu.Lock()
|
|
||||||
defer cr.mu.Unlock()
|
|
||||||
|
|
||||||
key, ok := cr.runIndex[runID]
|
|
||||||
if !ok {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
if info, ok := cr.clients[key]; ok && info.RunID == runID {
|
|
||||||
if info.RawClientID == "" {
|
|
||||||
delete(cr.clients, key)
|
|
||||||
} else {
|
|
||||||
info.RunID = ""
|
|
||||||
info.Online = false
|
|
||||||
now := time.Now()
|
|
||||||
info.DisconnectedAt = now
|
|
||||||
}
|
|
||||||
}
|
|
||||||
delete(cr.runIndex, runID)
|
|
||||||
}
|
|
||||||
|
|
||||||
// List returns a snapshot of all known clients.
|
|
||||||
func (cr *ClientRegistry) List() []ClientInfo {
|
|
||||||
cr.mu.RLock()
|
|
||||||
defer cr.mu.RUnlock()
|
|
||||||
|
|
||||||
result := make([]ClientInfo, 0, len(cr.clients))
|
|
||||||
for _, info := range cr.clients {
|
|
||||||
result = append(result, *info)
|
|
||||||
}
|
|
||||||
return result
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByKey retrieves a client by its composite key ({user}.{clientID} with runID fallback).
|
|
||||||
func (cr *ClientRegistry) GetByKey(key string) (ClientInfo, bool) {
|
|
||||||
cr.mu.RLock()
|
|
||||||
defer cr.mu.RUnlock()
|
|
||||||
|
|
||||||
info, ok := cr.clients[key]
|
|
||||||
if !ok {
|
|
||||||
return ClientInfo{}, false
|
|
||||||
}
|
|
||||||
return *info, true
|
|
||||||
}
|
|
||||||
|
|
||||||
// ClientID returns the resolved client identifier for external use.
|
|
||||||
func (info ClientInfo) ClientID() string {
|
|
||||||
if info.RawClientID != "" {
|
|
||||||
return info.RawClientID
|
|
||||||
}
|
|
||||||
return info.RunID
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetByRunID retrieves a client by its run ID.
|
|
||||||
func (cr *ClientRegistry) GetByRunID(runID string) (ClientInfo, bool) {
|
|
||||||
cr.mu.RLock()
|
|
||||||
defer cr.mu.RUnlock()
|
|
||||||
|
|
||||||
key, ok := cr.runIndex[runID]
|
|
||||||
if !ok {
|
|
||||||
return ClientInfo{}, false
|
|
||||||
}
|
|
||||||
info, ok := cr.clients[key]
|
|
||||||
if !ok {
|
|
||||||
return ClientInfo{}, false
|
|
||||||
}
|
|
||||||
return *info, true
|
|
||||||
}
|
|
||||||
|
|
||||||
func (cr *ClientRegistry) composeClientKey(user, id string) string {
|
|
||||||
switch {
|
|
||||||
case user == "":
|
|
||||||
return id
|
|
||||||
case id == "":
|
|
||||||
return user
|
|
||||||
default:
|
|
||||||
return fmt.Sprintf("%s.%s", user, id)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -28,7 +28,6 @@ import (
|
|||||||
"github.com/fatedier/golib/crypto"
|
"github.com/fatedier/golib/crypto"
|
||||||
"github.com/fatedier/golib/net/mux"
|
"github.com/fatedier/golib/net/mux"
|
||||||
fmux "github.com/hashicorp/yamux"
|
fmux "github.com/hashicorp/yamux"
|
||||||
"github.com/prometheus/client_golang/prometheus/promhttp"
|
|
||||||
quic "github.com/quic-go/quic-go"
|
quic "github.com/quic-go/quic-go"
|
||||||
"github.com/samber/lo"
|
"github.com/samber/lo"
|
||||||
|
|
||||||
@@ -48,13 +47,11 @@ import (
|
|||||||
"github.com/fatedier/frp/pkg/util/version"
|
"github.com/fatedier/frp/pkg/util/version"
|
||||||
"github.com/fatedier/frp/pkg/util/vhost"
|
"github.com/fatedier/frp/pkg/util/vhost"
|
||||||
"github.com/fatedier/frp/pkg/util/xlog"
|
"github.com/fatedier/frp/pkg/util/xlog"
|
||||||
"github.com/fatedier/frp/server/api"
|
|
||||||
"github.com/fatedier/frp/server/controller"
|
"github.com/fatedier/frp/server/controller"
|
||||||
"github.com/fatedier/frp/server/group"
|
"github.com/fatedier/frp/server/group"
|
||||||
"github.com/fatedier/frp/server/metrics"
|
"github.com/fatedier/frp/server/metrics"
|
||||||
"github.com/fatedier/frp/server/ports"
|
"github.com/fatedier/frp/server/ports"
|
||||||
"github.com/fatedier/frp/server/proxy"
|
"github.com/fatedier/frp/server/proxy"
|
||||||
"github.com/fatedier/frp/server/registry"
|
|
||||||
"github.com/fatedier/frp/server/visitor"
|
"github.com/fatedier/frp/server/visitor"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -99,9 +96,6 @@ type Service struct {
|
|||||||
// Manage all controllers
|
// Manage all controllers
|
||||||
ctlManager *ControlManager
|
ctlManager *ControlManager
|
||||||
|
|
||||||
// Track logical clients keyed by user.clientID (runID fallback when raw clientID is empty).
|
|
||||||
clientRegistry *registry.ClientRegistry
|
|
||||||
|
|
||||||
// Manage all proxies
|
// Manage all proxies
|
||||||
pxyManager *proxy.Manager
|
pxyManager *proxy.Manager
|
||||||
|
|
||||||
@@ -119,8 +113,8 @@ type Service struct {
|
|||||||
|
|
||||||
sshTunnelGateway *ssh.Gateway
|
sshTunnelGateway *ssh.Gateway
|
||||||
|
|
||||||
// Auth runtime and encryption materials
|
// Verifies authentication based on selected method
|
||||||
auth *auth.ServerAuth
|
authVerifier auth.Verifier
|
||||||
|
|
||||||
tlsConfig *tls.Config
|
tlsConfig *tls.Config
|
||||||
|
|
||||||
@@ -155,16 +149,10 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
authRuntime, err := auth.BuildServerAuth(&cfg.Auth)
|
|
||||||
if err != nil {
|
|
||||||
return nil, err
|
|
||||||
}
|
|
||||||
|
|
||||||
svr := &Service{
|
svr := &Service{
|
||||||
ctlManager: NewControlManager(),
|
ctlManager: NewControlManager(),
|
||||||
clientRegistry: registry.NewClientRegistry(),
|
pxyManager: proxy.NewManager(),
|
||||||
pxyManager: proxy.NewManager(),
|
pluginManager: plugin.NewManager(),
|
||||||
pluginManager: plugin.NewManager(),
|
|
||||||
rc: &controller.ResourceController{
|
rc: &controller.ResourceController{
|
||||||
VisitorManager: visitor.NewManager(),
|
VisitorManager: visitor.NewManager(),
|
||||||
TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts),
|
TCPPortManager: ports.NewManager("tcp", cfg.ProxyBindAddr, cfg.AllowPorts),
|
||||||
@@ -172,7 +160,7 @@ func NewService(cfg *v1.ServerConfig) (*Service, error) {
|
|||||||
},
|
},
|
||||||
sshTunnelListener: netpkg.NewInternalListener(),
|
sshTunnelListener: netpkg.NewInternalListener(),
|
||||||
httpVhostRouter: vhost.NewRouters(),
|
httpVhostRouter: vhost.NewRouters(),
|
||||||
auth: authRuntime,
|
authVerifier: auth.NewAuthVerifier(cfg.Auth),
|
||||||
webServer: webServer,
|
webServer: webServer,
|
||||||
tlsConfig: tlsConfig,
|
tlsConfig: tlsConfig,
|
||||||
cfg: cfg,
|
cfg: cfg,
|
||||||
@@ -598,7 +586,7 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
|
|||||||
ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch)
|
ctlConn.RemoteAddr().String(), loginMsg.Version, loginMsg.Hostname, loginMsg.Os, loginMsg.Arch)
|
||||||
|
|
||||||
// Check auth.
|
// Check auth.
|
||||||
authVerifier := svr.auth.Verifier
|
authVerifier := svr.authVerifier
|
||||||
if internal && loginMsg.ClientSpec.AlwaysAuthPass {
|
if internal && loginMsg.ClientSpec.AlwaysAuthPass {
|
||||||
authVerifier = auth.AlwaysPassVerifier
|
authVerifier = auth.AlwaysPassVerifier
|
||||||
}
|
}
|
||||||
@@ -607,29 +595,16 @@ func (svr *Service) RegisterControl(ctlConn net.Conn, loginMsg *msg.Login, inter
|
|||||||
}
|
}
|
||||||
|
|
||||||
// TODO(fatedier): use SessionContext
|
// TODO(fatedier): use SessionContext
|
||||||
ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, svr.auth.EncryptionKey(), ctlConn, !internal, loginMsg, svr.cfg)
|
ctl, err := NewControl(ctx, svr.rc, svr.pxyManager, svr.pluginManager, authVerifier, ctlConn, !internal, loginMsg, svr.cfg)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
xl.Warnf("create new controller error: %v", err)
|
xl.Warnf("create new controller error: %v", err)
|
||||||
// don't return detailed errors to client
|
// don't return detailed errors to client
|
||||||
return fmt.Errorf("unexpected error when creating new controller")
|
return fmt.Errorf("unexpected error when creating new controller")
|
||||||
}
|
}
|
||||||
|
|
||||||
if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
|
if oldCtl := svr.ctlManager.Add(loginMsg.RunID, ctl); oldCtl != nil {
|
||||||
oldCtl.WaitClosed()
|
oldCtl.WaitClosed()
|
||||||
}
|
}
|
||||||
|
|
||||||
remoteAddr := ctlConn.RemoteAddr().String()
|
|
||||||
if host, _, err := net.SplitHostPort(remoteAddr); err == nil {
|
|
||||||
remoteAddr = host
|
|
||||||
}
|
|
||||||
_, conflict := svr.clientRegistry.Register(loginMsg.User, loginMsg.ClientID, loginMsg.RunID, loginMsg.Hostname, remoteAddr)
|
|
||||||
if conflict {
|
|
||||||
svr.ctlManager.Del(loginMsg.RunID, ctl)
|
|
||||||
ctl.Close()
|
|
||||||
return fmt.Errorf("client_id [%s] for user [%s] is already online", loginMsg.ClientID, loginMsg.User)
|
|
||||||
}
|
|
||||||
ctl.clientRegistry = svr.clientRegistry
|
|
||||||
|
|
||||||
ctl.Start()
|
ctl.Start()
|
||||||
|
|
||||||
// for statistics
|
// for statistics
|
||||||
@@ -690,43 +665,3 @@ func (svr *Service) RegisterVisitorConn(visitorConn net.Conn, newMsg *msg.NewVis
|
|||||||
return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey,
|
return svr.rc.VisitorManager.NewConn(newMsg.ProxyName, visitorConn, newMsg.Timestamp, newMsg.SignKey,
|
||||||
newMsg.UseEncryption, newMsg.UseCompression, visitorUser)
|
newMsg.UseEncryption, newMsg.UseCompression, visitorUser)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (svr *Service) registerRouteHandlers(helper *httppkg.RouterRegisterHelper) {
|
|
||||||
helper.Router.HandleFunc("/healthz", healthz)
|
|
||||||
subRouter := helper.Router.NewRoute().Subrouter()
|
|
||||||
|
|
||||||
subRouter.Use(helper.AuthMiddleware)
|
|
||||||
subRouter.Use(httppkg.NewRequestLogger)
|
|
||||||
|
|
||||||
// metrics
|
|
||||||
if svr.cfg.EnablePrometheus {
|
|
||||||
subRouter.Handle("/metrics", promhttp.Handler())
|
|
||||||
}
|
|
||||||
|
|
||||||
apiController := api.NewController(svr.cfg, svr.clientRegistry, svr.pxyManager, svr.ctlManager)
|
|
||||||
|
|
||||||
// apis
|
|
||||||
subRouter.HandleFunc("/api/serverinfo", httppkg.MakeHTTPHandlerFunc(apiController.APIServerInfo)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/proxy/{type}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByType)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/proxy/{type}/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByTypeAndName)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/proxy/{name}/close", httppkg.MakeHTTPHandlerFunc(apiController.APICloseProxyByName)).Methods("POST")
|
|
||||||
subRouter.HandleFunc("/api/proxies/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyByName)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/traffic/{name}", httppkg.MakeHTTPHandlerFunc(apiController.APIProxyTraffic)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/clients", httppkg.MakeHTTPHandlerFunc(apiController.APIClientList)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/clients/{key}", httppkg.MakeHTTPHandlerFunc(apiController.APIClientDetail)).Methods("GET")
|
|
||||||
subRouter.HandleFunc("/api/proxies", httppkg.MakeHTTPHandlerFunc(apiController.DeleteProxies)).Methods("DELETE")
|
|
||||||
|
|
||||||
// view
|
|
||||||
subRouter.Handle("/favicon.ico", http.FileServer(helper.AssetsFS)).Methods("GET")
|
|
||||||
subRouter.PathPrefix("/static/").Handler(
|
|
||||||
netpkg.MakeHTTPGzipHandler(http.StripPrefix("/static/", http.FileServer(helper.AssetsFS))),
|
|
||||||
).Methods("GET")
|
|
||||||
|
|
||||||
subRouter.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) {
|
|
||||||
http.Redirect(w, r, "/static/", http.StatusMovedPermanently)
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
func healthz(w http.ResponseWriter, _ *http.Request) {
|
|
||||||
w.WriteHeader(200)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -29,18 +29,6 @@ import (
|
|||||||
var _ = ginkgo.Describe("[Feature: TokenSource]", func() {
|
var _ = ginkgo.Describe("[Feature: TokenSource]", func() {
|
||||||
f := framework.NewDefaultFramework()
|
f := framework.NewDefaultFramework()
|
||||||
|
|
||||||
createExecTokenScript := func(name string) string {
|
|
||||||
scriptPath := filepath.Join(f.TempDirectory, name)
|
|
||||||
scriptContent := `#!/bin/sh
|
|
||||||
printf '%s\n' "$1"
|
|
||||||
`
|
|
||||||
err := os.WriteFile(scriptPath, []byte(scriptContent), 0o600)
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
err = os.Chmod(scriptPath, 0o700)
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
return scriptPath
|
|
||||||
}
|
|
||||||
|
|
||||||
ginkgo.Describe("File-based token loading", func() {
|
ginkgo.Describe("File-based token loading", func() {
|
||||||
ginkgo.It("should work with file tokenSource", func() {
|
ginkgo.It("should work with file tokenSource", func() {
|
||||||
// Create a temporary token file
|
// Create a temporary token file
|
||||||
@@ -226,154 +214,4 @@ auth.tokenSource.file.path = "%s"
|
|||||||
f.RunProcesses([]string{serverConf}, []string{})
|
f.RunProcesses([]string{serverConf}, []string{})
|
||||||
})
|
})
|
||||||
})
|
})
|
||||||
|
|
||||||
ginkgo.Describe("Exec-based token loading", func() {
|
|
||||||
ginkgo.It("should work with server tokenSource", func() {
|
|
||||||
execValue := "exec-server-value"
|
|
||||||
scriptPath := createExecTokenScript("server_token_exec.sh")
|
|
||||||
|
|
||||||
serverPort := f.AllocPort()
|
|
||||||
remotePort := f.AllocPort()
|
|
||||||
|
|
||||||
serverConf := fmt.Sprintf(`
|
|
||||||
bindAddr = "0.0.0.0"
|
|
||||||
bindPort = %d
|
|
||||||
|
|
||||||
auth.tokenSource.type = "exec"
|
|
||||||
auth.tokenSource.exec.command = %q
|
|
||||||
auth.tokenSource.exec.args = [%q]
|
|
||||||
`, serverPort, scriptPath, execValue)
|
|
||||||
|
|
||||||
clientConf := fmt.Sprintf(`
|
|
||||||
serverAddr = "127.0.0.1"
|
|
||||||
serverPort = %d
|
|
||||||
loginFailExit = false
|
|
||||||
auth.token = %q
|
|
||||||
|
|
||||||
[[proxies]]
|
|
||||||
name = "tcp"
|
|
||||||
type = "tcp"
|
|
||||||
localPort = %d
|
|
||||||
remotePort = %d
|
|
||||||
`, serverPort, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort)
|
|
||||||
|
|
||||||
serverConfigPath := f.GenerateConfigFile(serverConf)
|
|
||||||
clientConfigPath := f.GenerateConfigFile(clientConf)
|
|
||||||
|
|
||||||
_, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec")
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
_, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec")
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("should work with client tokenSource", func() {
|
|
||||||
execValue := "exec-client-value"
|
|
||||||
scriptPath := createExecTokenScript("client_token_exec.sh")
|
|
||||||
|
|
||||||
serverPort := f.AllocPort()
|
|
||||||
remotePort := f.AllocPort()
|
|
||||||
|
|
||||||
serverConf := fmt.Sprintf(`
|
|
||||||
bindAddr = "0.0.0.0"
|
|
||||||
bindPort = %d
|
|
||||||
|
|
||||||
auth.token = %q
|
|
||||||
`, serverPort, execValue)
|
|
||||||
|
|
||||||
clientConf := fmt.Sprintf(`
|
|
||||||
serverAddr = "127.0.0.1"
|
|
||||||
serverPort = %d
|
|
||||||
loginFailExit = false
|
|
||||||
|
|
||||||
auth.tokenSource.type = "exec"
|
|
||||||
auth.tokenSource.exec.command = %q
|
|
||||||
auth.tokenSource.exec.args = [%q]
|
|
||||||
|
|
||||||
[[proxies]]
|
|
||||||
name = "tcp"
|
|
||||||
type = "tcp"
|
|
||||||
localPort = %d
|
|
||||||
remotePort = %d
|
|
||||||
`, serverPort, scriptPath, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort)
|
|
||||||
|
|
||||||
serverConfigPath := f.GenerateConfigFile(serverConf)
|
|
||||||
clientConfigPath := f.GenerateConfigFile(clientConf)
|
|
||||||
|
|
||||||
_, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec")
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
_, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec")
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("should work with both server and client tokenSource", func() {
|
|
||||||
execValue := "exec-shared-value"
|
|
||||||
scriptPath := createExecTokenScript("shared_token_exec.sh")
|
|
||||||
|
|
||||||
serverPort := f.AllocPort()
|
|
||||||
remotePort := f.AllocPort()
|
|
||||||
|
|
||||||
serverConf := fmt.Sprintf(`
|
|
||||||
bindAddr = "0.0.0.0"
|
|
||||||
bindPort = %d
|
|
||||||
|
|
||||||
auth.tokenSource.type = "exec"
|
|
||||||
auth.tokenSource.exec.command = %q
|
|
||||||
auth.tokenSource.exec.args = [%q]
|
|
||||||
`, serverPort, scriptPath, execValue)
|
|
||||||
|
|
||||||
clientConf := fmt.Sprintf(`
|
|
||||||
serverAddr = "127.0.0.1"
|
|
||||||
serverPort = %d
|
|
||||||
loginFailExit = false
|
|
||||||
|
|
||||||
auth.tokenSource.type = "exec"
|
|
||||||
auth.tokenSource.exec.command = %q
|
|
||||||
auth.tokenSource.exec.args = [%q]
|
|
||||||
|
|
||||||
[[proxies]]
|
|
||||||
name = "tcp"
|
|
||||||
type = "tcp"
|
|
||||||
localPort = %d
|
|
||||||
remotePort = %d
|
|
||||||
`, serverPort, scriptPath, execValue, f.PortByName(framework.TCPEchoServerPort), remotePort)
|
|
||||||
|
|
||||||
serverConfigPath := f.GenerateConfigFile(serverConf)
|
|
||||||
clientConfigPath := f.GenerateConfigFile(clientConf)
|
|
||||||
|
|
||||||
_, _, err := f.RunFrps("-c", serverConfigPath, "--allow-unsafe=TokenSourceExec")
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
_, _, err = f.RunFrpc("-c", clientConfigPath, "--allow-unsafe=TokenSourceExec")
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
|
|
||||||
framework.NewRequestExpect(f).Port(remotePort).Ensure()
|
|
||||||
})
|
|
||||||
|
|
||||||
ginkgo.It("should fail validation without allow-unsafe", func() {
|
|
||||||
execValue := "exec-unsafe-value"
|
|
||||||
scriptPath := createExecTokenScript("unsafe_token_exec.sh")
|
|
||||||
|
|
||||||
serverPort := f.AllocPort()
|
|
||||||
serverConf := fmt.Sprintf(`
|
|
||||||
bindAddr = "0.0.0.0"
|
|
||||||
bindPort = %d
|
|
||||||
|
|
||||||
auth.tokenSource.type = "exec"
|
|
||||||
auth.tokenSource.exec.command = %q
|
|
||||||
auth.tokenSource.exec.args = [%q]
|
|
||||||
`, serverPort, scriptPath, execValue)
|
|
||||||
|
|
||||||
serverConfigPath := f.GenerateConfigFile(serverConf)
|
|
||||||
|
|
||||||
_, output, err := f.RunFrps("verify", "-c", serverConfigPath)
|
|
||||||
framework.ExpectNoError(err)
|
|
||||||
framework.ExpectContainSubstring(output, "unsafe feature \"TokenSourceExec\" is not enabled")
|
|
||||||
})
|
|
||||||
})
|
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -1,9 +1,6 @@
|
|||||||
.PHONY: dist install build preview lint
|
.PHONY: dist build preview lint
|
||||||
|
|
||||||
install:
|
build:
|
||||||
@npm install
|
|
||||||
|
|
||||||
build: install
|
|
||||||
@npm run build
|
@npm run build
|
||||||
|
|
||||||
dev:
|
dev:
|
||||||
|
|||||||
16
web/frpc/components.d.ts
vendored
16
web/frpc/components.d.ts
vendored
@@ -7,22 +7,18 @@ export {}
|
|||||||
|
|
||||||
declare module 'vue' {
|
declare module 'vue' {
|
||||||
export interface GlobalComponents {
|
export interface GlobalComponents {
|
||||||
|
ClientConfigure: typeof import('./src/components/ClientConfigure.vue')['default']
|
||||||
ElButton: typeof import('element-plus/es')['ElButton']
|
ElButton: typeof import('element-plus/es')['ElButton']
|
||||||
ElCard: typeof import('element-plus/es')['ElCard']
|
|
||||||
ElCol: typeof import('element-plus/es')['ElCol']
|
ElCol: typeof import('element-plus/es')['ElCol']
|
||||||
ElEmpty: typeof import('element-plus/es')['ElEmpty']
|
|
||||||
ElIcon: typeof import('element-plus/es')['ElIcon']
|
|
||||||
ElInput: typeof import('element-plus/es')['ElInput']
|
ElInput: typeof import('element-plus/es')['ElInput']
|
||||||
|
ElMenu: typeof import('element-plus/es')['ElMenu']
|
||||||
|
ElMenuItem: typeof import('element-plus/es')['ElMenuItem']
|
||||||
ElRow: typeof import('element-plus/es')['ElRow']
|
ElRow: typeof import('element-plus/es')['ElRow']
|
||||||
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
ElSwitch: typeof import('element-plus/es')['ElSwitch']
|
||||||
ElTag: typeof import('element-plus/es')['ElTag']
|
ElTable: typeof import('element-plus/es')['ElTable']
|
||||||
ElTooltip: typeof import('element-plus/es')['ElTooltip']
|
ElTableColumn: typeof import('element-plus/es')['ElTableColumn']
|
||||||
ProxyCard: typeof import('./src/components/ProxyCard.vue')['default']
|
Overview: typeof import('./src/components/Overview.vue')['default']
|
||||||
RouterLink: typeof import('vue-router')['RouterLink']
|
RouterLink: typeof import('vue-router')['RouterLink']
|
||||||
RouterView: typeof import('vue-router')['RouterView']
|
RouterView: typeof import('vue-router')['RouterView']
|
||||||
StatCard: typeof import('./src/components/StatCard.vue')['default']
|
|
||||||
}
|
|
||||||
export interface ComponentCustomProperties {
|
|
||||||
vLoading: typeof import('element-plus/es')['ElLoadingDirective']
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||
<head>
|
<head>
|
||||||
<meta charset="utf-8">
|
<meta charset="utf-8">
|
||||||
<title>frp client</title>
|
<title>frp client admin UI</title>
|
||||||
</head>
|
</head>
|
||||||
|
|
||||||
<body>
|
<body>
|
||||||
|
|||||||
5907
web/frpc/package-lock.json
generated
5907
web/frpc/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user