Initial fork commit.
Some checks failed
Release S-UI / build-frontend (push) Failing after 1m53s
Release S-UI / build-386 (push) Has been skipped
Release S-UI / build-amd64 (push) Has been skipped
Release S-UI / build-armv5 (push) Has been skipped
Release S-UI / build-armv6 (push) Has been skipped
Release S-UI / build-armv7 (push) Has been skipped
Release S-UI / build-s390x (push) Has been skipped
Release S-UI / build-arm64 (push) Has been skipped
Build S-UI for Windows / build-frontend (push) Failing after 2s
Build S-UI for Windows / build-windows-amd64 (push) Has been skipped
Build S-UI for Windows / build-windows-arm64 (push) Has been skipped
Some checks failed
Release S-UI / build-frontend (push) Failing after 1m53s
Release S-UI / build-386 (push) Has been skipped
Release S-UI / build-amd64 (push) Has been skipped
Release S-UI / build-armv5 (push) Has been skipped
Release S-UI / build-armv6 (push) Has been skipped
Release S-UI / build-armv7 (push) Has been skipped
Release S-UI / build-s390x (push) Has been skipped
Release S-UI / build-arm64 (push) Has been skipped
Build S-UI for Windows / build-frontend (push) Failing after 2s
Build S-UI for Windows / build-windows-amd64 (push) Has been skipped
Build S-UI for Windows / build-windows-arm64 (push) Has been skipped
Signed-off-by: Pavel Kirilin <s3riussan@gmail.com>
This commit is contained in:
26
.dockerignore
Normal file
26
.dockerignore
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
.DS_Store
|
||||||
|
dist/
|
||||||
|
release/
|
||||||
|
backup/
|
||||||
|
bin/
|
||||||
|
db/
|
||||||
|
sui
|
||||||
|
web/html
|
||||||
|
main
|
||||||
|
tmp
|
||||||
|
.sync*
|
||||||
|
*.tar.gz
|
||||||
|
frontend/node_modules
|
||||||
|
frontend/.vite
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log*
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
1
.github/FUNDING.yml
vendored
Normal file
1
.github/FUNDING.yml
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
github: alireza0
|
||||||
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
38
.github/ISSUE_TEMPLATE/bug_report.md
vendored
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
---
|
||||||
|
name: Bug report
|
||||||
|
about: Create a report to help us improve
|
||||||
|
title: ''
|
||||||
|
labels: bug
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Describe the bug**
|
||||||
|
A clear and concise description of what the bug is.
|
||||||
|
|
||||||
|
**To Reproduce**
|
||||||
|
Steps to reproduce the behavior:
|
||||||
|
1. Go to '...'
|
||||||
|
2. Click on '....'
|
||||||
|
3. Scroll down to '....'
|
||||||
|
4. See error
|
||||||
|
|
||||||
|
**Expected behavior**
|
||||||
|
A clear and concise description of what you expected to happen.
|
||||||
|
|
||||||
|
**Screenshots**
|
||||||
|
If applicable, add screenshots to help explain your problem.
|
||||||
|
|
||||||
|
**Desktop (please complete the following information):**
|
||||||
|
- OS: [e.g. iOS]
|
||||||
|
- Browser [e.g. chrome, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Smartphone (please complete the following information):**
|
||||||
|
- Device: [e.g. iPhone6]
|
||||||
|
- OS: [e.g. iOS8.1]
|
||||||
|
- Browser [e.g. stock browser, safari]
|
||||||
|
- Version [e.g. 22]
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context about the problem here.
|
||||||
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
20
.github/ISSUE_TEMPLATE/feature_request.md
vendored
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
---
|
||||||
|
name: Feature request
|
||||||
|
about: Suggest an idea for this project
|
||||||
|
title: ''
|
||||||
|
labels: enhancement
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
**Is your feature request related to a problem? Please describe.**
|
||||||
|
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
|
||||||
|
|
||||||
|
**Describe the solution you'd like**
|
||||||
|
A clear and concise description of what you want to happen.
|
||||||
|
|
||||||
|
**Describe alternatives you've considered**
|
||||||
|
A clear and concise description of any alternative solutions or features you've considered.
|
||||||
|
|
||||||
|
**Additional context**
|
||||||
|
Add any other context or screenshots about the feature request here.
|
||||||
10
.github/ISSUE_TEMPLATE/question-template.md
vendored
Normal file
10
.github/ISSUE_TEMPLATE/question-template.md
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
---
|
||||||
|
name: Question template
|
||||||
|
about: Ask if it is not clear that it is a bug
|
||||||
|
title: ''
|
||||||
|
labels: question
|
||||||
|
assignees: ''
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
|
||||||
10
.github/dependabot.yml
vendored
Normal file
10
.github/dependabot.yml
vendored
Normal file
@@ -0,0 +1,10 @@
|
|||||||
|
version: 2
|
||||||
|
updates:
|
||||||
|
- package-ecosystem: "gomod"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
|
- package-ecosystem: "github-actions"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
159
.github/workflows/docker.yml
vendored
Normal file
159
.github/workflows/docker.yml
vendored
Normal file
@@ -0,0 +1,159 @@
|
|||||||
|
name: Docker Image CI
|
||||||
|
on:
|
||||||
|
push:
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
frontend-build:
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
- name: Set up Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: 25
|
||||||
|
- name: Install dependencies and build frontend
|
||||||
|
run: |
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
- name: Upload frontend build artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: frontend-dist
|
||||||
|
path: frontend/dist/
|
||||||
|
|
||||||
|
build:
|
||||||
|
needs: frontend-build
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { platform: linux/amd64 }
|
||||||
|
- { platform: linux/386 }
|
||||||
|
- { platform: linux/arm64/v8 }
|
||||||
|
- { platform: linux/arm/v7 }
|
||||||
|
- { platform: linux/arm/v6 }
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
- name: Download frontend build artifact
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: frontend-dist
|
||||||
|
path: frontend_dist
|
||||||
|
- name: Prepare
|
||||||
|
run: |
|
||||||
|
platform="${{ matrix.platform }}"
|
||||||
|
echo "PLATFORM_PAIR=${platform//\//-}" >> $GITHUB_ENV
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v6
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
alireza7/s-ui
|
||||||
|
ghcr.io/alireza0/s-ui
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=pep440,pattern={{version}}
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v4
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
- name: Cache Docker layers
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: /tmp/.buildx-cache
|
||||||
|
key: ${{ runner.os }}-buildx-${{ matrix.platform }}-${{ github.sha }}
|
||||||
|
restore-keys: |
|
||||||
|
${{ runner.os }}-buildx-${{ matrix.platform }}-
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Build and push by digest
|
||||||
|
id: build
|
||||||
|
uses: docker/build-push-action@v7
|
||||||
|
with:
|
||||||
|
context: .
|
||||||
|
file: Dockerfile.frontend-artifact
|
||||||
|
platforms: ${{ matrix.platform }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
|
tags: |
|
||||||
|
alireza7/s-ui
|
||||||
|
ghcr.io/alireza0/s-ui
|
||||||
|
cache-from: type=local,src=/tmp/.buildx-cache
|
||||||
|
cache-to: type=local,dest=/tmp/.buildx-cache,mode=max
|
||||||
|
outputs: type=image,push-by-digest=true,name-canonical=true,push=true
|
||||||
|
- name: Export digest
|
||||||
|
run: |
|
||||||
|
mkdir -p ${{ runner.temp }}/digests
|
||||||
|
digest="${{ steps.build.outputs.digest }}"
|
||||||
|
echo "${digest#sha256:}" > "${{ runner.temp }}/digests/${digest#sha256:}"
|
||||||
|
- name: Upload digest
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: digests-${{ env.PLATFORM_PAIR }}
|
||||||
|
path: ${{ runner.temp }}/digests/*
|
||||||
|
if-no-files-found: error
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
merge:
|
||||||
|
needs: build
|
||||||
|
runs-on: ubuntu-24.04
|
||||||
|
steps:
|
||||||
|
- name: Download digests
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
path: ${{ runner.temp }}/digests
|
||||||
|
pattern: digests-*
|
||||||
|
merge-multiple: true
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v4
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v4
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v6
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
alireza7/s-ui
|
||||||
|
ghcr.io/alireza0/s-ui
|
||||||
|
tags: |
|
||||||
|
type=ref,event=branch
|
||||||
|
type=ref,event=tag
|
||||||
|
type=pep440,pattern={{version}}
|
||||||
|
- name: Create manifest list and push
|
||||||
|
env:
|
||||||
|
DOCKER_METADATA_OUTPUT_JSON: ${{ steps.meta.outputs.json }}
|
||||||
|
working-directory: ${{ runner.temp }}/digests
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
for img in alireza7/s-ui ghcr.io/alireza0/s-ui; do
|
||||||
|
TAGS_ARGS=$(echo "$DOCKER_METADATA_OUTPUT_JSON" | jq -cr --arg img "$img" '.tags | map(select(startswith($img))) | map("-t " + .) | join(" ")')
|
||||||
|
DIGEST_REFS=$(for f in *; do echo -n "${img}@sha256:$(cat "$f") "; done)
|
||||||
|
docker buildx imagetools create $TAGS_ARGS $DIGEST_REFS
|
||||||
|
done
|
||||||
202
.github/workflows/release.yml
vendored
Normal file
202
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,202 @@
|
|||||||
|
name: Release S-UI
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/release.yml'
|
||||||
|
- 'frontend/**'
|
||||||
|
- '**.sh'
|
||||||
|
- '**.go'
|
||||||
|
- 'go.mod'
|
||||||
|
- 'go.sum'
|
||||||
|
- 's-ui.service'
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: "25"
|
||||||
|
CRONET_GO_VERSION: "2faf34666c2cc8234f10f2ab6d4c4d6104d34ae2"
|
||||||
|
CRONET_GO_REPO: https://github.com/sagernet/cronet-go.git
|
||||||
|
BOOTLIN_BASE_URL: https://toolchains.bootlin.com/downloads/releases/toolchains
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository (frontend only)
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
cache: 'npm'
|
||||||
|
cache-dependency-path: frontend/package-lock.json
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: |
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Upload frontend dist
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: frontend-dist
|
||||||
|
path: frontend/dist/
|
||||||
|
|
||||||
|
build-linux:
|
||||||
|
name: build-${{ matrix.platform }}
|
||||||
|
needs: build-frontend
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { platform: amd64, arch: amd64, bootlin: x86-64, naive: true }
|
||||||
|
- { platform: arm64, arch: arm64, bootlin: aarch64, naive: true }
|
||||||
|
- { platform: armv7, arch: arm, goarm: "7", bootlin: armv7-eabihf, naive: true }
|
||||||
|
- { platform: armv6, arch: arm, goarm: "6", bootlin: armv6-eabihf, naive: true }
|
||||||
|
- { platform: armv5, arch: arm, goarm: "5", bootlin: armv5-eabi, naive: false }
|
||||||
|
- { platform: "386", arch: "386", bootlin: x86-i686, naive: true }
|
||||||
|
- { platform: s390x, arch: s390x, bootlin: s390x-z13, naive: false }
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
|
||||||
|
- name: Download frontend dist
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: frontend-dist
|
||||||
|
path: web/html
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
cache: false
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
# Naive platforms: use cronet toolchain only (no Bootlin).
|
||||||
|
- name: Clone cronet-go (cronet toolchain for naive)
|
||||||
|
if: matrix.naive
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
git init ~/cronet-go
|
||||||
|
git -C ~/cronet-go remote add origin ${{ env.CRONET_GO_REPO }}
|
||||||
|
git -C ~/cronet-go fetch --depth=1 origin "${{ env.CRONET_GO_VERSION }}"
|
||||||
|
git -C ~/cronet-go checkout FETCH_HEAD
|
||||||
|
git -C ~/cronet-go submodule update --init --recursive --depth=1
|
||||||
|
|
||||||
|
- name: Regenerate Debian keyring (cronet sysroot)
|
||||||
|
if: matrix.naive
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
rm -f ~/cronet-go/naiveproxy/src/build/linux/sysroot_scripts/keyring.gpg
|
||||||
|
cd ~/cronet-go
|
||||||
|
GPG_TTY=/dev/null ./naiveproxy/src/build/linux/sysroot_scripts/generate_keyring.sh
|
||||||
|
|
||||||
|
- name: Cache Chromium toolchain
|
||||||
|
if: matrix.naive
|
||||||
|
id: cache-chromium-toolchain
|
||||||
|
uses: actions/cache@v5
|
||||||
|
with:
|
||||||
|
path: |
|
||||||
|
~/cronet-go/naiveproxy/src/third_party/llvm-build/
|
||||||
|
~/cronet-go/naiveproxy/src/gn/out/
|
||||||
|
~/cronet-go/naiveproxy/src/chrome/build/pgo_profiles/
|
||||||
|
~/cronet-go/naiveproxy/src/out/sysroot-build/
|
||||||
|
key: chromium-toolchain-${{ matrix.platform }}-musl-${{ env.CRONET_GO_VERSION }}
|
||||||
|
|
||||||
|
- name: Build cronet lib and set toolchain env (CC, CXX, CGO_LDFLAGS, PATH)
|
||||||
|
if: matrix.naive
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
cd ~/cronet-go
|
||||||
|
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl download-toolchain
|
||||||
|
go run ./cmd/build-naive --target=linux/${{ matrix.arch }} --libc=musl env | while IFS= read -r line; do
|
||||||
|
line="${line#export }"
|
||||||
|
[[ -z "$line" ]] && continue
|
||||||
|
echo "$line" >> $GITHUB_ENV
|
||||||
|
done
|
||||||
|
|
||||||
|
- name: Set Go build env (all platforms)
|
||||||
|
run: |
|
||||||
|
echo "CGO_ENABLED=1" >> $GITHUB_ENV
|
||||||
|
echo "GOOS=linux" >> $GITHUB_ENV
|
||||||
|
echo "GOARCH=${{ matrix.arch }}" >> $GITHUB_ENV
|
||||||
|
if [ -n "${{ matrix.goarm }}" ]; then echo "GOARM=${{ matrix.goarm }}" >> $GITHUB_ENV; fi
|
||||||
|
|
||||||
|
# Non-naive platforms only: Bootlin musl (armv5, s390x).
|
||||||
|
- name: Set up Bootlin musl (armv5, s390x)
|
||||||
|
if: ${{ matrix.naive != true }}
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
BOOTLIN_ARCH="${{ matrix.bootlin }}"
|
||||||
|
echo "Resolving Bootlin musl toolchain for arch=$BOOTLIN_ARCH (platform=${{ matrix.platform }})"
|
||||||
|
TARBALL_BASE="${{ env.BOOTLIN_BASE_URL }}/$BOOTLIN_ARCH/tarballs/"
|
||||||
|
TARBALL_URL=$(curl -fsSL "$TARBALL_BASE" | grep -oE "${BOOTLIN_ARCH}--musl--stable-[^\"]+\\.tar\\.xz" | sort -r | head -n1)
|
||||||
|
[ -z "$TARBALL_URL" ] && { echo "Failed to locate Bootlin musl toolchain for arch=$BOOTLIN_ARCH" >&2; exit 1; }
|
||||||
|
echo "Downloading: $TARBALL_URL"
|
||||||
|
cd /tmp
|
||||||
|
curl -fL -sS -o "$(basename "$TARBALL_URL")" "$TARBALL_BASE/$TARBALL_URL"
|
||||||
|
tar -xf "$(basename "$TARBALL_URL")"
|
||||||
|
TOOLCHAIN_DIR=$(find . -maxdepth 1 -type d -name "${BOOTLIN_ARCH}--musl--stable-*" | head -n1)
|
||||||
|
TOOLCHAIN_DIR="$(realpath "$TOOLCHAIN_DIR")"
|
||||||
|
BIN_DIR="$TOOLCHAIN_DIR/bin"
|
||||||
|
echo "PATH=$BIN_DIR:$PATH" >> $GITHUB_ENV
|
||||||
|
CC=$(find "$BIN_DIR" -maxdepth 1 \( -name '*-gcc.br_real' -o -name '*-gcc' \) -type f -executable 2>/dev/null | grep -v g++ | head -n1)
|
||||||
|
[ -z "$CC" ] && { echo "No gcc found in $BIN_DIR" >&2; exit 1; }
|
||||||
|
echo "CC=$(realpath "$CC")" >> $GITHUB_ENV
|
||||||
|
SYSROOT=""
|
||||||
|
F=$(find "$TOOLCHAIN_DIR" -name "libc-header-start.h" 2>/dev/null | head -1)
|
||||||
|
if [ -n "$F" ]; then SYSROOT=$(dirname "$(dirname "$(dirname "$(dirname "$F")")")"); fi
|
||||||
|
if [ -n "$SYSROOT" ] && [ -d "$SYSROOT" ]; then
|
||||||
|
echo "CGO_CFLAGS=--sysroot=$SYSROOT" >> $GITHUB_ENV
|
||||||
|
echo "CGO_LDFLAGS=--sysroot=$SYSROOT -static" >> $GITHUB_ENV
|
||||||
|
fi
|
||||||
|
|
||||||
|
- name: Build s-ui
|
||||||
|
run: |
|
||||||
|
set -e
|
||||||
|
BUILD_TAGS="with_quic,with_grpc,with_utls,with_acme,with_gvisor,badlinkname,tfogo_checklinkname0,with_tailscale"
|
||||||
|
[ "${{ matrix.naive }}" = "true" ] && BUILD_TAGS="${BUILD_TAGS},with_naive_outbound,with_musl"
|
||||||
|
go build -ldflags="-w -s -checklinkname=0 -linkmode external -extldflags '-static'" -tags "$BUILD_TAGS" -o sui main.go
|
||||||
|
file sui
|
||||||
|
ldd sui 2>/dev/null || echo "Static binary confirmed"
|
||||||
|
|
||||||
|
mkdir s-ui
|
||||||
|
cp sui s-ui/
|
||||||
|
cp s-ui.service s-ui/
|
||||||
|
cp s-ui.sh s-ui/
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
run: tar -zcvf s-ui-linux-${{ matrix.platform }}.tar.gz s-ui
|
||||||
|
|
||||||
|
- name: Upload artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: s-ui-linux-${{ matrix.platform }}
|
||||||
|
path: ./s-ui-linux-${{ matrix.platform }}.tar.gz
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Upload to Release
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'release' && github.event.action == 'published') ||
|
||||||
|
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
|
with:
|
||||||
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
tag: ${{ github.event_name == 'release' && github.event.release.tag_name || github.ref_name }}
|
||||||
|
file: s-ui-linux-${{ matrix.platform }}.tar.gz
|
||||||
|
asset_name: s-ui-linux-${{ matrix.platform }}.tar.gz
|
||||||
|
prerelease: true
|
||||||
|
overwrite: true
|
||||||
140
.github/workflows/windows.yml
vendored
Normal file
140
.github/workflows/windows.yml
vendored
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
name: Build S-UI for Windows
|
||||||
|
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
release:
|
||||||
|
types: [published]
|
||||||
|
push:
|
||||||
|
branches:
|
||||||
|
- main
|
||||||
|
tags:
|
||||||
|
- "*"
|
||||||
|
paths:
|
||||||
|
- '.github/workflows/windows.yml'
|
||||||
|
- 'frontend/**'
|
||||||
|
- '**.go'
|
||||||
|
- 'go.mod'
|
||||||
|
- 'go.sum'
|
||||||
|
- 'windows/**'
|
||||||
|
|
||||||
|
env:
|
||||||
|
NODE_VERSION: "25"
|
||||||
|
TAGS: "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_naive_outbound,with_purego,badlinkname,tfogo_checklinkname0,with_tailscale"
|
||||||
|
LIBCRONET_BASE_URL: "https://github.com/SagerNet/cronet-go/releases/latest/download"
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build-frontend:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
with:
|
||||||
|
submodules: recursive
|
||||||
|
fetch-depth: 1
|
||||||
|
|
||||||
|
- name: Setup Node.js
|
||||||
|
uses: actions/setup-node@v6
|
||||||
|
with:
|
||||||
|
node-version: ${{ env.NODE_VERSION }}
|
||||||
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
|
- name: Build frontend
|
||||||
|
run: |
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
|
||||||
|
- name: Upload frontend artifact
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: frontend-dist
|
||||||
|
path: frontend/dist
|
||||||
|
retention-days: 1
|
||||||
|
|
||||||
|
build-windows:
|
||||||
|
needs: build-frontend
|
||||||
|
name: build-windows-${{ matrix.arch }}
|
||||||
|
strategy:
|
||||||
|
fail-fast: false
|
||||||
|
matrix:
|
||||||
|
include:
|
||||||
|
- { arch: amd64, runner: windows-latest, cgo: "1" }
|
||||||
|
- { arch: arm64, runner: ubuntu-latest, cgo: "0" }
|
||||||
|
runs-on: ${{ matrix.runner }}
|
||||||
|
steps:
|
||||||
|
- name: Checkout repository
|
||||||
|
uses: actions/checkout@v6.0.2
|
||||||
|
|
||||||
|
- name: Download frontend artifact
|
||||||
|
uses: actions/download-artifact@v8
|
||||||
|
with:
|
||||||
|
name: frontend-dist
|
||||||
|
path: web/html
|
||||||
|
|
||||||
|
- name: Setup Go
|
||||||
|
uses: actions/setup-go@v6
|
||||||
|
with:
|
||||||
|
cache: false
|
||||||
|
go-version-file: go.mod
|
||||||
|
|
||||||
|
- name: Install zip for Windows
|
||||||
|
if: matrix.arch == 'amd64'
|
||||||
|
shell: powershell
|
||||||
|
run: |
|
||||||
|
# Install Chocolatey if not available
|
||||||
|
if (!(Get-Command choco -ErrorAction SilentlyContinue)) {
|
||||||
|
Set-ExecutionPolicy Bypass -Scope Process -Force
|
||||||
|
[System.Net.ServicePointManager]::SecurityProtocol = [System.Net.ServicePointManager]::SecurityProtocol -bor 3072
|
||||||
|
iex ((New-Object System.Net.WebClient).DownloadString('https://community.chocolatey.org/install.ps1'))
|
||||||
|
}
|
||||||
|
# Install zip
|
||||||
|
choco install zip -y
|
||||||
|
|
||||||
|
- name: Build s-ui
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
export CGO_ENABLED=${{ matrix.cgo }}
|
||||||
|
export GOOS=windows
|
||||||
|
export GOARCH=${{ matrix.arch }}
|
||||||
|
|
||||||
|
echo "Building for Windows ${{ matrix.arch }}"
|
||||||
|
go version
|
||||||
|
go env GOOS GOARCH
|
||||||
|
|
||||||
|
go build -ldflags="-w -s -checklinkname=0" -tags "${{ env.TAGS }}" -o sui.exe main.go
|
||||||
|
file sui.exe
|
||||||
|
|
||||||
|
mkdir s-ui-windows
|
||||||
|
cp sui.exe s-ui-windows/
|
||||||
|
cp -r windows/* s-ui-windows/
|
||||||
|
|
||||||
|
- name: Download libcronet-go
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
curl -qsL -o s-ui-windows/libcronet.dll ${{ env.LIBCRONET_BASE_URL }}/libcronet-windows-${{ matrix.arch }}.dll
|
||||||
|
|
||||||
|
- name: Package
|
||||||
|
shell: bash
|
||||||
|
run: |
|
||||||
|
zip -r "s-ui-windows-${{ matrix.arch }}.zip" s-ui-windows
|
||||||
|
|
||||||
|
- name: Upload files to Artifacts
|
||||||
|
uses: actions/upload-artifact@v7
|
||||||
|
with:
|
||||||
|
name: s-ui-windows-${{ matrix.arch }}
|
||||||
|
path: ./s-ui-windows-${{ matrix.arch }}.zip
|
||||||
|
retention-days: 30
|
||||||
|
|
||||||
|
- name: Upload to Release
|
||||||
|
uses: svenstaro/upload-release-action@v2
|
||||||
|
if: |
|
||||||
|
(github.event_name == 'release' && github.event.action == 'published') ||
|
||||||
|
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||||
|
with:
|
||||||
|
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
tag: ${{ github.ref }}
|
||||||
|
file: s-ui-windows-${{ matrix.arch }}.zip
|
||||||
|
asset_name: s-ui-windows-${{ matrix.arch }}.zip
|
||||||
|
prerelease: true
|
||||||
|
overwrite: true
|
||||||
33
.gitignore
vendored
Normal file
33
.gitignore
vendored
Normal file
@@ -0,0 +1,33 @@
|
|||||||
|
.DS_Store
|
||||||
|
dist/
|
||||||
|
release/
|
||||||
|
backup/
|
||||||
|
bin/
|
||||||
|
db/
|
||||||
|
sui
|
||||||
|
web/html
|
||||||
|
main
|
||||||
|
tmp
|
||||||
|
.sync*
|
||||||
|
*.tar.gz
|
||||||
|
frontend/
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
*.log*
|
||||||
|
.cache
|
||||||
|
|
||||||
|
# Windows build artifacts
|
||||||
|
*.exe
|
||||||
|
*.zip
|
||||||
|
s-ui-windows/
|
||||||
|
sui-*.exe
|
||||||
|
sui-*.zip
|
||||||
|
windows/sui-*.exe
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
4
.gitmodules
vendored
Normal file
4
.gitmodules
vendored
Normal file
@@ -0,0 +1,4 @@
|
|||||||
|
[submodule "frontend"]
|
||||||
|
path = frontend
|
||||||
|
url = https://github.com/alireza0/s-ui-frontend
|
||||||
|
branch = main
|
||||||
276
CONTRIBUTING.md
Normal file
276
CONTRIBUTING.md
Normal file
@@ -0,0 +1,276 @@
|
|||||||
|
# Contributing to S-UI
|
||||||
|
|
||||||
|
Thank you for your interest in contributing to S-UI. This document explains how to set up a development environment, follow project conventions, and submit changes. Your contributions help make the **multi-inbound-per-user** approach and the rest of the project better for everyone.
|
||||||
|
|
||||||
|
## Table of Contents
|
||||||
|
|
||||||
|
- [Code of Conduct](#code-of-conduct)
|
||||||
|
- [Development Environment Setup](#development-environment-setup)
|
||||||
|
- [Coding Conventions and Style Guide](#coding-conventions-and-style-guide)
|
||||||
|
- [Testing](#testing)
|
||||||
|
- [Features That Need Help](#features-that-need-help)
|
||||||
|
- [Pull Request Process](#pull-request-process)
|
||||||
|
- [Adding This Guide in Your Repository](#adding-this-guide-in-your-repository)
|
||||||
|
- [Reporting Bugs and Requesting Features](#reporting-bugs-and-requesting-features)
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Code of Conduct
|
||||||
|
|
||||||
|
Please be respectful and constructive when interacting with maintainers and other contributors. This project is for personal learning and communication; use it responsibly and legally.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Development Environment Setup
|
||||||
|
|
||||||
|
### Prerequisites
|
||||||
|
|
||||||
|
- **Go**: 1.25 or later (see `go.mod` for the exact version).
|
||||||
|
- **Git**: For cloning and submodules.
|
||||||
|
- **C compiler**: Required for CGO (e.g. `gcc`, `musl-dev` on Alpine).
|
||||||
|
- **Node.js** (optional): Only if you plan to work on or rebuild the frontend. The repo can be run with pre-built frontend assets.
|
||||||
|
|
||||||
|
### Clone and Submodules
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/alireza0/s-ui
|
||||||
|
cd s-ui
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
The **frontend** lives in a submodule. If you only work on the backend, you can use the existing `web/html` contents or build the frontend once (see below).
|
||||||
|
|
||||||
|
### Backend-Only Development (quickest)
|
||||||
|
|
||||||
|
1. Build and run with the provided script (builds backend and runs with debug + local DB):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./runSUI.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
This runs `./build.sh` then `SUI_DB_FOLDER="db" SUI_DEBUG=true ./sui`.
|
||||||
|
|
||||||
|
2. Or build manually:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
./build.sh
|
||||||
|
SUI_DB_FOLDER=db SUI_DEBUG=true ./sui
|
||||||
|
```
|
||||||
|
|
||||||
|
Default panel: **http://localhost:2095/app/** (user: `admin`, password: `admin` — change in production).
|
||||||
|
|
||||||
|
### Full Stack (Backend + Frontend)
|
||||||
|
|
||||||
|
1. **Frontend** (separate repo in submodule):
|
||||||
|
|
||||||
|
```bash
|
||||||
|
cd frontend
|
||||||
|
npm install
|
||||||
|
npm run build
|
||||||
|
cd ..
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Replace web assets and build backend**:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
mkdir -p web/html
|
||||||
|
rm -rf web/html/*
|
||||||
|
cp -R frontend/dist/* web/html/
|
||||||
|
go build -ldflags "-w -s" -tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_tailscale" -o sui main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
3. Run:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
SUI_DB_FOLDER=db SUI_DEBUG=true ./sui
|
||||||
|
```
|
||||||
|
|
||||||
|
### Build Tags
|
||||||
|
|
||||||
|
The backend is built with these tags for full functionality:
|
||||||
|
|
||||||
|
- `with_quic`, `with_grpc`, `with_utls`, `with_acme`, `with_gvisor`, `with_tailscale`
|
||||||
|
|
||||||
|
Use the same tags when building locally if you need feature parity with releases.
|
||||||
|
|
||||||
|
### Environment Variables (development)
|
||||||
|
|
||||||
|
| Variable | Description | Example |
|
||||||
|
|----------------|--------------------------------|-----------|
|
||||||
|
| `SUI_DB_FOLDER`| Directory for SQLite DB files | `db` |
|
||||||
|
| `SUI_DEBUG` | Enable debug mode | `true` |
|
||||||
|
| `SUI_LOG_LEVEL`| Log level | `debug` |
|
||||||
|
| `SUI_BIN_FOLDER` | Directory for binaries | `bin` |
|
||||||
|
|
||||||
|
### Docker (optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
git clone https://github.com/alireza0/s-ui
|
||||||
|
cd s-ui
|
||||||
|
git submodule update --init --recursive
|
||||||
|
docker build -t s-ui .
|
||||||
|
# or: docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Coding Conventions and Style Guide
|
||||||
|
|
||||||
|
### General
|
||||||
|
|
||||||
|
- Write clear, maintainable code. Prefer small, focused functions and packages.
|
||||||
|
- Comment non-obvious logic and public APIs.
|
||||||
|
- Handle errors explicitly; avoid ignoring `err` unless intentional.
|
||||||
|
|
||||||
|
### Go Style
|
||||||
|
|
||||||
|
- Follow **standard Go style** and **[Effective Go](https://go.dev/doc/effective_go)**.
|
||||||
|
- Run **gofmt** (or **goimports**) before committing:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
gofmt -w .
|
||||||
|
# or: goimports -w .
|
||||||
|
```
|
||||||
|
|
||||||
|
- Use **camelCase** for unexported names and **PascalCase** for exported names.
|
||||||
|
- Keep package names short and lowercase (e.g. `api`, `service`, `util`).
|
||||||
|
- Group imports: standard library, then third-party, then project imports (as in existing files).
|
||||||
|
|
||||||
|
### Project Structure Conventions
|
||||||
|
|
||||||
|
- **`api/`**: HTTP handlers and API routing (e.g. `apiHandler.go`, `apiV2Handler.go`).
|
||||||
|
- **`service/`**: Business logic and panel/core operations.
|
||||||
|
- **`database/model/`**: GORM models and DB entities.
|
||||||
|
- **`util/`**: Shared utilities (e.g. link/sub conversion, JSON).
|
||||||
|
- **`core/`**: sing-box integration and core runtime.
|
||||||
|
- **`sub/`**: Subscription (link/json) handling.
|
||||||
|
|
||||||
|
When adding new features, place code in the appropriate layer (handler → service → model/util) and avoid circular dependencies.
|
||||||
|
|
||||||
|
### Naming and Patterns
|
||||||
|
|
||||||
|
- Handlers: suffix `Handler` (e.g. `APIHandler`, `APIv2Handler`).
|
||||||
|
- Services: suffix `Service` or use package name (e.g. `ApiService`, `LinkService`).
|
||||||
|
- Models: clear struct names with JSON/gorm tags (see `database/model/`).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Testing
|
||||||
|
|
||||||
|
### Current State
|
||||||
|
|
||||||
|
- The project does not yet have a formal test suite (no `*_test.go` files in the repo).
|
||||||
|
- CI currently focuses on **builds** (e.g. `release.yml`) rather than automated tests.
|
||||||
|
|
||||||
|
### What You Can Do Now
|
||||||
|
|
||||||
|
1. **Build verification**: Before submitting a PR, ensure the project builds:
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go build -ldflags "-w -s" -tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_tailscale" -o sui main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
2. **Manual testing**: Run with `./runSUI.sh`, test the changed area (panel, API, subscription, etc.).
|
||||||
|
|
||||||
|
3. **Future tests**: Contributions that add **unit tests** (e.g. for `util/`, `service/`, or API handlers) or **integration tests** are very welcome. Prefer the standard library `testing` package and table-driven tests where appropriate.
|
||||||
|
|
||||||
|
### Running the Linter (optional)
|
||||||
|
|
||||||
|
```bash
|
||||||
|
go vet ./...
|
||||||
|
# Optional: staticcheck, golangci-lint, etc.
|
||||||
|
```
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Features That Need Help
|
||||||
|
|
||||||
|
Community help is especially valuable in these areas. Check the [Issues](https://github.com/alireza0/s-ui/issues) for current tasks and ideas.
|
||||||
|
|
||||||
|
### High-Value Areas
|
||||||
|
|
||||||
|
- **Multi-inbound per user**: Core differentiator of S-UI; improvements to UX, docs, and robustness are welcome.
|
||||||
|
- **API (v1 and v2)**: Completeness, consistency, and documentation (see [API Documentation](https://github.com/alireza0/s-ui/wiki/API-Documentation)).
|
||||||
|
- **Subscription service**: Link conversion, JSON subscription, and info endpoints (`sub/`, `util/`).
|
||||||
|
- **Testing**: Adding unit and integration tests for critical paths.
|
||||||
|
- **Documentation**: User docs, API examples, and contribution docs (like this file).
|
||||||
|
- **Platform support**: macOS is experimental; Windows and Linux improvements are welcome (see `windows/` and `.github/workflows/`).
|
||||||
|
|
||||||
|
### How to Find Tasks
|
||||||
|
|
||||||
|
- **Good first issue**: Look for issues labeled `good first issue` or `help wanted`.
|
||||||
|
- **Feature requests**: [Feature request template](.github/ISSUE_TEMPLATE/feature_request.md).
|
||||||
|
- **Bugs**: [Bug report template](.github/ISSUE_TEMPLATE/bug_report.md).
|
||||||
|
|
||||||
|
If you want to work on a larger feature, open an issue first to discuss approach and avoid duplicate work.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Pull Request Process
|
||||||
|
|
||||||
|
1. **Fork and branch**
|
||||||
|
|
||||||
|
- Fork the repository on GitHub.
|
||||||
|
- Create a branch from `main`: e.g. `git checkout -b fix/issue-123` or `feature/sub-improvements`.
|
||||||
|
|
||||||
|
2. **Make your changes**
|
||||||
|
|
||||||
|
- Follow the [Coding Conventions](#coding-conventions-and-style-guide).
|
||||||
|
- Run `gofmt` and ensure the project builds (see [Testing](#testing)).
|
||||||
|
- Keep commits focused and messages clear (e.g. "Fix link conversion for VMess", "Add tests for outJson").
|
||||||
|
|
||||||
|
3. **Push and open a PR**
|
||||||
|
|
||||||
|
- Push your branch and open a Pull Request against `main`.
|
||||||
|
- Use the PR description to explain:
|
||||||
|
- What problem or feature the PR addresses.
|
||||||
|
- What you changed and how to verify it.
|
||||||
|
- Reference any related issue (e.g. "Fixes #123").
|
||||||
|
|
||||||
|
4. **Review and CI**
|
||||||
|
|
||||||
|
- Maintainers will review your code. CI (e.g. build workflows) must pass.
|
||||||
|
- Address feedback by pushing new commits to the same branch.
|
||||||
|
|
||||||
|
5. **Merge**
|
||||||
|
|
||||||
|
- Once approved and CI is green, a maintainer will merge your PR. Thank you for contributing!
|
||||||
|
|
||||||
|
### PR Guidelines
|
||||||
|
|
||||||
|
- Prefer **small, reviewable PRs**. Split large features into logical steps.
|
||||||
|
- Avoid unrelated changes (e.g. formatting-only or refactors in a feature PR).
|
||||||
|
- Ensure your branch is up to date with `main` before submitting (rebase or merge as the project prefers).
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Adding This Guide in Your Repository
|
||||||
|
|
||||||
|
If you maintain a fork or your own repository and want the contribution guide to be visible and linked properly:
|
||||||
|
|
||||||
|
1. **Keep `CONTRIBUTING.md` in the repository root**
|
||||||
|
GitHub automatically discovers a file named `CONTRIBUTING.md` (or `CONTRIBUTING`) in the root. When someone opens a new issue or pull request, GitHub can show a link to it. The community profile also uses it for the “Contributing” section.
|
||||||
|
|
||||||
|
2. **Link from README**
|
||||||
|
Add a short line in your main `README.md` so new contributors see it when they land on the repo, for example:
|
||||||
|
```markdown
|
||||||
|
**Want to contribute?** See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, coding conventions, and the pull request process.
|
||||||
|
```
|
||||||
|
|
||||||
|
3. **Optional: GitHub “Contributing” link**
|
||||||
|
In the repository **Settings → General → Features**, ensure “Issues” (and optionally “Discussions”) are enabled. The link to `CONTRIBUTING.md` appears when users create a new issue or PR; no extra config is needed as long as the file is in the root.
|
||||||
|
|
||||||
|
4. **When forking**
|
||||||
|
If you fork S-UI, `CONTRIBUTING.md` is already in the repo. Update the clone URLs and repo names in this file if you want your fork’s contribution instructions to point to your own repository.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
## Reporting Bugs and Requesting Features
|
||||||
|
|
||||||
|
- **Bugs**: Use the [bug report template](.github/ISSUE_TEMPLATE/bug_report.md). Include version, OS, steps to reproduce, and expected vs actual behavior.
|
||||||
|
- **Features**: Use the [feature request template](.github/ISSUE_TEMPLATE/feature_request.md). Describe the use case and, if possible, a proposed approach.
|
||||||
|
- **Questions**: Use the [question template](.github/ISSUE_TEMPLATE/question-template.md) or discussions if enabled.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
Thank you for helping S-UI grow. Your contributions make it possible for more users to adopt S-UI in production and benefit from its multi-inbound-per-user design.
|
||||||
48
Dockerfile
Normal file
48
Dockerfile
Normal file
@@ -0,0 +1,48 @@
|
|||||||
|
FROM --platform=$BUILDPLATFORM node:alpine AS front-builder
|
||||||
|
WORKDIR /app
|
||||||
|
COPY frontend/ ./
|
||||||
|
RUN npm install && npm run build
|
||||||
|
|
||||||
|
FROM golang:1.26-alpine AS backend-builder
|
||||||
|
WORKDIR /app
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG TARGETVARIANT
|
||||||
|
ENV CGO_ENABLED=1
|
||||||
|
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
|
||||||
|
ENV GOARCH=$TARGETARCH
|
||||||
|
|
||||||
|
RUN apk update && apk add --no-cache \
|
||||||
|
gcc \
|
||||||
|
musl-dev \
|
||||||
|
libc-dev \
|
||||||
|
make \
|
||||||
|
git \
|
||||||
|
wget \
|
||||||
|
unzip \
|
||||||
|
bash \
|
||||||
|
curl
|
||||||
|
|
||||||
|
ENV CC=gcc
|
||||||
|
|
||||||
|
RUN CRONET_ARCH="$TARGETARCH" && \
|
||||||
|
CRONET_URL="https://github.com/SagerNet/cronet-go/releases/latest/download/libcronet-linux-${CRONET_ARCH}.so"; \
|
||||||
|
echo "Downloading $CRONET_URL" && \
|
||||||
|
wget -q -O ./libcronet.so "$CRONET_URL" && \
|
||||||
|
chmod 755 ./libcronet.so
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
COPY --from=front-builder /app/dist/ /app/web/html/
|
||||||
|
|
||||||
|
RUN if [ "$TARGETARCH" = "arm" ]; then export GOARM=7; [ "$TARGETVARIANT" = "v6" ] && export GOARM=6; fi; \
|
||||||
|
go build -ldflags="-w -s" \
|
||||||
|
-tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_naive_outbound,with_purego,with_tailscale" \
|
||||||
|
-o sui main.go
|
||||||
|
|
||||||
|
FROM alpine
|
||||||
|
LABEL org.opencontainers.image.authors="alireza7@gmail.com"
|
||||||
|
ENV TZ=Asia/Tehran
|
||||||
|
WORKDIR /app
|
||||||
|
RUN set -ex && apk add --no-cache --upgrade bash tzdata ca-certificates nftables
|
||||||
|
COPY --from=backend-builder /app/sui /app/libcronet.so /app/
|
||||||
|
COPY entrypoint.sh /app/
|
||||||
|
ENTRYPOINT [ "./entrypoint.sh" ]
|
||||||
43
Dockerfile.frontend-artifact
Normal file
43
Dockerfile.frontend-artifact
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
FROM golang:1.26-alpine AS backend-builder
|
||||||
|
WORKDIR /app
|
||||||
|
ARG TARGETARCH
|
||||||
|
ARG TARGETVARIANT
|
||||||
|
ENV CGO_ENABLED=1
|
||||||
|
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
|
||||||
|
ENV GOARCH=$TARGETARCH
|
||||||
|
|
||||||
|
RUN apk update && apk add --no-cache \
|
||||||
|
gcc \
|
||||||
|
musl-dev \
|
||||||
|
libc-dev \
|
||||||
|
make \
|
||||||
|
git \
|
||||||
|
wget \
|
||||||
|
unzip \
|
||||||
|
bash \
|
||||||
|
curl
|
||||||
|
|
||||||
|
ENV CC=gcc
|
||||||
|
|
||||||
|
RUN CRONET_ARCH="$TARGETARCH" && \
|
||||||
|
CRONET_URL="https://github.com/SagerNet/cronet-go/releases/latest/download/libcronet-linux-${CRONET_ARCH}.so"; \
|
||||||
|
echo "Downloading $CRONET_URL" && \
|
||||||
|
wget -q -O ./libcronet.so "$CRONET_URL" && \
|
||||||
|
chmod 755 ./libcronet.so
|
||||||
|
|
||||||
|
COPY . .
|
||||||
|
COPY frontend_dist/ /app/web/html/
|
||||||
|
|
||||||
|
RUN if [ "$TARGETARCH" = "arm" ]; then export GOARM=7; [ "$TARGETVARIANT" = "v6" ] && export GOARM=6; fi; \
|
||||||
|
go build -ldflags="-w -s" \
|
||||||
|
-tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_naive_outbound,with_purego,with_tailscale" \
|
||||||
|
-o sui main.go
|
||||||
|
|
||||||
|
FROM alpine
|
||||||
|
LABEL org.opencontainers.image.authors="alireza7@gmail.com"
|
||||||
|
ENV TZ=Asia/Tehran
|
||||||
|
WORKDIR /app
|
||||||
|
RUN set -ex && apk add --no-cache --upgrade bash tzdata ca-certificates nftables
|
||||||
|
COPY --from=backend-builder /app/sui /app/libcronet.so /app/
|
||||||
|
COPY entrypoint.sh /app/
|
||||||
|
ENTRYPOINT [ "./entrypoint.sh" ]
|
||||||
674
LICENSE
Normal file
674
LICENSE
Normal file
@@ -0,0 +1,674 @@
|
|||||||
|
GNU GENERAL PUBLIC LICENSE
|
||||||
|
Version 3, 29 June 2007
|
||||||
|
|
||||||
|
Copyright (C) 2007 Free Software Foundation, Inc. <https://fsf.org/>
|
||||||
|
Everyone is permitted to copy and distribute verbatim copies
|
||||||
|
of this license document, but changing it is not allowed.
|
||||||
|
|
||||||
|
Preamble
|
||||||
|
|
||||||
|
The GNU General Public License is a free, copyleft license for
|
||||||
|
software and other kinds of works.
|
||||||
|
|
||||||
|
The licenses for most software and other practical works are designed
|
||||||
|
to take away your freedom to share and change the works. By contrast,
|
||||||
|
the GNU General Public License is intended to guarantee your freedom to
|
||||||
|
share and change all versions of a program--to make sure it remains free
|
||||||
|
software for all its users. We, the Free Software Foundation, use the
|
||||||
|
GNU General Public License for most of our software; it applies also to
|
||||||
|
any other work released this way by its authors. You can apply it to
|
||||||
|
your programs, too.
|
||||||
|
|
||||||
|
When we speak of free software, we are referring to freedom, not
|
||||||
|
price. Our General Public Licenses are designed to make sure that you
|
||||||
|
have the freedom to distribute copies of free software (and charge for
|
||||||
|
them if you wish), that you receive source code or can get it if you
|
||||||
|
want it, that you can change the software or use pieces of it in new
|
||||||
|
free programs, and that you know you can do these things.
|
||||||
|
|
||||||
|
To protect your rights, we need to prevent others from denying you
|
||||||
|
these rights or asking you to surrender the rights. Therefore, you have
|
||||||
|
certain responsibilities if you distribute copies of the software, or if
|
||||||
|
you modify it: responsibilities to respect the freedom of others.
|
||||||
|
|
||||||
|
For example, if you distribute copies of such a program, whether
|
||||||
|
gratis or for a fee, you must pass on to the recipients the same
|
||||||
|
freedoms that you received. You must make sure that they, too, receive
|
||||||
|
or can get the source code. And you must show them these terms so they
|
||||||
|
know their rights.
|
||||||
|
|
||||||
|
Developers that use the GNU GPL protect your rights with two steps:
|
||||||
|
(1) assert copyright on the software, and (2) offer you this License
|
||||||
|
giving you legal permission to copy, distribute and/or modify it.
|
||||||
|
|
||||||
|
For the developers' and authors' protection, the GPL clearly explains
|
||||||
|
that there is no warranty for this free software. For both users' and
|
||||||
|
authors' sake, the GPL requires that modified versions be marked as
|
||||||
|
changed, so that their problems will not be attributed erroneously to
|
||||||
|
authors of previous versions.
|
||||||
|
|
||||||
|
Some devices are designed to deny users access to install or run
|
||||||
|
modified versions of the software inside them, although the manufacturer
|
||||||
|
can do so. This is fundamentally incompatible with the aim of
|
||||||
|
protecting users' freedom to change the software. The systematic
|
||||||
|
pattern of such abuse occurs in the area of products for individuals to
|
||||||
|
use, which is precisely where it is most unacceptable. Therefore, we
|
||||||
|
have designed this version of the GPL to prohibit the practice for those
|
||||||
|
products. If such problems arise substantially in other domains, we
|
||||||
|
stand ready to extend this provision to those domains in future versions
|
||||||
|
of the GPL, as needed to protect the freedom of users.
|
||||||
|
|
||||||
|
Finally, every program is threatened constantly by software patents.
|
||||||
|
States should not allow patents to restrict development and use of
|
||||||
|
software on general-purpose computers, but in those that do, we wish to
|
||||||
|
avoid the special danger that patents applied to a free program could
|
||||||
|
make it effectively proprietary. To prevent this, the GPL assures that
|
||||||
|
patents cannot be used to render the program non-free.
|
||||||
|
|
||||||
|
The precise terms and conditions for copying, distribution and
|
||||||
|
modification follow.
|
||||||
|
|
||||||
|
TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
0. Definitions.
|
||||||
|
|
||||||
|
"This License" refers to version 3 of the GNU General Public License.
|
||||||
|
|
||||||
|
"Copyright" also means copyright-like laws that apply to other kinds of
|
||||||
|
works, such as semiconductor masks.
|
||||||
|
|
||||||
|
"The Program" refers to any copyrightable work licensed under this
|
||||||
|
License. Each licensee is addressed as "you". "Licensees" and
|
||||||
|
"recipients" may be individuals or organizations.
|
||||||
|
|
||||||
|
To "modify" a work means to copy from or adapt all or part of the work
|
||||||
|
in a fashion requiring copyright permission, other than the making of an
|
||||||
|
exact copy. The resulting work is called a "modified version" of the
|
||||||
|
earlier work or a work "based on" the earlier work.
|
||||||
|
|
||||||
|
A "covered work" means either the unmodified Program or a work based
|
||||||
|
on the Program.
|
||||||
|
|
||||||
|
To "propagate" a work means to do anything with it that, without
|
||||||
|
permission, would make you directly or secondarily liable for
|
||||||
|
infringement under applicable copyright law, except executing it on a
|
||||||
|
computer or modifying a private copy. Propagation includes copying,
|
||||||
|
distribution (with or without modification), making available to the
|
||||||
|
public, and in some countries other activities as well.
|
||||||
|
|
||||||
|
To "convey" a work means any kind of propagation that enables other
|
||||||
|
parties to make or receive copies. Mere interaction with a user through
|
||||||
|
a computer network, with no transfer of a copy, is not conveying.
|
||||||
|
|
||||||
|
An interactive user interface displays "Appropriate Legal Notices"
|
||||||
|
to the extent that it includes a convenient and prominently visible
|
||||||
|
feature that (1) displays an appropriate copyright notice, and (2)
|
||||||
|
tells the user that there is no warranty for the work (except to the
|
||||||
|
extent that warranties are provided), that licensees may convey the
|
||||||
|
work under this License, and how to view a copy of this License. If
|
||||||
|
the interface presents a list of user commands or options, such as a
|
||||||
|
menu, a prominent item in the list meets this criterion.
|
||||||
|
|
||||||
|
1. Source Code.
|
||||||
|
|
||||||
|
The "source code" for a work means the preferred form of the work
|
||||||
|
for making modifications to it. "Object code" means any non-source
|
||||||
|
form of a work.
|
||||||
|
|
||||||
|
A "Standard Interface" means an interface that either is an official
|
||||||
|
standard defined by a recognized standards body, or, in the case of
|
||||||
|
interfaces specified for a particular programming language, one that
|
||||||
|
is widely used among developers working in that language.
|
||||||
|
|
||||||
|
The "System Libraries" of an executable work include anything, other
|
||||||
|
than the work as a whole, that (a) is included in the normal form of
|
||||||
|
packaging a Major Component, but which is not part of that Major
|
||||||
|
Component, and (b) serves only to enable use of the work with that
|
||||||
|
Major Component, or to implement a Standard Interface for which an
|
||||||
|
implementation is available to the public in source code form. A
|
||||||
|
"Major Component", in this context, means a major essential component
|
||||||
|
(kernel, window system, and so on) of the specific operating system
|
||||||
|
(if any) on which the executable work runs, or a compiler used to
|
||||||
|
produce the work, or an object code interpreter used to run it.
|
||||||
|
|
||||||
|
The "Corresponding Source" for a work in object code form means all
|
||||||
|
the source code needed to generate, install, and (for an executable
|
||||||
|
work) run the object code and to modify the work, including scripts to
|
||||||
|
control those activities. However, it does not include the work's
|
||||||
|
System Libraries, or general-purpose tools or generally available free
|
||||||
|
programs which are used unmodified in performing those activities but
|
||||||
|
which are not part of the work. For example, Corresponding Source
|
||||||
|
includes interface definition files associated with source files for
|
||||||
|
the work, and the source code for shared libraries and dynamically
|
||||||
|
linked subprograms that the work is specifically designed to require,
|
||||||
|
such as by intimate data communication or control flow between those
|
||||||
|
subprograms and other parts of the work.
|
||||||
|
|
||||||
|
The Corresponding Source need not include anything that users
|
||||||
|
can regenerate automatically from other parts of the Corresponding
|
||||||
|
Source.
|
||||||
|
|
||||||
|
The Corresponding Source for a work in source code form is that
|
||||||
|
same work.
|
||||||
|
|
||||||
|
2. Basic Permissions.
|
||||||
|
|
||||||
|
All rights granted under this License are granted for the term of
|
||||||
|
copyright on the Program, and are irrevocable provided the stated
|
||||||
|
conditions are met. This License explicitly affirms your unlimited
|
||||||
|
permission to run the unmodified Program. The output from running a
|
||||||
|
covered work is covered by this License only if the output, given its
|
||||||
|
content, constitutes a covered work. This License acknowledges your
|
||||||
|
rights of fair use or other equivalent, as provided by copyright law.
|
||||||
|
|
||||||
|
You may make, run and propagate covered works that you do not
|
||||||
|
convey, without conditions so long as your license otherwise remains
|
||||||
|
in force. You may convey covered works to others for the sole purpose
|
||||||
|
of having them make modifications exclusively for you, or provide you
|
||||||
|
with facilities for running those works, provided that you comply with
|
||||||
|
the terms of this License in conveying all material for which you do
|
||||||
|
not control copyright. Those thus making or running the covered works
|
||||||
|
for you must do so exclusively on your behalf, under your direction
|
||||||
|
and control, on terms that prohibit them from making any copies of
|
||||||
|
your copyrighted material outside their relationship with you.
|
||||||
|
|
||||||
|
Conveying under any other circumstances is permitted solely under
|
||||||
|
the conditions stated below. Sublicensing is not allowed; section 10
|
||||||
|
makes it unnecessary.
|
||||||
|
|
||||||
|
3. Protecting Users' Legal Rights From Anti-Circumvention Law.
|
||||||
|
|
||||||
|
No covered work shall be deemed part of an effective technological
|
||||||
|
measure under any applicable law fulfilling obligations under article
|
||||||
|
11 of the WIPO copyright treaty adopted on 20 December 1996, or
|
||||||
|
similar laws prohibiting or restricting circumvention of such
|
||||||
|
measures.
|
||||||
|
|
||||||
|
When you convey a covered work, you waive any legal power to forbid
|
||||||
|
circumvention of technological measures to the extent such circumvention
|
||||||
|
is effected by exercising rights under this License with respect to
|
||||||
|
the covered work, and you disclaim any intention to limit operation or
|
||||||
|
modification of the work as a means of enforcing, against the work's
|
||||||
|
users, your or third parties' legal rights to forbid circumvention of
|
||||||
|
technological measures.
|
||||||
|
|
||||||
|
4. Conveying Verbatim Copies.
|
||||||
|
|
||||||
|
You may convey verbatim copies of the Program's source code as you
|
||||||
|
receive it, in any medium, provided that you conspicuously and
|
||||||
|
appropriately publish on each copy an appropriate copyright notice;
|
||||||
|
keep intact all notices stating that this License and any
|
||||||
|
non-permissive terms added in accord with section 7 apply to the code;
|
||||||
|
keep intact all notices of the absence of any warranty; and give all
|
||||||
|
recipients a copy of this License along with the Program.
|
||||||
|
|
||||||
|
You may charge any price or no price for each copy that you convey,
|
||||||
|
and you may offer support or warranty protection for a fee.
|
||||||
|
|
||||||
|
5. Conveying Modified Source Versions.
|
||||||
|
|
||||||
|
You may convey a work based on the Program, or the modifications to
|
||||||
|
produce it from the Program, in the form of source code under the
|
||||||
|
terms of section 4, provided that you also meet all of these conditions:
|
||||||
|
|
||||||
|
a) The work must carry prominent notices stating that you modified
|
||||||
|
it, and giving a relevant date.
|
||||||
|
|
||||||
|
b) The work must carry prominent notices stating that it is
|
||||||
|
released under this License and any conditions added under section
|
||||||
|
7. This requirement modifies the requirement in section 4 to
|
||||||
|
"keep intact all notices".
|
||||||
|
|
||||||
|
c) You must license the entire work, as a whole, under this
|
||||||
|
License to anyone who comes into possession of a copy. This
|
||||||
|
License will therefore apply, along with any applicable section 7
|
||||||
|
additional terms, to the whole of the work, and all its parts,
|
||||||
|
regardless of how they are packaged. This License gives no
|
||||||
|
permission to license the work in any other way, but it does not
|
||||||
|
invalidate such permission if you have separately received it.
|
||||||
|
|
||||||
|
d) If the work has interactive user interfaces, each must display
|
||||||
|
Appropriate Legal Notices; however, if the Program has interactive
|
||||||
|
interfaces that do not display Appropriate Legal Notices, your
|
||||||
|
work need not make them do so.
|
||||||
|
|
||||||
|
A compilation of a covered work with other separate and independent
|
||||||
|
works, which are not by their nature extensions of the covered work,
|
||||||
|
and which are not combined with it such as to form a larger program,
|
||||||
|
in or on a volume of a storage or distribution medium, is called an
|
||||||
|
"aggregate" if the compilation and its resulting copyright are not
|
||||||
|
used to limit the access or legal rights of the compilation's users
|
||||||
|
beyond what the individual works permit. Inclusion of a covered work
|
||||||
|
in an aggregate does not cause this License to apply to the other
|
||||||
|
parts of the aggregate.
|
||||||
|
|
||||||
|
6. Conveying Non-Source Forms.
|
||||||
|
|
||||||
|
You may convey a covered work in object code form under the terms
|
||||||
|
of sections 4 and 5, provided that you also convey the
|
||||||
|
machine-readable Corresponding Source under the terms of this License,
|
||||||
|
in one of these ways:
|
||||||
|
|
||||||
|
a) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by the
|
||||||
|
Corresponding Source fixed on a durable physical medium
|
||||||
|
customarily used for software interchange.
|
||||||
|
|
||||||
|
b) Convey the object code in, or embodied in, a physical product
|
||||||
|
(including a physical distribution medium), accompanied by a
|
||||||
|
written offer, valid for at least three years and valid for as
|
||||||
|
long as you offer spare parts or customer support for that product
|
||||||
|
model, to give anyone who possesses the object code either (1) a
|
||||||
|
copy of the Corresponding Source for all the software in the
|
||||||
|
product that is covered by this License, on a durable physical
|
||||||
|
medium customarily used for software interchange, for a price no
|
||||||
|
more than your reasonable cost of physically performing this
|
||||||
|
conveying of source, or (2) access to copy the
|
||||||
|
Corresponding Source from a network server at no charge.
|
||||||
|
|
||||||
|
c) Convey individual copies of the object code with a copy of the
|
||||||
|
written offer to provide the Corresponding Source. This
|
||||||
|
alternative is allowed only occasionally and noncommercially, and
|
||||||
|
only if you received the object code with such an offer, in accord
|
||||||
|
with subsection 6b.
|
||||||
|
|
||||||
|
d) Convey the object code by offering access from a designated
|
||||||
|
place (gratis or for a charge), and offer equivalent access to the
|
||||||
|
Corresponding Source in the same way through the same place at no
|
||||||
|
further charge. You need not require recipients to copy the
|
||||||
|
Corresponding Source along with the object code. If the place to
|
||||||
|
copy the object code is a network server, the Corresponding Source
|
||||||
|
may be on a different server (operated by you or a third party)
|
||||||
|
that supports equivalent copying facilities, provided you maintain
|
||||||
|
clear directions next to the object code saying where to find the
|
||||||
|
Corresponding Source. Regardless of what server hosts the
|
||||||
|
Corresponding Source, you remain obligated to ensure that it is
|
||||||
|
available for as long as needed to satisfy these requirements.
|
||||||
|
|
||||||
|
e) Convey the object code using peer-to-peer transmission, provided
|
||||||
|
you inform other peers where the object code and Corresponding
|
||||||
|
Source of the work are being offered to the general public at no
|
||||||
|
charge under subsection 6d.
|
||||||
|
|
||||||
|
A separable portion of the object code, whose source code is excluded
|
||||||
|
from the Corresponding Source as a System Library, need not be
|
||||||
|
included in conveying the object code work.
|
||||||
|
|
||||||
|
A "User Product" is either (1) a "consumer product", which means any
|
||||||
|
tangible personal property which is normally used for personal, family,
|
||||||
|
or household purposes, or (2) anything designed or sold for incorporation
|
||||||
|
into a dwelling. In determining whether a product is a consumer product,
|
||||||
|
doubtful cases shall be resolved in favor of coverage. For a particular
|
||||||
|
product received by a particular user, "normally used" refers to a
|
||||||
|
typical or common use of that class of product, regardless of the status
|
||||||
|
of the particular user or of the way in which the particular user
|
||||||
|
actually uses, or expects or is expected to use, the product. A product
|
||||||
|
is a consumer product regardless of whether the product has substantial
|
||||||
|
commercial, industrial or non-consumer uses, unless such uses represent
|
||||||
|
the only significant mode of use of the product.
|
||||||
|
|
||||||
|
"Installation Information" for a User Product means any methods,
|
||||||
|
procedures, authorization keys, or other information required to install
|
||||||
|
and execute modified versions of a covered work in that User Product from
|
||||||
|
a modified version of its Corresponding Source. The information must
|
||||||
|
suffice to ensure that the continued functioning of the modified object
|
||||||
|
code is in no case prevented or interfered with solely because
|
||||||
|
modification has been made.
|
||||||
|
|
||||||
|
If you convey an object code work under this section in, or with, or
|
||||||
|
specifically for use in, a User Product, and the conveying occurs as
|
||||||
|
part of a transaction in which the right of possession and use of the
|
||||||
|
User Product is transferred to the recipient in perpetuity or for a
|
||||||
|
fixed term (regardless of how the transaction is characterized), the
|
||||||
|
Corresponding Source conveyed under this section must be accompanied
|
||||||
|
by the Installation Information. But this requirement does not apply
|
||||||
|
if neither you nor any third party retains the ability to install
|
||||||
|
modified object code on the User Product (for example, the work has
|
||||||
|
been installed in ROM).
|
||||||
|
|
||||||
|
The requirement to provide Installation Information does not include a
|
||||||
|
requirement to continue to provide support service, warranty, or updates
|
||||||
|
for a work that has been modified or installed by the recipient, or for
|
||||||
|
the User Product in which it has been modified or installed. Access to a
|
||||||
|
network may be denied when the modification itself materially and
|
||||||
|
adversely affects the operation of the network or violates the rules and
|
||||||
|
protocols for communication across the network.
|
||||||
|
|
||||||
|
Corresponding Source conveyed, and Installation Information provided,
|
||||||
|
in accord with this section must be in a format that is publicly
|
||||||
|
documented (and with an implementation available to the public in
|
||||||
|
source code form), and must require no special password or key for
|
||||||
|
unpacking, reading or copying.
|
||||||
|
|
||||||
|
7. Additional Terms.
|
||||||
|
|
||||||
|
"Additional permissions" are terms that supplement the terms of this
|
||||||
|
License by making exceptions from one or more of its conditions.
|
||||||
|
Additional permissions that are applicable to the entire Program shall
|
||||||
|
be treated as though they were included in this License, to the extent
|
||||||
|
that they are valid under applicable law. If additional permissions
|
||||||
|
apply only to part of the Program, that part may be used separately
|
||||||
|
under those permissions, but the entire Program remains governed by
|
||||||
|
this License without regard to the additional permissions.
|
||||||
|
|
||||||
|
When you convey a copy of a covered work, you may at your option
|
||||||
|
remove any additional permissions from that copy, or from any part of
|
||||||
|
it. (Additional permissions may be written to require their own
|
||||||
|
removal in certain cases when you modify the work.) You may place
|
||||||
|
additional permissions on material, added by you to a covered work,
|
||||||
|
for which you have or can give appropriate copyright permission.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, for material you
|
||||||
|
add to a covered work, you may (if authorized by the copyright holders of
|
||||||
|
that material) supplement the terms of this License with terms:
|
||||||
|
|
||||||
|
a) Disclaiming warranty or limiting liability differently from the
|
||||||
|
terms of sections 15 and 16 of this License; or
|
||||||
|
|
||||||
|
b) Requiring preservation of specified reasonable legal notices or
|
||||||
|
author attributions in that material or in the Appropriate Legal
|
||||||
|
Notices displayed by works containing it; or
|
||||||
|
|
||||||
|
c) Prohibiting misrepresentation of the origin of that material, or
|
||||||
|
requiring that modified versions of such material be marked in
|
||||||
|
reasonable ways as different from the original version; or
|
||||||
|
|
||||||
|
d) Limiting the use for publicity purposes of names of licensors or
|
||||||
|
authors of the material; or
|
||||||
|
|
||||||
|
e) Declining to grant rights under trademark law for use of some
|
||||||
|
trade names, trademarks, or service marks; or
|
||||||
|
|
||||||
|
f) Requiring indemnification of licensors and authors of that
|
||||||
|
material by anyone who conveys the material (or modified versions of
|
||||||
|
it) with contractual assumptions of liability to the recipient, for
|
||||||
|
any liability that these contractual assumptions directly impose on
|
||||||
|
those licensors and authors.
|
||||||
|
|
||||||
|
All other non-permissive additional terms are considered "further
|
||||||
|
restrictions" within the meaning of section 10. If the Program as you
|
||||||
|
received it, or any part of it, contains a notice stating that it is
|
||||||
|
governed by this License along with a term that is a further
|
||||||
|
restriction, you may remove that term. If a license document contains
|
||||||
|
a further restriction but permits relicensing or conveying under this
|
||||||
|
License, you may add to a covered work material governed by the terms
|
||||||
|
of that license document, provided that the further restriction does
|
||||||
|
not survive such relicensing or conveying.
|
||||||
|
|
||||||
|
If you add terms to a covered work in accord with this section, you
|
||||||
|
must place, in the relevant source files, a statement of the
|
||||||
|
additional terms that apply to those files, or a notice indicating
|
||||||
|
where to find the applicable terms.
|
||||||
|
|
||||||
|
Additional terms, permissive or non-permissive, may be stated in the
|
||||||
|
form of a separately written license, or stated as exceptions;
|
||||||
|
the above requirements apply either way.
|
||||||
|
|
||||||
|
8. Termination.
|
||||||
|
|
||||||
|
You may not propagate or modify a covered work except as expressly
|
||||||
|
provided under this License. Any attempt otherwise to propagate or
|
||||||
|
modify it is void, and will automatically terminate your rights under
|
||||||
|
this License (including any patent licenses granted under the third
|
||||||
|
paragraph of section 11).
|
||||||
|
|
||||||
|
However, if you cease all violation of this License, then your
|
||||||
|
license from a particular copyright holder is reinstated (a)
|
||||||
|
provisionally, unless and until the copyright holder explicitly and
|
||||||
|
finally terminates your license, and (b) permanently, if the copyright
|
||||||
|
holder fails to notify you of the violation by some reasonable means
|
||||||
|
prior to 60 days after the cessation.
|
||||||
|
|
||||||
|
Moreover, your license from a particular copyright holder is
|
||||||
|
reinstated permanently if the copyright holder notifies you of the
|
||||||
|
violation by some reasonable means, this is the first time you have
|
||||||
|
received notice of violation of this License (for any work) from that
|
||||||
|
copyright holder, and you cure the violation prior to 30 days after
|
||||||
|
your receipt of the notice.
|
||||||
|
|
||||||
|
Termination of your rights under this section does not terminate the
|
||||||
|
licenses of parties who have received copies or rights from you under
|
||||||
|
this License. If your rights have been terminated and not permanently
|
||||||
|
reinstated, you do not qualify to receive new licenses for the same
|
||||||
|
material under section 10.
|
||||||
|
|
||||||
|
9. Acceptance Not Required for Having Copies.
|
||||||
|
|
||||||
|
You are not required to accept this License in order to receive or
|
||||||
|
run a copy of the Program. Ancillary propagation of a covered work
|
||||||
|
occurring solely as a consequence of using peer-to-peer transmission
|
||||||
|
to receive a copy likewise does not require acceptance. However,
|
||||||
|
nothing other than this License grants you permission to propagate or
|
||||||
|
modify any covered work. These actions infringe copyright if you do
|
||||||
|
not accept this License. Therefore, by modifying or propagating a
|
||||||
|
covered work, you indicate your acceptance of this License to do so.
|
||||||
|
|
||||||
|
10. Automatic Licensing of Downstream Recipients.
|
||||||
|
|
||||||
|
Each time you convey a covered work, the recipient automatically
|
||||||
|
receives a license from the original licensors, to run, modify and
|
||||||
|
propagate that work, subject to this License. You are not responsible
|
||||||
|
for enforcing compliance by third parties with this License.
|
||||||
|
|
||||||
|
An "entity transaction" is a transaction transferring control of an
|
||||||
|
organization, or substantially all assets of one, or subdividing an
|
||||||
|
organization, or merging organizations. If propagation of a covered
|
||||||
|
work results from an entity transaction, each party to that
|
||||||
|
transaction who receives a copy of the work also receives whatever
|
||||||
|
licenses to the work the party's predecessor in interest had or could
|
||||||
|
give under the previous paragraph, plus a right to possession of the
|
||||||
|
Corresponding Source of the work from the predecessor in interest, if
|
||||||
|
the predecessor has it or can get it with reasonable efforts.
|
||||||
|
|
||||||
|
You may not impose any further restrictions on the exercise of the
|
||||||
|
rights granted or affirmed under this License. For example, you may
|
||||||
|
not impose a license fee, royalty, or other charge for exercise of
|
||||||
|
rights granted under this License, and you may not initiate litigation
|
||||||
|
(including a cross-claim or counterclaim in a lawsuit) alleging that
|
||||||
|
any patent claim is infringed by making, using, selling, offering for
|
||||||
|
sale, or importing the Program or any portion of it.
|
||||||
|
|
||||||
|
11. Patents.
|
||||||
|
|
||||||
|
A "contributor" is a copyright holder who authorizes use under this
|
||||||
|
License of the Program or a work on which the Program is based. The
|
||||||
|
work thus licensed is called the contributor's "contributor version".
|
||||||
|
|
||||||
|
A contributor's "essential patent claims" are all patent claims
|
||||||
|
owned or controlled by the contributor, whether already acquired or
|
||||||
|
hereafter acquired, that would be infringed by some manner, permitted
|
||||||
|
by this License, of making, using, or selling its contributor version,
|
||||||
|
but do not include claims that would be infringed only as a
|
||||||
|
consequence of further modification of the contributor version. For
|
||||||
|
purposes of this definition, "control" includes the right to grant
|
||||||
|
patent sublicenses in a manner consistent with the requirements of
|
||||||
|
this License.
|
||||||
|
|
||||||
|
Each contributor grants you a non-exclusive, worldwide, royalty-free
|
||||||
|
patent license under the contributor's essential patent claims, to
|
||||||
|
make, use, sell, offer for sale, import and otherwise run, modify and
|
||||||
|
propagate the contents of its contributor version.
|
||||||
|
|
||||||
|
In the following three paragraphs, a "patent license" is any express
|
||||||
|
agreement or commitment, however denominated, not to enforce a patent
|
||||||
|
(such as an express permission to practice a patent or covenant not to
|
||||||
|
sue for patent infringement). To "grant" such a patent license to a
|
||||||
|
party means to make such an agreement or commitment not to enforce a
|
||||||
|
patent against the party.
|
||||||
|
|
||||||
|
If you convey a covered work, knowingly relying on a patent license,
|
||||||
|
and the Corresponding Source of the work is not available for anyone
|
||||||
|
to copy, free of charge and under the terms of this License, through a
|
||||||
|
publicly available network server or other readily accessible means,
|
||||||
|
then you must either (1) cause the Corresponding Source to be so
|
||||||
|
available, or (2) arrange to deprive yourself of the benefit of the
|
||||||
|
patent license for this particular work, or (3) arrange, in a manner
|
||||||
|
consistent with the requirements of this License, to extend the patent
|
||||||
|
license to downstream recipients. "Knowingly relying" means you have
|
||||||
|
actual knowledge that, but for the patent license, your conveying the
|
||||||
|
covered work in a country, or your recipient's use of the covered work
|
||||||
|
in a country, would infringe one or more identifiable patents in that
|
||||||
|
country that you have reason to believe are valid.
|
||||||
|
|
||||||
|
If, pursuant to or in connection with a single transaction or
|
||||||
|
arrangement, you convey, or propagate by procuring conveyance of, a
|
||||||
|
covered work, and grant a patent license to some of the parties
|
||||||
|
receiving the covered work authorizing them to use, propagate, modify
|
||||||
|
or convey a specific copy of the covered work, then the patent license
|
||||||
|
you grant is automatically extended to all recipients of the covered
|
||||||
|
work and works based on it.
|
||||||
|
|
||||||
|
A patent license is "discriminatory" if it does not include within
|
||||||
|
the scope of its coverage, prohibits the exercise of, or is
|
||||||
|
conditioned on the non-exercise of one or more of the rights that are
|
||||||
|
specifically granted under this License. You may not convey a covered
|
||||||
|
work if you are a party to an arrangement with a third party that is
|
||||||
|
in the business of distributing software, under which you make payment
|
||||||
|
to the third party based on the extent of your activity of conveying
|
||||||
|
the work, and under which the third party grants, to any of the
|
||||||
|
parties who would receive the covered work from you, a discriminatory
|
||||||
|
patent license (a) in connection with copies of the covered work
|
||||||
|
conveyed by you (or copies made from those copies), or (b) primarily
|
||||||
|
for and in connection with specific products or compilations that
|
||||||
|
contain the covered work, unless you entered into that arrangement,
|
||||||
|
or that patent license was granted, prior to 28 March 2007.
|
||||||
|
|
||||||
|
Nothing in this License shall be construed as excluding or limiting
|
||||||
|
any implied license or other defenses to infringement that may
|
||||||
|
otherwise be available to you under applicable patent law.
|
||||||
|
|
||||||
|
12. No Surrender of Others' Freedom.
|
||||||
|
|
||||||
|
If conditions are imposed on you (whether by court order, agreement or
|
||||||
|
otherwise) that contradict the conditions of this License, they do not
|
||||||
|
excuse you from the conditions of this License. If you cannot convey a
|
||||||
|
covered work so as to satisfy simultaneously your obligations under this
|
||||||
|
License and any other pertinent obligations, then as a consequence you may
|
||||||
|
not convey it at all. For example, if you agree to terms that obligate you
|
||||||
|
to collect a royalty for further conveying from those to whom you convey
|
||||||
|
the Program, the only way you could satisfy both those terms and this
|
||||||
|
License would be to refrain entirely from conveying the Program.
|
||||||
|
|
||||||
|
13. Use with the GNU Affero General Public License.
|
||||||
|
|
||||||
|
Notwithstanding any other provision of this License, you have
|
||||||
|
permission to link or combine any covered work with a work licensed
|
||||||
|
under version 3 of the GNU Affero General Public License into a single
|
||||||
|
combined work, and to convey the resulting work. The terms of this
|
||||||
|
License will continue to apply to the part which is the covered work,
|
||||||
|
but the special requirements of the GNU Affero General Public License,
|
||||||
|
section 13, concerning interaction through a network will apply to the
|
||||||
|
combination as such.
|
||||||
|
|
||||||
|
14. Revised Versions of this License.
|
||||||
|
|
||||||
|
The Free Software Foundation may publish revised and/or new versions of
|
||||||
|
the GNU General Public License from time to time. Such new versions will
|
||||||
|
be similar in spirit to the present version, but may differ in detail to
|
||||||
|
address new problems or concerns.
|
||||||
|
|
||||||
|
Each version is given a distinguishing version number. If the
|
||||||
|
Program specifies that a certain numbered version of the GNU General
|
||||||
|
Public License "or any later version" applies to it, you have the
|
||||||
|
option of following the terms and conditions either of that numbered
|
||||||
|
version or of any later version published by the Free Software
|
||||||
|
Foundation. If the Program does not specify a version number of the
|
||||||
|
GNU General Public License, you may choose any version ever published
|
||||||
|
by the Free Software Foundation.
|
||||||
|
|
||||||
|
If the Program specifies that a proxy can decide which future
|
||||||
|
versions of the GNU General Public License can be used, that proxy's
|
||||||
|
public statement of acceptance of a version permanently authorizes you
|
||||||
|
to choose that version for the Program.
|
||||||
|
|
||||||
|
Later license versions may give you additional or different
|
||||||
|
permissions. However, no additional obligations are imposed on any
|
||||||
|
author or copyright holder as a result of your choosing to follow a
|
||||||
|
later version.
|
||||||
|
|
||||||
|
15. Disclaimer of Warranty.
|
||||||
|
|
||||||
|
THERE IS NO WARRANTY FOR THE PROGRAM, TO THE EXTENT PERMITTED BY
|
||||||
|
APPLICABLE LAW. EXCEPT WHEN OTHERWISE STATED IN WRITING THE COPYRIGHT
|
||||||
|
HOLDERS AND/OR OTHER PARTIES PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY
|
||||||
|
OF ANY KIND, EITHER EXPRESSED OR IMPLIED, INCLUDING, BUT NOT LIMITED TO,
|
||||||
|
THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
|
||||||
|
PURPOSE. THE ENTIRE RISK AS TO THE QUALITY AND PERFORMANCE OF THE PROGRAM
|
||||||
|
IS WITH YOU. SHOULD THE PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF
|
||||||
|
ALL NECESSARY SERVICING, REPAIR OR CORRECTION.
|
||||||
|
|
||||||
|
16. Limitation of Liability.
|
||||||
|
|
||||||
|
IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING
|
||||||
|
WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MODIFIES AND/OR CONVEYS
|
||||||
|
THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, INCLUDING ANY
|
||||||
|
GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING OUT OF THE
|
||||||
|
USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED TO LOSS OF
|
||||||
|
DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY YOU OR THIRD
|
||||||
|
PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER PROGRAMS),
|
||||||
|
EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE POSSIBILITY OF
|
||||||
|
SUCH DAMAGES.
|
||||||
|
|
||||||
|
17. Interpretation of Sections 15 and 16.
|
||||||
|
|
||||||
|
If the disclaimer of warranty and limitation of liability provided
|
||||||
|
above cannot be given local legal effect according to their terms,
|
||||||
|
reviewing courts shall apply local law that most closely approximates
|
||||||
|
an absolute waiver of all civil liability in connection with the
|
||||||
|
Program, unless a warranty or assumption of liability accompanies a
|
||||||
|
copy of the Program in return for a fee.
|
||||||
|
|
||||||
|
END OF TERMS AND CONDITIONS
|
||||||
|
|
||||||
|
How to Apply These Terms to Your New Programs
|
||||||
|
|
||||||
|
If you develop a new program, and you want it to be of the greatest
|
||||||
|
possible use to the public, the best way to achieve this is to make it
|
||||||
|
free software which everyone can redistribute and change under these terms.
|
||||||
|
|
||||||
|
To do so, attach the following notices to the program. It is safest
|
||||||
|
to attach them to the start of each source file to most effectively
|
||||||
|
state the exclusion of warranty; and each file should have at least
|
||||||
|
the "copyright" line and a pointer to where the full notice is found.
|
||||||
|
|
||||||
|
<one line to give the program's name and a brief idea of what it does.>
|
||||||
|
Copyright (C) <year> <name of author>
|
||||||
|
|
||||||
|
This program is free software: you can redistribute it and/or modify
|
||||||
|
it under the terms of the GNU General Public License as published by
|
||||||
|
the Free Software Foundation, either version 3 of the License, or
|
||||||
|
(at your option) any later version.
|
||||||
|
|
||||||
|
This program is distributed in the hope that it will be useful,
|
||||||
|
but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||||
|
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||||
|
GNU General Public License for more details.
|
||||||
|
|
||||||
|
You should have received a copy of the GNU General Public License
|
||||||
|
along with this program. If not, see <https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
Also add information on how to contact you by electronic and paper mail.
|
||||||
|
|
||||||
|
If the program does terminal interaction, make it output a short
|
||||||
|
notice like this when it starts in an interactive mode:
|
||||||
|
|
||||||
|
<program> Copyright (C) <year> <name of author>
|
||||||
|
This program comes with ABSOLUTELY NO WARRANTY; for details type `show w'.
|
||||||
|
This is free software, and you are welcome to redistribute it
|
||||||
|
under certain conditions; type `show c' for details.
|
||||||
|
|
||||||
|
The hypothetical commands `show w' and `show c' should show the appropriate
|
||||||
|
parts of the General Public License. Of course, your program's commands
|
||||||
|
might be different; for a GUI interface, you would use an "about box".
|
||||||
|
|
||||||
|
You should also get your employer (if you work as a programmer) or school,
|
||||||
|
if any, to sign a "copyright disclaimer" for the program, if necessary.
|
||||||
|
For more information on this, and how to apply and follow the GNU GPL, see
|
||||||
|
<https://www.gnu.org/licenses/>.
|
||||||
|
|
||||||
|
The GNU General Public License does not permit incorporating your program
|
||||||
|
into proprietary programs. If your program is a subroutine library, you
|
||||||
|
may consider it more useful to permit linking proprietary applications with
|
||||||
|
the library. If this is what you want to do, use the GNU Lesser General
|
||||||
|
Public License instead of this License. But first, please read
|
||||||
|
<https://www.gnu.org/licenses/why-not-lgpl.html>.
|
||||||
259
README.md
Normal file
259
README.md
Normal file
@@ -0,0 +1,259 @@
|
|||||||
|
# S-UI
|
||||||
|
**An Advanced Web Panel • Built on SagerNet/Sing-Box**
|
||||||
|
|
||||||
|

|
||||||
|

|
||||||
|
[](https://goreportcard.com/report/github.com/alireza0/s-ui)
|
||||||
|
[](https://img.shields.io/github/downloads/alireza0/s-ui/total.svg)
|
||||||
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|
||||||
|
> **Disclaimer:** This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment
|
||||||
|
|
||||||
|
**If you think this project is helpful to you, you may wish to give a**:star2:
|
||||||
|
|
||||||
|
**Want to contribute?** See [CONTRIBUTING.md](CONTRIBUTING.md) for development setup, coding conventions, testing, and the pull request process.
|
||||||
|
|
||||||
|
[](https://www.buymeacoffee.com/alireza7)
|
||||||
|
|
||||||
|
<a href="https://nowpayments.io/donation/alireza7" target="_blank" rel="noreferrer noopener">
|
||||||
|
<img src="https://nowpayments.io/images/embeds/donation-button-white.svg" alt="Crypto donation button by NOWPayments">
|
||||||
|
</a>
|
||||||
|
|
||||||
|
## Quick Overview
|
||||||
|
| Features | Enable? |
|
||||||
|
| -------------------------------------- | :----------------: |
|
||||||
|
| Multi-Protocol | :heavy_check_mark: |
|
||||||
|
| Multi-Language | :heavy_check_mark: |
|
||||||
|
| Multi-Client/Inbound | :heavy_check_mark: |
|
||||||
|
| Advanced Traffic Routing Interface | :heavy_check_mark: |
|
||||||
|
| Client & Traffic & System Status | :heavy_check_mark: |
|
||||||
|
| Subscription Link (link/json/clash + info)| :heavy_check_mark: |
|
||||||
|
| Dark/Light Theme | :heavy_check_mark: |
|
||||||
|
| API Interface | :heavy_check_mark: |
|
||||||
|
|
||||||
|
## Supported Platforms
|
||||||
|
| Platform | Architecture | Status |
|
||||||
|
|----------|--------------|---------|
|
||||||
|
| Linux | amd64, arm64, armv7, armv6, armv5, 386, s390x | ✅ Supported |
|
||||||
|
| Windows | amd64, 386, arm64 | ✅ Supported |
|
||||||
|
| macOS | amd64, arm64 | 🚧 Experimental |
|
||||||
|
|
||||||
|
## Screenshots
|
||||||
|
|
||||||
|

|
||||||
|
|
||||||
|
[Other UI Screenshots](https://github.com/alireza0/s-ui-frontend/blob/main/screenshots.md)
|
||||||
|
|
||||||
|
## API Documentation
|
||||||
|
|
||||||
|
[API-Documentation Wiki](https://github.com/alireza0/s-ui/wiki/API-Documentation)
|
||||||
|
|
||||||
|
## Default Installation Information
|
||||||
|
- Panel Port: 2095
|
||||||
|
- Panel Path: /app/
|
||||||
|
- Subscription Port: 2096
|
||||||
|
- Subscription Path: /sub/
|
||||||
|
- User/Password: admin
|
||||||
|
|
||||||
|
## Install & Upgrade to Latest Version
|
||||||
|
|
||||||
|
### Linux/macOS
|
||||||
|
```sh
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh)
|
||||||
|
```
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
1. Download the latest Windows release from [GitHub Releases](https://github.com/alireza0/s-ui/releases/latest)
|
||||||
|
2. Extract the ZIP file
|
||||||
|
3. Run `install-windows.bat` as Administrator
|
||||||
|
4. Follow the installation wizard
|
||||||
|
|
||||||
|
## Install legacy Version
|
||||||
|
|
||||||
|
**Step 1:** To install your desired legacy version, add the version to the end of the installation command. e.g., ver `1.0.0`:
|
||||||
|
|
||||||
|
```sh
|
||||||
|
VERSION=1.0.0 && bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/$VERSION/install.sh) $VERSION
|
||||||
|
```
|
||||||
|
|
||||||
|
## Manual installation
|
||||||
|
|
||||||
|
### Linux/macOS
|
||||||
|
1. Get the latest version of S-UI based on your OS/Architecture from GitHub: [https://github.com/alireza0/s-ui/releases/latest](https://github.com/alireza0/s-ui/releases/latest)
|
||||||
|
2. **OPTIONAL** Get the latest version of `s-ui.sh` [https://raw.githubusercontent.com/alireza0/s-ui/master/s-ui.sh](https://raw.githubusercontent.com/alireza0/s-ui/master/s-ui.sh)
|
||||||
|
3. **OPTIONAL** Copy `s-ui.sh` to /usr/bin/ and run `chmod +x /usr/bin/s-ui`.
|
||||||
|
4. Extract s-ui tar.gz file to a directory of your choice and navigate to the directory where you extracted the tar.gz file.
|
||||||
|
5. Copy *.service files to /etc/systemd/system/ and run `systemctl daemon-reload`.
|
||||||
|
6. Enable autostart and start S-UI service using `systemctl enable s-ui --now`
|
||||||
|
7. Start sing-box service using `systemctl enable sing-box --now`
|
||||||
|
|
||||||
|
### Windows
|
||||||
|
1. Get the latest Windows version from GitHub: [https://github.com/alireza0/s-ui/releases/latest](https://github.com/alireza0/s-ui/releases/latest)
|
||||||
|
2. Download the appropriate Windows package (e.g., `s-ui-windows-amd64.zip`)
|
||||||
|
3. Extract the ZIP file to a directory of your choice
|
||||||
|
4. Run `install-windows.bat` as Administrator
|
||||||
|
5. Follow the installation wizard
|
||||||
|
6. Access the panel at http://localhost:2095/app
|
||||||
|
|
||||||
|
## Uninstall S-UI
|
||||||
|
|
||||||
|
```sh
|
||||||
|
sudo -i
|
||||||
|
|
||||||
|
systemctl disable s-ui --now
|
||||||
|
|
||||||
|
rm -f /etc/systemd/system/sing-box.service
|
||||||
|
systemctl daemon-reload
|
||||||
|
|
||||||
|
rm -fr /usr/local/s-ui
|
||||||
|
rm /usr/bin/s-ui
|
||||||
|
```
|
||||||
|
|
||||||
|
## Install using Docker
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click for details</summary>
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
**Step 1:** Install Docker
|
||||||
|
|
||||||
|
```shell
|
||||||
|
curl -fsSL https://get.docker.com | sh
|
||||||
|
```
|
||||||
|
|
||||||
|
**Step 2:** Install S-UI
|
||||||
|
|
||||||
|
> Docker compose method
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mkdir s-ui && cd s-ui
|
||||||
|
wget -q https://raw.githubusercontent.com/alireza0/s-ui/master/docker-compose.yml
|
||||||
|
docker compose up -d
|
||||||
|
```
|
||||||
|
|
||||||
|
> Use docker
|
||||||
|
|
||||||
|
```shell
|
||||||
|
mkdir s-ui && cd s-ui
|
||||||
|
docker run -itd \
|
||||||
|
-p 2095:2095 -p 2096:2096 -p 443:443 -p 80:80 \
|
||||||
|
-v $PWD/db/:/app/db/ \
|
||||||
|
-v $PWD/cert/:/root/cert/ \
|
||||||
|
--name s-ui --restart=unless-stopped \
|
||||||
|
alireza7/s-ui:latest
|
||||||
|
```
|
||||||
|
|
||||||
|
> Build your own image
|
||||||
|
|
||||||
|
```shell
|
||||||
|
git clone https://github.com/alireza0/s-ui
|
||||||
|
git submodule update --init --recursive
|
||||||
|
docker build -t s-ui .
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Manual run ( contribution )
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click for details</summary>
|
||||||
|
|
||||||
|
### Build and run whole project
|
||||||
|
```shell
|
||||||
|
./runSUI.sh
|
||||||
|
```
|
||||||
|
|
||||||
|
### Clone the repository
|
||||||
|
```shell
|
||||||
|
# clone repository
|
||||||
|
git clone https://github.com/alireza0/s-ui
|
||||||
|
# clone submodules
|
||||||
|
git submodule update --init --recursive
|
||||||
|
```
|
||||||
|
|
||||||
|
|
||||||
|
### - Frontend
|
||||||
|
|
||||||
|
Visit [s-ui-frontend](https://github.com/alireza0/s-ui-frontend) for frontend code
|
||||||
|
|
||||||
|
### - Backend
|
||||||
|
> Please build frontend once before!
|
||||||
|
|
||||||
|
To build backend:
|
||||||
|
```shell
|
||||||
|
# remove old frontend compiled files
|
||||||
|
rm -fr web/html/*
|
||||||
|
# apply new frontend compiled files
|
||||||
|
cp -R frontend/dist/ web/html/
|
||||||
|
# build
|
||||||
|
go build -o sui main.go
|
||||||
|
```
|
||||||
|
|
||||||
|
To run backend (from root folder of repository):
|
||||||
|
```shell
|
||||||
|
./sui
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Languages
|
||||||
|
|
||||||
|
- English
|
||||||
|
- Farsi
|
||||||
|
- Vietnamese
|
||||||
|
- Chinese (Simplified)
|
||||||
|
- Chinese (Traditional)
|
||||||
|
- Russian
|
||||||
|
|
||||||
|
## Features
|
||||||
|
|
||||||
|
- Supported protocols:
|
||||||
|
- General: Mixed, SOCKS, HTTP, HTTPS, Direct, Redirect, TProxy
|
||||||
|
- V2Ray based: VLESS, VMess, Trojan, Shadowsocks
|
||||||
|
- Other protocols: ShadowTLS, Hysteria, Hysteria2, Naive, TUIC
|
||||||
|
- Supports XTLS protocols
|
||||||
|
- An advanced interface for routing traffic, incorporating PROXY Protocol, External, and Transparent Proxy, SSL Certificate, and Port
|
||||||
|
- An advanced interface for inbound and outbound configuration
|
||||||
|
- Clients’ traffic cap and expiration date
|
||||||
|
- Displays online clients, inbounds and outbounds with traffic statistics, and system status monitoring
|
||||||
|
- Subscription service with ability to add external links and subscription
|
||||||
|
- HTTPS for secure access to the web panel and subscription service (self-provided domain + SSL certificate)
|
||||||
|
- Dark/Light theme
|
||||||
|
|
||||||
|
## Environment Variables
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click for details</summary>
|
||||||
|
|
||||||
|
### Usage
|
||||||
|
|
||||||
|
| Variable | Type | Default |
|
||||||
|
| -------------- | :--------------------------------------------: | :------------ |
|
||||||
|
| SUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
|
||||||
|
| SUI_DEBUG | `boolean` | `false` |
|
||||||
|
| SUI_BIN_FOLDER | `string` | `"bin"` |
|
||||||
|
| SUI_DB_FOLDER | `string` | `"db"` |
|
||||||
|
| SINGBOX_API | `string` | - |
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## SSL Certificate
|
||||||
|
|
||||||
|
<details>
|
||||||
|
<summary>Click for details</summary>
|
||||||
|
|
||||||
|
### Certbot
|
||||||
|
|
||||||
|
```bash
|
||||||
|
snap install core; snap refresh core
|
||||||
|
snap install --classic certbot
|
||||||
|
ln -s /snap/bin/certbot /usr/bin/certbot
|
||||||
|
|
||||||
|
certbot certonly --standalone --register-unsafely-without-email --non-interactive --agree-tos -d <Your Domain Name>
|
||||||
|
```
|
||||||
|
|
||||||
|
</details>
|
||||||
|
|
||||||
|
## Stargazers over Time
|
||||||
|
[](https://starchart.cc/alireza0/s-ui)
|
||||||
107
api/apiHandler.go
Normal file
107
api/apiHandler.go
Normal file
@@ -0,0 +1,107 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIHandler struct {
|
||||||
|
ApiService
|
||||||
|
apiv2 *APIv2Handler
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIHandler(g *gin.RouterGroup, a2 *APIv2Handler) {
|
||||||
|
a := &APIHandler{
|
||||||
|
apiv2: a2,
|
||||||
|
}
|
||||||
|
a.initRouter(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIHandler) initRouter(g *gin.RouterGroup) {
|
||||||
|
g.Use(func(c *gin.Context) {
|
||||||
|
path := c.Request.URL.Path
|
||||||
|
if !strings.HasSuffix(path, "login") && !strings.HasSuffix(path, "logout") {
|
||||||
|
checkLogin(c)
|
||||||
|
}
|
||||||
|
})
|
||||||
|
g.POST("/:postAction", a.postHandler)
|
||||||
|
g.GET("/:getAction", a.getHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIHandler) postHandler(c *gin.Context) {
|
||||||
|
loginUser := GetLoginUser(c)
|
||||||
|
action := c.Param("postAction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "login":
|
||||||
|
a.ApiService.Login(c)
|
||||||
|
case "changePass":
|
||||||
|
a.ApiService.ChangePass(c)
|
||||||
|
case "save":
|
||||||
|
a.ApiService.Save(c, loginUser)
|
||||||
|
case "restartApp":
|
||||||
|
a.ApiService.RestartApp(c)
|
||||||
|
case "restartSb":
|
||||||
|
a.ApiService.RestartSb(c)
|
||||||
|
case "linkConvert":
|
||||||
|
a.ApiService.LinkConvert(c)
|
||||||
|
case "subConvert":
|
||||||
|
a.ApiService.SubConvert(c)
|
||||||
|
case "importdb":
|
||||||
|
a.ApiService.ImportDb(c)
|
||||||
|
case "addToken":
|
||||||
|
a.ApiService.AddToken(c)
|
||||||
|
a.apiv2.ReloadTokens()
|
||||||
|
case "deleteToken":
|
||||||
|
a.ApiService.DeleteToken(c)
|
||||||
|
a.apiv2.ReloadTokens()
|
||||||
|
default:
|
||||||
|
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIHandler) getHandler(c *gin.Context) {
|
||||||
|
action := c.Param("getAction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "logout":
|
||||||
|
a.ApiService.Logout(c)
|
||||||
|
case "load":
|
||||||
|
a.ApiService.LoadData(c)
|
||||||
|
case "inbounds", "outbounds", "endpoints", "services", "tls", "clients", "config":
|
||||||
|
err := a.ApiService.LoadPartialData(c, []string{action})
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, action, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case "users":
|
||||||
|
a.ApiService.GetUsers(c)
|
||||||
|
case "settings":
|
||||||
|
a.ApiService.GetSettings(c)
|
||||||
|
case "stats":
|
||||||
|
a.ApiService.GetStats(c)
|
||||||
|
case "status":
|
||||||
|
a.ApiService.GetStatus(c)
|
||||||
|
case "onlines":
|
||||||
|
a.ApiService.GetOnlines(c)
|
||||||
|
case "logs":
|
||||||
|
a.ApiService.GetLogs(c)
|
||||||
|
case "changes":
|
||||||
|
a.ApiService.CheckChanges(c)
|
||||||
|
case "keypairs":
|
||||||
|
a.ApiService.GetKeypairs(c)
|
||||||
|
case "getdb":
|
||||||
|
a.ApiService.GetDb(c)
|
||||||
|
case "tokens":
|
||||||
|
a.ApiService.GetTokens(c)
|
||||||
|
case "singbox-config":
|
||||||
|
a.ApiService.GetSingboxConfig(c)
|
||||||
|
case "checkOutbound":
|
||||||
|
a.ApiService.GetCheckOutbound(c)
|
||||||
|
default:
|
||||||
|
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
|
||||||
|
}
|
||||||
|
}
|
||||||
405
api/apiService.go
Normal file
405
api/apiService.go
Normal file
@@ -0,0 +1,405 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
"github.com/alireza0/s-ui/util"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ApiService struct {
|
||||||
|
service.SettingService
|
||||||
|
service.UserService
|
||||||
|
service.ConfigService
|
||||||
|
service.ClientService
|
||||||
|
service.TlsService
|
||||||
|
service.InboundService
|
||||||
|
service.OutboundService
|
||||||
|
service.EndpointService
|
||||||
|
service.ServicesService
|
||||||
|
service.PanelService
|
||||||
|
service.StatsService
|
||||||
|
service.ServerService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) LoadData(c *gin.Context) {
|
||||||
|
data, err := a.getData(c)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, data, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) getData(c *gin.Context) (interface{}, error) {
|
||||||
|
data := make(map[string]interface{}, 0)
|
||||||
|
lu := c.Query("lu")
|
||||||
|
isUpdated, err := a.ConfigService.CheckChanges(lu)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
onlines, err := a.StatsService.GetOnlines()
|
||||||
|
|
||||||
|
sysInfo := a.ServerService.GetSingboxInfo()
|
||||||
|
if sysInfo["running"] == false {
|
||||||
|
logs := a.ServerService.GetLogs("1", "debug")
|
||||||
|
if len(logs) > 0 {
|
||||||
|
data["lastLog"] = logs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if isUpdated {
|
||||||
|
config, err := a.SettingService.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
clients, err := a.ClientService.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
tlsConfigs, err := a.TlsService.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
inbounds, err := a.InboundService.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
outbounds, err := a.OutboundService.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
endpoints, err := a.EndpointService.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
services, err := a.ServicesService.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
subURI, err := a.SettingService.GetFinalSubURI(getHostname(c))
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
trafficAge, err := a.SettingService.GetTrafficAge()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
data["config"] = json.RawMessage(config)
|
||||||
|
data["clients"] = clients
|
||||||
|
data["tls"] = tlsConfigs
|
||||||
|
data["inbounds"] = inbounds
|
||||||
|
data["outbounds"] = outbounds
|
||||||
|
data["endpoints"] = endpoints
|
||||||
|
data["services"] = services
|
||||||
|
data["subURI"] = subURI
|
||||||
|
data["enableTraffic"] = trafficAge > 0
|
||||||
|
data["onlines"] = onlines
|
||||||
|
} else {
|
||||||
|
data["onlines"] = onlines
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) LoadPartialData(c *gin.Context, objs []string) error {
|
||||||
|
data := make(map[string]interface{}, 0)
|
||||||
|
id := c.Query("id")
|
||||||
|
|
||||||
|
for _, obj := range objs {
|
||||||
|
switch obj {
|
||||||
|
case "inbounds":
|
||||||
|
inbounds, err := a.InboundService.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data[obj] = inbounds
|
||||||
|
case "outbounds":
|
||||||
|
outbounds, err := a.OutboundService.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data[obj] = outbounds
|
||||||
|
case "endpoints":
|
||||||
|
endpoints, err := a.EndpointService.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data[obj] = endpoints
|
||||||
|
case "services":
|
||||||
|
services, err := a.ServicesService.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data[obj] = services
|
||||||
|
case "tls":
|
||||||
|
tlsConfigs, err := a.TlsService.GetAll()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data[obj] = tlsConfigs
|
||||||
|
case "clients":
|
||||||
|
clients, err := a.ClientService.Get(id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data[obj] = clients
|
||||||
|
case "config":
|
||||||
|
config, err := a.SettingService.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data[obj] = json.RawMessage(config)
|
||||||
|
case "settings":
|
||||||
|
settings, err := a.SettingService.GetAllSetting()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data[obj] = settings
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonObj(c, data, nil)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) GetUsers(c *gin.Context) {
|
||||||
|
users, err := a.UserService.GetUsers()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, *users, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) GetSettings(c *gin.Context) {
|
||||||
|
data, err := a.SettingService.GetAllSetting()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) GetStats(c *gin.Context) {
|
||||||
|
resource := c.Query("resource")
|
||||||
|
tag := c.Query("tag")
|
||||||
|
limit, err := strconv.Atoi(c.Query("limit"))
|
||||||
|
if err != nil {
|
||||||
|
limit = 100
|
||||||
|
}
|
||||||
|
data, err := a.StatsService.GetStats(resource, tag, limit)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, data, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) GetStatus(c *gin.Context) {
|
||||||
|
request := c.Query("r")
|
||||||
|
result := a.ServerService.GetStatus(request)
|
||||||
|
jsonObj(c, result, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) GetOnlines(c *gin.Context) {
|
||||||
|
onlines, err := a.StatsService.GetOnlines()
|
||||||
|
jsonObj(c, onlines, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) GetLogs(c *gin.Context) {
|
||||||
|
count := c.Query("c")
|
||||||
|
level := c.Query("l")
|
||||||
|
logs := a.ServerService.GetLogs(count, level)
|
||||||
|
jsonObj(c, logs, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) CheckChanges(c *gin.Context) {
|
||||||
|
actor := c.Query("a")
|
||||||
|
chngKey := c.Query("k")
|
||||||
|
count := c.Query("c")
|
||||||
|
changes := a.ConfigService.GetChanges(actor, chngKey, count)
|
||||||
|
jsonObj(c, changes, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) GetKeypairs(c *gin.Context) {
|
||||||
|
kType := c.Query("k")
|
||||||
|
options := c.Query("o")
|
||||||
|
keypair := a.ServerService.GenKeypair(kType, options)
|
||||||
|
jsonObj(c, keypair, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) GetDb(c *gin.Context) {
|
||||||
|
exclude := c.Query("exclude")
|
||||||
|
db, err := database.GetDb(exclude)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Header("Content-Type", "application/octet-stream")
|
||||||
|
c.Header("Content-Disposition", "attachment; filename=s-ui_"+time.Now().Format("20060102-150405")+".db")
|
||||||
|
c.Writer.Write(db)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) postActions(c *gin.Context) (string, json.RawMessage, error) {
|
||||||
|
var data map[string]json.RawMessage
|
||||||
|
err := c.ShouldBind(&data)
|
||||||
|
if err != nil {
|
||||||
|
return "", nil, err
|
||||||
|
}
|
||||||
|
return string(data["action"]), data["data"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) Login(c *gin.Context) {
|
||||||
|
remoteIP := getRemoteIp(c)
|
||||||
|
loginUser, err := a.UserService.Login(c.Request.FormValue("user"), c.Request.FormValue("pass"), remoteIP)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
sessionMaxAge, err := a.SettingService.GetSessionMaxAge()
|
||||||
|
if err != nil {
|
||||||
|
logger.Infof("Unable to get session's max age from DB")
|
||||||
|
}
|
||||||
|
|
||||||
|
err = SetLoginUser(c, loginUser, sessionMaxAge)
|
||||||
|
if err == nil {
|
||||||
|
logger.Info("user ", loginUser, " login success")
|
||||||
|
} else {
|
||||||
|
logger.Warning("login failed: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonMsg(c, "", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) ChangePass(c *gin.Context) {
|
||||||
|
id := c.Request.FormValue("id")
|
||||||
|
oldPass := c.Request.FormValue("oldPass")
|
||||||
|
newUsername := c.Request.FormValue("newUsername")
|
||||||
|
newPass := c.Request.FormValue("newPass")
|
||||||
|
err := a.UserService.ChangePass(id, oldPass, newUsername, newPass)
|
||||||
|
if err == nil {
|
||||||
|
logger.Info("change user credentials success")
|
||||||
|
jsonMsg(c, "save", nil)
|
||||||
|
} else {
|
||||||
|
logger.Warning("change user credentials failed:", err)
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) Save(c *gin.Context, loginUser string) {
|
||||||
|
hostname := getHostname(c)
|
||||||
|
obj := c.Request.FormValue("object")
|
||||||
|
act := c.Request.FormValue("action")
|
||||||
|
data := c.Request.FormValue("data")
|
||||||
|
initUsers := c.Request.FormValue("initUsers")
|
||||||
|
objs, err := a.ConfigService.Save(obj, act, json.RawMessage(data), initUsers, loginUser, hostname)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "save", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = a.LoadPartialData(c, objs)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, obj, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) RestartApp(c *gin.Context) {
|
||||||
|
err := a.PanelService.RestartPanel(3)
|
||||||
|
jsonMsg(c, "restartApp", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) RestartSb(c *gin.Context) {
|
||||||
|
err := a.ConfigService.RestartCore()
|
||||||
|
jsonMsg(c, "restartSb", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) LinkConvert(c *gin.Context) {
|
||||||
|
link := c.Request.FormValue("link")
|
||||||
|
result, _, err := util.GetOutbound(link, 0)
|
||||||
|
jsonObj(c, result, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) SubConvert(c *gin.Context) {
|
||||||
|
link := c.Request.FormValue("link")
|
||||||
|
result, err := util.GetExternalSub(link)
|
||||||
|
jsonObj(c, result, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) ImportDb(c *gin.Context) {
|
||||||
|
file, _, err := c.Request.FormFile("db")
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
err = database.ImportDB(file)
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) Logout(c *gin.Context) {
|
||||||
|
loginUser := GetLoginUser(c)
|
||||||
|
if loginUser != "" {
|
||||||
|
logger.Infof("user %s logout", loginUser)
|
||||||
|
}
|
||||||
|
ClearSession(c)
|
||||||
|
jsonMsg(c, "", nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) LoadTokens() ([]byte, error) {
|
||||||
|
return a.UserService.LoadTokens()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) GetTokens(c *gin.Context) {
|
||||||
|
loginUser := GetLoginUser(c)
|
||||||
|
tokens, err := a.UserService.GetUserTokens(loginUser)
|
||||||
|
jsonObj(c, tokens, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) AddToken(c *gin.Context) {
|
||||||
|
loginUser := GetLoginUser(c)
|
||||||
|
expiry := c.Request.FormValue("expiry")
|
||||||
|
expiryInt, err := strconv.ParseInt(expiry, 10, 64)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
desc := c.Request.FormValue("desc")
|
||||||
|
token, err := a.UserService.AddToken(loginUser, expiryInt, desc)
|
||||||
|
jsonObj(c, token, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) DeleteToken(c *gin.Context) {
|
||||||
|
tokenId := c.Request.FormValue("id")
|
||||||
|
err := a.UserService.DeleteToken(tokenId)
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) GetSingboxConfig(c *gin.Context) {
|
||||||
|
rawConfig, err := a.ConfigService.GetConfig("")
|
||||||
|
if err != nil {
|
||||||
|
c.Status(400)
|
||||||
|
c.Writer.WriteString(err.Error())
|
||||||
|
return
|
||||||
|
}
|
||||||
|
c.Header("Content-Type", "application/json")
|
||||||
|
c.Header("Content-Disposition", "attachment; filename=config_"+time.Now().Format("20060102-150405")+".json")
|
||||||
|
c.Writer.Write(*rawConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *ApiService) GetCheckOutbound(c *gin.Context) {
|
||||||
|
tag := c.Query("tag")
|
||||||
|
link := c.Query("link")
|
||||||
|
result := a.ConfigService.CheckOutbound(tag, link)
|
||||||
|
jsonObj(c, result, nil)
|
||||||
|
}
|
||||||
134
api/apiV2Handler.go
Normal file
134
api/apiV2Handler.go
Normal file
@@ -0,0 +1,134 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TokenInMemory struct {
|
||||||
|
Token string
|
||||||
|
Expiry int64
|
||||||
|
Username string
|
||||||
|
}
|
||||||
|
|
||||||
|
type APIv2Handler struct {
|
||||||
|
ApiService
|
||||||
|
tokens *[]TokenInMemory
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIv2Handler(g *gin.RouterGroup) *APIv2Handler {
|
||||||
|
a := &APIv2Handler{}
|
||||||
|
a.ReloadTokens()
|
||||||
|
a.initRouter(g)
|
||||||
|
return a
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIv2Handler) initRouter(g *gin.RouterGroup) {
|
||||||
|
g.Use(func(c *gin.Context) {
|
||||||
|
a.checkToken(c)
|
||||||
|
})
|
||||||
|
g.POST("/:postAction", a.postHandler)
|
||||||
|
g.GET("/:getAction", a.getHandler)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIv2Handler) postHandler(c *gin.Context) {
|
||||||
|
username := a.findUsername(c)
|
||||||
|
action := c.Param("postAction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "save":
|
||||||
|
a.ApiService.Save(c, username)
|
||||||
|
case "restartApp":
|
||||||
|
a.ApiService.RestartApp(c)
|
||||||
|
case "restartSb":
|
||||||
|
a.ApiService.RestartSb(c)
|
||||||
|
case "linkConvert":
|
||||||
|
a.ApiService.LinkConvert(c)
|
||||||
|
case "subConvert":
|
||||||
|
a.ApiService.SubConvert(c)
|
||||||
|
case "importdb":
|
||||||
|
a.ApiService.ImportDb(c)
|
||||||
|
default:
|
||||||
|
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIv2Handler) getHandler(c *gin.Context) {
|
||||||
|
action := c.Param("getAction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "load":
|
||||||
|
a.ApiService.LoadData(c)
|
||||||
|
case "inbounds", "outbounds", "endpoints", "services", "tls", "clients", "config":
|
||||||
|
err := a.ApiService.LoadPartialData(c, []string{action})
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, action, err)
|
||||||
|
}
|
||||||
|
return
|
||||||
|
case "users":
|
||||||
|
a.ApiService.GetUsers(c)
|
||||||
|
case "settings":
|
||||||
|
a.ApiService.GetSettings(c)
|
||||||
|
case "stats":
|
||||||
|
a.ApiService.GetStats(c)
|
||||||
|
case "status":
|
||||||
|
a.ApiService.GetStatus(c)
|
||||||
|
case "onlines":
|
||||||
|
a.ApiService.GetOnlines(c)
|
||||||
|
case "logs":
|
||||||
|
a.ApiService.GetLogs(c)
|
||||||
|
case "changes":
|
||||||
|
a.ApiService.CheckChanges(c)
|
||||||
|
case "keypairs":
|
||||||
|
a.ApiService.GetKeypairs(c)
|
||||||
|
case "getdb":
|
||||||
|
a.ApiService.GetDb(c)
|
||||||
|
case "checkOutbound":
|
||||||
|
a.ApiService.GetCheckOutbound(c)
|
||||||
|
default:
|
||||||
|
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIv2Handler) findUsername(c *gin.Context) string {
|
||||||
|
token := c.Request.Header.Get("Token")
|
||||||
|
for index, t := range *a.tokens {
|
||||||
|
if t.Expiry > 0 && t.Expiry < time.Now().Unix() {
|
||||||
|
(*a.tokens) = append((*a.tokens)[:index], (*a.tokens)[index+1:]...)
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if t.Token == token {
|
||||||
|
return t.Username
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIv2Handler) checkToken(c *gin.Context) {
|
||||||
|
username := a.findUsername(c)
|
||||||
|
if username != "" {
|
||||||
|
c.Next()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonMsg(c, "", common.NewError("invalid token"))
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIv2Handler) ReloadTokens() {
|
||||||
|
tokens, err := a.ApiService.LoadTokens()
|
||||||
|
if err == nil {
|
||||||
|
var newTokens []TokenInMemory
|
||||||
|
err = json.Unmarshal(tokens, &newTokens)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("unable to load tokens: ", err)
|
||||||
|
}
|
||||||
|
a.tokens = &newTokens
|
||||||
|
} else {
|
||||||
|
logger.Error("unable to load tokens: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
69
api/session.go
Normal file
69
api/session.go
Normal file
@@ -0,0 +1,69 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/gob"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
|
||||||
|
"github.com/gin-contrib/sessions"
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
loginUser = "LOGIN_USER"
|
||||||
|
)
|
||||||
|
|
||||||
|
func init() {
|
||||||
|
gob.Register(model.User{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetLoginUser(c *gin.Context, userName string, maxAge int) error {
|
||||||
|
options := sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
Secure: false,
|
||||||
|
}
|
||||||
|
if maxAge > 0 {
|
||||||
|
options.MaxAge = maxAge * 60
|
||||||
|
}
|
||||||
|
|
||||||
|
s := sessions.Default(c)
|
||||||
|
s.Set(loginUser, userName)
|
||||||
|
s.Options(options)
|
||||||
|
|
||||||
|
return s.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func SetMaxAge(c *gin.Context) error {
|
||||||
|
s := sessions.Default(c)
|
||||||
|
s.Options(sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
})
|
||||||
|
return s.Save()
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLoginUser(c *gin.Context) string {
|
||||||
|
s := sessions.Default(c)
|
||||||
|
obj := s.Get(loginUser)
|
||||||
|
if obj == nil {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
objStr, ok := obj.(string)
|
||||||
|
if !ok {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
return objStr
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsLogin(c *gin.Context) bool {
|
||||||
|
return GetLoginUser(c) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func ClearSession(c *gin.Context) {
|
||||||
|
s := sessions.Default(c)
|
||||||
|
s.Clear()
|
||||||
|
s.Options(sessions.Options{
|
||||||
|
Path: "/",
|
||||||
|
MaxAge: -1,
|
||||||
|
})
|
||||||
|
s.Save()
|
||||||
|
}
|
||||||
92
api/utils.go
Normal file
92
api/utils.go
Normal file
@@ -0,0 +1,92 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Msg struct {
|
||||||
|
Success bool `json:"success"`
|
||||||
|
Msg string `json:"msg"`
|
||||||
|
Obj interface{} `json:"obj"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func getRemoteIp(c *gin.Context) string {
|
||||||
|
value := c.GetHeader("X-Forwarded-For")
|
||||||
|
if value != "" {
|
||||||
|
ips := strings.Split(value, ",")
|
||||||
|
return ips[0]
|
||||||
|
} else {
|
||||||
|
addr := c.Request.RemoteAddr
|
||||||
|
ip, _, _ := net.SplitHostPort(addr)
|
||||||
|
return ip
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getHostname(c *gin.Context) string {
|
||||||
|
host := c.Request.Host
|
||||||
|
if strings.Contains(host, ":") {
|
||||||
|
host, _, _ = net.SplitHostPort(c.Request.Host)
|
||||||
|
if strings.Contains(host, ":") {
|
||||||
|
host = "[" + host + "]"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return host
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonMsg(c *gin.Context, msg string, err error) {
|
||||||
|
jsonMsgObj(c, msg, nil, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonObj(c *gin.Context, obj interface{}, err error) {
|
||||||
|
jsonMsgObj(c, "", obj, err)
|
||||||
|
}
|
||||||
|
|
||||||
|
func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
|
||||||
|
m := Msg{
|
||||||
|
Obj: obj,
|
||||||
|
}
|
||||||
|
if err == nil {
|
||||||
|
m.Success = true
|
||||||
|
if msg != "" {
|
||||||
|
m.Msg = msg
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
m.Success = false
|
||||||
|
m.Msg = msg + ": " + err.Error()
|
||||||
|
logger.Warning("failed :", err)
|
||||||
|
}
|
||||||
|
c.JSON(http.StatusOK, m)
|
||||||
|
}
|
||||||
|
|
||||||
|
func pureJsonMsg(c *gin.Context, success bool, msg string) {
|
||||||
|
if success {
|
||||||
|
c.JSON(http.StatusOK, Msg{
|
||||||
|
Success: true,
|
||||||
|
Msg: msg,
|
||||||
|
})
|
||||||
|
} else {
|
||||||
|
c.JSON(http.StatusOK, Msg{
|
||||||
|
Success: false,
|
||||||
|
Msg: msg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func checkLogin(c *gin.Context) {
|
||||||
|
if !IsLogin(c) {
|
||||||
|
if c.GetHeader("X-Requested-With") == "XMLHttpRequest" {
|
||||||
|
pureJsonMsg(c, false, "Invalid login")
|
||||||
|
} else {
|
||||||
|
c.Redirect(http.StatusTemporaryRedirect, "/login")
|
||||||
|
}
|
||||||
|
c.Abort()
|
||||||
|
} else {
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
128
app/app.go
Normal file
128
app/app.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package app
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/config"
|
||||||
|
"github.com/alireza0/s-ui/core"
|
||||||
|
"github.com/alireza0/s-ui/cronjob"
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
"github.com/alireza0/s-ui/sub"
|
||||||
|
"github.com/alireza0/s-ui/web"
|
||||||
|
|
||||||
|
"github.com/op/go-logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APP struct {
|
||||||
|
service.SettingService
|
||||||
|
configService *service.ConfigService
|
||||||
|
webServer *web.Server
|
||||||
|
subServer *sub.Server
|
||||||
|
cronJob *cronjob.CronJob
|
||||||
|
logger *logging.Logger
|
||||||
|
core *core.Core
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewApp() *APP {
|
||||||
|
return &APP{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APP) Init() error {
|
||||||
|
log.Printf("%v %v", config.GetName(), config.GetVersion())
|
||||||
|
|
||||||
|
a.initLog()
|
||||||
|
|
||||||
|
err := database.InitDB(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Init Setting
|
||||||
|
a.SettingService.GetAllSetting()
|
||||||
|
|
||||||
|
a.core = core.NewCore()
|
||||||
|
|
||||||
|
a.cronJob = cronjob.NewCronJob()
|
||||||
|
a.webServer = web.NewServer()
|
||||||
|
a.subServer = sub.NewServer()
|
||||||
|
|
||||||
|
a.configService = service.NewConfigService(a.core)
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APP) Start() error {
|
||||||
|
loc, err := a.SettingService.GetTimeLocation()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
trafficAge, err := a.SettingService.GetTrafficAge()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.cronJob.Start(loc, trafficAge)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.webServer.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.subServer.Start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = a.configService.StartCore()
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APP) Stop() {
|
||||||
|
a.cronJob.Stop()
|
||||||
|
err := a.subServer.Stop()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("stop Sub Server err:", err)
|
||||||
|
}
|
||||||
|
err = a.webServer.Stop()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("stop Web Server err:", err)
|
||||||
|
}
|
||||||
|
err = a.configService.StopCore()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("stop Core err:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APP) initLog() {
|
||||||
|
switch config.GetLogLevel() {
|
||||||
|
case config.Debug:
|
||||||
|
logger.InitLogger(logging.DEBUG)
|
||||||
|
case config.Info:
|
||||||
|
logger.InitLogger(logging.INFO)
|
||||||
|
case config.Warn:
|
||||||
|
logger.InitLogger(logging.WARNING)
|
||||||
|
case config.Error:
|
||||||
|
logger.InitLogger(logging.ERROR)
|
||||||
|
default:
|
||||||
|
log.Fatal("unknown log level:", config.GetLogLevel())
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APP) RestartApp() {
|
||||||
|
a.Stop()
|
||||||
|
a.Start()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APP) GetCore() *core.Core {
|
||||||
|
return a.core
|
||||||
|
}
|
||||||
15
build.sh
Executable file
15
build.sh
Executable file
@@ -0,0 +1,15 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
cd frontend
|
||||||
|
npm i
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
cd ..
|
||||||
|
echo "Backend"
|
||||||
|
|
||||||
|
mkdir -p web/html
|
||||||
|
rm -fr web/html/*
|
||||||
|
cp -R frontend/dist/* web/html/
|
||||||
|
|
||||||
|
BUILD_TAGS="with_quic,with_grpc,with_utls,with_acme,with_gvisor,with_naive_outbound,with_musl,badlinkname,tfogo_checklinkname0,with_tailscale"
|
||||||
|
go build -ldflags '-w -s -checklinkname=0 -extldflags "-Wl,-no_warn_duplicate_libraries"' -tags "$BUILD_TAGS" -o sui main.go
|
||||||
64
cmd/admin.go
Normal file
64
cmd/admin.go
Normal file
@@ -0,0 +1,64 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/config"
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resetAdmin() {
|
||||||
|
err := database.InitDB(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
userService := service.UserService{}
|
||||||
|
err = userService.UpdateFirstUser("admin", "admin")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("reset admin credentials failed:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("reset admin credentials success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateAdmin(username string, password string) {
|
||||||
|
err := database.InitDB(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if username != "" || password != "" {
|
||||||
|
userService := service.UserService{}
|
||||||
|
err := userService.UpdateFirstUser(username, password)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("reset admin credentials failed:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("reset admin credentials success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showAdmin() {
|
||||||
|
err := database.InitDB(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
userService := service.UserService{}
|
||||||
|
userModel, err := userService.GetFirstUser()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("get current user info failed,error info:", err)
|
||||||
|
}
|
||||||
|
username := userModel.Username
|
||||||
|
userpasswd := userModel.Password
|
||||||
|
if (username == "") || (userpasswd == "") {
|
||||||
|
fmt.Println("current username or password is empty")
|
||||||
|
}
|
||||||
|
fmt.Println("First admin credentials:")
|
||||||
|
fmt.Println("\tUsername:\t", username)
|
||||||
|
fmt.Println("\tPassword:\t", userpasswd)
|
||||||
|
}
|
||||||
112
cmd/cmd.go
Normal file
112
cmd/cmd.go
Normal file
@@ -0,0 +1,112 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"flag"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"runtime/debug"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/cmd/migration"
|
||||||
|
"github.com/alireza0/s-ui/config"
|
||||||
|
)
|
||||||
|
|
||||||
|
func ParseCmd() {
|
||||||
|
var showVersion bool
|
||||||
|
flag.BoolVar(&showVersion, "v", false, "show version")
|
||||||
|
|
||||||
|
adminCmd := flag.NewFlagSet("admin", flag.ExitOnError)
|
||||||
|
settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
|
||||||
|
|
||||||
|
var username string
|
||||||
|
var password string
|
||||||
|
var port int
|
||||||
|
var path string
|
||||||
|
var subPort int
|
||||||
|
var subPath string
|
||||||
|
var reset bool
|
||||||
|
var show bool
|
||||||
|
settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
|
||||||
|
settingCmd.BoolVar(&show, "show", false, "show current settings")
|
||||||
|
settingCmd.IntVar(&port, "port", 0, "set panel port")
|
||||||
|
settingCmd.StringVar(&path, "path", "", "set panel path")
|
||||||
|
settingCmd.IntVar(&subPort, "subPort", 0, "set sub port")
|
||||||
|
settingCmd.StringVar(&subPath, "subPath", "", "set sub path")
|
||||||
|
|
||||||
|
adminCmd.BoolVar(&show, "show", false, "show first admin credentials")
|
||||||
|
adminCmd.BoolVar(&reset, "reset", false, "reset first admin credentials")
|
||||||
|
adminCmd.StringVar(&username, "username", "", "set login username")
|
||||||
|
adminCmd.StringVar(&password, "password", "", "set login password")
|
||||||
|
|
||||||
|
oldUsage := flag.Usage
|
||||||
|
flag.Usage = func() {
|
||||||
|
oldUsage()
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Commands:")
|
||||||
|
fmt.Println(" admin set/reset/show first admin credentials")
|
||||||
|
fmt.Println(" uri Show panel URI")
|
||||||
|
fmt.Println(" migrate migrate form older version")
|
||||||
|
fmt.Println(" setting set/reset/show settings")
|
||||||
|
fmt.Println()
|
||||||
|
adminCmd.Usage()
|
||||||
|
fmt.Println()
|
||||||
|
settingCmd.Usage()
|
||||||
|
}
|
||||||
|
|
||||||
|
flag.Parse()
|
||||||
|
if showVersion {
|
||||||
|
fmt.Println("S-UI Panel\t", config.GetVersion())
|
||||||
|
info, ok := debug.ReadBuildInfo()
|
||||||
|
if ok {
|
||||||
|
for _, dep := range info.Deps {
|
||||||
|
if dep.Path == "github.com/sagernet/sing-box" {
|
||||||
|
fmt.Println("Sing-Box\t", dep.Version)
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
switch os.Args[1] {
|
||||||
|
case "admin":
|
||||||
|
err := adminCmd.Parse(os.Args[2:])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case show:
|
||||||
|
showAdmin()
|
||||||
|
case reset:
|
||||||
|
resetAdmin()
|
||||||
|
default:
|
||||||
|
updateAdmin(username, password)
|
||||||
|
showAdmin()
|
||||||
|
}
|
||||||
|
|
||||||
|
case "uri":
|
||||||
|
getPanelURI()
|
||||||
|
|
||||||
|
case "migrate":
|
||||||
|
migration.MigrateDb()
|
||||||
|
|
||||||
|
case "setting":
|
||||||
|
err := settingCmd.Parse(os.Args[2:])
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
switch {
|
||||||
|
case show:
|
||||||
|
showSetting()
|
||||||
|
case reset:
|
||||||
|
resetSetting()
|
||||||
|
default:
|
||||||
|
updateSetting(port, path, subPort, subPath)
|
||||||
|
showSetting()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
fmt.Println("Invalid subcommands")
|
||||||
|
flag.Usage()
|
||||||
|
}
|
||||||
|
}
|
||||||
88
cmd/migration/1_1.go
Normal file
88
cmd/migration/1_1.go
Normal file
@@ -0,0 +1,88 @@
|
|||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func migrateClientSchema(db *gorm.DB) error {
|
||||||
|
rows, err := db.Raw("PRAGMA table_info(clients)").Rows()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer rows.Close()
|
||||||
|
|
||||||
|
for rows.Next() {
|
||||||
|
var (
|
||||||
|
cid int
|
||||||
|
cname string
|
||||||
|
ctype string
|
||||||
|
notnull int
|
||||||
|
dfltValue interface{}
|
||||||
|
pk int
|
||||||
|
)
|
||||||
|
|
||||||
|
rows.Scan(&cid, &cname, &ctype, ¬null, &dfltValue, &pk)
|
||||||
|
if cname == "config" || cname == "inbounds" || cname == "links" {
|
||||||
|
if ctype == "text" {
|
||||||
|
fmt.Printf("Column %s has type TEXT\n", cname)
|
||||||
|
oldData := make([]struct {
|
||||||
|
Id uint
|
||||||
|
Data string
|
||||||
|
}, 0)
|
||||||
|
db.Model(model.Client{}).Select("id", cname+" as data").Scan(&oldData)
|
||||||
|
for _, data := range oldData {
|
||||||
|
var newData []byte
|
||||||
|
switch cname {
|
||||||
|
case "inbounds":
|
||||||
|
inbounds := strings.Split(data.Data, ",")
|
||||||
|
newData, _ = json.MarshalIndent(inbounds, "", " ")
|
||||||
|
case "config":
|
||||||
|
jsonData := map[string]interface{}{}
|
||||||
|
json.Unmarshal([]byte(data.Data), &jsonData)
|
||||||
|
newData, _ = json.MarshalIndent(jsonData, "", " ")
|
||||||
|
case "links":
|
||||||
|
jsonData := make([]interface{}, 0)
|
||||||
|
json.Unmarshal([]byte(data.Data), &jsonData)
|
||||||
|
newData, _ = json.MarshalIndent(jsonData, "", " ")
|
||||||
|
}
|
||||||
|
err = db.Model(model.Client{}).Where("id = ?", data.Id).UpdateColumn(cname, newData).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func deleteOldWebSecret(db *gorm.DB) error {
|
||||||
|
return db.Exec("DELETE FROM settings WHERE key = ?", "webSecret").Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func changesObj(db *gorm.DB) error {
|
||||||
|
return db.Exec("UPDATE changes SET obj = CAST('\"' || CAST(obj AS TEXT) || '\"' AS BLOB) WHERE actor = ? and obj not like ?", "DepleteJob", "\"%\"").Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func to1_1(db *gorm.DB) error {
|
||||||
|
err := migrateClientSchema(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = deleteOldWebSecret(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = changesObj(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
317
cmd/migration/1_2.go
Normal file
317
cmd/migration/1_2.go
Normal file
@@ -0,0 +1,317 @@
|
|||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"errors"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InboundData struct {
|
||||||
|
Id uint
|
||||||
|
Tag string
|
||||||
|
Addrs json.RawMessage
|
||||||
|
OutJson json.RawMessage
|
||||||
|
}
|
||||||
|
|
||||||
|
func moveJsonToDb(db *gorm.DB) error {
|
||||||
|
binFolderPath := os.Getenv("SUI_BIN_FOLDER")
|
||||||
|
if binFolderPath == "" {
|
||||||
|
binFolderPath = "bin"
|
||||||
|
}
|
||||||
|
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
configPath := dir + "/" + binFolderPath + "/config.json"
|
||||||
|
if _, err := os.Stat(configPath); errors.Is(err, os.ErrNotExist) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := os.ReadFile(configPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var oldConfig map[string]interface{}
|
||||||
|
err = json.Unmarshal(data, &oldConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
oldInbounds := oldConfig["inbounds"].([]interface{})
|
||||||
|
db.Migrator().DropTable(&model.Inbound{})
|
||||||
|
db.AutoMigrate(&model.Inbound{})
|
||||||
|
for _, inbound := range oldInbounds {
|
||||||
|
inbObj, _ := inbound.(map[string]interface{})
|
||||||
|
tag, _ := inbObj["tag"].(string)
|
||||||
|
if tlsObj, ok := inbObj["tls"]; ok {
|
||||||
|
var tls_id uint
|
||||||
|
err = db.Raw("SELECT id FROM tls WHERE inbounds like ?", `%"`+tag+`"%`).Find(&tls_id).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Bind or Create tls_id
|
||||||
|
if tls_id > 0 {
|
||||||
|
inbObj["tls_id"] = tls_id
|
||||||
|
} else {
|
||||||
|
tls_server, _ := json.MarshalIndent(tlsObj, "", " ")
|
||||||
|
if len(tls_server) > 5 {
|
||||||
|
newTls := &model.Tls{
|
||||||
|
Name: tag,
|
||||||
|
Server: tls_server,
|
||||||
|
Client: json.RawMessage("{}"),
|
||||||
|
}
|
||||||
|
err = db.Create(newTls).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inbObj["tls_id"] = newTls.Id
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var inbData InboundData
|
||||||
|
db.Raw("select id,addrs,out_json from inbound_data where tag = ?", tag).Find(&inbData)
|
||||||
|
if inbData.Id > 0 {
|
||||||
|
inbObj["out_json"] = inbData.OutJson
|
||||||
|
var addrs []map[string]interface{}
|
||||||
|
json.Unmarshal(inbData.Addrs, &addrs)
|
||||||
|
for index, addr := range addrs {
|
||||||
|
if tlsEnable, ok := addr["tls"].(bool); ok {
|
||||||
|
newTls := map[string]interface{}{
|
||||||
|
"enabled": tlsEnable,
|
||||||
|
}
|
||||||
|
if insecure, ok := addr["insecure"].(bool); ok {
|
||||||
|
newTls["insecure"] = insecure
|
||||||
|
delete(addrs[index], "insecure")
|
||||||
|
}
|
||||||
|
if sni, ok := addr["server_name"].(string); ok {
|
||||||
|
newTls["server_name"] = sni
|
||||||
|
delete(addrs[index], "server_name")
|
||||||
|
}
|
||||||
|
addrs[index]["tls"] = newTls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
inbObj["addrs"] = addrs
|
||||||
|
} else {
|
||||||
|
inbObj["out_json"] = json.RawMessage("{}")
|
||||||
|
inbObj["addrs"] = json.RawMessage("[]")
|
||||||
|
}
|
||||||
|
// Delete deprecated fields
|
||||||
|
delete(inbObj, "sniff")
|
||||||
|
delete(inbObj, "sniff_override_destination")
|
||||||
|
delete(inbObj, "sniff_timeout")
|
||||||
|
delete(inbObj, "domain_strategy")
|
||||||
|
inbJson, _ := json.Marshal(inbObj)
|
||||||
|
|
||||||
|
var newInbound model.Inbound
|
||||||
|
err = newInbound.UnmarshalJSON(inbJson)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.Create(&newInbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(oldConfig, "inbounds")
|
||||||
|
|
||||||
|
blockOutboundTags := []string{}
|
||||||
|
dnsOutboundTags := []string{}
|
||||||
|
|
||||||
|
oldOutbounds := oldConfig["outbounds"].([]interface{})
|
||||||
|
db.Migrator().DropTable(&model.Outbound{}, &model.Endpoint{})
|
||||||
|
db.AutoMigrate(&model.Outbound{}, &model.Endpoint{})
|
||||||
|
for _, outbound := range oldOutbounds {
|
||||||
|
outType, _ := outbound.(map[string]interface{})["type"].(string)
|
||||||
|
outboundRaw, _ := json.MarshalIndent(outbound, "", " ")
|
||||||
|
if outType == "wireguard" { // Check if it is Entrypoint
|
||||||
|
var newEntrypoint model.Endpoint
|
||||||
|
err = newEntrypoint.UnmarshalJSON(outboundRaw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = db.Create(&newEntrypoint).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else { // It is Outbound
|
||||||
|
var newOutbound model.Outbound
|
||||||
|
err = newOutbound.UnmarshalJSON(outboundRaw)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Delete deprecated fields
|
||||||
|
if newOutbound.Type == "direct" {
|
||||||
|
var options map[string]interface{}
|
||||||
|
json.Unmarshal(newOutbound.Options, &options)
|
||||||
|
delete(options, "override_address")
|
||||||
|
delete(options, "override_port")
|
||||||
|
newOutbound.Options, _ = json.Marshal(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
switch newOutbound.Type {
|
||||||
|
case "dns":
|
||||||
|
dnsOutboundTags = append(dnsOutboundTags, newOutbound.Tag)
|
||||||
|
case "block":
|
||||||
|
blockOutboundTags = append(blockOutboundTags, newOutbound.Tag)
|
||||||
|
default:
|
||||||
|
err = db.Create(&newOutbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(oldConfig, "outbounds")
|
||||||
|
|
||||||
|
// Check routing rules
|
||||||
|
if routingRules, ok := oldConfig["route"].(map[string]interface{}); ok {
|
||||||
|
if rules, hasRules := routingRules["rules"].([]interface{}); hasRules {
|
||||||
|
hasDns := false
|
||||||
|
for index, rule := range rules {
|
||||||
|
ruleObj, _ := rule.(map[string]interface{})
|
||||||
|
isBlock := false
|
||||||
|
isDns := false
|
||||||
|
outboundTag, _ := ruleObj["outbound"].(string)
|
||||||
|
for _, tag := range blockOutboundTags {
|
||||||
|
if tag == outboundTag {
|
||||||
|
isBlock = true
|
||||||
|
delete(ruleObj, "outbound")
|
||||||
|
ruleObj["action"] = "reject"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for _, tag := range dnsOutboundTags {
|
||||||
|
if tag == outboundTag {
|
||||||
|
isDns = true
|
||||||
|
hasDns = true
|
||||||
|
delete(ruleObj, "outbound")
|
||||||
|
ruleObj["action"] = "hijack-dns"
|
||||||
|
break
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !isBlock && !isDns {
|
||||||
|
ruleObj["action"] = "route"
|
||||||
|
}
|
||||||
|
rules[index] = ruleObj
|
||||||
|
}
|
||||||
|
if hasDns {
|
||||||
|
rules = append(rules, map[string]interface{}{"action": "sniff"})
|
||||||
|
}
|
||||||
|
routingRules["rules"] = rules
|
||||||
|
}
|
||||||
|
oldConfig["route"] = routingRules
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove v2rayapi and clashapi from experimental config
|
||||||
|
experimental := oldConfig["experimental"].(map[string]interface{})
|
||||||
|
delete(experimental, "v2ray_api")
|
||||||
|
delete(experimental, "clash_api")
|
||||||
|
oldConfig["experimental"] = experimental
|
||||||
|
|
||||||
|
// Save the other configs
|
||||||
|
var otherConfigs json.RawMessage
|
||||||
|
otherConfigs, err = json.MarshalIndent(oldConfig, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Save(&model.Setting{
|
||||||
|
Key: "config",
|
||||||
|
Value: string(otherConfigs),
|
||||||
|
}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateTls(db *gorm.DB) error {
|
||||||
|
if !db.Migrator().HasColumn(&model.Tls{}, "inbounds") {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := db.Migrator().DropColumn(&model.Tls{}, "inbounds")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var tlsConfig []model.Tls
|
||||||
|
err = db.Model(model.Tls{}).Scan(&tlsConfig).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, tls := range tlsConfig {
|
||||||
|
var tlsClient map[string]interface{}
|
||||||
|
err = json.Unmarshal(tls.Client, &tlsClient)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
for key := range tlsClient {
|
||||||
|
switch key {
|
||||||
|
case "insecure", "disable_sni", "utls", "ech", "reality":
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
delete(tlsClient, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tlsConfig[index].Client, _ = json.MarshalIndent(tlsClient, "", " ")
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Save(&tlsConfig).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func dropInboundData(db *gorm.DB) error {
|
||||||
|
if !db.Migrator().HasTable(&InboundData{}) {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
return db.Migrator().DropTable(&InboundData{})
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateClients(db *gorm.DB) error {
|
||||||
|
var oldClients []model.Client
|
||||||
|
err := db.Model(model.Client{}).Scan(&oldClients).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
for index, oldClient := range oldClients {
|
||||||
|
var old_inbounds []string
|
||||||
|
err = json.Unmarshal(oldClient.Inbounds, &old_inbounds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var inbound_ids []uint
|
||||||
|
err = db.Raw("SELECT id FROM inbounds WHERE tag in ?", old_inbounds).Find(&inbound_ids).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
oldClients[index].Inbounds, _ = json.Marshal(inbound_ids)
|
||||||
|
}
|
||||||
|
return db.Save(oldClients).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func migrateChanges(db *gorm.DB) error {
|
||||||
|
return db.Migrator().DropColumn(&model.Changes{}, "index")
|
||||||
|
}
|
||||||
|
|
||||||
|
func to1_2(db *gorm.DB) error {
|
||||||
|
err := moveJsonToDb(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = migrateTls(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = dropInboundData(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = migrateClients(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return migrateChanges(db)
|
||||||
|
}
|
||||||
155
cmd/migration/1_3.go
Normal file
155
cmd/migration/1_3.go
Normal file
@@ -0,0 +1,155 @@
|
|||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func migrate_dns(db *gorm.DB) error {
|
||||||
|
var configStr string
|
||||||
|
err := db.Model(model.Setting{}).Select("value").Where("key = ?", "config").First(&configStr).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if configStr == "" {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var config map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(configStr), &config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if dnsConfig, ok := config["dns"].(map[string]interface{}); ok {
|
||||||
|
if dnsServers, ok := dnsConfig["servers"].([]interface{}); ok {
|
||||||
|
for index, dnsServer := range dnsServers {
|
||||||
|
if dnsServer, ok := dnsServer.(map[string]interface{}); ok {
|
||||||
|
if addr, ok := dnsServer["address"].(string); ok && addr != "" {
|
||||||
|
switch addr {
|
||||||
|
case "local":
|
||||||
|
delete(dnsServer, "address")
|
||||||
|
dnsServer["type"] = "local"
|
||||||
|
case "fakeip":
|
||||||
|
delete(dnsServer, "address")
|
||||||
|
dnsServer["type"] = "fakeip"
|
||||||
|
default:
|
||||||
|
addrParsed, err := url.Parse(addr)
|
||||||
|
if err != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
switch addrParsed.Scheme {
|
||||||
|
case "":
|
||||||
|
dnsServer["type"] = "udp"
|
||||||
|
dnsServer["server"] = addr
|
||||||
|
case "udp", "tcp", "tls", "quic", "https", "h3":
|
||||||
|
dnsServer["type"] = addrParsed.Scheme
|
||||||
|
dnsServer["server"] = addrParsed.Host
|
||||||
|
case "dhcp":
|
||||||
|
dnsServer["type"] = addrParsed.Scheme
|
||||||
|
if addrParsed.Host != "auto" && addrParsed.Host != "" {
|
||||||
|
dnsServer["interface"] = addrParsed.Host
|
||||||
|
}
|
||||||
|
case "rcode":
|
||||||
|
dnsServer["type"] = "predefined"
|
||||||
|
dnsServer["responses"] = []map[string]string{
|
||||||
|
{
|
||||||
|
"rcode": strings.ToUpper(addrParsed.Host),
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
delete(dnsServer, "address")
|
||||||
|
if addrParsed.Port() != "" {
|
||||||
|
port, err := strconv.Atoi(addrParsed.Port())
|
||||||
|
if err == nil {
|
||||||
|
dnsServer["server_port"] = port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if address_resolver, ok := dnsServer["address_resolver"].(string); ok && address_resolver != "" {
|
||||||
|
delete(dnsServer, "address_resolver")
|
||||||
|
dnsServer["domain_resolver"] = address_resolver
|
||||||
|
}
|
||||||
|
delete(dnsServer, "strategy")
|
||||||
|
}
|
||||||
|
dnsServers[index] = dnsServer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
dnsConfig["servers"] = dnsServers
|
||||||
|
}
|
||||||
|
config["dns"] = dnsConfig
|
||||||
|
} else {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// save changes
|
||||||
|
configs, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return db.Model(model.Setting{}).Where("key = ?", "config").Update("value", string(configs)).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func remove_outbound_strategy(db *gorm.DB) error {
|
||||||
|
var outbounds []model.Outbound
|
||||||
|
err := db.Find(&outbounds).Where("json_extract(options, '$.domain_strategy') IS NOT NULL").Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, outbound := range outbounds {
|
||||||
|
var restFields map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(outbound.Options, &restFields); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
delete(restFields, "domain_strategy")
|
||||||
|
outbound.Options, _ = json.MarshalIndent(restFields, "", " ")
|
||||||
|
db.Save(&outbound)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func anytls_user_config(db *gorm.DB) error {
|
||||||
|
var clients []model.Client
|
||||||
|
err := db.Model(model.Client{}).Find(&clients).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for index, client := range clients {
|
||||||
|
var configs map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(client.Config, &configs); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if configs["anytls"] != nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
configs["anytls"] = configs["trojan"]
|
||||||
|
configJson, err := json.MarshalIndent(configs, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
clients[index].Config = configJson
|
||||||
|
db.Save(&clients[index])
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func to1_3(db *gorm.DB) error {
|
||||||
|
err := anytls_user_config(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = migrate_dns(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = remove_outbound_strategy(db)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
84
cmd/migration/main.go
Normal file
84
cmd/migration/main.go
Normal file
@@ -0,0 +1,84 @@
|
|||||||
|
package migration
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/config"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func MigrateDb() {
|
||||||
|
// void running on first install
|
||||||
|
path := config.GetDBPath()
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
if err != nil {
|
||||||
|
println("Database not found")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
db, err := gorm.Open(sqlite.Open(path))
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if sqlDB, e := db.DB(); e == nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
tx := db.Begin()
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
tx.Commit()
|
||||||
|
} else {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
currentVersion := config.GetVersion()
|
||||||
|
dbVersion := ""
|
||||||
|
tx.Raw("SELECT value FROM settings WHERE key = ?", "version").Find(&dbVersion)
|
||||||
|
fmt.Println("Current version:", currentVersion, "\nDatabase version:", dbVersion)
|
||||||
|
|
||||||
|
if currentVersion == dbVersion {
|
||||||
|
fmt.Println("Database is up to date, no need to migrate")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
fmt.Println("Start migrating database...")
|
||||||
|
|
||||||
|
// Before 1.2
|
||||||
|
if dbVersion == "" {
|
||||||
|
err = to1_1(tx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Migration to 1.1 failed: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = to1_2(tx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Migration to 1.2 failed: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
dbVersion = "1.2"
|
||||||
|
}
|
||||||
|
|
||||||
|
// Before 1.3
|
||||||
|
if dbVersion[0:3] == "1.2" {
|
||||||
|
err = to1_3(tx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Migration to 1.3 failed: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Set version
|
||||||
|
err = tx.Exec("UPDATE settings SET value = ? WHERE key = ?", currentVersion, "version").Error
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal("Update version failed: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Migration done!")
|
||||||
|
}
|
||||||
217
cmd/setting.go
Normal file
217
cmd/setting.go
Normal file
@@ -0,0 +1,217 @@
|
|||||||
|
package cmd
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/config"
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/v4/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
func resetSetting() {
|
||||||
|
err := database.InitDB(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settingService := service.SettingService{}
|
||||||
|
err = settingService.ResetSettings()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("reset setting failed:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("reset setting success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func updateSetting(port int, path string, subPort int, subPath string) {
|
||||||
|
err := database.InitDB(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
settingService := service.SettingService{}
|
||||||
|
|
||||||
|
if port > 0 {
|
||||||
|
err := settingService.SetPort(port)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("set port failed:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("set port success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if path != "" {
|
||||||
|
err := settingService.SetWebPath(path)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("set path failed:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("set path success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if subPort > 0 {
|
||||||
|
err := settingService.SetSubPort(subPort)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("set sub port failed:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("set sub port success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if subPath != "" {
|
||||||
|
err := settingService.SetSubPath(subPath)
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("set sub path failed:", err)
|
||||||
|
} else {
|
||||||
|
fmt.Println("set sub path success")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func showSetting() {
|
||||||
|
err := database.InitDB(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settingService := service.SettingService{}
|
||||||
|
allSetting, err := settingService.GetAllSetting()
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("get current port failed,error info:", err)
|
||||||
|
}
|
||||||
|
fmt.Println("Current panel settings:")
|
||||||
|
fmt.Println("\tPanel port:\t", (*allSetting)["webPort"])
|
||||||
|
fmt.Println("\tPanel path:\t", (*allSetting)["webPath"])
|
||||||
|
if (*allSetting)["webListen"] != "" {
|
||||||
|
fmt.Println("\tPanel IP:\t", (*allSetting)["webListen"])
|
||||||
|
}
|
||||||
|
if (*allSetting)["webDomain"] != "" {
|
||||||
|
fmt.Println("\tPanel Domain:\t", (*allSetting)["webDomain"])
|
||||||
|
}
|
||||||
|
if (*allSetting)["webURI"] != "" {
|
||||||
|
fmt.Println("\tPanel URI:\t", (*allSetting)["webURI"])
|
||||||
|
}
|
||||||
|
fmt.Println()
|
||||||
|
fmt.Println("Current subscription settings:")
|
||||||
|
fmt.Println("\tSub port:\t", (*allSetting)["subPort"])
|
||||||
|
fmt.Println("\tSub path:\t", (*allSetting)["subPath"])
|
||||||
|
if (*allSetting)["subListen"] != "" {
|
||||||
|
fmt.Println("\tSub IP:\t", (*allSetting)["subListen"])
|
||||||
|
}
|
||||||
|
if (*allSetting)["subDomain"] != "" {
|
||||||
|
fmt.Println("\tSub Domain:\t", (*allSetting)["subDomain"])
|
||||||
|
}
|
||||||
|
if (*allSetting)["subURI"] != "" {
|
||||||
|
fmt.Println("\tSub URI:\t", (*allSetting)["subURI"])
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPublicIP() string {
|
||||||
|
apis := []string{
|
||||||
|
"https://api64.ipify.org",
|
||||||
|
"https://ip.sb",
|
||||||
|
"https://icanhazip.com",
|
||||||
|
"https://ipinfo.io/ip",
|
||||||
|
"https://checkip.amazonaws.com",
|
||||||
|
}
|
||||||
|
type result struct {
|
||||||
|
ip string
|
||||||
|
err error
|
||||||
|
}
|
||||||
|
ch := make(chan result, len(apis))
|
||||||
|
var wg sync.WaitGroup
|
||||||
|
client := &http.Client{Timeout: 3 * time.Second}
|
||||||
|
|
||||||
|
for _, api := range apis {
|
||||||
|
wg.Add(1)
|
||||||
|
go func(url string) {
|
||||||
|
defer wg.Done()
|
||||||
|
resp, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
ch <- result{"", err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
body, err := io.ReadAll(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
ch <- result{"", err}
|
||||||
|
return
|
||||||
|
}
|
||||||
|
ch <- result{string(body), nil}
|
||||||
|
}(api)
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
wg.Wait()
|
||||||
|
close(ch)
|
||||||
|
}()
|
||||||
|
|
||||||
|
for res := range ch {
|
||||||
|
if res.err == nil && res.ip != "" {
|
||||||
|
return strings.TrimSpace(res.ip)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func getPanelURI() {
|
||||||
|
err := database.InitDB(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println(err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
settingService := service.SettingService{}
|
||||||
|
Port, _ := settingService.GetPort()
|
||||||
|
BasePath, _ := settingService.GetWebPath()
|
||||||
|
Listen, _ := settingService.GetListen()
|
||||||
|
Domain, _ := settingService.GetWebDomain()
|
||||||
|
KeyFile, _ := settingService.GetKeyFile()
|
||||||
|
CertFile, _ := settingService.GetCertFile()
|
||||||
|
TLS := false
|
||||||
|
if KeyFile != "" && CertFile != "" {
|
||||||
|
TLS = true
|
||||||
|
}
|
||||||
|
Proto := ""
|
||||||
|
if TLS {
|
||||||
|
Proto = "https://"
|
||||||
|
} else {
|
||||||
|
Proto = "http://"
|
||||||
|
}
|
||||||
|
PortText := fmt.Sprintf(":%d", Port)
|
||||||
|
if (Port == 443 && TLS) || (Port == 80 && !TLS) {
|
||||||
|
PortText = ""
|
||||||
|
}
|
||||||
|
if len(Domain) > 0 {
|
||||||
|
fmt.Println(Proto + Domain + PortText + BasePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(Listen) > 0 {
|
||||||
|
fmt.Println(Proto + Listen + PortText + BasePath)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
fmt.Println("Local address:")
|
||||||
|
netInterfaces, _ := net.Interfaces()
|
||||||
|
for i := 0; i < len(netInterfaces); i++ {
|
||||||
|
if len(netInterfaces[i].Flags) > 2 && netInterfaces[i].Flags[0] == "up" && netInterfaces[i].Flags[1] != "loopback" {
|
||||||
|
addrs := netInterfaces[i].Addrs
|
||||||
|
for _, address := range addrs {
|
||||||
|
IP := strings.Split(address.Addr, "/")[0]
|
||||||
|
if strings.Contains(address.Addr, ".") {
|
||||||
|
fmt.Println(Proto + IP + PortText + BasePath)
|
||||||
|
} else if address.Addr[0:6] != "fe80::" {
|
||||||
|
fmt.Println(Proto + "[" + IP + "]" + PortText + BasePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
pubIP := getPublicIP()
|
||||||
|
if pubIP != "" {
|
||||||
|
fmt.Printf("\nGlobal address:\n%s%s%s\n", Proto, pubIP, PortText+BasePath)
|
||||||
|
}
|
||||||
|
}
|
||||||
68
config/config.go
Normal file
68
config/config.go
Normal file
@@ -0,0 +1,68 @@
|
|||||||
|
package config
|
||||||
|
|
||||||
|
import (
|
||||||
|
_ "embed"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
//go:embed version
|
||||||
|
var version string
|
||||||
|
|
||||||
|
//go:embed name
|
||||||
|
var name string
|
||||||
|
|
||||||
|
type LogLevel string
|
||||||
|
|
||||||
|
const (
|
||||||
|
Debug LogLevel = "debug"
|
||||||
|
Info LogLevel = "info"
|
||||||
|
Warn LogLevel = "warn"
|
||||||
|
Error LogLevel = "error"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetVersion() string {
|
||||||
|
return strings.TrimSpace(version)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetName() string {
|
||||||
|
return strings.TrimSpace(name)
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogLevel() LogLevel {
|
||||||
|
if IsDebug() {
|
||||||
|
return Debug
|
||||||
|
}
|
||||||
|
logLevel := os.Getenv("SUI_LOG_LEVEL")
|
||||||
|
if logLevel == "" {
|
||||||
|
return Info
|
||||||
|
}
|
||||||
|
return LogLevel(logLevel)
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsDebug() bool {
|
||||||
|
return os.Getenv("SUI_DEBUG") == "true"
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDBFolderPath() string {
|
||||||
|
dbFolderPath := os.Getenv("SUI_DB_FOLDER")
|
||||||
|
if dbFolderPath == "" {
|
||||||
|
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||||
|
if err != nil {
|
||||||
|
// Cross-platform fallback path
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
return "C:\\Program Files\\s-ui\\db"
|
||||||
|
}
|
||||||
|
return "/usr/local/s-ui/db"
|
||||||
|
}
|
||||||
|
dbFolderPath = filepath.Join(dir, "db")
|
||||||
|
}
|
||||||
|
return dbFolderPath
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDBPath() string {
|
||||||
|
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
||||||
|
}
|
||||||
1
config/name
Normal file
1
config/name
Normal file
@@ -0,0 +1 @@
|
|||||||
|
s-ui
|
||||||
1
config/version
Normal file
1
config/version
Normal file
@@ -0,0 +1 @@
|
|||||||
|
1.4.2
|
||||||
592
core/box.go
Normal file
592
core/box.go
Normal file
@@ -0,0 +1,592 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
|
boxService "github.com/sagernet/sing-box/adapter/service"
|
||||||
|
"github.com/sagernet/sing-box/common/certificate"
|
||||||
|
"github.com/sagernet/sing-box/common/dialer"
|
||||||
|
"github.com/sagernet/sing-box/common/taskmonitor"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport/local"
|
||||||
|
"github.com/sagernet/sing-box/experimental"
|
||||||
|
"github.com/sagernet/sing-box/experimental/cachefile"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
"github.com/sagernet/sing-box/protocol/direct"
|
||||||
|
"github.com/sagernet/sing-box/route"
|
||||||
|
sbCommon "github.com/sagernet/sing/common"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
"github.com/sagernet/sing/common/ntp"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
"github.com/sagernet/sing/service/pause"
|
||||||
|
)
|
||||||
|
|
||||||
|
var _ adapter.SimpleLifecycle = (*Box)(nil)
|
||||||
|
|
||||||
|
type Box struct {
|
||||||
|
createdAt time.Time
|
||||||
|
logFactory log.Factory
|
||||||
|
logger log.ContextLogger
|
||||||
|
network *route.NetworkManager
|
||||||
|
endpoint *endpoint.Manager
|
||||||
|
inbound *inbound.Manager
|
||||||
|
outbound *outbound.Manager
|
||||||
|
service *boxService.Manager
|
||||||
|
dnsTransport *dns.TransportManager
|
||||||
|
dnsRouter *dns.Router
|
||||||
|
connection *route.ConnectionManager
|
||||||
|
router *route.Router
|
||||||
|
internalService []adapter.LifecycleService
|
||||||
|
statsTracker *StatsTracker
|
||||||
|
connTracker *ConnTracker
|
||||||
|
done chan struct{}
|
||||||
|
}
|
||||||
|
|
||||||
|
type Options struct {
|
||||||
|
option.Options
|
||||||
|
Context context.Context
|
||||||
|
}
|
||||||
|
|
||||||
|
func Context(
|
||||||
|
ctx context.Context,
|
||||||
|
inboundRegistry adapter.InboundRegistry,
|
||||||
|
outboundRegistry adapter.OutboundRegistry,
|
||||||
|
endpointRegistry adapter.EndpointRegistry,
|
||||||
|
dnsTransportRegistry adapter.DNSTransportRegistry,
|
||||||
|
serviceRegistry adapter.ServiceRegistry,
|
||||||
|
) context.Context {
|
||||||
|
if service.FromContext[option.InboundOptionsRegistry](ctx) == nil ||
|
||||||
|
service.FromContext[adapter.InboundRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.InboundOptionsRegistry](ctx, inboundRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.InboundRegistry](ctx, inboundRegistry)
|
||||||
|
}
|
||||||
|
if service.FromContext[option.OutboundOptionsRegistry](ctx) == nil ||
|
||||||
|
service.FromContext[adapter.OutboundRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.OutboundOptionsRegistry](ctx, outboundRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.OutboundRegistry](ctx, outboundRegistry)
|
||||||
|
}
|
||||||
|
if service.FromContext[option.EndpointOptionsRegistry](ctx) == nil ||
|
||||||
|
service.FromContext[adapter.EndpointRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.EndpointOptionsRegistry](ctx, endpointRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.EndpointRegistry](ctx, endpointRegistry)
|
||||||
|
}
|
||||||
|
if service.FromContext[adapter.DNSTransportRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.DNSTransportOptionsRegistry](ctx, dnsTransportRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.DNSTransportRegistry](ctx, dnsTransportRegistry)
|
||||||
|
}
|
||||||
|
if service.FromContext[adapter.ServiceRegistry](ctx) == nil {
|
||||||
|
ctx = service.ContextWith[option.ServiceOptionsRegistry](ctx, serviceRegistry)
|
||||||
|
ctx = service.ContextWith[adapter.ServiceRegistry](ctx, serviceRegistry)
|
||||||
|
}
|
||||||
|
return ctx
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewBox(options Options) (*Box, error) {
|
||||||
|
var err error
|
||||||
|
createdAt := time.Now()
|
||||||
|
ctx := options.Context
|
||||||
|
if ctx == nil {
|
||||||
|
ctx = context.Background()
|
||||||
|
}
|
||||||
|
ctx = service.ContextWithDefaultRegistry(ctx)
|
||||||
|
|
||||||
|
endpointRegistry := service.FromContext[adapter.EndpointRegistry](ctx)
|
||||||
|
inboundRegistry := service.FromContext[adapter.InboundRegistry](ctx)
|
||||||
|
outboundRegistry := service.FromContext[adapter.OutboundRegistry](ctx)
|
||||||
|
dnsTransportRegistry := service.FromContext[adapter.DNSTransportRegistry](ctx)
|
||||||
|
serviceRegistry := service.FromContext[adapter.ServiceRegistry](ctx)
|
||||||
|
|
||||||
|
if endpointRegistry == nil {
|
||||||
|
return nil, common.NewError("missing endpoint registry in context")
|
||||||
|
}
|
||||||
|
if inboundRegistry == nil {
|
||||||
|
return nil, common.NewError("missing inbound registry in context")
|
||||||
|
}
|
||||||
|
if outboundRegistry == nil {
|
||||||
|
return nil, common.NewError("missing outbound registry in context")
|
||||||
|
}
|
||||||
|
if dnsTransportRegistry == nil {
|
||||||
|
return nil, common.NewError("missing DNS transport registry in context")
|
||||||
|
}
|
||||||
|
if serviceRegistry == nil {
|
||||||
|
return nil, common.NewError("missing service registry in context")
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx = pause.WithDefaultManager(ctx)
|
||||||
|
experimentalOptions := sbCommon.PtrValueOrDefault(options.Experimental)
|
||||||
|
var needCacheFile bool
|
||||||
|
var needClashAPI bool
|
||||||
|
var needV2RayAPI bool
|
||||||
|
if experimentalOptions.CacheFile != nil && experimentalOptions.CacheFile.Enabled {
|
||||||
|
needCacheFile = true
|
||||||
|
}
|
||||||
|
if experimentalOptions.ClashAPI != nil {
|
||||||
|
needClashAPI = true
|
||||||
|
}
|
||||||
|
if experimentalOptions.V2RayAPI != nil && experimentalOptions.V2RayAPI.Listen != "" {
|
||||||
|
needV2RayAPI = true
|
||||||
|
}
|
||||||
|
platformInterface := service.FromContext[adapter.PlatformInterface](ctx)
|
||||||
|
var defaultLogWriter io.Writer
|
||||||
|
if platformInterface != nil {
|
||||||
|
defaultLogWriter = io.Discard
|
||||||
|
}
|
||||||
|
var logFactory log.Factory
|
||||||
|
logFactory, err = NewFactory(log.Options{
|
||||||
|
Context: ctx,
|
||||||
|
Options: sbCommon.PtrValueOrDefault(options.Log),
|
||||||
|
DefaultWriter: defaultLogWriter,
|
||||||
|
BaseTime: createdAt,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError("create log factory", err)
|
||||||
|
}
|
||||||
|
factory = logFactory
|
||||||
|
|
||||||
|
var internalServices []adapter.LifecycleService
|
||||||
|
certificateOptions := sbCommon.PtrValueOrDefault(options.Certificate)
|
||||||
|
if C.IsAndroid || certificateOptions.Store != "" && certificateOptions.Store != C.CertificateStoreSystem ||
|
||||||
|
len(certificateOptions.Certificate) > 0 ||
|
||||||
|
len(certificateOptions.CertificatePath) > 0 ||
|
||||||
|
len(certificateOptions.CertificateDirectoryPath) > 0 {
|
||||||
|
certificateStore, err := certificate.NewStore(ctx, logFactory.NewLogger("certificate"), certificateOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
service.MustRegister[adapter.CertificateStore](ctx, certificateStore)
|
||||||
|
internalServices = append(internalServices, certificateStore)
|
||||||
|
}
|
||||||
|
|
||||||
|
routeOptions := sbCommon.PtrValueOrDefault(options.Route)
|
||||||
|
dnsOptions := sbCommon.PtrValueOrDefault(options.DNS)
|
||||||
|
endpointManager := endpoint.NewManager(logFactory.NewLogger("endpoint"), endpointRegistry)
|
||||||
|
inboundManager := inbound.NewManager(logFactory.NewLogger("inbound"), inboundRegistry, endpointManager)
|
||||||
|
outboundManager := outbound.NewManager(logFactory.NewLogger("outbound"), outboundRegistry, endpointManager, routeOptions.Final)
|
||||||
|
dnsTransportManager := dns.NewTransportManager(logFactory.NewLogger("dns/transport"), dnsTransportRegistry, outboundManager, dnsOptions.Final)
|
||||||
|
serviceManager := boxService.NewManager(logFactory.NewLogger("service"), serviceRegistry)
|
||||||
|
|
||||||
|
service.MustRegister[adapter.EndpointManager](ctx, endpointManager)
|
||||||
|
service.MustRegister[adapter.InboundManager](ctx, inboundManager)
|
||||||
|
service.MustRegister[adapter.OutboundManager](ctx, outboundManager)
|
||||||
|
service.MustRegister[adapter.DNSTransportManager](ctx, dnsTransportManager)
|
||||||
|
service.MustRegister[adapter.ServiceManager](ctx, serviceManager)
|
||||||
|
|
||||||
|
dnsRouter := dns.NewRouter(ctx, logFactory, dnsOptions)
|
||||||
|
service.MustRegister[adapter.DNSRouter](ctx, dnsRouter)
|
||||||
|
|
||||||
|
networkManager, err := route.NewNetworkManager(ctx, logFactory.NewLogger("network"), routeOptions, dnsOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError("initialize network manager", err)
|
||||||
|
}
|
||||||
|
service.MustRegister[adapter.NetworkManager](ctx, networkManager)
|
||||||
|
connectionManager := route.NewConnectionManager(logFactory.NewLogger("connection"))
|
||||||
|
service.MustRegister[adapter.ConnectionManager](ctx, connectionManager)
|
||||||
|
router := route.NewRouter(ctx, logFactory, routeOptions, dnsOptions)
|
||||||
|
service.MustRegister[adapter.Router](ctx, router)
|
||||||
|
err = router.Initialize(routeOptions.Rules, routeOptions.RuleSet)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError("initialize router", err)
|
||||||
|
}
|
||||||
|
for i, transportOptions := range dnsOptions.Servers {
|
||||||
|
var tag string
|
||||||
|
if transportOptions.Tag != "" {
|
||||||
|
tag = transportOptions.Tag
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(i)
|
||||||
|
}
|
||||||
|
err = dnsTransportManager.Create(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger(F.ToString("dns/", transportOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
|
transportOptions.Type,
|
||||||
|
transportOptions.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError("initialize DNS server[", i, "]", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = dnsRouter.Initialize(dnsOptions.Rules)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError("initialize dns router", err)
|
||||||
|
}
|
||||||
|
for i, endpointOptions := range options.Endpoints {
|
||||||
|
var tag string
|
||||||
|
if endpointOptions.Tag != "" {
|
||||||
|
tag = endpointOptions.Tag
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(i)
|
||||||
|
}
|
||||||
|
err = endpointManager.Create(
|
||||||
|
ctx,
|
||||||
|
router,
|
||||||
|
logFactory.NewLogger(F.ToString("endpoint/", endpointOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
|
endpointOptions.Type,
|
||||||
|
endpointOptions.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError("initialize endpoint["+F.ToString(i)+"] "+tag, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, inboundOptions := range options.Inbounds {
|
||||||
|
var tag string
|
||||||
|
if inboundOptions.Tag != "" {
|
||||||
|
tag = inboundOptions.Tag
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(i)
|
||||||
|
}
|
||||||
|
err = inboundManager.Create(
|
||||||
|
ctx,
|
||||||
|
router,
|
||||||
|
logFactory.NewLogger(F.ToString("inbound/", inboundOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
|
inboundOptions.Type,
|
||||||
|
inboundOptions.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError("initialize inbound[", i, "] ", tag, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, outboundOptions := range options.Outbounds {
|
||||||
|
var tag string
|
||||||
|
if outboundOptions.Tag != "" {
|
||||||
|
tag = outboundOptions.Tag
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(i)
|
||||||
|
}
|
||||||
|
outboundCtx := ctx
|
||||||
|
if tag != "" {
|
||||||
|
// TODO: remove this
|
||||||
|
outboundCtx = adapter.WithContext(outboundCtx, &adapter.InboundContext{
|
||||||
|
Outbound: tag,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
err = outboundManager.Create(
|
||||||
|
outboundCtx,
|
||||||
|
router,
|
||||||
|
logFactory.NewLogger(F.ToString("outbound/", outboundOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
|
outboundOptions.Type,
|
||||||
|
outboundOptions.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError("initialize outbound["+F.ToString(i)+"] "+tag, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for i, serviceOptions := range options.Services {
|
||||||
|
var tag string
|
||||||
|
if serviceOptions.Tag != "" {
|
||||||
|
tag = serviceOptions.Tag
|
||||||
|
} else {
|
||||||
|
tag = F.ToString(i)
|
||||||
|
}
|
||||||
|
err = serviceManager.Create(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger(F.ToString("service/", serviceOptions.Type, "[", tag, "]")),
|
||||||
|
tag,
|
||||||
|
serviceOptions.Type,
|
||||||
|
serviceOptions.Options,
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError("initialize service["+F.ToString(i)+"]"+tag, err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
outboundManager.Initialize(func() (adapter.Outbound, error) {
|
||||||
|
return direct.NewOutbound(
|
||||||
|
ctx,
|
||||||
|
router,
|
||||||
|
logFactory.NewLogger("outbound/direct"),
|
||||||
|
"direct",
|
||||||
|
option.DirectOutboundOptions{},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
dnsTransportManager.Initialize(func() (adapter.DNSTransport, error) {
|
||||||
|
return local.NewTransport(
|
||||||
|
ctx,
|
||||||
|
logFactory.NewLogger("dns/local"),
|
||||||
|
"local",
|
||||||
|
option.LocalDNSServerOptions{},
|
||||||
|
)
|
||||||
|
})
|
||||||
|
if platformInterface != nil {
|
||||||
|
err = platformInterface.Initialize(networkManager)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError("initialize platform interface", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
statsTracker := NewStatsTracker()
|
||||||
|
connTracker := NewConnTracker()
|
||||||
|
router.AppendTracker(statsTracker)
|
||||||
|
router.AppendTracker(connTracker)
|
||||||
|
|
||||||
|
if needCacheFile {
|
||||||
|
cacheFile := cachefile.New(ctx, sbCommon.PtrValueOrDefault(experimentalOptions.CacheFile))
|
||||||
|
service.MustRegister[adapter.CacheFile](ctx, cacheFile)
|
||||||
|
internalServices = append(internalServices, cacheFile)
|
||||||
|
}
|
||||||
|
if needClashAPI {
|
||||||
|
clashAPIOptions := sbCommon.PtrValueOrDefault(experimentalOptions.ClashAPI)
|
||||||
|
clashAPIOptions.ModeList = experimental.CalculateClashModeList(options.Options)
|
||||||
|
clashServer, err := experimental.NewClashServer(ctx, logFactory.(log.ObservableFactory), clashAPIOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError(err, "create clash-server")
|
||||||
|
}
|
||||||
|
router.AppendTracker(clashServer)
|
||||||
|
service.MustRegister[adapter.ClashServer](ctx, clashServer)
|
||||||
|
internalServices = append(internalServices, clashServer)
|
||||||
|
}
|
||||||
|
if needV2RayAPI {
|
||||||
|
v2rayServer, err := experimental.NewV2RayServer(logFactory.NewLogger("v2ray-api"), sbCommon.PtrValueOrDefault(experimentalOptions.V2RayAPI))
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError(err, "create v2ray-server")
|
||||||
|
}
|
||||||
|
if v2rayServer.StatsService() != nil {
|
||||||
|
router.AppendTracker(v2rayServer.StatsService())
|
||||||
|
internalServices = append(internalServices, v2rayServer)
|
||||||
|
service.MustRegister[adapter.V2RayServer](ctx, v2rayServer)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
ntpOptions := sbCommon.PtrValueOrDefault(options.NTP)
|
||||||
|
if ntpOptions.Enabled {
|
||||||
|
ntpDialer, err := dialer.New(ctx, ntpOptions.DialerOptions, ntpOptions.ServerIsDomain())
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewError(err, "create NTP service")
|
||||||
|
}
|
||||||
|
timeService := ntp.NewService(ntp.Options{
|
||||||
|
Context: ctx,
|
||||||
|
Dialer: ntpDialer,
|
||||||
|
Logger: logFactory.NewLogger("ntp"),
|
||||||
|
Server: ntpOptions.ServerOptions.Build(),
|
||||||
|
Interval: time.Duration(ntpOptions.Interval),
|
||||||
|
WriteToSystem: ntpOptions.WriteToSystem,
|
||||||
|
})
|
||||||
|
service.MustRegister[ntp.TimeService](ctx, timeService)
|
||||||
|
internalServices = append(internalServices, adapter.NewLifecycleService(timeService, "ntp service"))
|
||||||
|
}
|
||||||
|
return &Box{
|
||||||
|
network: networkManager,
|
||||||
|
endpoint: endpointManager,
|
||||||
|
inbound: inboundManager,
|
||||||
|
outbound: outboundManager,
|
||||||
|
dnsTransport: dnsTransportManager,
|
||||||
|
service: serviceManager,
|
||||||
|
dnsRouter: dnsRouter,
|
||||||
|
connection: connectionManager,
|
||||||
|
router: router,
|
||||||
|
createdAt: createdAt,
|
||||||
|
logFactory: logFactory,
|
||||||
|
logger: logFactory.Logger(),
|
||||||
|
internalService: internalServices,
|
||||||
|
statsTracker: statsTracker,
|
||||||
|
connTracker: connTracker,
|
||||||
|
done: make(chan struct{}),
|
||||||
|
}, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) PreStart() error {
|
||||||
|
err := s.preStart()
|
||||||
|
if err != nil {
|
||||||
|
// TODO: remove catch error
|
||||||
|
defer func() {
|
||||||
|
v := recover()
|
||||||
|
if v != nil {
|
||||||
|
s.logger.Error(err.Error())
|
||||||
|
s.logger.Error("panic on early close: " + fmt.Sprint(v))
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
s.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.logger.Info("sing-box pre-started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) Start() error {
|
||||||
|
err := s.start()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
s.logger.Info("sing-box started (", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) preStart() error {
|
||||||
|
monitor := taskmonitor.New(s.logger, C.StartTimeout)
|
||||||
|
monitor.Start("start logger")
|
||||||
|
err := s.logFactory.Start()
|
||||||
|
monitor.Finish()
|
||||||
|
if err != nil {
|
||||||
|
return common.NewError(err, "start logger")
|
||||||
|
}
|
||||||
|
err = adapter.StartNamed(s.logger, adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = adapter.Start(s.logger, adapter.StartStateInitialize, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = adapter.Start(s.logger, adapter.StartStateStart, s.outbound, s.dnsTransport, s.dnsRouter, s.network, s.connection, s.router)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) start() error {
|
||||||
|
err := s.preStart()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = adapter.StartNamed(s.logger, adapter.StartStateStart, s.internalService)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = adapter.Start(s.logger, adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = adapter.Start(s.logger, adapter.StartStatePostStart, s.outbound, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.inbound, s.endpoint, s.service)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = adapter.StartNamed(s.logger, adapter.StartStatePostStart, s.internalService)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = adapter.Start(s.logger, adapter.StartStateStarted, s.network, s.dnsTransport, s.dnsRouter, s.connection, s.router, s.outbound, s.inbound, s.endpoint, s.service)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = adapter.StartNamed(s.logger, adapter.StartStateStarted, s.internalService)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) Close() error {
|
||||||
|
select {
|
||||||
|
case <-s.done:
|
||||||
|
return nil
|
||||||
|
default:
|
||||||
|
close(s.done)
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
s.logger.Info("closing sing-box")
|
||||||
|
for _, closeItem := range []struct {
|
||||||
|
name string
|
||||||
|
service adapter.Lifecycle
|
||||||
|
}{
|
||||||
|
{"service", s.service},
|
||||||
|
{"endpoint", s.endpoint},
|
||||||
|
{"inbound", s.inbound},
|
||||||
|
{"outbound", s.outbound},
|
||||||
|
{"router", s.router},
|
||||||
|
{"connection", s.connection},
|
||||||
|
{"dns-router", s.dnsRouter},
|
||||||
|
{"dns-transport", s.dnsTransport},
|
||||||
|
{"network", s.network},
|
||||||
|
} {
|
||||||
|
if closeItem.service == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if v := recover(); v != nil {
|
||||||
|
err = errors.Join(err, common.NewError(fmt.Errorf("panic: %v", v), "close "+closeItem.name))
|
||||||
|
s.logger.Error("panic closing ", closeItem.name, ": ", v)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
s.logger.Trace("close ", closeItem.name)
|
||||||
|
startTime := time.Now()
|
||||||
|
closeErr := closeItem.service.Close()
|
||||||
|
if closeErr != nil {
|
||||||
|
closeErr = common.NewError(closeErr, "close "+closeItem.name)
|
||||||
|
}
|
||||||
|
err = errors.Join(err, closeErr)
|
||||||
|
s.logger.Trace("close ", closeItem.name, " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
for _, lifecycleService := range s.internalService {
|
||||||
|
if lifecycleService == nil {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
func() {
|
||||||
|
defer func() {
|
||||||
|
if v := recover(); v != nil {
|
||||||
|
err = errors.Join(err, common.NewError(fmt.Errorf("panic: %v", v), "close "+lifecycleService.Name()))
|
||||||
|
s.logger.Error("panic closing ", lifecycleService.Name(), ": ", v)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
s.logger.Trace("close ", lifecycleService.Name())
|
||||||
|
startTime := time.Now()
|
||||||
|
closeErr := lifecycleService.Close()
|
||||||
|
if closeErr != nil {
|
||||||
|
closeErr = common.NewError(closeErr, "close "+lifecycleService.Name())
|
||||||
|
}
|
||||||
|
err = errors.Join(err, closeErr)
|
||||||
|
s.logger.Trace("close ", lifecycleService.Name(), " completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||||
|
}()
|
||||||
|
}
|
||||||
|
s.logger.Trace("close logger")
|
||||||
|
startTime := time.Now()
|
||||||
|
closeErr := s.logFactory.Close()
|
||||||
|
if closeErr != nil {
|
||||||
|
closeErr = common.NewError(closeErr, "close logger")
|
||||||
|
}
|
||||||
|
err = errors.Join(err, closeErr)
|
||||||
|
s.logger.Trace("close logger completed (", F.Seconds(time.Since(startTime).Seconds()), "s)")
|
||||||
|
s.logger.Info("sing-box closed (live time: ", F.Seconds(time.Since(s.createdAt).Seconds()), "s)")
|
||||||
|
if s.statsTracker != nil {
|
||||||
|
s.statsTracker.Reset()
|
||||||
|
}
|
||||||
|
if s.connTracker != nil {
|
||||||
|
s.connTracker.Reset()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) Uptime() uint32 {
|
||||||
|
return uint32(time.Since(s.createdAt).Seconds())
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) Network() adapter.NetworkManager {
|
||||||
|
return s.network
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) Router() adapter.Router {
|
||||||
|
return s.router
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) Inbound() adapter.InboundManager {
|
||||||
|
return s.inbound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) Outbound() adapter.OutboundManager {
|
||||||
|
return s.outbound
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) Endpoint() adapter.EndpointManager {
|
||||||
|
return s.endpoint
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) StatsTracker() *StatsTracker {
|
||||||
|
return s.statsTracker
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Box) ConnTracker() *ConnTracker {
|
||||||
|
return s.connTracker
|
||||||
|
}
|
||||||
147
core/endpoint.go
Normal file
147
core/endpoint.go
Normal file
@@ -0,0 +1,147 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
)
|
||||||
|
|
||||||
|
func (c *Core) AddInbound(config []byte) error {
|
||||||
|
if !c.isRunning {
|
||||||
|
return common.NewError("sing-box is not running")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
var inbound_config option.Inbound
|
||||||
|
err = inbound_config.UnmarshalJSONContext(c.GetCtx(), config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = inbound_manager.Create(
|
||||||
|
c.GetCtx(),
|
||||||
|
router,
|
||||||
|
factory.NewLogger("inbound/"+inbound_config.Type+"["+inbound_config.Tag+"]"),
|
||||||
|
inbound_config.Tag,
|
||||||
|
inbound_config.Type,
|
||||||
|
inbound_config.Options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) RemoveInbound(tag string) error {
|
||||||
|
if !c.isRunning {
|
||||||
|
return common.NewError("sing-box is not running")
|
||||||
|
}
|
||||||
|
logger.Info("remove inbound: ", tag)
|
||||||
|
return inbound_manager.Remove(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) AddOutbound(config []byte) error {
|
||||||
|
if !c.isRunning {
|
||||||
|
return common.NewError("sing-box is not running")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
var outbound_config option.Outbound
|
||||||
|
|
||||||
|
err = outbound_config.UnmarshalJSONContext(c.GetCtx(), config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outboundCtx := adapter.WithContext(c.GetCtx(), &adapter.InboundContext{
|
||||||
|
Outbound: outbound_config.Tag,
|
||||||
|
})
|
||||||
|
|
||||||
|
err = outbound_manager.Create(
|
||||||
|
outboundCtx,
|
||||||
|
router,
|
||||||
|
factory.NewLogger("outbound/"+outbound_config.Type+"["+outbound_config.Tag+"]"),
|
||||||
|
outbound_config.Tag,
|
||||||
|
outbound_config.Type,
|
||||||
|
outbound_config.Options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) RemoveOutbound(tag string) error {
|
||||||
|
if !c.isRunning {
|
||||||
|
return common.NewError("sing-box is not running")
|
||||||
|
}
|
||||||
|
logger.Info("remove outbound: ", tag)
|
||||||
|
return outbound_manager.Remove(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) AddEndpoint(config []byte) error {
|
||||||
|
if !c.isRunning {
|
||||||
|
return common.NewError("sing-box is not running")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
var endpoint_config option.Endpoint
|
||||||
|
|
||||||
|
err = endpoint_config.UnmarshalJSONContext(c.GetCtx(), config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = endpoint_manager.Create(
|
||||||
|
c.GetCtx(),
|
||||||
|
router,
|
||||||
|
factory.NewLogger("endpoint/"+endpoint_config.Type+"["+endpoint_config.Tag+"]"),
|
||||||
|
endpoint_config.Tag,
|
||||||
|
endpoint_config.Type,
|
||||||
|
endpoint_config.Options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) RemoveEndpoint(tag string) error {
|
||||||
|
if !c.isRunning {
|
||||||
|
return common.NewError("sing-box is not running")
|
||||||
|
}
|
||||||
|
logger.Info("remove endpoint: ", tag)
|
||||||
|
return endpoint_manager.Remove(tag)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) AddService(config []byte) error {
|
||||||
|
if !c.isRunning {
|
||||||
|
return common.NewError("sing-box is not running")
|
||||||
|
}
|
||||||
|
var err error
|
||||||
|
var srv_config option.Service
|
||||||
|
|
||||||
|
err = srv_config.UnmarshalJSONContext(c.GetCtx(), config)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = service_manager.Create(
|
||||||
|
c.GetCtx(),
|
||||||
|
factory.NewLogger("service/"+srv_config.Type+"["+srv_config.Tag+"]"),
|
||||||
|
srv_config.Tag,
|
||||||
|
srv_config.Type,
|
||||||
|
srv_config.Options)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) RemoveService(tag string) error {
|
||||||
|
if !c.isRunning {
|
||||||
|
return common.NewError("sing-box is not running")
|
||||||
|
}
|
||||||
|
logger.Info("remove service: ", tag)
|
||||||
|
return service_manager.Remove(tag)
|
||||||
|
}
|
||||||
242
core/log.go
Normal file
242
core/log.go
Normal file
@@ -0,0 +1,242 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"io"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
suiLog "github.com/alireza0/s-ui/logger"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing/common"
|
||||||
|
F "github.com/sagernet/sing/common/format"
|
||||||
|
"github.com/sagernet/sing/common/observable"
|
||||||
|
"github.com/sagernet/sing/service/filemanager"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PlatformWriter struct{}
|
||||||
|
|
||||||
|
func (p PlatformWriter) DisableColors() bool {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
func (p PlatformWriter) WriteMessage(level log.Level, message string) {
|
||||||
|
switch level {
|
||||||
|
case log.LevelInfo:
|
||||||
|
suiLog.Info(message)
|
||||||
|
case log.LevelWarn:
|
||||||
|
suiLog.Warning(message)
|
||||||
|
case log.LevelPanic:
|
||||||
|
case log.LevelFatal:
|
||||||
|
case log.LevelError:
|
||||||
|
suiLog.Error(message)
|
||||||
|
default:
|
||||||
|
suiLog.Debug(message)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewFactory(options log.Options) (log.Factory, error) {
|
||||||
|
logOptions := options.Options
|
||||||
|
|
||||||
|
if logOptions.Disabled {
|
||||||
|
return log.NewNOPFactory(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var logWriter io.Writer
|
||||||
|
var logFilePath string
|
||||||
|
|
||||||
|
switch logOptions.Output {
|
||||||
|
case "":
|
||||||
|
logWriter = options.DefaultWriter
|
||||||
|
if logWriter == nil {
|
||||||
|
logWriter = os.Stderr
|
||||||
|
}
|
||||||
|
case "stderr":
|
||||||
|
logWriter = os.Stderr
|
||||||
|
case "stdout":
|
||||||
|
logWriter = os.Stdout
|
||||||
|
default:
|
||||||
|
logFilePath = logOptions.Output
|
||||||
|
}
|
||||||
|
logFormatter := log.Formatter{
|
||||||
|
BaseTime: options.BaseTime,
|
||||||
|
DisableColors: logOptions.DisableColor || logFilePath != "",
|
||||||
|
DisableTimestamp: !logOptions.Timestamp && logFilePath != "",
|
||||||
|
FullTimestamp: logOptions.Timestamp,
|
||||||
|
TimestampFormat: "-0700 2006-01-02 15:04:05",
|
||||||
|
}
|
||||||
|
factory := NewDefaultFactory(
|
||||||
|
options.Context,
|
||||||
|
logFormatter,
|
||||||
|
logWriter,
|
||||||
|
logFilePath,
|
||||||
|
)
|
||||||
|
if logOptions.Level != "" {
|
||||||
|
logLevel, err := log.ParseLevel(logOptions.Level)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.Error("parse log level", err)
|
||||||
|
}
|
||||||
|
factory.SetLevel(logLevel)
|
||||||
|
} else {
|
||||||
|
factory.SetLevel(log.LevelTrace)
|
||||||
|
}
|
||||||
|
return factory, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var _ log.Factory = (*defaultFactory)(nil)
|
||||||
|
|
||||||
|
type defaultFactory struct {
|
||||||
|
ctx context.Context
|
||||||
|
formatter log.Formatter
|
||||||
|
writer io.Writer
|
||||||
|
file *os.File
|
||||||
|
filePath string
|
||||||
|
level log.Level
|
||||||
|
subscriber *observable.Subscriber[log.Entry]
|
||||||
|
observer *observable.Observer[log.Entry]
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDefaultFactory(
|
||||||
|
ctx context.Context,
|
||||||
|
formatter log.Formatter,
|
||||||
|
writer io.Writer,
|
||||||
|
filePath string,
|
||||||
|
) log.ObservableFactory {
|
||||||
|
factory := &defaultFactory{
|
||||||
|
ctx: ctx,
|
||||||
|
formatter: formatter,
|
||||||
|
writer: writer,
|
||||||
|
filePath: filePath,
|
||||||
|
level: log.LevelTrace,
|
||||||
|
subscriber: observable.NewSubscriber[log.Entry](128),
|
||||||
|
}
|
||||||
|
return factory
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *defaultFactory) Start() error {
|
||||||
|
if f.filePath != "" {
|
||||||
|
logFile, err := filemanager.OpenFile(f.ctx, f.filePath, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
f.writer = logFile
|
||||||
|
f.file = logFile
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *defaultFactory) Close() error {
|
||||||
|
return common.Close(
|
||||||
|
common.PtrOrNil(f.file),
|
||||||
|
f.subscriber,
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *defaultFactory) Level() log.Level {
|
||||||
|
return f.level
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *defaultFactory) SetLevel(level log.Level) {
|
||||||
|
f.level = level
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *defaultFactory) Logger() log.ContextLogger {
|
||||||
|
return f.NewLogger("")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *defaultFactory) NewLogger(tag string) log.ContextLogger {
|
||||||
|
return &observableLogger{f, tag}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *defaultFactory) Subscribe() (subscription observable.Subscription[log.Entry], done <-chan struct{}, err error) {
|
||||||
|
return f.observer.Subscribe()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (f *defaultFactory) UnSubscribe(sub observable.Subscription[log.Entry]) {
|
||||||
|
f.observer.UnSubscribe(sub)
|
||||||
|
}
|
||||||
|
|
||||||
|
type observableLogger struct {
|
||||||
|
*defaultFactory
|
||||||
|
tag string
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) Log(ctx context.Context, level log.Level, args []any) {
|
||||||
|
level = log.OverrideLevelFromContext(level, ctx)
|
||||||
|
if level > l.level {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
msg := F.ToString(args...)
|
||||||
|
switch level {
|
||||||
|
case log.LevelInfo:
|
||||||
|
suiLog.Info(l.tag, msg)
|
||||||
|
case log.LevelWarn:
|
||||||
|
suiLog.Warning(l.tag, msg)
|
||||||
|
case log.LevelPanic:
|
||||||
|
case log.LevelFatal:
|
||||||
|
case log.LevelError:
|
||||||
|
suiLog.Error(l.tag, msg)
|
||||||
|
default:
|
||||||
|
suiLog.Debug(l.tag, msg)
|
||||||
|
}
|
||||||
|
if (l.filePath != "" || l.writer != os.Stderr) && l.writer != nil {
|
||||||
|
message := l.formatter.Format(ctx, level, l.tag, msg, time.Now())
|
||||||
|
l.writer.Write([]byte(message))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) Trace(args ...any) {
|
||||||
|
l.TraceContext(context.Background(), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) Debug(args ...any) {
|
||||||
|
l.DebugContext(context.Background(), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) Info(args ...any) {
|
||||||
|
l.InfoContext(context.Background(), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) Warn(args ...any) {
|
||||||
|
l.WarnContext(context.Background(), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) Error(args ...any) {
|
||||||
|
l.ErrorContext(context.Background(), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) Fatal(args ...any) {
|
||||||
|
l.FatalContext(context.Background(), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) Panic(args ...any) {
|
||||||
|
l.PanicContext(context.Background(), args...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) TraceContext(ctx context.Context, args ...any) {
|
||||||
|
l.Log(ctx, log.LevelTrace, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) DebugContext(ctx context.Context, args ...any) {
|
||||||
|
l.Log(ctx, log.LevelDebug, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) InfoContext(ctx context.Context, args ...any) {
|
||||||
|
l.Log(ctx, log.LevelInfo, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) WarnContext(ctx context.Context, args ...any) {
|
||||||
|
l.Log(ctx, log.LevelWarn, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) ErrorContext(ctx context.Context, args ...any) {
|
||||||
|
l.Log(ctx, log.LevelError, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) FatalContext(ctx context.Context, args ...any) {
|
||||||
|
l.Log(ctx, log.LevelFatal, args)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *observableLogger) PanicContext(ctx context.Context, args ...any) {
|
||||||
|
l.Log(ctx, log.LevelPanic, args)
|
||||||
|
}
|
||||||
95
core/main.go
Normal file
95
core/main.go
Normal file
@@ -0,0 +1,95 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
|
||||||
|
sb "github.com/sagernet/sing-box"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
_ "github.com/sagernet/sing-box/experimental/clashapi"
|
||||||
|
_ "github.com/sagernet/sing-box/experimental/v2rayapi"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
_ "github.com/sagernet/sing-box/transport/v2rayquic"
|
||||||
|
"github.com/sagernet/sing/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
globalCtx context.Context
|
||||||
|
inbound_manager adapter.InboundManager
|
||||||
|
outbound_manager adapter.OutboundManager
|
||||||
|
service_manager adapter.ServiceManager
|
||||||
|
endpoint_manager adapter.EndpointManager
|
||||||
|
router adapter.Router
|
||||||
|
factory log.Factory
|
||||||
|
)
|
||||||
|
|
||||||
|
type Core struct {
|
||||||
|
isRunning bool
|
||||||
|
instance *Box
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCore() *Core {
|
||||||
|
globalCtx = context.Background()
|
||||||
|
globalCtx = sb.Context(globalCtx, InboundRegistry(), OutboundRegistry(), EndpointRegistry(), DNSTransportRegistry(), ServiceRegistry())
|
||||||
|
return &Core{
|
||||||
|
isRunning: false,
|
||||||
|
instance: nil,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) GetCtx() context.Context {
|
||||||
|
return globalCtx
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) GetInstance() *Box {
|
||||||
|
return c.instance
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) Start(sbConfig []byte) error {
|
||||||
|
var opt option.Options
|
||||||
|
err := opt.UnmarshalJSONContext(globalCtx, sbConfig)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("Unmarshal config err:", err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
c.instance, err = NewBox(Options{
|
||||||
|
Context: globalCtx,
|
||||||
|
Options: opt,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = c.instance.Start()
|
||||||
|
if err != nil {
|
||||||
|
_ = c.instance.Close()
|
||||||
|
c.instance = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
globalCtx = service.ContextWith(globalCtx, c)
|
||||||
|
inbound_manager = service.FromContext[adapter.InboundManager](globalCtx)
|
||||||
|
outbound_manager = service.FromContext[adapter.OutboundManager](globalCtx)
|
||||||
|
service_manager = service.FromContext[adapter.ServiceManager](globalCtx)
|
||||||
|
endpoint_manager = service.FromContext[adapter.EndpointManager](globalCtx)
|
||||||
|
router = service.FromContext[adapter.Router](globalCtx)
|
||||||
|
|
||||||
|
c.isRunning = true
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) Stop() error {
|
||||||
|
c.isRunning = false
|
||||||
|
if c.instance == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err := c.instance.Close()
|
||||||
|
c.instance = nil
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *Core) IsRunning() bool {
|
||||||
|
return c.isRunning
|
||||||
|
}
|
||||||
40
core/outbound_check.go
Normal file
40
core/outbound_check.go
Normal file
@@ -0,0 +1,40 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
urltest "github.com/sagernet/sing-box/common/urltest"
|
||||||
|
)
|
||||||
|
|
||||||
|
const checkTimeout = 15 * time.Second
|
||||||
|
|
||||||
|
type CheckOutboundResult struct {
|
||||||
|
OK bool
|
||||||
|
Delay uint16
|
||||||
|
Error string
|
||||||
|
}
|
||||||
|
|
||||||
|
func CheckOutbound(ctx context.Context, tag string, link string) (result CheckOutboundResult) {
|
||||||
|
if outbound_manager == nil {
|
||||||
|
result.Error = "core not running"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
ob, ok := outbound_manager.Outbound(tag)
|
||||||
|
if !ok {
|
||||||
|
result.Error = "outbound not found"
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx, cancel := context.WithTimeout(ctx, checkTimeout)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
|
delay, err := urltest.URLTest(ctx, link, ob)
|
||||||
|
if err != nil {
|
||||||
|
result.Error = err.Error()
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
result.OK = true
|
||||||
|
result.Delay = delay
|
||||||
|
return result
|
||||||
|
}
|
||||||
139
core/register.go
Normal file
139
core/register.go
Normal file
@@ -0,0 +1,139 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
|
"github.com/sagernet/sing-box/adapter/inbound"
|
||||||
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
|
"github.com/sagernet/sing-box/adapter/service"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport/dhcp"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport/fakeip"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport/hosts"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport/local"
|
||||||
|
"github.com/sagernet/sing-box/dns/transport/quic"
|
||||||
|
"github.com/sagernet/sing-box/protocol/anytls"
|
||||||
|
"github.com/sagernet/sing-box/protocol/block"
|
||||||
|
"github.com/sagernet/sing-box/protocol/direct"
|
||||||
|
"github.com/sagernet/sing-box/protocol/group"
|
||||||
|
"github.com/sagernet/sing-box/protocol/http"
|
||||||
|
"github.com/sagernet/sing-box/protocol/hysteria"
|
||||||
|
"github.com/sagernet/sing-box/protocol/hysteria2"
|
||||||
|
"github.com/sagernet/sing-box/protocol/mixed"
|
||||||
|
"github.com/sagernet/sing-box/protocol/naive"
|
||||||
|
_ "github.com/sagernet/sing-box/protocol/naive/quic"
|
||||||
|
"github.com/sagernet/sing-box/protocol/redirect"
|
||||||
|
"github.com/sagernet/sing-box/protocol/shadowsocks"
|
||||||
|
"github.com/sagernet/sing-box/protocol/shadowtls"
|
||||||
|
"github.com/sagernet/sing-box/protocol/socks"
|
||||||
|
"github.com/sagernet/sing-box/protocol/ssh"
|
||||||
|
"github.com/sagernet/sing-box/protocol/tor"
|
||||||
|
"github.com/sagernet/sing-box/protocol/trojan"
|
||||||
|
"github.com/sagernet/sing-box/protocol/tuic"
|
||||||
|
"github.com/sagernet/sing-box/protocol/tun"
|
||||||
|
"github.com/sagernet/sing-box/protocol/vless"
|
||||||
|
"github.com/sagernet/sing-box/protocol/vmess"
|
||||||
|
"github.com/sagernet/sing-box/protocol/wireguard"
|
||||||
|
"github.com/sagernet/sing-box/service/ccm"
|
||||||
|
"github.com/sagernet/sing-box/service/ocm"
|
||||||
|
"github.com/sagernet/sing-box/service/resolved"
|
||||||
|
"github.com/sagernet/sing-box/service/ssmapi"
|
||||||
|
_ "github.com/sagernet/sing-box/transport/v2rayquic"
|
||||||
|
)
|
||||||
|
|
||||||
|
func InboundRegistry() *inbound.Registry {
|
||||||
|
registry := inbound.NewRegistry()
|
||||||
|
|
||||||
|
tun.RegisterInbound(registry)
|
||||||
|
redirect.RegisterRedirect(registry)
|
||||||
|
redirect.RegisterTProxy(registry)
|
||||||
|
direct.RegisterInbound(registry)
|
||||||
|
|
||||||
|
socks.RegisterInbound(registry)
|
||||||
|
http.RegisterInbound(registry)
|
||||||
|
mixed.RegisterInbound(registry)
|
||||||
|
|
||||||
|
shadowsocks.RegisterInbound(registry)
|
||||||
|
vmess.RegisterInbound(registry)
|
||||||
|
trojan.RegisterInbound(registry)
|
||||||
|
naive.RegisterInbound(registry)
|
||||||
|
shadowtls.RegisterInbound(registry)
|
||||||
|
vless.RegisterInbound(registry)
|
||||||
|
anytls.RegisterInbound(registry)
|
||||||
|
|
||||||
|
hysteria.RegisterInbound(registry)
|
||||||
|
tuic.RegisterInbound(registry)
|
||||||
|
hysteria2.RegisterInbound(registry)
|
||||||
|
|
||||||
|
return registry
|
||||||
|
}
|
||||||
|
|
||||||
|
func OutboundRegistry() *outbound.Registry {
|
||||||
|
registry := outbound.NewRegistry()
|
||||||
|
|
||||||
|
direct.RegisterOutbound(registry)
|
||||||
|
|
||||||
|
block.RegisterOutbound(registry)
|
||||||
|
|
||||||
|
group.RegisterSelector(registry)
|
||||||
|
group.RegisterURLTest(registry)
|
||||||
|
|
||||||
|
socks.RegisterOutbound(registry)
|
||||||
|
http.RegisterOutbound(registry)
|
||||||
|
shadowsocks.RegisterOutbound(registry)
|
||||||
|
vmess.RegisterOutbound(registry)
|
||||||
|
trojan.RegisterOutbound(registry)
|
||||||
|
registerNaiveOutbound(registry)
|
||||||
|
tor.RegisterOutbound(registry)
|
||||||
|
ssh.RegisterOutbound(registry)
|
||||||
|
shadowtls.RegisterOutbound(registry)
|
||||||
|
vless.RegisterOutbound(registry)
|
||||||
|
anytls.RegisterOutbound(registry)
|
||||||
|
|
||||||
|
hysteria.RegisterOutbound(registry)
|
||||||
|
tuic.RegisterOutbound(registry)
|
||||||
|
hysteria2.RegisterOutbound(registry)
|
||||||
|
|
||||||
|
return registry
|
||||||
|
}
|
||||||
|
|
||||||
|
func EndpointRegistry() *endpoint.Registry {
|
||||||
|
registry := endpoint.NewRegistry()
|
||||||
|
|
||||||
|
wireguard.RegisterEndpoint(registry)
|
||||||
|
registerTailscaleEndpoint(registry)
|
||||||
|
|
||||||
|
return registry
|
||||||
|
}
|
||||||
|
|
||||||
|
func DNSTransportRegistry() *dns.TransportRegistry {
|
||||||
|
registry := dns.NewTransportRegistry()
|
||||||
|
|
||||||
|
transport.RegisterTCP(registry)
|
||||||
|
transport.RegisterUDP(registry)
|
||||||
|
transport.RegisterTLS(registry)
|
||||||
|
transport.RegisterHTTPS(registry)
|
||||||
|
hosts.RegisterTransport(registry)
|
||||||
|
local.RegisterTransport(registry)
|
||||||
|
fakeip.RegisterTransport(registry)
|
||||||
|
|
||||||
|
quic.RegisterTransport(registry)
|
||||||
|
quic.RegisterHTTP3Transport(registry)
|
||||||
|
dhcp.RegisterTransport(registry)
|
||||||
|
registerTailscaleTransport(registry)
|
||||||
|
|
||||||
|
return registry
|
||||||
|
}
|
||||||
|
|
||||||
|
func ServiceRegistry() *service.Registry {
|
||||||
|
registry := service.NewRegistry()
|
||||||
|
|
||||||
|
resolved.RegisterService(registry)
|
||||||
|
ssmapi.RegisterService(registry)
|
||||||
|
|
||||||
|
registerDERPService(registry)
|
||||||
|
ccm.RegisterService(registry)
|
||||||
|
ocm.RegisterService(registry)
|
||||||
|
|
||||||
|
return registry
|
||||||
|
}
|
||||||
12
core/register_naive.go
Normal file
12
core/register_naive.go
Normal file
@@ -0,0 +1,12 @@
|
|||||||
|
//go:build with_naive_outbound
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
|
"github.com/sagernet/sing-box/protocol/naive"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerNaiveOutbound(registry *outbound.Registry) {
|
||||||
|
naive.RegisterOutbound(registry)
|
||||||
|
}
|
||||||
13
core/register_naive_stub.go
Normal file
13
core/register_naive_stub.go
Normal file
@@ -0,0 +1,13 @@
|
|||||||
|
//go:build !with_naive_outbound
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/sagernet/sing-box/adapter/outbound"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerNaiveOutbound(registry *outbound.Registry) {
|
||||||
|
// naive outbound is disabled when built without with_naive_outbound tag
|
||||||
|
logger.Error("naive outbound is disabled when built without with_naive_outbound tag")
|
||||||
|
}
|
||||||
23
core/register_tailscale.go
Normal file
23
core/register_tailscale.go
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
//go:build with_tailscale
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
|
"github.com/sagernet/sing-box/adapter/service"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/protocol/tailscale"
|
||||||
|
"github.com/sagernet/sing-box/service/derp"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerTailscaleEndpoint(registry *endpoint.Registry) {
|
||||||
|
tailscale.RegisterEndpoint(registry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerTailscaleTransport(registry *dns.TransportRegistry) {
|
||||||
|
tailscale.RegistryTransport(registry)
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerDERPService(registry *service.Registry) {
|
||||||
|
derp.Register(registry)
|
||||||
|
}
|
||||||
34
core/register_tailscale_stub.go
Normal file
34
core/register_tailscale_stub.go
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
//go:build !with_tailscale
|
||||||
|
|
||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing-box/adapter/endpoint"
|
||||||
|
"github.com/sagernet/sing-box/adapter/service"
|
||||||
|
C "github.com/sagernet/sing-box/constant"
|
||||||
|
"github.com/sagernet/sing-box/dns"
|
||||||
|
"github.com/sagernet/sing-box/log"
|
||||||
|
"github.com/sagernet/sing-box/option"
|
||||||
|
E "github.com/sagernet/sing/common/exceptions"
|
||||||
|
)
|
||||||
|
|
||||||
|
func registerTailscaleEndpoint(registry *endpoint.Registry) {
|
||||||
|
endpoint.Register[option.TailscaleEndpointOptions](registry, C.TypeTailscale, func(ctx context.Context, router adapter.Router, logger log.ContextLogger, tag string, options option.TailscaleEndpointOptions) (adapter.Endpoint, error) {
|
||||||
|
return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerTailscaleTransport(registry *dns.TransportRegistry) {
|
||||||
|
dns.RegisterTransport[option.TailscaleDNSServerOptions](registry, C.DNSTypeTailscale, func(ctx context.Context, logger log.ContextLogger, tag string, options option.TailscaleDNSServerOptions) (adapter.DNSTransport, error) {
|
||||||
|
return nil, E.New(`Tailscale is not included in this build, rebuild with -tags with_tailscale`)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func registerDERPService(registry *service.Registry) {
|
||||||
|
service.Register[option.DERPServiceOptions](registry, C.TypeDERP, func(ctx context.Context, logger log.ContextLogger, tag string, options option.DERPServiceOptions) (adapter.Service, error) {
|
||||||
|
return nil, E.New(`DERP is not included in this build, rebuild with -tags with_tailscale`)
|
||||||
|
})
|
||||||
|
}
|
||||||
219
core/tracker_conn.go
Normal file
219
core/tracker_conn.go
Normal file
@@ -0,0 +1,219 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"errors"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
|
||||||
|
"github.com/gofrs/uuid/v5"
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing/common/buf"
|
||||||
|
M "github.com/sagernet/sing/common/metadata"
|
||||||
|
"github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConnectionInfo struct {
|
||||||
|
ID string
|
||||||
|
Conn net.Conn
|
||||||
|
PacketConn network.PacketConn
|
||||||
|
Inbound string
|
||||||
|
Type string // "tcp" or "udp"
|
||||||
|
}
|
||||||
|
|
||||||
|
type ConnTracker struct {
|
||||||
|
access sync.Mutex
|
||||||
|
connections map[string]*ConnectionInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConnTracker() *ConnTracker {
|
||||||
|
return &ConnTracker{
|
||||||
|
connections: make(map[string]*ConnectionInfo),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnTracker) Reset() {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
for _, connInfo := range c.connections {
|
||||||
|
if connInfo.Conn != nil {
|
||||||
|
_ = connInfo.Conn.Close()
|
||||||
|
}
|
||||||
|
if connInfo.PacketConn != nil {
|
||||||
|
_ = connInfo.PacketConn.Close()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
c.connections = make(map[string]*ConnectionInfo)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnTracker) generateConnectionID() string {
|
||||||
|
return uuid.Must(uuid.NewV4()).String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnTracker) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn {
|
||||||
|
connID := c.generateConnectionID()
|
||||||
|
connInfo := &ConnectionInfo{
|
||||||
|
ID: connID,
|
||||||
|
Conn: conn,
|
||||||
|
Inbound: metadata.Inbound,
|
||||||
|
Type: "tcp",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.trackConnection(connID, connInfo)
|
||||||
|
|
||||||
|
return c.createWrappedConn(conn, connID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnTracker) RoutedPacketConnection(ctx context.Context, conn network.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) network.PacketConn {
|
||||||
|
connID := c.generateConnectionID()
|
||||||
|
connInfo := &ConnectionInfo{
|
||||||
|
ID: connID,
|
||||||
|
PacketConn: conn,
|
||||||
|
Inbound: metadata.Inbound,
|
||||||
|
Type: "udp",
|
||||||
|
}
|
||||||
|
|
||||||
|
c.trackConnection(connID, connInfo)
|
||||||
|
|
||||||
|
return c.createWrappedPacketConn(conn, connID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnTracker) CloseConnByInbound(inbound string) int {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
|
||||||
|
closedCount := 0
|
||||||
|
for connID, connInfo := range c.connections {
|
||||||
|
if connInfo.Inbound == inbound {
|
||||||
|
if connInfo.Conn != nil {
|
||||||
|
connInfo.Conn.Close()
|
||||||
|
}
|
||||||
|
if connInfo.PacketConn != nil {
|
||||||
|
connInfo.PacketConn.Close()
|
||||||
|
}
|
||||||
|
delete(c.connections, connID)
|
||||||
|
closedCount++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return closedCount
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnTracker) trackConnection(connID string, connInfo *ConnectionInfo) {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
c.connections[connID] = connInfo
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnTracker) untrackConnection(connID string) {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
delete(c.connections, connID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// shouldUntrackIOErr reports whether err indicates the connection is done (peer closed, reset, etc.).
|
||||||
|
func shouldUntrackIOErr(err error) bool {
|
||||||
|
if err == nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
if errors.Is(err, io.EOF) {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
var ne net.Error
|
||||||
|
if errors.As(err, &ne) {
|
||||||
|
return !ne.Temporary()
|
||||||
|
}
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnTracker) createWrappedConn(conn net.Conn, connID string) *wrappedConn {
|
||||||
|
return &wrappedConn{
|
||||||
|
Conn: conn,
|
||||||
|
tracker: c,
|
||||||
|
connID: connID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *ConnTracker) createWrappedPacketConn(conn network.PacketConn, connID string) *wrappedPacketConn {
|
||||||
|
return &wrappedPacketConn{
|
||||||
|
PacketConn: conn,
|
||||||
|
tracker: c,
|
||||||
|
connID: connID,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrappedConn struct {
|
||||||
|
net.Conn
|
||||||
|
tracker *ConnTracker
|
||||||
|
connID string
|
||||||
|
untrackOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedConn) doUntrack() {
|
||||||
|
w.untrackOnce.Do(func() {
|
||||||
|
w.tracker.untrackConnection(w.connID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedConn) Read(b []byte) (int, error) {
|
||||||
|
n, err := w.Conn.Read(b)
|
||||||
|
if shouldUntrackIOErr(err) {
|
||||||
|
w.doUntrack()
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedConn) Write(b []byte) (int, error) {
|
||||||
|
n, err := w.Conn.Write(b)
|
||||||
|
if err != nil && shouldUntrackIOErr(err) {
|
||||||
|
w.doUntrack()
|
||||||
|
}
|
||||||
|
return n, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedConn) Close() error {
|
||||||
|
w.doUntrack()
|
||||||
|
return w.Conn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedConn) Upstream() any {
|
||||||
|
return w.Conn
|
||||||
|
}
|
||||||
|
|
||||||
|
type wrappedPacketConn struct {
|
||||||
|
network.PacketConn
|
||||||
|
tracker *ConnTracker
|
||||||
|
connID string
|
||||||
|
untrackOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedPacketConn) doUntrack() {
|
||||||
|
w.untrackOnce.Do(func() {
|
||||||
|
w.tracker.untrackConnection(w.connID)
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedPacketConn) ReadPacket(buffer *buf.Buffer) (destination M.Socksaddr, err error) {
|
||||||
|
dest, err := w.PacketConn.ReadPacket(buffer)
|
||||||
|
if shouldUntrackIOErr(err) {
|
||||||
|
w.doUntrack()
|
||||||
|
}
|
||||||
|
return dest, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedPacketConn) WritePacket(buffer *buf.Buffer, destination M.Socksaddr) error {
|
||||||
|
err := w.PacketConn.WritePacket(buffer, destination)
|
||||||
|
if err != nil && shouldUntrackIOErr(err) {
|
||||||
|
w.doUntrack()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedPacketConn) Close() error {
|
||||||
|
w.doUntrack()
|
||||||
|
return w.PacketConn.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (w *wrappedPacketConn) Upstream() any {
|
||||||
|
return w.PacketConn
|
||||||
|
}
|
||||||
153
core/tracker_stats.go
Normal file
153
core/tracker_stats.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package core
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"net"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/adapter"
|
||||||
|
"github.com/sagernet/sing/common/atomic"
|
||||||
|
"github.com/sagernet/sing/common/bufio"
|
||||||
|
"github.com/sagernet/sing/common/network"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Counter struct {
|
||||||
|
read *atomic.Int64
|
||||||
|
write *atomic.Int64
|
||||||
|
}
|
||||||
|
|
||||||
|
type StatsTracker struct {
|
||||||
|
access sync.Mutex
|
||||||
|
inbounds map[string]Counter
|
||||||
|
outbounds map[string]Counter
|
||||||
|
users map[string]Counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStatsTracker() *StatsTracker {
|
||||||
|
return &StatsTracker{
|
||||||
|
inbounds: make(map[string]Counter),
|
||||||
|
outbounds: make(map[string]Counter),
|
||||||
|
users: make(map[string]Counter),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatsTracker) Reset() {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
c.inbounds = make(map[string]Counter)
|
||||||
|
c.outbounds = make(map[string]Counter)
|
||||||
|
c.users = make(map[string]Counter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatsTracker) getReadCounters(inbound string, outbound string, user string) ([]*atomic.Int64, []*atomic.Int64) {
|
||||||
|
var readCounter []*atomic.Int64
|
||||||
|
var writeCounter []*atomic.Int64
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
|
||||||
|
if inbound != "" {
|
||||||
|
readCounter = append(readCounter, c.loadOrCreateCounter(&c.inbounds, inbound).read)
|
||||||
|
writeCounter = append(writeCounter, c.inbounds[inbound].write)
|
||||||
|
}
|
||||||
|
if outbound != "" {
|
||||||
|
readCounter = append(readCounter, c.loadOrCreateCounter(&c.outbounds, outbound).read)
|
||||||
|
writeCounter = append(writeCounter, c.outbounds[outbound].write)
|
||||||
|
}
|
||||||
|
if user != "" {
|
||||||
|
readCounter = append(readCounter, c.loadOrCreateCounter(&c.users, user).read)
|
||||||
|
writeCounter = append(writeCounter, c.users[user].write)
|
||||||
|
}
|
||||||
|
return readCounter, writeCounter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatsTracker) loadOrCreateCounter(obj *map[string]Counter, name string) Counter {
|
||||||
|
counter, loaded := (*obj)[name]
|
||||||
|
if loaded {
|
||||||
|
return counter
|
||||||
|
}
|
||||||
|
counter = Counter{read: &atomic.Int64{}, write: &atomic.Int64{}}
|
||||||
|
(*obj)[name] = counter
|
||||||
|
return counter
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatsTracker) RoutedConnection(ctx context.Context, conn net.Conn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) net.Conn {
|
||||||
|
readCounter, writeCounter := c.getReadCounters(metadata.Inbound, matchOutbound.Tag(), metadata.User)
|
||||||
|
return bufio.NewInt64CounterConn(conn, readCounter, writeCounter)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatsTracker) RoutedPacketConnection(ctx context.Context, conn network.PacketConn, metadata adapter.InboundContext, matchedRule adapter.Rule, matchOutbound adapter.Outbound) network.PacketConn {
|
||||||
|
readCounter, writeCounter := c.getReadCounters(metadata.Inbound, matchOutbound.Tag(), metadata.User)
|
||||||
|
return bufio.NewInt64CounterPacketConn(conn, readCounter, nil, writeCounter, nil)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *StatsTracker) GetStats() *[]model.Stats {
|
||||||
|
c.access.Lock()
|
||||||
|
defer c.access.Unlock()
|
||||||
|
|
||||||
|
dt := time.Now().Unix()
|
||||||
|
|
||||||
|
s := []model.Stats{}
|
||||||
|
for inbound, counter := range c.inbounds {
|
||||||
|
down := counter.write.Swap(0)
|
||||||
|
up := counter.read.Swap(0)
|
||||||
|
if down > 0 || up > 0 {
|
||||||
|
s = append(s, model.Stats{
|
||||||
|
DateTime: dt,
|
||||||
|
Resource: "inbound",
|
||||||
|
Tag: inbound,
|
||||||
|
Direction: false,
|
||||||
|
Traffic: down,
|
||||||
|
}, model.Stats{
|
||||||
|
DateTime: dt,
|
||||||
|
Resource: "inbound",
|
||||||
|
Tag: inbound,
|
||||||
|
Direction: true,
|
||||||
|
Traffic: up,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for outbound, counter := range c.outbounds {
|
||||||
|
down := counter.write.Swap(0)
|
||||||
|
up := counter.read.Swap(0)
|
||||||
|
if down > 0 || up > 0 {
|
||||||
|
s = append(s, model.Stats{
|
||||||
|
DateTime: dt,
|
||||||
|
Resource: "outbound",
|
||||||
|
Tag: outbound,
|
||||||
|
Direction: false,
|
||||||
|
Traffic: down,
|
||||||
|
}, model.Stats{
|
||||||
|
DateTime: dt,
|
||||||
|
Resource: "outbound",
|
||||||
|
Tag: outbound,
|
||||||
|
Direction: true,
|
||||||
|
Traffic: up,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
for user, counter := range c.users {
|
||||||
|
down := counter.write.Swap(0)
|
||||||
|
up := counter.read.Swap(0)
|
||||||
|
if down > 0 || up > 0 {
|
||||||
|
s = append(s, model.Stats{
|
||||||
|
DateTime: dt,
|
||||||
|
Resource: "user",
|
||||||
|
Tag: user,
|
||||||
|
Direction: false,
|
||||||
|
Traffic: down,
|
||||||
|
}, model.Stats{
|
||||||
|
DateTime: dt,
|
||||||
|
Resource: "user",
|
||||||
|
Tag: user,
|
||||||
|
Direction: true,
|
||||||
|
Traffic: up,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &s
|
||||||
|
}
|
||||||
19
cronjob/WALCheckpointJob.go
Normal file
19
cronjob/WALCheckpointJob.go
Normal file
@@ -0,0 +1,19 @@
|
|||||||
|
package cronjob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WALCheckpointJob struct{}
|
||||||
|
|
||||||
|
func NewWALCheckpointJob() *WALCheckpointJob {
|
||||||
|
return &WALCheckpointJob{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WALCheckpointJob) Run() {
|
||||||
|
db := database.GetDB()
|
||||||
|
if err := db.Exec("PRAGMA wal_checkpoint(FULL)").Error; err != nil {
|
||||||
|
logger.Error("Error checkpointing WAL: ", err.Error())
|
||||||
|
}
|
||||||
|
}
|
||||||
17
cronjob/checkCoreJob.go
Normal file
17
cronjob/checkCoreJob.go
Normal file
@@ -0,0 +1,17 @@
|
|||||||
|
package cronjob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CheckCoreJob struct {
|
||||||
|
service.ConfigService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCheckCoreJob() *CheckCoreJob {
|
||||||
|
return &CheckCoreJob{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *CheckCoreJob) Run() {
|
||||||
|
s.ConfigService.StartCore()
|
||||||
|
}
|
||||||
43
cronjob/cronJob.go
Normal file
43
cronjob/cronJob.go
Normal file
@@ -0,0 +1,43 @@
|
|||||||
|
package cronjob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/robfig/cron/v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type CronJob struct {
|
||||||
|
cron *cron.Cron
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewCronJob() *CronJob {
|
||||||
|
return &CronJob{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronJob) Start(loc *time.Location, trafficAge int) error {
|
||||||
|
c.cron = cron.New(cron.WithLocation(loc), cron.WithSeconds())
|
||||||
|
c.cron.Start()
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
// Start stats job
|
||||||
|
c.cron.AddJob("@every 10s", NewStatsJob(trafficAge > 0))
|
||||||
|
// Start expiry job
|
||||||
|
c.cron.AddJob("@every 1m", NewDepleteJob())
|
||||||
|
// Start deleting old stats
|
||||||
|
if trafficAge > 0 {
|
||||||
|
c.cron.AddJob("@daily", NewDelStatsJob(trafficAge))
|
||||||
|
}
|
||||||
|
// Start core if it is not running
|
||||||
|
c.cron.AddJob("@every 5s", NewCheckCoreJob())
|
||||||
|
// database WAL checkpoint
|
||||||
|
c.cron.AddJob("@every 10m", NewWALCheckpointJob())
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *CronJob) Stop() {
|
||||||
|
if c.cron != nil {
|
||||||
|
c.cron.Stop()
|
||||||
|
}
|
||||||
|
}
|
||||||
26
cronjob/delStatsJob.go
Normal file
26
cronjob/delStatsJob.go
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
package cronjob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DelStatsJob struct {
|
||||||
|
service.StatsService
|
||||||
|
trafficAge int
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDelStatsJob(ta int) *DelStatsJob {
|
||||||
|
return &DelStatsJob{
|
||||||
|
trafficAge: ta,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DelStatsJob) Run() {
|
||||||
|
err := s.StatsService.DelOldStats(s.trafficAge)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Deleting old statistics failed: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
logger.Debug("Stats older than ", s.trafficAge, " days were deleted")
|
||||||
|
}
|
||||||
30
cronjob/depleteJob.go
Normal file
30
cronjob/depleteJob.go
Normal file
@@ -0,0 +1,30 @@
|
|||||||
|
package cronjob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DepleteJob struct {
|
||||||
|
service.ClientService
|
||||||
|
service.InboundService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDepleteJob() *DepleteJob {
|
||||||
|
return new(DepleteJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DepleteJob) Run() {
|
||||||
|
inboundIds, err := s.ClientService.DepleteClients()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Disable depleted users failed: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if len(inboundIds) > 0 {
|
||||||
|
err := s.InboundService.RestartInbounds(database.GetDB(), inboundIds)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("unable to restart inbounds: ", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
25
cronjob/statsJob.go
Normal file
25
cronjob/statsJob.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package cronjob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type StatsJob struct {
|
||||||
|
service.StatsService
|
||||||
|
enableTraffic bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewStatsJob(saveTraffic bool) *StatsJob {
|
||||||
|
return &StatsJob{
|
||||||
|
enableTraffic: saveTraffic,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StatsJob) Run() {
|
||||||
|
err := s.StatsService.SaveStats(s.enableTraffic)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Get stats failed: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
309
database/backup.go
Normal file
309
database/backup.go
Normal file
@@ -0,0 +1,309 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"mime/multipart"
|
||||||
|
"os"
|
||||||
|
"path/filepath"
|
||||||
|
"runtime"
|
||||||
|
"strings"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/cmd/migration"
|
||||||
|
"github.com/alireza0/s-ui/config"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetDb(exclude string) ([]byte, error) {
|
||||||
|
exclude_changes, exclude_stats := false, false
|
||||||
|
for _, table := range strings.Split(exclude, ",") {
|
||||||
|
if table == "changes" {
|
||||||
|
exclude_changes = true
|
||||||
|
} else if table == "stats" {
|
||||||
|
exclude_stats = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
dbPath := dir + config.GetName() + "_" + time.Now().Format("20060102-200203") + ".db"
|
||||||
|
|
||||||
|
backupDb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer func() {
|
||||||
|
if sqlDB, e := backupDb.DB(); e == nil {
|
||||||
|
_ = sqlDB.Close()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
defer os.Remove(dbPath)
|
||||||
|
|
||||||
|
err = backupDb.AutoMigrate(
|
||||||
|
&model.Setting{},
|
||||||
|
&model.Tls{},
|
||||||
|
&model.Inbound{},
|
||||||
|
&model.Outbound{},
|
||||||
|
&model.Endpoint{},
|
||||||
|
&model.User{},
|
||||||
|
&model.Stats{},
|
||||||
|
&model.Client{},
|
||||||
|
&model.Changes{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
var settings []model.Setting
|
||||||
|
var tls []model.Tls
|
||||||
|
var inbound []model.Inbound
|
||||||
|
var outbound []model.Outbound
|
||||||
|
var endpoint []model.Endpoint
|
||||||
|
var users []model.User
|
||||||
|
var clients []model.Client
|
||||||
|
var stats []model.Stats
|
||||||
|
var changes []model.Changes
|
||||||
|
|
||||||
|
// Perform scans and handle errors
|
||||||
|
if err := db.Model(&model.Setting{}).Scan(&settings).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(settings) > 0 {
|
||||||
|
if err := backupDb.Save(settings).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.Tls{}).Scan(&tls).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(tls) > 0 {
|
||||||
|
if err := backupDb.Save(tls).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.Inbound{}).Scan(&inbound).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(inbound) > 0 {
|
||||||
|
if err := backupDb.Save(inbound).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.Outbound{}).Scan(&outbound).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(outbound) > 0 {
|
||||||
|
if err := backupDb.Save(outbound).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.Endpoint{}).Scan(&endpoint).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(endpoint) > 0 {
|
||||||
|
if err := backupDb.Save(endpoint).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.User{}).Scan(&users).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(users) > 0 {
|
||||||
|
if err := backupDb.Save(users).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if err := db.Model(&model.Client{}).Scan(&clients).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
} else if len(clients) > 0 {
|
||||||
|
if err := backupDb.Save(clients).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !exclude_stats {
|
||||||
|
if err := db.Model(&model.Stats{}).Scan(&stats).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(stats) > 0 {
|
||||||
|
if err := backupDb.Save(stats).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if !exclude_changes {
|
||||||
|
if err := db.Model(&model.Changes{}).Scan(&changes).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(changes) > 0 {
|
||||||
|
if err := backupDb.Save(changes).Error; err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Update WAL
|
||||||
|
err = backupDb.Exec("PRAGMA wal_checkpoint;").Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
bdb, _ := backupDb.DB()
|
||||||
|
bdb.Close()
|
||||||
|
|
||||||
|
// Open the file for reading
|
||||||
|
file, err := os.Open(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer file.Close()
|
||||||
|
|
||||||
|
// Read the file contents
|
||||||
|
fileContents, err := io.ReadAll(file)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return fileContents, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ImportDB(file multipart.File) error {
|
||||||
|
// Check if the file is a SQLite database
|
||||||
|
isValidDb, err := IsSQLiteDB(file)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error checking db file format: %v", err)
|
||||||
|
}
|
||||||
|
if !isValidDb {
|
||||||
|
return common.NewError("Invalid db file format")
|
||||||
|
}
|
||||||
|
|
||||||
|
// Reset the file reader to the beginning
|
||||||
|
_, err = file.Seek(0, 0)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error resetting file reader: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save the file as temporary file
|
||||||
|
tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
|
||||||
|
// Remove the existing fallback file (if any) before creating one
|
||||||
|
_, err = os.Stat(tempPath)
|
||||||
|
if err == nil {
|
||||||
|
errRemove := os.Remove(tempPath)
|
||||||
|
if errRemove != nil {
|
||||||
|
return common.NewErrorf("Error removing existing temporary db file: %v", errRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Create the temporary file
|
||||||
|
tempFile, err := os.Create(tempPath)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error creating temporary db file: %v", err)
|
||||||
|
}
|
||||||
|
defer tempFile.Close()
|
||||||
|
|
||||||
|
// Remove temp file before returning
|
||||||
|
defer os.Remove(tempPath)
|
||||||
|
|
||||||
|
// Close old DB
|
||||||
|
old_db, _ := db.DB()
|
||||||
|
old_db.Close()
|
||||||
|
|
||||||
|
// Save uploaded file to temporary file
|
||||||
|
_, err = io.Copy(tempFile, file)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error saving db: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if we can init db or not
|
||||||
|
newDb, err := gorm.Open(sqlite.Open(tempPath), &gorm.Config{})
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error checking db: %v", err)
|
||||||
|
}
|
||||||
|
newDb_db, _ := newDb.DB()
|
||||||
|
if newDb_db != nil {
|
||||||
|
newDb_db.Close()
|
||||||
|
}
|
||||||
|
|
||||||
|
// Backup the current database for fallback
|
||||||
|
fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
|
||||||
|
// Remove the existing fallback file (if any)
|
||||||
|
_, err = os.Stat(fallbackPath)
|
||||||
|
if err == nil {
|
||||||
|
errRemove := os.Remove(fallbackPath)
|
||||||
|
if errRemove != nil {
|
||||||
|
return common.NewErrorf("Error removing existing fallback db file: %v", errRemove)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Move the current database to the fallback location
|
||||||
|
err = os.Rename(config.GetDBPath(), fallbackPath)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error backing up temporary db file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Remove the temporary file before returning
|
||||||
|
defer os.Remove(fallbackPath)
|
||||||
|
|
||||||
|
// Move temp to DB path
|
||||||
|
err = os.Rename(tempPath, config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
errRename := os.Rename(fallbackPath, config.GetDBPath())
|
||||||
|
if errRename != nil {
|
||||||
|
return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename)
|
||||||
|
}
|
||||||
|
return common.NewErrorf("Error moving db file: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Migrate DB
|
||||||
|
migration.MigrateDb()
|
||||||
|
err = InitDB(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
errRename := os.Rename(fallbackPath, config.GetDBPath())
|
||||||
|
if errRename != nil {
|
||||||
|
return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
|
||||||
|
}
|
||||||
|
return common.NewErrorf("Error migrating db: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
// Restart app
|
||||||
|
err = SendSighup()
|
||||||
|
if err != nil {
|
||||||
|
return common.NewErrorf("Error restarting app: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsSQLiteDB(file io.Reader) (bool, error) {
|
||||||
|
signature := []byte("SQLite format 3\x00")
|
||||||
|
buf := make([]byte, len(signature))
|
||||||
|
_, err := file.Read(buf)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return bytes.Equal(buf, signature), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func SendSighup() error {
|
||||||
|
// Get the current process
|
||||||
|
process, err := os.FindProcess(os.Getpid())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Send SIGHUP to the current process
|
||||||
|
go func() {
|
||||||
|
time.Sleep(3 * time.Second)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
err = process.Kill()
|
||||||
|
} else {
|
||||||
|
err = process.Signal(syscall.SIGHUP)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("send signal SIGHUP failed:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
127
database/db.go
Normal file
127
database/db.go
Normal file
@@ -0,0 +1,127 @@
|
|||||||
|
package database
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"path"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/config"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
|
||||||
|
"gorm.io/driver/sqlite"
|
||||||
|
"gorm.io/gorm"
|
||||||
|
"gorm.io/gorm/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
var db *gorm.DB
|
||||||
|
|
||||||
|
func initUser() error {
|
||||||
|
var count int64
|
||||||
|
err := db.Model(&model.User{}).Count(&count).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if count == 0 {
|
||||||
|
user := &model.User{
|
||||||
|
Username: "admin",
|
||||||
|
Password: "admin",
|
||||||
|
}
|
||||||
|
return db.Create(user).Error
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func OpenDB(dbPath string) error {
|
||||||
|
dir := path.Dir(dbPath)
|
||||||
|
err := os.MkdirAll(dir, 01740)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var gormLogger logger.Interface
|
||||||
|
|
||||||
|
if config.IsDebug() {
|
||||||
|
gormLogger = logger.Default
|
||||||
|
} else {
|
||||||
|
gormLogger = logger.Discard
|
||||||
|
}
|
||||||
|
|
||||||
|
c := &gorm.Config{
|
||||||
|
Logger: gormLogger,
|
||||||
|
}
|
||||||
|
sep := "?"
|
||||||
|
if strings.Contains(dbPath, "?") {
|
||||||
|
sep = "&"
|
||||||
|
}
|
||||||
|
// _cache_size=-200 caps each connection's page cache at ~200 KiB
|
||||||
|
// (default is ~2 MiB), reducing memory amplification if a connection
|
||||||
|
// escapes the pool.
|
||||||
|
dsn := dbPath + sep + "_busy_timeout=10000&_journal_mode=WAL&_cache_size=-200"
|
||||||
|
db, err = gorm.Open(sqlite.Open(dsn), c)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
sqlDB, err := db.DB()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
sqlDB.SetMaxOpenConns(25)
|
||||||
|
sqlDB.SetMaxIdleConns(2)
|
||||||
|
sqlDB.SetConnMaxLifetime(time.Hour)
|
||||||
|
sqlDB.SetConnMaxIdleTime(5 * time.Minute)
|
||||||
|
|
||||||
|
if config.IsDebug() {
|
||||||
|
db = db.Debug()
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func InitDB(dbPath string) error {
|
||||||
|
err := OpenDB(dbPath)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Default Outbounds
|
||||||
|
if !db.Migrator().HasTable(&model.Outbound{}) {
|
||||||
|
db.Migrator().CreateTable(&model.Outbound{})
|
||||||
|
defaultOutbound := []model.Outbound{
|
||||||
|
{Type: "direct", Tag: "direct", Options: json.RawMessage(`{}`)},
|
||||||
|
}
|
||||||
|
db.Create(&defaultOutbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = db.AutoMigrate(
|
||||||
|
&model.Setting{},
|
||||||
|
&model.Tls{},
|
||||||
|
&model.Inbound{},
|
||||||
|
&model.Outbound{},
|
||||||
|
&model.Service{},
|
||||||
|
&model.Endpoint{},
|
||||||
|
&model.User{},
|
||||||
|
&model.Tokens{},
|
||||||
|
&model.Stats{},
|
||||||
|
&model.Client{},
|
||||||
|
&model.Changes{},
|
||||||
|
)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = initUser()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetDB() *gorm.DB {
|
||||||
|
return db
|
||||||
|
}
|
||||||
|
|
||||||
|
func IsNotFound(err error) bool {
|
||||||
|
return err == gorm.ErrRecordNotFound
|
||||||
|
}
|
||||||
63
database/model/endpoints.go
Normal file
63
database/model/endpoints.go
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Endpoint struct {
|
||||||
|
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
Type string `json:"type" form:"type"`
|
||||||
|
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||||
|
Options json.RawMessage `json:"-" form:"-"`
|
||||||
|
Ext json.RawMessage `json:"ext" form:"ext"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Endpoint) UnmarshalJSON(data []byte) error {
|
||||||
|
var err error
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err = json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract fixed fields and store the rest in Options
|
||||||
|
if val, exists := raw["id"].(float64); exists {
|
||||||
|
o.Id = uint(val)
|
||||||
|
}
|
||||||
|
delete(raw, "id")
|
||||||
|
o.Type, _ = raw["type"].(string)
|
||||||
|
delete(raw, "type")
|
||||||
|
o.Tag = raw["tag"].(string)
|
||||||
|
delete(raw, "tag")
|
||||||
|
o.Ext, _ = json.MarshalIndent(raw["ext"], "", " ")
|
||||||
|
delete(raw, "ext")
|
||||||
|
|
||||||
|
// Remaining fields
|
||||||
|
o.Options, err = json.MarshalIndent(raw, "", " ")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON customizes marshalling
|
||||||
|
func (o Endpoint) MarshalJSON() ([]byte, error) {
|
||||||
|
// Combine fixed fields and dynamic fields into one map
|
||||||
|
combined := make(map[string]interface{})
|
||||||
|
switch o.Type {
|
||||||
|
case "warp":
|
||||||
|
combined["type"] = "wireguard"
|
||||||
|
default:
|
||||||
|
combined["type"] = o.Type
|
||||||
|
}
|
||||||
|
combined["tag"] = o.Tag
|
||||||
|
|
||||||
|
if o.Options != nil {
|
||||||
|
var restFields map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(o.Options, &restFields); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range restFields {
|
||||||
|
combined[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(combined)
|
||||||
|
}
|
||||||
103
database/model/inbounds.go
Normal file
103
database/model/inbounds.go
Normal file
@@ -0,0 +1,103 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Inbound struct {
|
||||||
|
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
Type string `json:"type" form:"type"`
|
||||||
|
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||||
|
|
||||||
|
// Foreign key to tls table
|
||||||
|
TlsId uint `json:"tls_id" form:"tls_id"`
|
||||||
|
Tls *Tls `json:"tls" form:"tls" gorm:"foreignKey:TlsId;references:Id"`
|
||||||
|
|
||||||
|
Addrs json.RawMessage `json:"addrs" form:"addrs"`
|
||||||
|
OutJson json.RawMessage `json:"out_json" form:"out_json"`
|
||||||
|
Options json.RawMessage `json:"-" form:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Inbound) UnmarshalJSON(data []byte) error {
|
||||||
|
var err error
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err = json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract fixed fields and store the rest in Options
|
||||||
|
if val, exists := raw["id"].(float64); exists {
|
||||||
|
i.Id = uint(val)
|
||||||
|
}
|
||||||
|
delete(raw, "id")
|
||||||
|
i.Type, _ = raw["type"].(string)
|
||||||
|
delete(raw, "type")
|
||||||
|
i.Tag, _ = raw["tag"].(string)
|
||||||
|
delete(raw, "tag")
|
||||||
|
|
||||||
|
// TlsId
|
||||||
|
if val, exists := raw["tls_id"].(float64); exists {
|
||||||
|
i.TlsId = uint(val)
|
||||||
|
}
|
||||||
|
delete(raw, "tls_id")
|
||||||
|
delete(raw, "tls")
|
||||||
|
delete(raw, "users")
|
||||||
|
|
||||||
|
// Addrs
|
||||||
|
i.Addrs, _ = json.MarshalIndent(raw["addrs"], "", " ")
|
||||||
|
delete(raw, "addrs")
|
||||||
|
|
||||||
|
// OutJson
|
||||||
|
i.OutJson, _ = json.MarshalIndent(raw["out_json"], "", " ")
|
||||||
|
delete(raw, "out_json")
|
||||||
|
|
||||||
|
// Remaining fields
|
||||||
|
i.Options, err = json.MarshalIndent(raw, "", " ")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON customizes marshalling
|
||||||
|
func (i Inbound) MarshalJSON() ([]byte, error) {
|
||||||
|
// Combine fixed fields and dynamic fields into one map
|
||||||
|
combined := make(map[string]interface{})
|
||||||
|
combined["type"] = i.Type
|
||||||
|
combined["tag"] = i.Tag
|
||||||
|
if i.Tls != nil {
|
||||||
|
combined["tls"] = i.Tls.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.Options != nil {
|
||||||
|
var restFields map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(i.Options, &restFields); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range restFields {
|
||||||
|
combined[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(combined)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i Inbound) MarshalFull() (*map[string]interface{}, error) {
|
||||||
|
combined := make(map[string]interface{})
|
||||||
|
combined["id"] = i.Id
|
||||||
|
combined["type"] = i.Type
|
||||||
|
combined["tag"] = i.Tag
|
||||||
|
combined["tls_id"] = i.TlsId
|
||||||
|
combined["addrs"] = i.Addrs
|
||||||
|
combined["out_json"] = i.OutJson
|
||||||
|
|
||||||
|
if i.Options != nil {
|
||||||
|
var restFields map[string]interface{}
|
||||||
|
if err := json.Unmarshal(i.Options, &restFields); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range restFields {
|
||||||
|
combined[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &combined, nil
|
||||||
|
}
|
||||||
73
database/model/model.go
Normal file
73
database/model/model.go
Normal file
@@ -0,0 +1,73 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type Setting struct {
|
||||||
|
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
Key string `json:"key" form:"key"`
|
||||||
|
Value string `json:"value" form:"value"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tls struct {
|
||||||
|
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
Name string `json:"name" form:"name"`
|
||||||
|
Server json.RawMessage `json:"server" form:"server"`
|
||||||
|
Client json.RawMessage `json:"client" form:"client"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type User struct {
|
||||||
|
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
Username string `json:"username" form:"username"`
|
||||||
|
Password string `json:"password" form:"password"`
|
||||||
|
LastLogins string `json:"lastLogin"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Client struct {
|
||||||
|
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
Enable bool `json:"enable" form:"enable"`
|
||||||
|
Name string `json:"name" form:"name"`
|
||||||
|
Config json.RawMessage `json:"config,omitempty" form:"config"`
|
||||||
|
Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
|
||||||
|
Links json.RawMessage `json:"links,omitempty" form:"links"`
|
||||||
|
Volume int64 `json:"volume" form:"volume"`
|
||||||
|
Expiry int64 `json:"expiry" form:"expiry"`
|
||||||
|
Down int64 `json:"down" form:"down"`
|
||||||
|
Up int64 `json:"up" form:"up"`
|
||||||
|
Desc string `json:"desc" form:"desc"`
|
||||||
|
Group string `json:"group" form:"group"`
|
||||||
|
|
||||||
|
// Delay start and periodic reset
|
||||||
|
DelayStart bool `json:"delayStart" form:"delayStart" gorm:"default:false;not null"`
|
||||||
|
AutoReset bool `json:"autoReset" form:"autoReset" gorm:"default:false;not null"`
|
||||||
|
ResetDays int `json:"resetDays" form:"resetDays" gorm:"default:0;not null"`
|
||||||
|
NextReset int64 `json:"nextReset" form:"nextReset" gorm:"default:0;not null"`
|
||||||
|
TotalUp int64 `json:"totalUp" form:"totalUp" gorm:"default:0;not null"`
|
||||||
|
TotalDown int64 `json:"totalDown" form:"totalDown" gorm:"default:0;not null"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Stats struct {
|
||||||
|
Id uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
DateTime int64 `json:"dateTime"`
|
||||||
|
Resource string `json:"resource"`
|
||||||
|
Tag string `json:"tag"`
|
||||||
|
Direction bool `json:"direction"`
|
||||||
|
Traffic int64 `json:"traffic"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Changes struct {
|
||||||
|
Id uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
DateTime int64 `json:"dateTime"`
|
||||||
|
Actor string `json:"actor"`
|
||||||
|
Key string `json:"key"`
|
||||||
|
Action string `json:"action"`
|
||||||
|
Obj json.RawMessage `json:"obj"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type Tokens struct {
|
||||||
|
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
Desc string `json:"desc" form:"desc"`
|
||||||
|
Token string `json:"token" form:"token"`
|
||||||
|
Expiry int64 `json:"expiry" form:"expiry"`
|
||||||
|
UserId uint `json:"userId" form:"userId"`
|
||||||
|
User *User `json:"user" gorm:"foreignKey:UserId;references:Id"`
|
||||||
|
}
|
||||||
53
database/model/outbounds.go
Normal file
53
database/model/outbounds.go
Normal file
@@ -0,0 +1,53 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import "encoding/json"
|
||||||
|
|
||||||
|
type Outbound struct {
|
||||||
|
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
Type string `json:"type" form:"type"`
|
||||||
|
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||||
|
Options json.RawMessage `json:"-" form:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *Outbound) UnmarshalJSON(data []byte) error {
|
||||||
|
var err error
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err = json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract fixed fields and store the rest in Options
|
||||||
|
if val, exists := raw["id"].(float64); exists {
|
||||||
|
o.Id = uint(val)
|
||||||
|
}
|
||||||
|
delete(raw, "id")
|
||||||
|
o.Type, _ = raw["type"].(string)
|
||||||
|
delete(raw, "type")
|
||||||
|
o.Tag = raw["tag"].(string)
|
||||||
|
delete(raw, "tag")
|
||||||
|
|
||||||
|
// Remaining fields
|
||||||
|
o.Options, err = json.MarshalIndent(raw, "", " ")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON customizes marshalling
|
||||||
|
func (o Outbound) MarshalJSON() ([]byte, error) {
|
||||||
|
// Combine fixed fields and dynamic fields into one map
|
||||||
|
combined := make(map[string]interface{})
|
||||||
|
combined["type"] = o.Type
|
||||||
|
combined["tag"] = o.Tag
|
||||||
|
|
||||||
|
if o.Options != nil {
|
||||||
|
var restFields map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(o.Options, &restFields); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range restFields {
|
||||||
|
combined[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(combined)
|
||||||
|
}
|
||||||
90
database/model/services.go
Normal file
90
database/model/services.go
Normal file
@@ -0,0 +1,90 @@
|
|||||||
|
package model
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Service struct {
|
||||||
|
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
|
Type string `json:"type" form:"type"`
|
||||||
|
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||||
|
|
||||||
|
// Foreign key to tls table
|
||||||
|
TlsId uint `json:"tls_id" form:"tls_id"`
|
||||||
|
Tls *Tls `json:"tls" form:"tls" gorm:"foreignKey:TlsId;references:Id"`
|
||||||
|
|
||||||
|
Options json.RawMessage `json:"-" form:"-"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i *Service) UnmarshalJSON(data []byte) error {
|
||||||
|
var err error
|
||||||
|
var raw map[string]interface{}
|
||||||
|
if err = json.Unmarshal(data, &raw); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Extract fixed fields and store the rest in Options
|
||||||
|
if val, exists := raw["id"].(float64); exists {
|
||||||
|
i.Id = uint(val)
|
||||||
|
}
|
||||||
|
delete(raw, "id")
|
||||||
|
i.Type, _ = raw["type"].(string)
|
||||||
|
delete(raw, "type")
|
||||||
|
i.Tag, _ = raw["tag"].(string)
|
||||||
|
delete(raw, "tag")
|
||||||
|
|
||||||
|
// TlsId
|
||||||
|
if val, exists := raw["tls_id"].(float64); exists {
|
||||||
|
i.TlsId = uint(val)
|
||||||
|
}
|
||||||
|
delete(raw, "tls_id")
|
||||||
|
delete(raw, "tls")
|
||||||
|
|
||||||
|
// Remaining fields
|
||||||
|
i.Options, err = json.MarshalIndent(raw, "", " ")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// MarshalJSON customizes marshalling
|
||||||
|
func (i Service) MarshalJSON() ([]byte, error) {
|
||||||
|
// Combine fixed fields and dynamic fields into one map
|
||||||
|
combined := make(map[string]interface{})
|
||||||
|
combined["type"] = i.Type
|
||||||
|
combined["tag"] = i.Tag
|
||||||
|
if i.Tls != nil {
|
||||||
|
combined["tls"] = i.Tls.Server
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.Options != nil {
|
||||||
|
var restFields map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(i.Options, &restFields); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range restFields {
|
||||||
|
combined[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(combined)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (i Service) MarshalFull() (*map[string]interface{}, error) {
|
||||||
|
combined := make(map[string]interface{})
|
||||||
|
combined["id"] = i.Id
|
||||||
|
combined["type"] = i.Type
|
||||||
|
combined["tag"] = i.Tag
|
||||||
|
combined["tls_id"] = i.TlsId
|
||||||
|
|
||||||
|
if i.Options != nil {
|
||||||
|
var restFields map[string]interface{}
|
||||||
|
if err := json.Unmarshal(i.Options, &restFields); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range restFields {
|
||||||
|
combined[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &combined, nil
|
||||||
|
}
|
||||||
29
docker-build-test.sh
Executable file
29
docker-build-test.sh
Executable file
@@ -0,0 +1,29 @@
|
|||||||
|
#!/usr/bin/env bash
|
||||||
|
# Test Docker multi-platform build (linux/amd64, 386, arm64, arm/v7, arm/v6)
|
||||||
|
# Requires: frontend_dist/ (run from repo root after building frontend)
|
||||||
|
|
||||||
|
set -e
|
||||||
|
cd "$(dirname "$0")/.."
|
||||||
|
|
||||||
|
echo "==> Preparing frontend_dist..."
|
||||||
|
if [ ! -d "frontend_dist" ] || [ -z "$(ls -A frontend_dist 2>/dev/null)" ]; then
|
||||||
|
echo "Building frontend..."
|
||||||
|
(cd frontend && npm install --prefer-offline --no-audit && npm run build)
|
||||||
|
rm -rf frontend_dist
|
||||||
|
mkdir -p frontend_dist
|
||||||
|
cp -R frontend/dist/* frontend_dist/
|
||||||
|
echo "frontend_dist ready."
|
||||||
|
else
|
||||||
|
echo "frontend_dist exists, skipping frontend build."
|
||||||
|
fi
|
||||||
|
|
||||||
|
PLATFORMS="linux/amd64,linux/386,linux/arm64/v8,linux/arm/v7,linux/arm/v6"
|
||||||
|
echo "==> Testing Docker build for: $PLATFORMS"
|
||||||
|
docker buildx build \
|
||||||
|
--platform "$PLATFORMS" \
|
||||||
|
-f Dockerfile.frontend-artifact \
|
||||||
|
--build-arg CRONET_RELEASE=latest \
|
||||||
|
--progress=plain \
|
||||||
|
. 2>&1 | tee docker-build-test.log
|
||||||
|
|
||||||
|
echo "==> Done. Check docker-build-test.log for full output."
|
||||||
22
docker-compose.yml
Normal file
22
docker-compose.yml
Normal file
@@ -0,0 +1,22 @@
|
|||||||
|
---
|
||||||
|
services:
|
||||||
|
s-ui:
|
||||||
|
image: alireza7/s-ui
|
||||||
|
container_name: s-ui
|
||||||
|
hostname: "s-ui"
|
||||||
|
volumes:
|
||||||
|
- "./db:/app/db"
|
||||||
|
- "./cert:/app/cert"
|
||||||
|
tty: true
|
||||||
|
restart: unless-stopped
|
||||||
|
ports:
|
||||||
|
- "2095:2095"
|
||||||
|
- "2096:2096"
|
||||||
|
networks:
|
||||||
|
- s-ui
|
||||||
|
entrypoint: "./entrypoint.sh"
|
||||||
|
|
||||||
|
networks:
|
||||||
|
s-ui:
|
||||||
|
driver: bridge
|
||||||
|
|
||||||
8
entrypoint.sh
Executable file
8
entrypoint.sh
Executable file
@@ -0,0 +1,8 @@
|
|||||||
|
#!/bin/sh
|
||||||
|
|
||||||
|
DB_PATH="${SUI_DB_FOLDER:-/app/db}/s-ui.db"
|
||||||
|
if [ -f "$DB_PATH" ]; then
|
||||||
|
./sui migrate
|
||||||
|
fi
|
||||||
|
|
||||||
|
exec ./sui
|
||||||
199
go.mod
Normal file
199
go.mod
Normal file
@@ -0,0 +1,199 @@
|
|||||||
|
module github.com/alireza0/s-ui
|
||||||
|
|
||||||
|
go 1.26.3
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-contrib/gzip v1.2.6
|
||||||
|
github.com/gin-contrib/sessions v1.1.0
|
||||||
|
github.com/gin-gonic/gin v1.12.0
|
||||||
|
github.com/gofrs/uuid/v5 v5.4.0
|
||||||
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
github.com/sagernet/sing v0.8.10
|
||||||
|
github.com/sagernet/sing-box v1.13.12
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.4
|
||||||
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10
|
||||||
|
gopkg.in/yaml.v3 v3.0.1
|
||||||
|
gorm.io/driver/sqlite v1.6.0
|
||||||
|
gorm.io/gorm v1.31.1
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
filippo.io/edwards25519 v1.2.0 // indirect
|
||||||
|
github.com/ajg/form v1.5.1 // indirect
|
||||||
|
github.com/akutz/memconn v0.1.0 // indirect
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa // indirect
|
||||||
|
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||||
|
github.com/anthropics/anthropic-sdk-go v1.26.0 // indirect
|
||||||
|
github.com/anytls/sing-anytls v0.0.11 // indirect
|
||||||
|
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||||
|
github.com/bytedance/sonic v1.15.0 // indirect
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 // indirect
|
||||||
|
github.com/caddyserver/certmagic v0.25.2 // indirect
|
||||||
|
github.com/caddyserver/zerossl v0.1.5 // indirect
|
||||||
|
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||||
|
github.com/coder/websocket v1.8.14 // indirect
|
||||||
|
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 // indirect
|
||||||
|
github.com/cretz/bine v0.2.0 // indirect
|
||||||
|
github.com/database64128/netx-go v0.1.1 // indirect
|
||||||
|
github.com/database64128/tfo-go/v2 v2.3.2 // indirect
|
||||||
|
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa // indirect
|
||||||
|
github.com/ebitengine/purego v0.10.0 // indirect
|
||||||
|
github.com/florianl/go-nfqueue/v2 v2.0.2 // indirect
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 // indirect
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 // indirect
|
||||||
|
github.com/gaissmai/bart v0.18.0 // indirect
|
||||||
|
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5 // indirect
|
||||||
|
github.com/go-chi/render v1.0.3 // indirect
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced // indirect
|
||||||
|
github.com/go-ole/go-ole v1.3.0 // 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.30.1 // indirect
|
||||||
|
github.com/gobwas/httphead v0.1.0 // indirect
|
||||||
|
github.com/gobwas/pool v0.2.1 // indirect
|
||||||
|
github.com/goccy/go-json v0.10.5 // indirect
|
||||||
|
github.com/goccy/go-yaml v1.19.2 // indirect
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2 // indirect
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da // indirect
|
||||||
|
github.com/google/btree v1.1.3 // indirect
|
||||||
|
github.com/google/go-cmp v0.7.0 // indirect
|
||||||
|
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 // indirect
|
||||||
|
github.com/google/uuid v1.6.0 // indirect
|
||||||
|
github.com/gorilla/context v1.1.2 // indirect
|
||||||
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
|
github.com/gorilla/sessions v1.4.0 // indirect
|
||||||
|
github.com/hashicorp/yamux v0.1.2 // indirect
|
||||||
|
github.com/hdevalence/ed25519consensus v0.2.0 // indirect
|
||||||
|
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/jsimonetti/rtnetlink v1.4.0 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/keybase/go-keychain v0.0.1 // indirect
|
||||||
|
github.com/klauspost/compress v1.18.0 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/libdns/acmedns v0.5.0 // indirect
|
||||||
|
github.com/libdns/alidns v1.0.6 // indirect
|
||||||
|
github.com/libdns/cloudflare v0.2.2 // indirect
|
||||||
|
github.com/libdns/libdns v1.1.1 // indirect
|
||||||
|
github.com/logrusorgru/aurora v2.0.3+incompatible // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.30 // indirect
|
||||||
|
github.com/mdlayher/netlink v1.9.0 // indirect
|
||||||
|
github.com/mdlayher/socket v0.5.1 // indirect
|
||||||
|
github.com/metacubex/utls v1.8.4 // indirect
|
||||||
|
github.com/mholt/acmez/v3 v3.1.6 // indirect
|
||||||
|
github.com/miekg/dns v1.1.72 // indirect
|
||||||
|
github.com/mitchellh/go-ps v1.0.0 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/openai/openai-go/v3 v3.26.0 // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 // indirect
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.21 // indirect
|
||||||
|
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
|
github.com/prometheus-community/pro-bing v0.4.0 // indirect
|
||||||
|
github.com/quic-go/qpack v0.6.0 // indirect
|
||||||
|
github.com/quic-go/quic-go v0.59.0 // indirect
|
||||||
|
github.com/safchain/ethtool v0.3.0 // indirect
|
||||||
|
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a // indirect
|
||||||
|
github.com/sagernet/cors v1.2.1 // indirect
|
||||||
|
github.com/sagernet/cronet-go v0.0.0-20260513071958-2faf34666c2c // indirect
|
||||||
|
github.com/sagernet/cronet-go/all v0.0.0-20260513071958-2faf34666c2c // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260513071149-ade33496efb8 // indirect
|
||||||
|
github.com/sagernet/fswatch v0.1.2 // indirect
|
||||||
|
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 // indirect
|
||||||
|
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a // indirect
|
||||||
|
github.com/sagernet/nftables v0.3.0-mod.2 // indirect
|
||||||
|
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 // indirect
|
||||||
|
github.com/sagernet/sing-mux v0.3.4 // indirect
|
||||||
|
github.com/sagernet/sing-quic v0.6.1 // indirect
|
||||||
|
github.com/sagernet/sing-shadowsocks v0.2.8 // indirect
|
||||||
|
github.com/sagernet/sing-shadowsocks2 v0.2.1 // indirect
|
||||||
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 // indirect
|
||||||
|
github.com/sagernet/sing-tun v0.8.9 // indirect
|
||||||
|
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 // indirect
|
||||||
|
github.com/sagernet/smux v1.5.50-sing-box-mod.1 // indirect
|
||||||
|
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 // indirect
|
||||||
|
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c // indirect
|
||||||
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 // indirect
|
||||||
|
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e // indirect
|
||||||
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 // indirect
|
||||||
|
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 // indirect
|
||||||
|
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a // indirect
|
||||||
|
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 // indirect
|
||||||
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc // indirect
|
||||||
|
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 // indirect
|
||||||
|
github.com/tidwall/gjson v1.18.0 // indirect
|
||||||
|
github.com/tidwall/match v1.1.1 // indirect
|
||||||
|
github.com/tidwall/pretty v1.2.1 // indirect
|
||||||
|
github.com/tidwall/sjson v1.2.5 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
|
github.com/vishvananda/netns v0.0.5 // indirect
|
||||||
|
github.com/x448/float16 v0.8.4 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
github.com/zeebo/blake3 v0.2.4 // indirect
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 // indirect
|
||||||
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
go.uber.org/zap v1.27.1 // indirect
|
||||||
|
go.uber.org/zap/exp v0.3.0 // indirect
|
||||||
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 // indirect
|
||||||
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||||
|
golang.org/x/arch v0.22.0 // indirect
|
||||||
|
golang.org/x/crypto v0.48.0 // indirect
|
||||||
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 // indirect
|
||||||
|
golang.org/x/mod v0.33.0 // indirect
|
||||||
|
golang.org/x/net v0.51.0 // indirect
|
||||||
|
golang.org/x/oauth2 v0.34.0 // indirect
|
||||||
|
golang.org/x/sync v0.20.0 // indirect
|
||||||
|
golang.org/x/sys v0.41.0 // indirect
|
||||||
|
golang.org/x/term v0.40.0 // indirect
|
||||||
|
golang.org/x/text v0.35.0 // indirect
|
||||||
|
golang.org/x/time v0.12.0 // indirect
|
||||||
|
golang.org/x/tools v0.42.0 // indirect
|
||||||
|
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||||
|
golang.zx2c4.com/wireguard/windows v0.5.3 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||||
|
google.golang.org/grpc v1.79.3 // indirect
|
||||||
|
google.golang.org/protobuf v1.36.11 // indirect
|
||||||
|
lukechampine.com/blake3 v1.4.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
replace github.com/quic-go/quic-go => github.com/quic-go/quic-go v0.57.1
|
||||||
485
go.sum
Normal file
485
go.sum
Normal file
@@ -0,0 +1,485 @@
|
|||||||
|
code.pfad.fr/check v1.1.0 h1:GWvjdzhSEgHvEHe2uJujDcpmZoySKuHQNrZMfzfO0bE=
|
||||||
|
code.pfad.fr/check v1.1.0/go.mod h1:NiUH13DtYsb7xp5wll0U4SXx7KhXQVCtRgdC96IPfoM=
|
||||||
|
filippo.io/edwards25519 v1.2.0 h1:crnVqOiS4jqYleHd9vaKZ+HKtHfllngJIiOpNpoJsjo=
|
||||||
|
filippo.io/edwards25519 v1.2.0/go.mod h1:xzAOLCNug/yB62zG1bQ8uziwrIqIuxhctzJT18Q77mc=
|
||||||
|
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
|
github.com/akutz/memconn v0.1.0 h1:NawI0TORU4hcOMsMr11g7vwlCdkYeLKXBcxWu2W/P8A=
|
||||||
|
github.com/akutz/memconn v0.1.0/go.mod h1:Jo8rI7m0NieZyLI5e2CDlRdRqRRB4S7Xp77ukDjH+Fw=
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||||
|
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||||
|
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||||
|
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||||
|
github.com/anthropics/anthropic-sdk-go v1.26.0 h1:oUTzFaUpAevfuELAP1sjL6CQJ9HHAfT7CoSYSac11PY=
|
||||||
|
github.com/anthropics/anthropic-sdk-go v1.26.0/go.mod h1:qUKmaW+uuPB64iy1l+4kOSvaLqPXnHTTBKH6RVZ7q5Q=
|
||||||
|
github.com/anytls/sing-anytls v0.0.11 h1:w8e9Uj1oP3m4zxkyZDewPk0EcQbvVxb7Nn+rapEx4fc=
|
||||||
|
github.com/anytls/sing-anytls v0.0.11/go.mod h1:7rjN6IukwysmdusYsrV51Fgu1uW6vsrdd6ctjnEAln8=
|
||||||
|
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||||
|
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||||
|
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
|
||||||
|
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0 h1:gXH3KVnatgY7loH5/TkeVyXPfESoqSBSBEiDd5VjlgE=
|
||||||
|
github.com/bytedance/sonic/loader v0.5.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||||
|
github.com/caddyserver/certmagic v0.25.2 h1:D7xcS7ggX/WEY54x0czj7ioTkmDWKIgxtIi2OcQclUc=
|
||||||
|
github.com/caddyserver/certmagic v0.25.2/go.mod h1:llW/CvsNmza8S6hmsuggsZeiX+uS27dkqY27wDIuBWg=
|
||||||
|
github.com/caddyserver/zerossl v0.1.5 h1:dkvOjBAEEtY6LIGAHei7sw2UgqSD6TrWweXpV7lvEvE=
|
||||||
|
github.com/caddyserver/zerossl v0.1.5/go.mod h1:CxA0acn7oEGO6//4rtrRjYgEoa4MFw/XofZnrYwGqG4=
|
||||||
|
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/cilium/ebpf v0.15.0 h1:7NxJhNiBT3NG8pZJ3c+yfrVdHY8ScgKD27sScgjLMMk=
|
||||||
|
github.com/cilium/ebpf v0.15.0/go.mod h1:DHp1WyrLeiBh19Cf/tfiSMhqheEiK8fXFZ4No0P1Hso=
|
||||||
|
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||||
|
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||||
|
github.com/coder/websocket v1.8.14 h1:9L0p0iKiNOibykf283eHkKUHHrpG7f65OE3BhhO7v9g=
|
||||||
|
github.com/coder/websocket v1.8.14/go.mod h1:NX3SzP+inril6yawo5CQXx8+fk145lPDC6pumgx0mVg=
|
||||||
|
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6 h1:8h5+bWd7R6AYUslN6c6iuZWTKsKxUFDlpnmilO6R2n0=
|
||||||
|
github.com/coreos/go-iptables v0.7.1-0.20240112124308-65c67c9f46e6/go.mod h1:Qe8Bv2Xik5FyTXwgIbLAnv2sWSBmvWdFETJConOQ//Q=
|
||||||
|
github.com/cretz/bine v0.2.0 h1:8GiDRGlTgz+o8H9DSnsl+5MeBK4HsExxgl6WgzOCuZo=
|
||||||
|
github.com/cretz/bine v0.2.0/go.mod h1:WU4o9QR9wWp8AVKtTM1XD5vUHkEqnf2vVSo6dBqbetI=
|
||||||
|
github.com/database64128/netx-go v0.1.1 h1:dT5LG7Gs7zFZBthFBbzWE6K8wAHjSNAaK7wCYZT7NzM=
|
||||||
|
github.com/database64128/netx-go v0.1.1/go.mod h1:LNlYVipaYkQArRFDNNJ02VkNV+My9A5XR/IGS7sIBQc=
|
||||||
|
github.com/database64128/tfo-go/v2 v2.3.2 h1:UhZMKiMq3swZGUiETkLBDzQnZBPSAeBMClpJGlnJ5Fw=
|
||||||
|
github.com/database64128/tfo-go/v2 v2.3.2/go.mod h1:GC3uB5oa4beGpCUbRb2ZOWP73bJJFmMyAVgQSO7r724=
|
||||||
|
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/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa h1:h8TfIT1xc8FWbwwpmHn1J5i43Y0uZP97GqasGCzSRJk=
|
||||||
|
github.com/dblohm7/wingoes v0.0.0-20240119213807-a09d6be7affa/go.mod h1:Nx87SkVqTKd8UtT+xu7sM/l+LgXs6c0aHrlKusR+2EQ=
|
||||||
|
github.com/dnaeon/go-vcr v1.2.0 h1:zHCHvJYTMh1N7xnV7zf1m1GPBF9Ad0Jk/whtQ1663qI=
|
||||||
|
github.com/dnaeon/go-vcr v1.2.0/go.mod h1:R4UdLID7HZT3taECzJs4YgbbH6PIGXB6W/sc5OLb6RQ=
|
||||||
|
github.com/ebitengine/purego v0.10.0 h1:QIw4xfpWT6GWTzaW5XEKy3HXoqrJGx1ijYHzTF0/ISU=
|
||||||
|
github.com/ebitengine/purego v0.10.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||||
|
github.com/florianl/go-nfqueue/v2 v2.0.2 h1:FL5lQTeetgpCvac1TRwSfgaXUn0YSO7WzGvWNIp3JPE=
|
||||||
|
github.com/florianl/go-nfqueue/v2 v2.0.2/go.mod h1:VA09+iPOT43OMoCKNfXHyzujQUty2xmzyCRkBOlmabc=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S9k=
|
||||||
|
github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0 h1:iM5WgngdRBanHcxugY4JySA0nk1wZorNOpTgCMedv5E=
|
||||||
|
github.com/fxamacker/cbor/v2 v2.7.0/go.mod h1:pxXPTn3joSm21Gbwsv0w9OSA2y1HFR9qXEeXQVeNoDQ=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||||
|
github.com/gaissmai/bart v0.18.0 h1:jQLBT/RduJu0pv/tLwXE+xKPgtWJejbxuXAR+wLJafo=
|
||||||
|
github.com/gaissmai/bart v0.18.0/go.mod h1:JJzMAhNF5Rjo4SF4jWBrANuJfqY+FvsFhW7t1UZJ+XY=
|
||||||
|
github.com/gin-contrib/gzip v1.2.6 h1:OtN8DplD5DNZCSLAnQ5HxRkD2qZ5VU+JhOrcfJrcRvg=
|
||||||
|
github.com/gin-contrib/gzip v1.2.6/go.mod h1:BQy8/+JApnRjAVUplSGZiVtD2k8GmIE2e9rYu/hLzzU=
|
||||||
|
github.com/gin-contrib/sessions v1.1.0 h1:00mhHfNEGF5sP2fwxa98aRqj1FOJdL6IkR86n2hOiBo=
|
||||||
|
github.com/gin-contrib/sessions v1.1.0/go.mod h1:TyYZDIs6qCQg2SOoYPgMT9pAkmZceVNEJMcv5qbIy60=
|
||||||
|
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||||
|
github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM=
|
||||||
|
github.com/gin-gonic/gin v1.12.0 h1:b3YAbrZtnf8N//yjKeU2+MQsh2mY5htkZidOM7O0wG8=
|
||||||
|
github.com/gin-gonic/gin v1.12.0/go.mod h1:VxccKfsSllpKshkBWgVgRniFFAzFb9csfngsqANjnLc=
|
||||||
|
github.com/github/fakeca v0.1.0 h1:Km/MVOFvclqxPM9dZBC4+QE564nU4gz4iZ0D9pMw28I=
|
||||||
|
github.com/github/fakeca v0.1.0/go.mod h1:+bormgoGMMuamOscx7N91aOuUST7wdaJ2rNjeohylyo=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5 h1:Eg4myHZBjyvJmAFjFvWgrqDTXFyOzjj7YIm3L3mu6Ug=
|
||||||
|
github.com/go-chi/chi/v5 v5.2.5/go.mod h1:X7Gx4mteadT3eDOMTsXzmI4/rwUpOwBHLpAfupzFJP0=
|
||||||
|
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
|
||||||
|
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3 h1:CVLmWDhDVRa6Mi/IgCgaopNosCaHz7zrMeF9MlZRkrs=
|
||||||
|
github.com/go-jose/go-jose/v4 v4.1.3/go.mod h1:x4oUasVrzR7071A4TnHLGSPpNOm2a21K9Kf04k1rs08=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced h1:Q311OHjMh/u5E2TITc++WlTP5We0xNseRMkHDyvhW7I=
|
||||||
|
github.com/go-json-experiment/json v0.0.0-20250813024750-ebf49471dced/go.mod h1:TiCD2a1pcmjd7YnhGH0f/zKNcCD06B029pHhzV23c2M=
|
||||||
|
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||||
|
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||||
|
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||||
|
github.com/go-logr/stdr v1.2.2/go.mod h1:mMo/vtBO5dYbehREoey6XUKy/eSumjCCveDpRre4VKE=
|
||||||
|
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
|
||||||
|
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
|
||||||
|
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
|
||||||
|
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.30.1 h1:f3zDSN/zOma+w6+1Wswgd9fLkdwy06ntQJp0BBvFG0w=
|
||||||
|
github.com/go-playground/validator/v10 v10.30.1/go.mod h1:oSuBIQzuJxL//3MelwSLD5hc2Tu889bF0Idm9Dg26cM=
|
||||||
|
github.com/gobwas/httphead v0.1.0 h1:exrUm0f4YX0L7EBwZHuCF4GDp8aJfVeBrlLQrs6NqWU=
|
||||||
|
github.com/gobwas/httphead v0.1.0/go.mod h1:O/RXo79gxV8G+RqlR/otEwx4Q36zl9rqC5u12GKvMCM=
|
||||||
|
github.com/gobwas/pool v0.2.1 h1:xfeeEhW7pwmX8nuLVlqbzVc7udMDrwetjEv+TZIz1og=
|
||||||
|
github.com/gobwas/pool v0.2.1/go.mod h1:q8bcK0KcYlCgd9e7WYLm9LpyS+YeLd8JVDW6WezmKEw=
|
||||||
|
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||||
|
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||||
|
github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM=
|
||||||
|
github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2 h1:TUR3TgtSVDmjiXOgAAyaZbYmIeP3DPkld3jgKGV8mXQ=
|
||||||
|
github.com/godbus/dbus/v5 v5.2.2/go.mod h1:3AAv2+hPq5rdnr5txxxRwiGjPXamgoIHgz9FPBfOp3c=
|
||||||
|
github.com/gofrs/uuid/v5 v5.4.0 h1:EfbpCTjqMuGyq5ZJwxqzn3Cbr2d0rUZU7v5ycAk/e/0=
|
||||||
|
github.com/gofrs/uuid/v5 v5.4.0/go.mod h1:CDOjlDMVAtN56jqyRUZh58JT31Tiw7/oQyEXZV+9bD8=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da h1:oI5xCqsCo564l8iNU+DwB5epxmsaqB+rhGL0m5jtYqE=
|
||||||
|
github.com/golang/groupcache v0.0.0-20210331224755-41bb18bfe9da/go.mod h1:cIg4eruTrX1D+g88fzRXU5OdNfaM+9IcxsU14FzY7Hc=
|
||||||
|
github.com/golang/protobuf v1.5.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
|
||||||
|
github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
|
||||||
|
github.com/google/btree v1.1.3 h1:CVpQJjYgC4VbzxeGVHfvZrv1ctoYCAI8vbl07Fcxlyg=
|
||||||
|
github.com/google/btree v1.1.3/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
|
github.com/google/go-cmp v0.7.0 h1:wk8382ETsv4JYUZwIsn6YpYiWiBsYLSJiTsyBybVuN8=
|
||||||
|
github.com/google/go-cmp v0.7.0/go.mod h1:pXiqmnSA92OHEEa9HXL2W4E7lf9JzCmGVUdgjX3N/iU=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806 h1:wG8RYIyctLhdFk6Vl1yPGtSRtwGpVkWyZww1OCil2MI=
|
||||||
|
github.com/google/nftables v0.2.1-0.20240414091927-5e242ec57806/go.mod h1:Beg6V6zZ3oEn0JuiUQ4wqwuyqqzasOltcoXPtgLbFp4=
|
||||||
|
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/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o=
|
||||||
|
github.com/gorilla/context v1.1.2/go.mod h1:KDPwT9i/MeWHiLl90fuTgrt4/wPcv75vFAZLaOOcbxM=
|
||||||
|
github.com/gorilla/securecookie v1.1.2 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
|
||||||
|
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo=
|
||||||
|
github.com/gorilla/sessions v1.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
|
||||||
|
github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
|
||||||
|
github.com/hashicorp/yamux v0.1.2 h1:XtB8kyFOyHXYVFnwT5C3+Bdo8gArse7j2AQ0DA0Uey8=
|
||||||
|
github.com/hashicorp/yamux v0.1.2/go.mod h1:C+zze2n6e/7wshOZep2A70/aQU6QBRWJO/G6FT1wIns=
|
||||||
|
github.com/hdevalence/ed25519consensus v0.2.0 h1:37ICyZqdyj0lAZ8P4D1d1id3HqbbG1N3iBb1Tb4rdcU=
|
||||||
|
github.com/hdevalence/ed25519consensus v0.2.0/go.mod h1:w3BHWjwJbFU29IRHL1Iqkw3sus+7FctEyM4RqDxYNzo=
|
||||||
|
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91 h1:u9i04mGE3iliBh0EFuWaKsmcwrLacqGmq1G3XoaM7gY=
|
||||||
|
github.com/insomniacslk/dhcp v0.0.0-20260220084031-5adc3eb26f91/go.mod h1:qfvBmyDNp+/liLEYWRvqny/PEz9hGe2Dz833eXILSmo=
|
||||||
|
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E=
|
||||||
|
github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc=
|
||||||
|
github.com/jinzhu/now v1.1.5 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
|
||||||
|
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8=
|
||||||
|
github.com/jsimonetti/rtnetlink v1.4.0 h1:Z1BF0fRgcETPEa0Kt0MRk3yV5+kF1FWTni6KUFKrq2I=
|
||||||
|
github.com/jsimonetti/rtnetlink v1.4.0/go.mod h1:5W1jDvWdnthFJ7fxYX1GMK07BUpI4oskfOqvPteYS6E=
|
||||||
|
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/keybase/go-keychain v0.0.1 h1:way+bWYa6lDppZoZcgMbYsvC7GxljxrskdNInRtuthU=
|
||||||
|
github.com/keybase/go-keychain v0.0.1/go.mod h1:PdEILRW3i9D8JcdM+FmY6RwkHGnhHxXwkPPMeUgOK1k=
|
||||||
|
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||||
|
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||||
|
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/letsencrypt/challtestsrv v1.4.2 h1:0ON3ldMhZyWlfVNYYpFuWRTmZNnyfiL9Hh5YzC3JVwU=
|
||||||
|
github.com/letsencrypt/challtestsrv v1.4.2/go.mod h1:GhqMqcSoeGpYd5zX5TgwA6er/1MbWzx/o7yuuVya+Wk=
|
||||||
|
github.com/letsencrypt/pebble/v2 v2.10.0 h1:Wq6gYXlsY6ubqI3hhxsTzdyotvfdjFBxuwYqCLCnj/U=
|
||||||
|
github.com/letsencrypt/pebble/v2 v2.10.0/go.mod h1:Sk8cmUIPcIdv2nINo+9PB4L+ZBhzY+F9A1a/h/xmWiQ=
|
||||||
|
github.com/libdns/acmedns v0.5.0 h1:5pRtmUj4Lb/QkNJSl1xgOGBUJTWW7RjpNaIhjpDXjPE=
|
||||||
|
github.com/libdns/acmedns v0.5.0/go.mod h1:X7UAFP1Ep9NpTwWpVlrZzJLR7epynAy0wrIxSPFgKjQ=
|
||||||
|
github.com/libdns/alidns v1.0.6 h1:/Ii428ty6WHFJmE24rZxq2taq++gh7rf9jhgLfp8PmM=
|
||||||
|
github.com/libdns/alidns v1.0.6/go.mod h1:RECwyQ88e9VqQVtSrvX76o1ux3gQUKGzMgxICi+u7Ec=
|
||||||
|
github.com/libdns/cloudflare v0.2.2 h1:XWHv+C1dDcApqazlh08Q6pjytYLgR2a+Y3xrXFu0vsI=
|
||||||
|
github.com/libdns/cloudflare v0.2.2/go.mod h1:w9uTmRCDlAoafAsTPnn2nJ0XHK/eaUMh86DUk8BWi60=
|
||||||
|
github.com/libdns/libdns v1.1.1 h1:wPrHrXILoSHKWJKGd0EiAVmiJbFShguILTg9leS/P/U=
|
||||||
|
github.com/libdns/libdns v1.1.1/go.mod h1:4Bj9+5CQiNMVGf87wjX4CY3HQJypUHRuLvlsfsZqLWQ=
|
||||||
|
github.com/logrusorgru/aurora v2.0.3+incompatible h1:tOpm7WcpBTn4fjmVfgpQq0EfczGlG91VSDkswnjF5A8=
|
||||||
|
github.com/logrusorgru/aurora v2.0.3+incompatible/go.mod h1:7rIyQOR62GCctdiQpZ/zOJlFyk6y+94wXzv6RNZgaR4=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||||
|
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||||
|
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
|
||||||
|
github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||||
|
github.com/mdlayher/netlink v1.9.0 h1:G8+GLq2x3v4D4MVIqDdNUhTUC7TKiCy/6MDkmItfKco=
|
||||||
|
github.com/mdlayher/netlink v1.9.0/go.mod h1:YBnl5BXsCoRuwBjKKlZ+aYmEoq0r12FDA/3JC+94KDg=
|
||||||
|
github.com/mdlayher/socket v0.5.1 h1:VZaqt6RkGkt2OE9l3GcC6nZkqD3xKeQLyfleW/uBcos=
|
||||||
|
github.com/mdlayher/socket v0.5.1/go.mod h1:TjPLHI1UgwEv5J1B5q0zTZq12A/6H7nKmtTanQE37IQ=
|
||||||
|
github.com/metacubex/utls v1.8.4 h1:HmL9nUApDdWSkgUyodfwF6hSjtiwCGGdyhaSpEejKpg=
|
||||||
|
github.com/metacubex/utls v1.8.4/go.mod h1:kncGGVhFaoGn5M3pFe3SXhZCzsbCJayNOH4UEqTKTko=
|
||||||
|
github.com/mholt/acmez/v3 v3.1.6 h1:eGVQNObP0pBN4sxqrXeg7MYqTOWyoiYpQqITVWlrevk=
|
||||||
|
github.com/mholt/acmez/v3 v3.1.6/go.mod h1:5nTPosTGosLxF3+LU4ygbgMRFDhbAVpqMI4+a4aHLBY=
|
||||||
|
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
|
||||||
|
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
|
||||||
|
github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc=
|
||||||
|
github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg=
|
||||||
|
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/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
|
||||||
|
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
|
||||||
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||||
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||||
|
github.com/openai/openai-go/v3 v3.26.0 h1:bRt6H/ozMNt/dDkN4gobnLqaEGrRGBzmbVs0xxJEnQE=
|
||||||
|
github.com/openai/openai-go/v3 v3.26.0/go.mod h1:cdufnVK14cWcT9qA1rRtrXx4FTRsgbDPW7Ia7SS5cZo=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4 h1:mye9XuhQ6gvn5h28+VilKrrPoQVanw5PMw/TB0t5Ec4=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.2.4/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.21 h1:yOVMLb6qSIDP67pl/5F7RepeKYu/VmTyEXvuMI5d9mQ=
|
||||||
|
github.com/pierrec/lz4/v4 v4.1.21/go.mod h1:gZWDp/Ze/IJXGXf23ltt2EXimqmTUXEy0GFuRQyBid4=
|
||||||
|
github.com/pires/go-proxyproto v0.8.1 h1:9KEixbdJfhrbtjpz/ZwCdWDD2Xem0NZ38qMYaASJgp0=
|
||||||
|
github.com/pires/go-proxyproto v0.8.1/go.mod h1:ZKAAyp3cgy5Y5Mo4n9AlScrkCZwUy0g3Jf+slqQVcuU=
|
||||||
|
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-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
github.com/prometheus-community/pro-bing v0.4.0 h1:YMbv+i08gQz97OZZBwLyvmmQEEzyfyrrjEaAchdy3R4=
|
||||||
|
github.com/prometheus-community/pro-bing v0.4.0/go.mod h1:b7wRYZtCcPmt4Sz319BykUU241rWLe1VFXyiyWK/dH4=
|
||||||
|
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||||
|
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||||
|
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||||
|
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||||
|
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1 h1:KvO1DLK/DRN07sQ1LQKScxyZJuNnedQ5/wKSR38lUII=
|
||||||
|
github.com/rogpeppe/go-internal v1.13.1/go.mod h1:uMEvuHeurkdAXX61udpOXGD/AzZDWNMNyH2VO9fmH0o=
|
||||||
|
github.com/safchain/ethtool v0.3.0 h1:gimQJpsI6sc1yIqP/y8GYgiXn/NjgvpM0RNoWLVVmP0=
|
||||||
|
github.com/safchain/ethtool v0.3.0/go.mod h1:SA9BwrgyAqNo7M+uaL6IYbxpm5wk3L7Mm6ocLW+CJUs=
|
||||||
|
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a h1:+NkI2670SQpQWvkkD2QgdTuzQG263YZ+2emfpeyGqW0=
|
||||||
|
github.com/sagernet/bbolt v0.0.0-20231014093535-ea5cb2fe9f0a/go.mod h1:63s7jpZqcDAIpj8oI/1v4Izok+npJOHACFCU6+huCkM=
|
||||||
|
github.com/sagernet/cors v1.2.1 h1:Cv5Z8y9YSD6Gm+qSpNrL3LO4lD3eQVvbFYJSG7JCMHQ=
|
||||||
|
github.com/sagernet/cors v1.2.1/go.mod h1:O64VyOjjhrkLmQIjF4KGRrJO/5dVXFdpEmCW/eISRAI=
|
||||||
|
github.com/sagernet/cronet-go v0.0.0-20260513071958-2faf34666c2c h1:JatMWK/reVa5Y+x3D3l49SVtHB/EQUEtQnAFTxPBNxY=
|
||||||
|
github.com/sagernet/cronet-go v0.0.0-20260513071958-2faf34666c2c/go.mod h1:T/mwtrpC4JlWfScw73CmSBvHzIvc7BatQ1MhRr+cYNw=
|
||||||
|
github.com/sagernet/cronet-go/all v0.0.0-20260513071958-2faf34666c2c h1:F/tL+VzLZ2F4SNZZze6SRSRL/jcX7LwIsuL1+hECiz0=
|
||||||
|
github.com/sagernet/cronet-go/all v0.0.0-20260513071958-2faf34666c2c/go.mod h1:GGE1tBbFgHq8kV99AKX1JXFY+9FvgNSK/W6Z5j24Ihc=
|
||||||
|
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260513071149-ade33496efb8 h1:NCKxyAnEkwsEueAEbuuUUjs2FEZAIflr+WN3Mwbvsdg=
|
||||||
|
github.com/sagernet/cronet-go/lib/android_386 v0.0.0-20260513071149-ade33496efb8/go.mod h1:XXDwdjX/T8xftoeJxQmbBoYXZp8MAPFR2CwbFuTpEtw=
|
||||||
|
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260513071149-ade33496efb8 h1:o3AGm7/L/zAdBvPu0u1dFgDR/tH086qyuXZkjLNJ7/E=
|
||||||
|
github.com/sagernet/cronet-go/lib/android_amd64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:iNiUGoLtnr8/JTuVNj7XJbmpOAp2C6+B81KDrPxwaZM=
|
||||||
|
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260513071149-ade33496efb8 h1:AeO8yHQj7aNj16fiJNU797alyuM3T+3VASnETHeV220=
|
||||||
|
github.com/sagernet/cronet-go/lib/android_arm v0.0.0-20260513071149-ade33496efb8/go.mod h1:19ILNUOGIzRdOqa2mq+iY0JoHxuieB7/lnjYeaA2vEc=
|
||||||
|
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260513071149-ade33496efb8 h1:ZgW2/Qq/5Q6eTlW80QXLokU56kfjvbLJSEGYTkcG3hU=
|
||||||
|
github.com/sagernet/cronet-go/lib/android_arm64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:JxzGyQf94Cr6sBShKqODGDyRUlESfJK/Njcz9Lz6qMQ=
|
||||||
|
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260513071149-ade33496efb8 h1:orYgvX5X9aUa+sRrAuuqA6PXiiBUI2D367ZJqan4lIU=
|
||||||
|
github.com/sagernet/cronet-go/lib/darwin_amd64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:KN+9T9TBycGOLzmKU4QdcHAJEj6Nlx48ifnlTvvHMvs=
|
||||||
|
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260513071149-ade33496efb8 h1:2w1s3wEk7qW2w4IGwlJflxwXBM97UChNiqAErKpvHr0=
|
||||||
|
github.com/sagernet/cronet-go/lib/darwin_arm64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:kojvtUc29KKnk8hs2QIANynVR59921SnGWA9kXohHc0=
|
||||||
|
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260513071149-ade33496efb8 h1:22k6CB3d4gHT+SARUh2bgNyGU4QwYupcCdP8cGuwygY=
|
||||||
|
github.com/sagernet/cronet-go/lib/ios_amd64_simulator v0.0.0-20260513071149-ade33496efb8/go.mod h1:hkQzRE5GDbaH1/ioqYh0Taho4L6i0yLRCVEZ5xHz5M0=
|
||||||
|
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260513071149-ade33496efb8 h1:PkJ5EaqLrv6bNR+MHx1/joJXoRcoYcV7JA4NtXbFQsc=
|
||||||
|
github.com/sagernet/cronet-go/lib/ios_arm64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:tzVJFTOm66UxLxy6K0ZN5Ic2PC79e+sKKnt+V9puEa4=
|
||||||
|
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260513071149-ade33496efb8 h1:V629H+OQ9yOR2d0Jkq5y42j5btpvoSWJbUaBH7FCGPI=
|
||||||
|
github.com/sagernet/cronet-go/lib/ios_arm64_simulator v0.0.0-20260513071149-ade33496efb8/go.mod h1:M/pN6m3j0HFU6/y83n0HU6GLYys3tYdr/xTE8hVEGMo=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260513071149-ade33496efb8 h1:gfObF5uoqJslCdMRRm2Yo+gmPJQPVlrci5Myrki0Kzk=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_386 v0.0.0-20260513071149-ade33496efb8/go.mod h1:cGh5hO6eljCo6KMQ/Cel8Xgq4+etL0awZLRBDVG1EZQ=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260513071149-ade33496efb8 h1:JRPN0RBKvoOBEHezJh/54KD9ftWL7YadtcCgOf/vRnw=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_386_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:JFE0/cxaKkx0wqPMZU7MgaplQlU0zudv82dROJjClKU=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260513071149-ade33496efb8 h1:mM8gNdFlXSpjZFs9kgaMgW94oTRF8YdEEQgdOp/OEUA=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_amd64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:vU8VftFeSt7fURCa3JXD6+k6ss1YAX+idQjPvHmJ2tI=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260513071149-ade33496efb8 h1:ZtCH0fH07giTK6wqkenA9fdFYt7krjWiyOvC8z9nPwk=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_amd64_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:vCe4OUuL+XOUge9v3MyTD45BnuAXiH+DkjN9quDXJzQ=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260513071149-ade33496efb8 h1:Uviqmw+Q4No9kCxJWJ5CYcq6PNHB9f0jQhd15j39+no=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_arm v0.0.0-20260513071149-ade33496efb8/go.mod h1:w9amBWrvjtohQzBGCKJ7LCh22LhTIJs4sE7cYaKQzM0=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260513071149-ade33496efb8 h1:la4zRTE9zpZCmsixwzKT2LnHuo0e439EmGwOlB1An9Q=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_arm64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:TqlsFtcYS/etTeck46kHBeT8Le0Igw1Q/AV88UnMS3s=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260513071149-ade33496efb8 h1:KodFGMqn+X2dqET0O3xww3iemAGmpoC8U4JW8gwt0x4=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_arm64_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:B6Qd0vys8sv9OKVRN6J9RqDzYRGE938Fb2zrYdBDyTQ=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260513071149-ade33496efb8 h1:QTk1RXNLOIcorZYcF0rBrwLpCIZCKEA2Jr69eFrt8xg=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_arm_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:3tXMMFY7AHugOVBZ5Al7cL7JKsnFOe5bMVr0hZPk3ow=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260513071149-ade33496efb8 h1:SXqSlM/GjZFvNdUV3IvHq5gqHfW4iWlQHMGzEsgXGXE=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_loong64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:Wt5uFdU3tnmm8YzobYewwdF7Mt6SucRQg6xeTNWC3Tk=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260513071149-ade33496efb8 h1:aAgLWpfESvy7rfDVH7ioOZQ7u2kmRsbUqJVrwJtkFWs=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_loong64_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:lyIF6wKBLwWa5ZXaAKbAoewewl+yCHo2iYev39Mbj4E=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260513071149-ade33496efb8 h1:oTLUyhLckc8TZQ8SRCapgTYyRbz1pBpIvzjMCLMPFu8=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_mips64le v0.0.0-20260513071149-ade33496efb8/go.mod h1:H46PnSTTZNcZokLLiDeMDaHiS1l14PH3tzWi0eykjD8=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260513071149-ade33496efb8 h1:LHm/85Y3zN0kNgG+li5qHvP3dzvavEytCYzdLtrfrrg=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_mipsle v0.0.0-20260513071149-ade33496efb8/go.mod h1:RBhSUDAKWq7fswtV4nQUQhuaTLcX3ettR7teA7/yf2w=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260513071149-ade33496efb8 h1:Pom5TSHV8Cln73uOgQlJ+JtmEu9xh+OuLHWq57dBaVg=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_mipsle_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:wRzoIOGG4xbpp3Gh3triLKwMwYriScXzFtunLYhY4w0=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260513071149-ade33496efb8 h1:1pPcb15BonaFl4153tRo7zOJ7U2zD1vjH+5JipSfJ3g=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_riscv64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:LNiZXmWil1OPwKCheqQjtakZlJuKGFz+iv2eGF76Hhs=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260513071149-ade33496efb8 h1:3Dy4exYQ/IVJGcnTtvW3LmjfjDaxFgJT1hn/ALBpd2M=
|
||||||
|
github.com/sagernet/cronet-go/lib/linux_riscv64_musl v0.0.0-20260513071149-ade33496efb8/go.mod h1:YFDGKTkpkJGc5+hnX/RYosZyTWg9h+68VB55fYRRLYc=
|
||||||
|
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260513071149-ade33496efb8 h1:mo9YMCYTGCRUiWNKtPVQb+qEetufxnch372xUOh9q3M=
|
||||||
|
github.com/sagernet/cronet-go/lib/tvos_amd64_simulator v0.0.0-20260513071149-ade33496efb8/go.mod h1:aaX0YGl8nhGmfRWI8bc3BtDjY8Vzx6O0cS/e1uqxDq4=
|
||||||
|
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260513071149-ade33496efb8 h1:mhh3JEDDx68oKT4kfqKlWp5QTyzVR84OS/qgqHYIbq0=
|
||||||
|
github.com/sagernet/cronet-go/lib/tvos_arm64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:EdzMKA96xITc42QEI+ct4SwqX8Dn3ltKK8wzdkLWpSc=
|
||||||
|
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260513071149-ade33496efb8 h1:04KOo38hZojV3bJ5Vqwbpj48ZQy6o7aliYXLN/TNX6g=
|
||||||
|
github.com/sagernet/cronet-go/lib/tvos_arm64_simulator v0.0.0-20260513071149-ade33496efb8/go.mod h1:qix4kv1TTAJ5tY4lJ9vjhe9EY4mM+B7H5giOhbxDVcc=
|
||||||
|
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260513071149-ade33496efb8 h1:p535QakpDZEeBz/BfFZGZo0D+Pdn74TE8UTr6c6MSog=
|
||||||
|
github.com/sagernet/cronet-go/lib/windows_amd64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:lm9w/oCCRyBiUa3G8lDQTT8x/ONUvgVR2iV9fVzUZB8=
|
||||||
|
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260513071149-ade33496efb8 h1:dovTyKHh3toBIUOS70P4Yx+3Baw6Gppsfy1sJbXoAy0=
|
||||||
|
github.com/sagernet/cronet-go/lib/windows_arm64 v0.0.0-20260513071149-ade33496efb8/go.mod h1:n34YyLgapgjWdKa0IoeczjAFCwD3/dxbsH5sucKw0bw=
|
||||||
|
github.com/sagernet/fswatch v0.1.2 h1:/TT7k4mkce1qFPxamLO842WjqBgbTBiXP2mlUjp9PFk=
|
||||||
|
github.com/sagernet/fswatch v0.1.2/go.mod h1:5BpGmpUQVd3Mc5r313HRpvADHRg3/rKn5QbwFteB880=
|
||||||
|
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1 h1:AzCE2RhBjLJ4WIWc/GejpNh+z30d5H1hwaB0nD9eY3o=
|
||||||
|
github.com/sagernet/gvisor v0.0.0-20250811.0-sing-box-mod.1/go.mod h1:NJKBtm9nVEK3iyOYWsUlrDQuoGh4zJ4KOPhSYVidvQ4=
|
||||||
|
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a h1:ObwtHN2VpqE0ZNjr6sGeT00J8uU7JF4cNUdb44/Duis=
|
||||||
|
github.com/sagernet/netlink v0.0.0-20240612041022-b9a21c07ac6a/go.mod h1:xLnfdiJbSp8rNqYEdIW/6eDO4mVoogml14Bh2hSiFpM=
|
||||||
|
github.com/sagernet/nftables v0.3.0-mod.2 h1:ck2KMU02OxL1eDFgGaWYglMDpoOZ7OHzxje+vW5Q0OQ=
|
||||||
|
github.com/sagernet/nftables v0.3.0-mod.2/go.mod h1:8kslHG4VvYNihcco+i6uxIX7qbT8A56T0y5q7U44ZaQ=
|
||||||
|
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4 h1:6qvrUW79S+CrPwWz6cMePXohgjHoKxLo3c+MDhNwc3o=
|
||||||
|
github.com/sagernet/quic-go v0.59.0-sing-box-mod.4/go.mod h1:OqILvS182CyOol5zNNo6bguvOGgXzV459+chpRaUC+4=
|
||||||
|
github.com/sagernet/sing v0.8.10 h1:V5VZffy8rm4dtBVKIpKa8vibRR2SiJprtu/10DFUalU=
|
||||||
|
github.com/sagernet/sing v0.8.10/go.mod h1:olXxWQNqRW/l2Q6JI3b2Qmz8iQnIFlOeeH8bx6JhgUA=
|
||||||
|
github.com/sagernet/sing-box v1.13.12 h1:U7znhM2VRq76ZdN4X5eRKdaUxHJNHMGKpo2csKqGTsg=
|
||||||
|
github.com/sagernet/sing-box v1.13.12/go.mod h1:dgvcaNmNZTdeiDbnpQ+EghpE+KPR+8yKtfoVEhUi3jw=
|
||||||
|
github.com/sagernet/sing-mux v0.3.4 h1:ZQplKl8MNXutjzbMVtWvWG31fohhgOfCuUZR4dVQ8+s=
|
||||||
|
github.com/sagernet/sing-mux v0.3.4/go.mod h1:QvlKMyNBNrQoyX4x+gq028uPbLM2XeRpWtDsWBJbFSk=
|
||||||
|
github.com/sagernet/sing-quic v0.6.1 h1:lx0tcm99wIA1RkyvILNzRSsMy1k7TTQYIhx71E/WBlw=
|
||||||
|
github.com/sagernet/sing-quic v0.6.1/go.mod h1:K5bWvITOm4vE10fwLfrWpw27bCoVJ+tfQ79tOWg+Ko8=
|
||||||
|
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||||
|
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
||||||
|
github.com/sagernet/sing-shadowsocks2 v0.2.1 h1:dWV9OXCeFPuYGHb6IRqlSptVnSzOelnqqs2gQ2/Qioo=
|
||||||
|
github.com/sagernet/sing-shadowsocks2 v0.2.1/go.mod h1:RnXS0lExcDAovvDeniJ4IKa2IuChrdipolPYWBv9hWQ=
|
||||||
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11 h1:tK+75l64tm9WvEFrYRE1t0YxoFdWQqw/h7Uhzj0vJ+w=
|
||||||
|
github.com/sagernet/sing-shadowtls v0.2.1-0.20250503051639-fcd445d33c11/go.mod h1:sWqKnGlMipCHaGsw1sTTlimyUpgzP4WP3pjhCsYt9oA=
|
||||||
|
github.com/sagernet/sing-tun v0.8.9 h1:ixFKKUGdVcJl4wb0xbL36hobiw9l6DIH497EQf5ILpM=
|
||||||
|
github.com/sagernet/sing-tun v0.8.9/go.mod h1:QvarqUtHfj1ULaRR+6kZOS/OoCE+pYGq67A5tyIy+dQ=
|
||||||
|
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1 h1:aSwUNYUkVyVvdmBSufR8/nRFonwJeKSIROxHcm5br9o=
|
||||||
|
github.com/sagernet/sing-vmess v0.2.8-0.20250909125414-3aed155119a1/go.mod h1:P11scgTxMxVVQ8dlM27yNm3Cro40mD0+gHbnqrNGDuY=
|
||||||
|
github.com/sagernet/smux v1.5.50-sing-box-mod.1 h1:XkJcivBC9V4wBjiGXIXZ229aZCU1hzcbp6kSkkyQ478=
|
||||||
|
github.com/sagernet/smux v1.5.50-sing-box-mod.1/go.mod h1:NjhsCEWedJm7eFLyhuBgIEzwfhRmytrUoiLluxs5Sk8=
|
||||||
|
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7 h1:8zc1Aph1+ElqF9/7aSPkO0o4vTd+AfQC+CO324mLWGg=
|
||||||
|
github.com/sagernet/tailscale v1.92.4-sing-box-1.13-mod.7/go.mod h1:m87GAn4UcesHQF3leaPFEINZETO5za1LGn1GJdNDgNc=
|
||||||
|
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c h1:f9cXNB+IOOPnR8DOLMTpr42jf7naxh5Un5Y09BBf5Cg=
|
||||||
|
github.com/sagernet/wireguard-go v0.0.2-beta.1.0.20260224074747-506b7631853c/go.mod h1:WUxgxUDZoCF2sxVmW+STSxatP02Qn3FcafTiI2BLtE0=
|
||||||
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854 h1:6uUiZcDRnZSAegryaUGwPC/Fj13JSHwiTftrXhMmYOc=
|
||||||
|
github.com/sagernet/ws v0.0.0-20231204124109-acfe8907c854/go.mod h1:LtfoSK3+NG57tvnVEHgcuBW9ujgE8enPSgzgwStwCAA=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
|
||||||
|
github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
|
||||||
|
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/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||||
|
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
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.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||||
|
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||||
|
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||||
|
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e h1:PtWT87weP5LWHEY//SWsYkSO3RWRZo4OSWagh3YD2vQ=
|
||||||
|
github.com/tailscale/certstore v0.1.1-0.20231202035212-d3fa0460f47e/go.mod h1:XrBNfAFN+pwoWuksbFS9Ccxnopa15zJGgXRFN90l3K4=
|
||||||
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55 h1:Gzfnfk2TWrk8Jj4P4c1a3CtQyMaTVCznlkLZI++hok4=
|
||||||
|
github.com/tailscale/go-winio v0.0.0-20231025203758-c4f33415bf55/go.mod h1:4k4QO+dQ3R5FofL+SanAUZe+/QfeK0+OIuwDIRu2vSg=
|
||||||
|
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05 h1:4chzWmimtJPxRs2O36yuGRW3f9SYV+bMTTvMBI0EKio=
|
||||||
|
github.com/tailscale/goupnp v1.0.1-0.20210804011211-c64d0f06ea05/go.mod h1:PdCqy9JzfWMJf1H5UJW2ip33/d4YkoKN0r67yKH1mG8=
|
||||||
|
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a h1:SJy1Pu0eH1C29XwJucQo73FrleVK6t4kYz4NVhp34Yw=
|
||||||
|
github.com/tailscale/hujson v0.0.0-20221223112325-20486734a56a/go.mod h1:DFSS3NAGHthKo1gTlmEcSBiZrRJXi28rLNd/1udP1c8=
|
||||||
|
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7 h1:uFsXVBE9Qr4ZoF094vE6iYTLDl0qCiKzYXlL6UeWObU=
|
||||||
|
github.com/tailscale/netlink v1.1.1-0.20240822203006-4d49adab4de7/go.mod h1:NzVQi3Mleb+qzq8VmcWpSkcSYxXIg0DkI6XDzpVkhJ0=
|
||||||
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc h1:24heQPtnFR+yfntqhI3oAu9i27nEojcQ4NuBQOo5ZFA=
|
||||||
|
github.com/tailscale/peercred v0.0.0-20250107143737-35a0c7bd7edc/go.mod h1:f93CXfllFsO9ZQVq+Zocb1Gp4G5Fz0b0rXHLOzt/Djc=
|
||||||
|
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976 h1:UBPHPtv8+nEAy2PD8RyAhOYvau1ek0HDJqLS/Pysi14=
|
||||||
|
github.com/tailscale/web-client-prebuilt v0.0.0-20250124233751-d4cd19a26976/go.mod h1:agQPE6y6ldqCOui2gkIh7ZMztTkIQKH049tv8siLuNQ=
|
||||||
|
github.com/tc-hib/winres v0.2.1 h1:YDE0FiP0VmtRaDn7+aaChp1KiF4owBiJa5l964l5ujA=
|
||||||
|
github.com/tc-hib/winres v0.2.1/go.mod h1:C/JaNhH3KBvhNKVbvdlDWkbMDO9H4fKKDaN7/07SSuk=
|
||||||
|
github.com/tidwall/gjson v1.14.2/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/gjson v1.18.0 h1:FIDeeyB800efLX89e5a8Y0BNH+LOngJyGrIWxG2FKQY=
|
||||||
|
github.com/tidwall/gjson v1.18.0/go.mod h1:/wbyibRr2FHMks5tjHJ5F8dMZh3AcwJEMf5vlfC0lxk=
|
||||||
|
github.com/tidwall/match v1.1.1 h1:+Ho715JplO36QYgwN9PGYNhgZvoUSc9X2c80KVTi+GA=
|
||||||
|
github.com/tidwall/match v1.1.1/go.mod h1:eRSPERbgtNPcGhD8UCthc6PmLEQXEWd3PRB5JTxsfmM=
|
||||||
|
github.com/tidwall/pretty v1.2.0/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/pretty v1.2.1 h1:qjsOFOWWQl+N3RsoF5/ssm1pHmJJwhjlSbZ51I6wMl4=
|
||||||
|
github.com/tidwall/pretty v1.2.1/go.mod h1:ITEVvHYasfjBbM0u2Pg8T2nJnzm8xPwvNhhsoaGGjNU=
|
||||||
|
github.com/tidwall/sjson v1.2.5 h1:kLy8mja+1c9jlljvWTlSazM7cKDRfJuR/bOJhcY5NcY=
|
||||||
|
github.com/tidwall/sjson v1.2.5/go.mod h1:Fvgq9kS/6ociJEDnK0Fk1cpYF4FIW6ZF7LAe+6jwd28=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||||
|
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||||
|
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||||
|
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/u-root/uio v0.0.0-20240224005618-d2acac8f3701 h1:pyC9PaHYZFgEKFdlp3G8RaCKgVpHZnecvArXvPXcFkM=
|
||||||
|
github.com/u-root/uio v0.0.0-20240224005618-d2acac8f3701/go.mod h1:P3a5rG4X7tI17Nn3aOIAYr5HbIMukwXG0urG0WuL8OA=
|
||||||
|
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||||
|
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||||
|
github.com/vishvananda/netns v0.0.0-20200728191858-db3c7e526aae/go.mod h1:DD4vA1DwXk04H54A1oHXtwZmA0grkVMdPxx/VGLCah0=
|
||||||
|
github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zdEY=
|
||||||
|
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||||
|
github.com/x448/float16 v0.8.4 h1:qLwI1I70+NjRFUR3zs1JPUCgaCXSh3SW62uAKT1mSBM=
|
||||||
|
github.com/x448/float16 v0.8.4/go.mod h1:14CWIYCyZA/cWjXOioeEpHeN/83MdbZDRQHoFcYsOfg=
|
||||||
|
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/zeebo/assert v1.1.0 h1:hU1L1vLTHsnO8x8c9KAR5GmM5QscxHg5RNU5z5qbUWY=
|
||||||
|
github.com/zeebo/assert v1.1.0/go.mod h1:Pq9JiuJQpG8JLJdtkwrJESF0Foym2/D9XMU5ciN/wJ0=
|
||||||
|
github.com/zeebo/blake3 v0.2.4 h1:KYQPkhpRtcqh0ssGYcKLG1JYvddkEA8QwCM/yBqhaZI=
|
||||||
|
github.com/zeebo/blake3 v0.2.4/go.mod h1:7eeQ6d2iXWRGF6npfaxl2CU+xy2Fjo2gxeyZGCRUjcE=
|
||||||
|
github.com/zeebo/pcg v1.0.1 h1:lyqfGeWiv4ahac6ttHs+I5hwtH/+1mrhlCtVNQM2kHo=
|
||||||
|
github.com/zeebo/pcg v1.0.1/go.mod h1:09F0S9iiKrwn9rlI5yjLkmrug154/YRW6KnnXVDM/l4=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0 h1:yXUhImUjjAInNcpTcAlPHiT7bIXhshCTL3jVBkF3xaE=
|
||||||
|
go.mongodb.org/mongo-driver/v2 v2.5.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||||
|
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||||
|
go.opentelemetry.io/otel v1.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
|
||||||
|
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
|
||||||
|
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
|
||||||
|
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
|
||||||
|
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
|
||||||
|
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
|
||||||
|
go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto=
|
||||||
|
go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE=
|
||||||
|
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||||
|
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||||
|
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.27.1 h1:08RqriUEv8+ArZRYSTXy1LeBScaMpVSTBhCeaZYfMYc=
|
||||||
|
go.uber.org/zap v1.27.1/go.mod h1:GB2qFLM7cTU87MWRP2mPIjqfIDnGu+VIO4V/SdhGo2E=
|
||||||
|
go.uber.org/zap/exp v0.3.0 h1:6JYzdifzYkGmTdRR59oYH+Ng7k49H9qVpWwNSsGJj3U=
|
||||||
|
go.uber.org/zap/exp v0.3.0/go.mod h1:5I384qq7XGxYyByIhHm6jg5CHkGY0nsTfbDLgDDlgJQ=
|
||||||
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745 h1:Tl++JLUCe4sxGu8cTpDzRLd3tN7US4hOxG5YpKCzkek=
|
||||||
|
go4.org/mem v0.0.0-20240501181205-ae6ca9944745/go.mod h1:reUoABIJ9ikfM5sgtSF3Wushcza7+WeD01VB9Lirh3g=
|
||||||
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||||
|
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||||
|
golang.org/x/arch v0.22.0 h1:c/Zle32i5ttqRXjdLyyHZESLD/bB90DCU1g9l/0YBDI=
|
||||||
|
golang.org/x/arch v0.22.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||||
|
golang.org/x/crypto v0.0.0-20210513164829-c07d793c2f9a/go.mod h1:P+XmwS30IXTQdn5tA2iutPOUgjI07+tq3H3K9MVA1s8=
|
||||||
|
golang.org/x/crypto v0.48.0 h1:/VRzVqiRSggnhY7gNRxPauEQ5Drw9haKdM0jqfcCFts=
|
||||||
|
golang.org/x/crypto v0.48.0/go.mod h1:r0kV5h3qnFPlQnBSrULhlsRfryS2pmewsg+XfMgkVos=
|
||||||
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93 h1:fQsdNF2N+/YewlRZiricy4P1iimyPKZ/xwniHj8Q2a0=
|
||||||
|
golang.org/x/exp v0.0.0-20251219203646-944ab1f22d93/go.mod h1:EPRbTFwzwjXj9NpYyyrvenVh9Y+GFeEvMNh7Xuz7xgU=
|
||||||
|
golang.org/x/image v0.27.0 h1:C8gA4oWU/tKkdCfYT6T2u4faJu3MeNS5O8UPWlPF61w=
|
||||||
|
golang.org/x/image v0.27.0/go.mod h1:xbdrClrAUway1MUTEZDq9mz/UpRwYAkFFNUslZtcB+g=
|
||||||
|
golang.org/x/mod v0.33.0 h1:tHFzIWbBifEmbwtGz65eaWyGiGZatSrT9prnU8DbVL8=
|
||||||
|
golang.org/x/mod v0.33.0/go.mod h1:swjeQEj+6r7fODbD2cqrnje9PnziFuw4bmLbBZFrQ5w=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
|
||||||
|
golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo=
|
||||||
|
golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y=
|
||||||
|
golang.org/x/oauth2 v0.34.0 h1:hqK/t4AKgbqWkdkcAeI8XLmbK+4m4G5YeQRrmiotGlw=
|
||||||
|
golang.org/x/oauth2 v0.34.0/go.mod h1:lzm5WQJQwKZ3nwavOZ3IS5Aulzxi68dUSgRHujetwEA=
|
||||||
|
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
|
||||||
|
golang.org/x/sync v0.20.0 h1:e0PTpb7pjO8GAtTs2dQ6jYa5BWYlMuX047Dco/pItO4=
|
||||||
|
golang.org/x/sync v0.20.0/go.mod h1:9xrNwdLfx4jkKbNva9FpL6vEN7evnE43NNNJQ2LF3+0=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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-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-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.41.0 h1:Ivj+2Cp/ylzLiEU89QhWblYnOE9zerudt9Ftecq2C6k=
|
||||||
|
golang.org/x/sys v0.41.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
golang.org/x/term v0.40.0 h1:36e4zGLqU4yhjlmxEaagx2KuYbJq3EwY8K943ZsHcvg=
|
||||||
|
golang.org/x/term v0.40.0/go.mod h1:w2P8uVp06p2iyKKuvXIm7N/y0UCRt3UfJTfZ7oOpglM=
|
||||||
|
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.35.0 h1:JOVx6vVDFokkpaq1AEptVzLTpDe9KGpj5tR4/X+ybL8=
|
||||||
|
golang.org/x/text v0.35.0/go.mod h1:khi/HExzZJ2pGnjenulevKNX1W67CUy0AsXcNubPGCA=
|
||||||
|
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||||
|
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.42.0 h1:uNgphsn75Tdz5Ji2q36v/nsFSfR/9BRFvqhGBaJGd5k=
|
||||||
|
golang.org/x/tools v0.42.0/go.mod h1:Ma6lCIwGZvHK6XtgbswSoWroEkhugApmsXyrUmBhfr0=
|
||||||
|
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/wgctrl v0.0.0-20241231184526-a9ab2273dd10 h1:3GDAcqdIg1ozBNLgPy4SLT84nfcBjr6rhGtXYtrkWLU=
|
||||||
|
golang.zx2c4.com/wireguard/wgctrl v0.0.0-20241231184526-a9ab2273dd10/go.mod h1:T97yPqesLiNrOYxkwmhMI0ZIlJDm+p0PMR8eRVeR5tQ=
|
||||||
|
golang.zx2c4.com/wireguard/windows v0.5.3 h1:On6j2Rpn3OEMXqBq00QEDC7bWSZrPIHKIus8eIuExIE=
|
||||||
|
golang.zx2c4.com/wireguard/windows v0.5.3/go.mod h1:9TEe8TJmtwyQebdFwAkEWOPr3prrtqm+REGFifP60hI=
|
||||||
|
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||||
|
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||||
|
google.golang.org/grpc v1.79.3 h1:sybAEdRIEtvcD68Gx7dmnwjZKlyfuc61Dyo9pGXXkKE=
|
||||||
|
google.golang.org/grpc v1.79.3/go.mod h1:KmT0Kjez+0dde/v2j9vzwoAScgEPx/Bw1CYChhHLrHQ=
|
||||||
|
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
|
||||||
|
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||||
|
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/yaml.v2 v2.2.8 h1:obN1ZagJSUGI0Ek/LBmuj4SNLPfIny3KsKFopxRdj10=
|
||||||
|
gopkg.in/yaml.v2 v2.2.8/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=
|
||||||
|
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||||
|
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||||
|
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||||
|
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||||
|
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||||
|
lukechampine.com/blake3 v1.4.1/go.mod h1:QFosUxmjB8mnrWFSNwKmvxHpfY72bmD2tQ0kBMM3kwo=
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.4.0 h1:H2g08FrTvSFKUj+D309j1DPfk5APnIdAQAB8aEykJ5k=
|
||||||
|
software.sslmate.com/src/go-pkcs12 v0.4.0/go.mod h1:Qiz0EyvDRJjjxGyUQa2cCNZn/wMyzrRJ/qcDXOQazLI=
|
||||||
188
install.sh
Executable file
188
install.sh
Executable file
@@ -0,0 +1,188 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
red='\033[0;31m'
|
||||||
|
green='\033[0;32m'
|
||||||
|
yellow='\033[0;33m'
|
||||||
|
plain='\033[0m'
|
||||||
|
|
||||||
|
cur_dir=$(pwd)
|
||||||
|
|
||||||
|
# check root
|
||||||
|
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
|
||||||
|
|
||||||
|
# Check OS and set release variable
|
||||||
|
if [[ -f /etc/os-release ]]; then
|
||||||
|
source /etc/os-release
|
||||||
|
release=$ID
|
||||||
|
elif [[ -f /usr/lib/os-release ]]; then
|
||||||
|
source /usr/lib/os-release
|
||||||
|
release=$ID
|
||||||
|
else
|
||||||
|
echo "Failed to check the system OS, please contact the author!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo "The OS release is: $release"
|
||||||
|
|
||||||
|
arch() {
|
||||||
|
case "$(uname -m)" in
|
||||||
|
x86_64 | x64 | amd64) echo 'amd64' ;;
|
||||||
|
i*86 | x86) echo '386' ;;
|
||||||
|
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
|
||||||
|
armv7* | armv7 | arm) echo 'armv7' ;;
|
||||||
|
armv6* | armv6) echo 'armv6' ;;
|
||||||
|
armv5* | armv5) echo 'armv5' ;;
|
||||||
|
s390x) echo 's390x' ;;
|
||||||
|
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
echo "arch: $(arch)"
|
||||||
|
|
||||||
|
install_base() {
|
||||||
|
case "${release}" in
|
||||||
|
centos | almalinux | rocky | oracle)
|
||||||
|
yum -y update && yum install -y -q wget curl tar tzdata
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
dnf -y update && dnf install -y -q wget curl tar tzdata
|
||||||
|
;;
|
||||||
|
arch | manjaro | parch)
|
||||||
|
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
|
||||||
|
;;
|
||||||
|
opensuse-tumbleweed)
|
||||||
|
zypper refresh && zypper -q install -y wget curl tar timezone
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
apt-get update && apt-get install -y -q wget curl tar tzdata
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
config_after_install() {
|
||||||
|
echo -e "${yellow}Migration... ${plain}"
|
||||||
|
/usr/local/s-ui/sui migrate
|
||||||
|
|
||||||
|
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
|
||||||
|
read -p "Do you want to continue with the modification [y/n]? ": config_confirm
|
||||||
|
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
|
||||||
|
echo -e "Enter the ${yellow}panel port${plain} (leave blank for existing/default value):"
|
||||||
|
read config_port
|
||||||
|
echo -e "Enter the ${yellow}panel path${plain} (leave blank for existing/default value):"
|
||||||
|
read config_path
|
||||||
|
|
||||||
|
# Sub configuration
|
||||||
|
echo -e "Enter the ${yellow}subscription port${plain} (leave blank for existing/default value):"
|
||||||
|
read config_subPort
|
||||||
|
echo -e "Enter the ${yellow}subscription path${plain} (leave blank for existing/default value):"
|
||||||
|
read config_subPath
|
||||||
|
|
||||||
|
# Set configs
|
||||||
|
echo -e "${yellow}Initializing, please wait...${plain}"
|
||||||
|
params=""
|
||||||
|
[ -z "$config_port" ] || params="$params -port $config_port"
|
||||||
|
[ -z "$config_path" ] || params="$params -path $config_path"
|
||||||
|
[ -z "$config_subPort" ] || params="$params -subPort $config_subPort"
|
||||||
|
[ -z "$config_subPath" ] || params="$params -subPath $config_subPath"
|
||||||
|
/usr/local/s-ui/sui setting ${params}
|
||||||
|
|
||||||
|
read -p "Do you want to change admin credentials [y/n]? ": admin_confirm
|
||||||
|
if [[ "${admin_confirm}" == "y" || "${admin_confirm}" == "Y" ]]; then
|
||||||
|
# First admin credentials
|
||||||
|
read -p "Please set up your username:" config_account
|
||||||
|
read -p "Please set up your password:" config_password
|
||||||
|
|
||||||
|
# Set credentials
|
||||||
|
echo -e "${yellow}Initializing, please wait...${plain}"
|
||||||
|
/usr/local/s-ui/sui admin -username ${config_account} -password ${config_password}
|
||||||
|
else
|
||||||
|
echo -e "${yellow}Your current admin credentials: ${plain}"
|
||||||
|
/usr/local/s-ui/sui admin -show
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
echo -e "${red}cancel...${plain}"
|
||||||
|
if [[ ! -f "/usr/local/s-ui/db/s-ui.db" ]]; then
|
||||||
|
local usernameTemp=$(head -c 6 /dev/urandom | base64)
|
||||||
|
local passwordTemp=$(head -c 6 /dev/urandom | base64)
|
||||||
|
echo -e "this is a fresh installation,will generate random login info for security concerns:"
|
||||||
|
echo -e "###############################################"
|
||||||
|
echo -e "${green}username:${usernameTemp}${plain}"
|
||||||
|
echo -e "${green}password:${passwordTemp}${plain}"
|
||||||
|
echo -e "###############################################"
|
||||||
|
echo -e "${red}if you forgot your login info,you can type ${green}s-ui${red} for configuration menu${plain}"
|
||||||
|
/usr/local/s-ui/sui admin -username ${usernameTemp} -password ${passwordTemp}
|
||||||
|
else
|
||||||
|
echo -e "${red} this is your upgrade,will keep old settings,if you forgot your login info,you can type ${green}s-ui${red} for configuration menu${plain}"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
prepare_services() {
|
||||||
|
if [[ -f "/etc/systemd/system/sing-box.service" ]]; then
|
||||||
|
echo -e "${yellow}Stopping sing-box service... ${plain}"
|
||||||
|
systemctl stop sing-box
|
||||||
|
rm -f /usr/local/s-ui/bin/sing-box /usr/local/s-ui/bin/runSingbox.sh /usr/local/s-ui/bin/signal
|
||||||
|
fi
|
||||||
|
if [[ -e "/usr/local/s-ui/bin" ]]; then
|
||||||
|
echo -e "###############################################################"
|
||||||
|
echo -e "${green}/usr/local/s-ui/bin${red} directory exists yet!"
|
||||||
|
echo -e "Please check the content and delete it manually after migration ${plain}"
|
||||||
|
echo -e "###############################################################"
|
||||||
|
fi
|
||||||
|
systemctl daemon-reload
|
||||||
|
}
|
||||||
|
|
||||||
|
install_s-ui() {
|
||||||
|
cd /tmp/
|
||||||
|
|
||||||
|
if [ $# == 0 ]; then
|
||||||
|
last_version=$(curl -Ls "https://api.github.com/repos/alireza0/s-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||||
|
if [[ ! -n "$last_version" ]]; then
|
||||||
|
echo -e "${red}Failed to fetch s-ui version, it maybe due to Github API restrictions, please try it later${plain}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
echo -e "Got s-ui latest version: ${last_version}, beginning the installation..."
|
||||||
|
wget -N --no-check-certificate -O /tmp/s-ui-linux-$(arch).tar.gz https://github.com/alireza0/s-ui/releases/download/${last_version}/s-ui-linux-$(arch).tar.gz
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
echo -e "${red}Downloading s-ui failed, please be sure that your server can access Github ${plain}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
last_version=$1
|
||||||
|
url="https://github.com/alireza0/s-ui/releases/download/${last_version}/s-ui-linux-$(arch).tar.gz"
|
||||||
|
echo -e "Beginning the install s-ui v$1"
|
||||||
|
wget -N --no-check-certificate -O /tmp/s-ui-linux-$(arch).tar.gz ${url}
|
||||||
|
if [[ $? -ne 0 ]]; then
|
||||||
|
echo -e "${red}download s-ui v$1 failed,please check the version exists${plain}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ -e /usr/local/s-ui/ ]]; then
|
||||||
|
systemctl stop s-ui
|
||||||
|
fi
|
||||||
|
|
||||||
|
tar zxvf s-ui-linux-$(arch).tar.gz
|
||||||
|
rm s-ui-linux-$(arch).tar.gz -f
|
||||||
|
|
||||||
|
chmod +x s-ui/sui s-ui/s-ui.sh
|
||||||
|
cp s-ui/s-ui.sh /usr/bin/s-ui
|
||||||
|
cp -rf s-ui /usr/local/
|
||||||
|
cp -f s-ui/*.service /etc/systemd/system/
|
||||||
|
rm -rf s-ui
|
||||||
|
|
||||||
|
config_after_install
|
||||||
|
prepare_services
|
||||||
|
|
||||||
|
systemctl enable s-ui --now
|
||||||
|
|
||||||
|
echo -e "${green}s-ui v${last_version}${plain} installation finished, it is up and running now..."
|
||||||
|
echo -e "You may access the Panel with following URL(s):${green}"
|
||||||
|
/usr/local/s-ui/sui uri
|
||||||
|
echo -e "${plain}"
|
||||||
|
echo -e ""
|
||||||
|
s-ui help
|
||||||
|
}
|
||||||
|
|
||||||
|
echo -e "${green}Executing...${plain}"
|
||||||
|
install_base
|
||||||
|
install_s-ui $1
|
||||||
128
logger/logger.go
Normal file
128
logger/logger.go
Normal file
@@ -0,0 +1,128 @@
|
|||||||
|
package logger
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/op/go-logging"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
logger *logging.Logger
|
||||||
|
logBuffer []struct {
|
||||||
|
time string
|
||||||
|
level logging.Level
|
||||||
|
log string
|
||||||
|
}
|
||||||
|
)
|
||||||
|
|
||||||
|
func InitLogger(level logging.Level) {
|
||||||
|
newLogger := logging.MustGetLogger("s-ui")
|
||||||
|
var err error
|
||||||
|
var backend logging.Backend
|
||||||
|
var format logging.Formatter
|
||||||
|
|
||||||
|
_, inContainer := os.LookupEnv("container")
|
||||||
|
if !inContainer {
|
||||||
|
if _, statErr := os.Stat("/.dockerenv"); statErr == nil {
|
||||||
|
inContainer = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inContainer {
|
||||||
|
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||||
|
format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
|
||||||
|
} else {
|
||||||
|
backend, err = logging.NewSyslogBackend("")
|
||||||
|
if err != nil {
|
||||||
|
fmt.Println("Unable to use syslog: " + err.Error())
|
||||||
|
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
|
||||||
|
} else {
|
||||||
|
format = logging.MustStringFormatter(`%{level} - %{message}`)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
backendFormatter := logging.NewBackendFormatter(backend, format)
|
||||||
|
backendLeveled := logging.AddModuleLevel(backendFormatter)
|
||||||
|
backendLeveled.SetLevel(level, "s-ui")
|
||||||
|
newLogger.SetBackend(backendLeveled)
|
||||||
|
|
||||||
|
logger = newLogger
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogger() *logging.Logger {
|
||||||
|
return logger
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debug(args ...interface{}) {
|
||||||
|
logger.Debug(args...)
|
||||||
|
addToBuffer("DEBUG", fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Debugf(format string, args ...interface{}) {
|
||||||
|
logger.Debugf(format, args...)
|
||||||
|
addToBuffer("DEBUG", fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Info(args ...interface{}) {
|
||||||
|
logger.Info(args...)
|
||||||
|
addToBuffer("INFO", fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Infof(format string, args ...interface{}) {
|
||||||
|
logger.Infof(format, args...)
|
||||||
|
addToBuffer("INFO", fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warning(args ...interface{}) {
|
||||||
|
logger.Warning(args...)
|
||||||
|
addToBuffer("WARNING", fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Warningf(format string, args ...interface{}) {
|
||||||
|
logger.Warningf(format, args...)
|
||||||
|
addToBuffer("WARNING", fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Error(args ...interface{}) {
|
||||||
|
logger.Error(args...)
|
||||||
|
addToBuffer("ERROR", fmt.Sprint(args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func Errorf(format string, args ...interface{}) {
|
||||||
|
logger.Errorf(format, args...)
|
||||||
|
addToBuffer("ERROR", fmt.Sprintf(format, args...))
|
||||||
|
}
|
||||||
|
|
||||||
|
func addToBuffer(level string, newLog string) {
|
||||||
|
t := time.Now()
|
||||||
|
if len(logBuffer) >= 10240 {
|
||||||
|
logBuffer = logBuffer[1:]
|
||||||
|
}
|
||||||
|
|
||||||
|
logLevel, _ := logging.LogLevel(level)
|
||||||
|
logBuffer = append(logBuffer, struct {
|
||||||
|
time string
|
||||||
|
level logging.Level
|
||||||
|
log string
|
||||||
|
}{
|
||||||
|
time: t.Format("2006/01/02 15:04:05"),
|
||||||
|
level: logLevel,
|
||||||
|
log: newLog,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetLogs(c int, level string) []string {
|
||||||
|
var output []string
|
||||||
|
logLevel, _ := logging.LogLevel(level)
|
||||||
|
|
||||||
|
for i := len(logBuffer) - 1; i >= 0 && len(output) <= c; i-- {
|
||||||
|
if logBuffer[i].level <= logLevel {
|
||||||
|
output = append(output, fmt.Sprintf("%s %s - %s", logBuffer[i].time, logBuffer[i].level, logBuffer[i].log))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return output
|
||||||
|
}
|
||||||
49
main.go
Normal file
49
main.go
Normal file
@@ -0,0 +1,49 @@
|
|||||||
|
package main
|
||||||
|
|
||||||
|
import (
|
||||||
|
"log"
|
||||||
|
"os"
|
||||||
|
"os/signal"
|
||||||
|
"syscall"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/app"
|
||||||
|
"github.com/alireza0/s-ui/cmd"
|
||||||
|
)
|
||||||
|
|
||||||
|
func runApp() {
|
||||||
|
app := app.NewApp()
|
||||||
|
|
||||||
|
err := app.Init()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
err = app.Start()
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
|
||||||
|
sigCh := make(chan os.Signal, 1)
|
||||||
|
// Trap shutdown signals
|
||||||
|
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
|
||||||
|
for {
|
||||||
|
sig := <-sigCh
|
||||||
|
|
||||||
|
switch sig {
|
||||||
|
case syscall.SIGHUP:
|
||||||
|
app.RestartApp()
|
||||||
|
default:
|
||||||
|
app.Stop()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func main() {
|
||||||
|
if len(os.Args) < 2 {
|
||||||
|
runApp()
|
||||||
|
return
|
||||||
|
} else {
|
||||||
|
cmd.ParseCmd()
|
||||||
|
}
|
||||||
|
}
|
||||||
25
middleware/domainValidator.go
Normal file
25
middleware/domainValidator.go
Normal file
@@ -0,0 +1,25 @@
|
|||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
func DomainValidator(domain string) gin.HandlerFunc {
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
host := c.Request.Host
|
||||||
|
if colonIndex := strings.LastIndex(host, ":"); colonIndex != -1 {
|
||||||
|
host, _, _ = net.SplitHostPort(c.Request.Host)
|
||||||
|
}
|
||||||
|
|
||||||
|
if host != domain {
|
||||||
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
c.Next()
|
||||||
|
}
|
||||||
|
}
|
||||||
67
network/auto_https_conn.go
Normal file
67
network/auto_https_conn.go
Normal file
@@ -0,0 +1,67 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bufio"
|
||||||
|
"bytes"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"sync"
|
||||||
|
)
|
||||||
|
|
||||||
|
type AutoHttpsConn struct {
|
||||||
|
net.Conn
|
||||||
|
|
||||||
|
firstBuf []byte
|
||||||
|
bufStart int
|
||||||
|
|
||||||
|
readRequestOnce sync.Once
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAutoHttpsConn(conn net.Conn) net.Conn {
|
||||||
|
return &AutoHttpsConn{
|
||||||
|
Conn: conn,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AutoHttpsConn) readRequest() bool {
|
||||||
|
c.firstBuf = make([]byte, 2048)
|
||||||
|
n, err := c.Conn.Read(c.firstBuf)
|
||||||
|
c.firstBuf = c.firstBuf[:n]
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
reader := bytes.NewReader(c.firstBuf)
|
||||||
|
bufReader := bufio.NewReader(reader)
|
||||||
|
request, err := http.ReadRequest(bufReader)
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
resp := http.Response{
|
||||||
|
Header: http.Header{},
|
||||||
|
}
|
||||||
|
resp.StatusCode = http.StatusTemporaryRedirect
|
||||||
|
location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI)
|
||||||
|
resp.Header.Set("Location", location)
|
||||||
|
resp.Write(c.Conn)
|
||||||
|
c.Close()
|
||||||
|
c.firstBuf = nil
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
|
||||||
|
func (c *AutoHttpsConn) Read(buf []byte) (int, error) {
|
||||||
|
c.readRequestOnce.Do(func() {
|
||||||
|
c.readRequest()
|
||||||
|
})
|
||||||
|
|
||||||
|
if c.firstBuf != nil {
|
||||||
|
n := copy(buf, c.firstBuf[c.bufStart:])
|
||||||
|
c.bufStart += n
|
||||||
|
if c.bufStart >= len(c.firstBuf) {
|
||||||
|
c.firstBuf = nil
|
||||||
|
}
|
||||||
|
return n, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
return c.Conn.Read(buf)
|
||||||
|
}
|
||||||
21
network/auto_https_listener.go
Normal file
21
network/auto_https_listener.go
Normal file
@@ -0,0 +1,21 @@
|
|||||||
|
package network
|
||||||
|
|
||||||
|
import "net"
|
||||||
|
|
||||||
|
type AutoHttpsListener struct {
|
||||||
|
net.Listener
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAutoHttpsListener(listener net.Listener) net.Listener {
|
||||||
|
return &AutoHttpsListener{
|
||||||
|
Listener: listener,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (l *AutoHttpsListener) Accept() (net.Conn, error) {
|
||||||
|
conn, err := l.Listener.Accept()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return NewAutoHttpsConn(conn), nil
|
||||||
|
}
|
||||||
2
runSUI.sh
Executable file
2
runSUI.sh
Executable file
@@ -0,0 +1,2 @@
|
|||||||
|
./build.sh
|
||||||
|
SUI_DB_FOLDER="db" SUI_DEBUG=true ./sui
|
||||||
14
s-ui.service
Normal file
14
s-ui.service
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
[Unit]
|
||||||
|
Description=s-ui Service
|
||||||
|
After=network.target
|
||||||
|
Wants=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
Type=simple
|
||||||
|
WorkingDirectory=/usr/local/s-ui/
|
||||||
|
ExecStart=/usr/local/s-ui/sui
|
||||||
|
Restart=on-failure
|
||||||
|
RestartSec=10s
|
||||||
|
|
||||||
|
[Install]
|
||||||
|
WantedBy=multi-user.target
|
||||||
934
s-ui.sh
Normal file
934
s-ui.sh
Normal file
@@ -0,0 +1,934 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
red='\033[0;31m'
|
||||||
|
green='\033[0;32m'
|
||||||
|
yellow='\033[0;33m'
|
||||||
|
plain='\033[0m'
|
||||||
|
|
||||||
|
function LOGD() {
|
||||||
|
echo -e "${yellow}[DEG] $* ${plain}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function LOGE() {
|
||||||
|
echo -e "${red}[ERR] $* ${plain}"
|
||||||
|
}
|
||||||
|
|
||||||
|
function LOGI() {
|
||||||
|
echo -e "${green}[INF] $* ${plain}"
|
||||||
|
}
|
||||||
|
|
||||||
|
[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1
|
||||||
|
|
||||||
|
if [[ -f /etc/os-release ]]; then
|
||||||
|
source /etc/os-release
|
||||||
|
release=$ID
|
||||||
|
elif [[ -f /usr/lib/os-release ]]; then
|
||||||
|
source /usr/lib/os-release
|
||||||
|
release=$ID
|
||||||
|
else
|
||||||
|
echo "Failed to check the system OS, please contact the author!" >&2
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
echo "The OS release is: $release"
|
||||||
|
|
||||||
|
confirm() {
|
||||||
|
if [[ $# > 1 ]]; then
|
||||||
|
echo && read -p "$1 [Default$2]: " temp
|
||||||
|
if [[ x"${temp}" == x"" ]]; then
|
||||||
|
temp=$2
|
||||||
|
fi
|
||||||
|
else
|
||||||
|
read -p "$1 [y/n]: " temp
|
||||||
|
fi
|
||||||
|
if [[ x"${temp}" == x"y" || x"${temp}" == x"Y" ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
confirm_restart() {
|
||||||
|
confirm "Restart the ${1} service" "y"
|
||||||
|
if [[ $? == 0 ]]; then
|
||||||
|
restart
|
||||||
|
else
|
||||||
|
show_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
before_show_menu() {
|
||||||
|
echo && echo -n -e "${yellow}Press enter to return to the main menu: ${plain}" && read temp
|
||||||
|
show_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
install() {
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/main/install.sh)
|
||||||
|
if [[ $? == 0 ]]; then
|
||||||
|
if [[ $# == 0 ]]; then
|
||||||
|
start
|
||||||
|
else
|
||||||
|
start 0
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
update() {
|
||||||
|
confirm "This function will forcefully reinstall the latest version, and the data will not be lost. Do you want to continue?" "n"
|
||||||
|
if [[ $? != 0 ]]; then
|
||||||
|
LOGE "Cancelled"
|
||||||
|
if [[ $# == 0 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/main/install.sh)
|
||||||
|
if [[ $? == 0 ]]; then
|
||||||
|
LOGI "Update is complete, Panel has automatically restarted "
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
custom_version() {
|
||||||
|
echo "Enter the panel version (like 0.0.1):"
|
||||||
|
read panel_version
|
||||||
|
|
||||||
|
if [ -z "$panel_version" ]; then
|
||||||
|
echo "Panel version cannot be empty. Exiting."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
download_link="https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh"
|
||||||
|
|
||||||
|
install_command="bash <(curl -Ls $download_link) $panel_version"
|
||||||
|
|
||||||
|
echo "Downloading and installing panel version $panel_version..."
|
||||||
|
eval $install_command
|
||||||
|
}
|
||||||
|
|
||||||
|
uninstall() {
|
||||||
|
confirm "Are you sure you want to uninstall the panel?" "n"
|
||||||
|
if [[ $? != 0 ]]; then
|
||||||
|
if [[ $# == 0 ]]; then
|
||||||
|
show_menu
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
systemctl stop s-ui
|
||||||
|
systemctl disable s-ui
|
||||||
|
rm /etc/systemd/system/s-ui.service -f
|
||||||
|
systemctl daemon-reload
|
||||||
|
systemctl reset-failed
|
||||||
|
rm /etc/s-ui/ -rf
|
||||||
|
rm /usr/local/s-ui/ -rf
|
||||||
|
|
||||||
|
echo ""
|
||||||
|
echo -e "Uninstalled Successfully, If you want to remove this script, then after exiting the script run ${green}rm /usr/local/s-ui -f${plain} to delete it."
|
||||||
|
echo ""
|
||||||
|
|
||||||
|
if [[ $# == 0 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_admin() {
|
||||||
|
echo "It is not recommended to set admin's credentials to default!"
|
||||||
|
confirm "Are you sure you want to reset admin's credentials to default ?" "n"
|
||||||
|
if [[ $? == 0 ]]; then
|
||||||
|
/usr/local/s-ui/sui admin -reset
|
||||||
|
fi
|
||||||
|
before_show_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
set_admin() {
|
||||||
|
echo "It is not recommended to set admin's credentials to a complex text."
|
||||||
|
read -p "Please set up your username:" config_account
|
||||||
|
read -p "Please set up your password:" config_password
|
||||||
|
/usr/local/s-ui/sui admin -username ${config_account} -password ${config_password}
|
||||||
|
before_show_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
view_admin() {
|
||||||
|
/usr/local/s-ui/sui admin -show
|
||||||
|
before_show_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
reset_setting() {
|
||||||
|
confirm "Are you sure you want to reset settings to default ?" "n"
|
||||||
|
if [[ $? == 0 ]]; then
|
||||||
|
/usr/local/s-ui/sui setting -reset
|
||||||
|
fi
|
||||||
|
before_show_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
set_setting() {
|
||||||
|
echo -e "Enter the ${yellow}panel port${plain} (leave blank for existing/default value):"
|
||||||
|
read config_port
|
||||||
|
echo -e "Enter the ${yellow}panel path${plain} (leave blank for existing/default value):"
|
||||||
|
read config_path
|
||||||
|
|
||||||
|
echo -e "Enter the ${yellow}subscription port${plain} (leave blank for existing/default value):"
|
||||||
|
read config_subPort
|
||||||
|
echo -e "Enter the ${yellow}subscription path${plain} (leave blank for existing/default value):"
|
||||||
|
read config_subPath
|
||||||
|
|
||||||
|
echo -e "${yellow}Initializing, please wait...${plain}"
|
||||||
|
params=""
|
||||||
|
[ -z "$config_port" ] || params="$params -port $config_port"
|
||||||
|
[ -z "$config_path" ] || params="$params -path $config_path"
|
||||||
|
[ -z "$config_subPort" ] || params="$params -subPort $config_subPort"
|
||||||
|
[ -z "$config_subPath" ] || params="$params -subPath $config_subPath"
|
||||||
|
/usr/local/s-ui/sui setting ${params}
|
||||||
|
before_show_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
view_setting() {
|
||||||
|
/usr/local/s-ui/sui setting -show
|
||||||
|
view_uri
|
||||||
|
before_show_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
view_uri() {
|
||||||
|
info=$(/usr/local/s-ui/sui uri)
|
||||||
|
if [[ $? != 0 ]]; then
|
||||||
|
LOGE "Get current uri error"
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
LOGI "You may access the Panel with following URL(s):"
|
||||||
|
echo -e "${green}${info}${plain}"
|
||||||
|
}
|
||||||
|
|
||||||
|
start() {
|
||||||
|
check_status $1
|
||||||
|
if [[ $? == 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
LOGI -e "${1} is running, No need to start again, If you need to restart, please select restart"
|
||||||
|
else
|
||||||
|
systemctl start $1
|
||||||
|
sleep 2
|
||||||
|
check_status $1
|
||||||
|
if [[ $? == 0 ]]; then
|
||||||
|
LOGI "${1} Started Successfully"
|
||||||
|
else
|
||||||
|
LOGE "Failed to start ${1}, Probably because it takes longer than two seconds to start, Please check the log information later"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $# == 1 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
stop() {
|
||||||
|
check_status $1
|
||||||
|
if [[ $? == 1 ]]; then
|
||||||
|
echo ""
|
||||||
|
LOGI "${1} stopped, No need to stop again!"
|
||||||
|
else
|
||||||
|
systemctl stop $1
|
||||||
|
sleep 2
|
||||||
|
check_status
|
||||||
|
if [[ $? == 1 ]]; then
|
||||||
|
LOGI "${1} stopped successfully"
|
||||||
|
else
|
||||||
|
LOGE "Failed to stop ${1}, Probably because the stop time exceeds two seconds, Please check the log information later"
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $# == 1 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
restart() {
|
||||||
|
systemctl restart $1
|
||||||
|
sleep 2
|
||||||
|
check_status $1
|
||||||
|
if [[ $? == 0 ]]; then
|
||||||
|
LOGI "${1} Restarted successfully"
|
||||||
|
else
|
||||||
|
LOGE "Failed to restart ${1}, Probably because it takes longer than two seconds to start, Please check the log information later"
|
||||||
|
fi
|
||||||
|
if [[ $# == 1 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
status() {
|
||||||
|
systemctl status s-ui -l
|
||||||
|
if [[ $# == 0 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
enable() {
|
||||||
|
systemctl enable $1
|
||||||
|
if [[ $? == 0 ]]; then
|
||||||
|
LOGI "Set ${1} to boot automatically on startup successfully"
|
||||||
|
else
|
||||||
|
LOGE "Failed to set ${1} Autostart"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $# == 1 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
disable() {
|
||||||
|
systemctl disable $1
|
||||||
|
if [[ $? == 0 ]]; then
|
||||||
|
LOGI "Autostart ${1} Cancelled successfully"
|
||||||
|
else
|
||||||
|
LOGE "Failed to cancel ${1} autostart"
|
||||||
|
fi
|
||||||
|
|
||||||
|
if [[ $# == 1 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_log() {
|
||||||
|
journalctl -u $1.service -e --no-pager -f
|
||||||
|
if [[ $# == 1 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
update_shell() {
|
||||||
|
wget -O /usr/bin/s-ui -N --no-check-certificate https://github.com/alireza0/s-ui/raw/main/s-ui.sh
|
||||||
|
if [[ $? != 0 ]]; then
|
||||||
|
echo ""
|
||||||
|
LOGE "Failed to download script, Please check whether the machine can connect Github"
|
||||||
|
before_show_menu
|
||||||
|
else
|
||||||
|
chmod +x /usr/bin/s-ui
|
||||||
|
LOGI "Upgrade script succeeded, Please rerun the script" && exit 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_status() {
|
||||||
|
if [[ ! -f "/etc/systemd/system/$1.service" ]]; then
|
||||||
|
return 2
|
||||||
|
fi
|
||||||
|
temp=$(systemctl status "$1" | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1)
|
||||||
|
if [[ x"${temp}" == x"running" ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_enabled() {
|
||||||
|
temp=$(systemctl is-enabled $1)
|
||||||
|
if [[ x"${temp}" == x"enabled" ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_uninstall() {
|
||||||
|
check_status s-ui
|
||||||
|
if [[ $? != 2 ]]; then
|
||||||
|
echo ""
|
||||||
|
LOGE "Panel is already installed, Please do not reinstall"
|
||||||
|
if [[ $# == 0 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_install() {
|
||||||
|
check_status s-ui
|
||||||
|
if [[ $? == 2 ]]; then
|
||||||
|
echo ""
|
||||||
|
LOGE "Please install the panel first"
|
||||||
|
if [[ $# == 0 ]]; then
|
||||||
|
before_show_menu
|
||||||
|
fi
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
return 0
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_status() {
|
||||||
|
check_status $1
|
||||||
|
case $? in
|
||||||
|
0)
|
||||||
|
echo -e "${1} state: ${green}Running${plain}"
|
||||||
|
show_enable_status $1
|
||||||
|
;;
|
||||||
|
1)
|
||||||
|
echo -e "${1} state: ${yellow}Not Running${plain}"
|
||||||
|
show_enable_status $1
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
echo -e "${1} state: ${red}Not Installed${plain}"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
show_enable_status() {
|
||||||
|
check_enabled $1
|
||||||
|
if [[ $? == 0 ]]; then
|
||||||
|
echo -e "Start ${1} automatically: ${green}Yes${plain}"
|
||||||
|
else
|
||||||
|
echo -e "Start ${1} automatically: ${red}No${plain}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
check_s-ui_status() {
|
||||||
|
count=$(ps -ef | grep "sui" | grep -v "grep" | wc -l)
|
||||||
|
if [[ count -ne 0 ]]; then
|
||||||
|
return 0
|
||||||
|
else
|
||||||
|
return 1
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
show_s-ui_status() {
|
||||||
|
check_s-ui_status
|
||||||
|
if [[ $? == 0 ]]; then
|
||||||
|
echo -e "s-ui state: ${green}Running${plain}"
|
||||||
|
else
|
||||||
|
echo -e "s-ui state: ${red}Not Running${plain}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
bbr_menu() {
|
||||||
|
echo -e "${green}\t1.${plain} Enable BBR"
|
||||||
|
echo -e "${green}\t2.${plain} Disable BBR"
|
||||||
|
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||||
|
read -p "Choose an option: " choice
|
||||||
|
case "$choice" in
|
||||||
|
0)
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
1)
|
||||||
|
enable_bbr
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
disable_bbr
|
||||||
|
;;
|
||||||
|
*) echo "Invalid choice" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
disable_bbr() {
|
||||||
|
if ! grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf || ! grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then
|
||||||
|
echo -e "${yellow}BBR is not currently enabled.${plain}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf
|
||||||
|
sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf
|
||||||
|
sysctl -p
|
||||||
|
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "cubic" ]]; then
|
||||||
|
echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}"
|
||||||
|
else
|
||||||
|
echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
enable_bbr() {
|
||||||
|
if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then
|
||||||
|
echo -e "${green}BBR is already enabled!${plain}"
|
||||||
|
exit 0
|
||||||
|
fi
|
||||||
|
case "${release}" in
|
||||||
|
ubuntu | debian | armbian)
|
||||||
|
apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
|
||||||
|
;;
|
||||||
|
centos | almalinux | rocky | oracle)
|
||||||
|
yum -y update && yum -y install ca-certificates
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
dnf -y update && dnf -y install ca-certificates
|
||||||
|
;;
|
||||||
|
arch | manjaro | parch)
|
||||||
|
pacman -Sy --noconfirm ca-certificates
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
echo "net.core.default_qdisc=fq" | tee -a /etc/sysctl.conf
|
||||||
|
echo "net.ipv4.tcp_congestion_control=bbr" | tee -a /etc/sysctl.conf
|
||||||
|
sysctl -p
|
||||||
|
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then
|
||||||
|
echo -e "${green}BBR has been enabled successfully.${plain}"
|
||||||
|
else
|
||||||
|
echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}"
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
install_acme() {
|
||||||
|
cd ~
|
||||||
|
LOGI "install acme..."
|
||||||
|
curl https://get.acme.sh | sh
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOGE "install acme failed"
|
||||||
|
return 1
|
||||||
|
else
|
||||||
|
LOGI "install acme succeed"
|
||||||
|
fi
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl_cert_issue_main() {
|
||||||
|
echo -e "${green}\t1.${plain} Get SSL"
|
||||||
|
echo -e "${green}\t2.${plain} Revoke"
|
||||||
|
echo -e "${green}\t3.${plain} Force Renew"
|
||||||
|
echo -e "${green}\t4.${plain} Self-signed Certificate"
|
||||||
|
read -p "Choose an option: " choice
|
||||||
|
case "$choice" in
|
||||||
|
1) ssl_cert_issue ;;
|
||||||
|
2)
|
||||||
|
local domain=""
|
||||||
|
read -p "Please enter your domain name to revoke the certificate: " domain
|
||||||
|
~/.acme.sh/acme.sh --revoke -d ${domain}
|
||||||
|
LOGI "Certificate revoked"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
local domain=""
|
||||||
|
read -p "Please enter your domain name to forcefully renew an SSL certificate: " domain
|
||||||
|
~/.acme.sh/acme.sh --renew -d ${domain} --force ;;
|
||||||
|
4)
|
||||||
|
generate_self_signed_cert
|
||||||
|
;;
|
||||||
|
*) echo "Invalid choice" ;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl_cert_issue() {
|
||||||
|
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||||
|
echo "acme.sh could not be found. we will install it"
|
||||||
|
install_acme
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOGE "install acme failed, please check logs"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
case "${release}" in
|
||||||
|
ubuntu | debian | armbian)
|
||||||
|
apt update && apt install socat -y
|
||||||
|
;;
|
||||||
|
centos | almalinux | rocky | oracle)
|
||||||
|
yum -y update && yum -y install socat
|
||||||
|
;;
|
||||||
|
fedora)
|
||||||
|
dnf -y update && dnf -y install socat
|
||||||
|
;;
|
||||||
|
arch | manjaro | parch)
|
||||||
|
pacman -Sy --noconfirm socat
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||||
|
exit 1
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOGE "install socat failed, please check logs"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
LOGI "install socat succeed..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
local domain=""
|
||||||
|
read -p "Please enter your domain name:" domain
|
||||||
|
LOGD "your domain is:${domain},check it..."
|
||||||
|
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
|
||||||
|
|
||||||
|
if [ ${currentCert} == ${domain} ]; then
|
||||||
|
local certInfo=$(~/.acme.sh/acme.sh --list)
|
||||||
|
LOGE "system already has certs here,can not issue again,current certs details:"
|
||||||
|
LOGI "$certInfo"
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
LOGI "your domain is ready for issuing cert now..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
certPath="/root/cert/${domain}"
|
||||||
|
if [ ! -d "$certPath" ]; then
|
||||||
|
mkdir -p "$certPath"
|
||||||
|
else
|
||||||
|
rm -rf "$certPath"
|
||||||
|
mkdir -p "$certPath"
|
||||||
|
fi
|
||||||
|
|
||||||
|
local WebPort=80
|
||||||
|
read -p "please choose which port do you use,default will be 80 port:" WebPort
|
||||||
|
if [[ ${WebPort} -gt 65535 || ${WebPort} -lt 1 ]]; then
|
||||||
|
LOGE "your input ${WebPort} is invalid,will use default port"
|
||||||
|
fi
|
||||||
|
LOGI "will use port:${WebPort} to issue certs,please make sure this port is open..."
|
||||||
|
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||||
|
~/.acme.sh/acme.sh --issue -d ${domain} --standalone --httpport ${WebPort}
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOGE "issue certs failed,please check logs"
|
||||||
|
rm -rf ~/.acme.sh/${domain}
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
LOGE "issue certs succeed,installing certs..."
|
||||||
|
fi
|
||||||
|
~/.acme.sh/acme.sh --installcert -d ${domain} \
|
||||||
|
--key-file /root/cert/${domain}/privkey.pem \
|
||||||
|
--fullchain-file /root/cert/${domain}/fullchain.pem
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOGE "install certs failed,exit"
|
||||||
|
rm -rf ~/.acme.sh/${domain}
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
LOGI "install certs succeed,enable auto renew..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
~/.acme.sh/acme.sh --upgrade --auto-upgrade
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOGE "auto renew failed, certs details:"
|
||||||
|
ls -lah cert/*
|
||||||
|
chmod 755 $certPath/*
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
LOGI "auto renew succeed, certs details:"
|
||||||
|
ls -lah cert/*
|
||||||
|
chmod 755 $certPath/*
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
ssl_cert_issue_CF() {
|
||||||
|
echo -E ""
|
||||||
|
LOGD "******Instructions for use******"
|
||||||
|
echo "1) New certificate from Cloudflare"
|
||||||
|
echo "2) Force renew existing Certificates"
|
||||||
|
echo "3) Back to Menu"
|
||||||
|
read -p "Enter your choice [1-3]: " choice
|
||||||
|
|
||||||
|
certPath="/root/cert-CF"
|
||||||
|
|
||||||
|
case $choice in
|
||||||
|
1|2)
|
||||||
|
force_flag=""
|
||||||
|
if [ "$choice" -eq 2 ]; then
|
||||||
|
force_flag="--force"
|
||||||
|
echo "Forcing SSL certificate reissuance..."
|
||||||
|
else
|
||||||
|
echo "Starting SSL certificate issuance..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOGD "******Instructions for use******"
|
||||||
|
LOGI "This Acme script requires the following data:"
|
||||||
|
LOGI "1.Cloudflare Registered e-mail"
|
||||||
|
LOGI "2.Cloudflare Global API Key"
|
||||||
|
LOGI "3.The domain name that has been resolved DNS to the current server by Cloudflare"
|
||||||
|
LOGI "4.The script applies for a certificate. The default installation path is /root/cert "
|
||||||
|
confirm "Confirmed?[y/n]" "y"
|
||||||
|
if [ $? -eq 0 ]; then
|
||||||
|
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
|
||||||
|
echo "acme.sh could not be found. Installing..."
|
||||||
|
install_acme
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOGE "Install acme failed, please check logs"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
|
||||||
|
CF_Domain=""
|
||||||
|
if [ ! -d "$certPath" ]; then
|
||||||
|
mkdir -p $certPath
|
||||||
|
else
|
||||||
|
rm -rf $certPath
|
||||||
|
mkdir -p $certPath
|
||||||
|
fi
|
||||||
|
|
||||||
|
LOGD "Please set a domain name:"
|
||||||
|
read -p "Input your domain here: " CF_Domain
|
||||||
|
LOGD "Your domain name is set to: ${CF_Domain}"
|
||||||
|
|
||||||
|
CF_GlobalKey=""
|
||||||
|
CF_AccountEmail=""
|
||||||
|
LOGD "Please set the API key:"
|
||||||
|
read -p "Input your key here: " CF_GlobalKey
|
||||||
|
LOGD "Your API key is: ${CF_GlobalKey}"
|
||||||
|
|
||||||
|
LOGD "Please set up registered email:"
|
||||||
|
read -p "Input your email here: " CF_AccountEmail
|
||||||
|
LOGD "Your registered email address is: ${CF_AccountEmail}"
|
||||||
|
|
||||||
|
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOGE "Default CA, Let's Encrypt failed, script exiting..."
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
export CF_Key="${CF_GlobalKey}"
|
||||||
|
export CF_Email="${CF_AccountEmail}"
|
||||||
|
|
||||||
|
~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} $force_flag --log
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOGE "Certificate issuance failed, script exiting..."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
LOGI "Certificate issued Successfully, Installing..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
mkdir -p ${certPath}/${CF_Domain}
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOGE "Failed to create directory: ${certPath}/${CF_Domain}"
|
||||||
|
exit 1
|
||||||
|
fi
|
||||||
|
|
||||||
|
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \
|
||||||
|
--fullchain-file ${certPath}/${CF_Domain}/fullchain.pem \
|
||||||
|
--key-file ${certPath}/${CF_Domain}/privkey.pem
|
||||||
|
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOGE "Certificate installation failed, script exiting..."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
LOGI "Certificate installed Successfully, Turning on automatic updates..."
|
||||||
|
fi
|
||||||
|
|
||||||
|
~/.acme.sh/acme.sh --upgrade --auto-upgrade
|
||||||
|
if [ $? -ne 0 ]; then
|
||||||
|
LOGE "Auto update setup failed, script exiting..."
|
||||||
|
exit 1
|
||||||
|
else
|
||||||
|
LOGI "The certificate is installed and auto-renewal is turned on."
|
||||||
|
ls -lah ${certPath}/${CF_Domain}
|
||||||
|
chmod 755 ${certPath}/${CF_Domain}
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
echo "Exiting..."
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
echo "Invalid choice, please select again."
|
||||||
|
show_menu
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
generate_self_signed_cert() {
|
||||||
|
cert_dir="/etc/sing-box"
|
||||||
|
mkdir -p "$cert_dir"
|
||||||
|
LOGI "Choose certificate type:"
|
||||||
|
echo -e "${green}\t1.${plain} Ed25519 (*recommended*)"
|
||||||
|
echo -e "${green}\t2.${plain} RSA 2048"
|
||||||
|
echo -e "${green}\t3.${plain} RSA 4096"
|
||||||
|
echo -e "${green}\t4.${plain} ECDSA prime256v1"
|
||||||
|
echo -e "${green}\t5.${plain} ECDSA secp384r1"
|
||||||
|
read -p "Enter your choice [1-5, default 1]: " cert_type
|
||||||
|
cert_type=${cert_type:-1}
|
||||||
|
|
||||||
|
case "$cert_type" in
|
||||||
|
1)
|
||||||
|
algo="ed25519"
|
||||||
|
key_opt="-newkey ed25519"
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
algo="rsa"
|
||||||
|
key_opt="-newkey rsa:2048"
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
algo="rsa"
|
||||||
|
key_opt="-newkey rsa:4096"
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
algo="ecdsa"
|
||||||
|
key_opt="-newkey ec -pkeyopt ec_paramgen_curve:prime256v1"
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
algo="ecdsa"
|
||||||
|
key_opt="-newkey ec -pkeyopt ec_paramgen_curve:secp384r1"
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
algo="ed25519"
|
||||||
|
key_opt="-newkey ed25519"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
|
||||||
|
LOGI "Generating self-signed certificate ($algo)..."
|
||||||
|
sudo openssl req -x509 -nodes -days 3650 $key_opt \
|
||||||
|
-keyout "${cert_dir}/self.key" \
|
||||||
|
-out "${cert_dir}/self.crt" \
|
||||||
|
-subj "/CN=myserver"
|
||||||
|
if [[ $? -eq 0 ]]; then
|
||||||
|
sudo chmod 600 "${cert_dir}/self."*
|
||||||
|
LOGI "Self-signed certificate generated successfully!"
|
||||||
|
LOGI "Certificate path: ${cert_dir}/self.crt"
|
||||||
|
LOGI "Key path: ${cert_dir}/self.key"
|
||||||
|
else
|
||||||
|
LOGE "Failed to generate self-signed certificate."
|
||||||
|
fi
|
||||||
|
before_show_menu
|
||||||
|
}
|
||||||
|
|
||||||
|
show_usage() {
|
||||||
|
echo -e "S-UI Control Menu Usage"
|
||||||
|
echo -e "------------------------------------------"
|
||||||
|
echo -e "SUBCOMMANDS:"
|
||||||
|
echo -e "s-ui - Admin Management Script"
|
||||||
|
echo -e "s-ui start - Start s-ui"
|
||||||
|
echo -e "s-ui stop - Stop s-ui"
|
||||||
|
echo -e "s-ui restart - Restart s-ui"
|
||||||
|
echo -e "s-ui status - Current Status of s-ui"
|
||||||
|
echo -e "s-ui enable - Enable Autostart on OS Startup"
|
||||||
|
echo -e "s-ui disable - Disable Autostart on OS Startup"
|
||||||
|
echo -e "s-ui log - Check s-ui Logs"
|
||||||
|
echo -e "s-ui update - Update"
|
||||||
|
echo -e "s-ui install - Install"
|
||||||
|
echo -e "s-ui uninstall - Uninstall"
|
||||||
|
echo -e "s-ui help - Control Menu Usage"
|
||||||
|
echo -e "------------------------------------------"
|
||||||
|
}
|
||||||
|
|
||||||
|
show_menu() {
|
||||||
|
echo -e "
|
||||||
|
${green}S-UI Admin Management Script ${plain}
|
||||||
|
————————————————————————————————
|
||||||
|
${green}0.${plain} Exit
|
||||||
|
————————————————————————————————
|
||||||
|
${green}1.${plain} Install
|
||||||
|
${green}2.${plain} Update
|
||||||
|
${green}3.${plain} Custom Version
|
||||||
|
${green}4.${plain} Uninstall
|
||||||
|
————————————————————————————————
|
||||||
|
${green}5.${plain} Reset admin credentials to default
|
||||||
|
${green}6.${plain} Set admin credentials
|
||||||
|
${green}7.${plain} View admin credentials
|
||||||
|
————————————————————————————————
|
||||||
|
${green}8.${plain} Reset Panel Settings
|
||||||
|
${green}9.${plain} Set Panel settings
|
||||||
|
${green}10.${plain} View Panel Settings
|
||||||
|
————————————————————————————————
|
||||||
|
${green}11.${plain} S-UI Start
|
||||||
|
${green}12.${plain} S-UI Stop
|
||||||
|
${green}13.${plain} S-UI Restart
|
||||||
|
${green}14.${plain} S-UI Check State
|
||||||
|
${green}15.${plain} S-UI Check Logs
|
||||||
|
${green}16.${plain} S-UI Enable Autostart
|
||||||
|
${green}17.${plain} S-UI Disable Autostart
|
||||||
|
————————————————————————————————
|
||||||
|
${green}18.${plain} Enable or Disable BBR
|
||||||
|
${green}19.${plain} SSL Certificate Management
|
||||||
|
${green}20.${plain} Cloudflare SSL Certificate
|
||||||
|
————————————————————————————————
|
||||||
|
"
|
||||||
|
show_status s-ui
|
||||||
|
echo && read -p "Please enter your selection [0-20]: " num
|
||||||
|
|
||||||
|
case "${num}" in
|
||||||
|
0)
|
||||||
|
exit 0
|
||||||
|
;;
|
||||||
|
1)
|
||||||
|
check_uninstall && install
|
||||||
|
;;
|
||||||
|
2)
|
||||||
|
check_install && update
|
||||||
|
;;
|
||||||
|
3)
|
||||||
|
check_install && custom_version
|
||||||
|
;;
|
||||||
|
4)
|
||||||
|
check_install && uninstall
|
||||||
|
;;
|
||||||
|
5)
|
||||||
|
check_install && reset_admin
|
||||||
|
;;
|
||||||
|
6)
|
||||||
|
check_install && set_admin
|
||||||
|
;;
|
||||||
|
7)
|
||||||
|
check_install && view_admin
|
||||||
|
;;
|
||||||
|
8)
|
||||||
|
check_install && reset_setting
|
||||||
|
;;
|
||||||
|
9)
|
||||||
|
check_install && set_setting
|
||||||
|
;;
|
||||||
|
10)
|
||||||
|
check_install && view_setting
|
||||||
|
;;
|
||||||
|
11)
|
||||||
|
check_install && start s-ui
|
||||||
|
;;
|
||||||
|
12)
|
||||||
|
check_install && stop s-ui
|
||||||
|
;;
|
||||||
|
13)
|
||||||
|
check_install && restart s-ui
|
||||||
|
;;
|
||||||
|
14)
|
||||||
|
check_install && status s-ui
|
||||||
|
;;
|
||||||
|
15)
|
||||||
|
check_install && show_log s-ui
|
||||||
|
;;
|
||||||
|
16)
|
||||||
|
check_install && enable s-ui
|
||||||
|
;;
|
||||||
|
17)
|
||||||
|
check_install && disable s-ui
|
||||||
|
;;
|
||||||
|
18)
|
||||||
|
bbr_menu
|
||||||
|
;;
|
||||||
|
19)
|
||||||
|
ssl_cert_issue_main
|
||||||
|
;;
|
||||||
|
20)
|
||||||
|
ssl_cert_issue_CF
|
||||||
|
;;
|
||||||
|
*)
|
||||||
|
LOGE "Please enter the correct number [0-20]"
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
}
|
||||||
|
|
||||||
|
if [[ $# > 0 ]]; then
|
||||||
|
case $1 in
|
||||||
|
"start")
|
||||||
|
check_install 0 && start s-ui 0
|
||||||
|
;;
|
||||||
|
"stop")
|
||||||
|
check_install 0 && stop s-ui 0
|
||||||
|
;;
|
||||||
|
"restart")
|
||||||
|
check_install 0 && restart s-ui 0
|
||||||
|
;;
|
||||||
|
"status")
|
||||||
|
check_install 0 && status 0
|
||||||
|
;;
|
||||||
|
"enable")
|
||||||
|
check_install 0 && enable s-ui 0
|
||||||
|
;;
|
||||||
|
"disable")
|
||||||
|
check_install 0 && disable s-ui 0
|
||||||
|
;;
|
||||||
|
"log")
|
||||||
|
check_install 0 && show_log s-ui 0
|
||||||
|
;;
|
||||||
|
"update")
|
||||||
|
check_install 0 && update 0
|
||||||
|
;;
|
||||||
|
"install")
|
||||||
|
check_uninstall 0 && install 0
|
||||||
|
;;
|
||||||
|
"uninstall")
|
||||||
|
check_install 0 && uninstall 0
|
||||||
|
;;
|
||||||
|
*) show_usage ;;
|
||||||
|
esac
|
||||||
|
else
|
||||||
|
show_menu
|
||||||
|
fi
|
||||||
548
service/client.go
Normal file
548
service/client.go
Normal file
@@ -0,0 +1,548 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/util"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientService struct{}
|
||||||
|
|
||||||
|
func (s *ClientService) Get(id string) (*[]model.Client, error) {
|
||||||
|
if id == "" {
|
||||||
|
return s.GetAll()
|
||||||
|
}
|
||||||
|
return s.getById(id)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) getById(id string) (*[]model.Client, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var client []model.Client
|
||||||
|
err := db.Model(model.Client{}).Where("id in ?", strings.Split(id, ",")).Scan(&client).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return &client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) GetAll() (*[]model.Client, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var clients []model.Client
|
||||||
|
err := db.Model(model.Client{}).
|
||||||
|
Select("`id`, `enable`, `name`, `desc`, `group`, `inbounds`, `up`, `down`, `volume`, `expiry`").
|
||||||
|
Scan(&clients).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &clients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) Save(tx *gorm.DB, act string, data json.RawMessage, hostname string) ([]uint, error) {
|
||||||
|
var err error
|
||||||
|
var inboundIds []uint
|
||||||
|
|
||||||
|
switch act {
|
||||||
|
case "new", "edit":
|
||||||
|
var client model.Client
|
||||||
|
err = json.Unmarshal(data, &client)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = s.updateLinksWithFixedInbounds(tx, []*model.Client{&client}, hostname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if act == "edit" {
|
||||||
|
// Find changed inbounds
|
||||||
|
inboundIds, err = s.findInboundsChanges(tx, &client, false)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
err = json.Unmarshal(client.Inbounds, &inboundIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = tx.Save(&client).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "addbulk":
|
||||||
|
var clients []*model.Client
|
||||||
|
err = json.Unmarshal(data, &clients)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(clients[0].Inbounds, &inboundIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = s.updateLinksWithFixedInbounds(tx, clients, hostname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = tx.Save(clients).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "editbulk":
|
||||||
|
var clients []*model.Client
|
||||||
|
err = json.Unmarshal(data, &clients)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
changedInboundIds, err := s.findInboundsChanges(tx, client, true)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if len(changedInboundIds) > 0 {
|
||||||
|
inboundIds = common.UnionUintArray(inboundIds, changedInboundIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(inboundIds) > 0 {
|
||||||
|
err = s.updateLinksWithFixedInbounds(tx, clients, hostname)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = tx.Save(clients).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "delbulk":
|
||||||
|
var ids []uint
|
||||||
|
err = json.Unmarshal(data, &ids)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, id := range ids {
|
||||||
|
var client model.Client
|
||||||
|
err = tx.Where("id = ?", id).First(&client).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var clientInbounds []uint
|
||||||
|
err = json.Unmarshal(client.Inbounds, &clientInbounds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inboundIds = common.UnionUintArray(inboundIds, clientInbounds)
|
||||||
|
}
|
||||||
|
err = tx.Where("id in ?", ids).Delete(model.Client{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
case "del":
|
||||||
|
var id uint
|
||||||
|
err = json.Unmarshal(data, &id)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var client model.Client
|
||||||
|
err = tx.Where("id = ?", id).First(&client).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(client.Inbounds, &inboundIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = tx.Where("id = ?", id).Delete(model.Client{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, common.NewErrorf("unknown action: %s", act)
|
||||||
|
}
|
||||||
|
|
||||||
|
return inboundIds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) updateLinksWithFixedInbounds(tx *gorm.DB, clients []*model.Client, hostname string) error {
|
||||||
|
var err error
|
||||||
|
var inbounds []model.Inbound
|
||||||
|
var inboundIds []uint
|
||||||
|
|
||||||
|
err = json.Unmarshal(clients[0].Inbounds, &inboundIds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Zero inbounds means removing local links only
|
||||||
|
if len(inboundIds) > 0 {
|
||||||
|
err = tx.Model(model.Inbound{}).Preload("Tls").Where("id in ? and type in ?", inboundIds, util.InboundTypeWithLink).Find(&inbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
for index, client := range clients {
|
||||||
|
var clientLinks []map[string]string
|
||||||
|
err = json.Unmarshal(client.Links, &clientLinks)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
newClientLinks := []map[string]string{}
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
newLinks := util.LinkGenerator(client.Config, &inbound, hostname)
|
||||||
|
for _, newLink := range newLinks {
|
||||||
|
newClientLinks = append(newClientLinks, map[string]string{
|
||||||
|
"remark": inbound.Tag,
|
||||||
|
"type": "local",
|
||||||
|
"uri": newLink,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add non local links
|
||||||
|
for _, clientLink := range clientLinks {
|
||||||
|
if clientLink["type"] != "local" {
|
||||||
|
newClientLinks = append(newClientLinks, clientLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
clients[index].Links, err = json.MarshalIndent(newClientLinks, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) UpdateClientsOnInboundAdd(tx *gorm.DB, initIds string, inboundId uint, hostname string) error {
|
||||||
|
clientIds := strings.Split(initIds, ",")
|
||||||
|
var clients []model.Client
|
||||||
|
err := tx.Model(model.Client{}).Where("id in ?", clientIds).Find(&clients).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var inbound model.Inbound
|
||||||
|
err = tx.Model(model.Inbound{}).Preload("Tls").Where("id = ?", inboundId).Find(&inbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
// Add inbounds
|
||||||
|
var clientInbounds []uint
|
||||||
|
json.Unmarshal(client.Inbounds, &clientInbounds)
|
||||||
|
clientInbounds = append(clientInbounds, inboundId)
|
||||||
|
client.Inbounds, err = json.MarshalIndent(clientInbounds, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Add links
|
||||||
|
var clientLinks, newClientLinks []map[string]string
|
||||||
|
json.Unmarshal(client.Links, &clientLinks)
|
||||||
|
newLinks := util.LinkGenerator(client.Config, &inbound, hostname)
|
||||||
|
for _, newLink := range newLinks {
|
||||||
|
newClientLinks = append(newClientLinks, map[string]string{
|
||||||
|
"remark": inbound.Tag,
|
||||||
|
"type": "local",
|
||||||
|
"uri": newLink,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, clientLink := range clientLinks {
|
||||||
|
if clientLink["remark"] != inbound.Tag {
|
||||||
|
newClientLinks = append(newClientLinks, clientLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Links, err = json.MarshalIndent(newClientLinks, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = tx.Save(&client).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) UpdateClientsOnInboundDelete(tx *gorm.DB, id uint, tag string) error {
|
||||||
|
var clientIds []uint
|
||||||
|
err := tx.Raw("SELECT clients.id FROM clients, json_each(clients.inbounds) AS je WHERE je.value = ?", id).Scan(&clientIds).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(clientIds) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var clients []model.Client
|
||||||
|
err = tx.Model(model.Client{}).Where("id IN ?", clientIds).Find(&clients).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
// Delete inbounds
|
||||||
|
var clientInbounds, newClientInbounds []uint
|
||||||
|
json.Unmarshal(client.Inbounds, &clientInbounds)
|
||||||
|
for _, clientInbound := range clientInbounds {
|
||||||
|
if clientInbound != id {
|
||||||
|
newClientInbounds = append(newClientInbounds, clientInbound)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.Inbounds, err = json.MarshalIndent(newClientInbounds, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Delete links
|
||||||
|
var clientLinks, newClientLinks []map[string]string
|
||||||
|
json.Unmarshal(client.Links, &clientLinks)
|
||||||
|
for _, clientLink := range clientLinks {
|
||||||
|
if clientLink["remark"] != tag {
|
||||||
|
newClientLinks = append(newClientLinks, clientLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
client.Links, err = json.MarshalIndent(newClientLinks, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = tx.Save(&client).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) UpdateLinksByInboundChange(tx *gorm.DB, inbounds *[]model.Inbound, hostname string, oldTag string) error {
|
||||||
|
var err error
|
||||||
|
for _, inbound := range *inbounds {
|
||||||
|
var clientIds []uint
|
||||||
|
err = tx.Raw("SELECT clients.id FROM clients, json_each(clients.inbounds) AS je WHERE je.value = ?", inbound.Id).Scan(&clientIds).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(clientIds) == 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var clients []model.Client
|
||||||
|
err = tx.Model(model.Client{}).Where("id IN ?", clientIds).Find(&clients).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, client := range clients {
|
||||||
|
var clientLinks, newClientLinks []map[string]string
|
||||||
|
json.Unmarshal(client.Links, &clientLinks)
|
||||||
|
newLinks := util.LinkGenerator(client.Config, &inbound, hostname)
|
||||||
|
for _, newLink := range newLinks {
|
||||||
|
newClientLinks = append(newClientLinks, map[string]string{
|
||||||
|
"remark": inbound.Tag,
|
||||||
|
"type": "local",
|
||||||
|
"uri": newLink,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
for _, clientLink := range clientLinks {
|
||||||
|
if clientLink["type"] != "local" || (clientLink["remark"] != inbound.Tag && clientLink["remark"] != oldTag) {
|
||||||
|
newClientLinks = append(newClientLinks, clientLink)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.Links, err = json.MarshalIndent(newClientLinks, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = tx.Save(&client).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) DepleteClients() ([]uint, error) {
|
||||||
|
var err error
|
||||||
|
var clients []model.Client
|
||||||
|
var changes []model.Changes
|
||||||
|
var users []string
|
||||||
|
var inboundIds []uint
|
||||||
|
|
||||||
|
dt := time.Now().Unix()
|
||||||
|
db := database.GetDB()
|
||||||
|
|
||||||
|
tx := db.Begin()
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
tx.Commit()
|
||||||
|
if err1 := db.Exec("PRAGMA wal_checkpoint(FULL)").Error; err1 != nil {
|
||||||
|
logger.Error("Error checkpointing WAL: ", err1.Error())
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
// Reset clients
|
||||||
|
inboundIds, err = s.ResetClients(tx, dt)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Deplete clients
|
||||||
|
err = tx.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", dt).Scan(&clients).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, client := range clients {
|
||||||
|
logger.Debug("Client ", client.Name, " is going to be disabled")
|
||||||
|
users = append(users, client.Name)
|
||||||
|
var userInbounds []uint
|
||||||
|
json.Unmarshal(client.Inbounds, &userInbounds)
|
||||||
|
// Find changed inbounds
|
||||||
|
inboundIds = common.UnionUintArray(inboundIds, userInbounds)
|
||||||
|
changes = append(changes, model.Changes{
|
||||||
|
DateTime: dt,
|
||||||
|
Actor: "DepleteJob",
|
||||||
|
Key: "clients",
|
||||||
|
Action: "disable",
|
||||||
|
Obj: json.RawMessage("\"" + client.Name + "\""),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes
|
||||||
|
if len(changes) > 0 {
|
||||||
|
err = tx.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", dt).Update("enable", false).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = tx.Model(model.Changes{}).Create(&changes).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
LastUpdate = dt
|
||||||
|
}
|
||||||
|
|
||||||
|
return inboundIds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) ResetClients(tx *gorm.DB, dt int64) ([]uint, error) {
|
||||||
|
var err error
|
||||||
|
var resetClients, allClients []*model.Client
|
||||||
|
var changes []model.Changes
|
||||||
|
var inboundIds []uint
|
||||||
|
// Set delay start without periodic reset
|
||||||
|
err = tx.Model(model.Client{}).
|
||||||
|
Where("enable = true AND delay_start = true AND auto_reset = false AND (Up + Down) > 0").Find(&resetClients).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, client := range resetClients {
|
||||||
|
client.Expiry = dt + (int64(client.ResetDays) * 86400)
|
||||||
|
client.DelayStart = false
|
||||||
|
changes = append(changes, model.Changes{
|
||||||
|
DateTime: dt,
|
||||||
|
Actor: "ResetJob",
|
||||||
|
Key: "clients",
|
||||||
|
Action: "reset",
|
||||||
|
Obj: json.RawMessage("\"" + client.Name + "\""),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
allClients = append(allClients, resetClients...)
|
||||||
|
|
||||||
|
// Set delay start with periodic reset
|
||||||
|
err = tx.Model(model.Client{}).
|
||||||
|
Where("enable = true AND delay_start = true AND auto_reset = true AND (Up + Down) > 0").Find(&resetClients).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, client := range resetClients {
|
||||||
|
client.NextReset = dt + (int64(client.ResetDays) * 86400)
|
||||||
|
client.DelayStart = false
|
||||||
|
changes = append(changes, model.Changes{
|
||||||
|
DateTime: dt,
|
||||||
|
Actor: "ResetJob",
|
||||||
|
Key: "clients",
|
||||||
|
Action: "reset",
|
||||||
|
Obj: json.RawMessage("\"" + client.Name + "\""),
|
||||||
|
})
|
||||||
|
}
|
||||||
|
allClients = append(allClients, resetClients...)
|
||||||
|
|
||||||
|
// Set periodic reset
|
||||||
|
err = tx.Model(model.Client{}).
|
||||||
|
Where("delay_start = false AND auto_reset = true AND next_reset < ?", dt).Find(&resetClients).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, client := range resetClients {
|
||||||
|
client.NextReset = dt + (int64(client.ResetDays) * 86400)
|
||||||
|
client.TotalUp += client.Up
|
||||||
|
client.TotalDown += client.Down
|
||||||
|
client.Up = 0
|
||||||
|
client.Down = 0
|
||||||
|
if !client.Enable {
|
||||||
|
client.Enable = true
|
||||||
|
var clientInboundIds []uint
|
||||||
|
json.Unmarshal(client.Inbounds, &clientInboundIds)
|
||||||
|
inboundIds = common.UnionUintArray(inboundIds, clientInboundIds)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
allClients = append(allClients, resetClients...)
|
||||||
|
|
||||||
|
// Save clients
|
||||||
|
if len(allClients) > 0 {
|
||||||
|
err = tx.Save(allClients).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Save changes
|
||||||
|
if len(changes) > 0 {
|
||||||
|
err = tx.Model(model.Changes{}).Create(&changes).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
LastUpdate = dt
|
||||||
|
}
|
||||||
|
return inboundIds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) findInboundsChanges(tx *gorm.DB, client *model.Client, fillOmitted bool) ([]uint, error) {
|
||||||
|
var err error
|
||||||
|
var oldClient model.Client
|
||||||
|
var oldInboundIds, newInboundIds []uint
|
||||||
|
err = tx.Model(model.Client{}).Where("id = ?", client.Id).First(&oldClient).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if fillOmitted {
|
||||||
|
client.Links = oldClient.Links
|
||||||
|
client.Config = oldClient.Config
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(oldClient.Inbounds, &oldInboundIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(client.Inbounds, &newInboundIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check client.Config changes
|
||||||
|
if !bytes.Equal(oldClient.Config, client.Config) ||
|
||||||
|
oldClient.Name != client.Name ||
|
||||||
|
oldClient.Enable != client.Enable {
|
||||||
|
return common.UnionUintArray(oldInboundIds, newInboundIds), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check client.Inbounds changes
|
||||||
|
diffInbounds := common.DiffUintArray(oldInboundIds, newInboundIds)
|
||||||
|
|
||||||
|
return diffInbounds, nil
|
||||||
|
}
|
||||||
297
service/config.go
Normal file
297
service/config.go
Normal file
@@ -0,0 +1,297 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strconv"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/core"
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
LastUpdate int64
|
||||||
|
corePtr *core.Core
|
||||||
|
startCoreMu sync.Mutex
|
||||||
|
startCoreInProgress bool
|
||||||
|
lastStartFailTime time.Time
|
||||||
|
startCooldown = 15 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
type ConfigService struct {
|
||||||
|
ClientService
|
||||||
|
TlsService
|
||||||
|
SettingService
|
||||||
|
InboundService
|
||||||
|
OutboundService
|
||||||
|
ServicesService
|
||||||
|
EndpointService
|
||||||
|
}
|
||||||
|
|
||||||
|
type SingBoxConfig struct {
|
||||||
|
Log json.RawMessage `json:"log"`
|
||||||
|
Dns json.RawMessage `json:"dns"`
|
||||||
|
Ntp json.RawMessage `json:"ntp"`
|
||||||
|
Inbounds []json.RawMessage `json:"inbounds"`
|
||||||
|
Outbounds []json.RawMessage `json:"outbounds"`
|
||||||
|
Services []json.RawMessage `json:"services"`
|
||||||
|
Endpoints []json.RawMessage `json:"endpoints"`
|
||||||
|
Route json.RawMessage `json:"route"`
|
||||||
|
Experimental json.RawMessage `json:"experimental"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigService(core *core.Core) *ConfigService {
|
||||||
|
corePtr = core
|
||||||
|
return &ConfigService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) GetConfig(data string) (*[]byte, error) {
|
||||||
|
var err error
|
||||||
|
if len(data) == 0 {
|
||||||
|
data, err = s.SettingService.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
singboxConfig := SingBoxConfig{}
|
||||||
|
err = json.Unmarshal([]byte(data), &singboxConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
singboxConfig.Inbounds, err = s.InboundService.GetAllConfig(database.GetDB())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
singboxConfig.Outbounds, err = s.OutboundService.GetAllConfig(database.GetDB())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
singboxConfig.Services, err = s.ServicesService.GetAllConfig(database.GetDB())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
singboxConfig.Endpoints, err = s.EndpointService.GetAllConfig(database.GetDB())
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
rawConfig, err := json.MarshalIndent(singboxConfig, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &rawConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) StartCore() error {
|
||||||
|
if corePtr.IsRunning() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
startCoreMu.Lock()
|
||||||
|
if startCoreInProgress {
|
||||||
|
startCoreMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if time.Since(lastStartFailTime) < startCooldown {
|
||||||
|
logger.Info("start core cooldown ", startCooldown/time.Second, " seconds")
|
||||||
|
startCoreMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
startCoreInProgress = true
|
||||||
|
startCoreMu.Unlock()
|
||||||
|
defer func() {
|
||||||
|
startCoreMu.Lock()
|
||||||
|
startCoreInProgress = false
|
||||||
|
startCoreMu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
logger.Info("starting core")
|
||||||
|
rawConfig, err := s.GetConfig("")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = corePtr.Start(*rawConfig)
|
||||||
|
if err != nil {
|
||||||
|
startCoreMu.Lock()
|
||||||
|
lastStartFailTime = time.Now()
|
||||||
|
startCoreMu.Unlock()
|
||||||
|
logger.Error("start sing-box err:", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info("sing-box started")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) RestartCore() error {
|
||||||
|
err := s.StopCore()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return s.StartCore()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) restartCoreWithConfig(config json.RawMessage) error {
|
||||||
|
startCoreMu.Lock()
|
||||||
|
if startCoreInProgress {
|
||||||
|
startCoreMu.Unlock()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
startCoreInProgress = true
|
||||||
|
startCoreMu.Unlock()
|
||||||
|
defer func() {
|
||||||
|
startCoreMu.Lock()
|
||||||
|
startCoreInProgress = false
|
||||||
|
startCoreMu.Unlock()
|
||||||
|
}()
|
||||||
|
|
||||||
|
if corePtr.IsRunning() {
|
||||||
|
if err := corePtr.Stop(); err != nil {
|
||||||
|
logger.Error("restart sing-box err (stop):", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
rawConfig, err := s.GetConfig(string(config))
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("restart sing-box err (get config):", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := corePtr.Start(*rawConfig); err != nil {
|
||||||
|
logger.Error("restart sing-box err (start):", err.Error())
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info("sing-box restarted with new config")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) StopCore() error {
|
||||||
|
err := corePtr.Stop()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
logger.Info("sing-box stopped")
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) CheckOutbound(tag string, link string) core.CheckOutboundResult {
|
||||||
|
if tag == "" {
|
||||||
|
return core.CheckOutboundResult{Error: "missing query parameter: tag"}
|
||||||
|
}
|
||||||
|
if corePtr == nil || !corePtr.IsRunning() {
|
||||||
|
return core.CheckOutboundResult{Error: "core not running"}
|
||||||
|
}
|
||||||
|
return core.CheckOutbound(corePtr.GetCtx(), tag, link)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) Save(obj string, act string, data json.RawMessage, initUsers string, loginUser string, hostname string) ([]string, error) {
|
||||||
|
var err error
|
||||||
|
var objs []string = []string{obj}
|
||||||
|
|
||||||
|
db := database.GetDB()
|
||||||
|
tx := db.Begin()
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
tx.Commit()
|
||||||
|
// Try to start core if it is not running
|
||||||
|
if !corePtr.IsRunning() {
|
||||||
|
s.StartCore()
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
switch obj {
|
||||||
|
case "clients":
|
||||||
|
var inboundIds []uint
|
||||||
|
inboundIds, err = s.ClientService.Save(tx, act, data, hostname)
|
||||||
|
if err == nil && len(inboundIds) > 0 {
|
||||||
|
objs = append(objs, "inbounds")
|
||||||
|
err = s.InboundService.RestartInbounds(tx, inboundIds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, common.NewErrorf("failed to update users for inbounds: %v", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "tls":
|
||||||
|
err = s.TlsService.Save(tx, act, data, hostname)
|
||||||
|
objs = append(objs, "clients", "inbounds")
|
||||||
|
case "inbounds":
|
||||||
|
err = s.InboundService.Save(tx, act, data, initUsers, hostname)
|
||||||
|
objs = append(objs, "clients")
|
||||||
|
case "outbounds":
|
||||||
|
err = s.OutboundService.Save(tx, act, data)
|
||||||
|
case "services":
|
||||||
|
err = s.ServicesService.Save(tx, act, data)
|
||||||
|
case "endpoints":
|
||||||
|
err = s.EndpointService.Save(tx, act, data)
|
||||||
|
case "config":
|
||||||
|
err = s.SettingService.SaveConfig(tx, data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
configData := make(json.RawMessage, len(data))
|
||||||
|
copy(configData, data)
|
||||||
|
go func() { _ = s.restartCoreWithConfig(configData) }()
|
||||||
|
case "settings":
|
||||||
|
err = s.SettingService.Save(tx, data)
|
||||||
|
default:
|
||||||
|
return nil, common.NewError("unknown object: ", obj)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dt := time.Now().Unix()
|
||||||
|
err = tx.Create(&model.Changes{
|
||||||
|
DateTime: dt,
|
||||||
|
Actor: loginUser,
|
||||||
|
Key: obj,
|
||||||
|
Action: act,
|
||||||
|
Obj: data,
|
||||||
|
}).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
LastUpdate = time.Now().Unix()
|
||||||
|
|
||||||
|
return objs, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) CheckChanges(lu string) (bool, error) {
|
||||||
|
if lu == "" {
|
||||||
|
return true, nil
|
||||||
|
}
|
||||||
|
if LastUpdate == 0 {
|
||||||
|
db := database.GetDB()
|
||||||
|
var count int64
|
||||||
|
err := db.Model(model.Changes{}).Where("date_time > " + lu).Count(&count).Error
|
||||||
|
if err == nil {
|
||||||
|
LastUpdate = time.Now().Unix()
|
||||||
|
}
|
||||||
|
return count > 0, err
|
||||||
|
} else {
|
||||||
|
intLu, err := strconv.ParseInt(lu, 10, 64)
|
||||||
|
return LastUpdate > intLu, err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) GetChanges(actor string, chngKey string, count string) []model.Changes {
|
||||||
|
c, _ := strconv.Atoi(count)
|
||||||
|
whereString := "`id`>0"
|
||||||
|
if len(actor) > 0 {
|
||||||
|
whereString += " and `actor`='" + actor + "'"
|
||||||
|
}
|
||||||
|
if len(chngKey) > 0 {
|
||||||
|
whereString += " and `key`='" + chngKey + "'"
|
||||||
|
}
|
||||||
|
db := database.GetDB()
|
||||||
|
var chngs []model.Changes
|
||||||
|
err := db.Model(model.Changes{}).Where(whereString).Order("`id` desc").Limit(c).Scan(&chngs).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning(err)
|
||||||
|
}
|
||||||
|
return chngs
|
||||||
|
}
|
||||||
140
service/endpoints.go
Normal file
140
service/endpoints.go
Normal file
@@ -0,0 +1,140 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type EndpointService struct {
|
||||||
|
WarpService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *EndpointService) GetAll() (*[]map[string]interface{}, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
endpoints := []*model.Endpoint{}
|
||||||
|
err := db.Model(model.Endpoint{}).Scan(&endpoints).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var data []map[string]interface{}
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
epData := map[string]interface{}{
|
||||||
|
"id": endpoint.Id,
|
||||||
|
"type": endpoint.Type,
|
||||||
|
"tag": endpoint.Tag,
|
||||||
|
"ext": endpoint.Ext,
|
||||||
|
}
|
||||||
|
if endpoint.Options != nil {
|
||||||
|
var restFields map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(endpoint.Options, &restFields); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for k, v := range restFields {
|
||||||
|
epData[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = append(data, epData)
|
||||||
|
}
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *EndpointService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
|
||||||
|
var endpointsJson []json.RawMessage
|
||||||
|
var endpoints []*model.Endpoint
|
||||||
|
err := db.Model(model.Endpoint{}).Scan(&endpoints).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, endpoint := range endpoints {
|
||||||
|
endpointJson, err := endpoint.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
endpointsJson = append(endpointsJson, endpointJson)
|
||||||
|
}
|
||||||
|
return endpointsJson, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *EndpointService) Save(tx *gorm.DB, act string, data json.RawMessage) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch act {
|
||||||
|
case "new", "edit":
|
||||||
|
var endpoint model.Endpoint
|
||||||
|
err = endpoint.UnmarshalJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if endpoint.Type == "warp" {
|
||||||
|
if act == "new" {
|
||||||
|
err = s.WarpService.RegisterWarp(&endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
var old_license string
|
||||||
|
err = tx.Model(model.Endpoint{}).Select("json_extract(ext, '$.license_key')").Where("id = ?", endpoint.Id).Find(&old_license).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.WarpService.SetWarpLicense(old_license, &endpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if corePtr.IsRunning() {
|
||||||
|
configData, err := endpoint.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if act == "edit" {
|
||||||
|
var oldTag string
|
||||||
|
err = tx.Model(model.Endpoint{}).Select("tag").Where("id = ?", endpoint.Id).Find(&oldTag).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = corePtr.RemoveEndpoint(oldTag)
|
||||||
|
if err != nil && err != os.ErrInvalid {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = corePtr.AddEndpoint(configData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Save(&endpoint).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "del":
|
||||||
|
var tag string
|
||||||
|
err = json.Unmarshal(data, &tag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if corePtr.IsRunning() {
|
||||||
|
err = corePtr.RemoveEndpoint(tag)
|
||||||
|
if err != nil && err != os.ErrInvalid {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = tx.Where("tag = ?", tag).Delete(model.Endpoint{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return common.NewErrorf("unknown action: %s", act)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
362
service/inbounds.go
Normal file
362
service/inbounds.go
Normal file
@@ -0,0 +1,362 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"os"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/util"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type InboundService struct {
|
||||||
|
ClientService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) Get(ids string) (*[]map[string]interface{}, error) {
|
||||||
|
if ids == "" {
|
||||||
|
return s.GetAll()
|
||||||
|
}
|
||||||
|
return s.getById(ids)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) getById(ids string) (*[]map[string]interface{}, error) {
|
||||||
|
var inbound []model.Inbound
|
||||||
|
var result []map[string]interface{}
|
||||||
|
db := database.GetDB()
|
||||||
|
err := db.Model(model.Inbound{}).Where("id in ?", strings.Split(ids, ",")).Scan(&inbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, inb := range inbound {
|
||||||
|
inbData, err := inb.MarshalFull()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
result = append(result, *inbData)
|
||||||
|
}
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) GetAll() (*[]map[string]interface{}, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
inbounds := []model.Inbound{}
|
||||||
|
err := db.Model(model.Inbound{}).Scan(&inbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var data []map[string]interface{}
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
var shadowtls_version uint
|
||||||
|
ss_managed := false
|
||||||
|
inbData := map[string]interface{}{
|
||||||
|
"id": inbound.Id,
|
||||||
|
"type": inbound.Type,
|
||||||
|
"tag": inbound.Tag,
|
||||||
|
"tls_id": inbound.TlsId,
|
||||||
|
}
|
||||||
|
if inbound.Options != nil {
|
||||||
|
var restFields map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(inbound.Options, &restFields); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inbData["listen"] = restFields["listen"]
|
||||||
|
inbData["listen_port"] = restFields["listen_port"]
|
||||||
|
if inbound.Type == "shadowtls" {
|
||||||
|
json.Unmarshal(restFields["version"], &shadowtls_version)
|
||||||
|
}
|
||||||
|
if inbound.Type == "shadowsocks" {
|
||||||
|
json.Unmarshal(restFields["managed"], &ss_managed)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if s.hasUser(inbound.Type) &&
|
||||||
|
!(inbound.Type == "shadowtls" && shadowtls_version < 3) &&
|
||||||
|
!(inbound.Type == "shadowsocks" && ss_managed) {
|
||||||
|
users := []string{}
|
||||||
|
err = db.Raw("SELECT clients.name FROM clients, json_each(clients.inbounds) as je WHERE je.value = ?", inbound.Id).Scan(&users).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inbData["users"] = users
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, inbData)
|
||||||
|
}
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) FromIds(ids []uint) ([]*model.Inbound, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
inbounds := []*model.Inbound{}
|
||||||
|
err := db.Model(model.Inbound{}).Where("id in ?", ids).Scan(&inbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return inbounds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) Save(tx *gorm.DB, act string, data json.RawMessage, initUserIds string, hostname string) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch act {
|
||||||
|
case "new", "edit":
|
||||||
|
var inbound model.Inbound
|
||||||
|
err = inbound.UnmarshalJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if inbound.TlsId > 0 {
|
||||||
|
err = tx.Model(model.Tls{}).Where("id = ?", inbound.TlsId).Find(&inbound.Tls).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var oldTag string
|
||||||
|
if act == "edit" {
|
||||||
|
err = tx.Model(model.Inbound{}).Select("tag").Where("id = ?", inbound.Id).Find(&oldTag).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if corePtr.IsRunning() {
|
||||||
|
if act == "edit" {
|
||||||
|
err = corePtr.RemoveInbound(oldTag)
|
||||||
|
if err != nil && err != os.ErrInvalid {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inboundConfig, err := inbound.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if act == "edit" {
|
||||||
|
inboundConfig, err = s.addUsers(tx, inboundConfig, inbound.Id, inbound.Type)
|
||||||
|
} else {
|
||||||
|
inboundConfig, err = s.initUsers(tx, inboundConfig, initUserIds, inbound.Type)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = corePtr.AddInbound(inboundConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = util.FillOutJson(&inbound, hostname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Save(&inbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch act {
|
||||||
|
case "new":
|
||||||
|
err = s.ClientService.UpdateClientsOnInboundAdd(tx, initUserIds, inbound.Id, hostname)
|
||||||
|
case "edit":
|
||||||
|
err = s.ClientService.UpdateLinksByInboundChange(tx, &[]model.Inbound{inbound}, hostname, oldTag)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "del":
|
||||||
|
var tag string
|
||||||
|
err = json.Unmarshal(data, &tag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if corePtr.IsRunning() {
|
||||||
|
err = corePtr.RemoveInbound(tag)
|
||||||
|
if err != nil && err != os.ErrInvalid {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var id uint
|
||||||
|
err = tx.Model(model.Inbound{}).Select("id").Where("tag = ?", tag).Scan(&id).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.ClientService.UpdateClientsOnInboundDelete(tx, id, tag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = tx.Where("tag = ?", tag).Delete(model.Inbound{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return common.NewErrorf("unknown action: %s", act)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) UpdateOutJsons(tx *gorm.DB, inboundIds []uint, hostname string) error {
|
||||||
|
var inbounds []model.Inbound
|
||||||
|
err := tx.Model(model.Inbound{}).Preload("Tls").Where("id in ?", inboundIds).Find(&inbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
err = util.FillOutJson(&inbound, hostname)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = tx.Model(model.Inbound{}).Where("tag = ?", inbound.Tag).Update("out_json", inbound.OutJson).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
|
||||||
|
var inboundsJson []json.RawMessage
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
err := db.Model(model.Inbound{}).Preload("Tls").Find(&inbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
inboundJson, err := inbound.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inboundJson, err = s.addUsers(db, inboundJson, inbound.Id, inbound.Type)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
inboundsJson = append(inboundsJson, inboundJson)
|
||||||
|
}
|
||||||
|
return inboundsJson, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) hasUser(inboundType string) bool {
|
||||||
|
switch inboundType {
|
||||||
|
case "mixed", "socks", "http", "shadowsocks", "vmess", "trojan", "naive", "hysteria", "shadowtls", "tuic", "hysteria2", "vless", "anytls":
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) fetchUsers(db *gorm.DB, inboundType string, condition string, inbound map[string]interface{}) ([]json.RawMessage, error) {
|
||||||
|
if inboundType == "shadowtls" {
|
||||||
|
version, _ := inbound["version"].(float64)
|
||||||
|
if int(version) < 3 {
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if inboundType == "shadowsocks" {
|
||||||
|
method, _ := inbound["method"].(string)
|
||||||
|
if method == "2022-blake3-aes-128-gcm" {
|
||||||
|
inboundType = "shadowsocks16"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var users []string
|
||||||
|
|
||||||
|
err := db.Raw(
|
||||||
|
fmt.Sprintf(`SELECT json_extract(clients.config, "$.%s")
|
||||||
|
FROM clients WHERE enable = true AND %s`,
|
||||||
|
inboundType, condition)).Scan(&users).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var usersJson []json.RawMessage
|
||||||
|
for _, user := range users {
|
||||||
|
if inboundType == "vless" && inbound["tls"] == nil {
|
||||||
|
user = strings.Replace(user, "xtls-rprx-vision", "", -1)
|
||||||
|
}
|
||||||
|
usersJson = append(usersJson, json.RawMessage(user))
|
||||||
|
}
|
||||||
|
return usersJson, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) addUsers(db *gorm.DB, inboundJson []byte, inboundId uint, inboundType string) ([]byte, error) {
|
||||||
|
if !s.hasUser(inboundType) {
|
||||||
|
return inboundJson, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var inbound map[string]interface{}
|
||||||
|
err := json.Unmarshal(inboundJson, &inbound)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
condition := fmt.Sprintf("%d IN (SELECT json_each.value FROM json_each(clients.inbounds))", inboundId)
|
||||||
|
inbound["users"], err = s.fetchUsers(db, inboundType, condition, inbound)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(inbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) initUsers(db *gorm.DB, inboundJson []byte, clientIds string, inboundType string) ([]byte, error) {
|
||||||
|
ClientIds := strings.Split(clientIds, ",")
|
||||||
|
if len(ClientIds) == 0 {
|
||||||
|
return inboundJson, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
if !s.hasUser(inboundType) {
|
||||||
|
return inboundJson, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var inbound map[string]interface{}
|
||||||
|
err := json.Unmarshal(inboundJson, &inbound)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
condition := fmt.Sprintf("id IN (%s)", strings.Join(ClientIds, ","))
|
||||||
|
inbound["users"], err = s.fetchUsers(db, inboundType, condition, inbound)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return json.Marshal(inbound)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *InboundService) RestartInbounds(tx *gorm.DB, ids []uint) error {
|
||||||
|
if !corePtr.IsRunning() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
err := tx.Model(model.Inbound{}).Preload("Tls").Where("id in ?", ids).Find(&inbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
err = corePtr.RemoveInbound(inbound.Tag)
|
||||||
|
if err != nil && err != os.ErrInvalid {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
// Close all existing connections
|
||||||
|
corePtr.GetInstance().ConnTracker().CloseConnByInbound(inbound.Tag)
|
||||||
|
|
||||||
|
inboundConfig, err := inbound.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
inboundConfig, err = s.addUsers(tx, inboundConfig, inbound.Id, inbound.Type)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = corePtr.AddInbound(inboundConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
118
service/outbounds.go
Normal file
118
service/outbounds.go
Normal file
@@ -0,0 +1,118 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type OutboundService struct{}
|
||||||
|
|
||||||
|
func (o *OutboundService) GetAll() (*[]map[string]interface{}, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
outbounds := []*model.Outbound{}
|
||||||
|
err := db.Model(model.Outbound{}).Scan(&outbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var data []map[string]interface{}
|
||||||
|
for _, outbound := range outbounds {
|
||||||
|
outData := map[string]interface{}{
|
||||||
|
"id": outbound.Id,
|
||||||
|
"type": outbound.Type,
|
||||||
|
"tag": outbound.Tag,
|
||||||
|
}
|
||||||
|
if outbound.Options != nil {
|
||||||
|
var restFields map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(outbound.Options, &restFields); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for k, v := range restFields {
|
||||||
|
outData[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
data = append(data, outData)
|
||||||
|
}
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (o *OutboundService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
|
||||||
|
var outboundsJson []json.RawMessage
|
||||||
|
var outbounds []*model.Outbound
|
||||||
|
err := db.Model(model.Outbound{}).Scan(&outbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, outbound := range outbounds {
|
||||||
|
outboundJson, err := outbound.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
outboundsJson = append(outboundsJson, outboundJson)
|
||||||
|
}
|
||||||
|
return outboundsJson, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *OutboundService) Save(tx *gorm.DB, act string, data json.RawMessage) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch act {
|
||||||
|
case "new", "edit":
|
||||||
|
var outbound model.Outbound
|
||||||
|
err = outbound.UnmarshalJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if corePtr.IsRunning() {
|
||||||
|
configData, err := outbound.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if act == "edit" {
|
||||||
|
var oldTag string
|
||||||
|
err = tx.Model(model.Outbound{}).Select("tag").Where("id = ?", outbound.Id).Find(&oldTag).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = corePtr.RemoveOutbound(oldTag)
|
||||||
|
if err != nil && err != os.ErrInvalid {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = corePtr.AddOutbound(configData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Save(&outbound).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "del":
|
||||||
|
var tag string
|
||||||
|
err = json.Unmarshal(data, &tag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if corePtr.IsRunning() {
|
||||||
|
err = corePtr.RemoveOutbound(tag)
|
||||||
|
if err != nil && err != os.ErrInvalid {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = tx.Where("tag = ?", tag).Delete(model.Outbound{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return common.NewErrorf("unknown action: %s", act)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
32
service/panel.go
Normal file
32
service/panel.go
Normal file
@@ -0,0 +1,32 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"syscall"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
type PanelService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *PanelService) RestartPanel(delay time.Duration) error {
|
||||||
|
p, err := os.FindProcess(syscall.Getpid())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
go func() {
|
||||||
|
time.Sleep(delay)
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
err = p.Kill()
|
||||||
|
} else {
|
||||||
|
err = p.Signal(syscall.SIGHUP)
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
logger.Error("send signal SIGHUP failed:", err)
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
283
service/server.go
Normal file
283
service/server.go
Normal file
@@ -0,0 +1,283 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/config"
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
|
||||||
|
"github.com/sagernet/sing-box/common/tls"
|
||||||
|
"github.com/shirou/gopsutil/v4/cpu"
|
||||||
|
"github.com/shirou/gopsutil/v4/disk"
|
||||||
|
"github.com/shirou/gopsutil/v4/host"
|
||||||
|
"github.com/shirou/gopsutil/v4/mem"
|
||||||
|
"github.com/shirou/gopsutil/v4/net"
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerService struct{}
|
||||||
|
|
||||||
|
func (s *ServerService) GetStatus(request string) *map[string]interface{} {
|
||||||
|
status := make(map[string]interface{}, 0)
|
||||||
|
requests := strings.Split(request, ",")
|
||||||
|
for _, req := range requests {
|
||||||
|
switch req {
|
||||||
|
case "cpu":
|
||||||
|
status["cpu"] = s.GetCpuPercent()
|
||||||
|
case "mem":
|
||||||
|
status["mem"] = s.GetMemInfo()
|
||||||
|
case "dsk":
|
||||||
|
status["dsk"] = s.GetDiskInfo()
|
||||||
|
case "dio":
|
||||||
|
status["dio"] = s.GetDiskIO()
|
||||||
|
case "swp":
|
||||||
|
status["swp"] = s.GetSwapInfo()
|
||||||
|
case "net":
|
||||||
|
status["net"] = s.GetNetInfo()
|
||||||
|
case "sys":
|
||||||
|
status["sys"] = s.GetSystemInfo()
|
||||||
|
case "sbd":
|
||||||
|
status["sbd"] = s.GetSingboxInfo()
|
||||||
|
case "db":
|
||||||
|
status["db"] = s.GetDatabaseInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &status
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetCpuPercent() float64 {
|
||||||
|
percents, err := cpu.Percent(0, false)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("get cpu percent failed:", err)
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
return percents[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetMemInfo() map[string]interface{} {
|
||||||
|
info := make(map[string]interface{}, 0)
|
||||||
|
memInfo, err := mem.VirtualMemory()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("get virtual memory failed:", err)
|
||||||
|
} else {
|
||||||
|
info["current"] = memInfo.Used
|
||||||
|
info["total"] = memInfo.Total
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetDiskInfo() map[string]interface{} {
|
||||||
|
info := make(map[string]interface{}, 0)
|
||||||
|
diskInfo, err := disk.Usage("/")
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("get disk usage failed:", err)
|
||||||
|
} else {
|
||||||
|
info["current"] = diskInfo.Used
|
||||||
|
info["total"] = diskInfo.Total
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetDiskIO() map[string]interface{} {
|
||||||
|
info := make(map[string]interface{}, 0)
|
||||||
|
ioStats, err := disk.IOCounters()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("get disk io counters failed:", err)
|
||||||
|
} else if len(ioStats) > 0 {
|
||||||
|
infoR, infoW := uint64(0), uint64(0)
|
||||||
|
for _, ioStat := range ioStats {
|
||||||
|
infoR += ioStat.ReadBytes
|
||||||
|
infoW += ioStat.WriteBytes
|
||||||
|
}
|
||||||
|
info["read"] = infoR
|
||||||
|
info["write"] = infoW
|
||||||
|
} else {
|
||||||
|
logger.Warning("can not find disk io counters")
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetSwapInfo() map[string]interface{} {
|
||||||
|
info := make(map[string]interface{}, 0)
|
||||||
|
swapInfo, err := mem.SwapMemory()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("get swap memory failed:", err)
|
||||||
|
} else {
|
||||||
|
info["current"] = swapInfo.Used
|
||||||
|
info["total"] = swapInfo.Total
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetNetInfo() map[string]interface{} {
|
||||||
|
info := make(map[string]interface{}, 0)
|
||||||
|
ioStats, err := net.IOCounters(false)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("get io counters failed:", err)
|
||||||
|
} else if len(ioStats) > 0 {
|
||||||
|
ioStat := ioStats[0]
|
||||||
|
info["sent"] = ioStat.BytesSent
|
||||||
|
info["recv"] = ioStat.BytesRecv
|
||||||
|
info["psent"] = ioStat.PacketsSent
|
||||||
|
info["precv"] = ioStat.PacketsRecv
|
||||||
|
} else {
|
||||||
|
logger.Warning("can not find io counters")
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetSingboxInfo() map[string]interface{} {
|
||||||
|
var rtm runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&rtm)
|
||||||
|
isRunning := corePtr.IsRunning()
|
||||||
|
uptime := uint32(0)
|
||||||
|
if isRunning {
|
||||||
|
uptime = corePtr.GetInstance().Uptime()
|
||||||
|
}
|
||||||
|
return map[string]interface{}{
|
||||||
|
"running": isRunning,
|
||||||
|
"stats": map[string]interface{}{
|
||||||
|
"NumGoroutine": uint32(runtime.NumGoroutine()),
|
||||||
|
"Alloc": rtm.Alloc,
|
||||||
|
"Uptime": uptime,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetSystemInfo() map[string]interface{} {
|
||||||
|
info := make(map[string]interface{}, 0)
|
||||||
|
var rtm runtime.MemStats
|
||||||
|
runtime.ReadMemStats(&rtm)
|
||||||
|
|
||||||
|
info["appMem"] = rtm.Sys
|
||||||
|
info["appThreads"] = uint32(runtime.NumGoroutine())
|
||||||
|
cpuInfo, err := cpu.Info()
|
||||||
|
if err == nil {
|
||||||
|
info["cpuType"] = cpuInfo[0].ModelName
|
||||||
|
}
|
||||||
|
info["cpuCount"] = runtime.NumCPU()
|
||||||
|
info["hostName"], _ = os.Hostname()
|
||||||
|
info["appVersion"] = config.GetVersion()
|
||||||
|
ipv4 := make([]string, 0)
|
||||||
|
ipv6 := make([]string, 0)
|
||||||
|
// get ip address
|
||||||
|
netInterfaces, _ := net.Interfaces()
|
||||||
|
for i := 0; i < len(netInterfaces); i++ {
|
||||||
|
if len(netInterfaces[i].Flags) > 2 && netInterfaces[i].Flags[0] == "up" && netInterfaces[i].Flags[1] != "loopback" {
|
||||||
|
addrs := netInterfaces[i].Addrs
|
||||||
|
|
||||||
|
for _, address := range addrs {
|
||||||
|
if strings.Contains(address.Addr, ".") {
|
||||||
|
ipv4 = append(ipv4, address.Addr)
|
||||||
|
} else if address.Addr[0:6] != "fe80::" {
|
||||||
|
ipv6 = append(ipv6, address.Addr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
info["ipv4"] = ipv4
|
||||||
|
info["ipv6"] = ipv6
|
||||||
|
info["bootTime"], _ = host.BootTime()
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetLogs(count string, level string) []string {
|
||||||
|
c, err := strconv.Atoi(count)
|
||||||
|
if err != nil {
|
||||||
|
c = 10
|
||||||
|
}
|
||||||
|
return logger.GetLogs(c, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GenKeypair(keyType string, options string) []string {
|
||||||
|
if len(keyType) == 0 {
|
||||||
|
return []string{"No keypair to generate"}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch keyType {
|
||||||
|
case "ech":
|
||||||
|
return s.generateECHKeyPair(options)
|
||||||
|
case "tls":
|
||||||
|
return s.generateTLSKeyPair(options)
|
||||||
|
case "reality":
|
||||||
|
return s.generateRealityKeyPair()
|
||||||
|
case "wireguard":
|
||||||
|
return s.generateWireGuardKey(options)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{"Failed to generate keypair"}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) generateECHKeyPair(serverName string) []string {
|
||||||
|
configPem, keyPem, err := tls.ECHKeygenDefault(serverName)
|
||||||
|
if err != nil {
|
||||||
|
return []string{"Failed to generate ECH keypair: ", err.Error()}
|
||||||
|
}
|
||||||
|
return append(strings.Split(configPem, "\n"), strings.Split(keyPem, "\n")...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) generateTLSKeyPair(serverName string) []string {
|
||||||
|
privateKeyPem, publicKeyPem, err := tls.GenerateCertificate(nil, nil, time.Now, serverName, time.Now().AddDate(0, 12, 0))
|
||||||
|
if err != nil {
|
||||||
|
return []string{"Failed to generate TLS keypair: ", err.Error()}
|
||||||
|
}
|
||||||
|
return append(strings.Split(string(privateKeyPem), "\n"), strings.Split(string(publicKeyPem), "\n")...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) generateRealityKeyPair() []string {
|
||||||
|
privateKey, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return []string{"Failed to generate Reality keypair: ", err.Error()}
|
||||||
|
}
|
||||||
|
publicKey := privateKey.PublicKey()
|
||||||
|
return []string{"PrivateKey: " + base64.RawURLEncoding.EncodeToString(privateKey[:]), "PublicKey: " + base64.RawURLEncoding.EncodeToString(publicKey[:])}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) generateWireGuardKey(pk string) []string {
|
||||||
|
if len(pk) > 0 {
|
||||||
|
key, _ := wgtypes.ParseKey(pk)
|
||||||
|
return []string{key.PublicKey().String()}
|
||||||
|
}
|
||||||
|
wgKeys, err := wgtypes.GeneratePrivateKey()
|
||||||
|
if err != nil {
|
||||||
|
return []string{"Failed to generate wireguard keypair: ", err.Error()}
|
||||||
|
}
|
||||||
|
return []string{"PrivateKey: " + wgKeys.String(), "PublicKey: " + wgKeys.PublicKey().String()}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetDatabaseInfo() map[string]int64 {
|
||||||
|
info := make(map[string]int64, 0)
|
||||||
|
db := database.GetDB()
|
||||||
|
if db == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var clientsCount, inboundsCount, outboundsCount, servicesCount, endpointsCount, clientUp, clientDown int64
|
||||||
|
|
||||||
|
db.Model(&model.Client{}).Count(&clientsCount)
|
||||||
|
db.Model(&model.Inbound{}).Count(&inboundsCount)
|
||||||
|
db.Model(&model.Outbound{}).Count(&outboundsCount)
|
||||||
|
db.Model(&model.Service{}).Count(&servicesCount)
|
||||||
|
db.Model(&model.Endpoint{}).Count(&endpointsCount)
|
||||||
|
db.Model(&model.Client{}).Select("COALESCE(SUM(up+total_up),0)").Scan(&clientUp)
|
||||||
|
db.Model(&model.Client{}).Select("COALESCE(SUM(down+total_down),0)").Scan(&clientDown)
|
||||||
|
|
||||||
|
info["clients"] = clientsCount
|
||||||
|
info["inbounds"] = inboundsCount
|
||||||
|
info["outbounds"] = outboundsCount
|
||||||
|
info["services"] = servicesCount
|
||||||
|
info["endpoints"] = endpointsCount
|
||||||
|
info["clientUp"] = clientUp
|
||||||
|
info["clientDown"] = clientDown
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
153
service/services.go
Normal file
153
service/services.go
Normal file
@@ -0,0 +1,153 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServicesService struct{}
|
||||||
|
|
||||||
|
func (s *ServicesService) GetAll() (*[]map[string]interface{}, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
services := []model.Service{}
|
||||||
|
err := db.Model(model.Service{}).Scan(&services).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var data []map[string]interface{}
|
||||||
|
for _, srv := range services {
|
||||||
|
srvData := map[string]interface{}{
|
||||||
|
"id": srv.Id,
|
||||||
|
"type": srv.Type,
|
||||||
|
"tag": srv.Tag,
|
||||||
|
"tls_id": srv.TlsId,
|
||||||
|
}
|
||||||
|
if srv.Options != nil {
|
||||||
|
var restFields map[string]json.RawMessage
|
||||||
|
if err := json.Unmarshal(srv.Options, &restFields); err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for k, v := range restFields {
|
||||||
|
srvData[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
data = append(data, srvData)
|
||||||
|
}
|
||||||
|
return &data, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServicesService) GetAllConfig(db *gorm.DB) ([]json.RawMessage, error) {
|
||||||
|
var servicesJson []json.RawMessage
|
||||||
|
var services []*model.Service
|
||||||
|
err := db.Model(model.Service{}).Preload("Tls").Find(&services).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, srv := range services {
|
||||||
|
srvJson, err := srv.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
servicesJson = append(servicesJson, srvJson)
|
||||||
|
}
|
||||||
|
return servicesJson, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServicesService) Save(tx *gorm.DB, act string, data json.RawMessage) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch act {
|
||||||
|
case "new", "edit":
|
||||||
|
var srv model.Service
|
||||||
|
err = srv.UnmarshalJSON(data)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if srv.TlsId > 0 {
|
||||||
|
err = tx.Model(model.Tls{}).Where("id = ?", srv.TlsId).Find(&srv.Tls).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if corePtr.IsRunning() {
|
||||||
|
configData, err := srv.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if act == "edit" {
|
||||||
|
var oldTag string
|
||||||
|
err = tx.Model(model.Service{}).Select("tag").Where("id = ?", srv.Id).Find(&oldTag).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = corePtr.RemoveService(oldTag)
|
||||||
|
if err != nil && err != os.ErrInvalid {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = corePtr.AddService(configData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = tx.Save(&srv).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "del":
|
||||||
|
var tag string
|
||||||
|
err = json.Unmarshal(data, &tag)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if corePtr.IsRunning() {
|
||||||
|
err = corePtr.RemoveService(tag)
|
||||||
|
if err != nil && err != os.ErrInvalid {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = tx.Where("tag = ?", tag).Delete(model.Service{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return common.NewErrorf("unknown action: %s", act)
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServicesService) RestartServices(tx *gorm.DB, ids []uint) error {
|
||||||
|
if !corePtr.IsRunning() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var services []*model.Service
|
||||||
|
err := tx.Model(model.Service{}).Preload("Tls").Where("id in ?", ids).Find(&services).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for _, srv := range services {
|
||||||
|
err = corePtr.RemoveService(srv.Tag)
|
||||||
|
if err != nil && err != os.ErrInvalid {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
srvConfig, err := srv.MarshalJSON()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = corePtr.AddService(srvConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
421
service/setting.go
Normal file
421
service/setting.go
Normal file
@@ -0,0 +1,421 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"runtime"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/config"
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
var defaultConfig = `{
|
||||||
|
"log": {
|
||||||
|
"level": "info"
|
||||||
|
},
|
||||||
|
"dns": {
|
||||||
|
"servers": [],
|
||||||
|
"rules": []
|
||||||
|
},
|
||||||
|
"route": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"action": "sniff"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"protocol": [
|
||||||
|
"dns"
|
||||||
|
],
|
||||||
|
"action": "hijack-dns"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"experimental": {}
|
||||||
|
}`
|
||||||
|
|
||||||
|
var defaultValueMap = map[string]string{
|
||||||
|
"webListen": "",
|
||||||
|
"webDomain": "",
|
||||||
|
"webPort": "2095",
|
||||||
|
"secret": common.Random(32),
|
||||||
|
"webCertFile": "",
|
||||||
|
"webKeyFile": "",
|
||||||
|
"webPath": "/app/",
|
||||||
|
"webURI": "",
|
||||||
|
"sessionMaxAge": "0",
|
||||||
|
"trafficAge": "30",
|
||||||
|
"timeLocation": "Asia/Tehran",
|
||||||
|
"subListen": "",
|
||||||
|
"subPort": "2096",
|
||||||
|
"subPath": "/sub/",
|
||||||
|
"subDomain": "",
|
||||||
|
"subCertFile": "",
|
||||||
|
"subKeyFile": "",
|
||||||
|
"subUpdates": "12",
|
||||||
|
"subEncode": "true",
|
||||||
|
"subShowInfo": "false",
|
||||||
|
"subURI": "",
|
||||||
|
"subJsonExt": "",
|
||||||
|
"subClashExt": "",
|
||||||
|
"config": defaultConfig,
|
||||||
|
"version": config.GetVersion(),
|
||||||
|
}
|
||||||
|
|
||||||
|
type SettingService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetAllSetting() (*map[string]string, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
settings := make([]*model.Setting, 0)
|
||||||
|
err := db.Model(model.Setting{}).Find(&settings).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
allSetting := map[string]string{}
|
||||||
|
|
||||||
|
for _, setting := range settings {
|
||||||
|
allSetting[setting.Key] = setting.Value
|
||||||
|
}
|
||||||
|
|
||||||
|
for key, defaultValue := range defaultValueMap {
|
||||||
|
if _, exists := allSetting[key]; !exists {
|
||||||
|
err = s.saveSetting(key, defaultValue)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
allSetting[key] = defaultValue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Due to security principles
|
||||||
|
delete(allSetting, "secret")
|
||||||
|
delete(allSetting, "config")
|
||||||
|
delete(allSetting, "version")
|
||||||
|
|
||||||
|
return &allSetting, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) ResetSettings() error {
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Where("1 = 1").Delete(model.Setting{}).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) getSetting(key string) (*model.Setting, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
setting := &model.Setting{}
|
||||||
|
err := db.Model(model.Setting{}).Where("key = ?", key).First(setting).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return setting, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) getString(key string) (string, error) {
|
||||||
|
setting, err := s.getSetting(key)
|
||||||
|
if database.IsNotFound(err) {
|
||||||
|
value, ok := defaultValueMap[key]
|
||||||
|
if !ok {
|
||||||
|
return "", common.NewErrorf("key <%v> not in defaultValueMap", key)
|
||||||
|
}
|
||||||
|
return value, nil
|
||||||
|
} else if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return setting.Value, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) saveSetting(key string, value string) error {
|
||||||
|
setting, err := s.getSetting(key)
|
||||||
|
db := database.GetDB()
|
||||||
|
if database.IsNotFound(err) {
|
||||||
|
return db.Create(&model.Setting{
|
||||||
|
Key: key,
|
||||||
|
Value: value,
|
||||||
|
}).Error
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
setting.Key = key
|
||||||
|
setting.Value = value
|
||||||
|
return db.Save(setting).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) setString(key string, value string) error {
|
||||||
|
return s.saveSetting(key, value)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) getBool(key string) (bool, error) {
|
||||||
|
str, err := s.getString(key)
|
||||||
|
if err != nil {
|
||||||
|
return false, err
|
||||||
|
}
|
||||||
|
return strconv.ParseBool(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
// func (s *SettingService) setBool(key string, value bool) error {
|
||||||
|
// return s.setString(key, strconv.FormatBool(value))
|
||||||
|
// }
|
||||||
|
|
||||||
|
func (s *SettingService) getInt(key string) (int, error) {
|
||||||
|
str, err := s.getString(key)
|
||||||
|
if err != nil {
|
||||||
|
return 0, err
|
||||||
|
}
|
||||||
|
return strconv.Atoi(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) setInt(key string, value int) error {
|
||||||
|
return s.setString(key, strconv.Itoa(value))
|
||||||
|
}
|
||||||
|
func (s *SettingService) GetListen() (string, error) {
|
||||||
|
return s.getString("webListen")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetWebDomain() (string, error) {
|
||||||
|
return s.getString("webDomain")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetPort() (int, error) {
|
||||||
|
return s.getInt("webPort")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetPort(port int) error {
|
||||||
|
return s.setInt("webPort", port)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetCertFile() (string, error) {
|
||||||
|
return s.getString("webCertFile")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetKeyFile() (string, error) {
|
||||||
|
return s.getString("webKeyFile")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetWebPath() (string, error) {
|
||||||
|
webPath, err := s.getString("webPath")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(webPath, "/") {
|
||||||
|
webPath = "/" + webPath
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(webPath, "/") {
|
||||||
|
webPath += "/"
|
||||||
|
}
|
||||||
|
return webPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetWebPath(webPath string) error {
|
||||||
|
if !strings.HasPrefix(webPath, "/") {
|
||||||
|
webPath = "/" + webPath
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(webPath, "/") {
|
||||||
|
webPath += "/"
|
||||||
|
}
|
||||||
|
return s.setString("webPath", webPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSecret() ([]byte, error) {
|
||||||
|
secret, err := s.getString("secret")
|
||||||
|
if secret == defaultValueMap["secret"] {
|
||||||
|
err := s.saveSetting("secret", secret)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("save secret failed:", err)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return []byte(secret), err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSessionMaxAge() (int, error) {
|
||||||
|
return s.getInt("sessionMaxAge")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetTrafficAge() (int, error) {
|
||||||
|
return s.getInt("trafficAge")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetTimeLocation() (*time.Location, error) {
|
||||||
|
l, err := s.getString("timeLocation")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
if runtime.GOOS == "windows" {
|
||||||
|
l = "Local"
|
||||||
|
}
|
||||||
|
location, err := time.LoadLocation(l)
|
||||||
|
if err != nil {
|
||||||
|
defaultLocation := defaultValueMap["timeLocation"]
|
||||||
|
logger.Errorf("location <%v> not exist, using default location: %v", l, defaultLocation)
|
||||||
|
return time.LoadLocation(defaultLocation)
|
||||||
|
}
|
||||||
|
return location, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubListen() (string, error) {
|
||||||
|
return s.getString("subListen")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubPort() (int, error) {
|
||||||
|
return s.getInt("subPort")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetSubPort(subPort int) error {
|
||||||
|
return s.setInt("subPort", subPort)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubPath() (string, error) {
|
||||||
|
subPath, err := s.getString("subPath")
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if !strings.HasPrefix(subPath, "/") {
|
||||||
|
subPath = "/" + subPath
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(subPath, "/") {
|
||||||
|
subPath += "/"
|
||||||
|
}
|
||||||
|
return subPath, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetSubPath(subPath string) error {
|
||||||
|
if !strings.HasPrefix(subPath, "/") {
|
||||||
|
subPath = "/" + subPath
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(subPath, "/") {
|
||||||
|
subPath += "/"
|
||||||
|
}
|
||||||
|
return s.setString("subPath", subPath)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubDomain() (string, error) {
|
||||||
|
return s.getString("subDomain")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubCertFile() (string, error) {
|
||||||
|
return s.getString("subCertFile")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubKeyFile() (string, error) {
|
||||||
|
return s.getString("subKeyFile")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubUpdates() (int, error) {
|
||||||
|
return s.getInt("subUpdates")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubEncode() (bool, error) {
|
||||||
|
return s.getBool("subEncode")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubShowInfo() (bool, error) {
|
||||||
|
return s.getBool("subShowInfo")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubURI() (string, error) {
|
||||||
|
return s.getString("subURI")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetFinalSubURI(host string) (string, error) {
|
||||||
|
allSetting, err := s.GetAllSetting()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
SubURI := (*allSetting)["subURI"]
|
||||||
|
if SubURI != "" {
|
||||||
|
return SubURI, nil
|
||||||
|
}
|
||||||
|
protocol := "http"
|
||||||
|
if (*allSetting)["subKeyFile"] != "" && (*allSetting)["subCertFile"] != "" {
|
||||||
|
protocol = "https"
|
||||||
|
}
|
||||||
|
if (*allSetting)["subDomain"] != "" {
|
||||||
|
host = (*allSetting)["subDomain"]
|
||||||
|
}
|
||||||
|
port := ":" + (*allSetting)["subPort"]
|
||||||
|
if (port == "80" && protocol == "http") || (port == "443" && protocol == "https") {
|
||||||
|
port = ""
|
||||||
|
}
|
||||||
|
return protocol + "://" + host + port + (*allSetting)["subPath"], nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetConfig() (string, error) {
|
||||||
|
return s.getString("config")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SetConfig(config string) error {
|
||||||
|
return s.setString("config", config)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) SaveConfig(tx *gorm.DB, config json.RawMessage) error {
|
||||||
|
configs, err := json.MarshalIndent(config, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Model(model.Setting{}).Where("key = ?", "config").Update("value", string(configs)).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) Save(tx *gorm.DB, data json.RawMessage) error {
|
||||||
|
var err error
|
||||||
|
var settings map[string]string
|
||||||
|
err = json.Unmarshal(data, &settings)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for key, obj := range settings {
|
||||||
|
// Secure file existence check
|
||||||
|
if obj != "" && (key == "webCertFile" ||
|
||||||
|
key == "webKeyFile" ||
|
||||||
|
key == "subCertFile" ||
|
||||||
|
key == "subKeyFile") {
|
||||||
|
err = s.fileExists(obj)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewError(" -> ", obj, " is not exists")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Correct Pathes start and ends with `/`
|
||||||
|
if key == "webPath" ||
|
||||||
|
key == "subPath" {
|
||||||
|
if !strings.HasPrefix(obj, "/") {
|
||||||
|
obj = "/" + obj
|
||||||
|
}
|
||||||
|
if !strings.HasSuffix(obj, "/") {
|
||||||
|
obj += "/"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Delete all stats if it is set to 0
|
||||||
|
if key == "trafficAge" && obj == "0" {
|
||||||
|
err = tx.Where("id > 0").Delete(model.Stats{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
err = tx.Model(model.Setting{}).Where("key = ?", key).Update("value", obj).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubJsonExt() (string, error) {
|
||||||
|
return s.getString("subJsonExt")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) GetSubClashExt() (string, error) {
|
||||||
|
return s.getString("subClashExt")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SettingService) fileExists(path string) error {
|
||||||
|
_, err := os.Stat(path)
|
||||||
|
return err
|
||||||
|
}
|
||||||
163
service/stats.go
Normal file
163
service/stats.go
Normal file
@@ -0,0 +1,163 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"sort"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type onlines struct {
|
||||||
|
Inbound []string `json:"inbound,omitempty"`
|
||||||
|
User []string `json:"user,omitempty"`
|
||||||
|
Outbound []string `json:"outbound,omitempty"`
|
||||||
|
}
|
||||||
|
|
||||||
|
var onlineResources = &onlines{}
|
||||||
|
|
||||||
|
type StatsService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StatsService) SaveStats(enableTraffic bool) error {
|
||||||
|
if corePtr == nil || !corePtr.IsRunning() {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
box := corePtr.GetInstance()
|
||||||
|
if box == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
st := box.StatsTracker()
|
||||||
|
if st == nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
stats := st.GetStats()
|
||||||
|
|
||||||
|
// Reset onlines
|
||||||
|
onlineResources.Inbound = nil
|
||||||
|
onlineResources.Outbound = nil
|
||||||
|
onlineResources.User = nil
|
||||||
|
|
||||||
|
if len(*stats) == 0 {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
var err error
|
||||||
|
db := database.GetDB()
|
||||||
|
tx := db.Begin()
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
tx.Commit()
|
||||||
|
} else {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
for _, stat := range *stats {
|
||||||
|
if stat.Resource == "user" {
|
||||||
|
if stat.Direction {
|
||||||
|
err = tx.Model(model.Client{}).Where("name = ?", stat.Tag).
|
||||||
|
UpdateColumn("up", gorm.Expr("up + ?", stat.Traffic)).Error
|
||||||
|
} else {
|
||||||
|
err = tx.Model(model.Client{}).Where("name = ?", stat.Tag).
|
||||||
|
UpdateColumn("down", gorm.Expr("down + ?", stat.Traffic)).Error
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if stat.Direction {
|
||||||
|
switch stat.Resource {
|
||||||
|
case "inbound":
|
||||||
|
onlineResources.Inbound = append(onlineResources.Inbound, stat.Tag)
|
||||||
|
case "outbound":
|
||||||
|
onlineResources.Outbound = append(onlineResources.Outbound, stat.Tag)
|
||||||
|
case "user":
|
||||||
|
onlineResources.User = append(onlineResources.User, stat.Tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if !enableTraffic {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
err = tx.Create(&stats).Error
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StatsService) GetStats(resource string, tag string, limit int) ([]model.Stats, error) {
|
||||||
|
var err error
|
||||||
|
var result []model.Stats
|
||||||
|
|
||||||
|
currentTime := time.Now().Unix()
|
||||||
|
timeDiff := currentTime - (int64(limit) * 3600)
|
||||||
|
|
||||||
|
db := database.GetDB()
|
||||||
|
resources := []string{resource}
|
||||||
|
if resource == "endpoint" {
|
||||||
|
resources = []string{"inbound", "outbound"}
|
||||||
|
}
|
||||||
|
err = db.Model(model.Stats{}).Where("resource in ? AND tag = ? AND date_time > ?", resources, tag, timeDiff).Scan(&result).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result = s.downsampleStats(result, 60) // 60 rows for 30 buckets
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// downsampleStats reduces stats to maxRows rows.
|
||||||
|
// Each bucket outputs two rows (direction false and true) with average Traffic.
|
||||||
|
func (s *StatsService) downsampleStats(stats []model.Stats, maxRows int) []model.Stats {
|
||||||
|
if len(stats) <= maxRows {
|
||||||
|
return stats
|
||||||
|
}
|
||||||
|
numBuckets := int(maxRows / 2)
|
||||||
|
sort.Slice(stats, func(i, j int) bool { return stats[i].DateTime < stats[j].DateTime })
|
||||||
|
timeMin, timeMax := stats[0].DateTime, stats[len(stats)-1].DateTime
|
||||||
|
bucketSpan := (timeMax - timeMin) / int64(numBuckets)
|
||||||
|
if bucketSpan == 0 {
|
||||||
|
bucketSpan = 1
|
||||||
|
}
|
||||||
|
downsampled := make([]model.Stats, 0, maxRows)
|
||||||
|
for i := 0; i < numBuckets; i++ {
|
||||||
|
bucketStart := timeMin + int64(i)*bucketSpan
|
||||||
|
bucketEnd := timeMin + int64(i+1)*bucketSpan
|
||||||
|
if i == numBuckets-1 {
|
||||||
|
bucketEnd = timeMax + 1
|
||||||
|
}
|
||||||
|
for _, dir := range []bool{false, true} {
|
||||||
|
var sum int64
|
||||||
|
var count int
|
||||||
|
for _, r := range stats {
|
||||||
|
if r.DateTime >= bucketStart && r.DateTime < bucketEnd && r.Direction == dir {
|
||||||
|
sum += r.Traffic
|
||||||
|
count++
|
||||||
|
}
|
||||||
|
}
|
||||||
|
avg := int64(0)
|
||||||
|
if count > 0 {
|
||||||
|
avg = sum / int64(count)
|
||||||
|
}
|
||||||
|
downsampled = append(downsampled, model.Stats{
|
||||||
|
DateTime: bucketStart,
|
||||||
|
Resource: stats[0].Resource,
|
||||||
|
Tag: stats[0].Tag,
|
||||||
|
Direction: dir,
|
||||||
|
Traffic: avg,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return downsampled
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *StatsService) GetOnlines() (onlines, error) {
|
||||||
|
return *onlineResources, nil
|
||||||
|
}
|
||||||
|
func (s *StatsService) DelOldStats(days int) error {
|
||||||
|
oldTime := time.Now().AddDate(0, 0, -(days)).Unix()
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Where("date_time < ?", oldTime).Delete(model.Stats{}).Error
|
||||||
|
}
|
||||||
105
service/tls.go
Normal file
105
service/tls.go
Normal file
@@ -0,0 +1,105 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TlsService struct {
|
||||||
|
InboundService
|
||||||
|
ServicesService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TlsService) GetAll() ([]model.Tls, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
tlsConfig := []model.Tls{}
|
||||||
|
err := db.Model(model.Tls{}).Scan(&tlsConfig).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return tlsConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *TlsService) Save(tx *gorm.DB, action string, data json.RawMessage, hostname string) error {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "new", "edit":
|
||||||
|
var tls model.Tls
|
||||||
|
err = json.Unmarshal(data, &tls)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = tx.Save(&tls).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if action == "edit" {
|
||||||
|
var inbounds []model.Inbound
|
||||||
|
err = tx.Model(model.Inbound{}).Preload("Tls").Where("tls_id = ?", tls.Id).Find(&inbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(inbounds) > 0 {
|
||||||
|
err = s.ClientService.UpdateLinksByInboundChange(tx, &inbounds, hostname, "")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var inboundIds []uint
|
||||||
|
for _, inbound := range inbounds {
|
||||||
|
inboundIds = append(inboundIds, inbound.Id)
|
||||||
|
}
|
||||||
|
err = s.InboundService.UpdateOutJsons(tx, inboundIds, hostname)
|
||||||
|
if err != nil {
|
||||||
|
return common.NewError("unable to update out_json of inbounds: ", err.Error())
|
||||||
|
}
|
||||||
|
err = s.InboundService.RestartInbounds(tx, inboundIds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var serviceIds []uint
|
||||||
|
err = tx.Model(model.Service{}).Where("tls_id = ?", tls.Id).Scan(&serviceIds).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(serviceIds) > 0 {
|
||||||
|
err = s.ServicesService.RestartServices(tx, serviceIds)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "del":
|
||||||
|
var id uint
|
||||||
|
err = json.Unmarshal(data, &id)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var inboundCount int64
|
||||||
|
err = tx.Model(model.Inbound{}).Where("tls_id = ?", id).Count(&inboundCount).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var serviceCount int64
|
||||||
|
err = tx.Model(model.Service{}).Where("tls_id = ?", id).Count(&serviceCount).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if inboundCount > 0 || serviceCount > 0 {
|
||||||
|
return common.NewError("tls in use")
|
||||||
|
}
|
||||||
|
err = tx.Where("id = ?", id).Delete(model.Tls{}).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
161
service/user.go
Normal file
161
service/user.go
Normal file
@@ -0,0 +1,161 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
type UserService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) GetFirstUser() (*model.User, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
|
||||||
|
user := &model.User{}
|
||||||
|
err := db.Model(model.User{}).
|
||||||
|
First(user).
|
||||||
|
Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return user, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) UpdateFirstUser(username string, password string) error {
|
||||||
|
if username == "" {
|
||||||
|
return common.NewError("username can not be empty")
|
||||||
|
} else if password == "" {
|
||||||
|
return common.NewError("password can not be empty")
|
||||||
|
}
|
||||||
|
db := database.GetDB()
|
||||||
|
user := &model.User{}
|
||||||
|
err := db.Model(model.User{}).First(user).Error
|
||||||
|
if database.IsNotFound(err) {
|
||||||
|
user.Username = username
|
||||||
|
user.Password = password
|
||||||
|
return db.Model(model.User{}).Create(user).Error
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.Username = username
|
||||||
|
user.Password = password
|
||||||
|
return db.Save(user).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) Login(username string, password string, remoteIP string) (string, error) {
|
||||||
|
user := s.CheckUser(username, password, remoteIP)
|
||||||
|
if user == nil {
|
||||||
|
return "", common.NewError("wrong user or password! IP: ", remoteIP)
|
||||||
|
}
|
||||||
|
return user.Username, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) CheckUser(username string, password string, remoteIP string) *model.User {
|
||||||
|
db := database.GetDB()
|
||||||
|
|
||||||
|
user := &model.User{}
|
||||||
|
err := db.Model(model.User{}).
|
||||||
|
Where("username = ? and password = ?", username, password).
|
||||||
|
First(user).
|
||||||
|
Error
|
||||||
|
if database.IsNotFound(err) {
|
||||||
|
return nil
|
||||||
|
} else if err != nil {
|
||||||
|
logger.Warning("check user err:", err, " IP: ", remoteIP)
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
lastLoginTxt := time.Now().Format("2006-01-02 15:04:05") + " " + remoteIP
|
||||||
|
err = db.Model(model.User{}).
|
||||||
|
Where("username = ?", username).
|
||||||
|
Update("last_logins", &lastLoginTxt).Error
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("unable to log login data", err)
|
||||||
|
}
|
||||||
|
return user
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) GetUsers() (*[]model.User, error) {
|
||||||
|
var users []model.User
|
||||||
|
db := database.GetDB()
|
||||||
|
err := db.Model(model.User{}).Select("id,username,last_logins").Scan(&users).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &users, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) ChangePass(id string, oldPass string, newUser string, newPass string) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
user := &model.User{}
|
||||||
|
err := db.Model(model.User{}).Where("id = ? AND password = ?", id, oldPass).First(user).Error
|
||||||
|
if err != nil || database.IsNotFound(err) {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
user.Username = newUser
|
||||||
|
user.Password = newPass
|
||||||
|
return db.Save(user).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) LoadTokens() ([]byte, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var tokens []model.Tokens
|
||||||
|
err := db.Model(model.Tokens{}).Preload("User").Where("expiry == 0 or expiry > ?", time.Now().Unix()).Find(&tokens).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
var result []map[string]interface{}
|
||||||
|
for _, t := range tokens {
|
||||||
|
result = append(result, map[string]interface{}{
|
||||||
|
"token": t.Token,
|
||||||
|
"expiry": t.Expiry,
|
||||||
|
"username": t.User.Username,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
jsonResult, _ := json.MarshalIndent(result, "", " ")
|
||||||
|
return jsonResult, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) GetUserTokens(username string) (*[]model.Tokens, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var token []model.Tokens
|
||||||
|
err := db.Model(model.Tokens{}).Select("id,desc,'****' as token,expiry,user_id").Where("user_id = (select id from users where username = ?)", username).Find(&token).Error
|
||||||
|
if err != nil && !database.IsNotFound(err) {
|
||||||
|
println(err.Error())
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) AddToken(username string, expiry int64, desc string) (string, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
var userId uint
|
||||||
|
err := db.Model(model.User{}).Where("username = ?", username).Select("id").Scan(&userId).Error
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if expiry > 0 {
|
||||||
|
expiry = expiry*86400 + time.Now().Unix()
|
||||||
|
}
|
||||||
|
token := &model.Tokens{
|
||||||
|
Token: common.Random(32),
|
||||||
|
Desc: desc,
|
||||||
|
Expiry: expiry,
|
||||||
|
UserId: userId,
|
||||||
|
}
|
||||||
|
err = db.Create(token).Error
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return token.Token, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *UserService) DeleteToken(id string) error {
|
||||||
|
db := database.GetDB()
|
||||||
|
return db.Model(model.Tokens{}).Where("id = ?", id).Delete(&model.Tokens{}).Error
|
||||||
|
}
|
||||||
224
service/warp.go
Normal file
224
service/warp.go
Normal file
@@ -0,0 +1,224 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"os"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"golang.zx2c4.com/wireguard/wgctrl/wgtypes"
|
||||||
|
)
|
||||||
|
|
||||||
|
type WarpService struct{}
|
||||||
|
|
||||||
|
func (s *WarpService) getWarpInfo(deviceId string, accessToken string) ([]byte, error) {
|
||||||
|
url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s", deviceId)
|
||||||
|
|
||||||
|
req, err := http.NewRequest("GET", url, nil)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+accessToken)
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil || resp.StatusCode != 200 {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
buffer := bytes.NewBuffer(make([]byte, 8192))
|
||||||
|
buffer.Reset()
|
||||||
|
_, err = buffer.ReadFrom(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
return buffer.Bytes(), nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WarpService) RegisterWarp(ep *model.Endpoint) error {
|
||||||
|
tos := time.Now().UTC().Format("2006-01-02T15:04:05.000Z")
|
||||||
|
privateKey, _ := wgtypes.GenerateKey()
|
||||||
|
publicKey := privateKey.PublicKey().String()
|
||||||
|
hostName, _ := os.Hostname()
|
||||||
|
|
||||||
|
data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "s-ui", "name": "%s"}`, publicKey, tos, hostName)
|
||||||
|
url := "https://api.cloudflareclient.com/v0a2158/reg"
|
||||||
|
|
||||||
|
req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
req.Header.Add("CF-Client-Version", "a-7.21-0721")
|
||||||
|
req.Header.Add("Content-Type", "application/json")
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil || resp.StatusCode != 200 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
buffer := bytes.NewBuffer(make([]byte, 8192))
|
||||||
|
buffer.Reset()
|
||||||
|
_, err = buffer.ReadFrom(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var rspData map[string]interface{}
|
||||||
|
err = json.Unmarshal(buffer.Bytes(), &rspData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
deviceId := rspData["id"].(string)
|
||||||
|
token := rspData["token"].(string)
|
||||||
|
license, ok := rspData["account"].(map[string]interface{})["license"].(string)
|
||||||
|
if !ok {
|
||||||
|
logger.Debug("Error accessing license value.")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
warpInfo, err := s.getWarpInfo(deviceId, token)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var warpDetails map[string]interface{}
|
||||||
|
err = json.Unmarshal(warpInfo, &warpDetails)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
warpConfig, _ := warpDetails["config"].(map[string]interface{})
|
||||||
|
clientId, _ := warpConfig["client_id"].(string)
|
||||||
|
reserved := s.getReserved(clientId)
|
||||||
|
interfaceConfig, _ := warpConfig["interface"].(map[string]interface{})
|
||||||
|
addresses, _ := interfaceConfig["addresses"].(map[string]interface{})
|
||||||
|
v4, _ := addresses["v4"].(string)
|
||||||
|
v6, _ := addresses["v6"].(string)
|
||||||
|
peer, _ := warpConfig["peers"].([]interface{})[0].(map[string]interface{})
|
||||||
|
peerEndpoint, _ := peer["endpoint"].(map[string]interface{})["host"].(string)
|
||||||
|
peerEpAddress, peerEpPort, err := net.SplitHostPort(peerEndpoint)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
peerPublicKey, _ := peer["public_key"].(string)
|
||||||
|
peerPort, _ := strconv.Atoi(peerEpPort)
|
||||||
|
|
||||||
|
peers := []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"address": peerEpAddress,
|
||||||
|
"port": peerPort,
|
||||||
|
"public_key": peerPublicKey,
|
||||||
|
"allowed_ips": []string{"0.0.0.0/0", "::/0"},
|
||||||
|
"reserved": reserved,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
|
||||||
|
warpData := map[string]interface{}{
|
||||||
|
"access_token": token,
|
||||||
|
"device_id": deviceId,
|
||||||
|
"license_key": license,
|
||||||
|
}
|
||||||
|
|
||||||
|
ep.Ext, err = json.MarshalIndent(warpData, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
var epOptions map[string]interface{}
|
||||||
|
err = json.Unmarshal(ep.Options, &epOptions)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
epOptions["private_key"] = privateKey.String()
|
||||||
|
epOptions["address"] = []string{fmt.Sprintf("%s/32", v4), fmt.Sprintf("%s/128", v6)}
|
||||||
|
epOptions["listen_port"] = 0
|
||||||
|
epOptions["peers"] = peers
|
||||||
|
|
||||||
|
ep.Options, err = json.MarshalIndent(epOptions, "", " ")
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WarpService) getReserved(clientID string) []int {
|
||||||
|
var reserved []int
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(clientID)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
hexString := ""
|
||||||
|
for _, char := range decoded {
|
||||||
|
hex := fmt.Sprintf("%02x", char)
|
||||||
|
hexString += hex
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < len(hexString); i += 2 {
|
||||||
|
hexByte := hexString[i : i+2]
|
||||||
|
decValue, err := strconv.ParseInt(hexByte, 16, 32)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
reserved = append(reserved, int(decValue))
|
||||||
|
}
|
||||||
|
|
||||||
|
return reserved
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *WarpService) SetWarpLicense(old_license string, ep *model.Endpoint) error {
|
||||||
|
var warpData map[string]string
|
||||||
|
err := json.Unmarshal(ep.Ext, &warpData)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if warpData["license_key"] == old_license {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s/account", warpData["device_id"])
|
||||||
|
data := fmt.Sprintf(`{"license": "%s"}`, warpData["license_key"])
|
||||||
|
|
||||||
|
req, err := http.NewRequest("PUT", url, bytes.NewBuffer([]byte(data)))
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
req.Header.Set("Authorization", "Bearer "+warpData["access_token"])
|
||||||
|
|
||||||
|
client := &http.Client{}
|
||||||
|
resp, err := client.Do(req)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
defer resp.Body.Close()
|
||||||
|
buffer := bytes.NewBuffer(make([]byte, 8192))
|
||||||
|
buffer.Reset()
|
||||||
|
_, err = buffer.ReadFrom(resp.Body)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
var response map[string]interface{}
|
||||||
|
err = json.Unmarshal(buffer.Bytes(), &response)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if success, ok := response["success"].(bool); ok && success == false {
|
||||||
|
errorArr, _ := response["errors"].([]interface{})
|
||||||
|
errorObj := errorArr[0].(map[string]interface{})
|
||||||
|
return common.NewError(errorObj["code"], errorObj["message"])
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
393
sub/clashService.go
Normal file
393
sub/clashService.go
Normal file
@@ -0,0 +1,393 @@
|
|||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
"github.com/alireza0/s-ui/util"
|
||||||
|
|
||||||
|
"gopkg.in/yaml.v3"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClashService struct {
|
||||||
|
service.SettingService
|
||||||
|
JsonService
|
||||||
|
LinkService
|
||||||
|
}
|
||||||
|
|
||||||
|
const basicClashConfig = `mixed-port: 7890
|
||||||
|
allow-lan: false
|
||||||
|
mode: rule
|
||||||
|
log-level: info
|
||||||
|
external-controller: 127.0.0.1:9090
|
||||||
|
tun:
|
||||||
|
enable: true
|
||||||
|
stack: system
|
||||||
|
auto-route: true
|
||||||
|
auto-detect-interface: true
|
||||||
|
dns-hijack:
|
||||||
|
- any:53
|
||||||
|
dns:
|
||||||
|
enable: true
|
||||||
|
ipv6: false
|
||||||
|
enhanced-mode: fake-ip
|
||||||
|
fake-ip-range: 198.18.0.1/16
|
||||||
|
default-nameserver:
|
||||||
|
- 8.8.8.8
|
||||||
|
- 1.1.1.1
|
||||||
|
nameserver:
|
||||||
|
- https://doh.pub/dns-query
|
||||||
|
- https://1.0.0.1/dns-query
|
||||||
|
fallback:
|
||||||
|
- tcp://9.9.9.9:53
|
||||||
|
fake-ip-filter:
|
||||||
|
- "*.lan"
|
||||||
|
- localhost
|
||||||
|
- "*.local"
|
||||||
|
rules:
|
||||||
|
- GEOIP,Private,DIRECT
|
||||||
|
- MATCH,Proxy
|
||||||
|
`
|
||||||
|
|
||||||
|
const ProxyGroups = `- name: Proxy
|
||||||
|
type: select
|
||||||
|
proxies: []
|
||||||
|
- name: Auto
|
||||||
|
type: url-test
|
||||||
|
proxies: []
|
||||||
|
url: http://www.gstatic.com/generate_204
|
||||||
|
interval: 300
|
||||||
|
tolerance: 50
|
||||||
|
`
|
||||||
|
|
||||||
|
func (s *ClashService) GetClash(subId string) (*string, []string, error) {
|
||||||
|
|
||||||
|
client, inDatas, err := s.getData(subId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outbounds, outTags, err := s.getOutbounds(client.Config, inDatas)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
links := s.LinkService.GetLinks(&client.Links, "external", "")
|
||||||
|
tagNumEnable := 0
|
||||||
|
if len(links) > 1 {
|
||||||
|
tagNumEnable = 1
|
||||||
|
}
|
||||||
|
for index, link := range links {
|
||||||
|
json, tag, err := util.GetOutbound(link, (index+1)*tagNumEnable)
|
||||||
|
if err == nil && len(tag) > 0 {
|
||||||
|
*outbounds = append(*outbounds, *json)
|
||||||
|
*outTags = append(*outTags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
basicConfig, err := s.getClashConfig()
|
||||||
|
if err != nil || len(basicConfig) == 0 {
|
||||||
|
basicConfig = basicClashConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
resultStr, err := s.ConvertToClashMeta(outbounds, basicConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
updateInterval, _ := s.SettingService.GetSubUpdates()
|
||||||
|
headers := util.GetHeaders(client, updateInterval)
|
||||||
|
|
||||||
|
return &resultStr, headers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClashService) getClashConfig() (string, error) {
|
||||||
|
subClashExt, err := s.SettingService.GetSubClashExt()
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
|
||||||
|
return subClashExt, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClashService) ConvertToClashMeta(outbounds *[]map[string]interface{}, basicConfig string) (string, error) {
|
||||||
|
var proxies []interface{}
|
||||||
|
proxyTags := make([]string, 0)
|
||||||
|
for _, obMap := range *outbounds {
|
||||||
|
|
||||||
|
t, _ := obMap["type"].(string)
|
||||||
|
if t == "selector" || t == "urltest" || t == "direct" {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
proxy := make(map[string]interface{})
|
||||||
|
proxy["name"] = obMap["tag"]
|
||||||
|
proxy["type"] = t
|
||||||
|
|
||||||
|
server, _ := obMap["server"].(string)
|
||||||
|
if len(server) > 0 && strings.Contains(server, ":") && !strings.Contains(server, ".") && !(strings.HasPrefix(server, "[") && strings.HasSuffix(server, "]")) {
|
||||||
|
server = "'[" + server + "]'"
|
||||||
|
}
|
||||||
|
proxy["server"] = server
|
||||||
|
|
||||||
|
proxy["port"] = obMap["server_port"]
|
||||||
|
|
||||||
|
switch t {
|
||||||
|
case "vmess", "vless", "tuic":
|
||||||
|
proxy["uuid"] = obMap["uuid"]
|
||||||
|
if t == "vmess" {
|
||||||
|
if alterId, ok := obMap["alter_id"].(float64); ok {
|
||||||
|
proxy["alterId"] = int(alterId)
|
||||||
|
} else {
|
||||||
|
proxy["alterId"] = 0
|
||||||
|
}
|
||||||
|
proxy["cipher"] = "auto"
|
||||||
|
}
|
||||||
|
if t == "vless" {
|
||||||
|
if flow, ok := obMap["flow"].(string); ok {
|
||||||
|
proxy["flow"] = flow
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if t == "tuic" {
|
||||||
|
proxy["password"] = obMap["password"]
|
||||||
|
if congestion_control, ok := obMap["congestion_control"].(string); ok {
|
||||||
|
proxy["congestion-controller"] = congestion_control
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "trojan":
|
||||||
|
proxy["password"] = obMap["password"]
|
||||||
|
case "socks", "http":
|
||||||
|
if t == "socks" {
|
||||||
|
proxy["type"] = "socks5"
|
||||||
|
}
|
||||||
|
proxy["username"] = obMap["username"]
|
||||||
|
proxy["password"] = obMap["password"]
|
||||||
|
case "hysteria", "hysteria2":
|
||||||
|
if _, ok := obMap["up_mbps"].(float64); ok {
|
||||||
|
proxy["up"] = obMap["up_mbps"]
|
||||||
|
}
|
||||||
|
if _, ok := obMap["down_mbps"].(float64); ok {
|
||||||
|
proxy["down"] = obMap["down_mbps"]
|
||||||
|
}
|
||||||
|
if t == "hysteria" {
|
||||||
|
proxy["auth-str"] = obMap["auth_str"]
|
||||||
|
if obfs, ok := obMap["obfs"].(string); ok {
|
||||||
|
proxy["obfs"] = obfs
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
proxy["password"] = obMap["password"]
|
||||||
|
if obfs, ok := obMap["obfs"].(map[string]interface{}); ok {
|
||||||
|
proxy["obfs"] = obfs["type"]
|
||||||
|
proxy["obfs-password"] = obfs["password"]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if portLists, ok := obMap["server_ports"].([]interface{}); ok {
|
||||||
|
var ports []string
|
||||||
|
for _, portList := range portLists {
|
||||||
|
portRange, _ := portList.(string)
|
||||||
|
ports = append(ports, strings.ReplaceAll(portRange, ":", "-"))
|
||||||
|
}
|
||||||
|
proxy["ports"] = strings.Join(ports, ",")
|
||||||
|
}
|
||||||
|
case "anytls":
|
||||||
|
proxy["password"] = obMap["password"]
|
||||||
|
if tls, ok := obMap["tls"].(map[string]interface{}); ok {
|
||||||
|
proxy["sni"] = tls["server_name"]
|
||||||
|
proxy["skip-cert-verify"] = tls["insecure"]
|
||||||
|
}
|
||||||
|
case "shadowsocks":
|
||||||
|
proxy["type"] = "ss"
|
||||||
|
proxy["cipher"] = obMap["method"]
|
||||||
|
proxy["password"] = obMap["password"]
|
||||||
|
if network, ok := obMap["network"].(string); ok && network != "tcp" {
|
||||||
|
proxy["udp"] = true
|
||||||
|
}
|
||||||
|
if uot, ok := obMap["udp_over_tcp"].(bool); ok && uot {
|
||||||
|
proxy["udp-over-tcp"] = true
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
|
||||||
|
// TLS params
|
||||||
|
tls, isTls := obMap["tls"].(map[string]interface{})
|
||||||
|
if isTls {
|
||||||
|
tlsEnabled, ok := tls["enabled"].(bool)
|
||||||
|
if ok && !tlsEnabled {
|
||||||
|
isTls = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if isTls {
|
||||||
|
proxy["tls"] = tls["enabled"]
|
||||||
|
|
||||||
|
// ALPN if exists
|
||||||
|
if alpn, ok := tls["alpn"].([]interface{}); ok {
|
||||||
|
proxy["alpn"] = alpn
|
||||||
|
}
|
||||||
|
|
||||||
|
// Add reality if exists
|
||||||
|
if reality, ok := tls["reality"].(map[string]interface{}); ok && reality["enabled"].(bool) {
|
||||||
|
reality_opts := make(map[string]interface{})
|
||||||
|
if pbk, ok := reality["public_key"].(string); ok {
|
||||||
|
reality_opts["public-key"] = pbk
|
||||||
|
}
|
||||||
|
if sid, ok := reality["short_id"].(string); ok {
|
||||||
|
reality_opts["short-id"] = sid
|
||||||
|
}
|
||||||
|
proxy["reality-opts"] = reality_opts
|
||||||
|
}
|
||||||
|
if utls, ok := tls["utls"].(map[string]interface{}); ok {
|
||||||
|
if enabled, ok := utls["enabled"].(bool); ok && enabled {
|
||||||
|
if fp, ok := utls["fingerprint"].(string); ok {
|
||||||
|
proxy["client-fingerprint"] = fp
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sni, ok := tls["server_name"].(string); ok {
|
||||||
|
if t == "vless" || t == "vmess" {
|
||||||
|
proxy["servername"] = sni
|
||||||
|
} else {
|
||||||
|
proxy["sni"] = sni
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if insecure, ok := tls["insecure"].(bool); ok && insecure {
|
||||||
|
proxy["skip-cert-verify"] = insecure
|
||||||
|
}
|
||||||
|
// ech outbounds
|
||||||
|
if ech, ok := tls["ech"].(map[string]interface{}); ok && ech["enabled"].(bool) {
|
||||||
|
ech_config, _ := ech["config"].([]interface{})
|
||||||
|
ech_string := ""
|
||||||
|
for i := 1; i < len(ech_config)-1; i++ {
|
||||||
|
ech_string += ech_config[i].(string)
|
||||||
|
}
|
||||||
|
proxy["ech-opts"] = map[string]interface{}{
|
||||||
|
"enable": true,
|
||||||
|
"config": ech_string,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Transport if exist
|
||||||
|
if transport, ok := obMap["transport"].(map[string]interface{}); ok {
|
||||||
|
tt, _ := transport["type"].(string)
|
||||||
|
switch tt {
|
||||||
|
case "http":
|
||||||
|
httpOpts := make(map[string]interface{})
|
||||||
|
if path, ok := transport["path"].([]interface{}); ok {
|
||||||
|
httpOpts["path"] = path[0]
|
||||||
|
} else if path, ok := transport["path"].(string); ok {
|
||||||
|
httpOpts["path"] = path
|
||||||
|
}
|
||||||
|
if host, ok := transport["host"].([]interface{}); ok {
|
||||||
|
httpOpts["host"] = host[0]
|
||||||
|
}
|
||||||
|
if isTls {
|
||||||
|
proxy["network"] = "h2"
|
||||||
|
proxy["h2-opts"] = httpOpts
|
||||||
|
} else {
|
||||||
|
proxy["network"] = "http"
|
||||||
|
proxy["http-opts"] = map[string]interface{}{"path": []interface{}{httpOpts["path"]}, "host": httpOpts["host"]}
|
||||||
|
}
|
||||||
|
case "ws", "httpupgrade":
|
||||||
|
proxy["network"] = "ws"
|
||||||
|
wsOpts := make(map[string]interface{})
|
||||||
|
if path, ok := transport["path"].(string); ok {
|
||||||
|
wsOpts["path"] = path
|
||||||
|
}
|
||||||
|
if headers, ok := transport["headers"].([]interface{}); ok {
|
||||||
|
wsOpts["headers"] = headers
|
||||||
|
}
|
||||||
|
if ed, ok := transport["early_data_header_name"].(string); ok {
|
||||||
|
wsOpts["early-data-header-name"] = ed
|
||||||
|
}
|
||||||
|
if tt == "httpupgrade" {
|
||||||
|
wsOpts["v2ray-http-upgrade"] = true
|
||||||
|
}
|
||||||
|
proxy["ws-opts"] = wsOpts
|
||||||
|
case "grpc":
|
||||||
|
proxy["network"] = "grpc"
|
||||||
|
grpcOpts := make(map[string]interface{})
|
||||||
|
if service_name, ok := transport["service_name"].(string); ok {
|
||||||
|
grpcOpts["grpc-service-name"] = service_name
|
||||||
|
}
|
||||||
|
proxy["grpc-opts"] = grpcOpts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Multiplex
|
||||||
|
if mux, ok := obMap["multiplex"].(map[string]interface{}); ok {
|
||||||
|
if enabled, ok := mux["enabled"].(bool); ok && enabled {
|
||||||
|
smux := make(map[string]interface{})
|
||||||
|
smux["enabled"] = true
|
||||||
|
if protocol, ok := mux["protocol"].(string); ok {
|
||||||
|
smux["protocol"] = protocol
|
||||||
|
}
|
||||||
|
if _, ok := mux["max_connections"].(float64); ok {
|
||||||
|
smux["max-connections"] = mux["max_connections"]
|
||||||
|
}
|
||||||
|
if _, ok := mux["min_streams"].(float64); ok {
|
||||||
|
smux["min-streams"] = mux["min_streams"]
|
||||||
|
}
|
||||||
|
if _, ok := mux["max_streams"].(float64); ok {
|
||||||
|
smux["max-streams"] = mux["max_streams"]
|
||||||
|
}
|
||||||
|
if _, ok := mux["padding"].(bool); ok {
|
||||||
|
smux["padding"] = mux["padding"]
|
||||||
|
}
|
||||||
|
if brutal, ok := mux["brutal"].(map[string]interface{}); ok {
|
||||||
|
if enabled, ok := brutal["enabled"].(bool); ok && enabled {
|
||||||
|
brutalOpts := make(map[string]interface{})
|
||||||
|
brutalOpts["enabled"] = true
|
||||||
|
if _, ok := brutal["up_mbps"].(float64); ok {
|
||||||
|
brutalOpts["up"] = brutal["up_mbps"]
|
||||||
|
}
|
||||||
|
if _, ok := brutal["down_mbps"].(float64); ok {
|
||||||
|
brutalOpts["down"] = brutal["down_mbps"]
|
||||||
|
}
|
||||||
|
smux["brutal-opts"] = brutalOpts
|
||||||
|
}
|
||||||
|
}
|
||||||
|
proxy["smux"] = smux
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
proxies = append(proxies, proxy)
|
||||||
|
proxyTags = append(proxyTags, obMap["tag"].(string))
|
||||||
|
}
|
||||||
|
|
||||||
|
var proxyGroups []map[string]interface{}
|
||||||
|
err := yaml.Unmarshal([]byte(ProxyGroups), &proxyGroups)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
proxyGroups[1]["proxies"] = proxyTags
|
||||||
|
proxyGroups[0]["proxies"] = append([]string{proxyGroups[1]["name"].(string)}, proxyTags...)
|
||||||
|
|
||||||
|
// Merge proxies and proxy groups if exist
|
||||||
|
var output map[string]interface{}
|
||||||
|
err = yaml.Unmarshal([]byte(basicConfig), &output)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
if p, ok := output["proxies"].([]interface{}); ok {
|
||||||
|
output["proxies"] = append(p, proxies...)
|
||||||
|
} else {
|
||||||
|
output["proxies"] = proxies
|
||||||
|
}
|
||||||
|
|
||||||
|
if pg, ok := output["proxy-groups"].([]interface{}); ok {
|
||||||
|
output["proxy-groups"] = append(pg, proxyGroups[0], proxyGroups[1])
|
||||||
|
} else {
|
||||||
|
output["proxy-groups"] = proxyGroups
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := yaml.Marshal(output)
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
return string(result), nil
|
||||||
|
}
|
||||||
326
sub/jsonService.go
Normal file
326
sub/jsonService.go
Normal file
@@ -0,0 +1,326 @@
|
|||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
"github.com/alireza0/s-ui/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
const defaultJson = `
|
||||||
|
{
|
||||||
|
"inbounds": [
|
||||||
|
{
|
||||||
|
"type": "tun",
|
||||||
|
"address": [
|
||||||
|
"172.19.0.1/30",
|
||||||
|
"fdfe:dcba:9876::1/126"
|
||||||
|
],
|
||||||
|
"mtu": 9000,
|
||||||
|
"auto_route": true,
|
||||||
|
"strict_route": false,
|
||||||
|
"endpoint_independent_nat": false,
|
||||||
|
"stack": "system",
|
||||||
|
"platform": {
|
||||||
|
"http_proxy": {
|
||||||
|
"enabled": true,
|
||||||
|
"server": "127.0.0.1",
|
||||||
|
"server_port": 2080
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "mixed",
|
||||||
|
"listen": "127.0.0.1",
|
||||||
|
"listen_port": 2080,
|
||||||
|
"users": []
|
||||||
|
}
|
||||||
|
]
|
||||||
|
}
|
||||||
|
`
|
||||||
|
|
||||||
|
type JsonService struct {
|
||||||
|
service.SettingService
|
||||||
|
LinkService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JsonService) GetJson(subId string, format string) (*string, []string, error) {
|
||||||
|
var jsonConfig map[string]interface{}
|
||||||
|
|
||||||
|
client, inDatas, err := j.getData(subId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
outbounds, outTags, err := j.getOutbounds(client.Config, inDatas)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
links := j.LinkService.GetLinks(&client.Links, "external", "")
|
||||||
|
tagNumEnable := 0
|
||||||
|
if len(links) > 1 {
|
||||||
|
tagNumEnable = 1
|
||||||
|
}
|
||||||
|
for index, link := range links {
|
||||||
|
json, tag, err := util.GetOutbound(link, (index+1)*tagNumEnable)
|
||||||
|
if err == nil && len(tag) > 0 {
|
||||||
|
*outbounds = append(*outbounds, *json)
|
||||||
|
*outTags = append(*outTags, tag)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
j.addDefaultOutbounds(outbounds, outTags)
|
||||||
|
|
||||||
|
err = json.Unmarshal([]byte(defaultJson), &jsonConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonConfig["outbounds"] = outbounds
|
||||||
|
|
||||||
|
// Add other objects from settings
|
||||||
|
j.addOthers(&jsonConfig)
|
||||||
|
|
||||||
|
result, _ := json.MarshalIndent(jsonConfig, "", " ")
|
||||||
|
resultStr := string(result)
|
||||||
|
|
||||||
|
updateInterval, _ := j.SettingService.GetSubUpdates()
|
||||||
|
headers := util.GetHeaders(client, updateInterval)
|
||||||
|
|
||||||
|
return &resultStr, headers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JsonService) getData(subId string) (*model.Client, []*model.Inbound, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
client := &model.Client{}
|
||||||
|
err := db.Model(model.Client{}).Where("enable = true and name = ?", subId).First(client).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
var clientInbounds []uint
|
||||||
|
err = json.Unmarshal(client.Inbounds, &clientInbounds)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
var inbounds []*model.Inbound
|
||||||
|
err = db.Model(model.Inbound{}).Preload("Tls").Where("id in ?", clientInbounds).Find(&inbounds).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
return client, inbounds, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JsonService) getOutbounds(clientConfig json.RawMessage, inbounds []*model.Inbound) (*[]map[string]interface{}, *[]string, error) {
|
||||||
|
var outbounds []map[string]interface{}
|
||||||
|
var configs map[string]interface{}
|
||||||
|
var outTags []string
|
||||||
|
|
||||||
|
err := json.Unmarshal(clientConfig, &configs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
for _, inData := range inbounds {
|
||||||
|
if len(inData.OutJson) < 5 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
var outbound map[string]interface{}
|
||||||
|
err = json.Unmarshal(inData.OutJson, &outbound)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
protocol, _ := outbound["type"].(string)
|
||||||
|
|
||||||
|
// Shadowsocks
|
||||||
|
if protocol == "shadowsocks" {
|
||||||
|
var userPass []string
|
||||||
|
var inbOptions map[string]interface{}
|
||||||
|
err = json.Unmarshal(inData.Options, &inbOptions)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
method, _ := inbOptions["method"].(string)
|
||||||
|
if strings.HasPrefix(method, "2022") {
|
||||||
|
inbPass, _ := inbOptions["password"].(string)
|
||||||
|
userPass = append(userPass, inbPass)
|
||||||
|
}
|
||||||
|
var pass string
|
||||||
|
if method == "2022-blake3-aes-128-gcm" {
|
||||||
|
pass, _ = configs["shadowsocks16"].(map[string]interface{})["password"].(string)
|
||||||
|
} else {
|
||||||
|
pass, _ = configs["shadowsocks"].(map[string]interface{})["password"].(string)
|
||||||
|
}
|
||||||
|
userPass = append(userPass, pass)
|
||||||
|
outbound["password"] = strings.Join(userPass, ":")
|
||||||
|
} else { // Other protocols
|
||||||
|
config, _ := configs[protocol].(map[string]interface{})
|
||||||
|
for key, value := range config {
|
||||||
|
if key == "name" || key == "alterId" || (key == "flow" && inData.TlsId == 0) {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
outbound[key] = value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var addrs []map[string]interface{}
|
||||||
|
err = json.Unmarshal(inData.Addrs, &addrs)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
tag, _ := outbound["tag"].(string)
|
||||||
|
if len(addrs) == 0 {
|
||||||
|
// For mixed protocol, use separated socks and http
|
||||||
|
if protocol == "mixed" {
|
||||||
|
outbound["tag"] = tag
|
||||||
|
j.pushMixed(&outbounds, &outTags, outbound)
|
||||||
|
} else {
|
||||||
|
outTags = append(outTags, tag)
|
||||||
|
outbounds = append(outbounds, outbound)
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for index, addr := range addrs {
|
||||||
|
// Copy original config
|
||||||
|
newOut := make(map[string]interface{}, len(outbound))
|
||||||
|
for key, value := range outbound {
|
||||||
|
newOut[key] = value
|
||||||
|
}
|
||||||
|
// Change and push copied config
|
||||||
|
newOut["server"], _ = addr["server"].(string)
|
||||||
|
port, _ := addr["server_port"].(float64)
|
||||||
|
newOut["server_port"] = int(port)
|
||||||
|
|
||||||
|
// Override TLS
|
||||||
|
if addrTls, ok := addr["tls"].(map[string]interface{}); ok {
|
||||||
|
outTls, _ := newOut["tls"].(map[string]interface{})
|
||||||
|
if outTls == nil {
|
||||||
|
outTls = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
for key, value := range addrTls {
|
||||||
|
outTls[key] = value
|
||||||
|
}
|
||||||
|
newOut["tls"] = outTls
|
||||||
|
}
|
||||||
|
|
||||||
|
remark, _ := addr["remark"].(string)
|
||||||
|
newTag := fmt.Sprintf("%d.%s%s", index+1, tag, remark)
|
||||||
|
newOut["tag"] = newTag
|
||||||
|
// For mixed protocol, use separated socks and http
|
||||||
|
if protocol == "mixed" {
|
||||||
|
j.pushMixed(&outbounds, &outTags, newOut)
|
||||||
|
} else {
|
||||||
|
outTags = append(outTags, newTag)
|
||||||
|
outbounds = append(outbounds, newOut)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &outbounds, &outTags, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JsonService) addDefaultOutbounds(outbounds *[]map[string]interface{}, outTags *[]string) {
|
||||||
|
outbound := []map[string]interface{}{
|
||||||
|
{
|
||||||
|
"outbounds": append([]string{"auto", "direct"}, *outTags...),
|
||||||
|
"tag": "proxy",
|
||||||
|
"type": "selector",
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"tag": "auto",
|
||||||
|
"type": "urltest",
|
||||||
|
"outbounds": outTags,
|
||||||
|
"url": "http://www.gstatic.com/generate_204",
|
||||||
|
"interval": "10m",
|
||||||
|
"tolerance": 50,
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "direct",
|
||||||
|
"tag": "direct",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
*outbounds = append(outbound, *outbounds...)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JsonService) addOthers(jsonConfig *map[string]interface{}) error {
|
||||||
|
rules_start := []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"action": "sniff",
|
||||||
|
},
|
||||||
|
map[string]interface{}{
|
||||||
|
"clash_mode": "Direct",
|
||||||
|
"action": "route",
|
||||||
|
"outbound": "direct",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
rules_end := []interface{}{
|
||||||
|
map[string]interface{}{
|
||||||
|
"clash_mode": "Global",
|
||||||
|
"action": "route",
|
||||||
|
"outbound": "proxy",
|
||||||
|
},
|
||||||
|
}
|
||||||
|
route := map[string]interface{}{
|
||||||
|
"auto_detect_interface": true,
|
||||||
|
"final": "proxy",
|
||||||
|
"rules": rules_start,
|
||||||
|
}
|
||||||
|
|
||||||
|
othersStr, err := j.SettingService.GetSubJsonExt()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if len(othersStr) == 0 {
|
||||||
|
(*jsonConfig)["route"] = route
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var othersJson map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(othersStr), &othersJson)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if _, ok := othersJson["log"]; ok {
|
||||||
|
(*jsonConfig)["log"] = othersJson["log"]
|
||||||
|
}
|
||||||
|
if _, ok := othersJson["dns"]; ok {
|
||||||
|
(*jsonConfig)["dns"] = othersJson["dns"]
|
||||||
|
}
|
||||||
|
if _, ok := othersJson["inbounds"]; ok {
|
||||||
|
(*jsonConfig)["inbounds"] = othersJson["inbounds"]
|
||||||
|
}
|
||||||
|
if _, ok := othersJson["experimental"]; ok {
|
||||||
|
(*jsonConfig)["experimental"] = othersJson["experimental"]
|
||||||
|
}
|
||||||
|
if _, ok := othersJson["rule_set"]; ok {
|
||||||
|
route["rule_set"] = othersJson["rule_set"]
|
||||||
|
}
|
||||||
|
if settingRules, ok := othersJson["rules"].([]interface{}); ok {
|
||||||
|
rules := append(rules_start, settingRules...)
|
||||||
|
route["rules"] = append(rules, rules_end...)
|
||||||
|
}
|
||||||
|
if defaultDomainResolver, ok := othersJson["default_domain_resolver"].(string); ok {
|
||||||
|
route["default_domain_resolver"] = defaultDomainResolver
|
||||||
|
}
|
||||||
|
(*jsonConfig)["route"] = route
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *JsonService) pushMixed(outbounds *[]map[string]interface{}, outTags *[]string, out map[string]interface{}) {
|
||||||
|
socksOut := make(map[string]interface{}, 1)
|
||||||
|
httpOut := make(map[string]interface{}, 1)
|
||||||
|
for key, value := range out {
|
||||||
|
socksOut[key] = value
|
||||||
|
httpOut[key] = value
|
||||||
|
}
|
||||||
|
socksTag := fmt.Sprintf("%s-socks", out["tag"])
|
||||||
|
httpTag := fmt.Sprintf("%s-http", out["tag"])
|
||||||
|
socksOut["type"] = "socks"
|
||||||
|
httpOut["type"] = "http"
|
||||||
|
socksOut["tag"] = socksTag
|
||||||
|
httpOut["tag"] = httpTag
|
||||||
|
*outbounds = append(*outbounds, socksOut, httpOut)
|
||||||
|
*outTags = append(*outTags, socksTag, httpTag)
|
||||||
|
}
|
||||||
74
sub/linkService.go
Normal file
74
sub/linkService.go
Normal file
@@ -0,0 +1,74 @@
|
|||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
Uri string `json:"uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
type LinkService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkService) GetLinks(linkJson *json.RawMessage, types string, clientInfo string) []string {
|
||||||
|
links := []Link{}
|
||||||
|
var result []string
|
||||||
|
err := json.Unmarshal(*linkJson, &links)
|
||||||
|
if err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
for _, link := range links {
|
||||||
|
switch link.Type {
|
||||||
|
case "external":
|
||||||
|
result = append(result, link.Uri)
|
||||||
|
case "sub":
|
||||||
|
subLinks := util.GetExternalLink(link.Uri)
|
||||||
|
result = append(result, strings.Split(subLinks, "\n")...)
|
||||||
|
case "local":
|
||||||
|
if types == "all" {
|
||||||
|
result = append(result, s.addClientInfo(link.Uri, clientInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return result
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *LinkService) addClientInfo(uri string, clientInfo string) string {
|
||||||
|
if len(clientInfo) == 0 {
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
protocol := strings.Split(uri, "://")
|
||||||
|
if len(protocol) < 2 {
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
switch protocol[0] {
|
||||||
|
case "vmess":
|
||||||
|
var vmessJson map[string]interface{}
|
||||||
|
config, err := util.B64StrToByte(protocol[1])
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("sub: Error decoding vmess content:", err)
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(config, &vmessJson)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("sub: Error decoding vmess content:", err)
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
vmessJson["ps"] = vmessJson["ps"].(string) + clientInfo
|
||||||
|
result, err := json.MarshalIndent(vmessJson, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("sub: Error decoding vmess + clientInfo content:", err)
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
return "vmess://" + util.ByteToB64Str(result)
|
||||||
|
default:
|
||||||
|
return uri + clientInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
162
sub/sub.go
Normal file
162
sub/sub.go
Normal file
@@ -0,0 +1,162 @@
|
|||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
"crypto/tls"
|
||||||
|
"io"
|
||||||
|
"net"
|
||||||
|
"net/http"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/config"
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/middleware"
|
||||||
|
"github.com/alireza0/s-ui/network"
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type Server struct {
|
||||||
|
httpServer *http.Server
|
||||||
|
listener net.Listener
|
||||||
|
ctx context.Context
|
||||||
|
cancel context.CancelFunc
|
||||||
|
|
||||||
|
service.SettingService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewServer() *Server {
|
||||||
|
ctx, cancel := context.WithCancel(context.Background())
|
||||||
|
return &Server{
|
||||||
|
ctx: ctx,
|
||||||
|
cancel: cancel,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||||
|
if config.IsDebug() {
|
||||||
|
gin.SetMode(gin.DebugMode)
|
||||||
|
} else {
|
||||||
|
gin.DefaultWriter = io.Discard
|
||||||
|
gin.DefaultErrorWriter = io.Discard
|
||||||
|
gin.SetMode(gin.ReleaseMode)
|
||||||
|
}
|
||||||
|
|
||||||
|
engine := gin.Default()
|
||||||
|
|
||||||
|
subPath, err := s.SettingService.GetSubPath()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
subDomain, err := s.SettingService.GetSubDomain()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
if subDomain != "" {
|
||||||
|
engine.Use(middleware.DomainValidator(subDomain))
|
||||||
|
}
|
||||||
|
|
||||||
|
g := engine.Group(subPath)
|
||||||
|
NewSubHandler(g)
|
||||||
|
|
||||||
|
return engine, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Start() (err error) {
|
||||||
|
//This is an anonymous function, no function name
|
||||||
|
defer func() {
|
||||||
|
if err != nil {
|
||||||
|
s.Stop()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
engine, err := s.initRouter()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
certFile, err := s.SettingService.GetSubCertFile()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
keyFile, err := s.SettingService.GetSubKeyFile()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
listen, err := s.SettingService.GetSubListen()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
port, err := s.SettingService.GetSubPort()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
|
||||||
|
listener, err := net.Listen("tcp", listenAddr)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if certFile != "" || keyFile != "" {
|
||||||
|
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||||
|
if err != nil {
|
||||||
|
listener.Close()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
c := &tls.Config{
|
||||||
|
Certificates: []tls.Certificate{cert},
|
||||||
|
}
|
||||||
|
listener = network.NewAutoHttpsListener(listener)
|
||||||
|
listener = tls.NewListener(listener, c)
|
||||||
|
}
|
||||||
|
|
||||||
|
if certFile != "" || keyFile != "" {
|
||||||
|
logger.Info("Sub server run https on", listener.Addr())
|
||||||
|
} else {
|
||||||
|
logger.Info("Sub server run http on", listener.Addr())
|
||||||
|
}
|
||||||
|
s.listener = listener
|
||||||
|
|
||||||
|
s.httpServer = &http.Server{
|
||||||
|
Handler: engine,
|
||||||
|
}
|
||||||
|
|
||||||
|
go func() {
|
||||||
|
s.httpServer.Serve(listener)
|
||||||
|
}()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) Stop() error {
|
||||||
|
var err error
|
||||||
|
if s.httpServer != nil {
|
||||||
|
shutdownCtx, cancelShutdown := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
err = s.httpServer.Shutdown(shutdownCtx)
|
||||||
|
cancelShutdown()
|
||||||
|
if err != nil {
|
||||||
|
s.cancel()
|
||||||
|
if s.listener != nil {
|
||||||
|
_ = s.listener.Close()
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if s.listener != nil {
|
||||||
|
err = s.listener.Close()
|
||||||
|
if err != nil {
|
||||||
|
s.cancel()
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
s.cancel()
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Server) GetCtx() context.Context {
|
||||||
|
return s.ctx
|
||||||
|
}
|
||||||
78
sub/subHandler.go
Normal file
78
sub/subHandler.go
Normal file
@@ -0,0 +1,78 @@
|
|||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubHandler struct {
|
||||||
|
service.SettingService
|
||||||
|
SubService
|
||||||
|
JsonService
|
||||||
|
ClashService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSubHandler(g *gin.RouterGroup) {
|
||||||
|
a := &SubHandler{}
|
||||||
|
a.initRouter(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubHandler) initRouter(g *gin.RouterGroup) {
|
||||||
|
g.GET("/:subid", s.subs)
|
||||||
|
g.HEAD("/:subid", s.subHeaders)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubHandler) subs(c *gin.Context) {
|
||||||
|
var headers []string
|
||||||
|
var result *string
|
||||||
|
var err error
|
||||||
|
subId := c.Param("subid")
|
||||||
|
format, isFormat := c.GetQuery("format")
|
||||||
|
if isFormat {
|
||||||
|
switch format {
|
||||||
|
case "json":
|
||||||
|
result, headers, err = s.JsonService.GetJson(subId, format)
|
||||||
|
case "clash":
|
||||||
|
result, headers, err = s.ClashService.GetClash(subId)
|
||||||
|
}
|
||||||
|
if err != nil || result == nil {
|
||||||
|
logger.Error(err)
|
||||||
|
c.String(400, "Error!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
result, headers, err = s.SubService.GetSubs(subId)
|
||||||
|
if err != nil || result == nil {
|
||||||
|
logger.Error(err)
|
||||||
|
c.String(400, "Error!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
s.addHeaders(c, headers)
|
||||||
|
|
||||||
|
c.String(200, *result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubHandler) subHeaders(c *gin.Context) {
|
||||||
|
subId := c.Param("subid")
|
||||||
|
client, err := s.SubService.getClientBySubId(subId)
|
||||||
|
if err != nil {
|
||||||
|
logger.Error(err)
|
||||||
|
c.String(400, "Error!")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
headers := s.SubService.getClientHeaders(client)
|
||||||
|
s.addHeaders(c, headers)
|
||||||
|
|
||||||
|
c.Status(200)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubHandler) addHeaders(c *gin.Context, headers []string) {
|
||||||
|
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
|
||||||
|
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
|
||||||
|
c.Writer.Header().Set("Profile-Title", headers[2])
|
||||||
|
}
|
||||||
93
sub/subService.go
Normal file
93
sub/subService.go
Normal file
@@ -0,0 +1,93 @@
|
|||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"fmt"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database"
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/service"
|
||||||
|
"github.com/alireza0/s-ui/util"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubService struct {
|
||||||
|
service.SettingService
|
||||||
|
LinkService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) GetSubs(subId string) (*string, []string, error) {
|
||||||
|
var err error
|
||||||
|
|
||||||
|
client, err := s.getClientBySubId(subId)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientInfo := ""
|
||||||
|
subShowInfo, _ := s.SettingService.GetSubShowInfo()
|
||||||
|
if subShowInfo {
|
||||||
|
clientInfo = s.getClientInfo(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
linksArray := s.LinkService.GetLinks(&client.Links, "all", clientInfo)
|
||||||
|
result := strings.Join(linksArray, "\n")
|
||||||
|
|
||||||
|
headers := s.getClientHeaders(client)
|
||||||
|
|
||||||
|
subEncode, _ := s.SettingService.GetSubEncode()
|
||||||
|
if subEncode {
|
||||||
|
result = base64.StdEncoding.EncodeToString([]byte(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, headers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (j *SubService) getClientBySubId(subId string) (*model.Client, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
client := &model.Client{}
|
||||||
|
err := db.Model(model.Client{}).Where("enable = true and name = ?", subId).First(client).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return client, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) getClientHeaders(client *model.Client) []string {
|
||||||
|
updateInterval, _ := s.SettingService.GetSubUpdates()
|
||||||
|
return util.GetHeaders(client, updateInterval)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) getClientInfo(c *model.Client) string {
|
||||||
|
now := time.Now().Unix()
|
||||||
|
|
||||||
|
var result []string
|
||||||
|
if vol := c.Volume - (c.Up + c.Down); vol > 0 {
|
||||||
|
result = append(result, fmt.Sprintf("%s%s", s.formatTraffic(vol), "📊"))
|
||||||
|
}
|
||||||
|
if c.Expiry > 0 {
|
||||||
|
result = append(result, fmt.Sprintf("%d%s⏳", (c.Expiry-now)/86400, "Days"))
|
||||||
|
}
|
||||||
|
if len(result) > 0 {
|
||||||
|
return " " + strings.Join(result, " ")
|
||||||
|
} else {
|
||||||
|
return " ♾"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) formatTraffic(trafficBytes int64) string {
|
||||||
|
if trafficBytes < 1024 {
|
||||||
|
return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1))
|
||||||
|
} else if trafficBytes < (1024 * 1024) {
|
||||||
|
return fmt.Sprintf("%.2fKB", float64(trafficBytes)/float64(1024))
|
||||||
|
} else if trafficBytes < (1024 * 1024 * 1024) {
|
||||||
|
return fmt.Sprintf("%.2fMB", float64(trafficBytes)/float64(1024*1024))
|
||||||
|
} else if trafficBytes < (1024 * 1024 * 1024 * 1024) {
|
||||||
|
return fmt.Sprintf("%.2fGB", float64(trafficBytes)/float64(1024*1024*1024))
|
||||||
|
} else if trafficBytes < (1024 * 1024 * 1024 * 1024 * 1024) {
|
||||||
|
return fmt.Sprintf("%.2fTB", float64(trafficBytes)/float64(1024*1024*1024*1024))
|
||||||
|
} else {
|
||||||
|
return fmt.Sprintf("%.2fEB", float64(trafficBytes)/float64(1024*1024*1024*1024*1024))
|
||||||
|
}
|
||||||
|
}
|
||||||
20
util/base64.go
Normal file
20
util/base64.go
Normal file
@@ -0,0 +1,20 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import "encoding/base64"
|
||||||
|
|
||||||
|
// Function to return decoded bytes if a string is Base64 encoded
|
||||||
|
func StrOrBase64Encoded(str string) string {
|
||||||
|
decoded, err := base64.StdEncoding.DecodeString(str)
|
||||||
|
if err == nil {
|
||||||
|
return string(decoded)
|
||||||
|
}
|
||||||
|
return str
|
||||||
|
}
|
||||||
|
|
||||||
|
func B64StrToByte(str string) ([]byte, error) {
|
||||||
|
return base64.StdEncoding.DecodeString(str)
|
||||||
|
}
|
||||||
|
|
||||||
|
func ByteToB64Str(b []byte) string {
|
||||||
|
return base64.StdEncoding.EncodeToString(b)
|
||||||
|
}
|
||||||
45
util/common/array.go
Normal file
45
util/common/array.go
Normal file
@@ -0,0 +1,45 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
// UnionUintArray returns a new unique slice that contains all elements from both input slices
|
||||||
|
func UnionUintArray(a []uint, b []uint) []uint {
|
||||||
|
m := make(map[uint]bool)
|
||||||
|
for _, v := range a {
|
||||||
|
m[v] = true
|
||||||
|
}
|
||||||
|
for _, v := range b {
|
||||||
|
m[v] = true
|
||||||
|
}
|
||||||
|
var res []uint
|
||||||
|
for k := range m {
|
||||||
|
res = append(res, k)
|
||||||
|
}
|
||||||
|
return res
|
||||||
|
}
|
||||||
|
|
||||||
|
// Find different elements in two slices
|
||||||
|
// Returns elements in 'a' that are not in 'b' and elements in 'b' that are not in 'a'
|
||||||
|
func DiffUintArray(a []uint, b []uint) []uint {
|
||||||
|
different := []uint{}
|
||||||
|
set := make(map[uint]bool)
|
||||||
|
|
||||||
|
for _, item := range a {
|
||||||
|
set[item] = true
|
||||||
|
}
|
||||||
|
for _, item := range b {
|
||||||
|
if !set[item] {
|
||||||
|
different = append(different, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
set = make(map[uint]bool)
|
||||||
|
for _, item := range b {
|
||||||
|
set[item] = true
|
||||||
|
}
|
||||||
|
for _, item := range a {
|
||||||
|
if !set[item] {
|
||||||
|
different = append(different, item)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return different
|
||||||
|
}
|
||||||
28
util/common/err.go
Normal file
28
util/common/err.go
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
)
|
||||||
|
|
||||||
|
func NewErrorf(format string, a ...interface{}) error {
|
||||||
|
msg := fmt.Sprintf(format, a...)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewError(a ...interface{}) error {
|
||||||
|
msg := fmt.Sprintln(a...)
|
||||||
|
return errors.New(msg)
|
||||||
|
}
|
||||||
|
|
||||||
|
func Recover(msg string) interface{} {
|
||||||
|
panicErr := recover()
|
||||||
|
if panicErr != nil {
|
||||||
|
if msg != "" {
|
||||||
|
logger.Error(msg, "panic:", panicErr)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return panicErr
|
||||||
|
}
|
||||||
55
util/common/random.go
Normal file
55
util/common/random.go
Normal file
@@ -0,0 +1,55 @@
|
|||||||
|
package common
|
||||||
|
|
||||||
|
import (
|
||||||
|
crand "crypto/rand"
|
||||||
|
"math/big"
|
||||||
|
mrand "math/rand"
|
||||||
|
"sync"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var (
|
||||||
|
allSeq []rune = []rune{
|
||||||
|
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M', 'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z',
|
||||||
|
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm', 'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
|
||||||
|
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||||
|
}
|
||||||
|
|
||||||
|
fallbackRand = mrand.New(mrand.NewSource(time.Now().UnixNano()))
|
||||||
|
fallbackMu = sync.Mutex{}
|
||||||
|
)
|
||||||
|
|
||||||
|
func Random(n int) string {
|
||||||
|
if n <= 0 || len(allSeq) == 0 {
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
result := make([]rune, n)
|
||||||
|
maxBig := big.NewInt(int64(len(allSeq)))
|
||||||
|
for i := 0; i < n; i++ {
|
||||||
|
num, err := crand.Int(crand.Reader, maxBig)
|
||||||
|
if err != nil {
|
||||||
|
// fallback
|
||||||
|
fallbackMu.Lock()
|
||||||
|
result[i] = allSeq[fallbackRand.Intn(len(allSeq))]
|
||||||
|
fallbackMu.Unlock()
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
result[i] = allSeq[int(num.Int64())]
|
||||||
|
}
|
||||||
|
return string(result)
|
||||||
|
}
|
||||||
|
|
||||||
|
func RandomInt(n int) int {
|
||||||
|
if n <= 0 {
|
||||||
|
return 0
|
||||||
|
}
|
||||||
|
max := big.NewInt(int64(n))
|
||||||
|
result, err := crand.Int(crand.Reader, max)
|
||||||
|
if err != nil {
|
||||||
|
// fallback
|
||||||
|
fallbackMu.Lock()
|
||||||
|
defer fallbackMu.Unlock()
|
||||||
|
return fallbackRand.Intn(n)
|
||||||
|
}
|
||||||
|
return int(result.Int64())
|
||||||
|
}
|
||||||
615
util/genLink.go
Normal file
615
util/genLink.go
Normal file
@@ -0,0 +1,615 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net/url"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
var InboundTypeWithLink = []string{"socks", "http", "mixed", "shadowsocks", "naive", "hysteria", "hysteria2", "anytls", "tuic", "vless", "trojan", "vmess"}
|
||||||
|
|
||||||
|
type LinkParam struct {
|
||||||
|
Key string
|
||||||
|
Value string
|
||||||
|
}
|
||||||
|
|
||||||
|
func LinkGenerator(clientConfig json.RawMessage, i *model.Inbound, hostname string) []string {
|
||||||
|
inbound, err := i.MarshalFull()
|
||||||
|
if err != nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var tls map[string]interface{}
|
||||||
|
if i.TlsId > 0 {
|
||||||
|
tls = prepareTls(i.Tls)
|
||||||
|
}
|
||||||
|
|
||||||
|
var userConfig map[string]map[string]interface{}
|
||||||
|
if err := json.Unmarshal(clientConfig, &userConfig); err != nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
var Addrs []map[string]interface{}
|
||||||
|
if err := json.Unmarshal(i.Addrs, &Addrs); err != nil {
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
if len(Addrs) == 0 {
|
||||||
|
Addrs = append(Addrs, map[string]interface{}{
|
||||||
|
"server": hostname,
|
||||||
|
"server_port": (*inbound)["listen_port"],
|
||||||
|
"remark": i.Tag,
|
||||||
|
})
|
||||||
|
if i.TlsId > 0 {
|
||||||
|
Addrs[0]["tls"] = tls
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
for index, addr := range Addrs {
|
||||||
|
addrRemark, _ := addr["remark"].(string)
|
||||||
|
Addrs[index]["remark"] = i.Tag + addrRemark
|
||||||
|
if i.TlsId > 0 {
|
||||||
|
newTls := map[string]interface{}{}
|
||||||
|
for k, v := range tls {
|
||||||
|
newTls[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
// Override tls
|
||||||
|
if addrTls, ok := addr["tls"].(map[string]interface{}); ok {
|
||||||
|
for k, v := range addrTls {
|
||||||
|
newTls[k] = v
|
||||||
|
}
|
||||||
|
}
|
||||||
|
Addrs[index]["tls"] = newTls
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
switch i.Type {
|
||||||
|
case "socks":
|
||||||
|
return socksLink(userConfig["socks"], Addrs)
|
||||||
|
case "http":
|
||||||
|
return httpLink(userConfig["http"], Addrs)
|
||||||
|
case "mixed":
|
||||||
|
return append(
|
||||||
|
socksLink(userConfig["socks"], Addrs),
|
||||||
|
httpLink(userConfig["http"], Addrs)...,
|
||||||
|
)
|
||||||
|
case "shadowsocks":
|
||||||
|
return shadowsocksLink(userConfig, *inbound, Addrs)
|
||||||
|
case "naive":
|
||||||
|
return naiveLink(userConfig["naive"], *inbound, Addrs)
|
||||||
|
case "hysteria":
|
||||||
|
return hysteriaLink(userConfig["hysteria"], *inbound, Addrs)
|
||||||
|
case "hysteria2":
|
||||||
|
return hysteria2Link(userConfig["hysteria2"], *inbound, Addrs)
|
||||||
|
case "tuic":
|
||||||
|
return tuicLink(userConfig["tuic"], *inbound, Addrs)
|
||||||
|
case "vless":
|
||||||
|
return vlessLink(userConfig["vless"], *inbound, Addrs)
|
||||||
|
case "anytls":
|
||||||
|
return anytlsLink(userConfig["anytls"], Addrs)
|
||||||
|
case "trojan":
|
||||||
|
return trojanLink(userConfig["trojan"], *inbound, Addrs)
|
||||||
|
case "vmess":
|
||||||
|
return vmessLink(userConfig["vmess"], *inbound, Addrs)
|
||||||
|
}
|
||||||
|
|
||||||
|
return []string{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func prepareTls(t *model.Tls) map[string]interface{} {
|
||||||
|
var iTls, oTls map[string]interface{}
|
||||||
|
if err := json.Unmarshal(t.Client, &oTls); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
if err := json.Unmarshal(t.Server, &iTls); err != nil {
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
for k, v := range iTls {
|
||||||
|
switch k {
|
||||||
|
case "enabled", "server_name", "alpn":
|
||||||
|
oTls[k] = v
|
||||||
|
case "reality":
|
||||||
|
reality := v.(map[string]interface{})
|
||||||
|
clientReality := oTls["reality"].(map[string]interface{})
|
||||||
|
clientReality["enabled"] = reality["enabled"]
|
||||||
|
if shortIDs, hasSIds := reality["short_id"].([]interface{}); hasSIds && len(shortIDs) > 0 {
|
||||||
|
clientReality["short_id"] = shortIDs[common.RandomInt(len(shortIDs))]
|
||||||
|
}
|
||||||
|
oTls["reality"] = clientReality
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return oTls
|
||||||
|
}
|
||||||
|
|
||||||
|
func socksLink(userConfig map[string]interface{}, addrs []map[string]interface{}) []string {
|
||||||
|
var links []string
|
||||||
|
for _, addr := range addrs {
|
||||||
|
links = append(links, fmt.Sprintf("socks5://%s:%s@%s:%d", userConfig["username"], userConfig["password"], addr["server"].(string), uint(addr["server_port"].(float64))))
|
||||||
|
}
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func httpLink(userConfig map[string]interface{}, addrs []map[string]interface{}) []string {
|
||||||
|
var links []string
|
||||||
|
protocol := "http"
|
||||||
|
for _, addr := range addrs {
|
||||||
|
if addr["tls"] != nil {
|
||||||
|
protocol = "https"
|
||||||
|
}
|
||||||
|
links = append(links, fmt.Sprintf("%s://%s:%s@%s:%d", protocol, userConfig["username"], userConfig["password"], addr["server"].(string), uint(addr["server_port"].(float64))))
|
||||||
|
}
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func shadowsocksLink(
|
||||||
|
userConfig map[string]map[string]interface{},
|
||||||
|
inbound map[string]interface{},
|
||||||
|
addrs []map[string]interface{}) []string {
|
||||||
|
|
||||||
|
var userPass []string
|
||||||
|
method, _ := inbound["method"].(string)
|
||||||
|
if strings.HasPrefix(method, "2022") {
|
||||||
|
inbPass, _ := inbound["password"].(string)
|
||||||
|
userPass = append(userPass, inbPass)
|
||||||
|
}
|
||||||
|
var pass string
|
||||||
|
if method == "2022-blake3-aes-128-gcm" {
|
||||||
|
pass, _ = userConfig["shadowsocks16"]["password"].(string)
|
||||||
|
} else {
|
||||||
|
pass, _ = userConfig["shadowsocks"]["password"].(string)
|
||||||
|
}
|
||||||
|
userPass = append(userPass, pass)
|
||||||
|
|
||||||
|
uriBase := fmt.Sprintf("ss://%s", toBase64([]byte(fmt.Sprintf("%s:%s", method, strings.Join(userPass, ":")))))
|
||||||
|
|
||||||
|
var links []string
|
||||||
|
for _, addr := range addrs {
|
||||||
|
port, _ := addr["server_port"].(float64)
|
||||||
|
links = append(links, fmt.Sprintf("%s@%s:%.0f#%s", uriBase, addr["server"].(string), port, addr["remark"].(string)))
|
||||||
|
}
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func naiveLink(
|
||||||
|
userConfig map[string]interface{},
|
||||||
|
inbound map[string]interface{},
|
||||||
|
addrs []map[string]interface{}) []string {
|
||||||
|
|
||||||
|
password, _ := userConfig["password"].(string)
|
||||||
|
username, _ := userConfig["username"].(string)
|
||||||
|
|
||||||
|
baseUri := "http2://"
|
||||||
|
var links []string
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
var params []LinkParam
|
||||||
|
params = append(params, LinkParam{"padding", "1"})
|
||||||
|
if tls, ok := addr["tls"].(map[string]interface{}); ok {
|
||||||
|
if sni, ok := tls["server_name"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"peer", sni})
|
||||||
|
}
|
||||||
|
if alpn, ok := tls["alpn"].([]interface{}); ok {
|
||||||
|
alpnList := make([]string, len(alpn))
|
||||||
|
for i, v := range alpn {
|
||||||
|
alpnList[i] = v.(string)
|
||||||
|
}
|
||||||
|
params = append(params, LinkParam{"alpn", strings.Join(alpnList, ",")})
|
||||||
|
}
|
||||||
|
if insecure, ok := tls["insecure"].(bool); ok && insecure {
|
||||||
|
params = append(params, LinkParam{"insecure", "1"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tfo, ok := inbound["tcp_fast_open"].(bool); ok && tfo {
|
||||||
|
params = append(params, LinkParam{"tfo", "1"})
|
||||||
|
} else {
|
||||||
|
params = append(params, LinkParam{"tfo", "0"})
|
||||||
|
}
|
||||||
|
|
||||||
|
port, _ := addr["server_port"].(float64)
|
||||||
|
uri := baseUri + toBase64([]byte(fmt.Sprintf("%s:%s@%s:%.0f", username, password, addr["server"].(string), port)))
|
||||||
|
links = append(links, addParams(uri, params, addr["remark"].(string)))
|
||||||
|
}
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func hysteriaLink(
|
||||||
|
userConfig map[string]interface{},
|
||||||
|
inbound map[string]interface{},
|
||||||
|
addrs []map[string]interface{}) []string {
|
||||||
|
|
||||||
|
baseUri := "hysteria://"
|
||||||
|
var links []string
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
var params []LinkParam
|
||||||
|
if upmbps, ok := inbound["up_mbps"].(float64); ok {
|
||||||
|
params = append(params, LinkParam{"downmbps", fmt.Sprintf("%.0f", upmbps)})
|
||||||
|
}
|
||||||
|
if downmbps, ok := inbound["down_mbps"].(float64); ok {
|
||||||
|
params = append(params, LinkParam{"upmbps", fmt.Sprintf("%.0f", downmbps)})
|
||||||
|
}
|
||||||
|
if auth, ok := userConfig["auth_str"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"auth", auth})
|
||||||
|
}
|
||||||
|
if tls, ok := addr["tls"].(map[string]interface{}); ok {
|
||||||
|
getTlsParams(¶ms, tls, "insecure")
|
||||||
|
}
|
||||||
|
if obfs, ok := inbound["obfs"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"obfs", obfs})
|
||||||
|
}
|
||||||
|
if tfo, ok := inbound["tcp_fast_open"].(bool); ok && tfo {
|
||||||
|
params = append(params, LinkParam{"fastopen", "1"})
|
||||||
|
} else {
|
||||||
|
params = append(params, LinkParam{"fastopen", "0"})
|
||||||
|
}
|
||||||
|
var outJson map[string]interface{}
|
||||||
|
if err := json.Unmarshal(inbound["out_json"].(json.RawMessage), &outJson); err != nil {
|
||||||
|
return []string{} // Handle error
|
||||||
|
}
|
||||||
|
if mport, ok := outJson["server_ports"].([]interface{}); ok {
|
||||||
|
mportList := make([]string, len(mport))
|
||||||
|
for i, v := range mport {
|
||||||
|
mportList[i] = v.(string)
|
||||||
|
}
|
||||||
|
params = append(params, LinkParam{"mport", strings.Join(mportList, ",")})
|
||||||
|
}
|
||||||
|
|
||||||
|
port, _ := addr["server_port"].(float64)
|
||||||
|
uri := fmt.Sprintf("%s%s:%.0f", baseUri, addr["server"].(string), port)
|
||||||
|
links = append(links, addParams(uri, params, addr["remark"].(string)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func hysteria2Link(
|
||||||
|
userConfig map[string]interface{},
|
||||||
|
inbound map[string]interface{},
|
||||||
|
addrs []map[string]interface{}) []string {
|
||||||
|
|
||||||
|
password, _ := userConfig["password"].(string)
|
||||||
|
baseUri := fmt.Sprintf("%s%s@", "hysteria2://", password)
|
||||||
|
var links []string
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
var params []LinkParam
|
||||||
|
if upmbps, ok := inbound["up_mbps"].(float64); ok {
|
||||||
|
params = append(params, LinkParam{"downmbps", fmt.Sprintf("%.0f", upmbps)})
|
||||||
|
}
|
||||||
|
if downmbps, ok := inbound["down_mbps"].(float64); ok {
|
||||||
|
params = append(params, LinkParam{"upmbps", fmt.Sprintf("%.0f", downmbps)})
|
||||||
|
}
|
||||||
|
if tls, ok := addr["tls"].(map[string]interface{}); ok {
|
||||||
|
getTlsParams(¶ms, tls, "insecure")
|
||||||
|
}
|
||||||
|
if obfs, ok := inbound["obfs"].(map[string]interface{}); ok {
|
||||||
|
if obfsType, ok := obfs["type"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"obfs", obfsType})
|
||||||
|
}
|
||||||
|
if obfsPassword, ok := obfs["password"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"obfs-password", obfsPassword})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if tfo, ok := inbound["tcp_fast_open"].(bool); ok && tfo {
|
||||||
|
params = append(params, LinkParam{"fastopen", "1"})
|
||||||
|
} else {
|
||||||
|
params = append(params, LinkParam{"fastopen", "0"})
|
||||||
|
}
|
||||||
|
var outJson map[string]interface{}
|
||||||
|
if err := json.Unmarshal(inbound["out_json"].(json.RawMessage), &outJson); err != nil {
|
||||||
|
return []string{} // Handle error
|
||||||
|
}
|
||||||
|
if mport, ok := outJson["server_ports"].([]interface{}); ok {
|
||||||
|
mportList := make([]string, len(mport))
|
||||||
|
for i, v := range mport {
|
||||||
|
mportList[i] = v.(string)
|
||||||
|
}
|
||||||
|
params = append(params, LinkParam{"mport", strings.Join(mportList, ",")})
|
||||||
|
}
|
||||||
|
|
||||||
|
port, _ := addr["server_port"].(float64)
|
||||||
|
uri := fmt.Sprintf("%s%s:%.0f", baseUri, addr["server"].(string), port)
|
||||||
|
links = append(links, addParams(uri, params, addr["remark"].(string)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func anytlsLink(
|
||||||
|
userConfig map[string]interface{},
|
||||||
|
addrs []map[string]interface{}) []string {
|
||||||
|
|
||||||
|
password, _ := userConfig["password"].(string)
|
||||||
|
baseUri := fmt.Sprintf("%s%s@", "anytls://", password)
|
||||||
|
var links []string
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
var params []LinkParam
|
||||||
|
if tls, ok := addr["tls"].(map[string]interface{}); ok {
|
||||||
|
getTlsParams(¶ms, tls, "insecure")
|
||||||
|
}
|
||||||
|
|
||||||
|
port, _ := addr["server_port"].(float64)
|
||||||
|
uri := fmt.Sprintf("%s%s:%.0f", baseUri, addr["server"].(string), port)
|
||||||
|
links = append(links, addParams(uri, params, addr["remark"].(string)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func tuicLink(
|
||||||
|
userConfig map[string]interface{},
|
||||||
|
inbound map[string]interface{},
|
||||||
|
addrs []map[string]interface{}) []string {
|
||||||
|
|
||||||
|
password, _ := userConfig["password"].(string)
|
||||||
|
uuid, _ := userConfig["uuid"].(string)
|
||||||
|
baseUri := fmt.Sprintf("%s%s:%s@", "tuic://", uuid, password)
|
||||||
|
var links []string
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
var params []LinkParam
|
||||||
|
if tls, ok := addr["tls"].(map[string]interface{}); ok {
|
||||||
|
getTlsParams(¶ms, tls, "insecure")
|
||||||
|
}
|
||||||
|
if congestionControl, ok := inbound["congestion_control"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"congestion_control", congestionControl})
|
||||||
|
}
|
||||||
|
|
||||||
|
port, _ := addr["server_port"].(float64)
|
||||||
|
uri := fmt.Sprintf("%s%s:%.0f", baseUri, addr["server"].(string), port)
|
||||||
|
links = append(links, addParams(uri, params, addr["remark"].(string)))
|
||||||
|
}
|
||||||
|
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func vlessLink(
|
||||||
|
userConfig map[string]interface{},
|
||||||
|
inbound map[string]interface{},
|
||||||
|
addrs []map[string]interface{}) []string {
|
||||||
|
|
||||||
|
uuid, _ := userConfig["uuid"].(string)
|
||||||
|
baseParams := getTransportParams(inbound["transport"])
|
||||||
|
var links []string
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
params := make([]LinkParam, len(baseParams))
|
||||||
|
copy(params, baseParams)
|
||||||
|
if tls, ok := addr["tls"].(map[string]interface{}); ok && tls["enabled"].(bool) {
|
||||||
|
getTlsParams(¶ms, tls, "allowInsecure")
|
||||||
|
if flow, ok := userConfig["flow"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"flow", flow})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
port, _ := addr["server_port"].(float64)
|
||||||
|
uri := fmt.Sprintf("vless://%s@%s:%.0f", uuid, addr["server"].(string), port)
|
||||||
|
uri = addParams(uri, params, addr["remark"].(string))
|
||||||
|
links = append(links, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func trojanLink(
|
||||||
|
userConfig map[string]interface{},
|
||||||
|
inbound map[string]interface{},
|
||||||
|
addrs []map[string]interface{}) []string {
|
||||||
|
password, _ := userConfig["password"].(string)
|
||||||
|
baseParams := getTransportParams(inbound["transport"])
|
||||||
|
var links []string
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
params := make([]LinkParam, len(baseParams))
|
||||||
|
copy(params, baseParams)
|
||||||
|
if tls, ok := addr["tls"].(map[string]interface{}); ok && tls["enabled"].(bool) {
|
||||||
|
getTlsParams(¶ms, tls, "allowInsecure")
|
||||||
|
}
|
||||||
|
port, _ := addr["server_port"].(float64)
|
||||||
|
uri := fmt.Sprintf("trojan://%s@%s:%.0f", password, addr["server"].(string), port)
|
||||||
|
uri = addParams(uri, params, addr["remark"].(string))
|
||||||
|
links = append(links, uri)
|
||||||
|
}
|
||||||
|
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func vmessLink(
|
||||||
|
userConfig map[string]interface{},
|
||||||
|
inbound map[string]interface{},
|
||||||
|
addrs []map[string]interface{}) []string {
|
||||||
|
|
||||||
|
uuid, _ := userConfig["uuid"].(string)
|
||||||
|
transportParams := getTransportParams(inbound["transport"])
|
||||||
|
var links []string
|
||||||
|
|
||||||
|
baseParams := map[string]interface{}{
|
||||||
|
"v": "2",
|
||||||
|
"id": uuid,
|
||||||
|
"aid": 0,
|
||||||
|
}
|
||||||
|
|
||||||
|
var net, typ, host, path string
|
||||||
|
for _, p := range transportParams {
|
||||||
|
switch p.Key {
|
||||||
|
case "type":
|
||||||
|
net = p.Value
|
||||||
|
case "host":
|
||||||
|
host = p.Value
|
||||||
|
case "path":
|
||||||
|
path = p.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if net == "http" || net == "tcp" {
|
||||||
|
baseParams["net"] = "tcp"
|
||||||
|
if net == "http" {
|
||||||
|
typ = "http"
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
baseParams["net"] = net
|
||||||
|
}
|
||||||
|
|
||||||
|
for _, addr := range addrs {
|
||||||
|
obj := make(map[string]interface{})
|
||||||
|
for k, v := range baseParams {
|
||||||
|
obj[k] = v
|
||||||
|
}
|
||||||
|
|
||||||
|
obj["add"], _ = addr["server"].(string)
|
||||||
|
port, _ := addr["server_port"].(float64)
|
||||||
|
obj["port"] = fmt.Sprintf("%.0f", port)
|
||||||
|
obj["ps"], _ = addr["remark"].(string)
|
||||||
|
if typ != "" {
|
||||||
|
obj["type"] = typ
|
||||||
|
}
|
||||||
|
if host != "" {
|
||||||
|
obj["host"] = host
|
||||||
|
}
|
||||||
|
if path != "" {
|
||||||
|
obj["path"] = path
|
||||||
|
}
|
||||||
|
populateVmessTlsParams(obj, addr["tls"])
|
||||||
|
|
||||||
|
jsonStr, _ := json.Marshal(obj)
|
||||||
|
|
||||||
|
uri := fmt.Sprintf("vmess://%s", toBase64(jsonStr))
|
||||||
|
links = append(links, uri)
|
||||||
|
}
|
||||||
|
return links
|
||||||
|
}
|
||||||
|
|
||||||
|
func populateVmessTlsParams(obj map[string]interface{}, tlsConfig interface{}) {
|
||||||
|
if tlsMap, ok := tlsConfig.(map[string]interface{}); ok && tlsMap["enabled"].(bool) {
|
||||||
|
obj["tls"] = "tls"
|
||||||
|
var tlsParams []LinkParam
|
||||||
|
getTlsParams(&tlsParams, tlsMap, "allowInsecure")
|
||||||
|
for _, p := range tlsParams {
|
||||||
|
switch p.Key {
|
||||||
|
case "security":
|
||||||
|
// ignore, as "tls" is already set
|
||||||
|
case "allowInsecure":
|
||||||
|
obj["allowInsecure"] = 1
|
||||||
|
case "sni":
|
||||||
|
obj["sni"] = p.Value
|
||||||
|
case "fp":
|
||||||
|
obj["fp"] = p.Value
|
||||||
|
case "alpn":
|
||||||
|
obj["alpn"] = p.Value
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
obj["tls"] = "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func toBase64(d []byte) string {
|
||||||
|
return base64.StdEncoding.EncodeToString(d)
|
||||||
|
}
|
||||||
|
|
||||||
|
func addParams(uri string, params []LinkParam, remark string) string {
|
||||||
|
URL, _ := url.Parse(uri)
|
||||||
|
var q []string
|
||||||
|
for _, p := range params {
|
||||||
|
switch p.Key {
|
||||||
|
case "mport", "alpn":
|
||||||
|
q = append(q, fmt.Sprintf("%s=%s", p.Key, p.Value))
|
||||||
|
default:
|
||||||
|
q = append(q, fmt.Sprintf("%s=%s", p.Key, url.QueryEscape(p.Value)))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
URL.RawQuery = strings.Join(q, "&")
|
||||||
|
URL.Fragment = remark
|
||||||
|
return URL.String()
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTransportParams(t interface{}) []LinkParam {
|
||||||
|
var params []LinkParam
|
||||||
|
trasport, _ := t.(map[string]interface{})
|
||||||
|
var transportType string
|
||||||
|
if tt, ok := trasport["type"].(string); ok {
|
||||||
|
transportType = tt
|
||||||
|
} else {
|
||||||
|
transportType = "tcp"
|
||||||
|
}
|
||||||
|
params = append(params, LinkParam{"type", transportType})
|
||||||
|
if transportType == "tcp" {
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
switch transportType {
|
||||||
|
case "http":
|
||||||
|
if host, ok := trasport["host"].([]interface{}); ok {
|
||||||
|
var hosts []string
|
||||||
|
for _, v := range host {
|
||||||
|
hosts = append(hosts, v.(string))
|
||||||
|
}
|
||||||
|
params = append(params, LinkParam{"host", strings.Join(hosts, ",")})
|
||||||
|
}
|
||||||
|
if path, ok := trasport["path"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"path", path})
|
||||||
|
}
|
||||||
|
case "ws":
|
||||||
|
if path, ok := trasport["path"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"path", path})
|
||||||
|
}
|
||||||
|
if headers, ok := trasport["headers"].(map[string]interface{}); ok {
|
||||||
|
if host, ok := headers["Host"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"host", host})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "grpc":
|
||||||
|
if serviceName, ok := trasport["service_name"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"serviceName", serviceName})
|
||||||
|
}
|
||||||
|
case "httpupgrade":
|
||||||
|
if host, ok := trasport["host"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"host", host})
|
||||||
|
}
|
||||||
|
if path, ok := trasport["path"].(string); ok {
|
||||||
|
params = append(params, LinkParam{"path", path})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return params
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTlsParams(params *[]LinkParam, tls map[string]interface{}, insecureKey string) {
|
||||||
|
if reality, ok := tls["reality"].(map[string]interface{}); ok && reality["enabled"].(bool) {
|
||||||
|
*params = append(*params, LinkParam{"security", "reality"})
|
||||||
|
if pbk, ok := reality["public_key"].(string); ok {
|
||||||
|
*params = append(*params, LinkParam{"pbk", pbk})
|
||||||
|
}
|
||||||
|
if sid, ok := reality["short_id"].(string); ok {
|
||||||
|
*params = append(*params, LinkParam{"sid", sid})
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
*params = append(*params, LinkParam{"security", "tls"})
|
||||||
|
if insecure, ok := tls["insecure"].(bool); ok && insecure {
|
||||||
|
*params = append(*params, LinkParam{insecureKey, "1"})
|
||||||
|
}
|
||||||
|
if disableSni, ok := tls["disable_sni"].(bool); ok && disableSni {
|
||||||
|
*params = append(*params, LinkParam{"disable_sni", "1"})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if utls, ok := tls["utls"].(map[string]interface{}); ok {
|
||||||
|
if fingerprint, ok := utls["fingerprint"].(string); ok {
|
||||||
|
*params = append(*params, LinkParam{"fp", fingerprint})
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if sni, ok := tls["server_name"].(string); ok {
|
||||||
|
*params = append(*params, LinkParam{"sni", sni})
|
||||||
|
}
|
||||||
|
if alpn, ok := tls["alpn"].([]interface{}); ok {
|
||||||
|
alpnList := make([]string, len(alpn))
|
||||||
|
for i, v := range alpn {
|
||||||
|
alpnList[i] = v.(string)
|
||||||
|
}
|
||||||
|
*params = append(*params, LinkParam{"alpn", strings.Join(alpnList, ",")})
|
||||||
|
}
|
||||||
|
}
|
||||||
581
util/linkToJson.go
Normal file
581
util/linkToJson.go
Normal file
@@ -0,0 +1,581 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"net"
|
||||||
|
"net/url"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetOutbound(uri string, i int) (*map[string]interface{}, string, error) {
|
||||||
|
u, err := url.Parse(uri)
|
||||||
|
if err == nil {
|
||||||
|
switch u.Scheme {
|
||||||
|
case "vmess":
|
||||||
|
return vmess(u.Host, i)
|
||||||
|
case "vless":
|
||||||
|
return vless(u, i)
|
||||||
|
case "trojan":
|
||||||
|
return trojan(u, i)
|
||||||
|
case "hy", "hysteria":
|
||||||
|
return hy(u, i)
|
||||||
|
case "hy2", "hysteria2":
|
||||||
|
return hy2(u, i)
|
||||||
|
case "anytls":
|
||||||
|
return anytls(u, i)
|
||||||
|
case "tuic":
|
||||||
|
return tuic(u, i)
|
||||||
|
case "ss", "shadowsocks":
|
||||||
|
return ss(u, i)
|
||||||
|
case "naive+https", "naive+quic", "http2":
|
||||||
|
return parseNaiveLink(u, i)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return nil, "", common.NewError("Unsupported link format")
|
||||||
|
}
|
||||||
|
|
||||||
|
func vmess(data string, i int) (*map[string]interface{}, string, error) {
|
||||||
|
dataByte, err := B64StrToByte(data)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
var dataJson map[string]interface{}
|
||||||
|
err = json.Unmarshal(dataByte, &dataJson)
|
||||||
|
if err != nil {
|
||||||
|
return nil, "", err
|
||||||
|
}
|
||||||
|
transport := map[string]interface{}{}
|
||||||
|
tp_net, _ := dataJson["net"].(string)
|
||||||
|
tp_type, _ := dataJson["type"].(string)
|
||||||
|
tp_host, _ := dataJson["host"].(string)
|
||||||
|
tp_path, _ := dataJson["path"].(string)
|
||||||
|
switch strings.ToLower(tp_net) {
|
||||||
|
case "tcp", "":
|
||||||
|
if tp_type == "http" {
|
||||||
|
transport["type"] = tp_type
|
||||||
|
if len(tp_host) > 0 {
|
||||||
|
transport["host"] = strings.Split(tp_host, ",")
|
||||||
|
}
|
||||||
|
transport["path"] = tp_path
|
||||||
|
}
|
||||||
|
case "http", "h2":
|
||||||
|
transport["type"] = "http"
|
||||||
|
if len(tp_host) > 0 {
|
||||||
|
transport["host"] = strings.Split(tp_host, ",")
|
||||||
|
}
|
||||||
|
transport["path"] = tp_path
|
||||||
|
case "ws":
|
||||||
|
transport["type"] = tp_net
|
||||||
|
transport["path"] = tp_path
|
||||||
|
transport["early_data_header_name"] = "Sec-WebSocket-Protocol"
|
||||||
|
if len(tp_host) > 0 {
|
||||||
|
transport["headers"] = map[string]interface{}{
|
||||||
|
"Host": tp_host,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "quic":
|
||||||
|
transport["type"] = tp_net
|
||||||
|
case "grpc":
|
||||||
|
transport["type"] = tp_net
|
||||||
|
transport["service_name"] = tp_path
|
||||||
|
case "httpupgrade":
|
||||||
|
transport["type"] = tp_net
|
||||||
|
transport["path"] = tp_path
|
||||||
|
transport["host"] = tp_host
|
||||||
|
default:
|
||||||
|
return nil, "", common.NewError("Invalid vmess")
|
||||||
|
}
|
||||||
|
tls := map[string]interface{}{}
|
||||||
|
vmess_tls, _ := dataJson["tls"].(string)
|
||||||
|
if vmess_tls == "tls" {
|
||||||
|
tls["enabled"] = true
|
||||||
|
tls_sni, _ := dataJson["sni"].(string)
|
||||||
|
tls_alpn, _ := dataJson["alpn"].(string)
|
||||||
|
_, tls_insecure := dataJson["allowInsecure"]
|
||||||
|
tls_fp, _ := dataJson["fp"].(string)
|
||||||
|
if len(tls_sni) > 0 {
|
||||||
|
tls["server_name"] = tls_sni
|
||||||
|
}
|
||||||
|
if len(tls_alpn) > 0 {
|
||||||
|
tls["alpn"] = strings.Split(tls_alpn, ",")
|
||||||
|
}
|
||||||
|
if tls_insecure {
|
||||||
|
tls["insecure"] = true
|
||||||
|
}
|
||||||
|
if len(tls_fp) > 0 {
|
||||||
|
tls["utls"] = map[string]interface{}{
|
||||||
|
"enabled": true,
|
||||||
|
"fingerprint": tls_fp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tag, _ := dataJson["ps"].(string)
|
||||||
|
if i > 0 {
|
||||||
|
tag = fmt.Sprintf("%d.%s", i, tag)
|
||||||
|
}
|
||||||
|
alter_id := 0
|
||||||
|
if aid, ok := dataJson["aid"].(float64); ok {
|
||||||
|
alter_id = int(aid)
|
||||||
|
}
|
||||||
|
vmess := map[string]interface{}{
|
||||||
|
"type": "vmess",
|
||||||
|
"tag": tag,
|
||||||
|
"server": dataJson["add"],
|
||||||
|
"server_port": dataJson["port"],
|
||||||
|
"uuid": dataJson["id"],
|
||||||
|
"security": "auto",
|
||||||
|
"alter_id": alter_id,
|
||||||
|
"tls": tls,
|
||||||
|
"transport": transport,
|
||||||
|
}
|
||||||
|
return &vmess, tag, err
|
||||||
|
}
|
||||||
|
|
||||||
|
func vless(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||||
|
query, _ := url.ParseQuery(u.RawQuery)
|
||||||
|
security := query.Get("security")
|
||||||
|
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||||
|
port := 80
|
||||||
|
if len(portStr) > 0 {
|
||||||
|
port, _ = strconv.Atoi(portStr)
|
||||||
|
} else {
|
||||||
|
if security == "tls" || security == "reality" {
|
||||||
|
port = 443
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tp_type := query.Get("type")
|
||||||
|
tag := u.Fragment
|
||||||
|
if i > 0 {
|
||||||
|
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||||
|
}
|
||||||
|
vless := map[string]interface{}{
|
||||||
|
"type": "vless",
|
||||||
|
"tag": tag,
|
||||||
|
"server": host,
|
||||||
|
"server_port": port,
|
||||||
|
"uuid": u.User.Username(),
|
||||||
|
"flow": query.Get("flow"),
|
||||||
|
"tls": getTls(security, &query),
|
||||||
|
"transport": getTransport(tp_type, &query),
|
||||||
|
}
|
||||||
|
return &vless, tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func trojan(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||||
|
query, _ := url.ParseQuery(u.RawQuery)
|
||||||
|
security := query.Get("security")
|
||||||
|
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||||
|
port := 80
|
||||||
|
if len(portStr) > 0 {
|
||||||
|
port, _ = strconv.Atoi(portStr)
|
||||||
|
} else {
|
||||||
|
if security == "tls" || security == "reality" {
|
||||||
|
port = 443
|
||||||
|
}
|
||||||
|
}
|
||||||
|
tp_type := query.Get("type")
|
||||||
|
tag := u.Fragment
|
||||||
|
if i > 0 {
|
||||||
|
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||||
|
}
|
||||||
|
trojan := map[string]interface{}{
|
||||||
|
"type": "trojan",
|
||||||
|
"tag": tag,
|
||||||
|
"server": host,
|
||||||
|
"server_port": port,
|
||||||
|
"password": u.User.Username(),
|
||||||
|
"tls": getTls(security, &query),
|
||||||
|
"transport": getTransport(tp_type, &query),
|
||||||
|
}
|
||||||
|
return &trojan, tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hy(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||||
|
query, _ := url.ParseQuery(u.RawQuery)
|
||||||
|
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||||
|
port := 443
|
||||||
|
if len(portStr) > 0 {
|
||||||
|
port, _ = strconv.Atoi(portStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
security := query.Get("security")
|
||||||
|
if len(security) == 0 {
|
||||||
|
security = "tls"
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := u.Fragment
|
||||||
|
if i > 0 {
|
||||||
|
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||||
|
}
|
||||||
|
hy := map[string]interface{}{
|
||||||
|
"type": "hysteria",
|
||||||
|
"tag": tag,
|
||||||
|
"server": host,
|
||||||
|
"server_port": port,
|
||||||
|
"obfs": query.Get("obfsParam"),
|
||||||
|
"auth_str": query.Get("auth"),
|
||||||
|
"tls": getTls(security, &query),
|
||||||
|
}
|
||||||
|
down, _ := strconv.Atoi(query.Get("downmbps"))
|
||||||
|
up, _ := strconv.Atoi(query.Get("upmbps"))
|
||||||
|
recv_window_conn, _ := strconv.Atoi(query.Get("recv_window_conn"))
|
||||||
|
recv_window, _ := strconv.Atoi(query.Get("recv_window"))
|
||||||
|
if down > 0 {
|
||||||
|
hy["down_mbps"] = down
|
||||||
|
}
|
||||||
|
if up > 0 {
|
||||||
|
hy["up_mbps"] = up
|
||||||
|
}
|
||||||
|
if recv_window_conn > 0 {
|
||||||
|
hy["recv_window_conn"] = recv_window_conn
|
||||||
|
}
|
||||||
|
if recv_window > 0 {
|
||||||
|
hy["recv_window"] = recv_window
|
||||||
|
}
|
||||||
|
return &hy, tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func hy2(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||||
|
query, _ := url.ParseQuery(u.RawQuery)
|
||||||
|
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||||
|
port := 443
|
||||||
|
if len(portStr) > 0 {
|
||||||
|
port, _ = strconv.Atoi(portStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
security := query.Get("security")
|
||||||
|
if len(security) == 0 {
|
||||||
|
security = "tls"
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := u.Fragment
|
||||||
|
if i > 0 {
|
||||||
|
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||||
|
}
|
||||||
|
hy2 := map[string]interface{}{
|
||||||
|
"type": "hysteria2",
|
||||||
|
"tag": tag,
|
||||||
|
"server": host,
|
||||||
|
"server_port": port,
|
||||||
|
"password": u.User.Username(),
|
||||||
|
"tls": getTls(security, &query),
|
||||||
|
}
|
||||||
|
down, _ := strconv.Atoi(query.Get("downmbps"))
|
||||||
|
up, _ := strconv.Atoi(query.Get("upmbps"))
|
||||||
|
obfs := query.Get("obfs")
|
||||||
|
mport := strings.ReplaceAll(query.Get("mport"), "-", ":")
|
||||||
|
fastopen := query.Get("fastopen")
|
||||||
|
if down > 0 {
|
||||||
|
hy2["down_mbps"] = down
|
||||||
|
}
|
||||||
|
if up > 0 {
|
||||||
|
hy2["up_mbps"] = up
|
||||||
|
}
|
||||||
|
if obfs == "salamander" {
|
||||||
|
hy2["obfs"] = map[string]interface{}{
|
||||||
|
"type": "salamander",
|
||||||
|
"password": query.Get("obfs-password"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(mport) > 0 {
|
||||||
|
hy2["server_ports"] = strings.Split(mport, ",")
|
||||||
|
}
|
||||||
|
if fastopen == "1" || fastopen == "true" {
|
||||||
|
hy2["fastopen"] = true
|
||||||
|
}
|
||||||
|
return &hy2, tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func anytls(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||||
|
query, _ := url.ParseQuery(u.RawQuery)
|
||||||
|
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||||
|
port := 443
|
||||||
|
if len(portStr) > 0 {
|
||||||
|
port, _ = strconv.Atoi(portStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
security := query.Get("security")
|
||||||
|
if len(security) == 0 {
|
||||||
|
security = "tls"
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := u.Fragment
|
||||||
|
if i > 0 {
|
||||||
|
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||||
|
}
|
||||||
|
anytls := map[string]interface{}{
|
||||||
|
"type": "anytls",
|
||||||
|
"tag": tag,
|
||||||
|
"server": host,
|
||||||
|
"server_port": port,
|
||||||
|
"password": u.User.Username(),
|
||||||
|
"tls": getTls(security, &query),
|
||||||
|
}
|
||||||
|
return &anytls, tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func tuic(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||||
|
query, _ := url.ParseQuery(u.RawQuery)
|
||||||
|
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||||
|
port := 443
|
||||||
|
if len(portStr) > 0 {
|
||||||
|
port, _ = strconv.Atoi(portStr)
|
||||||
|
}
|
||||||
|
|
||||||
|
security := query.Get("security")
|
||||||
|
if len(security) == 0 {
|
||||||
|
security = "tls"
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := u.Fragment
|
||||||
|
if i > 0 {
|
||||||
|
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||||
|
}
|
||||||
|
password, _ := u.User.Password()
|
||||||
|
tuic := map[string]interface{}{
|
||||||
|
"type": "tuic",
|
||||||
|
"tag": tag,
|
||||||
|
"server": host,
|
||||||
|
"server_port": port,
|
||||||
|
"uuid": u.User.Username(),
|
||||||
|
"password": password,
|
||||||
|
"congestion_control": query.Get("congestion_control"),
|
||||||
|
"udp_relay_mode": query.Get("udp_relay_mode"),
|
||||||
|
"tls": getTls(security, &query),
|
||||||
|
}
|
||||||
|
return &tuic, tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func ss(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||||
|
query, _ := url.ParseQuery(u.RawQuery)
|
||||||
|
host, portStr, _ := net.SplitHostPort(u.Host)
|
||||||
|
port := 443
|
||||||
|
if len(portStr) > 0 {
|
||||||
|
port, _ = strconv.Atoi(portStr)
|
||||||
|
}
|
||||||
|
method := u.User.Username()
|
||||||
|
password, ok := u.User.Password()
|
||||||
|
if !ok {
|
||||||
|
decrypted := StrOrBase64Encoded(method)
|
||||||
|
decrypted_arr := strings.Split(decrypted, ":")
|
||||||
|
if len(decrypted_arr) > 1 {
|
||||||
|
method = decrypted_arr[0]
|
||||||
|
password = strings.Join(decrypted_arr[1:], ":")
|
||||||
|
} else {
|
||||||
|
return nil, "", common.NewError("Unsupported shadowsocks")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := u.Fragment
|
||||||
|
if i > 0 {
|
||||||
|
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||||
|
}
|
||||||
|
ss := map[string]interface{}{
|
||||||
|
"type": "shadowsocks",
|
||||||
|
"tag": tag,
|
||||||
|
"server": host,
|
||||||
|
"server_port": port,
|
||||||
|
"method": method,
|
||||||
|
"password": password,
|
||||||
|
}
|
||||||
|
|
||||||
|
v2ray_type := query.Get("type")
|
||||||
|
if len(v2ray_type) > 0 {
|
||||||
|
pl_arr := []string{}
|
||||||
|
host_header := query.Get("host")
|
||||||
|
if query.Get("security") == "tls" {
|
||||||
|
pl_arr = append(pl_arr, "tls")
|
||||||
|
}
|
||||||
|
if v2ray_type == "quic" {
|
||||||
|
pl_arr = append(pl_arr, "mode=quic")
|
||||||
|
}
|
||||||
|
if len(host_header) > 0 {
|
||||||
|
pl_arr = append(pl_arr, "host="+host_header)
|
||||||
|
}
|
||||||
|
ss["plugin"] = "v2ray-plugin"
|
||||||
|
ss["plugin_opts"] = strings.Join(pl_arr, ";")
|
||||||
|
}
|
||||||
|
plugin := query.Get("plugin")
|
||||||
|
if len(plugin) > 0 {
|
||||||
|
pl_arr := strings.Split(plugin, ";")
|
||||||
|
if len(pl_arr) > 0 {
|
||||||
|
ss["plugin"] = pl_arr[0]
|
||||||
|
ss["plugin_opts"] = strings.Join(pl_arr[1:], ";")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return &ss, tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func parseNaiveLink(u *url.URL, i int) (*map[string]interface{}, string, error) {
|
||||||
|
var host, portStr, username, password string
|
||||||
|
var port int
|
||||||
|
|
||||||
|
switch u.Scheme {
|
||||||
|
case "http2":
|
||||||
|
decoded := StrOrBase64Encoded(u.Hostname())
|
||||||
|
if idx := strings.Index(decoded, "@"); idx != -1 {
|
||||||
|
userInfo := decoded[:idx]
|
||||||
|
hostPort := decoded[idx+1:]
|
||||||
|
if idx2 := strings.Index(userInfo, ":"); idx2 != -1 {
|
||||||
|
username = userInfo[:idx2]
|
||||||
|
password = userInfo[idx2+1:]
|
||||||
|
} else {
|
||||||
|
username = userInfo
|
||||||
|
}
|
||||||
|
host, portStr, _ = net.SplitHostPort(hostPort)
|
||||||
|
if portStr != "" {
|
||||||
|
port, _ = strconv.Atoi(portStr)
|
||||||
|
} else {
|
||||||
|
port = 443
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
return nil, "", common.NewError("Invalid naive link (http2)")
|
||||||
|
}
|
||||||
|
case "naive+https", "naive+quic":
|
||||||
|
host, portStr, _ = net.SplitHostPort(u.Host)
|
||||||
|
if portStr != "" {
|
||||||
|
port, _ = strconv.Atoi(portStr)
|
||||||
|
} else {
|
||||||
|
port = 443
|
||||||
|
}
|
||||||
|
if u.User != nil {
|
||||||
|
username = u.User.Username()
|
||||||
|
password, _ = u.User.Password()
|
||||||
|
}
|
||||||
|
default:
|
||||||
|
return nil, "", common.NewError("Unsupported naive scheme")
|
||||||
|
}
|
||||||
|
|
||||||
|
tag := u.Fragment
|
||||||
|
if i > 0 {
|
||||||
|
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
|
||||||
|
}
|
||||||
|
if tag == "" {
|
||||||
|
tag = fmt.Sprintf("naive-%d", i)
|
||||||
|
}
|
||||||
|
|
||||||
|
naive := map[string]interface{}{
|
||||||
|
"type": "naive",
|
||||||
|
"tag": tag,
|
||||||
|
"server": host,
|
||||||
|
"server_port": port,
|
||||||
|
"username": username,
|
||||||
|
"password": password,
|
||||||
|
"tls": map[string]interface{}{"enabled": true},
|
||||||
|
}
|
||||||
|
|
||||||
|
query := u.Query()
|
||||||
|
if peer := query.Get("peer"); peer != "" {
|
||||||
|
if tls, ok := naive["tls"].(map[string]interface{}); ok {
|
||||||
|
tls["server_name"] = peer
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if insecure := query.Get("insecure"); insecure == "1" || insecure == "true" {
|
||||||
|
if tls, ok := naive["tls"].(map[string]interface{}); ok {
|
||||||
|
tls["insecure"] = true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if alpn := query.Get("alpn"); alpn != "" {
|
||||||
|
if tls, ok := naive["tls"].(map[string]interface{}); ok {
|
||||||
|
tls["alpn"] = strings.Split(alpn, ",")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if u.Scheme == "naive+quic" {
|
||||||
|
naive["quic"] = true
|
||||||
|
}
|
||||||
|
|
||||||
|
return &naive, tag, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTransport(tp_type string, q *url.Values) map[string]interface{} {
|
||||||
|
transport := map[string]interface{}{}
|
||||||
|
tp_host := q.Get("host")
|
||||||
|
tp_path := q.Get("path")
|
||||||
|
switch strings.ToLower(tp_type) {
|
||||||
|
case "tcp", "":
|
||||||
|
if q.Get("headerType") == "http" {
|
||||||
|
transport["type"] = "http"
|
||||||
|
if len(tp_host) > 0 {
|
||||||
|
transport["host"] = strings.Split(tp_host, ",")
|
||||||
|
}
|
||||||
|
transport["path"] = tp_path
|
||||||
|
}
|
||||||
|
case "http", "h2":
|
||||||
|
transport["type"] = "http"
|
||||||
|
if len(tp_host) > 0 {
|
||||||
|
transport["host"] = strings.Split(tp_host, ",")
|
||||||
|
}
|
||||||
|
transport["path"] = tp_path
|
||||||
|
case "ws":
|
||||||
|
transport["type"] = "ws"
|
||||||
|
transport["path"] = tp_path
|
||||||
|
if len(tp_host) > 0 {
|
||||||
|
transport["headers"] = map[string]interface{}{
|
||||||
|
"Host": tp_host,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
case "quic":
|
||||||
|
transport["type"] = "quic"
|
||||||
|
case "grpc":
|
||||||
|
transport["type"] = "grpc"
|
||||||
|
transport["service_name"] = q.Get("serviceName")
|
||||||
|
case "httpupgrade":
|
||||||
|
transport["type"] = "httpupgrade"
|
||||||
|
transport["path"] = tp_path
|
||||||
|
transport["host"] = tp_host
|
||||||
|
}
|
||||||
|
return transport
|
||||||
|
}
|
||||||
|
|
||||||
|
func getTls(security string, q *url.Values) map[string]interface{} {
|
||||||
|
tls := map[string]interface{}{}
|
||||||
|
tls_fp := q.Get("fp")
|
||||||
|
tls_sni := q.Get("sni")
|
||||||
|
tls_allow_insecure := q.Get("allowInsecure")
|
||||||
|
tls_insecure := q.Get("insecure")
|
||||||
|
tls_alpn := q.Get("alpn")
|
||||||
|
tls_ech := q.Get("ech")
|
||||||
|
disable_sni := q.Get("disable_sni")
|
||||||
|
switch security {
|
||||||
|
case "tls":
|
||||||
|
tls["enabled"] = true
|
||||||
|
case "reality":
|
||||||
|
tls["enabled"] = true
|
||||||
|
tls["reality"] = map[string]interface{}{
|
||||||
|
"enabled": true,
|
||||||
|
"public_key": q.Get("pbk"),
|
||||||
|
"short_id": q.Get("sid"),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(tls_sni) > 0 {
|
||||||
|
tls["server_name"] = tls_sni
|
||||||
|
}
|
||||||
|
if len(tls_alpn) > 0 {
|
||||||
|
tls["alpn"] = strings.Split(tls_alpn, ",")
|
||||||
|
}
|
||||||
|
if tls_insecure == "1" || tls_insecure == "true" || tls_allow_insecure == "1" || tls_allow_insecure == "true" {
|
||||||
|
tls["insecure"] = true
|
||||||
|
}
|
||||||
|
if len(tls_fp) > 0 {
|
||||||
|
tls["utls"] = map[string]interface{}{
|
||||||
|
"enabled": true,
|
||||||
|
"fingerprint": tls_fp,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(tls_ech) > 0 {
|
||||||
|
tls["ech"] = map[string]interface{}{
|
||||||
|
"enabled": true,
|
||||||
|
"config": []string{
|
||||||
|
tls_ech,
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if disable_sni == "1" || disable_sni == "true" {
|
||||||
|
tls["disable_sni"] = true
|
||||||
|
}
|
||||||
|
return tls
|
||||||
|
}
|
||||||
237
util/outJson.go
Normal file
237
util/outJson.go
Normal file
@@ -0,0 +1,237 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
// Fill Inbound's out_json
|
||||||
|
func FillOutJson(i *model.Inbound, hostname string) error {
|
||||||
|
switch i.Type {
|
||||||
|
case "direct", "tun", "redirect", "tproxy":
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
var outJson map[string]interface{}
|
||||||
|
err := json.Unmarshal(i.OutJson, &outJson)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
if outJson == nil {
|
||||||
|
outJson = make(map[string]interface{})
|
||||||
|
}
|
||||||
|
|
||||||
|
if i.TlsId > 0 {
|
||||||
|
addTls(&outJson, i.Tls)
|
||||||
|
} else {
|
||||||
|
delete(outJson, "tls")
|
||||||
|
}
|
||||||
|
|
||||||
|
inbound, err := i.MarshalFull()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
outJson["type"] = i.Type
|
||||||
|
outJson["tag"] = i.Tag
|
||||||
|
outJson["server"] = hostname
|
||||||
|
outJson["server_port"] = (*inbound)["listen_port"]
|
||||||
|
|
||||||
|
switch i.Type {
|
||||||
|
case "http", "socks", "mixed", "anytls":
|
||||||
|
case "naive":
|
||||||
|
naiveOut(&outJson, *inbound)
|
||||||
|
case "shadowsocks":
|
||||||
|
shadowsocksOut(&outJson, *inbound)
|
||||||
|
case "shadowtls":
|
||||||
|
shadowTlsOut(&outJson, *inbound)
|
||||||
|
case "hysteria":
|
||||||
|
hysteriaOut(&outJson, *inbound)
|
||||||
|
case "hysteria2":
|
||||||
|
hysteria2Out(&outJson, *inbound)
|
||||||
|
case "tuic":
|
||||||
|
tuicOut(&outJson, *inbound)
|
||||||
|
case "vless":
|
||||||
|
vlessOut(&outJson, *inbound)
|
||||||
|
case "trojan":
|
||||||
|
trojanOut(&outJson, *inbound)
|
||||||
|
case "vmess":
|
||||||
|
vmessOut(&outJson, *inbound)
|
||||||
|
default:
|
||||||
|
for key := range outJson {
|
||||||
|
delete(outJson, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
i.OutJson, err = json.MarshalIndent(outJson, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// addTls function
|
||||||
|
func addTls(out *map[string]interface{}, tls *model.Tls) {
|
||||||
|
var tlsServer, tlsConfig map[string]interface{}
|
||||||
|
err := json.Unmarshal(tls.Server, &tlsServer)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(tls.Client, &tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
if enabled, ok := tlsServer["enabled"]; ok {
|
||||||
|
tlsConfig["enabled"] = enabled
|
||||||
|
}
|
||||||
|
if serverName, ok := tlsServer["server_name"]; ok {
|
||||||
|
tlsConfig["server_name"] = serverName
|
||||||
|
}
|
||||||
|
if alpn, ok := tlsServer["alpn"]; ok {
|
||||||
|
tlsConfig["alpn"] = alpn
|
||||||
|
}
|
||||||
|
if minVersion, ok := tlsServer["min_version"]; ok {
|
||||||
|
tlsConfig["min_version"] = minVersion
|
||||||
|
}
|
||||||
|
if maxVersion, ok := tlsServer["max_version"]; ok {
|
||||||
|
tlsConfig["max_version"] = maxVersion
|
||||||
|
}
|
||||||
|
if certificate, ok := tlsServer["certificate"]; ok {
|
||||||
|
tlsConfig["certificate"] = certificate
|
||||||
|
}
|
||||||
|
if cipherSuites, ok := tlsServer["cipher_suites"]; ok {
|
||||||
|
tlsConfig["cipher_suites"] = cipherSuites
|
||||||
|
}
|
||||||
|
if reality, ok := tlsServer["reality"].(map[string]interface{}); ok && reality["enabled"].(bool) {
|
||||||
|
realityConfig := tlsConfig["reality"].(map[string]interface{})
|
||||||
|
realityConfig["enabled"] = true
|
||||||
|
if shortIDs, ok := reality["short_id"].([]interface{}); ok && len(shortIDs) > 0 {
|
||||||
|
realityConfig["short_id"] = shortIDs[common.RandomInt(len(shortIDs))]
|
||||||
|
}
|
||||||
|
tlsConfig["reality"] = realityConfig
|
||||||
|
}
|
||||||
|
if ech, ok := tlsServer["ech"].(map[string]interface{}); ok && ech["enabled"].(bool) {
|
||||||
|
echConfig := tlsConfig["ech"].(map[string]interface{})
|
||||||
|
echConfig["enabled"] = true
|
||||||
|
echConfig["pq_signature_schemes_enabled"] = ech["pq_signature_schemes_enabled"]
|
||||||
|
echConfig["dynamic_record_sizing_disabled"] = ech["dynamic_record_sizing_disabled"]
|
||||||
|
tlsConfig["ech"] = echConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
(*out)["tls"] = tlsConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func naiveOut(out *map[string]interface{}, inbound map[string]interface{}) {
|
||||||
|
if quic_congestion_control, ok := inbound["quic_congestion_control"].(string); ok {
|
||||||
|
(*out)["quic"] = true
|
||||||
|
switch quic_congestion_control {
|
||||||
|
case "bbr_standard":
|
||||||
|
(*out)["quic_congestion_control"] = "bbr"
|
||||||
|
case "bbr2_variant":
|
||||||
|
(*out)["quic_congestion_control"] = "bbr2"
|
||||||
|
default:
|
||||||
|
(*out)["quic_congestion_control"] = quic_congestion_control
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
func shadowsocksOut(out *map[string]interface{}, inbound map[string]interface{}) {
|
||||||
|
if method, ok := inbound["method"].(string); ok {
|
||||||
|
(*out)["method"] = method
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func shadowTlsOut(out *map[string]interface{}, inbound map[string]interface{}) {
|
||||||
|
if version, ok := inbound["version"].(float64); ok && int(version) == 3 {
|
||||||
|
(*out)["version"] = 3
|
||||||
|
} else {
|
||||||
|
for key := range *out {
|
||||||
|
delete(*out, key)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
(*out)["tls"] = map[string]interface{}{"enabled": true}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hysteriaOut(out *map[string]interface{}, inbound map[string]interface{}) {
|
||||||
|
delete(*out, "down_mbps")
|
||||||
|
delete(*out, "up_mbps")
|
||||||
|
delete(*out, "obfs")
|
||||||
|
delete(*out, "recv_window_conn")
|
||||||
|
delete(*out, "disable_mtu_discovery")
|
||||||
|
|
||||||
|
if upMbps, ok := inbound["down_mbps"]; ok {
|
||||||
|
(*out)["up_mbps"] = upMbps
|
||||||
|
}
|
||||||
|
if downMbps, ok := inbound["up_mbps"]; ok {
|
||||||
|
(*out)["down_mbps"] = downMbps
|
||||||
|
}
|
||||||
|
if obfs, ok := inbound["obfs"]; ok {
|
||||||
|
(*out)["obfs"] = obfs
|
||||||
|
}
|
||||||
|
if recvWindow, ok := inbound["recv_window_conn"]; ok {
|
||||||
|
(*out)["recv_window_conn"] = recvWindow
|
||||||
|
}
|
||||||
|
if disableMTU, ok := inbound["disable_mtu_discovery"]; ok {
|
||||||
|
(*out)["disable_mtu_discovery"] = disableMTU
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func hysteria2Out(out *map[string]interface{}, inbound map[string]interface{}) {
|
||||||
|
delete(*out, "down_mbps")
|
||||||
|
delete(*out, "up_mbps")
|
||||||
|
delete(*out, "obfs")
|
||||||
|
|
||||||
|
if upMbps, ok := inbound["down_mbps"]; ok {
|
||||||
|
(*out)["up_mbps"] = upMbps
|
||||||
|
}
|
||||||
|
if downMbps, ok := inbound["up_mbps"]; ok {
|
||||||
|
(*out)["down_mbps"] = downMbps
|
||||||
|
}
|
||||||
|
if obfs, ok := inbound["obfs"]; ok {
|
||||||
|
(*out)["obfs"] = obfs
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func tuicOut(out *map[string]interface{}, inbound map[string]interface{}) {
|
||||||
|
delete(*out, "zero_rtt_handshake")
|
||||||
|
delete(*out, "heartbeat")
|
||||||
|
if congestionControl, ok := inbound["congestion_control"].(string); ok {
|
||||||
|
(*out)["congestion_control"] = congestionControl
|
||||||
|
} else {
|
||||||
|
(*out)["congestion_control"] = "cubic"
|
||||||
|
}
|
||||||
|
if zeroRTT, ok := inbound["zero_rtt_handshake"].(bool); ok {
|
||||||
|
(*out)["zero_rtt_handshake"] = zeroRTT
|
||||||
|
}
|
||||||
|
if heartbeat, ok := inbound["heartbeat"]; ok {
|
||||||
|
(*out)["heartbeat"] = heartbeat
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func vlessOut(out *map[string]interface{}, inbound map[string]interface{}) {
|
||||||
|
delete(*out, "transport")
|
||||||
|
if transport, ok := inbound["transport"]; ok {
|
||||||
|
(*out)["transport"] = transport
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func trojanOut(out *map[string]interface{}, inbound map[string]interface{}) {
|
||||||
|
delete(*out, "transport")
|
||||||
|
if transport, ok := inbound["transport"]; ok {
|
||||||
|
(*out)["transport"] = transport
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func vmessOut(out *map[string]interface{}, inbound map[string]interface{}) {
|
||||||
|
(*out)["alter_id"] = 0
|
||||||
|
delete(*out, "transport")
|
||||||
|
if transport, ok := inbound["transport"]; ok {
|
||||||
|
(*out)["transport"] = transport
|
||||||
|
}
|
||||||
|
}
|
||||||
15
util/subInfo.go
Normal file
15
util/subInfo.go
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"fmt"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/database/model"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetHeaders(client *model.Client, updateInterval int) []string {
|
||||||
|
var headers []string
|
||||||
|
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", client.Up, client.Down, client.Volume, client.Expiry))
|
||||||
|
headers = append(headers, fmt.Sprintf("%d", updateInterval))
|
||||||
|
headers = append(headers, client.Name)
|
||||||
|
return headers
|
||||||
|
}
|
||||||
97
util/subToJson.go
Normal file
97
util/subToJson.go
Normal file
@@ -0,0 +1,97 @@
|
|||||||
|
package util
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/json"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/alireza0/s-ui/logger"
|
||||||
|
"github.com/alireza0/s-ui/util/common"
|
||||||
|
)
|
||||||
|
|
||||||
|
func GetExternalLink(url string) string {
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Transport: tr}
|
||||||
|
|
||||||
|
response, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("sub: Error making HTTP request:", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("sub: Error reading response body:", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
data := StrOrBase64Encoded(string(body))
|
||||||
|
return data
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetExternalSub(url string) ([]map[string]interface{}, error) {
|
||||||
|
var err error
|
||||||
|
var result []map[string]interface{}
|
||||||
|
|
||||||
|
if len(url) == 0 {
|
||||||
|
return nil, common.NewError("no url")
|
||||||
|
}
|
||||||
|
|
||||||
|
data := GetExternalLink(url)
|
||||||
|
if len(data) == 0 {
|
||||||
|
return nil, common.NewError("no result")
|
||||||
|
}
|
||||||
|
|
||||||
|
// if the data is a JSON object
|
||||||
|
if strings.HasPrefix(data, "{") && strings.HasSuffix(data, "}") {
|
||||||
|
var jsonData map[string]interface{}
|
||||||
|
err = json.Unmarshal([]byte(data), &jsonData)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("sub: Error unmarshalling JSON:", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
outbounds, ok := jsonData["outbounds"].([]any)
|
||||||
|
if !ok {
|
||||||
|
logger.Warning("sub: Error getting outbounds:", err)
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
for _, outbound := range outbounds {
|
||||||
|
outboundMap, ok := outbound.(map[string]interface{})
|
||||||
|
if ok && len(outboundMap) > 0 {
|
||||||
|
oType, _ := outboundMap["type"].(string)
|
||||||
|
switch oType {
|
||||||
|
case "urltest":
|
||||||
|
case "direct":
|
||||||
|
case "selector":
|
||||||
|
case "block":
|
||||||
|
continue
|
||||||
|
default:
|
||||||
|
result = append(result, outboundMap)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, common.NewError("no result")
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
} else {
|
||||||
|
// if data is a text
|
||||||
|
links := strings.Split(data, "\n")
|
||||||
|
for _, link := range links {
|
||||||
|
linkToJson, _, err := GetOutbound(link, 0)
|
||||||
|
if err == nil {
|
||||||
|
result = append(result, *linkToJson)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(result) == 0 {
|
||||||
|
return nil, common.NewError("no result")
|
||||||
|
}
|
||||||
|
return result, nil
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user