Compare commits

...

84 Commits

Author SHA1 Message Date
Alireza Ahmadi e89ac96885 v1.1.0 2024-10-29 20:07:35 +01:00
Alireza Ahmadi fe5b6cf922 Fix volume configuration in docker-compose.yml #295
Co-authored-by: admiralhr99 <admiralhr99@gmail.com>
2024-10-29 13:22:07 +01:00
Alireza Ahmadi 42f24c45c9 update dependencies 2024-10-29 13:16:47 +01:00
Alireza Ahmadi 282e244517 improve systemd or docker detection 2024-10-29 13:14:25 +01:00
Alireza Ahmadi 1d46d72186 [tun] disable auto_route in server #207 2024-10-29 12:51:11 +01:00
Alireza Ahmadi 7554b02a61 add tun inbound #207 2024-10-29 01:21:12 +01:00
Alireza Ahmadi fecb29f6ab [subJson] inbound options #240 #318 2024-10-28 21:59:48 +01:00
Alireza Ahmadi 7b7e5ac79d fix typo 2024-10-28 16:54:15 +01:00
Alireza Ahmadi e5fc14efd4 [rules] drag&drop rules on desktop view 2024-10-28 16:09:22 +01:00
Alireza Ahmadi 837150e065 [rules] better view 2024-10-28 15:37:02 +01:00
Alireza Ahmadi 93868b02d4 better auto start status #260 2024-10-28 13:56:20 +01:00
Alireza Ahmadi 50d1177443 restart singbox button #328 2024-10-28 13:49:38 +01:00
Alireza Ahmadi d255905907 bulk client creation #285 2024-10-28 13:48:06 +01:00
Alireza Ahmadi 5f3963ff1c update go and dependencies 2024-10-22 23:59:54 +02:00
Alireza Ahmadi bc6f356789 upgrade deprecated grpc call in v2rayAPI 2024-10-22 23:59:11 +02:00
Alireza Ahmadi 119cff3d85 separated inbound's client mux #257 2024-10-22 23:55:37 +02:00
Alireza Ahmadi 90b2876867 fix typo 2024-10-22 22:20:44 +02:00
Alireza Ahmadi 5105c138f7 [ss] fix 128 bit shadowsocks password #208 2024-10-22 22:11:14 +02:00
Alireza Ahmadi b019633c3f [ssTLS] fix handshake port #267 2024-10-22 22:09:28 +02:00
Alireza Ahmadi 419cce250f Latest working docker compose file on docs 2024-10-08 00:08:08 +02:00
Alireza Ahmadi bdc458dfa9 Merge pull request #277 from TheyCallMeSecond/main
Update jsonService.go
2024-10-07 22:05:56 +02:00
Alireza Ahmadi 3dff49d6e4 Merge pull request #310 from alireza0/dependabot/npm_and_yarn/frontend/rollup-4.22.4
Bump rollup from 4.20.0 to 4.22.4 in /frontend
2024-10-03 11:04:46 +02:00
dependabot[bot] 47e3c6944a Bump rollup from 4.20.0 to 4.22.4 in /frontend
Bumps [rollup](https://github.com/rollup/rollup) from 4.20.0 to 4.22.4.
- [Release notes](https://github.com/rollup/rollup/releases)
- [Changelog](https://github.com/rollup/rollup/blob/master/CHANGELOG.md)
- [Commits](https://github.com/rollup/rollup/compare/v4.20.0...v4.22.4)

---
updated-dependencies:
- dependency-name: rollup
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-24 03:42:42 +00:00
Alireza Ahmadi 54e48c8c76 Merge pull request #304 from alireza0/dependabot/npm_and_yarn/frontend/vite-5.4.6
Bump vite from 5.3.5 to 5.4.6 in /frontend
2024-09-21 23:05:05 +02:00
dependabot[bot] 9c7814a765 Bump vite from 5.3.5 to 5.4.6 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 5.3.5 to 5.4.6.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v5.4.6/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v5.4.6/packages/vite)

---
updated-dependencies:
- dependency-name: vite
  dependency-type: direct:development
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-09-17 20:04:08 +00:00
Alireza Ahmadi 6a174cf4db fix uninstall #287 2024-08-31 16:14:32 +02:00
Alireza Ahmadi 19e060ad33 fix override tls #273 2024-08-31 16:09:50 +02:00
Alireza Ahmadi 5c09bc011e fix outbound tls text #276 2024-08-31 14:18:15 +02:00
Alireza Ahmadi f6f90b07d3 Merge pull request #291 from alireza0/dependabot/npm_and_yarn/frontend/axios-1.7.4
Bump axios from 1.7.3 to 1.7.4 in /frontend
2024-08-31 13:52:00 +02:00
dependabot[bot] 2da30dc596 Bump axios from 1.7.3 to 1.7.4 in /frontend
Bumps [axios](https://github.com/axios/axios) from 1.7.3 to 1.7.4.
- [Release notes](https://github.com/axios/axios/releases)
- [Changelog](https://github.com/axios/axios/blob/v1.x/CHANGELOG.md)
- [Commits](https://github.com/axios/axios/compare/v1.7.3...v1.7.4)

---
updated-dependencies:
- dependency-name: axios
  dependency-type: direct:production
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-08-31 11:42:20 +00:00
Alexander Chepurnoy bdad92fe01 feat: add ru locale (#280)
* feat: add ru locale

* feat: add russian lang to readme
2024-08-31 13:29:01 +02:00
TheyCallMeSecond 5ddee6aa12 Update jsonService.go
add IPV6 traffic to the tunnel
2024-08-18 18:22:12 +03:30
Alireza Ahmadi 6f0df2d555 [client] add table and filter 2024-08-06 00:31:14 +02:00
Alireza Ahmadi 0bb3a67f79 add client group 2024-07-23 22:57:19 +02:00
Alireza Ahmadi 869c51885f fix vmess remark #198 2024-07-23 18:18:56 +02:00
Alireza Ahmadi 7b58edeaaf small fixes 2024-07-23 17:28:43 +02:00
Alireza Ahmadi e287ced0e4 fix tls change affect on inData 2024-07-23 17:27:51 +02:00
Alireza Ahmadi c518bf5a86 fix random ShortId 2024-07-23 17:26:50 +02:00
Alireza Ahmadi 89b85f818d fix reality in json-sub #224 2024-07-23 16:34:35 +02:00
Alireza Ahmadi ea3ad15b76 fix tls in new inbound 2024-07-23 16:25:22 +02:00
Alireza Ahmadi 5cc3791f79 fix api call error 2024-07-23 16:24:56 +02:00
Alireza Ahmadi eef7e200ba better link generator 2024-07-23 16:24:26 +02:00
Alireza Ahmadi 39022c1b2d fix typo in shadowTLS #199 2024-07-18 23:34:16 +02:00
Alireza Ahmadi 2b6874a58d better docker solution #176 2024-07-18 23:13:09 +02:00
Alireza Ahmadi 1631ac0c30 fix container logger #176 2024-07-18 23:12:31 +02:00
Alireza Ahmadi d70006cd91 independent migration 2024-07-18 23:10:49 +02:00
Alireza Ahmadi 19901efeaa fix sessions 2024-07-18 23:08:55 +02:00
Alireza Ahmadi b2d0134567 fix domain validator 2024-07-18 22:44:59 +02:00
Alireza Ahmadi 58f4a676b5 fix json marshal 2024-07-18 22:44:33 +02:00
Alireza Ahmadi 7904cb3db0 update packages 2024-07-18 22:42:55 +02:00
Alireza Ahmadi 6222533594 multi os/arch #72 2024-07-12 23:16:30 +02:00
Alireza Ahmadi cb4a7fe6df reload sing-box instead of restart 2024-07-07 23:37:03 +02:00
Alireza Ahmadi 96564f1f86 v1.0.0 2024-07-04 22:04:44 +02:00
Alireza Ahmadi cf6b61fe96 fix change detection 2024-07-04 11:29:46 +02:00
Alireza Ahmadi e1aaa3d748 fix sing-box client dns 2024-07-04 11:29:02 +02:00
Alireza Ahmadi 209561497a fix tls and ech auto generate 2024-07-01 00:30:13 +02:00
Alireza Ahmadi 60b374e5d4 upate readme: dev guide #111 2024-06-30 23:29:00 +02:00
Alireza Ahmadi ea8538148f update frontend 2024-06-30 23:28:00 +02:00
Alireza Ahmadi b3a2078ed6 client filter by opacity 2024-06-30 22:32:35 +02:00
Alireza Ahmadi f169064fbc small fixes 2024-06-30 22:32:19 +02:00
Alireza Ahmadi 7d441723ba update qr-code with singbox 2024-06-30 22:30:34 +02:00
Alireza Ahmadi a41140190f clone TLS 2024-06-30 22:29:44 +02:00
Alireza Ahmadi bb5cd91bc9 update translation 2024-06-30 22:29:30 +02:00
Alireza Ahmadi 5b6f6daaa8 add outbound by link #156 2024-06-28 18:17:45 +02:00
Alireza Ahmadi ba06ad598d fix reality utls #173 2024-06-28 16:09:42 +02:00
Alireza Ahmadi 69725ee5af option button better color 2024-06-28 15:59:22 +02:00
Alireza Ahmadi 0d36b811dc omit migration on first install #171 2024-06-28 15:58:15 +02:00
Alireza Ahmadi 6672a2721f subjson and multidomain 2024-06-28 15:55:37 +02:00
Alireza Ahmadi 6b24506ddd Merge pull request #165 from alireza0/dependabot/github_actions/docker/build-push-action-6
Bump docker/build-push-action from 5 to 6
2024-06-18 12:21:14 +02:00
dependabot[bot] 3298fd4e0d Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

---
updated-dependencies:
- dependency-name: docker/build-push-action
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 16:22:20 +00:00
Alireza Ahmadi 6dc7c93030 filter clients #69 2024-06-15 23:26:06 +02:00
Alireza Ahmadi 7c127f07bb small changes 2024-06-15 23:22:23 +02:00
Alireza Ahmadi b5a2dd18f5 add dns resolver #159 2024-06-15 22:10:58 +02:00
Alireza Ahmadi 53ed86c373 better traffic chart #120 2024-06-15 21:53:45 +02:00
Alireza Ahmadi ccbd591b39 cn locales to vuetify standard 2024-06-15 20:11:39 +02:00
Alireza Ahmadi f5792c9d82 show/reset client traffics #129 2024-06-15 12:45:13 +02:00
Alireza Ahmadi 4a2ac30a95 Merge pull request #161 from leic4u/patch-1
Update zhcn.ts
2024-06-15 10:32:48 +02:00
Alireza Ahmadi 45b03d8472 move notification to bottom 2024-06-14 01:05:44 +02:00
Alireza Ahmadi db3270feaa small fixes 2024-06-13 20:16:29 +02:00
Alireza Ahmadi b144aecb6a auto generate cert keys 2024-06-13 20:16:04 +02:00
leic4u 474e5156bb Update zhcn.ts
Updated Simplified Chinese translation
2024-06-13 00:44:50 +08:00
Alireza Ahmadi d057076251 fix client inbounds #154 2024-06-10 17:19:32 +02:00
Alireza Ahmadi 29cc6fd3e3 using migration in docker #153 2024-06-10 16:33:55 +02:00
Alireza Ahmadi dd0f770c1a v0.0.5 2024-06-09 23:03:03 +02:00
98 changed files with 5929 additions and 2128 deletions
+1 -1
View File
@@ -1 +1 @@
github: alireza0 buy_me_a_coffee: alireza7
+1 -1
View File
@@ -47,7 +47,7 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: core/ context: core/
push: true push: true
+2 -2
View File
@@ -44,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@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/386 platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
+23 -3
View File
@@ -14,7 +14,10 @@ jobs:
- amd64 - amd64
- arm64 - arm64
- armv7 - armv7
- armv6
- armv5
- 386 - 386
- s390x
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Checkout repository - name: Checkout repository
@@ -24,12 +27,12 @@ jobs:
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
cache: false cache: false
go-version: '1.22' go-version-file: backend/go.mod
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
with: with:
node-version: '20' node-version: '22'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install dependencies - name: Install dependencies
@@ -39,8 +42,14 @@ 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
@@ -63,13 +72,24 @@ 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 #### Build Sing-Box
export VERSION=v1.8.14 export VERSION=v1.10.1
git clone -b $VERSION https://github.com/SagerNet/sing-box git clone -b $VERSION https://github.com/SagerNet/sing-box
cd 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 \ 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 \
+4 -3
View File
@@ -1,9 +1,9 @@
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.22-alpine AS backend-builder FROM golang:1.23-alpine AS backend-builder
WORKDIR /app WORKDIR /app
ARG TARGETARCH ARG TARGETARCH
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE" ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
@@ -20,5 +20,6 @@ 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" ]
CMD [ "./sui" ] ENTRYPOINT [ "./entrypoint.sh" ]
+68 -9
View File
@@ -23,11 +23,11 @@
| 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 + info) | :heavy_check_mark: | | Subscription Service (link/json + info)| :heavy_check_mark: |
| Dark/Light Theme | :heavy_check_mark: | | Dark/Light Theme | :heavy_check_mark: |
## Default Installation Informarion ## Default Installation Information
- Panel Port: 2095 - Panel Port: 2095
- Panel Path: /app/ - Panel Path: /app/
- Subscription Port: 2096 - Subscription Port: 2096
@@ -40,12 +40,12 @@
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 Custom Version ## Install legacy Version
**Step 1:** To install your desired version, add the version to the end of the installation command. e.g., ver `0.0.1`: **Step 1:** To install your desired legacy version, add the version to the end of the installation command. e.g., ver `1.0.0`:
```sh ```sh
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh) 0.0.1 bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh) 1.0.0
``` ```
## Uninstall S-UI ## Uninstall S-UI
@@ -80,7 +80,7 @@ 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/main/docker-compose.yml wget -q https://raw.githubusercontent.com/alireza0/s-ui/master/docker-compose.yml
docker compose up -d docker compose up -d
``` ```
@@ -104,6 +104,56 @@ docker build -t s-ui .
</details> </details>
## Manual run ( contribution )
<details>
<summary>Click for details</summary>
### Build and run whole project
```shell
./runSUI.sh
```
### - Frontend
Frontend codes are in `frontend` folder in the root of repository.
To run it localy for instant developement you can use (apply automatic changes on file save):
```shell
cd frontend
npm run dev
```
> By this command it will run a `vite` web server on separate port `3000`, with backend proxy to `http://localhost:2095`. You can change it in `frontend/vite.config.mts`.
To build frontend:
```shell
cd frontend
npm run build
```
### - Backend
Backend codes are in `backend` folder in the root of repository.
> Please build frontend once before!
To build backend:
```shell
cd backend
# 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
@@ -111,6 +161,7 @@ docker build -t s-ui .
- Vietnamese - Vietnamese
- Chinese (Simplified) - Chinese (Simplified)
- Chinese (Traditional) - Chinese (Traditional)
- Russian
## Features ## Features
@@ -129,10 +180,18 @@ docker build -t s-ui .
## 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
@@ -169,4 +228,4 @@ certbot certonly --standalone --register-unsafely-without-email --non-interactiv
</details> </details>
## Stargazers over Time ## Stargazers over Time
[![Stargazers over time](https://starchart.cc/alireza0/s-ui.svg?variant=adaptive)](https://starchart.cc/alireza0/s-ui) [![Stargazers over time](https://starchart.cc/alireza0/s-ui.svg)](https://starchart.cc/alireza0/s-ui)
+22 -8
View File
@@ -3,6 +3,8 @@ package api
import ( import (
"s-ui/logger" "s-ui/logger"
"s-ui/service" "s-ui/service"
"s-ui/singbox"
"s-ui/util"
"strconv" "strconv"
"strings" "strings"
@@ -15,9 +17,11 @@ type APIHandler struct {
service.ConfigService service.ConfigService
service.ClientService service.ClientService
service.TlsService service.TlsService
service.InDataService
service.PanelService service.PanelService
service.StatsService service.StatsService
service.ServerService service.ServerService
singbox.Controller
} }
func NewAPIHandler(g *gin.RouterGroup) { func NewAPIHandler(g *gin.RouterGroup) {
@@ -55,14 +59,7 @@ func (a *APIHandler) postHandler(c *gin.Context) {
logger.Infof("Unable to get session's max age from DB") logger.Infof("Unable to get session's max age from DB")
} }
if sessionMaxAge > 0 { err = SetLoginUser(c, loginUser, sessionMaxAge)
err = SetMaxAge(c, sessionMaxAge*60)
if err != nil {
logger.Infof("Unable to set session's max age")
}
}
err = SetLoginUser(c, loginUser)
if err == nil { if err == nil {
logger.Info("user ", loginUser, " login success") logger.Info("user ", loginUser, " login success")
} else { } else {
@@ -94,6 +91,13 @@ func (a *APIHandler) postHandler(c *gin.Context) {
case "restartApp": case "restartApp":
err = a.PanelService.RestartPanel(3) err = a.PanelService.RestartPanel(3)
jsonMsg(c, "restartApp", err) jsonMsg(c, "restartApp", err)
case "restartSb":
err = a.Controller.Restart()
jsonMsg(c, "restartSb", err)
case "linkConvert":
link := c.Request.FormValue("link")
result, _, err := util.GetOutbound(link, 0)
jsonObj(c, result, err)
default: default:
jsonMsg(c, "API call", nil) jsonMsg(c, "API call", nil)
} }
@@ -163,6 +167,11 @@ func (a *APIHandler) getHandler(c *gin.Context) {
count := c.Query("c") count := c.Query("c")
changes := a.ConfigService.GetChanges(actor, chngKey, count) changes := a.ConfigService.GetChanges(actor, chngKey, count)
jsonObj(c, changes, nil) jsonObj(c, changes, nil)
case "keypairs":
kType := c.Query("k")
options := c.Query("o")
keypair := a.ServerService.GenKeypair(kType, options)
jsonObj(c, keypair, nil)
default: default:
jsonMsg(c, "API call", nil) jsonMsg(c, "API call", nil)
} }
@@ -201,6 +210,10 @@ func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
inData, err := a.InDataService.GetAll()
if err != nil {
return "", err
}
subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0]) subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0])
if err != nil { if err != nil {
return "", err return "", err
@@ -208,6 +221,7 @@ func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
data["config"] = *config data["config"] = *config
data["clients"] = clients data["clients"] = clients
data["tls"] = tlsConfigs data["tls"] = tlsConfigs
data["inData"] = inData
data["subURI"] = subURI data["subURI"] = subURI
data["onlines"] = onlines data["onlines"] = onlines
} else { } else {
+14 -5
View File
@@ -4,7 +4,7 @@ import (
"encoding/gob" "encoding/gob"
"s-ui/database/model" "s-ui/database/model"
sessions "github.com/Calidity/gin-sessions" "github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -16,17 +16,26 @@ func init() {
gob.Register(model.User{}) gob.Register(model.User{})
} }
func SetLoginUser(c *gin.Context, userName string) error { func SetLoginUser(c *gin.Context, userName string, maxAge int) error {
options := sessions.Options{
Path: "/",
Secure: false,
}
if maxAge > 0 {
options.MaxAge = maxAge * 60
}
s := sessions.Default(c) s := 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, maxAge int) error { func SetMaxAge(c *gin.Context) 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()
} }
+13 -6
View File
@@ -4,20 +4,27 @@ import (
"encoding/json" "encoding/json"
"fmt" "fmt"
"log" "log"
"os"
"s-ui/config" "s-ui/config"
"s-ui/database"
"s-ui/database/model" "s-ui/database/model"
"strings" "strings"
"gorm.io/driver/sqlite"
"gorm.io/gorm" "gorm.io/gorm"
) )
func migrateDb() { func migrateDb() {
err := database.OpenDB(config.GetDBPath()) // void running on first install
path := config.GetDBPath()
_, err := os.Stat(path)
if err != nil {
return
}
db, err := gorm.Open(sqlite.Open(path))
if err != nil { if err != nil {
log.Fatal(err) log.Fatal(err)
} }
db := database.GetDB()
tx := db.Begin() tx := db.Begin()
defer func() { defer func() {
if err == nil { if err == nil {
@@ -70,15 +77,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 {
+9
View File
@@ -79,3 +79,12 @@ func GetDefaultConfig() string {
func GetEnvApi() string { func GetEnvApi() string {
return os.Getenv("SINGBOX_API") return os.Getenv("SINGBOX_API")
} }
func IsSystemd() bool {
pid := os.Getppid()
cmdline, err := os.ReadFile(fmt.Sprintf("/proc/%d/comm", pid))
if err != nil {
return false
}
return string(cmdline) == "systemd\n"
}
+1 -1
View File
@@ -1 +1 @@
0.0.4 1.1.0
+1
View File
@@ -60,6 +60,7 @@ func InitDB(dbPath string) error {
err = db.AutoMigrate( err = db.AutoMigrate(
&model.Setting{}, &model.Setting{},
&model.Tls{}, &model.Tls{},
&model.InboundData{},
&model.User{}, &model.User{},
&model.Stats{}, &model.Stats{},
&model.Client{}, &model.Client{},
+9 -1
View File
@@ -16,6 +16,13 @@ type Tls struct {
Client json.RawMessage `json:"client" form:"client"` Client json.RawMessage `json:"client" form:"client"`
} }
type InboundData struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Tag string `json:"tag" form:"tag"`
Addrs json.RawMessage `json:"addrs" form:"addrs"`
OutJson json.RawMessage `json:"outJson" form:"outJson"`
}
type User struct { type User struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username" form:"username"` Username string `json:"username" form:"username"`
@@ -34,7 +41,8 @@ type Client struct {
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" from:"desc"` Desc string `json:"desc" form:"desc"`
Group string `json:"group" form:"group"`
} }
type Stats struct { type Stats struct {
+43 -39
View File
@@ -1,64 +1,68 @@
module s-ui module s-ui
go 1.22.0 go 1.23.2
require ( require (
github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/gzip v1.0.1
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.10.0
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/v2fly/v2ray-core/v5 v5.13.0 github.com/robfig/cron/v3 v3.0.1
gorm.io/driver/sqlite v1.5.5 github.com/shirou/gopsutil/v3 v3.24.5
gorm.io/gorm v1.25.7 github.com/v2fly/v2ray-core/v5 v5.17.1
google.golang.org/grpc v1.67.1
gorm.io/driver/sqlite v1.5.6
gorm.io/gorm v1.25.12
) )
require ( require (
github.com/adrg/xdg v0.4.0 // indirect github.com/adrg/xdg v0.5.0 // indirect
github.com/bytedance/sonic v1.11.1 // indirect github.com/bytedance/sonic v1.12.3 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/bytedance/sonic/loader v0.2.1 // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/cloudwego/base64x v0.1.4 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/cloudwego/iasm v0.2.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.6 // indirect
github.com/gin-contrib/sessions v1.0.1
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/go-playground/validator/v10 v10.22.1 // indirect
github.com/golang/protobuf v1.5.3 // indirect github.com/goccy/go-json v0.10.3 // indirect
github.com/golang/protobuf v1.5.4 // indirect
github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a // indirect
github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/context v1.1.2 // indirect
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.2 // indirect github.com/gorilla/sessions v1.4.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.7 // indirect github.com/klauspost/cpuid/v2 v2.2.8 // indirect
github.com/kr/text v0.2.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a // indirect github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683 // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e // indirect
github.com/pelletier/go-toml/v2 v2.2.3 // indirect
github.com/pires/go-proxyproto v0.8.0 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.13 // indirect github.com/tklauser/go-sysconf v0.3.14 // indirect
github.com/tklauser/numcpus v0.7.0 // indirect github.com/tklauser/numcpus v0.9.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/arch v0.7.0 // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/crypto v0.21.0 // indirect golang.org/x/arch v0.11.0 // indirect
golang.org/x/net v0.23.0 // indirect golang.org/x/crypto v0.28.0 // indirect
golang.org/x/sys v0.18.0 // indirect golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/net v0.30.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect golang.org/x/sys v0.26.0 // indirect
google.golang.org/protobuf v1.33.0 // indirect golang.org/x/text v0.19.0 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 // indirect
google.golang.org/protobuf v1.35.1 // indirect
gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f // indirect
gopkg.in/yaml.v3 v3.0.1 // 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
)
+111 -166
View File
@@ -1,30 +1,26 @@
github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE= github.com/adrg/xdg v0.5.0 h1:dDaZvhMXatArP1NPHhnfaQUqWBLBsmx1h1HXQdMoFCY=
github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns= github.com/adrg/xdg v0.5.0/go.mod h1:dDdY4M4DF9Rjy4kHPeNL+ilVF+p2lK8IdM9/rTSGcI4=
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 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY=
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1/go.mod h1:nuudZmJhzWtx2212z+pkuy7B6nkBqa+xwNXZHL1j8cg= 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 h1:t9c7v8JUKu/XxOGBU0yjNpaMloxGEJhUkqFRq0ibGeU=
github.com/ajg/form v1.5.1/go.mod h1:uL1WgH+h2mgNtvBq0339dVnzXdBETtL2LeUXaIv25UY= 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.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.0.5/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig= github.com/andybalholm/brotli v1.0.6/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 h1:zsO4lp+bjv5XvPTF58Vq+qgmZEYZttJK+CWtSZhKenI=
github.com/boljen/go-bitmap v0.0.0-20151001105940-23cd2fb0ce7d/go.mod h1:f1iKL6ZhUWvbk7PdWVmOaak10o86cqMUYEmn1CZNGEI= 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.10.0 h1:+jW/wnLMLxaCEG8AX9lD0bQ5v9h1RUiMKOBOT5ll9dM=
github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE= github.com/bufbuild/protocompile v0.10.0/go.mod h1:G9qQIQo0xZ6Uyj6CMNz0saGmx2so+KONo8/KrELABiY=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.12.3 h1:W2MGa7RCU1QTeYRTPE3+88mVC0yXmsRQRChiyVocVjU=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.12.3/go.mod h1:B8Gt/XvtZ3Fqj+iSKMypzymZxw/FVwgIGKzMzT9r/rk=
github.com/bytedance/sonic v1.11.1 h1:JC0+6c9FoWYYxakaoa+c5QTtJeiSZNeByOBhXtAFSn4= github.com/bytedance/sonic/loader v0.1.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/bytedance/sonic v1.11.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/bytedance/sonic/loader v0.2.1 h1:1GgorWTqf12TA8mma4DDSbaQigE2wOgQo7iCjjJv3+E=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/bytedance/sonic/loader v0.2.1/go.mod h1:ncP89zfokxS5LZrJxl5z0UJcsk4M4yY2JpfqGeCtNLU=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/cloudflare/circl v1.3.7 h1:qlCDlTPz2n9fu58M0Nh1J/JzcFpfgkFHHX3O35r5vcU=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/cloudflare/circl v1.3.7/go.mod h1:sRTcRWXGLrKw6yIGJ+l7amYJFfAXbZG0kBSc8r4zxgA=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d/go.mod h1:8EPpVsBuRksnlj1mLy4AWzRNQYxauNi62uWcE3to6eA= github.com/cloudwego/base64x v0.1.4 h1:jwCgWpFanWmN8xoIUHa2rtzmkd5J2plF/dnLS6Xd/0Y=
github.com/chenzhuoyu/iasm v0.9.0/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/cloudwego/base64x v0.1.4/go.mod h1:0zlkT4Wn5C6NdauXdJRhSKRlJvmclQ1hhJgA0rcu/8w=
github.com/chenzhuoyu/iasm v0.9.1 h1:tUHQJXo3NhBqw6s33wkGn9SP3bvrWLdlVIJ3hQBL7P0= github.com/cloudwego/iasm v0.2.0 h1:1KNIy1I1H9hNNFEEH3DVnI4UujN+1zjpuk6gwHLTssg=
github.com/chenzhuoyu/iasm v0.9.1/go.mod h1:Xjy2NpN3h7aUqeqM+woSuuvxmIe6+DDsiNLIrkAmYog= github.com/cloudwego/iasm v0.2.0/go.mod h1:8rXZaNYT2n95jn+zTI1sDr+IgcD2GVs0nlbbQPiEFhY=
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/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.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 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
@@ -33,51 +29,43 @@ github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 h1:y7y0Oa6UawqTFP
github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw= 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 h1:YtdtTUN1iH97s+6PUjLnaiKSQj4oG1/EZ3N9bx6g4kU=
github.com/ebfe/bcrypt_pbkdf v0.0.0-20140212075826-3c8d2dcb253a/go.mod h1:/CZpbhAusDOobpcb9yubw46kdYjq0zRC0Wpg9a9zFQM= 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.6 h1:3+PzJTKLkvgjeTbts6msPJt4DixhT4YtFNf1gtGe3zc=
github.com/gabriel-vasile/mimetype v1.4.3/go.mod h1:d8uq/6HKRL6CGdk+aubisF/M5GcPfT7nKyLpA0lbSSk= github.com/gabriel-vasile/mimetype v1.4.6/go.mod h1:JX1qVKqZd40hUPpAfiNTe0Sne7hdfKSbOqqmkq8GCXc=
github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXbk= github.com/gin-contrib/gzip v1.0.1 h1:HQ8ENHODeLY7a4g1Au/46Z92bdGFl74OhxcZble9WJE=
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/gin-contrib/gzip v1.0.1/go.mod h1:njt428fdUNRvjuJf16tZMYZ2Yl+WQB53X5wmhDwXvC4=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/sessions v1.0.1 h1:3hsJyNs7v7N8OtelFmYXFrulAf6zSR7nW/putcPEHxI=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/sessions v1.0.1/go.mod h1:ouxSFM24/OgIud5MJYQJLpy6AwxQ5EYO9yLhbtObGkM=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= 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-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.10.0 h1:nTuyha1TYqgedzytsKYqna+DfLos46nTv2ygFy86HFU=
github.com/gin-gonic/gin v1.9.1 h1:4idEAncQnU5cB7BeOkPtxjfCSye0AAm1R0RVIqJ+Jmg= github.com/gin-gonic/gin v1.10.0/go.mod h1:4PMNQiOhvDRa013RKVbsiNwoyezlm2rm0uX/T7kzp5Y=
github.com/gin-gonic/gin v1.9.1/go.mod h1:hPrL7YrpYKXt5YId3A/Tnip5kqbEAP+KLuI3SUcPTeU= github.com/go-chi/chi/v5 v5.1.0 h1:acVI1TYaD+hhedDJ3r54HyA6sExp3HfXq7QWEEY/xMw=
github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk= github.com/go-chi/chi/v5 v5.1.0/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
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 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= 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.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 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78= 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 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= 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 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/oXslEjJA=
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= 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 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= 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.22.1 h1:40JcKH+bBNGFczGuoBYgX4I6m/i27HYW8P9FDk5PbgA=
github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U= github.com/go-playground/validator/v10 v10.22.1/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
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 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= 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.3 h1:KZ5WoDbxAIgm2HNbYckL0se1fHD6rz5j4ywS6ebzDqA=
github.com/goccy/go-json v0.10.2 h1:CrxCmQqYDkv1z7lO7Wbh2HN93uovUHgrECaO5ZrCXAU= github.com/goccy/go-json v0.10.3/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-json v0.10.2/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/golang-collections/go-datastructures v0.0.0-20150211160725-59788d5eb259 h1:ZHJ7+IGpuOXtVf6Zk/a3WuHQgkC+vXwaqfUBDFwahtI=
github.com/golang-collections/go-datastructures v0.0.0-20150211160725-59788d5eb259/go.mod h1:9Qcha0gTWLw//0VNka1Cbnjvg3pNKGFdAm7E9sBabxE=
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= 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/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.4 h1:i7eJL8qZTpSEXOPTxNKhASYpMn+8e5Q6AdndVa1dWek=
github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg= github.com/golang/protobuf v1.5.4/go.mod h1:lnTiLA8Wa4RWRcIUkrtSVa5nRhsEGBg48fD6rSs7xps=
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 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/btree v1.1.2/go.mod h1:qOPhT0dTNdNzV6Z/lhRX0YXUafgPLFUh+gZMl761Gm4= 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 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI=
github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 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.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
@@ -85,49 +73,42 @@ 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/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 h1:ves8RnFZPGiFnTS0uPQStjwru6uO6h+nlr9j6fL7kF8=
github.com/google/gopacket v1.1.19/go.mod h1:iJ8V8n6KS+z2U1A8pUwu8bW5SyEMkXJB8Yo/Vo+TKTo= 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-20231101202521-4ca4178f5c7a h1:fEBsGL/sjAuJrgah5XqmmYsTLzJp/TO9Lhy39gkverk=
github.com/google/pprof v0.0.0-20230602150820-91b7bce49751/go.mod h1:Jh3hGz2jkYak8qXPD19ryItVnUgpgeqzdkY/D0EaeuA= github.com/google/pprof v0.0.0-20231101202521-4ca4178f5c7a/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
github.com/gorilla/context v1.1.2 h1:WRkNAv2uoa03QNIc1A6u4O7DAGMUVoopZhkiXWA2V1o= 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/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 h1:YCIWL56dvtr73r6715mJs5ZvhtnY73hBvEF8kXD8ePA=
github.com/gorilla/securecookie v1.1.2/go.mod h1:NfCASbcHqRSY+3a8tlWJwsQap2VX5pwzwo4h3eOamfo= 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.4.0 h1:kpIYOp/oi6MG/p5PgxApU8srsSw9tuFbt46Lt7auzqQ=
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.4.0/go.mod h1:FLWm50oby91+hl7p/wRxDth9bWSuk0qVL2emc7lT5ik=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.3 h1:saDtZ6Pbx/0u+bgYQ3q96pZgCzfhKXGPqt7kZ72aNNg=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gorilla/websocket v1.5.3/go.mod h1:YR8l580nyteQvAITg2hZ9XVh4b55+EU/adAjf1fMHhE=
github.com/jhump/protoreflect v1.15.3 h1:6SFRuqU45u9hIZPJAoZ8c28T3nK64BNdp9w6jFonzls= github.com/jhump/protoreflect v1.16.0 h1:54fZg+49widqXYQ0b+usAFHbMkBGR4PpXrsHc8+TBDg=
github.com/jhump/protoreflect v1.15.3/go.mod h1:4ORHmSBmlCW8fh3xHmJMGyul1zNqZK4Elxc8qKP+p1k= github.com/jhump/protoreflect v1.16.0/go.mod h1:oYPd7nPvcBw/5wlDfm/AVmU9zH9BgqGCI469pGxfj/8=
github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= 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/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 h1:/o9tlHleP7gOFmsnYNz3RGnqzefHA47wQpKrrdTIwXQ=
github.com/jinzhu/now v1.1.5/go.mod h1:d3SSVoowX0Lcu0IBviAWJpolVfI5UJVZZ7cO71lE/z8= 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 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= 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.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= 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.8 h1:+StwCXwm9PdpiEkPyzBXIy+M9KUb4ODm0Zarf1kS5BM=
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.8/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/reedsolomon v1.11.7 h1:9uaHU0slncktTEEg4+7Vl7q7XUNMBUOK4R9gnKhMjAU= github.com/klauspost/reedsolomon v1.11.7 h1:9uaHU0slncktTEEg4+7Vl7q7XUNMBUOK4R9gnKhMjAU=
github.com/klauspost/reedsolomon v1.11.7/go.mod h1:4bXRN+cVzMdml6ti7qLouuYi32KHJ5MGv0Qd8a47h6A= 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/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/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.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 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE= github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/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 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= 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-20240909124753-873cd0166683 h1:7UMa6KCCMjZEMDtTVdcGu0B1GmmC7QJKiCCjyTAWQy0=
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI= github.com/lufia/plan9stats v0.0.0-20240909124753-873cd0166683/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
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 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= 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 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y= 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 h1:gXHsfypPkaMZrKbD5209QV9jbUTJKjyR5WD3HYQSd+U=
@@ -141,6 +122,8 @@ github.com/mustafaturan/bus v1.0.2 h1:2x3ErwZ0uUPwwZ5ZZoknEQprdaxr68Yl3mY8jDye1W
github.com/mustafaturan/bus v1.0.2/go.mod h1:h7gfehm8TThv4Dcaa+wDQG7r7j6p74v+7ftr0Rq9i1Q= 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 h1:8SCej+JiNn0lyps7V+Jzc1CRAkDR4EZPWrTupQ61YCQ=
github.com/mustafaturan/monoton v1.0.0/go.mod h1:FOnE7NV3s3EWPXb8/7+/OSdiMBbdlkV0Lz8p1dc+vy8= github.com/mustafaturan/monoton v1.0.0/go.mod h1:FOnE7NV3s3EWPXb8/7+/OSdiMBbdlkV0Lz8p1dc+vy8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/onsi/ginkgo/v2 v2.10.0 h1:sfUl4qgLdvkChZrWCYndY2EAu9BRIw1YphNAzy1VNWs= 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/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 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
@@ -149,46 +132,38 @@ github.com/patrickmn/go-cache v2.1.0+incompatible h1:HRMgzkcYKYpi3C8ajMPV8OFXaaR
github.com/patrickmn/go-cache v2.1.0+incompatible/go.mod h1:3Qf8kWWT7OJRJbdiICTKqZju1ZixQ/KpMGzzAfe6+WQ= 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 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c= 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.2.3 h1:YmeHyLY8mFWbdkNWwpr+qIL2bEqT0o95WSdkNHvL12M=
github.com/pelletier/go-toml/v2 v2.1.1 h1:LWAJwfNvjQZCFIDKWYQaM62NcYeYViCmWIwmOStowAI= github.com/pelletier/go-toml/v2 v2.2.3/go.mod h1:MfCQTFTvCcUyyvvwm1+G6H/jORL20Xlb6rzQu9GuUkc=
github.com/pelletier/go-toml/v2 v2.1.1/go.mod h1:tJU2Z3ZkXwnxa4DPO899bsyIoywizdUvyaeZurnPPDc= github.com/pion/dtls/v2 v2.2.12 h1:KP7H5/c1EiVAAKUmXyCzPiQe5+bCJrpOeKg/L05dunk=
github.com/pion/dtls/v2 v2.2.7 h1:cSUBsETxepsCSFSxC3mc/aDo14qQLMSL+O6IjG28yV8= github.com/pion/dtls/v2 v2.2.12/go.mod h1:d9SYc9fch0CqK90mRk1dC7AkzzpwJj6u2GU3u+9pqFE=
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 h1:M9+AIj/+pxNsDfAT64+MAVgJO0rsyLnoJKCqf//DoeY=
github.com/pion/logging v0.2.2/go.mod h1:k0/tDVsRCX2Mb2ZEmTqNa7CWsQPc+YYCB7Q+5pahoms= 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 h1:CFG1UdESneORglEsnimhUjf33Rwjubwj6xfiOXBa3mA=
github.com/pion/randutil v0.1.0/go.mod h1:XcJrSMMbbMRhASFVOlj/5hQial/Y8oH/HVo7TBZq+j8= 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 h1:JnABvFakZueGAn4KU/4PSKg+GWbF6QWbKTWZOSGJjXw=
github.com/pion/sctp v1.8.7/go.mod h1:g1Ul+ARqZq5JEmoFy87Q/4CePtKnTJ1QCL9dBBdN6AU= 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.10 h1:ucLBLE8nuxiHfvkFKnkDQRYWYfp8ejf4YBOPfaQpw6Q=
github.com/pion/transport/v2 v2.2.4/go.mod h1:q2U/tf9FEfnSBGSW6w5Qp5PFWRLRj3NjLhCCgpRK4p0= github.com/pion/transport/v2 v2.2.10/go.mod h1:sq1kSLWs+cHW9E+2fJP95QudkzbK7wscs8yYgQToO5E=
github.com/pires/go-proxyproto v0.7.0 h1:IukmRewDQFWC7kfnb66CSomk2q/seBuilHBYFwyq0Hs= github.com/pires/go-proxyproto v0.8.0 h1:5unRmEAPbHXHuLjDg01CxJWf91cw3lKHc/0xzKpXEe0=
github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP40O9LbuiFR4= github.com/pires/go-proxyproto v0.8.0/go.mod h1:iknsfgnH8EkjrMeMyvfKByp9TiBZCKZM0jx2xmKqnVY=
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 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= 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 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= 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/quic-go v0.46.0 h1:uuwLClEEyk1DNvchH8uCByQVjo3yKL9opKulExNDs7Y=
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.46.0/go.mod h1:1dLehS7TIR64+vxGR70GDcatWTOtMX2PUtnKsjbTurI=
github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw= github.com/refraction-networking/utls v1.6.7 h1:zVJ7sP1dJx/WtVuITug3qYUq034cDq9B2MR1K67ULZM=
github.com/quic-go/quic-go v0.40.0/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c= github.com/refraction-networking/utls v1.6.7/go.mod h1:BC3O4vQzye5hqpmDTWUqi4P5DDhzJfkV1tdqtawQIH0=
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 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s= 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 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= 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 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/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 h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= 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.5 h1:i0t8kL+kQTvpAYToeuiVk3TgDeKOFioZO3Ztz/iZ9pI=
github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU= github.com/shirou/gopsutil/v3 v3.24.5/go.mod h1:bsoOS1aStSs9ErQ1WWfxllSeS1K5D+U30r2NfcubMVk=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= 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 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
@@ -197,23 +172,18 @@ github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI= 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.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.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.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.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.9.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.14 h1:g5vzr9iPFFz24v2KZXs/pvpvh8/V9Fw6vQK5ZZb78yU=
github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4= github.com/tklauser/go-sysconf v0.3.14/go.mod h1:1ym4lWMLUOhuBOPGtRcJm7tEGX4SCYNEEEtghGG/8uY=
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0= github.com/tklauser/numcpus v0.9.0 h1:lmyCHtANi8aRUgkckBgoDk1nHCux3n2cgkJLXdQGPDo=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tklauser/numcpus v0.9.0/go.mod h1:SN6Nq1O3VychhC1npsWostA+oW+VOQTxZrS604NSRyI=
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 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= 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 h1:9LC83zGrHhuUA9l16C9AHXAqEV/2wBQ4nkvumAE65EE=
github.com/ugorji/go/codec v1.2.12/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= 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 h1:4Yh46CVE3k/lPq6hUbEdbB1u1anRBXLewm3k+L0iOMc=
@@ -222,90 +192,65 @@ github.com/v2fly/VSign v0.0.0-20201108000810-e2adc24bf848 h1:p1UzXK6VAutXFFQMnre
github.com/v2fly/VSign v0.0.0-20201108000810-e2adc24bf848/go.mod h1:p80Bv154ZtrGpXMN15slDCqc9UGmfBuUzheDFBYaW/M= 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 h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= 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.17.1 h1:IIMMtmRdaG5HTYNn6VX1xKULknJl7nhkSFnmoTb5TDQ=
github.com/v2fly/v2ray-core/v5 v5.13.0/go.mod h1:Bc3gmQWLr8UR7xBSCYI9FbfKuVvqA9lbkeBTWNRRAS4= github.com/v2fly/v2ray-core/v5 v5.17.1/go.mod h1:IhDN0rhXJnNcs9jUuC5sILTGCT2L+4yr0+tfD8ZVuL8=
github.com/vincent-petithory/dataurl v1.0.0 h1:cXw+kPto8NLuJtlMsI152irrVw9fRDX8AbShPRpg2CI= 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/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 h1:I/ATawgO2RerCq9ACwL0wBB8xNXZdE3J+93MCEHReRs=
github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432/go.mod h1:QN7Go2ftTVfx0aCTh9RXHV8pkpi0FtmbwQw40dy61wQ= 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 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY=
github.com/xtaci/smux v1.5.24/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= 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 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= 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 h1:2/wtqS591wZyD2OsClsVBKRPEvBsQt/Js+fsCiYhwu8=
go.starlark.net v0.0.0-20230612165344-9532f5667272/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= 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.4.0 h1:VcM4ZOtdbR4f6VXfiOpwpVJDL6lCReaZ6mw31wqh7KU=
go.uber.org/mock v0.3.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc= go.uber.org/mock v0.4.0/go.mod h1:a6FSlNadKUHUa9IP5Vyt1zh4fC7uAwxMutEAscFbkZc=
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35 h1:nJAwRlGWZZDOD+6wni9KVUNHMpHko/OnRwsrCYeAzPo= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
go4.org/netipx v0.0.0-20230303233057-f1b76eb4bb35/go.mod h1:TQvodOM+hJTioNQJilmLXu08JNb8i+ccq418+KWu1/Y= go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.11.0 h1:KXV8WWKCXm6tRpLirl2szsO5j/oOODwZf4hATmGVNs4=
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.11.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/crypto v0.28.0 h1:GBDwsMXVQi34v5CCYUm2jkJvu4cbtru2U4TN2PSyQnw=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.28.0/go.mod h1:rmgy+3RHxRZMyY0jjAJShp2zgEdOqj2AO7U0pYmeQ7U=
golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56 h1:2dVuKD2vS7b0QIHQbpyTISPd0LeHDbnYEryqj5Q1ug8=
golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs= golang.org/x/exp v0.0.0-20240719175910-8a7402abbf56/go.mod h1:M4RDyNAINzryxdtnbRXRL/OHtkFuWGRjvuhBJpk2IlY=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= golang.org/x/mod v0.19.0 h1:fEdghXQSo20giMthA7cd28ZC+jts4amQ3YMXiP5oMQ8=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/mod v0.19.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY= golang.org/x/net v0.30.0 h1:AcW1SDZMkb8IpzCdQUaIq2sP4sZ4zw+55h6ynffypl4=
golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/net v0.30.0/go.mod h1:2wGyMJ5iFasEhkwi13ChkO/t1ECNC4X4eBKkVFyYFlU=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/sync v0.8.0 h1:3NFvSEYkUoMifnESzZl15y791HH1qU2xm6eCJU5ZPXQ=
golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs= golang.org/x/sync v0.8.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
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-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-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.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0 h1:KHjCJyddX0LoSTb3J+vWpupP9p0oznkqVk/IfjymZbo=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.26.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/text v0.19.0 h1:kTxAhCbGbxhK0IwgSKiMO5awPoDQ0RpfiVYBfK860YM=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4= golang.org/x/text v0.19.0/go.mod h1:BuEKDfySbSR4drPmRPG/7iBdf8hvFMuRexcpahXilzY=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/time v0.5.0 h1:o7cqy6amK/52YcAKIPlM3a+Fpj35zvRj2TP+e1xFSfk=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/time v0.5.0/go.mod h1:3BpzKBy/shNhVucY/MWOyx10tF3SFh9QdLuxbVysPQM=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.23.0 h1:SGsXPZ+2l4JsgaCKkx+FQ9YZ5XEtA1GZYuoDjenLjvg=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/tools v0.23.0/go.mod h1:pnu6ufv6vQkll6szChhK3C3L/ruaIv5eBeztNG8wtsI=
golang.org/x/text v0.14.0 h1:ScX5w1eTa3QqT8oi6+ziP7dTV1S2+ALU0bI+0zXKWiQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53 h1:X58yt85/IXCx0Y3ZwN6sEIKZzQtDEYaBWrDvErdXrRE=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU= google.golang.org/genproto/googleapis/rpc v0.0.0-20241015192408-796eee8c2d53/go.mod h1:GX3210XPVPUjJbTUbvwI8f2IpZDMZuPJWDzDuebbviI=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= google.golang.org/grpc v1.67.1 h1:zWnc1Vrcno+lHZCOofnIMvycFcc0QRGIzm9dhnDX68E=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= google.golang.org/grpc v1.67.1/go.mod h1:1gLDyUQU7CTLJI90u3nXZ9ekeghjeM7pTDZlqFNg2AA=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= google.golang.org/protobuf v1.35.1 h1:m3LfL6/Ca+fqnjnlqQXNpFPABW1UD7mjh8KO2mKFytA=
golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc= google.golang.org/protobuf v1.35.1/go.mod h1:9fA7Ob0pmnwhb644+1+CVWFRbNajQ6iRojtC/QF5bRE=
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 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-20200227125254-8fa46927fb4f h1:BLraFXnmrev5lT+xlilqcH8XK9/i0At2xKjWk4p6zsU=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20200227125254-8fa46927fb4f/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
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-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 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= 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.6 h1:fO/X46qn5NUEEOZtnjJRWRzZMe8nqJiQ9E+0hi+hKQE=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= gorm.io/driver/sqlite v1.5.6/go.mod h1:U+J8craQU6Fzkcvu8oLeAQmi50TkwPEhHDEjQZXDah4=
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= gorm.io/gorm v1.25.12 h1:I0u8i2hWQItBq1WfE0o2+WuL9+8L21K9e2HHSTE/0f8=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.12/go.mod h1:xh7N7RHfYlNc5EmcI/El95gXusucDrQnHXe0+CgWcLQ=
gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 h1:qDCwdCWECGnwQSQC01Dpnp09fRHxJs9PbktotUqG+hs= gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1 h1:qDCwdCWECGnwQSQC01Dpnp09fRHxJs9PbktotUqG+hs=
gvisor.dev/gvisor v0.0.0-20231020174304-b8a429915ff1/go.mod h1:8hmigyCdYtw5xJGfQDJzSH5Ju8XEIDBnpyi8+O6GRt8= 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.3.0 h1:sJ3XhFINmHSrYCgl958hscfIa3bw8x4DqMP3u1YvoYE=
lukechampine.com/blake3 v1.2.1/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k= lukechampine.com/blake3 v1.3.0/go.mod h1:0OFRp7fBtAylGVCO40o87sbupkyIGgbpv1+M1k1LM6k=
nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50= nullprogram.com/x/optparse v1.0.0/go.mod h1:KdyPE+Igbe0jQUrVfMqDMeJQIJZEuyV7pjYmp6pbG50=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=
+11 -13
View File
@@ -3,35 +3,33 @@ package logger
import ( import (
"fmt" "fmt"
"os" "os"
"s-ui/config"
"time" "time"
"github.com/op/go-logging" "github.com/op/go-logging"
) )
var logger *logging.Logger var (
var logBuffer []struct { logger *logging.Logger
time string logBuffer []struct {
level logging.Level time string
log string level logging.Level
} 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 {
println(err) println("Unable to use syslog: " + err.Error())
backend = logging.NewLogBackend(os.Stderr, "", 0) backend = logging.NewLogBackend(os.Stderr, "", 0)
} }
if ppid > 0 && err != nil { if config.IsSystemd() && 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}`)
+5 -1
View File
@@ -1,6 +1,7 @@
package middleware package middleware
import ( import (
"net"
"net/http" "net/http"
"strings" "strings"
@@ -9,7 +10,10 @@ import (
func DomainValidator(domain string) gin.HandlerFunc { func DomainValidator(domain string) gin.HandlerFunc {
return func(c *gin.Context) { return func(c *gin.Context) {
host := strings.Split(c.Request.Host, ":")[0] host := c.Request.Host
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)
+30 -10
View File
@@ -14,10 +14,12 @@ import (
var ApiAddr string var ApiAddr string
var LastUpdate int64 var LastUpdate int64
var IsSystemd bool
type ConfigService struct { type ConfigService struct {
ClientService ClientService
TlsService TlsService
InDataService
singbox.Controller singbox.Controller
SettingService SettingService
} }
@@ -37,6 +39,7 @@ func NewConfigService() *ConfigService {
} }
func (s *ConfigService) InitConfig() error { func (s *ConfigService) InitConfig() error {
IsSystemd = config.IsSystemd()
configPath := config.GetBinFolderPath() configPath := config.GetBinFolderPath()
data, err := os.ReadFile(configPath + "/config.json") data, err := os.ReadFile(configPath + "/config.json")
if err != nil { if err != nil {
@@ -80,7 +83,7 @@ func (s *ConfigService) GetConfig() (*SingBoxConfig, error) {
func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string) error { func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string) error {
var err error var err error
var clientChanges, tlsChanges, settingChanges, configChanges []model.Changes var clientChanges, tlsChanges, inChanges, settingChanges, configChanges []model.Changes
if _, ok := changes["clients"]; ok { if _, ok := changes["clients"]; ok {
err = json.Unmarshal([]byte(changes["clients"]), &clientChanges) err = json.Unmarshal([]byte(changes["clients"]), &clientChanges)
if err != nil { if err != nil {
@@ -93,6 +96,12 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
return err return err
} }
} }
if _, ok := changes["inData"]; ok {
err = json.Unmarshal([]byte(changes["inData"]), &inChanges)
if err != nil {
return err
}
}
if _, ok := changes["settings"]; ok { if _, ok := changes["settings"]; ok {
err = json.Unmarshal([]byte(changes["settings"]), &settingChanges) err = json.Unmarshal([]byte(changes["settings"]), &settingChanges)
if err != nil { if err != nil {
@@ -128,6 +137,12 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
return err return err
} }
} }
if len(inChanges) > 0 {
err = s.InDataService.Save(tx, inChanges)
if err != nil {
return err
}
}
if len(settingChanges) > 0 { if len(settingChanges) > 0 {
err = s.SettingService.Save(tx, settingChanges) err = s.SettingService.Save(tx, settingChanges)
if err != nil { if err != nil {
@@ -185,14 +200,19 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
// Log changes // Log changes
dt := time.Now().Unix() dt := time.Now().Unix()
allChanges := append(append(clientChanges, settingChanges...), append(configChanges, tlsChanges...)...) allChanges := append(clientChanges, settingChanges...)
for index := range allChanges { allChanges = append(allChanges, configChanges...)
allChanges[index].DateTime = dt allChanges = append(allChanges, tlsChanges...)
allChanges[index].Actor = loginUser allChanges = append(allChanges, inChanges...)
} if len(allChanges) > 0 {
err = tx.Model(model.Changes{}).Create(&allChanges).Error for index := range allChanges {
if err != nil { allChanges[index].DateTime = dt
return err allChanges[index].Actor = loginUser
}
err = tx.Model(model.Changes{}).Create(&allChanges).Error
if err != nil {
return err
}
} }
LastUpdate = dt LastUpdate = dt
@@ -230,7 +250,7 @@ func (s *ConfigService) Save(singboxConfig *SingBoxConfig) error {
return err return err
} }
data, err := json.MarshalIndent(singboxConfig, " ", " ") data, err := json.MarshalIndent(singboxConfig, "", " ")
if err != nil { if err != nil {
return err return err
} }
+46
View File
@@ -0,0 +1,46 @@
package service
import (
"encoding/json"
"s-ui/database"
"s-ui/database/model"
"gorm.io/gorm"
)
type InDataService struct {
}
func (s *InDataService) GetAll() ([]model.InboundData, error) {
db := database.GetDB()
inData := []model.InboundData{}
err := db.Model(model.InboundData{}).Scan(&inData).Error
if err != nil {
return nil, err
}
return inData, nil
}
func (s *InDataService) Save(tx *gorm.DB, changes []model.Changes) error {
var err error
for _, change := range changes {
inData := model.InboundData{}
err = json.Unmarshal(change.Obj, &inData)
if err != nil {
return err
}
switch change.Action {
case "new":
err = tx.Create(&inData).Error
case "del":
err = tx.Where("id = ?", change.Index).Delete(model.InboundData{}).Error
default:
err = tx.Save(inData).Error
}
if err != nil {
return err
}
}
return err
}
+39 -14
View File
@@ -141,21 +141,46 @@ func (s *ServerService) GetSystemInfo() map[string]interface{} {
func (s *ServerService) GetLogs(service string, count string, level string) []string { func (s *ServerService) GetLogs(service string, count string, level string) []string {
c, _ := strconv.Atoi(count) c, _ := strconv.Atoi(count)
var lines []string
if service == "sing-box" { if service == "s-ui" {
cmdArgs := []string{"journalctl", "-u", service, "--no-pager", "-n", count, "-p", level} return logger.GetLogs(c, 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)
} }
var lines []string
var cmdArgs []string
if IsSystemd {
cmdArgs = []string{"journalctl", "-u", service, "--no-pager", "-n", count, "-p", level}
} else {
cmdArgs = []string{"tail", "/logs/" + service + ".log", "-n", count}
}
// 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 get logs!", err.Error()}
}
lines = strings.Split(out.String(), "\n")
return lines return lines
} }
func (s *ServerService) GenKeypair(keyType string, options string) []string {
if len(keyType) == 0 {
return []string{"No keypair to generate"}
}
sbExec := s.GetBinaryPath()
cmdArgs := []string{"generate", keyType + "-keypair"}
if keyType == "tls" || keyType == "ech" {
cmdArgs = append(cmdArgs, options)
}
// Run the command
cmd := exec.Command(sbExec, cmdArgs...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return []string{"Failed to generate keypair"}
}
return strings.Split(out.String(), "\n")
}
+9 -4
View File
@@ -36,6 +36,7 @@ var defaultValueMap = map[string]string{
"subEncode": "true", "subEncode": "true",
"subShowInfo": "false", "subShowInfo": "false",
"subURI": "", "subURI": "",
"subJsonExt": "",
} }
type SettingService struct { type SettingService struct {
@@ -65,7 +66,7 @@ func (s *SettingService) GetAllSetting() (*map[string]string, error) {
} }
// Due to security principles // Due to security principles
delete(allSetting, "webSecret") delete(allSetting, "secret")
return &allSetting, nil return &allSetting, nil
} }
@@ -127,9 +128,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)
@@ -347,6 +348,10 @@ func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) error {
return err return err
} }
func (s *SettingService) GetSubJsonExt() (string, error) {
return s.getString("subJsonExt")
}
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
+2 -1
View File
@@ -10,6 +10,7 @@ import (
statsService "github.com/v2fly/v2ray-core/v5/app/stats/command" statsService "github.com/v2fly/v2ray-core/v5/app/stats/command"
"google.golang.org/grpc" "google.golang.org/grpc"
"google.golang.org/grpc/credentials/insecure"
) )
type V2rayAPI struct { type V2rayAPI struct {
@@ -22,7 +23,7 @@ func (v *V2rayAPI) Init(ApiAddr string) (err error) {
if len(ApiAddr) == 0 { if len(ApiAddr) == 0 {
return common.NewError("The api address is wrong: ", ApiAddr) return common.NewError("The api address is wrong: ", ApiAddr)
} }
v.grpcClient, err = grpc.Dial(ApiAddr, grpc.WithInsecure()) v.grpcClient, err = grpc.NewClient(ApiAddr, grpc.WithTransportCredentials(insecure.NewCredentials()))
if err != nil { if err != nil {
return err return err
} }
+267
View File
@@ -0,0 +1,267 @@
package sub
import (
"encoding/json"
"fmt"
"s-ui/database"
"s-ui/database/model"
"s-ui/service"
"s-ui/util"
)
const defaultJson = `
{
"inbounds": [
{
"type": "tun",
"address": [
"172.19.0.1/30",
"fdfe:dcba:9876::1/126"
],
"mtu": 9000,
"auto_route": true,
"strict_route": false,
"sniff": true,
"endpoint_independent_nat": false,
"stack": "system",
"platform": {
"http_proxy": {
"enabled": true,
"server": "127.0.0.1",
"server_port": 2080
}
}
},
{
"type": "mixed",
"listen": "127.0.0.1",
"listen_port": 2080,
"sniff": true,
"users": []
}
]
}
`
type JsonService struct {
service.SettingService
LinkService
}
func (j *JsonService) GetJson(subId string, format string) (*string, error) {
var jsonConfig map[string]interface{}
client, inDatas, err := j.getData(subId)
if err != nil {
return nil, err
}
outbounds, outTags, err := j.getOutbounds(client.Config, inDatas)
if err != nil {
return nil, err
}
links := j.LinkService.GetLinks(&client.Links, "external", "")
for index, link := range links {
json, tag, err := util.GetOutbound(link, index)
if err == nil && len(tag) > 0 {
*outbounds = append(*outbounds, *json)
*outTags = append(*outTags, tag)
}
}
j.addDefaultOutbounds(outbounds, outTags)
err = json.Unmarshal([]byte(defaultJson), &jsonConfig)
if err != nil {
return nil, err
}
jsonConfig["outbounds"] = outbounds
// Add other objects from settings
j.addOthers(&jsonConfig)
result, _ := json.MarshalIndent(jsonConfig, "", " ")
resultStr := string(result)
return &resultStr, nil
}
func (j *JsonService) getData(subId string) (*model.Client, *[]model.InboundData, error) {
db := database.GetDB()
client := &model.Client{}
err := db.Model(model.Client{}).Where("enable = true and name = ?", subId).First(client).Error
if err != nil {
return nil, nil, err
}
var inbounds []string
err = json.Unmarshal(client.Inbounds, &inbounds)
if err != nil {
return nil, nil, err
}
inDatas := &[]model.InboundData{}
err = db.Model(model.InboundData{}).Where("tag in ?", inbounds).Find(&inDatas).Error
if err != nil {
return nil, nil, err
}
return client, inDatas, nil
}
func (j *JsonService) getOutbounds(clientConfig json.RawMessage, inDatas *[]model.InboundData) (*[]map[string]interface{}, *[]string, error) {
var outbounds []map[string]interface{}
var configs map[string]interface{}
var outTags []string
err := json.Unmarshal(clientConfig, &configs)
if err != nil {
return nil, nil, err
}
for _, inData := range *inDatas {
if len(inData.OutJson) < 5 {
continue
}
var outbound map[string]interface{}
err = json.Unmarshal(inData.OutJson, &outbound)
if err != nil {
return nil, nil, err
}
protocol, _ := outbound["type"].(string)
config, _ := configs[protocol].(map[string]interface{})
for key, value := range config {
if key != "alterId" && key != "name" && key != "username" {
outbound[key] = value
}
}
var addrs []map[string]interface{}
err = json.Unmarshal(inData.Addrs, &addrs)
if err != nil {
return nil, nil, err
}
tag := outbound["tag"].(string)
if len(addrs) == 0 {
outTags = append(outTags, tag)
outbounds = append(outbounds, outbound)
} else {
for index, addr := range addrs {
// Copy original config
newOut := make(map[string]interface{}, len(outbound))
for key, value := range outbound {
newOut[key] = value
}
// Change and push copied config
newOut["server"], _ = addr["server"].(string)
port, _ := addr["server_port"].(float64)
newOut["server_port"] = int(port)
// Override TLS
newTls, overrideTls := addr["tls"].(bool)
if overrideTls {
tlsIf := map[string]interface{}{}
if newTls {
tlsIf["enabled"] = true
newSNI, overrideSNI := addr["server_name"].(string)
if overrideSNI {
tlsIf["server_name"] = newSNI
}
newInsecure, overrideInsecure := addr["insecure"].(bool)
if overrideInsecure {
tlsIf["insecure"] = newInsecure
}
}
newOut["tls"] = tlsIf
}
remark, _ := addr["remark"].(string)
newTag := fmt.Sprintf("%d.%s%s", index+1, tag, remark)
outTags = append(outTags, newTag)
newOut["tag"] = newTag
outbounds = append(outbounds, newOut)
}
}
}
return &outbounds, &outTags, nil
}
func (j *JsonService) addDefaultOutbounds(outbounds *[]map[string]interface{}, outTags *[]string) {
outbound := []map[string]interface{}{
{
"outbounds": append([]string{"auto", "direct"}, *outTags...),
"tag": "proxy",
"type": "selector",
},
{
"tag": "auto",
"type": "urltest",
"outbounds": outTags,
"url": "http://www.gstatic.com/generate_204",
"interval": "10m",
"tolerance": 50,
},
{
"type": "direct",
"tag": "direct",
},
{
"type": "dns",
"tag": "dns-out",
},
{
"type": "block",
"tag": "block",
},
}
*outbounds = append(outbound, *outbounds...)
}
func (j *JsonService) addOthers(jsonConfig *map[string]interface{}) error {
rules := []interface{}{
map[string]interface{}{
"clash_mode": "Direct",
"outbound": "direct",
},
map[string]interface{}{
"clash_mode": "Global",
"outbound": "proxy",
},
}
route := map[string]interface{}{
"auto_detect_interface": true,
"final": "proxy",
"rules": rules,
}
othersStr, err := j.SettingService.GetSubJsonExt()
if err != nil {
return err
}
if len(othersStr) == 0 {
(*jsonConfig)["route"] = route
return nil
}
var othersJson map[string]interface{}
err = json.Unmarshal([]byte(othersStr), &othersJson)
if err != nil {
return err
}
if _, ok := othersJson["log"]; ok {
(*jsonConfig)["log"] = othersJson["log"]
}
if _, ok := othersJson["dns"]; ok {
(*jsonConfig)["dns"] = othersJson["dns"]
}
if _, ok := othersJson["inbounds"]; ok {
(*jsonConfig)["inbounds"] = othersJson["inbounds"]
}
if _, ok := othersJson["experimental"]; ok {
(*jsonConfig)["experimental"] = othersJson["experimental"]
}
if _, ok := othersJson["rule_set"]; ok {
route["rule_set"] = othersJson["rule_set"]
}
if settingRules, ok := othersJson["rules"].([]interface{}); ok {
route["rules"] = append(rules, settingRules...)
}
(*jsonConfig)["route"] = route
return nil
}
+103
View File
@@ -0,0 +1,103 @@
package sub
import (
"crypto/tls"
"encoding/json"
"io"
"net/http"
"s-ui/logger"
"s-ui/util"
"strings"
)
type Link struct {
Type string `json:"type"`
Remark string `json:"remark"`
Uri string `json:"uri"`
}
type LinkService struct {
}
func (s *LinkService) GetLinks(linkJson *json.RawMessage, types string, clientInfo string) []string {
links := []Link{}
var result []string
err := json.Unmarshal(*linkJson, &links)
if err != nil {
return nil
}
for _, link := range links {
switch link.Type {
case "external":
result = append(result, link.Uri)
case "sub":
result = append(result, s.getExternalSub(link.Uri)...)
case "local":
if types == "all" {
result = append(result, s.addClientInfo(link.Uri, clientInfo))
}
}
}
return result
}
func (s *LinkService) addClientInfo(uri string, clientInfo string) string {
if len(clientInfo) == 0 {
return uri
}
protocol := strings.Split(uri, "://")
if len(protocol) < 2 {
return uri
}
switch protocol[0] {
case "vmess":
var vmessJson map[string]interface{}
config, err := util.B64StrToByte(protocol[1])
if err != nil {
logger.Warning("sub: Error decoding vmess content:", err)
return uri
}
err = json.Unmarshal(config, &vmessJson)
if err != nil {
logger.Warning("sub: Error decoding vmess content:", err)
return uri
}
vmessJson["ps"] = vmessJson["ps"].(string) + clientInfo
result, err := json.MarshalIndent(vmessJson, "", " ")
if err != nil {
logger.Warning("sub: Error decoding vmess + clientInfo content:", err)
return uri
}
return "vmess://" + util.ByteToB64Str(result)
default:
return uri + clientInfo
}
}
func (s *LinkService) 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 nil
}
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 nil
}
// Convert if the content is Base64 encoded
links := util.StrOrBase64Encoded(string(body))
return strings.Split(links, "\n")
}
+21 -9
View File
@@ -10,6 +10,7 @@ import (
type SubHandler struct { type SubHandler struct {
service.SettingService service.SettingService
SubService SubService
JsonService
} }
func NewSubHandler(g *gin.RouterGroup) { func NewSubHandler(g *gin.RouterGroup) {
@@ -23,17 +24,28 @@ func (s *SubHandler) initRouter(g *gin.RouterGroup) {
func (s *SubHandler) subs(c *gin.Context) { func (s *SubHandler) subs(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
result, headers, err := s.SubService.GetSubs(subId) format, isFormat := c.GetQuery("format")
if err != nil || result == nil { if isFormat {
logger.Error(err) result, err := s.JsonService.GetJson(subId, format)
c.String(400, "Error!") if err != nil || result == nil {
logger.Error(err)
c.String(400, "Error!")
} else {
c.String(200, *result)
}
} else { } else {
result, headers, err := s.SubService.GetSubs(subId)
if err != nil || result == nil {
logger.Error(err)
c.String(400, "Error!")
} else {
// Add headers // Add headers
c.Writer.Header().Set("Subscription-Userinfo", headers[0]) c.Writer.Header().Set("Subscription-Userinfo", headers[0])
c.Writer.Header().Set("Profile-Update-Interval", headers[1]) c.Writer.Header().Set("Profile-Update-Interval", headers[1])
c.Writer.Header().Set("Profile-Title", headers[2]) c.Writer.Header().Set("Profile-Title", headers[2])
c.String(200, *result) c.String(200, *result)
}
} }
} }
+3 -102
View File
@@ -1,15 +1,10 @@
package sub package sub
import ( import (
"crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"io"
"net/http"
"s-ui/database" "s-ui/database"
"s-ui/database/model" "s-ui/database/model"
"s-ui/logger"
"s-ui/service" "s-ui/service"
"strings" "strings"
"time" "time"
@@ -17,12 +12,7 @@ import (
type SubService struct { type SubService struct {
service.SettingService service.SettingService
} LinkService
type Link struct {
Type string `json:"type"`
Remark string `json:"remark"`
Uri string `json:"uri"`
} }
func (s *SubService) GetSubs(subId string) (*string, []string, error) { func (s *SubService) GetSubs(subId string) (*string, []string, error) {
@@ -35,29 +25,14 @@ func (s *SubService) GetSubs(subId string) (*string, []string, error) {
return nil, nil, err return nil, nil, err
} }
links := []Link{}
err = json.Unmarshal([]byte(client.Links), &links)
if err != nil {
return nil, nil, err
}
clientInfo := "" clientInfo := ""
subShowInfo, _ := s.SettingService.GetSubShowInfo() subShowInfo, _ := s.SettingService.GetSubShowInfo()
if subShowInfo { if subShowInfo {
clientInfo = s.getClientInfo(client) clientInfo = s.getClientInfo(client)
} }
var result string linksArray := s.LinkService.GetLinks(&client.Links, "all", clientInfo)
for _, link := range links { result := strings.Join(linksArray, "\n")
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 var headers []string
updateInterval, _ := s.SettingService.GetSubUpdates() updateInterval, _ := s.SettingService.GetSubUpdates()
@@ -90,80 +65,6 @@ func (s *SubService) getClientInfo(c *model.Client) string {
} }
} }
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 { func (s *SubService) formatTraffic(trafficBytes int64) string {
if trafficBytes < 1024 { if trafficBytes < 1024 {
return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1)) return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1))
+20
View File
@@ -0,0 +1,20 @@
package util
import "encoding/base64"
// Function to return decoded bytes if a string is Base64 encoded
func StrOrBase64Encoded(str string) string {
decoded, err := base64.StdEncoding.DecodeString(str)
if err == nil {
return string(decoded)
}
return str
}
func B64StrToByte(str string) ([]byte, error) {
return base64.StdEncoding.DecodeString(str)
}
func ByteToB64Str(b []byte) string {
return base64.StdEncoding.EncodeToString(b)
}
+473
View File
@@ -0,0 +1,473 @@
package util
import (
"encoding/json"
"fmt"
"net"
"net/url"
"s-ui/util/common"
"strconv"
"strings"
)
func GetOutbound(uri string, i int) (*map[string]interface{}, string, error) {
u, err := url.Parse(uri)
if err == nil {
switch u.Scheme {
case "vmess":
return vmess(u.Host, i)
case "vless":
return vless(u, i)
case "trojan":
return trojan(u, i)
case "hy", "hysteria":
return hy(u, i)
case "hy2", "hysteria2":
return hy2(u, i)
case "tuic":
return tuic(u, i)
case "ss", "shadowsocks":
return ss(u, i)
}
}
return nil, "", common.NewError("Unsupported link format")
}
func vmess(data string, i int) (*map[string]interface{}, string, error) {
dataByte, err := B64StrToByte(data)
if err != nil {
return nil, "", err
}
var dataJson map[string]interface{}
err = json.Unmarshal(dataByte, &dataJson)
if err != nil {
return nil, "", err
}
transport := map[string]interface{}{}
tp_net, _ := dataJson["net"].(string)
tp_type, _ := dataJson["type"].(string)
tp_host, _ := dataJson["host"].(string)
tp_path, _ := dataJson["path"].(string)
switch strings.ToLower(tp_net) {
case "tcp", "":
if tp_type == "http" {
transport["type"] = tp_type
if len(tp_host) > 0 {
transport["host"] = strings.Split(tp_host, ",")
}
transport["path"] = tp_path
}
case "http", "h2":
transport["type"] = "http"
if len(tp_host) > 0 {
transport["host"] = strings.Split(tp_host, ",")
}
transport["path"] = tp_path
case "ws":
transport["type"] = tp_net
transport["path"] = tp_path
transport["early_data_header_name"] = "Sec-WebSocket-Protocol"
if len(tp_host) > 0 {
transport["headers"] = map[string]interface{}{
"Host": tp_host,
}
}
case "quic":
transport["type"] = tp_net
case "grpc":
transport["type"] = tp_net
transport["service_name"] = tp_path
case "httpupgrade":
transport["type"] = tp_net
transport["path"] = tp_path
transport["host"] = tp_host
default:
return nil, "", common.NewError("Invalid vmess")
}
tls := map[string]interface{}{}
vmess_tls, _ := dataJson["tls"].(string)
if vmess_tls == "tls" {
tls["enabled"] = true
tls_sni, _ := dataJson["sni"].(string)
tls_alpn, _ := dataJson["alpn"].(string)
_, tls_insecure := dataJson["allowInsecure"]
tls_fp, _ := dataJson["fp"].(string)
if len(tls_sni) > 0 {
tls["server_name"] = tls_sni
}
if len(tls_alpn) > 0 {
tls["alpn"] = strings.Split(tls_alpn, ",")
}
if tls_insecure {
tls["insecure"] = true
}
if len(tls_fp) > 0 {
tls["utls"] = map[string]interface{}{
"enabled": true,
"fingerprint": tls_fp,
}
}
}
tag, _ := dataJson["ps"].(string)
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, tag)
}
alter_id, ok := dataJson["aid"].(int)
if !ok {
alter_id = 0
}
vmess := map[string]interface{}{
"type": "vmess",
"tag": tag,
"server": dataJson["add"],
"server_port": dataJson["port"],
"uuid": dataJson["id"],
"security": "auto",
"alter_id": alter_id,
"tls": tls,
"transport": transport,
}
return &vmess, tag, err
}
func vless(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
security := query.Get("security")
host, portStr, _ := net.SplitHostPort(u.Host)
port := 80
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
} else {
if security == "tls" || security == "reality" {
port = 443
}
}
tp_type := query.Get("type")
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
vless := map[string]interface{}{
"type": "vless",
"tag": tag,
"server": host,
"server_port": port,
"uuid": u.User.Username(),
"flow": query.Get("flow"),
"tls": getTls(security, &query),
"transport": getTransport(tp_type, &query),
}
return &vless, tag, nil
}
func trojan(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
security := query.Get("security")
host, portStr, _ := net.SplitHostPort(u.Host)
port := 80
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
} else {
if security == "tls" || security == "reality" {
port = 443
}
}
tp_type := query.Get("type")
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
trojan := map[string]interface{}{
"type": "trojan",
"tag": tag,
"server": host,
"server_port": port,
"password": u.User.Username(),
"tls": getTls(security, &query),
"transport": getTransport(tp_type, &query),
}
return &trojan, tag, nil
}
func hy(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
}
tls := map[string]interface{}{
"enabled": true,
"server_name": query.Get("peer"),
}
alpn := query.Get("alpn")
insecure := query.Get("insecure")
if len(alpn) > 0 {
tls["alpn"] = strings.Split(alpn, ",")
}
if insecure == "1" || insecure == "true" {
tls["insecure"] = true
}
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
hy := map[string]interface{}{
"type": "hysteria",
"tag": tag,
"server": host,
"server_port": port,
"obfs": query.Get("obfsParam"),
"auth_str": query.Get("auth"),
"tls": tls,
}
down, _ := strconv.Atoi(query.Get("downmbps"))
up, _ := strconv.Atoi(query.Get("upmbps"))
recv_window_conn, _ := strconv.Atoi(query.Get("recv_window_conn"))
recv_window, _ := strconv.Atoi(query.Get("recv_window"))
if down > 0 {
hy["down_mbps"] = down
}
if up > 0 {
hy["up_mbps"] = up
}
if recv_window_conn > 0 {
hy["recv_window_conn"] = recv_window_conn
}
if recv_window > 0 {
hy["recv_window"] = recv_window
}
return &hy, tag, nil
}
func hy2(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
}
tls := map[string]interface{}{
"enabled": true,
"server_name": query.Get("sni"),
}
alpn := query.Get("alpn")
insecure := query.Get("insecure")
if len(alpn) > 0 {
tls["alpn"] = strings.Split(alpn, ",")
}
if insecure == "1" || insecure == "true" {
tls["insecure"] = true
}
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
hy2 := map[string]interface{}{
"type": "hysteria2",
"tag": tag,
"server": host,
"server_port": port,
"password": u.User.Username(),
"tls": tls,
}
down, _ := strconv.Atoi(query.Get("downmbps"))
up, _ := strconv.Atoi(query.Get("upmbps"))
obfs := query.Get("obfs")
if down > 0 {
hy2["down_mbps"] = down
}
if up > 0 {
hy2["up_mbps"] = up
}
if obfs == "salamander" {
hy2["obfs"] = map[string]interface{}{
"type": "salamander",
"password": query.Get("obfs-password"),
}
}
return &hy2, tag, nil
}
func tuic(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
}
tls := map[string]interface{}{
"enabled": true,
"server_name": query.Get("sni"),
}
alpn := query.Get("alpn")
insecure := query.Get("allow_insecure")
disable_sni := query.Get("disable_sni")
if len(alpn) > 0 {
tls["alpn"] = strings.Split(alpn, ",")
}
if insecure == "1" || insecure == "true" {
tls["insecure"] = true
}
if disable_sni == "1" || disable_sni == "true" {
tls["disable_sni"] = true
}
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
password, _ := u.User.Password()
tuic := map[string]interface{}{
"type": "tuic",
"tag": tag,
"server": host,
"server_port": port,
"uuid": u.User.Username(),
"password": password,
"congestion_control": query.Get("congestion_control"),
"udp_relay_mode": query.Get("udp_relay_mode"),
"tls": tls,
}
return &tuic, tag, nil
}
func ss(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
}
method := u.User.Username()
password, ok := u.User.Password()
if !ok {
decrypted := StrOrBase64Encoded(method)
decrypted_arr := strings.Split(decrypted, ":")
if len(decrypted_arr) > 1 {
method = decrypted_arr[0]
password = strings.Join(decrypted_arr[1:], ":")
} else {
return nil, "", common.NewError("Unsupported shadowsocks")
}
}
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
ss := map[string]interface{}{
"type": "shadowsocks",
"tag": tag,
"server": host,
"server_port": port,
"method": method,
"password": password,
}
v2ray_type := query.Get("type")
if len(v2ray_type) > 0 {
pl_arr := []string{}
host_header := query.Get("host")
if query.Get("security") == "tls" {
pl_arr = append(pl_arr, "tls")
}
if v2ray_type == "quic" {
pl_arr = append(pl_arr, "mode=quic")
}
if len(host_header) > 0 {
pl_arr = append(pl_arr, "host="+host_header)
}
ss["plugin"] = "v2ray-plugin"
ss["plugin_opts"] = strings.Join(pl_arr, ";")
}
plugin := query.Get("plugin")
if len(plugin) > 0 {
pl_arr := strings.Split(plugin, ";")
if len(pl_arr) > 0 {
ss["plugin"] = pl_arr[0]
ss["plugin_opts"] = strings.Join(pl_arr[1:], ";")
}
}
return &ss, tag, nil
}
func getTransport(tp_type string, q *url.Values) *map[string]interface{} {
transport := map[string]interface{}{}
tp_host := q.Get("host")
tp_path := q.Get("path")
switch strings.ToLower(tp_type) {
case "tcp", "":
if q.Get("headerType") == "http" {
transport["type"] = "http"
if len(tp_host) > 0 {
transport["host"] = strings.Split(tp_host, ",")
}
transport["path"] = tp_path
}
case "http", "h2":
transport["type"] = "http"
if len(tp_host) > 0 {
transport["host"] = strings.Split(tp_host, ",")
}
transport["path"] = tp_path
case "ws":
transport["type"] = "ws"
transport["path"] = tp_path
if len(tp_host) > 0 {
transport["headers"] = map[string]interface{}{
"Host": tp_host,
}
}
case "quic":
transport["type"] = "quic"
case "grpc":
transport["type"] = "grpc"
transport["service_name"] = q.Get("serviceName")
case "httpupgrade":
transport["type"] = "httpupgrade"
transport["path"] = tp_path
transport["host"] = tp_host
}
return &transport
}
func getTls(security string, q *url.Values) *map[string]interface{} {
tls := map[string]interface{}{}
tls_fp := q.Get("fp")
tls_sni := q.Get("sni")
tls_insecure := q.Get("allowInsecure")
tls_alpn := q.Get("alpn")
switch security {
case "tls":
tls["enabled"] = true
case "reality":
tls["enabled"] = true
tls["reality"] = map[string]interface{}{
"enabled": true,
"public_key": q.Get("pbk"),
"short_id": q.Get("sid"),
}
}
if len(tls_sni) > 0 {
tls["server_name"] = tls_sni
}
if len(tls_alpn) > 0 {
tls["alpn"] = strings.Split(tls_alpn, ",")
}
if tls_insecure == "1" || tls_insecure == "true" {
tls["insecure"] = true
}
if len(tls_fp) > 0 {
tls["utls"] = map[string]interface{}{
"enabled": true,
"fingerprint": tls_fp,
}
}
return &tls
}
+2 -2
View File
@@ -18,9 +18,9 @@ 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"
) )
+2 -2
View File
@@ -1,8 +1,8 @@
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS singbox-builder FROM --platform=$BUILDPLATFORM golang:1.23-alpine AS singbox-builder
LABEL maintainer="Alireza <alireza7@gmail.com>" LABEL maintainer="Alireza <alireza7@gmail.com>"
WORKDIR /app WORKDIR /app
ARG TARGETOS TARGETARCH ARG TARGETOS TARGETARCH
ARG SINGBOX_VER=v1.8.13 ARG SINGBOX_VER=v1.10.1
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 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="" ARG GOPROXY=""
ENV GOPROXY ${GOPROXY} ENV GOPROXY ${GOPROXY}
+13 -4
View File
@@ -20,7 +20,17 @@ terminateSingbox()
fi fi
} }
reloadSingbox()
{
if kill -0 $tokill > /dev/null 2>&1; then
kill -HUP $tokill
else
runSingbox
fi
}
trap terminateSingbox SIGINT SIGTERM SIGKILL trap terminateSingbox SIGINT SIGTERM SIGKILL
trap reloadSingbox SIGHUP
runSingbox runSingbox
@@ -32,13 +42,12 @@ do
echo "Signal received: $signal" echo "Signal received: $signal"
# Remove singnal file # Remove singnal file
rm -f signal >> /dev/null 2>&1 rm -f signal >> /dev/null 2>&1
case ${signal} in case ${signal} in
"stop") "stop")
terminateSingbox terminateSingbox
;; ;;
"restart") "restart")
terminateSingbox reloadSingbox
runSingbox
;; ;;
esac esac
fi fi
@@ -47,7 +56,7 @@ do
if ! kill -0 $tokill > /dev/null 2>&1; then if ! kill -0 $tokill > /dev/null 2>&1; then
if [ "$signal" != "stop" ]; then if [ "$signal" != "stop" ]; then
echo "Sing-Box with PID $tokill crashed. Breaking the loop..." echo "Sing-Box with PID $tokill crashed. Breaking the loop..."
break exit 1
fi fi
fi fi
done done
+42 -5
View File
@@ -6,8 +6,9 @@ services:
hostname: "S-UI docker" hostname: "S-UI docker"
volumes: volumes:
- "singbox:/app/bin" - "singbox:/app/bin"
- "$PWD/db:/app/db" - "./db:/app/db"
- "$PWD/cert:/app/cert" - "./cert:/app/cert"
- "logs:/logs"
environment: environment:
SINGBOX_API: "sing-box:1080" SINGBOX_API: "sing-box:1080"
SUI_DB_FOLDER: "db" SUI_DB_FOLDER: "db"
@@ -18,14 +19,23 @@ services:
- "2096:2096" - "2096:2096"
networks: networks:
- s-ui - s-ui
entrypoint: "./sui" links:
- syslog
logging:
driver: syslog
options:
tag: "s-ui"
syslog-address: "udp://127.0.0.1:1514"
entrypoint: "./entrypoint.sh"
depends_on:
- syslog
sing-box: sing-box:
image: alireza7/s-ui-singbox image: alireza7/s-ui-singbox
container_name: sing-box container_name: sing-box
volumes: volumes:
- "singbox:/app/" - "singbox:/app/"
- "$PWD/cert:/cert" - "./cert:/cert"
networks: networks:
- s-ui - s-ui
ports: ports:
@@ -34,12 +44,39 @@ services:
- "2443:2443" - "2443:2443"
- "3443:3443" - "3443:3443"
restart: unless-stopped restart: unless-stopped
links:
- syslog
logging:
driver: syslog
options:
tag: "sing-box"
syslog-address: "udp://127.0.0.1:1514"
depends_on: depends_on:
- s-ui - s-ui
- syslog
syslog:
image: rsyslog/syslog_appliance_alpine
container_name: syslog
volumes:
- "logs:/logs"
networks:
- s-ui
ports:
- "127.0.0.1:1514:1514/udp"
restart: unless-stopped
environment:
- RSYSLOG_CONF_GLOBAL_CONF=template(name="RemoteLogs" type="string" string="/logs/%programname%.log")
- RSYSLOG_CONF_INPUT_UDP="input(type=\"imudp\" port=\"1514\" ruleset=\"remote\")"
- RSYSLOG_CONF_RULESET_REMOTE="ruleset(name=\"remote\") { action(type=\"omfile\" dynaFile=\"RemoteLogs\") }"
command: >
sh -c 'touch /config/container_config'
networks: networks:
s-ui: s-ui:
driver: bridge driver: bridge
volumes: volumes:
singbox: logs:
singbox:
Executable
+4
View File
@@ -0,0 +1,4 @@
#!/bin/sh
./sui migrate
./sui
+1001 -1104
View File
File diff suppressed because it is too large Load Diff
+14 -16
View File
@@ -1,6 +1,6 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "1.1.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite --host", "dev": "vite --host",
@@ -9,8 +9,8 @@
"lint": "eslint . --fix --ignore-path .gitignore" "lint": "eslint . --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"@mdi/font": "7.0.96", "@mdi/font": "7.4.47",
"axios": "^1.7.2", "axios": "^1.7.4",
"chart.js": "^4.4.3", "chart.js": "^4.4.3",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"core-js": "^3.37.1", "core-js": "^3.37.1",
@@ -19,26 +19,24 @@
"pinia": "^2.1.7", "pinia": "^2.1.7",
"qrcode.vue": "^3.4.1", "qrcode.vue": "^3.4.1",
"roboto-fontface": "^0.10.0", "roboto-fontface": "^0.10.0",
"vue": "^3.2.0", "vue": "^3.4.31",
"vue-chartjs": "^5.3.1", "vue-chartjs": "^5.3.1",
"vue-i18n": "^9.13.1", "vue-i18n": "^9.13.1",
"vue-router": "^4.3.2", "vue-router": "^4.4.0",
"vue3-persian-datetime-picker": "^1.2.2", "vue3-persian-datetime-picker": "^1.2.2",
"vuetify": "^3.6.7" "vuetify": "^3.6.10"
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.24.5", "@babel/types": "^7.24.7",
"@types/node": "^18.19.33", "@types/node": "^20.14.9",
"@vitejs/plugin-vue": "^4.6.2", "@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-typescript": "^11.0.3",
"eslint": "^8.57.0",
"eslint-plugin-vue": "^9.26.0", "eslint-plugin-vue": "^9.26.0",
"material-design-icons-iconfont": "^6.7.0", "material-design-icons-iconfont": "^6.7.0",
"sass": "^1.77.2", "sass": "1.77.6",
"typescript": "^5.4.5", "typescript": "^5.5.2",
"unplugin-fonts": "^1.1.1", "unplugin-fonts": "^1.1.1",
"vite": "^4.5.3", "vite": "^5.4.6",
"vite-plugin-vuetify": "^1.0.2", "vite-plugin-vuetify": "^2.0.3",
"vue-tsc": "^1.8.27" "vue-tsc": "^2.0.22"
} }
} }
+114
View File
@@ -0,0 +1,114 @@
<template>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.addr')"
hide-details
required
v-model="addr.server">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.port')"
hide-details
type="number"
required
v-model.number="addr.server_port"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionRemark">
<v-text-field
:label="$t('in.remark')"
hide-details
v-model="addr.remark">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="optionTLS">
<v-switch
:label="$t('tls.enable')"
color="primary"
hide-details
@update:model-value="updateTls($event)"
v-model="addr.tls" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionSNI">
<v-text-field
label="SNI"
hide-details
v-model="addr.server_name">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionInsecure">
<v-switch
:label="$t('tls.insecure')"
hide-details
color="primary"
v-model="addr.insecure" />
</v-col>
</v-row>
<v-row>
<v-spacer></v-spacer>
<v-col cols="auto" align="end" justify="center">
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('in.mdOption') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionRemark" color="primary" :label="$t('in.remark')" hide-details></v-switch>
</v-list-item>
<v-list-item v-if="hasTls">
<v-switch v-model="optionTLS" color="primary" :label="$t('objects.tls')" hide-details></v-switch>
</v-list-item>
<v-list-item v-if="addr.tls">
<v-switch v-model="optionSNI" color="primary" label="SNI" hide-details></v-switch>
</v-list-item>
<v-list-item v-if="addr.tls">
<v-switch v-model="optionInsecure" color="primary" :label="$t('tls.insecure')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-col>
</v-row>
</template>
<script lang="ts">
export default {
props: ['addr', 'hasTls'],
data() {
return {
menu: false
}
},
computed: {
optionTLS: {
get(): boolean { return this.$props.addr.tls != undefined },
set(v:boolean) { this.$props.addr.tls = v ? true : undefined; this.updateTls(v) }
},
optionSNI: {
get(): boolean { return this.$props.addr.server_name != undefined },
set(v:boolean) { this.$props.addr.server_name = v ? '' : undefined }
},
optionRemark: {
get(): boolean { return this.$props.addr.remark != undefined },
set(v:boolean) { this.$props.addr.remark = v ? '' : undefined }
},
optionInsecure: {
get(): boolean { return this.$props.addr.insecure != undefined },
set(v:boolean) { this.$props.addr.insecure = v ? false : undefined }
}
},
methods: {
updateTls(v:boolean) {
if (!v) {
delete this.$props.addr.insecure
delete this.$props.addr.server_name
}
}
}
}
</script>
+9 -1
View File
@@ -42,6 +42,7 @@
<script lang="ts"> <script lang="ts">
import DatePicker from 'vue3-persian-datetime-picker' import DatePicker from 'vue3-persian-datetime-picker'
import { i18n } from '@/locales' import { i18n } from '@/locales'
import 'moment/locale/vi'
import 'moment/locale/zh-cn' import 'moment/locale/zh-cn'
import 'moment/locale/zh-tw' import 'moment/locale/zh-tw'
@@ -58,7 +59,14 @@ export default {
computed: { computed: {
locale() { locale() {
const l = i18n.global.locale.value const l = i18n.global.locale.value
return l.replace('zh', 'zh-') switch (l) {
case "zhHans":
return "zh-cn"
case "zhHant":
return "zh-tw"
default:
return l
}
}, },
dateFormatted() { dateFormatted() {
if (this.expDate == 0) return i18n.global.t('unlimited') if (this.expDate == 0) return i18n.global.t('unlimited')
+1 -1
View File
@@ -89,7 +89,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('dial.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('dial.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
+3 -3
View File
@@ -1,6 +1,6 @@
<template> <template>
<v-card :subtitle="$t('objects.listen')"> <v-card :subtitle="$t('objects.listen')">
<v-row> <v-row v-if="inbound.type != 'tun'">
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
:label="$t('in.addr')" :label="$t('in.addr')"
@@ -78,11 +78,11 @@
</v-select> </v-select>
</v-col> </v-col>
</v-row> </v-row>
<v-card-actions class="pt-0"> <v-card-actions class="pt-0" v-if="inbound.type != 'tun'">
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('listen.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('listen.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
+15 -2
View File
@@ -5,7 +5,7 @@
:logType="logModal.logType" :logType="logModal.logType"
@close="closeLogs" @close="closeLogs"
/> />
<v-container class="fill-height"> <v-container class="fill-height" :loading="loading">
<v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" > <v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" >
<v-row class="d-flex align-center justify-center"> <v-row class="d-flex align-center justify-center">
<v-col cols="auto"> <v-col cols="auto">
@@ -16,7 +16,7 @@
<v-col cols="auto"> <v-col cols="auto">
<v-dialog v-model="menu" :close-on-content-click="false" transition="scale-transition" max-width="800"> <v-dialog v-model="menu" :close-on-content-click="false" transition="scale-transition" max-width="800">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" variant="tonal">{{ $t('main.tiles') }} <v-icon icon="mdi-star-plus" /></v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('main.tiles') }} <v-icon icon="mdi-star-plus" /></v-btn>
</template> </template>
<v-card rounded="xl"> <v-card rounded="xl">
<v-card-title> <v-card-title>
@@ -116,6 +116,12 @@
</v-tooltip> </v-tooltip>
<v-icon icon="mdi-list-box-outline" :color="tilesData.sbd?.running ? 'success': 'error'" /> <v-icon icon="mdi-list-box-outline" :color="tilesData.sbd?.running ? 'success': 'error'" />
</v-chip> </v-chip>
<v-chip density="compact" color="transparent" v-if="tilesData.sbd?.running && !loading" style="cursor: pointer;" @click="restartSingbox()">
<v-tooltip activator="parent" location="top">
{{ $t('actions.restartSb') }}
</v-tooltip>
<v-icon icon="mdi-restart" color="warning" />
</v-chip>
</v-col> </v-col>
<v-col cols="4">{{ $t('main.info.memory') }}</v-col> <v-col cols="4">{{ $t('main.info.memory') }}</v-col>
<v-col cols="8"> <v-col cols="8">
@@ -168,6 +174,7 @@ import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { i18n } from '@/locales' import { i18n } from '@/locales'
import LogVue from '@/layouts/modals/Logs.vue' import LogVue from '@/layouts/modals/Logs.vue'
const loading = ref(false)
const menu = ref(false) const menu = ref(false)
const menuItems = [ const menuItems = [
{ title: i18n.global.t('main.gauges'), value: [ { title: i18n.global.t('main.gauges'), value: [
@@ -249,4 +256,10 @@ const closeLogs = () => {
logModal.value.logType = "s-ui" logModal.value.logType = "s-ui"
logModal.value.visible = false logModal.value.visible = false
} }
const restartSingbox = async () => {
loading.value = true
await HttpUtils.post('api/restartSb',{})
loading.value = false
}
</script> </script>
+1
View File
@@ -85,6 +85,7 @@ export default {
}, },
computed: { computed: {
mux(): oMultiplex { mux(): oMultiplex {
if (!Object.hasOwn(this.$props.data,"multiplex")) this.$props.data.multiplex = {}
return <oMultiplex> this.$props.data.multiplex return <oMultiplex> this.$props.data.multiplex
}, },
muxEnable: { muxEnable: {
+123
View File
@@ -0,0 +1,123 @@
<template>
<v-card :subtitle="type">
<v-row>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.SOCKS">
<v-select
hide-details
:items="['4','4a','5']"
:label="$t('version')"
v-model="inData.outJson.version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="needNetwork">
<Network :data="inData.outJson" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="needUot">
<UoT :data="inData.outJson" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.HTTP">
<v-text-field
:label="$t('transport.path')"
hide-details
v-model="inData.outJson.path">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.VMess || type == inTypes.VLESS">
<v-select
hide-details
:label="$t('types.vless.udpEnc')"
:items="['none','packetaddr','xudp']"
v-model="packet_encoding">
</v-select>
</v-col>
<template v-if="type == inTypes.VMess">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('types.vmess.security')"
:items="vmessSecurities"
v-model="inData.outJson.security">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inData.outJson.global_padding" color="primary" :label="$t('types.vmess.globalPadding')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inData.outJson.authenticated_length" color="primary" :label="$t('types.vmess.authLen')" hide-details></v-switch>
</v-col>
</template>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.Hysteria">
<v-text-field
label="Recv window"
hide-details
type="number"
min="0"
v-model.number="inData.outJson.recv_window">
</v-text-field>
</v-col>
<template v-if="type == inTypes.TUIC">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
label="UDP Relay Mode"
:items="['native', 'quic']"
clearable
@click:clear="delete inData.outJson.udp_relay_mode"
v-model="inData.outJson.udp_relay_mode">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="UDP Over Stream" v-model="inData.outJson.udp_over_stream" hide-details></v-switch>
</v-col>
</template>
</v-row>
<Headers :data="inData.outJson" v-if="type == inTypes.HTTP" />
</v-card>
</template>
<script lang="ts">
import { InTypes } from '@/types/inbounds'
import Network from './Network.vue'
import UoT from './UoT.vue'
import Headers from './Headers.vue'
export default {
props: ['inData', 'type'],
data() {
return {
inTypes: InTypes,
vmessSecurities: [
"auto",
"none",
"zero",
"aes-128-gcm",
"aes-128-ctr",
"chacha20-poly1305",
],
haveNetwork: [
InTypes.SOCKS,
InTypes.Shadowsocks,
InTypes.VMess,
InTypes.Trojan,
InTypes.Hysteria,
InTypes.VLESS,
InTypes.TUIC,
InTypes.Hysteria2,
],
havUoT: [
InTypes.SOCKS,
InTypes.Shadowsocks,
],
}
},
computed: {
needNetwork():boolean { return this.haveNetwork.includes(this.$props.type) },
needUot():boolean { return this.havUoT.includes(this.$props.type) },
packet_encoding: {
get() { return this.$props.inData.outJson.packet_encoding != undefined ? this.$props.inData.outJson.packet_encoding : 'none'; },
set(v:string) { this.$props.inData.outJson.packet_encoding = v != "none" ? v : undefined }
},
},
components: { Network, UoT, Headers }
}
</script>
+1 -1
View File
@@ -116,7 +116,7 @@
</v-col> </v-col>
<v-col cols="12" sm="6" v-if="rule.source_ip_cidr != undefined"> <v-col cols="12" sm="6" v-if="rule.source_ip_cidr != undefined">
<v-text-field <v-text-field
:label="$t('rule.srcIp') + ' ' + $t('commaSeparated')" :label="$t('rule.srcCidr') + ' ' + $t('commaSeparated')"
hide-details hide-details
v-model="source_ip_cidr"></v-text-field> v-model="source_ip_cidr"></v-text-field>
</v-col> </v-col>
+452
View File
@@ -0,0 +1,452 @@
<template>
<v-card>
<v-row>
<v-col cols="12" sm="6" md="3">
<v-select
v-model="ruleToDirect"
:items="geoList"
:label="$t('setting.toDirect')"
multiple
chips
hide-details
></v-select>
</v-col>
<v-col cols="12" sm="6" md="3">
<v-select
v-model="ruleToBlock"
:items="geoList"
:label="$t('setting.toBlock')"
multiple
chips
hide-details
></v-select>
</v-col>
</v-row>
<v-row v-if="enableLog">
<v-col cols="12" sm="6" md="3" lg="2">
<v-select
hide-details
:label="$t('basic.log.level')"
:items="levels"
v-model="subJsonExt.log.level">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2">
<v-switch v-model="subJsonExt.log.timestamp" color="primary" :label="$t('setting.timestamp')" hide-details />
</v-col>
</v-row>
<v-row v-if="enableDns">
<v-col cols="12" sm="6" md="3" lg="2">
<v-text-field
v-model="proxyDns"
hide-details
:label="$t('setting.globalDns')"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2">
<v-text-field
v-model="directDns"
hide-details
clearable
:label="$t('setting.directDns')"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" v-if="directDns.length>0">
<v-select
v-model="dnsToDirect"
:items="geositeList"
:label="$t('setting.toDirectDns')"
multiple
chips
hide-details
></v-select>
</v-col>
</v-row>
<template v-if="enableInb">
<v-row>
<v-col cols="12" sm="6" md="3">
<v-combobox
v-model="inbounds[0].address"
:items="defaultInb[0].address"
chips
multiple
hide-details
:label="$t('in.addr')"
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2">
<v-text-field
type="number"
v-model.number="inbounds[0].mtu"
hide-details
label="MTU"
></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="3">
<v-combobox
v-model="inbounds[0].exclude_package"
:items="['ir.mci.ecareapp','com.myirancell']"
chips
multiple
hide-details
:label="$t('setting.excludePkg')"
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2">
<v-switch
v-model="platformProxy"
hide-details
color="primary"
label="Platform HTTP proxy"
></v-switch>
</v-col>
</v-row>
</template>
<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 variant="tonal">{{ $t('setting.jsonSubOptions') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="enableLog" color="primary" :label="$t('basic.log.title')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="enableDns" color="primary" label="DNS" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="enableInb" color="primary" :label="$t('objects.inbound')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="enableExp" color="primary" label="Experimental" 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: ['settings'],
data() {
return {
menu: false,
subJsonExt: <any>{},
levels: ["trace", "debug", "info", "warn", "error", "fatal", "panic"],
defaultLog: {
"level": "info",
"timestamp": true
},
defaultInb: [
{
"type": "tun",
"address": [
"172.19.0.1/30",
"fdfe:dcba:9876::1/126"
],
"mtu": 9000,
"auto_route": true,
"strict_route": false,
"sniff": true,
"endpoint_independent_nat": false,
"stack": "system",
"exclude_package": [],
"platform": {
"http_proxy": {
"enabled": true,
"server": "127.0.0.1",
"server_port": 2080
}
}
},
{
"type": "mixed",
"listen": "127.0.0.1",
"listen_port": 2080,
"sniff": true,
"users": []
}
],
defaultExp: {
"clash_api": {
"external_controller": "127.0.0.1:9090",
"external_ui": "ui",
"secret": "",
"external_ui_download_url": "https://mirror.ghproxy.com/https://github.com/MetaCubeX/Yacd-meta/archive/gh-pages.zip",
"external_ui_download_detour": "direct",
"default_mode": "rule"
},
"cache_file": {
"enabled": true,
"store_fakeip": false
}
},
defaultDns: {
"servers": [
{
"address": "tcp://8.8.8.8",
"detour": "proxy",
"address_resolver": "local-dns",
"tag": "proxy-dns"
},
{
"tag": "local-dns",
"address": "local",
"detour": "direct"
},
{
"address": "rcode://success",
"tag": "block"
}
],
"rules": [
{
"clash_mode": "Global",
"source_ip_cidr": [
"172.19.0.0/30"
],
"server": "proxy-dns"
},
{
"source_ip_cidr": [
"172.19.0.0/30"
],
"server": "proxy-dns"
}
],
"final": "local-dns",
"strategy": "prefer_ipv4"
},
geositeList: [
{ title: "Private", value: "geosite-private" },
{ title: "Ads", value: "geosite-ads" },
{ title: "🇮🇷 Iran", value: "geosite-ir" },
{ title: "🇨🇳 China", value: "geosite-cn" },
{ title: "🇻🇳 Vietnam", value: "geosite-vn" },
],
geoList: [
{ title: "Site-Private", value: "geoip-private" },
{ title: "IP-Private", value: "geosite-private" },
{ title: "Site-Ads", value: "geosite-ads" },
{ title: "🇮🇷 Site-Iran", value: "geosite-ir" },
{ title: "🇮🇷 IP-Iran", value: "geoip-ir" },
{ title: "🇨🇳 Site-China", value: "geosite-cn" },
{ title: "🇨🇳 IP-China", value: "geoip-cn" },
{ title: "🇻🇳 Site-Vietnam", value: "geosite-vn" },
{ title: "🇻🇳 IP-Vietnam", value: "geoip-vn" },
],
geo: [
{
tag: "geosite-ads",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ads-all.srs",
download_detour: "direct"
},
{
tag: "geosite-private",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/private.srs",
download_detour: "direct"
},
{
tag: "geosite-ir",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/category-ir.srs",
download_detour: "direct"
},
{
tag: "geosite-cn",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geosite/cn.srs",
download_detour: "direct"
},
{
tag: "geosite-vn",
type: "remote",
format: "binary",
url: "https://github.com/Thaomtam/Geosite-vn/raw/rule-set/Geosite-vn.srs",
download_detour: "direct"
},
{
tag: "geoip-private",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/private.srs",
download_detour: "direct"
},
{
tag: "geoip-ir",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/ir.srs",
download_detour: "direct"
},
{
tag: "geoip-cn",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/cn.srs",
download_detour: "direct"
},
{
tag: "geoip-vn",
type: "remote",
format: "binary",
url: "https://testingcf.jsdelivr.net/gh/MetaCubeX/meta-rules-dat@sing/geo/geoip/vn.srs",
download_detour: "direct"
}
],
}
},
computed: {
enableLog: {
get() :boolean { return this.subJsonExt?.log != undefined },
set(v:boolean) { v ? this.subJsonExt.log = this.defaultLog : delete this.subJsonExt.log }
},
enableDns: {
get() :boolean { return this.subJsonExt?.dns != undefined },
set(v:boolean) {
if (v) {
this.subJsonExt.dns = this.defaultDns
if (this.rules == undefined) this.subJsonExt.rules = []
this.subJsonExt.rules.unshift({ protocol: "dns", outbound: "dns-out" })
} else {
delete this.subJsonExt.dns
const ruleDnsIndex = this.subJsonExt?.rules?.findIndex((r:any) => r.protocol == "dns" && r.outbound == "dns-out")
if (ruleDnsIndex >= 0) this.subJsonExt.rules.splice(ruleDnsIndex,1)
if (this.rules.length == 0) delete this.subJsonExt.rules
}
}
},
enableInb: {
get() :boolean { return this.subJsonExt?.inbounds != undefined },
set(v:boolean) { v ? this.subJsonExt.inbounds = this.defaultInb.slice() : delete this.subJsonExt.inbounds }
},
enableExp: {
get() :boolean { return this.subJsonExt?.experimental != undefined },
set(v:boolean) { v ? this.subJsonExt.experimental = this.defaultExp : delete this.subJsonExt.experimental }
},
dns():any { return this.subJsonExt?.dns?? undefined },
proxyDns: {
get() :string { return this.dns?.servers[0]?.address?? "" },
set(v:string) { this.dns.servers[0].address = v.length>0 ? v : "8.8.8.8" }
},
directDns: {
get() :string { return this.dns?.servers?.findLast((d:any) => d.tag == "direct-dns")?.address?? "" },
set(v:string) {
const sIndex = this.dns.servers.findIndex((d:any) => d.tag == "direct-dns")
if (v?.length>0) {
if (sIndex === -1) {
this.dns.servers.push({ tag: "direct-dns", address: v, detour: "direct" })
this.dns.rules.push({ clash_mode: "Direct", server: "direct-dns" })
} else {
this.dns.servers[sIndex].address = v
}
} else {
this.dns.servers.splice(sIndex,1)
this.dns.rules = this.dns.rules.filter((r:any) => r.server != "direct-dns")
}
},
},
dnsToDirect: {
get() :string[] {
const ruleIndex = this.dns?.rules?.findIndex((r:any) => r.server == "direct-dns" && Object.hasOwn(r,'rule_set'))
return ruleIndex >= 0 ? this.dns.rules[ruleIndex].rule_set : []
},
set(v:string[]) {
const ruleIndex = this.dns?.rules?.findIndex((r:any) => r.server == "direct-dns" && Object.hasOwn(r,'rule_set'))
if (v.length>0) {
if (ruleIndex >= 0){
this.dns.rules[ruleIndex].rule_set = v
} else {
this.dns.rules.push({ rule_set: v, server: "direct-dns" })
}
} else {
if (ruleIndex != -1) this.dns.rules.splice(ruleIndex,1)
}
this.updateRuleSets()
}
},
inbounds():any[] { return this.subJsonExt?.inbounds?? undefined },
platformProxy: {
get() :boolean { return this.inbounds[0]?.platform != undefined },
set(v:boolean) { this.subJsonExt.inbounds[0].platform = v ? this.defaultInb[0].platform : undefined }
},
rules():any { return this.subJsonExt?.rules?? undefined },
ruleToDirect: {
get() :string[] {
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "direct" && Object.hasOwn(r,'rule_set'))
return ruleIndex >= 0 ? this.rules[ruleIndex].rule_set : []
},
set(v:string[]) {
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "direct" && Object.hasOwn(r,'rule_set'))
if (v.length>0) {
if (ruleIndex >= 0){
this.rules[ruleIndex].rule_set = v
} else {
if (this.rules == undefined) this.subJsonExt.rules = []
this.rules.push({ rule_set: v, outbound: "direct" })
}
} else {
if (ruleIndex != -1) this.rules.splice(ruleIndex,1)
}
this.updateRuleSets()
}
},
ruleToBlock: {
get() :string[] {
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "block" && Object.hasOwn(r,'rule_set'))
return ruleIndex >= 0 ? this.rules[ruleIndex].rule_set : []
},
set(v:string[]) {
const ruleIndex = this.rules?.findIndex((r:any) => r.outbound == "block" && Object.hasOwn(r,'rule_set'))
if (v.length>0) {
if (ruleIndex >= 0){
this.rules[ruleIndex].rule_set = v
} else {
if (this.rules == undefined) this.subJsonExt.rules = []
this.rules.push({ rule_set: v, outbound: "block" })
}
} else {
if (ruleIndex != -1) this.rules.splice(ruleIndex,1)
}
this.updateRuleSets()
}
}
},
methods: {
updateRuleSets(){
let tags = <string[]>[]
if (this.dns?.rules?.length>0) this.dns.rules.forEach((r:any) => { if (r.rule_set) tags.push(...r.rule_set) })
if (this.rules?.length>0) this.rules.forEach((r:any) => { if (r.rule_set) tags.push(...r.rule_set) })
if (tags.length>0){
this.subJsonExt.rule_set = this.geo.filter((g:any) => tags.includes(g.tag))
} else {
delete this.subJsonExt.rule_set
}
if (this.rules.length == 0) delete this.subJsonExt.rules
}
},
mounted(){
this.subJsonExt = this.$props.settings?.subJsonExt?.length>0 ? JSON.parse(this.$props.settings.subJsonExt) : <any>{}
},
watch:{
subJsonExt:{
handler(v) {
this.$props.settings.subJsonExt = Object.keys(v).length>0 ? JSON.stringify(v, null, 2) : ""
},
deep: true
},
}
}
</script>
+1 -1
View File
@@ -16,7 +16,7 @@
<script lang="ts"> <script lang="ts">
export default { export default {
props: ['inbound', 'id'], props: ['inbound'],
data() { data() {
return { return {
hasUser: false, hasUser: false,
+1 -1
View File
@@ -26,7 +26,7 @@ const theme = computed(() =>{
}) })
const direction = computed(() => { const direction = computed(() => {
return vuetify.locale.current.value == 'fa' ? 'rtl' : 'ltr' return vuetify.locale.isRtl ? 'rtl' : 'ltr'
}) })
</script> </script>
@@ -87,7 +87,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('types.hy.hyOptions') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.hy.hyOptions') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
@@ -59,7 +59,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('types.hy.hy2Options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.hy.hy2Options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
@@ -31,7 +31,7 @@
type="number" type="number"
min="0" min="0"
hide-details hide-details
v-model="server_port"> v-model.number="server_port">
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -78,7 +78,7 @@
type="number" type="number"
min="0" min="0"
hide-details hide-details
v-model="value.server_port"> v-model.number="value.server_port">
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -114,22 +114,22 @@ export default {
set(newValue: any) { set(newValue: any) {
switch (newValue) { switch (newValue) {
case 1: case 1:
this.Inbound.password = undefined delete this.Inbound.password
this.Inbound.users = undefined delete this.Inbound.users
this.Inbound.handshake_for_server_name = undefined delete this.Inbound.handshake_for_server_name
break; break;
case 2: case 2:
if (!this.Inbound.password) { if (!this.Inbound.password) {
this.Inbound.password = "" this.Inbound.password = ""
} }
this.Inbound.users = undefined delete this.Inbound.users
if (!this.Inbound.handshake_for_server_name) { if (!this.Inbound.handshake_for_server_name) {
this.Inbound.handshake_for_server_name = {} this.Inbound.handshake_for_server_name = {}
} }
break; break;
case 3: case 3:
this.Inbound.password = undefined delete this.Inbound.password
if (Object.hasOwn(this.Inbound, 'users')) { if (!Object.hasOwn(this.Inbound, 'users')) {
this.Inbound.users = [] this.Inbound.users = []
} }
if (!this.Inbound.handshake_for_server_name) { if (!this.Inbound.handshake_for_server_name) {
@@ -6,12 +6,10 @@
hide-details hide-details
:label="$t('in.ssMethod')" :label="$t('in.ssMethod')"
:items="ssMethods" :items="ssMethods"
@update:model-value="changeMethod($event)"
v-model="data.method"> v-model="data.method">
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="data.password" :label="$t('types.pw')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<Network :data="data" /> <Network :data="data" />
</v-col> </v-col>
@@ -19,12 +17,24 @@
<UoT :data="data" /> <UoT :data="data" />
</v-col> </v-col>
</v-row> </v-row>
<v-row v-if="data.method.startsWith('2022')">
<v-col cols="12" sm="8">
<v-text-field
v-model="data.password"
:label="$t('types.pw')"
hide-details
append-inner-icon="mdi-refresh"
@click:append-inner="changeMethod(data.method)">
</v-text-field>
</v-col>
</v-row>
</v-card> </v-card>
</template> </template>
<script lang="ts"> <script lang="ts">
import Network from '@/components/Network.vue' import Network from '@/components/Network.vue'
import UoT from '@/components/UoT.vue'; import UoT from '@/components/UoT.vue';
import RandomUtil from '@/plugins/randomUtil';
export default { export default {
props: ['direction','data'], props: ['direction','data'],
@@ -43,6 +53,15 @@ export default {
] ]
} }
}, },
methods: {
changeMethod(ssMethod :string) {
if (ssMethod.startsWith('2022')) {
this.$props.data.password = ssMethod == "2022-blake3-aes-128-gcm" ? RandomUtil.randomShadowsocksPassword(16) : RandomUtil.randomShadowsocksPassword(32)
} else {
this.$props.data.password = ''
}
}
},
components: { Network, UoT } components: { Network, UoT }
} }
</script> </script>
+1 -1
View File
@@ -77,7 +77,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('types.ssh.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.ssh.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
+62
View File
@@ -0,0 +1,62 @@
<template>
<v-card subtitle="Tun">
<v-row>
<v-col cols="12" sm="8">
<v-text-field v-model="addrs" :label="$t('types.tun.addr') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="data.interface_name" :label="$t('types.tun.ifName')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field type="number" v-model.number="data.mtu" label="MTU" hide-details></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
type="number"
v-model.number="udpTimeout"
label="UDP timeout"
min="1"
:suffix="$t('date.m')"
hide-details>
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-select
v-model="data.stack"
label="Stack"
:items="['system','gvisor','mixed']"
hide-details
></v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.endpoint_independent_nat" color="primary" label="Independent NAT" hide-details></v-switch>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
export default {
props: ['data'],
data() {
return {
menu: false
}
},
computed: {
addrs: {
get() { return this.$props.data.address?.join(',') },
set(v:string) { this.$props.data.address = v.length > 0 ? v.split(',') : undefined }
},
udpTimeout: {
get() { return this.$props.data.udp_timeout ? parseInt(this.$props.data.udp_timeout.replace('m','')) : 5 },
set(v:number) { this.$props.data.udp_timeout = v > 0 ? v + 'm' : '5m' }
}
},
}
</script>
@@ -55,7 +55,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('types.lb.urlTestOptions') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.lb.urlTestOptions') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
@@ -61,7 +61,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('types.wg.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.wg.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
@@ -124,7 +124,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('tls.acme.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.acme.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
@@ -30,9 +30,23 @@
>{{ $t('tls.useText') }}</v-btn> >{{ $t('tls.useText') }}</v-btn>
</v-btn-toggle> </v-btn-toggle>
</v-col> </v-col>
<v-spacer></v-spacer>
<v-col cols="auto">
<v-btn
variant="tonal"
density="compact"
icon="mdi-key-star"
@click="genECH"
:loading="loading">
<v-icon />
<v-tooltip activator="parent" location="top">
{{ $t('actions.generate') }}
</v-tooltip>
</v-btn>
</v-col>
</v-row> </v-row>
<v-row v-if="useEchPath == 0"> <v-row v-if="useEchPath == 0">
<v-col cols="12" sm="6"> <v-col cols="12">
<v-text-field <v-text-field
:label="$t('tls.keyPath')" :label="$t('tls.keyPath')"
hide-details hide-details
@@ -41,7 +55,7 @@
</v-col> </v-col>
</v-row> </v-row>
<v-row v-else> <v-row v-else>
<v-col cols="12" sm="6"> <v-col cols="12">
<v-textarea <v-textarea
:label="$t('tls.key')" :label="$t('tls.key')"
hide-details hide-details
@@ -50,7 +64,7 @@
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6"> <v-col cols="12">
<v-textarea <v-textarea
:label="$t('tls.cert')" :label="$t('tls.cert')"
hide-details hide-details
@@ -63,15 +77,65 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { i18n } from '@/locales'
import HttpUtils from '@/plugins/httputil'
import { ech } from '@/types/inTls' import { ech } from '@/types/inTls'
import { push } from 'notivue'
export default { export default {
props: ['iTls','oTls'], props: ['iTls','oTls'],
data() { data() {
return { return {
useEchPath: 0 useEchPath: this.$props.iTls?.ech?.key? 1:0,
loading: false,
} }
}, },
methods: {
async genECH(){
this.loading = true
const msg = await HttpUtils.get('api/keypairs', { k: "ech", o: this.iTls.server_name?? "''" })
this.loading = false
if (msg.success && this.iTls.ech && this.oTls.ech) {
this.iTls.ech.key_path=undefined
this.useEchPath = 1
if (msg.obj.length>0){
let config = <string[]>[]
let key = <string[]>[]
let isConfig = false
let isKey = false
msg.obj.forEach((line:string) => {
if (line === "-----BEGIN ECH CONFIGS-----") {
isConfig = true
isKey = false
config.push(line)
} else if (line === "-----END ECH CONFIGS-----") {
isConfig = false
config.push(line)
} else if (line === "-----BEGIN ECH KEYS-----") {
isKey = true
isConfig = false
key.push(line)
} else if (line === "-----END ECH KEYS-----") {
isKey = false
key.push(line)
} else if (isConfig) {
config.push(line)
} else if (isKey) {
key.push(line)
}
})
this.iTls.ech.key = key?? undefined
this.oTls.ech.config = config?? undefined
} else {
push.error({
message: i18n.global.t('error') + ": " + msg.obj
})
}
}
},
},
computed: { computed: {
ech() { ech() {
return <ech>this.$props.iTls.ech return <ech>this.$props.iTls.ech
@@ -7,7 +7,7 @@
<v-col cols="12" sm="6" md="4" v-if="tls.enabled"> <v-col cols="12" sm="6" md="4" v-if="tls.enabled">
<v-select <v-select
hide-details hide-details
label="Preset" :label="$t('template')"
:items="tlsItems" :items="tlsItems"
@update:model-value="changeTlsItem($event)" @update:model-value="changeTlsItem($event)"
v-model="tlsId"> v-model="tlsId">
@@ -116,7 +116,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start" v-if="tls.enabled"> <v-menu v-model="menu" :close-on-content-click="false" location="start" v-if="tls.enabled">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('tls.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
@@ -143,6 +143,7 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { i18n } from '@/locales'
import { iTls, defaultInTls } from '@/types/inTls' import { iTls, defaultInTls } from '@/types/inTls'
export default { export default {
props: ['inbound', 'tlsConfigs', 'tls_id'], props: ['inbound', 'tlsConfigs', 'tls_id'],
@@ -183,7 +184,7 @@ export default {
return <iTls> this.$props.inbound.tls return <iTls> this.$props.inbound.tls
}, },
tlsItems(): any[] { tlsItems(): any[] {
return [ { title: '', value: 0 }, ...this.$props.tlsConfigs?.map((t:any) => { return { title: t.name, value: t.id } } )] return [ { title: i18n.global.t('none'), value: 0 }, ...this.$props.tlsConfigs?.map((t:any) => { return { title: t.name, value: t.id } } )]
}, },
tlsId: { tlsId: {
get() { return this.tls_id.value?? 0 }, get() { return this.tls_id.value?? 0 },
@@ -191,7 +192,10 @@ export default {
}, },
tlsEnable: { tlsEnable: {
get() { return this.tls.enabled?? false }, get() { return this.tls.enabled?? false },
set(newValue: boolean) { this.$props.inbound.tls = newValue ? { enabled: true } : {} } set(newValue: boolean) {
this.$props.inbound.tls = newValue ? { enabled: true } : {}
this.$props.tls_id.value = 0
}
}, },
tlsOptional(): boolean { tlsOptional(): boolean {
return !['hysteria','hysteria2','tuic','naive'].includes(this.$props.inbound.type) return !['hysteria','hysteria2','tuic','naive'].includes(this.$props.inbound.type)
@@ -177,7 +177,7 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('tls.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
@@ -222,8 +222,8 @@ export default {
data() { data() {
return { return {
menu: false, menu: false,
usePath: 0, usePath: this.$props.outbound?.tls?.certificate? 1:0,
useEchPath: 0, useEchPath: this.$props.outbound?.tls.ech?.config? 1:0,
defaults: defaultOutTls, defaults: defaultOutTls,
alpn: [ alpn: [
{ title: "H3", value: 'h3' }, { title: "H3", value: 'h3' },
+5 -3
View File
@@ -2,7 +2,7 @@
<v-app-bar :elevation="5"> <v-app-bar :elevation="5">
<v-icon v-if="isMobile" icon="mdi-menu" @click="$emit('toggleDrawer')" /> <v-icon v-if="isMobile" icon="mdi-menu" @click="$emit('toggleDrawer')" />
<span v-else style="width: 24px"></span> <span v-else style="width: 24px"></span>
<v-app-bar-title :text="$t(<string>$router.currentRoute.value.name)" class="align-center text-center " /> <v-app-bar-title :text="$t(<string>route.name)" class="align-center text-center " />
<v-btn prepend-icon="mdi-content-save" v-if="stateChange" :text="$t('actions.save')" @click="saveChanges"></v-btn> <v-btn prepend-icon="mdi-content-save" v-if="stateChange" :text="$t('actions.save')" @click="saveChanges"></v-btn>
<v-icon icon="mdi-theme-light-dark" @click="toggleTheme()" style="margin: 0 10px;"></v-icon> <v-icon icon="mdi-theme-light-dark" @click="toggleTheme()" style="margin: 0 10px;"></v-icon>
</v-app-bar> </v-app-bar>
@@ -13,9 +13,11 @@ import { computed, ref } from "vue"
import { useTheme } from "vuetify" import { useTheme } from "vuetify"
import { FindDiff } from "@/plugins/utils" import { FindDiff } from "@/plugins/utils"
import Data from "@/store/modules/data" import Data from "@/store/modules/data"
import { useRoute } from "vue-router";
defineProps(['isMobile']) defineProps(['isMobile'])
const route = useRoute();
const theme = useTheme() const theme = useTheme()
const darkMode = ref(localStorage.getItem('theme') == "dark") const darkMode = ref(localStorage.getItem('theme') == "dark")
@@ -32,11 +34,11 @@ const saveChanges = () => {
} }
const oldData = computed((): any => { const oldData = computed((): any => {
return {config: store.oldData.config, clients: store.oldData.clients, tls: store.oldData.tlsConfigs} return {config: store.oldData.config, clients: store.oldData.clients, tls: store.oldData.tlsConfigs, inData: store.oldData.inData}
}) })
const newData = computed((): any => { const newData = computed((): any => {
return {config: store.config, clients: store.clients, tls: store.tlsConfigs} return {config: store.config, clients: store.clients, tls: store.tlsConfigs, inData: store.inData}
}) })
const stateChange = computed((): any => { const stateChange = computed((): any => {
+8 -1
View File
@@ -120,7 +120,14 @@ export default {
computed: { computed: {
locale() { locale() {
const l = i18n.global.locale.value const l = i18n.global.locale.value
return l.replace('zh', 'zh-') switch (l) {
case "zhHans":
return "zh-cn"
case "zhHant":
return "zh-tw"
default:
return l
}
}, },
}, },
watch: { watch: {
+39 -3
View File
@@ -5,7 +5,7 @@
{{ $t('actions.' + title) + " " + $t('objects.client') }} {{ $t('actions.' + title) + " " + $t('objects.client') }}
</v-card-title> </v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-text style="padding: 0 16px;"> <v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-container style="padding: 0;"> <v-container style="padding: 0;">
<v-tabs <v-tabs
v-model="tab" v-model="tab"
@@ -21,6 +21,9 @@
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-switch color="primary" v-model="client.enable" :label="$t('enable')" hide-details></v-switch> <v-switch color="primary" v-model="client.enable" :label="$t('enable')" hide-details></v-switch>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4">
<v-combobox v-model="client.group" :items="groups" :label="$t('client.group')" hide-details></v-combobox>
</v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
@@ -38,6 +41,33 @@
<DatePick :expiry="expDate" @submit="setDate" /> <DatePick :expiry="expDate" @submit="setDate" />
</v-col> </v-col>
</v-row> </v-row>
<v-row v-if="index != -1">
<v-col cols="12" sm="6" md="4" class="d-flex flex-column">
<div class="d-flex justify-space-between align-center">
<div>
{{ $t('stats.usage') }}: {{ total }}<sup dir="ltr" v-if="percent>0">({{ percent }}%)</sup>
</div>
<v-btn density="compact" variant="text" icon="mdi-restore" @click="client.up=0;client.down=0">
<v-tooltip activator="parent" location="top">
{{ $t('reset') }}
</v-tooltip>
<v-icon />
</v-btn>
</div>
<v-progress-linear
v-model="percent"
:color="percentColor"
v-if="client.volume>0"
bottom
>
</v-progress-linear>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-icon icon="mdi-upload" color="orange" /><span class="text-orange">{{ up }}</span>
/
<v-icon icon="mdi-download" color="success" /><span class="text-success">{{ down }}</span>
</v-col>
</v-row>
<v-row> <v-row>
<v-col> <v-col>
<v-combobox <v-combobox
@@ -156,9 +186,10 @@
import { Link } from '@/plugins/link' import { Link } from '@/plugins/link'
import { createClient, randomConfigs, updateConfigs } from '@/types/clients' import { createClient, randomConfigs, updateConfigs } from '@/types/clients'
import DatePick from '@/components/DateTime.vue' import DatePick from '@/components/DateTime.vue'
import { HumanReadable } from '@/plugins/utils'
export default { export default {
props: ['visible', 'data', 'index', 'inboundTags', 'stats'], props: ['visible', 'data', 'index', 'inboundTags', 'groups', 'stats'],
emits: ['close', 'save'], emits: ['close', 'save'],
data() { data() {
return { return {
@@ -222,7 +253,12 @@ export default {
Volume: { Volume: {
get() { return this.client.volume == 0 ? 0 : (this.client.volume / (1024 ** 3)) }, get() { return this.client.volume == 0 ? 0 : (this.client.volume / (1024 ** 3)) },
set(v:number) { this.client.volume = v > 0 ? v*(1024 ** 3) : 0 } set(v:number) { this.client.volume = v > 0 ? v*(1024 ** 3) : 0 }
} },
up() :string { return HumanReadable.sizeFormat(this.client.up) },
down() :string { return HumanReadable.sizeFormat(this.client.down) },
total() :string { return HumanReadable.sizeFormat(this.client.down + this.client.up) },
percent() :number { return this.client.volume>0 ? Math.round((this.client.up + this.client.down) *100 / this.client.volume) : 0 },
percentColor() :string { return (this.client.up+this.client.down) >= this.client.volume ? 'error' : this.percent>90 ? 'warning' : 'success' },
}, },
watch: { watch: {
visible(newValue) { visible(newValue) {
+199
View File
@@ -0,0 +1,199 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg">
<v-card-title>
{{ $t('bulk.add') }}
</v-card-title>
<v-divider></v-divider>
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-container style="padding: 0;">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model.number="count" type="number" min="1" max="100" :label="$t('count')" hide-details></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="8">
<v-combobox
chips
multiple
v-model="bulkData.name"
:items="patterns"
:label="$t('client.name')"
hide-details>
</v-combobox>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="8">
<v-combobox
chips
multiple
v-model="bulkData.desc"
:items="patterns"
:label="$t('client.desc')"
hide-details>
</v-combobox>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-combobox v-model="bulkData.group" :items="groups" :label="$t('client.group')" hide-details></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model.number="bulkData.Volume" type="number" min="0" :label="$t('stats.volume')" suffix="GiB" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<DatePick :expiry="bulkData.expiry" @submit="setDate" />
</v-col>
</v-row>
<v-row>
<v-col>
<v-combobox
v-model="bulkData.clientInbounds"
:items="inboundTags"
:label="$t('client.inboundTags')"
multiple
chips
hide-details
></v-combobox>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-switch v-model="bulkData.clientStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-col>
</v-row>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
variant="outlined"
@click="closeModal"
>
{{ $t('actions.close') }}
</v-btn>
<v-btn
color="blue-darken-1"
variant="tonal"
:loading="loading"
@click="saveChanges"
>
{{ $t('actions.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import DatePick from '@/components/DateTime.vue'
import { push } from 'notivue'
import RandomUtil from '@/plugins/randomUtil'
import { Client, createClient, randomConfigs } from '@/types/clients'
import { i18n } from '@/locales';
export default {
props: ['visible', 'inboundTags', 'groups'],
emits: ['close', 'save'],
data() {
return {
count: 1,
clients: <Client[]>[],
bulkData: {
name: <any[]>[],
desc: <any[]>[],
group: '',
clientInbounds: [],
expiry: 0,
Volume: 0,
clientStats: false,
},
patterns: [
{ title: i18n.global.t("bulk.random"), value: "random" },
{ title: i18n.global.t("bulk.order"), value: "order" },
],
loading: false,
}
},
methods: {
resetData() {
this.count = 1,
this.clients = [],
this.bulkData = {
name: [this.patterns[1], "-", this.patterns[0]],
desc: [],
group: '',
clientInbounds: [],
expiry: 0,
Volume: 0,
clientStats: false,
}
},
closeModal() {
this.$emit('close')
},
saveChanges() {
if (this.bulkData.name.findIndex(n => typeof(n) == 'object') == -1) {
push.error(i18n.global.t('error.dplData'))
return
}
this.loading = true
for(let i=0;i<this.count;i++){
const name = this.genByPattern(this.bulkData.name, i)
this.clients.push(createClient({
enable: true,
name: name,
config: randomConfigs(name),
inbounds: this.bulkData.clientInbounds,
links: [],
volume: this.bulkData.Volume*(1024 ** 3),
expiry: this.bulkData.expiry,
up: 0,
down: 0,
desc: this.genByPattern(this.bulkData.desc, i),
group: this.bulkData.group
}))
}
this.$emit('save', this.clients, this.bulkData.clientInbounds, this.bulkData.clientStats)
this.resetData() // reset to default
this.loading = false
},
genByPattern(pattern: any[], order :number){
if (pattern.length == 0) return RandomUtil.randomSeq(8)
let result = ''
pattern.forEach(p => {
switch(typeof p){
case 'object':
switch(p.value){
case "random":
result += RandomUtil.randomSeq(8)
break
case "order":
result += order+1
}
break
default:
result += p
}
})
return result
},
setDate(v:number){
this.bulkData.expiry = v
}
},
computed: {},
watch: {
visible(newValue) {
if (newValue) {
this.resetData()
}
},
},
components: { DatePick },
}
</script>
+120 -39
View File
@@ -5,35 +5,66 @@
{{ $t('actions.' + title) + " " + $t('objects.inbound') }} {{ $t('actions.' + title) + " " + $t('objects.inbound') }}
</v-card-title> </v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-text> <v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-row> <v-container style="padding: 0;">
<v-col cols="12" sm="6" md="4"> <v-row>
<v-select <v-col cols="12" sm="6" md="4">
hide-details <v-select
:label="$t('type')" hide-details
:items="Object.keys(inTypes).map((key,index) => ({title: key, value: Object.values(inTypes)[index]}))" :label="$t('type')"
v-model="inbound.type" :items="Object.keys(inTypes).map((key,index) => ({title: key, value: Object.values(inTypes)[index]}))"
@update:modelValue="changeType"> v-model="inbound.type"
</v-select> @update:modelValue="changeType">
</v-col> </v-select>
<v-col cols="12" sm="6" md="4"> </v-col>
<v-text-field v-model="inbound.tag" :label="$t('objects.tag')" hide-details></v-text-field> <v-col cols="12" sm="6" md="4">
</v-col> <v-text-field v-model="inbound.tag" :label="$t('objects.tag')" hide-details></v-text-field>
</v-row> </v-col>
<Listen :inbound="inbound" :inTags="inTags" /> </v-row>
<Direct v-if="inbound.type == inTypes.Direct" direction="in" :data="inbound" /> <v-tabs
<Shadowsocks v-if="inbound.type == inTypes.Shadowsocks" direction="in" :data="inbound" /> v-if="HasInData.includes(inbound.type)"
<Hysteria v-if="inbound.type == inTypes.Hysteria" direction="in" :data="inbound" /> v-model="side"
<Hysteria2 v-if="inbound.type == inTypes.Hysteria2" direction="in" :data="inbound" /> density="compact"
<Naive v-if="inbound.type == inTypes.Naive" :inbound="inbound" /> fixed-tabs
<ShadowTls v-if="inbound.type == inTypes.ShadowTLS" direction="in" :data="inbound" :outTags="outTags" /> align-tabs="center"
<Tuic v-if="inbound.type == inTypes.TUIC" direction="in" :data="inbound" /> >
<TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" /> <v-tab value="s">{{ $t('in.sSide') }}</v-tab>
<Transport v-if="Object.hasOwn(inbound,'transport')" :data="inbound" /> <v-tab value="c">{{ $t('in.cSide') }}</v-tab>
<Users v-if="HasOptionalUser.includes(inbound.type)" :inbound="inbound" :id="id" /> </v-tabs>
<InTls v-if="Object.hasOwn(inbound,'tls')" :inbound="inbound" :tlsConfigs="tlsConfigs" :tls_id="tls_id" /> <v-window v-model="side" style="margin-top: 10px;">
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="in" :data="inbound" /> <v-window-item value="s">
<v-switch v-model="inboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch> <Listen :inbound="inbound" :inTags="inTags" />
<Direct v-if="inbound.type == inTypes.Direct" direction="in" :data="inbound" />
<Shadowsocks v-if="inbound.type == inTypes.Shadowsocks" direction="in" :data="inbound" />
<Hysteria v-if="inbound.type == inTypes.Hysteria" direction="in" :data="inbound" />
<Hysteria2 v-if="inbound.type == inTypes.Hysteria2" direction="in" :data="inbound" />
<Naive v-if="inbound.type == inTypes.Naive" :inbound="inbound" />
<ShadowTls v-if="inbound.type == inTypes.ShadowTLS" direction="in" :data="inbound" :outTags="outTags" />
<Tuic v-if="inbound.type == inTypes.TUIC" direction="in" :data="inbound" />
<Tun v-if="inbound.type == inTypes.Tun" :data="inbound" />
<TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" />
<Transport v-if="Object.hasOwn(inbound,'transport')" :data="inbound" />
<Users v-if="HasOptionalUser.includes(inbound.type)" :inbound="inbound" />
<InTls v-if="Object.hasOwn(inbound,'tls')" :inbound="inbound" :tlsConfigs="tlsConfigs" :tls_id="tls_id" />
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="in" :data="inbound" />
<v-switch v-model="inboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-window-item>
<v-window-item value="c">
<OutJsonVue :inData="inData" :type="inbound.type" />
<Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="out" :data="inData.outJson" />
<v-card>
<v-card-subtitle>{{ $t('in.multiDomain') }}
<v-icon @click="add_addr" icon="mdi-plus"></v-icon>
</v-card-subtitle>
<template v-for="addr,index in inData.addrs">
{{ $t('in.addr') }} #{{ (index+1) }} <v-icon icon="mdi-delete" @click="inData.addrs.splice(index,1)" />
<v-divider></v-divider>
<AddrVue :addr="addr" :hasTls="Object.hasOwn(inbound,'tls')" />
</template>
</v-card>
</v-window-item>
</v-window>
</v-container>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@@ -59,6 +90,9 @@
<script lang="ts"> <script lang="ts">
import { InTypes, createInbound } from '@/types/inbounds' import { InTypes, createInbound } from '@/types/inbounds'
import { Addr, InData } from '@/plugins/inData'
import RandomUtil from '@/plugins/randomUtil'
import Listen from '@/components/Listen.vue' import Listen from '@/components/Listen.vue'
import Direct from '@/components/protocols/Direct.vue' import Direct from '@/components/protocols/Direct.vue'
import Users from '@/components/Users.vue' import Users from '@/components/Users.vue'
@@ -68,46 +102,89 @@ import Hysteria2 from '@/components/protocols/Hysteria2.vue'
import Naive from '@/components/protocols/Naive.vue' import Naive from '@/components/protocols/Naive.vue'
import ShadowTls from '@/components/protocols/ShadowTls.vue' import ShadowTls from '@/components/protocols/ShadowTls.vue'
import Tuic from '@/components/protocols/Tuic.vue' import Tuic from '@/components/protocols/Tuic.vue'
import InTls from '@/components/InTLS.vue' import Tun from '@/components/protocols/Tun.vue'
import InTls from '@/components/tls/InTLS.vue'
import TProxy from '@/components/protocols/TProxy.vue' import TProxy from '@/components/protocols/TProxy.vue'
import RandomUtil from '@/plugins/randomUtil'
import Multiplex from '@/components/Multiplex.vue' import Multiplex from '@/components/Multiplex.vue'
import Transport from '@/components/Transport.vue' import Transport from '@/components/Transport.vue'
import AddrVue from '@/components/Addr.vue'
import OutJsonVue from '@/components/OutJson.vue'
export default { export default {
props: ['visible', 'data', 'id', 'stats', 'inTags', 'outTags', 'tlsConfigs'], props: ['visible', 'data', 'cData', 'index', 'stats', 'inTags', 'outTags', 'tlsConfigs'],
emits: ['close', 'save'], emits: ['close', 'save'],
data() { data() {
return { return {
inbound: createInbound("direct",{ "tag": "" }), inbound: createInbound("direct",{ "tag": "" }),
inData: <InData>{},
title: "add", title: "add",
loading: false, loading: false,
side: "s",
inTypes: InTypes, inTypes: InTypes,
inboundStats: false, inboundStats: false,
tls_id: { value: 0 }, tls_id: { value: 0 },
HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks], HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks],
HasInData: [
InTypes.SOCKS,
InTypes.HTTP,
InTypes.Shadowsocks,
InTypes.VMess,
InTypes.ShadowTLS,
InTypes.Trojan,
InTypes.Hysteria,
InTypes.VLESS,
InTypes.TUIC,
InTypes.Hysteria2,
InTypes.Naive,
]
} }
}, },
methods: { methods: {
updateData() { updateData() {
if (this.$props.id != -1) { if (this.$props.index != -1) {
const newData = JSON.parse(this.$props.data) const newData = JSON.parse(this.$props.data)
this.inbound = createInbound(newData.type, newData) this.inbound = createInbound(newData.type, newData)
this.tls_id.value = this.$props.tlsConfigs?.findLast((t:any) => t.inbounds?.includes(this.inbound.tag))?.id?? 0 this.tls_id.value = this.$props.tlsConfigs?.findLast((t:any) => t.inbounds?.includes(this.inbound.tag))?.id?? 0
if (this.HasInData.includes(this.inbound.type)){
this.inData = this.$props.cData?.length> 0 ? <InData>JSON.parse(this.$props.cData) : <InData>{id: 0, tag: this.inbound.tag, addrs: [], outJson: {}}
} else {
this.inData = <InData>{id: -1}
}
this.title = "edit" this.title = "edit"
} }
else { else {
const port = RandomUtil.randomIntRange(10000, 60000) const port = RandomUtil.randomIntRange(10000, 60000)
this.inbound = createInbound("direct",{ tag: "direct-"+port ,listen: "::", listen_port: port }) this.inbound = createInbound("direct",{ tag: "direct-"+port ,listen: "::", listen_port: port })
this.tls_id.value = 0
if (this.HasInData.includes(this.inbound.type)){
this.inData = <InData>{id: 0, tag: this.inbound.tag, addrs: [], outJson: {}}
} else {
this.inData = <InData>{id: -1}
}
this.title = "add" this.title = "add"
} }
this.inboundStats = this.$props.stats this.inboundStats = this.$props.stats
this.side = "s"
}, },
changeType() { changeType() {
// Tag change only in add outbound if (!this.inbound.listen_port) this.inbound.listen_port = RandomUtil.randomIntRange(10000, 60000)
const tag = this.$props.id != -1 ? this.inbound.tag : this.inbound.type + "-" + this.inbound.listen_port // Tag change only in add inbound
const tag = this.$props.index != -1 ? this.inbound.tag : this.inbound.type + "-" + this.inbound.listen_port
// Use previous data // Use previous data
const prevConfig = { tag: tag ,listen: this.inbound.listen, listen_port: this.inbound.listen_port } const prevConfig = { tag: tag ,listen: this.inbound.listen?? "::", listen_port: this.inbound.listen_port }
this.inbound = createInbound(this.inbound.type, prevConfig) this.inbound = createInbound(this.inbound.type, this.inbound.type != this.inTypes.Tun ? prevConfig : { tag: tag })
if (this.HasInData.includes(this.inbound.type)){
if (this.inData.id == -1) this.inData.id = 0
this.inData.addrs = []
this.inData.outJson = {}
this.inData.tag = tag
} else {
this.inData = <InData>{id: -1}
}
this.tls_id.value = 0
this.side = "s"
},
add_addr() {
this.inData.addrs.push(<Addr>{ server: location.hostname, server_port: this.inbound.listen_port })
}, },
closeModal() { closeModal() {
this.updateData() // reset this.updateData() // reset
@@ -115,7 +192,7 @@ export default {
}, },
saveChanges() { saveChanges() {
this.loading = true this.loading = true
this.$emit('save', this.inbound, this.inboundStats, this.tls_id.value) this.$emit('save', this.inbound, this.inboundStats, this.tls_id.value, this.inData)
this.loading = false this.loading = false
}, },
}, },
@@ -126,6 +203,10 @@ export default {
} }
}, },
}, },
components: { Listen, InTls, Hysteria2, Naive, Direct, Shadowsocks, Users, Hysteria, ShadowTls, TProxy, Multiplex, Tuic, Transport } components: {
Listen, InTls, Hysteria2, Naive, Direct, Shadowsocks,
Users, Hysteria, ShadowTls, TProxy, Multiplex, Tuic, Tun, Transport,
AddrVue, OutJsonVue
}
} }
</script> </script>
+94 -55
View File
@@ -5,61 +5,84 @@
{{ $t('actions.' + title) + " " + $t('objects.outbound') }} {{ $t('actions.' + title) + " " + $t('objects.outbound') }}
</v-card-title> </v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-text> <v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-row> <v-container style="padding: 0;">
<v-col cols="12" sm="6" md="4"> <v-tabs
<v-select v-model="tab"
hide-details align-tabs="center"
:label="$t('type')" >
:items="Object.keys(outTypes).map((key,index) => ({title: key, value: Object.values(outTypes)[index]}))" <v-tab value="t1">{{ $t('client.basics') }}</v-tab>
v-model="outbound.type" <v-tab value="t2">{{ $t('client.external') }}</v-tab>
@update:modelValue="changeType"> </v-tabs>
</v-select> <v-window v-model="tab">
</v-col> <v-window-item value="t1">
<v-col cols="12" sm="6" md="4"> <v-row>
<v-text-field v-model="outbound.tag" :label="$t('objects.tag')" hide-details></v-text-field> <v-col cols="12" sm="6" md="4">
</v-col> <v-select
</v-row> hide-details
<v-row v-if="!NoServer.includes(outbound.type)"> :label="$t('type')"
<v-col cols="12" sm="6" md="4"> :items="Object.keys(outTypes).map((key,index) => ({title: key, value: Object.values(outTypes)[index]}))"
<v-text-field v-model="outbound.type"
:label="$t('out.addr')" @update:modelValue="changeType">
hide-details </v-select>
v-model="outbound.server"> </v-col>
</v-text-field> <v-col cols="12" sm="6" md="4">
</v-col> <v-text-field v-model="outbound.tag" :label="$t('objects.tag')" hide-details></v-text-field>
<v-col cols="12" sm="6" md="4"> </v-col>
<v-text-field </v-row>
:label="$t('out.port')" <v-row v-if="!NoServer.includes(outbound.type)">
type="number" <v-col cols="12" sm="6" md="4">
min="0" <v-text-field
hide-details :label="$t('out.addr')"
v-model.number="outbound.server_port"> hide-details
</v-text-field> v-model="outbound.server">
</v-col> </v-text-field>
</v-row> </v-col>
<Direct v-if="outbound.type == outTypes.Direct" direction="out" :data="outbound" /> <v-col cols="12" sm="6" md="4">
<Socks v-if="outbound.type == outTypes.SOCKS" :data="outbound" /> <v-text-field
<Http v-if="outbound.type == outTypes.HTTP" :data="outbound" /> :label="$t('out.port')"
<Shadowsocks v-if="outbound.type == outTypes.Shadowsocks" direction="out" :data="outbound" /> type="number"
<Vmess v-if="outbound.type == outTypes.VMess" :data="outbound" /> min="0"
<Trojan v-if="outbound.type == outTypes.Trojan" :data="outbound" /> hide-details
<Wireguard v-if="outbound.type == outTypes.Wireguard" :data="outbound" /> v-model.number="outbound.server_port">
<Hysteria v-if="outbound.type == outTypes.Hysteria" direction="out" :data="outbound" /> </v-text-field>
<ShadowTls v-if="outbound.type == outTypes.ShadowTLS" :data="outbound" /> </v-col>
<Vless v-if="outbound.type == outTypes.VLESS" :data="outbound" /> </v-row>
<Tuic v-if="outbound.type == outTypes.TUIC" direction="out" :data="outbound" /> <Direct v-if="outbound.type == outTypes.Direct" direction="out" :data="outbound" />
<Hysteria2 v-if="outbound.type == outTypes.Hysteria2" direction="out" :data="outbound" /> <Socks v-if="outbound.type == outTypes.SOCKS" :data="outbound" />
<Tor v-if="outbound.type == outTypes.Tor" :data="outbound" /> <Http v-if="outbound.type == outTypes.HTTP" :data="outbound" />
<Ssh v-if="outbound.type == outTypes.SSH" :data="outbound" /> <Shadowsocks v-if="outbound.type == outTypes.Shadowsocks" direction="out" :data="outbound" />
<Selector v-if="outbound.type == outTypes.Selector" :data="outbound" :tags="tags" /> <Vmess v-if="outbound.type == outTypes.VMess" :data="outbound" />
<UrlTest v-if="outbound.type == outTypes.URLTest" :data="outbound" :tags="tags" /> <Trojan v-if="outbound.type == outTypes.Trojan" :data="outbound" />
<Wireguard v-if="outbound.type == outTypes.Wireguard" :data="outbound" />
<Hysteria v-if="outbound.type == outTypes.Hysteria" direction="out" :data="outbound" />
<ShadowTls v-if="outbound.type == outTypes.ShadowTLS" :data="outbound" />
<Vless v-if="outbound.type == outTypes.VLESS" :data="outbound" />
<Tuic v-if="outbound.type == outTypes.TUIC" direction="out" :data="outbound" />
<Hysteria2 v-if="outbound.type == outTypes.Hysteria2" direction="out" :data="outbound" />
<Tor v-if="outbound.type == outTypes.Tor" :data="outbound" />
<Ssh v-if="outbound.type == outTypes.SSH" :data="outbound" />
<Selector v-if="outbound.type == outTypes.Selector" :data="outbound" :tags="tags" />
<UrlTest v-if="outbound.type == outTypes.URLTest" :data="outbound" :tags="tags" />
<Transport v-if="Object.hasOwn(outbound,'transport')" :data="outbound" /> <Transport v-if="Object.hasOwn(outbound,'transport')" :data="outbound" />
<OutTLS v-if="Object.hasOwn(outbound,'tls')" :outbound="outbound" /> <OutTLS v-if="Object.hasOwn(outbound,'tls')" :outbound="outbound" />
<Multiplex v-if="Object.hasOwn(outbound,'multiplex')" direction="out" :data="outbound" /> <Multiplex v-if="Object.hasOwn(outbound,'multiplex')" direction="out" :data="outbound" />
<Dial v-if="!NoDial.includes(outbound.type)" :dial="outbound" :outTags="tags" /> <Dial v-if="!NoDial.includes(outbound.type)" :dial="outbound" :outTags="tags" />
<v-switch v-model="outboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch> <v-switch v-model="outboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-window-item>
<v-window-item value="t2">
<v-row>
<v-col cols="12">
<v-text-field v-model="link" :label="$t('client.external')" hide-details />
</v-col>
<v-col cols="12" align="center">
<v-btn hide-details variant="tonal" :loading="loading" @click="linkConvert">{{ $t('submit') }}</v-btn>
</v-col>
</v-row>
</v-window-item>
</v-window>
</v-container>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@@ -89,7 +112,7 @@ import RandomUtil from '@/plugins/randomUtil'
import Dial from '@/components/Dial.vue' import Dial from '@/components/Dial.vue'
import Multiplex from '@/components/Multiplex.vue' import Multiplex from '@/components/Multiplex.vue'
import Transport from '@/components/Transport.vue' import Transport from '@/components/Transport.vue'
import OutTLS from '@/components/OutTLS.vue' import OutTLS from '@/components/tls/OutTLS.vue'
import Direct from '@/components/protocols/Direct.vue' import Direct from '@/components/protocols/Direct.vue'
import Socks from '@/components/protocols/Socks.vue' import Socks from '@/components/protocols/Socks.vue'
import Http from '@/components/protocols/Http.vue' import Http from '@/components/protocols/Http.vue'
@@ -106,6 +129,7 @@ import Tor from '@/components/protocols/Tor.vue'
import Ssh from '@/components/protocols/Ssh.vue' import Ssh from '@/components/protocols/Ssh.vue'
import Selector from '@/components/protocols/Selector.vue' import Selector from '@/components/protocols/Selector.vue'
import UrlTest from '@/components/protocols/UrlTest.vue' import UrlTest from '@/components/protocols/UrlTest.vue'
import HttpUtils from '@/plugins/httputil'
export default { export default {
props: ['visible', 'data', 'id', 'stats', 'tags'], props: ['visible', 'data', 'id', 'stats', 'tags'],
emits: ['close', 'save'], emits: ['close', 'save'],
@@ -113,6 +137,8 @@ export default {
return { return {
outbound: createOutbound("direct",{ "tag": "" }), outbound: createOutbound("direct",{ "tag": "" }),
title: "add", title: "add",
tab: "t1",
link: "",
loading: false, loading: false,
outTypes: OutTypes, outTypes: OutTypes,
outboundStats: false, outboundStats: false,
@@ -131,6 +157,7 @@ export default {
this.outbound = createOutbound("direct",{ tag: "direct-" + RandomUtil.randomSeq(3) }) this.outbound = createOutbound("direct",{ tag: "direct-" + RandomUtil.randomSeq(3) })
this.title = "add" this.title = "add"
} }
this.tab = "t1"
this.outboundStats = this.$props.stats this.outboundStats = this.$props.stats
}, },
changeType() { changeType() {
@@ -149,6 +176,18 @@ export default {
this.$emit('save', this.outbound, this.outboundStats) this.$emit('save', this.outbound, this.outboundStats)
this.loading = false this.loading = false
}, },
async linkConvert() {
if (this.link.length>0){
this.loading = true
const msg = await HttpUtils.post('api/linkConvert', { link: this.link })
this.loading = false
if (msg.success) {
this.outbound = msg.obj
this.tab = "t1"
this.link = ""
}
}
}
}, },
watch: { watch: {
visible(newValue) { visible(newValue) {
+58 -14
View File
@@ -9,19 +9,46 @@
</v-row> </v-row>
</v-card-title> </v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-text> <v-card-text style="overflow-y: auto; padding: 0">
<v-row> <v-tabs
<v-col style="text-align: center;" @click="copyToClipboard(clientSub)"> v-model="tab"
<v-chip>{{ $t('setting.sub') }}</v-chip> density="compact"
<QrcodeVue :value="clientSub" :size="300" :margin="1" style="border-radius: 1rem;" /> fixed-tabs
</v-col> align-tabs="center"
</v-row> >
<v-row v-for="l in clientLinks"> <v-tab value="sub">{{ $t('setting.sub') }}</v-tab>
<v-col style="text-align: center;" @click="copyToClipboard(l.uri)"> <v-tab value="link">{{ $t('client.links') }}</v-tab>
<v-chip>{{ l.remark?? "-" }}</v-chip><br /> </v-tabs>
<QrcodeVue :value="l.uri" :size="300" :margin="1" style="border-radius: 1rem;" /> <v-window v-model="tab" style="margin-top: 10px;">
</v-col> <v-window-item value="sub">
</v-row> <v-row>
<v-col style="text-align: center;">
<v-chip>{{ $t('setting.sub') }}</v-chip><br />
<QrcodeVue :value="clientSub" :size="size" @click="copyToClipboard(clientSub)" :margin="1" style="border-radius: 1rem;" />
</v-col>
</v-row>
<v-row>
<v-col style="text-align: center;">
<v-chip>{{ $t('setting.jsonSub') }}</v-chip><br />
<QrcodeVue :value="clientSub + '?format=json'" :size="size" @click="copyToClipboard(clientSub + '?format=json')" :margin="1" style="border-radius: 1rem;" />
</v-col>
</v-row>
<v-row>
<v-col style="text-align: center;">
<v-chip>SING-BOX</v-chip><br />
<QrcodeVue :value="singbox" :size="size" @click="copyToClipboard(singbox)" :margin="1" style="border-radius: .8rem;" />
</v-col>
</v-row>
</v-window-item>
<v-window-item value="link">
<v-row v-for="l in clientLinks">
<v-col style="text-align: center;">
<v-chip>{{ l.remark?? $t('client.' + l.type) }}</v-chip><br />
<QrcodeVue :value="l.uri" :size="size" @click="copyToClipboard(l.uri)" :margin="1" style="border-radius: .5rem;" />
</v-col>
</v-row>
</v-window-item>
</v-window>
</v-card-text> </v-card-text>
</v-card> </v-card>
</v-dialog> </v-dialog>
@@ -35,9 +62,10 @@ import { i18n } from '@/locales'
import { push } from 'notivue' import { push } from 'notivue'
export default { export default {
props: ['index'], props: ['index', 'visible'],
data() { data() {
return { return {
tab: "sub",
} }
}, },
methods: { methods: {
@@ -81,10 +109,26 @@ export default {
clientSub() { clientSub() {
return Data().subURI + this.client.name return Data().subURI + this.client.name
}, },
singbox() {
const url = Data().subURI + this.client.name + "?format=json"
return "sing-box://import-remote-profile?url=" + encodeURIComponent(url) + "#" + this.client.name
},
clientLinks() { clientLinks() {
return this.client.links?? [] return this.client.links?? []
},
size() {
if (window.innerWidth > 380) return 300
if (window.innerWidth > 330) return 280
return 250
} }
}, },
watch: {
visible(v) {
if (v) {
this.tab = "sub"
}
},
},
components: { QrcodeVue } components: { QrcodeVue }
} }
</script> </script>
+34 -10
View File
@@ -3,8 +3,8 @@
<v-card class="rounded-lg" :loading="loading" color="background"> <v-card class="rounded-lg" :loading="loading" color="background">
<v-card-title> <v-card-title>
<v-row> <v-row>
<v-col> <v-col cols="auto">
{{ $t('stats.graphTitle') + " - " + $t('objects.' + resource) + " : " + tag }} {{ $t('stats.graphTitle') }}
</v-col> </v-col>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-col cols="auto"><v-icon icon="mdi-close" @click="$emit('close')"></v-icon></v-col> <v-col cols="auto"><v-icon icon="mdi-close" @click="$emit('close')"></v-icon></v-col>
@@ -12,7 +12,13 @@
</v-card-title> </v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-text style="padding: 0 16px;"> <v-card-text style="padding: 0 16px;">
<v-container id="container"> <div style="text-align: center; margin: 5px;">
{{ $t('objects.' + resource) + " : " + tag }}
</div>
<v-radio-group v-model="limit" @change="loadData" density="compact" :loading="loading" inline hide-details>
<v-radio v-for="p in periods" :label="p.title" :value="p.value"></v-radio>
</v-radio-group>
<v-container id="container" style="height:40vh;">
<v-alert :text="$t('noData')" type="warning" variant="outlined" v-if="alert"></v-alert> <v-alert :text="$t('noData')" type="warning" variant="outlined" v-if="alert"></v-alert>
<Line v-if="loaded" :data="usage" :options="<any>options" /> <Line v-if="loaded" :data="usage" :options="<any>options" />
</v-container> </v-container>
@@ -60,13 +66,29 @@ export default {
loaded: false, loaded: false,
alert: false, alert: false,
intervalId: <any>0, intervalId: <any>0,
limit: 1,
periods: [
{ value: 1, title: i18n.global.n(1) + i18n.global.t('date.h')},
{ value: 6, title: i18n.global.n(6) + i18n.global.t('date.h')},
{ value: 12, title: i18n.global.n(12) + i18n.global.t('date.h')},
{ value: 24, title: i18n.global.n(1) + i18n.global.t('date.d')},
{ value: 48, title: i18n.global.n(2) + i18n.global.t('date.d')},
{ value: 240, title: i18n.global.n(10) + i18n.global.t('date.d')},
{ value: 480, title: i18n.global.n(20) + i18n.global.t('date.d')},
{ value: 720, title: i18n.global.n(30) + i18n.global.t('date.d')},
{ value: 1440, title: i18n.global.n(60) + i18n.global.t('date.d')},
{ value: 2160, title: i18n.global.n(90) + i18n.global.t('date.d')},
],
options: { options: {
responsive: true, responsive: true,
maintainAspectRatio: true, maintainAspectRatio: false,
interaction: { interaction: {
intersect: false, intersect: false,
mode: 'index', mode: 'index',
}, },
elements: {
point: { pointStyle: 'crossRot' }
},
plugins: { plugins: {
tooltip: { tooltip: {
callbacks: { callbacks: {
@@ -99,13 +121,13 @@ export default {
} }
}, },
methods: { methods: {
async loadData(limit: number) { async loadData() {
this.loading = true this.loading = true
const data = await HttpUtils.get('api/stats', { resource: this.resource, tag: this.tag, limit: limit }) const data = await HttpUtils.get('api/stats', { resource: this.resource, tag: this.tag, limit: this.limit })
if (data.success && data.obj) { if (data.success && data.obj) {
const obj = <any[]>data.obj const obj = <any[]>data.obj
const l = String(i18n.global.locale) == 'fa' ? "fa-IR" : "en-US" const l = String(i18n.global.locale) == 'fa' ? "fa-IR" : "en-US"
const oneStep = limit * 3600 * 1000 / 360 // Each 10 sec const oneStep = this.limit * 3600 * 1000 / 360 // Each 10 sec
const now = new Date().getTime() const now = new Date().getTime()
const steps = <number[]>[] const steps = <number[]>[]
for (let i = 360; i >= 0; i--) { for (let i = 360; i >= 0; i--) {
@@ -145,8 +167,10 @@ export default {
], ],
} }
this.loaded = true this.loaded = true
this.alert = false
} else { } else {
this.alert = true this.alert = true
this.loaded = false
} }
this.loading = false this.loading = false
}, },
@@ -163,10 +187,10 @@ export default {
watch: { watch: {
visible(v) { visible(v) {
if (v) { if (v) {
const limit = 1 this.limit = 1
this.loadData(limit) this.loadData()
this.intervalId = setInterval(() => { this.intervalId = setInterval(() => {
this.loadData(limit) this.loadData()
}, 10000) }, 10000)
} else { } else {
this.loaded = false this.loaded = false
+129 -21
View File
@@ -5,7 +5,7 @@
{{ $t('actions.' + title) + " " + $t('objects.tls') }} {{ $t('actions.' + title) + " " + $t('objects.tls') }}
</v-card-title> </v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-text> <v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-card class="rounded-lg"> <v-card class="rounded-lg">
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
@@ -90,6 +90,20 @@
>{{ $t('tls.useText') }}</v-btn> >{{ $t('tls.useText') }}</v-btn>
</v-btn-toggle> </v-btn-toggle>
</v-col> </v-col>
<v-spacer></v-spacer>
<v-col cols="auto">
<v-btn
variant="tonal"
density="compact"
icon="mdi-key-star"
@click="genSelfSigned"
:loading="loading">
<v-icon />
<v-tooltip activator="parent" location="top">
{{ $t('actions.generate') }}
</v-tooltip>
</v-btn>
</v-col>
</v-row> </v-row>
<v-row v-if="usePath == 0"> <v-row v-if="usePath == 0">
<v-col cols="12" sm="6"> <v-col cols="12" sm="6">
@@ -108,14 +122,14 @@
</v-col> </v-col>
</v-row> </v-row>
<v-row v-else> <v-row v-else>
<v-col cols="12" sm="6"> <v-col cols="12">
<v-textarea <v-textarea
:label="$t('tls.cert')" :label="$t('tls.cert')"
hide-details hide-details
v-model="certText"> v-model="certText">
</v-textarea> </v-textarea>
</v-col> </v-col>
<v-col cols="12" sm="6"> <v-col cols="12">
<v-textarea <v-textarea
:label="$t('tls.key')" :label="$t('tls.key')"
hide-details hide-details
@@ -124,14 +138,6 @@
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6" md="4" v-if="outTls.utls != undefined">
<v-select
hide-details
label="Fingerprint"
:items="fingerprints"
v-model="outTls.utls.fingerprint">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.disableSni')" v-model="disableSni" hide-details></v-switch> <v-switch color="primary" :label="$t('tls.disableSni')" v-model="disableSni" hide-details></v-switch>
</v-col> </v-col>
@@ -158,26 +164,42 @@
v-model="server_port"> v-model="server_port">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-spacer></v-spacer>
<v-col cols="auto">
<v-btn
variant="tonal"
density="compact"
icon="mdi-key-star"
@click="genRealityKey"
:loading="loading">
<v-icon />
<v-tooltip activator="parent" location="top">
{{ $t('actions.generate') }}
</v-tooltip>
</v-btn>
</v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" md="6"> <v-col cols="12">
<v-text-field <v-text-field
:label="$t('tls.privKey')" :label="$t('tls.privKey')"
hide-details hide-details
v-model="inTls.reality.private_key"> v-model="inTls.reality.private_key">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" md="6"> <v-col cols="12">
<v-text-field <v-text-field
:label="$t('tls.pubKey')" :label="$t('tls.pubKey')"
hide-details hide-details
v-model="outTls.reality.public_key"> v-model="outTls.reality.public_key">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" md="4"> <v-col cols="12">
<v-text-field <v-text-field
label="Short IDs" label="Short IDs"
hide-details hide-details
append-icon="mdi-refresh"
@click:append="randomSID"
v-model="short_id"> v-model="short_id">
</v-text-field> </v-text-field>
</v-col> </v-col>
@@ -193,11 +215,21 @@
</v-col> </v-col>
</v-row> </v-row>
</template> </template>
<v-row v-if="outTls.utls != undefined">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
label="Fingerprint"
:items="fingerprints"
v-model="outTls.utls.fingerprint">
</v-select>
</v-col>
</v-row>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>{{ $t('tls.options') }}</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
@@ -217,15 +249,15 @@
<v-list-item> <v-list-item>
<v-switch v-model="optionCS" color="primary" :label="$t('tls.cs')" hide-details></v-switch> <v-switch v-model="optionCS" color="primary" :label="$t('tls.cs')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item>
<v-switch v-model="optionFP" color="primary" label="UTLS" hide-details></v-switch>
</v-list-item>
</template> </template>
<template v-else> <template v-else>
<v-list-item> <v-list-item>
<v-switch v-model="optionTime" color="primary" label="Max Time Difference" hide-details></v-switch> <v-switch v-model="optionTime" color="primary" label="Max Time Difference" hide-details></v-switch>
</v-list-item> </v-list-item>
</template> </template>
<v-list-item>
<v-switch v-model="optionFP" color="primary" label="UTLS" hide-details></v-switch>
</v-list-item>
</v-list> </v-list>
</v-card> </v-card>
</v-menu> </v-menu>
@@ -259,8 +291,12 @@
<script lang="ts"> <script lang="ts">
import { iTls, defaultInTls } from '@/types/inTls' import { iTls, defaultInTls } from '@/types/inTls'
import { oTls, defaultOutTls } from '@/types/outTls' import { oTls, defaultOutTls } from '@/types/outTls'
import AcmeVue from '@/components/Acme.vue' import AcmeVue from '@/components/tls/Acme.vue'
import EchVue from '@/components/Ech.vue' import EchVue from '@/components/tls/Ech.vue'
import HttpUtils from '@/plugins/httputil'
import { push } from 'notivue'
import { i18n } from '@/locales'
import RandomUtil from '@/plugins/randomUtil'
export default { export default {
props: ['visible', 'data', 'index'], props: ['visible', 'data', 'index'],
emits: ['close', 'save'], emits: ['close', 'save'],
@@ -334,7 +370,10 @@ export default {
}, },
changeTlsType(){ changeTlsType(){
if (this.tlsType) { if (this.tlsType) {
this.tls.server = <iTls>{ enabled: true, reality: { enabled: true, handshake: { server_port: 443 } }, server_name: "" } this.tls.server = <iTls>{
enabled: true,
reality: { enabled: true, handshake: { server_port: 443 }, short_id: RandomUtil.randomShortId() },
server_name: "" }
this.tls.client = <oTls>{ reality: { public_key: "" } } this.tls.client = <oTls>{ reality: { public_key: "" } }
} else { } else {
this.tls.server = <iTls>{ enabled: true } this.tls.server = <iTls>{ enabled: true }
@@ -350,6 +389,75 @@ export default {
this.$emit('save', this.tls) this.$emit('save', this.tls)
this.loading = false this.loading = false
}, },
async genSelfSigned(){
this.loading = true
const msg = await HttpUtils.get('api/keypairs', { k: "tls", o: this.inTls.server_name?? "''" })
this.loading = false
if (msg.success) {
this.inTls.key_path=undefined
this.inTls.certificate_path=undefined
this.usePath = 1
if (msg.obj.length>0){
let privateKey = <string[]>[]
let publicKey = <string[]>[]
let isPrivateKey = false
let isPublicKey = false
msg.obj.forEach((line:string) => {
if (line === "-----BEGIN PRIVATE KEY-----") {
isPrivateKey = true
isPublicKey = false
privateKey.push(line)
} else if (line === "-----END PRIVATE KEY-----") {
isPrivateKey = false
privateKey.push(line)
} else if (line === "-----BEGIN CERTIFICATE-----") {
isPublicKey = true
isPrivateKey = false
publicKey.push(line)
} else if (line === "-----END CERTIFICATE-----") {
isPublicKey = false
publicKey.push(line)
} else if (isPrivateKey) {
privateKey.push(line)
} else if (isPublicKey) {
publicKey.push(line)
}
})
this.inTls.key = privateKey?? undefined
this.inTls.certificate = publicKey?? undefined
} else {
push.error({
message: i18n.global.t('error') + ": " + msg.obj
})
}
}
},
async genRealityKey(){
this.loading = true
const msg = await HttpUtils.get('api/keypairs', { k: "reality" })
this.loading = false
if (msg.success) {
msg.obj.forEach((line:string) => {
if (this.inTls.reality && this.outTls.reality){
if (line.startsWith("PrivateKey")){
this.inTls.reality.private_key = line.substring(12)
}
if (line.startsWith("PublicKey")){
this.outTls.reality.public_key = line.substring(11)
}
}
})
} else {
push.error({
message: i18n.global.t('error') + ": " + msg.obj
})
}
},
randomSID(){
this.short_id = RandomUtil.randomShortId().join(',')
}
}, },
computed: { computed: {
inTls(): iTls { inTls(): iTls {
+33 -1
View File
@@ -4,6 +4,9 @@ export default {
failed: "failed", failed: "failed",
enable: "Enable", enable: "Enable",
disable: "Disable", disable: "Disable",
none: "None",
all: "All",
filter: "Filter",
loading: "Loading...", loading: "Loading...",
confirm: "Are you sure ?", confirm: "Are you sure ?",
yes: "yes", yes: "yes",
@@ -24,6 +27,7 @@ export default {
email: "Email", email: "Email",
commaSeparated: "(comma separated)", commaSeparated: "(comma separated)",
count: "Count", count: "Count",
template: "Template",
error: { error: {
dplData: "Duplicate Data", dplData: "Duplicate Data",
core: "Sing-Box Error", core: "Sing-Box Error",
@@ -87,16 +91,19 @@ export default {
actions: { actions: {
action: "Action", action: "Action",
add: "Add", add: "Add",
new: "Add", new: "New",
edit: "Edit", edit: "Edit",
del: "Delete", del: "Delete",
clone: "Clone",
save: "Save", save: "Save",
update: "Update", update: "Update",
submit: "Submit", submit: "Submit",
set: "Set", set: "Set",
generate: "Generate",
disable: "Disable", disable: "Disable",
close: "Close", close: "Close",
restartApp: "Restart App", restartApp: "Restart App",
restartSb: "Restart Singbox",
}, },
login: { login: {
title: "Login", title: "Login",
@@ -139,10 +146,20 @@ export default {
path: "Default Path", path: "Default Path",
update: "Automatic Update Time", update: "Automatic Update Time",
subUri: "Subscription URI", subUri: "Subscription URI",
jsonSub: "JSON Subscription",
toDirect: "Route to Direct",
toBlock: "Route to Block",
timestamp: "Timestamp",
globalDns: "Global DNS",
directDns: "Direct DNS",
toDirectDns: "Route to Direct DNS",
jsonSubOptions: "Other Options",
excludePkg: "Exclude Packages",
}, },
client: { client: {
name: "Name", name: "Name",
desc: "Description", desc: "Description",
group: "Group",
inboundTags: "Inbound Tags", inboundTags: "Inbound Tags",
basics: "Basics", basics: "Basics",
config: "Config", config: "Config",
@@ -150,6 +167,11 @@ export default {
external: "External Link", external: "External Link",
sub: "External Subscription", sub: "External Subscription",
}, },
bulk: {
add: "Add Bulk",
order: "Order",
random: "Random",
},
types: { types: {
un: "Username", un: "Username",
pw: "Password", pw: "Password",
@@ -185,6 +207,10 @@ export default {
authTimeout: "Authentication Timeout", authTimeout: "Authentication Timeout",
hb: "Heartbeat", hb: "Heartbeat",
}, },
tun: {
addr: "Addresses",
ifName: "Interface Name",
},
vless: { vless: {
flow: "Flow", flow: "Flow",
udpEnc: "UDP Packet Encoding", udpEnc: "UDP Packet Encoding",
@@ -223,6 +249,11 @@ export default {
port: "Port", port: "Port",
clients: "Enable Clients", clients: "Enable Clients",
ssMethod: "Method", ssMethod: "Method",
sSide: "Server Side",
cSide: "Client Side",
multiDomain: "Multi Domain",
remark: "Remark",
mdOption: "Multi Domain Options",
}, },
listen: { listen: {
sniffing: "Sniffing", sniffing: "Sniffing",
@@ -312,6 +343,7 @@ export default {
final: "Final", final: "Final",
server: "Server", server: "Server",
firstServer: "First Server", firstServer: "First Server",
addrResolver: "Address Resolver",
}, },
routing: { routing: {
title: "Routing", title: "Routing",
+34 -2
View File
@@ -4,6 +4,9 @@ export default {
failed: "خطا", failed: "خطا",
enable: "فعال", enable: "فعال",
disable: "غیرفعال", disable: "غیرفعال",
none: "هیچ",
all: "همه",
filter: "فیلتر",
loading: "در حال بارگذاری...", loading: "در حال بارگذاری...",
confirm: "آیا مطمئن هستید ؟", confirm: "آیا مطمئن هستید ؟",
yes: "بله", yes: "بله",
@@ -24,6 +27,7 @@ export default {
email: "ایمیل", email: "ایمیل",
commaSeparated: "(جداشده با کاما)", commaSeparated: "(جداشده با کاما)",
count: "تعداد", count: "تعداد",
template: "الگو",
error: { error: {
dplData: "داده تکراری", dplData: "داده تکراری",
core: "خطا در سینگ‌باکس", core: "خطا در سینگ‌باکس",
@@ -86,16 +90,19 @@ export default {
actions: { actions: {
action: "فرمان", action: "فرمان",
add: "ایجاد", add: "ایجاد",
new: "ایجاد", new: "جدید",
edit: "ویرایش", edit: "ویرایش",
del: "حذف", del: "حذف",
clone: "شبیه‌سازی",
save: "ذخیره", save: "ذخیره",
update: "بروزرسانی", update: "بروزرسانی",
submit: "ارسال", submit: "ارسال",
set: "تنظیم", set: "تنظیم",
generate: "تولید",
disable: "غیرفعال", disable: "غیرفعال",
close: "بستن", close: "بستن",
restartApp: "ریستارت پنل", restartApp: "ریستارت پنل",
restartSb: "ریستارت سینگ‌باکس",
}, },
login: { login: {
title: "ورود", title: "ورود",
@@ -138,10 +145,20 @@ export default {
path: "مسیر پیشفرض", path: "مسیر پیشفرض",
update: "زمان بروزرسانی خودکار", update: "زمان بروزرسانی خودکار",
subUri: "آدرس نهایی سابسکریپشن", subUri: "آدرس نهایی سابسکریپشن",
jsonSub: "سابسکریپشن JSON",
toDirect: "هدایت مستقیم",
toBlock: "بستن مسیر",
timestamp: "نمایش زمان",
globalDns: "DNS کلی",
directDns: "DNS مستقیم",
toDirectDns: "هدایت به DNS مستقیم",
jsonSubOptions: "گزینه‌های دیگر",
excludePkg: "نرم‌افزارهای استثنا",
}, },
client: { client: {
name: "نام", name: "نام",
desc: "شرح", desc: "شرح",
group: "گروه",
inboundTags: "برچسب‌های ورودی", inboundTags: "برچسب‌های ورودی",
basics: "پایه", basics: "پایه",
config: "تنظیم", config: "تنظیم",
@@ -149,6 +166,11 @@ export default {
external: "لینک‌ خارجی", external: "لینک‌ خارجی",
sub: "سابسکریپشن خارجی", sub: "سابسکریپشن خارجی",
}, },
bulk: {
add: "ایجاد انبوه",
order: "ترتیب",
random: "تصادفی",
},
types: { types: {
un: "نام کاربری", un: "نام کاربری",
pw: "رمز", pw: "رمز",
@@ -184,6 +206,10 @@ export default {
authTimeout: "مهلت احراز هویت", authTimeout: "مهلت احراز هویت",
hb: "ضربان قلب", hb: "ضربان قلب",
}, },
tun: {
addr: "آدرس‌ها",
ifName: "نام اینترفیس",
},
vless: { vless: {
flow: "جریان", flow: "جریان",
udpEnc: "کدگذاری بسته UDP", udpEnc: "کدگذاری بسته UDP",
@@ -222,6 +248,11 @@ export default {
port: "پورت", port: "پورت",
clients: "فعال‌سازی کاربران", clients: "فعال‌سازی کاربران",
ssMethod: "روش", ssMethod: "روش",
sSide: "سمت سرور",
cSide: "سمت کاربر",
multiDomain: "دامنه چندگانه",
remark: "شرح",
mdOption: "گزینه‌های دامنه چندگانه",
}, },
listen: { listen: {
sniffing: "شنود آدرس", sniffing: "شنود آدرس",
@@ -282,7 +313,7 @@ export default {
privateIp: "آدرس های IP نامعتبر", privateIp: "آدرس های IP نامعتبر",
port: "پورت‌ها", port: "پورت‌ها",
portRange: "محدوده‌های پورت", portRange: "محدوده‌های پورت",
srcIp: "محدوده‌های آدرس IP مبدا", srcCidr: "محدوده‌های آدرس IP مبدا",
srcPrivateIp: "آدرس‌های IP مبدا نامعتبر", srcPrivateIp: "آدرس‌های IP مبدا نامعتبر",
srcPort: "پورت‌های مبدا", srcPort: "پورت‌های مبدا",
srcPortRange: "محدوده پورتهای منبع", srcPortRange: "محدوده پورتهای منبع",
@@ -311,6 +342,7 @@ export default {
final: "سرور نهایی", final: "سرور نهایی",
server: "سرور", server: "سرور",
firstServer: "سرور نخست", firstServer: "سرور نخست",
addrResolver: "حل کننده دامنه",
}, },
routing: { routing: {
title: "مسیریابی", title: "مسیریابی",
+7 -4
View File
@@ -4,6 +4,7 @@ import fa from './fa'
import vi from './vi' import vi from './vi'
import zhcn from './zhcn' import zhcn from './zhcn'
import zhtw from './zhtw' import zhtw from './zhtw'
import ru from './ru'
export const i18n = createI18n({ export const i18n = createI18n({
legacy: false, legacy: false,
@@ -13,8 +14,9 @@ export const i18n = createI18n({
en: en, en: en,
fa: fa, fa: fa,
vi: vi, vi: vi,
zhcn: zhcn, zhHans: zhcn,
zhtw: zhtw zhHant: zhtw,
ru: ru
}, },
}) })
@@ -22,6 +24,7 @@ export const languages = [
{ title: 'English', value: 'en' }, { title: 'English', value: 'en' },
{ title: 'فارسی', value: 'fa' }, { title: 'فارسی', value: 'fa' },
{ title: 'Tiếng Việt', value: 'vi' }, { title: 'Tiếng Việt', value: 'vi' },
{ title: '简体中文', value: 'zhcn' }, { title: '简体中文', value: 'zhHans' },
{ title: '繁體中文', value: 'zhtw' }, { title: '繁體中文', value: 'zhHant' },
{ title: 'Русский', value: 'ru' },
] ]
+422
View File
@@ -0,0 +1,422 @@
export default {
message: "Добро пожаловать",
success: "успех",
failed: "ошибка",
enable: "Включить",
disable: "Отключить",
none: "Никакие",
all: "Все",
filter: "Фильтр",
loading: "Загрузка...",
confirm: "Вы уверены?",
yes: "да",
no: "нет",
unlimited: "бесконечный",
remained: "Осталось",
type: "Тип",
protocol: "Протокол",
submit: "Отправить",
reset: "Сбросить",
now: "Сейчас",
network: "Сеть",
copyToClipboard: "Копировать в буфер обмена",
noData: "Нет данных!",
invalidLogin: "Неверный логин!",
online: "В сети",
version: "Версия",
email: "Электронная почта",
commaSeparated: "(разделено запятыми)",
count: "Количество",
template: "Шаблон",
error: {
dplData: "Дублирующие данные",
core: "Ошибка Sing-Box",
},
pages: {
login: "Вход",
home: "Главная",
inbounds: "Входящие",
outbounds: "Исходящие",
clients: "Клиенты",
rules: "Правила",
tls: "Настройки TLS",
basics: "Основы",
admins: "Администраторы",
settings: "Настройки",
},
main: {
tiles: "Плитки",
gauges: "Датчики",
charts: "Графики",
infos: "Информация",
gauge: {
cpu: "Загрузка ЦП",
mem: "Загрузка ОЗУ",
},
chart: {
cpu: "Мониторинг ЦП",
mem: "Мониторинг ОЗУ",
net: "Сетевой трафик",
pnet: "Сетевые пакеты",
},
info: {
sys: "Информация о системе",
sbd: "Информация о Sing-Box",
host: "Хост",
cpu: "ЦП",
core: "Ядро",
uptime: "Время работы",
threads: "Потоки",
memory: "Память",
running: "Работает"
}
},
objects: {
inbound: "Входящий",
client: "Клиент",
outbound: "Исходящий",
rule: "Правило",
user: "Пользователь",
tag: "Тег",
listen: "Прослушивание",
dial: "Звонок",
tls: "TLS",
multiplex: "Мультиплекс",
transport: "Транспорт",
method: "Метод",
headers: "Заголовки",
key: "Ключ",
value: "Значение",
},
actions: {
action: "Действие",
add: "Добавить",
new: "Новый",
edit: "Редактировать",
del: "Удалить",
clone: "Клонировать",
save: "Сохранить",
update: "Обновить",
submit: "Отправить",
set: "Установить",
generate: "Генерировать",
disable: "Отключить",
close: "Закрыть",
restartApp: "Перезапустить приложение",
restartSb: "Перезапустить Singbox",
},
login: {
title: "Вход",
username: "Имя пользователя",
unRules: "Имя пользователя не может быть пустым",
password: "Пароль",
pwRules: "Пароль не может быть пустым",
},
menu: {
logout: "Выйти",
},
admin: {
changeCred: "Изменить учетные данные",
oldPass: "Текущий пароль",
newUname: "Новое имя пользователя",
newPass: "Новый пароль",
lastLogin: "Последний вход",
date: "Дата",
time: "Время",
changes: "Изменения",
actor: "Исполнитель",
key: "Ключ",
action: "Действие",
},
setting: {
interface: "Интерфейс",
sub: "Подписка",
addr: "Адрес",
port: "Порт",
webPath: "Базовый URI",
domain: "Домен",
sslKey: "Путь к SSL ключу",
sslCert: "Путь к SSL сертификату",
webUri: "URI панели",
sessionAge: "Максимальная длительность сессии",
trafficAge: "Максимальная длительность трафика",
timeLoc: "Часовой пояс",
subEncode: "Включить кодирование",
subInfo: "Включить информацию о клиенте",
path: "Путь по умолчанию",
update: "Время автоматического обновления",
subUri: "URI подписки",
jsonSub: "JSON подписка",
toDirect: "Маршрутизация на Direct",
toBlock: "Маршрутизация на Block",
timestamp: "Метка времени",
globalDns: "Глобальный DNS",
directDns: "Прямой DNS",
toDirectDns: "Маршрутизация на Direct DNS",
jsonSubOptions: "Другие параметры",
excludePkg: "Исключить пакеты",
},
client: {
name: "Имя",
desc: "Описание",
group: "Группа",
inboundTags: "Теги входящих",
basics: "Основы",
config: "Конфигурация",
links: "Ссылки",
external: "Внешняя ссылка",
sub: "Внешняя подписка",
},
bulk: {
add: "Добавить пакетно",
order: "Порядок",
random: "Случайный",
},
types: {
un: "Имя пользователя",
pw: "Пароль",
direct: {
overrideAddr: "Переопределить адрес",
overridePort: "Переопределить порт",
},
hy: {
obfs: "Обфусцированный пароль",
auth: "Пароль аутентификации",
hyOptions: "Параметры Hysteria",
hy2Options: "Параметры Hysteria2",
ignoreBw: "Игнорировать пропускную способность клиента",
},
shdwTls: {
hs: "Сервер рукопожатий",
addHS: "Добавить сервер рукопожатий",
},
ssh: {
passphrase: "Парольная фраза",
hostKey: "Ключи хоста",
algorithm: "Алгоритмы ключей",
clientVer: "Версия клиента",
options: "Параметры SSH",
},
tor: {
execPath: "Путь к исполняемому файлу",
dataDir: "Каталог данных",
extArgs: "Дополнительные аргументы",
},
tuic: {
congControl: "Контроль перегрузок",
authTimeout: "Таймаут аутентификации",
hb: "Сердцебиение",
},
tun: {
addr: "Адреса",
ifName: "Имя интерфейса",
},
vless: {
flow: "Поток",
udpEnc: "Кодирование UDP пакетов",
},
vmess: {
security: "Безопасность",
globalPadding: "Глобальная подкладка",
authLen: "Длина шифрования",
},
wg: {
privKey: "Приватный ключ",
pubKey: "Публичный ключ пира",
psk: "Предварительно разделенный ключ",
localIp: "Локальные IP",
worker: "Работники",
ifName: "Имя интерфейса",
sysIf: "Системный интерфейс",
gso: "Отключение сегментации",
options: "Параметры Wireguard",
multiPeer: "Множественный пир",
allowedIp: "Разрешенные IP",
peer: "Пир",
peers: "Пиры",
},
lb: {
defaultOut: "Исходящий по умолчанию",
interruptConn: "Прервать существующие соединения",
testUrl: "Тестовый URL",
interval: "Интервал",
tolerance: "Толерантность",
urlTestOptions: "Параметры теста URL"
}
},
in: {
addr: "Адрес",
port: "Порт",
clients: "Включить клиентов",
ssMethod: "Метод",
sSide: "Сторона сервера",
cSide: "Сторона клиента",
multiDomain: "Мультидомен",
remark: "Примечание",
mdOption: "Параметры мультидомена",
},
listen: {
sniffing: "Обнаружение",
sniffingTimeout: "Таймаут обнаружения",
sniffingOverride: "Переопределение назначения",
options: "Параметры прослушивания",
tcpOptions: "Параметры TCP",
udpOptions: "Параметры UDP",
detour: "Обход",
detourText: "Переадресация на входящий",
domainStrategy: "Стратегия домена",
},
dial: {
bindIf: "Привязка к сетевому интерфейсу",
bindIp4: "Привязка к IPv4",
bindIp6: "Привязка к IPv6",
reuseAddr: "Повторное использование адреса слушателя",
connTimeout: "Таймаут подключения",
fbTimeout: "Таймаут возврата",
options: "Параметры вызова",
detourText: "Переадресация на исходящий",
},
transport: {
enable: "Включить транспорт",
host: "Хост",
hosts: "Хосты",
path: "Путь",
httpMethod: "Метод запроса",
idleTimeout: "Таймаут бездействия",
pingTimeout: "Таймаут пинга",
grpcServiceName: "Имя службы",
grpcPws: "Разрешить без потока",
},
mux: {
enable: "Включить мультиплекс",
maxConn: "Максимальное количество соединений",
minStr: "Минимальное количество потоков",
maxStr: "Максимальное количество потоков",
padding: "Только подкладка",
enableBrutal: "Включить Brutal",
},
out: {
addr: "Адрес сервера",
port: "Порт сервера",
},
rule: {
add: "Добавить правило",
simple: "Простое",
logical: "Логическое",
mode: "Режим",
invert: "Инвертировать",
ipVer: "Версия IP",
domain: "Домены",
domainSufix: "Суффиксы доменов",
domainKw: "Ключевые слова домена",
domainRgx: "Регулярные выражения домена",
ip: "CIDR IP",
privateIp: "Недействительные диапазоны IP",
port: "Порты",
portRange: "Диапазоны портов",
srcCidr: "CIDR исходного IP",
srcPrivateIp: "Недействительные исходные IP",
srcPort: "Исходные порты",
srcPortRange: "Диапазоны исходных портов",
ruleset: "Наборы правил",
rulesetMatchSrc: "Набор правил для соответствия источника IPcidr",
options: "Параметры правила",
domainRules: "Домен/IP",
srcIpRules: "Источник IP",
srcPortRules: "Источник порта",
},
ruleset: {
add: "Добавить набор правил",
format: "Формат данных",
interval: "Интервалы обновления",
remote: "Удаленный",
local: "Локальный",
},
basic: {
log: {
title: "Журналы",
level: "Уровень",
output: "Вывод",
timestamp: "Включить метку времени",
},
dns: {
final: "Итоговый",
server: "Сервер",
firstServer: "Первый сервер",
addrResolver: "Разрешение адреса",
},
routing: {
title: "Маршрутизация",
defaultOut: "Исходящий по умолчанию",
defaultIf: "Сетевой интерфейс по умолчанию",
defaultRm: "Маршрут по умолчанию",
autoBind: "Автопривязка сетевого интерфейса",
},
exp: {
storeFakeIp: "Хранить поддельный IP",
},
},
tls: {
enable: "Включить TLS",
usePath: "Использовать путь",
useText: "Использовать текст",
certPath: "Путь к файлу сертификата",
keyPath: "Путь к файлу ключа",
cert: "Сертификат",
key: "Ключ",
options: "Параметры TLS",
minVer: "Минимальная версия",
maxVer: "Максимальная версия",
cs: "Наборы шифров",
privKey: "Приватный ключ",
pubKey: "Публичный ключ",
disableSni: "Отключить SNI",
insecure: "Разрешить небезопасное",
acme: {
options: "Параметры ACME",
dataDir: "Каталог данных",
defaultDomain: "Домен по умолчанию",
disableChallenges: "Отключить вызовы",
httpChallenge: "Отключить HTTP вызов",
tlsChallenge: "Отключить TLS вызов",
altPorts: "Альтернативные порты",
altHport: "Альтернативный HTTP порт",
altTport: "Альтернативный TLS порт",
caProvider: "Поставщик CA",
customCa: "Пользовательский поставщик CA",
extAcc: "Внешний аккаунт",
dns01: "DNS01 вызов",
dns01Provider: "Поставщик DNS01 вызова",
},
},
stats: {
upload: "Загрузка",
download: "Скачивание",
volume: "Объем",
usage: "Использование",
enable: "Включить статистику",
graphTitle: "График трафика",
B: "Б",
KB: "КБ",
MB: "МБ",
GB: "ГБ",
TB: "ТБ",
PB: "ПБ",
p: "п",
Kp: "Кп",
Mp: "Мп",
Gb: "Гб",
bps: "б/с",
Kbps: "Кб/с",
Mbps: "Мб/с",
},
date: {
expiry: "Срок действия",
expired: "Истек",
d: "д",
h: "ч",
m: "м",
s: "с",
ms: "мс",
},
}
+33 -1
View File
@@ -4,6 +4,9 @@ export default {
failed: "Thất bại", failed: "Thất bại",
enable: "Kích hoạt", enable: "Kích hoạt",
disable: "Vô hiệu hóa", disable: "Vô hiệu hóa",
none: "Không",
all: "Tất cả",
filter: "Bộ lọc",
loading: "Đang tải...", loading: "Đang tải...",
confirm: "Bạn chắc chắn chứ?", confirm: "Bạn chắc chắn chứ?",
yes: "có", yes: "có",
@@ -24,6 +27,7 @@ export default {
email: "Email", email: "Email",
commaSeparated: "(được phân tách bằng dấu phẩy)", commaSeparated: "(được phân tách bằng dấu phẩy)",
count: "Đếm", count: "Đếm",
template: "Mẫu",
error: { error: {
dplData: "Dữ liệu trùng lặp", dplData: "Dữ liệu trùng lặp",
core: "Lỗi Sing-Box", core: "Lỗi Sing-Box",
@@ -87,16 +91,19 @@ export default {
actions: { actions: {
action: "Hành động", action: "Hành động",
add: "Thêm", add: "Thêm",
new: "Thêm", new: "Mới",
edit: "Chỉnh sửa", edit: "Chỉnh sửa",
del: "Xóa", del: "Xóa",
clone: "Nhân bản",
save: "Lưu", save: "Lưu",
update: "Cập nhật", update: "Cập nhật",
submit: "Gửi", submit: "Gửi",
set: "Đặt", set: "Đặt",
generate: "Tạo ra",
disable: "Vô hiệu hóa", disable: "Vô hiệu hóa",
close: "Đóng", close: "Đóng",
restartApp: "Khởi động lại ứng dụng", restartApp: "Khởi động lại ứng dụng",
restartSb: "Khởi động lại Singbox",
}, },
login: { login: {
title: "Đăng nhập", title: "Đăng nhập",
@@ -139,10 +146,20 @@ export default {
path: "Đường dẫn mặc định", path: "Đường dẫn mặc định",
update: "Thời gian cập nhật tự động", update: "Thời gian cập nhật tự động",
subUri: "URI đăng ký", subUri: "URI đăng ký",
jsonSub: "Đăng ký JSON",
toDirect: "Chuyển hướng tới Trực tiếp",
toBlock: "Chuyển hướng tới Chặn",
timestamp: "Dấu thời gian",
globalDns: "DNS Toàn cầu",
directDns: "DNS Trực tiếp",
toDirectDns: "Chuyển hướng tới DNS Trực tiếp",
jsonSubOptions: "Tùy chọn Khác",
excludePkg: "Loại trừ Gói",
}, },
client: { client: {
name: "Tên", name: "Tên",
desc: "Mô tả", desc: "Mô tả",
group: "Nhóm",
inboundTags: "Thẻ đầu vào", inboundTags: "Thẻ đầu vào",
basics: "Cơ bản", basics: "Cơ bản",
config: "Cấu hình", config: "Cấu hình",
@@ -150,6 +167,11 @@ export default {
external: "Liên kết bên ngoài", external: "Liên kết bên ngoài",
sub: "Đăng ký bên ngoài", sub: "Đăng ký bên ngoài",
}, },
bulk: {
add: "Thêm Hàng loạt",
order: "Sắp xếp",
random: "Ngẫu nhiên",
},
types: { types: {
un: "Tên người dùng", un: "Tên người dùng",
pw: "Mật khẩu", pw: "Mật khẩu",
@@ -185,6 +207,10 @@ export default {
authTimeout: "Thời gian chờ Xác thực", authTimeout: "Thời gian chờ Xác thực",
hb: "Nhịp tim", hb: "Nhịp tim",
}, },
tun: {
addr: "Địa chỉ",
ifName: "Tên Giao diện",
},
vless: { vless: {
flow: "Luồng", flow: "Luồng",
udpEnc: "Mã hóa Gói UDP", udpEnc: "Mã hóa Gói UDP",
@@ -224,6 +250,11 @@ export default {
sniffing: "Đang Sniffing", sniffing: "Đang Sniffing",
clients: "Kích hoạt khách hàng", clients: "Kích hoạt khách hàng",
ssMethod: "Phương thức", ssMethod: "Phương thức",
sSide: "Phía Máy chủ",
cSide: "Phía Khách hàng",
multiDomain: "Nhiều Tên miền",
remark: "Ghi chú",
mdOption: "Tùy chọn Nhiều Tên miền",
}, },
listen: { listen: {
sniffing: "Đang Sniffing", sniffing: "Đang Sniffing",
@@ -313,6 +344,7 @@ export default {
final: "Cuối cùng", final: "Cuối cùng",
server: "Máy chủ", server: "Máy chủ",
firstServer: "Máy chủ Đầu tiên", firstServer: "Máy chủ Đầu tiên",
addrResolver: "Trình phân giải địa chỉ",
}, },
routing: { routing: {
title: "Định tuyến", title: "Định tuyến",
+65 -33
View File
@@ -4,6 +4,9 @@ export default {
failed: "失败", failed: "失败",
enable: "启用", enable: "启用",
disable: "禁用", disable: "禁用",
none: "无",
all: "全部",
filter: "过滤器",
loading: "加载中...", loading: "加载中...",
confirm: "是否确定?", confirm: "是否确定?",
yes: "确认", yes: "确认",
@@ -24,6 +27,7 @@ export default {
email: "电子邮件", email: "电子邮件",
commaSeparated: "(逗号分隔)", commaSeparated: "(逗号分隔)",
count: "计数", count: "计数",
template: "模板",
error: { error: {
dplData: "重复数据", dplData: "重复数据",
core: "Sing-Box 错误", core: "Sing-Box 错误",
@@ -74,29 +78,32 @@ export default {
rule: "规则", rule: "规则",
user: "用户", user: "用户",
tag: "标签", tag: "标签",
listen: "听", listen: "听",
dial: "拨号", dial: "拨号",
tls: "TLS", tls: "TLS",
multiplex: "多路复用", multiplex: "多路复用",
transport: "传输", transport: "传输",
method: "方法", method: "方法",
headers: "标头", headers: "标头",
key: "钥匙", key: "",
value: "值", value: "值",
}, },
actions: { actions: {
action: "操作", action: "操作",
add: "添加", add: "添加",
new: "添加", new: "新建",
edit: "编辑", edit: "编辑",
del: "删除", del: "删除",
clone: "克隆",
save: "保存", save: "保存",
update: "更新", update: "更新",
submit: "提交", submit: "提交",
set: "设置", set: "设置",
generate: "生成",
disable: "禁用", disable: "禁用",
close: "关闭", close: "关闭",
restartApp: "重启面板", restartApp: "重启面板",
restartSb: "重启 Singbox",
}, },
login: { login: {
title: "登录", title: "登录",
@@ -126,23 +133,33 @@ export default {
sub: "订阅", sub: "订阅",
addr: "地址", addr: "地址",
port: "端口", port: "端口",
webPath: "基本 URI", webPath: "面板路径",
domain: "域名", domain: "域名",
sslKey: "SSL 密钥 (Key) 路径", sslKey: "SSL 密钥 (Key) 路径",
sslCert: "SSL 证书 (cert) 路径", sslCert: "SSL 证书 (cert) 路径",
webUri: "面板 URI", webUri: "面板 URI",
sessionAge: "会话最大连接数", sessionAge: "会话超时时限",
trafficAge: "流量最大年龄", trafficAge: "流量过期时限",
timeLoc: "时区", timeLoc: "时区",
subEncode: "启用编码", subEncode: "启用 Base64 编码",
subInfo: "启用用户信息", subInfo: "启用用户信息",
path: "默认路径", path: "默认路径",
update: "自动更新时间", update: "自动更新时间",
subUri: "订阅 URL", subUri: "订阅 URI",
jsonSub: "JSON 订阅",
toDirect: "路由到直连",
toBlock: "路由到阻止",
timestamp: "时间戳",
globalDns: "全局 DNS",
directDns: "直连 DNS",
toDirectDns: "路由到直连 DNS",
jsonSubOptions: "其他选项",
excludePkg: "排除包",
}, },
client: { client: {
name: "名称", name: "名称",
desc: "描述", desc: "描述",
group: "组",
inboundTags: "入站标签", inboundTags: "入站标签",
basics: "基础", basics: "基础",
config: "配置", config: "配置",
@@ -150,6 +167,11 @@ export default {
external: "外部链接", external: "外部链接",
sub: "外部订阅", sub: "外部订阅",
}, },
bulk: {
add: "批量添加",
order: "排序",
random: "随机",
},
types: { types: {
un: "用户名", un: "用户名",
pw: "密码", pw: "密码",
@@ -183,10 +205,14 @@ export default {
tuic: { tuic: {
congControl: "拥塞控制", congControl: "拥塞控制",
authTimeout: "认证超时", authTimeout: "认证超时",
hb: "心跳", hb: "心跳",
},
tun: {
addr: "地址",
ifName: "接口名称",
}, },
vless: { vless: {
flow: "流", flow: "流",
udpEnc: "UDP 数据包编码", udpEnc: "UDP 数据包编码",
}, },
vmess: { vmess: {
@@ -224,22 +250,27 @@ export default {
sniffing: "嗅探", sniffing: "嗅探",
clients: "启用客户端", clients: "启用客户端",
ssMethod: "方法", ssMethod: "方法",
sSide: "服务器端",
cSide: "客户端",
multiDomain: "多域名",
remark: "备注",
mdOption: "多域名选项",
}, },
listen: { listen: {
sniffing: "嗅探", sniffing: "嗅探",
sniffingTimeout: "嗅探超时", sniffingTimeout: "嗅探超时",
sniffingOverride: "覆盖目的地", sniffingOverride: "覆盖目标地址",
options: "监听选项", options: "监听选项",
tcpOptions: "TCP选项", tcpOptions: "TCP 选项",
udpOptions: "UDP选项", udpOptions: "UDP 选项",
detour: "绕道", detour: "转发",
detourText: "转发到入站", detourText: "转发到入站",
domainStrategy: "域名策略", domainStrategy: "域名解析策略",
}, },
dial: { dial: {
bindIf: "绑定到网络接口", bindIf: "绑定到网络接口",
bindIp4: "绑定到IPv4", bindIp4: "绑定到 IPv4",
bindIp6: "绑定到IPv6", bindIp6: "绑定到 IPv6",
reuseAddr: "重用监听地址", reuseAddr: "重用监听地址",
connTimeout: "连接超时", connTimeout: "连接超时",
fbTimeout: "回退超时", fbTimeout: "回退超时",
@@ -248,22 +279,22 @@ export default {
}, },
transport: { transport: {
enable: "启用传输", enable: "启用传输",
host: "主机", host: "主机域名",
hosts: "主机列表", hosts: "主机域名列表",
path: "路径", path: "HTTP 请求路径",
httpMethod: "请求方法", httpMethod: "HTTP 请求方法",
idleTimeout: "空闲超时", idleTimeout: "空闲超时",
pingTimeout: "Ping超时", pingTimeout: "Ping 超时",
grpcServiceName: "服务名称", grpcServiceName: "gRPC 服务名称",
grpcPws: "允许无流", grpcPws: "允许无流时保持连接",
}, },
mux: { mux: {
enable: "启用多路复用", enable: "启用多路复用",
maxConn: "最大连接数", maxConn: "最大连接数",
minStr: "最小流数", minStr: "最小流数",
maxStr: "最大流数", maxStr: "最大流数",
padding: "仅填充", padding: "仅允许填充连接",
enableBrutal: "启用强力模式", enableBrutal: "启用 TCP Brutal",
}, },
out: { out: {
addr: "服务器地址", addr: "服务器地址",
@@ -274,22 +305,22 @@ export default {
simple: "简单", simple: "简单",
logical: "逻辑", logical: "逻辑",
mode: "模式", mode: "模式",
invert: "反", invert: "反选结果",
ipVer: "IP 版本", ipVer: "IP 版本",
domain: "域名", domain: "域名",
domainSufix: "域名后缀", domainSufix: "域名后缀",
domainKw: "域名关键词", domainKw: "域名关键词",
domainRgx: "域名正则表达式", domainRgx: "域名正则表达式",
ip: "IP CIDR", ip: "IP CIDR",
privateIp: "无效 IP 范围", privateIp: "匹配非公开 IP",
port: "端口", port: "端口",
portRange: "端口范围", portRange: "端口范围",
srcCidr: "源 IP CIDR", srcCidr: "源 IP CIDR",
srcPrivateIp: "无效源 IP", srcPrivateIp: "匹配非公开源 IP",
srcPort: "源端口", srcPort: "源端口",
srcPortRange: "源端口范围", srcPortRange: "源端口范围",
ruleset: "规则集", ruleset: "规则集",
rulesetMatchSrc: "规则集 IP CIDR 匹配源", rulesetMatchSrc: "规则集 IP CIDR 匹配源 IP",
options: "规则选项", options: "规则选项",
domainRules: "域名/IP", domainRules: "域名/IP",
srcIpRules: "源 IP", srcIpRules: "源 IP",
@@ -313,6 +344,7 @@ export default {
final: "最终", final: "最终",
server: "服务器", server: "服务器",
firstServer: "首选服务器", firstServer: "首选服务器",
addrResolver: "地址解析器",
}, },
routing: { routing: {
title: "路由", title: "路由",
@@ -322,7 +354,7 @@ export default {
autoBind: "自动绑定网卡", autoBind: "自动绑定网卡",
}, },
exp: { exp: {
storeFakeIp: "存储虚假 IP", storeFakeIp: "持久化 Fake-IP",
}, },
}, },
tls : { tls : {
@@ -339,7 +371,7 @@ export default {
cs: "密码套件", cs: "密码套件",
privKey: "私钥", privKey: "私钥",
pubKey: "公钥", pubKey: "公钥",
disableSni: "禁用SNI", disableSni: "禁用 SNI",
insecure: "允许不安全", insecure: "允许不安全",
acme: { acme: {
options: "ACME 选项", options: "ACME 选项",
+33 -1
View File
@@ -5,6 +5,9 @@ export default {
failed: "失敗", failed: "失敗",
enable: "啟用", enable: "啟用",
disable: "禁用", disable: "禁用",
none: "無",
all: "全部",
filter: "過濾器",
loading: "加載中...", loading: "加載中...",
confirm: "是否確定?", confirm: "是否確定?",
yes: "確認", yes: "確認",
@@ -25,6 +28,7 @@ export default {
email: "電子郵件", email: "電子郵件",
commaSeparated: "(逗號分隔)", commaSeparated: "(逗號分隔)",
count: "計數", count: "計數",
template: "模板",
error: { error: {
dplData: "重複數據", dplData: "重複數據",
core: "Sing-Box 錯誤", core: "Sing-Box 錯誤",
@@ -88,16 +92,19 @@ export default {
actions: { actions: {
action: "操作", action: "操作",
add: "添加", add: "添加",
new: "添加", new: "新建",
edit: "編輯", edit: "編輯",
del: "刪除", del: "刪除",
clone: "克隆",
save: "保存", save: "保存",
update: "更新", update: "更新",
submit: "提交", submit: "提交",
set: "設置", set: "設置",
generate: "生成",
disable: "禁用", disable: "禁用",
close: "關閉", close: "關閉",
restartApp: "重啟面板", restartApp: "重啟面板",
restartSb: "重啟 Singbox",
}, },
login: { login: {
title: "登錄", title: "登錄",
@@ -140,10 +147,20 @@ export default {
path: "默認路徑", path: "默認路徑",
update: "自動更新時間", update: "自動更新時間",
subUri: "訂閱 URL", subUri: "訂閱 URL",
jsonSub: "JSON 訂閱",
toDirect: "路由到直連",
toBlock: "路由到阻止",
timestamp: "時間戳",
globalDns: "全局 DNS",
directDns: "直連 DNS",
toDirectDns: "路由到直連 DNS",
jsonSubOptions: "其他選項",
excludePkg: "排除包",
}, },
client: { client: {
name: "名稱", name: "名稱",
desc: "描述", desc: "描述",
group: "組",
inboundTags: "入站標簽", inboundTags: "入站標簽",
basics: "基礎", basics: "基礎",
config: "配置", config: "配置",
@@ -151,6 +168,11 @@ export default {
external: "外部鏈接", external: "外部鏈接",
sub: "外部訂閱", sub: "外部訂閱",
}, },
bulk: {
add: "批量添加",
order: "排序",
random: "隨機",
},
types: { types: {
un: "用戶名", un: "用戶名",
pw: "密碼", pw: "密碼",
@@ -186,6 +208,10 @@ export default {
authTimeout: "身份驗證超時", authTimeout: "身份驗證超時",
hb: "心跳", hb: "心跳",
}, },
tun: {
addr: "地址",
ifName: "介面名稱",
},
vless: { vless: {
flow: "流量", flow: "流量",
udpEnc: "UDP 封包編碼", udpEnc: "UDP 封包編碼",
@@ -225,6 +251,11 @@ export default {
sniffing: "嗅探", sniffing: "嗅探",
clients: "啟用客戶端", clients: "啟用客戶端",
ssMethod: "方法", ssMethod: "方法",
sSide: "服務器端",
cSide: "客戶端",
multiDomain: "多域名",
remark: "備註",
mdOption: "多域名選項",
}, },
listen: { listen: {
sniffing: "嗅探", sniffing: "嗅探",
@@ -314,6 +345,7 @@ export default {
final: "最終", final: "最終",
server: "服務器", server: "服務器",
firstServer: "首選服務器", firstServer: "首選服務器",
addrResolver: "地址解析器",
}, },
routing: { routing: {
title: "路由", title: "路由",
+2 -2
View File
@@ -28,10 +28,10 @@ import { createNotivue } from 'notivue'
import 'notivue/notification.css' import 'notivue/notification.css'
import 'notivue/animations.css' import 'notivue/animations.css'
const notivue = createNotivue({ const notivue = createNotivue({
position: 'top-center', position: 'bottom-center',
limit: 4, limit: 4,
enqueue: false, enqueue: false,
avoidDuplicates: false, avoidDuplicates: true,
notifications: { notifications: {
global: { global: {
duration: 3000 duration: 3000
+2 -2
View File
@@ -1,4 +1,4 @@
import axios from 'axios'; import axios from 'axios'
axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8' axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8'
axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest' axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest'
@@ -50,7 +50,7 @@ axios.interceptors.response.use(
} }
return Promise.reject(error) return Promise.reject(error)
} }
); )
const api = axios.create() const api = axios.create()
+2 -2
View File
@@ -57,7 +57,7 @@ function _respToMsg(resp: any): Msg {
} }
function isMsg(obj: any): obj is Msg { function isMsg(obj: any): obj is Msg {
return 'success' in obj && 'msg' in obj && 'obj' in obj return Object.hasOwn(obj,'success') && Object.hasOwn(obj,'msg') && Object.hasOwn(obj, 'obj')
} }
const HttpUtils = { const HttpUtils = {
@@ -85,4 +85,4 @@ const HttpUtils = {
}, },
} }
export default HttpUtils; export default HttpUtils
+15
View File
@@ -0,0 +1,15 @@
export interface Addr {
server: string
server_port: number
tls?: boolean
insecure?: boolean
server_name?: string
remark?: string
}
export interface InData {
id: number
tag: string
addrs: Addr[]
outJson: any
}
+301 -86
View File
@@ -1,5 +1,7 @@
import { Hysteria, Hysteria2, InTypes, Inbound, Naive, Shadowsocks, TUIC, Trojan, VLESS, VMess } from "@/types/inbounds" import { Hysteria, Hysteria2, InTypes, Inbound, Naive, Shadowsocks, TUIC, Trojan, VLESS, VMess } from "@/types/inbounds"
import { HTTP, WebSocket, gRPC, HTTPUpgrade, Transport, TrspTypes } from "@/types/transport"; import { HTTP, WebSocket, gRPC, HTTPUpgrade, Transport, TrspTypes } from "@/types/transport"
import RandomUtil from "./randomUtil"
import { Client } from "@/types/clients"
export interface Link { export interface Link {
type: "local" | "external" | "sub" type: "local" | "external" | "sub"
@@ -8,55 +10,69 @@ export interface Link {
} }
function utf8ToBase64(utf8String: string): string { function utf8ToBase64(utf8String: string): string {
const encodedUtf8 = encodeURIComponent(utf8String).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16))); const encodedUtf8 = encodeURIComponent(utf8String).replace(/%([0-9A-F]{2})/g, (_, p1) => String.fromCharCode(parseInt(p1, 16)))
return btoa(encodedUtf8); return btoa(encodedUtf8)
} }
export namespace LinkUtil { export namespace LinkUtil {
export function linkGenerator(user: string, inbound: Inbound, tlsClient: any = null): string { export function linkGenerator(user: Client, inbound: Inbound, tlsClient: any = {}, addrs: any[] = []): string[] {
const addr = location.hostname
switch(inbound.type){ switch(inbound.type){
case InTypes.Shadowsocks: case InTypes.Shadowsocks:
return shadowsocksLink(user,<Shadowsocks>inbound, addr) return shadowsocksLink(user,<Shadowsocks>inbound, addrs)
case InTypes.Naive: case InTypes.Naive:
return naiveLink(user,<Naive>inbound, addr, tlsClient) return naiveLink(user,<Naive>inbound, addrs, tlsClient)
case InTypes.Hysteria: case InTypes.Hysteria:
return hysteriaLink(user,<Hysteria>inbound, addr, tlsClient) return hysteriaLink(user,<Hysteria>inbound, addrs, tlsClient)
case InTypes.Hysteria2: case InTypes.Hysteria2:
return hysteria2Link(user,<Hysteria2>inbound, addr, tlsClient) return hysteria2Link(user,<Hysteria2>inbound, addrs, tlsClient)
case InTypes.TUIC: case InTypes.TUIC:
return tuicLink(user,<TUIC>inbound, addr, tlsClient) return tuicLink(user,<TUIC>inbound, addrs, tlsClient)
case InTypes.VLESS: case InTypes.VLESS:
return vlessLink(user,<VLESS>inbound, addr, tlsClient) return vlessLink(user,<VLESS>inbound, addrs, tlsClient)
case InTypes.Trojan: case InTypes.Trojan:
return trojanLink(user,<Trojan>inbound, addr, tlsClient) return trojanLink(user,<Trojan>inbound, addrs, tlsClient)
case InTypes.VMess: case InTypes.VMess:
return vmessLink(user,<VMess>inbound, addr, tlsClient) return vmessLink(user,<VMess>inbound, addrs, tlsClient)
} }
return '' return []
} }
function shadowsocksLink(user: string, inbound: Shadowsocks, addr: string): string { function shadowsocksLink(user: Client, inbound: Shadowsocks, addrs: any[]): string[] {
const userPass = inbound.users?.find(i => i.name == user)?.password const userPass = inbound.method == "2022-blake3-aes-128-gcm" ? user.config.shadowsocks16?.password : user.config.shadowsocks?.password
const password = [userPass] const password = [userPass]
if (inbound.method.startsWith('2022')) password.push(inbound.password) if (inbound.method.startsWith('2022')) password.push(inbound.password)
const params = { const params = {
tfo: inbound.tcp_fast_open? 1 : null, tfo: inbound.tcp_fast_open? 1 : null,
network: inbound.network?? null network: inbound.network?? null
}
const uri = new URL(`ss://${utf8ToBase64(inbound.method + ':' + password.join(':'))}@${addr}:${inbound.listen_port}`)
for (const [key, value] of Object.entries(params)){
if (value) {
uri.searchParams.set(key, value.toString())
}
} }
uri.hash = encodeURIComponent(inbound.tag)
return uri.toString() let links = <string[]>[]
if (addrs.length == 0) {
const uri = new URL(`ss://${utf8ToBase64(inbound.method + ':' + password.join(':'))}@${location.hostname}:${inbound.listen_port}`)
for (const [key, value] of Object.entries(params)){
if (value) {
uri.searchParams.set(key, value.toString())
}
}
uri.hash = encodeURIComponent(inbound.tag)
links.push(uri.toString())
} else {
addrs.forEach(a => {
const uri = new URL(`ss://${utf8ToBase64(inbound.method + ':' + password.join(':'))}@${a.server}:${a.server_port}`)
for (const [key, value] of Object.entries(params)){
if (value) {
uri.searchParams.set(key, value.toString())
}
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
})
}
return links
} }
function hysteriaLink(user: string, inbound: Hysteria, addr: string, tlsClient: any): string { function hysteriaLink(user: Client, inbound: Hysteria, addrs: any[], tlsClient: any): string[] {
const auth = inbound.users.find(i => i.name == user)?.auth_str const auth = user.config.hysteria.auth_str
const params = { const params = {
upmbps: inbound.up_mbps?? null, upmbps: inbound.up_mbps?? null,
downmbps: inbound.down_mbps?? null, downmbps: inbound.down_mbps?? null,
@@ -67,18 +83,44 @@ export namespace LinkUtil {
fastopen: inbound.tcp_fast_open? 1 : 0, fastopen: inbound.tcp_fast_open? 1 : 0,
insecure: tlsClient?.insecure ? 1 : null insecure: tlsClient?.insecure ? 1 : null
} }
const uri = new URL(`hysteria://${addr}:${inbound.listen_port}`)
for (const [key, value] of Object.entries(params)){ let links = <string[]>[]
if (value) { if (addrs.length == 0) {
uri.searchParams.set(key, value.toString()) const uri = new URL(`hysteria://${location.hostname}:${inbound.listen_port}`)
for (const [key, value] of Object.entries(params)){
if (value) {
uri.searchParams.set(key, value.toString())
}
} }
uri.hash = encodeURIComponent(inbound.tag)
links.push(uri.toString())
} else {
addrs.forEach(a => {
const uri = new URL(`hysteria://${a.server}:${a.server_port}`)
for (const [key, value] of Object.entries(params)){
if (value) {
uri.searchParams.set(key, value.toString())
}
}
if (a.server_name?.length>0) {
uri.searchParams.set('peer', a.server_name)
} else {
inbound.tls.server_name ? uri.searchParams.set('peer', inbound.tls.server_name) : uri.searchParams.delete('peer')
}
if (a.insecure) {
uri.searchParams.set('insecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('insecure', '1') : uri.searchParams.delete('insecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
})
} }
uri.hash = encodeURIComponent(inbound.tag) return links
return uri.toString()
} }
function hysteria2Link(user: string, inbound: Hysteria2, addr: string, tlsClient: any): string { function hysteria2Link(user: Client, inbound: Hysteria2, addrs: any[], tlsClient: any): string[] {
const password = inbound.users.find(i => i.name == user)?.password const password = user.config.hysteria2.password
const params = { const params = {
upmbps: inbound.up_mbps?? null, upmbps: inbound.up_mbps?? null,
downmbps: inbound.down_mbps?? null, downmbps: inbound.down_mbps?? null,
@@ -89,37 +131,86 @@ export namespace LinkUtil {
fastopen: inbound.tcp_fast_open? 1 : 0, fastopen: inbound.tcp_fast_open? 1 : 0,
insecure: tlsClient?.insecure ? 1 : null insecure: tlsClient?.insecure ? 1 : null
} }
const uri = new URL(`hysteria2://${password}@${addr}:${inbound.listen_port}`)
for (const [key, value] of Object.entries(params)){ let links = <string[]>[]
if (value) { if (addrs.length == 0) {
uri.searchParams.set(key, value.toString()) const uri = new URL(`hysteria2://${password}@${location.hostname}:${inbound.listen_port}`)
for (const [key, value] of Object.entries(params)){
if (value) {
uri.searchParams.set(key, value.toString())
}
} }
uri.hash = encodeURIComponent(inbound.tag)
links.push(uri.toString())
} else {
addrs.forEach(a => {
const uri = new URL(`hysteria2://${password}@${a.server}:${a.server_port}`)
for (const [key, value] of Object.entries(params)){
if (value) {
uri.searchParams.set(key, value.toString())
}
}
if (a.server_name?.length>0) {
uri.searchParams.set('sni', a.server_name)
} else {
inbound.tls.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
}
if (a.insecure) {
uri.searchParams.set('insecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('insecure', '1') : uri.searchParams.delete('insecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
})
} }
uri.hash = encodeURIComponent(inbound.tag) return links
return uri.toString()
} }
function naiveLink(user: string, inbound: Naive, addr: string, tlsClient: any): string { function naiveLink(user: Client, inbound: Naive, addrs: any[], tlsClient: any): string[] {
const password = inbound.users.find(i => i.username == user)?.password const password = user.config.naive.password
const params = {
padding: 1, let links = <string[]>[]
peer: inbound.tls.server_name?? null, if (addrs.length == 0) {
alpn: inbound.tls.alpn?.join(',')?? null, const params = {
tfo: inbound.tcp_fast_open? 1 : 0, padding: 1,
allowInsecure: tlsClient?.insecure ? 1 : null peer: inbound.tls.server_name?? null,
} alpn: inbound.tls.alpn?.join(',')?? null,
const uri = `http2://${utf8ToBase64(user + ":" + password + "@" + addr + ":" + inbound.listen_port)}` tfo: inbound.tcp_fast_open? 1 : 0,
const paramsArray = [] allowInsecure: tlsClient?.insecure ? 1 : null
for (const [key, value] of Object.entries(params)){
if (value) {
paramsArray.push(`${key}=${encodeURIComponent(value.toString())}`)
} }
const uri = `http2://${utf8ToBase64(user + ":" + password + "@" + location.hostname + ":" + inbound.listen_port)}`
const paramsArray = []
for (const [key, value] of Object.entries(params)){
if (value) {
paramsArray.push(`${key}=${encodeURIComponent(value.toString())}`)
}
}
links.push(uri.toString() + "?" + paramsArray.join('&') + "#" + inbound.tag)
} else {
addrs.forEach(a => {
const params = {
padding: 1,
peer: a.server_name?.length>0 ? a.server_name : inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null,
tfo: inbound.tcp_fast_open? 1 : 0,
allowInsecure: a.insecure ? 1 : tlsClient?.insecure ? 1 : null
}
const uri = `http2://${utf8ToBase64(user + ":" + password + "@" + a.server + ":" + a.server_port)}`
const paramsArray = []
for (const [key, value] of Object.entries(params)){
if (value) {
paramsArray.push(`${key}=${encodeURIComponent(value.toString())}`)
}
}
links.push(uri.toString() + "?" + paramsArray.join('&') + "#" + encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag))
})
} }
return uri.toString() + "?" + paramsArray.join('&') + "#" + inbound.tag return links
} }
function tuicLink(user: string, inbound: TUIC, addr: string, tlsClient: any): string { function tuicLink(user: Client, inbound: TUIC, addrs: any[], tlsClient: any): string[] {
const u = inbound.users.find(i => i.name == user) const u = user.config.tuic
const params = { const params = {
sni: inbound.tls.server_name?? null, sni: inbound.tls.server_name?? null,
alpn: inbound.tls.alpn?.join(',')?? null, alpn: inbound.tls.alpn?.join(',')?? null,
@@ -127,14 +218,40 @@ export namespace LinkUtil {
allowInsecure: tlsClient?.insecure ? 1 : null, allowInsecure: tlsClient?.insecure ? 1 : null,
disable_sni: tlsClient?.disable_sni ? 1 : null disable_sni: tlsClient?.disable_sni ? 1 : null
} }
const uri = new URL(`tuic://${u?.uuid}:${u?.password}@${addr}:${inbound.listen_port}`)
for (const [key, value] of Object.entries(params)){ let links = <string[]>[]
if (value) { if (addrs.length == 0) {
uri.searchParams.set(key, value.toString()) const uri = new URL(`tuic://${u?.uuid}:${u?.password}@${location.hostname}:${inbound.listen_port}`)
for (const [key, value] of Object.entries(params)){
if (value) {
uri.searchParams.set(key, value.toString())
}
} }
uri.hash = encodeURIComponent(inbound.tag)
links.push(uri.toString())
} else {
addrs.forEach(a => {
const uri = new URL(`tuic://${u?.uuid}:${u?.password}@${a.server}:${a.server_port}`)
for (const [key, value] of Object.entries(params)){
if (value) {
uri.searchParams.set(key, value.toString())
}
}
if (a.server_name?.length>0) {
uri.searchParams.set('sni', a.server_name)
} else {
inbound.tls.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
}
if (a.insecure) {
uri.searchParams.set('allowInsecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
})
} }
uri.hash = encodeURIComponent(inbound.tag) return links
return uri.toString()
} }
function getTransportParams(t:Transport): any { function getTransportParams(t:Transport): any {
@@ -170,8 +287,8 @@ export namespace LinkUtil {
return params return params
} }
function vlessLink(user: string, inbound: VLESS, addr: string, tlsClient: any): string { function vlessLink(user: Client, inbound: VLESS, addrs: any[], tlsClient: any): string[] {
const u = inbound.users.find(i => i.name == user) const u = user.config.vless
const transport = <Transport>inbound.transport const transport = <Transport>inbound.transport
const tParams = getTransportParams(transport) const tParams = getTransportParams(transport)
@@ -185,20 +302,55 @@ export namespace LinkUtil {
allowInsecure: tlsClient?.insecure ? 1 : null, allowInsecure: tlsClient?.insecure ? 1 : null,
fp: tlsClient?.utls?.enabled ? tlsClient.utls.fingerprint : null, fp: tlsClient?.utls?.enabled ? tlsClient.utls.fingerprint : null,
pbk: tlsClient?.reality?.public_key?? null, pbk: tlsClient?.reality?.public_key?? null,
sid: inbound.tls?.reality?.enabled ? (inbound.tls?.reality?.short_id?.length>0 ? inbound.tls.reality.short_id[0] : null) : null sid: inbound.tls?.reality?.enabled ? (inbound.tls?.reality?.short_id?.length>0 ? inbound.tls.reality.short_id[RandomUtil.randomInt(inbound.tls.reality.short_id.length)] : null) : null
} }
const uri = new URL(`vless://${u?.uuid}@${addr}:${inbound.listen_port}`) let links = <string[]>[]
for (const [key, value] of Object.entries({...params, ...tParams})){ if (addrs.length == 0) {
if (value) { const uri = new URL(`vless://${u?.uuid}@${location.hostname}:${inbound.listen_port}`)
uri.searchParams.set(key, value.toString()) for (const [key, value] of Object.entries({...params, ...tParams})){
if (value) {
uri.searchParams.set(key, value.toString())
}
} }
uri.hash = encodeURIComponent(inbound.tag)
links.push(uri.toString())
} else {
addrs.forEach(a => {
const uri = new URL(`vless://${u?.uuid}@${a.server}:${a.server_port}`)
for (const [key, value] of Object.entries({...params, ...tParams})){
if (value) {
uri.searchParams.set(key, value.toString())
}
}
if (a.tls != undefined){
if (a.tls) {
uri.searchParams.set('security','tls')
} else {
uri.searchParams.delete('security')
uri.searchParams.delete('sni')
uri.searchParams.delete('alpn')
uri.searchParams.delete('allowInsecure')
}
}
if (a.server_name?.length>0) {
uri.searchParams.set('sni', a.server_name)
} else {
inbound.tls?.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
}
if (a.insecure) {
uri.searchParams.set('allowInsecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
})
} }
uri.hash = encodeURIComponent(inbound.tag) return links
return uri.toString()
} }
function trojanLink(user: string, inbound: Trojan, addr: string, tlsClient: any): string { function trojanLink(user: Client, inbound: Trojan, addrs: any[], tlsClient: any): string[] {
const u = inbound.users.find(i => i.name == user) const u = user.config.trojan
const transport = <Transport>inbound.transport const transport = <Transport>inbound.transport
const tParams = getTransportParams(transport) const tParams = getTransportParams(transport)
@@ -211,20 +363,56 @@ export namespace LinkUtil {
allowInsecure: tlsClient?.insecure ? 1 : null, allowInsecure: tlsClient?.insecure ? 1 : null,
fp: tlsClient?.utls?.enabled ? tlsClient.utls.fingerprint : null, fp: tlsClient?.utls?.enabled ? tlsClient.utls.fingerprint : null,
pbk: tlsClient?.reality?.public_key?? null, pbk: tlsClient?.reality?.public_key?? null,
sid: inbound.tls?.reality?.enabled ? (inbound.tls?.reality?.short_id?.length>0 ? inbound.tls.reality.short_id[0] : null) : null sid: inbound.tls?.reality?.enabled ? (inbound.tls?.reality?.short_id?.length>0 ? inbound.tls.reality.short_id[RandomUtil.randomInt(inbound.tls.reality.short_id.length)] : null) : null
} }
const uri = new URL(`trojan://${u?.password}@${addr}:${inbound.listen_port}`)
for (const [key, value] of Object.entries({...params, ...tParams})){ let links = <string[]>[]
if (value) { if (addrs.length == 0) {
uri.searchParams.set(key, value.toString()) const uri = new URL(`trojan://${u?.password}@${location.hostname}:${inbound.listen_port}`)
for (const [key, value] of Object.entries({...params, ...tParams})){
if (value) {
uri.searchParams.set(key, value.toString())
}
} }
uri.hash = encodeURIComponent(inbound.tag)
links.push(uri.toString())
} else {
addrs.forEach(a => {
const uri = new URL(`trojan://${u?.password}@${a.server}:${a.server_port}`)
for (const [key, value] of Object.entries({...params, ...tParams})){
if (value) {
uri.searchParams.set(key, value.toString())
}
}
if (a.tls != undefined){
if (a.tls) {
uri.searchParams.set('security','tls')
} else {
uri.searchParams.delete('security')
uri.searchParams.delete('sni')
uri.searchParams.delete('alpn')
uri.searchParams.delete('allowInsecure')
}
}
if (a.server_name?.length>0) {
uri.searchParams.set('sni', a.server_name)
} else {
inbound.tls?.server_name ? uri.searchParams.set('sni', inbound.tls.server_name) : uri.searchParams.delete('sni')
}
if (a.insecure) {
uri.searchParams.set('allowInsecure', '1')
} else {
tlsClient.insecure ? uri.searchParams.set('allowInsecure', '1') : uri.searchParams.delete('allowInsecure')
}
uri.hash = encodeURIComponent(a.remark ? inbound.tag + a.remark : inbound.tag)
links.push(uri.toString())
})
} }
uri.hash = encodeURIComponent(inbound.tag) return links
return uri.toString()
} }
function vmessLink(user: string, inbound: VMess, addr: string, tlsClient: any): string { function vmessLink(user: Client, inbound: VMess, addrs: any[], tlsClient: any): string[] {
const u = inbound.users.find(i => i.name == user) const u = user.config.vmess
const transport = <Transport>inbound.transport const transport = <Transport>inbound.transport
const tParams = getTransportParams(transport) const tParams = getTransportParams(transport)
@@ -232,7 +420,7 @@ export namespace LinkUtil {
const params = { const params = {
v: 2, v: 2,
add: addr, add: location.hostname,
aid: u?.alterId, aid: u?.alterId,
host: tParams.host?? undefined, host: tParams.host?? undefined,
id: u?.uuid, id: u?.uuid,
@@ -245,6 +433,33 @@ export namespace LinkUtil {
tls: Object.keys(inbound.tls).length>0? 'tls' : 'none', tls: Object.keys(inbound.tls).length>0? 'tls' : 'none',
allowInsecure: tlsClient?.insecure ? 1 : undefined allowInsecure: tlsClient?.insecure ? 1 : undefined
} }
return 'vmess://' + utf8ToBase64(JSON.stringify(params, null, 2)) let links = <string[]>[]
if (addrs.length == 0) {
links.push('vmess://' + utf8ToBase64(JSON.stringify(params, null, 2)))
} else {
addrs.forEach(a => {
let newParams = {...params}
newParams.add = a.server
newParams.port = a.server_port
if (a.tls != undefined){
if (a.tls) {
newParams.tls = 'tls'
} else {
newParams.tls = 'none'
delete newParams.sni
delete newParams.allowInsecure
}
}
if (a.server_name?.length>0) {
newParams.sni = a.server_name
}
if (a.insecure) {
newParams.allowInsecure = 1
}
newParams.ps = inbound.tag + (a.remark??'')
links.push('vmess://' + utf8ToBase64(JSON.stringify(newParams, null, 2)))
})
}
return links
} }
} }
+105
View File
@@ -0,0 +1,105 @@
import { Hysteria, Hysteria2, Inbound, InTypes, Shadowsocks, Trojan, TUIC, VLESS, VMess, ShadowTLS } from "@/types/inbounds"
import { iTls } from "@/types/inTls"
import { oTls } from "@/types/outTls"
import RandomUtil from "./randomUtil"
export function fillData(out: any, inbound: Inbound, tlsClient: any) {
if (Object.hasOwn(inbound, 'tls')) {
const inb = <any>inbound
addTls(out,inb.tls,tlsClient)
} else {
delete out.tls
}
out.type = inbound.type
out.tag = inbound.tag
out.server = location.hostname
out.server_port = inbound.listen_port
switch(inbound.type){
case InTypes.HTTP || InTypes.SOCKS:
return
case InTypes.Shadowsocks:
shadowsocksOut(out, <Shadowsocks>inbound)
return
case InTypes.ShadowTLS:
shadowTlsOut(out, <ShadowTLS>inbound)
return
case InTypes.Hysteria:
hysteriaOut(out, <Hysteria>inbound)
return
case InTypes.Hysteria2:
hysteria2Out(out, <Hysteria2>inbound)
return
case InTypes.TUIC:
tuicOut(out, <TUIC>inbound)
return
case InTypes.VLESS:
vlessOut(out, <VLESS>inbound)
return
case InTypes.Trojan:
trojanOut(out, <Trojan>inbound)
return
case InTypes.VMess:
vmessOut(out, <VMess>inbound)
return
}
Object.keys(out).forEach(key => delete out[key])
}
function addTls(out: any, tls: iTls, tlsClient: oTls){
out.tls = tlsClient
if(tls.enabled) out.tls.enabled = tls.enabled
if(tls.server_name) out.tls.server_name = tls.server_name
if(tls.alpn) out.tls.alpn = tls.alpn
if(tls.min_version) out.tls.min_version = tls.min_version
if(tls.max_version) out.tls.max_version = tls.max_version
if(tls.cipher_suites) out.tls.cipher_suites = tls.cipher_suites
if(tls.reality?.enabled){
out.tls.reality.enabled = true
out.tls.reality.short_id = tls.reality.short_id[RandomUtil.randomInt(tls.reality.short_id.length)]
}
}
function shadowsocksOut(out: any, inbound: Shadowsocks) {
out.method = inbound.method
}
function shadowTlsOut(out: any, inbound: ShadowTLS) {
if (inbound.version == 3) {
out.version = 3
} else {
Object.keys(out).forEach(key => delete out[key])
}
out.tls = { enabled: true }
}
function hysteriaOut(out: any, inbound: Hysteria) {
out.up_mbps = inbound.down_mbps
out.down_mbps = inbound.up_mbps
out.obfs = inbound.obfs
out.recv_window_conn = inbound.recv_window_conn
out.disable_mtu_discovery = inbound.disable_mtu_discovery
}
function hysteria2Out(out: any, inbound: Hysteria2) {
out.up_mbps = inbound.down_mbps
out.down_mbps = inbound.up_mbps
out.obfs = inbound.obfs
}
function tuicOut(out: any, inbound: TUIC) {
out.congestion_control = inbound.congestion_control?? "cubic"
out.zero_rtt_handshake = inbound.zero_rtt_handshake
out.heartbeat = inbound.heartbeat
}
function vlessOut(out: any, inbound: VLESS) {
out.transport = inbound.transport
}
function trojanOut(out: any, inbound: Trojan) {
out.transport = inbound.transport
}
function vmessOut(out: any, inbound: VMess) {
out.transport = inbound.transport
}
+8 -8
View File
@@ -35,14 +35,14 @@ const RandomUtil = {
return btoa(String.fromCharCode(...array)) return btoa(String.fromCharCode(...array))
}, },
randomShortId(): string[] { randomShortId(): string[] {
let shortIds = ['','','',''] let shortIds = new Array(24).fill('')
for (var ii = 0; ii < 4; ii++) { for (var ii = 1; ii < 24; ii++) {
for (var jj = 0; jj < this.randomInt(8); jj++){ for (var jj = 0; jj <= this.randomInt(7); jj++){
let randomNum = this.randomInt(256) let randomNum = this.randomInt(256)
shortIds[ii] += ('0' + randomNum.toString(16)).slice(-2) shortIds[ii] += ('0' + randomNum.toString(16)).slice(-2)
} }
} }
return shortIds return shortIds
} }
} }
+5 -5
View File
@@ -37,11 +37,11 @@ export const FindDiff = {
return differences return differences
}, },
Clients(value1: any[], value2: any[]): any { ArrObj(value1: any[], value2: any[], key: string): any {
const differences: any[] = [] const differences: any[] = []
value1.forEach((v1,index) => { value1.forEach((v1,index) => {
if(index >= value2.length) differences.push({key: "clients", action: "new", obj: v1}) if(index >= value2.length) differences.push({key: key, action: "new", obj: v1})
else if(!this.deepCompare(v1,value2[index])) differences.push({key: "clients", action: "edit", obj: v1}) else if(!this.deepCompare(v1,value2[index])) differences.push({key: key, action: "edit", obj: v1})
}) })
return differences return differences
}, },
@@ -76,8 +76,8 @@ export const FindDiff = {
// Check if both objects are plain objects // Check if both objects are plain objects
if (typeof obj1 === 'object' && typeof obj2 === 'object' && obj1 !== null && obj2 !== null) { if (typeof obj1 === 'object' && typeof obj2 === 'object' && obj1 !== null && obj2 !== null) {
const keys1 = Object.keys(obj1) const keys1 = Object.keys(obj1).filter(key => obj1[key] !== undefined)
const keys2 = Object.keys(obj2) const keys2 = Object.keys(obj2).filter(key => obj2[key] !== undefined)
if (keys1.length !== keys2.length) { if (keys1.length !== keys2.length) {
return false return false
+2 -2
View File
@@ -9,7 +9,7 @@ import '@mdi/font/css/materialdesignicons.css'
import 'vuetify/styles' import 'vuetify/styles'
import colors from 'vuetify/util/colors' import colors from 'vuetify/util/colors'
import { fa, en, vi, zhHans as zhcn, zhHant as zhtw } from 'vuetify/locale' import { fa, en, vi, zhHans, zhHant, ru } from 'vuetify/locale'
// Composables // Composables
import { createVuetify } from 'vuetify' import { createVuetify } from 'vuetify'
@@ -53,6 +53,6 @@ export default createVuetify({
locale: { locale: {
locale: localStorage.getItem("locale") ?? 'en', locale: localStorage.getItem("locale") ?? 'en',
fallback: 'en', fallback: 'en',
messages: { en, fa, vi, zhcn, zhtw }, messages: { en, fa, vi, zhHans, zhHant, ru },
}, },
}) })
+29 -7
View File
@@ -10,10 +10,11 @@ const Data = defineStore('Data', {
reloadItems: localStorage.getItem("reloadItems")?.split(',')?? <string[]>[], reloadItems: localStorage.getItem("reloadItems")?.split(',')?? <string[]>[],
subURI: "", subURI: "",
onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]}, onlines: {inbound: <string[]>[], outbound: <string[]>[], user: <string[]>[]},
oldData: <{config: any, clients: any[], tlsConfigs: any[]}>{}, oldData: <{config: any, clients: any[], tlsConfigs: any[], inData: any[]}>{},
config: {}, config: <any>{},
clients: [], clients: [],
tlsConfigs: [], tlsConfigs: [],
inData: [],
}), }),
actions: { actions: {
async loadData() { async loadData() {
@@ -25,6 +26,7 @@ const Data = defineStore('Data', {
if (msg.obj.config) this.oldData.config = msg.obj.config if (msg.obj.config) this.oldData.config = msg.obj.config
if (msg.obj.clients) this.oldData.clients = msg.obj.clients if (msg.obj.clients) this.oldData.clients = msg.obj.clients
if (msg.obj.tls) this.oldData.tlsConfigs = msg.obj.tls if (msg.obj.tls) this.oldData.tlsConfigs = msg.obj.tls
if (msg.obj.inData) this.oldData.inData = msg.obj.inData
this.onlines = msg.obj.onlines this.onlines = msg.obj.onlines
if (msg.obj.lastLog) { if (msg.obj.lastLog) {
push.error({ push.error({
@@ -41,31 +43,51 @@ const Data = defineStore('Data', {
if (data.config) this.config = data.config if (data.config) this.config = data.config
if (data.clients) this.clients = data.clients if (data.clients) this.clients = data.clients
if (data.tls) this.tlsConfigs = data.tls if (data.tls) this.tlsConfigs = data.tls
if (data.inData) this.inData = data.inData
} }
} }
}, },
async pushData() { async pushData() {
const diff = { const diff = {
config: JSON.stringify(FindDiff.Config(this.config,this.oldData.config)), config: JSON.stringify(FindDiff.Config(this.config,this.oldData.config), null, 2),
clients: JSON.stringify(FindDiff.Clients(this.clients,this.oldData.clients)), clients: JSON.stringify(FindDiff.ArrObj(this.clients,this.oldData.clients, "clients"), null, 2),
tls: JSON.stringify(FindDiff.Clients(this.tlsConfigs,this.oldData.tlsConfigs)), tls: JSON.stringify(FindDiff.ArrObj(this.tlsConfigs,this.oldData.tlsConfigs, "tls"), null, 2),
inData: JSON.stringify(FindDiff.ArrObj(this.inData,this.oldData.inData, "inData"), null, 2),
} }
const msg = await HttpUtils.post('api/save',diff) const msg = await HttpUtils.post('api/save',diff)
if(msg.success) { if(msg.success) {
this.lastLoad = 0
this.loadData() this.loadData()
} }
}, },
async delInbound(index: number) { async delInbound(index: number) {
const diff = { const diff = {
config: JSON.stringify([{key: "inbounds", action: "del", index: index, obj: null}]), config: JSON.stringify([{key: "inbounds", action: "del", index: index, obj: null}]),
clients: JSON.stringify(FindDiff.Clients(this.clients,this.oldData.clients)), clients: JSON.stringify(FindDiff.ArrObj(this.clients,this.oldData.clients, "clients"), null, 2),
tls: JSON.stringify(FindDiff.Clients(this.tlsConfigs,this.oldData.tlsConfigs)), tls: JSON.stringify(FindDiff.ArrObj(this.tlsConfigs,this.oldData.tlsConfigs, "tls"), null, 2),
inData: <string|undefined> undefined,
}
// Validate inData
let invalidInData = <any[]>[]
this.inData.forEach((d:any) => {
const inboundIndex = this.config.inbounds.findIndex((i:any) => i.tag == d.tag)
if (inboundIndex == -1) invalidInData.push({key: "inData", action: "del", index: d.id, obj: null})
})
if (invalidInData.length>0) {
diff.inData = JSON.stringify(invalidInData)
} }
const msg = await HttpUtils.post('api/save',diff) const msg = await HttpUtils.post('api/save',diff)
if(msg.success) { if(msg.success) {
this.loadData() this.loadData()
} }
}, },
async delInData(id: number) {
const diff = {
inData: JSON.stringify([{key: "inData", action: "del", index: id, obj: null}])
}
await HttpUtils.post('api/save',diff)
},
async delOutbound(index: number) { async delOutbound(index: number) {
const diff = { const diff = {
config: JSON.stringify([{key: "outbounds", action: "del", index: index, obj: null}]), config: JSON.stringify([{key: "outbounds", action: "del", index: index, obj: null}]),
+14 -3
View File
@@ -13,6 +13,7 @@ export interface Client {
up: number up: number
down: number down: number
desc: string desc: string
group: string
} }
const defaultClient: Client = { const defaultClient: Client = {
@@ -26,6 +27,7 @@ const defaultClient: Client = {
up: 0, up: 0,
down: 0, down: 0,
desc: "", desc: "",
group: "",
} }
type Config = { type Config = {
@@ -54,7 +56,8 @@ export function updateConfigs(configs: Config, newUserName: string): Config {
export function randomConfigs(user: string): Config { export function randomConfigs(user: string): Config {
const mixedPassword = RandomUtil.randomSeq(10) const mixedPassword = RandomUtil.randomSeq(10)
const ssPassword = RandomUtil.randomShadowsocksPassword(32) const ssPassword16 = RandomUtil.randomShadowsocksPassword(16)
const ssPassword32 = RandomUtil.randomShadowsocksPassword(32)
const uuid = RandomUtil.randomUUID() const uuid = RandomUtil.randomUUID()
return { return {
mixed: { mixed: {
@@ -71,11 +74,15 @@ export function randomConfigs(user: string): Config {
}, },
shadowsocks: { shadowsocks: {
name: user, name: user,
password: ssPassword, password: ssPassword32,
},
shadowsocks16: {
name: user,
password: ssPassword16,
}, },
shadowtls: { shadowtls: {
name: user, name: user,
password: ssPassword, password: ssPassword32,
}, },
vmess: { vmess: {
name: user, name: user,
@@ -114,5 +121,9 @@ export function randomConfigs(user: string): Config {
export function createClient<T extends Client>(json?: Partial<T>): Client { export function createClient<T extends Client>(json?: Partial<T>): Client {
defaultClient.name = RandomUtil.randomSeq(8) defaultClient.name = RandomUtil.randomSeq(8)
const defaultObject: Client = { ...defaultClient, ...(json || {}) } const defaultObject: Client = { ...defaultClient, ...(json || {}) }
// Add missing config
defaultObject.config = { ...randomConfigs(defaultObject.name), ...defaultObject.config }
return defaultObject return defaultObject
} }
+28 -4
View File
@@ -17,7 +17,7 @@ export const InTypes = {
TUIC: 'tuic', TUIC: 'tuic',
Hysteria2: 'hysteria2', Hysteria2: 'hysteria2',
VLESS: 'vless', VLESS: 'vless',
// Tun: 'tun', Tun: 'tun',
Redirect: 'redirect', Redirect: 'redirect',
TProxy: 'tproxy', TProxy: 'tproxy',
} }
@@ -165,7 +165,31 @@ export interface Hysteria2 extends InboundBasics {
brutal_debug?: boolean brutal_debug?: boolean
} }
export interface Tun extends InboundBasics { export interface Tun extends InboundBasics {
[otherProperties: string]: any interface_name?: string
address?: string[]
mtu?: number
endpoint_independent_nat?: boolean
udp_timeout?: string
stack?: string
auto_route?: boolean
// gso?: boolean
// strict_route?: boolean
// iproute2_table_index?: number
// iproute2_rule_index?: number
// auto_redirect?: boolean
// auto_redirect_input_mark?: string
// auto_redirect_output_mark?: string
// route_address?: string[]
// route_exclude_address?: string[]
// include_interface?: string[]
// exclude_interface?: string[]
// include_uid?: string[]
// include_uid_range?: string[]
// exclude_uid?: number[]
// exclude_uid_range?: string[]
// include_android_user?: number[]
// include_package?: string[]
// exclude_package?: string[]
} }
export interface Redirect extends InboundBasics {} export interface Redirect extends InboundBasics {}
export interface TProxy extends InboundBasics { export interface TProxy extends InboundBasics {
@@ -187,7 +211,7 @@ type InterfaceMap = {
tuic: TUIC tuic: TUIC
hysteria2: Hysteria2 hysteria2: Hysteria2
vless: VLESS vless: VLESS
// tun: Tun tun: Tun
redirect: Redirect redirect: Redirect
tproxy: TProxy tproxy: TProxy
} }
@@ -228,7 +252,7 @@ const defaultValues: Record<InType, Inbound> = {
tuic: <TUIC>{ type: InTypes.TUIC, users: <TuicUser[]>[], congestion_control: "cubic", tls: { enabled: true } }, tuic: <TUIC>{ type: InTypes.TUIC, users: <TuicUser[]>[], congestion_control: "cubic", tls: { enabled: true } },
hysteria2: <Hysteria2>{ type: InTypes.Hysteria2, users: <NamePass[]>[], tls: { enabled: true } }, hysteria2: <Hysteria2>{ type: InTypes.Hysteria2, users: <NamePass[]>[], tls: { enabled: true } },
vless: <VLESS>{ type: InTypes.VLESS, users: <VlessUser[]>[], tls: {}, multiplex: {}, transport: {} }, vless: <VLESS>{ type: InTypes.VLESS, users: <VlessUser[]>[], tls: {}, multiplex: {}, transport: {} },
// tun: <Tun>{ type: InTypes.Tun }, tun: <Tun>{ type: InTypes.Tun, mtu: 9000, stack: 'system', udp_timeout: '5m', auto_route: false },
redirect: <Redirect>{ type: InTypes.Redirect }, redirect: <Redirect>{ type: InTypes.Redirect },
tproxy: <TProxy>{ type: InTypes.TProxy }, tproxy: <TProxy>{ type: InTypes.TProxy },
} }
-1
View File
@@ -78,7 +78,6 @@ const loadData = async () => {
const msg = await HttpUtils.get('api/users') const msg = await HttpUtils.get('api/users')
loading.value = false loading.value = false
if (msg.success) { if (msg.success) {
console.log(msg.obj)
msg.obj.forEach((u:any) => { msg.obj.forEach((u:any) => {
const lastLogin = u.lastLogin.split(" ") const lastLogin = u.lastLogin.split(" ")
const localLastLogin = lastLogin.length > 2 ? dateFormatted(Date.parse(lastLogin[0] + " " + lastLogin[1])) : "- -" const localLastLogin = lastLogin.length > 2 ? dateFormatted(Date.parse(lastLogin[0] + " " + lastLogin[1])) : "- -"
+41 -32
View File
@@ -3,10 +3,10 @@
<v-expansion-panel :title="$t('basic.log.title')"> <v-expansion-panel :title="$t('basic.log.title')">
<v-expansion-panel-text> <v-expansion-panel-text>
<v-row> <v-row>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-switch v-model="appConfig.log.disabled" color="primary" :label="$t('disable')" hide-details></v-switch> <v-switch v-model="appConfig.log.disabled" color="primary" :label="$t('disable')" hide-details></v-switch>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-select <v-select
hide-details hide-details
:label="$t('basic.log.level')" :label="$t('basic.log.level')"
@@ -14,14 +14,14 @@
v-model="appConfig.log.level"> v-model="appConfig.log.level">
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-text-field <v-text-field
v-model="appConfig.log.output" v-model="appConfig.log.output"
hide-details hide-details
:label="$t('basic.log.output')" :label="$t('basic.log.output')"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-switch v-model="appConfig.log.timestamp" color="primary" :label="$t('basic.log.timestamp')" hide-details></v-switch> <v-switch v-model="appConfig.log.timestamp" color="primary" :label="$t('basic.log.timestamp')" hide-details></v-switch>
</v-col> </v-col>
</v-row> </v-row>
@@ -30,7 +30,7 @@
<v-expansion-panel title="DNS"> <v-expansion-panel title="DNS">
<v-expansion-panel-text> <v-expansion-panel-text>
<v-row> <v-row>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-select <v-select
hide-details hide-details
:label="$t('basic.dns.final')" :label="$t('basic.dns.final')"
@@ -38,7 +38,7 @@
v-model="finalDns"> v-model="finalDns">
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-select <v-select
hide-details hide-details
:label="$t('listen.domainStrategy')" :label="$t('listen.domainStrategy')"
@@ -48,7 +48,7 @@
v-model="appConfig.dns.strategy"> v-model="appConfig.dns.strategy">
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" align-self="center"> <v-col cols="12" sm="6" md="3" lg="2" align-self="center">
<v-btn @click="addDnsServer" rounded> <v-btn @click="addDnsServer" rounded>
<v-icon icon="mdi-plus" />{{ $t('basic.dns.server') }} <v-icon icon="mdi-plus" />{{ $t('basic.dns.server') }}
</v-btn> </v-btn>
@@ -58,7 +58,7 @@
{{ $t('basic.dns.server') + ' ' + (index+1) }} <v-icon icon="mdi-delete" @click="appConfig.dns.servers.splice(index,1)" /> {{ $t('basic.dns.server') + ' ' + (index+1) }} <v-icon icon="mdi-delete" @click="appConfig.dns.servers.splice(index,1)" />
<v-divider></v-divider> <v-divider></v-divider>
<v-row> <v-row>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-text-field <v-text-field
v-model="s.tag" v-model="s.tag"
hide-details hide-details
@@ -67,14 +67,23 @@
:label="$t('objects.tag')" :label="$t('objects.tag')"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-text-field <v-text-field
v-model="s.address" v-model="s.address"
hide-details hide-details
:label="$t('out.addr')" :label="$t('out.addr')"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-text-field
v-model="s.address_resolver"
hide-details
clearable
@click:clear="delete s.address_resolver"
:label="$t('basic.dns.addrResolver')"
></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="3" lg="2">
<v-select <v-select
hide-details hide-details
:label="$t('objects.outbound')" :label="$t('objects.outbound')"
@@ -84,7 +93,7 @@
v-model="s.detour"> v-model="s.detour">
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-select <v-select
hide-details hide-details
:label="$t('listen.domainStrategy')" :label="$t('listen.domainStrategy')"
@@ -101,17 +110,17 @@
<v-expansion-panel title="NTP"> <v-expansion-panel title="NTP">
<v-expansion-panel-text> <v-expansion-panel-text>
<v-row> <v-row>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-switch v-model="enableNtp" color="primary" :label="$t('enable')" hide-details></v-switch> <v-switch v-model="enableNtp" color="primary" :label="$t('enable')" hide-details></v-switch>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" v-if="appConfig.ntp?.enabled"> <v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.ntp?.enabled">
<v-text-field <v-text-field
v-model="appConfig.ntp.server" v-model="appConfig.ntp.server"
hide-details hide-details
:label="$t('out.addr')" :label="$t('out.addr')"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" v-if="appConfig.ntp?.enabled"> <v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.ntp?.enabled">
<v-text-field <v-text-field
v-model="appConfig.ntp.server_port" v-model="appConfig.ntp.server_port"
hide-details hide-details
@@ -121,7 +130,7 @@
:label="$t('out.port')" :label="$t('out.port')"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" v-if="appConfig.ntp?.enabled"> <v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.ntp?.enabled">
<v-text-field <v-text-field
v-model="ntpInterval" v-model="ntpInterval"
hide-details hide-details
@@ -138,7 +147,7 @@
<v-expansion-panel :title="$t('basic.routing.title')"> <v-expansion-panel :title="$t('basic.routing.title')">
<v-expansion-panel-text> <v-expansion-panel-text>
<v-row> <v-row>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-select <v-select
hide-details hide-details
:label="$t('basic.routing.defaultOut')" :label="$t('basic.routing.defaultOut')"
@@ -148,7 +157,7 @@
v-model="appConfig.route.final"> v-model="appConfig.route.final">
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-text-field <v-text-field
v-model="appConfig.route.default_interface" v-model="appConfig.route.default_interface"
hide-details hide-details
@@ -157,7 +166,7 @@
:label="$t('basic.routing.defaultIf')" :label="$t('basic.routing.defaultIf')"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-text-field <v-text-field
v-model.number="routeMark" v-model.number="routeMark"
hide-details hide-details
@@ -166,7 +175,7 @@
:label="$t('basic.routing.defaultRm')" :label="$t('basic.routing.defaultRm')"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-switch <v-switch
v-model="appConfig.route.auto_detect_interface" v-model="appConfig.route.auto_detect_interface"
color="primary" color="primary"
@@ -182,24 +191,24 @@
Cache File Cache File
<v-divider></v-divider> <v-divider></v-divider>
<v-row> <v-row>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-switch v-model="enableCacheFile" color="primary" :label="$t('enable')" hide-details></v-switch> <v-switch v-model="enableCacheFile" color="primary" :label="$t('enable')" hide-details></v-switch>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.cache_file"> <v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.cache_file">
<v-text-field <v-text-field
v-model="appConfig.experimental.cache_file.path" v-model="appConfig.experimental.cache_file.path"
hide-details hide-details
:label="$t('transport.path')" :label="$t('transport.path')"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.cache_file"> <v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.cache_file">
<v-text-field <v-text-field
v-model="appConfig.experimental.cache_file.cache_id" v-model="appConfig.experimental.cache_file.cache_id"
hide-details hide-details
label="Cache ID" label="Cache ID"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.cache_file"> <v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.cache_file">
<v-switch v-model="appConfig.experimental.cache_file.store_fakeip" <v-switch v-model="appConfig.experimental.cache_file.store_fakeip"
color="primary" color="primary"
:label="$t('basic.exp.storeFakeIp')" :label="$t('basic.exp.storeFakeIp')"
@@ -209,45 +218,45 @@
Clash API Clash API
<v-divider></v-divider> <v-divider></v-divider>
<v-row> <v-row>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-switch v-model="enableClashApi" color="primary" :label="$t('enable')" hide-details></v-switch> <v-switch v-model="enableClashApi" color="primary" :label="$t('enable')" hide-details></v-switch>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api"> <v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field <v-text-field
v-model="appConfig.experimental.clash_api.external_controller" v-model="appConfig.experimental.clash_api.external_controller"
hide-details hide-details
label="External Controller" label="External Controller"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api"> <v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field <v-text-field
v-model="appConfig.experimental.clash_api.external_ui" v-model="appConfig.experimental.clash_api.external_ui"
hide-details hide-details
label="External UI" label="External UI"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api"> <v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field <v-text-field
v-model="appConfig.experimental.clash_api.external_ui_download_url" v-model="appConfig.experimental.clash_api.external_ui_download_url"
hide-details hide-details
label="UI Download URL" label="UI Download URL"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api"> <v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field <v-text-field
v-model="appConfig.experimental.clash_api.external_ui_download_detour" v-model="appConfig.experimental.clash_api.external_ui_download_detour"
hide-details hide-details
label="UI Download detour" label="UI Download detour"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api"> <v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field <v-text-field
v-model="appConfig.experimental.clash_api.secret" v-model="appConfig.experimental.clash_api.secret"
hide-details hide-details
label="Secret" label="Secret"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3" v-if="appConfig.experimental.clash_api"> <v-col cols="12" sm="6" md="3" lg="2" v-if="appConfig.experimental.clash_api">
<v-text-field <v-text-field
v-model="appConfig.experimental.clash_api.default_mode" v-model="appConfig.experimental.clash_api.default_mode"
hide-details hide-details
@@ -258,14 +267,14 @@
V2Ray API V2Ray API
<v-divider></v-divider> <v-divider></v-divider>
<v-row> <v-row>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-text-field <v-text-field
v-model="appConfig.experimental.v2ray_api.listen" v-model="appConfig.experimental.v2ray_api.listen"
hide-details hide-details
:label="$t('objects.listen')" :label="$t('objects.listen')"
></v-text-field> ></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="3"> <v-col cols="12" sm="6" md="3" lg="2">
<v-switch v-model="appConfig.experimental.v2ray_api.stats.enabled" <v-switch v-model="appConfig.experimental.v2ray_api.stats.enabled"
color="primary" color="primary"
:label="$t('stats.enable')" :label="$t('stats.enable')"
+440 -108
View File
@@ -5,11 +5,20 @@
:visible="modal.visible" :visible="modal.visible"
:index="modal.index" :index="modal.index"
:data="modal.data" :data="modal.data"
:groups="groups"
:stats="modal.stats" :stats="modal.stats"
:inboundTags="inboundTags" :inboundTags="inboundTags"
@close="closeModal" @close="closeModal"
@save="saveModal" @save="saveModal"
/> />
<ClientBulk
v-model="addBulkModal"
:visible="addBulkModal"
:groups="groups"
:inboundTags="inboundTags"
@close="closeBulk"
@save="saveBulk"
/>
<QrCode <QrCode
v-model="qrcode.visible" v-model="qrcode.visible"
:visible="qrcode.visible" :visible="qrcode.visible"
@@ -23,110 +32,319 @@
:tag="stats.tag" :tag="stats.tag"
@close="closeStats" @close="closeStats"
/> />
<v-row> <v-row justify="center" align="center">
<v-col cols="12" justify="center" align="center"> <v-col cols="auto">
<v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn> <v-btn color="primary" @click="showModal(-1)">{{ $t('actions.add') }}</v-btn>
</v-col> </v-col>
<v-col cols="auto">
<v-menu v-model="actionMenu" :close-on-content-click="false" location="bottom center">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="text" icon>
<v-icon icon="mdi-tools" color="primary" />
</v-btn>
</template>
<v-list density="compact" nav>
<v-list-item link @click="addBulk">
<template v-slot:prepend>
<v-icon icon="mdi-account-multiple-plus"></v-icon>
</template>
<v-list-item-title v-text="$t('bulk.add')"></v-list-item-title>
</v-list-item>
</v-list>
</v-menu>
</v-col>
<v-col cols="auto">
<v-menu v-model="filterMenu" :close-on-content-click="false" location="bottom center">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="text" icon>
<v-icon :icon="filterSettings.enabled ? 'mdi-filter-check-outline' : 'mdi-filter-menu-outline'" :color="filterSettings.enabled ? 'primary' : ''" />
</v-btn>
</template>
<v-card>
<v-container>
<v-row>
<v-col>
<v-select
variant="underlined"
density="compact"
:label="$t('type')"
:items="filterItems"
v-model="filterSettings.state">
</v-select>
</v-col>
</v-row>
<v-row>
<v-col>
<v-select
variant="underlined"
density="compact"
:label="$t('client.group')"
:items="[ {title: $t('all'), value: '-'}, ...groups.map(g => ({ title: g.length>0 ? g : $t('none'), value: g}))]"
v-model="filterSettings.group">
</v-select>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field
variant="underlined"
density="compact"
:label="$t('client.name')"
v-model="filterSettings.text">
</v-text-field>
</v-col>
</v-row>
</v-container>
<v-card-actions>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
variant="outlined"
@click="clearFilter"
>
{{ $t('actions.del') }}
</v-btn>
<v-btn
color="blue-darken-1"
variant="tonal"
@click="doFilter"
>
{{ $t('actions.update') }}
</v-btn>
</v-card-actions>
</v-card-actions>
</v-card>
</v-menu>
</v-col>
<v-col cols="auto">
<v-btn hide-details variant="text" icon @click="toggleClientView">
<v-icon :icon="tableView ? 'mdi-table-eye' : 'mdi-table-eye-off'" :color="tableView ? 'primary' : ''"></v-icon>
</v-btn>
</v-col>
</v-row> </v-row>
<v-row> <template v-for="group in groups" v-if="!tableView">
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in clients" :key="item.id"> <v-row>
<v-card rounded="xl" elevation="5" min-width="200"> <v-col class="v-card-subtitle">
<v-card-title> {{ group.length>0 ? group : $t('none') }}
<v-row> <v-badge :content="(filterSettings.enabled ? filterSettings.filteredClients : clients).filter(c => c.group == group).length" inline color="info" />
<v-col>{{ item.name }}</v-col> <v-icon
<v-spacer></v-spacer> :icon="openedGroups.includes(group) ? 'mdi-arrow-collapse-up' : 'mdi-arrow-collapse-down'"
<v-col cols="auto"> size="small"
<v-switch color="primary" variant="text"
v-model="clients[index].enable" @click="toggleGroupOpen(group)"
@update:model-value="buildInboundsUsers(item.inbounds)" ></v-icon>
hideDetails density="compact" /> </v-col>
</v-col> </v-row>
</v-row> <v-row v-if="openedGroups.includes(group)">
</v-card-title> <template v-for="item in (filterSettings.enabled ? filterSettings.filteredClients : clients).filter(c => c.group == group)" :key="item.id">
<v-card-subtitle style="margin-top: -20px;"> <v-col cols="12" sm="4" md="3" lg="2">
<v-row> <v-card rounded="xl" elevation="5" min-width="200">
<v-col>{{ item.desc }}</v-col> <v-card-title>
</v-row> <v-row>
</v-card-subtitle> <v-col>{{ item.name }}</v-col>
<v-card-text> <v-spacer></v-spacer>
<v-row> <v-col cols="auto">
<v-col>{{ $t('pages.inbounds') }}</v-col> <v-switch color="primary"
<v-col dir="ltr"> v-model="item.enable"
<v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.inbounds != ''"> @update:model-value="buildInboundsUsers(item.inbounds)"
<span v-for="i in item.inbounds">{{ i }}<br /></span> hideDetails density="compact" />
</v-tooltip> </v-col>
{{ item.inbounds.length }} </v-row>
</v-col> </v-card-title>
</v-row> <v-card-subtitle style="margin-top: -20px;">
<v-row> <v-row>
<v-col>{{ $t('stats.volume') }}</v-col> <v-col>{{ item.desc }}</v-col>
<v-col dir="ltr"> </v-row>
{{ item.volume == 0 ? $t('unlimited') : HumanReadable.sizeFormat(item.volume) }} </v-card-subtitle>
</v-col> <v-card-text>
</v-row> <v-row>
<v-row> <v-col>{{ $t('pages.inbounds') }}</v-col>
<v-col>{{ $t('date.expiry') }}</v-col> <v-col dir="ltr">
<v-col dir="ltr"> <v-tooltip activator="parent" dir="ltr" location="bottom" v-if="item.inbounds != ''">
{{ item.expiry == 0 ? $t('unlimited') : HumanReadable.remainedDays(item.expiry)?? $t('date.expired') }} <span v-for="i in item.inbounds">{{ i }}<br /></span>
</v-col> </v-tooltip>
</v-row> {{ item.inbounds.length }}
<v-row> </v-col>
<v-col>{{ $t('stats.usage') }}</v-col> </v-row>
<v-col dir="ltr"> <v-row>
<v-tooltip activator="parent" location="bottom"> <v-col>{{ $t('stats.volume') }}</v-col>
{{ $t('stats.upload') }}:{{ HumanReadable.sizeFormat(item.up) }}<br /> <v-col dir="ltr">
{{ $t('stats.download') }}:{{ HumanReadable.sizeFormat(item.down) }}<br /> {{ item.volume == 0 ? $t('unlimited') : HumanReadable.sizeFormat(item.volume) }}
<template v-if="item.volume>0"> </v-col>
{{ $t('remained') }}: {{ HumanReadable.sizeFormat(item.volume - (item.up + item.down)) }} </v-row>
<v-row>
<v-col>{{ $t('date.expiry') }}</v-col>
<v-col dir="ltr">
{{ item.expiry == 0 ? $t('unlimited') : HumanReadable.remainedDays(item.expiry)?? $t('date.expired') }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('stats.usage') }}</v-col>
<v-col dir="ltr">
<v-tooltip activator="parent" location="bottom">
{{ $t('stats.upload') }}:{{ HumanReadable.sizeFormat(item.up) }}<br />
{{ $t('stats.download') }}:{{ HumanReadable.sizeFormat(item.down) }}<br />
<template v-if="item.volume>0">
{{ $t('remained') }}: {{ HumanReadable.sizeFormat(item.volume - (item.up + item.down)) }}
</template>
</v-tooltip>
{{ HumanReadable.sizeFormat(item.up + item.down) }}
</v-col>
</v-row>
<v-row>
<v-col>{{ $t('online') }}</v-col>
<v-col dir="ltr">
<template v-if="isOnline(item.name).value">
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
</template> </template>
</v-tooltip> <template v-else>-</template>
{{ HumanReadable.sizeFormat(item.up + item.down) }} </v-col>
</v-col> </v-row>
</v-row> </v-card-text>
<v-row> <v-divider></v-divider>
<v-col>{{ $t('online') }}</v-col> <v-card-actions style="padding: 0;">
<v-col dir="ltr"> <v-btn icon="mdi-account-edit" @click="showModal(item.id)">
<template v-if="onlines[index]"> <v-icon />
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip> <v-tooltip activator="parent" location="top" :text="$t('actions.edit') + item.id"></v-tooltip>
</template> </v-btn>
<template v-else>-</template> <v-btn style="margin-inline-start:0;" icon="mdi-account-minus" color="warning" @click="delOverlay[clients.findIndex(c => c.id == item.id)] = true">
</v-col> <v-icon />
</v-row> <v-tooltip activator="parent" location="top" :text="$t('actions.del')"></v-tooltip>
</v-card-text> </v-btn>
<v-divider></v-divider> <v-overlay
<v-card-actions style="padding: 0;"> v-model="delOverlay[clients.findIndex(c => c.id == item.id)]"
<v-btn icon="mdi-account-edit" @click="showModal(index)"> contained
<v-icon /> class="align-center justify-center"
<v-tooltip activator="parent" location="top" :text="$t('actions.edit')"></v-tooltip> >
</v-btn> <v-card :title="$t('actions.del')" rounded="lg">
<v-btn style="margin-inline-start:0;" icon="mdi-account-minus" color="warning" @click="delOverlay[index] = true"> <v-divider></v-divider>
<v-icon /> <v-card-text>{{ $t('confirm') }}</v-card-text>
<v-tooltip activator="parent" location="top" :text="$t('actions.del')"></v-tooltip> <v-card-actions>
</v-btn> <v-btn color="error" variant="outlined" @click="delClient(item.id)">{{ $t('yes') }}</v-btn>
<v-overlay <v-btn color="success" variant="outlined" @click="delOverlay[clients.findIndex(c => c.id == item.id)] = false">{{ $t('no') }}</v-btn>
v-model="delOverlay[index]" </v-card-actions>
contained </v-card>
class="align-center justify-center" </v-overlay>
> <v-btn icon="mdi-qrcode" @click="showQrCode(item.id)">
<v-card :title="$t('actions.del')" rounded="lg"> <v-icon />
<v-divider></v-divider> <v-tooltip activator="parent" location="top" text="QR-Code"></v-tooltip>
<v-card-text>{{ $t('confirm') }}</v-card-text> </v-btn>
<v-card-actions> <v-btn icon="mdi-chart-line" @click="showStats(item.name)" v-if="v2rayStats.users.includes(item.name)">
<v-btn color="error" variant="outlined" @click="delClient(index)">{{ $t('yes') }}</v-btn> <v-icon />
<v-btn color="success" variant="outlined" @click="delOverlay[index] = false">{{ $t('no') }}</v-btn> <v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-card-actions> </v-btn>
</v-card> </v-card-actions>
</v-overlay> </v-card>
<v-btn icon="mdi-qrcode" @click="showQrCode(index)" /> </v-col>
<v-btn icon="mdi-chart-line" @click="showStats(item.name)" /> </template>
</v-card-actions> </v-row>
</v-card> </template>
<v-row v-else>
<v-col cols="12">
<v-data-table
:headers="headers"
:items="filterSettings.enabled ? filterSettings.filteredClients : clients"
:hide-default-footer="filterSettings.enabled ? filterSettings.filteredClients.length<=10 : clients.length<=10"
hide-no-data
fixed-header
:group-by="groupBy"
item-value="name"
:mobile="smAndDown"
mobile-breakpoint="sm"
width="100%"
class="elevation-3 rounded"
>
<template v-slot:group-header="{ item, columns, toggleGroup, isGroupOpen }">
<tr>
<td :colspan="columns.length" @click="toggleGroup(item)" style="min-height: fit-content; text-align: center;">
<v-icon :icon="isGroupOpen(item) ? '$expand' : '$next'"></v-icon>
{{ item.value.length>0 ? item.value : $t('none') }}
<v-badge :content="(filterSettings.enabled ? filterSettings.filteredClients : clients).filter(c => c.group == item.value).length" inline color="success" />
</td>
</tr>
</template>
<template v-slot:item.volume="{ item }">
<div class="text-start">
<v-chip
size="small"
label
>{{ item.volume == 0 ? $t('unlimited') : HumanReadable.sizeFormat(item.volume) }}</v-chip>
</div>
</template>
<template v-slot:item.expiry="{ item }">
<div class="text-start">
<v-chip
size="small"
label
>{{ item.expiry == 0 ? $t('unlimited') : HumanReadable.remainedDays(item.expiry)?? $t('date.expired') }}</v-chip>
</div>
</template>
<template v-slot:item.online="{ item }">
<div class="text-start">
<template v-if="isOnline(item.name).value">
<v-chip density="comfortable" size="small" color="success" variant="flat">{{ $t('online') }}</v-chip>
</template>
<template v-else>-</template>
</div>
</template>
<template v-slot:item.actions="{ item }">
<v-icon
class="me-2"
@click="showModal(item.id)"
>
mdi-pencil
</v-icon>
<v-menu
v-model="delOverlay[clients.findIndex(c => c.id == item.id)]"
:close-on-content-click="false"
location="top center"
>
<template v-slot:activator="{ props }">
<v-icon
class="me-2"
color="error"
v-bind="props"
>
mdi-delete
</v-icon>
</template>
<v-card :title="$t('actions.del')" rounded="lg">
<v-divider></v-divider>
<v-card-text>{{ $t('confirm') }}</v-card-text>
<v-card-actions>
<v-btn color="error" variant="outlined" @click="delClient(item.id)">{{ $t('yes') }}</v-btn>
<v-btn color="success" variant="outlined" @click="delOverlay[clients.findIndex(c => c.id == item.id)] = false">{{ $t('no') }}</v-btn>
</v-card-actions>
</v-card>
</v-menu>
<v-icon
class="me-2"
@click="showQrCode(item.id)"
>
mdi-qrcode
</v-icon>
<v-icon icon="mdi-chart-line" @click="showStats(item.name)" v-if="v2rayStats.users.includes(item.name)">
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-icon>
</template>
</v-data-table>
</v-col> </v-col>
</v-row> </v-row>
</template> </template>
<style>
.v-data-table__tr--mobile td {
height: fit-content;
min-height: 36px !important;
}
.v-data-table__tr--mobile td div {
width:max-content;
}
</style>
<script lang="ts" setup> <script lang="ts" setup>
import Data from '@/store/modules/data' import Data from '@/store/modules/data'
import ClientModal from '@/layouts/modals/Client.vue' import ClientModal from '@/layouts/modals/Client.vue'
import ClientBulk from '@/layouts/modals/ClientBulk.vue'
import QrCode from '@/layouts/modals/QrCode.vue' import QrCode from '@/layouts/modals/QrCode.vue'
import Stats from '@/layouts/modals/Stats.vue' import Stats from '@/layouts/modals/Stats.vue'
import { Client, createClient } from '@/types/clients' import { Client, createClient } from '@/types/clients'
@@ -137,13 +355,16 @@ import { Link, LinkUtil } from '@/plugins/link'
import { HumanReadable } from '@/plugins/utils' import { HumanReadable } from '@/plugins/utils'
import { i18n } from '@/locales' import { i18n } from '@/locales'
import { push } from 'notivue' import { push } from 'notivue'
import { useDisplay } from 'vuetify'
const { smAndDown } = useDisplay()
const clients = computed((): any[] => { const clients = computed((): any[] => {
return Data().clients return Data().clients
}) })
const onlines = computed(() => { const isOnline = (cname: string) => computed(() => {
return Data().onlines.user ? clients.value.map(c => Data().onlines.user.includes(c.name)) : [] return Data().onlines?.user ? Data().onlines.user.includes(cname) : false
}) })
const appConfig = computed((): Config => { const appConfig = computed((): Config => {
@@ -163,6 +384,50 @@ const inboundTags = computed((): string[] => {
return inbounds.value?.filter(i => i.tag != "" && Object.hasOwn(i,'users')).map(i => i.tag) return inbounds.value?.filter(i => i.tag != "" && Object.hasOwn(i,'users')).map(i => i.tag)
}) })
const groups = computed((): string[] => {
if (!clients.value) return []
if (filterSettings?.value.enabled) return Array.from(new Set(filterSettings.value.filteredClients?.map(c => c.group)))
return Array.from(new Set(clients.value?.map(c => c.group)))
})
const actionMenu = ref(false)
const filterMenu = ref(false)
const filterSettings = ref({
enabled: false,
state: '',
group: '-',
text: '',
filteredClients: <any[]>[]
})
const tableView = ref(localStorage.getItem('clientView') == 'table')
const toggleClientView = () => {
localStorage.setItem('clientView',tableView.value ? 'tile' : 'table')
tableView.value = !tableView.value
}
const filterItems = [
{ title: i18n.global.t('none'), value: '' },
{ title: i18n.global.t('disable'), value: 'disable' },
{ title: i18n.global.t('date.expired'), value: 'expired' },
{ title: i18n.global.t('online'), value: 'online' },
]
const headers = [
{ title: i18n.global.t('client.name'), key: 'name' },
{ title: i18n.global.t('client.desc'), key: 'desc', sortable: false },
{ title: i18n.global.t('actions.action'), key: 'actions', sortable: false},
{ title: i18n.global.t('stats.volume'), key: 'volume' },
{ title: i18n.global.t('date.expiry'), key: 'expiry' },
{ title: i18n.global.t('online'), key: 'online' },
{ key: 'data-table-group', width: 0 },
]
const groupBy = [
{
key: 'group'
}
]
const modal = ref({ const modal = ref({
visible: false, visible: false,
index: -1, index: -1,
@@ -172,7 +437,8 @@ const modal = ref({
const delOverlay = ref(new Array<boolean>(clients.value.length).fill(false)) const delOverlay = ref(new Array<boolean>(clients.value.length).fill(false))
const showModal = (index: number) => { const showModal = (id: number) => {
const index = id == -1 ? -1 : clients.value.findIndex(c => c.id == id)
modal.value.index = index modal.value.index = index
modal.value.data = index == -1 ? '' : JSON.stringify(clients.value[index]) modal.value.data = index == -1 ? '' : JSON.stringify(clients.value[index])
modal.value.stats = index == -1 ? false : v2rayStats.value.users.includes(clients.value[index].name) modal.value.stats = index == -1 ? false : v2rayStats.value.users.includes(clients.value[index].name)
@@ -193,10 +459,6 @@ const saveModal = (data:any, stats:boolean) => {
if(modal.value.index == -1) { if(modal.value.index == -1) {
clients.value.push(data) clients.value.push(data)
} else { } else {
const oldData = createClient(clients.value[modal.value.index])
oldData.inbounds.forEach((i:string) => {
if (!data.inbounds.includes(i)) data.inbounds.push(i)
})
clients.value[modal.value.index] = data clients.value[modal.value.index] = data
} }
@@ -261,9 +523,13 @@ const updateLinks = (c:Client):Link[] => {
const newLinks = <Link[]>[] const newLinks = <Link[]>[]
clientInbounds.forEach(i =>{ clientInbounds.forEach(i =>{
const tlsConfig = <any>Data().tlsConfigs?.findLast((t:any) => t.inbounds.includes(i.tag)) const tlsConfig = <any>Data().tlsConfigs?.findLast((t:any) => t.inbounds.includes(i.tag))
const uri = LinkUtil.linkGenerator(c.name,i,tlsConfig?.client) const cData = <any>Data().inData?.findLast((d:any) => d.tag == i.tag)
if (uri.length>0){ const addrs = cData ? <any[]>cData.addrs : []
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri }) const uris = LinkUtil.linkGenerator(c,i, tlsConfig?.client?? {}, addrs)
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
})
} }
}) })
let links = c.links && c.links.length>0? c.links : <Link[]>[] let links = c.links && c.links.length>0? c.links : <Link[]>[]
@@ -271,8 +537,8 @@ const updateLinks = (c:Client):Link[] => {
return links return links
} }
const delClient = (clientIndex: number) => { const delClient = (id: number) => {
const id = clients.value[clientIndex].id const clientIndex = clients.value.findIndex(c => c.id === id)
const oldData = createClient(clients.value[clientIndex]) const oldData = createClient(clients.value[clientIndex])
// Delete stats if exists and will be orphaned // Delete stats if exists and will be orphaned
@@ -293,8 +559,9 @@ const qrcode = ref({
index: 0, index: 0,
}) })
const showQrCode = (index: number) => { const showQrCode = (id: number) => {
qrcode.value.index = index const clientIndex = clients.value.findIndex(c => c.id === id)
qrcode.value.index = clientIndex
qrcode.value.visible = true qrcode.value.visible = true
} }
const closeQrCode = () => { const closeQrCode = () => {
@@ -314,4 +581,69 @@ const showStats = (tag: string) => {
const closeStats = () => { const closeStats = () => {
stats.value.visible = false stats.value.visible = false
} }
var openedGroups = ref(<string[]>[""])
const toggleGroupOpen = (g: string) => {
const index = openedGroups.value.findIndex(og => og == g)
index == -1 ? openedGroups.value.push(g) : openedGroups.value.splice(index,1)
}
const doFilter = () => {
let filteredClients = clients.value.slice()
if (filterSettings.value.group != '-') {
filteredClients = filteredClients.filter(c => c.group == filterSettings.value.group)
}
if (filterSettings.value.text.length>0) {
const txt = filterSettings.value.text
filteredClients = filteredClients.filter(c => c.name.search(txt) != -1 || c.desc.search(txt) != -1)
}
switch (filterSettings.value.state) {
case "disable":
filteredClients = filteredClients.filter(c => c.enable == false)
break
case "expired":
filteredClients = filteredClients.filter(c => HumanReadable.remainedDays(c.expiry) == null)
break
case "online":
filteredClients = filteredClients.filter(c => Data().onlines?.user?.includes(c.name))
break
}
filterSettings.value.filteredClients = filteredClients
filterSettings.value.enabled = true
filterMenu.value = false
}
const clearFilter = () => {
filterSettings.value = {
enabled: false,
state: '',
group: '-',
text: '',
filteredClients: <any[]>[]
}
filterMenu.value = false
}
const addBulkModal = ref(false)
const addBulk = () => {
addBulkModal.value = true
actionMenu.value = false
}
const closeBulk = () => {
addBulkModal.value = false
}
const saveBulk = (bulkClients: Client[], clientInbounds: string[], clientStats: boolean) => {
bulkClients.forEach((c,c_index) => {
bulkClients[c_index].links = updateLinks(c)
})
clients.value.push(...bulkClients)
buildInboundsUsers(clientInbounds)
// Stats
if (clientStats) v2rayStats.value.users.push(...bulkClients.map(bc => bc.name))
closeBulk()
}
</script> </script>
+80 -39
View File
@@ -2,9 +2,10 @@
<InboundVue <InboundVue
v-model="modal.visible" v-model="modal.visible"
:visible="modal.visible" :visible="modal.visible"
:id="modal.id" :index="modal.index"
:stats="modal.stats" :stats="modal.stats"
:data="modal.data" :data="modal.data"
:cData="modal.cData"
:inTags="inTags" :inTags="inTags"
:outTags="outTags" :outTags="outTags"
:tlsConfigs="tlsConfigs" :tlsConfigs="tlsConfigs"
@@ -93,7 +94,10 @@
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-overlay> </v-overlay>
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)" /> <v-btn icon="mdi-chart-line" @click="showStats(item.tag)" v-if="v2rayStats.inbounds.includes(item.tag)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-col> </v-col>
@@ -111,6 +115,7 @@ import { Client } from '@/types/clients'
import { Link, LinkUtil } from '@/plugins/link' import { Link, LinkUtil } from '@/plugins/link'
import { i18n } from '@/locales' import { i18n } from '@/locales'
import { push } from 'notivue' import { push } from 'notivue'
import { fillData } from '@/plugins/outJson'
const appConfig = computed((): Config => { const appConfig = computed((): Config => {
return <Config> Data().config return <Config> Data().config
@@ -124,6 +129,10 @@ const tlsConfigs = computed((): any[] => {
return <any[]> Data().tlsConfigs return <any[]> Data().tlsConfigs
}) })
const inData = computed((): any[] => {
return <any[]> Data().inData
})
const inTags = computed((): string[] => { const inTags = computed((): string[] => {
return inbounds.value?.map(i => i.tag) return inbounds.value?.map(i => i.tag)
}) })
@@ -146,44 +155,56 @@ const v2rayStats = computed((): V2rayApiStats => {
const modal = ref({ const modal = ref({
visible: false, visible: false,
id: -1, index: -1,
data: "", data: "",
cData: "",
stats: false, stats: false,
}) })
let delOverlay = ref(new Array<boolean>) let delOverlay = ref(new Array<boolean>)
const showModal = (id: number) => { const showModal = (index: number) => {
modal.value.id = id modal.value.index = index
modal.value.data = id == -1 ? '' : JSON.stringify(inbounds.value[id]) if (index == -1){
modal.value.stats = id == -1 ? false : v2rayStats.value.inbounds.includes(inbounds.value[id].tag) modal.value.data = ''
modal.value.cData = ''
modal.value.stats = false
} else {
modal.value.data = JSON.stringify(inbounds.value[index])
modal.value.stats = v2rayStats.value.inbounds.includes(inbounds.value[index].tag)
const inDataIndex = inData.value.findIndex(d => d.tag == inbounds.value[index].tag)
modal.value.cData = inDataIndex == -1 ? '' : JSON.stringify(inData.value[inDataIndex])
}
modal.value.visible = true modal.value.visible = true
} }
const closeModal = () => { const closeModal = () => {
modal.value.visible = false modal.value.visible = false
} }
const saveModal = (data:Inbound, stats: boolean, tls_id: number) => { const saveModal = (data:Inbound, stats: boolean, tls_id: number, cData: any) => {
// Check duplicate tag // Check duplicate tag
const oldTag = modal.value.id != -1 ? inbounds.value[modal.value.id].tag : null const oldTag = modal.value.index != -1 ? inbounds.value[modal.value.index].tag : null
if (data.tag != oldTag && inTags.value.includes(data.tag)) { if (data.tag != oldTag && inTags.value.includes(data.tag)) {
push.error({ push.error({
message: i18n.global.t('error.dplData') + ": " + i18n.global.t('objects.tag') message: i18n.global.t('error.dplData') + ": " + i18n.global.t('objects.tag')
}) })
return return
} }
if (cData.id != -1) {
cData.tag = data.tag
fillData(cData.outJson, data,tls_id>0 ? tlsConfigs.value.findLast(t => t.id == tls_id).client : {})
}
// New or Edit // New or Edit
if (modal.value.id == -1) { if (modal.value.index == -1) {
inbounds.value.push(data) inbounds.value.push(data)
if (stats && data.tag.length>0) { if (stats && data.tag.length>0) {
v2rayStats.value.inbounds.push(data.tag) v2rayStats.value.inbounds.push(data.tag)
} }
// Update tls preset if (cData.id != -1){
if (tls_id>0) { inData.value.push(cData)
tlsConfigs.value.findLast(t => t.id == tls_id).inbounds.push(data.tag)
} }
} else { } else {
const oldTag = inbounds.value[modal.value.id].tag const oldTag = inbounds.value[modal.value.index].tag
const sIndex = v2rayStats.value.inbounds.findIndex(i => i == data.tag) // Find if new tag exists const sIndex = v2rayStats.value.inbounds.findIndex(i => i == data.tag) // Find if new tag exists
// Update tls preset // Update tls preset
@@ -191,9 +212,6 @@ const saveModal = (data:Inbound, stats: boolean, tls_id: number) => {
if (oldTlsConfigIndex != -1){ if (oldTlsConfigIndex != -1){
tlsConfigs.value[oldTlsConfigIndex].inbounds = tlsConfigs?.value[oldTlsConfigIndex].inbounds.filter((i:string) => i != oldTag) tlsConfigs.value[oldTlsConfigIndex].inbounds = tlsConfigs?.value[oldTlsConfigIndex].inbounds.filter((i:string) => i != oldTag)
} }
if (tls_id>0) {
tlsConfigs.value.findLast(t => t.id == tls_id).inbounds.push(data.tag)
}
if (oldTag != data.tag) { if (oldTag != data.tag) {
v2rayStats.value.inbounds = v2rayStats.value.inbounds.filter(item => item != oldTag) v2rayStats.value.inbounds = v2rayStats.value.inbounds.filter(item => item != oldTag)
@@ -208,8 +226,25 @@ const saveModal = (data:Inbound, stats: boolean, tls_id: number) => {
if (sIndex != -1) v2rayStats.value.inbounds.splice(sIndex,1) if (sIndex != -1) v2rayStats.value.inbounds.splice(sIndex,1)
} }
inbounds.value[modal.value.id] = data inbounds.value[modal.value.index] = data
const inDataIndex = inData.value.findIndex(indata => indata.tag == oldTag)
if (cData.id != -1) {
if (inDataIndex == -1){
inData.value.push(cData)
} else {
inData.value[inDataIndex] = cData
}
} else if (inDataIndex != -1) {
Data().delInData(inData.value[inDataIndex].id)
inData.value.splice(inDataIndex,1)
}
} }
// Update tls preset
if (tls_id>0) {
tlsConfigs.value.findLast(t => t.id == tls_id).inbounds.push(data.tag)
tlsConfigs.value.sort()
}
if (Object.hasOwn(data,'users')) { if (Object.hasOwn(data,'users')) {
// Set users // Set users
data = buildInboundsUsers(data) data = buildInboundsUsers(data)
@@ -218,25 +253,27 @@ const saveModal = (data:Inbound, stats: boolean, tls_id: number) => {
} }
modal.value.visible = false modal.value.visible = false
} }
const updateLinks = (i: InboundWithUser) => { const updateLinks = (i: any) => {
if(i.users && i.users.length>0){ if(i.users){
i.users.forEach((u:any) => { const uClients = clients.value.filter(c => c.inbounds.includes(i.tag))
const client = clients.value.find(c => u.username? c.name == u.username : c.name == u.name) uClients.forEach((u:Client) => {
if (client){ const clientInbounds = <Inbound[]>inbounds.value.filter(inb => u.inbounds.includes(inb.tag))
const clientInbounds = <Inbound[]>inbounds.value.filter(inb => client?.inbounds.includes(inb.tag)) const newLinks = <Link[]>[]
const newLinks = <Link[]>[] clientInbounds.forEach(i =>{
clientInbounds.forEach(i =>{ const tlsClient = tlsConfigs?.value.findLast((t:any) => t.inbounds.includes(i.tag))?.client?? {}
const tlsClient = tlsConfigs?.value.findLast((t:any) => t.inbounds.includes(i.tag))?.client?? null const cData = <any>Data().inData?.findLast((d:any) => d.tag == i.tag)
const uri = LinkUtil.linkGenerator(client.name,i, tlsClient) const addrs = cData ? <any[]>cData.addrs : []
if (uri.length>0){ const uris = LinkUtil.linkGenerator(u,i, tlsClient, addrs)
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri }) newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
} })
}) }
let links = client.links && client.links.length>0? client.links : <Link[]>[] })
links = [...newLinks, ...links.filter(l => l.type != 'local')] let links = u.links && u.links.length>0? u.links : <Link[]>[]
links = [...newLinks, ...links.filter(l => l.type != 'local')]
client.links = links u.links = links
}
}) })
} }
} }
@@ -249,10 +286,10 @@ const delInbound = (index: number) => {
const inbU = <InboundWithUser>inb const inbU = <InboundWithUser>inb
if (inbU.users && inbU.users.length>0){ if (inbU.users && inbU.users.length>0){
inbU.users.forEach((u:any) => { inbU.users.forEach((u:any) => {
const c_index = clients.value.findIndex(c => u.username? u.username == c.name : u.user == c.name) const c_index = clients.value.findIndex(c => u.username? u.username == c.name : u.name == c.name)
if (c_index != -1) { if (c_index != -1) {
const clientInbounds = clients.value[c_index].inbounds.filter((x:string) => x!=tag) clients.value[c_index].inbounds = clients.value[c_index].inbounds.filter((x:string) => x!=tag)
clients.value[c_index].inbounds = clientInbounds clients.value[c_index].links = clients.value[c_index].links.filter((x:any) => x.remark!=tag)
} }
}) })
} }
@@ -274,10 +311,14 @@ const delInbound = (index: number) => {
} }
if (index < Data().oldData.config.inbounds.length){ if (index < Data().oldData.config.inbounds.length){
Data().delInbound(index) Data().delInbound(index)
} else {
// Delete new inbound's inData if exists
const inDataIndex = Data().inData.findIndex((d:any) => d.tag == tag)
if (inDataIndex != -1) Data().inData.splice(inDataIndex, 1)
} }
delOverlay.value[index] = false delOverlay.value[index] = false
} }
const buildInboundsUsers = (inbound:InboundWithUser):Inbound => { const buildInboundsUsers = (inbound:any):Inbound => {
const users = <any>[] const users = <any>[]
const inboundClients = clients.value.filter(c => c.enable && c.inbounds.includes(inbound.tag)) const inboundClients = clients.value.filter(c => c.enable && c.inbounds.includes(inbound.tag))
inboundClients.forEach(c => { inboundClients.forEach(c => {
+4 -1
View File
@@ -82,7 +82,10 @@
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-overlay> </v-overlay>
<v-btn icon="mdi-chart-line" @click="showStats(item.tag)" /> <v-btn icon="mdi-chart-line" @click="showStats(item.tag)" v-if="v2rayStats.outbounds.includes(item.tag)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('stats.graphTitle')"></v-tooltip>
</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-col> </v-col>
+27 -11
View File
@@ -27,21 +27,15 @@
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12">{{ $t('rule.ruleset') }}</v-col> <v-col class="v-card-subtitle" cols="12">{{ $t('rule.ruleset') }}</v-col>
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>rulesets" :key="item.tag"> <v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>rulesets" :key="item.tag">
<v-card rounded="xl" elevation="5" min-width="200" :title="index"> <v-card rounded="xl" elevation="5" min-width="200" :title="item.tag">
<v-card-subtitle style="margin-top: -20px;"> <v-card-subtitle style="margin-top: -20px;">
<v-row> <v-row>
<v-col>{{ $t('ruleset.' + item.type) }}</v-col> <v-col>{{ $t('ruleset.' + item.type) }}</v-col>
</v-row> </v-row>
</v-card-subtitle> </v-card-subtitle>
<v-card-text> <v-card-text>
<v-row>
<v-col>{{ $t('objects.tag') }}</v-col>
<v-col dir="ltr">
{{ item.tag }}
</v-col>
</v-row>
<v-row> <v-row>
<v-col>{{ $t('ruleset.format') }}</v-col> <v-col>{{ $t('ruleset.format') }}</v-col>
<v-col dir="ltr"> <v-col dir="ltr">
@@ -84,9 +78,15 @@
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12">{{ $t('pages.rules') }}</v-col> <v-col class="v-card-subtitle" cols="12">{{ $t('pages.rules') }}</v-col>
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>rules"> <v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>rules"
<v-card rounded="xl" elevation="5" min-width="200" :title="index"> :key="item.id"
:draggable="true"
@dragstart="onDragStart(index)"
@dragover.prevent
@drop="onDrop(index)"
>
<v-card rounded="xl" elevation="5" min-width="200" :title="index+1">
<v-card-subtitle style="margin-top: -20px;"> <v-card-subtitle style="margin-top: -20px;">
<v-row> <v-row>
<v-col>{{ item.type != undefined ? $t('rule.logical') + ' (' + item.mode + ')' : $t('rule.simple') }}</v-col> <v-col>{{ item.type != undefined ? $t('rule.logical') + ' (' + item.mode + ')' : $t('rule.simple') }}</v-col>
@@ -263,4 +263,20 @@ const delRuleset = (index: number) => {
rulesets.value.splice(index,1) rulesets.value.splice(index,1)
delRulesetOverlay.value[index] = false delRulesetOverlay.value[index] = false
} }
const draggedItemIndex = ref(null);
const onDragStart = (index: any) => {
draggedItemIndex.value = index;
};
const onDrop = (index: any) => {
if (draggedItemIndex.value !== null) {
// Swap the dragged item with the dropped one
const draggedItem = rules.value[draggedItemIndex.value];
rules.value.splice(draggedItemIndex.value, 1);
rules.value.splice(index, 0, draggedItem);
draggedItemIndex.value = null;
}
};
</script> </script>
+12 -5
View File
@@ -8,7 +8,8 @@
> >
<v-tab value="t1">{{ $t('setting.interface') }}</v-tab> <v-tab value="t1">{{ $t('setting.interface') }}</v-tab>
<v-tab value="t2">{{ $t('setting.sub') }}</v-tab> <v-tab value="t2">{{ $t('setting.sub') }}</v-tab>
<v-tab value="t3">Language</v-tab> <v-tab value="t3">{{ $t('setting.jsonSub') }}</v-tab>
<v-tab value="t4">Language</v-tab>
</v-tabs> </v-tabs>
<v-card-text> <v-card-text>
<v-row align="center" justify="center" style="margin-bottom: 10px;"> <v-row align="center" justify="center" style="margin-bottom: 10px;">
@@ -128,6 +129,10 @@
</v-window-item> </v-window-item>
<v-window-item value="t3"> <v-window-item value="t3">
<SubJsonExtVue :settings="settings" />
</v-window-item>
<v-window-item value="t4">
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-select <v-select
@@ -146,11 +151,12 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { useLocale } from "vuetify" import { useLocale } from 'vuetify'
import { languages } from '@/locales' import { languages } from '@/locales'
import { Ref, computed, inject, onMounted, ref } from "vue" import { Ref, computed, inject, onMounted, ref } from 'vue'
import HttpUtils from "@/plugins/httputil" import HttpUtils from '@/plugins/httputil'
import { FindDiff } from "@/plugins/utils" import { FindDiff } from '@/plugins/utils'
import SubJsonExtVue from '@/components/SubJsonExt.vue'
const locale = useLocale() const locale = useLocale()
const tab = ref("t1") const tab = ref("t1")
const loading:Ref = inject('loading')?? ref(false) const loading:Ref = inject('loading')?? ref(false)
@@ -177,6 +183,7 @@ const settings = ref({
subEncode: "true", subEncode: "true",
subShowInfo: "false", subShowInfo: "false",
subURI: "", subURI: "",
subJsonExt: "",
}) })
onMounted(async () => {loadData()}) onMounted(async () => {loadData()})
+48 -20
View File
@@ -14,7 +14,7 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>tlsConfigs" :key="item.id"> <v-col cols="12" sm="4" md="3" lg="2" v-for="(item, index) in <any[]>tlsConfigs" :key="item.id">
<v-card rounded="xl" elevation="5" min-width="200" :title="item.id + '. ' + item.name"> <v-card rounded="xl" elevation="5" min-width="200" :title="(item.id? item.id + '. ' : '*') + item.name">
<v-card-subtitle style="margin-top: -20px;"> <v-card-subtitle style="margin-top: -20px;">
{{ item.server?.server_name?.length>0 ? item.server.server_name : "-" }} {{ item.server?.server_name?.length>0 ? item.server.server_name : "-" }}
</v-card-subtitle> </v-card-subtitle>
@@ -71,6 +71,10 @@
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-overlay> </v-overlay>
<v-btn icon="mdi-content-duplicate" @click="clone(index)">
<v-icon />
<v-tooltip activator="parent" location="top" :text="$t('actions.clone')"></v-tooltip>
</v-btn>
</v-card-actions> </v-card-actions>
</v-card> </v-card>
</v-col> </v-col>
@@ -81,10 +85,11 @@
import TlsVue from '@/layouts/modals/Tls.vue' import TlsVue from '@/layouts/modals/Tls.vue'
import Data from '@/store/modules/data' import Data from '@/store/modules/data'
import { computed, ref } from 'vue' import { computed, ref } from 'vue'
import { Config } from '@/types/config'; import { Config } from '@/types/config'
import { Inbound } from '@/types/inbounds'; import { Inbound } from '@/types/inbounds'
import { Client } from '@/types/clients'; import { Client } from '@/types/clients'
import { Link, LinkUtil } from '@/plugins/link'; import { Link, LinkUtil } from '@/plugins/link'
import { fillData } from '@/plugins/outJson'
const tlsConfigs = computed((): any[] => { const tlsConfigs = computed((): any[] => {
return Data().tlsConfigs return Data().tlsConfigs
@@ -94,6 +99,10 @@ const inbounds = computed((): any[] => {
return <any[]>(<Config>Data().config)?.inbounds return <any[]>(<Config>Data().config)?.inbounds
}) })
const inData = computed((): any[] => {
return <any[]> Data().inData
})
const clients = computed((): any[] => { const clients = computed((): any[] => {
return <Client[]>Data().clients return <Client[]>Data().clients
}) })
@@ -111,6 +120,15 @@ const showModal = (index: number) => {
modal.value.data = index == -1 ? '{}' : JSON.stringify(tlsConfigs.value[index]) modal.value.data = index == -1 ? '{}' : JSON.stringify(tlsConfigs.value[index])
modal.value.visible = true modal.value.visible = true
} }
const clone = (index: number) => {
let data = JSON.parse(JSON.stringify(tlsConfigs.value[index]))
data.id = 0
data.inbounds = []
while (tlsConfigs.value.findIndex(t => t.name == data.name) != -1){
data.name += "-copy"
}
saveModal(data)
}
const closeModal = () => { const closeModal = () => {
modal.value.visible = false modal.value.visible = false
} }
@@ -122,6 +140,7 @@ const saveModal = (data:any) => {
tlsConfigs.value[modal.value.index] = data tlsConfigs.value[modal.value.index] = data
inbounds?.value.filter(i => tlsConfigs.value[modal.value.index].inbounds.includes(i.tag)).forEach(i =>{ inbounds?.value.filter(i => tlsConfigs.value[modal.value.index].inbounds.includes(i.tag)).forEach(i =>{
if (i.tls != undefined) i.tls = data.server if (i.tls != undefined) i.tls = data.server
updateInData(i,data.client)
updateLinks(i,data.client) updateLinks(i,data.client)
}) })
} }
@@ -137,24 +156,33 @@ const delTls = (index: number) => {
} }
const updateLinks = (i:any,tlsClient:any) => { const updateLinks = (i:any,tlsClient:any) => {
if(i.users && i.users.length>0){ if(i.users){
i.users.forEach((u:any) => { const uClients = clients.value.filter(c => c.inbounds.includes(i.tag))
const client = clients.value.find(c => u.username? c.name == u.username : c.name == u.name) uClients.forEach((client:any) => {
if (client){ const clientInbounds = <Inbound[]>inbounds.value.filter(inb => client?.inbounds.includes(inb.tag))
const clientInbounds = <Inbound[]>inbounds.value.filter(inb => client?.inbounds.includes(inb.tag)) const newLinks = <Link[]>[]
const newLinks = <Link[]>[] clientInbounds.forEach(i =>{
clientInbounds.forEach(i =>{ const cData = <any>Data().inData?.findLast((d:any) => d.tag == i.tag)
const uri = LinkUtil.linkGenerator(client.name,i,tlsClient) const addrs = cData ? <any[]>cData.addrs : []
if (uri.length>0){ const uris = LinkUtil.linkGenerator(client,i, tlsClient, addrs)
if (uris.length>0){
uris.forEach(uri => {
newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri }) newLinks.push(<Link>{ type: 'local', remark: i.tag, uri: uri })
} })
}) }
let links = client.links && client.links.length>0? client.links : <Link[]>[] })
links = [...newLinks, ...links.filter((l:Link) => l.type != 'local')] let links = client.links && client.links.length>0? client.links : <Link[]>[]
links = [...newLinks, ...links.filter((l:Link) => l.type != 'local')]
client.links = links client.links = links
}
}) })
} }
} }
const updateInData = (i:any, c:any) => {
const inDataIndex = inData.value.findIndex(d => d.tag == i.tag)
if (inDataIndex != -1) {
fillData(inData.value[inDataIndex].outJson, i, c)
}
}
</script> </script>
+2 -1
View File
@@ -5,5 +5,6 @@
"moduleResolution": "Node", "moduleResolution": "Node",
"allowSyntheticDefaultImports": true "allowSyntheticDefaultImports": true
}, },
"include": ["vite.config.ts"] "include": ["vite.config.mts"],
"exclude": []
} }
+53 -10
View File
@@ -29,6 +29,9 @@ arch() {
i*86 | x86) echo '386' ;; i*86 | x86) echo '386' ;;
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;; armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
armv7* | armv7 | arm) echo 'armv7' ;; armv7* | armv7 | arm) echo 'armv7' ;;
armv6* | armv6) echo 'armv6' ;;
armv5* | armv5) echo 'armv5' ;;
s390x) echo 's390x' ;;
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;; *) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
esac esac
} }
@@ -38,39 +41,79 @@ echo "arch: $(arch)"
os_version="" os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1) os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
if [[ "${release}" == "centos" ]]; then if [[ "${release}" == "arch" ]]; then
echo "Your OS is Arch Linux"
elif [[ "${release}" == "parch" ]]; then
echo "Your OS is Parch linux"
elif [[ "${release}" == "manjaro" ]]; then
echo "Your OS is Manjaro"
elif [[ "${release}" == "armbian" ]]; then
echo "Your OS is Armbian"
elif [[ "${release}" == "opensuse-tumbleweed" ]]; then
echo "Your OS is OpenSUSE Tumbleweed"
elif [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 8 ]]; then if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1 echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "ubuntu" ]]; then elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1 echo -e "${red} Please use Ubuntu 20 or higher version!${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "fedora" ]]; then elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then if [[ ${os_version} -lt 36 ]]; then
echo -e "${red}please use Fedora 36 or higher version! ${plain}\n" && exit 1 echo -e "${red} Please use Fedora 36 or higher version!${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "debian" ]]; then elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 10 ]]; then if [[ ${os_version} -lt 11 ]]; then
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1 echo -e "${red} Please use Debian 11 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "almalinux" ]]; then
if [[ ${os_version} -lt 9 ]]; then
echo -e "${red} Please use AlmaLinux 9 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "rocky" ]]; then
if [[ ${os_version} -lt 9 ]]; then
echo -e "${red} Please use Rocky Linux 9 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "oracle" ]]; then
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use Oracle Linux 8 or higher ${plain}\n" && exit 1
fi fi
else else
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1 echo -e "${red}Your operating system is not supported by this script.${plain}\n"
echo "Please ensure you are using one of the following supported operating systems:"
echo "- Ubuntu 20.04+"
echo "- Debian 11+"
echo "- CentOS 8+"
echo "- Fedora 36+"
echo "- Arch Linux"
echo "- Parch Linux"
echo "- Manjaro"
echo "- Armbian"
echo "- AlmaLinux 9+"
echo "- Rocky Linux 9+"
echo "- Oracle Linux 8+"
echo "- OpenSUSE Tumbleweed"
exit 1
fi fi
install_base() { install_base() {
case "${release}" in case "${release}" in
centos) centos | almalinux | rocky | oracle)
yum -y update && yum install -y -q wget curl tar tzdata yum -y update && yum install -y -q wget curl tar tzdata
;; ;;
fedora) fedora)
dnf -y update && dnf install -y -q wget curl tar tzdata dnf -y update && dnf install -y -q wget curl tar tzdata
;; ;;
arch | manjaro | parch)
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
;;
opensuse-tumbleweed)
zypper refresh && zypper -q install -y wget curl tar timezone
;;
*) *)
apt-get update && apt install -y -q wget curl tar tzdata apt-get update && apt-get install -y -q wget curl tar tzdata
;; ;;
esac esac
} }
+69 -21
View File
@@ -39,24 +39,61 @@ echo "The OS release is: $release"
os_version="" os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1) os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
if [[ "${release}" == "centos" ]]; then if [[ "${release}" == "arch" ]]; then
echo "Your OS is Arch Linux"
elif [[ "${release}" == "parch" ]]; then
echo "Your OS is Parch linux"
elif [[ "${release}" == "manjaro" ]]; then
echo "Your OS is Manjaro"
elif [[ "${release}" == "armbian" ]]; then
echo "Your OS is Armbian"
elif [[ "${release}" == "opensuse-tumbleweed" ]]; then
echo "Your OS is OpenSUSE Tumbleweed"
elif [[ "${release}" == "centos" ]]; then
if [[ ${os_version} -lt 8 ]]; then if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1 echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "ubuntu" ]]; then elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version! ${plain}\n" && exit 1 echo -e "${red} Please use Ubuntu 20 or higher version!${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "fedora" ]]; then elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then if [[ ${os_version} -lt 36 ]]; then
echo -e "${red}please use Fedora 36 or higher version! ${plain}\n" && exit 1 echo -e "${red} Please use Fedora 36 or higher version!${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "debian" ]]; then elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 10 ]]; then if [[ ${os_version} -lt 11 ]]; then
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1 echo -e "${red} Please use Debian 11 or higher ${plain}\n" && exit 1
fi fi
elif [[ "${release}" == "almalinux" ]]; then
if [[ ${os_version} -lt 9 ]]; then
echo -e "${red} Please use AlmaLinux 9 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "rocky" ]]; then
if [[ ${os_version} -lt 9 ]]; then
echo -e "${red} Please use Rocky Linux 9 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "oracle" ]]; then
if [[ ${os_version} -lt 8 ]]; then
echo -e "${red} Please use Oracle Linux 8 or higher ${plain}\n" && exit 1
fi
else
echo -e "${red}Your operating system is not supported by this script.${plain}\n"
echo "Please ensure you are using one of the following supported operating systems:"
echo "- Ubuntu 20.04+"
echo "- Debian 11+"
echo "- CentOS 8+"
echo "- Fedora 36+"
echo "- Arch Linux"
echo "- Parch Linux"
echo "- Manjaro"
echo "- Armbian"
echo "- AlmaLinux 9+"
echo "- Rocky Linux 9+"
echo "- Oracle Linux 8+"
echo "- OpenSUSE Tumbleweed"
exit 1
fi fi
confirm() { confirm() {
@@ -147,6 +184,7 @@ uninstall() {
systemctl stop sing-box systemctl stop sing-box
systemctl disable sing-box systemctl disable sing-box
rm /etc/systemd/system/s-ui.service -f rm /etc/systemd/system/s-ui.service -f
rm /etc/systemd/system/sing-box.service -f
systemctl daemon-reload systemctl daemon-reload
systemctl reset-failed systemctl reset-failed
rm /etc/s-ui/ -rf rm /etc/s-ui/ -rf
@@ -398,9 +436,9 @@ show_status() {
show_enable_status() { show_enable_status() {
check_enabled $1 check_enabled $1
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
echo -e "Start automatically: ${green}Yes${plain}" echo -e "Start ${1} automatically: ${green}Yes${plain}"
else else
echo -e "Start automatically: ${red}No${plain}" echo -e "Start ${1} automatically: ${red}No${plain}"
fi fi
} }
@@ -471,15 +509,18 @@ enable_bbr() {
# Check the OS and install necessary packages # Check the OS and install necessary packages
case "${release}" in case "${release}" in
ubuntu | debian) ubuntu | debian | armbian)
apt-get update && apt-get install -yqq --no-install-recommends ca-certificates apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
;; ;;
centos | almalinux | rocky) centos | almalinux | rocky | oracle)
yum -y update && yum -y install ca-certificates yum -y update && yum -y install ca-certificates
;; ;;
fedora) fedora)
dnf -y update && dnf -y install ca-certificates dnf -y update && dnf -y install ca-certificates
;; ;;
arch | manjaro | parch)
pacman -Sy --noconfirm ca-certificates
;;
*) *)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1 exit 1
@@ -547,15 +588,22 @@ ssl_cert_issue() {
fi fi
# install socat second # install socat second
case "${release}" in case "${release}" in
ubuntu|debian) ubuntu | debian | armbian)
apt update && apt install socat -y ;; apt update && apt install socat -y
centos) ;;
yum -y update && yum -y install socat ;; centos | almalinux | rocky | oracle)
fedora) yum -y update && yum -y install socat
dnf -y update && dnf -y install socat ;; ;;
*) fedora)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n" dnf -y update && dnf -y install socat
exit 1 ;; ;;
arch | manjaro | parch)
pacman -Sy --noconfirm socat
;;
*)
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
exit 1
;;
esac esac
if [ $? -ne 0 ]; then if [ $? -ne 0 ]; then
LOGE "install socat failed, please check logs" LOGE "install socat failed, please check logs"