mirror of
https://github.com/Sagit-chu/flvx.git
synced 2026-09-28 23:56:36 +08:00
Compare commits
389 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 96eec61a92 | |||
| 6e4bde4053 | |||
| 33f347d1db | |||
| 17e5f4f95a | |||
| b0304876a8 | |||
| 2710cd1674 | |||
| a74eb9f431 | |||
| d5c0060cd9 | |||
| 7f9c05172b | |||
| d720b9e00c | |||
| 15cf25f74b | |||
| ae6c65e9a9 | |||
| 8a85ab2844 | |||
| b9b4312768 | |||
| a2b819dbcc | |||
| 6f0412de0f | |||
| e2ae241f8c | |||
| 1a8f424d53 | |||
| 1977ba9eab | |||
| b2407c3442 | |||
| f9bc165c16 | |||
| b6333d81a2 | |||
| 507893dc55 | |||
| 652b44e08e | |||
| 95b3803745 | |||
| 0b85cd2af1 | |||
| 0f37017760 | |||
| 4d0dcf5db5 | |||
| 4c756e9156 | |||
| f5a40bf530 | |||
| c7dbbef0d9 | |||
| a6773fe65d | |||
| 27a32b3ff4 | |||
| 06a45a87f6 | |||
| 053aef42c0 | |||
| 9f79efd44b | |||
| 08ba876291 | |||
| 59af67a5b5 | |||
| 65f8f7506e | |||
| efd5a107b9 | |||
| 35c8063ac5 | |||
| e0efadf298 | |||
| a9fadfc08c | |||
| 529257c8d0 | |||
| a667c03b6c | |||
| 5a1fc808b2 | |||
| 02ff215f99 | |||
| 2a4e7777ab | |||
| eac94a5719 | |||
| 96fcd0fc57 | |||
| 06869aedfd | |||
| 6a201131a3 | |||
| 1130a55ef5 | |||
| 265cd0a50e | |||
| e7ffa77b15 | |||
| 51cbd4b9de | |||
| e122e7460d | |||
| 7c898154b3 | |||
| 1d19d68019 | |||
| 09c58e2298 | |||
| 583905b7ed | |||
| 6e3f045b9b | |||
| ec41202b3c | |||
| 1ee7dea8b4 | |||
| 2d69350bab | |||
| c984e5b62a | |||
| 5b79b11101 | |||
| 2c22e600f7 | |||
| aef284c474 | |||
| 0443cd9ceb | |||
| 3337422775 | |||
| 3e046fc80e | |||
| 0273bc6921 | |||
| be095057bd | |||
| a98057d06a | |||
| 7d47903541 | |||
| a7aabdd1dd | |||
| 0357a92960 | |||
| 8ff41c962e | |||
| 6f6fececa8 | |||
| 72c2e28667 | |||
| 74c78851ca | |||
| 42732f844a | |||
| 99b8ac206a | |||
| 0e5cd86ed1 | |||
| 60fc80b6ac | |||
| c8f0c55fda | |||
| bfdaa47ea5 | |||
| d7b76b4590 | |||
| 2c2262b55d | |||
| 7ca01aba5d | |||
| 0f57ec58b3 | |||
| ac30f0172f | |||
| 531ba0bfed | |||
| d787e4b07a | |||
| 68e5d0ac0b | |||
| 38b70821c5 | |||
| 936158dd32 | |||
| e0d0553fd3 | |||
| 78aa86b23c | |||
| 1f850593dd | |||
| c4519c243a | |||
| 31ccc48436 | |||
| 1c4914ec77 | |||
| 71c40127d5 | |||
| aac1d63ac0 | |||
| 8222b31917 | |||
| 81c9c117a2 | |||
| de8e487dff | |||
| f4c59f64b0 | |||
| 6adf90d45c | |||
| 71eab8e07e | |||
| dd58ac67de | |||
| e6226d8069 | |||
| 6e25e1838d | |||
| c0c88a8466 | |||
| 4edbb66224 | |||
| 84e01c7851 | |||
| 761db1844d | |||
| ca76cc29de | |||
| c20a78a196 | |||
| 2053cbec32 | |||
| 687e9c78ad | |||
| 07105309fb | |||
| 31290137fe | |||
| 4a23f3b45a | |||
| 0b2974726c | |||
| 43aebdf8f4 | |||
| 37ea338a35 | |||
| 4063d39d6a | |||
| f6dd3ad657 | |||
| 252c16de29 | |||
| 4fef8eeb32 | |||
| 9146eb0375 | |||
| ad974875e4 | |||
| acd00ae413 | |||
| 65ca18d649 | |||
| a27b4d781e | |||
| ba50076468 | |||
| b83f6c0023 | |||
| d0822fc3ef | |||
| f83d4d76be | |||
| 70ff6969e7 | |||
| 374bafec57 | |||
| d9992d433d | |||
| d6feb17b86 | |||
| b587193e74 | |||
| 5387b09dfe | |||
| 2fa9f53e7c | |||
| 50e7ff0f36 | |||
| fa1c390b33 | |||
| 69b3d3002d | |||
| 5fa361d88b | |||
| dec0690f7a | |||
| 4754fc03a7 | |||
| d09183270f | |||
| 170744fb04 | |||
| 8f229f3f27 | |||
| 03f1123c5c | |||
| 7984129b4c | |||
| 414a3a2c91 | |||
| afdafe3593 | |||
| 59ddb9bbbb | |||
| ed098fd423 | |||
| 1b537b0de4 | |||
| c2f364773f | |||
| 5b33ffae17 | |||
| 66d5e35054 | |||
| 2d4d3275a6 | |||
| 7446d0a442 | |||
| 5bf91a30eb | |||
| ca68dcd50a | |||
| 218294c06f | |||
| d14a455407 | |||
| 9696c8f107 | |||
| d1ad31cdde | |||
| 8456ab7f3f | |||
| b604f3b24b | |||
| cab1ebc5c9 | |||
| 5ff01c348b | |||
| 365387576f | |||
| 07a0b9e3fa | |||
| 6c4f9cd24c | |||
| 821ab5c76b | |||
| f5c8d6d7aa | |||
| b05d974ac8 | |||
| ac404044c4 | |||
| a8e283f31b | |||
| 8dc171fe4b | |||
| c43e0fecdc | |||
| b1647ed309 | |||
| 42d04da2a2 | |||
| 73ce0bdaed | |||
| 00829b9435 | |||
| 5ec7d48740 | |||
| 2f4f1c9a1d | |||
| d6cac71ada | |||
| 035672fa1a | |||
| 2943d8de76 | |||
| 04a0ffdc50 | |||
| 86988b48f9 | |||
| 74f7eaeac1 | |||
| 2a037cc72e | |||
| 018ed3538b | |||
| 3e7e13cc63 | |||
| 1ddf996b88 | |||
| 51e4f590f5 | |||
| cdfad3f224 | |||
| a046365763 | |||
| 79618585aa | |||
| 6dfe55a9f0 | |||
| 3d20d633fe | |||
| 5be8082d96 | |||
| 8f4d5897fa | |||
| 23980578d7 | |||
| 6f80e26a8f | |||
| 23ad5ef68b | |||
| 6b65a7dd2d | |||
| 24dcd58d80 | |||
| 31728cf704 | |||
| fe5c92bf22 | |||
| d6685447b3 | |||
| d675effd58 | |||
| 791cde3eb2 | |||
| e158e6a0bc | |||
| b567628700 | |||
| 496e00ec2a | |||
| 2a8f8075ab | |||
| 62f9da97f4 | |||
| a06904ee3a | |||
| 8e4b69e335 | |||
| cb60bcc1b0 | |||
| 614d147032 | |||
| 8ffb9218c4 | |||
| 50876037e8 | |||
| 05e1814be1 | |||
| 64f2748051 | |||
| 5c96e4e2fd | |||
| a086a20eb0 | |||
| 9a68cda83b | |||
| 224c649d79 | |||
| a1f08d13f2 | |||
| f35e6e95f5 | |||
| c56e647358 | |||
| c03249864d | |||
| 050daf6323 | |||
| b1da4d1c30 | |||
| fd60869f0b | |||
| 264e06486c | |||
| 73c1af715e | |||
| 4e8e366f75 | |||
| 9fab17a732 | |||
| 51b4d90244 | |||
| decda2c892 | |||
| 83017b56f1 | |||
| 31e9ee621f | |||
| ecda75bf08 | |||
| d28c04d7ba | |||
| 3668a07f35 | |||
| 971f45eb42 | |||
| b3135d3b0f | |||
| 30f4940745 | |||
| de4da12f42 | |||
| 1448f96fa5 | |||
| 6d7393337b | |||
| 9b10c4bd53 | |||
| 9b8a3483ba | |||
| ec8a2692d8 | |||
| 1d0e5c2eca | |||
| b5fa66f5e5 | |||
| c2101c1d00 | |||
| a2e8c7a731 | |||
| b09f7830fe | |||
| ed43a3e280 | |||
| 2262911dd4 | |||
| 48a7a05a69 | |||
| 4c0b3a4cf8 | |||
| 5908604548 | |||
| a71e0c257e | |||
| 8934c809e7 | |||
| e313a1953c | |||
| b690e68951 | |||
| 286962ff33 | |||
| 070a8e8623 | |||
| c038dbb614 | |||
| ad5fbcd9e6 | |||
| ac4f078014 | |||
| 0adc09f5de | |||
| cd9f86dd02 | |||
| edb7d2601f | |||
| e348083287 | |||
| 9c7ac11ced | |||
| 6f7a2cead0 | |||
| 1e9720a027 | |||
| 2db8e834a8 | |||
| 58353dec5c | |||
| f8f83039f9 | |||
| 47696e928b | |||
| f1cab0563a | |||
| 94e01ae63b | |||
| d7339af37b | |||
| 79a00f9f12 | |||
| af53f98426 | |||
| 0414c886c1 | |||
| 42a47ddd3d | |||
| a45c3e3464 | |||
| bdaca235fb | |||
| 34b943d673 | |||
| 2401a2c2cb | |||
| 2321a7cb8b | |||
| 0499f97e04 | |||
| fd6b5ddcdd | |||
| 34f5015369 | |||
| f044152c0c | |||
| e1c5bd96cc | |||
| 595cacafed | |||
| 2db15448d9 | |||
| 68cf2126ff | |||
| a6d2bc63b8 | |||
| 2789fc8f5e | |||
| a5d7be8405 | |||
| a5e7062c15 | |||
| 3cb5eafe29 | |||
| de695e4d77 | |||
| 5a11c37f90 | |||
| f940addacb | |||
| bafe5dbbf2 | |||
| b3308cdb46 | |||
| 8972f6d9fc | |||
| 325b1da3da | |||
| e62a5c29b0 | |||
| bb3bc4d257 | |||
| 3d8e701a05 | |||
| 57ec706783 | |||
| c05f847f1b | |||
| 30fc503731 | |||
| 53d3649df3 | |||
| a13def849e | |||
| b5e6e5af4a | |||
| 1d2eb2d80c | |||
| 588886f38d | |||
| 3b1a5ed1b5 | |||
| f7e82cdc8b | |||
| f3e9b31ed3 | |||
| a2c60d1812 | |||
| 661bb79710 | |||
| 17fc3de53e | |||
| 52d9d18371 | |||
| f6a777da43 | |||
| 61c6a287e7 | |||
| 7a117fff68 | |||
| 25a1d0daa0 | |||
| bd799d3f07 | |||
| 6df61832b9 | |||
| c3077b2922 | |||
| a52faa96a8 | |||
| 8ddd65a58c | |||
| d56c94fd43 | |||
| f686c77da8 | |||
| 837d1ff39d | |||
| ce6f5917c2 | |||
| 08cb8a4b53 | |||
| 557ed8103a | |||
| 56ae921850 | |||
| 9f6eb2d34f | |||
| b158c444cf | |||
| fbcac9b6d7 | |||
| a75722e418 | |||
| 9dca2de8b9 | |||
| d7ffaf515b | |||
| d64a1a343d | |||
| 1c23753175 | |||
| 9fa968d07c | |||
| 4a6b2c8e0f | |||
| 5000f7fa81 | |||
| abba1c94f1 | |||
| 769f73c8ea | |||
| fbe923301f | |||
| 04902d67b4 | |||
| 47d477c7d0 | |||
| 7f42e3142c | |||
| aade2cf0c9 | |||
| 2fa15fe4c3 | |||
| 344262cc34 | |||
| bc7e4fe36b | |||
| 16b6a85689 | |||
| cf4a2bcc45 | |||
| eca31838a5 | |||
| 959ff9e2f2 |
@@ -0,0 +1,70 @@
|
||||
name: CI Build Check
|
||||
|
||||
on:
|
||||
push:
|
||||
branches: ['**']
|
||||
pull_request:
|
||||
branches: ['**']
|
||||
|
||||
jobs:
|
||||
frontend:
|
||||
name: Build Frontend
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: vite-frontend
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Node.js
|
||||
uses: actions/setup-node@v4
|
||||
with:
|
||||
node-version: '20.19.0'
|
||||
|
||||
- name: Install dependencies
|
||||
run: npm install --legacy-peer-deps
|
||||
|
||||
- name: Build
|
||||
run: npm run build
|
||||
|
||||
backend:
|
||||
name: Build Go Backend
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: go-backend
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
cache-dependency-path: go-backend/go.sum
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Build
|
||||
run: go build -v ./...
|
||||
|
||||
agent:
|
||||
name: Build Agent
|
||||
runs-on: ubuntu-latest
|
||||
defaults:
|
||||
run:
|
||||
working-directory: go-gost
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
cache-dependency-path: go-gost/go.sum
|
||||
|
||||
- name: Download dependencies
|
||||
run: go mod download
|
||||
|
||||
- name: Build
|
||||
run: go build -v .
|
||||
@@ -0,0 +1,370 @@
|
||||
name: Build and Push Images
|
||||
|
||||
env:
|
||||
VERSION: "2.0.7-beta" # 分支推送时使用的默认版本
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- '[0-9]*' # 匹配 2.0.8, 2.0.8-beta 等格式
|
||||
|
||||
jobs:
|
||||
check-version:
|
||||
name: Check Version and Decide Build
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
version: ${{ steps.version.outputs.version }}
|
||||
should_build: ${{ steps.version.outputs.should_build }}
|
||||
should_build_gost: ${{ steps.version.outputs.should_build_gost }}
|
||||
is_tag: ${{ steps.version.outputs.is_tag }}
|
||||
image_owner: ${{ steps.version.outputs.image_owner }}
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
with:
|
||||
fetch-depth: 0
|
||||
|
||||
- name: Determine version and build strategy
|
||||
id: version
|
||||
run: |
|
||||
# 镜像 owner 需要小写
|
||||
IMAGE_OWNER=$(echo "${{ github.repository_owner }}" | tr '[:upper:]' '[:lower:]')
|
||||
echo "image_owner=$IMAGE_OWNER" >> $GITHUB_OUTPUT
|
||||
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
# Tag 触发:直接使用 tag 名作为版本,全量构建
|
||||
VERSION="${{ github.ref_name }}"
|
||||
echo "🏷️ Tag trigger detected: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "is_tag=true" >> $GITHUB_OUTPUT
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
echo "should_build_gost=true" >> $GITHUB_OUTPUT
|
||||
else
|
||||
# 分支触发:使用 env.VERSION,检查是否需要构建
|
||||
VERSION="${{ env.VERSION }}"
|
||||
echo "🌿 Branch trigger detected, using version: $VERSION"
|
||||
echo "version=$VERSION" >> $GITHUB_OUTPUT
|
||||
echo "is_tag=false" >> $GITHUB_OUTPUT
|
||||
|
||||
# 检查 tag 是否已存在
|
||||
if git rev-parse "$VERSION" >/dev/null 2>&1; then
|
||||
echo "Tag $VERSION already exists"
|
||||
echo "should_build=false" >> $GITHUB_OUTPUT
|
||||
|
||||
# 检查 go-gost 目录是否有变化
|
||||
TAG_COMMIT=$(git rev-list -n 1 "$VERSION")
|
||||
if git diff --quiet --ignore-all-space --ignore-blank-lines $TAG_COMMIT HEAD -- go-gost/ 2>/dev/null; then
|
||||
echo "✅ GOST files unchanged since tag"
|
||||
echo "should_build_gost=false" >> $GITHUB_OUTPUT
|
||||
else
|
||||
echo "🔄 Detected changes in go-gost directory"
|
||||
git diff --stat $TAG_COMMIT HEAD -- go-gost/ || true
|
||||
echo "should_build_gost=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
else
|
||||
echo "Tag $VERSION does not exist, will build all components"
|
||||
echo "should_build=true" >> $GITHUB_OUTPUT
|
||||
echo "should_build_gost=true" >> $GITHUB_OUTPUT
|
||||
fi
|
||||
fi
|
||||
|
||||
build-gost:
|
||||
name: Build & Compress GOST Binary
|
||||
needs: check-version
|
||||
if: needs.check-version.outputs.should_build_gost == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Cache Go modules
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-${{ hashFiles('go-gost/go.sum') }}
|
||||
restore-keys: |
|
||||
${{ runner.os }}-go-
|
||||
|
||||
- name: Install UPX
|
||||
run: |
|
||||
wget -q https://github.com/upx/upx/releases/download/v4.2.1/upx-4.2.1-amd64_linux.tar.xz
|
||||
tar -xf upx-4.2.1-amd64_linux.tar.xz
|
||||
sudo mv upx-4.2.1-amd64_linux/upx /usr/local/bin/
|
||||
rm -rf upx-4.2.1-amd64_linux*
|
||||
|
||||
- name: Build GOST binary (AMD64)
|
||||
working-directory: ./go-gost
|
||||
run: CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -ldflags="-s -w -X main.version=${{ needs.check-version.outputs.version }}" -o gost-amd64
|
||||
|
||||
- name: Build GOST binary (ARM64)
|
||||
working-directory: ./go-gost
|
||||
run: CGO_ENABLED=0 GOOS=linux GOARCH=arm64 go build -ldflags="-s -w -X main.version=${{ needs.check-version.outputs.version }}" -o gost-arm64
|
||||
|
||||
- name: Compress with UPX
|
||||
working-directory: ./go-gost
|
||||
run: |
|
||||
upx --best --lzma gost-amd64
|
||||
upx --best --lzma gost-arm64
|
||||
|
||||
- name: Upload GOST AMD64 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gost-binary-amd64
|
||||
path: ./go-gost/gost-amd64
|
||||
|
||||
- name: Upload GOST ARM64 artifact
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: gost-binary-arm64
|
||||
path: ./go-gost/gost-arm64
|
||||
|
||||
build-vite:
|
||||
name: Build & Push Vite Frontend
|
||||
needs: check-version
|
||||
if: needs.check-version.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- 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: Prepare build args
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
{
|
||||
echo "VITE_GITHUB_REPO=https://github.com/${{ github.repository }}"
|
||||
echo "VITE_APP_VERSION=$VERSION"
|
||||
} > ./vite-frontend/.env.production
|
||||
|
||||
- name: Build and push Vite Docker images
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
OWNER="${{ needs.check-version.outputs.image_owner }}"
|
||||
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
-t ${{ env.REGISTRY }}/${OWNER}/vite-frontend:latest \
|
||||
-t ${{ env.REGISTRY }}/${OWNER}/vite-frontend:${VERSION} \
|
||||
./vite-frontend
|
||||
|
||||
build-go-backend:
|
||||
name: Build & Push Go Backend
|
||||
needs: check-version
|
||||
if: needs.check-version.outputs.should_build == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.23'
|
||||
|
||||
- name: Cache Go dependencies
|
||||
uses: actions/cache@v4
|
||||
with:
|
||||
path: |
|
||||
~/.cache/go-build
|
||||
~/go/pkg/mod
|
||||
key: ${{ runner.os }}-go-backend-${{ hashFiles('go-backend/go.sum') }}
|
||||
restore-keys: ${{ runner.os }}-go-backend-
|
||||
|
||||
- name: Download dependencies
|
||||
working-directory: ./go-backend
|
||||
run: go mod download
|
||||
|
||||
- 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: Build and push Go backend Docker images
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
OWNER="${{ needs.check-version.outputs.image_owner }}"
|
||||
|
||||
docker buildx build \
|
||||
--platform linux/amd64,linux/arm64 \
|
||||
--push \
|
||||
-t ${{ env.REGISTRY }}/${OWNER}/flux-panel-backend:latest \
|
||||
-t ${{ env.REGISTRY }}/${OWNER}/flux-panel-backend:${VERSION} \
|
||||
./go-backend
|
||||
|
||||
create-release:
|
||||
name: Create Release (Tag Only)
|
||||
needs: [check-version, build-gost, build-vite, build-go-backend]
|
||||
if: needs.check-version.outputs.is_tag == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download GOST AMD64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gost-binary-amd64
|
||||
path: ./artifacts/amd64
|
||||
|
||||
- name: Download GOST ARM64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gost-binary-arm64
|
||||
path: ./artifacts/arm64
|
||||
|
||||
- name: Prepare release files
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
OWNER="${{ needs.check-version.outputs.image_owner }}"
|
||||
REPO="${{ github.repository }}"
|
||||
|
||||
# 移动二进制文件
|
||||
mv ./artifacts/amd64/gost-amd64 ./artifacts/gost-amd64
|
||||
mv ./artifacts/arm64/gost-arm64 ./artifacts/gost-arm64
|
||||
|
||||
# 复制并修改 docker-compose 文件
|
||||
cp docker-compose-v4.yml ./artifacts/docker-compose-v4.yml
|
||||
cp docker-compose-v6.yml ./artifacts/docker-compose-v6.yml
|
||||
|
||||
# 替换镜像地址为 GHCR
|
||||
sed -i "s|image: .*flux-panel-backend:[^[:space:]]*|image: ${{ env.REGISTRY }}/${OWNER}/flux-panel-backend:${VERSION}|g" ./artifacts/docker-compose-v4.yml
|
||||
sed -i "s|image: .*vite-frontend:[^[:space:]]*|image: ${{ env.REGISTRY }}/${OWNER}/vite-frontend:${VERSION}|g" ./artifacts/docker-compose-v4.yml
|
||||
sed -i "s|image: .*flux-panel-backend:[^[:space:]]*|image: ${{ env.REGISTRY }}/${OWNER}/flux-panel-backend:${VERSION}|g" ./artifacts/docker-compose-v6.yml
|
||||
sed -i "s|image: .*vite-frontend:[^[:space:]]*|image: ${{ env.REGISTRY }}/${OWNER}/vite-frontend:${VERSION}|g" ./artifacts/docker-compose-v6.yml
|
||||
|
||||
# 复制并修改安装脚本
|
||||
cp install.sh ./artifacts/install.sh
|
||||
cp panel_install.sh ./artifacts/panel_install.sh
|
||||
|
||||
# 替换仓库地址和版本号
|
||||
sed -i "s|bqlpfy/flux-panel|${REPO}|g" ./artifacts/install.sh
|
||||
sed -i "s|bqlpfy/flux-panel|${REPO}|g" ./artifacts/panel_install.sh
|
||||
sed -i "s|2.0.7-beta|${VERSION}|g" ./artifacts/install.sh
|
||||
sed -i "s|2.0.7-beta|${VERSION}|g" ./artifacts/panel_install.sh
|
||||
|
||||
- name: Create Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
OWNER="${{ needs.check-version.outputs.image_owner }}"
|
||||
|
||||
# 获取 commit 信息
|
||||
COMMIT_MSG=$(git log -1 --pretty=format:"%s")
|
||||
COMMIT_AUTHOR=$(git log -1 --pretty=format:"%an")
|
||||
COMMIT_DATE=$(git log -1 --pretty=format:"%ai")
|
||||
|
||||
# 创建 release
|
||||
gh release create "${VERSION}" \
|
||||
--title "Release ${VERSION}" \
|
||||
--notes "## 📝 Release Information
|
||||
|
||||
- **Version**: ${VERSION}
|
||||
- **Commit**: [\`${GITHUB_SHA:0:7}\`](https://github.com/${{ github.repository }}/commit/${{ github.sha }})
|
||||
- **Author**: ${COMMIT_AUTHOR}
|
||||
- **Date**: ${COMMIT_DATE}
|
||||
- **Message**: ${COMMIT_MSG}
|
||||
|
||||
## 📦 Docker Images
|
||||
|
||||
\`\`\`bash
|
||||
# Backend
|
||||
docker pull ${{ env.REGISTRY }}/${OWNER}/flux-panel-backend:${VERSION}
|
||||
|
||||
# Frontend
|
||||
docker pull ${{ env.REGISTRY }}/${OWNER}/vite-frontend:${VERSION}
|
||||
\`\`\`
|
||||
|
||||
## 🚀 Quick Install
|
||||
|
||||
**Panel:**
|
||||
\`\`\`bash
|
||||
curl -L https://github.com/${{ github.repository }}/releases/download/${VERSION}/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
\`\`\`
|
||||
|
||||
**Node:**
|
||||
\`\`\`bash
|
||||
curl -L https://github.com/${{ github.repository }}/releases/download/${VERSION}/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
\`\`\`" \
|
||||
--repo ${{ github.repository }}
|
||||
|
||||
# 上传所有文件到 release
|
||||
echo "📤 上传 GOST 二进制文件..."
|
||||
gh release upload "${VERSION}" ./artifacts/gost-amd64 --clobber
|
||||
gh release upload "${VERSION}" ./artifacts/gost-arm64 --clobber
|
||||
|
||||
echo "📤 上传安装脚本..."
|
||||
gh release upload "${VERSION}" ./artifacts/install.sh --clobber
|
||||
gh release upload "${VERSION}" ./artifacts/panel_install.sh --clobber
|
||||
|
||||
echo "📤 上传 Docker Compose 配置文件..."
|
||||
gh release upload "${VERSION}" ./artifacts/docker-compose-v4.yml --clobber
|
||||
gh release upload "${VERSION}" ./artifacts/docker-compose-v6.yml --clobber
|
||||
|
||||
echo "✅ Release ${VERSION} 创建完成"
|
||||
|
||||
update-release-gost:
|
||||
name: Update GOST Binaries in Release
|
||||
needs: [check-version, build-gost]
|
||||
if: needs.check-version.outputs.is_tag == 'false' && needs.check-version.outputs.should_build == 'false' && needs.check-version.outputs.should_build_gost == 'true'
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: write
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
|
||||
- name: Download GOST AMD64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gost-binary-amd64
|
||||
path: ./artifacts/amd64
|
||||
|
||||
- name: Download GOST ARM64 binary
|
||||
uses: actions/download-artifact@v4
|
||||
with:
|
||||
name: gost-binary-arm64
|
||||
path: ./artifacts/arm64
|
||||
|
||||
- name: Rename binaries
|
||||
run: |
|
||||
mv ./artifacts/amd64/gost-amd64 ./artifacts/gost-amd64
|
||||
mv ./artifacts/arm64/gost-arm64 ./artifacts/gost-arm64
|
||||
|
||||
- name: Update GOST binaries in Release
|
||||
env:
|
||||
GH_TOKEN: ${{ github.token }}
|
||||
run: |
|
||||
VERSION="${{ needs.check-version.outputs.version }}"
|
||||
|
||||
echo "🔄 更新 Release ${VERSION} 中的 GOST 二进制文件..."
|
||||
|
||||
gh release upload "${VERSION}" ./artifacts/gost-amd64 --clobber
|
||||
gh release upload "${VERSION}" ./artifacts/gost-arm64 --clobber
|
||||
|
||||
echo "✅ GOST 二进制文件更新完成"
|
||||
+263
@@ -0,0 +1,263 @@
|
||||
# ==========================================
|
||||
# 操作系统生成的文件
|
||||
# ==========================================
|
||||
flutter_app/
|
||||
|
||||
# macOS
|
||||
.DS_Store
|
||||
.AppleDouble
|
||||
.LSOverride
|
||||
Icon
|
||||
._*
|
||||
.DocumentRevisions-V100
|
||||
.fseventsd
|
||||
.Spotlight-V100
|
||||
.TemporaryItems
|
||||
.Trashes
|
||||
.VolumeIcon.icns
|
||||
.com.apple.timemachine.donotpresent
|
||||
|
||||
# Windows
|
||||
Thumbs.db
|
||||
Thumbs.db:encryptable
|
||||
ehthumbs.db
|
||||
ehthumbs_vista.db
|
||||
*.tmp
|
||||
*.temp
|
||||
Desktop.ini
|
||||
$RECYCLE.BIN/
|
||||
*.cab
|
||||
*.msi
|
||||
*.msix
|
||||
*.msm
|
||||
*.msp
|
||||
*.lnk
|
||||
|
||||
# Linux
|
||||
*~
|
||||
.fuse_hidden*
|
||||
.directory
|
||||
.Trash-*
|
||||
.nfs*
|
||||
|
||||
# ==========================================
|
||||
# IDE 和编辑器配置文件
|
||||
# ==========================================
|
||||
|
||||
# IntelliJ IDEA
|
||||
.idea/
|
||||
.vitepress/dist/
|
||||
.vitepress/cache/
|
||||
*.iws
|
||||
*.iml
|
||||
*.ipr
|
||||
out/
|
||||
go-gost/ss/
|
||||
# VS Code
|
||||
.vscode/
|
||||
*.code-workspace
|
||||
|
||||
# Eclipse
|
||||
.metadata
|
||||
.classpath
|
||||
.project
|
||||
.settings/
|
||||
bin/
|
||||
tmp/
|
||||
*.swp
|
||||
*.bak
|
||||
|
||||
# Vim
|
||||
*.swp
|
||||
*.swo
|
||||
*~
|
||||
|
||||
# Emacs
|
||||
*~
|
||||
\#*\#
|
||||
/.emacs.desktop
|
||||
/.emacs.desktop.lock
|
||||
*.elc
|
||||
auto-save-list
|
||||
tramp
|
||||
.\#*
|
||||
|
||||
# ==========================================
|
||||
# Java / Maven / Spring Boot
|
||||
# ==========================================
|
||||
|
||||
# Maven
|
||||
target/
|
||||
pom.xml.tag
|
||||
pom.xml.releaseBackup
|
||||
pom.xml.versionsBackup
|
||||
pom.xml.next
|
||||
release.properties
|
||||
dependency-reduced-pom.xml
|
||||
buildNumber.properties
|
||||
.mvn/timing.properties
|
||||
.mvn/wrapper/maven-wrapper.jar
|
||||
|
||||
# Spring Boot
|
||||
*.jar
|
||||
*.war
|
||||
*.nar
|
||||
*.ear
|
||||
*.zip
|
||||
*.tar.gz
|
||||
*.rar
|
||||
application-local.properties
|
||||
application-dev.properties
|
||||
|
||||
# Java编译输出
|
||||
*.class
|
||||
|
||||
# 日志文件
|
||||
logs/
|
||||
applog/
|
||||
*.log
|
||||
log/
|
||||
|
||||
# ==========================================
|
||||
# Node.js / Vue.js / 前端
|
||||
# ==========================================
|
||||
|
||||
# 依赖目录
|
||||
node_modules/
|
||||
npm-debug.log*
|
||||
yarn-debug.log*
|
||||
yarn-error.log*
|
||||
lerna-debug.log*
|
||||
|
||||
# 运行时数据
|
||||
pids
|
||||
*.pid
|
||||
*.seed
|
||||
*.pid.lock
|
||||
|
||||
# Coverage directory used by tools like istanbul
|
||||
coverage/
|
||||
*.lcov
|
||||
|
||||
# Dependency directories
|
||||
jspm_packages/
|
||||
|
||||
# TypeScript cache
|
||||
*.tsbuildinfo
|
||||
|
||||
# Optional npm cache directory
|
||||
.npm
|
||||
|
||||
# Optional eslint cache
|
||||
.eslintcache
|
||||
|
||||
# Yarn Integrity file
|
||||
.yarn-integrity
|
||||
|
||||
# dotenv environment variables file
|
||||
.env
|
||||
.env.local
|
||||
.env.development.local
|
||||
.env.test.local
|
||||
.env.production.local
|
||||
|
||||
# 构建输出
|
||||
dist/
|
||||
build/
|
||||
|
||||
# ==========================================
|
||||
# Go
|
||||
# ==========================================
|
||||
|
||||
# Go 二进制文件
|
||||
*.exe
|
||||
*.exe~
|
||||
*.dll
|
||||
*.so
|
||||
*.dylib
|
||||
your_app.exe
|
||||
|
||||
# Go 测试二进制文件
|
||||
*.test
|
||||
|
||||
# Go 输出覆盖率
|
||||
*.out
|
||||
|
||||
# Go 模块代理缓存
|
||||
go.work
|
||||
go.work.sum
|
||||
|
||||
# 编译器输出
|
||||
vendor/
|
||||
|
||||
# ==========================================
|
||||
# Docker
|
||||
# ==========================================
|
||||
|
||||
# Docker 本地配置
|
||||
.dockerignore.local
|
||||
|
||||
# ==========================================
|
||||
# 数据库和配置文件
|
||||
# ==========================================
|
||||
|
||||
# 数据库文件
|
||||
*.db
|
||||
*.sqlite
|
||||
*.sqlite3
|
||||
|
||||
# 配置文件中的敏感信息
|
||||
config-local.json
|
||||
secrets.json
|
||||
*.key
|
||||
*.pem
|
||||
*.p12
|
||||
*.pfx
|
||||
|
||||
# Go-gost 特定配置文件
|
||||
go-gost/config.json
|
||||
go-gost/gost.json
|
||||
|
||||
# ==========================================
|
||||
# 备份和临时文件
|
||||
# ==========================================
|
||||
|
||||
# 备份文件
|
||||
*.bak
|
||||
*.backup
|
||||
*.orig
|
||||
*.save
|
||||
|
||||
# 临时文件
|
||||
*.tmp
|
||||
*.temp
|
||||
.cache/
|
||||
|
||||
|
||||
gitee/
|
||||
|
||||
|
||||
|
||||
# ==========================================
|
||||
# 其他
|
||||
# ==========================================
|
||||
|
||||
# 压缩文件
|
||||
*.7z
|
||||
*.dmg
|
||||
*.gz
|
||||
*.iso
|
||||
*.rar
|
||||
*.tar
|
||||
*.zip
|
||||
|
||||
# 证书文件
|
||||
*.crt
|
||||
*.cert
|
||||
doraemon.jks
|
||||
device.id
|
||||
commit.sh
|
||||
sql/
|
||||
!go-backend/internal/store/sqlite/sql/
|
||||
!go-backend/internal/store/sqlite/sql/schema.sql
|
||||
!go-backend/internal/store/sqlite/sql/data.sql
|
||||
@@ -0,0 +1,71 @@
|
||||
# Plan: 搭建开发环境
|
||||
|
||||
## 目标
|
||||
为 Flux Panel 项目安装所有缺失的开发依赖,使 3 个子项目都能本地开发和构建。
|
||||
|
||||
## 当前状态
|
||||
|
||||
### ✅ 已安装
|
||||
| 工具 | 版本 | 用途 |
|
||||
|------|------|------|
|
||||
| Node.js | v20.19.2 | vite-frontend |
|
||||
| npm | 9.2.0 | vite-frontend |
|
||||
| Go | 1.24.4 | go-gost |
|
||||
| Docker | 29.1.4 | 容器化部署 |
|
||||
|
||||
### ❌ 缺失
|
||||
| 工具 | 需求版本 | 用途 |
|
||||
|------|----------|------|
|
||||
| Java | 21 | springboot-backend |
|
||||
| Maven | 3.x | 构建后端 |
|
||||
| Docker Compose | v2 | 容器编排 |
|
||||
|
||||
---
|
||||
|
||||
## 执行任务
|
||||
|
||||
### Task 1: 安装 Java 21
|
||||
```bash
|
||||
apt-get update && apt-get install -y openjdk-21-jdk
|
||||
```
|
||||
**验证**: `java -version` 应显示 openjdk 21
|
||||
|
||||
### Task 2: 安装 Maven
|
||||
```bash
|
||||
apt-get install -y maven
|
||||
```
|
||||
**验证**: `mvn -v` 应显示 Maven 3.x
|
||||
|
||||
### Task 3: 安装 Docker Compose Plugin
|
||||
```bash
|
||||
apt-get install -y docker-compose-plugin
|
||||
```
|
||||
**验证**: `docker compose version` 应显示版本号
|
||||
|
||||
### Task 4: 安装前端依赖
|
||||
```bash
|
||||
cd /root/flux-panel/vite-frontend && npm install
|
||||
```
|
||||
**验证**: `node_modules/` 目录存在
|
||||
|
||||
### Task 5: 验证后端可构建
|
||||
```bash
|
||||
cd /root/flux-panel/springboot-backend && mvn clean compile -q
|
||||
```
|
||||
**验证**: 编译成功无错误
|
||||
|
||||
### Task 6: 验证 Go 模块
|
||||
```bash
|
||||
cd /root/flux-panel/go-gost && go mod download
|
||||
```
|
||||
**验证**: 依赖下载成功
|
||||
|
||||
---
|
||||
|
||||
## 完成标准
|
||||
- [ ] `java -version` → openjdk 21
|
||||
- [ ] `mvn -v` → Maven 3.x
|
||||
- [ ] `docker compose version` → v2.x
|
||||
- [ ] 前端: `npm run dev` 可启动
|
||||
- [ ] 后端: `mvn compile` 成功
|
||||
- [ ] Go: `go build .` 成功
|
||||
@@ -1,586 +0,0 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="zh" class="no-js">
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<meta name="description" content="FLVX官方文档">
|
||||
|
||||
|
||||
<meta name="author" content="Sagit-chu">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="icon" href="/flux-panel/assets/images/favicon.png">
|
||||
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.7">
|
||||
|
||||
|
||||
|
||||
<title>FLVX 文档</title>
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/flux-panel/assets/stylesheets/main.ec1eaa64.min.css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="/flux-panel/assets/stylesheets/palette.ab4e12ef.min.css">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
|
||||
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
|
||||
|
||||
|
||||
|
||||
<script>__md_scope=new URL("/flux-panel/",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<body dir="ltr" data-md-color-scheme="default" data-md-color-primary="indigo" data-md-color-accent="indigo">
|
||||
|
||||
|
||||
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
|
||||
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
|
||||
<label class="md-overlay" for="__drawer"></label>
|
||||
<div data-md-component="skip">
|
||||
|
||||
</div>
|
||||
<div data-md-component="announce">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<header class="md-header" data-md-component="header">
|
||||
<nav class="md-header__inner md-grid" aria-label="页眉">
|
||||
<a href="/flux-panel/." title="FLVX 文档" class="md-header__button md-logo" aria-label="FLVX 文档" data-md-component="logo">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
|
||||
|
||||
</a>
|
||||
<label class="md-header__button md-icon" for="__drawer">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||
</label>
|
||||
<div class="md-header__title" data-md-component="header-title">
|
||||
<div class="md-header__ellipsis">
|
||||
<div class="md-header__topic">
|
||||
<span class="md-ellipsis">
|
||||
FLVX 文档
|
||||
</span>
|
||||
</div>
|
||||
<div class="md-header__topic" data-md-component="header-topic">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<form class="md-header__option" data-md-component="palette">
|
||||
|
||||
|
||||
|
||||
|
||||
<input class="md-option" data-md-color-media="" data-md-color-scheme="default" data-md-color-primary="indigo" data-md-color-accent="indigo" aria-hidden="true" type="radio" name="__palette" id="__palette_0">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="indigo" data-md-color-accent="indigo" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_1">
|
||||
|
||||
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_0" hidden>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 18c-.89 0-1.74-.2-2.5-.55C11.56 16.5 13 14.42 13 12s-1.44-4.5-3.5-5.45C10.26 6.2 11.11 6 12 6a6 6 0 0 1 6 6 6 6 0 0 1-6 6m8-9.31V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-header__button md-icon" for="__search">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||
</label>
|
||||
<div class="md-search" data-md-component="search" role="dialog">
|
||||
<label class="md-search__overlay" for="__search"></label>
|
||||
<div class="md-search__inner" role="search">
|
||||
<form class="md-search__form" name="search">
|
||||
<input type="text" class="md-search__input" name="query" aria-label="搜索" placeholder="搜索" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
|
||||
<label class="md-search__icon md-icon" for="__search">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
|
||||
</label>
|
||||
<nav class="md-search__options" aria-label="查找">
|
||||
|
||||
<button type="reset" class="md-search__icon md-icon" title="清空当前内容" aria-label="清空当前内容" tabindex="-1">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="md-search__suggest" data-md-component="search-suggest"></div>
|
||||
|
||||
</form>
|
||||
<div class="md-search__output">
|
||||
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
|
||||
<div class="md-search-result" data-md-component="search-result">
|
||||
<div class="md-search-result__meta">
|
||||
正在初始化搜索引擎
|
||||
</div>
|
||||
<ol class="md-search-result__list" role="presentation"></ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</nav>
|
||||
|
||||
</header>
|
||||
|
||||
<div class="md-container" data-md-component="container">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<nav class="md-tabs" aria-label="标签" data-md-component="tabs">
|
||||
<div class="md-grid">
|
||||
<ul class="md-tabs__list">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="/flux-panel/." class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
首页
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="/flux-panel/install/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
安装部署
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="/flux-panel/usage/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
使用指南
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="/flux-panel/ai-skill/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
AI Skill 接入
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="/flux-panel/postgresql/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
PostgreSQL
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="/flux-panel/faq/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
常见问题
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<main class="md-main" data-md-component="main">
|
||||
<div class="md-main__inner md-grid">
|
||||
|
||||
|
||||
|
||||
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div class="md-sidebar__inner">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="导航栏" data-md-level="0">
|
||||
<label class="md-nav__title" for="__drawer">
|
||||
<a href="/flux-panel/." title="FLVX 文档" class="md-nav__button md-logo" aria-label="FLVX 文档" data-md-component="logo">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
|
||||
|
||||
</a>
|
||||
FLVX 文档
|
||||
</label>
|
||||
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/flux-panel/." class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
首页
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/flux-panel/install/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
安装部署
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/flux-panel/usage/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
使用指南
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/flux-panel/ai-skill/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
AI Skill 接入
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/flux-panel/postgresql/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
PostgreSQL
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="/flux-panel/faq/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
常见问题
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div class="md-sidebar__inner">
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--secondary" aria-label="目录">
|
||||
|
||||
|
||||
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="md-content" data-md-component="content">
|
||||
|
||||
<article class="md-content__inner md-typeset">
|
||||
|
||||
<h1>404 - Not found</h1>
|
||||
|
||||
</article>
|
||||
</div>
|
||||
|
||||
|
||||
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
|
||||
</div>
|
||||
|
||||
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
|
||||
回到页面顶部
|
||||
</button>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="md-footer">
|
||||
|
||||
<div class="md-footer-meta md-typeset">
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
<div class="md-copyright">
|
||||
|
||||
|
||||
Made with
|
||||
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
|
||||
Material for MkDocs
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
<div class="md-dialog" data-md-component="dialog">
|
||||
<div class="md-dialog__inner md-typeset"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script id="__config" type="application/json">{"annotate": null, "base": "/flux-panel/", "features": ["navigation.tabs", "navigation.sections", "navigation.top", "search.suggest", "search.highlight", "content.code.copy"], "search": "/flux-panel/assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "\u5df2\u590d\u5236", "clipboard.copy": "\u590d\u5236", "search.result.more.one": "\u5728\u8be5\u9875\u4e0a\u8fd8\u6709 1 \u4e2a\u7b26\u5408\u6761\u4ef6\u7684\u7ed3\u679c", "search.result.more.other": "\u5728\u8be5\u9875\u4e0a\u8fd8\u6709 # \u4e2a\u7b26\u5408\u6761\u4ef6\u7684\u7ed3\u679c", "search.result.none": "\u6ca1\u6709\u627e\u5230\u7b26\u5408\u6761\u4ef6\u7684\u7ed3\u679c", "search.result.one": "\u627e\u5230 1 \u4e2a\u7b26\u5408\u6761\u4ef6\u7684\u7ed3\u679c", "search.result.other": "# \u4e2a\u7b26\u5408\u6761\u4ef6\u7684\u7ed3\u679c", "search.result.placeholder": "\u952e\u5165\u4ee5\u5f00\u59cb\u641c\u7d22", "search.result.term.missing": "\u7f3a\u5c11", "select.version": "\u9009\u62e9\u5f53\u524d\u7248\u672c"}, "version": null}</script>
|
||||
|
||||
|
||||
<script src="/flux-panel/assets/javascripts/bundle.d7400e89.min.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,66 @@
|
||||
# PROJECT KNOWLEDGE BASE
|
||||
|
||||
**Generated:** Mon Feb 02 2026
|
||||
**Commit:** 7ca01ab
|
||||
**Branch:** beta
|
||||
|
||||
## OVERVIEW
|
||||
Flux Panel is a traffic forwarding management system built on a forked GOST v3 stack. It ships as Dockerized Spring Boot (admin API) + Vite/React UI + Go forwarding agent, with optional mobile WebView wrappers.
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
./
|
||||
├── go-gost/ # Go forwarding agent (forked gost + local x/)
|
||||
│ └── x/ # Local fork of github.com/go-gost/x (replace => ./x)
|
||||
├── springboot-backend/ # Java/Spring Boot admin API (SQLite/MyBatis)
|
||||
├── vite-frontend/ # React/Vite dashboard (HeroUI + Tailwind)
|
||||
├── android-app/ # Android WebView wrapper (optional)
|
||||
├── ios-app/ # iOS WebView wrapper (optional)
|
||||
├── docker-compose-v4.yml # Panel deploy (IPv4-only bridge)
|
||||
├── docker-compose-v6.yml # Panel deploy (IPv6-enabled bridge)
|
||||
├── panel_install.sh # Panel installer/upgrader (downloads compose)
|
||||
├── install.sh # Node installer/upgrader (downloads gost binary)
|
||||
└── .github/workflows/ # CI: build/push images + release artifacts
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| **Deploy (Docker)** | `docker-compose-v4.yml` | Env: `JWT_SECRET`, `BACKEND_PORT`, `FRONTEND_PORT` |
|
||||
| **Deploy (IPv6)** | `docker-compose-v6.yml` | Same as v4 + IPv6-enabled bridge |
|
||||
| **Panel install** | `panel_install.sh` | Picks v4/v6, generates `JWT_SECRET`, downloads compose |
|
||||
| **Node install** | `install.sh` | Installs `/etc/flux_agent/flux_agent` + writes `config.json`/`gost.json` + systemd `flux_agent.service` |
|
||||
| **Admin API entry** | `springboot-backend/src/main/java/com/admin/AdminApplication.java` | Spring Boot app |
|
||||
| **Admin API routes** | `springboot-backend/src/main/java/com/admin/controller/` | Mostly `/api/v1/*` controllers |
|
||||
| **Admin auth** | `springboot-backend/src/main/java/com/admin/common/interceptor/JwtInterceptor.java` | Checks `Authorization` header |
|
||||
| **Web UI routing** | `vite-frontend/src/App.tsx` | React Router v6 + ProtectedRoute/H5 layouts |
|
||||
| **Web UI API client** | `vite-frontend/src/api/network.ts` | Axios `baseURL` + `Authorization` header |
|
||||
| **Go agent entry** | `go-gost/main.go` | Reads panel `config.json` + starts gost services |
|
||||
| **Go x fork** | `go-gost/x/` | Handlers/listeners/dialers + management API |
|
||||
|
||||
## CONVENTIONS
|
||||
- `Authorization` header carries the raw JWT token (no `Bearer` prefix) between `vite-frontend/` and `springboot-backend/`.
|
||||
- `go-gost/` uses `replace github.com/go-gost/x => ./x` and `go-gost/x/` is also its own Go module.
|
||||
|
||||
## ANTI-PATTERNS (THIS PROJECT)
|
||||
- Do not edit generated protobuf output: `go-gost/x/internal/util/grpc/proto/*.pb.go`, `go-gost/x/internal/util/grpc/proto/*_grpc.pb.go`.
|
||||
|
||||
## COMMANDS
|
||||
```bash
|
||||
# Panel (Docker)
|
||||
docker compose -f docker-compose-v4.yml up -d
|
||||
docker compose -f docker-compose-v6.yml up -d
|
||||
|
||||
# Release-based install scripts
|
||||
./panel_install.sh
|
||||
./install.sh
|
||||
|
||||
# Local dev (per subproject)
|
||||
(cd springboot-backend && mvn clean package)
|
||||
(cd vite-frontend && npm run dev)
|
||||
(cd go-gost && go run .)
|
||||
```
|
||||
|
||||
## NOTES
|
||||
- LSP servers are not installed in this environment (gopls/jdtls/typescript-language-server); rely on grep-based navigation.
|
||||
- `vite-frontend/vite.config.ts` sets `minify: false` and disables treeshake; expect larger bundles.
|
||||
@@ -0,0 +1,201 @@
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright [yyyy] [name of copyright owner]
|
||||
|
||||
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.
|
||||
@@ -0,0 +1,68 @@
|
||||
# flux-panel转发面板 哆啦A梦转发面板
|
||||
|
||||
本项目基于 [go-gost/gost](https://github.com/go-gost/gost) 和 [go-gost/x](https://github.com/go-gost/x) 两个开源库,实现了转发面板。
|
||||
---
|
||||
## 特性
|
||||
|
||||
- 支持按 **隧道账号级别** 管理流量转发数量,可用于用户/隧道配额控制
|
||||
- 支持 **TCP** 和 **UDP** 协议的转发
|
||||
- 支持两种转发模式:**端口转发** 与 **隧道转发**
|
||||
- 可针对 **指定用户的指定隧道进行限速** 设置
|
||||
- 支持配置 **单向或双向流量计费方式**,灵活适配不同计费模型
|
||||
- 提供灵活的转发策略配置,适用于多种网络场景
|
||||
|
||||
|
||||
## 部署流程
|
||||
---
|
||||
### Docker Compose部署
|
||||
#### 快速部署
|
||||
面板端:
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/main/panel_install.sh -o panel_install.sh && chmod +x panel_install.sh && ./panel_install.sh
|
||||
```
|
||||
节点端:
|
||||
```bash
|
||||
curl -L https://raw.githubusercontent.com/Sagit-chu/flux-panel/main/install.sh -o install.sh && chmod +x install.sh && ./install.sh
|
||||
```
|
||||
|
||||
#### 默认管理员账号
|
||||
|
||||
- **账号**: admin_user
|
||||
- **密码**: admin_user
|
||||
|
||||
> ⚠️ 首次登录后请立即修改默认密码!
|
||||
|
||||
|
||||
## 免责声明
|
||||
|
||||
本项目仅供个人学习与研究使用,基于开源项目进行二次开发。
|
||||
|
||||
使用本项目所带来的任何风险均由使用者自行承担,包括但不限于:
|
||||
|
||||
- 配置不当或使用错误导致的服务异常或不可用;
|
||||
- 使用本项目引发的网络攻击、封禁、滥用等行为;
|
||||
- 服务器因使用本项目被入侵、渗透、滥用导致的数据泄露、资源消耗或损失;
|
||||
- 因违反当地法律法规所产生的任何法律责任。
|
||||
|
||||
本项目为开源的流量转发工具,仅限合法、合规用途。
|
||||
使用者必须确保其使用行为符合所在国家或地区的法律法规。
|
||||
|
||||
**作者不对因使用本项目导致的任何法律责任、经济损失或其他后果承担责任。**
|
||||
**禁止将本项目用于任何违法或未经授权的行为,包括但不限于网络攻击、数据窃取、非法访问等。**
|
||||
|
||||
如不同意上述条款,请立即停止使用本项目。
|
||||
|
||||
作者对因使用本项目所造成的任何直接或间接损失概不负责,亦不提供任何形式的担保、承诺或技术支持。
|
||||
|
||||
|
||||
请务必在合法、合规、安全的前提下使用本项目。
|
||||
|
||||
---
|
||||
## ⭐ 喝杯咖啡!(USDT)
|
||||
|
||||
| 网络 | 地址 |
|
||||
|------------|----------------------------------------------------------------------|
|
||||
| BNB(BEP20) | `0xa608708fdc6279a2433fd4b82f0b72b8cbe97ed5` |
|
||||
| TRC20 | `TM8VYdU3s3gSX5PC8swjAJrAzZFCHKqG2k` |
|
||||
| Aptos | `0x49427bfcba1006a346447430689b2307ac156316bb34850d1d3029ff9d118da5` |
|
||||
| polygon | `0xa608708fdc6279a2433fd4b82f0b72b8cbe97ed5` |
|
||||
-1212
File diff suppressed because it is too large
Load Diff
Binary file not shown.
|
Before Width: | Height: | Size: 1.8 KiB |
-16
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
-18
@@ -1,18 +0,0 @@
|
||||
/*!
|
||||
* Lunr languages, `Danish` language
|
||||
* https://github.com/MihaiValentin/lunr-languages
|
||||
*
|
||||
* Copyright 2014, Mihai Valentin
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
/*!
|
||||
* based on
|
||||
* Snowball JavaScript Library v0.3
|
||||
* http://code.google.com/p/urim/
|
||||
* http://snowball.tartarus.org/
|
||||
*
|
||||
* Copyright 2010, Oleg Mazko
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.da=function(){this.pipeline.reset(),this.pipeline.add(e.da.trimmer,e.da.stopWordFilter,e.da.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.da.stemmer))},e.da.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.da.trimmer=e.trimmerSupport.generateTrimmer(e.da.wordCharacters),e.Pipeline.registerFunction(e.da.trimmer,"trimmer-da"),e.da.stemmer=function(){var r=e.stemmerSupport.Among,i=e.stemmerSupport.SnowballProgram,n=new function(){function e(){var e,r=f.cursor+3;if(d=f.limit,0<=r&&r<=f.limit){for(a=r;;){if(e=f.cursor,f.in_grouping(w,97,248)){f.cursor=e;break}if(f.cursor=e,e>=f.limit)return;f.cursor++}for(;!f.out_grouping(w,97,248);){if(f.cursor>=f.limit)return;f.cursor++}d=f.cursor,d<a&&(d=a)}}function n(){var e,r;if(f.cursor>=d&&(r=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,e=f.find_among_b(c,32),f.limit_backward=r,e))switch(f.bra=f.cursor,e){case 1:f.slice_del();break;case 2:f.in_grouping_b(p,97,229)&&f.slice_del()}}function t(){var e,r=f.limit-f.cursor;f.cursor>=d&&(e=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,f.find_among_b(l,4)?(f.bra=f.cursor,f.limit_backward=e,f.cursor=f.limit-r,f.cursor>f.limit_backward&&(f.cursor--,f.bra=f.cursor,f.slice_del())):f.limit_backward=e)}function s(){var e,r,i,n=f.limit-f.cursor;if(f.ket=f.cursor,f.eq_s_b(2,"st")&&(f.bra=f.cursor,f.eq_s_b(2,"ig")&&f.slice_del()),f.cursor=f.limit-n,f.cursor>=d&&(r=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,e=f.find_among_b(m,5),f.limit_backward=r,e))switch(f.bra=f.cursor,e){case 1:f.slice_del(),i=f.limit-f.cursor,t(),f.cursor=f.limit-i;break;case 2:f.slice_from("løs")}}function o(){var e;f.cursor>=d&&(e=f.limit_backward,f.limit_backward=d,f.ket=f.cursor,f.out_grouping_b(w,97,248)?(f.bra=f.cursor,u=f.slice_to(u),f.limit_backward=e,f.eq_v_b(u)&&f.slice_del()):f.limit_backward=e)}var a,d,u,c=[new r("hed",-1,1),new r("ethed",0,1),new r("ered",-1,1),new r("e",-1,1),new r("erede",3,1),new r("ende",3,1),new r("erende",5,1),new r("ene",3,1),new r("erne",3,1),new r("ere",3,1),new r("en",-1,1),new r("heden",10,1),new r("eren",10,1),new r("er",-1,1),new r("heder",13,1),new r("erer",13,1),new r("s",-1,2),new r("heds",16,1),new r("es",16,1),new r("endes",18,1),new r("erendes",19,1),new r("enes",18,1),new r("ernes",18,1),new r("eres",18,1),new r("ens",16,1),new r("hedens",24,1),new r("erens",24,1),new r("ers",16,1),new r("ets",16,1),new r("erets",28,1),new r("et",-1,1),new r("eret",30,1)],l=[new r("gd",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1)],m=[new r("ig",-1,1),new r("lig",0,1),new r("elig",1,1),new r("els",-1,1),new r("løst",-1,2)],w=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],p=[239,254,42,3,0,0,0,0,0,0,0,0,0,0,0,0,16],f=new i;this.setCurrent=function(e){f.setCurrent(e)},this.getCurrent=function(){return f.getCurrent()},this.stem=function(){var r=f.cursor;return e(),f.limit_backward=r,f.cursor=f.limit,n(),f.cursor=f.limit,t(),f.cursor=f.limit,s(),f.cursor=f.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return n.setCurrent(e),n.stem(),n.getCurrent()}):(n.setCurrent(e),n.stem(),n.getCurrent())}}(),e.Pipeline.registerFunction(e.da.stemmer,"stemmer-da"),e.da.stopWordFilter=e.generateStopWordFilter("ad af alle alt anden at blev blive bliver da de dem den denne der deres det dette dig din disse dog du efter eller en end er et for fra ham han hans har havde have hende hendes her hos hun hvad hvis hvor i ikke ind jeg jer jo kunne man mange med meget men mig min mine mit mod ned noget nogle nu når og også om op os over på selv sig sin sine sit skal skulle som sådan thi til ud under var vi vil ville vor være været".split(" ")),e.Pipeline.registerFunction(e.da.stopWordFilter,"stopWordFilter-da")}});
|
||||
-18
File diff suppressed because one or more lines are too long
-18
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
-18
File diff suppressed because one or more lines are too long
-18
File diff suppressed because one or more lines are too long
-18
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hi=function(){this.pipeline.reset(),this.pipeline.add(e.hi.trimmer,e.hi.stopWordFilter,e.hi.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.hi.stemmer))},e.hi.wordCharacters="ऀ-ःऄ-एऐ-टठ-यर-िी-ॏॐ-य़ॠ-९॰-ॿa-zA-Za-zA-Z0-90-9",e.hi.trimmer=e.trimmerSupport.generateTrimmer(e.hi.wordCharacters),e.Pipeline.registerFunction(e.hi.trimmer,"trimmer-hi"),e.hi.stopWordFilter=e.generateStopWordFilter("अत अपना अपनी अपने अभी अंदर आदि आप इत्यादि इन इनका इन्हीं इन्हें इन्हों इस इसका इसकी इसके इसमें इसी इसे उन उनका उनकी उनके उनको उन्हीं उन्हें उन्हों उस उसके उसी उसे एक एवं एस ऐसे और कई कर करता करते करना करने करें कहते कहा का काफ़ी कि कितना किन्हें किन्हों किया किर किस किसी किसे की कुछ कुल के को कोई कौन कौनसा गया घर जब जहाँ जा जितना जिन जिन्हें जिन्हों जिस जिसे जीधर जैसा जैसे जो तक तब तरह तिन तिन्हें तिन्हों तिस तिसे तो था थी थे दबारा दिया दुसरा दूसरे दो द्वारा न नके नहीं ना निहायत नीचे ने पर पहले पूरा पे फिर बनी बही बहुत बाद बाला बिलकुल भी भीतर मगर मानो मे में यदि यह यहाँ यही या यिह ये रखें रहा रहे ऱ्वासा लिए लिये लेकिन व वग़ैरह वर्ग वह वहाँ वहीं वाले वुह वे वो सकता सकते सबसे सभी साथ साबुत साभ सारा से सो संग ही हुआ हुई हुए है हैं हो होता होती होते होना होने".split(" ")),e.hi.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.hi.tokenizer=function(i){if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var t=i.toString().toLowerCase().replace(/^\s+/,"");return r.cut(t).split("|")},e.Pipeline.registerFunction(e.hi.stemmer,"stemmer-hi"),e.Pipeline.registerFunction(e.hi.stopWordFilter,"stopWordFilter-hi")}});
|
||||
-18
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.hy=function(){this.pipeline.reset(),this.pipeline.add(e.hy.trimmer,e.hy.stopWordFilter)},e.hy.wordCharacters="[A-Za-z-֏ff-ﭏ]",e.hy.trimmer=e.trimmerSupport.generateTrimmer(e.hy.wordCharacters),e.Pipeline.registerFunction(e.hy.trimmer,"trimmer-hy"),e.hy.stopWordFilter=e.generateStopWordFilter("դու և եք էիր էիք հետո նաև նրանք որը վրա է որ պիտի են այս մեջ ն իր ու ի այդ որոնք այն կամ էր մի ես համար այլ իսկ էին ենք հետ ին թ էինք մենք նրա նա դուք եմ էի ըստ որպես ում".split(" ")),e.Pipeline.registerFunction(e.hy.stopWordFilter,"stopWordFilter-hy"),e.hy.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}(),e.Pipeline.registerFunction(e.hy.stemmer,"stemmer-hy")}});
|
||||
-18
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r="2"==e.version[0];e.ja=function(){this.pipeline.reset(),this.pipeline.add(e.ja.trimmer,e.ja.stopWordFilter,e.ja.stemmer),r?this.tokenizer=e.ja.tokenizer:(e.tokenizer&&(e.tokenizer=e.ja.tokenizer),this.tokenizerFn&&(this.tokenizerFn=e.ja.tokenizer))};var t=new e.TinySegmenter;e.ja.tokenizer=function(i){var n,o,s,p,a,u,m,l,c,f;if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(t){return r?new e.Token(t.toLowerCase()):t.toLowerCase()});for(o=i.toString().toLowerCase().replace(/^\s+/,""),n=o.length-1;n>=0;n--)if(/\S/.test(o.charAt(n))){o=o.substring(0,n+1);break}for(a=[],s=o.length,c=0,l=0;c<=s;c++)if(u=o.charAt(c),m=c-l,u.match(/\s/)||c==s){if(m>0)for(p=t.segment(o.slice(l,c)).filter(function(e){return!!e}),f=l,n=0;n<p.length;n++)r?a.push(new e.Token(p[n],{position:[f,p[n].length],index:a.length})):a.push(p[n]),f+=p[n].length;l=c+1}return a},e.ja.stemmer=function(){return function(e){return e}}(),e.Pipeline.registerFunction(e.ja.stemmer,"stemmer-ja"),e.ja.wordCharacters="一二三四五六七八九十百千万億兆一-龠々〆ヵヶぁ-んァ-ヴーア-ン゙a-zA-Za-zA-Z0-90-9",e.ja.trimmer=e.trimmerSupport.generateTrimmer(e.ja.wordCharacters),e.Pipeline.registerFunction(e.ja.trimmer,"trimmer-ja"),e.ja.stopWordFilter=e.generateStopWordFilter("これ それ あれ この その あの ここ そこ あそこ こちら どこ だれ なに なん 何 私 貴方 貴方方 我々 私達 あの人 あのかた 彼女 彼 です あります おります います は が の に を で え から まで より も どの と し それで しかし".split(" ")),e.Pipeline.registerFunction(e.ja.stopWordFilter,"stopWordFilter-ja"),e.jp=e.ja,e.Pipeline.registerFunction(e.jp.stemmer,"stemmer-jp"),e.Pipeline.registerFunction(e.jp.trimmer,"trimmer-jp"),e.Pipeline.registerFunction(e.jp.stopWordFilter,"stopWordFilter-jp")}});
|
||||
-1
@@ -1 +0,0 @@
|
||||
module.exports=require("./lunr.ja");
|
||||
-1
@@ -1 +0,0 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.kn=function(){this.pipeline.reset(),this.pipeline.add(e.kn.trimmer,e.kn.stopWordFilter,e.kn.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.kn.stemmer))},e.kn.wordCharacters="ಀ-಄ಅ-ಔಕ-ಹಾ-ೌ಼-ಽೕ-ೖೝ-ೞೠ-ೡೢ-ೣ೦-೯ೱ-ೳ",e.kn.trimmer=e.trimmerSupport.generateTrimmer(e.kn.wordCharacters),e.Pipeline.registerFunction(e.kn.trimmer,"trimmer-kn"),e.kn.stopWordFilter=e.generateStopWordFilter("ಮತ್ತು ಈ ಒಂದು ರಲ್ಲಿ ಹಾಗೂ ಎಂದು ಅಥವಾ ಇದು ರ ಅವರು ಎಂಬ ಮೇಲೆ ಅವರ ತನ್ನ ಆದರೆ ತಮ್ಮ ನಂತರ ಮೂಲಕ ಹೆಚ್ಚು ನ ಆ ಕೆಲವು ಅನೇಕ ಎರಡು ಹಾಗು ಪ್ರಮುಖ ಇದನ್ನು ಇದರ ಸುಮಾರು ಅದರ ಅದು ಮೊದಲ ಬಗ್ಗೆ ನಲ್ಲಿ ರಂದು ಇತರ ಅತ್ಯಂತ ಹೆಚ್ಚಿನ ಸಹ ಸಾಮಾನ್ಯವಾಗಿ ನೇ ಹಲವಾರು ಹೊಸ ದಿ ಕಡಿಮೆ ಯಾವುದೇ ಹೊಂದಿದೆ ದೊಡ್ಡ ಅನ್ನು ಇವರು ಪ್ರಕಾರ ಇದೆ ಮಾತ್ರ ಕೂಡ ಇಲ್ಲಿ ಎಲ್ಲಾ ವಿವಿಧ ಅದನ್ನು ಹಲವು ರಿಂದ ಕೇವಲ ದ ದಕ್ಷಿಣ ಗೆ ಅವನ ಅತಿ ನೆಯ ಬಹಳ ಕೆಲಸ ಎಲ್ಲ ಪ್ರತಿ ಇತ್ಯಾದಿ ಇವು ಬೇರೆ ಹೀಗೆ ನಡುವೆ ಇದಕ್ಕೆ ಎಸ್ ಇವರ ಮೊದಲು ಶ್ರೀ ಮಾಡುವ ಇದರಲ್ಲಿ ರೀತಿಯ ಮಾಡಿದ ಕಾಲ ಅಲ್ಲಿ ಮಾಡಲು ಅದೇ ಈಗ ಅವು ಗಳು ಎ ಎಂಬುದು ಅವನು ಅಂದರೆ ಅವರಿಗೆ ಇರುವ ವಿಶೇಷ ಮುಂದೆ ಅವುಗಳ ಮುಂತಾದ ಮೂಲ ಬಿ ಮೀ ಒಂದೇ ಇನ್ನೂ ಹೆಚ್ಚಾಗಿ ಮಾಡಿ ಅವರನ್ನು ಇದೇ ಯ ರೀತಿಯಲ್ಲಿ ಜೊತೆ ಅದರಲ್ಲಿ ಮಾಡಿದರು ನಡೆದ ಆಗ ಮತ್ತೆ ಪೂರ್ವ ಆತ ಬಂದ ಯಾವ ಒಟ್ಟು ಇತರೆ ಹಿಂದೆ ಪ್ರಮಾಣದ ಗಳನ್ನು ಕುರಿತು ಯು ಆದ್ದರಿಂದ ಅಲ್ಲದೆ ನಗರದ ಮೇಲಿನ ಏಕೆಂದರೆ ರಷ್ಟು ಎಂಬುದನ್ನು ಬಾರಿ ಎಂದರೆ ಹಿಂದಿನ ಆದರೂ ಆದ ಸಂಬಂಧಿಸಿದ ಮತ್ತೊಂದು ಸಿ ಆತನ ".split(" ")),e.kn.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.kn.tokenizer=function(t){if(!arguments.length||null==t||void 0==t)return[];if(Array.isArray(t))return t.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var n=t.toString().toLowerCase().replace(/^\s+/,"");return r.cut(n).split("|")},e.Pipeline.registerFunction(e.kn.stemmer,"stemmer-kn"),e.Pipeline.registerFunction(e.kn.stopWordFilter,"stopWordFilter-kn")}});
|
||||
-1
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){e.multiLanguage=function(){for(var t=Array.prototype.slice.call(arguments),i=t.join("-"),r="",n=[],s=[],p=0;p<t.length;++p)"en"==t[p]?(r+="\\w",n.unshift(e.stopWordFilter),n.push(e.stemmer),s.push(e.stemmer)):(r+=e[t[p]].wordCharacters,e[t[p]].stopWordFilter&&n.unshift(e[t[p]].stopWordFilter),e[t[p]].stemmer&&(n.push(e[t[p]].stemmer),s.push(e[t[p]].stemmer)));var o=e.trimmerSupport.generateTrimmer(r);return e.Pipeline.registerFunction(o,"lunr-multi-trimmer-"+i),n.unshift(o),function(){this.pipeline.reset(),this.pipeline.add.apply(this.pipeline,n),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add.apply(this.searchPipeline,s))}}}});
|
||||
-18
File diff suppressed because one or more lines are too long
-18
@@ -1,18 +0,0 @@
|
||||
/*!
|
||||
* Lunr languages, `Norwegian` language
|
||||
* https://github.com/MihaiValentin/lunr-languages
|
||||
*
|
||||
* Copyright 2014, Mihai Valentin
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
/*!
|
||||
* based on
|
||||
* Snowball JavaScript Library v0.3
|
||||
* http://code.google.com/p/urim/
|
||||
* http://snowball.tartarus.org/
|
||||
*
|
||||
* Copyright 2010, Oleg Mazko
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.no=function(){this.pipeline.reset(),this.pipeline.add(e.no.trimmer,e.no.stopWordFilter,e.no.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.no.stemmer))},e.no.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.no.trimmer=e.trimmerSupport.generateTrimmer(e.no.wordCharacters),e.Pipeline.registerFunction(e.no.trimmer,"trimmer-no"),e.no.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,i=new function(){function e(){var e,r=w.cursor+3;if(a=w.limit,0<=r||r<=w.limit){for(s=r;;){if(e=w.cursor,w.in_grouping(d,97,248)){w.cursor=e;break}if(e>=w.limit)return;w.cursor=e+1}for(;!w.out_grouping(d,97,248);){if(w.cursor>=w.limit)return;w.cursor++}a=w.cursor,a<s&&(a=s)}}function i(){var e,r,n;if(w.cursor>=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(m,29),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:n=w.limit-w.cursor,w.in_grouping_b(c,98,122)?w.slice_del():(w.cursor=w.limit-n,w.eq_s_b(1,"k")&&w.out_grouping_b(d,97,248)&&w.slice_del());break;case 3:w.slice_from("er")}}function t(){var e,r=w.limit-w.cursor;w.cursor>=a&&(e=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,w.find_among_b(u,2)?(w.bra=w.cursor,w.limit_backward=e,w.cursor=w.limit-r,w.cursor>w.limit_backward&&(w.cursor--,w.bra=w.cursor,w.slice_del())):w.limit_backward=e)}function o(){var e,r;w.cursor>=a&&(r=w.limit_backward,w.limit_backward=a,w.ket=w.cursor,e=w.find_among_b(l,11),e?(w.bra=w.cursor,w.limit_backward=r,1==e&&w.slice_del()):w.limit_backward=r)}var s,a,m=[new r("a",-1,1),new r("e",-1,1),new r("ede",1,1),new r("ande",1,1),new r("ende",1,1),new r("ane",1,1),new r("ene",1,1),new r("hetene",6,1),new r("erte",1,3),new r("en",-1,1),new r("heten",9,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",12,1),new r("s",-1,2),new r("as",14,1),new r("es",14,1),new r("edes",16,1),new r("endes",16,1),new r("enes",16,1),new r("hetenes",19,1),new r("ens",14,1),new r("hetens",21,1),new r("ers",14,1),new r("ets",14,1),new r("et",-1,1),new r("het",25,1),new r("ert",-1,3),new r("ast",-1,1)],u=[new r("dt",-1,-1),new r("vt",-1,-1)],l=[new r("leg",-1,1),new r("eleg",0,1),new r("ig",-1,1),new r("eig",2,1),new r("lig",2,1),new r("elig",4,1),new r("els",-1,1),new r("lov",-1,1),new r("elov",7,1),new r("slov",7,1),new r("hetslov",9,1)],d=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,48,0,128],c=[119,125,149,1],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,i(),w.cursor=w.limit,t(),w.cursor=w.limit,o(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return i.setCurrent(e),i.stem(),i.getCurrent()}):(i.setCurrent(e),i.stem(),i.getCurrent())}}(),e.Pipeline.registerFunction(e.no.stemmer,"stemmer-no"),e.no.stopWordFilter=e.generateStopWordFilter("alle at av bare begge ble blei bli blir blitt både båe da de deg dei deim deira deires dem den denne der dere deres det dette di din disse ditt du dykk dykkar då eg ein eit eitt eller elles en enn er et ett etter for fordi fra før ha hadde han hans har hennar henne hennes her hjå ho hoe honom hoss hossen hun hva hvem hver hvilke hvilken hvis hvor hvordan hvorfor i ikke ikkje ikkje ingen ingi inkje inn inni ja jeg kan kom korleis korso kun kunne kva kvar kvarhelst kven kvi kvifor man mange me med medan meg meget mellom men mi min mine mitt mot mykje ned no noe noen noka noko nokon nokor nokre nå når og også om opp oss over på samme seg selv si si sia sidan siden sin sine sitt sjøl skal skulle slik so som som somme somt så sånn til um upp ut uten var vart varte ved vere verte vi vil ville vore vors vort vår være være vært å".split(" ")),e.Pipeline.registerFunction(e.no.stopWordFilter,"stopWordFilter-no")}});
|
||||
-18
File diff suppressed because one or more lines are too long
-18
File diff suppressed because one or more lines are too long
-18
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.sa=function(){this.pipeline.reset(),this.pipeline.add(e.sa.trimmer,e.sa.stopWordFilter,e.sa.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.sa.stemmer))},e.sa.wordCharacters="ऀ-ःऄ-एऐ-टठ-यर-िी-ॏॐ-य़ॠ-९॰-ॿ꣠-꣱ꣲ-ꣷ꣸-ꣻ꣼-ꣽꣾ-ꣿᆰ0-ᆰ9",e.sa.trimmer=e.trimmerSupport.generateTrimmer(e.sa.wordCharacters),e.Pipeline.registerFunction(e.sa.trimmer,"trimmer-sa"),e.sa.stopWordFilter=e.generateStopWordFilter('तथा अयम् एकम् इत्यस्मिन् तथा तत् वा अयम् इत्यस्य ते आहूत उपरि तेषाम् किन्तु तेषाम् तदा इत्यनेन अधिकः इत्यस्य तत् केचन बहवः द्वि तथा महत्वपूर्णः अयम् अस्य विषये अयं अस्ति तत् प्रथमः विषये इत्युपरि इत्युपरि इतर अधिकतमः अधिकः अपि सामान्यतया ठ इतरेतर नूतनम् द न्यूनम् कश्चित् वा विशालः द सः अस्ति तदनुसारम् तत्र अस्ति केवलम् अपि अत्र सर्वे विविधाः तत् बहवः यतः इदानीम् द दक्षिण इत्यस्मै तस्य उपरि नथ अतीव कार्यम् सर्वे एकैकम् इत्यादि। एते सन्ति उत इत्थम् मध्ये एतदर्थं . स कस्य प्रथमः श्री. करोति अस्मिन् प्रकारः निर्मिता कालः तत्र कर्तुं समान अधुना ते सन्ति स एकः अस्ति सः अर्थात् तेषां कृते . स्थितम् विशेषः अग्रिम तेषाम् समान स्रोतः ख म समान इदानीमपि अधिकतया करोतु ते समान इत्यस्य वीथी सह यस्मिन् कृतवान् धृतः तदा पुनः पूर्वं सः आगतः किम् कुल इतर पुरा मात्रा स विषये उ अतएव अपि नगरस्य उपरि यतः प्रतिशतं कतरः कालः साधनानि भूत तथापि जात सम्बन्धि अन्यत् ग अतः अस्माकं स्वकीयाः अस्माकं इदानीं अन्तः इत्यादयः भवन्तः इत्यादयः एते एताः तस्य अस्य इदम् एते तेषां तेषां तेषां तान् तेषां तेषां तेषां समानः सः एकः च तादृशाः बहवः अन्ये च वदन्ति यत् कियत् कस्मै कस्मै यस्मै यस्मै यस्मै यस्मै न अतिनीचः किन्तु प्रथमं सम्पूर्णतया ततः चिरकालानन्तरं पुस्तकं सम्पूर्णतया अन्तः किन्तु अत्र वा इह इव श्रद्धाय अवशिष्यते परन्तु अन्ये वर्गाः सन्ति ते सन्ति शक्नुवन्ति सर्वे मिलित्वा सर्वे एकत्र"'.split(" ")),e.sa.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var r=e.wordcut;r.init(),e.sa.tokenizer=function(t){if(!arguments.length||null==t||void 0==t)return[];if(Array.isArray(t))return t.map(function(r){return isLunr2?new e.Token(r.toLowerCase()):r.toLowerCase()});var i=t.toString().toLowerCase().replace(/^\s+/,"");return r.cut(i).split("|")},e.Pipeline.registerFunction(e.sa.stemmer,"stemmer-sa"),e.Pipeline.registerFunction(e.sa.stopWordFilter,"stopWordFilter-sa")}});
|
||||
@@ -1 +0,0 @@
|
||||
!function(r,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(r.lunr)}(this,function(){return function(r){r.stemmerSupport={Among:function(r,t,i,s){if(this.toCharArray=function(r){for(var t=r.length,i=new Array(t),s=0;s<t;s++)i[s]=r.charCodeAt(s);return i},!r&&""!=r||!t&&0!=t||!i)throw"Bad Among initialisation: s:"+r+", substring_i: "+t+", result: "+i;this.s_size=r.length,this.s=this.toCharArray(r),this.substring_i=t,this.result=i,this.method=s},SnowballProgram:function(){var r;return{bra:0,ket:0,limit:0,cursor:0,limit_backward:0,setCurrent:function(t){r=t,this.cursor=0,this.limit=t.length,this.limit_backward=0,this.bra=this.cursor,this.ket=this.limit},getCurrent:function(){var t=r;return r=null,t},in_grouping:function(t,i,s){if(this.cursor<this.limit){var e=r.charCodeAt(this.cursor);if(e<=s&&e>=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor++,!0}return!1},in_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e<=s&&e>=i&&(e-=i,t[e>>3]&1<<(7&e)))return this.cursor--,!0}return!1},out_grouping:function(t,i,s){if(this.cursor<this.limit){var e=r.charCodeAt(this.cursor);if(e>s||e<i)return this.cursor++,!0;if(e-=i,!(t[e>>3]&1<<(7&e)))return this.cursor++,!0}return!1},out_grouping_b:function(t,i,s){if(this.cursor>this.limit_backward){var e=r.charCodeAt(this.cursor-1);if(e>s||e<i)return this.cursor--,!0;if(e-=i,!(t[e>>3]&1<<(7&e)))return this.cursor--,!0}return!1},eq_s:function(t,i){if(this.limit-this.cursor<t)return!1;for(var s=0;s<t;s++)if(r.charCodeAt(this.cursor+s)!=i.charCodeAt(s))return!1;return this.cursor+=t,!0},eq_s_b:function(t,i){if(this.cursor-this.limit_backward<t)return!1;for(var s=0;s<t;s++)if(r.charCodeAt(this.cursor-t+s)!=i.charCodeAt(s))return!1;return this.cursor-=t,!0},find_among:function(t,i){for(var s=0,e=i,n=this.cursor,u=this.limit,o=0,h=0,c=!1;;){for(var a=s+(e-s>>1),f=0,l=o<h?o:h,_=t[a],m=l;m<_.s_size;m++){if(n+l==u){f=-1;break}if(f=r.charCodeAt(n+l)-_.s[m])break;l++}if(f<0?(e=a,h=l):(s=a,o=l),e-s<=1){if(s>0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n+_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n+_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},find_among_b:function(t,i){for(var s=0,e=i,n=this.cursor,u=this.limit_backward,o=0,h=0,c=!1;;){for(var a=s+(e-s>>1),f=0,l=o<h?o:h,_=t[a],m=_.s_size-1-l;m>=0;m--){if(n-l==u){f=-1;break}if(f=r.charCodeAt(n-1-l)-_.s[m])break;l++}if(f<0?(e=a,h=l):(s=a,o=l),e-s<=1){if(s>0||e==s||c)break;c=!0}}for(;;){var _=t[s];if(o>=_.s_size){if(this.cursor=n-_.s_size,!_.method)return _.result;var b=_.method();if(this.cursor=n-_.s_size,b)return _.result}if((s=_.substring_i)<0)return 0}},replace_s:function(t,i,s){var e=s.length-(i-t),n=r.substring(0,t),u=r.substring(i);return r=n+s+u,this.limit+=e,this.cursor>=i?this.cursor+=e:this.cursor>t&&(this.cursor=t),e},slice_check:function(){if(this.bra<0||this.bra>this.ket||this.ket>this.limit||this.limit>r.length)throw"faulty slice operation"},slice_from:function(r){this.slice_check(),this.replace_s(this.bra,this.ket,r)},slice_del:function(){this.slice_from("")},insert:function(r,t,i){var s=this.replace_s(r,t,i);r<=this.bra&&(this.bra+=s),r<=this.ket&&(this.ket+=s)},slice_to:function(){return this.slice_check(),r.substring(this.bra,this.ket)},eq_v_b:function(r){return this.eq_s_b(r.length,r)}}}},r.trimmerSupport={generateTrimmer:function(r){var t=new RegExp("^[^"+r+"]+"),i=new RegExp("[^"+r+"]+$");return function(r){return"function"==typeof r.update?r.update(function(r){return r.replace(t,"").replace(i,"")}):r.replace(t,"").replace(i,"")}}}}});
|
||||
-18
@@ -1,18 +0,0 @@
|
||||
/*!
|
||||
* Lunr languages, `Swedish` language
|
||||
* https://github.com/MihaiValentin/lunr-languages
|
||||
*
|
||||
* Copyright 2014, Mihai Valentin
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
/*!
|
||||
* based on
|
||||
* Snowball JavaScript Library v0.3
|
||||
* http://code.google.com/p/urim/
|
||||
* http://snowball.tartarus.org/
|
||||
*
|
||||
* Copyright 2010, Oleg Mazko
|
||||
* http://www.mozilla.org/MPL/
|
||||
*/
|
||||
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.sv=function(){this.pipeline.reset(),this.pipeline.add(e.sv.trimmer,e.sv.stopWordFilter,e.sv.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.sv.stemmer))},e.sv.wordCharacters="A-Za-zªºÀ-ÖØ-öø-ʸˠ-ˤᴀ-ᴥᴬ-ᵜᵢ-ᵥᵫ-ᵷᵹ-ᶾḀ-ỿⁱⁿₐ-ₜKÅℲⅎⅠ-ↈⱠ-ⱿꜢ-ꞇꞋ-ꞭꞰ-ꞷꟷ-ꟿꬰ-ꭚꭜ-ꭤff-stA-Za-z",e.sv.trimmer=e.trimmerSupport.generateTrimmer(e.sv.wordCharacters),e.Pipeline.registerFunction(e.sv.trimmer,"trimmer-sv"),e.sv.stemmer=function(){var r=e.stemmerSupport.Among,n=e.stemmerSupport.SnowballProgram,t=new function(){function e(){var e,r=w.cursor+3;if(o=w.limit,0<=r||r<=w.limit){for(a=r;;){if(e=w.cursor,w.in_grouping(l,97,246)){w.cursor=e;break}if(w.cursor=e,w.cursor>=w.limit)return;w.cursor++}for(;!w.out_grouping(l,97,246);){if(w.cursor>=w.limit)return;w.cursor++}o=w.cursor,o<a&&(o=a)}}function t(){var e,r=w.limit_backward;if(w.cursor>=o&&(w.limit_backward=o,w.cursor=w.limit,w.ket=w.cursor,e=w.find_among_b(u,37),w.limit_backward=r,e))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:w.in_grouping_b(d,98,121)&&w.slice_del()}}function i(){var e=w.limit_backward;w.cursor>=o&&(w.limit_backward=o,w.cursor=w.limit,w.find_among_b(c,7)&&(w.cursor=w.limit,w.ket=w.cursor,w.cursor>w.limit_backward&&(w.bra=--w.cursor,w.slice_del())),w.limit_backward=e)}function s(){var e,r;if(w.cursor>=o){if(r=w.limit_backward,w.limit_backward=o,w.cursor=w.limit,w.ket=w.cursor,e=w.find_among_b(m,5))switch(w.bra=w.cursor,e){case 1:w.slice_del();break;case 2:w.slice_from("lös");break;case 3:w.slice_from("full")}w.limit_backward=r}}var a,o,u=[new r("a",-1,1),new r("arna",0,1),new r("erna",0,1),new r("heterna",2,1),new r("orna",0,1),new r("ad",-1,1),new r("e",-1,1),new r("ade",6,1),new r("ande",6,1),new r("arne",6,1),new r("are",6,1),new r("aste",6,1),new r("en",-1,1),new r("anden",12,1),new r("aren",12,1),new r("heten",12,1),new r("ern",-1,1),new r("ar",-1,1),new r("er",-1,1),new r("heter",18,1),new r("or",-1,1),new r("s",-1,2),new r("as",21,1),new r("arnas",22,1),new r("ernas",22,1),new r("ornas",22,1),new r("es",21,1),new r("ades",26,1),new r("andes",26,1),new r("ens",21,1),new r("arens",29,1),new r("hetens",29,1),new r("erns",21,1),new r("at",-1,1),new r("andet",-1,1),new r("het",-1,1),new r("ast",-1,1)],c=[new r("dd",-1,-1),new r("gd",-1,-1),new r("nn",-1,-1),new r("dt",-1,-1),new r("gt",-1,-1),new r("kt",-1,-1),new r("tt",-1,-1)],m=[new r("ig",-1,1),new r("lig",0,1),new r("els",-1,1),new r("fullt",-1,3),new r("löst",-1,2)],l=[17,65,16,1,0,0,0,0,0,0,0,0,0,0,0,0,24,0,32],d=[119,127,149],w=new n;this.setCurrent=function(e){w.setCurrent(e)},this.getCurrent=function(){return w.getCurrent()},this.stem=function(){var r=w.cursor;return e(),w.limit_backward=r,w.cursor=w.limit,t(),w.cursor=w.limit,i(),w.cursor=w.limit,s(),!0}};return function(e){return"function"==typeof e.update?e.update(function(e){return t.setCurrent(e),t.stem(),t.getCurrent()}):(t.setCurrent(e),t.stem(),t.getCurrent())}}(),e.Pipeline.registerFunction(e.sv.stemmer,"stemmer-sv"),e.sv.stopWordFilter=e.generateStopWordFilter("alla allt att av blev bli blir blivit de dem den denna deras dess dessa det detta dig din dina ditt du där då efter ej eller en er era ert ett från för ha hade han hans har henne hennes hon honom hur här i icke ingen inom inte jag ju kan kunde man med mellan men mig min mina mitt mot mycket ni nu när någon något några och om oss på samma sedan sig sin sina sitta själv skulle som så sådan sådana sådant till under upp ut utan vad var vara varför varit varje vars vart vem vi vid vilka vilkas vilken vilket vår våra vårt än är åt över".split(" ")),e.Pipeline.registerFunction(e.sv.stopWordFilter,"stopWordFilter-sv")}});
|
||||
-1
@@ -1 +0,0 @@
|
||||
!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.ta=function(){this.pipeline.reset(),this.pipeline.add(e.ta.trimmer,e.ta.stopWordFilter,e.ta.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.ta.stemmer))},e.ta.wordCharacters="-உஊ-ஏஐ-ஙச-ட-னப-யர-ஹ-ிீ-ொ-ௐ---௩௪-௯௰-௹௺-a-zA-Za-zA-Z0-90-9",e.ta.trimmer=e.trimmerSupport.generateTrimmer(e.ta.wordCharacters),e.Pipeline.registerFunction(e.ta.trimmer,"trimmer-ta"),e.ta.stopWordFilter=e.generateStopWordFilter("அங்கு அங்கே அது அதை அந்த அவர் அவர்கள் அவள் அவன் அவை ஆக ஆகவே ஆகையால் ஆதலால் ஆதலினால் ஆனாலும் ஆனால் இங்கு இங்கே இது இதை இந்த இப்படி இவர் இவர்கள் இவள் இவன் இவை இவ்வளவு உனக்கு உனது உன் உன்னால் எங்கு எங்கே எது எதை எந்த எப்படி எவர் எவர்கள் எவள் எவன் எவை எவ்வளவு எனக்கு எனது எனவே என் என்ன என்னால் ஏது ஏன் தனது தன்னால் தானே தான் நாங்கள் நாம் நான் நீ நீங்கள்".split(" ")),e.ta.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var t=e.wordcut;t.init(),e.ta.tokenizer=function(r){if(!arguments.length||null==r||void 0==r)return[];if(Array.isArray(r))return r.map(function(t){return isLunr2?new e.Token(t.toLowerCase()):t.toLowerCase()});var i=r.toString().toLowerCase().replace(/^\s+/,"");return t.cut(i).split("|")},e.Pipeline.registerFunction(e.ta.stemmer,"stemmer-ta"),e.Pipeline.registerFunction(e.ta.stopWordFilter,"stopWordFilter-ta")}});
|
||||
-1
@@ -1 +0,0 @@
|
||||
!function(e,t){"function"==typeof define&&define.amd?define(t):"object"==typeof exports?module.exports=t():t()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.te=function(){this.pipeline.reset(),this.pipeline.add(e.te.trimmer,e.te.stopWordFilter,e.te.stemmer),this.searchPipeline&&(this.searchPipeline.reset(),this.searchPipeline.add(e.te.stemmer))},e.te.wordCharacters="ఀ-ఄఅ-ఔక-హా-ౌౕ-ౖౘ-ౚౠ-ౡౢ-ౣ౦-౯౸-౿఼ఽ్ౝ౷",e.te.trimmer=e.trimmerSupport.generateTrimmer(e.te.wordCharacters),e.Pipeline.registerFunction(e.te.trimmer,"trimmer-te"),e.te.stopWordFilter=e.generateStopWordFilter("అందరూ అందుబాటులో అడగండి అడగడం అడ్డంగా అనుగుణంగా అనుమతించు అనుమతిస్తుంది అయితే ఇప్పటికే ఉన్నారు ఎక్కడైనా ఎప్పుడు ఎవరైనా ఎవరో ఏ ఏదైనా ఏమైనప్పటికి ఒక ఒకరు కనిపిస్తాయి కాదు కూడా గా గురించి చుట్టూ చేయగలిగింది తగిన తర్వాత దాదాపు దూరంగా నిజంగా పై ప్రకారం ప్రక్కన మధ్య మరియు మరొక మళ్ళీ మాత్రమే మెచ్చుకో వద్ద వెంట వేరుగా వ్యతిరేకంగా సంబంధం".split(" ")),e.te.stemmer=function(){return function(e){return"function"==typeof e.update?e.update(function(e){return e}):e}}();var t=e.wordcut;t.init(),e.te.tokenizer=function(r){if(!arguments.length||null==r||void 0==r)return[];if(Array.isArray(r))return r.map(function(t){return isLunr2?new e.Token(t.toLowerCase()):t.toLowerCase()});var i=r.toString().toLowerCase().replace(/^\s+/,"");return t.cut(i).split("|")},e.Pipeline.registerFunction(e.te.stemmer,"stemmer-te"),e.Pipeline.registerFunction(e.te.stopWordFilter,"stopWordFilter-te")}});
|
||||
-1
@@ -1 +0,0 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var r="2"==e.version[0];e.th=function(){this.pipeline.reset(),this.pipeline.add(e.th.trimmer),r?this.tokenizer=e.th.tokenizer:(e.tokenizer&&(e.tokenizer=e.th.tokenizer),this.tokenizerFn&&(this.tokenizerFn=e.th.tokenizer))},e.th.wordCharacters="[-]",e.th.trimmer=e.trimmerSupport.generateTrimmer(e.th.wordCharacters),e.Pipeline.registerFunction(e.th.trimmer,"trimmer-th");var t=e.wordcut;t.init(),e.th.tokenizer=function(i){if(!arguments.length||null==i||void 0==i)return[];if(Array.isArray(i))return i.map(function(t){return r?new e.Token(t):t});var n=i.toString().replace(/^\s+/,"");return t.cut(n).split("|")}}});
|
||||
-18
File diff suppressed because one or more lines are too long
-1
@@ -1 +0,0 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r():r()(e.lunr)}(this,function(){return function(e){if(void 0===e)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===e.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");e.vi=function(){this.pipeline.reset(),this.pipeline.add(e.vi.stopWordFilter,e.vi.trimmer)},e.vi.wordCharacters="[A-Za-ẓ̀͐́͑̉̃̓ÂâÊêÔôĂ-ăĐ-đƠ-ơƯ-ư]",e.vi.trimmer=e.trimmerSupport.generateTrimmer(e.vi.wordCharacters),e.Pipeline.registerFunction(e.vi.trimmer,"trimmer-vi"),e.vi.stopWordFilter=e.generateStopWordFilter("là cái nhưng mà".split(" "))}});
|
||||
-1
@@ -1 +0,0 @@
|
||||
!function(e,r){"function"==typeof define&&define.amd?define(r):"object"==typeof exports?module.exports=r(require("@node-rs/jieba")):r()(e.lunr)}(this,function(e){return function(r,t){if(void 0===r)throw new Error("Lunr is not present. Please include / require Lunr before this script.");if(void 0===r.stemmerSupport)throw new Error("Lunr stemmer support is not present. Please include / require Lunr stemmer support before this script.");var i="2"==r.version[0];r.zh=function(){this.pipeline.reset(),this.pipeline.add(r.zh.trimmer,r.zh.stopWordFilter,r.zh.stemmer),i?this.tokenizer=r.zh.tokenizer:(r.tokenizer&&(r.tokenizer=r.zh.tokenizer),this.tokenizerFn&&(this.tokenizerFn=r.zh.tokenizer))},r.zh.tokenizer=function(n){if(!arguments.length||null==n||void 0==n)return[];if(Array.isArray(n))return n.map(function(e){return i?new r.Token(e.toLowerCase()):e.toLowerCase()});t&&e.load(t);var o=n.toString().trim().toLowerCase(),s=[];e.cut(o,!0).forEach(function(e){s=s.concat(e.split(" "))}),s=s.filter(function(e){return!!e});var u=0;return s.map(function(e,t){if(i){var n=o.indexOf(e,u),s={};return s.position=[n,e.length],s.index=t,u=n,new r.Token(e,s)}return e})},r.zh.wordCharacters="\\w一-龥",r.zh.trimmer=r.trimmerSupport.generateTrimmer(r.zh.wordCharacters),r.Pipeline.registerFunction(r.zh.trimmer,"trimmer-zh"),r.zh.stemmer=function(){return function(e){return e}}(),r.Pipeline.registerFunction(r.zh.stemmer,"stemmer-zh"),r.zh.stopWordFilter=r.generateStopWordFilter("的 一 不 在 人 有 是 为 為 以 于 於 上 他 而 后 後 之 来 來 及 了 因 下 可 到 由 这 這 与 與 也 此 但 并 並 个 個 其 已 无 無 小 我 们 們 起 最 再 今 去 好 只 又 或 很 亦 某 把 那 你 乃 它 吧 被 比 别 趁 当 當 从 從 得 打 凡 儿 兒 尔 爾 该 該 各 给 給 跟 和 何 还 還 即 几 幾 既 看 据 據 距 靠 啦 另 么 麽 每 嘛 拿 哪 您 凭 憑 且 却 卻 让 讓 仍 啥 如 若 使 谁 誰 虽 雖 随 隨 同 所 她 哇 嗡 往 些 向 沿 哟 喲 用 咱 则 則 怎 曾 至 致 着 著 诸 諸 自".split(" ")),r.Pipeline.registerFunction(r.zh.stopWordFilter,"stopWordFilter-zh")}});
|
||||
@@ -1,206 +0,0 @@
|
||||
/**
|
||||
* export the module via AMD, CommonJS or as a browser global
|
||||
* Export code from https://github.com/umdjs/umd/blob/master/returnExports.js
|
||||
*/
|
||||
;(function (root, factory) {
|
||||
if (typeof define === 'function' && define.amd) {
|
||||
// AMD. Register as an anonymous module.
|
||||
define(factory)
|
||||
} else if (typeof exports === 'object') {
|
||||
/**
|
||||
* Node. Does not work with strict CommonJS, but
|
||||
* only CommonJS-like environments that support module.exports,
|
||||
* like Node.
|
||||
*/
|
||||
module.exports = factory()
|
||||
} else {
|
||||
// Browser globals (root is window)
|
||||
factory()(root.lunr);
|
||||
}
|
||||
}(this, function () {
|
||||
/**
|
||||
* Just return a value to define the module export.
|
||||
* This example returns an object, but the module
|
||||
* can return a function as the exported value.
|
||||
*/
|
||||
|
||||
return function(lunr) {
|
||||
// TinySegmenter 0.1 -- Super compact Japanese tokenizer in Javascript
|
||||
// (c) 2008 Taku Kudo <taku@chasen.org>
|
||||
// TinySegmenter is freely distributable under the terms of a new BSD licence.
|
||||
// For details, see http://chasen.org/~taku/software/TinySegmenter/LICENCE.txt
|
||||
|
||||
function TinySegmenter() {
|
||||
var patterns = {
|
||||
"[一二三四五六七八九十百千万億兆]":"M",
|
||||
"[一-龠々〆ヵヶ]":"H",
|
||||
"[ぁ-ん]":"I",
|
||||
"[ァ-ヴーア-ン゙ー]":"K",
|
||||
"[a-zA-Za-zA-Z]":"A",
|
||||
"[0-90-9]":"N"
|
||||
}
|
||||
this.chartype_ = [];
|
||||
for (var i in patterns) {
|
||||
var regexp = new RegExp(i);
|
||||
this.chartype_.push([regexp, patterns[i]]);
|
||||
}
|
||||
|
||||
this.BIAS__ = -332
|
||||
this.BC1__ = {"HH":6,"II":2461,"KH":406,"OH":-1378};
|
||||
this.BC2__ = {"AA":-3267,"AI":2744,"AN":-878,"HH":-4070,"HM":-1711,"HN":4012,"HO":3761,"IA":1327,"IH":-1184,"II":-1332,"IK":1721,"IO":5492,"KI":3831,"KK":-8741,"MH":-3132,"MK":3334,"OO":-2920};
|
||||
this.BC3__ = {"HH":996,"HI":626,"HK":-721,"HN":-1307,"HO":-836,"IH":-301,"KK":2762,"MK":1079,"MM":4034,"OA":-1652,"OH":266};
|
||||
this.BP1__ = {"BB":295,"OB":304,"OO":-125,"UB":352};
|
||||
this.BP2__ = {"BO":60,"OO":-1762};
|
||||
this.BQ1__ = {"BHH":1150,"BHM":1521,"BII":-1158,"BIM":886,"BMH":1208,"BNH":449,"BOH":-91,"BOO":-2597,"OHI":451,"OIH":-296,"OKA":1851,"OKH":-1020,"OKK":904,"OOO":2965};
|
||||
this.BQ2__ = {"BHH":118,"BHI":-1159,"BHM":466,"BIH":-919,"BKK":-1720,"BKO":864,"OHH":-1139,"OHM":-181,"OIH":153,"UHI":-1146};
|
||||
this.BQ3__ = {"BHH":-792,"BHI":2664,"BII":-299,"BKI":419,"BMH":937,"BMM":8335,"BNN":998,"BOH":775,"OHH":2174,"OHM":439,"OII":280,"OKH":1798,"OKI":-793,"OKO":-2242,"OMH":-2402,"OOO":11699};
|
||||
this.BQ4__ = {"BHH":-3895,"BIH":3761,"BII":-4654,"BIK":1348,"BKK":-1806,"BMI":-3385,"BOO":-12396,"OAH":926,"OHH":266,"OHK":-2036,"ONN":-973};
|
||||
this.BW1__ = {",と":660,",同":727,"B1あ":1404,"B1同":542,"、と":660,"、同":727,"」と":1682,"あっ":1505,"いう":1743,"いっ":-2055,"いる":672,"うし":-4817,"うん":665,"から":3472,"がら":600,"こう":-790,"こと":2083,"こん":-1262,"さら":-4143,"さん":4573,"した":2641,"して":1104,"すで":-3399,"そこ":1977,"それ":-871,"たち":1122,"ため":601,"った":3463,"つい":-802,"てい":805,"てき":1249,"でき":1127,"です":3445,"では":844,"とい":-4915,"とみ":1922,"どこ":3887,"ない":5713,"なっ":3015,"など":7379,"なん":-1113,"にし":2468,"には":1498,"にも":1671,"に対":-912,"の一":-501,"の中":741,"ませ":2448,"まで":1711,"まま":2600,"まる":-2155,"やむ":-1947,"よっ":-2565,"れた":2369,"れで":-913,"をし":1860,"を見":731,"亡く":-1886,"京都":2558,"取り":-2784,"大き":-2604,"大阪":1497,"平方":-2314,"引き":-1336,"日本":-195,"本当":-2423,"毎日":-2113,"目指":-724,"B1あ":1404,"B1同":542,"」と":1682};
|
||||
this.BW2__ = {"..":-11822,"11":-669,"――":-5730,"−−":-13175,"いう":-1609,"うか":2490,"かし":-1350,"かも":-602,"から":-7194,"かれ":4612,"がい":853,"がら":-3198,"きた":1941,"くな":-1597,"こと":-8392,"この":-4193,"させ":4533,"され":13168,"さん":-3977,"しい":-1819,"しか":-545,"した":5078,"して":972,"しな":939,"その":-3744,"たい":-1253,"たた":-662,"ただ":-3857,"たち":-786,"たと":1224,"たは":-939,"った":4589,"って":1647,"っと":-2094,"てい":6144,"てき":3640,"てく":2551,"ては":-3110,"ても":-3065,"でい":2666,"でき":-1528,"でし":-3828,"です":-4761,"でも":-4203,"とい":1890,"とこ":-1746,"とと":-2279,"との":720,"とみ":5168,"とも":-3941,"ない":-2488,"なが":-1313,"など":-6509,"なの":2614,"なん":3099,"にお":-1615,"にし":2748,"にな":2454,"によ":-7236,"に対":-14943,"に従":-4688,"に関":-11388,"のか":2093,"ので":-7059,"のに":-6041,"のの":-6125,"はい":1073,"はが":-1033,"はず":-2532,"ばれ":1813,"まし":-1316,"まで":-6621,"まれ":5409,"めて":-3153,"もい":2230,"もの":-10713,"らか":-944,"らし":-1611,"らに":-1897,"りし":651,"りま":1620,"れた":4270,"れて":849,"れば":4114,"ろう":6067,"われ":7901,"を通":-11877,"んだ":728,"んな":-4115,"一人":602,"一方":-1375,"一日":970,"一部":-1051,"上が":-4479,"会社":-1116,"出て":2163,"分の":-7758,"同党":970,"同日":-913,"大阪":-2471,"委員":-1250,"少な":-1050,"年度":-8669,"年間":-1626,"府県":-2363,"手権":-1982,"新聞":-4066,"日新":-722,"日本":-7068,"日米":3372,"曜日":-601,"朝鮮":-2355,"本人":-2697,"東京":-1543,"然と":-1384,"社会":-1276,"立て":-990,"第に":-1612,"米国":-4268,"11":-669};
|
||||
this.BW3__ = {"あた":-2194,"あり":719,"ある":3846,"い.":-1185,"い。":-1185,"いい":5308,"いえ":2079,"いく":3029,"いた":2056,"いっ":1883,"いる":5600,"いわ":1527,"うち":1117,"うと":4798,"えと":1454,"か.":2857,"か。":2857,"かけ":-743,"かっ":-4098,"かに":-669,"から":6520,"かり":-2670,"が,":1816,"が、":1816,"がき":-4855,"がけ":-1127,"がっ":-913,"がら":-4977,"がり":-2064,"きた":1645,"けど":1374,"こと":7397,"この":1542,"ころ":-2757,"さい":-714,"さを":976,"し,":1557,"し、":1557,"しい":-3714,"した":3562,"して":1449,"しな":2608,"しま":1200,"す.":-1310,"す。":-1310,"する":6521,"ず,":3426,"ず、":3426,"ずに":841,"そう":428,"た.":8875,"た。":8875,"たい":-594,"たの":812,"たり":-1183,"たる":-853,"だ.":4098,"だ。":4098,"だっ":1004,"った":-4748,"って":300,"てい":6240,"てお":855,"ても":302,"です":1437,"でに":-1482,"では":2295,"とう":-1387,"とし":2266,"との":541,"とも":-3543,"どう":4664,"ない":1796,"なく":-903,"など":2135,"に,":-1021,"に、":-1021,"にし":1771,"にな":1906,"には":2644,"の,":-724,"の、":-724,"の子":-1000,"は,":1337,"は、":1337,"べき":2181,"まし":1113,"ます":6943,"まっ":-1549,"まで":6154,"まれ":-793,"らし":1479,"られ":6820,"るる":3818,"れ,":854,"れ、":854,"れた":1850,"れて":1375,"れば":-3246,"れる":1091,"われ":-605,"んだ":606,"んで":798,"カ月":990,"会議":860,"入り":1232,"大会":2217,"始め":1681,"市":965,"新聞":-5055,"日,":974,"日、":974,"社会":2024,"カ月":990};
|
||||
this.TC1__ = {"AAA":1093,"HHH":1029,"HHM":580,"HII":998,"HOH":-390,"HOM":-331,"IHI":1169,"IOH":-142,"IOI":-1015,"IOM":467,"MMH":187,"OOI":-1832};
|
||||
this.TC2__ = {"HHO":2088,"HII":-1023,"HMM":-1154,"IHI":-1965,"KKH":703,"OII":-2649};
|
||||
this.TC3__ = {"AAA":-294,"HHH":346,"HHI":-341,"HII":-1088,"HIK":731,"HOH":-1486,"IHH":128,"IHI":-3041,"IHO":-1935,"IIH":-825,"IIM":-1035,"IOI":-542,"KHH":-1216,"KKA":491,"KKH":-1217,"KOK":-1009,"MHH":-2694,"MHM":-457,"MHO":123,"MMH":-471,"NNH":-1689,"NNO":662,"OHO":-3393};
|
||||
this.TC4__ = {"HHH":-203,"HHI":1344,"HHK":365,"HHM":-122,"HHN":182,"HHO":669,"HIH":804,"HII":679,"HOH":446,"IHH":695,"IHO":-2324,"IIH":321,"III":1497,"IIO":656,"IOO":54,"KAK":4845,"KKA":3386,"KKK":3065,"MHH":-405,"MHI":201,"MMH":-241,"MMM":661,"MOM":841};
|
||||
this.TQ1__ = {"BHHH":-227,"BHHI":316,"BHIH":-132,"BIHH":60,"BIII":1595,"BNHH":-744,"BOHH":225,"BOOO":-908,"OAKK":482,"OHHH":281,"OHIH":249,"OIHI":200,"OIIH":-68};
|
||||
this.TQ2__ = {"BIHH":-1401,"BIII":-1033,"BKAK":-543,"BOOO":-5591};
|
||||
this.TQ3__ = {"BHHH":478,"BHHM":-1073,"BHIH":222,"BHII":-504,"BIIH":-116,"BIII":-105,"BMHI":-863,"BMHM":-464,"BOMH":620,"OHHH":346,"OHHI":1729,"OHII":997,"OHMH":481,"OIHH":623,"OIIH":1344,"OKAK":2792,"OKHH":587,"OKKA":679,"OOHH":110,"OOII":-685};
|
||||
this.TQ4__ = {"BHHH":-721,"BHHM":-3604,"BHII":-966,"BIIH":-607,"BIII":-2181,"OAAA":-2763,"OAKK":180,"OHHH":-294,"OHHI":2446,"OHHO":480,"OHIH":-1573,"OIHH":1935,"OIHI":-493,"OIIH":626,"OIII":-4007,"OKAK":-8156};
|
||||
this.TW1__ = {"につい":-4681,"東京都":2026};
|
||||
this.TW2__ = {"ある程":-2049,"いった":-1256,"ころが":-2434,"しょう":3873,"その後":-4430,"だって":-1049,"ていた":1833,"として":-4657,"ともに":-4517,"もので":1882,"一気に":-792,"初めて":-1512,"同時に":-8097,"大きな":-1255,"対して":-2721,"社会党":-3216};
|
||||
this.TW3__ = {"いただ":-1734,"してい":1314,"として":-4314,"につい":-5483,"にとっ":-5989,"に当た":-6247,"ので,":-727,"ので、":-727,"のもの":-600,"れから":-3752,"十二月":-2287};
|
||||
this.TW4__ = {"いう.":8576,"いう。":8576,"からな":-2348,"してい":2958,"たが,":1516,"たが、":1516,"ている":1538,"という":1349,"ました":5543,"ません":1097,"ようと":-4258,"よると":5865};
|
||||
this.UC1__ = {"A":484,"K":93,"M":645,"O":-505};
|
||||
this.UC2__ = {"A":819,"H":1059,"I":409,"M":3987,"N":5775,"O":646};
|
||||
this.UC3__ = {"A":-1370,"I":2311};
|
||||
this.UC4__ = {"A":-2643,"H":1809,"I":-1032,"K":-3450,"M":3565,"N":3876,"O":6646};
|
||||
this.UC5__ = {"H":313,"I":-1238,"K":-799,"M":539,"O":-831};
|
||||
this.UC6__ = {"H":-506,"I":-253,"K":87,"M":247,"O":-387};
|
||||
this.UP1__ = {"O":-214};
|
||||
this.UP2__ = {"B":69,"O":935};
|
||||
this.UP3__ = {"B":189};
|
||||
this.UQ1__ = {"BH":21,"BI":-12,"BK":-99,"BN":142,"BO":-56,"OH":-95,"OI":477,"OK":410,"OO":-2422};
|
||||
this.UQ2__ = {"BH":216,"BI":113,"OK":1759};
|
||||
this.UQ3__ = {"BA":-479,"BH":42,"BI":1913,"BK":-7198,"BM":3160,"BN":6427,"BO":14761,"OI":-827,"ON":-3212};
|
||||
this.UW1__ = {",":156,"、":156,"「":-463,"あ":-941,"う":-127,"が":-553,"き":121,"こ":505,"で":-201,"と":-547,"ど":-123,"に":-789,"の":-185,"は":-847,"も":-466,"や":-470,"よ":182,"ら":-292,"り":208,"れ":169,"を":-446,"ん":-137,"・":-135,"主":-402,"京":-268,"区":-912,"午":871,"国":-460,"大":561,"委":729,"市":-411,"日":-141,"理":361,"生":-408,"県":-386,"都":-718,"「":-463,"・":-135};
|
||||
this.UW2__ = {",":-829,"、":-829,"〇":892,"「":-645,"」":3145,"あ":-538,"い":505,"う":134,"お":-502,"か":1454,"が":-856,"く":-412,"こ":1141,"さ":878,"ざ":540,"し":1529,"す":-675,"せ":300,"そ":-1011,"た":188,"だ":1837,"つ":-949,"て":-291,"で":-268,"と":-981,"ど":1273,"な":1063,"に":-1764,"の":130,"は":-409,"ひ":-1273,"べ":1261,"ま":600,"も":-1263,"や":-402,"よ":1639,"り":-579,"る":-694,"れ":571,"を":-2516,"ん":2095,"ア":-587,"カ":306,"キ":568,"ッ":831,"三":-758,"不":-2150,"世":-302,"中":-968,"主":-861,"事":492,"人":-123,"会":978,"保":362,"入":548,"初":-3025,"副":-1566,"北":-3414,"区":-422,"大":-1769,"天":-865,"太":-483,"子":-1519,"学":760,"実":1023,"小":-2009,"市":-813,"年":-1060,"強":1067,"手":-1519,"揺":-1033,"政":1522,"文":-1355,"新":-1682,"日":-1815,"明":-1462,"最":-630,"朝":-1843,"本":-1650,"東":-931,"果":-665,"次":-2378,"民":-180,"気":-1740,"理":752,"発":529,"目":-1584,"相":-242,"県":-1165,"立":-763,"第":810,"米":509,"自":-1353,"行":838,"西":-744,"見":-3874,"調":1010,"議":1198,"込":3041,"開":1758,"間":-1257,"「":-645,"」":3145,"ッ":831,"ア":-587,"カ":306,"キ":568};
|
||||
this.UW3__ = {",":4889,"1":-800,"−":-1723,"、":4889,"々":-2311,"〇":5827,"」":2670,"〓":-3573,"あ":-2696,"い":1006,"う":2342,"え":1983,"お":-4864,"か":-1163,"が":3271,"く":1004,"け":388,"げ":401,"こ":-3552,"ご":-3116,"さ":-1058,"し":-395,"す":584,"せ":3685,"そ":-5228,"た":842,"ち":-521,"っ":-1444,"つ":-1081,"て":6167,"で":2318,"と":1691,"ど":-899,"な":-2788,"に":2745,"の":4056,"は":4555,"ひ":-2171,"ふ":-1798,"へ":1199,"ほ":-5516,"ま":-4384,"み":-120,"め":1205,"も":2323,"や":-788,"よ":-202,"ら":727,"り":649,"る":5905,"れ":2773,"わ":-1207,"を":6620,"ん":-518,"ア":551,"グ":1319,"ス":874,"ッ":-1350,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278,"・":-3794,"一":-1619,"下":-1759,"世":-2087,"両":3815,"中":653,"主":-758,"予":-1193,"二":974,"人":2742,"今":792,"他":1889,"以":-1368,"低":811,"何":4265,"作":-361,"保":-2439,"元":4858,"党":3593,"全":1574,"公":-3030,"六":755,"共":-1880,"円":5807,"再":3095,"分":457,"初":2475,"別":1129,"前":2286,"副":4437,"力":365,"動":-949,"務":-1872,"化":1327,"北":-1038,"区":4646,"千":-2309,"午":-783,"協":-1006,"口":483,"右":1233,"各":3588,"合":-241,"同":3906,"和":-837,"員":4513,"国":642,"型":1389,"場":1219,"外":-241,"妻":2016,"学":-1356,"安":-423,"実":-1008,"家":1078,"小":-513,"少":-3102,"州":1155,"市":3197,"平":-1804,"年":2416,"広":-1030,"府":1605,"度":1452,"建":-2352,"当":-3885,"得":1905,"思":-1291,"性":1822,"戸":-488,"指":-3973,"政":-2013,"教":-1479,"数":3222,"文":-1489,"新":1764,"日":2099,"旧":5792,"昨":-661,"時":-1248,"曜":-951,"最":-937,"月":4125,"期":360,"李":3094,"村":364,"東":-805,"核":5156,"森":2438,"業":484,"氏":2613,"民":-1694,"決":-1073,"法":1868,"海":-495,"無":979,"物":461,"特":-3850,"生":-273,"用":914,"町":1215,"的":7313,"直":-1835,"省":792,"県":6293,"知":-1528,"私":4231,"税":401,"立":-960,"第":1201,"米":7767,"系":3066,"約":3663,"級":1384,"統":-4229,"総":1163,"線":1255,"者":6457,"能":725,"自":-2869,"英":785,"見":1044,"調":-562,"財":-733,"費":1777,"車":1835,"軍":1375,"込":-1504,"通":-1136,"選":-681,"郎":1026,"郡":4404,"部":1200,"金":2163,"長":421,"開":-1432,"間":1302,"関":-1282,"雨":2009,"電":-1045,"非":2066,"駅":1620,"1":-800,"」":2670,"・":-3794,"ッ":-1350,"ア":551,"グ":1319,"ス":874,"ト":521,"ム":1109,"ル":1591,"ロ":2201,"ン":278};
|
||||
this.UW4__ = {",":3930,".":3508,"―":-4841,"、":3930,"。":3508,"〇":4999,"「":1895,"」":3798,"〓":-5156,"あ":4752,"い":-3435,"う":-640,"え":-2514,"お":2405,"か":530,"が":6006,"き":-4482,"ぎ":-3821,"く":-3788,"け":-4376,"げ":-4734,"こ":2255,"ご":1979,"さ":2864,"し":-843,"じ":-2506,"す":-731,"ず":1251,"せ":181,"そ":4091,"た":5034,"だ":5408,"ち":-3654,"っ":-5882,"つ":-1659,"て":3994,"で":7410,"と":4547,"な":5433,"に":6499,"ぬ":1853,"ね":1413,"の":7396,"は":8578,"ば":1940,"ひ":4249,"び":-4134,"ふ":1345,"へ":6665,"べ":-744,"ほ":1464,"ま":1051,"み":-2082,"む":-882,"め":-5046,"も":4169,"ゃ":-2666,"や":2795,"ょ":-1544,"よ":3351,"ら":-2922,"り":-9726,"る":-14896,"れ":-2613,"ろ":-4570,"わ":-1783,"を":13150,"ん":-2352,"カ":2145,"コ":1789,"セ":1287,"ッ":-724,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637,"・":-4371,"ー":-11870,"一":-2069,"中":2210,"予":782,"事":-190,"井":-1768,"人":1036,"以":544,"会":950,"体":-1286,"作":530,"側":4292,"先":601,"党":-2006,"共":-1212,"内":584,"円":788,"初":1347,"前":1623,"副":3879,"力":-302,"動":-740,"務":-2715,"化":776,"区":4517,"協":1013,"参":1555,"合":-1834,"和":-681,"員":-910,"器":-851,"回":1500,"国":-619,"園":-1200,"地":866,"場":-1410,"塁":-2094,"士":-1413,"多":1067,"大":571,"子":-4802,"学":-1397,"定":-1057,"寺":-809,"小":1910,"屋":-1328,"山":-1500,"島":-2056,"川":-2667,"市":2771,"年":374,"庁":-4556,"後":456,"性":553,"感":916,"所":-1566,"支":856,"改":787,"政":2182,"教":704,"文":522,"方":-856,"日":1798,"時":1829,"最":845,"月":-9066,"木":-485,"来":-442,"校":-360,"業":-1043,"氏":5388,"民":-2716,"気":-910,"沢":-939,"済":-543,"物":-735,"率":672,"球":-1267,"生":-1286,"産":-1101,"田":-2900,"町":1826,"的":2586,"目":922,"省":-3485,"県":2997,"空":-867,"立":-2112,"第":788,"米":2937,"系":786,"約":2171,"経":1146,"統":-1169,"総":940,"線":-994,"署":749,"者":2145,"能":-730,"般":-852,"行":-792,"規":792,"警":-1184,"議":-244,"谷":-1000,"賞":730,"車":-1481,"軍":1158,"輪":-1433,"込":-3370,"近":929,"道":-1291,"選":2596,"郎":-4866,"都":1192,"野":-1100,"銀":-2213,"長":357,"間":-2344,"院":-2297,"際":-2604,"電":-878,"領":-1659,"題":-792,"館":-1984,"首":1749,"高":2120,"「":1895,"」":3798,"・":-4371,"ッ":-724,"ー":-11870,"カ":2145,"コ":1789,"セ":1287,"ト":-403,"メ":-1635,"ラ":-881,"リ":-541,"ル":-856,"ン":-3637};
|
||||
this.UW5__ = {",":465,".":-299,"1":-514,"E2":-32768,"]":-2762,"、":465,"。":-299,"「":363,"あ":1655,"い":331,"う":-503,"え":1199,"お":527,"か":647,"が":-421,"き":1624,"ぎ":1971,"く":312,"げ":-983,"さ":-1537,"し":-1371,"す":-852,"だ":-1186,"ち":1093,"っ":52,"つ":921,"て":-18,"で":-850,"と":-127,"ど":1682,"な":-787,"に":-1224,"の":-635,"は":-578,"べ":1001,"み":502,"め":865,"ゃ":3350,"ょ":854,"り":-208,"る":429,"れ":504,"わ":419,"を":-1264,"ん":327,"イ":241,"ル":451,"ン":-343,"中":-871,"京":722,"会":-1153,"党":-654,"務":3519,"区":-901,"告":848,"員":2104,"大":-1296,"学":-548,"定":1785,"嵐":-1304,"市":-2991,"席":921,"年":1763,"思":872,"所":-814,"挙":1618,"新":-1682,"日":218,"月":-4353,"査":932,"格":1356,"機":-1508,"氏":-1347,"田":240,"町":-3912,"的":-3149,"相":1319,"省":-1052,"県":-4003,"研":-997,"社":-278,"空":-813,"統":1955,"者":-2233,"表":663,"語":-1073,"議":1219,"選":-1018,"郎":-368,"長":786,"間":1191,"題":2368,"館":-689,"1":-514,"E2":-32768,"「":363,"イ":241,"ル":451,"ン":-343};
|
||||
this.UW6__ = {",":227,".":808,"1":-270,"E1":306,"、":227,"。":808,"あ":-307,"う":189,"か":241,"が":-73,"く":-121,"こ":-200,"じ":1782,"す":383,"た":-428,"っ":573,"て":-1014,"で":101,"と":-105,"な":-253,"に":-149,"の":-417,"は":-236,"も":-206,"り":187,"る":-135,"を":195,"ル":-673,"ン":-496,"一":-277,"中":201,"件":-800,"会":624,"前":302,"区":1792,"員":-1212,"委":798,"学":-960,"市":887,"広":-695,"後":535,"業":-697,"相":753,"社":-507,"福":974,"空":-822,"者":1811,"連":463,"郎":1082,"1":-270,"E1":306,"ル":-673,"ン":-496};
|
||||
|
||||
return this;
|
||||
}
|
||||
TinySegmenter.prototype.ctype_ = function(str) {
|
||||
for (var i in this.chartype_) {
|
||||
if (str.match(this.chartype_[i][0])) {
|
||||
return this.chartype_[i][1];
|
||||
}
|
||||
}
|
||||
return "O";
|
||||
}
|
||||
|
||||
TinySegmenter.prototype.ts_ = function(v) {
|
||||
if (v) { return v; }
|
||||
return 0;
|
||||
}
|
||||
|
||||
TinySegmenter.prototype.segment = function(input) {
|
||||
if (input == null || input == undefined || input == "") {
|
||||
return [];
|
||||
}
|
||||
var result = [];
|
||||
var seg = ["B3","B2","B1"];
|
||||
var ctype = ["O","O","O"];
|
||||
var o = input.split("");
|
||||
for (i = 0; i < o.length; ++i) {
|
||||
seg.push(o[i]);
|
||||
ctype.push(this.ctype_(o[i]))
|
||||
}
|
||||
seg.push("E1");
|
||||
seg.push("E2");
|
||||
seg.push("E3");
|
||||
ctype.push("O");
|
||||
ctype.push("O");
|
||||
ctype.push("O");
|
||||
var word = seg[3];
|
||||
var p1 = "U";
|
||||
var p2 = "U";
|
||||
var p3 = "U";
|
||||
for (var i = 4; i < seg.length - 3; ++i) {
|
||||
var score = this.BIAS__;
|
||||
var w1 = seg[i-3];
|
||||
var w2 = seg[i-2];
|
||||
var w3 = seg[i-1];
|
||||
var w4 = seg[i];
|
||||
var w5 = seg[i+1];
|
||||
var w6 = seg[i+2];
|
||||
var c1 = ctype[i-3];
|
||||
var c2 = ctype[i-2];
|
||||
var c3 = ctype[i-1];
|
||||
var c4 = ctype[i];
|
||||
var c5 = ctype[i+1];
|
||||
var c6 = ctype[i+2];
|
||||
score += this.ts_(this.UP1__[p1]);
|
||||
score += this.ts_(this.UP2__[p2]);
|
||||
score += this.ts_(this.UP3__[p3]);
|
||||
score += this.ts_(this.BP1__[p1 + p2]);
|
||||
score += this.ts_(this.BP2__[p2 + p3]);
|
||||
score += this.ts_(this.UW1__[w1]);
|
||||
score += this.ts_(this.UW2__[w2]);
|
||||
score += this.ts_(this.UW3__[w3]);
|
||||
score += this.ts_(this.UW4__[w4]);
|
||||
score += this.ts_(this.UW5__[w5]);
|
||||
score += this.ts_(this.UW6__[w6]);
|
||||
score += this.ts_(this.BW1__[w2 + w3]);
|
||||
score += this.ts_(this.BW2__[w3 + w4]);
|
||||
score += this.ts_(this.BW3__[w4 + w5]);
|
||||
score += this.ts_(this.TW1__[w1 + w2 + w3]);
|
||||
score += this.ts_(this.TW2__[w2 + w3 + w4]);
|
||||
score += this.ts_(this.TW3__[w3 + w4 + w5]);
|
||||
score += this.ts_(this.TW4__[w4 + w5 + w6]);
|
||||
score += this.ts_(this.UC1__[c1]);
|
||||
score += this.ts_(this.UC2__[c2]);
|
||||
score += this.ts_(this.UC3__[c3]);
|
||||
score += this.ts_(this.UC4__[c4]);
|
||||
score += this.ts_(this.UC5__[c5]);
|
||||
score += this.ts_(this.UC6__[c6]);
|
||||
score += this.ts_(this.BC1__[c2 + c3]);
|
||||
score += this.ts_(this.BC2__[c3 + c4]);
|
||||
score += this.ts_(this.BC3__[c4 + c5]);
|
||||
score += this.ts_(this.TC1__[c1 + c2 + c3]);
|
||||
score += this.ts_(this.TC2__[c2 + c3 + c4]);
|
||||
score += this.ts_(this.TC3__[c3 + c4 + c5]);
|
||||
score += this.ts_(this.TC4__[c4 + c5 + c6]);
|
||||
// score += this.ts_(this.TC5__[c4 + c5 + c6]);
|
||||
score += this.ts_(this.UQ1__[p1 + c1]);
|
||||
score += this.ts_(this.UQ2__[p2 + c2]);
|
||||
score += this.ts_(this.UQ3__[p3 + c3]);
|
||||
score += this.ts_(this.BQ1__[p2 + c2 + c3]);
|
||||
score += this.ts_(this.BQ2__[p2 + c3 + c4]);
|
||||
score += this.ts_(this.BQ3__[p3 + c2 + c3]);
|
||||
score += this.ts_(this.BQ4__[p3 + c3 + c4]);
|
||||
score += this.ts_(this.TQ1__[p2 + c1 + c2 + c3]);
|
||||
score += this.ts_(this.TQ2__[p2 + c2 + c3 + c4]);
|
||||
score += this.ts_(this.TQ3__[p3 + c1 + c2 + c3]);
|
||||
score += this.ts_(this.TQ4__[p3 + c2 + c3 + c4]);
|
||||
var p = "O";
|
||||
if (score > 0) {
|
||||
result.push(word);
|
||||
word = "";
|
||||
p = "B";
|
||||
}
|
||||
p1 = p2;
|
||||
p2 = p3;
|
||||
p3 = p;
|
||||
word += seg[i];
|
||||
}
|
||||
result.push(word);
|
||||
|
||||
return result;
|
||||
}
|
||||
|
||||
lunr.TinySegmenter = TinySegmenter;
|
||||
};
|
||||
|
||||
}));
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
File diff suppressed because one or more lines are too long
-1
File diff suppressed because one or more lines are too long
@@ -1 +0,0 @@
|
||||
{"version":3,"sources":["src/templates/assets/stylesheets/palette/_scheme.scss","../../../../src/templates/assets/stylesheets/palette.scss","src/templates/assets/stylesheets/palette/_accent.scss","src/templates/assets/stylesheets/palette/_primary.scss","src/templates/assets/stylesheets/utilities/_break.scss"],"names":[],"mappings":"AA2BA,cAGE,6BAME,sDAAA,CACA,6DAAA,CACA,+DAAA,CACA,gEAAA,CACA,mDAAA,CACA,6DAAA,CACA,+DAAA,CACA,gEAAA,CAGA,mDAAA,CACA,gDAAA,CACA,yDAAA,CACA,4DAAA,CAGA,0BAAA,CACA,mCAAA,CAGA,iCAAA,CACA,kCAAA,CACA,mCAAA,CACA,mCAAA,CACA,kCAAA,CACA,iCAAA,CACA,+CAAA,CACA,6DAAA,CACA,gEAAA,CACA,4DAAA,CACA,4DAAA,CACA,6DAAA,CAGA,6CAAA,CAGA,+CAAA,CAGA,uDAAA,CACA,6DAAA,CACA,2DAAA,CAGA,iCAAA,CAGA,yDAAA,CACA,iEAAA,CAGA,mDAAA,CACA,mDAAA,CAGA,qDAAA,CACA,uDAAA,CAGA,8DAAA,CAKA,8DAAA,CAKA,0DAAA,CAzEA,iBCiBF,CD6DE,kHAEE,YC3DJ,CDkFE,yDACE,4BChFJ,CD+EE,2DACE,4BC7EJ,CD4EE,gEACE,4BC1EJ,CDyEE,2DACE,4BCvEJ,CDsEE,yDACE,4BCpEJ,CDmEE,0DACE,4BCjEJ,CDgEE,gEACE,4BC9DJ,CD6DE,0DACE,4BC3DJ,CD0DE,2OACE,4BC/CJ,CDsDA,+FAGE,iCCpDF,CACF,CCjDE,2BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD6CN,CCvDE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDoDN,CC9DE,8BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD2DN,CCrEE,mCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDkEN,CC5EE,8BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDyEN,CCnFE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDgFN,CC1FE,kCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDuFN,CCjGE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD8FN,CCxGE,4BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDqGN,CC/GE,6BACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCD4GN,CCtHE,mCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDmHN,CC7HE,4BACE,4BAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCD6HN,CCpIE,8BACE,4BAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCDoIN,CC3IE,6BACE,yBAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCD2IN,CClJE,8BACE,4BAAA,CACA,2CAAA,CAIE,8BAAA,CACA,qCDkJN,CCzJE,mCACE,4BAAA,CACA,2CAAA,CAOE,yBAAA,CACA,qCDsJN,CE3JE,4BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwJN,CEnKE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgKN,CE3KE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwKN,CEnLE,oCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgLN,CE3LE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwLN,CEnME,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgMN,CE3ME,mCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwMN,CEnNE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgNN,CE3NE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwNN,CEnOE,8BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgON,CE3OE,oCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwON,CEnPE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCFmPN,CE3PE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCF2PN,CEnQE,8BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCFmQN,CE3QE,+BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAIE,+BAAA,CACA,sCF2QN,CEnRE,oCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFgRN,CE3RE,8BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCFwRN,CEnSE,6BACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCAAA,CAKA,4BF4RN,CE5SE,kCACE,6BAAA,CACA,oCAAA,CACA,mCAAA,CAOE,0BAAA,CACA,sCAAA,CAKA,4BFqSN,CEtRE,sEACE,4BFyRJ,CE1RE,+DACE,4BF6RJ,CE9RE,iEACE,4BFiSJ,CElSE,gEACE,4BFqSJ,CEtSE,iEACE,4BFySJ,CEhSA,8BACE,mDAAA,CACA,4DAAA,CACA,0DAAA,CACA,oDAAA,CACA,2DAAA,CAGA,4BFiSF,CE9RE,yCACE,+BFgSJ,CE7RI,kDAEE,0CAAA,CACA,sCAAA,CAFA,mCFiSN,CG7MI,mCD1EA,+CACE,8CF0RJ,CEvRI,qDACE,8CFyRN,CEpRE,iEACE,mCFsRJ,CACF,CGxNI,sCDvDA,uCACE,oCFkRJ,CACF,CEzQA,8BACE,kDAAA,CACA,4DAAA,CACA,wDAAA,CACA,oDAAA,CACA,6DAAA,CAGA,4BF0QF,CEvQE,yCACE,+BFyQJ,CEtQI,kDAEE,0CAAA,CACA,sCAAA,CAFA,mCF0QN,CEnQE,yCACE,6CFqQJ,CG9NI,0CDhCA,8CACE,gDFiQJ,CACF,CGnOI,0CDvBA,iFACE,6CF6PJ,CACF,CG3PI,sCDKA,uCACE,6CFyPJ,CACF","file":"palette.css"}
|
||||
Binary file not shown.
|
After Width: | Height: | Size: 14 KiB |
Binary file not shown.
|
After Width: | Height: | Size: 1.3 MiB |
@@ -0,0 +1,63 @@
|
||||
services:
|
||||
backend:
|
||||
image: ghcr.io/sagit-chu/flux-panel-backend:${FLUX_VERSION:-latest}
|
||||
container_name: flux-panel-backend
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
environment:
|
||||
DB_PATH: /app/data/gost.db
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
LOG_DIR: /app/logs
|
||||
SERVER_ADDR: :6365
|
||||
ports:
|
||||
- "${BACKEND_PORT}:6365"
|
||||
volumes:
|
||||
- backend_logs:/app/logs
|
||||
- sqlite_data:/app/data
|
||||
networks:
|
||||
- gost-network
|
||||
stop_grace_period: 30s
|
||||
stop_signal: SIGTERM
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
frontend:
|
||||
image: ghcr.io/sagit-chu/vite-frontend:${FLUX_VERSION:-latest}
|
||||
container_name: vite-frontend
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
ports:
|
||||
- "${FRONTEND_PORT}:80"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- gost-network
|
||||
|
||||
|
||||
volumes:
|
||||
sqlite_data:
|
||||
name: sqlite_data
|
||||
driver: local
|
||||
backend_logs:
|
||||
name: backend_logs
|
||||
driver: local
|
||||
|
||||
|
||||
networks:
|
||||
gost-network:
|
||||
name: gost-network
|
||||
driver: bridge
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
@@ -0,0 +1,65 @@
|
||||
services:
|
||||
backend:
|
||||
image: ghcr.io/sagit-chu/flux-panel-backend:${FLUX_VERSION:-latest}
|
||||
container_name: flux-panel-backend
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
environment:
|
||||
DB_PATH: /app/data/gost.db
|
||||
JWT_SECRET: ${JWT_SECRET}
|
||||
LOG_DIR: /app/logs
|
||||
SERVER_ADDR: :6365
|
||||
ports:
|
||||
- "${BACKEND_PORT}:6365"
|
||||
volumes:
|
||||
- backend_logs:/app/logs
|
||||
- sqlite_data:/app/data
|
||||
networks:
|
||||
- gost-network
|
||||
stop_grace_period: 30s
|
||||
stop_signal: SIGTERM
|
||||
healthcheck:
|
||||
test: ["CMD", "sh", "-c", "wget --no-verbose --tries=1 --spider http://localhost:6365/flow/test || exit 1"]
|
||||
interval: 30s
|
||||
timeout: 10s
|
||||
retries: 5
|
||||
start_period: 30s
|
||||
|
||||
frontend:
|
||||
image: ghcr.io/sagit-chu/vite-frontend:${FLUX_VERSION:-latest}
|
||||
container_name: vite-frontend
|
||||
restart: unless-stopped
|
||||
logging:
|
||||
driver: json-file
|
||||
options:
|
||||
max-size: "20m"
|
||||
ports:
|
||||
- "${FRONTEND_PORT}:80"
|
||||
depends_on:
|
||||
backend:
|
||||
condition: service_healthy
|
||||
networks:
|
||||
- gost-network
|
||||
|
||||
|
||||
volumes:
|
||||
sqlite_data:
|
||||
name: sqlite_data
|
||||
driver: local
|
||||
backend_logs:
|
||||
name: backend_logs
|
||||
driver: local
|
||||
|
||||
|
||||
networks:
|
||||
gost-network:
|
||||
name: gost-network
|
||||
driver: bridge
|
||||
enable_ipv6: true
|
||||
ipam:
|
||||
config:
|
||||
- subnet: 172.20.0.0/16
|
||||
- subnet: fd00:dead:beef::/48
|
||||
-877
@@ -1,877 +0,0 @@
|
||||
|
||||
<!doctype html>
|
||||
<html lang="zh" class="no-js">
|
||||
<head>
|
||||
|
||||
<meta charset="utf-8">
|
||||
<meta name="viewport" content="width=device-width,initial-scale=1">
|
||||
|
||||
<meta name="description" content="FLVX官方文档">
|
||||
|
||||
|
||||
<meta name="author" content="Sagit-chu">
|
||||
|
||||
|
||||
<link rel="canonical" href="https://sagit-chu.github.io/flux-panel/faq/">
|
||||
|
||||
|
||||
<link rel="prev" href="../postgresql/">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="icon" href="../assets/images/favicon.png">
|
||||
<meta name="generator" content="mkdocs-1.6.1, mkdocs-material-9.7.7">
|
||||
|
||||
|
||||
|
||||
<title>常见问题 - FLVX 文档</title>
|
||||
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../assets/stylesheets/main.ec1eaa64.min.css">
|
||||
|
||||
|
||||
<link rel="stylesheet" href="../assets/stylesheets/palette.ab4e12ef.min.css">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<link rel="preconnect" href="https://fonts.gstatic.com" crossorigin>
|
||||
<link rel="stylesheet" href="https://fonts.googleapis.com/css?family=Roboto:300,300i,400,400i,700,700i%7CRoboto+Mono:400,400i,700,700i&display=fallback">
|
||||
<style>:root{--md-text-font:"Roboto";--md-code-font:"Roboto Mono"}</style>
|
||||
|
||||
|
||||
|
||||
<script>__md_scope=new URL("..",location),__md_hash=e=>[...e].reduce(((e,_)=>(e<<5)-e+_.charCodeAt(0)),0),__md_get=(e,_=localStorage,t=__md_scope)=>JSON.parse(_.getItem(t.pathname+"."+e)),__md_set=(e,_,t=localStorage,a=__md_scope)=>{try{t.setItem(a.pathname+"."+e,JSON.stringify(_))}catch(e){}}</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</head>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<body dir="ltr" data-md-color-scheme="default" data-md-color-primary="indigo" data-md-color-accent="indigo">
|
||||
|
||||
|
||||
<input class="md-toggle" data-md-toggle="drawer" type="checkbox" id="__drawer" autocomplete="off">
|
||||
<input class="md-toggle" data-md-toggle="search" type="checkbox" id="__search" autocomplete="off">
|
||||
<label class="md-overlay" for="__drawer"></label>
|
||||
<div data-md-component="skip">
|
||||
|
||||
|
||||
<a href="#faq" class="md-skip">
|
||||
跳转至
|
||||
</a>
|
||||
|
||||
</div>
|
||||
<div data-md-component="announce">
|
||||
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
<header class="md-header" data-md-component="header">
|
||||
<nav class="md-header__inner md-grid" aria-label="页眉">
|
||||
<a href=".." title="FLVX 文档" class="md-header__button md-logo" aria-label="FLVX 文档" data-md-component="logo">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
|
||||
|
||||
</a>
|
||||
<label class="md-header__button md-icon" for="__drawer">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M3 6h18v2H3zm0 5h18v2H3zm0 5h18v2H3z"/></svg>
|
||||
</label>
|
||||
<div class="md-header__title" data-md-component="header-title">
|
||||
<div class="md-header__ellipsis">
|
||||
<div class="md-header__topic">
|
||||
<span class="md-ellipsis">
|
||||
FLVX 文档
|
||||
</span>
|
||||
</div>
|
||||
<div class="md-header__topic" data-md-component="header-topic">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
常见问题
|
||||
|
||||
</span>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
<form class="md-header__option" data-md-component="palette">
|
||||
|
||||
|
||||
|
||||
|
||||
<input class="md-option" data-md-color-media="" data-md-color-scheme="default" data-md-color-primary="indigo" data-md-color-accent="indigo" aria-hidden="true" type="radio" name="__palette" id="__palette_0">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<input class="md-option" data-md-color-media="" data-md-color-scheme="slate" data-md-color-primary="indigo" data-md-color-accent="indigo" aria-label="Switch to dark mode" type="radio" name="__palette" id="__palette_1">
|
||||
|
||||
<label class="md-header__button md-icon" title="Switch to dark mode" for="__palette_0" hidden>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 18c-.89 0-1.74-.2-2.5-.55C11.56 16.5 13 14.42 13 12s-1.44-4.5-3.5-5.45C10.26 6.2 11.11 6 12 6a6 6 0 0 1 6 6 6 6 0 0 1-6 6m8-9.31V4h-4.69L12 .69 8.69 4H4v4.69L.69 12 4 15.31V20h4.69L12 23.31 15.31 20H20v-4.69L23.31 12z"/></svg>
|
||||
</label>
|
||||
|
||||
|
||||
</form>
|
||||
|
||||
|
||||
|
||||
<script>var palette=__md_get("__palette");if(palette&&palette.color){if("(prefers-color-scheme)"===palette.color.media){var media=matchMedia("(prefers-color-scheme: light)"),input=document.querySelector(media.matches?"[data-md-color-media='(prefers-color-scheme: light)']":"[data-md-color-media='(prefers-color-scheme: dark)']");palette.color.media=input.getAttribute("data-md-color-media"),palette.color.scheme=input.getAttribute("data-md-color-scheme"),palette.color.primary=input.getAttribute("data-md-color-primary"),palette.color.accent=input.getAttribute("data-md-color-accent")}for(var[key,value]of Object.entries(palette.color))document.body.setAttribute("data-md-color-"+key,value)}</script>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-header__button md-icon" for="__search">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||
</label>
|
||||
<div class="md-search" data-md-component="search" role="dialog">
|
||||
<label class="md-search__overlay" for="__search"></label>
|
||||
<div class="md-search__inner" role="search">
|
||||
<form class="md-search__form" name="search">
|
||||
<input type="text" class="md-search__input" name="query" aria-label="搜索" placeholder="搜索" autocapitalize="off" autocorrect="off" autocomplete="off" spellcheck="false" data-md-component="search-query" required>
|
||||
<label class="md-search__icon md-icon" for="__search">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M9.5 3A6.5 6.5 0 0 1 16 9.5c0 1.61-.59 3.09-1.56 4.23l.27.27h.79l5 5-1.5 1.5-5-5v-.79l-.27-.27A6.52 6.52 0 0 1 9.5 16 6.5 6.5 0 0 1 3 9.5 6.5 6.5 0 0 1 9.5 3m0 2C7 5 5 7 5 9.5S7 14 9.5 14 14 12 14 9.5 12 5 9.5 5"/></svg>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M20 11v2H8l5.5 5.5-1.42 1.42L4.16 12l7.92-7.92L13.5 5.5 8 11z"/></svg>
|
||||
</label>
|
||||
<nav class="md-search__options" aria-label="查找">
|
||||
|
||||
<button type="reset" class="md-search__icon md-icon" title="清空当前内容" aria-label="清空当前内容" tabindex="-1">
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M19 6.41 17.59 5 12 10.59 6.41 5 5 6.41 10.59 12 5 17.59 6.41 19 12 13.41 17.59 19 19 17.59 13.41 12z"/></svg>
|
||||
</button>
|
||||
</nav>
|
||||
|
||||
<div class="md-search__suggest" data-md-component="search-suggest"></div>
|
||||
|
||||
</form>
|
||||
<div class="md-search__output">
|
||||
<div class="md-search__scrollwrap" tabindex="0" data-md-scrollfix>
|
||||
<div class="md-search-result" data-md-component="search-result">
|
||||
<div class="md-search-result__meta">
|
||||
正在初始化搜索引擎
|
||||
</div>
|
||||
<ol class="md-search-result__list" role="presentation"></ol>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
</nav>
|
||||
|
||||
</header>
|
||||
|
||||
<div class="md-container" data-md-component="container">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<nav class="md-tabs" aria-label="标签" data-md-component="tabs">
|
||||
<div class="md-grid">
|
||||
<ul class="md-tabs__list">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href=".." class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
首页
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="../install/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
安装部署
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="../usage/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
使用指南
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="../ai-skill/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
AI Skill 接入
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item">
|
||||
<a href="../postgresql/" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
PostgreSQL
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-tabs__item md-tabs__item--active">
|
||||
<a href="./" class="md-tabs__link">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
常见问题
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</div>
|
||||
</nav>
|
||||
|
||||
|
||||
|
||||
<main class="md-main" data-md-component="main">
|
||||
<div class="md-main__inner md-grid">
|
||||
|
||||
|
||||
|
||||
<div class="md-sidebar md-sidebar--primary" data-md-component="sidebar" data-md-type="navigation" >
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div class="md-sidebar__inner">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--primary md-nav--lifted" aria-label="导航栏" data-md-level="0">
|
||||
<label class="md-nav__title" for="__drawer">
|
||||
<a href=".." title="FLVX 文档" class="md-nav__button md-logo" aria-label="FLVX 文档" data-md-component="logo">
|
||||
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M12 8a3 3 0 0 0 3-3 3 3 0 0 0-3-3 3 3 0 0 0-3 3 3 3 0 0 0 3 3m0 3.54C9.64 9.35 6.5 8 3 8v11c3.5 0 6.64 1.35 9 3.54 2.36-2.19 5.5-3.54 9-3.54V8c-3.5 0-6.64 1.35-9 3.54"/></svg>
|
||||
|
||||
</a>
|
||||
FLVX 文档
|
||||
</label>
|
||||
|
||||
<ul class="md-nav__list" data-md-scrollfix>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href=".." class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
首页
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../install/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
安装部署
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../usage/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
使用指南
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../ai-skill/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
AI Skill 接入
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="../postgresql/" class="md-nav__link">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
PostgreSQL
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<li class="md-nav__item md-nav__item--active">
|
||||
|
||||
<input class="md-nav__toggle md-toggle" type="checkbox" id="__toc">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__link md-nav__link--active" for="__toc">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
常见问题
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
</label>
|
||||
|
||||
<a href="./" class="md-nav__link md-nav__link--active">
|
||||
|
||||
|
||||
|
||||
<span class="md-ellipsis">
|
||||
|
||||
|
||||
常见问题
|
||||
|
||||
|
||||
|
||||
</span>
|
||||
|
||||
|
||||
|
||||
</a>
|
||||
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--secondary" aria-label="目录">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__title" for="__toc">
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
目录
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q1-docker-command-not-found" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q1: 安装脚本提示 "Docker command not found"?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q2-connection-refused" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q2: 面板无法访问 (Connection Refused)?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q3" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q3: 节点显示离线?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q4-tcp-udp" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q4: 只有 TCP 能通,UDP 不通?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q5-ipv6" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q5: IPv6 无法使用?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q6-postgresql" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q6: 如何切换到 PostgreSQL?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q7-sqlite-postgresql" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q7: 从 SQLite 迁移到 PostgreSQL 后数据丢失?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q8-postgresql" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q8: PostgreSQL 容器启动失败?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
|
||||
</li>
|
||||
|
||||
|
||||
|
||||
</ul>
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="md-sidebar md-sidebar--secondary" data-md-component="sidebar" data-md-type="toc" >
|
||||
<div class="md-sidebar__scrollwrap">
|
||||
<div class="md-sidebar__inner">
|
||||
|
||||
|
||||
<nav class="md-nav md-nav--secondary" aria-label="目录">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<label class="md-nav__title" for="__toc">
|
||||
<span class="md-nav__icon md-icon"></span>
|
||||
目录
|
||||
</label>
|
||||
<ul class="md-nav__list" data-md-component="toc" data-md-scrollfix>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q1-docker-command-not-found" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q1: 安装脚本提示 "Docker command not found"?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q2-connection-refused" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q2: 面板无法访问 (Connection Refused)?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q3" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q3: 节点显示离线?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q4-tcp-udp" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q4: 只有 TCP 能通,UDP 不通?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q5-ipv6" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q5: IPv6 无法使用?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q6-postgresql" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q6: 如何切换到 PostgreSQL?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q7-sqlite-postgresql" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q7: 从 SQLite 迁移到 PostgreSQL 后数据丢失?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
<li class="md-nav__item">
|
||||
<a href="#q8-postgresql" class="md-nav__link">
|
||||
<span class="md-ellipsis">
|
||||
|
||||
Q8: PostgreSQL 容器启动失败?
|
||||
|
||||
</span>
|
||||
</a>
|
||||
|
||||
</li>
|
||||
|
||||
</ul>
|
||||
|
||||
</nav>
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
<div class="md-content" data-md-component="content">
|
||||
|
||||
<article class="md-content__inner md-typeset">
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<h1 id="faq">常见问题 (FAQ)</h1>
|
||||
<h3 id="q1-docker-command-not-found">Q1: 安装脚本提示 "Docker command not found"?</h3>
|
||||
<p><strong>A</strong>: 请确保您的系统已安装 Docker 和 Docker Compose。
|
||||
- Ubuntu/Debian 安装 Docker: <code>curl -fsSL https://get.docker.com | bash</code></p>
|
||||
<h3 id="q2-connection-refused">Q2: 面板无法访问 (Connection Refused)?</h3>
|
||||
<p><strong>A</strong>:
|
||||
1. 检查防火墙是否放行了前端端口(默认 <code>6366</code>)。
|
||||
2. 检查容器是否正常运行: <code>docker ps</code>。
|
||||
3. 查看容器日志: <code>docker logs flux-panel-backend</code> 或 <code>docker logs vite-frontend</code>。</p>
|
||||
<h3 id="q3">Q3: 节点显示离线?</h3>
|
||||
<p><strong>A</strong>:
|
||||
1. 检查节点服务器与面板服务器之间的网络连通性。
|
||||
2. 确认在节点端安装时输入的 <strong>面板地址</strong> 和 <strong>密钥</strong> 是否正确。
|
||||
3. 检查节点端服务状态: <code>systemctl status flux_agent</code>。
|
||||
4. 查看节点端日志: <code>journalctl -u flux_agent -f</code>。</p>
|
||||
<h3 id="q4-tcp-udp">Q4: 只有 TCP 能通,UDP 不通?</h3>
|
||||
<p><strong>A</strong>: 请检查服务器防火墙和安全组(AWS/阿里云/腾讯云等)是否同时放行了对应端口的 <strong>TCP 和 UDP</strong> 协议。</p>
|
||||
<h3 id="q5-ipv6">Q5: IPv6 无法使用?</h3>
|
||||
<p><strong>A</strong>: 面板安装脚本会自动尝试配置 Docker 的 IPv6。如果失败,请手动检查 <code>/etc/docker/daemon.json</code> 配置,确保 <code>ipv6: true</code> 且分配了正确的 <code>fixed-cidr-v6</code> 子网。</p>
|
||||
<h3 id="q6-postgresql">Q6: 如何切换到 PostgreSQL?</h3>
|
||||
<p><strong>A</strong>: 在 <code>.env</code> 文件中设置 <code>DB_TYPE=postgres</code>,并让 <code>DATABASE_URL</code> 与 <code>POSTGRES_*</code> 保持一致,然后执行 <code>docker compose up -d</code> 重启服务即可。使用安装脚本部署时,<code>POSTGRES_PASSWORD</code> 会自动随机生成并写入 <code>.env</code>。详见 <a href="../postgresql/">PostgreSQL 数据库指南</a>。</p>
|
||||
<h3 id="q7-sqlite-postgresql">Q7: 从 SQLite 迁移到 PostgreSQL 后数据丢失?</h3>
|
||||
<p><strong>A</strong>:
|
||||
1. 确认迁移前已备份 SQLite 文件(<code>gost.db.bak</code>)。
|
||||
2. 确认 <code>pgloader</code> 命令执行成功,检查其输出是否有报错。
|
||||
3. 确认 <code>.env</code> 中 <code>DATABASE_URL</code> 的密码与 <code>POSTGRES_PASSWORD</code> 一致。
|
||||
4. 详细迁移步骤参考 <a href="../postgresql/">PostgreSQL 数据库指南 - 从 SQLite 迁移</a>。</p>
|
||||
<h3 id="q8-postgresql">Q8: PostgreSQL 容器启动失败?</h3>
|
||||
<p><strong>A</strong>:
|
||||
1. 检查 <code>POSTGRES_PASSWORD</code> 是否已设置(不能为空)。
|
||||
2. 查看容器日志:<code>docker logs flux-panel-postgres</code>。
|
||||
3. 如果是首次启动后修改了密码,需要删除旧的数据卷重新初始化:<code>docker volume rm postgres_data</code>。</p>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
</article>
|
||||
</div>
|
||||
|
||||
|
||||
<script>var target=document.getElementById(location.hash.slice(1));target&&target.name&&(target.checked=target.name.startsWith("__tabbed_"))</script>
|
||||
</div>
|
||||
|
||||
<button type="button" class="md-top md-icon" data-md-component="top" hidden>
|
||||
|
||||
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24"><path d="M13 20h-2V8l-5.5 5.5-1.42-1.42L12 4.16l7.92 7.92-1.42 1.42L13 8z"/></svg>
|
||||
回到页面顶部
|
||||
</button>
|
||||
|
||||
</main>
|
||||
|
||||
<footer class="md-footer">
|
||||
|
||||
<div class="md-footer-meta md-typeset">
|
||||
<div class="md-footer-meta__inner md-grid">
|
||||
<div class="md-copyright">
|
||||
|
||||
|
||||
Made with
|
||||
<a href="https://squidfunk.github.io/mkdocs-material/" target="_blank" rel="noopener">
|
||||
Material for MkDocs
|
||||
</a>
|
||||
|
||||
</div>
|
||||
|
||||
</div>
|
||||
</div>
|
||||
</footer>
|
||||
|
||||
</div>
|
||||
<div class="md-dialog" data-md-component="dialog">
|
||||
<div class="md-dialog__inner md-typeset"></div>
|
||||
</div>
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
<script id="__config" type="application/json">{"annotate": null, "base": "..", "features": ["navigation.tabs", "navigation.sections", "navigation.top", "search.suggest", "search.highlight", "content.code.copy"], "search": "../assets/javascripts/workers/search.2c215733.min.js", "tags": null, "translations": {"clipboard.copied": "\u5df2\u590d\u5236", "clipboard.copy": "\u590d\u5236", "search.result.more.one": "\u5728\u8be5\u9875\u4e0a\u8fd8\u6709 1 \u4e2a\u7b26\u5408\u6761\u4ef6\u7684\u7ed3\u679c", "search.result.more.other": "\u5728\u8be5\u9875\u4e0a\u8fd8\u6709 # \u4e2a\u7b26\u5408\u6761\u4ef6\u7684\u7ed3\u679c", "search.result.none": "\u6ca1\u6709\u627e\u5230\u7b26\u5408\u6761\u4ef6\u7684\u7ed3\u679c", "search.result.one": "\u627e\u5230 1 \u4e2a\u7b26\u5408\u6761\u4ef6\u7684\u7ed3\u679c", "search.result.other": "# \u4e2a\u7b26\u5408\u6761\u4ef6\u7684\u7ed3\u679c", "search.result.placeholder": "\u952e\u5165\u4ee5\u5f00\u59cb\u641c\u7d22", "search.result.term.missing": "\u7f3a\u5c11", "select.version": "\u9009\u62e9\u5f53\u524d\u7248\u672c"}, "version": null}</script>
|
||||
|
||||
|
||||
<script src="../assets/javascripts/bundle.d7400e89.min.js"></script>
|
||||
|
||||
|
||||
</body>
|
||||
</html>
|
||||
@@ -0,0 +1,17 @@
|
||||
FROM golang:1.23-bookworm AS builder
|
||||
WORKDIR /src
|
||||
|
||||
COPY go.mod ./
|
||||
RUN go mod download
|
||||
|
||||
COPY . .
|
||||
RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build -o /out/paneld ./cmd/paneld
|
||||
|
||||
FROM debian:bookworm-slim
|
||||
WORKDIR /app
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends ca-certificates wget && rm -rf /var/lib/apt/lists/*
|
||||
COPY --from=builder /out/paneld /app/paneld
|
||||
|
||||
ENV SERVER_ADDR=:6365
|
||||
EXPOSE 6365
|
||||
ENTRYPOINT ["/app/paneld"]
|
||||
@@ -0,0 +1,12 @@
|
||||
GO ?= go
|
||||
|
||||
.PHONY: test build run
|
||||
|
||||
test:
|
||||
$(GO) test ./...
|
||||
|
||||
build:
|
||||
$(GO) build ./cmd/paneld
|
||||
|
||||
run:
|
||||
SERVER_ADDR=:6365 $(GO) run ./cmd/paneld
|
||||
@@ -0,0 +1,51 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"log"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"syscall"
|
||||
"time"
|
||||
|
||||
"go-backend/internal/app"
|
||||
"go-backend/internal/config"
|
||||
)
|
||||
|
||||
func main() {
|
||||
cfg := config.FromEnv()
|
||||
if cfg.JWTSecret == "" {
|
||||
log.Println("warning: JWT_SECRET is empty")
|
||||
}
|
||||
log.Printf("starting go-backend on %s (db=%s)", cfg.Addr, cfg.DBPath)
|
||||
|
||||
a, err := app.New(cfg)
|
||||
if err != nil {
|
||||
log.Fatalf("failed to create app: %v", err)
|
||||
}
|
||||
|
||||
errCh := make(chan error, 1)
|
||||
go func() {
|
||||
errCh <- a.Run()
|
||||
}()
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
signal.Notify(sigCh, syscall.SIGINT, syscall.SIGTERM)
|
||||
|
||||
select {
|
||||
case sig := <-sigCh:
|
||||
log.Printf("received signal %s, shutting down", sig)
|
||||
case runErr := <-errCh:
|
||||
if runErr != nil && !errors.Is(runErr, http.ErrServerClosed) {
|
||||
log.Fatalf("server stopped unexpectedly: %v", runErr)
|
||||
}
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
if err := a.Shutdown(ctx); err != nil {
|
||||
log.Fatalf("shutdown failed: %v", err)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,23 @@
|
||||
module go-backend
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.24.4
|
||||
|
||||
require (
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
modernc.org/sqlite v1.37.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/dustin/go-humanize v1.0.1 // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/ncruces/go-strftime v0.1.9 // indirect
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec // indirect
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
modernc.org/libc v1.65.7 // indirect
|
||||
modernc.org/mathutil v1.7.1 // indirect
|
||||
modernc.org/memory v1.11.0 // indirect
|
||||
)
|
||||
@@ -0,0 +1,49 @@
|
||||
github.com/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e h1:ijClszYn+mADRFY17kjQEVQ1XRhq2/JR1M3sGqeJoxs=
|
||||
github.com/google/pprof v0.0.0-20250317173921-a4b03ec1a45e/go.mod h1:boTsfXsheKC2y+lKOCMpSfarhxDeIzfZG1jqGcPl3cA=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
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/ncruces/go-strftime v0.1.9 h1:bY0MQC28UADQmHmaF5dgpLmImcShSi2kHU9XLdhx/f4=
|
||||
github.com/ncruces/go-strftime v0.1.9/go.mod h1:Fwc5htZGVVkseilnfgOVb9mKy6w1naJmn9CehxcKcls=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0 h1:R84qjqJb5nVJMxqWYb3np9L5ZsaDtB+a39EqjV0JSUM=
|
||||
golang.org/x/exp v0.0.0-20250408133849-7e4ce0ab07d0/go.mod h1:S9Xr4PYopiDyqSyp5NjCrhFrqg6A5zA2E/iPHPhqnS8=
|
||||
golang.org/x/mod v0.24.0 h1:ZfthKaKaT4NrhGVZHO1/WDTwGES4De8KtWO0SIbNJMU=
|
||||
golang.org/x/mod v0.24.0/go.mod h1:IXM97Txy2VM4PJ3gI61r1YEk/gAj6zAHN3AdZt6S9Ww=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/tools v0.33.0 h1:4qz2S3zmRxbGIhDIAgjxvFutSvH5EfnsYrRBj0UI0bc=
|
||||
golang.org/x/tools v0.33.0/go.mod h1:CIJMaWEY88juyUfo7UbgPqbC8rU2OqfAV1h2Qp0oMYI=
|
||||
modernc.org/cc/v4 v4.26.1 h1:+X5NtzVBn0KgsBCBe+xkDC7twLb/jNVj9FPgiwSQO3s=
|
||||
modernc.org/cc/v4 v4.26.1/go.mod h1:uVtb5OGqUKpoLWhqwNQo/8LwvoiEBLvZXIQ/SmO6mL0=
|
||||
modernc.org/ccgo/v4 v4.28.0 h1:rjznn6WWehKq7dG4JtLRKxb52Ecv8OUGah8+Z/SfpNU=
|
||||
modernc.org/ccgo/v4 v4.28.0/go.mod h1:JygV3+9AV6SmPhDasu4JgquwU81XAKLd3OKTUDNOiKE=
|
||||
modernc.org/fileutil v1.3.1 h1:8vq5fe7jdtEvoCf3Zf9Nm0Q05sH6kGx0Op2CPx1wTC8=
|
||||
modernc.org/fileutil v1.3.1/go.mod h1:HxmghZSZVAz/LXcMNwZPA/DRrQZEVP9VX0V4LQGQFOc=
|
||||
modernc.org/gc/v2 v2.6.5 h1:nyqdV8q46KvTpZlsw66kWqwXRHdjIlJOhG6kxiV/9xI=
|
||||
modernc.org/gc/v2 v2.6.5/go.mod h1:YgIahr1ypgfe7chRuJi2gD7DBQiKSLMPgBQe9oIiito=
|
||||
modernc.org/libc v1.65.7 h1:Ia9Z4yzZtWNtUIuiPuQ7Qf7kxYrxP1/jeHZzG8bFu00=
|
||||
modernc.org/libc v1.65.7/go.mod h1:011EQibzzio/VX3ygj1qGFt5kMjP0lHb0qCW5/D/pQU=
|
||||
modernc.org/mathutil v1.7.1 h1:GCZVGXdaN8gTqB1Mf/usp1Y/hSqgI2vAGGP4jZMCxOU=
|
||||
modernc.org/mathutil v1.7.1/go.mod h1:4p5IwJITfppl0G4sUEDtCr4DthTaT47/N3aT6MhfgJg=
|
||||
modernc.org/memory v1.11.0 h1:o4QC8aMQzmcwCK3t3Ux/ZHmwFPzE6hf2Y5LbkRs+hbI=
|
||||
modernc.org/memory v1.11.0/go.mod h1:/JP4VbVC+K5sU2wZi9bHoq2MAkCnrt2r98UGeSK7Mjw=
|
||||
modernc.org/opt v0.1.4 h1:2kNGMRiUjrp4LcaPuLY2PzUfqM/w9N23quVwhKt5Qm8=
|
||||
modernc.org/opt v0.1.4/go.mod h1:03fq9lsNfvkYSfxrfUhZCWPk1lm4cq4N+Bh//bEtgns=
|
||||
modernc.org/sortutil v1.2.1 h1:+xyoGf15mM3NMlPDnFqrteY07klSFxLElE2PVuWIJ7w=
|
||||
modernc.org/sortutil v1.2.1/go.mod h1:7ZI3a3REbai7gzCLcotuw9AC4VZVpYMjDzETGsSMqJE=
|
||||
modernc.org/sqlite v1.37.1 h1:EgHJK/FPoqC+q2YBXg7fUmES37pCHFc97sI7zSayBEs=
|
||||
modernc.org/sqlite v1.37.1/go.mod h1:XwdRtsE1MpiBcL54+MbKcaDvcuej+IYSMfLN6gSKV8g=
|
||||
modernc.org/strutil v1.2.1 h1:UneZBkQA+DX2Rp35KcM69cSsNES9ly8mQWD71HKlOA0=
|
||||
modernc.org/strutil v1.2.1/go.mod h1:EHkiggD70koQxjVdSBM3JKM7k6L0FbGE5eymy9i3B9A=
|
||||
modernc.org/token v1.1.0 h1:Xl7Ap9dKaEs5kLoOQeQmPWevfnk/DM5qcLcYlA8ys6Y=
|
||||
modernc.org/token v1.1.0/go.mod h1:UGzOrNV1mAFSEB63lOFHIpNRUVMvYTc6yu1SMY/XTDM=
|
||||
@@ -0,0 +1,60 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"go-backend/internal/config"
|
||||
httpserver "go-backend/internal/http"
|
||||
"go-backend/internal/http/handler"
|
||||
"go-backend/internal/store/sqlite"
|
||||
)
|
||||
|
||||
type App struct {
|
||||
cfg config.Config
|
||||
server *http.Server
|
||||
repo *sqlite.Repository
|
||||
h *handler.Handler
|
||||
}
|
||||
|
||||
func New(cfg config.Config) (*App, error) {
|
||||
repo, err := sqlite.Open(cfg.DBPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("open sqlite: %w", err)
|
||||
}
|
||||
|
||||
h := handler.New(repo, cfg.JWTSecret)
|
||||
router := httpserver.NewRouter(h, cfg.JWTSecret)
|
||||
|
||||
s := &http.Server{
|
||||
Addr: cfg.Addr,
|
||||
Handler: router,
|
||||
ReadTimeout: 30 * time.Second,
|
||||
ReadHeaderTimeout: 5 * time.Second,
|
||||
WriteTimeout: 30 * time.Second,
|
||||
IdleTimeout: 60 * time.Second,
|
||||
}
|
||||
|
||||
return &App{cfg: cfg, server: s, repo: repo, h: h}, nil
|
||||
}
|
||||
|
||||
func (a *App) Run() error {
|
||||
if a.h != nil {
|
||||
a.h.StartBackgroundJobs()
|
||||
}
|
||||
return a.server.ListenAndServe()
|
||||
}
|
||||
|
||||
func (a *App) Shutdown(ctx context.Context) error {
|
||||
if a.h != nil {
|
||||
a.h.StopBackgroundJobs()
|
||||
}
|
||||
shutdownErr := a.server.Shutdown(ctx)
|
||||
closeErr := a.repo.Close()
|
||||
if shutdownErr != nil {
|
||||
return shutdownErr
|
||||
}
|
||||
return closeErr
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"crypto/hmac"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"strconv"
|
||||
"time"
|
||||
)
|
||||
|
||||
const (
|
||||
algorithm = "HmacSHA256"
|
||||
expireTime = 90 * 24 * time.Hour
|
||||
)
|
||||
|
||||
type Claims struct {
|
||||
Sub string `json:"sub"`
|
||||
Iat int64 `json:"iat"`
|
||||
Exp int64 `json:"exp"`
|
||||
User string `json:"user"`
|
||||
Name string `json:"name"`
|
||||
RoleID int `json:"role_id"`
|
||||
}
|
||||
|
||||
type tokenHeader struct {
|
||||
Alg string `json:"alg"`
|
||||
Typ string `json:"typ"`
|
||||
}
|
||||
|
||||
func GenerateToken(userID int64, username string, roleID int, secret string) (string, error) {
|
||||
now := time.Now()
|
||||
header := tokenHeader{Alg: algorithm, Typ: "JWT"}
|
||||
claims := Claims{
|
||||
Sub: strconv.FormatInt(userID, 10),
|
||||
Iat: now.Unix(),
|
||||
Exp: now.Add(expireTime).Unix(),
|
||||
User: username,
|
||||
Name: username,
|
||||
RoleID: roleID,
|
||||
}
|
||||
|
||||
headerPart, err := encodeJSON(header)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
payloadPart, err := encodeJSON(claims)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
sig := sign(headerPart+"."+payloadPart, secret)
|
||||
|
||||
return headerPart + "." + payloadPart + "." + sig, nil
|
||||
}
|
||||
|
||||
func ValidateToken(token, secret string) (Claims, bool) {
|
||||
claims, err := ParseClaims(token, secret)
|
||||
if err != nil {
|
||||
return Claims{}, false
|
||||
}
|
||||
return claims, true
|
||||
}
|
||||
|
||||
func ParseClaims(token, secret string) (Claims, error) {
|
||||
parts := splitToken(token)
|
||||
if len(parts) != 3 {
|
||||
return Claims{}, errors.New("invalid token")
|
||||
}
|
||||
|
||||
signedContent := parts[0] + "." + parts[1]
|
||||
expected := sign(signedContent, secret)
|
||||
if !hmac.Equal([]byte(expected), []byte(parts[2])) {
|
||||
return Claims{}, errors.New("invalid signature")
|
||||
}
|
||||
|
||||
payloadBytes, err := base64.RawURLEncoding.DecodeString(parts[1])
|
||||
if err != nil {
|
||||
return Claims{}, err
|
||||
}
|
||||
|
||||
var claims Claims
|
||||
if err := json.Unmarshal(payloadBytes, &claims); err != nil {
|
||||
return Claims{}, err
|
||||
}
|
||||
|
||||
if claims.Exp <= time.Now().Unix() {
|
||||
return Claims{}, errors.New("token expired")
|
||||
}
|
||||
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
func splitToken(token string) []string {
|
||||
parts := make([]string, 0, 3)
|
||||
current := ""
|
||||
for i := 0; i < len(token); i++ {
|
||||
if token[i] == '.' {
|
||||
parts = append(parts, current)
|
||||
current = ""
|
||||
continue
|
||||
}
|
||||
current += string(token[i])
|
||||
}
|
||||
parts = append(parts, current)
|
||||
return parts
|
||||
}
|
||||
|
||||
func encodeJSON(v interface{}) (string, error) {
|
||||
raw, err := json.Marshal(v)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return base64.RawURLEncoding.EncodeToString(raw), nil
|
||||
}
|
||||
|
||||
func sign(content, secret string) string {
|
||||
h := hmac.New(sha256.New, []byte(secret))
|
||||
h.Write([]byte(content))
|
||||
return base64.RawURLEncoding.EncodeToString(h.Sum(nil))
|
||||
}
|
||||
@@ -0,0 +1,28 @@
|
||||
package config
|
||||
|
||||
import "os"
|
||||
|
||||
type Config struct {
|
||||
Addr string
|
||||
DBPath string
|
||||
JWTSecret string
|
||||
LogDir string
|
||||
}
|
||||
|
||||
func FromEnv() Config {
|
||||
cfg := Config{
|
||||
Addr: getEnv("SERVER_ADDR", ":6365"),
|
||||
DBPath: getEnv("DB_PATH", "/app/data/gost.db"),
|
||||
JWTSecret: getEnv("JWT_SECRET", ""),
|
||||
LogDir: getEnv("LOG_DIR", "/app/logs"),
|
||||
}
|
||||
|
||||
return cfg
|
||||
}
|
||||
|
||||
func getEnv(key, fallback string) string {
|
||||
if v := os.Getenv(key); v != "" {
|
||||
return v
|
||||
}
|
||||
return fallback
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,55 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
)
|
||||
|
||||
func TestBuildForwardControlServiceNamesPauseResume(t *testing.T) {
|
||||
base := "12_34_56"
|
||||
want := []string{base + "_tcp", base + "_udp"}
|
||||
|
||||
for _, command := range []string{"PauseService", "ResumeService"} {
|
||||
got := buildForwardControlServiceNames(base, command)
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("command %s expected %v, got %v", command, want, got)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildForwardControlServiceNamesDelete(t *testing.T) {
|
||||
base := "12_34_56"
|
||||
want := []string{base, base + "_tcp", base + "_udp"}
|
||||
got := buildForwardControlServiceNames(base, " DeleteService ")
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("expected %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildForwardServiceBaseCandidates(t *testing.T) {
|
||||
got := buildForwardServiceBaseCandidates(12, 34, 56, []int64{56, 78, 90})
|
||||
want := []string{"12_34_56", "12_34_78", "12_34_90", "12_34_0"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("expected %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestBuildForwardServiceBaseCandidatesWithZeroPreferred(t *testing.T) {
|
||||
got := buildForwardServiceBaseCandidates(12, 34, 0, []int64{78, 0, 90})
|
||||
want := []string{"12_34_0", "12_34_78", "12_34_90"}
|
||||
if !reflect.DeepEqual(got, want) {
|
||||
t.Fatalf("expected %v, got %v", want, got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestShouldTryLegacySingleService(t *testing.T) {
|
||||
if !shouldTryLegacySingleService("PauseService") {
|
||||
t.Fatalf("PauseService should require legacy fallback")
|
||||
}
|
||||
if !shouldTryLegacySingleService("resumeService") {
|
||||
t.Fatalf("ResumeService should require legacy fallback")
|
||||
}
|
||||
if shouldTryLegacySingleService("DeleteService") {
|
||||
t.Fatalf("DeleteService should not require legacy fallback")
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,338 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
)
|
||||
|
||||
const bytesPerGB int64 = 1024 * 1024 * 1024
|
||||
|
||||
type userTunnelPolicy struct {
|
||||
ID int64
|
||||
UserID int64
|
||||
TunnelID int64
|
||||
Flow int64
|
||||
InFlow int64
|
||||
OutFlow int64
|
||||
ExpTime int64
|
||||
Status int
|
||||
}
|
||||
|
||||
type gostConfigSnapshot struct {
|
||||
Services []namedConfigItem `json:"services"`
|
||||
Chains []namedConfigItem `json:"chains"`
|
||||
Limiters []namedConfigItem `json:"limiters"`
|
||||
}
|
||||
|
||||
type namedConfigItem struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
func (h *Handler) processFlowItem(item flowItem) {
|
||||
serviceName := strings.TrimSpace(item.N)
|
||||
if serviceName == "" || serviceName == "web_api" {
|
||||
return
|
||||
}
|
||||
|
||||
forwardID, userID, userTunnelID, ok := parseFlowServiceIDs(serviceName)
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
inFlow, outFlow := h.scaleFlowByTunnel(forwardID, item.D, item.U)
|
||||
_ = h.repo.AddFlow(forwardID, userID, userTunnelID, inFlow, outFlow)
|
||||
|
||||
if userTunnelID > 0 {
|
||||
h.enforceFlowPolicies(userID, userTunnelID)
|
||||
}
|
||||
}
|
||||
|
||||
func parseFlowServiceIDs(serviceName string) (int64, int64, int64, bool) {
|
||||
parts := strings.Split(serviceName, "_")
|
||||
if len(parts) < 3 {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
|
||||
forwardID, err1 := strconv.ParseInt(parts[0], 10, 64)
|
||||
userID, err2 := strconv.ParseInt(parts[1], 10, 64)
|
||||
userTunnelID, err3 := strconv.ParseInt(parts[2], 10, 64)
|
||||
if err1 != nil || err2 != nil || err3 != nil || forwardID <= 0 || userID <= 0 {
|
||||
return 0, 0, 0, false
|
||||
}
|
||||
|
||||
return forwardID, userID, userTunnelID, true
|
||||
}
|
||||
|
||||
func (h *Handler) scaleFlowByTunnel(forwardID int64, inFlow int64, outFlow int64) (int64, int64) {
|
||||
forward, err := h.getForwardRecord(forwardID)
|
||||
if err != nil || forward == nil {
|
||||
return inFlow, outFlow
|
||||
}
|
||||
|
||||
tunnel, err := h.getTunnelRecord(forward.TunnelID)
|
||||
if err != nil || tunnel == nil {
|
||||
return inFlow, outFlow
|
||||
}
|
||||
|
||||
scaledIn := int64(float64(inFlow)*tunnel.TrafficRatio) * tunnel.Flow
|
||||
scaledOut := int64(float64(outFlow)*tunnel.TrafficRatio) * tunnel.Flow
|
||||
return scaledIn, scaledOut
|
||||
}
|
||||
|
||||
func (h *Handler) enforceFlowPolicies(userID int64, userTunnelID int64) {
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
if h.shouldPauseUser(userID, now) {
|
||||
h.pauseUserForwards(userID, now)
|
||||
}
|
||||
|
||||
policy, err := h.getUserTunnelPolicy(userTunnelID)
|
||||
if err != nil || policy == nil {
|
||||
return
|
||||
}
|
||||
|
||||
if shouldPauseUserTunnel(policy, now) {
|
||||
h.pauseUserTunnelForwards(policy.UserID, policy.TunnelID, now)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) shouldPauseUser(userID int64, now int64) bool {
|
||||
user, err := h.repo.GetUserByID(userID)
|
||||
if err != nil || user == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
flowLimit := user.Flow * bytesPerGB
|
||||
current := user.InFlow + user.OutFlow
|
||||
if flowLimit < current {
|
||||
return true
|
||||
}
|
||||
if user.ExpTime > 0 && user.ExpTime <= now {
|
||||
return true
|
||||
}
|
||||
return user.Status != 1
|
||||
}
|
||||
|
||||
func shouldPauseUserTunnel(policy *userTunnelPolicy, now int64) bool {
|
||||
if policy == nil {
|
||||
return false
|
||||
}
|
||||
|
||||
flowLimit := policy.Flow * bytesPerGB
|
||||
current := policy.InFlow + policy.OutFlow
|
||||
if current >= flowLimit {
|
||||
return true
|
||||
}
|
||||
if policy.ExpTime > 0 && policy.ExpTime <= now {
|
||||
return true
|
||||
}
|
||||
return policy.Status != 1
|
||||
}
|
||||
|
||||
func (h *Handler) getUserTunnelPolicy(userTunnelID int64) (*userTunnelPolicy, error) {
|
||||
if userTunnelID <= 0 {
|
||||
return nil, nil
|
||||
}
|
||||
|
||||
row := h.repo.DB().QueryRow(`
|
||||
SELECT id, user_id, tunnel_id, flow, in_flow, out_flow, exp_time, status
|
||||
FROM user_tunnel
|
||||
WHERE id = ?
|
||||
LIMIT 1
|
||||
`, userTunnelID)
|
||||
|
||||
var policy userTunnelPolicy
|
||||
if err := row.Scan(&policy.ID, &policy.UserID, &policy.TunnelID, &policy.Flow, &policy.InFlow, &policy.OutFlow, &policy.ExpTime, &policy.Status); err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
return nil, nil
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
return &policy, nil
|
||||
}
|
||||
|
||||
func (h *Handler) pauseUserForwards(userID int64, now int64) {
|
||||
forwards, err := h.listActiveForwardsByUser(userID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h.pauseForwardRecords(forwards, now)
|
||||
}
|
||||
|
||||
func (h *Handler) pauseUserTunnelForwards(userID int64, tunnelID int64, now int64) {
|
||||
forwards, err := h.listActiveForwardsByUserTunnel(userID, tunnelID)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
h.pauseForwardRecords(forwards, now)
|
||||
}
|
||||
|
||||
func (h *Handler) pauseForwardRecords(forwards []forwardRecord, now int64) {
|
||||
for i := range forwards {
|
||||
forward := forwards[i]
|
||||
_ = h.controlForwardServices(&forward, "PauseService", false)
|
||||
_, _ = h.repo.DB().Exec(`UPDATE forward SET status = 0, updated_time = ? WHERE id = ?`, now, forward.ID)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) listActiveForwardsByUser(userID int64) ([]forwardRecord, error) {
|
||||
rows, err := h.repo.DB().Query(`
|
||||
SELECT id, user_id, user_name, name, tunnel_id, remote_addr, strategy, status
|
||||
FROM forward
|
||||
WHERE user_id = ? AND status = 1
|
||||
ORDER BY id ASC
|
||||
`, userID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanForwardRecords(rows)
|
||||
}
|
||||
|
||||
func (h *Handler) listActiveForwardsByUserTunnel(userID int64, tunnelID int64) ([]forwardRecord, error) {
|
||||
rows, err := h.repo.DB().Query(`
|
||||
SELECT id, user_id, user_name, name, tunnel_id, remote_addr, strategy, status
|
||||
FROM forward
|
||||
WHERE user_id = ? AND tunnel_id = ? AND status = 1
|
||||
ORDER BY id ASC
|
||||
`, userID, tunnelID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
defer rows.Close()
|
||||
|
||||
return scanForwardRecords(rows)
|
||||
}
|
||||
|
||||
func scanForwardRecords(rows *sql.Rows) ([]forwardRecord, error) {
|
||||
out := make([]forwardRecord, 0)
|
||||
for rows.Next() {
|
||||
var record forwardRecord
|
||||
if err := rows.Scan(&record.ID, &record.UserID, &record.UserName, &record.Name, &record.TunnelID, &record.RemoteAddr, &record.Strategy, &record.Status); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if strings.TrimSpace(record.Strategy) == "" {
|
||||
record.Strategy = "fifo"
|
||||
}
|
||||
out = append(out, record)
|
||||
}
|
||||
if err := rows.Err(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return out, nil
|
||||
}
|
||||
|
||||
func (h *Handler) cleanNodeConfigs(nodeID int64, rawConfig string) {
|
||||
if h == nil || h.repo == nil || h.repo.DB() == nil || nodeID <= 0 {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(rawConfig) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var snapshot gostConfigSnapshot
|
||||
if err := json.Unmarshal([]byte(rawConfig), &snapshot); err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
h.cleanOrphanedServices(nodeID, snapshot.Services)
|
||||
h.cleanOrphanedChains(nodeID, snapshot.Chains)
|
||||
h.cleanOrphanedLimiters(nodeID, snapshot.Limiters)
|
||||
}
|
||||
|
||||
func (h *Handler) cleanOrphanedServices(nodeID int64, services []namedConfigItem) {
|
||||
for _, item := range services {
|
||||
name := strings.TrimSpace(item.Name)
|
||||
if name == "" || name == "web_api" {
|
||||
continue
|
||||
}
|
||||
|
||||
parts := strings.Split(name, "_")
|
||||
if len(parts) >= 3 {
|
||||
forwardID, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err == nil && forwardID > 0 && !h.forwardExists(forwardID) {
|
||||
_, _ = h.sendNodeCommand(nodeID, "DeleteService", map[string]interface{}{"services": []string{name, parts[0] + "_" + parts[1] + "_" + parts[2], parts[0] + "_" + parts[1] + "_" + parts[2] + "_tcp", parts[0] + "_" + parts[1] + "_" + parts[2] + "_udp"}}, false, true)
|
||||
continue
|
||||
}
|
||||
}
|
||||
suffix := parts[len(parts)-1]
|
||||
|
||||
switch suffix {
|
||||
case "tls":
|
||||
tunnelID, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil || tunnelID <= 0 || h.tunnelExists(tunnelID) {
|
||||
continue
|
||||
}
|
||||
_, _ = h.sendNodeCommand(nodeID, "DeleteService", map[string]interface{}{"services": []string{name}}, false, true)
|
||||
case "tcp":
|
||||
if len(parts) < 4 {
|
||||
continue
|
||||
}
|
||||
forwardID, err := strconv.ParseInt(parts[0], 10, 64)
|
||||
if err != nil || forwardID <= 0 || h.forwardExists(forwardID) {
|
||||
continue
|
||||
}
|
||||
base := strings.TrimSuffix(name, "_tcp")
|
||||
_, _ = h.sendNodeCommand(nodeID, "DeleteService", map[string]interface{}{"services": []string{base + "_tcp", base + "_udp"}}, false, true)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) cleanOrphanedChains(nodeID int64, chains []namedConfigItem) {
|
||||
for _, item := range chains {
|
||||
name := strings.TrimSpace(item.Name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
idx := strings.LastIndex(name, "_")
|
||||
if idx <= 0 || idx >= len(name)-1 {
|
||||
continue
|
||||
}
|
||||
tunnelID, err := strconv.ParseInt(name[idx+1:], 10, 64)
|
||||
if err != nil || tunnelID <= 0 || h.tunnelExists(tunnelID) {
|
||||
continue
|
||||
}
|
||||
_, _ = h.sendNodeCommand(nodeID, "DeleteChains", map[string]interface{}{"chain": name}, false, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) cleanOrphanedLimiters(nodeID int64, limiters []namedConfigItem) {
|
||||
for _, item := range limiters {
|
||||
name := strings.TrimSpace(item.Name)
|
||||
if name == "" || h.speedLimiterExists(name) {
|
||||
continue
|
||||
}
|
||||
_, _ = h.sendNodeCommand(nodeID, "DeleteLimiters", map[string]interface{}{"limiter": name}, false, true)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) tunnelExists(tunnelID int64) bool {
|
||||
var count int
|
||||
err := h.repo.DB().QueryRow(`SELECT COUNT(1) FROM tunnel WHERE id = ?`, tunnelID).Scan(&count)
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
func (h *Handler) forwardExists(forwardID int64) bool {
|
||||
var count int
|
||||
err := h.repo.DB().QueryRow(`SELECT COUNT(1) FROM forward WHERE id = ?`, forwardID).Scan(&count)
|
||||
return err == nil && count > 0
|
||||
}
|
||||
|
||||
func (h *Handler) speedLimiterExists(name string) bool {
|
||||
if name == "" {
|
||||
return false
|
||||
}
|
||||
id, err := strconv.ParseInt(name, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return false
|
||||
}
|
||||
|
||||
var count int
|
||||
err = h.repo.DB().QueryRow(`SELECT COUNT(1) FROM speed_limit WHERE id = ?`, id).Scan(&count)
|
||||
return err == nil && count > 0
|
||||
}
|
||||
@@ -0,0 +1,988 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"sort"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"go-backend/internal/auth"
|
||||
"go-backend/internal/http/middleware"
|
||||
"go-backend/internal/http/response"
|
||||
"go-backend/internal/security"
|
||||
"go-backend/internal/store/sqlite"
|
||||
"go-backend/internal/ws"
|
||||
)
|
||||
|
||||
type Handler struct {
|
||||
repo *sqlite.Repository
|
||||
jwtSecret string
|
||||
wsServer *ws.Server
|
||||
|
||||
jobsMu sync.Mutex
|
||||
jobsCancel context.CancelFunc
|
||||
jobsStarted bool
|
||||
jobsWG sync.WaitGroup
|
||||
}
|
||||
|
||||
type loginRequest struct {
|
||||
Username string `json:"username"`
|
||||
Password string `json:"password"`
|
||||
CaptchaID string `json:"captchaId"`
|
||||
}
|
||||
|
||||
type nameRequest struct {
|
||||
Name string `json:"name"`
|
||||
}
|
||||
|
||||
type configSingleRequest struct {
|
||||
Name string `json:"name"`
|
||||
Value string `json:"value"`
|
||||
}
|
||||
|
||||
type changePasswordRequest struct {
|
||||
NewUsername string `json:"newUsername"`
|
||||
CurrentPassword string `json:"currentPassword"`
|
||||
NewPassword string `json:"newPassword"`
|
||||
ConfirmPassword string `json:"confirmPassword"`
|
||||
}
|
||||
|
||||
type flowItem struct {
|
||||
N string `json:"n"`
|
||||
U int64 `json:"u"`
|
||||
D int64 `json:"d"`
|
||||
}
|
||||
|
||||
func New(repo *sqlite.Repository, jwtSecret string) *Handler {
|
||||
return &Handler{
|
||||
repo: repo,
|
||||
jwtSecret: jwtSecret,
|
||||
wsServer: ws.NewServer(repo, jwtSecret),
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) WebSocketHandler() http.Handler {
|
||||
return h.wsServer
|
||||
}
|
||||
|
||||
func (h *Handler) Register(mux *http.ServeMux) {
|
||||
mux.HandleFunc("/api/v1/user/login", h.login)
|
||||
mux.HandleFunc("/api/v1/user/list", h.userList)
|
||||
mux.HandleFunc("/api/v1/user/create", h.userCreate)
|
||||
mux.HandleFunc("/api/v1/user/update", h.userUpdate)
|
||||
mux.HandleFunc("/api/v1/user/delete", h.userDelete)
|
||||
mux.HandleFunc("/api/v1/user/reset", h.userResetFlow)
|
||||
mux.HandleFunc("/api/v1/config/get", h.getConfigByName)
|
||||
mux.HandleFunc("/api/v1/config/list", h.getConfigs)
|
||||
mux.HandleFunc("/api/v1/config/update", h.updateConfigs)
|
||||
mux.HandleFunc("/api/v1/config/update-single", h.updateSingleConfig)
|
||||
mux.HandleFunc("/api/v1/captcha/check", h.checkCaptcha)
|
||||
mux.HandleFunc("/api/v1/user/package", h.userPackage)
|
||||
mux.HandleFunc("/api/v1/user/updatePassword", h.updatePassword)
|
||||
mux.HandleFunc("/api/v1/node/list", h.nodeList)
|
||||
mux.HandleFunc("/api/v1/node/create", h.nodeCreate)
|
||||
mux.HandleFunc("/api/v1/node/update", h.nodeUpdate)
|
||||
mux.HandleFunc("/api/v1/node/delete", h.nodeDelete)
|
||||
mux.HandleFunc("/api/v1/node/install", h.nodeInstall)
|
||||
mux.HandleFunc("/api/v1/node/update-order", h.nodeUpdateOrder)
|
||||
mux.HandleFunc("/api/v1/node/batch-delete", h.nodeBatchDelete)
|
||||
mux.HandleFunc("/api/v1/node/check-status", h.nodeCheckStatus)
|
||||
mux.HandleFunc("/api/v1/tunnel/list", h.tunnelList)
|
||||
mux.HandleFunc("/api/v1/tunnel/create", h.tunnelCreate)
|
||||
mux.HandleFunc("/api/v1/tunnel/get", h.tunnelGet)
|
||||
mux.HandleFunc("/api/v1/tunnel/update", h.tunnelUpdate)
|
||||
mux.HandleFunc("/api/v1/tunnel/delete", h.tunnelDelete)
|
||||
mux.HandleFunc("/api/v1/tunnel/diagnose", h.tunnelDiagnose)
|
||||
mux.HandleFunc("/api/v1/tunnel/update-order", h.tunnelUpdateOrder)
|
||||
mux.HandleFunc("/api/v1/tunnel/batch-delete", h.tunnelBatchDelete)
|
||||
mux.HandleFunc("/api/v1/tunnel/batch-redeploy", h.tunnelBatchRedeploy)
|
||||
mux.HandleFunc("/api/v1/tunnel/user/assign", h.userTunnelAssign)
|
||||
mux.HandleFunc("/api/v1/tunnel/user/batch-assign", h.userTunnelBatchAssign)
|
||||
mux.HandleFunc("/api/v1/tunnel/user/remove", h.userTunnelRemove)
|
||||
mux.HandleFunc("/api/v1/tunnel/user/update", h.userTunnelUpdate)
|
||||
mux.HandleFunc("/api/v1/forward/list", h.forwardList)
|
||||
mux.HandleFunc("/api/v1/forward/create", h.forwardCreate)
|
||||
mux.HandleFunc("/api/v1/forward/update", h.forwardUpdate)
|
||||
mux.HandleFunc("/api/v1/forward/delete", h.forwardDelete)
|
||||
mux.HandleFunc("/api/v1/forward/force-delete", h.forwardForceDelete)
|
||||
mux.HandleFunc("/api/v1/forward/pause", h.forwardPause)
|
||||
mux.HandleFunc("/api/v1/forward/resume", h.forwardResume)
|
||||
mux.HandleFunc("/api/v1/forward/diagnose", h.forwardDiagnose)
|
||||
mux.HandleFunc("/api/v1/forward/update-order", h.forwardUpdateOrder)
|
||||
mux.HandleFunc("/api/v1/forward/batch-delete", h.forwardBatchDelete)
|
||||
mux.HandleFunc("/api/v1/forward/batch-pause", h.forwardBatchPause)
|
||||
mux.HandleFunc("/api/v1/forward/batch-resume", h.forwardBatchResume)
|
||||
mux.HandleFunc("/api/v1/forward/batch-redeploy", h.forwardBatchRedeploy)
|
||||
mux.HandleFunc("/api/v1/forward/batch-change-tunnel", h.forwardBatchChangeTunnel)
|
||||
mux.HandleFunc("/api/v1/speed-limit/list", h.speedLimitList)
|
||||
mux.HandleFunc("/api/v1/speed-limit/create", h.speedLimitCreate)
|
||||
mux.HandleFunc("/api/v1/speed-limit/update", h.speedLimitUpdate)
|
||||
mux.HandleFunc("/api/v1/speed-limit/delete", h.speedLimitDelete)
|
||||
mux.HandleFunc("/api/v1/speed-limit/tunnels", h.tunnelList)
|
||||
mux.HandleFunc("/api/v1/tunnel/user/tunnel", h.userTunnelVisibleList)
|
||||
mux.HandleFunc("/api/v1/tunnel/user/list", h.userTunnelList)
|
||||
mux.HandleFunc("/api/v1/group/tunnel/list", h.tunnelGroupList)
|
||||
mux.HandleFunc("/api/v1/group/tunnel/create", h.groupTunnelCreate)
|
||||
mux.HandleFunc("/api/v1/group/tunnel/update", h.groupTunnelUpdate)
|
||||
mux.HandleFunc("/api/v1/group/tunnel/delete", h.groupTunnelDelete)
|
||||
mux.HandleFunc("/api/v1/group/tunnel/assign", h.groupTunnelAssign)
|
||||
mux.HandleFunc("/api/v1/group/user/list", h.userGroupList)
|
||||
mux.HandleFunc("/api/v1/group/user/create", h.groupUserCreate)
|
||||
mux.HandleFunc("/api/v1/group/user/update", h.groupUserUpdate)
|
||||
mux.HandleFunc("/api/v1/group/user/delete", h.groupUserDelete)
|
||||
mux.HandleFunc("/api/v1/group/user/assign", h.groupUserAssign)
|
||||
mux.HandleFunc("/api/v1/group/permission/list", h.groupPermissionList)
|
||||
mux.HandleFunc("/api/v1/group/permission/assign", h.groupPermissionAssign)
|
||||
mux.HandleFunc("/api/v1/group/permission/remove", h.groupPermissionRemove)
|
||||
mux.HandleFunc("/api/v1/open_api/sub_store", h.openAPISubStore)
|
||||
|
||||
mux.HandleFunc("/flow/test", h.flowTest)
|
||||
mux.HandleFunc("/flow/config", h.flowConfig)
|
||||
mux.HandleFunc("/flow/upload", h.flowUpload)
|
||||
mux.HandleFunc("/error", h.errorPage)
|
||||
}
|
||||
|
||||
func (h *Handler) login(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
var req loginRequest
|
||||
if err := decodeJSON(r.Body, &req); err != nil {
|
||||
response.WriteJSON(w, response.Err(500, "请求参数错误"))
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.Username) == "" {
|
||||
response.WriteJSON(w, response.Err(500, "用户名不能为空"))
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Password) == "" {
|
||||
response.WriteJSON(w, response.Err(500, "密码不能为空"))
|
||||
return
|
||||
}
|
||||
|
||||
captchaEnabled, err := h.captchaEnabled()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if captchaEnabled {
|
||||
if strings.TrimSpace(req.CaptchaID) == "" {
|
||||
response.WriteJSON(w, response.ErrDefault("验证码校验失败"))
|
||||
return
|
||||
}
|
||||
|
||||
secretCfg, err := h.repo.GetConfigByName("cloudflare_secret_key")
|
||||
if err != nil || secretCfg == nil || secretCfg.Value == "" {
|
||||
response.WriteJSON(w, response.ErrDefault("验证码配置错误:未配置Secret Key"))
|
||||
return
|
||||
}
|
||||
|
||||
if !h.verifyCloudflareTurnstile(req.CaptchaID, secretCfg.Value) {
|
||||
response.WriteJSON(w, response.ErrDefault("验证码校验失败"))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
user, err := h.repo.GetUserByUsername(req.Username)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
response.WriteJSON(w, response.ErrDefault("账号或密码错误"))
|
||||
return
|
||||
}
|
||||
if user.Pwd != security.MD5(req.Password) {
|
||||
response.WriteJSON(w, response.ErrDefault("账号或密码错误"))
|
||||
return
|
||||
}
|
||||
if user.Status == 0 {
|
||||
response.WriteJSON(w, response.ErrDefault("账号被停用"))
|
||||
return
|
||||
}
|
||||
|
||||
token, err := auth.GenerateToken(user.ID, user.User, user.RoleID, h.jwtSecret)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
requirePasswordChange := req.Username == "admin_user" || req.Password == "admin_user"
|
||||
response.WriteJSON(w, response.OK(map[string]interface{}{
|
||||
"token": token,
|
||||
"name": user.User,
|
||||
"role_id": user.RoleID,
|
||||
"requirePasswordChange": requirePasswordChange,
|
||||
}))
|
||||
}
|
||||
|
||||
func (h *Handler) getConfigByName(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
var req nameRequest
|
||||
if err := decodeJSON(r.Body, &req); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault("配置名称不能为空"))
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Name) == "" {
|
||||
response.WriteJSON(w, response.ErrDefault("配置名称不能为空"))
|
||||
return
|
||||
}
|
||||
|
||||
cfg, err := h.repo.GetConfigByName(req.Name)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if cfg == nil {
|
||||
response.WriteJSON(w, response.ErrDefault("配置不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OK(cfg))
|
||||
}
|
||||
|
||||
func (h *Handler) getConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
cfgMap, err := h.repo.ListConfigs()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, response.OK(cfgMap))
|
||||
}
|
||||
|
||||
func (h *Handler) userList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
users, err := h.repo.ListUsers()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, response.OK(users))
|
||||
}
|
||||
|
||||
func (h *Handler) nodeList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.repo.ListNodes()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, response.OK(items))
|
||||
}
|
||||
|
||||
func (h *Handler) tunnelList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.repo.ListTunnels()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, response.OK(items))
|
||||
}
|
||||
|
||||
func (h *Handler) forwardList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
userID, roleID, err := userRoleFromRequest(r)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(401, "无效的token或token已过期"))
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.repo.ListForwards()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if roleID != 0 {
|
||||
filtered := make([]map[string]interface{}, 0, len(items))
|
||||
for _, item := range items {
|
||||
if asInt64(item["userId"], 0) == userID {
|
||||
filtered = append(filtered, item)
|
||||
}
|
||||
}
|
||||
items = filtered
|
||||
}
|
||||
response.WriteJSON(w, response.OK(items))
|
||||
}
|
||||
|
||||
func (h *Handler) speedLimitList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.repo.ListSpeedLimits()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, response.OK(items))
|
||||
}
|
||||
|
||||
func (h *Handler) openAPISubStore(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodGet {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
if h == nil || h.repo == nil || h.repo.DB() == nil {
|
||||
response.WriteJSON(w, response.Err(-2, "database unavailable"))
|
||||
return
|
||||
}
|
||||
|
||||
username := strings.TrimSpace(r.URL.Query().Get("user"))
|
||||
password := strings.TrimSpace(r.URL.Query().Get("pwd"))
|
||||
tunnel := strings.TrimSpace(r.URL.Query().Get("tunnel"))
|
||||
if tunnel == "" {
|
||||
tunnel = "-1"
|
||||
}
|
||||
|
||||
if username == "" {
|
||||
response.WriteJSON(w, response.ErrDefault("用户不能为空"))
|
||||
return
|
||||
}
|
||||
if password == "" {
|
||||
response.WriteJSON(w, response.ErrDefault("密码不能为空"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.repo.GetUserByUsername(username)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if user == nil || user.Pwd != security.MD5(password) {
|
||||
response.WriteJSON(w, response.ErrDefault("鉴权失败"))
|
||||
return
|
||||
}
|
||||
|
||||
const giga = int64(1024 * 1024 * 1024)
|
||||
headerValue := ""
|
||||
|
||||
if tunnel == "-1" {
|
||||
headerValue = buildSubscriptionHeader(user.OutFlow, user.InFlow, user.Flow*giga, user.ExpTime/1000)
|
||||
} else {
|
||||
tunnelID, parseErr := strconv.ParseInt(tunnel, 10, 64)
|
||||
if parseErr != nil || tunnelID <= 0 {
|
||||
response.WriteJSON(w, response.ErrDefault("隧道不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
var userID int64
|
||||
var inFlow int64
|
||||
var outFlow int64
|
||||
var flow int64
|
||||
var expTime int64
|
||||
err = h.repo.DB().QueryRow(`SELECT user_id, in_flow, out_flow, flow, exp_time FROM user_tunnel WHERE id = ? LIMIT 1`, tunnelID).
|
||||
Scan(&userID, &inFlow, &outFlow, &flow, &expTime)
|
||||
if err != nil {
|
||||
if err == sql.ErrNoRows {
|
||||
response.WriteJSON(w, response.ErrDefault("隧道不存在"))
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if userID != user.ID {
|
||||
response.WriteJSON(w, response.ErrDefault("隧道不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
headerValue = buildSubscriptionHeader(outFlow, inFlow, flow*giga, expTime/1000)
|
||||
}
|
||||
|
||||
w.Header().Set("subscription-userinfo", headerValue)
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte(headerValue))
|
||||
}
|
||||
|
||||
func (h *Handler) errorPage(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/html; charset=UTF-8")
|
||||
w.WriteHeader(http.StatusNotFound)
|
||||
_, _ = w.Write([]byte("<!DOCTYPE html><html lang='zh-CN'><head><meta charset='UTF-8'><meta name='viewport' content='width=device-width, initial-scale=1.0'><title>错误 404</title></head><body><div style='min-height:100vh;display:flex;align-items:center;justify-content:center;flex-direction:column;font-family:-apple-system,BlinkMacSystemFont,Segoe UI,Arial,sans-serif;'><div style='font-size:6rem;color:#333;font-weight:300;'>404</div><div style='font-size:1.2rem;color:#666;'>你推开了后端的大门,却发现里面只有寂寞。</div></div></body></html>"))
|
||||
}
|
||||
|
||||
func buildSubscriptionHeader(upload, download, total, expire int64) string {
|
||||
return fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", download, upload, total, expire)
|
||||
}
|
||||
|
||||
func (h *Handler) userTunnelVisibleList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
userID, roleID, err := userRoleFromRequest(r)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(401, "无效的token或token已过期"))
|
||||
return
|
||||
}
|
||||
|
||||
items := make([]map[string]interface{}, 0)
|
||||
if roleID == 0 {
|
||||
items, err = h.repo.ListEnabledTunnelSummaries()
|
||||
} else {
|
||||
items, err = h.repo.ListUserAccessibleTunnels(userID)
|
||||
}
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, response.OK(items))
|
||||
}
|
||||
|
||||
func (h *Handler) userTunnelList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
var req struct {
|
||||
UserID int64 `json:"userId"`
|
||||
}
|
||||
if err := decodeJSON(r.Body, &req); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault("请求参数错误"))
|
||||
return
|
||||
}
|
||||
if req.UserID <= 0 {
|
||||
response.WriteJSON(w, response.OK([]interface{}{}))
|
||||
return
|
||||
}
|
||||
|
||||
tunnels, err := h.repo.GetUserPackageTunnels(req.UserID)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
out := make([]map[string]interface{}, 0, len(tunnels))
|
||||
for _, t := range tunnels {
|
||||
item := map[string]interface{}{
|
||||
"id": t.ID,
|
||||
"userId": t.UserID,
|
||||
"tunnelId": t.TunnelID,
|
||||
"tunnelName": t.TunnelName,
|
||||
"status": 1,
|
||||
"flow": t.Flow,
|
||||
"num": t.Num,
|
||||
"expTime": t.ExpTime,
|
||||
"flowResetTime": t.FlowResetTime,
|
||||
"inFlow": t.InFlow,
|
||||
"outFlow": t.OutFlow,
|
||||
"tunnelFlow": t.TunnelFlow,
|
||||
"speedId": nil,
|
||||
"speedLimitName": nil,
|
||||
}
|
||||
if t.SpeedID.Valid {
|
||||
item["speedId"] = t.SpeedID.Int64
|
||||
}
|
||||
if t.SpeedLimit.Valid {
|
||||
item["speedLimitName"] = t.SpeedLimit.String
|
||||
}
|
||||
out = append(out, item)
|
||||
}
|
||||
response.WriteJSON(w, response.OK(out))
|
||||
}
|
||||
|
||||
func (h *Handler) tunnelGroupList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.repo.ListTunnelGroups()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, response.OK(items))
|
||||
}
|
||||
|
||||
func (h *Handler) userGroupList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.repo.ListUserGroups()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, response.OK(items))
|
||||
}
|
||||
|
||||
func (h *Handler) groupPermissionList(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
items, err := h.repo.ListGroupPermissions()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, response.OK(items))
|
||||
}
|
||||
|
||||
func (h *Handler) checkCaptcha(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
enabled, err := h.captchaEnabled()
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if enabled {
|
||||
response.WriteJSON(w, response.OK(1))
|
||||
return
|
||||
}
|
||||
response.WriteJSON(w, response.OK(0))
|
||||
}
|
||||
|
||||
func (h *Handler) flowTest(w http.ResponseWriter, _ *http.Request) {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte("test"))
|
||||
}
|
||||
|
||||
func (h *Handler) flowConfig(w http.ResponseWriter, r *http.Request) {
|
||||
secret := r.URL.Query().Get("secret")
|
||||
node, err := h.repo.GetNodeBySecret(secret)
|
||||
if err != nil || node == nil {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
return
|
||||
}
|
||||
|
||||
rawData, err := readAndDecryptFlowBody(r.Body, secret)
|
||||
if err == nil && strings.TrimSpace(rawData) != "" {
|
||||
h.cleanNodeConfigs(node.ID, rawData)
|
||||
}
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
func (h *Handler) flowUpload(w http.ResponseWriter, r *http.Request) {
|
||||
secret := r.URL.Query().Get("secret")
|
||||
if ok, _ := h.repo.NodeExistsBySecret(secret); !ok {
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
return
|
||||
}
|
||||
|
||||
raw, err := readAndDecryptFlowBody(r.Body, secret)
|
||||
if err == nil && strings.TrimSpace(raw) != "" {
|
||||
var items []flowItem
|
||||
if json.Unmarshal([]byte(raw), &items) == nil {
|
||||
for _, item := range items {
|
||||
h.processFlowItem(item)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
w.Header().Set("Content-Type", "text/plain; charset=utf-8")
|
||||
_, _ = w.Write([]byte("ok"))
|
||||
}
|
||||
|
||||
func (h *Handler) updateConfigs(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
var payload map[string]string
|
||||
if err := decodeJSON(r.Body, &payload); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault("配置数据不能为空"))
|
||||
return
|
||||
}
|
||||
if len(payload) == 0 {
|
||||
response.WriteJSON(w, response.ErrDefault("配置数据不能为空"))
|
||||
return
|
||||
}
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
for k, v := range payload {
|
||||
key := strings.TrimSpace(k)
|
||||
if key == "" {
|
||||
continue
|
||||
}
|
||||
if err := h.repo.UpsertConfig(key, v, now); err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OKEmpty())
|
||||
}
|
||||
|
||||
func (h *Handler) updateSingleConfig(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
var req configSingleRequest
|
||||
if err := decodeJSON(r.Body, &req); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault("配置名称不能为空"))
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Name) == "" {
|
||||
response.WriteJSON(w, response.ErrDefault("配置名称不能为空"))
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.Value) == "" {
|
||||
response.WriteJSON(w, response.ErrDefault("配置值不能为空"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.repo.UpsertConfig(strings.TrimSpace(req.Name), req.Value, time.Now().UnixMilli()); err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OKEmpty())
|
||||
}
|
||||
|
||||
func (h *Handler) userPackage(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims)
|
||||
if !ok {
|
||||
response.WriteJSON(w, response.Err(401, "无效的token或token已过期"))
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := parseUserID(claims.Sub)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(401, "无效的token或token已过期"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.repo.GetUserByID(userID)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
response.WriteJSON(w, response.ErrDefault("用户不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
tunnels, err := h.repo.GetUserPackageTunnels(userID)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
forwards, err := h.repo.GetUserPackageForwards(userID)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
stats, err := h.repo.GetStatisticsFlows(userID, 24)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
sort.Slice(stats, func(i, j int) bool { return stats[i].ID < stats[j].ID })
|
||||
|
||||
tunnelOut := make([]map[string]interface{}, 0, len(tunnels))
|
||||
for _, t := range tunnels {
|
||||
item := map[string]interface{}{
|
||||
"id": t.ID,
|
||||
"userId": t.UserID,
|
||||
"tunnelId": t.TunnelID,
|
||||
"tunnelName": t.TunnelName,
|
||||
"tunnelFlow": t.TunnelFlow,
|
||||
"flow": t.Flow,
|
||||
"inFlow": t.InFlow,
|
||||
"outFlow": t.OutFlow,
|
||||
"num": t.Num,
|
||||
"flowResetTime": t.FlowResetTime,
|
||||
"expTime": t.ExpTime,
|
||||
"speedId": nil,
|
||||
"speedLimitName": nil,
|
||||
"speed": nil,
|
||||
}
|
||||
if t.SpeedID.Valid {
|
||||
item["speedId"] = t.SpeedID.Int64
|
||||
}
|
||||
if t.SpeedLimit.Valid {
|
||||
item["speedLimitName"] = t.SpeedLimit.String
|
||||
}
|
||||
if t.Speed.Valid {
|
||||
item["speed"] = t.Speed.Int64
|
||||
}
|
||||
tunnelOut = append(tunnelOut, item)
|
||||
}
|
||||
|
||||
forwardOut := make([]map[string]interface{}, 0, len(forwards))
|
||||
for _, f := range forwards {
|
||||
item := map[string]interface{}{
|
||||
"id": f.ID,
|
||||
"name": f.Name,
|
||||
"tunnelId": f.TunnelID,
|
||||
"tunnelName": f.TunnelName,
|
||||
"inIp": f.InIP,
|
||||
"inPort": nil,
|
||||
"remoteAddr": f.RemoteAddr,
|
||||
"inFlow": f.InFlow,
|
||||
"outFlow": f.OutFlow,
|
||||
"status": f.Status,
|
||||
"createdTime": f.CreatedAt,
|
||||
}
|
||||
if f.InPort.Valid {
|
||||
item["inPort"] = f.InPort.Int64
|
||||
}
|
||||
forwardOut = append(forwardOut, item)
|
||||
}
|
||||
|
||||
payload := map[string]interface{}{
|
||||
"userInfo": map[string]interface{}{
|
||||
"id": user.ID,
|
||||
"name": user.User,
|
||||
"user": user.User,
|
||||
"status": user.Status,
|
||||
"flow": user.Flow,
|
||||
"inFlow": user.InFlow,
|
||||
"outFlow": user.OutFlow,
|
||||
"num": user.Num,
|
||||
"expTime": user.ExpTime,
|
||||
"flowResetTime": user.FlowResetTime,
|
||||
"createdTime": user.CreatedTime,
|
||||
"updatedTime": nullableNullInt64(user.UpdatedTime),
|
||||
},
|
||||
"tunnelPermissions": tunnelOut,
|
||||
"forwards": forwardOut,
|
||||
"statisticsFlows": stats,
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OK(payload))
|
||||
}
|
||||
|
||||
func (h *Handler) updatePassword(w http.ResponseWriter, r *http.Request) {
|
||||
if r.Method != http.MethodPost {
|
||||
response.WriteJSON(w, response.ErrDefault("请求失败"))
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims)
|
||||
if !ok {
|
||||
response.WriteJSON(w, response.Err(401, "无效的token或token已过期"))
|
||||
return
|
||||
}
|
||||
|
||||
userID, err := parseUserID(claims.Sub)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(401, "无效的token或token已过期"))
|
||||
return
|
||||
}
|
||||
|
||||
var req changePasswordRequest
|
||||
if err := decodeJSON(r.Body, &req); err != nil {
|
||||
response.WriteJSON(w, response.ErrDefault("修改账号密码时发生错误"))
|
||||
return
|
||||
}
|
||||
|
||||
if strings.TrimSpace(req.NewUsername) == "" {
|
||||
response.WriteJSON(w, response.ErrDefault("新用户名不能为空"))
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.CurrentPassword) == "" {
|
||||
response.WriteJSON(w, response.ErrDefault("当前密码不能为空"))
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.NewPassword) == "" {
|
||||
response.WriteJSON(w, response.ErrDefault("新密码不能为空"))
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(req.ConfirmPassword) == "" {
|
||||
response.WriteJSON(w, response.ErrDefault("确认密码不能为空"))
|
||||
return
|
||||
}
|
||||
if req.NewPassword != req.ConfirmPassword {
|
||||
response.WriteJSON(w, response.ErrDefault("新密码和确认密码不匹配"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.repo.GetUserByID(userID)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if user == nil {
|
||||
response.WriteJSON(w, response.ErrDefault("用户不存在"))
|
||||
return
|
||||
}
|
||||
|
||||
if user.Pwd != security.MD5(req.CurrentPassword) {
|
||||
response.WriteJSON(w, response.ErrDefault("当前密码错误"))
|
||||
return
|
||||
}
|
||||
|
||||
exists, err := h.repo.UsernameExistsExceptID(req.NewUsername, userID)
|
||||
if err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
if exists {
|
||||
response.WriteJSON(w, response.ErrDefault("用户名已存在"))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.repo.UpdateUserNameAndPassword(userID, req.NewUsername, security.MD5(req.NewPassword), time.Now().UnixMilli()); err != nil {
|
||||
response.WriteJSON(w, response.Err(-2, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
response.WriteJSON(w, response.OKEmpty())
|
||||
}
|
||||
|
||||
func (h *Handler) captchaEnabled() (bool, error) {
|
||||
cfg, err := h.repo.GetConfigByName("captcha_enabled")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if cfg == nil {
|
||||
return false, nil
|
||||
}
|
||||
return strings.EqualFold(cfg.Value, "true"), nil
|
||||
}
|
||||
|
||||
func decodeJSON(body io.ReadCloser, out interface{}) error {
|
||||
defer body.Close()
|
||||
decoder := json.NewDecoder(body)
|
||||
decoder.DisallowUnknownFields()
|
||||
return decoder.Decode(out)
|
||||
}
|
||||
|
||||
func parseUserID(sub string) (int64, error) {
|
||||
id, err := strconv.ParseInt(sub, 10, 64)
|
||||
if err != nil || id <= 0 {
|
||||
return 0, strconv.ErrSyntax
|
||||
}
|
||||
return id, nil
|
||||
}
|
||||
|
||||
func userIDFromRequest(r *http.Request) (int64, error) {
|
||||
claims, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims)
|
||||
if !ok {
|
||||
return 0, strconv.ErrSyntax
|
||||
}
|
||||
return parseUserID(claims.Sub)
|
||||
}
|
||||
|
||||
func userRoleFromRequest(r *http.Request) (int64, int, error) {
|
||||
claims, ok := r.Context().Value(middleware.ClaimsContextKey).(auth.Claims)
|
||||
if !ok {
|
||||
return 0, 0, strconv.ErrSyntax
|
||||
}
|
||||
userID, err := parseUserID(claims.Sub)
|
||||
if err != nil {
|
||||
return 0, 0, err
|
||||
}
|
||||
return userID, claims.RoleID, nil
|
||||
}
|
||||
|
||||
func nullableNullInt64(v sql.NullInt64) interface{} {
|
||||
if v.Valid {
|
||||
return v.Int64
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func readAndDecryptFlowBody(body io.ReadCloser, secret string) (string, error) {
|
||||
defer body.Close()
|
||||
raw, err := io.ReadAll(body)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
text := strings.TrimSpace(string(raw))
|
||||
if text == "" {
|
||||
return "", nil
|
||||
}
|
||||
|
||||
var wrap struct {
|
||||
Encrypted bool `json:"encrypted"`
|
||||
Data string `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
if err := json.Unmarshal(raw, &wrap); err != nil || !wrap.Encrypted || strings.TrimSpace(wrap.Data) == "" {
|
||||
return text, nil
|
||||
}
|
||||
|
||||
crypto, err := security.NewAESCrypto(secret)
|
||||
if err != nil {
|
||||
return text, nil
|
||||
}
|
||||
plain, err := crypto.Decrypt(wrap.Data)
|
||||
if err != nil {
|
||||
return text, nil
|
||||
}
|
||||
return string(plain), nil
|
||||
}
|
||||
|
||||
func (h *Handler) verifyCloudflareTurnstile(token, secretKey string) bool {
|
||||
if token == "" || secretKey == "" {
|
||||
return false
|
||||
}
|
||||
resp, err := http.PostForm("https://challenges.cloudflare.com/turnstile/v0/siteverify", url.Values{
|
||||
"secret": {secretKey},
|
||||
"response": {token},
|
||||
})
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
var body struct {
|
||||
Success bool `json:"success"`
|
||||
}
|
||||
if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
|
||||
return false
|
||||
}
|
||||
return body.Success
|
||||
}
|
||||
@@ -0,0 +1,270 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"time"
|
||||
)
|
||||
|
||||
func (h *Handler) StartBackgroundJobs() {
|
||||
if h == nil || h.repo == nil || h.repo.DB() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
h.jobsMu.Lock()
|
||||
if h.jobsStarted {
|
||||
h.jobsMu.Unlock()
|
||||
return
|
||||
}
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
h.jobsCancel = cancel
|
||||
h.jobsStarted = true
|
||||
h.jobsWG.Add(2)
|
||||
h.jobsMu.Unlock()
|
||||
|
||||
go h.runHourlyStatsLoop(ctx)
|
||||
go h.runDailyMaintenanceLoop(ctx)
|
||||
}
|
||||
|
||||
func (h *Handler) StopBackgroundJobs() {
|
||||
if h == nil {
|
||||
return
|
||||
}
|
||||
|
||||
h.jobsMu.Lock()
|
||||
if !h.jobsStarted {
|
||||
h.jobsMu.Unlock()
|
||||
return
|
||||
}
|
||||
cancel := h.jobsCancel
|
||||
h.jobsCancel = nil
|
||||
h.jobsStarted = false
|
||||
h.jobsMu.Unlock()
|
||||
|
||||
if cancel != nil {
|
||||
cancel()
|
||||
}
|
||||
h.jobsWG.Wait()
|
||||
}
|
||||
|
||||
func (h *Handler) runHourlyStatsLoop(ctx context.Context) {
|
||||
defer h.jobsWG.Done()
|
||||
|
||||
for {
|
||||
wait := durationUntilNextHour(time.Now())
|
||||
timer := time.NewTimer(wait)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
return
|
||||
case <-timer.C:
|
||||
h.runStatisticsFlowJob(time.Now())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) runDailyMaintenanceLoop(ctx context.Context) {
|
||||
defer h.jobsWG.Done()
|
||||
|
||||
for {
|
||||
wait := durationUntilNextDailyMaintenance(time.Now())
|
||||
timer := time.NewTimer(wait)
|
||||
select {
|
||||
case <-ctx.Done():
|
||||
if !timer.Stop() {
|
||||
<-timer.C
|
||||
}
|
||||
return
|
||||
case <-timer.C:
|
||||
h.runResetAndExpiryJob(time.Now())
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func durationUntilNextHour(now time.Time) time.Duration {
|
||||
next := now.Truncate(time.Hour).Add(time.Hour)
|
||||
return next.Sub(now)
|
||||
}
|
||||
|
||||
func durationUntilNextDailyMaintenance(now time.Time) time.Duration {
|
||||
next := time.Date(now.Year(), now.Month(), now.Day(), 0, 0, 5, 0, now.Location())
|
||||
if !next.After(now) {
|
||||
next = next.Add(24 * time.Hour)
|
||||
}
|
||||
return next.Sub(now)
|
||||
}
|
||||
|
||||
func (h *Handler) runStatisticsFlowJob(now time.Time) {
|
||||
if h == nil || h.repo == nil || h.repo.DB() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
db := h.repo.DB()
|
||||
nowMs := now.UnixMilli()
|
||||
cutoffMs := nowMs - int64((48*time.Hour)/time.Millisecond)
|
||||
_, _ = db.Exec(`DELETE FROM statistics_flow WHERE created_time < ?`, cutoffMs)
|
||||
|
||||
hourMark := now.Truncate(time.Hour)
|
||||
hourText := hourMark.Format("15:04")
|
||||
createdTime := hourMark.UnixMilli()
|
||||
|
||||
rows, err := db.Query(`SELECT id, in_flow, out_flow FROM user ORDER BY id ASC`)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
type userFlowSnapshot struct {
|
||||
userID int64
|
||||
inFlow int64
|
||||
outFlow int64
|
||||
}
|
||||
users := make([]userFlowSnapshot, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var userID int64
|
||||
var inFlow int64
|
||||
var outFlow int64
|
||||
if err := rows.Scan(&userID, &inFlow, &outFlow); err != nil {
|
||||
continue
|
||||
}
|
||||
users = append(users, userFlowSnapshot{userID: userID, inFlow: inFlow, outFlow: outFlow})
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
for _, user := range users {
|
||||
currentTotal := user.inFlow + user.outFlow
|
||||
increment := currentTotal
|
||||
|
||||
var lastTotal sql.NullInt64
|
||||
err := db.QueryRow(`SELECT total_flow FROM statistics_flow WHERE user_id = ? ORDER BY id DESC LIMIT 1`, user.userID).Scan(&lastTotal)
|
||||
if err == nil && lastTotal.Valid {
|
||||
increment = currentTotal - lastTotal.Int64
|
||||
if increment < 0 {
|
||||
increment = currentTotal
|
||||
}
|
||||
}
|
||||
|
||||
_, _ = db.Exec(`
|
||||
INSERT INTO statistics_flow(user_id, flow, total_flow, time, created_time)
|
||||
VALUES(?, ?, ?, ?, ?)
|
||||
`, user.userID, increment, currentTotal, hourText, createdTime)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) runResetAndExpiryJob(now time.Time) {
|
||||
if h == nil || h.repo == nil || h.repo.DB() == nil {
|
||||
return
|
||||
}
|
||||
|
||||
h.resetMonthlyFlow(now)
|
||||
h.disableExpiredUsers(now.UnixMilli())
|
||||
h.disableExpiredUserTunnels(now.UnixMilli())
|
||||
}
|
||||
|
||||
func (h *Handler) resetMonthlyFlow(now time.Time) {
|
||||
db := h.repo.DB()
|
||||
currentDay := now.Day()
|
||||
lastDay := time.Date(now.Year(), now.Month()+1, 0, 0, 0, 0, 0, now.Location()).Day()
|
||||
|
||||
if currentDay == lastDay {
|
||||
_, _ = db.Exec(`
|
||||
UPDATE user
|
||||
SET in_flow = 0, out_flow = 0
|
||||
WHERE flow_reset_time != 0
|
||||
AND (flow_reset_time = ? OR flow_reset_time > ?)
|
||||
`, currentDay, lastDay)
|
||||
_, _ = db.Exec(`
|
||||
UPDATE user_tunnel
|
||||
SET in_flow = 0, out_flow = 0
|
||||
WHERE flow_reset_time != 0
|
||||
AND (flow_reset_time = ? OR flow_reset_time > ?)
|
||||
`, currentDay, lastDay)
|
||||
return
|
||||
}
|
||||
|
||||
_, _ = db.Exec(`
|
||||
UPDATE user
|
||||
SET in_flow = 0, out_flow = 0
|
||||
WHERE flow_reset_time != 0
|
||||
AND flow_reset_time = ?
|
||||
`, currentDay)
|
||||
_, _ = db.Exec(`
|
||||
UPDATE user_tunnel
|
||||
SET in_flow = 0, out_flow = 0
|
||||
WHERE flow_reset_time != 0
|
||||
AND flow_reset_time = ?
|
||||
`, currentDay)
|
||||
}
|
||||
|
||||
func (h *Handler) disableExpiredUsers(nowMs int64) {
|
||||
db := h.repo.DB()
|
||||
rows, err := db.Query(`
|
||||
SELECT id
|
||||
FROM user
|
||||
WHERE role_id != 0
|
||||
AND status = 1
|
||||
AND exp_time IS NOT NULL
|
||||
AND exp_time < ?
|
||||
`, nowMs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
userIDs := make([]int64, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var userID int64
|
||||
if err := rows.Scan(&userID); err != nil {
|
||||
continue
|
||||
}
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
for _, userID := range userIDs {
|
||||
forwards, err := h.listActiveForwardsByUser(userID)
|
||||
if err == nil {
|
||||
h.pauseForwardRecords(forwards, nowMs)
|
||||
}
|
||||
_, _ = db.Exec(`UPDATE user SET status = 0 WHERE id = ?`, userID)
|
||||
}
|
||||
}
|
||||
|
||||
func (h *Handler) disableExpiredUserTunnels(nowMs int64) {
|
||||
db := h.repo.DB()
|
||||
rows, err := db.Query(`
|
||||
SELECT id, user_id, tunnel_id
|
||||
FROM user_tunnel
|
||||
WHERE status = 1
|
||||
AND exp_time IS NOT NULL
|
||||
AND exp_time < ?
|
||||
`, nowMs)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
type expiredUserTunnel struct {
|
||||
userTunnelID int64
|
||||
userID int64
|
||||
tunnelID int64
|
||||
}
|
||||
items := make([]expiredUserTunnel, 0)
|
||||
|
||||
for rows.Next() {
|
||||
var userTunnelID int64
|
||||
var userID int64
|
||||
var tunnelID int64
|
||||
if err := rows.Scan(&userTunnelID, &userID, &tunnelID); err != nil {
|
||||
continue
|
||||
}
|
||||
items = append(items, expiredUserTunnel{userTunnelID: userTunnelID, userID: userID, tunnelID: tunnelID})
|
||||
}
|
||||
_ = rows.Close()
|
||||
|
||||
for _, item := range items {
|
||||
forwards, err := h.listActiveForwardsByUserTunnel(item.userID, item.tunnelID)
|
||||
if err == nil {
|
||||
h.pauseForwardRecords(forwards, nowMs)
|
||||
}
|
||||
_, _ = db.Exec(`UPDATE user_tunnel SET status = 0 WHERE id = ?`, item.userTunnelID)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,128 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"path/filepath"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-backend/internal/store/sqlite"
|
||||
)
|
||||
|
||||
func TestRunStatisticsFlowJobTracksIncrementAndPrunes(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "jobs-stats.db")
|
||||
repo, err := sqlite.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repo.Close() })
|
||||
|
||||
h := New(repo, "secret")
|
||||
now := time.Date(2026, 2, 7, 12, 0, 0, 0, time.UTC)
|
||||
nowMs := now.UnixMilli()
|
||||
|
||||
if _, err := repo.DB().Exec(`UPDATE user SET in_flow = 100, out_flow = 200 WHERE id = 1`); err != nil {
|
||||
t.Fatalf("seed user flow: %v", err)
|
||||
}
|
||||
|
||||
if _, err := repo.DB().Exec(`INSERT INTO statistics_flow(user_id, flow, total_flow, time, created_time) VALUES(1, 250, 250, '11:00', ?)`, now.Add(-time.Hour).UnixMilli()); err != nil {
|
||||
t.Fatalf("seed recent statistics row: %v", err)
|
||||
}
|
||||
if _, err := repo.DB().Exec(`INSERT INTO statistics_flow(user_id, flow, total_flow, time, created_time) VALUES(1, 10, 10, '00:00', ?)`, now.Add(-49*time.Hour).UnixMilli()); err != nil {
|
||||
t.Fatalf("seed stale statistics row: %v", err)
|
||||
}
|
||||
|
||||
h.runStatisticsFlowJob(now)
|
||||
|
||||
var staleCount int
|
||||
if err := repo.DB().QueryRow(`SELECT COUNT(1) FROM statistics_flow WHERE created_time < ?`, nowMs-int64((48*time.Hour)/time.Millisecond)).Scan(&staleCount); err != nil {
|
||||
t.Fatalf("query stale statistics rows: %v", err)
|
||||
}
|
||||
if staleCount != 0 {
|
||||
t.Fatalf("expected stale statistics rows to be pruned, got %d", staleCount)
|
||||
}
|
||||
|
||||
var flow int64
|
||||
var total int64
|
||||
var hour string
|
||||
if err := repo.DB().QueryRow(`SELECT flow, total_flow, time FROM statistics_flow WHERE user_id = 1 ORDER BY id DESC LIMIT 1`).Scan(&flow, &total, &hour); err != nil {
|
||||
t.Fatalf("query latest statistics row: %v", err)
|
||||
}
|
||||
if flow != 50 {
|
||||
t.Fatalf("expected increment flow 50, got %d", flow)
|
||||
}
|
||||
if total != 300 {
|
||||
t.Fatalf("expected total flow 300, got %d", total)
|
||||
}
|
||||
if hour != "12:00" {
|
||||
t.Fatalf("expected hour mark 12:00, got %s", hour)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRunResetAndExpiryJobResetsFlowAndDisablesExpiredRecords(t *testing.T) {
|
||||
dbPath := filepath.Join(t.TempDir(), "jobs-reset.db")
|
||||
repo, err := sqlite.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
t.Cleanup(func() { _ = repo.Close() })
|
||||
|
||||
h := New(repo, "secret")
|
||||
now := time.Date(2026, 3, 15, 0, 0, 5, 0, time.UTC)
|
||||
nowMs := now.UnixMilli()
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user(id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status)
|
||||
VALUES(2, 'expired_user', 'x', 1, ?, 100, 1000, 2000, 15, 1, ?, ?, 1)
|
||||
`, nowMs-1000, nowMs, nowMs); err != nil {
|
||||
t.Fatalf("insert expired user: %v", err)
|
||||
}
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO tunnel(id, name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||
VALUES(1, 't1', 1.0, 1, 'tls', 1, ?, ?, 1, NULL, 0)
|
||||
`, nowMs, nowMs); err != nil {
|
||||
t.Fatalf("insert tunnel: %v", err)
|
||||
}
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user_tunnel(id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status)
|
||||
VALUES(10, 2, 1, NULL, 1, 1, 300, 400, 15, ?, 1)
|
||||
`, nowMs-1000); err != nil {
|
||||
t.Fatalf("insert expired user_tunnel: %v", err)
|
||||
}
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO forward(id, user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx)
|
||||
VALUES(20, 2, 'expired_user', 'f1', 1, '1.1.1.1:443', 'fifo', 0, 0, ?, ?, 1, 0)
|
||||
`, nowMs, nowMs); err != nil {
|
||||
t.Fatalf("insert forward: %v", err)
|
||||
}
|
||||
|
||||
h.runResetAndExpiryJob(now)
|
||||
|
||||
var userIn, userOut int64
|
||||
var userStatus int
|
||||
if err := repo.DB().QueryRow(`SELECT in_flow, out_flow, status FROM user WHERE id = 2`).Scan(&userIn, &userOut, &userStatus); err != nil {
|
||||
t.Fatalf("query user after maintenance: %v", err)
|
||||
}
|
||||
if userIn != 0 || userOut != 0 || userStatus != 0 {
|
||||
t.Fatalf("expected user reset+disabled, got in=%d out=%d status=%d", userIn, userOut, userStatus)
|
||||
}
|
||||
|
||||
var utIn, utOut int64
|
||||
var utStatus int
|
||||
if err := repo.DB().QueryRow(`SELECT in_flow, out_flow, status FROM user_tunnel WHERE id = 10`).Scan(&utIn, &utOut, &utStatus); err != nil {
|
||||
t.Fatalf("query user_tunnel after maintenance: %v", err)
|
||||
}
|
||||
if utIn != 0 || utOut != 0 || utStatus != 0 {
|
||||
t.Fatalf("expected user_tunnel reset+disabled, got in=%d out=%d status=%d", utIn, utOut, utStatus)
|
||||
}
|
||||
|
||||
var forwardStatus int
|
||||
if err := repo.DB().QueryRow(`SELECT status FROM forward WHERE id = 20`).Scan(&forwardStatus); err != nil {
|
||||
t.Fatalf("query forward after maintenance: %v", err)
|
||||
}
|
||||
if forwardStatus != 0 {
|
||||
t.Fatalf("expected forward status=0 after expiry handling, got %d", forwardStatus)
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,117 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"context"
|
||||
"net/http"
|
||||
"strings"
|
||||
|
||||
"go-backend/internal/auth"
|
||||
"go-backend/internal/http/response"
|
||||
)
|
||||
|
||||
type contextKey string
|
||||
|
||||
const ClaimsContextKey contextKey = "claims"
|
||||
|
||||
type AuthOptions struct {
|
||||
JWTSecret string
|
||||
}
|
||||
|
||||
func JWT(opts AuthOptions) func(http.Handler) http.Handler {
|
||||
return func(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
if shouldSkip(r.URL.Path) {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(r.URL.Path, "/api/") {
|
||||
next.ServeHTTP(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
token := strings.TrimSpace(r.Header.Get("Authorization"))
|
||||
if token == "" {
|
||||
response.WriteJSON(w, response.Err(401, "未登录或token已过期"))
|
||||
return
|
||||
}
|
||||
|
||||
claims, ok := auth.ValidateToken(token, opts.JWTSecret)
|
||||
if !ok {
|
||||
response.WriteJSON(w, response.Err(401, "无效的token或token已过期"))
|
||||
return
|
||||
}
|
||||
|
||||
if requiresAdmin(r.URL.Path) && claims.RoleID != 0 {
|
||||
response.WriteJSON(w, response.Err(403, "权限不足,仅管理员可操作"))
|
||||
return
|
||||
}
|
||||
|
||||
ctx := context.WithValue(r.Context(), ClaimsContextKey, claims)
|
||||
next.ServeHTTP(w, r.WithContext(ctx))
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func RequireAdmin(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
raw := r.Context().Value(ClaimsContextKey)
|
||||
claims, ok := raw.(auth.Claims)
|
||||
if !ok {
|
||||
response.WriteJSON(w, response.Err(401, "无法获取用户权限信息"))
|
||||
return
|
||||
}
|
||||
if claims.RoleID != 0 {
|
||||
response.WriteJSON(w, response.Err(403, "权限不足,仅管理员可操作"))
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
|
||||
func shouldSkip(path string) bool {
|
||||
switch {
|
||||
case strings.HasPrefix(path, "/flow/"):
|
||||
return true
|
||||
case strings.HasPrefix(path, "/api/v1/open_api/"):
|
||||
return true
|
||||
case strings.HasPrefix(path, "/api/v1/captcha/"):
|
||||
return true
|
||||
case path == "/api/v1/config/get":
|
||||
return true
|
||||
case path == "/api/v1/user/login":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
|
||||
func requiresAdmin(path string) bool {
|
||||
if strings.HasPrefix(path, "/api/v1/group/") {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/api/v1/node/") {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/api/v1/speed-limit/") {
|
||||
return true
|
||||
}
|
||||
|
||||
if strings.HasPrefix(path, "/api/v1/tunnel/") {
|
||||
if strings.HasPrefix(path, "/api/v1/tunnel/user/tunnel") {
|
||||
return false
|
||||
}
|
||||
return true
|
||||
}
|
||||
|
||||
switch path {
|
||||
case "/api/v1/user/create", "/api/v1/user/list", "/api/v1/user/update", "/api/v1/user/delete", "/api/v1/user/reset":
|
||||
return true
|
||||
case "/api/v1/config/update", "/api/v1/config/update-single":
|
||||
return true
|
||||
default:
|
||||
return false
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,17 @@
|
||||
package middleware
|
||||
|
||||
import "net/http"
|
||||
|
||||
func CORS(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
w.Header().Set("Access-Control-Allow-Origin", "*")
|
||||
w.Header().Set("Access-Control-Allow-Headers", "*")
|
||||
w.Header().Set("Access-Control-Allow-Methods", "GET, POST, DELETE, PUT, OPTIONS")
|
||||
w.Header().Set("Access-Control-Expose-Headers", "Authorization")
|
||||
if r.Method == http.MethodOptions {
|
||||
w.WriteHeader(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,19 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net/http"
|
||||
|
||||
"go-backend/internal/http/response"
|
||||
)
|
||||
|
||||
func Recover(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
defer func() {
|
||||
if rec := recover(); rec != nil {
|
||||
response.WriteJSON(w, response.Err(-2, fmt.Sprint(rec)))
|
||||
}
|
||||
}()
|
||||
next.ServeHTTP(w, r)
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,57 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"io"
|
||||
"log"
|
||||
"net"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type statusWriter struct {
|
||||
http.ResponseWriter
|
||||
status int
|
||||
}
|
||||
|
||||
func (w *statusWriter) WriteHeader(code int) {
|
||||
w.status = code
|
||||
w.ResponseWriter.WriteHeader(code)
|
||||
}
|
||||
|
||||
func (w *statusWriter) Hijack() (net.Conn, *bufio.ReadWriter, error) {
|
||||
hj, ok := w.ResponseWriter.(http.Hijacker)
|
||||
if !ok {
|
||||
return nil, nil, http.ErrNotSupported
|
||||
}
|
||||
return hj.Hijack()
|
||||
}
|
||||
|
||||
func (w *statusWriter) Flush() {
|
||||
if f, ok := w.ResponseWriter.(http.Flusher); ok {
|
||||
f.Flush()
|
||||
}
|
||||
}
|
||||
|
||||
func (w *statusWriter) ReadFrom(r io.Reader) (int64, error) {
|
||||
if rf, ok := w.ResponseWriter.(io.ReaderFrom); ok {
|
||||
return rf.ReadFrom(r)
|
||||
}
|
||||
return io.Copy(w.ResponseWriter, r)
|
||||
}
|
||||
|
||||
func (w *statusWriter) Push(target string, opts *http.PushOptions) error {
|
||||
if p, ok := w.ResponseWriter.(http.Pusher); ok {
|
||||
return p.Push(target, opts)
|
||||
}
|
||||
return http.ErrNotSupported
|
||||
}
|
||||
|
||||
func RequestLog(next http.Handler) http.Handler {
|
||||
return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
sw := &statusWriter{ResponseWriter: w, status: http.StatusOK}
|
||||
start := time.Now()
|
||||
next.ServeHTTP(sw, r)
|
||||
log.Printf("%s %s -> %d (%s)", r.Method, r.URL.Path, sw.status, time.Since(start).String())
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,48 @@
|
||||
package response
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"time"
|
||||
)
|
||||
|
||||
type R struct {
|
||||
Code int `json:"code"`
|
||||
Msg string `json:"msg"`
|
||||
TS int64 `json:"ts"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
func OK(data interface{}) R {
|
||||
return R{
|
||||
Code: 0,
|
||||
Msg: "操作成功",
|
||||
TS: time.Now().UnixMilli(),
|
||||
Data: data,
|
||||
}
|
||||
}
|
||||
|
||||
func OKEmpty() R {
|
||||
return R{
|
||||
Code: 0,
|
||||
Msg: "操作成功",
|
||||
TS: time.Now().UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
func Err(code int, msg string) R {
|
||||
return R{
|
||||
Code: code,
|
||||
Msg: msg,
|
||||
TS: time.Now().UnixMilli(),
|
||||
}
|
||||
}
|
||||
|
||||
func ErrDefault(msg string) R {
|
||||
return Err(-1, msg)
|
||||
}
|
||||
|
||||
func WriteJSON(w http.ResponseWriter, payload R) {
|
||||
w.Header().Set("Content-Type", "application/json; charset=utf-8")
|
||||
_ = json.NewEncoder(w).Encode(payload)
|
||||
}
|
||||
@@ -0,0 +1,20 @@
|
||||
package httpserver
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
|
||||
"go-backend/internal/http/handler"
|
||||
"go-backend/internal/http/middleware"
|
||||
)
|
||||
|
||||
func NewRouter(h *handler.Handler, jwtSecret string) http.Handler {
|
||||
mux := http.NewServeMux()
|
||||
h.Register(mux)
|
||||
mux.Handle("/system-info", h.WebSocketHandler())
|
||||
|
||||
wrapped := middleware.Recover(mux)
|
||||
wrapped = middleware.JWT(middleware.AuthOptions{JWTSecret: jwtSecret})(wrapped)
|
||||
wrapped = middleware.RequestLog(wrapped)
|
||||
wrapped = middleware.CORS(wrapped)
|
||||
return wrapped
|
||||
}
|
||||
@@ -0,0 +1,65 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/aes"
|
||||
"crypto/cipher"
|
||||
"crypto/rand"
|
||||
"crypto/sha256"
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
type AESCrypto struct {
|
||||
key []byte
|
||||
}
|
||||
|
||||
func NewAESCrypto(secret string) (*AESCrypto, error) {
|
||||
if secret == "" {
|
||||
return nil, fmt.Errorf("secret is empty")
|
||||
}
|
||||
hash := sha256.Sum256([]byte(secret))
|
||||
return &AESCrypto{key: hash[:]}, nil
|
||||
}
|
||||
|
||||
func (a *AESCrypto) Encrypt(plain []byte) (string, error) {
|
||||
if len(plain) == 0 {
|
||||
return "", fmt.Errorf("empty plaintext")
|
||||
}
|
||||
block, err := aes.NewCipher(a.key)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
nonce := make([]byte, gcm.NonceSize())
|
||||
if _, err := rand.Read(nonce); err != nil {
|
||||
return "", err
|
||||
}
|
||||
sealed := gcm.Seal(nil, nonce, plain, nil)
|
||||
data := append(nonce, sealed...)
|
||||
return base64.StdEncoding.EncodeToString(data), nil
|
||||
}
|
||||
|
||||
func (a *AESCrypto) Decrypt(cipherText string) ([]byte, error) {
|
||||
raw, err := base64.StdEncoding.DecodeString(cipherText)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
block, err := aes.NewCipher(a.key)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
gcm, err := cipher.NewGCM(block)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
nonceSize := gcm.NonceSize()
|
||||
if len(raw) < nonceSize {
|
||||
return nil, fmt.Errorf("ciphertext too short")
|
||||
}
|
||||
nonce := raw[:nonceSize]
|
||||
data := raw[nonceSize:]
|
||||
return gcm.Open(nil, nonce, data, nil)
|
||||
}
|
||||
@@ -0,0 +1,11 @@
|
||||
package security
|
||||
|
||||
import (
|
||||
"crypto/md5"
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func MD5(input string) string {
|
||||
hash := md5.Sum([]byte(input))
|
||||
return fmt.Sprintf("%x", hash)
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,5 @@
|
||||
INSERT OR IGNORE INTO user (id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status)
|
||||
VALUES (1, 'admin_user', '3c85cdebade1c51cf64ca9f3c09d182d', 0, 2727251700000, 99999, 0, 0, 1, 99999, 1748914865000, 1754011744252, 1);
|
||||
|
||||
INSERT OR IGNORE INTO vite_config (id, name, value, time)
|
||||
VALUES (1, 'app_name', 'flux', 1755147963000);
|
||||
@@ -0,0 +1,183 @@
|
||||
-- SQLite Auto-generated schema
|
||||
-- This will be executed automatically on startup if tables don't exist
|
||||
|
||||
CREATE TABLE IF NOT EXISTS forward (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
user_name VARCHAR(100) NOT NULL,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
tunnel_id INTEGER NOT NULL,
|
||||
remote_addr TEXT NOT NULL,
|
||||
strategy VARCHAR(100) NOT NULL DEFAULT 'fifo',
|
||||
in_flow INTEGER NOT NULL DEFAULT 0,
|
||||
out_flow INTEGER NOT NULL DEFAULT 0,
|
||||
created_time INTEGER NOT NULL,
|
||||
updated_time INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
inx INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS forward_port (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
forward_id INTEGER NOT NULL,
|
||||
node_id INTEGER NOT NULL,
|
||||
port INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS node (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
secret VARCHAR(100) NOT NULL,
|
||||
server_ip VARCHAR(100) NOT NULL,
|
||||
server_ip_v4 VARCHAR(100),
|
||||
server_ip_v6 VARCHAR(100),
|
||||
port TEXT NOT NULL,
|
||||
interface_name VARCHAR(200),
|
||||
version VARCHAR(100),
|
||||
http INTEGER NOT NULL DEFAULT 0,
|
||||
tls INTEGER NOT NULL DEFAULT 0,
|
||||
socks INTEGER NOT NULL DEFAULT 0,
|
||||
created_time INTEGER NOT NULL,
|
||||
updated_time INTEGER,
|
||||
status INTEGER NOT NULL,
|
||||
tcp_listen_addr VARCHAR(100) NOT NULL DEFAULT '[::]',
|
||||
udp_listen_addr VARCHAR(100) NOT NULL DEFAULT '[::]',
|
||||
inx INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS speed_limit (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
speed INTEGER NOT NULL,
|
||||
tunnel_id INTEGER NOT NULL,
|
||||
tunnel_name VARCHAR(100) NOT NULL,
|
||||
created_time INTEGER NOT NULL,
|
||||
updated_time INTEGER,
|
||||
status INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS statistics_flow (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
flow INTEGER NOT NULL,
|
||||
total_flow INTEGER NOT NULL,
|
||||
time VARCHAR(100) NOT NULL,
|
||||
created_time INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tunnel (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
traffic_ratio REAL NOT NULL DEFAULT 1.0,
|
||||
type INTEGER NOT NULL,
|
||||
protocol VARCHAR(10) NOT NULL DEFAULT 'tls',
|
||||
flow INTEGER NOT NULL,
|
||||
created_time INTEGER NOT NULL,
|
||||
updated_time INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL,
|
||||
in_ip TEXT,
|
||||
inx INTEGER NOT NULL DEFAULT 0
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS chain_tunnel (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
tunnel_id INTEGER NOT NULL ,
|
||||
chain_type VARCHAR(10) NOT NULL,
|
||||
node_id INTEGER NOT NULL ,
|
||||
port INTEGER,
|
||||
strategy VARCHAR(10),
|
||||
inx INTEGER,
|
||||
protocol VARCHAR(10)
|
||||
);
|
||||
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user VARCHAR(100) NOT NULL,
|
||||
pwd VARCHAR(100) NOT NULL,
|
||||
role_id INTEGER NOT NULL,
|
||||
exp_time INTEGER NOT NULL,
|
||||
flow INTEGER NOT NULL,
|
||||
in_flow INTEGER NOT NULL DEFAULT 0,
|
||||
out_flow INTEGER NOT NULL DEFAULT 0,
|
||||
flow_reset_time INTEGER NOT NULL,
|
||||
num INTEGER NOT NULL,
|
||||
created_time INTEGER NOT NULL,
|
||||
updated_time INTEGER,
|
||||
status INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_tunnel (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_id INTEGER NOT NULL,
|
||||
tunnel_id INTEGER NOT NULL,
|
||||
speed_id INTEGER,
|
||||
num INTEGER NOT NULL,
|
||||
flow INTEGER NOT NULL,
|
||||
in_flow INTEGER NOT NULL DEFAULT 0,
|
||||
out_flow INTEGER NOT NULL DEFAULT 0,
|
||||
flow_reset_time INTEGER NOT NULL,
|
||||
exp_time INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tunnel_group (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
created_time INTEGER NOT NULL,
|
||||
updated_time INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_group (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(100) NOT NULL,
|
||||
created_time INTEGER NOT NULL,
|
||||
updated_time INTEGER NOT NULL,
|
||||
status INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS tunnel_group_tunnel (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
tunnel_group_id INTEGER NOT NULL,
|
||||
tunnel_id INTEGER NOT NULL,
|
||||
created_time INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS user_group_user (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_group_id INTEGER NOT NULL,
|
||||
user_id INTEGER NOT NULL,
|
||||
created_time INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS group_permission (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_group_id INTEGER NOT NULL,
|
||||
tunnel_group_id INTEGER NOT NULL,
|
||||
created_time INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS group_permission_grant (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
user_group_id INTEGER NOT NULL,
|
||||
tunnel_group_id INTEGER NOT NULL,
|
||||
user_tunnel_id INTEGER NOT NULL,
|
||||
created_by_group INTEGER NOT NULL DEFAULT 0,
|
||||
created_time INTEGER NOT NULL
|
||||
);
|
||||
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_name ON tunnel_group(name);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_name ON user_group(name);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_tunnel_group_tunnel_unique ON tunnel_group_tunnel(tunnel_group_id, tunnel_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_group_user_unique ON user_group_user(user_group_id, user_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_unique ON group_permission(user_group_id, tunnel_group_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_group_permission_grant_unique ON group_permission_grant(user_group_id, tunnel_group_id, user_tunnel_id);
|
||||
CREATE UNIQUE INDEX IF NOT EXISTS idx_user_tunnel_unique ON user_tunnel(user_id, tunnel_id);
|
||||
|
||||
CREATE TABLE IF NOT EXISTS vite_config (
|
||||
id INTEGER PRIMARY KEY AUTOINCREMENT,
|
||||
name VARCHAR(200) NOT NULL UNIQUE,
|
||||
value VARCHAR(200) NOT NULL,
|
||||
time INTEGER NOT NULL
|
||||
);
|
||||
@@ -0,0 +1,430 @@
|
||||
package ws
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"log"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
"sync"
|
||||
"time"
|
||||
|
||||
"github.com/gorilla/websocket"
|
||||
|
||||
"go-backend/internal/auth"
|
||||
"go-backend/internal/security"
|
||||
"go-backend/internal/store/sqlite"
|
||||
)
|
||||
|
||||
type encryptedMessage struct {
|
||||
Encrypted bool `json:"encrypted"`
|
||||
Data string `json:"data"`
|
||||
Timestamp int64 `json:"timestamp"`
|
||||
}
|
||||
|
||||
type broadcastMessage struct {
|
||||
ID int64 `json:"id"`
|
||||
Type string `json:"type"`
|
||||
Data string `json:"data"`
|
||||
}
|
||||
|
||||
type connWrap struct {
|
||||
conn *websocket.Conn
|
||||
mu sync.Mutex
|
||||
}
|
||||
|
||||
type nodeSession struct {
|
||||
nodeID int64
|
||||
secret string
|
||||
conn *connWrap
|
||||
}
|
||||
|
||||
type commandResponse struct {
|
||||
Type string `json:"type"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data json.RawMessage `json:"data,omitempty"`
|
||||
RequestID string `json:"requestId,omitempty"`
|
||||
}
|
||||
|
||||
type pendingRequest struct {
|
||||
nodeID int64
|
||||
ch chan CommandResult
|
||||
}
|
||||
|
||||
type CommandResult struct {
|
||||
Type string `json:"type"`
|
||||
Success bool `json:"success"`
|
||||
Message string `json:"message"`
|
||||
Data map[string]interface{} `json:"data,omitempty"`
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
repo *sqlite.Repository
|
||||
jwtSecret string
|
||||
upgrader websocket.Upgrader
|
||||
|
||||
mu sync.RWMutex
|
||||
admins map[*connWrap]struct{}
|
||||
nodes map[int64]*nodeSession
|
||||
byConn map[*websocket.Conn]*nodeSession
|
||||
pending map[string]pendingRequest
|
||||
}
|
||||
|
||||
func NewServer(repo *sqlite.Repository, jwtSecret string) *Server {
|
||||
return &Server{
|
||||
repo: repo,
|
||||
jwtSecret: jwtSecret,
|
||||
upgrader: websocket.Upgrader{
|
||||
CheckOrigin: func(r *http.Request) bool { return true },
|
||||
},
|
||||
admins: make(map[*connWrap]struct{}),
|
||||
nodes: make(map[int64]*nodeSession),
|
||||
byConn: make(map[*websocket.Conn]*nodeSession),
|
||||
pending: make(map[string]pendingRequest),
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) ServeHTTP(w http.ResponseWriter, r *http.Request) {
|
||||
query := r.URL.Query()
|
||||
typeVal := query.Get("type")
|
||||
secret := query.Get("secret")
|
||||
|
||||
if typeVal == "1" {
|
||||
node, err := s.repo.GetNodeBySecret(secret)
|
||||
if err != nil || node == nil {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
s.handleNode(w, r, node.ID, secret)
|
||||
return
|
||||
}
|
||||
|
||||
if typeVal == "0" {
|
||||
if _, ok := auth.ValidateToken(secret, s.jwtSecret); !ok {
|
||||
http.Error(w, "forbidden", http.StatusForbidden)
|
||||
return
|
||||
}
|
||||
s.handleAdmin(w, r)
|
||||
return
|
||||
}
|
||||
|
||||
http.Error(w, "bad request", http.StatusBadRequest)
|
||||
}
|
||||
|
||||
func (s *Server) handleAdmin(w http.ResponseWriter, r *http.Request) {
|
||||
conn, err := s.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cw := &connWrap{conn: conn}
|
||||
|
||||
s.mu.Lock()
|
||||
s.admins[cw] = struct{}{}
|
||||
s.mu.Unlock()
|
||||
|
||||
defer func() {
|
||||
s.mu.Lock()
|
||||
delete(s.admins, cw)
|
||||
s.mu.Unlock()
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
if _, _, err := conn.ReadMessage(); err != nil {
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) handleNode(w http.ResponseWriter, r *http.Request, nodeID int64, secret string) {
|
||||
conn, err := s.upgrader.Upgrade(w, r, nil)
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
cw := &connWrap{conn: conn}
|
||||
|
||||
version := r.URL.Query().Get("version")
|
||||
httpVal := parseIntDefault(r.URL.Query().Get("http"), 0)
|
||||
tlsVal := parseIntDefault(r.URL.Query().Get("tls"), 0)
|
||||
socksVal := parseIntDefault(r.URL.Query().Get("socks"), 0)
|
||||
|
||||
s.mu.Lock()
|
||||
if old, ok := s.nodes[nodeID]; ok {
|
||||
_ = old.conn.conn.Close()
|
||||
delete(s.byConn, old.conn.conn)
|
||||
}
|
||||
ns := &nodeSession{nodeID: nodeID, secret: secret, conn: cw}
|
||||
s.nodes[nodeID] = ns
|
||||
s.byConn[conn] = ns
|
||||
s.mu.Unlock()
|
||||
|
||||
_ = s.repo.UpdateNodeOnline(nodeID, 1, version, httpVal, tlsVal, socksVal)
|
||||
s.broadcastStatus(nodeID, 1)
|
||||
|
||||
defer func() {
|
||||
needOfflineBroadcast := false
|
||||
s.mu.Lock()
|
||||
current, ok := s.nodes[nodeID]
|
||||
if ok && current.conn.conn == conn {
|
||||
delete(s.nodes, nodeID)
|
||||
needOfflineBroadcast = true
|
||||
}
|
||||
delete(s.byConn, conn)
|
||||
s.mu.Unlock()
|
||||
if needOfflineBroadcast {
|
||||
s.failPendingForNode(nodeID, "节点连接已断开")
|
||||
_ = s.repo.UpdateNodeStatus(nodeID, 0)
|
||||
s.broadcastStatus(nodeID, 0)
|
||||
}
|
||||
_ = conn.Close()
|
||||
}()
|
||||
|
||||
for {
|
||||
_, payload, err := conn.ReadMessage()
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
msg := decryptIfNeeded(payload, secret)
|
||||
s.tryResolvePending(nodeID, msg)
|
||||
s.broadcastInfo(nodeID, msg)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) SendCommand(nodeID int64, cmdType string, data interface{}, timeout time.Duration) (CommandResult, error) {
|
||||
if s == nil {
|
||||
return CommandResult{}, errors.New("server not initialized")
|
||||
}
|
||||
if strings.TrimSpace(cmdType) == "" {
|
||||
return CommandResult{}, errors.New("command type is empty")
|
||||
}
|
||||
if timeout <= 0 {
|
||||
timeout = 10 * time.Second
|
||||
}
|
||||
|
||||
s.mu.RLock()
|
||||
ns, ok := s.nodes[nodeID]
|
||||
s.mu.RUnlock()
|
||||
if !ok || ns == nil || ns.conn == nil || ns.conn.conn == nil {
|
||||
return CommandResult{}, errors.New("节点不在线")
|
||||
}
|
||||
|
||||
requestID := fmt.Sprintf("%d_%d", nodeID, time.Now().UnixNano())
|
||||
ch := make(chan CommandResult, 1)
|
||||
|
||||
s.mu.Lock()
|
||||
s.pending[requestID] = pendingRequest{nodeID: nodeID, ch: ch}
|
||||
s.mu.Unlock()
|
||||
|
||||
cleanup := func() {
|
||||
s.mu.Lock()
|
||||
if p, exists := s.pending[requestID]; exists {
|
||||
delete(s.pending, requestID)
|
||||
close(p.ch)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
}
|
||||
|
||||
cmdPayload := map[string]interface{}{
|
||||
"type": cmdType,
|
||||
"data": data,
|
||||
"requestId": requestID,
|
||||
}
|
||||
rawCmd, err := json.Marshal(cmdPayload)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return CommandResult{}, err
|
||||
}
|
||||
|
||||
messageData := rawCmd
|
||||
if strings.TrimSpace(ns.secret) != "" {
|
||||
crypto, err := security.NewAESCrypto(ns.secret)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return CommandResult{}, err
|
||||
}
|
||||
encrypted, err := crypto.Encrypt(rawCmd)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return CommandResult{}, err
|
||||
}
|
||||
wrapper := map[string]interface{}{
|
||||
"encrypted": true,
|
||||
"data": encrypted,
|
||||
"timestamp": time.Now().UnixMilli(),
|
||||
}
|
||||
messageData, err = json.Marshal(wrapper)
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return CommandResult{}, err
|
||||
}
|
||||
}
|
||||
|
||||
ns.conn.mu.Lock()
|
||||
err = ns.conn.conn.WriteMessage(websocket.TextMessage, messageData)
|
||||
ns.conn.mu.Unlock()
|
||||
if err != nil {
|
||||
cleanup()
|
||||
return CommandResult{}, err
|
||||
}
|
||||
|
||||
select {
|
||||
case result, ok := <-ch:
|
||||
if !ok {
|
||||
return CommandResult{}, errors.New("命令通道已关闭")
|
||||
}
|
||||
if !result.Success {
|
||||
if strings.TrimSpace(result.Message) == "" {
|
||||
result.Message = "命令执行失败"
|
||||
}
|
||||
return result, errors.New(result.Message)
|
||||
}
|
||||
return result, nil
|
||||
case <-time.After(timeout):
|
||||
cleanup()
|
||||
return CommandResult{}, errors.New("等待节点响应超时")
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) tryResolvePending(nodeID int64, message string) {
|
||||
if s == nil || strings.TrimSpace(message) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
var resp commandResponse
|
||||
if err := json.Unmarshal([]byte(message), &resp); err != nil {
|
||||
return
|
||||
}
|
||||
if strings.TrimSpace(resp.RequestID) == "" {
|
||||
return
|
||||
}
|
||||
|
||||
s.mu.Lock()
|
||||
p, ok := s.pending[resp.RequestID]
|
||||
if ok {
|
||||
delete(s.pending, resp.RequestID)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
if p.nodeID != nodeID {
|
||||
select {
|
||||
case p.ch <- CommandResult{Type: resp.Type, Success: false, Message: "节点响应与请求不匹配"}:
|
||||
default:
|
||||
}
|
||||
close(p.ch)
|
||||
return
|
||||
}
|
||||
|
||||
result := CommandResult{
|
||||
Type: resp.Type,
|
||||
Success: resp.Success,
|
||||
Message: resp.Message,
|
||||
}
|
||||
if len(resp.Data) > 0 {
|
||||
var data map[string]interface{}
|
||||
if err := json.Unmarshal(resp.Data, &data); err == nil {
|
||||
result.Data = data
|
||||
}
|
||||
}
|
||||
|
||||
select {
|
||||
case p.ch <- result:
|
||||
default:
|
||||
}
|
||||
close(p.ch)
|
||||
}
|
||||
|
||||
func (s *Server) failPendingForNode(nodeID int64, message string) {
|
||||
if s == nil {
|
||||
return
|
||||
}
|
||||
|
||||
type pair struct {
|
||||
id string
|
||||
pr pendingRequest
|
||||
}
|
||||
items := make([]pair, 0)
|
||||
|
||||
s.mu.Lock()
|
||||
for id, pr := range s.pending {
|
||||
if pr.nodeID != nodeID {
|
||||
continue
|
||||
}
|
||||
items = append(items, pair{id: id, pr: pr})
|
||||
delete(s.pending, id)
|
||||
}
|
||||
s.mu.Unlock()
|
||||
|
||||
for _, item := range items {
|
||||
select {
|
||||
case item.pr.ch <- CommandResult{Success: false, Message: message}:
|
||||
default:
|
||||
}
|
||||
close(item.pr.ch)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) broadcastStatus(nodeID int64, status int) {
|
||||
payload := map[string]interface{}{
|
||||
"id": strconv.FormatInt(nodeID, 10),
|
||||
"type": "status",
|
||||
"data": status,
|
||||
}
|
||||
raw, _ := json.Marshal(payload)
|
||||
s.broadcastToAdmins(string(raw))
|
||||
}
|
||||
|
||||
func (s *Server) broadcastInfo(nodeID int64, data string) {
|
||||
payload := broadcastMessage{ID: nodeID, Type: "info", Data: data}
|
||||
raw, _ := json.Marshal(payload)
|
||||
s.broadcastToAdmins(string(raw))
|
||||
}
|
||||
|
||||
func (s *Server) broadcastToAdmins(message string) {
|
||||
s.mu.RLock()
|
||||
admins := make([]*connWrap, 0, len(s.admins))
|
||||
for c := range s.admins {
|
||||
admins = append(admins, c)
|
||||
}
|
||||
s.mu.RUnlock()
|
||||
|
||||
for _, c := range admins {
|
||||
c.mu.Lock()
|
||||
err := c.conn.WriteMessage(websocket.TextMessage, []byte(message))
|
||||
c.mu.Unlock()
|
||||
if err != nil {
|
||||
log.Printf("websocket broadcast failed: %v", err)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func decryptIfNeeded(payload []byte, secret string) string {
|
||||
text := string(payload)
|
||||
var wrap encryptedMessage
|
||||
if err := json.Unmarshal(payload, &wrap); err != nil || !wrap.Encrypted || strings.TrimSpace(wrap.Data) == "" {
|
||||
return text
|
||||
}
|
||||
|
||||
crypto, err := security.NewAESCrypto(secret)
|
||||
if err != nil {
|
||||
return text
|
||||
}
|
||||
plain, err := crypto.Decrypt(wrap.Data)
|
||||
if err != nil {
|
||||
return text
|
||||
}
|
||||
return string(plain)
|
||||
}
|
||||
|
||||
func parseIntDefault(v string, fallback int) int {
|
||||
x, err := strconv.Atoi(v)
|
||||
if err != nil {
|
||||
return fallback
|
||||
}
|
||||
return x
|
||||
}
|
||||
@@ -0,0 +1,90 @@
|
||||
package contract_test
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"go-backend/internal/auth"
|
||||
"go-backend/internal/http/middleware"
|
||||
"go-backend/internal/http/response"
|
||||
)
|
||||
|
||||
func TestJWTMiddlewareContracts(t *testing.T) {
|
||||
secret := "unit-test-secret"
|
||||
|
||||
next := http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) {
|
||||
response.WriteJSON(w, response.OK("pass"))
|
||||
})
|
||||
|
||||
wrapped := middleware.JWT(middleware.AuthOptions{JWTSecret: secret})(next)
|
||||
|
||||
t.Run("login path is excluded", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", nil)
|
||||
res := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(res, req)
|
||||
assertCode(t, res, 0)
|
||||
})
|
||||
|
||||
t.Run("missing token returns 401 contract message", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/list", nil)
|
||||
res := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(res, req)
|
||||
assertCodeMsg(t, res, 401, "未登录或token已过期")
|
||||
})
|
||||
|
||||
t.Run("invalid token returns 401 contract message", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/list", nil)
|
||||
req.Header.Set("Authorization", "invalid.token.value")
|
||||
res := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(res, req)
|
||||
assertCodeMsg(t, res, 401, "无效的token或token已过期")
|
||||
})
|
||||
|
||||
t.Run("valid token reaches next", func(t *testing.T) {
|
||||
token, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate token: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/list", nil)
|
||||
req.Header.Set("Authorization", token)
|
||||
res := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(res, req)
|
||||
assertCode(t, res, 0)
|
||||
})
|
||||
|
||||
t.Run("non-admin blocked on admin path", func(t *testing.T) {
|
||||
token, err := auth.GenerateToken(2, "normal_user", 1, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate token: %v", err)
|
||||
}
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/config/update", nil)
|
||||
req.Header.Set("Authorization", token)
|
||||
res := httptest.NewRecorder()
|
||||
wrapped.ServeHTTP(res, req)
|
||||
assertCodeMsg(t, res, 403, "权限不足,仅管理员可操作")
|
||||
})
|
||||
}
|
||||
|
||||
func assertCode(t *testing.T, rec *httptest.ResponseRecorder, expected int) {
|
||||
t.Helper()
|
||||
var out response.R
|
||||
if err := json.NewDecoder(rec.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code != expected {
|
||||
t.Fatalf("expected code %d, got %d", expected, out.Code)
|
||||
}
|
||||
}
|
||||
|
||||
func assertCodeMsg(t *testing.T, rec *httptest.ResponseRecorder, expectedCode int, expectedMsg string) {
|
||||
t.Helper()
|
||||
var out response.R
|
||||
if err := json.NewDecoder(rec.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code != expectedCode || out.Msg != expectedMsg {
|
||||
t.Fatalf("expected (%d,%q), got (%d,%q)", expectedCode, expectedMsg, out.Code, out.Msg)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,239 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-backend/internal/auth"
|
||||
httpserver "go-backend/internal/http"
|
||||
"go-backend/internal/http/handler"
|
||||
"go-backend/internal/http/response"
|
||||
"go-backend/internal/store/sqlite"
|
||||
)
|
||||
|
||||
func TestDiagnosisChainCoverageContracts(t *testing.T) {
|
||||
secret := "contract-jwt-secret"
|
||||
router, repo := setupDiagnosisContractRouter(t, secret)
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user(id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status)
|
||||
VALUES(2, 'normal_user', '3c85cdebade1c51cf64ca9f3c09d182d', 1, 2727251700000, 99999, 0, 0, 1, 99999, ?, ?, 1)
|
||||
`, now, now); err != nil {
|
||||
t.Fatalf("insert user: %v", err)
|
||||
}
|
||||
|
||||
tunnelRes, err := repo.DB().Exec(`
|
||||
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, "diagnose-chain-tunnel", 1.0, 2, "tls", 99999, now, now, 1, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("insert tunnel: %v", err)
|
||||
}
|
||||
tunnelID, err := tunnelRes.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get tunnel id: %v", err)
|
||||
}
|
||||
|
||||
insertNode := func(name, ip string) int64 {
|
||||
res, err := repo.DB().Exec(`
|
||||
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, name, name+"-secret", ip, ip, "", "30000-30010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("insert node %s: %v", name, err)
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get node id %s: %v", name, err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
entryNodeID := insertNode("entry-node", "10.0.1.10")
|
||||
chainNodeID := insertNode("chain-node", "10.0.1.20")
|
||||
exitNodeID := insertNode("exit-node", "10.0.1.30")
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
||||
VALUES(?, 1, ?, 30001, 'round', 1, 'tls')
|
||||
`, tunnelID, entryNodeID); err != nil {
|
||||
t.Fatalf("insert entry chain: %v", err)
|
||||
}
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
||||
VALUES(?, 2, ?, 30002, 'round', 1, 'tls')
|
||||
`, tunnelID, chainNodeID); err != nil {
|
||||
t.Fatalf("insert middle chain: %v", err)
|
||||
}
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
||||
VALUES(?, 3, ?, 30003, 'round', 1, 'tls')
|
||||
`, tunnelID, exitNodeID); err != nil {
|
||||
t.Fatalf("insert exit chain: %v", err)
|
||||
}
|
||||
|
||||
forwardRes, err := repo.DB().Exec(`
|
||||
INSERT INTO forward(user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, 0, 0, ?, ?, 1, ?)
|
||||
`, 2, "normal_user", "chain-forward", tunnelID, "8.8.8.8:53", "fifo", now, now, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("insert forward: %v", err)
|
||||
}
|
||||
forwardID, err := forwardRes.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get forward id: %v", err)
|
||||
}
|
||||
|
||||
userToken, err := auth.GenerateToken(2, "normal_user", 1, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate user token: %v", err)
|
||||
}
|
||||
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate admin token: %v", err)
|
||||
}
|
||||
|
||||
t.Run("forward diagnose includes entry chain exit paths", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/diagnose", bytes.NewBufferString(`{"forwardId":`+strconv.FormatInt(forwardID, 10)+`}`))
|
||||
req.Header.Set("Authorization", userToken)
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code != 0 {
|
||||
t.Fatalf("expected code 0, got %d (%s)", out.Code, out.Msg)
|
||||
}
|
||||
|
||||
payload, ok := out.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected object payload, got %T", out.Data)
|
||||
}
|
||||
results, ok := payload["results"].([]interface{})
|
||||
if !ok || len(results) == 0 {
|
||||
t.Fatalf("expected non-empty results, got %v", payload["results"])
|
||||
}
|
||||
|
||||
hasEntryToChain := false
|
||||
hasChainToExit := false
|
||||
hasExitToTarget := false
|
||||
for _, raw := range results {
|
||||
item, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected result object, got %T", raw)
|
||||
}
|
||||
if strings.TrimSpace(valueAsString(item["message"])) == "" {
|
||||
t.Fatalf("expected non-empty message field")
|
||||
}
|
||||
from := valueAsInt(item["fromChainType"])
|
||||
to := valueAsInt(item["toChainType"])
|
||||
if from == 1 && to == 2 {
|
||||
hasEntryToChain = true
|
||||
}
|
||||
if from == 2 && to == 3 {
|
||||
hasChainToExit = true
|
||||
}
|
||||
if from == 3 {
|
||||
hasExitToTarget = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasEntryToChain || !hasChainToExit || !hasExitToTarget {
|
||||
t.Fatalf("expected entry->chain, chain->exit, exit->target coverage; got entry=%v chain=%v exit=%v", hasEntryToChain, hasChainToExit, hasExitToTarget)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tunnel diagnose includes entry chain exit groups", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/diagnose", bytes.NewBufferString(`{"tunnelId":`+strconv.FormatInt(tunnelID, 10)+`}`))
|
||||
req.Header.Set("Authorization", adminToken)
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code != 0 {
|
||||
t.Fatalf("expected code 0, got %d (%s)", out.Code, out.Msg)
|
||||
}
|
||||
|
||||
payload, ok := out.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected object payload, got %T", out.Data)
|
||||
}
|
||||
results, ok := payload["results"].([]interface{})
|
||||
if !ok || len(results) == 0 {
|
||||
t.Fatalf("expected non-empty results, got %v", payload["results"])
|
||||
}
|
||||
|
||||
hasEntry := false
|
||||
hasChain := false
|
||||
hasExit := false
|
||||
for _, raw := range results {
|
||||
item, ok := raw.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected result object, got %T", raw)
|
||||
}
|
||||
if strings.TrimSpace(valueAsString(item["message"])) == "" {
|
||||
t.Fatalf("expected non-empty message field")
|
||||
}
|
||||
switch valueAsInt(item["fromChainType"]) {
|
||||
case 1:
|
||||
hasEntry = true
|
||||
case 2:
|
||||
hasChain = true
|
||||
case 3:
|
||||
hasExit = true
|
||||
}
|
||||
}
|
||||
|
||||
if !hasEntry || !hasChain || !hasExit {
|
||||
t.Fatalf("expected entry/chain/exit groups, got entry=%v chain=%v exit=%v", hasEntry, hasChain, hasExit)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func valueAsInt(v interface{}) int {
|
||||
switch n := v.(type) {
|
||||
case float64:
|
||||
return int(n)
|
||||
case int:
|
||||
return n
|
||||
case int64:
|
||||
return int(n)
|
||||
default:
|
||||
return 0
|
||||
}
|
||||
}
|
||||
|
||||
func valueAsString(v interface{}) string {
|
||||
s, _ := v.(string)
|
||||
return s
|
||||
}
|
||||
|
||||
func setupDiagnosisContractRouter(t *testing.T, jwtSecret string) (http.Handler, *sqlite.Repository) {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "diagnosis-contract.db")
|
||||
repo, err := sqlite.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = repo.Close()
|
||||
})
|
||||
|
||||
h := handler.New(repo, jwtSecret)
|
||||
return httpserver.NewRouter(h, jwtSecret), repo
|
||||
}
|
||||
@@ -0,0 +1,44 @@
|
||||
package contract_test
|
||||
|
||||
import (
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
|
||||
"go-backend/internal/http/handler"
|
||||
)
|
||||
|
||||
func TestFlowEndpointsStringResponses(t *testing.T) {
|
||||
h := handler.New(nil, "secret")
|
||||
mux := http.NewServeMux()
|
||||
h.Register(mux)
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
method string
|
||||
path string
|
||||
expected string
|
||||
}{
|
||||
{name: "flow test", method: http.MethodGet, path: "/flow/test", expected: "test"},
|
||||
{name: "flow config", method: http.MethodPost, path: "/flow/config?secret=abc", expected: "ok"},
|
||||
{name: "flow upload", method: http.MethodPost, path: "/flow/upload?secret=abc", expected: "ok"},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
req := httptest.NewRequest(tc.method, tc.path, nil)
|
||||
res := httptest.NewRecorder()
|
||||
mux.ServeHTTP(res, req)
|
||||
|
||||
body, err := io.ReadAll(res.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read body: %v", err)
|
||||
}
|
||||
|
||||
if string(body) != tc.expected {
|
||||
t.Fatalf("expected %q, got %q", tc.expected, string(body))
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,535 @@
|
||||
package contract_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-backend/internal/auth"
|
||||
"go-backend/internal/http/response"
|
||||
)
|
||||
|
||||
func TestForwardOwnershipAndScopeContracts(t *testing.T) {
|
||||
secret := "contract-jwt-secret"
|
||||
router, repo := setupContractRouter(t, secret)
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user(id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status)
|
||||
VALUES(2, 'normal_user', '3c85cdebade1c51cf64ca9f3c09d182d', 1, 2727251700000, 99999, 0, 0, 1, 99999, ?, ?, 1)
|
||||
`, now, now); err != nil {
|
||||
t.Fatalf("insert user: %v", err)
|
||||
}
|
||||
|
||||
res, err := repo.DB().Exec(`
|
||||
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, "contract-tunnel", 1.0, 1, "tls", 99999, now, now, 1, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("insert tunnel: %v", err)
|
||||
}
|
||||
tunnelID, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get tunnel id: %v", err)
|
||||
}
|
||||
|
||||
nodeRes, err := repo.DB().Exec(`
|
||||
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, "entry-node", "entry-secret", "10.0.0.10", "10.0.0.10", "", "20000-20010", "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("insert node: %v", err)
|
||||
}
|
||||
entryNodeID, err := nodeRes.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get node id: %v", err)
|
||||
}
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
||||
VALUES(?, 1, ?, 20001, 'round', 1, 'tls')
|
||||
`, tunnelID, entryNodeID); err != nil {
|
||||
t.Fatalf("insert chain_tunnel: %v", err)
|
||||
}
|
||||
|
||||
resAdmin, err := repo.DB().Exec(`
|
||||
INSERT INTO forward(user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, 0, 0, ?, ?, 1, ?)
|
||||
`, 1, "admin_user", "admin-forward", tunnelID, "1.1.1.1:443", "fifo", now, now, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("insert admin forward: %v", err)
|
||||
}
|
||||
adminForwardID, err := resAdmin.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get admin forward id: %v", err)
|
||||
}
|
||||
|
||||
resUser, err := repo.DB().Exec(`
|
||||
INSERT INTO forward(user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, 0, 0, ?, ?, 1, ?)
|
||||
`, 2, "normal_user", "user-forward", tunnelID, "8.8.8.8:53", "fifo", now, now, 1)
|
||||
if err != nil {
|
||||
t.Fatalf("insert user forward: %v", err)
|
||||
}
|
||||
userForwardID, err := resUser.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get user forward id: %v", err)
|
||||
}
|
||||
|
||||
userToken, err := auth.GenerateToken(2, "normal_user", 1, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate user token: %v", err)
|
||||
}
|
||||
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate admin token: %v", err)
|
||||
}
|
||||
|
||||
t.Run("non-owner cannot delete another user's forward", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/delete", bytes.NewBufferString(`{"id":`+jsonNumber(adminForwardID)+`}`))
|
||||
req.Header.Set("Authorization", userToken)
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
assertCodeMsg(t, res, -1, "转发不存在")
|
||||
})
|
||||
|
||||
t.Run("non-admin forward list is scoped to owner", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/list", bytes.NewBufferString(`{}`))
|
||||
req.Header.Set("Authorization", userToken)
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code != 0 {
|
||||
t.Fatalf("expected code 0, got %d (%s)", out.Code, out.Msg)
|
||||
}
|
||||
arr, ok := out.Data.([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected array data, got %T", out.Data)
|
||||
}
|
||||
if len(arr) != 1 {
|
||||
t.Fatalf("expected 1 forward, got %d", len(arr))
|
||||
}
|
||||
item, ok := arr[0].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected object item, got %T", arr[0])
|
||||
}
|
||||
if got := int64(item["id"].(float64)); got != userForwardID {
|
||||
t.Fatalf("expected forward id %d, got %d", userForwardID, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("forward diagnose returns structured payload", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/diagnose", bytes.NewBufferString(`{"forwardId":`+jsonNumber(userForwardID)+`}`))
|
||||
req.Header.Set("Authorization", userToken)
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code != 0 {
|
||||
t.Fatalf("expected code 0, got %d (%s)", out.Code, out.Msg)
|
||||
}
|
||||
|
||||
payload, ok := out.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected object payload, got %T", out.Data)
|
||||
}
|
||||
results, ok := payload["results"].([]interface{})
|
||||
if !ok || len(results) == 0 {
|
||||
t.Fatalf("expected non-empty results, got %v", payload["results"])
|
||||
}
|
||||
first, ok := results[0].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected result object, got %T", results[0])
|
||||
}
|
||||
if _, ok := first["message"]; !ok {
|
||||
t.Fatalf("expected message field in diagnosis result")
|
||||
}
|
||||
if got := int(first["fromChainType"].(float64)); got != 1 {
|
||||
t.Fatalf("expected fromChainType=1, got %d", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tunnel diagnose returns structured payload", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/diagnose", bytes.NewBufferString(`{"tunnelId":`+jsonNumber(tunnelID)+`}`))
|
||||
req.Header.Set("Authorization", adminToken)
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code != 0 {
|
||||
t.Fatalf("expected code 0, got %d (%s)", out.Code, out.Msg)
|
||||
}
|
||||
|
||||
payload, ok := out.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected object payload, got %T", out.Data)
|
||||
}
|
||||
results, ok := payload["results"].([]interface{})
|
||||
if !ok || len(results) == 0 {
|
||||
t.Fatalf("expected non-empty results, got %v", payload["results"])
|
||||
}
|
||||
first, ok := results[0].(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected result object, got %T", results[0])
|
||||
}
|
||||
if _, ok := first["message"]; !ok {
|
||||
t.Fatalf("expected message field in tunnel diagnosis result")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func TestForwardSwitchTunnelRollbackOnSyncFailure(t *testing.T) {
|
||||
secret := "contract-jwt-secret"
|
||||
router, repo := setupContractRouter(t, secret)
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate admin token: %v", err)
|
||||
}
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user(id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status)
|
||||
VALUES(2, 'switch_user', '3c85cdebade1c51cf64ca9f3c09d182d', 1, 2727251700000, 99999, 0, 0, 1, 99999, ?, ?, 1)
|
||||
`, now, now); err != nil {
|
||||
t.Fatalf("insert user: %v", err)
|
||||
}
|
||||
|
||||
insertTunnel := func(name string, inx int) int64 {
|
||||
res, err := repo.DB().Exec(`
|
||||
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, name, 1.0, 1, "tls", 99999, now, now, 1, nil, inx)
|
||||
if err != nil {
|
||||
t.Fatalf("insert tunnel %s: %v", name, err)
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get tunnel id %s: %v", name, err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
insertNode := func(name, ip, portRange string, inx int) int64 {
|
||||
res, err := repo.DB().Exec(`
|
||||
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, name, name+"-secret", ip, ip, "", portRange, "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", inx)
|
||||
if err != nil {
|
||||
t.Fatalf("insert node %s: %v", name, err)
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get node id %s: %v", name, err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
tunnelA := insertTunnel("switch-tunnel-a", 0)
|
||||
tunnelB := insertTunnel("switch-tunnel-b", 1)
|
||||
nodeA := insertNode("switch-node-a", "10.10.0.1", "21000-21010", 0)
|
||||
nodeB := insertNode("switch-node-b", "10.10.0.2", "22000-22010", 1)
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
||||
VALUES(?, 1, ?, 21001, 'round', 1, 'tls')
|
||||
`, tunnelA, nodeA); err != nil {
|
||||
t.Fatalf("insert chain_tunnel tunnelA: %v", err)
|
||||
}
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol)
|
||||
VALUES(?, 1, ?, 22001, 'round', 1, 'tls')
|
||||
`, tunnelB, nodeB); err != nil {
|
||||
t.Fatalf("insert chain_tunnel tunnelB: %v", err)
|
||||
}
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user_tunnel(id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status)
|
||||
VALUES(10, 2, ?, NULL, 999, 99999, 0, 0, 1, 2727251700000, 1)
|
||||
`, tunnelA); err != nil {
|
||||
t.Fatalf("insert user_tunnel A: %v", err)
|
||||
}
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user_tunnel(id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status)
|
||||
VALUES(11, 2, ?, NULL, 999, 99999, 0, 0, 1, 2727251700000, 1)
|
||||
`, tunnelB); err != nil {
|
||||
t.Fatalf("insert user_tunnel B: %v", err)
|
||||
}
|
||||
|
||||
forwardRes, err := repo.DB().Exec(`
|
||||
INSERT INTO forward(user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx)
|
||||
VALUES(2, 'switch_user', 'switch-forward', ?, '8.8.8.8:53', 'fifo', 0, 0, ?, ?, 1, 0)
|
||||
`, tunnelA, now, now)
|
||||
if err != nil {
|
||||
t.Fatalf("insert forward: %v", err)
|
||||
}
|
||||
forwardID, err := forwardRes.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get forward id: %v", err)
|
||||
}
|
||||
|
||||
if _, err := repo.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, forwardID, nodeA, 21001); err != nil {
|
||||
t.Fatalf("insert forward_port: %v", err)
|
||||
}
|
||||
|
||||
payload := `{"id":` + jsonNumber(forwardID) + `,"tunnelId":` + jsonNumber(tunnelB) + `}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/update", bytes.NewBufferString(payload))
|
||||
req.Header.Set("Authorization", adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code == 0 {
|
||||
t.Fatalf("expected update failure when node is offline")
|
||||
}
|
||||
|
||||
var tunnelAfter int64
|
||||
if err := repo.DB().QueryRow(`SELECT tunnel_id FROM forward WHERE id = ?`, forwardID).Scan(&tunnelAfter); err != nil {
|
||||
t.Fatalf("query forward tunnel_id: %v", err)
|
||||
}
|
||||
if tunnelAfter != tunnelA {
|
||||
t.Fatalf("expected tunnel rollback to %d, got %d", tunnelA, tunnelAfter)
|
||||
}
|
||||
|
||||
var nodeAfter int64
|
||||
var portAfter int
|
||||
if err := repo.DB().QueryRow(`SELECT node_id, port FROM forward_port WHERE forward_id = ? LIMIT 1`, forwardID).Scan(&nodeAfter, &portAfter); err != nil {
|
||||
t.Fatalf("query forward_port: %v", err)
|
||||
}
|
||||
if nodeAfter != nodeA || portAfter != 21001 {
|
||||
t.Fatalf("expected forward_port rollback to node=%d port=21001, got node=%d port=%d", nodeA, nodeAfter, portAfter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestForwardBatchChangeTunnelRollbackOnSyncFailure(t *testing.T) {
|
||||
secret := "contract-jwt-secret"
|
||||
router, repo := setupContractRouter(t, secret)
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate admin token: %v", err)
|
||||
}
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user(id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status)
|
||||
VALUES(2, 'batch_switch_user', '3c85cdebade1c51cf64ca9f3c09d182d', 1, 2727251700000, 99999, 0, 0, 1, 99999, ?, ?, 1)
|
||||
`, now, now); err != nil {
|
||||
t.Fatalf("insert user: %v", err)
|
||||
}
|
||||
|
||||
tunnelResA, err := repo.DB().Exec(`
|
||||
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||
VALUES('batch-switch-tunnel-a', 1.0, 1, 'tls', 99999, ?, ?, 1, NULL, 0)
|
||||
`, now, now)
|
||||
if err != nil {
|
||||
t.Fatalf("insert tunnel A: %v", err)
|
||||
}
|
||||
tunnelA, _ := tunnelResA.LastInsertId()
|
||||
|
||||
tunnelResB, err := repo.DB().Exec(`
|
||||
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||
VALUES('batch-switch-tunnel-b', 1.0, 1, 'tls', 99999, ?, ?, 1, NULL, 1)
|
||||
`, now, now)
|
||||
if err != nil {
|
||||
t.Fatalf("insert tunnel B: %v", err)
|
||||
}
|
||||
tunnelB, _ := tunnelResB.LastInsertId()
|
||||
|
||||
nodeResA, err := repo.DB().Exec(`
|
||||
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||
VALUES('batch-switch-node-a', 'batch-switch-node-a-secret', '10.11.0.1', '10.11.0.1', '', '23000-23010', '', 'v1', 1, 1, 1, ?, ?, 1, '[::]', '[::]', 0)
|
||||
`, now, now)
|
||||
if err != nil {
|
||||
t.Fatalf("insert node A: %v", err)
|
||||
}
|
||||
nodeA, _ := nodeResA.LastInsertId()
|
||||
|
||||
nodeResB, err := repo.DB().Exec(`
|
||||
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||
VALUES('batch-switch-node-b', 'batch-switch-node-b-secret', '10.11.0.2', '10.11.0.2', '', '24000-24010', '', 'v1', 1, 1, 1, ?, ?, 1, '[::]', '[::]', 1)
|
||||
`, now, now)
|
||||
if err != nil {
|
||||
t.Fatalf("insert node B: %v", err)
|
||||
}
|
||||
nodeB, _ := nodeResB.LastInsertId()
|
||||
|
||||
if _, err := repo.DB().Exec(`INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES(?, 1, ?, 23001, 'round', 1, 'tls')`, tunnelA, nodeA); err != nil {
|
||||
t.Fatalf("insert chain_tunnel A: %v", err)
|
||||
}
|
||||
if _, err := repo.DB().Exec(`INSERT INTO chain_tunnel(tunnel_id, chain_type, node_id, port, strategy, inx, protocol) VALUES(?, 1, ?, 24001, 'round', 1, 'tls')`, tunnelB, nodeB); err != nil {
|
||||
t.Fatalf("insert chain_tunnel B: %v", err)
|
||||
}
|
||||
|
||||
if _, err := repo.DB().Exec(`INSERT INTO user_tunnel(id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status) VALUES(20, 2, ?, NULL, 999, 99999, 0, 0, 1, 2727251700000, 1)`, tunnelA); err != nil {
|
||||
t.Fatalf("insert user_tunnel A: %v", err)
|
||||
}
|
||||
if _, err := repo.DB().Exec(`INSERT INTO user_tunnel(id, user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status) VALUES(21, 2, ?, NULL, 999, 99999, 0, 0, 1, 2727251700000, 1)`, tunnelB); err != nil {
|
||||
t.Fatalf("insert user_tunnel B: %v", err)
|
||||
}
|
||||
|
||||
forwardRes, err := repo.DB().Exec(`
|
||||
INSERT INTO forward(user_id, user_name, name, tunnel_id, remote_addr, strategy, in_flow, out_flow, created_time, updated_time, status, inx)
|
||||
VALUES(2, 'batch_switch_user', 'batch-switch-forward', ?, '1.1.1.1:443', 'fifo', 0, 0, ?, ?, 1, 0)
|
||||
`, tunnelA, now, now)
|
||||
if err != nil {
|
||||
t.Fatalf("insert forward: %v", err)
|
||||
}
|
||||
forwardID, _ := forwardRes.LastInsertId()
|
||||
|
||||
if _, err := repo.DB().Exec(`INSERT INTO forward_port(forward_id, node_id, port) VALUES(?, ?, ?)`, forwardID, nodeA, 23001); err != nil {
|
||||
t.Fatalf("insert forward_port: %v", err)
|
||||
}
|
||||
|
||||
payload := `{"forwardIds":[` + jsonNumber(forwardID) + `],"targetTunnelId":` + jsonNumber(tunnelB) + `}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/forward/batch-change-tunnel", bytes.NewBufferString(payload))
|
||||
req.Header.Set("Authorization", adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code != 0 {
|
||||
t.Fatalf("expected API success envelope, got code=%d msg=%q", out.Code, out.Msg)
|
||||
}
|
||||
|
||||
result, ok := out.Data.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected map result, got %T", out.Data)
|
||||
}
|
||||
if int(result["failCount"].(float64)) != 1 {
|
||||
t.Fatalf("expected failCount=1, got %v", result["failCount"])
|
||||
}
|
||||
|
||||
var tunnelAfter int64
|
||||
if err := repo.DB().QueryRow(`SELECT tunnel_id FROM forward WHERE id = ?`, forwardID).Scan(&tunnelAfter); err != nil {
|
||||
t.Fatalf("query forward tunnel_id: %v", err)
|
||||
}
|
||||
if tunnelAfter != tunnelA {
|
||||
t.Fatalf("expected tunnel rollback to %d, got %d", tunnelA, tunnelAfter)
|
||||
}
|
||||
|
||||
var nodeAfter int64
|
||||
var portAfter int
|
||||
if err := repo.DB().QueryRow(`SELECT node_id, port FROM forward_port WHERE forward_id = ? LIMIT 1`, forwardID).Scan(&nodeAfter, &portAfter); err != nil {
|
||||
t.Fatalf("query forward_port: %v", err)
|
||||
}
|
||||
if nodeAfter != nodeA || portAfter != 23001 {
|
||||
t.Fatalf("expected forward_port rollback to node=%d port=23001, got node=%d port=%d", nodeA, nodeAfter, portAfter)
|
||||
}
|
||||
}
|
||||
|
||||
func TestUserTunnelReassignmentKeepsStableID(t *testing.T) {
|
||||
secret := "contract-jwt-secret"
|
||||
router, repo := setupContractRouter(t, secret)
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate admin token: %v", err)
|
||||
}
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user(id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status)
|
||||
VALUES(100, 'stable_user', 'pwd', 1, 2727251700000, 99999, 0, 0, 1, 99999, ?, ?, 1)
|
||||
`, now, now); err != nil {
|
||||
t.Fatalf("insert user: %v", err)
|
||||
}
|
||||
|
||||
tunnelRes, err := repo.DB().Exec(`
|
||||
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||
VALUES('stable-tunnel', 1.0, 1, 'tls', 99999, ?, ?, 1, NULL, 0)
|
||||
`, now, now)
|
||||
if err != nil {
|
||||
t.Fatalf("insert tunnel: %v", err)
|
||||
}
|
||||
tunnelID, _ := tunnelRes.LastInsertId()
|
||||
|
||||
// 1. Assign permission (creates new user_tunnel)
|
||||
// userTunnelBatchAssign expects structure: {userId: 123, tunnels: [{tunnelId: 456, ...}]}
|
||||
assignPayload := `{"userId":100,"tunnels":[{"tunnelId":` + jsonNumber(tunnelID) + `}]}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/user/batch-assign", bytes.NewBufferString(assignPayload))
|
||||
req.Header.Set("Authorization", adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code != 0 {
|
||||
t.Fatalf("expected code 0, got %d msg=%q", out.Code, out.Msg)
|
||||
}
|
||||
|
||||
var initialID int64
|
||||
if err := repo.DB().QueryRow(`SELECT id FROM user_tunnel WHERE user_id = 100 AND tunnel_id = ?`, tunnelID).Scan(&initialID); err != nil {
|
||||
t.Fatalf("query initial user_tunnel id: %v", err)
|
||||
}
|
||||
|
||||
// 2. Re-assign permission (should UPDATE, not INSERT)
|
||||
reassignPayload := `{"userId":100,"tunnels":[{"tunnelId":` + jsonNumber(tunnelID) + `}]}`
|
||||
req2 := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/user/batch-assign", bytes.NewBufferString(reassignPayload))
|
||||
req2.Header.Set("Authorization", adminToken)
|
||||
req2.Header.Set("Content-Type", "application/json")
|
||||
res2 := httptest.NewRecorder()
|
||||
router.ServeHTTP(res2, req2)
|
||||
|
||||
var out2 response.R
|
||||
if err := json.NewDecoder(res2.Body).Decode(&out2); err != nil {
|
||||
t.Fatalf("decode response 2: %v", err)
|
||||
}
|
||||
if out2.Code != 0 {
|
||||
t.Fatalf("expected code 0, got %d msg=%q", out2.Code, out2.Msg)
|
||||
}
|
||||
|
||||
// 3. Verify stable ID and no duplicates
|
||||
var count int
|
||||
if err := repo.DB().QueryRow(`SELECT COUNT(1) FROM user_tunnel WHERE user_id = 100 AND tunnel_id = ?`, tunnelID).Scan(&count); err != nil {
|
||||
t.Fatalf("query count: %v", err)
|
||||
}
|
||||
if count != 1 {
|
||||
t.Fatalf("expected exactly 1 user_tunnel record, got %d", count)
|
||||
}
|
||||
|
||||
var currentID int64
|
||||
if err := repo.DB().QueryRow(`SELECT id FROM user_tunnel WHERE user_id = 100 AND tunnel_id = ?`, tunnelID).Scan(¤tID); err != nil {
|
||||
t.Fatalf("query current user_tunnel: %v", err)
|
||||
}
|
||||
|
||||
if currentID != initialID {
|
||||
t.Fatalf("user_tunnel ID changed from %d to %d (unstable ID!)", initialID, currentID)
|
||||
}
|
||||
}
|
||||
|
||||
func jsonNumber(v int64) string {
|
||||
return strconv.FormatInt(v, 10)
|
||||
}
|
||||
@@ -0,0 +1,215 @@
|
||||
package contract_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-backend/internal/auth"
|
||||
httpserver "go-backend/internal/http"
|
||||
"go-backend/internal/http/handler"
|
||||
"go-backend/internal/http/response"
|
||||
"go-backend/internal/store/sqlite"
|
||||
)
|
||||
|
||||
func TestCaptchaVerifyLoginContract(t *testing.T) {
|
||||
secret := "contract-jwt-secret"
|
||||
router, repo := setupContractRouter(t, secret)
|
||||
|
||||
_, err := repo.DB().Exec(`
|
||||
INSERT INTO vite_config(name, value, time)
|
||||
VALUES(?, ?, ?)
|
||||
ON CONFLICT(name) DO UPDATE SET value = excluded.value, time = excluded.time
|
||||
`, "captcha_enabled", "true", time.Now().UnixMilli())
|
||||
if err != nil {
|
||||
t.Fatalf("enable captcha: %v", err)
|
||||
}
|
||||
|
||||
t.Run("login denied without verified captcha token", func(t *testing.T) {
|
||||
body := bytes.NewBufferString(`{"username":"admin_user","password":"admin_user","captchaId":""}`)
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", body)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
assertCodeMsg(t, resp, -1, "验证码校验失败")
|
||||
})
|
||||
|
||||
t.Run("captcha token is one-time and consumed by login", func(t *testing.T) {
|
||||
verifyReq := httptest.NewRequest(http.MethodPost, "/api/v1/captcha/verify", bytes.NewBufferString(`{"id":"captcha-token-1","data":"ok"}`))
|
||||
verifyReq.Header.Set("Content-Type", "application/json")
|
||||
verifyResp := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(verifyResp, verifyReq)
|
||||
|
||||
var verifyOut struct {
|
||||
Success bool `json:"success"`
|
||||
Data struct {
|
||||
ValidToken string `json:"validToken"`
|
||||
} `json:"data"`
|
||||
}
|
||||
if err := json.NewDecoder(verifyResp.Body).Decode(&verifyOut); err != nil {
|
||||
t.Fatalf("decode captcha verify response: %v", err)
|
||||
}
|
||||
if !verifyOut.Success || verifyOut.Data.ValidToken != "captcha-token-1" {
|
||||
t.Fatalf("unexpected captcha verify payload: success=%v token=%q", verifyOut.Success, verifyOut.Data.ValidToken)
|
||||
}
|
||||
|
||||
loginBody := bytes.NewBufferString(`{"username":"admin_user","password":"admin_user","captchaId":"captcha-token-1"}`)
|
||||
loginReq := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", loginBody)
|
||||
loginReq.Header.Set("Content-Type", "application/json")
|
||||
loginResp := httptest.NewRecorder()
|
||||
router.ServeHTTP(loginResp, loginReq)
|
||||
assertCode(t, loginResp, 0)
|
||||
|
||||
replayBody := bytes.NewBufferString(`{"username":"admin_user","password":"admin_user","captchaId":"captcha-token-1"}`)
|
||||
replayReq := httptest.NewRequest(http.MethodPost, "/api/v1/user/login", replayBody)
|
||||
replayReq.Header.Set("Content-Type", "application/json")
|
||||
replayResp := httptest.NewRecorder()
|
||||
router.ServeHTTP(replayResp, replayReq)
|
||||
assertCodeMsg(t, replayResp, -1, "验证码校验失败")
|
||||
})
|
||||
}
|
||||
|
||||
func TestOpenAPISubStoreContracts(t *testing.T) {
|
||||
router, repo := setupContractRouter(t, "contract-jwt-secret")
|
||||
|
||||
const tunnelFlowGB = int64(500)
|
||||
const tunnelInFlow = int64(123)
|
||||
const tunnelOutFlow = int64(456)
|
||||
const tunnelExpTimeMs = int64(2727251700000)
|
||||
|
||||
now := time.Now().UnixMilli()
|
||||
res, err := repo.DB().Exec(`INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx) VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
"contract-tunnel", 1.0, 1, "tls", 1, now, now, 1, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("insert tunnel: %v", err)
|
||||
}
|
||||
tunnelID, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("last insert id: %v", err)
|
||||
}
|
||||
if _, err := repo.DB().Exec(`INSERT INTO user_tunnel(user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status) VALUES(?, ?, NULL, ?, ?, ?, ?, ?, ?, ?)`,
|
||||
1, tunnelID, 99999, tunnelFlowGB, tunnelInFlow, tunnelOutFlow, 1, tunnelExpTimeMs, 1); err != nil {
|
||||
t.Fatalf("insert user_tunnel: %v", err)
|
||||
}
|
||||
|
||||
t.Run("default user subscription payload", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/open_api/sub_store?user=admin_user&pwd=admin_user", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read body: %v", err)
|
||||
}
|
||||
|
||||
expected := "upload=0; download=0; total=107373108658176; expire=2727251700"
|
||||
if string(body) != expected {
|
||||
t.Fatalf("expected body %q, got %q", expected, string(body))
|
||||
}
|
||||
if got := resp.Header().Get("subscription-userinfo"); got != expected {
|
||||
t.Fatalf("expected subscription-userinfo %q, got %q", expected, got)
|
||||
}
|
||||
if !strings.Contains(resp.Header().Get("Content-Type"), "text/plain") {
|
||||
t.Fatalf("expected text/plain content type, got %q", resp.Header().Get("Content-Type"))
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tunnel scoped subscription payload", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/open_api/sub_store?user=admin_user&pwd=admin_user&tunnel="+strconv.FormatInt(tunnelID, 10), nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
body, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.Fatalf("read body: %v", err)
|
||||
}
|
||||
|
||||
expected := "upload=123; download=456; total=536870912000; expire=2727251700"
|
||||
if string(body) != expected {
|
||||
t.Fatalf("expected body %q, got %q", expected, string(body))
|
||||
}
|
||||
if got := resp.Header().Get("subscription-userinfo"); got != expected {
|
||||
t.Fatalf("expected subscription-userinfo %q, got %q", expected, got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("invalid credentials returns contract error", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/open_api/sub_store?user=admin_user&pwd=wrong", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
assertCodeMsg(t, resp, -1, "鉴权失败")
|
||||
})
|
||||
|
||||
t.Run("missing tunnel returns contract error", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodGet, "/api/v1/open_api/sub_store?user=admin_user&pwd=admin_user&tunnel=999999", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
assertCodeMsg(t, resp, -1, "隧道不存在")
|
||||
})
|
||||
}
|
||||
|
||||
func TestSpeedLimitTunnelsRouteAlias(t *testing.T) {
|
||||
secret := "contract-jwt-secret"
|
||||
router, _ := setupContractRouter(t, secret)
|
||||
|
||||
t.Run("missing token blocked", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/speed-limit/tunnels", nil)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
assertCodeMsg(t, resp, 401, "未登录或token已过期")
|
||||
})
|
||||
|
||||
t.Run("admin token receives success envelope", func(t *testing.T) {
|
||||
token, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate token: %v", err)
|
||||
}
|
||||
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/speed-limit/tunnels", nil)
|
||||
req.Header.Set("Authorization", token)
|
||||
resp := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(resp, req)
|
||||
|
||||
var out response.R
|
||||
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code != 0 {
|
||||
t.Fatalf("expected code 0, got %d (%s)", out.Code, out.Msg)
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func setupContractRouter(t *testing.T, jwtSecret string) (http.Handler, *sqlite.Repository) {
|
||||
t.Helper()
|
||||
dbPath := filepath.Join(t.TempDir(), "contract.db")
|
||||
repo, err := sqlite.Open(dbPath)
|
||||
if err != nil {
|
||||
t.Fatalf("open sqlite: %v", err)
|
||||
}
|
||||
t.Cleanup(func() {
|
||||
_ = repo.Close()
|
||||
})
|
||||
|
||||
h := handler.New(repo, jwtSecret)
|
||||
return httpserver.NewRouter(h, jwtSecret), repo
|
||||
}
|
||||
@@ -0,0 +1,151 @@
|
||||
package contract_test
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"strconv"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-backend/internal/auth"
|
||||
"go-backend/internal/http/response"
|
||||
)
|
||||
|
||||
func TestTunnelCreateRuntimeRollbackContract(t *testing.T) {
|
||||
secret := "contract-jwt-secret"
|
||||
router, repo := setupContractRouter(t, secret)
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate admin token: %v", err)
|
||||
}
|
||||
|
||||
insertNode := func(name, ip, portRange string) int64 {
|
||||
res, err := repo.DB().Exec(`
|
||||
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, name, name+"-secret", ip, ip, "", portRange, "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("insert node %s: %v", name, err)
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get node id %s: %v", name, err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
entryID := insertNode("create-entry", "10.20.0.1", "30000-30010")
|
||||
chainID := insertNode("create-chain", "10.20.0.2", "31000-31010")
|
||||
exitID := insertNode("create-exit", "10.20.0.3", "32000-32010")
|
||||
|
||||
payload := `{"name":"runtime-rollback-tunnel","type":2,"flow":99999,"status":1,"inNodeId":[{"nodeId":` + jsonInt(entryID) + `,"protocol":"tls"}],"chainNodes":[[{"nodeId":` + jsonInt(chainID) + `,"protocol":"tls","strategy":"round"}]],"outNodeId":[{"nodeId":` + jsonInt(exitID) + `,"protocol":"tls"}]}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/create", bytes.NewBufferString(payload))
|
||||
req.Header.Set("Authorization", adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code == 0 {
|
||||
t.Fatalf("expected create failure when nodes are offline")
|
||||
}
|
||||
if !strings.Contains(out.Msg, "节点") {
|
||||
t.Fatalf("expected node-related error, got %q", out.Msg)
|
||||
}
|
||||
|
||||
var tunnelCount int
|
||||
if err := repo.DB().QueryRow(`SELECT COUNT(1) FROM tunnel WHERE name = ?`, "runtime-rollback-tunnel").Scan(&tunnelCount); err != nil {
|
||||
t.Fatalf("count tunnel: %v", err)
|
||||
}
|
||||
if tunnelCount != 0 {
|
||||
t.Fatalf("expected tunnel rollback, found %d records", tunnelCount)
|
||||
}
|
||||
|
||||
var chainCount int
|
||||
if err := repo.DB().QueryRow(`SELECT COUNT(1) FROM chain_tunnel`).Scan(&chainCount); err != nil {
|
||||
t.Fatalf("count chain_tunnel: %v", err)
|
||||
}
|
||||
if chainCount != 0 {
|
||||
t.Fatalf("expected chain_tunnel rollback, found %d records", chainCount)
|
||||
}
|
||||
}
|
||||
|
||||
func TestTunnelUpdateAssignsChainPortsContract(t *testing.T) {
|
||||
secret := "contract-jwt-secret"
|
||||
router, repo := setupContractRouter(t, secret)
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate admin token: %v", err)
|
||||
}
|
||||
|
||||
insertNode := func(name, ip, portRange string) int64 {
|
||||
res, err := repo.DB().Exec(`
|
||||
INSERT INTO node(name, secret, server_ip, server_ip_v4, server_ip_v6, port, interface_name, version, http, tls, socks, created_time, updated_time, status, tcp_listen_addr, udp_listen_addr, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, name, name+"-secret", ip, ip, "", portRange, "", "v1", 1, 1, 1, now, now, 1, "[::]", "[::]", 0)
|
||||
if err != nil {
|
||||
t.Fatalf("insert node %s: %v", name, err)
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get node id %s: %v", name, err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
entryID := insertNode("update-entry", "10.30.0.1", "40000-40010")
|
||||
chainID := insertNode("update-chain", "10.30.0.2", "41000-41010")
|
||||
exitID := insertNode("update-exit", "10.30.0.3", "42000-42010")
|
||||
|
||||
tunnelRes, err := repo.DB().Exec(`
|
||||
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, "update-port-tunnel", 1.0, 1, "tls", 99999, now, now, 1, nil, 0)
|
||||
if err != nil {
|
||||
t.Fatalf("insert tunnel: %v", err)
|
||||
}
|
||||
tunnelID, err := tunnelRes.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get tunnel id: %v", err)
|
||||
}
|
||||
|
||||
payload := `{"id":` + jsonInt(tunnelID) + `,"name":"update-port-tunnel","type":2,"flow":99999,"trafficRatio":1.0,"status":1,"inNodeId":[{"nodeId":` + jsonInt(entryID) + `,"protocol":"tls"}],"chainNodes":[[{"nodeId":` + jsonInt(chainID) + `,"protocol":"tls","strategy":"round"}]],"outNodeId":[{"nodeId":` + jsonInt(exitID) + `,"protocol":"tls"}]}`
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/update", bytes.NewBufferString(payload))
|
||||
req.Header.Set("Authorization", adminToken)
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(res, req)
|
||||
assertCode(t, res, 0)
|
||||
|
||||
var chainPort int
|
||||
if err := repo.DB().QueryRow(`SELECT port FROM chain_tunnel WHERE tunnel_id = ? AND chain_type = 2 LIMIT 1`, tunnelID).Scan(&chainPort); err != nil {
|
||||
t.Fatalf("query chain port: %v", err)
|
||||
}
|
||||
if chainPort <= 0 {
|
||||
t.Fatalf("expected chain node port to be assigned, got %d", chainPort)
|
||||
}
|
||||
|
||||
var outPort int
|
||||
if err := repo.DB().QueryRow(`SELECT port FROM chain_tunnel WHERE tunnel_id = ? AND chain_type = 3 LIMIT 1`, tunnelID).Scan(&outPort); err != nil {
|
||||
t.Fatalf("query out port: %v", err)
|
||||
}
|
||||
if outPort <= 0 {
|
||||
t.Fatalf("expected out node port to be assigned, got %d", outPort)
|
||||
}
|
||||
}
|
||||
|
||||
func jsonInt(v int64) string {
|
||||
return strconv.FormatInt(v, 10)
|
||||
}
|
||||
@@ -0,0 +1,138 @@
|
||||
package contract
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"net/http"
|
||||
"net/http/httptest"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"go-backend/internal/auth"
|
||||
"go-backend/internal/http/response"
|
||||
)
|
||||
|
||||
func TestUserTunnelVisibleListContracts(t *testing.T) {
|
||||
secret := "contract-jwt-secret"
|
||||
router, repo := setupDiagnosisContractRouter(t, secret)
|
||||
now := time.Now().UnixMilli()
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user(id, user, pwd, role_id, exp_time, flow, in_flow, out_flow, flow_reset_time, num, created_time, updated_time, status)
|
||||
VALUES(2, 'normal_user', '3c85cdebade1c51cf64ca9f3c09d182d', 1, 2727251700000, 99999, 0, 0, 1, 99999, ?, ?, 1)
|
||||
`, now, now); err != nil {
|
||||
t.Fatalf("insert user: %v", err)
|
||||
}
|
||||
|
||||
insertTunnel := func(name string, status int, inx int64) int64 {
|
||||
res, err := repo.DB().Exec(`
|
||||
INSERT INTO tunnel(name, traffic_ratio, type, protocol, flow, created_time, updated_time, status, in_ip, inx)
|
||||
VALUES(?, ?, ?, ?, ?, ?, ?, ?, ?, ?)
|
||||
`, name, 1.0, 1, "tls", 99999, now, now, status, nil, inx)
|
||||
if err != nil {
|
||||
t.Fatalf("insert tunnel %s: %v", name, err)
|
||||
}
|
||||
id, err := res.LastInsertId()
|
||||
if err != nil {
|
||||
t.Fatalf("get tunnel id %s: %v", name, err)
|
||||
}
|
||||
return id
|
||||
}
|
||||
|
||||
enabledA := insertTunnel("enabled-A", 1, 1)
|
||||
enabledB := insertTunnel("enabled-B", 1, 2)
|
||||
disabledC := insertTunnel("disabled-C", 0, 3)
|
||||
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user_tunnel(user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status)
|
||||
VALUES(?, ?, NULL, ?, ?, 0, 0, ?, ?, ?)
|
||||
`, 2, enabledA, 100, 1000, 1, 2727251700000, 0); err != nil {
|
||||
t.Fatalf("insert user_tunnel enabledA: %v", err)
|
||||
}
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user_tunnel(user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status)
|
||||
VALUES(?, ?, NULL, ?, ?, 0, 0, ?, ?, ?)
|
||||
`, 2, enabledB, 100, 1000, 1, 2727251700000, 1); err != nil {
|
||||
t.Fatalf("insert user_tunnel enabledB: %v", err)
|
||||
}
|
||||
if _, err := repo.DB().Exec(`
|
||||
INSERT INTO user_tunnel(user_id, tunnel_id, speed_id, num, flow, in_flow, out_flow, flow_reset_time, exp_time, status)
|
||||
VALUES(?, ?, NULL, ?, ?, 0, 0, ?, ?, ?)
|
||||
`, 2, disabledC, 100, 1000, 1, 2727251700000, 1); err != nil {
|
||||
t.Fatalf("insert user_tunnel disabledC: %v", err)
|
||||
}
|
||||
|
||||
adminToken, err := auth.GenerateToken(1, "admin_user", 0, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate admin token: %v", err)
|
||||
}
|
||||
userToken, err := auth.GenerateToken(2, "normal_user", 1, secret)
|
||||
if err != nil {
|
||||
t.Fatalf("generate user token: %v", err)
|
||||
}
|
||||
|
||||
t.Run("admin sees all enabled tunnels without user_tunnel rows", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/user/tunnel", nil)
|
||||
req.Header.Set("Authorization", adminToken)
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code != 0 {
|
||||
t.Fatalf("expected code 0, got %d (%s)", out.Code, out.Msg)
|
||||
}
|
||||
|
||||
ids := collectTunnelIDs(t, out.Data)
|
||||
if !ids[enabledA] || !ids[enabledB] {
|
||||
t.Fatalf("expected enabled tunnels for admin, got %v", ids)
|
||||
}
|
||||
if ids[disabledC] {
|
||||
t.Fatalf("did not expect disabled tunnel for admin")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("normal user sees enabled assigned tunnels regardless of user_tunnel status", func(t *testing.T) {
|
||||
req := httptest.NewRequest(http.MethodPost, "/api/v1/tunnel/user/tunnel", nil)
|
||||
req.Header.Set("Authorization", userToken)
|
||||
res := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(res, req)
|
||||
|
||||
var out response.R
|
||||
if err := json.NewDecoder(res.Body).Decode(&out); err != nil {
|
||||
t.Fatalf("decode response: %v", err)
|
||||
}
|
||||
if out.Code != 0 {
|
||||
t.Fatalf("expected code 0, got %d (%s)", out.Code, out.Msg)
|
||||
}
|
||||
|
||||
ids := collectTunnelIDs(t, out.Data)
|
||||
if !ids[enabledA] || !ids[enabledB] {
|
||||
t.Fatalf("expected enabled assigned tunnels for user, got %v", ids)
|
||||
}
|
||||
if ids[disabledC] {
|
||||
t.Fatalf("did not expect disabled tunnel for user")
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
func collectTunnelIDs(t *testing.T, data interface{}) map[int64]bool {
|
||||
t.Helper()
|
||||
arr, ok := data.([]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected array data, got %T", data)
|
||||
}
|
||||
ids := make(map[int64]bool, len(arr))
|
||||
for _, item := range arr {
|
||||
obj, ok := item.(map[string]interface{})
|
||||
if !ok {
|
||||
t.Fatalf("expected object item, got %T", item)
|
||||
}
|
||||
id := int64(obj["id"].(float64))
|
||||
ids[id] = true
|
||||
}
|
||||
return ids
|
||||
}
|
||||
@@ -0,0 +1,37 @@
|
||||
# GO-GOST SERVICE KNOWLEDGE BASE
|
||||
|
||||
**Generated:** Mon Feb 02 2026
|
||||
|
||||
## OVERVIEW
|
||||
Forwarding agent built on GOST v3 with a local fork of `github.com/go-gost/x` under `x/`.
|
||||
**Stack:** Go 1.23, github.com/go-gost/core v0.3.1, local `go-gost/x` module.
|
||||
|
||||
## STRUCTURE
|
||||
```
|
||||
go-gost/
|
||||
├── main.go # Entry; reads panel config.json; starts svc.Run(program)
|
||||
├── config.go # Panel config.json loader (addr/secret + ports)
|
||||
├── program.go # GOST runtime: parse config, run/reload services
|
||||
├── x/ # Local fork of github.com/go-gost/x (has its own go.mod)
|
||||
└── go.mod # replace github.com/go-gost/x => ./x
|
||||
```
|
||||
|
||||
## WHERE TO LOOK
|
||||
| Task | Location | Notes |
|
||||
|------|----------|-------|
|
||||
| Panel integration config | `go-gost/config.go` | Expects `config.json` in cwd by default |
|
||||
| Service lifecycle/reload | `go-gost/program.go` | Parses config; handles SIGHUP reload |
|
||||
| WebSocket reporting | `go-gost/main.go` | Starts reporter + sets HTTP report URL |
|
||||
| Protocol behaviors | `go-gost/x/` | Handlers/listeners/dialers live here |
|
||||
|
||||
## CONVENTIONS
|
||||
- Two configs exist: panel integration uses `config.json`; forwarding services use GOST config (defaults to `gost.{json,yaml}` via viper search paths).
|
||||
- `go-gost/x/` is the primary extension surface; avoid editing vendored deps.
|
||||
|
||||
## COMMANDS
|
||||
```bash
|
||||
cd go-gost
|
||||
go run .
|
||||
go test ./...
|
||||
go build .
|
||||
```
|
||||
@@ -0,0 +1,43 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"fmt"
|
||||
"os"
|
||||
)
|
||||
|
||||
// Config 配置结构体
|
||||
type Config struct {
|
||||
Addr string `json:"addr"`
|
||||
Secret string `json:"secret"`
|
||||
Http int `json:"http"`
|
||||
Tls int `json:"tls"`
|
||||
Socks int `json:"socks"`
|
||||
}
|
||||
|
||||
// LoadConfig 加载配置文件
|
||||
func LoadConfig(configPath string) (*Config, error) {
|
||||
// 检查文件是否存在
|
||||
if _, err := os.Stat(configPath); os.IsNotExist(err) {
|
||||
return nil, fmt.Errorf("配置文件不存在: %s", configPath)
|
||||
}
|
||||
|
||||
// 读取文件内容
|
||||
data, err := os.ReadFile(configPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("读取配置文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 解析JSON
|
||||
var config Config
|
||||
if err := json.Unmarshal(data, &config); err != nil {
|
||||
return nil, fmt.Errorf("解析配置文件失败: %v", err)
|
||||
}
|
||||
|
||||
// 验证必要的配置项
|
||||
if config.Addr == "" {
|
||||
return nil, fmt.Errorf("服务器地址不能为空")
|
||||
}
|
||||
|
||||
return &config, nil
|
||||
}
|
||||
+150
@@ -0,0 +1,150 @@
|
||||
module github.com/go-gost/gost
|
||||
|
||||
go 1.23.0
|
||||
|
||||
toolchain go1.23.4
|
||||
|
||||
require (
|
||||
github.com/go-gost/core v0.3.1
|
||||
github.com/go-gost/x v0.5.3
|
||||
github.com/judwhite/go-svc v1.2.1
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da // indirect
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 // indirect
|
||||
github.com/alessio/shellescape v1.4.1 // indirect
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d // indirect
|
||||
github.com/beorn7/perks v1.0.1 // indirect
|
||||
github.com/bwmarrin/snowflake v0.3.0 // indirect
|
||||
github.com/bytedance/sonic v1.11.6 // indirect
|
||||
github.com/bytedance/sonic/loader v0.1.1 // indirect
|
||||
github.com/cespare/xxhash/v2 v2.3.0 // indirect
|
||||
github.com/cloudwego/base64x v0.1.4 // indirect
|
||||
github.com/cloudwego/iasm v0.2.0 // indirect
|
||||
github.com/coreos/go-iptables v0.7.0 // indirect
|
||||
github.com/danieljoos/wincred v1.2.0 // indirect
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f // indirect
|
||||
github.com/expr-lang/expr v1.16.3 // indirect
|
||||
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf // indirect
|
||||
github.com/fsnotify/fsnotify v1.7.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 // indirect
|
||||
github.com/gin-contrib/cors v1.7.2 // indirect
|
||||
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||
github.com/gin-gonic/gin v1.10.0 // indirect
|
||||
github.com/go-gost/gosocks4 v0.0.1 // indirect
|
||||
github.com/go-gost/gosocks5 v0.4.2 // indirect
|
||||
github.com/go-gost/plugin v0.2.0 // indirect
|
||||
github.com/go-gost/relay v0.5.0 // indirect
|
||||
github.com/go-gost/tls-dissector v0.1.1 // indirect
|
||||
github.com/go-ole/go-ole v1.2.6 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.20.0 // indirect
|
||||
github.com/go-redis/redis/v8 v8.11.5 // indirect
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 // indirect
|
||||
github.com/gobwas/glob v0.2.3 // indirect
|
||||
github.com/goccy/go-json v0.10.2 // indirect
|
||||
github.com/godbus/dbus/v5 v5.1.0 // indirect
|
||||
github.com/golang/snappy v0.0.4 // indirect
|
||||
github.com/google/go-cmp v0.6.0 // indirect
|
||||
github.com/google/gopacket v1.1.20-0.20220810144506-32ee38206866 // indirect
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad // indirect
|
||||
github.com/google/uuid v1.6.0 // indirect
|
||||
github.com/gorilla/websocket v1.5.3 // indirect
|
||||
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf // indirect
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 // indirect
|
||||
github.com/hashicorp/hcl v1.0.0 // indirect
|
||||
github.com/inconshreveable/mousetrap v1.1.0 // indirect
|
||||
github.com/jonboulle/clockwork v0.2.2 // indirect
|
||||
github.com/josharian/native v1.0.0 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/klauspost/reedsolomon v1.11.8 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect
|
||||
github.com/magiconair/properties v1.8.7 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mdlayher/netlink v1.6.0 // indirect
|
||||
github.com/mdlayher/socket v0.1.1 // indirect
|
||||
github.com/miekg/dns v1.1.61 // indirect
|
||||
github.com/mitchellh/go-homedir v1.1.0 // indirect
|
||||
github.com/mitchellh/mapstructure v1.5.0 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 // indirect
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible // indirect
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 // indirect
|
||||
github.com/pion/dtls/v2 v2.2.6 // indirect
|
||||
github.com/pion/logging v0.2.2 // indirect
|
||||
github.com/pion/transport/v2 v2.0.2 // indirect
|
||||
github.com/pion/udp/v2 v2.0.1 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/pkg/errors v0.9.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect
|
||||
github.com/prometheus/client_golang v1.19.1 // indirect
|
||||
github.com/prometheus/client_model v0.6.0 // indirect
|
||||
github.com/prometheus/common v0.48.0 // indirect
|
||||
github.com/prometheus/procfs v0.12.0 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.48.2 // indirect
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/rs/xid v1.3.0 // indirect
|
||||
github.com/sagikazarmark/locafero v0.4.0 // indirect
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 // indirect
|
||||
github.com/shadowsocks/go-shadowsocks2 v0.1.5 // indirect
|
||||
github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601 // indirect
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 // indirect
|
||||
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||
github.com/sirupsen/logrus v1.8.1 // indirect
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 // indirect
|
||||
github.com/sourcegraph/conc v0.3.0 // indirect
|
||||
github.com/spf13/afero v1.11.0 // indirect
|
||||
github.com/spf13/cast v1.6.0 // indirect
|
||||
github.com/spf13/cobra v1.8.0 // indirect
|
||||
github.com/spf13/pflag v1.0.5 // indirect
|
||||
github.com/spf13/viper v1.19.0 // indirect
|
||||
github.com/subosito/gotenv v1.6.0 // indirect
|
||||
github.com/templexxx/cpu v0.1.0 // indirect
|
||||
github.com/templexxx/xorsimd v0.4.2 // indirect
|
||||
github.com/tjfoc/gmsm v1.4.1 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.12 // indirect
|
||||
github.com/tklauser/numcpus v0.6.1 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 // indirect
|
||||
github.com/vishvananda/netns v0.0.4 // indirect
|
||||
github.com/vulcand/predicate v1.2.0 // indirect
|
||||
github.com/xtaci/kcp-go/v5 v5.6.5 // indirect
|
||||
github.com/xtaci/smux v1.5.31 // indirect
|
||||
github.com/xtaci/tcpraw v1.2.25 // indirect
|
||||
github.com/yl2chen/cidranger v1.0.2 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
github.com/zalando/go-keyring v0.2.4 // indirect
|
||||
go.uber.org/mock v0.5.0 // indirect
|
||||
go.uber.org/multierr v1.11.0 // indirect
|
||||
go.uber.org/zap v1.26.0 // indirect
|
||||
golang.org/x/arch v0.8.0 // indirect
|
||||
golang.org/x/crypto v0.38.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 // indirect
|
||||
golang.org/x/mod v0.22.0 // indirect
|
||||
golang.org/x/net v0.33.0 // indirect
|
||||
golang.org/x/sync v0.14.0 // indirect
|
||||
golang.org/x/sys v0.33.0 // indirect
|
||||
golang.org/x/term v0.32.0 // indirect
|
||||
golang.org/x/text v0.25.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.28.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 // indirect
|
||||
google.golang.org/grpc v1.67.1 // indirect
|
||||
google.golang.org/protobuf v1.35.1 // indirect
|
||||
gopkg.in/ini.v1 v1.67.0 // indirect
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
)
|
||||
|
||||
replace github.com/go-gost/x => ./x
|
||||
|
||||
+471
@@ -0,0 +1,471 @@
|
||||
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
|
||||
github.com/BurntSushi/toml v0.3.1/go.mod h1:xHWCNGjB5oqiDr8zfno3MHue2Ht5sIBksp03qcyfWMU=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da h1:KjTM2ks9d14ZYCvmHS9iAKVt9AyzRSqNU1qabPih5BY=
|
||||
github.com/aead/chacha20 v0.0.0-20180709150244-8b13a72661da/go.mod h1:eHEWzANqSiWQsof+nXEI9bUVUyV6F53Fp89EuCh2EAA=
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137 h1:s6gZFSlWYmbqAuRjVTiNNhvNRfY2Wxp9nhfyel4rklc=
|
||||
github.com/alecthomas/units v0.0.0-20211218093645-b94a6e3cc137/go.mod h1:OMCwj8VM1Kc9e19TLln2VL61YJF0x1XFtfdL4JdbSyE=
|
||||
github.com/alessio/shellescape v1.4.1 h1:V7yhSDDn8LP4lc4jS8pFkt0zCnzVJlG5JXy9BVKJUX0=
|
||||
github.com/alessio/shellescape v1.4.1/go.mod h1:PZAiSCk0LJaZkiCSkPv8qIobYglO3FPpyFjDCtHLS30=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d h1:Byv0BzEl3/e6D5CLfI0j/7hiIEtvGVFPCZ7Ei2oq8iQ=
|
||||
github.com/asaskevich/govalidator v0.0.0-20210307081110-f21760c49a8d/go.mod h1:WaHUgvxTVq04UNunO+XhnAqY/wQc+bxr74GqbsZ/Jqw=
|
||||
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/bwmarrin/snowflake v0.3.0 h1:xm67bEhkKh6ij1790JB83OujPR5CzNe8QuQqAgISZN0=
|
||||
github.com/bwmarrin/snowflake v0.3.0/go.mod h1:NdZxfVWX+oR6y2K0o6qAYv6gIOP9rjG0/E9WsDpxqwE=
|
||||
github.com/bytedance/sonic v1.11.6 h1:oUp34TzMlL+OY1OUWxHqsdkgC/Zfc85zGqw9siXjrc0=
|
||||
github.com/bytedance/sonic v1.11.6/go.mod h1:LysEHSvpvDySVdC2f87zGWf6CIKJcAvqab1ZaiQtds4=
|
||||
github.com/bytedance/sonic/loader v0.1.1 h1:c+e5Pt1k/cy5wMveRDyk2X4B9hF4g7an8N3zCYjJFNM=
|
||||
github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
|
||||
github.com/census-instrumentation/opencensus-proto v0.2.1/go.mod h1:f6KPmirojxKA12rnyqOA5BBL4O983OfeGPqjHWSTneU=
|
||||
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
|
||||
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
|
||||
github.com/client9/misspell v0.3.4/go.mod h1:qj6jICC3Q7zFZvVWo7KLAzC3yx5G7kyvSDkc90ppPyw=
|
||||
github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
|
||||
github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
|
||||
github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
|
||||
github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
|
||||
github.com/cncf/udpa/go v0.0.0-20191209042840-269d4d468f6f/go.mod h1:M8M6+tZqaGXZJjfX53e64911xZQV5JYwmTeXPW+k8Sc=
|
||||
github.com/coreos/go-iptables v0.7.0 h1:XWM3V+MPRr5/q51NuWSgU0fqMad64Zyxs8ZUoMsamr8=
|
||||
github.com/coreos/go-iptables v0.7.0/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||
github.com/cpuguy83/go-md2man/v2 v2.0.3/go.mod h1:tgQtvFlXSQOSOSIRvRPT7W67SCa46tRHOmNcaadrF8o=
|
||||
github.com/danieljoos/wincred v1.2.0 h1:ozqKHaLK0W/ii4KVbbvluM91W2H3Sh0BncbUNPS7jLE=
|
||||
github.com/danieljoos/wincred v1.2.0/go.mod h1:FzQLLMKBFdvu+osBrnFODiv32YGwCfx0SkRa/eYHgec=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc h1:U9qPSI2PIWSS1VwoXQT9A3Wy9MM3WgvqSxFWenqJduM=
|
||||
github.com/davecgh/go-spew v1.1.2-0.20180830191138-d8f796af33cc/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f h1:lO4WD4F/rVNCu3HqELle0jiPLLBs70cWOduZpkS1E78=
|
||||
github.com/dgryski/go-rendezvous v0.0.0-20200823014737-9f7001d12a5f/go.mod h1:cuUVRXasLTGF7a8hSLbxyZXjz+1KgoB3wDUb6vlszIc=
|
||||
github.com/envoyproxy/go-control-plane v0.9.0/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.1-0.20191026205805-5f8ba28d4473/go.mod h1:YTl/9mNaCwkRvm6d1a2C3ymFceY/DCBVvsKhRF0iEA4=
|
||||
github.com/envoyproxy/go-control-plane v0.9.4/go.mod h1:6rpuAdCZL397s3pYoYcLgu1mIlRU8Am5FuJP05cCM98=
|
||||
github.com/envoyproxy/protoc-gen-validate v0.1.0/go.mod h1:iSmxcyjqTsJpI2R4NaDN7+kN2VEUnK/pcBlmesArF7c=
|
||||
github.com/expr-lang/expr v1.16.3 h1:NLldf786GffptcXNxxJx5dQ+FzeWDKChBDqOOwyK8to=
|
||||
github.com/expr-lang/expr v1.16.3/go.mod h1:uCkhfG+x7fcZ5A5sXHKuQ07jGZRl6J0FCAaf2k4PtVQ=
|
||||
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf h1:NqGS3vTHzVENbIfd87cXZwdpO6MB2R1PjHMJLi4Z3ow=
|
||||
github.com/florianl/go-nfqueue v1.3.2-0.20231218173729-f2bdeb033acf/go.mod h1:eSnAor2YCfMCVYrVNEhkLGN/r1L+J4uDjc0EUy0tfq4=
|
||||
github.com/francoispqt/gojay v1.2.13 h1:d2m3sFjloqoIUQU3TsHBgj6qg/BVGlTBeHDUmyJnXKk=
|
||||
github.com/francoispqt/gojay v1.2.13/go.mod h1:ehT5mTG4ua4581f1++1WLG0vPdaA9HaiDsoyrBGkyDY=
|
||||
github.com/frankban/quicktest v1.14.6 h1:7Xjx+VpznH+oBnejlPUj8oUpdxnVs4f8XU8WnHkI4W8=
|
||||
github.com/frankban/quicktest v1.14.6/go.mod h1:4ptaffx2x8+WTWXmUCuVU6aPUX1/Mz7zb5vbUoiM6w0=
|
||||
github.com/fsnotify/fsnotify v1.7.0 h1:8JEhPFa5W2WU7YfeZzPNqzMP6Lwt7L2715Ggo0nosvA=
|
||||
github.com/fsnotify/fsnotify v1.7.0/go.mod h1:40Bi/Hjc2AVfZrqy+aj+yEI+/bRxZnMJyTJwOpGvigM=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||
github.com/gin-contrib/cors v1.7.2 h1:oLDHxdg8W/XDoN/8zamqk/Drgt4oVZDvaV0YmvVICQw=
|
||||
github.com/gin-contrib/cors v1.7.2/go.mod h1:SUJVARKgQ40dmrzgXEVxj2m7Ig1v1qIboQkPDTQ9t2E=
|
||||
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||
github.com/gin-gonic/gin v1.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
|
||||
github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
|
||||
github.com/go-gost/core v0.3.1 h1:Fo1WvyUTlxSF36d31aRC2LE9UPBDQ4hTMv8oQWoP2wE=
|
||||
github.com/go-gost/core v0.3.1/go.mod h1:WGI43jOka7FAsSAwi/fSMaqxdR+E339ycb4NBGlFr6A=
|
||||
github.com/go-gost/gosocks4 v0.0.1 h1:+k1sec8HlELuQV7rWftIkmy8UijzUt2I6t+iMPlGB2s=
|
||||
github.com/go-gost/gosocks4 v0.0.1/go.mod h1:3B6L47HbU/qugDg4JnoFPHgJXE43Inz8Bah1QaN9qCc=
|
||||
github.com/go-gost/gosocks5 v0.4.2 h1:IianxHTkACPqCwiOAT3MHoMdSUl+SEPSRu1ikawC1Pc=
|
||||
github.com/go-gost/gosocks5 v0.4.2/go.mod h1:1G6I7HP7VFVxveGkoK8mnprnJqSqJjdcASKsdUn4Pp4=
|
||||
github.com/go-gost/plugin v0.2.0 h1:Xxqq3oJi6d2dZ/PqqEpefrUckT6aVz8JUxQ/sb7tmTM=
|
||||
github.com/go-gost/plugin v0.2.0/go.mod h1:oN23l+yGDCIP9G3KnDl/I/0zVGOobZUDCB2Z5yYYXts=
|
||||
github.com/go-gost/relay v0.5.0 h1:JG1tgy/KWiVXS0ukuVXvbM0kbYuJTWxYpJ5JwzsCf/c=
|
||||
github.com/go-gost/relay v0.5.0/go.mod h1:lcX+23LCQ3khIeASBo+tJ/WbwXFO32/N5YN6ucuYTG8=
|
||||
github.com/go-gost/tls-dissector v0.1.1 h1:2zUOTPzCQAUQ54Rpy0UEi3JPMQSYsIFSeFeKrzmkCoU=
|
||||
github.com/go-gost/tls-dissector v0.1.1/go.mod h1:/9QfdewqmHdaE362Hv5nDaSWLx3pCmtD870d6GaquXs=
|
||||
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-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
|
||||
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
|
||||
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||
github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
|
||||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.20.0 h1:K9ISHbSaI0lyB2eWMPJo+kOS/FBExVwjEviJTixqxL8=
|
||||
github.com/go-playground/validator/v10 v10.20.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||
github.com/go-redis/redis/v8 v8.11.5 h1:AcZZR7igkdvfVmQTPnu9WE37LRrO/YrBH5zWyjDC0oI=
|
||||
github.com/go-redis/redis/v8 v8.11.5/go.mod h1:gREzHqY1hg6oD9ngVRbLStwAWKhA0FEgq8Jd4h5lpwo=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0 h1:sUs3vkvUymDpBKi3qH1YSqBQk9+9D/8M2mN1vB6EwHI=
|
||||
github.com/go-task/slim-sprig/v3 v3.0.0/go.mod h1:W848ghGpv3Qj3dhTPRyJypKRiqCdHZiAzKg9hl15HA8=
|
||||
github.com/gobwas/glob v0.2.3 h1:A4xDbljILXROh+kObIiy5kIaPYD8e96x1tgBhUI5J+Y=
|
||||
github.com/gobwas/glob v0.2.3/go.mod h1:d3Ez4x06l9bZtSvzIay5+Yzi0fmZzPgnTbPcKjJAkT8=
|
||||
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||
github.com/godbus/dbus/v5 v5.1.0 h1:4KLkAxT3aOY8Li4FRJe/KvhoNFFxo0m6fNuFUO8QJUk=
|
||||
github.com/godbus/dbus/v5 v5.1.0/go.mod h1:xhWf0FNVPg57R7Z0UbKHbJfkEywrmjJnf7w5xrFpKfA=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.2/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
|
||||
github.com/golang/protobuf v1.4.0-rc.1/go.mod h1:ceaxUfeHdC40wWswd/P6IGgMaK3YpKi5j83Wpe3EHw8=
|
||||
github.com/golang/protobuf v1.4.0-rc.1.0.20200221234624-67d41d38c208/go.mod h1:xKAWHe0F5eneWXFV3EuXVDTCmh+JuBKY0li0aMyXATA=
|
||||
github.com/golang/protobuf v1.4.0-rc.2/go.mod h1:LlEzMj4AhA7rCAGe4KMBDvJI+AwstrUpVNzEA03Pprs=
|
||||
github.com/golang/protobuf v1.4.0-rc.4.0.20200313231945-b860323f09d0/go.mod h1:WU3c8KckQ9AFe+yFwt9sWVRKCVIyN9cPHBJSNnbL67w=
|
||||
github.com/golang/protobuf v1.4.0/go.mod h1:jodUvKwWbYaEsadDk5Fwe5c77LiNKVO9IDvqG2KuDX0=
|
||||
github.com/golang/protobuf v1.4.2/go.mod h1:oDoupMAO8OvCJWAcko0GGGIgR6R6ocIYbsSw735rRwI=
|
||||
github.com/golang/snappy v0.0.4 h1:yAGX7huGHXlcLOEtBnF4w7FQwA26wojNCwOYAEhLjQM=
|
||||
github.com/golang/snappy v0.0.4/go.mod h1:/XxbfmMg8lxefKM7IXC3fBNl/7bRcc72aCRzEWrmP2Q=
|
||||
github.com/google/btree v1.0.1 h1:gK4Kx5IaGY9CD5sPJ36FHiBJ6ZXl0kilRiiCj+jdYp4=
|
||||
github.com/google/btree v1.0.1/go.mod h1:xXMiIv4Fb/0kKde4SpL7qlzvu5cMJDRkFDxJfI9uaxA=
|
||||
github.com/google/go-cmp v0.2.0/go.mod h1:oXzfMopK8JAjlY9xF4vHSVASa0yLyX7SntLO5aqRK0M=
|
||||
github.com/google/go-cmp v0.3.0/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.3.1/go.mod h1:8QqcDgzrUqlUb/G2PQTWiueGozuR1884gddMywk6iLU=
|
||||
github.com/google/go-cmp v0.4.0/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||
github.com/google/go-cmp v0.5.7/go.mod h1:n+brtR0CgQNWTVd5ZUFpTBC8YFBDLK/h/bpaJ8/DtOE=
|
||||
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/gopacket v1.1.20-0.20220810144506-32ee38206866 h1:NaJi58bCZZh0jjPw78EqDZekPEfhlzYE01C5R+zh1tE=
|
||||
github.com/google/gopacket v1.1.20-0.20220810144506-32ee38206866/go.mod h1:riddUzxTSBpJXk3qBHtYr4qOhFhT6k/1c0E3qkQjQpA=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad h1:a6HEuzUHeKH6hwfN/ZoQgRgVIWFJljSWa/zetS2WTvg=
|
||||
github.com/google/pprof v0.0.0-20241210010833-40e02aabc2ad/go.mod h1:vavhavw2zAxS5dIdcRluK6cSGGPlZynqzFM8NdvU144=
|
||||
github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
|
||||
github.com/google/uuid v1.6.0/go.mod h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
|
||||
github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
|
||||
github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
|
||||
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf h1:C1GPyPJrOlJlIrcaBBiBpDsqZena2Ks8spa5xZqr1XQ=
|
||||
github.com/gravitational/trace v1.1.16-0.20220114165159-14a9a7dd6aaf/go.mod h1:zXqxTI6jXDdKnlf8s+nT+3c8LrwUEy3yNpO4XJL90lA=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7 h1:a+bsQ5rvGLjzHuww6tVxozPZFVghXaHOwFs4luLUK2k=
|
||||
github.com/hashicorp/golang-lru/v2 v2.0.7/go.mod h1:QeFd9opnmA6QUJc5vARoKUSoFhyfM2/ZepoAG6RGpeM=
|
||||
github.com/hashicorp/hcl v1.0.0 h1:0Anlzjpi4vEasTeNFn2mLJgTSwt0+6sfsiTG8qcWGx4=
|
||||
github.com/hashicorp/hcl v1.0.0/go.mod h1:E5yfLk+7swimpb2L/Alb/PJmXilQ/rhwaUYs4T20WEQ=
|
||||
github.com/inconshreveable/mousetrap v1.1.0 h1:wN+x4NVGpMsO7ErUn/mUI3vEoE6Jt13X2s0bqwp9tc8=
|
||||
github.com/inconshreveable/mousetrap v1.1.0/go.mod h1:vpF70FUmC8bwa3OWnCshd2FqLfsEA9PFc4w1p2J65bw=
|
||||
github.com/jonboulle/clockwork v0.2.2 h1:UOGuzwb1PwsrDAObMuhUnj0p5ULPj8V/xJ7Kx9qUBdQ=
|
||||
github.com/jonboulle/clockwork v0.2.2/go.mod h1:Pkfl5aHPm1nk2H9h0bjmnJD/BcgbGXUBGnn1kMkgxc8=
|
||||
github.com/josharian/native v1.0.0 h1:Ts/E8zCSEsG17dUqv7joXJFybuMLjQfWE04tsBODTxk=
|
||||
github.com/josharian/native v1.0.0/go.mod h1:7X/raswPFr05uY3HiLlYeyQntB6OO7E/d2Cu7qoaN2w=
|
||||
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/judwhite/go-svc v1.2.1 h1:a7fsJzYUa33sfDJRF2N/WXhA+LonCEEY8BJb1tuS5tA=
|
||||
github.com/judwhite/go-svc v1.2.1/go.mod h1:mo/P2JNX8C07ywpP9YtO2gnBgnUiFTHqtsZekJrUuTk=
|
||||
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/reedsolomon v1.11.8 h1:s8RpUW5TK4hjr+djiOpbZJB4ksx+TdYbRH7vHQpwPOY=
|
||||
github.com/klauspost/reedsolomon v1.11.8/go.mod h1:4bXRN+cVzMdml6ti7qLouuYi32KHJ5MGv0Qd8a47h6A=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
github.com/kr/pretty v0.3.1/go.mod h1:hoEshYVHaxMs3cyo3Yncou5ZscifuDolrwPKZanG3xk=
|
||||
github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
|
||||
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||
github.com/magiconair/properties v1.8.7 h1:IeQXZAiQcpL9mgcAe1Nu6cX9LLw6ExEHKjN0VQdvPDY=
|
||||
github.com/magiconair/properties v1.8.7/go.mod h1:Dhd985XPs7jluiymwWYZ0G4Z61jb3vdS329zhj2hYo0=
|
||||
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/mdlayher/netlink v1.6.0 h1:rOHX5yl7qnlpiVkFWoqccueppMtXzeziFjWAjLg6sz0=
|
||||
github.com/mdlayher/netlink v1.6.0/go.mod h1:0o3PlBmGst1xve7wQ7j/hwpNaFaH4qCRyWCdcZk8/vA=
|
||||
github.com/mdlayher/socket v0.1.1 h1:q3uOGirUPfAV2MUoaC7BavjQ154J7+JOkTWyiV+intI=
|
||||
github.com/mdlayher/socket v0.1.1/go.mod h1:mYV5YIZAfHh4dzDVzI8x8tWLWCliuX8Mon5Awbj+qDs=
|
||||
github.com/miekg/dns v1.1.61 h1:nLxbwF3XxhwVSm8g9Dghm9MHPaUZuqhPiGL+675ZmEs=
|
||||
github.com/miekg/dns v1.1.61/go.mod h1:mnAarhS3nWaW+NVP2wTkYVIZyHNJ098SJZUki3eykwQ=
|
||||
github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y=
|
||||
github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0=
|
||||
github.com/mitchellh/mapstructure v1.5.0 h1:jeMsZIYE/09sWLaz43PL7Gy6RuMjD2eJVyuac5Z2hdY=
|
||||
github.com/mitchellh/mapstructure v1.5.0/go.mod h1:bFUtVrKA4DC2yAKiSyO/QUcy7e+RRV2QTWOzhPopBRo=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/nxadm/tail v1.4.8 h1:nPr65rt6Y5JFSKQO7qToXr7pePgD6Gwiw05lkbyAQTE=
|
||||
github.com/nxadm/tail v1.4.8/go.mod h1:+ncqLTQzXmGhMZNUePPaPqPvBxHAIsmXswZKocGu+AU=
|
||||
github.com/onsi/ginkgo v1.16.5 h1:8xi0RTUf59SOSfEtZMvwTvXYMzG4gV23XVHOZiXNtnE=
|
||||
github.com/onsi/ginkgo v1.16.5/go.mod h1:+E8gABHa3K6zRBolWtd+ROzc/U5bkGt0FwiG042wbpU=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0 h1:Yed107/8DjTr0lKCNt7Dn8yQ6ybuDRQoMGrNFKzMfHg=
|
||||
github.com/onsi/ginkgo/v2 v2.22.0/go.mod h1:7Du3c42kxCUegi0IImZ1wUQzMBVecgIHjR1C+NkhLQo=
|
||||
github.com/onsi/gomega v1.34.2 h1:pNCwDkzrsv7MS9kpaQvVb1aVLahQXyJ/Tv5oAZMI3i8=
|
||||
github.com/onsi/gomega v1.34.2/go.mod h1:v1xfxRgk0KIsG+QOdm7p8UosrOzPYRo60fd3B/1Dukc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2 h1:aYUidT7k73Pcl9nb2gScu7NSrKCSHIDE89b3+6Wq+LM=
|
||||
github.com/pelletier/go-toml/v2 v2.2.2/go.mod h1:1t835xjRzz80PqgE6HHgN2JOsmgYu/h4qDAS4n929Rs=
|
||||
github.com/pion/dtls/v2 v2.2.6 h1:yXMxKr0Skd+Ub6A8UqXTRLSywskx93ooMRHsQUtd+Z4=
|
||||
github.com/pion/dtls/v2 v2.2.6/go.mod h1:t8fWJCIquY5rlQZwA2yWxUS1+OCrAdXrhVKXB5oD/wY=
|
||||
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||
github.com/pion/transport/v2 v2.0.2 h1:St+8o+1PEzPT51O9bv+tH/KYYLMNR5Vwm5Z3Qkjsywg=
|
||||
github.com/pion/transport/v2 v2.0.2/go.mod h1:vrz6bUbFr/cjdwbnxq8OdDDzHf7JJfGsIRkxfpZoTA0=
|
||||
github.com/pion/udp/v2 v2.0.1 h1:xP0z6WNux1zWEjhC7onRA3EwwSliXqu1ElUZAQhUP54=
|
||||
github.com/pion/udp/v2 v2.0.1/go.mod h1:B7uvTMP00lzWdyMr/1PVZXtV3wpPIxBRd4Wl6AksXn8=
|
||||
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||
github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4=
|
||||
github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0=
|
||||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2 h1:Jamvg5psRIccs7FGNTlIRMkT8wgtp5eCXdBlqhYGL6U=
|
||||
github.com/pmezard/go-difflib v1.0.1-0.20181226105442-5d4384ee4fb2/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
|
||||
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/prometheus/client_golang v1.19.1 h1:wZWJDwK+NameRJuPGDhlnFgx8e8HN3XHQeLaYJFJBOE=
|
||||
github.com/prometheus/client_golang v1.19.1/go.mod h1:mP78NwGzrVks5S2H6ab8+ZZGJLZUq1hoULYBAYBw1Ho=
|
||||
github.com/prometheus/client_model v0.0.0-20190812154241-14fe0d1b01d4/go.mod h1:xMI15A0UPsDsEKsMN9yxemIoYk6Tm2C1GtYGdfGttqA=
|
||||
github.com/prometheus/client_model v0.6.0 h1:k1v3CzpSRUTrKMppY35TLwPvxHqBu0bYgxZzqGIgaos=
|
||||
github.com/prometheus/client_model v0.6.0/go.mod h1:NTQHnmxFpouOD0DpvP4XujX3CdOAGQPoaGhyTchlyt8=
|
||||
github.com/prometheus/common v0.48.0 h1:QO8U2CdOzSn1BBsmXJXduaaW+dY/5QLjfB8svtSzKKE=
|
||||
github.com/prometheus/common v0.48.0/go.mod h1:0/KsvlIEfPQCQ5I2iNSAWKPZziNCvRs5EC6ILDTlAPc=
|
||||
github.com/prometheus/procfs v0.12.0 h1:jluTpSng7V9hY0O2R9DzzJHYb2xULk9VTR1V1R/k6Bo=
|
||||
github.com/prometheus/procfs v0.12.0/go.mod h1:pcuDEFsWDnvcgNzo4EEweacyhjeA9Zk3cnaOZAZEfOo=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.48.2 h1:wsKXZPeGWpMpCGSWqOcqpW2wZYic/8T3aqiOID0/KWE=
|
||||
github.com/quic-go/quic-go v0.48.2/go.mod h1:yBgs3rWBOADpga7F+jJsb6Ybg1LSYiQvwWlLX+/6HMs=
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66 h1:4WFk6u3sOT6pLa1kQ50ZVdm8BQFgJNA117cepZxtLIg=
|
||||
github.com/quic-go/webtransport-go v0.8.1-0.20241018022711-4ac2c9250e66/go.mod h1:Vp72IJajgeOL6ddqrAhmp7IM9zbTcgkQxD/YdxrVwMw=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/rogpeppe/go-internal v1.10.0 h1:TMyTOH3F/DB16zRVcYyreMH6GnZZrwQVAoYjRBZyWFQ=
|
||||
github.com/rogpeppe/go-internal v1.10.0/go.mod h1:UQnix2H7Ngw/k4C5ijL5+65zddjncjaFoBhdsK/akog=
|
||||
github.com/rs/xid v1.3.0 h1:6NjYksEUlhurdVehpc7S7dk6DAmcKv8V9gG0FsVN2U4=
|
||||
github.com/rs/xid v1.3.0/go.mod h1:trrq9SKmegXys3aeAKXMUTdJsYXVwGY3RLcfgqegfbg=
|
||||
github.com/russross/blackfriday/v2 v2.1.0/go.mod h1:+Rmxgy9KzJVeS9/2gXHxylqXiyQDYRxCVz55jmeOWTM=
|
||||
github.com/sagikazarmark/locafero v0.4.0 h1:HApY1R9zGo4DBgr7dqsTH/JJxLTTsOt7u6keLGt6kNQ=
|
||||
github.com/sagikazarmark/locafero v0.4.0/go.mod h1:Pe1W6UlPYUk/+wc/6KFhbORCfqzgYEpgQ3O5fPuL3H4=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0 h1:diDBnUNK9N/354PgrxMywXnAwEr1QZcOr6gto+ugjYE=
|
||||
github.com/sagikazarmark/slog-shim v0.1.0/go.mod h1:SrcSrq8aKtyuqEI1uvTDTK1arOWRIczQRv+GVI1AkeQ=
|
||||
github.com/shadowsocks/go-shadowsocks2 v0.1.5 h1:PDSQv9y2S85Fl7VBeOMF9StzeXZyK1HakRm86CUbr28=
|
||||
github.com/shadowsocks/go-shadowsocks2 v0.1.5/go.mod h1:AGGpIoek4HRno4xzyFiAtLHkOpcoznZEkAccaI/rplM=
|
||||
github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601 h1:XU9hik0exChEmY92ALW4l9WnDodxLVS9yOSNh2SizaQ=
|
||||
github.com/shadowsocks/shadowsocks-go v0.0.0-20200409064450-3e585ff90601/go.mod h1:mttDPaeLm87u74HMrP+n2tugXvIKWcwff/cqSX0lehY=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
|
||||
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||
github.com/sirupsen/logrus v1.7.0/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/sirupsen/logrus v1.8.1 h1:dJKuHgqk1NNQlqoA6BTlM1Wf9DOH3NBjQyu0h9+AZZE=
|
||||
github.com/sirupsen/logrus v1.8.1/go.mod h1:yWOB1SBYBC5VeMP7gHvWumXLIWorT60ONWic61uBYv0=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8 h1:TG/diQgUe0pntT/2D9tmUCz4VNwm9MfrtPr0SU2qSX8=
|
||||
github.com/songgao/water v0.0.0-20200317203138-2b4b6d7c09d8/go.mod h1:P5HUIBuIWKbyjl083/loAegFkfbFNx5i2qEP4CNbm7E=
|
||||
github.com/sourcegraph/conc v0.3.0 h1:OQTbbt6P72L20UqAkXXuLOj79LfEanQ+YQFNpLA9ySo=
|
||||
github.com/sourcegraph/conc v0.3.0/go.mod h1:Sdozi7LEKbFPqYX2/J+iBAM6HpqSLTASQIKqDmF7Mt0=
|
||||
github.com/spf13/afero v1.11.0 h1:WJQKhtpdm3v2IzqG8VMqrr6Rf3UYpEF239Jy9wNepM8=
|
||||
github.com/spf13/afero v1.11.0/go.mod h1:GH9Y3pIexgf1MTIWtNGyogA5MwRIDXGUr+hbWNoBjkY=
|
||||
github.com/spf13/cast v1.6.0 h1:GEiTHELF+vaR5dhz3VqZfFSzZjYbgeKDpBxQVS4GYJ0=
|
||||
github.com/spf13/cast v1.6.0/go.mod h1:ancEpBxwJDODSW/UG4rDrAqiKolqNNh2DX3mk86cAdo=
|
||||
github.com/spf13/cobra v1.8.0 h1:7aJaZx1B85qltLMc546zn58BxxfZdR/W22ej9CFoEf0=
|
||||
github.com/spf13/cobra v1.8.0/go.mod h1:WXLWApfZ71AjXPya3WOlMsY9yMs7YeiHhFVlvLyhcho=
|
||||
github.com/spf13/pflag v1.0.5 h1:iy+VFUOCP1a+8yFto/drg2CJ5u0yRoB7fZw3DKv/JXA=
|
||||
github.com/spf13/pflag v1.0.5/go.mod h1:McXfInJRrz4CZXVZOBLb0bTZqETkiAhM9Iw0y3An2Bg=
|
||||
github.com/spf13/viper v1.19.0 h1:RWq5SEjt8o25SROyN3z2OrDB9l7RPd3lwTWU8EcEdcI=
|
||||
github.com/spf13/viper v1.19.0/go.mod h1:GQUN9bilAbhU/jgc1bKs99f/suXKeUMct8Adx5+Ntkg=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
github.com/stretchr/objx v0.5.2 h1:xuMeJ0Sdp5ZMRXx/aWO6RZxdr3beISkG5/G/aIRr3pY=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.2.2/go.mod h1:a8OnRcib4nhh0OaRAV+Yts87kKdq0PP7pXfy6kDkUVs=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.4.0/go.mod h1:j7eGeouHqKxXV5pUuKE4zz7dFj8WfuZ+81PSLYec5m4=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
|
||||
github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/subosito/gotenv v1.6.0 h1:9NlTDc1FTs4qu0DDq7AEtTPNw6SVm7uBMsUCUjABIf8=
|
||||
github.com/subosito/gotenv v1.6.0/go.mod h1:Dk4QP5c2W3ibzajGcXpNraDfq2IrhjMIvMSWPKKo0FU=
|
||||
github.com/templexxx/cpu v0.1.0 h1:wVM+WIJP2nYaxVxqgHPD4wGA2aJ9rvrQRV8CvFzNb40=
|
||||
github.com/templexxx/cpu v0.1.0/go.mod h1:w7Tb+7qgcAlIyX4NhLuDKt78AHA5SzPmq0Wj6HiEnnk=
|
||||
github.com/templexxx/xorsimd v0.4.2 h1:ocZZ+Nvu65LGHmCLZ7OoCtg8Fx8jnHKK37SjvngUoVI=
|
||||
github.com/templexxx/xorsimd v0.4.2/go.mod h1:HgwaPoDREdi6OnULpSfxhzaiiSUY4Fi3JPn1wpt28NI=
|
||||
github.com/tjfoc/gmsm v1.4.1 h1:aMe1GlZb+0bLjn+cKTPEvvn9oUEBlJitaZiiBwsbgho=
|
||||
github.com/tjfoc/gmsm v1.4.1/go.mod h1:j4INPkHWMrhJb38G+J6W4Tw0AbuN8Thu3PbdVYhVcTE=
|
||||
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
|
||||
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk=
|
||||
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||
github.com/vishvananda/netlink v1.1.0/go.mod h1:cTgwzPIzzgDAYoQrMm0EdrjRUBkTqKYppBueQtXaqoE=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54 h1:8mhqcHPqTMhSPoslhGYihEgSfc77+7La1P6kiB6+9So=
|
||||
github.com/vishvananda/netlink v1.1.1-0.20211118161826-650dca95af54/go.mod h1:twkDnbuQxJYemMlGd4JFIcuhgX83tXhKS2B/PRMpOho=
|
||||
github.com/vishvananda/netns v0.0.0-20191106174202-0a2b9b5464df/go.mod h1:JP3t17pCcGlemwknint6hfoeCVQrEMVwxRLRjXpq+BU=
|
||||
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/vishvananda/netns v0.0.0-20210104183010-2eb08e3e575f/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||
github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1YX8=
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/vulcand/predicate v1.2.0 h1:uFsW1gcnnR7R+QTID+FVcs0sSYlIGntoGOTb3rQJt50=
|
||||
github.com/vulcand/predicate v1.2.0/go.mod h1:VipoNYXny6c8N381zGUWkjuuNHiRbeAZhE7Qm9c+2GA=
|
||||
github.com/xtaci/kcp-go/v5 v5.6.5 h1:oxGZNobj3OddrLzwdJYnR/waNgwrL98u02u0DWNHE3k=
|
||||
github.com/xtaci/kcp-go/v5 v5.6.5/go.mod h1:Qy3Zf2tWTdFdEs0E8JvhrX+39r5UDZoYac8anvud7/Q=
|
||||
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae h1:J0GxkO96kL4WF+AIT3M4mfUVinOCPgf2uUWYFUzN0sM=
|
||||
github.com/xtaci/lossyconn v0.0.0-20190602105132-8df528c0c9ae/go.mod h1:gXtu8J62kEgmN++bm9BVICuT/e8yiLI2KFobd/TRFsE=
|
||||
github.com/xtaci/smux v1.5.31 h1:3ha7sHtH46h85Iv7MfQogxasuRt1KPRhoFB3S4rmHgU=
|
||||
github.com/xtaci/smux v1.5.31/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
|
||||
github.com/xtaci/tcpraw v1.2.25 h1:VDlqo0op17JeXBM6e2G9ocCNLOJcw9mZbobMbJjo0vk=
|
||||
github.com/xtaci/tcpraw v1.2.25/go.mod h1:dKyZ2V75s0cZ7cbgJYdxPvms7af0joIeOyx1GgJQbLk=
|
||||
github.com/yl2chen/cidranger v1.0.2 h1:lbOWZVCG1tCRX4u24kuM1Tb4nHqWkDxwLdoS+SevawU=
|
||||
github.com/yl2chen/cidranger v1.0.2/go.mod h1:9U1yz7WPYDwf0vpNWFaeRh0bjwz5RVgRy/9UEQfHl0g=
|
||||
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
github.com/zalando/go-keyring v0.2.4 h1:wi2xxTqdiwMKbM6TWwi+uJCG/Tum2UV0jqaQhCa9/68=
|
||||
github.com/zalando/go-keyring v0.2.4/go.mod h1:HL4k+OXQfJUWaMnqyuSOc0drfGPX2b51Du6K+MRgZMk=
|
||||
go.uber.org/goleak v1.2.0 h1:xqgm/S+aQvhWFTtR0XK3Jvg7z8kGV8P4X14IzwN3Eqk=
|
||||
go.uber.org/goleak v1.2.0/go.mod h1:XJYK+MuIchqpmGmUSAzotztawfKvYLUIgg7guXrwVUo=
|
||||
go.uber.org/mock v0.5.0 h1:KAMbZvZPyBPWgD14IrIQ38QCyjwpvVVV6K/bHl1IwQU=
|
||||
go.uber.org/mock v0.5.0/go.mod h1:ge71pBPLYDk7QIi1LupWxdAykm7KIEFchiOqd6z7qMM=
|
||||
go.uber.org/multierr v1.11.0 h1:blXXJkSxSSfBVBlC76pxqeO+LN3aDfLQo+309xJstO0=
|
||||
go.uber.org/multierr v1.11.0/go.mod h1:20+QtiLqy0Nd6FdQB9TLXag12DsQkrbs3htMFfDN80Y=
|
||||
go.uber.org/zap v1.26.0 h1:sI7k6L95XOKS281NhVKOFCUNIvv9e0w4BF8N3u+tCRo=
|
||||
go.uber.org/zap v1.26.0/go.mod h1:dtElttAiwGvoJ/vj4IwHBS/gXsEu/pZ50mUIRWuG0so=
|
||||
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||
golang.org/x/arch v0.8.0 h1:3wRIsP3pM4yUptoR96otTUOXI367OS0+c9eeRi9doIc=
|
||||
golang.org/x/arch v0.8.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
|
||||
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
|
||||
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201012173705-84dcc777aaee/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20201016220609-9e8e0b390897/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
|
||||
golang.org/x/crypto v0.0.0-20210220033148-5ea612d1eb83/go.mod h1:jdWPYTVW3xRLrWPugEBEK3UY2ZEsg3UU495nc5E+M+I=
|
||||
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||
golang.org/x/crypto v0.5.0/go.mod h1:NK/OQwhpMQP3MwtdjgLlYHnH9ebylxKWv3e0fK+mkQU=
|
||||
golang.org/x/crypto v0.38.0 h1:jt+WWG8IZlBnVbomuhg2Mdq0+BBQaHbtqHEFEigjUV8=
|
||||
golang.org/x/crypto v0.38.0/go.mod h1:MvrbAqul58NNYPKnOra203SB9vpuZW0e+RRZV+Ggqjw=
|
||||
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
|
||||
golang.org/x/exp v0.0.0-20241210194714-1829a127f884 h1:Y/Mj/94zIQQGHVSv1tTtQBDaQaJe62U9bkDZKKyhPCU=
|
||||
golang.org/x/exp v0.0.0-20241210194714-1829a127f884/go.mod h1:qj5a5QZpwLU2NLQudwIN5koi3beDhSAlJwa67PuM98c=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/lint v0.0.0-20190313153728-d0100b6bd8b3/go.mod h1:6SW0HCj/g11FgYtHlgUYUwCkIfeOF89ocIRzGO/8vkc=
|
||||
golang.org/x/lint v0.0.0-20200302205851-738671d3881b/go.mod h1:3xt1FjdF8hUf6vQPIChWIBhFzV8gjjsPE/fR3IyQdNY=
|
||||
golang.org/x/mod v0.1.1-0.20191105210325-c90efee705ee/go.mod h1:QqPTAvyqsEbceGzBzNggFXnrqF1CaUcvgkdR5Ot7KZg=
|
||||
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
|
||||
golang.org/x/mod v0.22.0 h1:D4nJWe9zXqHOmWqj4VMOJhvzj7bEZg4wEYa759z1pH4=
|
||||
golang.org/x/mod v0.22.0/go.mod h1:6SkKJ3Xj0I0BrPOZoBy3bdMptDDU9oJrpohJ3eWZ1fY=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20190311183353-d8887717615a/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
|
||||
golang.org/x/net v0.0.0-20201010224723-4f7140c49acb/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20201031054903-ff519b6c9102/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.0.0-20210928044308-7d9f5e0b762b/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||
golang.org/x/net v0.0.0-20220127200216-cd36cc0744dd/go.mod h1:CfG3xpIq0wQ8r1q4Su4UZFWDARRcnwPjda9FqA0JpMk=
|
||||
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
|
||||
golang.org/x/net v0.5.0/go.mod h1:DivGGAXEgPSlEBzxGzZI+ZLohi+xUj054jfeKui00ws=
|
||||
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
|
||||
golang.org/x/net v0.33.0 h1:74SYHlV8BIgHIFC/LrYkOGIwL19eTYXQ5wc6TBuO36I=
|
||||
golang.org/x/net v0.33.0/go.mod h1:HXLR5J+9DxmrqMwG9qjGCxZ+zKXxBru04zlTvWlWuN4=
|
||||
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20181108010431-42b317875d0f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||
golang.org/x/sync v0.14.0 h1:woo0S4Yywslg6hp4eUFjTVOyKt0RookbpAHG4c1HmhQ=
|
||||
golang.org/x/sync v0.14.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sys v0.0.0-20180830151530-49385e6e1522/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190215142949-d0b11bdaac8a/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
|
||||
golang.org/x/sys v0.0.0-20190412213103-97732733099d/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190606203320-7fc4e5ec1444/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20191026070338-33540a1f6037/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200217220822-9197077df867/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200728102440-3e129f6d46b1/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20200930185726-fdedc70b468f/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201119102817-f84b799fce68/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210124154548-22da62e12c0c/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20210927094055-39ccf1dd6fa6/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20211216021012-1d35b9e2eb4e/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.4.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.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.33.0 h1:q3i8TbbEz+JRD9ywIRlyRAQbM0qF7hu24q3teo2hbuw=
|
||||
golang.org/x/sys v0.33.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/term v0.0.0-20201117132131-f5c789dd3221/go.mod h1:Nr5EML6q2oocZ2LXRh80K7BxOlk5/8JxuGnuhpl+muw=
|
||||
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
|
||||
golang.org/x/term v0.4.0/go.mod h1:9P2UbLfCdcvo3p/nzKvsmas4TnlujnuoV9hGgYzW1lQ=
|
||||
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
|
||||
golang.org/x/term v0.32.0 h1:DR4lr0TjUs3epypdhTOkMmuF5CDFJ/8pOnbzMZPQ7bg=
|
||||
golang.org/x/term v0.32.0/go.mod h1:uZG1FhGx848Sqfsq4/DlJr3xGGsYMu/L5GW4abiaEPQ=
|
||||
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
|
||||
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
|
||||
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
|
||||
golang.org/x/text v0.6.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
|
||||
golang.org/x/text v0.25.0 h1:qVyWApTSYLk/drJRO5mDlNYskwQznZmkpV2c8q9zls4=
|
||||
golang.org/x/text v0.25.0/go.mod h1:WEdwpYrmk1qmdHvhkSTNPm3app7v4rsT8F2UD6+VHIA=
|
||||
golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
|
||||
golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
|
||||
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
|
||||
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
|
||||
golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
|
||||
golang.org/x/tools v0.0.0-20200130002326-2f3ba24bd6e7/go.mod h1:TB2adYChydJhpapKDTa4BR/hXlZSLoq2Wpct/0txZ28=
|
||||
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
|
||||
golang.org/x/tools v0.28.0 h1:WuB6qZ4RPCQo5aP3WdKZS7i595EdWqWR8vqJTlwTVK8=
|
||||
golang.org/x/tools v0.28.0/go.mod h1:dcIOrVd3mfQKTgrDVQHqCPMWy6lnhfhtX3hLXYVLfRw=
|
||||
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 h1:/jFs0duh4rdb8uIfPMv78iAJGcPKDeqAFnaLBropIC4=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173/go.mod h1:tkCQ4FQXmpAgYVh++1cq16/dH4QJtmvpRv19DWGAHSA=
|
||||
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
|
||||
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
|
||||
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
|
||||
google.golang.org/genproto v0.0.0-20190819201941-24fa4b261c55/go.mod h1:DMBHOl98Agz4BDEuKkezgsaosCRResVns1a3J2ZsMNc=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1 h1:pPJltXNxVzT4pK9yD8vR9X75DaWYYmLGMsEvBfFQZzQ=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240903143218-8af14fe29dc1/go.mod h1:UqMtugtsSgubUsoxbuAoiCXvqvErP7Gf0so0mK9tHxU=
|
||||
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
|
||||
google.golang.org/grpc v1.23.0/go.mod h1:Y5yQAOtifL1yxbo5wqy6BxZv8vAUGQwXBOALyacEbxg=
|
||||
google.golang.org/grpc v1.25.1/go.mod h1:c3i+UQWmh7LiEpx4sFZnkU36qjEYZ0imhYfXVyQciAY=
|
||||
google.golang.org/grpc v1.27.1/go.mod h1:qbnxyOmOxrQa7FizSgH+ReBfzJrCY1pSN7KXBS8abTk=
|
||||
google.golang.org/grpc v1.31.0/go.mod h1:N36X2cJ7JwdamYAgDz+s+rVMFjt3numwzf/HckM8pak=
|
||||
google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
|
||||
google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
|
||||
google.golang.org/protobuf v0.0.0-20200109180630-ec00e32a8dfd/go.mod h1:DFci5gLYBciE7Vtevhsrf46CRTquxDuWsQurQQe4oz8=
|
||||
google.golang.org/protobuf v0.0.0-20200221191635-4d8936d0db64/go.mod h1:kwYJMbMJ01Woi6D6+Kah6886xMZcty6N08ah7+eCXa0=
|
||||
google.golang.org/protobuf v0.0.0-20200228230310-ab0ca4ff8a60/go.mod h1:cfTl7dwQJ+fmap5saPgwCLgHXTUD7jkjRqWcaiX5VyM=
|
||||
google.golang.org/protobuf v1.20.1-0.20200309200217-e05f789c0967/go.mod h1:A+miEFZTKqfCUM6K7xSMQL9OKL/b6hQv+e19PK+JZNE=
|
||||
google.golang.org/protobuf v1.21.0/go.mod h1:47Nbq4nVaFHyn7ilMalzfO3qCViNmqZ2kzikPIcrTAo=
|
||||
google.golang.org/protobuf v1.23.0/go.mod h1:EGpADcykh3NcUnDUJcl1+ZksZNG86OlYog2l/sGQquU=
|
||||
google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
|
||||
google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
|
||||
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
|
||||
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7 h1:uRGJdciOHaEIrze2W8Q3AKkepLTh2hOroT7a+7czfdQ=
|
||||
gopkg.in/tomb.v1 v1.0.0-20141024135613-dd632973f1e7/go.mod h1:dt/ZhP58zS4L8KSrWDmTeBkI65Dw0HsyUHuEVlX15mw=
|
||||
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
|
||||
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259 h1:TbRPT0HtzFP3Cno1zZo7yPzEEnfu8EjLfl6IU9VfqkQ=
|
||||
gvisor.dev/gvisor v0.0.0-20230927004350-cbd86285d259/go.mod h1:AVgIgHMwK63XvmAzWG9vLQ41YnVHN0du0tEC46fI7yY=
|
||||
honnef.co/go/tools v0.0.0-20190102054323-c2f93a96b099/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
honnef.co/go/tools v0.0.0-20190523083050-ea95bdfd59fc/go.mod h1:rf3lG4BRIbNafJWhAfAdb/ePZxsR/4RtNHQocxwk9r4=
|
||||
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||
+133
@@ -0,0 +1,133 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"flag"
|
||||
"fmt"
|
||||
"log"
|
||||
_ "net/http/pprof"
|
||||
"os"
|
||||
"os/exec"
|
||||
"runtime"
|
||||
"strings"
|
||||
"sync"
|
||||
|
||||
"github.com/go-gost/core/logger"
|
||||
xlogger "github.com/go-gost/x/logger"
|
||||
"github.com/go-gost/x/service"
|
||||
"github.com/go-gost/x/socket"
|
||||
"github.com/judwhite/go-svc"
|
||||
)
|
||||
|
||||
type stringList []string
|
||||
|
||||
func (l *stringList) String() string {
|
||||
return fmt.Sprintf("%s", *l)
|
||||
}
|
||||
func (l *stringList) Set(value string) error {
|
||||
*l = append(*l, value)
|
||||
return nil
|
||||
}
|
||||
|
||||
var (
|
||||
cfgFile string
|
||||
outputFormat string
|
||||
services stringList
|
||||
nodes stringList
|
||||
debug bool
|
||||
trace bool
|
||||
apiAddr string
|
||||
metricsAddr string
|
||||
)
|
||||
|
||||
func init() {
|
||||
log.SetFlags(log.LstdFlags | log.Lshortfile | log.Lmicroseconds)
|
||||
|
||||
args := strings.Join(os.Args[1:], " ")
|
||||
|
||||
if strings.Contains(args, " -- ") {
|
||||
var (
|
||||
wg sync.WaitGroup
|
||||
ret int
|
||||
)
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
defer cancel()
|
||||
|
||||
for wid, wargs := range strings.Split(" "+args+" ", " -- ") {
|
||||
wg.Add(1)
|
||||
go func(wid int, wargs string) {
|
||||
defer wg.Done()
|
||||
defer cancel()
|
||||
worker(wid, strings.Split(wargs, " "), &ctx, &ret)
|
||||
}(wid, strings.TrimSpace(wargs))
|
||||
}
|
||||
|
||||
wg.Wait()
|
||||
|
||||
os.Exit(ret)
|
||||
}
|
||||
}
|
||||
|
||||
func worker(id int, args []string, ctx *context.Context, ret *int) {
|
||||
cmd := exec.CommandContext(*ctx, os.Args[0], args...)
|
||||
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stderr = os.Stderr
|
||||
cmd.Env = append(os.Environ(), fmt.Sprintf("_GOST_ID=%d", id))
|
||||
|
||||
if err := cmd.Run(); err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
if cmd.ProcessState.Exited() {
|
||||
*ret = cmd.ProcessState.ExitCode()
|
||||
}
|
||||
}
|
||||
|
||||
func init() {
|
||||
var printVersion bool
|
||||
|
||||
flag.Var(&services, "L", "service list")
|
||||
flag.Var(&nodes, "F", "chain node list")
|
||||
flag.StringVar(&cfgFile, "C", "", "configuration file")
|
||||
flag.BoolVar(&printVersion, "V", false, "print version")
|
||||
flag.StringVar(&outputFormat, "O", "", "output format, one of yaml|json format")
|
||||
flag.BoolVar(&debug, "D", false, "debug mode")
|
||||
flag.BoolVar(&trace, "DD", false, "trace mode")
|
||||
flag.StringVar(&apiAddr, "api", "", "api service address")
|
||||
flag.StringVar(&metricsAddr, "metrics", "", "metrics service address")
|
||||
flag.Parse()
|
||||
|
||||
if printVersion {
|
||||
fmt.Fprintf(os.Stdout, "gost %s (%s %s/%s)\n",
|
||||
version, runtime.Version(), runtime.GOOS, runtime.GOARCH)
|
||||
os.Exit(0)
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
// 加载配置文件
|
||||
config, err := LoadConfig("config.json")
|
||||
if err != nil {
|
||||
fmt.Println("❌ 配置加载失败: %v\n", err)
|
||||
fmt.Println("请确保当前目录存在 config.json 文件")
|
||||
os.Exit(1)
|
||||
}
|
||||
|
||||
fmt.Println("✅ 配置加载成功 - addr: %s", config.Addr)
|
||||
|
||||
log := xlogger.NewLogger()
|
||||
logger.SetDefault(log)
|
||||
|
||||
wsReporter := socket.StartWebSocketReporterWithConfig(config.Addr, config.Secret, config.Http, config.Tls, config.Socks, version)
|
||||
defer wsReporter.Stop()
|
||||
service.SetHTTPReportURL(config.Addr, config.Secret)
|
||||
|
||||
p := &program{}
|
||||
if err := svc.Run(p); err != nil {
|
||||
logger.Default().Fatal(err)
|
||||
}
|
||||
}
|
||||
|
||||
// GOOS=linux GOARCH=amd64 go build -ldflags="-s -w" -o gost
|
||||
// upx --best --lzma gost
|
||||
@@ -0,0 +1,285 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"github.com/go-gost/core/auth"
|
||||
"github.com/go-gost/core/logger"
|
||||
"github.com/go-gost/core/service"
|
||||
api_service "github.com/go-gost/x/api/service"
|
||||
xauth "github.com/go-gost/x/auth"
|
||||
"github.com/go-gost/x/config"
|
||||
"github.com/go-gost/x/config/loader"
|
||||
auth_parser "github.com/go-gost/x/config/parsing/auth"
|
||||
"github.com/go-gost/x/config/parsing/parser"
|
||||
xmetrics "github.com/go-gost/x/metrics"
|
||||
metrics "github.com/go-gost/x/metrics/service"
|
||||
"github.com/go-gost/x/registry"
|
||||
xservice "github.com/go-gost/x/service"
|
||||
"github.com/judwhite/go-svc"
|
||||
"net/http"
|
||||
"os"
|
||||
"os/signal"
|
||||
"strings"
|
||||
"syscall"
|
||||
"time"
|
||||
)
|
||||
|
||||
type program struct {
|
||||
srvApi service.Service
|
||||
srvMetrics service.Service
|
||||
srvProfiling *http.Server
|
||||
|
||||
cancel context.CancelFunc
|
||||
}
|
||||
|
||||
func (p *program) Init(env svc.Environment) error {
|
||||
parser.Init(parser.Args{
|
||||
CfgFile: cfgFile,
|
||||
Services: services,
|
||||
Nodes: nodes,
|
||||
Debug: debug,
|
||||
Trace: trace,
|
||||
ApiAddr: apiAddr,
|
||||
MetricsAddr: metricsAddr,
|
||||
})
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *program) Start() error {
|
||||
cfg, err := parser.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if outputFormat != "" {
|
||||
if err := cfg.Write(os.Stdout, outputFormat); err != nil {
|
||||
return err
|
||||
}
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
config.Set(cfg)
|
||||
|
||||
if err := loader.Load(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.run(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
p.cancel = cancel
|
||||
go p.reload(ctx)
|
||||
|
||||
go func() {
|
||||
select {
|
||||
case <-time.After(10 * time.Second):
|
||||
xservice.StartConfigReporter(ctx)
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *program) run(cfg *config.Config) error {
|
||||
for _, svc := range registry.ServiceRegistry().GetAll() {
|
||||
svc := svc
|
||||
go func() {
|
||||
svc.Serve()
|
||||
}()
|
||||
}
|
||||
|
||||
if p.srvApi != nil {
|
||||
p.srvApi.Close()
|
||||
p.srvApi = nil
|
||||
}
|
||||
if cfg.API != nil {
|
||||
s, err := buildApiService(cfg.API)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.srvApi = s
|
||||
|
||||
go func() {
|
||||
defer s.Close()
|
||||
|
||||
log := logger.Default().WithFields(map[string]any{"kind": "service", "service": "@api"})
|
||||
|
||||
log.Info("listening on ", s.Addr())
|
||||
if err := s.Serve(); !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Error(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
xmetrics.Enable(false)
|
||||
if p.srvMetrics != nil {
|
||||
p.srvMetrics.Close()
|
||||
p.srvMetrics = nil
|
||||
}
|
||||
if cfg.Metrics != nil && cfg.Metrics.Addr != "" {
|
||||
s, err := buildMetricsService(cfg.Metrics)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
p.srvMetrics = s
|
||||
|
||||
xmetrics.Enable(true)
|
||||
|
||||
go func() {
|
||||
defer s.Close()
|
||||
|
||||
log := logger.Default().WithFields(map[string]any{"kind": "service", "service": "@metrics"})
|
||||
|
||||
log.Info("listening on ", s.Addr())
|
||||
if err := s.Serve(); !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Error(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
if p.srvProfiling != nil {
|
||||
p.srvProfiling.Close()
|
||||
p.srvProfiling = nil
|
||||
}
|
||||
if cfg.Profiling != nil {
|
||||
addr := cfg.Profiling.Addr
|
||||
if addr == "" {
|
||||
addr = ":6060"
|
||||
}
|
||||
s := &http.Server{
|
||||
Addr: addr,
|
||||
}
|
||||
p.srvProfiling = s
|
||||
|
||||
go func() {
|
||||
defer s.Close()
|
||||
|
||||
log := logger.Default().WithFields(map[string]any{"kind": "service", "service": "@profiling"})
|
||||
|
||||
log.Info("listening on ", addr)
|
||||
if err := s.ListenAndServe(); !errors.Is(err, http.ErrServerClosed) {
|
||||
log.Error(err)
|
||||
}
|
||||
}()
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *program) Stop() error {
|
||||
if p.cancel != nil {
|
||||
p.cancel()
|
||||
}
|
||||
|
||||
for name, srv := range registry.ServiceRegistry().GetAll() {
|
||||
srv.Close()
|
||||
logger.Default().Debugf("service %s shutdown", name)
|
||||
}
|
||||
|
||||
if p.srvApi != nil {
|
||||
p.srvApi.Close()
|
||||
logger.Default().Debug("service @api shutdown")
|
||||
}
|
||||
if p.srvMetrics != nil {
|
||||
p.srvMetrics.Close()
|
||||
logger.Default().Debug("service @metrics shutdown")
|
||||
}
|
||||
if p.srvProfiling != nil {
|
||||
p.srvProfiling.Close()
|
||||
logger.Default().Debug("service @profiling shutdown")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (p *program) reload(ctx context.Context) {
|
||||
c := make(chan os.Signal, 1)
|
||||
signal.Notify(c, syscall.SIGHUP)
|
||||
|
||||
for {
|
||||
select {
|
||||
case <-c:
|
||||
if err := p.reloadConfig(); err != nil {
|
||||
logger.Default().Error(err)
|
||||
} else {
|
||||
logger.Default().Info("config reloaded")
|
||||
}
|
||||
|
||||
case <-ctx.Done():
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (p *program) reloadConfig() error {
|
||||
cfg, err := parser.Parse()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
config.Set(cfg)
|
||||
|
||||
if err := loader.Load(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := p.run(cfg); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func buildApiService(cfg *config.APIConfig) (service.Service, error) {
|
||||
var authers []auth.Authenticator
|
||||
if auther := auth_parser.ParseAutherFromAuth(cfg.Auth); auther != nil {
|
||||
authers = append(authers, auther)
|
||||
}
|
||||
if cfg.Auther != "" {
|
||||
authers = append(authers, registry.AutherRegistry().Get(cfg.Auther))
|
||||
}
|
||||
|
||||
var auther auth.Authenticator
|
||||
if len(authers) > 0 {
|
||||
auther = xauth.AuthenticatorGroup(authers...)
|
||||
}
|
||||
|
||||
network := "tcp"
|
||||
addr := cfg.Addr
|
||||
if strings.HasPrefix(addr, "unix://") {
|
||||
network = "unix"
|
||||
addr = strings.TrimPrefix(addr, "unix://")
|
||||
}
|
||||
return api_service.NewService(
|
||||
network, addr,
|
||||
api_service.PathPrefixOption(cfg.PathPrefix),
|
||||
api_service.AccessLogOption(cfg.AccessLog),
|
||||
api_service.AutherOption(auther),
|
||||
)
|
||||
}
|
||||
|
||||
func buildMetricsService(cfg *config.MetricsConfig) (service.Service, error) {
|
||||
auther := auth_parser.ParseAutherFromAuth(cfg.Auth)
|
||||
if cfg.Auther != "" {
|
||||
auther = registry.AutherRegistry().Get(cfg.Auther)
|
||||
}
|
||||
|
||||
network := "tcp"
|
||||
addr := cfg.Addr
|
||||
if strings.HasPrefix(addr, "unix://") {
|
||||
network = "unix"
|
||||
addr = strings.TrimPrefix(addr, "unix://")
|
||||
}
|
||||
return metrics.NewService(
|
||||
network, addr,
|
||||
metrics.PathOption(cfg.Path),
|
||||
metrics.AutherOption(auther),
|
||||
)
|
||||
}
|
||||
@@ -0,0 +1,109 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
// Register connectors
|
||||
_ "github.com/go-gost/x/connector/direct"
|
||||
_ "github.com/go-gost/x/connector/forward"
|
||||
_ "github.com/go-gost/x/connector/http"
|
||||
_ "github.com/go-gost/x/connector/http2"
|
||||
_ "github.com/go-gost/x/connector/relay"
|
||||
_ "github.com/go-gost/x/connector/router"
|
||||
_ "github.com/go-gost/x/connector/serial"
|
||||
_ "github.com/go-gost/x/connector/sni"
|
||||
_ "github.com/go-gost/x/connector/socks/v4"
|
||||
_ "github.com/go-gost/x/connector/socks/v5"
|
||||
_ "github.com/go-gost/x/connector/ss"
|
||||
_ "github.com/go-gost/x/connector/ss/udp"
|
||||
_ "github.com/go-gost/x/connector/sshd"
|
||||
_ "github.com/go-gost/x/connector/tcp"
|
||||
_ "github.com/go-gost/x/connector/tunnel"
|
||||
_ "github.com/go-gost/x/connector/unix"
|
||||
|
||||
// Register dialers
|
||||
_ "github.com/go-gost/x/dialer/direct"
|
||||
_ "github.com/go-gost/x/dialer/dtls"
|
||||
_ "github.com/go-gost/x/dialer/ftcp"
|
||||
_ "github.com/go-gost/x/dialer/grpc"
|
||||
_ "github.com/go-gost/x/dialer/http2"
|
||||
_ "github.com/go-gost/x/dialer/http2/h2"
|
||||
_ "github.com/go-gost/x/dialer/http3"
|
||||
_ "github.com/go-gost/x/dialer/http3/wt"
|
||||
_ "github.com/go-gost/x/dialer/icmp"
|
||||
_ "github.com/go-gost/x/dialer/kcp"
|
||||
_ "github.com/go-gost/x/dialer/mtcp"
|
||||
_ "github.com/go-gost/x/dialer/mtls"
|
||||
_ "github.com/go-gost/x/dialer/mws"
|
||||
_ "github.com/go-gost/x/dialer/obfs/http"
|
||||
_ "github.com/go-gost/x/dialer/obfs/tls"
|
||||
_ "github.com/go-gost/x/dialer/pht"
|
||||
_ "github.com/go-gost/x/dialer/quic"
|
||||
_ "github.com/go-gost/x/dialer/serial"
|
||||
_ "github.com/go-gost/x/dialer/ssh"
|
||||
_ "github.com/go-gost/x/dialer/sshd"
|
||||
_ "github.com/go-gost/x/dialer/tcp"
|
||||
_ "github.com/go-gost/x/dialer/tls"
|
||||
_ "github.com/go-gost/x/dialer/udp"
|
||||
_ "github.com/go-gost/x/dialer/unix"
|
||||
_ "github.com/go-gost/x/dialer/ws"
|
||||
|
||||
// Register handlers
|
||||
_ "github.com/go-gost/x/handler/api"
|
||||
_ "github.com/go-gost/x/handler/auto"
|
||||
_ "github.com/go-gost/x/handler/dns"
|
||||
_ "github.com/go-gost/x/handler/file"
|
||||
_ "github.com/go-gost/x/handler/forward/local"
|
||||
_ "github.com/go-gost/x/handler/forward/remote"
|
||||
_ "github.com/go-gost/x/handler/http"
|
||||
_ "github.com/go-gost/x/handler/http2"
|
||||
_ "github.com/go-gost/x/handler/http3"
|
||||
_ "github.com/go-gost/x/handler/metrics"
|
||||
_ "github.com/go-gost/x/handler/redirect/tcp"
|
||||
_ "github.com/go-gost/x/handler/redirect/udp"
|
||||
_ "github.com/go-gost/x/handler/relay"
|
||||
_ "github.com/go-gost/x/handler/router"
|
||||
_ "github.com/go-gost/x/handler/serial"
|
||||
_ "github.com/go-gost/x/handler/sni"
|
||||
_ "github.com/go-gost/x/handler/socks/v4"
|
||||
_ "github.com/go-gost/x/handler/socks/v5"
|
||||
_ "github.com/go-gost/x/handler/ss"
|
||||
_ "github.com/go-gost/x/handler/ss/udp"
|
||||
_ "github.com/go-gost/x/handler/sshd"
|
||||
_ "github.com/go-gost/x/handler/tap"
|
||||
_ "github.com/go-gost/x/handler/tun"
|
||||
_ "github.com/go-gost/x/handler/tunnel"
|
||||
_ "github.com/go-gost/x/handler/unix"
|
||||
|
||||
// Register listeners
|
||||
_ "github.com/go-gost/x/listener/dns"
|
||||
_ "github.com/go-gost/x/listener/dtls"
|
||||
_ "github.com/go-gost/x/listener/ftcp"
|
||||
_ "github.com/go-gost/x/listener/grpc"
|
||||
_ "github.com/go-gost/x/listener/http2"
|
||||
_ "github.com/go-gost/x/listener/http2/h2"
|
||||
_ "github.com/go-gost/x/listener/http3"
|
||||
_ "github.com/go-gost/x/listener/http3/h3"
|
||||
_ "github.com/go-gost/x/listener/http3/wt"
|
||||
_ "github.com/go-gost/x/listener/icmp"
|
||||
_ "github.com/go-gost/x/listener/kcp"
|
||||
_ "github.com/go-gost/x/listener/mtcp"
|
||||
_ "github.com/go-gost/x/listener/mtls"
|
||||
_ "github.com/go-gost/x/listener/mws"
|
||||
_ "github.com/go-gost/x/listener/obfs/http"
|
||||
_ "github.com/go-gost/x/listener/obfs/tls"
|
||||
_ "github.com/go-gost/x/listener/pht"
|
||||
_ "github.com/go-gost/x/listener/quic"
|
||||
_ "github.com/go-gost/x/listener/redirect/tcp"
|
||||
_ "github.com/go-gost/x/listener/redirect/udp"
|
||||
_ "github.com/go-gost/x/listener/rtcp"
|
||||
_ "github.com/go-gost/x/listener/rudp"
|
||||
_ "github.com/go-gost/x/listener/serial"
|
||||
_ "github.com/go-gost/x/listener/ssh"
|
||||
_ "github.com/go-gost/x/listener/sshd"
|
||||
_ "github.com/go-gost/x/listener/tap"
|
||||
_ "github.com/go-gost/x/listener/tcp"
|
||||
_ "github.com/go-gost/x/listener/tls"
|
||||
_ "github.com/go-gost/x/listener/tun"
|
||||
_ "github.com/go-gost/x/listener/udp"
|
||||
_ "github.com/go-gost/x/listener/unix"
|
||||
_ "github.com/go-gost/x/listener/ws"
|
||||
)
|
||||
@@ -0,0 +1,5 @@
|
||||
package main
|
||||
|
||||
var (
|
||||
version = "dev"
|
||||
)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user