Compare commits
1 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 4ed6d49d61 |
+1
-1
@@ -1 +1 @@
|
|||||||
buy_me_a_coffee: alireza7
|
github: alireza0
|
||||||
|
|||||||
@@ -1,5 +1,9 @@
|
|||||||
version: 2
|
version: 2
|
||||||
updates:
|
updates:
|
||||||
|
- package-ecosystem: "npm"
|
||||||
|
directory: "/"
|
||||||
|
schedule:
|
||||||
|
interval: "daily"
|
||||||
- package-ecosystem: "gomod"
|
- package-ecosystem: "gomod"
|
||||||
directory: "/"
|
directory: "/"
|
||||||
schedule:
|
schedule:
|
||||||
|
|||||||
@@ -0,0 +1,57 @@
|
|||||||
|
name: Sing-box Docker Image CI
|
||||||
|
on:
|
||||||
|
workflow_dispatch:
|
||||||
|
|
||||||
|
jobs:
|
||||||
|
build:
|
||||||
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
|
steps:
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
|
||||||
|
- name: Get latest release
|
||||||
|
id: get_release
|
||||||
|
run: |
|
||||||
|
latest_release=$(curl -Ls "https://api.github.com/repos/sagernet/sing-box/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||||
|
echo "latest_release: $latest_release"
|
||||||
|
echo "latest_release=$latest_release" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Docker meta
|
||||||
|
id: meta
|
||||||
|
uses: docker/metadata-action@v5
|
||||||
|
with:
|
||||||
|
images: |
|
||||||
|
alireza7/s-ui-singbox
|
||||||
|
ghcr.io/alireza0/s-ui-singbox
|
||||||
|
tags: |
|
||||||
|
type=sha
|
||||||
|
type=pep440,pattern=${{ steps.get_release.outputs.latest_release }}
|
||||||
|
|
||||||
|
- name: Set up QEMU
|
||||||
|
uses: docker/setup-qemu-action@v3
|
||||||
|
|
||||||
|
- name: Set up Docker Buildx
|
||||||
|
uses: docker/setup-buildx-action@v3
|
||||||
|
|
||||||
|
- name: Login to Docker Hub
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
username: ${{ secrets.DOCKER_HUB_USERNAME }}
|
||||||
|
password: ${{ secrets.DOCKER_HUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Login to GHCR
|
||||||
|
uses: docker/login-action@v3
|
||||||
|
with:
|
||||||
|
registry: ghcr.io
|
||||||
|
username: ${{ github.repository_owner }}
|
||||||
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
|
- name: Build and push
|
||||||
|
uses: docker/build-push-action@v5
|
||||||
|
with:
|
||||||
|
context: core/
|
||||||
|
push: true
|
||||||
|
build-args: SINGBOX_VER=${{ steps.get_release.outputs.latest_release }}
|
||||||
|
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/386
|
||||||
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -7,13 +7,10 @@ on:
|
|||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
build:
|
build:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-latest
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- uses: actions/checkout@v4
|
||||||
uses: actions/checkout@v4.2.2
|
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Docker meta
|
- name: Docker meta
|
||||||
id: meta
|
id: meta
|
||||||
@@ -47,10 +44,10 @@ jobs:
|
|||||||
password: ${{ secrets.GITHUB_TOKEN }}
|
password: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
- name: Build and push
|
- name: Build and push
|
||||||
uses: docker/build-push-action@v6
|
uses: docker/build-push-action@v5
|
||||||
with:
|
with:
|
||||||
context: .
|
context: .
|
||||||
push: true
|
push: true
|
||||||
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
|
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/386
|
||||||
tags: ${{ steps.meta.outputs.tags }}
|
tags: ${{ steps.meta.outputs.tags }}
|
||||||
labels: ${{ steps.meta.outputs.labels }}
|
labels: ${{ steps.meta.outputs.labels }}
|
||||||
@@ -14,27 +14,22 @@ jobs:
|
|||||||
- amd64
|
- amd64
|
||||||
- arm64
|
- arm64
|
||||||
- armv7
|
- armv7
|
||||||
- armv6
|
|
||||||
- armv5
|
|
||||||
- 386
|
- 386
|
||||||
- s390x
|
runs-on: ubuntu-20.04
|
||||||
runs-on: ubuntu-22.04
|
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout repository
|
- name: Checkout repository
|
||||||
uses: actions/checkout@v4.2.2
|
uses: actions/checkout@v4.1.1
|
||||||
with:
|
|
||||||
submodules: recursive
|
|
||||||
|
|
||||||
- name: Setup Go
|
- name: Setup Go
|
||||||
uses: actions/setup-go@v5
|
uses: actions/setup-go@v5
|
||||||
with:
|
with:
|
||||||
cache: false
|
cache: false
|
||||||
go-version-file: go.mod
|
go-version: '1.22'
|
||||||
|
|
||||||
- name: Setup Node.js
|
- name: Setup Node.js
|
||||||
uses: actions/setup-node@v4
|
uses: actions/setup-node@v4
|
||||||
with:
|
with:
|
||||||
node-version: '22'
|
node-version: '20'
|
||||||
registry-url: 'https://registry.npmjs.org'
|
registry-url: 'https://registry.npmjs.org'
|
||||||
|
|
||||||
- name: Install dependencies
|
- name: Install dependencies
|
||||||
@@ -44,14 +39,8 @@ jobs:
|
|||||||
sudo apt install gcc-aarch64-linux-gnu
|
sudo apt install gcc-aarch64-linux-gnu
|
||||||
elif [ "${{ matrix.platform }}" == "armv7" ]; then
|
elif [ "${{ matrix.platform }}" == "armv7" ]; then
|
||||||
sudo apt install gcc-arm-linux-gnueabihf
|
sudo apt install gcc-arm-linux-gnueabihf
|
||||||
elif [ "${{ matrix.platform }}" == "armv6" ]; then
|
|
||||||
sudo apt install gcc-arm-linux-gnueabihf
|
|
||||||
elif [ "${{ matrix.platform }}" == "armv5" ]; then
|
|
||||||
sudo apt install gcc-arm-linux-gnueabi
|
|
||||||
elif [ "${{ matrix.platform }}" == "386" ]; then
|
elif [ "${{ matrix.platform }}" == "386" ]; then
|
||||||
sudo apt install gcc-i686-linux-gnu
|
sudo apt install gcc-i686-linux-gnu
|
||||||
elif [ "${{ matrix.platform }}" == "s390x" ]; then
|
|
||||||
sudo apt install gcc-s390x-linux-gnu
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
- name: Build frontend
|
- name: Build frontend
|
||||||
@@ -60,10 +49,9 @@ jobs:
|
|||||||
npm install
|
npm install
|
||||||
npm run build
|
npm run build
|
||||||
cd ..
|
cd ..
|
||||||
mv frontend/dist web/html
|
mv frontend/dist backend/web/html
|
||||||
rm -fr frontend
|
|
||||||
|
|
||||||
- name: Build s-ui
|
- name: Build s-ui & singbox
|
||||||
run: |
|
run: |
|
||||||
export CGO_ENABLED=1
|
export CGO_ENABLED=1
|
||||||
export GOOS=linux
|
export GOOS=linux
|
||||||
@@ -75,29 +63,32 @@ jobs:
|
|||||||
export GOARCH=arm
|
export GOARCH=arm
|
||||||
export GOARM=7
|
export GOARM=7
|
||||||
export CC=arm-linux-gnueabihf-gcc
|
export CC=arm-linux-gnueabihf-gcc
|
||||||
elif [ "${{ matrix.platform }}" == "armv6" ]; then
|
|
||||||
export GOARCH=arm
|
|
||||||
export GOARM=6
|
|
||||||
export CC=arm-linux-gnueabihf-gcc
|
|
||||||
elif [ "${{ matrix.platform }}" == "armv5" ]; then
|
|
||||||
export GOARCH=arm
|
|
||||||
export GOARM=5
|
|
||||||
export CC=arm-linux-gnueabi-gcc
|
|
||||||
elif [ "${{ matrix.platform }}" == "386" ]; then
|
elif [ "${{ matrix.platform }}" == "386" ]; then
|
||||||
export GOARCH=386
|
export GOARCH=386
|
||||||
export CC=i686-linux-gnu-gcc
|
export CC=i686-linux-gnu-gcc
|
||||||
elif [ "${{ matrix.platform }}" == "s390x" ]; then
|
|
||||||
export GOARCH=s390x
|
|
||||||
export CC=s390x-linux-gnu-gcc
|
|
||||||
fi
|
fi
|
||||||
|
|
||||||
|
#### Build Sing-Box
|
||||||
|
export VERSION=v1.8.14
|
||||||
|
git clone -b $VERSION https://github.com/SagerNet/sing-box
|
||||||
|
cd sing-box
|
||||||
|
go build -tags with_quic,with_grpc,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_v2ray_api,with_clash_api,with_gvisor \
|
||||||
|
-v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' -s -w -buildid=" \
|
||||||
|
-o sing-box ./cmd/sing-box
|
||||||
|
cd ..
|
||||||
|
|
||||||
### Build s-ui
|
### Build s-ui
|
||||||
go build -ldflags="-w -s" -tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor" -o sui main.go
|
cd backend
|
||||||
|
go build -o ../sui main.go
|
||||||
|
cd ..
|
||||||
|
|
||||||
mkdir s-ui
|
mkdir s-ui
|
||||||
cp sui s-ui/
|
cp sui s-ui/
|
||||||
cp s-ui.service s-ui/
|
cp s-ui.service s-ui/
|
||||||
cp s-ui.sh s-ui/
|
cp sing-box.service s-ui/
|
||||||
|
mkdir s-ui/bin
|
||||||
|
cp sing-box/sing-box s-ui/bin/
|
||||||
|
cp core/runSingbox.sh s-ui/bin/
|
||||||
|
|
||||||
- name: Package
|
- name: Package
|
||||||
run: tar -zcvf s-ui-linux-${{ matrix.platform }}.tar.gz s-ui
|
run: tar -zcvf s-ui-linux-${{ matrix.platform }}.tar.gz s-ui
|
||||||
|
|||||||
@@ -5,12 +5,10 @@ backup/
|
|||||||
bin/
|
bin/
|
||||||
db/
|
db/
|
||||||
sui
|
sui
|
||||||
web/html
|
|
||||||
main
|
main
|
||||||
tmp
|
tmp
|
||||||
.sync*
|
.sync*
|
||||||
*.tar.gz
|
*.tar.gz
|
||||||
frontend/
|
|
||||||
|
|
||||||
# local env files
|
# local env files
|
||||||
.env.local
|
.env.local
|
||||||
|
|||||||
@@ -1,4 +0,0 @@
|
|||||||
[submodule "frontend"]
|
|
||||||
path = frontend
|
|
||||||
url = https://github.com/alireza0/s-ui-frontend
|
|
||||||
branch = main
|
|
||||||
+10
-26
@@ -1,40 +1,24 @@
|
|||||||
FROM --platform=$BUILDPLATFORM node:alpine AS front-builder
|
FROM --platform=$BUILDPLATFORM node:alpine as front-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
COPY frontend/ ./
|
COPY frontend/ ./
|
||||||
RUN npm install && npm run build
|
RUN npm install && npm run build
|
||||||
|
|
||||||
FROM golang:1.24-alpine AS backend-builder
|
FROM golang:1.22-alpine AS backend-builder
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
ARG TARGETARCH
|
ARG TARGETARCH
|
||||||
ENV CGO_ENABLED=1
|
|
||||||
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
|
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
|
||||||
|
ENV CGO_ENABLED=1
|
||||||
ENV GOARCH=$TARGETARCH
|
ENV GOARCH=$TARGETARCH
|
||||||
|
RUN apk update && apk --no-cache --update add build-base gcc wget unzip
|
||||||
RUN apk update && apk add --no-cache \
|
COPY backend/ ./
|
||||||
gcc \
|
COPY --from=front-builder /app/dist/ /app/web/html/
|
||||||
musl-dev \
|
RUN go build -ldflags="-w -s" -o sui main.go
|
||||||
libc-dev \
|
|
||||||
make \
|
|
||||||
git \
|
|
||||||
wget \
|
|
||||||
unzip \
|
|
||||||
bash
|
|
||||||
|
|
||||||
ENV CC=gcc
|
|
||||||
|
|
||||||
COPY . .
|
|
||||||
COPY --from=front-builder /app/dist/ /app/web/html/
|
|
||||||
|
|
||||||
RUN go build -ldflags="-w -s" \
|
|
||||||
-tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor" \
|
|
||||||
-o sui main.go
|
|
||||||
|
|
||||||
FROM --platform=$TARGETPLATFORM alpine
|
FROM --platform=$TARGETPLATFORM alpine
|
||||||
LABEL org.opencontainers.image.authors="alireza7@gmail.com"
|
LABEL org.opencontainers.image.authors="alireza7@gmail.com"
|
||||||
ENV TZ=Asia/Tehran
|
ENV TZ=Asia/Tehran
|
||||||
WORKDIR /app
|
WORKDIR /app
|
||||||
RUN apk add --no-cache --update ca-certificates tzdata
|
RUN apk add --no-cache --update ca-certificates tzdata
|
||||||
COPY --from=backend-builder /app/sui /app/
|
COPY --from=backend-builder /app/sui /app/
|
||||||
COPY entrypoint.sh /app/
|
|
||||||
VOLUME [ "s-ui" ]
|
VOLUME [ "s-ui" ]
|
||||||
ENTRYPOINT [ "./entrypoint.sh" ]
|
CMD [ "./sui" ]
|
||||||
@@ -3,7 +3,7 @@
|
|||||||
|
|
||||||

|

|
||||||

|

|
||||||
[](https://goreportcard.com/report/github.com/alireza0/s-ui)
|

|
||||||
[](https://img.shields.io/github/downloads/alireza0/s-ui/total.svg)
|
[](https://img.shields.io/github/downloads/alireza0/s-ui/total.svg)
|
||||||
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
[](https://www.gnu.org/licenses/gpl-3.0.en.html)
|
||||||
|
|
||||||
@@ -23,26 +23,16 @@
|
|||||||
| Multi-Client/Inbound | :heavy_check_mark: |
|
| Multi-Client/Inbound | :heavy_check_mark: |
|
||||||
| Advanced Traffic Routing Interface | :heavy_check_mark: |
|
| Advanced Traffic Routing Interface | :heavy_check_mark: |
|
||||||
| Client & Traffic & System Status | :heavy_check_mark: |
|
| Client & Traffic & System Status | :heavy_check_mark: |
|
||||||
| Subscription Service (link/json + info)| :heavy_check_mark: |
|
| Subscription Service (link + info) | :heavy_check_mark: |
|
||||||
| Dark/Light Theme | :heavy_check_mark: |
|
| Dark/Light Theme | :heavy_check_mark: |
|
||||||
| API Interface | :heavy_check_mark: |
|
|
||||||
|
|
||||||
## Screenshots
|
|
||||||
|
|
||||||

|
## Default Installation Informarion
|
||||||
|
|
||||||
[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 Port: 2095
|
||||||
- Panel Path: /app/
|
- Panel Path: /app/
|
||||||
- Subscription Port: 2096
|
- Subscription Port: 2096
|
||||||
- Subscription Path: /sub/
|
- Subscription Path: /sub/
|
||||||
- User/Password: admin
|
- User/Passowrd: admin
|
||||||
|
|
||||||
## Install & Upgrade to Latest Version
|
## Install & Upgrade to Latest Version
|
||||||
|
|
||||||
@@ -50,36 +40,25 @@
|
|||||||
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh)
|
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh)
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install legacy Version
|
## Install Custom 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`:
|
**Step 1:** To install your desired version, add the version to the end of the installation command. e.g., ver `0.0.1`:
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
VERSION=1.0.0 && bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/$VERSION/install.sh) $VERSION
|
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh) 0.0.1
|
||||||
```
|
```
|
||||||
|
|
||||||
## Manual installation
|
|
||||||
|
|
||||||
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`
|
|
||||||
|
|
||||||
## Uninstall S-UI
|
## Uninstall S-UI
|
||||||
|
|
||||||
```sh
|
```sh
|
||||||
sudo -i
|
systemctl disable sing-box --now
|
||||||
|
|
||||||
systemctl disable s-ui --now
|
systemctl disable s-ui --now
|
||||||
|
|
||||||
|
rm -f /etc/systemd/system/s-ui.service
|
||||||
rm -f /etc/systemd/system/sing-box.service
|
rm -f /etc/systemd/system/sing-box.service
|
||||||
systemctl daemon-reload
|
systemctl daemon-reload
|
||||||
|
|
||||||
rm -fr /usr/local/s-ui
|
rm -fr /usr/local/s-ui
|
||||||
rm /usr/bin/s-ui
|
|
||||||
```
|
```
|
||||||
|
|
||||||
## Install using Docker
|
## Install using Docker
|
||||||
@@ -101,11 +80,11 @@ curl -fsSL https://get.docker.com | sh
|
|||||||
|
|
||||||
```shell
|
```shell
|
||||||
mkdir s-ui && cd s-ui
|
mkdir s-ui && cd s-ui
|
||||||
wget -q https://raw.githubusercontent.com/alireza0/s-ui/master/docker-compose.yml
|
wget -q https://raw.githubusercontent.com/alireza0/s-ui/main/docker-compose.yml
|
||||||
docker compose up -d
|
docker compose up -d
|
||||||
```
|
```
|
||||||
|
|
||||||
> Use docker
|
> Use docker for s-ui only
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
mkdir s-ui && cd s-ui
|
mkdir s-ui && cd s-ui
|
||||||
@@ -120,56 +99,11 @@ docker run -itd \
|
|||||||
> Build your own image
|
> Build your own image
|
||||||
|
|
||||||
```shell
|
```shell
|
||||||
git clone https://github.com/alireza0/s-ui
|
|
||||||
git submodule update --init --recursive
|
|
||||||
docker build -t s-ui .
|
docker build -t s-ui .
|
||||||
```
|
```
|
||||||
|
|
||||||
</details>
|
</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
|
## Languages
|
||||||
|
|
||||||
- English
|
- English
|
||||||
@@ -177,14 +111,13 @@ To run backend (from root folder of repository):
|
|||||||
- Vietnamese
|
- Vietnamese
|
||||||
- Chinese (Simplified)
|
- Chinese (Simplified)
|
||||||
- Chinese (Traditional)
|
- Chinese (Traditional)
|
||||||
- Russian
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
- Supported protocols:
|
- Supported protocols:
|
||||||
- General: Mixed, SOCKS, HTTP, HTTPS, Direct, Redirect, TProxy
|
- General: Mixed, SOCKS, HTTP, HTTPS, Direct, Redirect, TProxy
|
||||||
- V2Ray based: VLESS, VMess, Trojan, Shadowsocks
|
- V2Ray based: VLESS, VMess, Trojan, Shadowsocks
|
||||||
- Other protocols: ShadowTLS, Hysteria, Hysteria2, Naive, TUIC
|
- Other protocols: ShadowTLS, Hysteria, Hysteri2, Naive, TUIC
|
||||||
- Supports XTLS protocols
|
- Supports XTLS protocols
|
||||||
- An advanced interface for routing traffic, incorporating PROXY Protocol, External, and Transparent Proxy, SSL Certificate, and Port
|
- 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
|
- An advanced interface for inbound and outbound configuration
|
||||||
@@ -196,18 +129,10 @@ To run backend (from root folder of repository):
|
|||||||
|
|
||||||
## Recommended OS
|
## Recommended OS
|
||||||
|
|
||||||
- Ubuntu 20.04+
|
|
||||||
- Debian 11+
|
|
||||||
- CentOS 8+
|
- CentOS 8+
|
||||||
|
- Ubuntu 20+
|
||||||
|
- Debian 10+
|
||||||
- Fedora 36+
|
- Fedora 36+
|
||||||
- Arch Linux
|
|
||||||
- Parch Linux
|
|
||||||
- Manjaro
|
|
||||||
- Armbian
|
|
||||||
- AlmaLinux 9+
|
|
||||||
- Rocky Linux 9+
|
|
||||||
- Oracle Linux 8+
|
|
||||||
- OpenSUSE Tubleweed
|
|
||||||
|
|
||||||
## Environment Variables
|
## Environment Variables
|
||||||
|
|
||||||
@@ -244,4 +169,4 @@ certbot certonly --standalone --register-unsafely-without-email --non-interactiv
|
|||||||
</details>
|
</details>
|
||||||
|
|
||||||
## Stargazers over Time
|
## Stargazers over Time
|
||||||
[](https://starchart.cc/alireza0/s-ui)
|
[](https://starchart.cc/alireza0/s-ui)
|
||||||
|
|||||||
@@ -1,100 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"s-ui/util/common"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"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 "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", "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)
|
|
||||||
default:
|
|
||||||
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,375 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"s-ui/database"
|
|
||||||
"s-ui/logger"
|
|
||||||
"s-ui/service"
|
|
||||||
"s-ui/util"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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(strings.Split(c.Request.Host, ":")[0])
|
|
||||||
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["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) 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)
|
|
||||||
}
|
|
||||||
@@ -1,129 +0,0 @@
|
|||||||
package api
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"s-ui/logger"
|
|
||||||
"s-ui/util/common"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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 "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", "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)
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -3,7 +3,6 @@ dist/
|
|||||||
release/
|
release/
|
||||||
backup/
|
backup/
|
||||||
bin/
|
bin/
|
||||||
db/
|
|
||||||
sui
|
sui
|
||||||
web/html
|
web/html
|
||||||
main
|
main
|
||||||
@@ -0,0 +1,218 @@
|
|||||||
|
package api
|
||||||
|
|
||||||
|
import (
|
||||||
|
"s-ui/logger"
|
||||||
|
"s-ui/service"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type APIHandler struct {
|
||||||
|
service.SettingService
|
||||||
|
service.UserService
|
||||||
|
service.ConfigService
|
||||||
|
service.ClientService
|
||||||
|
service.TlsService
|
||||||
|
service.PanelService
|
||||||
|
service.StatsService
|
||||||
|
service.ServerService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewAPIHandler(g *gin.RouterGroup) {
|
||||||
|
a := &APIHandler{}
|
||||||
|
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) {
|
||||||
|
var err error
|
||||||
|
action := c.Param("postAction")
|
||||||
|
remoteIP := getRemoteIp(c)
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "login":
|
||||||
|
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")
|
||||||
|
}
|
||||||
|
|
||||||
|
if sessionMaxAge > 0 {
|
||||||
|
err = SetMaxAge(c, sessionMaxAge*60)
|
||||||
|
if err != nil {
|
||||||
|
logger.Infof("Unable to set session's max age")
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = SetLoginUser(c, loginUser)
|
||||||
|
if err == nil {
|
||||||
|
logger.Info("user ", loginUser, " login success")
|
||||||
|
} else {
|
||||||
|
logger.Warning("login failed: ", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
jsonMsg(c, "", nil)
|
||||||
|
case "changePass":
|
||||||
|
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)
|
||||||
|
}
|
||||||
|
case "save":
|
||||||
|
loginUser := GetLoginUser(c)
|
||||||
|
data := map[string]string{}
|
||||||
|
err = c.ShouldBind(&data)
|
||||||
|
if err == nil {
|
||||||
|
err = a.ConfigService.SaveChanges(data, loginUser)
|
||||||
|
}
|
||||||
|
jsonMsg(c, "save", err)
|
||||||
|
case "restartApp":
|
||||||
|
err = a.PanelService.RestartPanel(3)
|
||||||
|
jsonMsg(c, "restartApp", err)
|
||||||
|
default:
|
||||||
|
jsonMsg(c, "API call", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIHandler) getHandler(c *gin.Context) {
|
||||||
|
action := c.Param("getAction")
|
||||||
|
|
||||||
|
switch action {
|
||||||
|
case "logout":
|
||||||
|
loginUser := GetLoginUser(c)
|
||||||
|
if loginUser != "" {
|
||||||
|
logger.Infof("user %s logout", loginUser)
|
||||||
|
}
|
||||||
|
ClearSession(c)
|
||||||
|
jsonMsg(c, "", nil)
|
||||||
|
case "load":
|
||||||
|
data, err := a.loadData(c)
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, data, nil)
|
||||||
|
case "users":
|
||||||
|
users, err := a.UserService.GetUsers()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, *users, nil)
|
||||||
|
case "setting":
|
||||||
|
data, err := a.SettingService.GetAllSetting()
|
||||||
|
if err != nil {
|
||||||
|
jsonMsg(c, "", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
jsonObj(c, data, err)
|
||||||
|
case "stats":
|
||||||
|
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)
|
||||||
|
case "status":
|
||||||
|
request := c.Query("r")
|
||||||
|
result := a.ServerService.GetStatus(request)
|
||||||
|
jsonObj(c, result, nil)
|
||||||
|
case "onlines":
|
||||||
|
onlines, err := a.StatsService.GetOnlines()
|
||||||
|
jsonObj(c, onlines, err)
|
||||||
|
case "logs":
|
||||||
|
service := c.Query("s")
|
||||||
|
count := c.Query("c")
|
||||||
|
level := c.Query("l")
|
||||||
|
logs := a.ServerService.GetLogs(service, count, level)
|
||||||
|
jsonObj(c, logs, nil)
|
||||||
|
case "changes":
|
||||||
|
actor := c.Query("a")
|
||||||
|
chngKey := c.Query("k")
|
||||||
|
count := c.Query("c")
|
||||||
|
changes := a.ConfigService.GetChanges(actor, chngKey, count)
|
||||||
|
jsonObj(c, changes, nil)
|
||||||
|
default:
|
||||||
|
jsonMsg(c, "API call", nil)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (a *APIHandler) loadData(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("sing-box", "1", "debug")
|
||||||
|
if len(logs) > 0 {
|
||||||
|
data["lastLog"] = logs[0]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
if isUpdated {
|
||||||
|
config, err := a.ConfigService.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
|
||||||
|
}
|
||||||
|
subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0])
|
||||||
|
if err != nil {
|
||||||
|
return "", err
|
||||||
|
}
|
||||||
|
data["config"] = *config
|
||||||
|
data["clients"] = clients
|
||||||
|
data["tls"] = tlsConfigs
|
||||||
|
data["subURI"] = subURI
|
||||||
|
data["onlines"] = onlines
|
||||||
|
} else {
|
||||||
|
data["onlines"] = onlines
|
||||||
|
}
|
||||||
|
|
||||||
|
return data, nil
|
||||||
|
}
|
||||||
@@ -4,7 +4,7 @@ import (
|
|||||||
"encoding/gob"
|
"encoding/gob"
|
||||||
"s-ui/database/model"
|
"s-ui/database/model"
|
||||||
|
|
||||||
"github.com/gin-contrib/sessions"
|
sessions "github.com/Calidity/gin-sessions"
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -16,26 +16,17 @@ func init() {
|
|||||||
gob.Register(model.User{})
|
gob.Register(model.User{})
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetLoginUser(c *gin.Context, userName string, maxAge int) error {
|
func SetLoginUser(c *gin.Context, userName string) error {
|
||||||
options := sessions.Options{
|
|
||||||
Path: "/",
|
|
||||||
Secure: false,
|
|
||||||
}
|
|
||||||
if maxAge > 0 {
|
|
||||||
options.MaxAge = maxAge * 60
|
|
||||||
}
|
|
||||||
|
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
s.Set(loginUser, userName)
|
s.Set(loginUser, userName)
|
||||||
s.Options(options)
|
|
||||||
|
|
||||||
return s.Save()
|
return s.Save()
|
||||||
}
|
}
|
||||||
|
|
||||||
func SetMaxAge(c *gin.Context) error {
|
func SetMaxAge(c *gin.Context, maxAge int) error {
|
||||||
s := sessions.Default(c)
|
s := sessions.Default(c)
|
||||||
s.Options(sessions.Options{
|
s.Options(sessions.Options{
|
||||||
Path: "/",
|
Path: "/",
|
||||||
|
MaxAge: maxAge,
|
||||||
})
|
})
|
||||||
return s.Save()
|
return s.Save()
|
||||||
}
|
}
|
||||||
@@ -27,14 +27,6 @@ func getRemoteIp(c *gin.Context) string {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func getHostname(c *gin.Context) string {
|
|
||||||
host := c.Request.Host
|
|
||||||
if colonIndex := strings.LastIndex(host, ":"); colonIndex != -1 {
|
|
||||||
host, _, _ = net.SplitHostPort(c.Request.Host)
|
|
||||||
}
|
|
||||||
return host
|
|
||||||
}
|
|
||||||
|
|
||||||
func jsonMsg(c *gin.Context, msg string, err error) {
|
func jsonMsg(c *gin.Context, msg string, err error) {
|
||||||
jsonMsgObj(c, msg, nil, err)
|
jsonMsgObj(c, msg, nil, err)
|
||||||
}
|
}
|
||||||
@@ -54,7 +46,7 @@ func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
|
|||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
m.Success = false
|
m.Success = false
|
||||||
m.Msg = msg + ": " + err.Error()
|
m.Msg = msg + err.Error()
|
||||||
logger.Warning("failed :", err)
|
logger.Warning("failed :", err)
|
||||||
}
|
}
|
||||||
c.JSON(http.StatusOK, m)
|
c.JSON(http.StatusOK, m)
|
||||||
@@ -3,7 +3,6 @@ package app
|
|||||||
import (
|
import (
|
||||||
"log"
|
"log"
|
||||||
"s-ui/config"
|
"s-ui/config"
|
||||||
"s-ui/core"
|
|
||||||
"s-ui/cronjob"
|
"s-ui/cronjob"
|
||||||
"s-ui/database"
|
"s-ui/database"
|
||||||
"s-ui/logger"
|
"s-ui/logger"
|
||||||
@@ -16,12 +15,9 @@ import (
|
|||||||
|
|
||||||
type APP struct {
|
type APP struct {
|
||||||
service.SettingService
|
service.SettingService
|
||||||
configService *service.ConfigService
|
webServer *web.Server
|
||||||
webServer *web.Server
|
subServer *sub.Server
|
||||||
subServer *sub.Server
|
cronJob *cronjob.CronJob
|
||||||
cronJob *cronjob.CronJob
|
|
||||||
logger *logging.Logger
|
|
||||||
core *core.Core
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewApp() *APP {
|
func NewApp() *APP {
|
||||||
@@ -38,17 +34,15 @@ func (a *APP) Init() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
// Init Setting
|
|
||||||
a.SettingService.GetAllSetting()
|
|
||||||
|
|
||||||
a.core = core.NewCore()
|
|
||||||
|
|
||||||
a.cronJob = cronjob.NewCronJob()
|
a.cronJob = cronjob.NewCronJob()
|
||||||
a.webServer = web.NewServer()
|
a.webServer = web.NewServer()
|
||||||
a.subServer = sub.NewServer()
|
a.subServer = sub.NewServer()
|
||||||
|
|
||||||
a.configService = service.NewConfigService(a.core)
|
configService := service.NewConfigService()
|
||||||
|
err = configService.InitConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -78,11 +72,6 @@ func (a *APP) Start() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
err = a.configService.StartCore("")
|
|
||||||
if err != nil {
|
|
||||||
logger.Error(err)
|
|
||||||
}
|
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -96,10 +85,6 @@ func (a *APP) Stop() {
|
|||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("stop Web Server err:", err)
|
logger.Warning("stop Web Server err:", err)
|
||||||
}
|
}
|
||||||
err = a.configService.StopCore()
|
|
||||||
if err != nil {
|
|
||||||
logger.Warning("stop Core err:", err)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *APP) initLog() {
|
func (a *APP) initLog() {
|
||||||
@@ -121,7 +106,3 @@ func (a *APP) RestartApp() {
|
|||||||
a.Stop()
|
a.Stop()
|
||||||
a.Start()
|
a.Start()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (a *APP) GetCore() *core.Core {
|
|
||||||
return a.core
|
|
||||||
}
|
|
||||||
@@ -4,8 +4,6 @@ import (
|
|||||||
"flag"
|
"flag"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"runtime/debug"
|
|
||||||
"s-ui/cmd/migration"
|
|
||||||
"s-ui/config"
|
"s-ui/config"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -42,7 +40,6 @@ func ParseCmd() {
|
|||||||
fmt.Println()
|
fmt.Println()
|
||||||
fmt.Println("Commands:")
|
fmt.Println("Commands:")
|
||||||
fmt.Println(" admin set/reset/show first admin credentials")
|
fmt.Println(" admin set/reset/show first admin credentials")
|
||||||
fmt.Println(" uri Show panel URI")
|
|
||||||
fmt.Println(" migrate migrate form older version")
|
fmt.Println(" migrate migrate form older version")
|
||||||
fmt.Println(" setting set/reset/show settings")
|
fmt.Println(" setting set/reset/show settings")
|
||||||
fmt.Println()
|
fmt.Println()
|
||||||
@@ -53,16 +50,7 @@ func ParseCmd() {
|
|||||||
|
|
||||||
flag.Parse()
|
flag.Parse()
|
||||||
if showVersion {
|
if showVersion {
|
||||||
fmt.Println("S-UI Panel\t", config.GetVersion())
|
fmt.Println(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
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -83,11 +71,8 @@ func ParseCmd() {
|
|||||||
showAdmin()
|
showAdmin()
|
||||||
}
|
}
|
||||||
|
|
||||||
case "uri":
|
|
||||||
getPanelURI()
|
|
||||||
|
|
||||||
case "migrate":
|
case "migrate":
|
||||||
migration.MigrateDb()
|
migrateDb()
|
||||||
|
|
||||||
case "setting":
|
case "setting":
|
||||||
err := settingCmd.Parse(os.Args[2:])
|
err := settingCmd.Parse(os.Args[2:])
|
||||||
@@ -1,14 +1,43 @@
|
|||||||
package migration
|
package cmd
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"fmt"
|
"fmt"
|
||||||
|
"log"
|
||||||
|
"s-ui/config"
|
||||||
|
"s-ui/database"
|
||||||
"s-ui/database/model"
|
"s-ui/database/model"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
func migrateDb() {
|
||||||
|
err := database.OpenDB(config.GetDBPath())
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
db := database.GetDB()
|
||||||
|
tx := db.Begin()
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
tx.Commit()
|
||||||
|
} else {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
fmt.Println("Start migrating database...")
|
||||||
|
err = migrateClientSchema(tx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
err = changesObj(tx)
|
||||||
|
if err != nil {
|
||||||
|
log.Fatal(err)
|
||||||
|
}
|
||||||
|
fmt.Println("Migration done!")
|
||||||
|
}
|
||||||
|
|
||||||
func migrateClientSchema(db *gorm.DB) error {
|
func migrateClientSchema(db *gorm.DB) error {
|
||||||
rows, err := db.Raw("PRAGMA table_info(clients)").Rows()
|
rows, err := db.Raw("PRAGMA table_info(clients)").Rows()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -41,15 +70,15 @@ func migrateClientSchema(db *gorm.DB) error {
|
|||||||
switch cname {
|
switch cname {
|
||||||
case "inbounds":
|
case "inbounds":
|
||||||
inbounds := strings.Split(data.Data, ",")
|
inbounds := strings.Split(data.Data, ",")
|
||||||
newData, _ = json.MarshalIndent(inbounds, "", " ")
|
newData, _ = json.MarshalIndent(inbounds, " ", " ")
|
||||||
case "config":
|
case "config":
|
||||||
jsonData := map[string]interface{}{}
|
jsonData := map[string]interface{}{}
|
||||||
json.Unmarshal([]byte(data.Data), &jsonData)
|
json.Unmarshal([]byte(data.Data), &jsonData)
|
||||||
newData, _ = json.MarshalIndent(jsonData, "", " ")
|
newData, _ = json.MarshalIndent(jsonData, " ", " ")
|
||||||
case "links":
|
case "links":
|
||||||
jsonData := make([]interface{}, 0)
|
jsonData := make([]interface{}, 0)
|
||||||
json.Unmarshal([]byte(data.Data), &jsonData)
|
json.Unmarshal([]byte(data.Data), &jsonData)
|
||||||
newData, _ = json.MarshalIndent(jsonData, "", " ")
|
newData, _ = json.MarshalIndent(jsonData, " ", " ")
|
||||||
}
|
}
|
||||||
err = db.Model(model.Client{}).Where("id = ?", data.Id).UpdateColumn(cname, newData).Error
|
err = db.Model(model.Client{}).Where("id = ?", data.Id).UpdateColumn(cname, newData).Error
|
||||||
if err != nil {
|
if err != nil {
|
||||||
@@ -59,29 +88,10 @@ func migrateClientSchema(db *gorm.DB) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
db.AutoMigrate(model.Client{})
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func deleteOldWebSecret(db *gorm.DB) error {
|
|
||||||
return db.Exec("DELETE FROM settings WHERE key = ?", "webSecret").Error
|
|
||||||
}
|
|
||||||
|
|
||||||
func changesObj(db *gorm.DB) 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
|
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
|
|
||||||
}
|
|
||||||
@@ -2,14 +2,9 @@ package cmd
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"fmt"
|
"fmt"
|
||||||
"io"
|
|
||||||
"net/http"
|
|
||||||
"s-ui/config"
|
"s-ui/config"
|
||||||
"s-ui/database"
|
"s-ui/database"
|
||||||
"s-ui/service"
|
"s-ui/service"
|
||||||
"strings"
|
|
||||||
|
|
||||||
"github.com/shirou/gopsutil/v4/net"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
func resetSetting() {
|
func resetSetting() {
|
||||||
@@ -108,64 +103,3 @@ func showSetting() {
|
|||||||
fmt.Println("\tSub URI:\t", (*allSetting)["subURI"])
|
fmt.Println("\tSub URI:\t", (*allSetting)["subURI"])
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
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:")
|
|
||||||
// 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 {
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
resp, err := http.Get("https://api.ipify.org?format=text")
|
|
||||||
if err == nil {
|
|
||||||
defer resp.Body.Close()
|
|
||||||
ip, err := io.ReadAll(resp.Body)
|
|
||||||
if err == nil {
|
|
||||||
fmt.Printf("\nGlobal address:\n%s%s%s%s\n", Proto, ip, PortText, BasePath)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
_ "embed"
|
_ "embed"
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"path/filepath"
|
|
||||||
"strings"
|
"strings"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -14,6 +13,9 @@ var version string
|
|||||||
//go:embed name
|
//go:embed name
|
||||||
var name string
|
var name string
|
||||||
|
|
||||||
|
//go:embed config.json
|
||||||
|
var defaultConfig string
|
||||||
|
|
||||||
type LogLevel string
|
type LogLevel string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -46,14 +48,18 @@ func IsDebug() bool {
|
|||||||
return os.Getenv("SUI_DEBUG") == "true"
|
return os.Getenv("SUI_DEBUG") == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetBinFolderPath() string {
|
||||||
|
binFolderPath := os.Getenv("SUI_BIN_FOLDER")
|
||||||
|
if binFolderPath == "" {
|
||||||
|
binFolderPath = "bin"
|
||||||
|
}
|
||||||
|
return binFolderPath
|
||||||
|
}
|
||||||
|
|
||||||
func GetDBFolderPath() string {
|
func GetDBFolderPath() string {
|
||||||
dbFolderPath := os.Getenv("SUI_DB_FOLDER")
|
dbFolderPath := os.Getenv("SUI_DB_FOLDER")
|
||||||
if dbFolderPath == "" {
|
if dbFolderPath == "" {
|
||||||
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
|
dbFolderPath = "/usr/local/s-ui/db"
|
||||||
if err != nil {
|
|
||||||
return "/usr/local/s-ui/db"
|
|
||||||
}
|
|
||||||
dbFolderPath = dir + "/db"
|
|
||||||
}
|
}
|
||||||
return dbFolderPath
|
return dbFolderPath
|
||||||
}
|
}
|
||||||
@@ -61,3 +67,15 @@ func GetDBFolderPath() string {
|
|||||||
func GetDBPath() string {
|
func GetDBPath() string {
|
||||||
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func GetDefaultConfig() string {
|
||||||
|
apiEnv := GetEnvApi()
|
||||||
|
if len(apiEnv) > 0 {
|
||||||
|
return strings.Replace(defaultConfig, "127.0.0.1:1080", apiEnv, 1)
|
||||||
|
}
|
||||||
|
return defaultConfig
|
||||||
|
}
|
||||||
|
|
||||||
|
func GetEnvApi() string {
|
||||||
|
return os.Getenv("SINGBOX_API")
|
||||||
|
}
|
||||||
@@ -0,0 +1,38 @@
|
|||||||
|
{
|
||||||
|
"log": {
|
||||||
|
"level": "info"
|
||||||
|
},
|
||||||
|
"dns": {},
|
||||||
|
"inbounds": [],
|
||||||
|
"outbounds": [
|
||||||
|
{
|
||||||
|
"tag": "direct",
|
||||||
|
"type": "direct"
|
||||||
|
},
|
||||||
|
{
|
||||||
|
"type": "dns",
|
||||||
|
"tag": "dns-out"
|
||||||
|
}
|
||||||
|
],
|
||||||
|
"route": {
|
||||||
|
"rules": [
|
||||||
|
{
|
||||||
|
"protocol": "dns",
|
||||||
|
"outbound": "dns-out"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
},
|
||||||
|
"experimental": {
|
||||||
|
"v2ray_api": {
|
||||||
|
"listen": "127.0.0.1:1080",
|
||||||
|
"stats": {
|
||||||
|
"enabled": true,
|
||||||
|
"inbounds": [],
|
||||||
|
"outbounds": [
|
||||||
|
"direct"
|
||||||
|
],
|
||||||
|
"users": []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1 @@
|
|||||||
|
0.0.5
|
||||||
@@ -25,8 +25,6 @@ func (c *CronJob) Start(loc *time.Location, trafficAge int) error {
|
|||||||
c.cron.AddJob("@every 1m", NewDepleteJob())
|
c.cron.AddJob("@every 1m", NewDepleteJob())
|
||||||
// Start deleting old stats
|
// Start deleting old stats
|
||||||
c.cron.AddJob("@daily", NewDelStatsJob(trafficAge))
|
c.cron.AddJob("@daily", NewDelStatsJob(trafficAge))
|
||||||
// Start core if it is not running
|
|
||||||
c.cron.AddJob("@every 5s", NewCheckCoreJob())
|
|
||||||
}()
|
}()
|
||||||
|
|
||||||
return nil
|
return nil
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
package cronjob
|
||||||
|
|
||||||
|
import (
|
||||||
|
"s-ui/logger"
|
||||||
|
"s-ui/service"
|
||||||
|
)
|
||||||
|
|
||||||
|
type DepleteJob struct {
|
||||||
|
service.ConfigService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewDepleteJob() *DepleteJob {
|
||||||
|
return new(DepleteJob)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *DepleteJob) Run() {
|
||||||
|
err := s.ConfigService.DepleteClients()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("Disable depleted users failed: ", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -6,15 +6,15 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
type StatsJob struct {
|
type StatsJob struct {
|
||||||
service.StatsService
|
service.SingBoxService
|
||||||
}
|
}
|
||||||
|
|
||||||
func NewStatsJob() *StatsJob {
|
func NewStatsJob() *StatsJob {
|
||||||
return &StatsJob{}
|
return new(StatsJob)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StatsJob) Run() {
|
func (s *StatsJob) Run() {
|
||||||
err := s.StatsService.SaveStats()
|
err := s.SingBoxService.GetStats()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Warning("Get stats failed: ", err)
|
logger.Warning("Get stats failed: ", err)
|
||||||
return
|
return
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package database
|
package database
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"os"
|
"os"
|
||||||
"path"
|
"path"
|
||||||
"s-ui/config"
|
"s-ui/config"
|
||||||
@@ -49,10 +48,6 @@ func OpenDB(dbPath string) error {
|
|||||||
Logger: gormLogger,
|
Logger: gormLogger,
|
||||||
}
|
}
|
||||||
db, err = gorm.Open(sqlite.Open(dbPath), c)
|
db, err = gorm.Open(sqlite.Open(dbPath), c)
|
||||||
|
|
||||||
if config.IsDebug() {
|
|
||||||
db = db.Debug()
|
|
||||||
}
|
|
||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,24 +57,10 @@ func InitDB(dbPath string) error {
|
|||||||
return err
|
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(
|
err = db.AutoMigrate(
|
||||||
&model.Setting{},
|
&model.Setting{},
|
||||||
&model.Tls{},
|
&model.Tls{},
|
||||||
&model.Inbound{},
|
|
||||||
&model.Outbound{},
|
|
||||||
&model.Service{},
|
|
||||||
&model.Endpoint{},
|
|
||||||
&model.User{},
|
&model.User{},
|
||||||
&model.Tokens{},
|
|
||||||
&model.Stats{},
|
&model.Stats{},
|
||||||
&model.Client{},
|
&model.Client{},
|
||||||
&model.Changes{},
|
&model.Changes{},
|
||||||
@@ -9,10 +9,11 @@ type Setting struct {
|
|||||||
}
|
}
|
||||||
|
|
||||||
type Tls struct {
|
type Tls struct {
|
||||||
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
Name string `json:"name" form:"name"`
|
Name string `json:"name" form:"name"`
|
||||||
Server json.RawMessage `json:"server" form:"server"`
|
Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
|
||||||
Client json.RawMessage `json:"client" form:"client"`
|
Server json.RawMessage `json:"server" form:"server"`
|
||||||
|
Client json.RawMessage `json:"client" form:"client"`
|
||||||
}
|
}
|
||||||
|
|
||||||
type User struct {
|
type User struct {
|
||||||
@@ -26,15 +27,14 @@ type Client struct {
|
|||||||
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
Enable bool `json:"enable" form:"enable"`
|
Enable bool `json:"enable" form:"enable"`
|
||||||
Name string `json:"name" form:"name"`
|
Name string `json:"name" form:"name"`
|
||||||
Config json.RawMessage `json:"config,omitempty" form:"config"`
|
Config json.RawMessage `json:"config" form:"config"`
|
||||||
Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
|
Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
|
||||||
Links json.RawMessage `json:"links,omitempty" form:"links"`
|
Links json.RawMessage `json:"links" form:"links"`
|
||||||
Volume int64 `json:"volume" form:"volume"`
|
Volume int64 `json:"volume" form:"volume"`
|
||||||
Expiry int64 `json:"expiry" form:"expiry"`
|
Expiry int64 `json:"expiry" form:"expiry"`
|
||||||
Down int64 `json:"down" form:"down"`
|
Down int64 `json:"down" form:"down"`
|
||||||
Up int64 `json:"up" form:"up"`
|
Up int64 `json:"up" form:"up"`
|
||||||
Desc string `json:"desc" form:"desc"`
|
Desc string `json:"desc" from:"desc"`
|
||||||
Group string `json:"group" form:"group"`
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type Stats struct {
|
type Stats struct {
|
||||||
@@ -49,17 +49,9 @@ type Stats struct {
|
|||||||
type Changes struct {
|
type Changes struct {
|
||||||
Id uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
Id uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||||
DateTime int64 `json:"dateTime"`
|
DateTime int64 `json:"dateTime"`
|
||||||
Actor string `json:"actor"`
|
Actor string `json:"Actor"`
|
||||||
Key string `json:"key"`
|
Key string `json:"key" form:"key"`
|
||||||
Action string `json:"action"`
|
Action string `json:"action" form:"action"`
|
||||||
Obj json.RawMessage `json:"obj"`
|
Index uint `json:"index" form:"index"`
|
||||||
}
|
Obj json.RawMessage `json:"obj" form:"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"`
|
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,64 @@
|
|||||||
|
module s-ui
|
||||||
|
|
||||||
|
go 1.22.0
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/gin-contrib/gzip v0.0.6
|
||||||
|
github.com/gin-gonic/gin v1.9.1
|
||||||
|
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||||
|
github.com/v2fly/v2ray-core/v5 v5.13.0
|
||||||
|
gorm.io/driver/sqlite v1.5.5
|
||||||
|
gorm.io/gorm v1.25.7
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/adrg/xdg v0.4.0 // indirect
|
||||||
|
github.com/bytedance/sonic v1.11.1 // indirect
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||||
|
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 // 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/goccy/go-json v0.10.2 // indirect
|
||||||
|
github.com/golang/protobuf v1.5.3 // indirect
|
||||||
|
github.com/gorilla/context v1.1.2 // indirect
|
||||||
|
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||||
|
github.com/gorilla/sessions v1.2.2 // indirect
|
||||||
|
github.com/json-iterator/go v1.1.12 // indirect
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||||
|
github.com/leodido/go-urn v1.4.0 // indirect
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a // indirect
|
||||||
|
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||||
|
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||||
|
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||||
|
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.6 // indirect
|
||||||
|
github.com/tklauser/go-sysconf v0.3.13 // indirect
|
||||||
|
github.com/tklauser/numcpus v0.7.0 // indirect
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
|
github.com/ugorji/go/codec v1.2.12 // indirect
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||||
|
golang.org/x/arch v0.7.0 // indirect
|
||||||
|
golang.org/x/crypto v0.21.0 // indirect
|
||||||
|
golang.org/x/net v0.23.0 // indirect
|
||||||
|
golang.org/x/sys v0.18.0 // indirect
|
||||||
|
golang.org/x/text v0.14.0 // indirect
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
|
||||||
|
google.golang.org/protobuf v1.33.0 // indirect
|
||||||
|
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||||
|
)
|
||||||
|
|
||||||
|
require (
|
||||||
|
github.com/Calidity/gin-sessions v1.3.1
|
||||||
|
github.com/gin-contrib/sse v0.1.0 // indirect
|
||||||
|
github.com/go-playground/validator/v10 v10.18.0 // indirect
|
||||||
|
github.com/jinzhu/inflection v1.0.0 // indirect
|
||||||
|
github.com/jinzhu/now v1.1.5 // indirect
|
||||||
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
|
||||||
|
github.com/pelletier/go-toml/v2 v2.1.1 // indirect
|
||||||
|
github.com/robfig/cron/v3 v3.0.1
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.1
|
||||||
|
google.golang.org/grpc v1.62.0
|
||||||
|
)
|
||||||
+311
@@ -0,0 +1,311 @@
|
|||||||
|
github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE=
|
||||||
|
github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns=
|
||||||
|
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
|
||||||
|
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
|
||||||
|
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY=
|
||||||
|
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1/go.mod h1:nuudZmJhzWtx2212z+pkuy7B6nkBqa+xwNXZHL1j8cg=
|
||||||
|
github.com/ajg/form v1.5.1 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
|
||||||
|
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY=
|
||||||
|
github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
|
||||||
|
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||||
|
github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d h1:zsO4lp+bjv5XvPTF58Vq+qgmZEYZttJK+CWtSZhKenI=
|
||||||
|
github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d/go.mod h1:f1iKL6ZhUWvbk7PdWVmOaak10o86cqMUYEmn1CZNGEI=
|
||||||
|
github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28/ZlunY=
|
||||||
|
github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE=
|
||||||
|
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
|
||||||
|
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
|
||||||
|
github.com/bytedance/sonic v1.11.1 h1:JC0+6c9FoWYYxakaoa+c5QTtJeiSZNeByOBhXtAFSn4=
|
||||||
|
github.com/bytedance/sonic v1.11.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
|
||||||
|
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA=
|
||||||
|
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||||
|
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0=
|
||||||
|
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog=
|
||||||
|
github.com/cloudflare/circl v1.3.3 h1:fE/Qz0QdIGqeWfnwq0RE0R7MI51s0M2E4Ga9kq5AEMs=
|
||||||
|
github.com/cloudflare/circl v1.3.3/go.mod h1:5XYMA4rFBvNIrhs50XuiBJ15vF2pZn4nnUKZrLbUZFA=
|
||||||
|
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||||
|
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||||
|
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||||
|
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFPCDw9JG6pdKt4F9pAhHv0B7FMGaGD0=
|
||||||
|
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||||
|
github.com/ebfe/bcrypt_pbkdf v0.0.0-20140212075826-3c8d2dcb253a h1:YtdtTUN1iH97s+6PUjLnaiKSQj4oG1/EZ3N9bx6g4kU=
|
||||||
|
github.com/ebfe/bcrypt_pbkdf v0.0.0-20140212075826-3c8d2dcb253a/go.mod h1:/CZpbhAusDOobpcb9yubw46kdYjq0zRC0Wpg9a9zFQM=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3 h1:in2uUcidCuFcDKtdcBxlR0rJ1+fsokWf+uqxgUFjbI0=
|
||||||
|
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk=
|
||||||
|
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk=
|
||||||
|
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
|
||||||
|
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
|
||||||
|
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
|
||||||
|
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
|
||||||
|
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
|
||||||
|
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
|
||||||
|
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg=
|
||||||
|
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
|
||||||
|
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
|
||||||
|
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-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.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
|
||||||
|
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.0/go.mod h1:sawfccIbzZTqEDETgFXqTho0QybSa7l++s0DH+LDiLs=
|
||||||
|
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.0/go.mod h1:UvRDBj+xPUEGrFYl+lu/H90nyDXpg0fqeB/AQUGNTVA=
|
||||||
|
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.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
|
||||||
|
github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
|
||||||
|
github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
|
||||||
|
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
|
||||||
|
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU=
|
||||||
|
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
|
||||||
|
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||||
|
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||||
|
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||||
|
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
|
||||||
|
github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
|
||||||
|
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
|
||||||
|
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4=
|
||||||
|
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
|
||||||
|
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
|
||||||
|
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
|
||||||
|
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||||
|
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||||
|
github.com/google/gopacket v1.1.19 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
|
||||||
|
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo=
|
||||||
|
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751 h1:hR7/MlvK23p6+lIw9SN1TigNLn9ZnF3W4SYRKq2gAHs=
|
||||||
|
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA=
|
||||||
|
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.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTjY=
|
||||||
|
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
|
||||||
|
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||||
|
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
|
||||||
|
github.com/jhump/protoreflect v1.15.3 h1:6SFRuqU45u9hIZPJAoZ8c28T3nK64BNdp9w6jFonzls=
|
||||||
|
github.com/jhump/protoreflect v1.15.3/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k=
|
||||||
|
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/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
|
||||||
|
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||||
|
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
|
||||||
|
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||||
|
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||||
|
github.com/klauspost/reedsolomon v1.11.7 h1:9uaHU0slncktTEEg4+7Vl7q7XUNMBUOK4R9gnKhMjAU=
|
||||||
|
github.com/klauspost/reedsolomon v1.11.7/go.mod h1:4bXRN+cVzMdml6ti7qLouuYi32KHJ5MGv0Qd8a47h6A=
|
||||||
|
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||||
|
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||||
|
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||||
|
github.com/kr/pretty v0.3.0 h1:WgNl7dwNpEZ6jJ9k1snq4pZsg7DOEN8hP9Xw0Tsjwk0=
|
||||||
|
github.com/kr/pretty v0.3.0/go.mod h1:640gp4NfQd8pI5XOwp5fnNeVWj67G7CFk/SaSQn7NBk=
|
||||||
|
github.com/kr/pty v1.1.1/go.mod h1:pFQYn66WHrOpPYNljwOMqo10TkYh1fy3cYio2l3bCsQ=
|
||||||
|
github.com/kr/text v0.1.0/go.mod h1:4Jbv+DJW3UT/LiOwJeYQe1efqtUx/iVham/4vfdArNI=
|
||||||
|
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.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
|
||||||
|
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||||
|
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI=
|
||||||
|
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
|
||||||
|
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
|
||||||
|
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
|
||||||
|
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
|
||||||
|
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 v2.0.3+incompatible h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
|
||||||
|
github.com/mattn/go-sqlite3 v2.0.3+incompatible/go.mod h1:FPy6KqzDD04eiIsT53CuJW3U88zkxoIYsOqkbpncsNc=
|
||||||
|
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/mustafaturan/bus v1.0.2 h1:2x3ErwZ0uUPwwZ5ZZoknEQprdaxr68Yl3mY8jDye1Ws=
|
||||||
|
github.com/mustafaturan/bus v1.0.2/go.mod h1:h7gfehm8TThv4Dcaa+wDQG7r7j6p74v+7ftr0Rq9i1Q=
|
||||||
|
github.com/mustafaturan/monoton v1.0.0 h1:8SCej+JiNn0lyps7V+Jzc1CRAkDR4EZPWrTupQ61YCQ=
|
||||||
|
github.com/mustafaturan/monoton v1.0.0/go.mod h1:FOnE7NV3s3EWPXb8/7+/OSdiMBbdlkV0Lz8p1dc+vy8=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs=
|
||||||
|
github.com/onsi/ginkgo/v2 v2.10.0/go.mod h1:UDQOh5wbQUlMnkLfVaIUMtQ1Vus92oM+P2JX1aulgcE=
|
||||||
|
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/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaRUnok+kx1WdO15EQc=
|
||||||
|
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ=
|
||||||
|
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
|
||||||
|
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.0.1/go.mod h1:r9LEWfGN8R5k0VXJ+0BkIe7MYkRdwZOjgMj2KwnJFUo=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI=
|
||||||
|
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc=
|
||||||
|
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8=
|
||||||
|
github.com/pion/dtls/v2 v2.2.7/go.mod h1:8WiMkebSHFD0T+dIU+UeBaoV7kDhOW5oDCzZ7WZ/F9s=
|
||||||
|
github.com/pion/logging v0.2.2 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
|
||||||
|
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms=
|
||||||
|
github.com/pion/randutil v0.1.0 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
|
||||||
|
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8=
|
||||||
|
github.com/pion/sctp v1.8.7 h1:JnABvFakZueGAn4KU/4PSKg+GWbF6QWbKTWZOSGJjXw=
|
||||||
|
github.com/pion/sctp v1.8.7/go.mod h1:g1Ul+ARqZq5JEmoFy87Q/4CePtKnTJ1QCL9dBBdN6AU=
|
||||||
|
github.com/pion/transport/v2 v2.2.4 h1:41JJK6DZQYSeVLxILA2+F4ZkKb4Xd/tFJZRFZQ9QAlo=
|
||||||
|
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0=
|
||||||
|
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs=
|
||||||
|
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4=
|
||||||
|
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
|
||||||
|
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||||
|
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||||
|
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/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||||
|
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||||
|
github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw=
|
||||||
|
github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
|
||||||
|
github.com/refraction-networking/utls v1.5.4 h1:9k6EO2b8TaOGsQ7Pl7p9w6PUhx18/ZCeT0WNTZ7Uw4o=
|
||||||
|
github.com/refraction-networking/utls v1.5.4/go.mod h1:SPuDbBmgLGp8s+HLNc83FuavwZCFoMmExj+ltUHiHUw=
|
||||||
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||||
|
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||||
|
github.com/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.6.1/go.mod h1:xXDCJY+GAPziupqXw64V24skbSoqbTEfhy4qGm1nDQc=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
|
||||||
|
github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6poM+XZ2dLUbcbE=
|
||||||
|
github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4 h1:zOjq+1/uLzn/Xo40stbvjIY/yehG0+mfmlsiEmc0xmQ=
|
||||||
|
github.com/secure-io/siv-go v0.0.0-20180922214919-5ff40651e2c4/go.mod h1:aI+8yClBW+1uovkHw6HM01YXnYB8vohtB9C83wzx34E=
|
||||||
|
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
|
||||||
|
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI=
|
||||||
|
github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU=
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
|
||||||
|
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
|
||||||
|
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||||
|
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
|
||||||
|
github.com/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/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||||
|
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||||
|
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||||
|
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||||
|
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
|
||||||
|
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
|
||||||
|
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
|
||||||
|
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
|
||||||
|
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
|
||||||
|
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||||
|
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||||
|
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
|
||||||
|
github.com/ugorji/go/codec v1.2.7/go.mod h1:WGN1fab3R1fzQlVQTkfxVtIBhWDRqOviHU95kRgeqEY=
|
||||||
|
github.com/ugorji/go/codec v1.2.12 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
|
||||||
|
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
|
||||||
|
github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08 h1:4Yh46CVE3k/lPq6hUbEdbB1u1anRBXLewm3k+L0iOMc=
|
||||||
|
github.com/v2fly/BrowserBridge v0.0.0-20210430233438-0570fc1d7d08/go.mod h1:KAuQNm+LWQCOFqdBcUgihPzRpVXRKzGbTNhfEfRZ4wY=
|
||||||
|
github.com/v2fly/VSign v0.0.0-20201108000810-e2adc24bf848 h1:p1UzXK6VAutXFFQMnre66h7g1BjRKUnLv0HfmmRoz7w=
|
||||||
|
github.com/v2fly/VSign v0.0.0-20201108000810-e2adc24bf848/go.mod h1:p80Bv154ZtrGpXMN15slDCqc9UGmfBuUzheDFBYaW/M=
|
||||||
|
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||||
|
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||||
|
github.com/v2fly/v2ray-core/v5 v5.13.0 h1:BDJfi3Ftx1NpQlZZPpeWJe3RDqRNyIVBs+YGG4RRMDU=
|
||||||
|
github.com/v2fly/v2ray-core/v5 v5.13.0/go.mod h1:Bc3gmQWLr8UR7xBSCYI9FbfKuVvqA9lbkeBTWNRRAS4=
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI=
|
||||||
|
github.com/vincent-petithory/dataurl v1.0.0/go.mod h1:FHafX5vmDzyP+1CQATJn7WFKc9CvnvxyvZy6I1MrG/U=
|
||||||
|
github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 h1:I/ATawgO2RerCq9ACwL0wBB8xNXZdE3J+93MCEHReRs=
|
||||||
|
github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432/go.mod h1:QN7Go2ftTVfx0aCTh9RXHV8pkpi0FtmbwQw40dy61wQ=
|
||||||
|
github.com/xtaci/smux v1.5.24 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY=
|
||||||
|
github.com/xtaci/smux v1.5.24/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||||
|
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||||
|
go.starlark.net v0.0.0-20230612165344-9532f5667272 h1:2/wtqS591wZyD2OsClsVBKRPEvBsQt/Js+fsCiYhwu8=
|
||||||
|
go.starlark.net v0.0.0-20230612165344-9532f5667272/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
|
||||||
|
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
|
||||||
|
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
|
||||||
|
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 h1:nJAwRlGWZZDOD+6wni9KVUNHMpHko/OnRwsrCYeAzPo=
|
||||||
|
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y=
|
||||||
|
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
|
||||||
|
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
|
||||||
|
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
|
||||||
|
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
|
||||||
|
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
|
||||||
|
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
|
||||||
|
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
|
||||||
|
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
|
||||||
|
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
|
||||||
|
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||||
|
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||||
|
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
|
||||||
|
golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
|
||||||
|
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
|
||||||
|
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
|
||||||
|
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/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-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||||
|
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
|
||||||
|
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
|
||||||
|
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
|
||||||
|
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.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ=
|
||||||
|
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
|
||||||
|
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
|
||||||
|
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
|
||||||
|
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||||
|
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
|
||||||
|
golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
|
||||||
|
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
|
||||||
|
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
||||||
|
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
|
||||||
|
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||||
|
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
|
||||||
|
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
|
||||||
|
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||||
|
google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
|
||||||
|
google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
|
||||||
|
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||||
|
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/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/errgo.v2 v2.1.0/go.mod h1:hNsd1EY+bozCKY1Ytp96fpM3vjJbqLJn88ws8XvfDNI=
|
||||||
|
gopkg.in/yaml.v2 v2.4.0/go.mod h1:RDklbk79AGWmwhnvt/jBztapEOGDOx6ZbXqjP6csGnQ=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||||
|
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/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.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
||||||
|
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
||||||
|
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
|
||||||
|
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||||
|
gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 h1:qDCwdCWECGnwQSQC01Dpnp09fRHxJs9PbktotUqG+hs=
|
||||||
|
gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1/go.mod h1:8hmigyCdYtw5xJGfQDJzSH5Ju8XEIDBnpyi8+O6GRt8=
|
||||||
|
lukechampine.com/blake3 v1.2.1 h1:YuqqRuaqsGV71BV/nm9xlI0MKUv4QC54jQnBChWbGnI=
|
||||||
|
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
|
||||||
|
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
|
||||||
|
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
|
||||||
@@ -8,27 +8,30 @@ import (
|
|||||||
"github.com/op/go-logging"
|
"github.com/op/go-logging"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
var logger *logging.Logger
|
||||||
logger *logging.Logger
|
var logBuffer []struct {
|
||||||
logBuffer []struct {
|
time string
|
||||||
time string
|
level logging.Level
|
||||||
level logging.Level
|
log string
|
||||||
log string
|
}
|
||||||
}
|
|
||||||
)
|
func init() {
|
||||||
|
InitLogger(logging.INFO)
|
||||||
|
}
|
||||||
|
|
||||||
func InitLogger(level logging.Level) {
|
func InitLogger(level logging.Level) {
|
||||||
newLogger := logging.MustGetLogger("s-ui")
|
newLogger := logging.MustGetLogger("s-ui")
|
||||||
var err error
|
var err error
|
||||||
var backend logging.Backend
|
var backend logging.Backend
|
||||||
var format logging.Formatter
|
var format logging.Formatter
|
||||||
|
ppid := os.Getppid()
|
||||||
|
|
||||||
backend, err = logging.NewSyslogBackend("")
|
backend, err = logging.NewSyslogBackend("")
|
||||||
if err != nil {
|
if err != nil {
|
||||||
fmt.Println("Unable to use syslog: " + err.Error())
|
println(err)
|
||||||
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||||
}
|
}
|
||||||
if err != nil {
|
if ppid > 0 && err != nil {
|
||||||
format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
|
format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
|
||||||
} else {
|
} else {
|
||||||
format = logging.MustStringFormatter(`%{level} - %{message}`)
|
format = logging.MustStringFormatter(`%{level} - %{message}`)
|
||||||
@@ -42,10 +45,6 @@ func InitLogger(level logging.Level) {
|
|||||||
logger = newLogger
|
logger = newLogger
|
||||||
}
|
}
|
||||||
|
|
||||||
func GetLogger() *logging.Logger {
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
func Debug(args ...interface{}) {
|
func Debug(args ...interface{}) {
|
||||||
logger.Debug(args...)
|
logger.Debug(args...)
|
||||||
addToBuffer("DEBUG", fmt.Sprint(args...))
|
addToBuffer("DEBUG", fmt.Sprint(args...))
|
||||||
@@ -1,7 +1,6 @@
|
|||||||
package middleware
|
package middleware
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"net"
|
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
@@ -10,10 +9,7 @@ import (
|
|||||||
|
|
||||||
func DomainValidator(domain string) gin.HandlerFunc {
|
func DomainValidator(domain string) gin.HandlerFunc {
|
||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
host := c.Request.Host
|
host := strings.Split(c.Request.Host, ":")[0]
|
||||||
if colonIndex := strings.LastIndex(host, ":"); colonIndex != -1 {
|
|
||||||
host, _, _ = net.SplitHostPort(c.Request.Host)
|
|
||||||
}
|
|
||||||
|
|
||||||
if host != domain {
|
if host != domain {
|
||||||
c.AbortWithStatus(http.StatusForbidden)
|
c.AbortWithStatus(http.StatusForbidden)
|
||||||
@@ -0,0 +1,91 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"s-ui/database"
|
||||||
|
"s-ui/database/model"
|
||||||
|
"s-ui/logger"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ClientService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) GetAll() ([]model.Client, error) {
|
||||||
|
db := database.GetDB()
|
||||||
|
clients := []model.Client{}
|
||||||
|
err := db.Model(model.Client{}).Scan(&clients).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return clients, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) Save(tx *gorm.DB, changes []model.Changes) error {
|
||||||
|
var err error
|
||||||
|
for _, change := range changes {
|
||||||
|
client := model.Client{}
|
||||||
|
err = json.Unmarshal(change.Obj, &client)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch change.Action {
|
||||||
|
case "new":
|
||||||
|
err = tx.Create(&client).Error
|
||||||
|
case "del":
|
||||||
|
err = tx.Where("id = ?", change.Index).Delete(model.Client{}).Error
|
||||||
|
default:
|
||||||
|
err = tx.Save(client).Error
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ClientService) DepleteClients() ([]string, []string, error) {
|
||||||
|
var err error
|
||||||
|
var clients []model.Client
|
||||||
|
var changes []model.Changes
|
||||||
|
now := time.Now().Unix()
|
||||||
|
db := database.GetDB()
|
||||||
|
err = db.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", now).Scan(&clients).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dt := time.Now().Unix()
|
||||||
|
var users, inbounds []string
|
||||||
|
for _, client := range clients {
|
||||||
|
logger.Debug("Client ", client.Name, " is going to be disabled")
|
||||||
|
users = append(users, client.Name)
|
||||||
|
var userInbounds []string
|
||||||
|
json.Unmarshal(client.Inbounds, &userInbounds)
|
||||||
|
inbounds = append(inbounds, 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 = db.Model(model.Client{}).Where("enable = true AND ((volume >0 AND up+down > volume) OR (expiry > 0 AND expiry < ?))", now).Update("enable", false).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
err = db.Model(model.Changes{}).Create(&changes).Error
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
LastUpdate = dt
|
||||||
|
}
|
||||||
|
|
||||||
|
return users, inbounds, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,363 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"os"
|
||||||
|
"s-ui/config"
|
||||||
|
"s-ui/database"
|
||||||
|
"s-ui/database/model"
|
||||||
|
"s-ui/logger"
|
||||||
|
"s-ui/singbox"
|
||||||
|
"strconv"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
var ApiAddr string
|
||||||
|
var LastUpdate int64
|
||||||
|
|
||||||
|
type ConfigService struct {
|
||||||
|
ClientService
|
||||||
|
TlsService
|
||||||
|
singbox.Controller
|
||||||
|
SettingService
|
||||||
|
}
|
||||||
|
|
||||||
|
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"`
|
||||||
|
Route json.RawMessage `json:"route"`
|
||||||
|
Experimental json.RawMessage `json:"experimental"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewConfigService() *ConfigService {
|
||||||
|
return &ConfigService{}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) InitConfig() error {
|
||||||
|
configPath := config.GetBinFolderPath()
|
||||||
|
data, err := os.ReadFile(configPath + "/config.json")
|
||||||
|
if err != nil {
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
defaultConfig := []byte(config.GetDefaultConfig())
|
||||||
|
err = os.MkdirAll(configPath, 01764)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = os.WriteFile(configPath+"/config.json", defaultConfig, 0764)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
data = defaultConfig
|
||||||
|
} else {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
var singboxConfig SingBoxConfig
|
||||||
|
err = json.Unmarshal(data, &singboxConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.RefreshApiAddr(&singboxConfig)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) GetConfig() (*SingBoxConfig, error) {
|
||||||
|
configPath := config.GetBinFolderPath()
|
||||||
|
data, err := os.ReadFile(configPath + "/config.json")
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
singboxConfig := SingBoxConfig{}
|
||||||
|
err = json.Unmarshal(data, &singboxConfig)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return &singboxConfig, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string) error {
|
||||||
|
var err error
|
||||||
|
var clientChanges, tlsChanges, settingChanges, configChanges []model.Changes
|
||||||
|
if _, ok := changes["clients"]; ok {
|
||||||
|
err = json.Unmarshal([]byte(changes["clients"]), &clientChanges)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := changes["tls"]; ok {
|
||||||
|
err = json.Unmarshal([]byte(changes["tls"]), &tlsChanges)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := changes["settings"]; ok {
|
||||||
|
err = json.Unmarshal([]byte(changes["settings"]), &settingChanges)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if _, ok := changes["config"]; ok {
|
||||||
|
err = json.Unmarshal([]byte(changes["config"]), &configChanges)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
db := database.GetDB()
|
||||||
|
tx := db.Begin()
|
||||||
|
defer func() {
|
||||||
|
if err == nil {
|
||||||
|
tx.Commit()
|
||||||
|
} else {
|
||||||
|
tx.Rollback()
|
||||||
|
}
|
||||||
|
}()
|
||||||
|
|
||||||
|
if len(clientChanges) > 0 {
|
||||||
|
err = s.ClientService.Save(tx, clientChanges)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(tlsChanges) > 0 {
|
||||||
|
err = s.TlsService.Save(tx, tlsChanges)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(settingChanges) > 0 {
|
||||||
|
err = s.SettingService.Save(tx, settingChanges)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
if len(configChanges) > 0 {
|
||||||
|
singboxConfig, err := s.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
newConfig := *singboxConfig
|
||||||
|
for _, change := range configChanges {
|
||||||
|
rawObject := change.Obj
|
||||||
|
switch change.Key {
|
||||||
|
case "all":
|
||||||
|
err = json.Unmarshal(rawObject, &newConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
case "log":
|
||||||
|
newConfig.Log = rawObject
|
||||||
|
case "dns":
|
||||||
|
newConfig.Dns = rawObject
|
||||||
|
case "ntp":
|
||||||
|
newConfig.Ntp = rawObject
|
||||||
|
case "route":
|
||||||
|
newConfig.Route = rawObject
|
||||||
|
case "experimental":
|
||||||
|
newConfig.Experimental = rawObject
|
||||||
|
case "inbounds":
|
||||||
|
if change.Action == "edit" {
|
||||||
|
newConfig.Inbounds[change.Index] = rawObject
|
||||||
|
} else if change.Action == "del" {
|
||||||
|
newConfig.Inbounds = append(newConfig.Inbounds[:change.Index], newConfig.Inbounds[change.Index+1:]...)
|
||||||
|
} else {
|
||||||
|
newConfig.Inbounds = append(newConfig.Inbounds, rawObject)
|
||||||
|
}
|
||||||
|
case "outbounds":
|
||||||
|
if change.Action == "edit" {
|
||||||
|
newConfig.Outbounds[change.Index] = rawObject
|
||||||
|
} else if change.Action == "del" {
|
||||||
|
newConfig.Outbounds = append(newConfig.Outbounds[:change.Index], newConfig.Outbounds[change.Index+1:]...)
|
||||||
|
} else {
|
||||||
|
newConfig.Outbounds = append(newConfig.Outbounds, rawObject)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Save(&newConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Log changes
|
||||||
|
dt := time.Now().Unix()
|
||||||
|
allChanges := append(append(clientChanges, settingChanges...), append(configChanges, tlsChanges...)...)
|
||||||
|
for index := range allChanges {
|
||||||
|
allChanges[index].DateTime = dt
|
||||||
|
allChanges[index].Actor = loginUser
|
||||||
|
}
|
||||||
|
err = tx.Model(model.Changes{}).Create(&allChanges).Error
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
LastUpdate = dt
|
||||||
|
|
||||||
|
return 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) Save(singboxConfig *SingBoxConfig) error {
|
||||||
|
configPath := config.GetBinFolderPath()
|
||||||
|
_, err := os.Stat(configPath + "/config.json")
|
||||||
|
if os.IsNotExist(err) {
|
||||||
|
err = os.MkdirAll(configPath, 01764)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
} else if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
data, err := json.MarshalIndent(singboxConfig, " ", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
err = os.WriteFile(configPath+"/config.json", data, 0764)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
s.RefreshApiAddr(singboxConfig)
|
||||||
|
s.Controller.Restart()
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) RefreshApiAddr(singboxConfig *SingBoxConfig) error {
|
||||||
|
Env_API := config.GetEnvApi()
|
||||||
|
if len(Env_API) > 0 {
|
||||||
|
ApiAddr = Env_API
|
||||||
|
} else {
|
||||||
|
var err error
|
||||||
|
if singboxConfig == nil {
|
||||||
|
singboxConfig, err = s.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
var experimental struct {
|
||||||
|
V2rayApi struct {
|
||||||
|
Listen string `json:"listen"`
|
||||||
|
Stats interface{} `jaon:"stats"`
|
||||||
|
} `json:"v2ray_api"`
|
||||||
|
}
|
||||||
|
err = json.Unmarshal(singboxConfig.Experimental, &experimental)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
ApiAddr = experimental.V2rayApi.Listen
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) DepleteClients() error {
|
||||||
|
users, inbounds, err := s.ClientService.DepleteClients()
|
||||||
|
if err != nil || len(users) == 0 || len(inbounds) == 0 {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
singboxConfig, err := s.GetConfig()
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
for inbound_index, inbound := range singboxConfig.Inbounds {
|
||||||
|
var inboundJson map[string]interface{}
|
||||||
|
json.Unmarshal(inbound, &inboundJson)
|
||||||
|
if s.contains(inbounds, inboundJson["tag"].(string)) {
|
||||||
|
inbound_users, ok := inboundJson["users"].([]interface{})
|
||||||
|
if ok {
|
||||||
|
var updatedUsers []interface{}
|
||||||
|
for _, user := range inbound_users {
|
||||||
|
userMap, ok := user.(map[string]interface{})
|
||||||
|
if ok {
|
||||||
|
name, exists := userMap["name"].(string)
|
||||||
|
if exists && s.contains(users, name) {
|
||||||
|
// Skip the user exists
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
username, exists := userMap["username"].(string)
|
||||||
|
if exists && s.contains(users, username) {
|
||||||
|
// Skip the username exists
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
}
|
||||||
|
updatedUsers = append(updatedUsers, user)
|
||||||
|
}
|
||||||
|
// Exception for Naive and ShadowTLSv3
|
||||||
|
if len(updatedUsers) == 0 {
|
||||||
|
if inboundJson["type"].(string) == "naive" ||
|
||||||
|
(inboundJson["type"].(string) == "shadowtls" &&
|
||||||
|
inboundJson["version"].(float64) == 3) {
|
||||||
|
updatedUsers = append(updatedUsers, make(map[string]interface{}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
inboundJson["users"] = updatedUsers
|
||||||
|
}
|
||||||
|
}
|
||||||
|
modifiedInbound, err := json.MarshalIndent(inboundJson, "", " ")
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
singboxConfig.Inbounds[inbound_index] = modifiedInbound
|
||||||
|
}
|
||||||
|
|
||||||
|
err = s.Save(singboxConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ConfigService) contains(slice []string, item string) bool {
|
||||||
|
for _, str := range slice {
|
||||||
|
if str == item {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
}
|
||||||
@@ -0,0 +1,161 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"bytes"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"runtime"
|
||||||
|
"s-ui/config"
|
||||||
|
"s-ui/logger"
|
||||||
|
"strconv"
|
||||||
|
"strings"
|
||||||
|
|
||||||
|
"github.com/shirou/gopsutil/v3/cpu"
|
||||||
|
"github.com/shirou/gopsutil/v3/host"
|
||||||
|
"github.com/shirou/gopsutil/v3/mem"
|
||||||
|
"github.com/shirou/gopsutil/v3/net"
|
||||||
|
)
|
||||||
|
|
||||||
|
type ServerService struct {
|
||||||
|
SingBoxService
|
||||||
|
}
|
||||||
|
|
||||||
|
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 "net":
|
||||||
|
status["net"] = s.GetNetInfo()
|
||||||
|
case "sys":
|
||||||
|
status["uptime"] = s.GetUptime()
|
||||||
|
status["sys"] = s.GetSystemInfo()
|
||||||
|
case "sbd":
|
||||||
|
status["sbd"] = s.GetSingboxInfo()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
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) GetUptime() uint64 {
|
||||||
|
upTime, err := host.Uptime()
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("get uptime failed:", err)
|
||||||
|
return 0
|
||||||
|
} else {
|
||||||
|
return upTime
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
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) 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{} {
|
||||||
|
info := make(map[string]interface{}, 0)
|
||||||
|
sysStats, err := s.SingBoxService.GetSysStats()
|
||||||
|
if err == nil {
|
||||||
|
info["running"] = true
|
||||||
|
info["stats"] = sysStats
|
||||||
|
} else {
|
||||||
|
info["running"] = s.SingBoxService.IsRunning()
|
||||||
|
}
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
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
|
||||||
|
|
||||||
|
return info
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ServerService) GetLogs(service string, count string, level string) []string {
|
||||||
|
c, _ := strconv.Atoi(count)
|
||||||
|
var lines []string
|
||||||
|
if service == "sing-box" {
|
||||||
|
cmdArgs := []string{"journalctl", "-u", service, "--no-pager", "-n", count, "-p", level}
|
||||||
|
// Run the command
|
||||||
|
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
|
||||||
|
var out bytes.Buffer
|
||||||
|
cmd.Stdout = &out
|
||||||
|
err := cmd.Run()
|
||||||
|
if err != nil {
|
||||||
|
return []string{"Failed to run journalctl command!"}
|
||||||
|
}
|
||||||
|
lines = strings.Split(out.String(), "\n")
|
||||||
|
} else {
|
||||||
|
lines = logger.GetLogs(c, level)
|
||||||
|
}
|
||||||
|
|
||||||
|
return lines
|
||||||
|
}
|
||||||
@@ -3,7 +3,6 @@ package service
|
|||||||
import (
|
import (
|
||||||
"encoding/json"
|
"encoding/json"
|
||||||
"os"
|
"os"
|
||||||
"s-ui/config"
|
|
||||||
"s-ui/database"
|
"s-ui/database"
|
||||||
"s-ui/database/model"
|
"s-ui/database/model"
|
||||||
"s-ui/logger"
|
"s-ui/logger"
|
||||||
@@ -15,24 +14,6 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var defaultConfig = `{
|
|
||||||
"log": {
|
|
||||||
"level": "info"
|
|
||||||
},
|
|
||||||
"dns": {},
|
|
||||||
"route": {
|
|
||||||
"rules": [
|
|
||||||
{
|
|
||||||
"protocol": [
|
|
||||||
"dns"
|
|
||||||
],
|
|
||||||
"action": "hijack-dns"
|
|
||||||
}
|
|
||||||
]
|
|
||||||
},
|
|
||||||
"experimental": {}
|
|
||||||
}`
|
|
||||||
|
|
||||||
var defaultValueMap = map[string]string{
|
var defaultValueMap = map[string]string{
|
||||||
"webListen": "",
|
"webListen": "",
|
||||||
"webDomain": "",
|
"webDomain": "",
|
||||||
@@ -55,10 +36,6 @@ var defaultValueMap = map[string]string{
|
|||||||
"subEncode": "true",
|
"subEncode": "true",
|
||||||
"subShowInfo": "false",
|
"subShowInfo": "false",
|
||||||
"subURI": "",
|
"subURI": "",
|
||||||
"subJsonExt": "",
|
|
||||||
"subClashExt": "",
|
|
||||||
"config": defaultConfig,
|
|
||||||
"version": config.GetVersion(),
|
|
||||||
}
|
}
|
||||||
|
|
||||||
type SettingService struct {
|
type SettingService struct {
|
||||||
@@ -88,9 +65,7 @@ func (s *SettingService) GetAllSetting() (*map[string]string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// Due to security principles
|
// Due to security principles
|
||||||
delete(allSetting, "secret")
|
delete(allSetting, "webSecret")
|
||||||
delete(allSetting, "config")
|
|
||||||
delete(allSetting, "version")
|
|
||||||
|
|
||||||
return &allSetting, nil
|
return &allSetting, nil
|
||||||
}
|
}
|
||||||
@@ -152,9 +127,9 @@ func (s *SettingService) getBool(key string) (bool, error) {
|
|||||||
return strconv.ParseBool(str)
|
return strconv.ParseBool(str)
|
||||||
}
|
}
|
||||||
|
|
||||||
// func (s *SettingService) setBool(key string, value bool) error {
|
func (s *SettingService) setBool(key string, value bool) error {
|
||||||
// return s.setString(key, strconv.FormatBool(value))
|
return s.setString(key, strconv.FormatBool(value))
|
||||||
// }
|
}
|
||||||
|
|
||||||
func (s *SettingService) getInt(key string) (int, error) {
|
func (s *SettingService) getInt(key string) (int, error) {
|
||||||
str, err := s.getString(key)
|
str, err := s.getString(key)
|
||||||
@@ -335,31 +310,14 @@ func (s *SettingService) GetFinalSubURI(host string) (string, error) {
|
|||||||
return protocol + "://" + host + port + (*allSetting)["subPath"], nil
|
return protocol + "://" + host + port + (*allSetting)["subPath"], nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *SettingService) GetConfig() (string, error) {
|
func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) 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 err error
|
||||||
var settings map[string]string
|
for _, change := range changes {
|
||||||
err = json.Unmarshal(data, &settings)
|
key := change.Key
|
||||||
if err != nil {
|
var obj string
|
||||||
return err
|
json.Unmarshal(change.Obj, &obj)
|
||||||
}
|
|
||||||
for key, obj := range settings {
|
// Secure file existance check
|
||||||
// Secure file existence check
|
|
||||||
if obj != "" && (key == "webCertFile" ||
|
if obj != "" && (key == "webCertFile" ||
|
||||||
key == "webKeyFile" ||
|
key == "webKeyFile" ||
|
||||||
key == "subCertFile" ||
|
key == "subCertFile" ||
|
||||||
@@ -389,14 +347,6 @@ func (s *SettingService) Save(tx *gorm.DB, data json.RawMessage) error {
|
|||||||
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 {
|
func (s *SettingService) fileExists(path string) error {
|
||||||
_, err := os.Stat(path)
|
_, err := os.Stat(path)
|
||||||
return err
|
return err
|
||||||
@@ -0,0 +1,45 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"s-ui/singbox"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SingBoxService struct {
|
||||||
|
singbox.V2rayAPI
|
||||||
|
singbox.Controller
|
||||||
|
StatsService
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SingBoxService) GetStats() error {
|
||||||
|
s.V2rayAPI.Init(ApiAddr)
|
||||||
|
defer s.V2rayAPI.Close()
|
||||||
|
stats, err := s.V2rayAPI.GetStats(true)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
err = s.StatsService.SaveStats(stats)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SingBoxService) GetSysStats() (*map[string]interface{}, error) {
|
||||||
|
err := s.V2rayAPI.Init(ApiAddr)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
defer s.V2rayAPI.Close()
|
||||||
|
resp, err := s.V2rayAPI.GetSysStats()
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
result := make(map[string]interface{})
|
||||||
|
result["NumGoroutine"] = resp.NumGoroutine
|
||||||
|
result["Alloc"] = resp.Alloc
|
||||||
|
result["Uptime"] = resp.Uptime
|
||||||
|
|
||||||
|
return &result, nil
|
||||||
|
}
|
||||||
@@ -19,22 +19,18 @@ var onlineResources = &onlines{}
|
|||||||
type StatsService struct {
|
type StatsService struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StatsService) SaveStats() error {
|
func (s *StatsService) SaveStats(stats []*model.Stats) error {
|
||||||
if !corePtr.IsRunning() {
|
var err error
|
||||||
return nil
|
|
||||||
}
|
|
||||||
stats := corePtr.GetInstance().ConnTracker().GetStats()
|
|
||||||
|
|
||||||
// Reset onlines
|
// Reset onlines
|
||||||
onlineResources.Inbound = nil
|
onlineResources.Inbound = nil
|
||||||
onlineResources.Outbound = nil
|
onlineResources.Outbound = nil
|
||||||
onlineResources.User = nil
|
onlineResources.User = nil
|
||||||
|
|
||||||
if len(*stats) == 0 {
|
if len(stats) == 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
var err error
|
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
tx := db.Begin()
|
tx := db.Begin()
|
||||||
defer func() {
|
defer func() {
|
||||||
@@ -45,7 +41,7 @@ func (s *StatsService) SaveStats() error {
|
|||||||
}
|
}
|
||||||
}()
|
}()
|
||||||
|
|
||||||
for _, stat := range *stats {
|
for _, stat := range stats {
|
||||||
if stat.Resource == "user" {
|
if stat.Resource == "user" {
|
||||||
if stat.Direction {
|
if stat.Direction {
|
||||||
err = tx.Model(model.Client{}).Where("name = ?", stat.Tag).
|
err = tx.Model(model.Client{}).Where("name = ?", stat.Tag).
|
||||||
@@ -74,7 +70,7 @@ func (s *StatsService) SaveStats() error {
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *StatsService) GetStats(resource string, tag string, limit int) ([]model.Stats, error) {
|
func (s *StatsService) GetStats(resorce string, tag string, limit int) ([]model.Stats, error) {
|
||||||
var err error
|
var err error
|
||||||
var result []model.Stats
|
var result []model.Stats
|
||||||
|
|
||||||
@@ -82,11 +78,7 @@ func (s *StatsService) GetStats(resource string, tag string, limit int) ([]model
|
|||||||
timeDiff := currentTime - (int64(limit) * 3600)
|
timeDiff := currentTime - (int64(limit) * 3600)
|
||||||
|
|
||||||
db := database.GetDB()
|
db := database.GetDB()
|
||||||
resources := []string{resource}
|
err = db.Model(model.Stats{}).Where("resource = ? AND tag = ? AND date_time > ?", resorce, tag, timeDiff).Scan(&result).Error
|
||||||
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 {
|
if err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
@@ -0,0 +1,46 @@
|
|||||||
|
package service
|
||||||
|
|
||||||
|
import (
|
||||||
|
"encoding/json"
|
||||||
|
"s-ui/database"
|
||||||
|
"s-ui/database/model"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
|
)
|
||||||
|
|
||||||
|
type TlsService struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
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, changes []model.Changes) error {
|
||||||
|
var err error
|
||||||
|
for _, change := range changes {
|
||||||
|
tlsConfig := model.Tls{}
|
||||||
|
err = json.Unmarshal(change.Obj, &tlsConfig)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
switch change.Action {
|
||||||
|
case "new":
|
||||||
|
err = tx.Create(&tlsConfig).Error
|
||||||
|
case "del":
|
||||||
|
err = tx.Where("id = ?", change.Index).Delete(model.Tls{}).Error
|
||||||
|
default:
|
||||||
|
err = tx.Save(tlsConfig).Error
|
||||||
|
}
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return err
|
||||||
|
}
|
||||||
@@ -1,12 +1,13 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"encoding/json"
|
|
||||||
"s-ui/database"
|
"s-ui/database"
|
||||||
"s-ui/database/model"
|
"s-ui/database/model"
|
||||||
"s-ui/logger"
|
"s-ui/logger"
|
||||||
"s-ui/util/common"
|
"s-ui/util/common"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
type UserService struct {
|
type UserService struct {
|
||||||
@@ -62,7 +63,7 @@ func (s *UserService) CheckUser(username string, password string, remoteIP strin
|
|||||||
Where("username = ? and password = ?", username, password).
|
Where("username = ? and password = ?", username, password).
|
||||||
First(user).
|
First(user).
|
||||||
Error
|
Error
|
||||||
if database.IsNotFound(err) {
|
if err == gorm.ErrRecordNotFound {
|
||||||
return nil
|
return nil
|
||||||
} else if err != nil {
|
} else if err != nil {
|
||||||
logger.Warning("check user err:", err, " IP: ", remoteIP)
|
logger.Warning("check user err:", err, " IP: ", remoteIP)
|
||||||
@@ -100,61 +101,3 @@ func (s *UserService) ChangePass(id string, oldPass string, newUser string, newP
|
|||||||
user.Password = newPass
|
user.Password = newPass
|
||||||
return db.Save(user).Error
|
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
|
|
||||||
}
|
|
||||||
@@ -0,0 +1,54 @@
|
|||||||
|
package singbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"errors"
|
||||||
|
"io/fs"
|
||||||
|
"os"
|
||||||
|
"os/exec"
|
||||||
|
"s-ui/config"
|
||||||
|
"strings"
|
||||||
|
)
|
||||||
|
|
||||||
|
var serviceName = "sing-box"
|
||||||
|
|
||||||
|
type Controller struct {
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Controller) GetBinaryName() string {
|
||||||
|
return "sing-box"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Controller) GetBinaryPath() string {
|
||||||
|
return config.GetBinFolderPath() + "/" + s.GetBinaryName()
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Controller) GetConfigPath() string {
|
||||||
|
return config.GetBinFolderPath() + "/config.json"
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Controller) IsRunning() bool {
|
||||||
|
cmd := exec.Command("pgrep", "sing-box")
|
||||||
|
output, err := cmd.Output()
|
||||||
|
if err != nil {
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
|
// If pgrep found the Controller, its output will not be empty
|
||||||
|
return strings.TrimSpace(string(output)) != ""
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Controller) signalSingbox(signal string) error {
|
||||||
|
return os.WriteFile(config.GetBinFolderPath()+"/signal", []byte(signal), fs.ModePerm)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Controller) Restart() error {
|
||||||
|
return s.signalSingbox("restart")
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *Controller) Stop() error {
|
||||||
|
if !s.IsRunning() {
|
||||||
|
return errors.New("Sing-Box is not running")
|
||||||
|
}
|
||||||
|
|
||||||
|
return s.signalSingbox("stop")
|
||||||
|
}
|
||||||
@@ -0,0 +1,95 @@
|
|||||||
|
package singbox
|
||||||
|
|
||||||
|
import (
|
||||||
|
"context"
|
||||||
|
|
||||||
|
"regexp"
|
||||||
|
"s-ui/database/model"
|
||||||
|
"s-ui/util/common"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
statsService "github.com/v2fly/v2ray-core/v5/app/stats/command"
|
||||||
|
"google.golang.org/grpc"
|
||||||
|
)
|
||||||
|
|
||||||
|
type V2rayAPI struct {
|
||||||
|
StatsServiceClient *statsService.StatsServiceClient
|
||||||
|
grpcClient *grpc.ClientConn
|
||||||
|
isConnected bool
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *V2rayAPI) Init(ApiAddr string) (err error) {
|
||||||
|
if len(ApiAddr) == 0 {
|
||||||
|
return common.NewError("The api address is wrong: ", ApiAddr)
|
||||||
|
}
|
||||||
|
v.grpcClient, err = grpc.Dial(ApiAddr, grpc.WithInsecure())
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
v.isConnected = true
|
||||||
|
|
||||||
|
ssClient := statsService.NewStatsServiceClient(v.grpcClient)
|
||||||
|
|
||||||
|
v.StatsServiceClient = &ssClient
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *V2rayAPI) Close() {
|
||||||
|
v.grpcClient.Close()
|
||||||
|
v.StatsServiceClient = nil
|
||||||
|
v.isConnected = false
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *V2rayAPI) GetStats(reset bool) ([]*model.Stats, error) {
|
||||||
|
if v.grpcClient == nil {
|
||||||
|
return nil, common.NewError("v2ray api is not initialized")
|
||||||
|
}
|
||||||
|
var trafficRegex = regexp.MustCompile("(inbound|outbound|user)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||||
|
|
||||||
|
client := *v.StatsServiceClient
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
defer cancel()
|
||||||
|
request := &statsService.QueryStatsRequest{
|
||||||
|
Reset_: reset,
|
||||||
|
}
|
||||||
|
resp, err := client.QueryStats(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
dt := time.Now().Unix()
|
||||||
|
stats := make([]*model.Stats, 0)
|
||||||
|
for _, stat := range resp.GetStat() {
|
||||||
|
if stat.Value > 0 {
|
||||||
|
matchs := trafficRegex.FindStringSubmatch(stat.Name)
|
||||||
|
if len(matchs) > 3 {
|
||||||
|
stat := model.Stats{
|
||||||
|
DateTime: dt,
|
||||||
|
Resource: matchs[1],
|
||||||
|
Tag: matchs[2],
|
||||||
|
Direction: matchs[3] == "uplink",
|
||||||
|
Traffic: stat.Value,
|
||||||
|
}
|
||||||
|
stats = append(stats, &stat)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return stats, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
func (v *V2rayAPI) GetSysStats() (*statsService.SysStatsResponse, error) {
|
||||||
|
if v.grpcClient == nil {
|
||||||
|
return nil, common.NewError("v2ray api is not initialized")
|
||||||
|
}
|
||||||
|
client := *v.StatsServiceClient
|
||||||
|
ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
|
||||||
|
defer cancel()
|
||||||
|
request := &statsService.SysStatsRequest{}
|
||||||
|
resp, err := client.GetSysStats(ctx, request)
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp, nil
|
||||||
|
}
|
||||||
@@ -0,0 +1,39 @@
|
|||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"s-ui/logger"
|
||||||
|
"s-ui/service"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubHandler struct {
|
||||||
|
service.SettingService
|
||||||
|
SubService
|
||||||
|
}
|
||||||
|
|
||||||
|
func NewSubHandler(g *gin.RouterGroup) {
|
||||||
|
a := &SubHandler{}
|
||||||
|
a.initRouter(g)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubHandler) initRouter(g *gin.RouterGroup) {
|
||||||
|
g.GET("/:subid", s.subs)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubHandler) subs(c *gin.Context) {
|
||||||
|
subId := c.Param("subid")
|
||||||
|
result, headers, err := s.SubService.GetSubs(subId)
|
||||||
|
if err != nil || result == nil {
|
||||||
|
logger.Error(err)
|
||||||
|
c.String(400, "Error!")
|
||||||
|
} else {
|
||||||
|
|
||||||
|
// Add headers
|
||||||
|
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])
|
||||||
|
|
||||||
|
c.String(200, *result)
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -0,0 +1,181 @@
|
|||||||
|
package sub
|
||||||
|
|
||||||
|
import (
|
||||||
|
"crypto/tls"
|
||||||
|
"encoding/base64"
|
||||||
|
"encoding/json"
|
||||||
|
"fmt"
|
||||||
|
"io"
|
||||||
|
"net/http"
|
||||||
|
"s-ui/database"
|
||||||
|
"s-ui/database/model"
|
||||||
|
"s-ui/logger"
|
||||||
|
"s-ui/service"
|
||||||
|
"strings"
|
||||||
|
"time"
|
||||||
|
)
|
||||||
|
|
||||||
|
type SubService struct {
|
||||||
|
service.SettingService
|
||||||
|
}
|
||||||
|
|
||||||
|
type Link struct {
|
||||||
|
Type string `json:"type"`
|
||||||
|
Remark string `json:"remark"`
|
||||||
|
Uri string `json:"uri"`
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) GetSubs(subId string) (*string, []string, error) {
|
||||||
|
var err 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
|
||||||
|
}
|
||||||
|
|
||||||
|
links := []Link{}
|
||||||
|
err = json.Unmarshal([]byte(client.Links), &links)
|
||||||
|
if err != nil {
|
||||||
|
return nil, nil, err
|
||||||
|
}
|
||||||
|
|
||||||
|
clientInfo := ""
|
||||||
|
subShowInfo, _ := s.SettingService.GetSubShowInfo()
|
||||||
|
if subShowInfo {
|
||||||
|
clientInfo = s.getClientInfo(client)
|
||||||
|
}
|
||||||
|
|
||||||
|
var result string
|
||||||
|
for _, link := range links {
|
||||||
|
switch link.Type {
|
||||||
|
case "external":
|
||||||
|
result += fmt.Sprintln(link.Uri)
|
||||||
|
case "sub":
|
||||||
|
result += s.getExternalSub(link.Uri)
|
||||||
|
case "local":
|
||||||
|
result += fmt.Sprintln(s.addClientInfo(link.Uri, clientInfo))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
var headers []string
|
||||||
|
updateInterval, _ := s.SettingService.GetSubUpdates()
|
||||||
|
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, subId)
|
||||||
|
|
||||||
|
subEncode, _ := s.SettingService.GetSubEncode()
|
||||||
|
if subEncode {
|
||||||
|
result = base64.StdEncoding.EncodeToString([]byte(result))
|
||||||
|
}
|
||||||
|
|
||||||
|
return &result, headers, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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) addClientInfo(uri string, clientInfo string) string {
|
||||||
|
protocol := strings.Split(uri, "://")
|
||||||
|
if len(protocol) < 2 {
|
||||||
|
return uri
|
||||||
|
}
|
||||||
|
switch protocol[0] {
|
||||||
|
case "vmess":
|
||||||
|
var vmessJson map[string]interface{}
|
||||||
|
config, err := base64.StdEncoding.DecodeString(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://" + base64.StdEncoding.EncodeToString(result)
|
||||||
|
default:
|
||||||
|
return uri + clientInfo
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *SubService) getExternalSub(url string) string {
|
||||||
|
tr := &http.Transport{
|
||||||
|
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
|
||||||
|
}
|
||||||
|
|
||||||
|
client := &http.Client{Transport: tr}
|
||||||
|
|
||||||
|
// Make the HTTP request
|
||||||
|
response, err := client.Get(url)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("sub: Error making HTTP request:", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
defer response.Body.Close()
|
||||||
|
|
||||||
|
// Read the response body
|
||||||
|
body, err := io.ReadAll(response.Body)
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("sub: Error reading response body:", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the content is Base64 encoded
|
||||||
|
isBase64 := s.isBase64Encoded(string(body))
|
||||||
|
if isBase64 {
|
||||||
|
// Decode Base64 content
|
||||||
|
decodedText, err := base64.StdEncoding.DecodeString(string(body))
|
||||||
|
if err != nil {
|
||||||
|
logger.Warning("sub: Error decoding Base64 content:", err)
|
||||||
|
return ""
|
||||||
|
}
|
||||||
|
|
||||||
|
return string(decodedText)
|
||||||
|
} else {
|
||||||
|
return string(body)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Function to check if a string is Base64 encoded
|
||||||
|
func (s *SubService) isBase64Encoded(str string) bool {
|
||||||
|
_, err := base64.StdEncoding.DecodeString(str)
|
||||||
|
return err == nil
|
||||||
|
}
|
||||||
|
|
||||||
|
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))
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -24,7 +24,3 @@ func Random(n int) string {
|
|||||||
}
|
}
|
||||||
return string(runes)
|
return string(runes)
|
||||||
}
|
}
|
||||||
|
|
||||||
func RandomInt(n int) int {
|
|
||||||
return rnd.Intn(n)
|
|
||||||
}
|
|
||||||
@@ -18,13 +18,13 @@ import (
|
|||||||
"strconv"
|
"strconv"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
sessions "github.com/Calidity/gin-sessions"
|
||||||
|
"github.com/Calidity/gin-sessions/cookie"
|
||||||
"github.com/gin-contrib/gzip"
|
"github.com/gin-contrib/gzip"
|
||||||
"github.com/gin-contrib/sessions"
|
|
||||||
"github.com/gin-contrib/sessions/cookie"
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
//go:embed *
|
//go:embed html/*
|
||||||
var content embed.FS
|
var content embed.FS
|
||||||
|
|
||||||
type Server struct {
|
type Server struct {
|
||||||
@@ -102,11 +102,8 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||||||
|
|
||||||
engine.StaticFS(assetsBasePath, http.FS(assetsFS))
|
engine.StaticFS(assetsBasePath, http.FS(assetsFS))
|
||||||
|
|
||||||
group_apiv2 := engine.Group(base_url + "apiv2")
|
|
||||||
apiv2 := api.NewAPIv2Handler(group_apiv2)
|
|
||||||
|
|
||||||
group_api := engine.Group(base_url + "api")
|
group_api := engine.Group(base_url + "api")
|
||||||
api.NewAPIHandler(group_api, apiv2)
|
api.NewAPIHandler(group_api)
|
||||||
|
|
||||||
// Serve index.html as the entry point
|
// Serve index.html as the entry point
|
||||||
// Handle all other routes by serving index.html
|
// Handle all other routes by serving index.html
|
||||||
@@ -1,14 +1,13 @@
|
|||||||
#!/bin/sh
|
#!/bin/sh
|
||||||
|
|
||||||
cd frontend
|
cd frontend
|
||||||
npm i
|
|
||||||
npm run build
|
npm run build
|
||||||
|
|
||||||
cd ..
|
cd ..
|
||||||
|
cd backend
|
||||||
echo "Backend"
|
echo "Backend"
|
||||||
|
|
||||||
mkdir -p web/html
|
|
||||||
rm -fr web/html/*
|
rm -fr web/html/*
|
||||||
cp -R frontend/dist/* web/html/
|
cp -R ../frontend/dist/ web/html/
|
||||||
|
|
||||||
go build -ldflags "-w -s" -tags "with_quic,with_grpc,with_utls,with_acme,with_gvisor" -o sui main.go
|
go build -o ../sui main.go
|
||||||
@@ -1,316 +0,0 @@
|
|||||||
package migration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"errors"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"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)
|
|
||||||
}
|
|
||||||
@@ -1,154 +0,0 @@
|
|||||||
package migration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"encoding/json"
|
|
||||||
"net/url"
|
|
||||||
"s-ui/database/model"
|
|
||||||
"strconv"
|
|
||||||
"strings"
|
|
||||||
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
@@ -1,78 +0,0 @@
|
|||||||
package migration
|
|
||||||
|
|
||||||
import (
|
|
||||||
"fmt"
|
|
||||||
"log"
|
|
||||||
"os"
|
|
||||||
"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
|
|
||||||
}
|
|
||||||
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!")
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
1.3.0-beta.5
|
|
||||||
@@ -0,0 +1,28 @@
|
|||||||
|
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS singbox-builder
|
||||||
|
LABEL maintainer="Alireza <alireza7@gmail.com>"
|
||||||
|
WORKDIR /app
|
||||||
|
ARG TARGETOS TARGETARCH
|
||||||
|
ARG SINGBOX_VER=v1.8.13
|
||||||
|
ARG SINGBOX_TAGS="with_quic,with_grpc,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_v2ray_api,with_clash_api,with_gvisor"
|
||||||
|
ARG GOPROXY=""
|
||||||
|
ENV GOPROXY ${GOPROXY}
|
||||||
|
ENV CGO_ENABLED=0
|
||||||
|
ENV GOOS=$TARGETOS
|
||||||
|
ENV GOARCH=$TARGETARCH
|
||||||
|
RUN apk --no-cache --update add build-base gcc wget unzip git
|
||||||
|
RUN set -ex \
|
||||||
|
&& git clone --depth 1 --branch $SINGBOX_VER https://github.com/SagerNet/sing-box.git \
|
||||||
|
&& cd sing-box \
|
||||||
|
&& go build -v -trimpath -tags \
|
||||||
|
$SINGBOX_TAGS \
|
||||||
|
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$SINGBOX_VER\" -s -w -buildid=" \
|
||||||
|
./cmd/sing-box
|
||||||
|
|
||||||
|
FROM --platform=$TARGETPLATFORM alpine
|
||||||
|
LABEL maintainer="Alireza <alireza7@gmail.com>"
|
||||||
|
ENV TZ=Asia/Tehran
|
||||||
|
WORKDIR /app
|
||||||
|
RUN apk add --no-cache --update ca-certificates tzdata bash
|
||||||
|
COPY --from=singbox-builder /app/sing-box/sing-box .
|
||||||
|
COPY runSingbox.sh .
|
||||||
|
ENTRYPOINT [ "./runSingbox.sh" ]
|
||||||
-535
@@ -1,535 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
"s-ui/util/common"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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/experimental/libbox/platform"
|
|
||||||
"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
|
|
||||||
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[platform.Interface](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)
|
|
||||||
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(sbCommon.Must1(
|
|
||||||
direct.NewOutbound(
|
|
||||||
ctx,
|
|
||||||
router,
|
|
||||||
logFactory.NewLogger("outbound/direct"),
|
|
||||||
"direct",
|
|
||||||
option.DirectOutboundOptions{},
|
|
||||||
),
|
|
||||||
))
|
|
||||||
dnsTransportManager.Initialize(sbCommon.Must1(
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if connTracker == nil {
|
|
||||||
connTracker = NewConnTracker()
|
|
||||||
}
|
|
||||||
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,
|
|
||||||
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 {
|
|
||||||
// TODO: remove catch error
|
|
||||||
defer func() {
|
|
||||||
v := recover()
|
|
||||||
if v != nil {
|
|
||||||
s.logger.Debug(err.Error())
|
|
||||||
s.logger.Error("panic on early start: " + fmt.Sprint(v))
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
s.Close()
|
|
||||||
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(adapter.StartStateInitialize, s.internalService) // cache-file clash-api v2ray-api
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = adapter.Start(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(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(adapter.StartStateStart, s.internalService)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = adapter.Start(adapter.StartStateStart, s.inbound, s.endpoint, s.service)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = adapter.Start(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(adapter.StartStatePostStart, s.internalService)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
err = adapter.Start(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(adapter.StartStateStarted, s.internalService)
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Box) Close() error {
|
|
||||||
select {
|
|
||||||
case <-s.done:
|
|
||||||
return os.ErrClosed
|
|
||||||
default:
|
|
||||||
close(s.done)
|
|
||||||
}
|
|
||||||
err := sbCommon.Close(
|
|
||||||
s.service, s.endpoint, s.inbound, s.outbound, s.router, s.connection, s.dnsRouter, s.dnsTransport, s.network,
|
|
||||||
)
|
|
||||||
for _, lifecycleService := range s.internalService {
|
|
||||||
err1 := lifecycleService.Close()
|
|
||||||
if err1 != nil {
|
|
||||||
s.logger.Debug(lifecycleService.Name(), " close error: ", err1)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
err1 := s.logFactory.Close()
|
|
||||||
if err1 != nil {
|
|
||||||
s.logger.Debug("logger close error: ", err1)
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *Box) Uptime() uint32 {
|
|
||||||
return uint32(time.Now().Sub(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) ConnTracker() *ConnTracker {
|
|
||||||
return s.connTracker
|
|
||||||
}
|
|
||||||
@@ -1,145 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"net"
|
|
||||||
"s-ui/database/model"
|
|
||||||
"sync"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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 ConnTracker struct {
|
|
||||||
access sync.Mutex
|
|
||||||
createdAt time.Time
|
|
||||||
inbounds map[string]Counter
|
|
||||||
outbounds map[string]Counter
|
|
||||||
users map[string]Counter
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewConnTracker() *ConnTracker {
|
|
||||||
return &ConnTracker{
|
|
||||||
createdAt: time.Now(),
|
|
||||||
inbounds: make(map[string]Counter),
|
|
||||||
outbounds: make(map[string]Counter),
|
|
||||||
users: make(map[string]Counter),
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConnTracker) getReadCounters(inbound string, outbound string, user string) ([]*atomic.Int64, []*atomic.Int64) {
|
|
||||||
var readCounter []*atomic.Int64
|
|
||||||
var writeCounter []*atomic.Int64
|
|
||||||
c.access.Lock()
|
|
||||||
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)
|
|
||||||
}
|
|
||||||
c.access.Unlock()
|
|
||||||
return readCounter, writeCounter
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConnTracker) 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 *ConnTracker) 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 *ConnTracker) 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, writeCounter)
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *ConnTracker) 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
|
|
||||||
}
|
|
||||||
@@ -1,147 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"s-ui/logger"
|
|
||||||
"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)
|
|
||||||
}
|
|
||||||
-241
@@ -1,241 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"io"
|
|
||||||
"os"
|
|
||||||
suiLog "s-ui/logger"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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)
|
|
||||||
}
|
|
||||||
@@ -1,92 +0,0 @@
|
|||||||
package core
|
|
||||||
|
|
||||||
import (
|
|
||||||
"context"
|
|
||||||
"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-dns/quic"
|
|
||||||
"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
|
|
||||||
connTracker *ConnTracker
|
|
||||||
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 {
|
|
||||||
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 {
|
|
||||||
if c.isRunning {
|
|
||||||
c.isRunning = false
|
|
||||||
return c.instance.Close()
|
|
||||||
}
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
func (c *Core) IsRunning() bool {
|
|
||||||
return c.isRunning
|
|
||||||
}
|
|
||||||
@@ -1,160 +0,0 @@
|
|||||||
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"
|
|
||||||
protocolDNS "github.com/sagernet/sing-box/protocol/dns"
|
|
||||||
"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/tailscale"
|
|
||||||
"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/derp"
|
|
||||||
"github.com/sagernet/sing-box/service/resolved"
|
|
||||||
"github.com/sagernet/sing-box/service/ssmapi"
|
|
||||||
_ "github.com/sagernet/sing-box/transport/v2rayquic"
|
|
||||||
_ "github.com/sagernet/sing-dns/quic"
|
|
||||||
)
|
|
||||||
|
|
||||||
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)
|
|
||||||
protocolDNS.RegisterOutbound(registry)
|
|
||||||
|
|
||||||
group.RegisterSelector(registry)
|
|
||||||
group.RegisterURLTest(registry)
|
|
||||||
|
|
||||||
socks.RegisterOutbound(registry)
|
|
||||||
http.RegisterOutbound(registry)
|
|
||||||
shadowsocks.RegisterOutbound(registry)
|
|
||||||
vmess.RegisterOutbound(registry)
|
|
||||||
trojan.RegisterOutbound(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)
|
|
||||||
wireguard.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)
|
|
||||||
|
|
||||||
registerQUICTransports(registry)
|
|
||||||
registerDHCPTransport(registry)
|
|
||||||
registerTailscaleTransport(registry)
|
|
||||||
|
|
||||||
return registry
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerTailscaleEndpoint(registry *endpoint.Registry) {
|
|
||||||
tailscale.RegisterEndpoint(registry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerTailscaleTransport(registry *dns.TransportRegistry) {
|
|
||||||
tailscale.RegistryTransport(registry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerDERPService(registry *service.Registry) {
|
|
||||||
derp.Register(registry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerQUICTransports(registry *dns.TransportRegistry) {
|
|
||||||
quic.RegisterTransport(registry)
|
|
||||||
quic.RegisterHTTP3Transport(registry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func registerDHCPTransport(registry *dns.TransportRegistry) {
|
|
||||||
dhcp.RegisterTransport(registry)
|
|
||||||
}
|
|
||||||
|
|
||||||
func ServiceRegistry() *service.Registry {
|
|
||||||
registry := service.NewRegistry()
|
|
||||||
|
|
||||||
resolved.RegisterService(registry)
|
|
||||||
ssmapi.RegisterService(registry)
|
|
||||||
|
|
||||||
registerDERPService(registry)
|
|
||||||
|
|
||||||
return registry
|
|
||||||
}
|
|
||||||
Executable
+53
@@ -0,0 +1,53 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
set -e
|
||||||
|
|
||||||
|
tokill=$$
|
||||||
|
|
||||||
|
runSingbox(){
|
||||||
|
./sing-box run &
|
||||||
|
tokill=$!
|
||||||
|
}
|
||||||
|
|
||||||
|
terminateSingbox()
|
||||||
|
{
|
||||||
|
if kill -0 $tokill > /dev/null 2>&1; then
|
||||||
|
echo "Terminating singbox PID=$tokill"
|
||||||
|
kill $tokill
|
||||||
|
while kill -0 $tokill > /dev/null 2>&1; do
|
||||||
|
sleep 1
|
||||||
|
done
|
||||||
|
fi
|
||||||
|
}
|
||||||
|
|
||||||
|
trap terminateSingbox SIGINT SIGTERM SIGKILL
|
||||||
|
|
||||||
|
runSingbox
|
||||||
|
|
||||||
|
while true
|
||||||
|
do
|
||||||
|
sleep 5
|
||||||
|
if [ -f "signal" ]; then
|
||||||
|
signal=`cat signal`
|
||||||
|
echo "Signal received: $signal"
|
||||||
|
# Remove singnal file
|
||||||
|
rm -f signal >> /dev/null 2>&1
|
||||||
|
case ${signal} in
|
||||||
|
"stop")
|
||||||
|
terminateSingbox
|
||||||
|
;;
|
||||||
|
"restart")
|
||||||
|
terminateSingbox
|
||||||
|
runSingbox
|
||||||
|
;;
|
||||||
|
esac
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Check if sin-box crashed
|
||||||
|
if ! kill -0 $tokill > /dev/null 2>&1; then
|
||||||
|
if [ "$signal" != "stop" ]; then
|
||||||
|
echo "Sing-Box with PID $tokill crashed. Breaking the loop..."
|
||||||
|
break
|
||||||
|
fi
|
||||||
|
fi
|
||||||
|
done
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
package cronjob
|
|
||||||
|
|
||||||
import (
|
|
||||||
"s-ui/service"
|
|
||||||
)
|
|
||||||
|
|
||||||
type CheckCoreJob struct {
|
|
||||||
service.ConfigService
|
|
||||||
}
|
|
||||||
|
|
||||||
func NewCheckCoreJob() *CheckCoreJob {
|
|
||||||
return &CheckCoreJob{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func (s *CheckCoreJob) Run() {
|
|
||||||
s.ConfigService.StartCore("")
|
|
||||||
}
|
|
||||||
@@ -1,30 +0,0 @@
|
|||||||
package cronjob
|
|
||||||
|
|
||||||
import (
|
|
||||||
"s-ui/database"
|
|
||||||
"s-ui/logger"
|
|
||||||
"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)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,296 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"bytes"
|
|
||||||
"fmt"
|
|
||||||
"io"
|
|
||||||
"mime/multipart"
|
|
||||||
"os"
|
|
||||||
"path/filepath"
|
|
||||||
"s-ui/cmd/migration"
|
|
||||||
"s-ui/config"
|
|
||||||
"s-ui/database/model"
|
|
||||||
"s-ui/logger"
|
|
||||||
"s-ui/util/common"
|
|
||||||
"strings"
|
|
||||||
"syscall"
|
|
||||||
"time"
|
|
||||||
|
|
||||||
"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 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()
|
|
||||||
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)
|
|
||||||
err := process.Signal(syscall.SIGHUP)
|
|
||||||
if err != nil {
|
|
||||||
logger.Error("send signal SIGHUP failed:", err)
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
@@ -1,63 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,103 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
@@ -1,53 +0,0 @@
|
|||||||
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)
|
|
||||||
}
|
|
||||||
@@ -1,90 +0,0 @@
|
|||||||
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
|
|
||||||
}
|
|
||||||
+27
-4
@@ -3,10 +3,14 @@ services:
|
|||||||
s-ui:
|
s-ui:
|
||||||
image: alireza7/s-ui
|
image: alireza7/s-ui
|
||||||
container_name: s-ui
|
container_name: s-ui
|
||||||
hostname: "s-ui"
|
hostname: "S-UI docker"
|
||||||
volumes:
|
volumes:
|
||||||
- "./db:/app/db"
|
- "singbox:/app/bin"
|
||||||
- "./cert:/app/cert"
|
- "$PWD/db:/app/db"
|
||||||
|
- "$PWD/cert:/app/cert"
|
||||||
|
environment:
|
||||||
|
SINGBOX_API: "sing-box:1080"
|
||||||
|
SUI_DB_FOLDER: "db"
|
||||||
tty: true
|
tty: true
|
||||||
restart: unless-stopped
|
restart: unless-stopped
|
||||||
ports:
|
ports:
|
||||||
@@ -14,9 +18,28 @@ services:
|
|||||||
- "2096:2096"
|
- "2096:2096"
|
||||||
networks:
|
networks:
|
||||||
- s-ui
|
- s-ui
|
||||||
entrypoint: "./entrypoint.sh"
|
entrypoint: "./sui"
|
||||||
|
|
||||||
|
sing-box:
|
||||||
|
image: alireza7/s-ui-singbox
|
||||||
|
container_name: sing-box
|
||||||
|
volumes:
|
||||||
|
- "singbox:/app/"
|
||||||
|
- "$PWD/cert:/cert"
|
||||||
|
networks:
|
||||||
|
- s-ui
|
||||||
|
ports:
|
||||||
|
- "443:443"
|
||||||
|
- "1443:1443"
|
||||||
|
- "2443:2443"
|
||||||
|
- "3443:3443"
|
||||||
|
restart: unless-stopped
|
||||||
|
depends_on:
|
||||||
|
- s-ui
|
||||||
|
|
||||||
networks:
|
networks:
|
||||||
s-ui:
|
s-ui:
|
||||||
driver: bridge
|
driver: bridge
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
singbox:
|
||||||
@@ -1,4 +0,0 @@
|
|||||||
#!/bin/sh
|
|
||||||
|
|
||||||
./sui migrate
|
|
||||||
./sui
|
|
||||||
-1
Submodule frontend deleted from e109f9f759
@@ -0,0 +1,4 @@
|
|||||||
|
> 1%
|
||||||
|
last 2 versions
|
||||||
|
not dead
|
||||||
|
not ie 11
|
||||||
@@ -0,0 +1,5 @@
|
|||||||
|
[*.{js,jsx,ts,tsx,vue}]
|
||||||
|
indent_style = space
|
||||||
|
indent_size = 2
|
||||||
|
trim_trailing_whitespace = true
|
||||||
|
insert_final_newline = true
|
||||||
@@ -0,0 +1,14 @@
|
|||||||
|
module.exports = {
|
||||||
|
root: true,
|
||||||
|
env: {
|
||||||
|
node: true,
|
||||||
|
},
|
||||||
|
extends: [
|
||||||
|
'plugin:vue/vue3-essential',
|
||||||
|
'eslint:recommended',
|
||||||
|
'@vue/eslint-config-typescript',
|
||||||
|
],
|
||||||
|
rules: {
|
||||||
|
'vue/multi-word-component-names': 'off',
|
||||||
|
},
|
||||||
|
}
|
||||||
@@ -0,0 +1,23 @@
|
|||||||
|
.DS_Store
|
||||||
|
node_modules
|
||||||
|
/dist
|
||||||
|
/bin
|
||||||
|
|
||||||
|
# local env files
|
||||||
|
.env.local
|
||||||
|
.env.*.local
|
||||||
|
|
||||||
|
# Log files
|
||||||
|
npm-debug.log*
|
||||||
|
yarn-debug.log*
|
||||||
|
yarn-error.log*
|
||||||
|
pnpm-debug.log*
|
||||||
|
|
||||||
|
# Editor directories and files
|
||||||
|
.idea
|
||||||
|
.vscode
|
||||||
|
*.suo
|
||||||
|
*.ntvs*
|
||||||
|
*.njsproj
|
||||||
|
*.sln
|
||||||
|
*.sw?
|
||||||
@@ -0,0 +1,69 @@
|
|||||||
|
# base
|
||||||
|
|
||||||
|
## Project setup
|
||||||
|
|
||||||
|
```
|
||||||
|
# yarn
|
||||||
|
yarn
|
||||||
|
|
||||||
|
# npm
|
||||||
|
npm install
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm install
|
||||||
|
|
||||||
|
# bun
|
||||||
|
bun install
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and hot-reloads for development
|
||||||
|
|
||||||
|
```
|
||||||
|
# yarn
|
||||||
|
yarn dev
|
||||||
|
|
||||||
|
# npm
|
||||||
|
npm run dev
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm dev
|
||||||
|
|
||||||
|
# bun
|
||||||
|
pnpm run dev
|
||||||
|
```
|
||||||
|
|
||||||
|
### Compiles and minifies for production
|
||||||
|
|
||||||
|
```
|
||||||
|
# yarn
|
||||||
|
yarn build
|
||||||
|
|
||||||
|
# npm
|
||||||
|
npm run build
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm build
|
||||||
|
|
||||||
|
# bun
|
||||||
|
pnpm run build
|
||||||
|
```
|
||||||
|
|
||||||
|
### Lints and fixes files
|
||||||
|
|
||||||
|
```
|
||||||
|
# yarn
|
||||||
|
yarn lint
|
||||||
|
|
||||||
|
# npm
|
||||||
|
npm run lint
|
||||||
|
|
||||||
|
# pnpm
|
||||||
|
pnpm lint
|
||||||
|
|
||||||
|
# bun
|
||||||
|
pnpm run lint
|
||||||
|
```
|
||||||
|
|
||||||
|
### Customize configuration
|
||||||
|
|
||||||
|
See [Configuration Reference](https://vitejs.dev/config/).
|
||||||
@@ -0,0 +1,22 @@
|
|||||||
|
<!DOCTYPE html>
|
||||||
|
<html lang="en">
|
||||||
|
|
||||||
|
<head>
|
||||||
|
<meta charset="UTF-8" />
|
||||||
|
<link rel="icon" href="assets/favicon.ico" />
|
||||||
|
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
|
||||||
|
<script>
|
||||||
|
window.BASE_URL = "{{ .BASE_URL }}"
|
||||||
|
|
||||||
|
// Dev Mode
|
||||||
|
if (window.BASE_URL.charAt(0) === '{') window.BASE_URL = "/app/"
|
||||||
|
</script>
|
||||||
|
<title>S-UI</title>
|
||||||
|
</head>
|
||||||
|
|
||||||
|
<body>
|
||||||
|
<div id="app"></div>
|
||||||
|
<script type="module" src="/src/main.ts"></script>
|
||||||
|
</body>
|
||||||
|
|
||||||
|
</html>
|
||||||
Generated
+3650
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,44 @@
|
|||||||
|
{
|
||||||
|
"name": "frontend",
|
||||||
|
"version": "0.0.0",
|
||||||
|
"private": true,
|
||||||
|
"scripts": {
|
||||||
|
"dev": "vite --host",
|
||||||
|
"build": "vue-tsc --noEmit && vite build",
|
||||||
|
"preview": "vite preview",
|
||||||
|
"lint": "eslint . --fix --ignore-path .gitignore"
|
||||||
|
},
|
||||||
|
"dependencies": {
|
||||||
|
"@mdi/font": "7.0.96",
|
||||||
|
"axios": "^1.7.2",
|
||||||
|
"chart.js": "^4.4.3",
|
||||||
|
"clipboard": "^2.0.11",
|
||||||
|
"core-js": "^3.37.1",
|
||||||
|
"moment": "^2.30.1",
|
||||||
|
"notivue": "^2.4.4",
|
||||||
|
"pinia": "^2.1.7",
|
||||||
|
"qrcode.vue": "^3.4.1",
|
||||||
|
"roboto-fontface": "^0.10.0",
|
||||||
|
"vue": "^3.2.0",
|
||||||
|
"vue-chartjs": "^5.3.1",
|
||||||
|
"vue-i18n": "^9.13.1",
|
||||||
|
"vue-router": "^4.3.2",
|
||||||
|
"vue3-persian-datetime-picker": "^1.2.2",
|
||||||
|
"vuetify": "^3.6.7"
|
||||||
|
},
|
||||||
|
"devDependencies": {
|
||||||
|
"@babel/types": "^7.24.5",
|
||||||
|
"@types/node": "^18.19.33",
|
||||||
|
"@vitejs/plugin-vue": "^4.6.2",
|
||||||
|
"@vue/eslint-config-typescript": "^11.0.3",
|
||||||
|
"eslint": "^8.57.0",
|
||||||
|
"eslint-plugin-vue": "^9.26.0",
|
||||||
|
"material-design-icons-iconfont": "^6.7.0",
|
||||||
|
"sass": "^1.77.2",
|
||||||
|
"typescript": "^5.4.5",
|
||||||
|
"unplugin-fonts": "^1.1.1",
|
||||||
|
"vite": "^4.5.3",
|
||||||
|
"vite-plugin-vuetify": "^1.0.2",
|
||||||
|
"vue-tsc": "^1.8.27"
|
||||||
|
}
|
||||||
|
}
|
||||||
Binary file not shown.
|
After Width: | Height: | Size: 1.5 KiB |
@@ -0,0 +1,34 @@
|
|||||||
|
<template>
|
||||||
|
<v-overlay
|
||||||
|
:model-value="loading"
|
||||||
|
persistent
|
||||||
|
content-class="text-center"
|
||||||
|
class="align-center justify-center"
|
||||||
|
>
|
||||||
|
<v-progress-circular
|
||||||
|
indeterminate
|
||||||
|
size="64"
|
||||||
|
></v-progress-circular>
|
||||||
|
<br />
|
||||||
|
{{ $t('loading') }}
|
||||||
|
</v-overlay>
|
||||||
|
<Message />
|
||||||
|
<router-view />
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import Message from '@/components/message.vue'
|
||||||
|
import { inject, ref, Ref } from 'vue'
|
||||||
|
|
||||||
|
const loading:Ref = inject('loading')?? ref(false)
|
||||||
|
|
||||||
|
// Change page title
|
||||||
|
document.title = "S-UI " + document.location.hostname
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.v-overlay .v-list-item,
|
||||||
|
.v-field__input {
|
||||||
|
direction: ltr;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
Binary file not shown.
Binary file not shown.
|
After Width: | Height: | Size: 763 B |
@@ -0,0 +1,24 @@
|
|||||||
|
<?xml version="1.0" encoding="utf-8"?>
|
||||||
|
<svg viewBox="1.019 0.0225 45.9789 46.9775" width="45.9789" height="46.9775" xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g featurekey="symbolFeature-0" transform="matrix(0.4545450210571289, 0, 0, 0.4545450210571289, 0.7917079329490662, 1.7009549140930176)" fill="#737373">
|
||||||
|
<g xmlns="http://www.w3.org/2000/svg">
|
||||||
|
<g>
|
||||||
|
<path d="M50,99.658L0.5,70.699V29.301L50,0.341l49.5,28.959v41.398L50,99.658z M2.5,69.553L50,97.342l47.5-27.789V30.448L50,2.659 L2.5,30.448V69.553z"/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<polygon points="51,98.376 49,98.376 49,58.822 0.995,30.738 2.005,29.011 50,57.091 97.995,29.011 99.005,30.738 51,58.822 "/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<polyline points="28.494,14.082 76.994,42.457 71.506,45.667 23.006,17.292 "/>
|
||||||
|
<polygon points="71.507,46.246 71.254,46.098 22.754,17.724 23.259,16.861 71.507,45.087 76.003,42.457 28.241,14.514 28.746,13.65 77.983,42.457 "/>
|
||||||
|
</g>
|
||||||
|
<g>
|
||||||
|
<polyline points="71.506,45.667 71.506,57.982 71.51,57.982 76.993,54.775 76.993,42.457 "/>
|
||||||
|
<polyline points="71.006,45.667 72.006,45.667 72.006,57.113 76.493,54.487 76.493,42.457 77.493,42.457 77.493,55.062 71.646,58.482 71,58.85 "/>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
</g>
|
||||||
|
<g featurekey="nameFeature-0" transform="matrix(1.6160469055175781, 0, 0, 1.6160469055175781, 3.2854819297790527, -21.369783401489258)" fill="#a6a6a6">
|
||||||
|
<path d="M10.904 40.4028 c-5.316 0 -9.2256 -3.048 -9.2256 -6.6192 c0 -1.8592 1.2372 -2.8152 2.5116 -2.8152 c1.0924 0 2.2288 0.7184 2.2288 2.1488 c0 1.4428 -1.4112 1.9972 -1.4112 2.9972 c0 1.782 2.974 3.0552 5.338 3.0552 c3.244 0 6.382 -1.5736 6.382 -5.102 c0 -6.2716 -14.403 -3.6536 -14.403 -13.229 c0 -5.0924 4.3436 -7.6012 9.9036 -7.6012 c4.7364 0 8.9656 2.6496 8.9656 6.1048 c0 1.946 -1.2372 2.9308 -2.4828 2.9308 c-1.1216 0 -2.258 -0.7476 -2.258 -2.178 c0 -1.5584 1.382 -1.8812 1.382 -2.8812 c0 -1.6952 -2.974 -2.7436 -5.3092 -2.7436 c-3.04 0 -5.9296 1.1848 -5.9296 4.6496 c0 5.866 15.193 3.7708 15.193 13.345 c0 4.0208 -3.8304 7.938 -10.886 7.938 z"/>
|
||||||
|
</g>
|
||||||
|
</svg>
|
||||||
|
After Width: | Height: | Size: 2.0 KiB |
@@ -0,0 +1,252 @@
|
|||||||
|
<template>
|
||||||
|
<v-card subtitle="ACME" style="background-color: inherit;">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="8" v-if="enabled">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('rule.domain') + ' ' + $t('commaSeparated')"
|
||||||
|
hide-details
|
||||||
|
v-model="domains">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<template v-if="enabled">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="optionDir">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('tls.acme.dataDir')"
|
||||||
|
hide-details
|
||||||
|
v-model="acme.data_directory">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="optionDefault">
|
||||||
|
<v-combobox
|
||||||
|
v-model="acme.default_server_name"
|
||||||
|
:items="acme.domain"
|
||||||
|
:label="$t('tls.acme.defaultDomain')"
|
||||||
|
hide-details
|
||||||
|
></v-combobox>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="optionEmail">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('email')"
|
||||||
|
hide-details
|
||||||
|
v-model="acme.email">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="optionChallenge">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch color="primary" :label="$t('tls.acme.httpChallenge')" v-model="acme.disable_http_challenge" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch color="primary" :label="$t('tls.acme.tlsChallenge')" v-model="acme.disable_tls_alpn_challenge" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="optionPorts">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('tls.acme.altHport')"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
min=1
|
||||||
|
max="65532"
|
||||||
|
v-model.number="acme.alternative_http_port">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('tls.acme.altTport')"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
min=1
|
||||||
|
max="65532"
|
||||||
|
v-model.number="acme.alternative_tls_port">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="optionProvider">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-select
|
||||||
|
v-model="caProvider"
|
||||||
|
:items="providerList"
|
||||||
|
:label="$t('tls.acme.caProvider')"
|
||||||
|
hide-details
|
||||||
|
></v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" md="8" v-if="caProvider == ''">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('tls.acme.customCa')"
|
||||||
|
hide-details
|
||||||
|
v-model="acme.provider">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="acme.external_account != undefined">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
label="Key ID"
|
||||||
|
hide-details
|
||||||
|
v-model="acme.external_account.key_id">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
label="MAC Key"
|
||||||
|
hide-details
|
||||||
|
v-model="acme.external_account.mac_key">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="acme.dns01_challenge != undefined">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-select
|
||||||
|
:label="$t('tls.acme.dns01Provider')"
|
||||||
|
hide-details
|
||||||
|
:items="dnsProviders.map(d => d.provider)"
|
||||||
|
@update:model-value="acme.dns01_challenge = { provider: $event }"
|
||||||
|
v-model="acme.dns01_challenge.provider">
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4"
|
||||||
|
v-for="item in dnsProviders.filter(d => d.provider == acme.dns01_challenge?.provider)[0]?.params"
|
||||||
|
:key="item">
|
||||||
|
<v-text-field
|
||||||
|
:label="item"
|
||||||
|
hide-details
|
||||||
|
v-model="acme.dns01_challenge[item]">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-card-actions>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" hide-details>{{ $t('tls.acme.options') }}</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-card>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionDir" color="primary" :label="$t('tls.acme.dataDir')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionDefault" color="primary" :label="$t('tls.acme.defaultDomain')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionEmail" color="primary" :label="$t('email')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionChallenge" color="primary" :label="$t('tls.acme.disableChallenges')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionPorts" color="primary" :label="$t('tls.acme.altPorts')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionProvider" color="primary" :label="$t('tls.acme.caProvider')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionExt" color="primary" :label="$t('tls.acme.extAcc')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionDns01" color="primary" :label="$t('tls.acme.dns01')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
|
</v-card-actions>
|
||||||
|
</template>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { acme } from '@/types/inTls'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['tls'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
menu: false,
|
||||||
|
providerList: [
|
||||||
|
{ title: "Let's Encrypt", value: "letsencrypt" },
|
||||||
|
{ title: "ZeroSSL", value: "zerossl" },
|
||||||
|
{ title: "Custom", value: "" }
|
||||||
|
],
|
||||||
|
dnsProviders: [
|
||||||
|
{ provider: "cloudflare", params: [ "api_token" ] },
|
||||||
|
{ provider: "alidns", params: [ "access_key_id","access_key_secret","region_id" ] }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
acme() {
|
||||||
|
return <acme>this.$props.tls.acme
|
||||||
|
},
|
||||||
|
enabled: {
|
||||||
|
get() { return this.acme != undefined },
|
||||||
|
set(v: boolean) { this.$props.tls.acme = v ? { domain: [] } : undefined }
|
||||||
|
},
|
||||||
|
domains: {
|
||||||
|
get() { return this.acme?.domain ? this.acme.domain.join(',') : "" },
|
||||||
|
set(v: string) {
|
||||||
|
if(!v.endsWith(',')) {
|
||||||
|
this.acme.domain = v.length > 0 ? v.split(',') : []
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
caProvider: {
|
||||||
|
get() { return this.acme?.provider && ['letsencrypt','zerossl'].includes(this.acme.provider) ? this.acme?.provider : '' },
|
||||||
|
set(v: string) { this.acme.provider = ['letsencrypt','zerossl'].includes(v) ? v : 'https://' }
|
||||||
|
},
|
||||||
|
optionDir: {
|
||||||
|
get(): boolean { return this.acme?.data_directory != undefined },
|
||||||
|
set(v:boolean) { this.acme.data_directory = v ? '' : undefined }
|
||||||
|
},
|
||||||
|
optionDefault: {
|
||||||
|
get(): boolean { return this.acme?.default_server_name != undefined },
|
||||||
|
set(v:boolean) { this.acme.default_server_name = v ? this.domains.length>0 ? this.domains[0] : '' : undefined }
|
||||||
|
},
|
||||||
|
optionEmail: {
|
||||||
|
get(): boolean { return this.acme?.email != undefined },
|
||||||
|
set(v:boolean) { this.acme.email = v ? '' : undefined }
|
||||||
|
},
|
||||||
|
optionChallenge: {
|
||||||
|
get(): boolean { return this.acme?.disable_http_challenge != undefined || this.acme?.disable_tls_alpn_challenge != undefined },
|
||||||
|
set(v:boolean) {
|
||||||
|
if (v) {
|
||||||
|
this.acme.disable_http_challenge = false
|
||||||
|
this.acme.disable_tls_alpn_challenge = false
|
||||||
|
} else {
|
||||||
|
delete this.acme.disable_http_challenge
|
||||||
|
delete this.acme.disable_tls_alpn_challenge
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optionPorts: {
|
||||||
|
get(): boolean { return this.acme?.alternative_http_port != undefined || this.acme?.alternative_tls_port != undefined },
|
||||||
|
set(v:boolean) {
|
||||||
|
if (v) {
|
||||||
|
this.acme.alternative_http_port = 80
|
||||||
|
this.acme.alternative_tls_port = 443
|
||||||
|
} else {
|
||||||
|
delete this.acme.alternative_http_port
|
||||||
|
delete this.acme.alternative_tls_port
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optionProvider: {
|
||||||
|
get(): boolean { return this.acme?.provider != undefined },
|
||||||
|
set(v:boolean) { this.acme.provider = v ? 'letsencrypt' : undefined }
|
||||||
|
},
|
||||||
|
optionExt: {
|
||||||
|
get(): boolean { return this.acme?.external_account != undefined },
|
||||||
|
set(v:boolean) { this.acme.external_account = v ? { key_id: '', mac_key: '' } : undefined }
|
||||||
|
},
|
||||||
|
optionDns01: {
|
||||||
|
get(): boolean { return this.acme?.dns01_challenge != undefined },
|
||||||
|
set(v:boolean) { this.acme.dns01_challenge = v ? { provider: 'cloudflare' } : undefined }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,138 @@
|
|||||||
|
<template>
|
||||||
|
<v-text-field
|
||||||
|
id="expiry"
|
||||||
|
:label="$t('date.expiry')"
|
||||||
|
v-model="dateFormatted"
|
||||||
|
prepend-inner-icon="mdi-calendar"
|
||||||
|
readonly
|
||||||
|
hide-details
|
||||||
|
></v-text-field>
|
||||||
|
<DatePicker
|
||||||
|
v-model="Input"
|
||||||
|
@input="Input=$event"
|
||||||
|
:locale="locale"
|
||||||
|
element="expiry"
|
||||||
|
compact-time
|
||||||
|
type="datetime">
|
||||||
|
<template v-slot:next-month>
|
||||||
|
<v-icon icon="mdi-chevron-right" />
|
||||||
|
</template>
|
||||||
|
<template v-slot:prev-month>
|
||||||
|
<v-icon icon="mdi-chevron-left" />
|
||||||
|
</template>
|
||||||
|
<template #submit-btn="{ submit, canSubmit }">
|
||||||
|
<v-btn
|
||||||
|
:disabled="!canSubmit"
|
||||||
|
@click="submit"
|
||||||
|
>{{ $t('submit') }}</v-btn>
|
||||||
|
</template>
|
||||||
|
<template #cancel-btn="{ vm }">
|
||||||
|
<v-btn
|
||||||
|
@click="reset(vm)"
|
||||||
|
>{{ $t('reset') }}</v-btn>
|
||||||
|
</template>
|
||||||
|
<template #now-btn="{ goToday }">
|
||||||
|
<v-btn
|
||||||
|
@click="goToday"
|
||||||
|
>{{ $t('now') }}</v-btn>
|
||||||
|
</template>
|
||||||
|
</DatePicker>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import DatePicker from 'vue3-persian-datetime-picker'
|
||||||
|
import { i18n } from '@/locales'
|
||||||
|
import 'moment/locale/zh-cn'
|
||||||
|
import 'moment/locale/zh-tw'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['expiry'],
|
||||||
|
emits: ['submit'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
menu: false,
|
||||||
|
input: new Date(),
|
||||||
|
}
|
||||||
|
},
|
||||||
|
components: { DatePicker },
|
||||||
|
computed: {
|
||||||
|
locale() {
|
||||||
|
const l = i18n.global.locale.value
|
||||||
|
return l.replace('zh', 'zh-')
|
||||||
|
},
|
||||||
|
dateFormatted() {
|
||||||
|
if (this.expDate == 0) return i18n.global.t('unlimited')
|
||||||
|
const date = new Date(this.expDate*1000)
|
||||||
|
return date.toLocaleString(this.locale)
|
||||||
|
},
|
||||||
|
expDate() {
|
||||||
|
return parseInt(this.expiry?? 0)
|
||||||
|
},
|
||||||
|
Input: {
|
||||||
|
get() { return this.expDate == 0 ? new Date() : new Date(this.expDate*1000) },
|
||||||
|
set(v:string) {
|
||||||
|
this.input = new Date(v)
|
||||||
|
this.submit()
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
updateInput(v:Date) {
|
||||||
|
this.input = v
|
||||||
|
},
|
||||||
|
setNow() {
|
||||||
|
this.input = new Date()
|
||||||
|
},
|
||||||
|
submit() {
|
||||||
|
this.$emit('submit',Math.floor(this.input.getTime()/1000))
|
||||||
|
},
|
||||||
|
reset(vm:any) {
|
||||||
|
this.$emit('submit',0)
|
||||||
|
this.input = new Date()
|
||||||
|
vm.visible = false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
watch: {
|
||||||
|
menu(v) {
|
||||||
|
if (v) {
|
||||||
|
this.input = this.expiry == 0 ? new Date() : new Date(this.expDate*1000)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
</script>
|
||||||
|
|
||||||
|
<style>
|
||||||
|
.vpd-addon-list,
|
||||||
|
.vpd-addon-list-item {
|
||||||
|
background-color: rgb(var(--v-theme-background)) !important;
|
||||||
|
border-color: rgb(var(--v-theme-background)) !important;
|
||||||
|
}
|
||||||
|
.vpd-content {
|
||||||
|
background-color: rgb(var(--v-theme-background)) !important;
|
||||||
|
}
|
||||||
|
.vpd-addon-list-item.vpd-selected,
|
||||||
|
.vpd-addon-list-item:hover {
|
||||||
|
background-color: rgb(var(--v-theme-primary)) !important;
|
||||||
|
}
|
||||||
|
.vpd-close-addon {
|
||||||
|
color: rgb(var(--v-theme-on-surface)) !important;
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.vpd-controls {
|
||||||
|
overflow-x: hidden;
|
||||||
|
}
|
||||||
|
.vpd-month-label {
|
||||||
|
width: auto;
|
||||||
|
}
|
||||||
|
.vpd-actions button:hover {
|
||||||
|
background-color: transparent;
|
||||||
|
}
|
||||||
|
.vpd-wrapper[data-type=datetime].vpd-compact-time .vpd-time {
|
||||||
|
border-top: 0;
|
||||||
|
}
|
||||||
|
.vpd-time .vpd-time-h .vpd-counter-item,
|
||||||
|
.vpd-time .vpd-time-m .vpd-counter-item {
|
||||||
|
vertical-align: top;
|
||||||
|
}
|
||||||
|
</style>
|
||||||
@@ -0,0 +1,215 @@
|
|||||||
|
<template>
|
||||||
|
<v-card :subtitle="$t('objects.dial')" style="background-color: inherit;">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="optionDetour">
|
||||||
|
<v-select
|
||||||
|
hide-details
|
||||||
|
:label="$t('dial.detourText')"
|
||||||
|
:items="outTags"
|
||||||
|
v-model="dial.detour">
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="optionBind">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('dial.bindIf')"
|
||||||
|
hide-details
|
||||||
|
v-model="dial.bind_interface"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="optionIPV4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('dial.bindIp4')"
|
||||||
|
hide-details
|
||||||
|
v-model="dial.inet4_bind_address"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="optionIPV6">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('dial.bindIp6')"
|
||||||
|
hide-details
|
||||||
|
v-model="dial.inet6_bind_address"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="optionRM">
|
||||||
|
<v-text-field
|
||||||
|
label="Linux Routing Mark"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
min="0"
|
||||||
|
v-model.number="routingMark"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="optionRA">
|
||||||
|
<v-switch v-model="dial.reuse_addr" color="primary" :label="$t('dial.reuseAddr')" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="optionTCP">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch v-model="dial.tcp_fast_open" color="primary" label="TCP Fast Open" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch v-model="dial.tcp_multi_path" color="primary" label="TCP Multi Path" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="optionUDP">
|
||||||
|
<v-switch v-model="dial.udp_fragment" color="primary" label="UDP Fragment" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="optionCT">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('dial.connTimeout')"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
:suffix="$t('date.s')"
|
||||||
|
v-model.number="connectTimeout"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="optionDS">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-select
|
||||||
|
hide-details
|
||||||
|
:label="$t('listen.domainStrategy')"
|
||||||
|
:items="['prefer_ipv4','prefer_ipv6','ipv4_only','ipv6_only']"
|
||||||
|
v-model="dial.domain_strategy">
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('dial.fbTimeout')"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
min="50"
|
||||||
|
step="50"
|
||||||
|
:suffix="$t('date.ms')"
|
||||||
|
v-model.number="fallbackDelay"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-card-actions class="pt-0">
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" hide-details>{{ $t('dial.options') }}</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-card>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionDetour" color="primary" :label="$t('listen.detour')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionBind" color="primary" :label="$t('dial.bindIf')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionIPV4" color="primary" :label="$t('dial.bindIp4')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionIPV6" color="primary" :label="$t('dial.bindIp6')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionRM" color="primary" label="Routing Mark" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionRA" color="primary" :label="$t('dial.reuseAddr')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionTCP" color="primary" :label="$t('listen.tcpOptions')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionUDP" color="primary" :label="$t('listen.udpOptions')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionCT" color="primary" :label="$t('dial.connTimeout')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionDS" color="primary" :label="$t('listen.domainStrategy')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
props: ['dial', 'outTags'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
menu: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
fallbackDelay: {
|
||||||
|
get() { return this.$props.dial.fallback_delay ? parseInt(this.$props.dial.fallback_delay.replace('ms','')) : 300 },
|
||||||
|
set(newValue:number) { this.$props.dial.fallback_delay = newValue > 0 ? newValue + 'ms' : '300ms' }
|
||||||
|
},
|
||||||
|
connectTimeout: {
|
||||||
|
get() { return this.$props.dial.connect_timeout ? parseInt(this.$props.dial.connect_timeout.replace('s','')) : 5 },
|
||||||
|
set(newValue:number) { this.$props.dial.connect_timeout = newValue > 0 ? newValue + 's' : '5s' }
|
||||||
|
},
|
||||||
|
routingMark: {
|
||||||
|
get() { return this.$props.dial.routing_mark?? 0 },
|
||||||
|
set(newValue:number) { this.$props.dial.routing_mark = newValue > 0 ? newValue : 0 }
|
||||||
|
},
|
||||||
|
optionDetour: {
|
||||||
|
get(): boolean { return this.$props.dial.detour != undefined },
|
||||||
|
set(v:boolean) { v ? this.$props.dial.detour = this.outTags[0]?? '' : delete this.$props.dial.detour }
|
||||||
|
},
|
||||||
|
optionBind: {
|
||||||
|
get(): boolean { return this.$props.dial.bind_interface != undefined },
|
||||||
|
set(v:boolean) { v ? this.$props.dial.bind_interface = '' : delete this.$props.dial.bind_interface }
|
||||||
|
},
|
||||||
|
optionIPV4: {
|
||||||
|
get(): boolean { return this.$props.dial.inet4_bind_address != undefined },
|
||||||
|
set(v:boolean) { v ? this.$props.dial.inet4_bind_address = '' : delete this.$props.dial.inet4_bind_address }
|
||||||
|
},
|
||||||
|
optionIPV6: {
|
||||||
|
get(): boolean { return this.$props.dial.inet6_bind_address != undefined },
|
||||||
|
set(v:boolean) { v ? this.$props.dial.inet6_bind_address = '' : delete this.$props.dial.inet6_bind_address }
|
||||||
|
},
|
||||||
|
optionRM: {
|
||||||
|
get(): boolean { return this.$props.dial.routing_mark != undefined },
|
||||||
|
set(v:boolean) { v ? this.$props.dial.routing_mark = 0 : delete this.$props.dial.routing_mark }
|
||||||
|
},
|
||||||
|
optionRA: {
|
||||||
|
get(): boolean { return this.$props.dial.reuse_addr != undefined },
|
||||||
|
set(v:boolean) { v ? this.$props.dial.reuse_addr = true : delete this.$props.dial.reuse_addr }
|
||||||
|
},
|
||||||
|
optionTCP: {
|
||||||
|
get(): boolean {
|
||||||
|
return this.$props.dial.tcp_fast_open != undefined &&
|
||||||
|
this.$props.dial.tcp_multi_path != undefined
|
||||||
|
},
|
||||||
|
set(v:boolean) {
|
||||||
|
if (v) {
|
||||||
|
this.$props.dial.tcp_fast_open = false
|
||||||
|
this.$props.dial.tcp_multi_path = false
|
||||||
|
} else {
|
||||||
|
delete this.$props.dial.tcp_fast_open
|
||||||
|
delete this.$props.dial.tcp_multi_path
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optionUDP: {
|
||||||
|
get(): boolean { return this.$props.dial.udp_fragment != undefined },
|
||||||
|
set(v:boolean) { v ? this.$props.dial.udp_fragment = true : delete this.$props.dial.udp_fragment }
|
||||||
|
},
|
||||||
|
optionCT: {
|
||||||
|
get(): boolean { return this.$props.dial.connect_timeout != undefined },
|
||||||
|
set(v:boolean) { v ? this.$props.dial.connect_timeout = '5s' : delete this.$props.dial.connect_timeout }
|
||||||
|
},
|
||||||
|
optionDS: {
|
||||||
|
get(): boolean { return this.$props.dial.domain_strategy != undefined },
|
||||||
|
set(v:boolean) {
|
||||||
|
if (v) {
|
||||||
|
this.$props.dial.domain_strategy = 'prefer_ipv4'
|
||||||
|
this.$props.dial.fallback_delay = '300ms'
|
||||||
|
} else {
|
||||||
|
delete this.$props.dial.domain_strategy
|
||||||
|
delete this.$props.dial.fallback_delay
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,96 @@
|
|||||||
|
<template>
|
||||||
|
<v-card subtitle="ECH" style="background-color: inherit;">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<template v-if="enabled">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch color="primary" label="Post-Quantum Schemes" v-model="ech.pq_signature_schemes_enabled" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch color="primary" label="Disable Adaptive Size" v-model="ech.dynamic_record_sizing_disabled" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn-toggle v-model="useEchPath"
|
||||||
|
class="rounded-xl"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
shaped
|
||||||
|
mandatory>
|
||||||
|
<v-btn
|
||||||
|
@click="delete ech.key"
|
||||||
|
>{{ $t('tls.usePath') }}</v-btn>
|
||||||
|
<v-btn
|
||||||
|
@click="delete ech.key_path"
|
||||||
|
>{{ $t('tls.useText') }}</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="useEchPath == 0">
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('tls.keyPath')"
|
||||||
|
hide-details
|
||||||
|
v-model="ech.key_path">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-else>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-textarea
|
||||||
|
:label="$t('tls.key')"
|
||||||
|
hide-details
|
||||||
|
v-model="echKeyText">
|
||||||
|
</v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-textarea
|
||||||
|
:label="$t('tls.cert')"
|
||||||
|
hide-details
|
||||||
|
v-model="echConfigText">
|
||||||
|
</v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { ech } from '@/types/inTls'
|
||||||
|
|
||||||
|
export default {
|
||||||
|
props: ['iTls','oTls'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
useEchPath: 0
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
ech() {
|
||||||
|
return <ech>this.$props.iTls.ech
|
||||||
|
},
|
||||||
|
enabled: {
|
||||||
|
get() { return this.ech?.enabled?? false },
|
||||||
|
set(v: boolean) {
|
||||||
|
this.$props.iTls.ech = v ? { enabled: true } : undefined
|
||||||
|
this.$props.oTls.ech = v ? {} : undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
echKeyText: {
|
||||||
|
get(): string { return this.ech?.key ? this.ech.key.join('\n') : '' },
|
||||||
|
set(newValue:string) { this.ech.key = newValue.split('\n') }
|
||||||
|
},
|
||||||
|
echConfigText: {
|
||||||
|
get(): string { return this.oTls.ech?.config ? this.oTls.ech.config.join('\n') : '' },
|
||||||
|
set(newValue:string) { this.oTls.ech.config = newValue.split('\n') }
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,99 @@
|
|||||||
|
<template>
|
||||||
|
<v-card>
|
||||||
|
<v-card-subtitle>
|
||||||
|
{{ $t('objects.headers') }}
|
||||||
|
<v-icon @click="add_header" icon="mdi-plus"></v-icon>
|
||||||
|
</v-card-subtitle>
|
||||||
|
<v-row v-for="(header, index) in hdrs">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('objects.key')"
|
||||||
|
hide-details
|
||||||
|
@input="update_key(index,$event.target.value)"
|
||||||
|
v-model="header.name">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('objects.value')"
|
||||||
|
hide-details
|
||||||
|
@input="update_value(index,$event.target.value)"
|
||||||
|
append-icon="mdi-delete"
|
||||||
|
@click:append="del_header(index)"
|
||||||
|
v-model="header.value">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
|
||||||
|
type Header = {
|
||||||
|
name: string
|
||||||
|
value: string
|
||||||
|
}
|
||||||
|
export default {
|
||||||
|
props: ['data'],
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
add_header() {
|
||||||
|
this.hdrs = [...this.hdrs, {name: "Host", value: ""}]
|
||||||
|
},
|
||||||
|
del_header(i:number) {
|
||||||
|
let h = this.hdrs
|
||||||
|
h.splice(i,1)
|
||||||
|
this.hdrs = h
|
||||||
|
},
|
||||||
|
update_key(i:number,k:string) {
|
||||||
|
let h = this.hdrs
|
||||||
|
h[i].name = k
|
||||||
|
this.hdrs = h
|
||||||
|
},
|
||||||
|
update_value(i:number,v:string) {
|
||||||
|
let h = this.hdrs
|
||||||
|
h[i].value = v
|
||||||
|
this.hdrs = h
|
||||||
|
},
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
hdrs: {
|
||||||
|
get() :Header[] {
|
||||||
|
let headers: Header[] = []
|
||||||
|
const h = this.$props.data.headers
|
||||||
|
if (h) {
|
||||||
|
Object.keys(h).forEach(key => {
|
||||||
|
if (Array.isArray(h[key])){
|
||||||
|
h[key].forEach((v:string) => headers.push({ name: key, value: v }))
|
||||||
|
} else {
|
||||||
|
headers.push({ name: key, value: h[key] })
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
return headers
|
||||||
|
},
|
||||||
|
set(v:Header[]) {
|
||||||
|
if (v.length>0) {
|
||||||
|
let headers:any = {}
|
||||||
|
v.forEach((h:Header) => {
|
||||||
|
if (headers[h.name]) {
|
||||||
|
if (Array.isArray(headers[h.name])) {
|
||||||
|
headers[h.name].push(h.value)
|
||||||
|
} else {
|
||||||
|
headers[h.name] = [headers[h.name], h.value]
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
headers[h.name] = h.value
|
||||||
|
}
|
||||||
|
})
|
||||||
|
this.$props.data.headers = headers
|
||||||
|
} else {
|
||||||
|
this.$props.data.headers = undefined
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,239 @@
|
|||||||
|
<template>
|
||||||
|
<v-card :subtitle="$t('objects.tls')">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="tlsOptional">
|
||||||
|
<v-switch color="primary" :label="$t('tls.enable')" v-model="tlsEnable" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="tls.enabled">
|
||||||
|
<v-select
|
||||||
|
hide-details
|
||||||
|
label="Preset"
|
||||||
|
:items="tlsItems"
|
||||||
|
@update:model-value="changeTlsItem($event)"
|
||||||
|
v-model="tlsId">
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<template v-if="tls.enabled && tlsId == 0">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-btn-toggle v-model="usePath"
|
||||||
|
class="rounded-xl"
|
||||||
|
density="compact"
|
||||||
|
variant="outlined"
|
||||||
|
shaped
|
||||||
|
mandatory>
|
||||||
|
<v-btn
|
||||||
|
@click="tls.key=undefined; tls.certificate=undefined"
|
||||||
|
>{{ $t('tls.usePath') }}</v-btn>
|
||||||
|
<v-btn
|
||||||
|
@click="tls.key_path=undefined; tls.certificate_path=undefined"
|
||||||
|
>{{ $t('tls.useText') }}</v-btn>
|
||||||
|
</v-btn-toggle>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="usePath == 0">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('tls.certPath')"
|
||||||
|
hide-details
|
||||||
|
v-model="tls.certificate_path">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('tls.keyPath')"
|
||||||
|
hide-details
|
||||||
|
v-model="tls.key_path">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-else>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-textarea
|
||||||
|
:label="$t('tls.cert')"
|
||||||
|
hide-details
|
||||||
|
v-model="certText">
|
||||||
|
</v-textarea>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6">
|
||||||
|
<v-textarea
|
||||||
|
:label="$t('tls.key')"
|
||||||
|
hide-details
|
||||||
|
v-model="keyText">
|
||||||
|
</v-textarea>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="tls.server_name != undefined">
|
||||||
|
<v-text-field
|
||||||
|
label="SNI"
|
||||||
|
hide-details
|
||||||
|
v-model="tls.server_name">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="tls.alpn">
|
||||||
|
<v-select
|
||||||
|
hide-details
|
||||||
|
label="ALPN"
|
||||||
|
multiple
|
||||||
|
:items="alpn"
|
||||||
|
v-model="tls.alpn">
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="tls.min_version">
|
||||||
|
<v-select
|
||||||
|
hide-details
|
||||||
|
:label="$t('tls.minVer')"
|
||||||
|
:items="tlsVersions"
|
||||||
|
v-model="tls.min_version">
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="tls.max_version">
|
||||||
|
<v-select
|
||||||
|
hide-details
|
||||||
|
:label="$t('tls.maxVer')"
|
||||||
|
:items="tlsVersions"
|
||||||
|
v-model="tls.max_version">
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" md="8" v-if="tls.cipher_suites != undefined">
|
||||||
|
<v-select
|
||||||
|
hide-details
|
||||||
|
:label="$t('tls.cs')"
|
||||||
|
multiple
|
||||||
|
:items="cipher_suites"
|
||||||
|
v-model="tls.cipher_suites">
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
<v-card-actions v-if="tls.enabled && tlsId == 0">
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-menu v-model="menu" :close-on-content-click="false" location="start" v-if="tls.enabled">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" hide-details>{{ $t('tls.options') }}</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-card>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionSNI" color="primary" label="SNI" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionALPN" color="primary" label="ALPN" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionMinV" color="primary" :label="$t('tls.minVer')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionMaxV" color="primary" :label="$t('tls.maxVer')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionCS" color="primary" :label="$t('tls.cs')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { iTls, defaultInTls } from '@/types/inTls'
|
||||||
|
export default {
|
||||||
|
props: ['inbound', 'tlsConfigs', 'tls_id'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
menu: false,
|
||||||
|
usePath: this.$props.inbound.tls.key == undefined ? 0 : 1,
|
||||||
|
defaults: defaultInTls,
|
||||||
|
alpn: [
|
||||||
|
{ title: "H3", value: 'h3' },
|
||||||
|
{ title: "H2", value: 'h2' },
|
||||||
|
{ title: "Http/1.1", value: 'http/1.1' },
|
||||||
|
],
|
||||||
|
tlsVersions: [ '1.0', '1.1', '1.2', '1.3' ],
|
||||||
|
cipher_suites: [
|
||||||
|
{ title: "RSA-AES128-CBC-SHA", value: "TLS_RSA_WITH_AES_128_CBC_SHA" },
|
||||||
|
{ title: "RSA-AES256-CBC-SHA", value: "TLS_RSA_WITH_AES_256_CBC_SHA" },
|
||||||
|
{ title: "RSA-AES128-GCM-SHA256", value: "TLS_RSA_WITH_AES_128_GCM_SHA256" },
|
||||||
|
{ title: "RSA-AES256-GCM-SHA384", value: "TLS_RSA_WITH_AES_256_GCM_SHA384" },
|
||||||
|
{ title: "AES128-GCM-SHA256", value: "TLS_AES_128_GCM_SHA256" },
|
||||||
|
{ title: "AES256-GCM-SHA384", value: "TLS_AES_256_GCM_SHA384" },
|
||||||
|
{ title: "CHACHA20-POLY1305-SHA256", value: "TLS_CHACHA20_POLY1305_SHA256" },
|
||||||
|
{ title: "ECDHE-ECDSA-AES128-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" },
|
||||||
|
{ title: "ECDHE-ECDSA-AES256-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" },
|
||||||
|
{ title: "ECDHE-RSA-AES128-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" },
|
||||||
|
{ title: "ECDHE-RSA-AES256-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" },
|
||||||
|
{ title: "ECDHE-ECDSA-AES128-GCM-SHA256", value: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" },
|
||||||
|
{ title: "ECDHE-ECDSA-AES256-GCM-SHA384", value: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" },
|
||||||
|
{ title: "ECDHE-RSA-AES128-GCM-SHA256", value: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" },
|
||||||
|
{ title: "ECDHE-RSA-AES256-GCM-SHA384", value: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" },
|
||||||
|
{ title: "ECDHE-ECDSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" },
|
||||||
|
{ title: "ECDHE-RSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" }
|
||||||
|
]
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
tls(): iTls {
|
||||||
|
return <iTls> this.$props.inbound.tls
|
||||||
|
},
|
||||||
|
tlsItems(): any[] {
|
||||||
|
return [ { title: '', value: 0 }, ...this.$props.tlsConfigs?.map((t:any) => { return { title: t.name, value: t.id } } )]
|
||||||
|
},
|
||||||
|
tlsId: {
|
||||||
|
get() { return this.tls_id.value?? 0 },
|
||||||
|
set(newValue: boolean) { this.$props.tls_id.value = newValue }
|
||||||
|
},
|
||||||
|
tlsEnable: {
|
||||||
|
get() { return this.tls.enabled?? false },
|
||||||
|
set(newValue: boolean) { this.$props.inbound.tls = newValue ? { enabled: true } : {} }
|
||||||
|
},
|
||||||
|
tlsOptional(): boolean {
|
||||||
|
return !['hysteria','hysteria2','tuic','naive'].includes(this.$props.inbound.type)
|
||||||
|
},
|
||||||
|
certText: {
|
||||||
|
get(): string { return this.tls.certificate ? this.tls.certificate.join('\n') : '' },
|
||||||
|
set(newValue:string) { this.tls.certificate = newValue.split('\n') }
|
||||||
|
},
|
||||||
|
keyText: {
|
||||||
|
get(): string { return this.tls.key ? this.tls.key.join('\n') : '' },
|
||||||
|
set(newValue:string) { this.tls.key = newValue.split('\n') }
|
||||||
|
},
|
||||||
|
optionSNI: {
|
||||||
|
get(): boolean { return this.tls.server_name != undefined },
|
||||||
|
set(v:boolean) { this.tls.server_name = v ? '' : undefined }
|
||||||
|
},
|
||||||
|
optionALPN: {
|
||||||
|
get(): boolean { return this.tls.alpn != undefined },
|
||||||
|
set(v:boolean) { this.tls.alpn = v ? defaultInTls.alpn : undefined }
|
||||||
|
},
|
||||||
|
optionMinV: {
|
||||||
|
get(): boolean { return this.tls.min_version != undefined },
|
||||||
|
set(v:boolean) { this.tls.min_version = v ? defaultInTls.min_version : undefined }
|
||||||
|
},
|
||||||
|
optionMaxV: {
|
||||||
|
get(): boolean { return this.tls.max_version != undefined },
|
||||||
|
set(v:boolean) { this.tls.max_version = v ? defaultInTls.max_version : undefined }
|
||||||
|
},
|
||||||
|
optionCS: {
|
||||||
|
get(): boolean { return this.tls.cipher_suites != undefined },
|
||||||
|
set(v:boolean) { this.tls.cipher_suites = v ? defaultInTls.cipher_suites : undefined }
|
||||||
|
}
|
||||||
|
},
|
||||||
|
methods: {
|
||||||
|
changeTlsItem(id: number){
|
||||||
|
if (id>0) {
|
||||||
|
const tlsConfig = this.$props.tlsConfigs?.findLast((t:any) => t.id == id)
|
||||||
|
if (tlsConfig) this.$props.inbound.tls = tlsConfig.server
|
||||||
|
} else {
|
||||||
|
this.$props.inbound.tls = { enabled: this.tls.enabled }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,155 @@
|
|||||||
|
<template>
|
||||||
|
<v-card :subtitle="$t('objects.listen')">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('in.addr')"
|
||||||
|
hide-details
|
||||||
|
required
|
||||||
|
v-model="inbound.listen">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('in.port')"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
required
|
||||||
|
v-model.number="inbound.listen_port"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-if="optionDetour">
|
||||||
|
<v-select
|
||||||
|
:label="$t('listen.detourText')"
|
||||||
|
hide-details
|
||||||
|
:items="inTags"
|
||||||
|
v-model="inbound.detour">
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch v-model="inbound.sniff" color="primary" :label="$t('listen.sniffing')" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="inbound.sniff">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch v-model="inbound.sniff_override_destination" color="primary" :label="$t('listen.sniffingOverride')" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('listen.sniffingTimeout')"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
min="50"
|
||||||
|
step="50"
|
||||||
|
:suffix="$t('date.ms')"
|
||||||
|
v-model.number="sniffTimeout"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="optionTCP">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch v-model="inbound.tcp_fast_open" color="primary" label="TCP Fast Open" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch v-model="inbound.tcp_multi_path" color="primary" label="TCP Multi Path" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="optionUDP">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch v-model="inbound.udp_fragment" color="primary" label="UDP Fragment" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
label="UDP NAT expiration"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
min="1"
|
||||||
|
:suffix="$t('date.m')"
|
||||||
|
v-model.number="udpTimeout"></v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="optionDS">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-select
|
||||||
|
hide-details
|
||||||
|
:label="$t('listen.domainStrategy')"
|
||||||
|
:items="['prefer_ipv4','prefer_ipv6','ipv4_only','ipv6_only']"
|
||||||
|
v-model="inbound.domain_strategy">
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-card-actions class="pt-0">
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-menu v-model="menu" :close-on-content-click="false" location="start">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" hide-details>{{ $t('listen.options') }}</v-btn>
|
||||||
|
</template>
|
||||||
|
<v-card>
|
||||||
|
<v-list>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionDetour" color="primary" :label="$t('listen.detour')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionTCP" color="primary" :label="$t('listen.tcpOptions')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionUDP" color="primary" :label="$t('listen.udpOptions')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch v-model="optionDS" color="primary" :label="$t('listen.domainStrategy')" hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-menu>
|
||||||
|
</v-card-actions>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
export default {
|
||||||
|
props: ['inbound', 'inTags'],
|
||||||
|
data() {
|
||||||
|
return {
|
||||||
|
menu: false
|
||||||
|
}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
udpTimeout: {
|
||||||
|
get() { return this.$props.inbound.udp_timeout ? parseInt(this.$props.inbound.udp_timeout.replace('m','')) : 5 },
|
||||||
|
set(newValue:number) { this.$props.inbound.udp_timeout = newValue > 0 ? newValue + 'm' : '5m' }
|
||||||
|
},
|
||||||
|
sniffTimeout: {
|
||||||
|
get() { return this.$props.inbound.sniff_timeout ? parseInt(this.$props.inbound.sniff_timeout.replace('ms','')) : 300 },
|
||||||
|
set(newValue:number) { this.$props.inbound.sniff_timeout = newValue > 0 ? newValue + 'ms' : '300ms' }
|
||||||
|
},
|
||||||
|
optionTCP: {
|
||||||
|
get(): boolean {
|
||||||
|
return this.$props.inbound.tcp_fast_open != undefined &&
|
||||||
|
this.$props.inbound.tcp_multi_path != undefined
|
||||||
|
},
|
||||||
|
set(v:boolean) {
|
||||||
|
this.$props.inbound.tcp_fast_open = v ? false : undefined
|
||||||
|
this.$props.inbound.tcp_multi_path = v ? false : undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optionUDP: {
|
||||||
|
get(): boolean {
|
||||||
|
return this.$props.inbound.udp_fragment != undefined &&
|
||||||
|
this.$props.inbound.udp_timeout != undefined
|
||||||
|
},
|
||||||
|
set(v:boolean) {
|
||||||
|
this.$props.inbound.udp_fragment = v ? false : undefined
|
||||||
|
this.$props.inbound.udp_timeout = v ? '5m' : undefined
|
||||||
|
}
|
||||||
|
},
|
||||||
|
optionDetour: {
|
||||||
|
get(): boolean { return this.$props.inbound.detour != undefined },
|
||||||
|
set(v:boolean) { this.$props.inbound.detour = v ? this.inTags[0]?? '' : undefined }
|
||||||
|
},
|
||||||
|
optionDS: {
|
||||||
|
get(): boolean { return this.$props.inbound.domain_strategy != undefined },
|
||||||
|
set(v:boolean) { this.$props.inbound.domain_strategy = v ? 'prefer_ipv4' : undefined }
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,252 @@
|
|||||||
|
<template>
|
||||||
|
<LogVue
|
||||||
|
v-model="logModal.visible"
|
||||||
|
:visible="logModal.visible"
|
||||||
|
:logType="logModal.logType"
|
||||||
|
@close="closeLogs"
|
||||||
|
/>
|
||||||
|
<v-container class="fill-height">
|
||||||
|
<v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" >
|
||||||
|
<v-row class="d-flex align-center justify-center">
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-img src="@/assets/logo.svg" :width="reloadItems.length>0 ? 100 : 200"></v-img>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row class="d-flex align-center justify-center">
|
||||||
|
<v-col cols="auto">
|
||||||
|
<v-dialog v-model="menu" :close-on-content-click="false" transition="scale-transition" max-width="800">
|
||||||
|
<template v-slot:activator="{ props }">
|
||||||
|
<v-btn v-bind="props" variant="tonal">{{ $t('main.tiles') }} <v-icon icon="mdi-star-plus" /></v-btn>
|
||||||
|
</template>
|
||||||
|
<v-card rounded="xl">
|
||||||
|
<v-card-title>
|
||||||
|
<v-row>
|
||||||
|
<v-col>
|
||||||
|
{{ $t('main.tiles') }}
|
||||||
|
</v-col>
|
||||||
|
<v-spacer></v-spacer>
|
||||||
|
<v-col cols="auto"><v-icon icon="mdi-close" @click="menu = false"></v-icon></v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card-title>
|
||||||
|
<v-divider></v-divider>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4" v-for="items in menuItems">
|
||||||
|
<v-card variant="flat" :title="items.title">
|
||||||
|
<v-list v-for="item in items.value">
|
||||||
|
<v-list-item>
|
||||||
|
<v-switch
|
||||||
|
v-model="reloadItems"
|
||||||
|
:value="item.value"
|
||||||
|
color="primary"
|
||||||
|
:label="item.title"
|
||||||
|
hide-details></v-switch>
|
||||||
|
</v-list-item>
|
||||||
|
</v-list>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card>
|
||||||
|
</v-dialog>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="3" v-for="i in reloadItems" :key="i">
|
||||||
|
<v-card class="rounded-lg" variant="outlined" height="210px"
|
||||||
|
:title="menuItems.flatMap(cat => cat.value).find(m => m.value == i)?.title">
|
||||||
|
<v-card-text style="padding: 0 16px;" align="center" justify="center">
|
||||||
|
<Gauge :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'g'" />
|
||||||
|
<History :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'h'" />
|
||||||
|
<template v-if="i == 'i-sys'">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="3">{{ $t('main.info.host') }}</v-col>
|
||||||
|
<v-col cols="9" style="text-wrap: nowrap; overflow: hidden">{{ tilesData.sys?.hostName }}</v-col>
|
||||||
|
<v-col cols="3">{{ $t('main.info.cpu') }}</v-col>
|
||||||
|
<v-col cols="9">
|
||||||
|
<v-chip density="compact" variant="flat">
|
||||||
|
<v-tooltip activator="parent" location="top" style="direction: ltr;">
|
||||||
|
{{ tilesData.sys?.cpuType }}
|
||||||
|
</v-tooltip>
|
||||||
|
{{ tilesData.sys?.cpuCount }} {{ $t('main.info.core') }}
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3">IP</v-col>
|
||||||
|
<v-col cols="9">
|
||||||
|
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sys?.ipv4?.length>0">
|
||||||
|
<v-tooltip activator="parent" location="top" style="direction: ltr;">
|
||||||
|
<span v-html="tilesData.sys?.ipv4?.join('<br />')"></span>
|
||||||
|
</v-tooltip>
|
||||||
|
IPv4
|
||||||
|
</v-chip>
|
||||||
|
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sys?.ipv6?.length>0">
|
||||||
|
<v-tooltip activator="parent" location="top" style="direction: ltr;">
|
||||||
|
<span v-html="tilesData.sys?.ipv6?.join('<br />')"></span>
|
||||||
|
</v-tooltip>
|
||||||
|
IPv6
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3">S-UI</v-col>
|
||||||
|
<v-col cols="9">
|
||||||
|
<v-chip density="compact" color="blue">
|
||||||
|
<v-tooltip activator="parent" location="top">
|
||||||
|
{{ $t('main.info.threads') }}: {{ tilesData.sys?.appThreads }}<br />
|
||||||
|
{{ $t('main.info.memory') }}: {{ HumanReadable.sizeFormat(tilesData.sys?.appMem) }}
|
||||||
|
</v-tooltip>
|
||||||
|
v{{ tilesData.sys?.appVersion }}
|
||||||
|
</v-chip>
|
||||||
|
<v-chip density="compact" color="transparent" style="cursor: pointer;" @click="openLogs('s-ui')">
|
||||||
|
<v-tooltip activator="parent" location="top">
|
||||||
|
{{ $t('basic.log.title') + " - S-UI" }}
|
||||||
|
</v-tooltip>
|
||||||
|
<v-icon icon="mdi-list-box-outline" color="blue" />
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="3">{{ $t('main.info.uptime') }}</v-col>
|
||||||
|
<v-col cols="9">{{ HumanReadable.formatSecond(tilesData.uptime) }}</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
<template v-if="i == 'i-sbd'">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="4">{{ $t('main.info.running') }}</v-col>
|
||||||
|
<v-col cols="8">
|
||||||
|
<v-chip density="compact" color="success" variant="flat" v-if="tilesData.sbd?.running">{{ $t('yes') }}</v-chip>
|
||||||
|
<v-chip density="compact" color="error" variant="flat" v-else>{{ $t('no') }}</v-chip>
|
||||||
|
<v-chip density="compact" color="transparent" style="cursor: pointer;" @click="openLogs('sing-box')">
|
||||||
|
<v-tooltip activator="parent" location="top">
|
||||||
|
{{ $t('basic.log.title') + " - Sing-Box" }}
|
||||||
|
</v-tooltip>
|
||||||
|
<v-icon icon="mdi-list-box-outline" :color="tilesData.sbd?.running ? 'success': 'error'" />
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="4">{{ $t('main.info.memory') }}</v-col>
|
||||||
|
<v-col cols="8">
|
||||||
|
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sbd?.stats?.Alloc">
|
||||||
|
{{ HumanReadable.sizeFormat(tilesData.sbd?.stats?.Alloc) }}
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="4">{{ $t('main.info.threads') }}</v-col>
|
||||||
|
<v-col cols="8">
|
||||||
|
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sbd?.stats?.NumGoroutine">
|
||||||
|
{{ tilesData.sbd?.stats?.NumGoroutine }}
|
||||||
|
</v-chip>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="4">{{ $t('main.info.uptime') }}</v-col>
|
||||||
|
<v-col cols="8">{{ HumanReadable.formatSecond(tilesData.sbd?.stats?.Uptime) }}</v-col>
|
||||||
|
<v-col cols="4">{{ $t('online') }}</v-col>
|
||||||
|
<v-col cols="8">
|
||||||
|
<template v-if="tilesData.sbd?.running">
|
||||||
|
<v-chip density="compact" color="primary" variant="flat" v-if="Data().onlines.user">
|
||||||
|
<v-tooltip activator="parent" location="top" :text="$t('pages.clients')" />
|
||||||
|
{{ Data().onlines.user?.length }}
|
||||||
|
</v-chip>
|
||||||
|
<v-chip density="compact" color="success" variant="flat" v-if="Data().onlines.inbound">
|
||||||
|
<v-tooltip activator="parent" location="top" :text="$t('pages.inbounds')" />
|
||||||
|
{{ Data().onlines.inbound?.length }}
|
||||||
|
</v-chip>
|
||||||
|
<v-chip density="compact" color="info" variant="flat" v-if="Data().onlines.outbound">
|
||||||
|
<v-tooltip activator="parent" location="top" :text="$t('pages.outbounds')" />
|
||||||
|
{{ Data().onlines.outbound?.length }}
|
||||||
|
</v-chip>
|
||||||
|
</template>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</template>
|
||||||
|
</v-card-text>
|
||||||
|
</v-card>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-responsive>
|
||||||
|
</v-container>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts" setup>
|
||||||
|
import HttpUtils from '@/plugins/httputil'
|
||||||
|
import { HumanReadable } from '@/plugins/utils'
|
||||||
|
import Data from '@/store/modules/data'
|
||||||
|
import Gauge from '@/components/tiles/Gauge.vue'
|
||||||
|
import History from '@/components/tiles/History.vue'
|
||||||
|
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
|
import { i18n } from '@/locales'
|
||||||
|
import LogVue from '@/layouts/modals/Logs.vue'
|
||||||
|
|
||||||
|
const menu = ref(false)
|
||||||
|
const menuItems = [
|
||||||
|
{ title: i18n.global.t('main.gauges'), value: [
|
||||||
|
{ title: i18n.global.t('main.gauge.cpu'), value: "g-cpu" },
|
||||||
|
{ title: i18n.global.t('main.gauge.mem'), value: "g-mem" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ title: i18n.global.t('main.charts'), value: [
|
||||||
|
{ title: i18n.global.t('main.chart.cpu'), value: "h-cpu" },
|
||||||
|
{ title: i18n.global.t('main.chart.mem'), value: "h-mem" },
|
||||||
|
{ title: i18n.global.t('main.chart.net'), value: "h-net" },
|
||||||
|
{ title: i18n.global.t('main.chart.pnet'), value: "hp-net" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
{ title: i18n.global.t('main.infos'), value: [
|
||||||
|
{ title: i18n.global.t('main.info.sys'), value: "i-sys" },
|
||||||
|
{ title: i18n.global.t('main.info.sbd'), value: "i-sbd" },
|
||||||
|
]
|
||||||
|
},
|
||||||
|
]
|
||||||
|
|
||||||
|
const tilesData = ref(<any>{})
|
||||||
|
|
||||||
|
const reloadItems = computed({
|
||||||
|
get() { return Data().reloadItems },
|
||||||
|
set(v:string[]) {
|
||||||
|
if (Data().reloadItems.length == 0 && v.length>0) startTimer()
|
||||||
|
if (Data().reloadItems.length > 0 && v.length == 0) stopTimer()
|
||||||
|
Data().reloadItems = v
|
||||||
|
v.length>0 ? localStorage.setItem("reloadItems",v.join(',')) : localStorage.removeItem("reloadItems")
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
const reloadData = async () => {
|
||||||
|
const request = [...new Set(reloadItems.value.map(r => r.split('-')[1]))]
|
||||||
|
const data = await HttpUtils.get('api/status',{ r: request.join(',')})
|
||||||
|
if (data.success) {
|
||||||
|
tilesData.value = data.obj
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
let intervalId: NodeJS.Timeout | null = null
|
||||||
|
|
||||||
|
const startTimer = () => {
|
||||||
|
intervalId = setInterval(() => {
|
||||||
|
reloadData()
|
||||||
|
}, 2000)
|
||||||
|
}
|
||||||
|
|
||||||
|
const stopTimer = () => {
|
||||||
|
if (intervalId) {
|
||||||
|
clearInterval(intervalId);
|
||||||
|
intervalId = null
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
onMounted(() => {
|
||||||
|
if (Data().reloadItems.length != 0) {
|
||||||
|
reloadData()
|
||||||
|
startTimer()
|
||||||
|
}
|
||||||
|
})
|
||||||
|
|
||||||
|
onBeforeUnmount(() => {
|
||||||
|
stopTimer()
|
||||||
|
})
|
||||||
|
|
||||||
|
const logModal = ref({
|
||||||
|
visible: false,
|
||||||
|
logType: "s-ui"
|
||||||
|
})
|
||||||
|
|
||||||
|
const openLogs = (logType: string) => {
|
||||||
|
logModal.value.logType = logType
|
||||||
|
logModal.value.visible = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const closeLogs = () => {
|
||||||
|
logModal.value.logType = "s-ui"
|
||||||
|
logModal.value.visible = false
|
||||||
|
}
|
||||||
|
</script>
|
||||||
@@ -0,0 +1,128 @@
|
|||||||
|
<template>
|
||||||
|
<v-card :subtitle="$t('objects.multiplex')">
|
||||||
|
<v-row>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch color="primary" :label="$t('mux.enable')" v-model="muxEnable" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<template v-if="mux.enabled">
|
||||||
|
<template v-if="direction=='out'">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-select
|
||||||
|
hide-details
|
||||||
|
:items="[ 'smux', 'yamux', 'h2mux']"
|
||||||
|
:label="$t('protocol')"
|
||||||
|
clearable
|
||||||
|
@click:clear="mux.protocol=undefined"
|
||||||
|
v-model="mux.protocol">
|
||||||
|
</v-select>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('mux.maxConn')"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
min=0
|
||||||
|
v-model.number="max_connections">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('mux.minStr')"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
min=0
|
||||||
|
v-model.number="min_streams">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('mux.maxStr')"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
:min="min_streams"
|
||||||
|
v-model.number="max_streams">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</template>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch color="primary" :label="$t('mux.padding')" v-model="mux.padding" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-switch color="primary" :label="$t('mux.enableBrutal')" v-model="burtalEnable" hide-details></v-switch>
|
||||||
|
</v-col>
|
||||||
|
</template>
|
||||||
|
</v-row>
|
||||||
|
<v-row v-if="mux.brutal?.enabled">
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('stats.upload')"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
:suffix="$t('stats.Mbps')"
|
||||||
|
v-model.number="up_mbps">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
<v-col cols="12" sm="6" md="4">
|
||||||
|
<v-text-field
|
||||||
|
:label="$t('stats.download')"
|
||||||
|
hide-details
|
||||||
|
type="number"
|
||||||
|
:suffix="$t('stats.Mbps')"
|
||||||
|
min="0"
|
||||||
|
v-model.number="down_mbps">
|
||||||
|
</v-text-field>
|
||||||
|
</v-col>
|
||||||
|
</v-row>
|
||||||
|
</v-card>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<script lang="ts">
|
||||||
|
import { oMultiplex } from '@/types/multiplex'
|
||||||
|
export default {
|
||||||
|
props: ['data', 'direction'],
|
||||||
|
data() {
|
||||||
|
return {}
|
||||||
|
},
|
||||||
|
computed: {
|
||||||
|
mux(): oMultiplex {
|
||||||
|
return <oMultiplex> this.$props.data.multiplex
|
||||||
|
},
|
||||||
|
muxEnable: {
|
||||||
|
get(): boolean { return this.mux ? this.mux.enabled : false },
|
||||||
|
set(newValue:boolean) { this.$props.data.multiplex = newValue ? { enabled: newValue } : {} }
|
||||||
|
},
|
||||||
|
max_connections: {
|
||||||
|
get(): number { return this.mux.max_connections ? this.mux.max_connections : 0 },
|
||||||
|
set(newValue:number) { this.mux.max_connections = newValue > 0 ? newValue : undefined }
|
||||||
|
},
|
||||||
|
min_streams: {
|
||||||
|
get(): number { return this.mux.min_streams ? this.mux.min_streams : 0 },
|
||||||
|
set(newValue:number) { this.mux.min_streams = newValue > 0 ? newValue : undefined }
|
||||||
|
},
|
||||||
|
max_streams: {
|
||||||
|
get(): number { return this.mux.max_streams ? this.mux.max_streams : 0 },
|
||||||
|
set(newValue:number) { this.mux.max_streams = newValue > 0 ? newValue : undefined }
|
||||||
|
},
|
||||||
|
burtalEnable: {
|
||||||
|
get(): boolean { return this.mux.brutal ? this.mux.brutal.enabled : false },
|
||||||
|
set(newValue:boolean) { this.mux.brutal = newValue ? { enabled: newValue, up_mbps: 100, down_mbps: 100 } : undefined }
|
||||||
|
},
|
||||||
|
down_mbps: {
|
||||||
|
get() { return this.mux.brutal && this.mux.brutal.down_mbps ? this.mux.brutal.down_mbps : 0 },
|
||||||
|
set(newValue:any) {
|
||||||
|
if (this.mux.brutal){
|
||||||
|
this.mux.brutal.down_mbps = newValue.length != 0 ? newValue : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
up_mbps: {
|
||||||
|
get() { return this.mux.brutal && this.mux.brutal.up_mbps ? this.mux.brutal.up_mbps : 0 },
|
||||||
|
set(newValue:any) {
|
||||||
|
if (this.mux.brutal){
|
||||||
|
this.mux.brutal.up_mbps = newValue.length != 0 ? newValue : 0
|
||||||
|
}
|
||||||
|
}
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
</script>
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user