Compare commits

..

63 Commits

Author SHA1 Message Date
Alireza Ahmadi 66c3f142a7 v1.2.1 2025-02-10 00:45:03 +01:00
Alireza Ahmadi e197a7081b fix exceptions for outjson #459 2025-02-09 14:27:27 +01:00
Alireza Ahmadi 875c660fb2 fix vless flow in reality links #463 2025-02-09 02:17:41 +01:00
Alireza Ahmadi f233f1c6b6 fix utl, flow and insecure in links #463 #445 2025-02-09 02:16:26 +01:00
Alireza Ahmadi 1c4a927e0d small fixes and typos 2025-02-09 00:56:13 +01:00
Alireza Ahmadi ea6ceac2f2 fix backup empty tables 2025-02-09 00:00:33 +01:00
Alireza Ahmadi 99d3cc5c6d Merge pull request #452 from alireza0/dependabot/go_modules/github.com/sagernet/sing-box-1.11.1
Bump github.com/sagernet/sing-box from 1.11.0 to 1.11.1
2025-02-08 17:46:48 +01:00
dependabot[bot] 95855092fd Bump github.com/sagernet/sing-box from 1.11.0 to 1.11.1
Bumps [github.com/sagernet/sing-box](https://github.com/sagernet/sing-box) from 1.11.0 to 1.11.1.
- [Release notes](https://github.com/sagernet/sing-box/releases)
- [Changelog](https://github.com/SagerNet/sing-box/blob/dev-next/docs/changelog.md)
- [Commits](https://github.com/sagernet/sing-box/compare/v1.11.0...v1.11.1)

---
updated-dependencies:
- dependency-name: github.com/sagernet/sing-box
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-08 16:46:22 +00:00
Alireza Ahmadi b1d3cfab1c Merge pull request #456 from alireza0/dependabot/go_modules/github.com/sagernet/sing-dns-0.4.0
Bump github.com/sagernet/sing-dns from 0.4.0-beta.2 to 0.4.0
2025-02-08 17:45:16 +01:00
Alireza Ahmadi 4af00b560f Merge pull request #457 from alireza0/dependabot/go_modules/github.com/sagernet/sing-0.6.0
Bump github.com/sagernet/sing from 0.6.0-beta.12 to 0.6.0
2025-02-08 17:45:06 +01:00
Alireza Ahmadi 1ccbbf14dc fix tls override in json sub 2025-02-08 17:23:49 +01:00
dependabot[bot] 917c2aa734 Bump github.com/sagernet/sing from 0.6.0-beta.12 to 0.6.0
Bumps [github.com/sagernet/sing](https://github.com/sagernet/sing) from 0.6.0-beta.12 to 0.6.0.
- [Commits](https://github.com/sagernet/sing/compare/v0.6.0-beta.12...v0.6.0)

---
updated-dependencies:
- dependency-name: github.com/sagernet/sing
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-06 16:52:56 +00:00
dependabot[bot] 17fe80bd9b Bump github.com/sagernet/sing-dns from 0.4.0-beta.2 to 0.4.0
Bumps [github.com/sagernet/sing-dns](https://github.com/sagernet/sing-dns) from 0.4.0-beta.2 to 0.4.0.
- [Commits](https://github.com/sagernet/sing-dns/compare/v0.4.0-beta.2...v0.4.0)

---
updated-dependencies:
- dependency-name: github.com/sagernet/sing-dns
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-06 16:52:52 +00:00
Alireza Ahmadi f0a7481d72 fix empty outJson error 2025-02-06 11:48:41 +01:00
Alireza Ahmadi 4b1654e3eb fix docker pipeline 2025-02-06 11:48:08 +01:00
Alireza Ahmadi 6304f4b263 v1.2.0 2025-02-05 08:57:15 +01:00
Alireza Ahmadi ba225aedc7 Merge pull request #442 from alireza0/dependabot/go_modules/github.com/shirou/gopsutil/v4-4.25.1
Bump github.com/shirou/gopsutil/v4 from 4.24.12 to 4.25.1
2025-02-04 23:34:31 +01:00
Alireza Ahmadi e4e692abdd Merge pull request #444 from alireza0/dependabot/go_modules/github.com/gin-contrib/gzip-1.2.2
Bump github.com/gin-contrib/gzip from 1.0.1 to 1.2.2
2025-02-04 23:34:17 +01:00
dependabot[bot] b3c26a2af2 Bump github.com/gin-contrib/gzip from 1.0.1 to 1.2.2
Bumps [github.com/gin-contrib/gzip](https://github.com/gin-contrib/gzip) from 1.0.1 to 1.2.2.
- [Release notes](https://github.com/gin-contrib/gzip/releases)
- [Changelog](https://github.com/gin-contrib/gzip/blob/master/.goreleaser.yaml)
- [Commits](https://github.com/gin-contrib/gzip/compare/v1.0.1...v1.2.2)

---
updated-dependencies:
- dependency-name: github.com/gin-contrib/gzip
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-04 22:33:07 +00:00
Alireza Ahmadi bf0df2f625 Merge pull request #443 from alireza0/dependabot/go_modules/github.com/gin-contrib/sessions-1.0.2
Bump github.com/gin-contrib/sessions from 1.0.1 to 1.0.2
2025-02-04 23:31:57 +01:00
Alireza Ahmadi 7af80ae577 Merge pull request #440 from alireza0/dependabot/github_actions/actions/checkout-4.2.2
Bump actions/checkout from 4.1.1 to 4.2.2
2025-02-04 23:31:37 +01:00
dependabot[bot] 1a0c01c092 Bump github.com/gin-contrib/sessions from 1.0.1 to 1.0.2
Bumps [github.com/gin-contrib/sessions](https://github.com/gin-contrib/sessions) from 1.0.1 to 1.0.2.
- [Release notes](https://github.com/gin-contrib/sessions/releases)
- [Changelog](https://github.com/gin-contrib/sessions/blob/master/.goreleaser.yaml)
- [Commits](https://github.com/gin-contrib/sessions/compare/v1.0.1...v1.0.2)

---
updated-dependencies:
- dependency-name: github.com/gin-contrib/sessions
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-04 22:31:37 +00:00
dependabot[bot] 0e82b0442a Bump github.com/shirou/gopsutil/v4 from 4.24.12 to 4.25.1
Bumps [github.com/shirou/gopsutil/v4](https://github.com/shirou/gopsutil) from 4.24.12 to 4.25.1.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v4.24.12...v4.25.1)

---
updated-dependencies:
- dependency-name: github.com/shirou/gopsutil/v4
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-04 22:31:33 +00:00
dependabot[bot] 11970cb514 Bump actions/checkout from 4.1.1 to 4.2.2
Bumps [actions/checkout](https://github.com/actions/checkout) from 4.1.1 to 4.2.2.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v4.1.1...v4.2.2)

---
updated-dependencies:
- dependency-name: actions/checkout
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2025-02-04 22:30:59 +00:00
Alireza Ahmadi 928fc228d4 Merge pull request #439 from alireza0/consolidation
Consolidation
2025-02-04 23:30:21 +01:00
Alireza Ahmadi 840a28e072 v1.2.0-rc0 2025-02-03 01:37:39 +01:00
Alireza Ahmadi 29283ce83b sing-box v1.11.0 2025-02-02 22:54:10 +01:00
Alireza Ahmadi 3fd2cf05bb api docs point to wiki 2025-02-02 22:49:50 +01:00
Alireza Ahmadi 1ac7bce6b4 new api with token 2025-02-02 21:46:36 +01:00
Alireza Ahmadi 4865072f55 fix ss link #443 2025-02-02 13:59:28 +01:00
Alireza Ahmadi 7faa28a89d separate frontend repository 2025-01-29 00:08:36 +01:00
Alireza Ahmadi f4b1b09362 fix backup 2025-01-28 20:22:11 +01:00
Alireza Ahmadi c97467da3c fix vmess link #411 2025-01-28 19:42:34 +01:00
Alireza Ahmadi cde8eb100e save item per page in browser 2025-01-26 17:00:54 +01:00
Alireza Ahmadi 20520552c2 desc on client table #421 2025-01-26 16:58:51 +01:00
Alireza Ahmadi e380cb888f fix restart inbounds after commit db changes 2025-01-22 22:13:54 +01:00
Alireza Ahmadi 24a3e7b0da small fixes 2025-01-22 00:45:10 +01:00
Alireza Ahmadi b6c16e1bdf fix inbound users 2025-01-22 00:44:52 +01:00
Alireza Ahmadi 5da87cf14d v1.2.0-beta.3 2025-01-20 01:39:05 +01:00
Alireza Ahmadi 1edd9d4323 upgrade backend dependencies 2025-01-20 01:28:32 +01:00
Alireza Ahmadi ce968d20ac upgrade frontend dependencies 2025-01-20 01:28:17 +01:00
Alireza Ahmadi 60c64bab18 small fixes 2025-01-20 01:27:37 +01:00
Alireza Ahmadi f3432b119c option backup restore #238 2025-01-20 01:26:02 +01:00
Alireza Ahmadi 049cfc5287 clean up changes 2025-01-19 19:43:19 +01:00
Alireza Ahmadi 891a61ac16 small fixes 2025-01-19 02:26:10 +01:00
Alireza Ahmadi a0dc165995 option warp endpoint #345 2025-01-19 01:52:25 +01:00
Alireza Ahmadi 4dc02f783a skeleton for stats 2025-01-18 16:04:00 +01:00
Alireza Ahmadi 1282d67640 qrcode loading data 2025-01-18 16:03:16 +01:00
Alireza Ahmadi f116e7f5ea option all inbounds for client modal #399 2025-01-18 12:28:35 +01:00
Alireza Ahmadi d06b6be4a2 init users on create inbound #411 2025-01-18 10:55:22 +01:00
Alireza Ahmadi b43a6ade97 shadowtls users only in v3 2025-01-18 10:34:42 +01:00
Alireza Ahmadi f18345b30d only table in clients page & better load and save 2025-01-18 10:28:04 +01:00
Alireza Ahmadi 67582015d3 tls fix and migration patch 2025-01-11 12:32:52 +01:00
Alireza Ahmadi 94473b40de fix errors in UI 2025-01-11 11:26:32 +01:00
Alireza Ahmadi 7e41af0da8 fix tls in link and json 2025-01-11 11:25:40 +01:00
Alireza Ahmadi 88adcc7c9a update default config 2025-01-11 11:24:45 +01:00
Alireza Ahmadi 8fb5d3964a v1.2.0-beta.2 2025-01-07 23:55:28 +01:00
Alireza Ahmadi 3d92431269 upgrade to sing-box v1.11.0-beta.20 2025-01-07 23:50:37 +01:00
Alireza Ahmadi f06840eaea fix scripts 2025-01-07 23:41:38 +01:00
Alireza Ahmadi 906769e21a fix hysteria2 masquerade options 2025-01-07 23:37:06 +01:00
Alireza Ahmadi 3641bbe25f fix wireguard and peer data types 2025-01-07 22:22:19 +01:00
Alireza Ahmadi a39a669d75 small fixes 2025-01-07 19:27:26 +01:00
Alireza Ahmadi 9f6771ec09 fix save settings 2025-01-07 19:26:47 +01:00
198 changed files with 1729 additions and 19382 deletions
+1
View File
@@ -3,6 +3,7 @@ dist/
release/ release/
backup/ backup/
bin/ bin/
db/
sui sui
web/html web/html
main main
-4
View File
@@ -1,9 +1,5 @@
version: 2 version: 2
updates: updates:
- package-ecosystem: "npm"
directory: "/"
schedule:
interval: "daily"
- package-ecosystem: "gomod" - package-ecosystem: "gomod"
directory: "/" directory: "/"
schedule: schedule:
+5 -2
View File
@@ -7,10 +7,13 @@ on:
jobs: jobs:
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-22.04
steps: steps:
- uses: actions/checkout@v4 - name: Checkout repository
uses: actions/checkout@v4.2.2
with:
submodules: recursive
- name: Docker meta - name: Docker meta
id: meta id: meta
+7 -6
View File
@@ -21,13 +21,15 @@ jobs:
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- name: Checkout repository - name: Checkout repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.2.2
with:
submodules: recursive
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5 uses: actions/setup-go@v5
with: with:
cache: false cache: false
go-version-file: backend/go.mod go-version-file: go.mod
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -58,7 +60,8 @@ jobs:
npm install npm install
npm run build npm run build
cd .. cd ..
mv frontend/dist backend/web/html mv frontend/dist web/html
rm -fr frontend
- name: Build s-ui - name: Build s-ui
run: | run: |
@@ -89,9 +92,7 @@ jobs:
fi fi
### Build s-ui ### Build s-ui
cd backend go build -ldflags="-w -s" -tags "with_quic,with_grpc,with_ech,with_utls,with_reality_server,with_acme,with_gvisor" -o sui main.go
go build -ldflags="-w -s" -tags "with_quic,with_grpc,with_ech,with_utls,with_reality_server,with_acme,with_gvisor" -o ../sui main.go
cd ..
mkdir s-ui mkdir s-ui
cp sui s-ui/ cp sui s-ui/
+2
View File
@@ -5,10 +5,12 @@ backup/
bin/ bin/
db/ db/
sui sui
web/html
main main
tmp tmp
.sync* .sync*
*.tar.gz *.tar.gz
frontend/
# local env files # local env files
.env.local .env.local
+4
View File
@@ -0,0 +1,4 @@
[submodule "frontend"]
path = frontend
url = https://github.com/alireza0/s-ui-frontend
branch = main
+1 -1
View File
@@ -10,7 +10,7 @@ ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
ENV CGO_ENABLED=1 ENV CGO_ENABLED=1
ENV GOARCH=$TARGETARCH ENV GOARCH=$TARGETARCH
RUN apk update && apk --no-cache --update add build-base gcc wget unzip RUN apk update && apk --no-cache --update add build-base gcc wget unzip
COPY backend/ ./ COPY . .
COPY --from=front-builder /app/dist/ /app/web/html/ COPY --from=front-builder /app/dist/ /app/web/html/
RUN go build -ldflags="-w -s" -tags "with_quic,with_grpc,with_ech,with_utls,with_reality_server,with_acme,with_gvisor" -o sui main.go RUN go build -ldflags="-w -s" -tags "with_quic,with_grpc,with_ech,with_utls,with_reality_server,with_acme,with_gvisor" -o sui main.go
+21 -25
View File
@@ -3,7 +3,7 @@
![](https://img.shields.io/github/v/release/alireza0/s-ui.svg) ![](https://img.shields.io/github/v/release/alireza0/s-ui.svg)
![S-UI Docker pull](https://img.shields.io/docker/pulls/alireza7/s-ui.svg) ![S-UI Docker pull](https://img.shields.io/docker/pulls/alireza7/s-ui.svg)
![S-UI-Singbox Docker pull](https://img.shields.io/docker/pulls/alireza7/s-ui-singbox.svg) [![Go Report Card](https://goreportcard.com/badge/github.com/alireza0/s-ui)](https://goreportcard.com/report/github.com/alireza0/s-ui)
[![Downloads](https://img.shields.io/github/downloads/alireza0/s-ui/total.svg)](https://img.shields.io/github/downloads/alireza0/s-ui/total.svg) [![Downloads](https://img.shields.io/github/downloads/alireza0/s-ui/total.svg)](https://img.shields.io/github/downloads/alireza0/s-ui/total.svg)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
@@ -25,7 +25,11 @@
| Client & Traffic & System Status | :heavy_check_mark: | | Client & Traffic & System Status | :heavy_check_mark: |
| Subscription Service (link/json + info)| :heavy_check_mark: | | Subscription Service (link/json + info)| :heavy_check_mark: |
| Dark/Light Theme | :heavy_check_mark: | | Dark/Light Theme | :heavy_check_mark: |
| API Interface | :heavy_check_mark: |
## API Documentation
[API-Documentation Wiki](https://github.com/alireza0/s-ui/wiki/API-Documentation)
## Default Installation Information ## Default Installation Information
- Panel Port: 2095 - Panel Port: 2095
@@ -63,16 +67,13 @@ VERSION=1.0.0 && bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui
```sh ```sh
sudo -i sudo -i
systemctl disable sing-box --now
systemctl disable s-ui --now systemctl disable s-ui --now
rm -f /etc/systemd/system/s-ui.service
rm -f /etc/systemd/system/sing-box.service rm -f /etc/systemd/system/sing-box.service
systemctl daemon-reload systemctl daemon-reload
rm -fr /usr/local/s-ui rm -fr /usr/local/s-ui
rm /usr/bin/s-ui rm /usr/bin/s-ui
``` ```
## Install using Docker ## Install using Docker
@@ -98,7 +99,7 @@ wget -q https://raw.githubusercontent.com/alireza0/s-ui/master/docker-compose.ym
docker compose up -d docker compose up -d
``` ```
> Use docker for s-ui only > Use docker
```shell ```shell
mkdir s-ui && cd s-ui mkdir s-ui && cd s-ui
@@ -113,6 +114,8 @@ docker run -itd \
> Build your own image > Build your own image
```shell ```shell
git clone https://github.com/alireza0/s-ui
git submodule update --init --recursive
docker build -t s-ui . docker build -t s-ui .
``` ```
@@ -128,37 +131,30 @@ docker build -t s-ui .
./runSUI.sh ./runSUI.sh
``` ```
### Clone the repository
```shell
# clone repository
git clone https://github.com/alireza0/s-ui
# clone submodules
git submodule update --init --recursive
```
### - Frontend ### - Frontend
Frontend codes are in `frontend` folder in the root of repository. Visit [s-ui-frontend](https://github.com/alireza0/s-ui-frontend) for frontend code
To run it locally for instant development 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
Backend codes are in `backend` folder in the root of repository.
> Please build frontend once before! > Please build frontend once before!
To build backend: To build backend:
```shell ```shell
cd backend
# remove old frontend compiled files # remove old frontend compiled files
rm -fr web/html/* rm -fr web/html/*
# apply new frontend compiled files # apply new frontend compiled files
cp -R ../frontend/dist/ web/html/ cp -R frontend/dist/ web/html/
# build # build
go build -o ../sui main.go go build -o sui main.go
``` ```
To run backend (from root folder of repository): To run backend (from root folder of repository):
@@ -182,7 +178,7 @@ To run backend (from root folder of repository):
- Supported protocols: - Supported protocols:
- General: Mixed, SOCKS, HTTP, HTTPS, Direct, Redirect, TProxy - General: Mixed, SOCKS, HTTP, HTTPS, Direct, Redirect, TProxy
- V2Ray based: VLESS, VMess, Trojan, Shadowsocks - V2Ray based: VLESS, VMess, Trojan, Shadowsocks
- Other protocols: ShadowTLS, Hysteria, Hysteri2, Naive, TUIC - Other protocols: ShadowTLS, Hysteria, Hysteria2, Naive, TUIC
- Supports XTLS protocols - Supports XTLS protocols
- An advanced interface for routing traffic, incorporating PROXY Protocol, External, and Transparent Proxy, SSL Certificate, and Port - An advanced interface for routing traffic, incorporating PROXY Protocol, External, and Transparent Proxy, SSL Certificate, and Port
- An advanced interface for inbound and outbound configuration - An advanced interface for inbound and outbound configuration
+100
View File
@@ -0,0 +1,100 @@
package api
import (
"s-ui/util/common"
"strings"
"github.com/gin-gonic/gin"
)
type APIHandler struct {
ApiService
apiv2 *APIv2Handler
}
func NewAPIHandler(g *gin.RouterGroup, a2 *APIv2Handler) {
a := &APIHandler{
apiv2: a2,
}
a.initRouter(g)
}
func (a *APIHandler) initRouter(g *gin.RouterGroup) {
g.Use(func(c *gin.Context) {
path := c.Request.URL.Path
if !strings.HasSuffix(path, "login") && !strings.HasSuffix(path, "logout") {
checkLogin(c)
}
})
g.POST("/:postAction", a.postHandler)
g.GET("/:getAction", a.getHandler)
}
func (a *APIHandler) postHandler(c *gin.Context) {
loginUser := GetLoginUser(c)
action := c.Param("postAction")
switch action {
case "login":
a.ApiService.Login(c)
case "changePass":
a.ApiService.ChangePass(c)
case "save":
a.ApiService.Save(c, loginUser)
case "restartApp":
a.ApiService.RestartApp(c)
case "restartSb":
a.ApiService.RestartSb(c)
case "linkConvert":
a.ApiService.LinkConvert(c)
case "importdb":
a.ApiService.ImportDb(c)
case "addToken":
a.ApiService.AddToken(c)
a.apiv2.ReloadTokens()
case "deleteToken":
a.ApiService.DeleteToken(c)
a.apiv2.ReloadTokens()
default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
}
func (a *APIHandler) getHandler(c *gin.Context) {
action := c.Param("getAction")
switch action {
case "logout":
a.ApiService.Logout(c)
case "load":
a.ApiService.LoadData(c)
case "inbounds", "outbounds", "endpoints", "tls", "clients", "config":
err := a.ApiService.LoadPartialData(c, []string{action})
if err != nil {
jsonMsg(c, action, err)
}
return
case "users":
a.ApiService.GetUsers(c)
case "settings":
a.ApiService.GetSettings(c)
case "stats":
a.ApiService.GetStats(c)
case "status":
a.ApiService.GetStatus(c)
case "onlines":
a.ApiService.GetOnlines(c)
case "logs":
a.ApiService.GetLogs(c)
case "changes":
a.ApiService.CheckChanges(c)
case "keypairs":
a.ApiService.GetKeypairs(c)
case "getdb":
a.ApiService.GetDb(c)
case "tokens":
a.ApiService.GetTokens(c)
default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
}
+363
View File
@@ -0,0 +1,363 @@
package api
import (
"encoding/json"
"s-ui/database"
"s-ui/logger"
"s-ui/service"
"s-ui/util"
"strconv"
"strings"
"time"
"github.com/gin-gonic/gin"
)
type ApiService struct {
service.SettingService
service.UserService
service.ConfigService
service.ClientService
service.TlsService
service.InboundService
service.OutboundService
service.EndpointService
service.PanelService
service.StatsService
service.ServerService
}
func (a *ApiService) LoadData(c *gin.Context) {
data, err := a.getData(c)
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, data, nil)
}
func (a *ApiService) getData(c *gin.Context) (interface{}, error) {
data := make(map[string]interface{}, 0)
lu := c.Query("lu")
isUpdated, err := a.ConfigService.CheckChanges(lu)
if err != nil {
return "", err
}
onlines, err := a.StatsService.GetOnlines()
sysInfo := a.ServerService.GetSingboxInfo()
if sysInfo["running"] == false {
logs := a.ServerService.GetLogs("1", "debug")
if len(logs) > 0 {
data["lastLog"] = logs[0]
}
}
if err != nil {
return "", err
}
if isUpdated {
config, err := a.SettingService.GetConfig()
if err != nil {
return "", err
}
clients, err := a.ClientService.GetAll()
if err != nil {
return "", err
}
tlsConfigs, err := a.TlsService.GetAll()
if err != nil {
return "", err
}
inbounds, err := a.InboundService.GetAll()
if err != nil {
return "", err
}
outbounds, err := a.OutboundService.GetAll()
if err != nil {
return "", err
}
endpoints, err := a.EndpointService.GetAll()
if err != nil {
return "", err
}
subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0])
if err != nil {
return "", err
}
data["config"] = json.RawMessage(config)
data["clients"] = clients
data["tls"] = tlsConfigs
data["inbounds"] = inbounds
data["outbounds"] = outbounds
data["endpoints"] = endpoints
data["subURI"] = subURI
data["onlines"] = onlines
} else {
data["onlines"] = onlines
}
return data, nil
}
func (a *ApiService) LoadPartialData(c *gin.Context, objs []string) error {
data := make(map[string]interface{}, 0)
id := c.Query("id")
for _, obj := range objs {
switch obj {
case "inbounds":
inbounds, err := a.InboundService.Get(id)
if err != nil {
return err
}
data[obj] = inbounds
case "outbounds":
outbounds, err := a.OutboundService.GetAll()
if err != nil {
return err
}
data[obj] = outbounds
case "endpoints":
endpoints, err := a.EndpointService.GetAll()
if err != nil {
return err
}
data[obj] = endpoints
case "tls":
tlsConfigs, err := a.TlsService.GetAll()
if err != nil {
return err
}
data[obj] = tlsConfigs
case "clients":
clients, err := a.ClientService.Get(id)
if err != nil {
return err
}
data[obj] = clients
case "config":
config, err := a.SettingService.GetConfig()
if err != nil {
return err
}
data[obj] = json.RawMessage(config)
case "settings":
settings, err := a.SettingService.GetAllSetting()
if err != nil {
return err
}
data[obj] = settings
}
}
jsonObj(c, data, nil)
return nil
}
func (a *ApiService) GetUsers(c *gin.Context) {
users, err := a.UserService.GetUsers()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, *users, nil)
}
func (a *ApiService) GetSettings(c *gin.Context) {
data, err := a.SettingService.GetAllSetting()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, data, err)
}
func (a *ApiService) GetStats(c *gin.Context) {
resource := c.Query("resource")
tag := c.Query("tag")
limit, err := strconv.Atoi(c.Query("limit"))
if err != nil {
limit = 100
}
data, err := a.StatsService.GetStats(resource, tag, limit)
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, data, err)
}
func (a *ApiService) GetStatus(c *gin.Context) {
request := c.Query("r")
result := a.ServerService.GetStatus(request)
jsonObj(c, result, nil)
}
func (a *ApiService) GetOnlines(c *gin.Context) {
onlines, err := a.StatsService.GetOnlines()
jsonObj(c, onlines, err)
}
func (a *ApiService) GetLogs(c *gin.Context) {
count := c.Query("c")
level := c.Query("l")
logs := a.ServerService.GetLogs(count, level)
jsonObj(c, logs, nil)
}
func (a *ApiService) CheckChanges(c *gin.Context) {
actor := c.Query("a")
chngKey := c.Query("k")
count := c.Query("c")
changes := a.ConfigService.GetChanges(actor, chngKey, count)
jsonObj(c, changes, nil)
}
func (a *ApiService) GetKeypairs(c *gin.Context) {
kType := c.Query("k")
options := c.Query("o")
keypair := a.ServerService.GenKeypair(kType, options)
jsonObj(c, keypair, nil)
}
func (a *ApiService) GetDb(c *gin.Context) {
exclude := c.Query("exclude")
db, err := database.GetDb(exclude)
if err != nil {
jsonMsg(c, "", err)
return
}
c.Header("Content-Type", "application/octet-stream")
c.Header("Content-Disposition", "attachment; filename=s-ui_"+time.Now().Format("20060102-150405")+".db")
c.Writer.Write(db)
}
func (a *ApiService) postActions(c *gin.Context) (string, json.RawMessage, error) {
var data map[string]json.RawMessage
err := c.ShouldBind(&data)
if err != nil {
return "", nil, err
}
return string(data["action"]), data["data"], nil
}
func (a *ApiService) Login(c *gin.Context) {
remoteIP := getRemoteIp(c)
loginUser, err := a.UserService.Login(c.Request.FormValue("user"), c.Request.FormValue("pass"), remoteIP)
if err != nil {
jsonMsg(c, "", err)
return
}
sessionMaxAge, err := a.SettingService.GetSessionMaxAge()
if err != nil {
logger.Infof("Unable to get session's max age from DB")
}
err = SetLoginUser(c, loginUser, sessionMaxAge)
if err == nil {
logger.Info("user ", loginUser, " login success")
} else {
logger.Warning("login failed: ", err)
}
jsonMsg(c, "", nil)
}
func (a *ApiService) ChangePass(c *gin.Context) {
id := c.Request.FormValue("id")
oldPass := c.Request.FormValue("oldPass")
newUsername := c.Request.FormValue("newUsername")
newPass := c.Request.FormValue("newPass")
err := a.UserService.ChangePass(id, oldPass, newUsername, newPass)
if err == nil {
logger.Info("change user credentials success")
jsonMsg(c, "save", nil)
} else {
logger.Warning("change user credentials failed:", err)
jsonMsg(c, "", err)
}
}
func (a *ApiService) Save(c *gin.Context, loginUser string) {
hostname := getHostname(c)
obj := c.Request.FormValue("object")
act := c.Request.FormValue("action")
data := c.Request.FormValue("data")
initUsers := c.Request.FormValue("initUsers")
objs, err := a.ConfigService.Save(obj, act, json.RawMessage(data), initUsers, loginUser, hostname)
if err != nil {
jsonMsg(c, "save", err)
return
}
err = a.LoadPartialData(c, objs)
if err != nil {
jsonMsg(c, obj, err)
}
}
func (a *ApiService) RestartApp(c *gin.Context) {
err := a.PanelService.RestartPanel(3)
jsonMsg(c, "restartApp", err)
}
func (a *ApiService) RestartSb(c *gin.Context) {
err := a.ConfigService.RestartCore()
jsonMsg(c, "restartSb", err)
}
func (a *ApiService) LinkConvert(c *gin.Context) {
link := c.Request.FormValue("link")
result, _, err := util.GetOutbound(link, 0)
jsonObj(c, result, err)
}
func (a *ApiService) ImportDb(c *gin.Context) {
file, _, err := c.Request.FormFile("db")
if err != nil {
jsonMsg(c, "", err)
return
}
defer file.Close()
err = database.ImportDB(file)
jsonMsg(c, "", err)
}
func (a *ApiService) Logout(c *gin.Context) {
loginUser := GetLoginUser(c)
if loginUser != "" {
logger.Infof("user %s logout", loginUser)
}
ClearSession(c)
jsonMsg(c, "", nil)
}
func (a *ApiService) LoadTokens() ([]byte, error) {
return a.UserService.LoadTokens()
}
func (a *ApiService) GetTokens(c *gin.Context) {
loginUser := GetLoginUser(c)
tokens, err := a.UserService.GetUserTokens(loginUser)
jsonObj(c, tokens, err)
}
func (a *ApiService) AddToken(c *gin.Context) {
loginUser := GetLoginUser(c)
expiry := c.Request.FormValue("expiry")
expiryInt, err := strconv.ParseInt(expiry, 10, 64)
if err != nil {
jsonMsg(c, "", err)
return
}
desc := c.Request.FormValue("desc")
token, err := a.UserService.AddToken(loginUser, expiryInt, desc)
jsonObj(c, token, err)
}
func (a *ApiService) DeleteToken(c *gin.Context) {
tokenId := c.Request.FormValue("id")
err := a.UserService.DeleteToken(tokenId)
jsonMsg(c, "", err)
}
+129
View File
@@ -0,0 +1,129 @@
package api
import (
"encoding/json"
"s-ui/logger"
"s-ui/util/common"
"time"
"github.com/gin-gonic/gin"
)
type TokenInMemory struct {
Token string
Expiry int64
Username string
}
type APIv2Handler struct {
ApiService
tokens *[]TokenInMemory
}
func NewAPIv2Handler(g *gin.RouterGroup) *APIv2Handler {
a := &APIv2Handler{}
a.ReloadTokens()
a.initRouter(g)
return a
}
func (a *APIv2Handler) initRouter(g *gin.RouterGroup) {
g.Use(func(c *gin.Context) {
a.checkToken(c)
})
g.POST("/:postAction", a.postHandler)
g.GET("/:getAction", a.getHandler)
}
func (a *APIv2Handler) postHandler(c *gin.Context) {
username := a.findUsername(c)
action := c.Param("postAction")
switch action {
case "save":
a.ApiService.Save(c, username)
case "restartApp":
a.ApiService.RestartApp(c)
case "restartSb":
a.ApiService.RestartSb(c)
case "linkConvert":
a.ApiService.LinkConvert(c)
case "importdb":
a.ApiService.ImportDb(c)
default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
}
func (a *APIv2Handler) getHandler(c *gin.Context) {
action := c.Param("getAction")
switch action {
case "load":
a.ApiService.LoadData(c)
case "inbounds", "outbounds", "endpoints", "tls", "clients", "config":
err := a.ApiService.LoadPartialData(c, []string{action})
if err != nil {
jsonMsg(c, action, err)
}
return
case "users":
a.ApiService.GetUsers(c)
case "settings":
a.ApiService.GetSettings(c)
case "stats":
a.ApiService.GetStats(c)
case "status":
a.ApiService.GetStatus(c)
case "onlines":
a.ApiService.GetOnlines(c)
case "logs":
a.ApiService.GetLogs(c)
case "changes":
a.ApiService.CheckChanges(c)
case "keypairs":
a.ApiService.GetKeypairs(c)
case "getdb":
a.ApiService.GetDb(c)
default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
}
func (a *APIv2Handler) findUsername(c *gin.Context) string {
token := c.Request.Header.Get("Token")
for index, t := range *a.tokens {
if t.Expiry > 0 && t.Expiry < time.Now().Unix() {
(*a.tokens) = append((*a.tokens)[:index], (*a.tokens)[index+1:]...)
continue
}
if t.Token == token {
return t.Username
}
}
return ""
}
func (a *APIv2Handler) checkToken(c *gin.Context) {
username := a.findUsername(c)
if username != "" {
c.Next()
return
}
jsonMsg(c, "", common.NewError("invalid token"))
c.Abort()
}
func (a *APIv2Handler) ReloadTokens() {
tokens, err := a.ApiService.LoadTokens()
if err == nil {
var newTokens []TokenInMemory
err = json.Unmarshal(tokens, &newTokens)
if err != nil {
logger.Error("unable to load tokens: ", err)
}
a.tokens = &newTokens
} else {
logger.Error("unable to load tokens: ", err)
}
}
View File
-315
View File
@@ -1,315 +0,0 @@
package api
import (
"encoding/json"
"s-ui/logger"
"s-ui/service"
"s-ui/util"
"s-ui/util/common"
"strconv"
"strings"
"github.com/gin-gonic/gin"
)
type APIHandler struct {
service.SettingService
service.UserService
service.ConfigService
service.ClientService
service.TlsService
service.InboundService
service.OutboundService
service.EndpointService
service.PanelService
service.StatsService
service.ServerService
}
func NewAPIHandler(g *gin.RouterGroup) {
a := &APIHandler{}
a.initRouter(g)
}
func (a *APIHandler) initRouter(g *gin.RouterGroup) {
g.Use(func(c *gin.Context) {
path := c.Request.URL.Path
if !strings.HasSuffix(path, "login") && !strings.HasSuffix(path, "logout") {
checkLogin(c)
}
})
g.POST("/:postAction", a.postHandler)
g.GET("/:getAction", a.getHandler)
}
func (a *APIHandler) postHandler(c *gin.Context) {
var err error
action := c.Param("postAction")
remoteIP := getRemoteIp(c)
loginUser := GetLoginUser(c)
hostname := getHostname(c)
switch action {
case "login":
loginUser, err := a.UserService.Login(c.Request.FormValue("user"), c.Request.FormValue("pass"), remoteIP)
if err != nil {
jsonMsg(c, "", err)
return
}
sessionMaxAge, err := a.SettingService.GetSessionMaxAge()
if err != nil {
logger.Infof("Unable to get session's max age from DB")
}
err = SetLoginUser(c, loginUser, sessionMaxAge)
if err == nil {
logger.Info("user ", loginUser, " login success")
} else {
logger.Warning("login failed: ", err)
}
jsonMsg(c, "", nil)
case "changePass":
id := c.Request.FormValue("id")
oldPass := c.Request.FormValue("oldPass")
newUsername := c.Request.FormValue("newUsername")
newPass := c.Request.FormValue("newPass")
err = a.UserService.ChangePass(id, oldPass, newUsername, newPass)
if err == nil {
logger.Info("change user credentials success")
jsonMsg(c, "save", nil)
} else {
logger.Warning("change user credentials failed:", err)
jsonMsg(c, "", err)
}
case "save":
obj := c.Request.FormValue("object")
act := c.Request.FormValue("action")
data := c.Request.FormValue("data")
objs, err := a.ConfigService.Save(obj, act, json.RawMessage(data), loginUser, hostname)
if err != nil {
jsonMsg(c, "save", err)
return
}
err = a.loadPartialData(c, objs)
if err != nil {
jsonMsg(c, obj, err)
}
return
case "restartApp":
err = a.PanelService.RestartPanel(3)
jsonMsg(c, "restartApp", err)
case "restartSb":
err = a.ConfigService.RestartCore()
jsonMsg(c, "restartSb", err)
case "linkConvert":
link := c.Request.FormValue("link")
result, _, err := util.GetOutbound(link, 0)
jsonObj(c, result, err)
default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
}
func (a *APIHandler) getHandler(c *gin.Context) {
action := c.Param("getAction")
switch action {
case "logout":
loginUser := GetLoginUser(c)
if loginUser != "" {
logger.Infof("user %s logout", loginUser)
}
ClearSession(c)
jsonMsg(c, "", nil)
case "load":
data, err := a.loadData(c)
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, data, nil)
case "inbounds", "outbounds", "endpoints", "tls", "clients", "config":
err := a.loadPartialData(c, []string{action})
if err != nil {
jsonMsg(c, action, err)
}
return
case "users":
users, err := a.UserService.GetUsers()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, *users, nil)
case "setting":
data, err := a.SettingService.GetAllSetting()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, data, err)
case "stats":
resource := c.Query("resource")
tag := c.Query("tag")
limit, err := strconv.Atoi(c.Query("limit"))
if err != nil {
limit = 100
}
data, err := a.StatsService.GetStats(resource, tag, limit)
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, data, err)
case "status":
request := c.Query("r")
result := a.ServerService.GetStatus(request)
jsonObj(c, result, nil)
case "onlines":
onlines, err := a.StatsService.GetOnlines()
jsonObj(c, onlines, err)
case "logs":
count := c.Query("c")
level := c.Query("l")
logs := a.ServerService.GetLogs(count, level)
jsonObj(c, logs, nil)
case "changes":
actor := c.Query("a")
chngKey := c.Query("k")
count := c.Query("c")
changes := a.ConfigService.GetChanges(actor, chngKey, count)
jsonObj(c, changes, nil)
case "keypairs":
kType := c.Query("k")
options := c.Query("o")
keypair := a.ServerService.GenKeypair(kType, options)
jsonObj(c, keypair, nil)
default:
jsonMsg(c, "failed", common.NewError("unknown action: ", action))
}
}
func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
data := make(map[string]interface{}, 0)
lu := c.Query("lu")
isUpdated, err := a.ConfigService.CheckChanges(lu)
if err != nil {
return "", err
}
onlines, err := a.StatsService.GetOnlines()
sysInfo := a.ServerService.GetSingboxInfo()
if sysInfo["running"] == false {
logs := a.ServerService.GetLogs("1", "debug")
if len(logs) > 0 {
data["lastLog"] = logs[0]
}
}
if err != nil {
return "", err
}
if isUpdated {
config, err := a.SettingService.GetConfig()
if err != nil {
return "", err
}
clients, err := a.ClientService.GetAll()
if err != nil {
return "", err
}
tlsConfigs, err := a.TlsService.GetAll()
if err != nil {
return "", err
}
inbounds, err := a.InboundService.GetAll()
if err != nil {
return "", err
}
outbounds, err := a.OutboundService.GetAll()
if err != nil {
return "", err
}
endpoints, err := a.EndpointService.GetAll()
if err != nil {
return "", err
}
subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0])
if err != nil {
return "", err
}
data["config"] = json.RawMessage(config)
data["clients"] = clients
data["tls"] = tlsConfigs
data["inbounds"] = inbounds
data["outbounds"] = outbounds
data["endpoints"] = endpoints
data["subURI"] = subURI
data["onlines"] = onlines
} else {
data["onlines"] = onlines
}
return data, nil
}
func (a *APIHandler) loadPartialData(c *gin.Context, objs []string) error {
data := make(map[string]interface{}, 0)
for _, obj := range objs {
switch obj {
case "inbounds":
id := c.Query("id")
inbounds, err := a.InboundService.Get(id)
if err != nil {
return err
}
data[obj] = inbounds
case "outbounds":
outbounds, err := a.OutboundService.GetAll()
if err != nil {
return err
}
data[obj] = outbounds
case "endpoints":
endpoints, err := a.EndpointService.GetAll()
if err != nil {
return err
}
data[obj] = endpoints
case "tls":
tlsConfigs, err := a.TlsService.GetAll()
if err != nil {
return err
}
data[obj] = tlsConfigs
case "clients":
clients, err := a.ClientService.GetAll()
if err != nil {
return err
}
data[obj] = clients
case "config":
config, err := a.SettingService.GetConfig()
if err != nil {
return err
}
data[obj] = json.RawMessage(config)
}
}
jsonObj(c, data, nil)
return nil
}
func (a *APIHandler) postActions(c *gin.Context) (string, json.RawMessage, error) {
var data map[string]json.RawMessage
err := c.ShouldBind(&data)
if err != nil {
return "", nil, err
}
return string(data["action"]), data["data"], nil
}
-1
View File
@@ -1 +0,0 @@
1.2.0-beta.1
+2 -3
View File
@@ -5,11 +5,10 @@ npm i
npm run build npm run build
cd .. cd ..
cd backend
echo "Backend" echo "Backend"
mkdir -p web/html mkdir -p web/html
rm -fr web/html/* rm -fr web/html/*
cp -R ../frontend/dist/* web/html/ cp -R frontend/dist/* web/html/
go build -tags "with_quic,with_grpc,with_ech,with_utls,with_reality_server,with_acme,with_gvisor" -o ../sui main.go go build -ldflags "-w -s" -tags "with_quic,with_grpc,with_ech,with_utls,with_reality_server,with_acme,with_gvisor" -o sui main.go
View File
@@ -62,6 +62,10 @@ func migrateClientSchema(db *gorm.DB) error {
return nil return nil
} }
func deleteOldWebSecret(db *gorm.DB) error {
return db.Exec("DELETE FROM settings WHERE key = ?", "webSecret").Error
}
func changesObj(db *gorm.DB) error { func changesObj(db *gorm.DB) error {
return db.Exec("UPDATE changes SET obj = CAST('\"' || CAST(obj AS TEXT) || '\"' AS BLOB) WHERE actor = ? and obj not like ?", "DepleteJob", "\"%\"").Error return db.Exec("UPDATE changes SET obj = CAST('\"' || CAST(obj AS TEXT) || '\"' AS BLOB) WHERE actor = ? and obj not like ?", "DepleteJob", "\"%\"").Error
} }
@@ -71,6 +75,10 @@ func to1_1(db *gorm.DB) error {
if err != nil { if err != nil {
return err return err
} }
err = deleteOldWebSecret(db)
if err != nil {
return err
}
err = changesObj(db) err = changesObj(db)
if err != nil { if err != nil {
return err return err
@@ -60,22 +60,10 @@ func moveJsonToDb(db *gorm.DB) error {
} else { } else {
tls_server, _ := json.MarshalIndent(tlsObj, "", " ") tls_server, _ := json.MarshalIndent(tlsObj, "", " ")
if len(tls_server) > 5 { if len(tls_server) > 5 {
tlsObject := tlsObj.(map[string]interface{})
tlsClientObj := map[string]interface{}{}
if enabled, ok := tlsObject["enabled"]; ok {
tlsClientObj["enabled"] = enabled
}
if alpn, ok := tlsObject["alpn"]; ok {
tlsClientObj["alpn"] = alpn
}
if sni, ok := tlsObject["server_name"]; ok {
tlsClientObj["server_name"] = sni
}
tls_client, _ := json.MarshalIndent(tlsClientObj, "", " ")
newTls := &model.Tls{ newTls := &model.Tls{
Name: tag, Name: tag,
Server: tls_server, Server: tls_server,
Client: tls_client, Client: json.RawMessage("{}"),
} }
err = db.Create(newTls).Error err = db.Create(newTls).Error
if err != nil { if err != nil {
@@ -243,7 +231,34 @@ func migrateTls(db *gorm.DB) error {
if !db.Migrator().HasColumn(&model.Tls{}, "inbounds") { if !db.Migrator().HasColumn(&model.Tls{}, "inbounds") {
return nil return nil
} }
return db.Migrator().DropColumn(&model.Tls{}, "inbounds") err := db.Migrator().DropColumn(&model.Tls{}, "inbounds")
if err != nil {
return err
}
var tlsConfig []model.Tls
err = db.Model(model.Tls{}).Scan(&tlsConfig).Error
if err != nil {
return err
}
for index, tls := range tlsConfig {
var tlsClient map[string]interface{}
err = json.Unmarshal(tls.Client, &tlsClient)
if err != nil {
continue
}
for key := range tlsClient {
switch key {
case "insecure", "disable_sni", "utls", "ech", "reality":
continue
default:
delete(tlsClient, key)
}
}
tlsConfig[index].Client, _ = json.MarshalIndent(tlsClient, "", " ")
}
return db.Save(&tlsConfig).Error
} }
func dropInboundData(db *gorm.DB) error { func dropInboundData(db *gorm.DB) error {
@@ -276,6 +291,10 @@ func migrateClients(db *gorm.DB) error {
return db.Save(oldClients).Error return db.Save(oldClients).Error
} }
func migrateChanges(db *gorm.DB) error {
return db.Migrator().DropColumn(&model.Changes{}, "index")
}
func to1_2(db *gorm.DB) error { func to1_2(db *gorm.DB) error {
err := moveJsonToDb(db) err := moveJsonToDb(db)
if err != nil { if err != nil {
@@ -289,5 +308,9 @@ func to1_2(db *gorm.DB) error {
if err != nil { if err != nil {
return err return err
} }
return migrateClients(db) err = migrateClients(db)
if err != nil {
return err
}
return migrateChanges(db)
} }
@@ -51,7 +51,7 @@ func GetDBFolderPath() string {
if dbFolderPath == "" { if dbFolderPath == "" {
dir, err := filepath.Abs(filepath.Dir(os.Args[0])) dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil { if err != nil {
dbFolderPath = "/usr/local/s-ui/db" return "/usr/local/s-ui/db"
} }
dbFolderPath = dir + "/db" dbFolderPath = dir + "/db"
} }
View File
+1
View File
@@ -0,0 +1 @@
1.2.1
View File
View File
+296
View File
@@ -0,0 +1,296 @@
package database
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"os"
"path/filepath"
"s-ui/cmd/migration"
"s-ui/config"
"s-ui/database/model"
"s-ui/logger"
"s-ui/util/common"
"strings"
"syscall"
"time"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
)
func GetDb(exclude string) ([]byte, error) {
exclude_changes, exclude_stats := false, false
for _, table := range strings.Split(exclude, ",") {
if table == "changes" {
exclude_changes = true
} else if table == "stats" {
exclude_stats = true
}
}
dir, err := filepath.Abs(filepath.Dir(os.Args[0]))
if err != nil {
return nil, err
}
dbPath := dir + config.GetName() + "_" + time.Now().Format("20060102-200203") + ".db"
backupDb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{})
if err != nil {
return nil, err
}
defer os.Remove(dbPath)
err = backupDb.AutoMigrate(
&model.Setting{},
&model.Tls{},
&model.Inbound{},
&model.Outbound{},
&model.Endpoint{},
&model.User{},
&model.Stats{},
&model.Client{},
&model.Changes{},
)
if err != nil {
return nil, err
}
var settings []model.Setting
var tls []model.Tls
var inbound []model.Inbound
var outbound []model.Outbound
var endpoint []model.Endpoint
var users []model.User
var clients []model.Client
var stats []model.Stats
var changes []model.Changes
// Perform scans and handle errors
if err := db.Model(&model.Setting{}).Scan(&settings).Error; err != nil {
return nil, err
} else if len(settings) > 0 {
if err := backupDb.Save(settings).Error; err != nil {
return nil, err
}
}
if err := db.Model(&model.Tls{}).Scan(&tls).Error; err != nil {
return nil, err
} else if len(tls) > 0 {
if err := backupDb.Save(tls).Error; err != nil {
return nil, err
}
}
if err := db.Model(&model.Inbound{}).Scan(&inbound).Error; err != nil {
return nil, err
} else if len(inbound) > 0 {
if err := backupDb.Save(inbound).Error; err != nil {
return nil, err
}
}
if err := db.Model(&model.Outbound{}).Scan(&outbound).Error; err != nil {
return nil, err
} else if len(outbound) > 0 {
if err := backupDb.Save(outbound).Error; err != nil {
return nil, err
}
}
if err := db.Model(&model.Endpoint{}).Scan(&endpoint).Error; err != nil {
return nil, err
} else if len(endpoint) > 0 {
if err := backupDb.Save(endpoint).Error; err != nil {
return nil, err
}
}
if err := db.Model(&model.User{}).Scan(&users).Error; err != nil {
return nil, err
} else if len(users) > 0 {
if err := backupDb.Save(users).Error; err != nil {
return nil, err
}
}
if err := db.Model(&model.Client{}).Scan(&clients).Error; err != nil {
return nil, err
} else if len(clients) > 0 {
if err := backupDb.Save(clients).Error; err != nil {
return nil, err
}
}
if !exclude_stats {
if err := db.Model(&model.Stats{}).Scan(&stats).Error; err != nil {
return nil, err
}
if len(stats) > 0 {
if err := backupDb.Save(stats).Error; err != nil {
return nil, err
}
}
}
if !exclude_changes {
if err := db.Model(&model.Changes{}).Scan(&changes).Error; err != nil {
return nil, err
}
if len(changes) > 0 {
if err := backupDb.Save(changes).Error; err != nil {
return nil, err
}
}
}
// Update WAL
err = backupDb.Exec("PRAGMA wal_checkpoint;").Error
if err != nil {
return nil, err
}
bdb, _ := backupDb.DB()
bdb.Close()
// Open the file for reading
file, err := os.Open(dbPath)
if err != nil {
return nil, err
}
defer file.Close()
// Read the file contents
fileContents, err := io.ReadAll(file)
if err != nil {
return nil, err
}
return fileContents, nil
}
func ImportDB(file multipart.File) error {
// Check if the file is a SQLite database
isValidDb, err := IsSQLiteDB(file)
if err != nil {
return common.NewErrorf("Error checking db file format: %v", err)
}
if !isValidDb {
return common.NewError("Invalid db file format")
}
// Reset the file reader to the beginning
_, err = file.Seek(0, 0)
if err != nil {
return common.NewErrorf("Error resetting file reader: %v", err)
}
// Save the file as temporary file
tempPath := fmt.Sprintf("%s.temp", config.GetDBPath())
// Remove the existing fallback file (if any) before creating one
_, err = os.Stat(tempPath)
if err == nil {
errRemove := os.Remove(tempPath)
if errRemove != nil {
return common.NewErrorf("Error removing existing temporary db file: %v", errRemove)
}
}
// Create the temporary file
tempFile, err := os.Create(tempPath)
if err != nil {
return common.NewErrorf("Error creating temporary db file: %v", err)
}
defer tempFile.Close()
// Remove temp file before returning
defer os.Remove(tempPath)
// Close old DB
old_db, _ := db.DB()
old_db.Close()
// Save uploaded file to temporary file
_, err = io.Copy(tempFile, file)
if err != nil {
return common.NewErrorf("Error saving db: %v", err)
}
// Check if we can init db or not
newDb, err := gorm.Open(sqlite.Open(tempPath), &gorm.Config{})
if err != nil {
return common.NewErrorf("Error checking db: %v", err)
}
newDb_db, _ := newDb.DB()
newDb_db.Close()
// Backup the current database for fallback
fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
// Remove the existing fallback file (if any)
_, err = os.Stat(fallbackPath)
if err == nil {
errRemove := os.Remove(fallbackPath)
if errRemove != nil {
return common.NewErrorf("Error removing existing fallback db file: %v", errRemove)
}
}
// Move the current database to the fallback location
err = os.Rename(config.GetDBPath(), fallbackPath)
if err != nil {
return common.NewErrorf("Error backing up temporary db file: %v", err)
}
// Remove the temporary file before returning
defer os.Remove(fallbackPath)
// Move temp to DB path
err = os.Rename(tempPath, config.GetDBPath())
if err != nil {
errRename := os.Rename(fallbackPath, config.GetDBPath())
if errRename != nil {
return common.NewErrorf("Error moving db file and restoring fallback: %v", errRename)
}
return common.NewErrorf("Error moving db file: %v", err)
}
// Migrate DB
migration.MigrateDb()
err = InitDB(config.GetDBPath())
if err != nil {
errRename := os.Rename(fallbackPath, config.GetDBPath())
if errRename != nil {
return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
}
return common.NewErrorf("Error migrating db: %v", err)
}
// Restart app
err = SendSighup()
if err != nil {
return common.NewErrorf("Error restarting app: %v", err)
}
return nil
}
func IsSQLiteDB(file io.Reader) (bool, error) {
signature := []byte("SQLite format 3\x00")
buf := make([]byte, len(signature))
_, err := file.Read(buf)
if err != nil {
return false, err
}
return bytes.Equal(buf, signature), nil
}
func SendSighup() error {
// Get the current process
process, err := os.FindProcess(os.Getpid())
if err != nil {
return err
}
// Send SIGHUP to the current process
go func() {
time.Sleep(3 * time.Second)
err := process.Signal(syscall.SIGHUP)
if err != nil {
logger.Error("send signal SIGHUP failed:", err)
}
}()
return nil
}
@@ -79,6 +79,7 @@ func InitDB(dbPath string) error {
&model.Outbound{}, &model.Outbound{},
&model.Endpoint{}, &model.Endpoint{},
&model.User{}, &model.User{},
&model.Tokens{},
&model.Stats{}, &model.Stats{},
&model.Client{}, &model.Client{},
&model.Changes{}, &model.Changes{},
@@ -9,6 +9,7 @@ type Endpoint struct {
Type string `json:"type" form:"type"` Type string `json:"type" form:"type"`
Tag string `json:"tag" form:"tag" gorm:"unique"` Tag string `json:"tag" form:"tag" gorm:"unique"`
Options json.RawMessage `json:"-" form:"-"` Options json.RawMessage `json:"-" form:"-"`
Ext json.RawMessage `json:"ext" form:"ext"`
} }
func (o *Endpoint) UnmarshalJSON(data []byte) error { func (o *Endpoint) UnmarshalJSON(data []byte) error {
@@ -27,9 +28,11 @@ func (o *Endpoint) UnmarshalJSON(data []byte) error {
delete(raw, "type") delete(raw, "type")
o.Tag = raw["tag"].(string) o.Tag = raw["tag"].(string)
delete(raw, "tag") delete(raw, "tag")
o.Ext, _ = json.MarshalIndent(raw["ext"], "", " ")
delete(raw, "ext")
// Remaining fields // Remaining fields
o.Options, err = json.Marshal(raw) o.Options, err = json.MarshalIndent(raw, "", " ")
return err return err
} }
@@ -37,7 +40,12 @@ func (o *Endpoint) UnmarshalJSON(data []byte) error {
func (o Endpoint) MarshalJSON() ([]byte, error) { func (o Endpoint) MarshalJSON() ([]byte, error) {
// Combine fixed fields and dynamic fields into one map // Combine fixed fields and dynamic fields into one map
combined := make(map[string]interface{}) combined := make(map[string]interface{})
switch o.Type {
case "warp":
combined["type"] = "wireguard"
default:
combined["type"] = o.Type combined["type"] = o.Type
}
combined["tag"] = o.Tag combined["tag"] = o.Tag
if o.Options != nil { if o.Options != nil {
@@ -26,9 +26,9 @@ type Client struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"`
Name string `json:"name" form:"name"` Name string `json:"name" form:"name"`
Config json.RawMessage `json:"config" form:"config"` Config json.RawMessage `json:"config,omitempty" form:"config"`
Inbounds json.RawMessage `json:"inbounds" form:"inbounds"` Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
Links json.RawMessage `json:"links" form:"links"` Links json.RawMessage `json:"links,omitempty" form:"links"`
Volume int64 `json:"volume" form:"volume"` Volume int64 `json:"volume" form:"volume"`
Expiry int64 `json:"expiry" form:"expiry"` Expiry int64 `json:"expiry" form:"expiry"`
Down int64 `json:"down" form:"down"` Down int64 `json:"down" form:"down"`
@@ -49,9 +49,17 @@ type Stats struct {
type Changes struct { type Changes struct {
Id uint64 `json:"id" gorm:"primaryKey;autoIncrement"` Id uint64 `json:"id" gorm:"primaryKey;autoIncrement"`
DateTime int64 `json:"dateTime"` DateTime int64 `json:"dateTime"`
Actor string `json:"Actor"` Actor string `json:"actor"`
Key string `json:"key" form:"key"` Key string `json:"key"`
Action string `json:"action" form:"action"` Action string `json:"action"`
Index uint `json:"index" form:"index"` Obj json.RawMessage `json:"obj"`
Obj json.RawMessage `json:"obj" form:"obj"` }
type Tokens struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Desc string `json:"desc" form:"desc"`
Token string `json:"token" form:"token"`
Expiry int64 `json:"expiry" form:"expiry"`
UserId uint `json:"userId" form:"userId"`
User *User `json:"user" gorm:"foreignKey:UserId;references:Id"`
} }
@@ -27,7 +27,7 @@ func (o *Outbound) UnmarshalJSON(data []byte) error {
delete(raw, "tag") delete(raw, "tag")
// Remaining fields // Remaining fields
o.Options, err = json.Marshal(raw) o.Options, err = json.MarshalIndent(raw, "", " ")
return err return err
} }
Submodule
+1
Submodule frontend added at cb5b0e119b
-4
View File
@@ -1,4 +0,0 @@
> 1%
last 2 versions
not dead
not ie 11
-5
View File
@@ -1,5 +0,0 @@
[*.{js,jsx,ts,tsx,vue}]
indent_style = space
indent_size = 2
trim_trailing_whitespace = true
insert_final_newline = true
-14
View File
@@ -1,14 +0,0 @@
module.exports = {
root: true,
env: {
node: true,
},
extends: [
'plugin:vue/vue3-essential',
'eslint:recommended',
'@vue/eslint-config-typescript',
],
rules: {
'vue/multi-word-component-names': 'off',
},
}
-23
View File
@@ -1,23 +0,0 @@
.DS_Store
node_modules
/dist
/bin
# local env files
.env.local
.env.*.local
# Log files
npm-debug.log*
yarn-debug.log*
yarn-error.log*
pnpm-debug.log*
# Editor directories and files
.idea
.vscode
*.suo
*.ntvs*
*.njsproj
*.sln
*.sw?
-69
View File
@@ -1,69 +0,0 @@
# base
## Project setup
```
# yarn
yarn
# npm
npm install
# pnpm
pnpm install
# bun
bun install
```
### Compiles and hot-reloads for development
```
# yarn
yarn dev
# npm
npm run dev
# pnpm
pnpm dev
# bun
pnpm run dev
```
### Compiles and minifies for production
```
# yarn
yarn build
# npm
npm run build
# pnpm
pnpm build
# bun
pnpm run build
```
### Lints and fixes files
```
# yarn
yarn lint
# npm
npm run lint
# pnpm
pnpm lint
# bun
pnpm run lint
```
### Customize configuration
See [Configuration Reference](https://vitejs.dev/config/).
-22
View File
@@ -1,22 +0,0 @@
<!DOCTYPE html>
<html lang="en">
<head>
<meta charset="UTF-8" />
<link rel="icon" href="assets/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" />
<script>
window.BASE_URL = "{{ .BASE_URL }}"
// Dev Mode
if (window.BASE_URL.charAt(0) === '{') window.BASE_URL = "/app/"
</script>
<title>S-UI</title>
</head>
<body>
<div id="app"></div>
<script type="module" src="/src/main.ts"></script>
</body>
</html>
-3557
View File
File diff suppressed because it is too large Load Diff
-42
View File
@@ -1,42 +0,0 @@
{
"name": "frontend",
"version": "1.2.0-beta.1",
"private": true,
"scripts": {
"dev": "vite --host",
"build": "vue-tsc --noEmit && vite build",
"preview": "vite preview",
"lint": "eslint . --fix --ignore-path .gitignore"
},
"dependencies": {
"@mdi/font": "7.4.47",
"axios": "^1.7.4",
"chart.js": "^4.4.3",
"clipboard": "^2.0.11",
"core-js": "^3.37.1",
"moment": "^2.30.1",
"notivue": "^2.4.4",
"pinia": "^2.1.7",
"qrcode.vue": "^3.4.1",
"roboto-fontface": "^0.10.0",
"vue": "^3.4.31",
"vue-chartjs": "^5.3.1",
"vue-i18n": "^9.14.2",
"vue-router": "^4.4.0",
"vue3-persian-datetime-picker": "^1.2.2",
"vuetify": "^3.6.10"
},
"devDependencies": {
"@babel/types": "^7.24.7",
"@types/node": "^20.14.9",
"@vitejs/plugin-vue": "^5.0.5",
"eslint-plugin-vue": "^9.26.0",
"material-design-icons-iconfont": "^6.7.0",
"sass": "1.77.6",
"typescript": "^5.5.2",
"unplugin-fonts": "^1.1.1",
"vite": "^5.4.6",
"vite-plugin-vuetify": "^2.0.3",
"vue-tsc": "^2.0.22"
}
}
Binary file not shown.

Before

Width:  |  Height:  |  Size: 1.5 KiB

-34
View File
@@ -1,34 +0,0 @@
<template>
<v-overlay
:model-value="loading"
persistent
content-class="text-center"
class="align-center justify-center"
>
<v-progress-circular
indeterminate
size="64"
></v-progress-circular>
<br />
{{ $t('loading') }}
</v-overlay>
<Message />
<router-view />
</template>
<script lang="ts" setup>
import Message from '@/components/message.vue'
import { inject, ref, Ref } from 'vue'
const loading:Ref = inject('loading')?? ref(false)
// Change page title
document.title = "S-UI " + document.location.hostname
</script>
<style>
.v-overlay .v-list-item,
.v-field__input {
direction: ltr;
}
</style>
Binary file not shown.
Binary file not shown.

Before

Width:  |  Height:  |  Size: 763 B

-24
View File
@@ -1,24 +0,0 @@
<?xml version="1.0" encoding="utf-8"?>
<svg viewBox="1.019 0.0225 45.9789 46.9775" width="45.9789" height="46.9775" xmlns="http://www.w3.org/2000/svg">
<g featurekey="symbolFeature-0" transform="matrix(0.4545450210571289, 0, 0, 0.4545450210571289, 0.7917079329490662, 1.7009549140930176)" fill="#737373">
<g xmlns="http://www.w3.org/2000/svg">
<g>
<path d="M50,99.658L0.5,70.699V29.301L50,0.341l49.5,28.959v41.398L50,99.658z M2.5,69.553L50,97.342l47.5-27.789V30.448L50,2.659 L2.5,30.448V69.553z"/>
</g>
<g>
<polygon points="51,98.376 49,98.376 49,58.822 0.995,30.738 2.005,29.011 50,57.091 97.995,29.011 99.005,30.738 51,58.822 "/>
</g>
<g>
<polyline points="28.494,14.082 76.994,42.457 71.506,45.667 23.006,17.292 "/>
<polygon points="71.507,46.246 71.254,46.098 22.754,17.724 23.259,16.861 71.507,45.087 76.003,42.457 28.241,14.514 28.746,13.65 77.983,42.457 "/>
</g>
<g>
<polyline points="71.506,45.667 71.506,57.982 71.51,57.982 76.993,54.775 76.993,42.457 "/>
<polyline points="71.006,45.667 72.006,45.667 72.006,57.113 76.493,54.487 76.493,42.457 77.493,42.457 77.493,55.062 71.646,58.482 71,58.85 "/>
</g>
</g>
</g>
<g featurekey="nameFeature-0" transform="matrix(1.6160469055175781, 0, 0, 1.6160469055175781, 3.2854819297790527, -21.369783401489258)" fill="#a6a6a6">
<path d="M10.904 40.4028 c-5.316 0 -9.2256 -3.048 -9.2256 -6.6192 c0 -1.8592 1.2372 -2.8152 2.5116 -2.8152 c1.0924 0 2.2288 0.7184 2.2288 2.1488 c0 1.4428 -1.4112 1.9972 -1.4112 2.9972 c0 1.782 2.974 3.0552 5.338 3.0552 c3.244 0 6.382 -1.5736 6.382 -5.102 c0 -6.2716 -14.403 -3.6536 -14.403 -13.229 c0 -5.0924 4.3436 -7.6012 9.9036 -7.6012 c4.7364 0 8.9656 2.6496 8.9656 6.1048 c0 1.946 -1.2372 2.9308 -2.4828 2.9308 c-1.1216 0 -2.258 -0.7476 -2.258 -2.178 c0 -1.5584 1.382 -1.8812 1.382 -2.8812 c0 -1.6952 -2.974 -2.7436 -5.3092 -2.7436 c-3.04 0 -5.9296 1.1848 -5.9296 4.6496 c0 5.866 15.193 3.7708 15.193 13.345 c0 4.0208 -3.8304 7.938 -10.886 7.938 z"/>
</g>
</svg>

Before

Width:  |  Height:  |  Size: 2.0 KiB

-73
View File
@@ -1,73 +0,0 @@
<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>
<OutTLS :outbound="addr" v-if="optionTLS" />
<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>
</v-card>
</v-menu>
</v-col>
</v-row>
</template>
<script lang="ts">
import OutTLS from '@/components/tls/OutTLS.vue'
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 ? { enabled: true } : undefined; }
},
optionRemark: {
get(): boolean { return this.$props.addr.remark != undefined },
set(v:boolean) { this.$props.addr.remark = v ? '' : undefined }
}
},
components: {
OutTLS
}
}
</script>
-146
View File
@@ -1,146 +0,0 @@
<template>
<v-text-field
id="expiry"
:label="$t('date.expiry')"
v-model="dateFormatted"
prepend-inner-icon="mdi-calendar"
readonly
hide-details
></v-text-field>
<DatePicker
v-model="Input"
@input="Input=$event"
:locale="locale"
element="expiry"
compact-time
type="datetime">
<template v-slot:next-month>
<v-icon icon="mdi-chevron-right" />
</template>
<template v-slot:prev-month>
<v-icon icon="mdi-chevron-left" />
</template>
<template #submit-btn="{ submit, canSubmit }">
<v-btn
:disabled="!canSubmit"
@click="submit"
>{{ $t('submit') }}</v-btn>
</template>
<template #cancel-btn="{ vm }">
<v-btn
@click="reset(vm)"
>{{ $t('reset') }}</v-btn>
</template>
<template #now-btn="{ goToday }">
<v-btn
@click="goToday"
>{{ $t('now') }}</v-btn>
</template>
</DatePicker>
</template>
<script lang="ts">
import DatePicker from 'vue3-persian-datetime-picker'
import { i18n } from '@/locales'
import 'moment/locale/vi'
import 'moment/locale/zh-cn'
import 'moment/locale/zh-tw'
export default {
props: ['expiry'],
emits: ['submit'],
data() {
return {
menu: false,
input: new Date(),
}
},
components: { DatePicker },
computed: {
locale() {
const l = i18n.global.locale.value
switch (l) {
case "zhHans":
return "zh-cn"
case "zhHant":
return "zh-tw"
default:
return l
}
},
dateFormatted() {
if (this.expDate == 0) return i18n.global.t('unlimited')
const date = new Date(this.expDate*1000)
return date.toLocaleString(this.locale)
},
expDate() {
return parseInt(this.expiry?? 0)
},
Input: {
get() { return this.expDate == 0 ? new Date() : new Date(this.expDate*1000) },
set(v:string) {
this.input = new Date(v)
this.submit()
}
}
},
methods: {
updateInput(v:Date) {
this.input = v
},
setNow() {
this.input = new Date()
},
submit() {
this.$emit('submit',Math.floor(this.input.getTime()/1000))
},
reset(vm:any) {
this.$emit('submit',0)
this.input = new Date()
vm.visible = false
}
},
watch: {
menu(v) {
if (v) {
this.input = this.expiry == 0 ? new Date() : new Date(this.expDate*1000)
}
}
}
};
</script>
<style>
.vpd-addon-list,
.vpd-addon-list-item {
background-color: rgb(var(--v-theme-background)) !important;
border-color: rgb(var(--v-theme-background)) !important;
}
.vpd-content {
background-color: rgb(var(--v-theme-background)) !important;
}
.vpd-addon-list-item.vpd-selected,
.vpd-addon-list-item:hover {
background-color: rgb(var(--v-theme-primary)) !important;
}
.vpd-close-addon {
color: rgb(var(--v-theme-on-surface)) !important;
background-color: transparent;
}
.vpd-controls {
overflow-x: hidden;
}
.vpd-month-label {
width: auto;
}
.vpd-actions button:hover {
background-color: transparent;
}
.vpd-wrapper[data-type=datetime].vpd-compact-time .vpd-time {
border-top: 0;
}
.vpd-time .vpd-time-h .vpd-counter-item,
.vpd-time .vpd-time-m .vpd-counter-item {
vertical-align: top;
}
</style>
-215
View File
@@ -1,215 +0,0 @@
<template>
<v-card :subtitle="$t('objects.dial')" style="background-color: inherit;">
<v-row>
<v-col cols="12" sm="6" md="4" v-if="optionDetour">
<v-select
hide-details
:label="$t('dial.detourText')"
:items="outTags"
v-model="dial.detour">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionBind">
<v-text-field
:label="$t('dial.bindIf')"
hide-details
v-model="dial.bind_interface"></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="optionIPV4">
<v-text-field
:label="$t('dial.bindIp4')"
hide-details
v-model="dial.inet4_bind_address"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionIPV6">
<v-text-field
:label="$t('dial.bindIp6')"
hide-details
v-model="dial.inet6_bind_address"></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="optionRM">
<v-text-field
label="Linux Routing Mark"
hide-details
type="number"
min="0"
v-model.number="routingMark"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionRA">
<v-switch v-model="dial.reuse_addr" color="primary" :label="$t('dial.reuseAddr')" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="optionTCP">
<v-col cols="12" sm="6" md="4">
<v-switch v-model="dial.tcp_fast_open" color="primary" label="TCP Fast Open" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="dial.tcp_multi_path" color="primary" label="TCP Multi Path" hide-details></v-switch>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="optionUDP">
<v-switch v-model="dial.udp_fragment" color="primary" label="UDP Fragment" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionCT">
<v-text-field
:label="$t('dial.connTimeout')"
hide-details
type="number"
min="1"
:suffix="$t('date.s')"
v-model.number="connectTimeout"></v-text-field>
</v-col>
</v-row>
<v-row v-if="optionDS">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('listen.domainStrategy')"
:items="['prefer_ipv4','prefer_ipv6','ipv4_only','ipv6_only']"
v-model="dial.domain_strategy">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('dial.fbTimeout')"
hide-details
type="number"
min="50"
step="50"
:suffix="$t('date.ms')"
v-model.number="fallbackDelay"></v-text-field>
</v-col>
</v-row>
<v-card-actions class="pt-0">
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('dial.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionDetour" color="primary" :label="$t('listen.detour')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionBind" color="primary" :label="$t('dial.bindIf')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionIPV4" color="primary" :label="$t('dial.bindIp4')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionIPV6" color="primary" :label="$t('dial.bindIp6')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionRM" color="primary" label="Routing Mark" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionRA" color="primary" :label="$t('dial.reuseAddr')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionTCP" color="primary" :label="$t('listen.tcpOptions')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionUDP" color="primary" :label="$t('listen.udpOptions')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionCT" color="primary" :label="$t('dial.connTimeout')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionDS" color="primary" :label="$t('listen.domainStrategy')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
export default {
props: ['dial', 'outTags'],
data() {
return {
menu: false
}
},
computed: {
fallbackDelay: {
get() { return this.$props.dial.fallback_delay ? parseInt(this.$props.dial.fallback_delay.replace('ms','')) : 300 },
set(newValue:number) { this.$props.dial.fallback_delay = newValue > 0 ? newValue + 'ms' : '300ms' }
},
connectTimeout: {
get() { return this.$props.dial.connect_timeout ? parseInt(this.$props.dial.connect_timeout.replace('s','')) : 5 },
set(newValue:number) { this.$props.dial.connect_timeout = newValue > 0 ? newValue + 's' : '5s' }
},
routingMark: {
get() { return this.$props.dial.routing_mark?? 0 },
set(newValue:number) { this.$props.dial.routing_mark = newValue > 0 ? newValue : 0 }
},
optionDetour: {
get(): boolean { return this.$props.dial.detour != undefined },
set(v:boolean) { v ? this.$props.dial.detour = this.outTags[0]?? '' : delete this.$props.dial.detour }
},
optionBind: {
get(): boolean { return this.$props.dial.bind_interface != undefined },
set(v:boolean) { v ? this.$props.dial.bind_interface = '' : delete this.$props.dial.bind_interface }
},
optionIPV4: {
get(): boolean { return this.$props.dial.inet4_bind_address != undefined },
set(v:boolean) { v ? this.$props.dial.inet4_bind_address = '' : delete this.$props.dial.inet4_bind_address }
},
optionIPV6: {
get(): boolean { return this.$props.dial.inet6_bind_address != undefined },
set(v:boolean) { v ? this.$props.dial.inet6_bind_address = '' : delete this.$props.dial.inet6_bind_address }
},
optionRM: {
get(): boolean { return this.$props.dial.routing_mark != undefined },
set(v:boolean) { v ? this.$props.dial.routing_mark = 0 : delete this.$props.dial.routing_mark }
},
optionRA: {
get(): boolean { return this.$props.dial.reuse_addr != undefined },
set(v:boolean) { v ? this.$props.dial.reuse_addr = true : delete this.$props.dial.reuse_addr }
},
optionTCP: {
get(): boolean {
return this.$props.dial.tcp_fast_open != undefined &&
this.$props.dial.tcp_multi_path != undefined
},
set(v:boolean) {
if (v) {
this.$props.dial.tcp_fast_open = false
this.$props.dial.tcp_multi_path = false
} else {
delete this.$props.dial.tcp_fast_open
delete this.$props.dial.tcp_multi_path
}
}
},
optionUDP: {
get(): boolean { return this.$props.dial.udp_fragment != undefined },
set(v:boolean) { v ? this.$props.dial.udp_fragment = true : delete this.$props.dial.udp_fragment }
},
optionCT: {
get(): boolean { return this.$props.dial.connect_timeout != undefined },
set(v:boolean) { v ? this.$props.dial.connect_timeout = '5s' : delete this.$props.dial.connect_timeout }
},
optionDS: {
get(): boolean { return this.$props.dial.domain_strategy != undefined },
set(v:boolean) {
if (v) {
this.$props.dial.domain_strategy = 'prefer_ipv4'
this.$props.dial.fallback_delay = '300ms'
} else {
delete this.$props.dial.domain_strategy
delete this.$props.dial.fallback_delay
}
}
}
}
}
</script>
-99
View File
@@ -1,99 +0,0 @@
<template>
<v-card>
<v-card-subtitle>
{{ $t('objects.headers') }}
<v-icon @click="add_header" icon="mdi-plus"></v-icon>
</v-card-subtitle>
<v-row v-for="(header, index) in hdrs">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('objects.key')"
hide-details
@input="update_key(index,$event.target.value)"
v-model="header.name">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('objects.value')"
hide-details
@input="update_value(index,$event.target.value)"
append-icon="mdi-delete"
@click:append="del_header(index)"
v-model="header.value">
</v-text-field>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
type Header = {
name: string
value: string
}
export default {
props: ['data'],
data() {
return {}
},
methods: {
add_header() {
this.hdrs = [...this.hdrs, {name: "Host", value: ""}]
},
del_header(i:number) {
let h = this.hdrs
h.splice(i,1)
this.hdrs = h
},
update_key(i:number,k:string) {
let h = this.hdrs
h[i].name = k
this.hdrs = h
},
update_value(i:number,v:string) {
let h = this.hdrs
h[i].value = v
this.hdrs = h
},
},
computed: {
hdrs: {
get() :Header[] {
let headers: Header[] = []
const h = this.$props.data.headers
if (h) {
Object.keys(h).forEach(key => {
if (Array.isArray(h[key])){
h[key].forEach((v:string) => headers.push({ name: key, value: v }))
} else {
headers.push({ name: key, value: h[key] })
}
})
}
return headers
},
set(v:Header[]) {
if (v.length>0) {
let headers:any = {}
v.forEach((h:Header) => {
if (headers[h.name]) {
if (Array.isArray(headers[h.name])) {
headers[h.name].push(h.value)
} else {
headers[h.name] = [headers[h.name], h.value]
}
} else {
headers[h.name] = h.value
}
})
this.$props.data.headers = headers
} else {
this.$props.data.headers = undefined
}
}
}
}
}
</script>
-118
View File
@@ -1,118 +0,0 @@
<template>
<v-card :subtitle="$t('objects.listen')">
<v-row v-if="inbound.type != 'tun'">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('in.addr')"
hide-details
required
v-model="inbound.listen">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('in.port')"
hide-details
type="number"
min="1"
max="65535"
required
v-model.number="inbound.listen_port"></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="optionDetour">
<v-select
:label="$t('listen.detourText')"
hide-details
:items="inTags"
v-model="inbound.detour">
</v-select>
</v-col>
</v-row>
<v-row v-if="optionTCP">
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inbound.tcp_fast_open" color="primary" label="TCP Fast Open" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inbound.tcp_multi_path" color="primary" label="TCP Multi Path" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="optionUDP">
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inbound.udp_fragment" color="primary" label="UDP Fragment" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="UDP NAT expiration"
hide-details
type="number"
min="1"
:suffix="$t('date.m')"
v-model.number="udpTimeout"></v-text-field>
</v-col>
</v-row>
<v-card-actions class="pt-0" v-if="inbound.type != 'tun'">
<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('listen.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionDetour" color="primary" :label="$t('listen.detour')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionTCP" color="primary" :label="$t('listen.tcpOptions')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionUDP" color="primary" :label="$t('listen.udpOptions')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
export default {
props: ['inbound', 'inTags'],
data() {
return {
menu: false
}
},
computed: {
udpTimeout: {
get() { return this.$props.inbound.udp_timeout ? parseInt(this.$props.inbound.udp_timeout.replace('m','')) : 5 },
set(newValue:number) { this.$props.inbound.udp_timeout = newValue > 0 ? newValue + 'm' : '5m' }
},
optionTCP: {
get(): boolean {
return this.$props.inbound.tcp_fast_open != undefined &&
this.$props.inbound.tcp_multi_path != undefined
},
set(v:boolean) {
this.$props.inbound.tcp_fast_open = v ? false : undefined
this.$props.inbound.tcp_multi_path = v ? false : undefined
}
},
optionUDP: {
get(): boolean {
return this.$props.inbound.udp_fragment != undefined &&
this.$props.inbound.udp_timeout != undefined
},
set(v:boolean) {
this.$props.inbound.udp_fragment = v ? false : undefined
this.$props.inbound.udp_timeout = v ? '5m' : undefined
}
},
optionDetour: {
get(): boolean { return this.$props.inbound.detour != undefined },
set(v:boolean) { this.$props.inbound.detour = v ? this.inTags[0]?? '' : undefined }
}
}
}
</script>
-255
View File
@@ -1,255 +0,0 @@
<template>
<LogVue
v-model="logModal.visible"
:visible="logModal.visible"
@close="closeLogs"
/>
<v-container class="fill-height" :loading="loading">
<v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" >
<v-row class="d-flex align-center justify-center">
<v-col cols="auto">
<v-img src="@/assets/logo.svg" :width="reloadItems.length>0 ? 100 : 200"></v-img>
</v-col>
</v-row>
<v-row class="d-flex align-center justify-center">
<v-col cols="auto">
<v-dialog v-model="menu" :close-on-content-click="false" transition="scale-transition" max-width="800">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('main.tiles') }} <v-icon icon="mdi-star-plus" /></v-btn>
</template>
<v-card rounded="xl">
<v-card-title>
<v-row>
<v-col>
{{ $t('main.tiles') }}
</v-col>
<v-spacer></v-spacer>
<v-col cols="auto"><v-icon icon="mdi-close" @click="menu = false"></v-icon></v-col>
</v-row>
</v-card-title>
<v-divider></v-divider>
<v-row>
<v-col cols="12" sm="6" md="4" v-for="items in menuItems">
<v-card variant="flat" :title="items.title">
<v-list v-for="item in items.value">
<v-list-item>
<v-switch
v-model="reloadItems"
:value="item.value"
color="primary"
:label="item.title"
hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-col>
</v-row>
</v-card>
</v-dialog>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="3" v-for="i in reloadItems" :key="i">
<v-card class="rounded-lg" variant="outlined" height="210px"
:title="menuItems.flatMap(cat => cat.value).find(m => m.value == i)?.title">
<v-card-text style="padding: 0 16px;" align="center" justify="center">
<Gauge :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'g'" />
<History :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'h'" />
<template v-if="i == 'i-sys'">
<v-row>
<v-col cols="3">{{ $t('main.info.host') }}</v-col>
<v-col cols="9" style="text-wrap: nowrap; overflow: hidden">{{ tilesData.sys?.hostName }}</v-col>
<v-col cols="3">{{ $t('main.info.cpu') }}</v-col>
<v-col cols="9">
<v-chip density="compact" variant="flat">
<v-tooltip activator="parent" location="top" style="direction: ltr;">
{{ tilesData.sys?.cpuType }}
</v-tooltip>
{{ tilesData.sys?.cpuCount }} {{ $t('main.info.core') }}
</v-chip>
</v-col>
<v-col cols="3">IP</v-col>
<v-col cols="9">
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sys?.ipv4?.length>0">
<v-tooltip activator="parent" location="top" style="direction: ltr;">
<span v-html="tilesData.sys?.ipv4?.join('<br />')"></span>
</v-tooltip>
IPv4
</v-chip>
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sys?.ipv6?.length>0">
<v-tooltip activator="parent" location="top" style="direction: ltr;">
<span v-html="tilesData.sys?.ipv6?.join('<br />')"></span>
</v-tooltip>
IPv6
</v-chip>
</v-col>
<v-col cols="3">S-UI</v-col>
<v-col cols="9">
<v-chip density="compact" color="blue">
<v-tooltip activator="parent" location="top">
{{ $t('main.info.threads') }}: {{ tilesData.sys?.appThreads }}<br />
{{ $t('main.info.memory') }}: {{ HumanReadable.sizeFormat(tilesData.sys?.appMem) }}
</v-tooltip>
v{{ tilesData.sys?.appVersion }}
</v-chip>
<v-chip density="compact" color="transparent" style="cursor: pointer;" @click="openLogs()">
<v-tooltip activator="parent" location="top">
{{ $t('basic.log.title') + " - S-UI" }}
</v-tooltip>
<v-icon icon="mdi-list-box-outline" color="blue" />
</v-chip>
</v-col>
<v-col cols="3">{{ $t('main.info.uptime') }}</v-col>
<v-col cols="9">{{ HumanReadable.formatSecond(tilesData.uptime) }}</v-col>
</v-row>
</template>
<template v-if="i == 'i-sbd'">
<v-row>
<v-col cols="4">{{ $t('main.info.running') }}</v-col>
<v-col cols="8">
<v-chip density="compact" color="success" variant="flat" v-if="tilesData.sbd?.running">{{ $t('yes') }}</v-chip>
<v-chip density="compact" color="error" variant="flat" v-else>{{ $t('no') }}</v-chip>
<v-chip density="compact" color="transparent" 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 cols="4">{{ $t('main.info.memory') }}</v-col>
<v-col cols="8">
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sbd?.stats?.Alloc">
{{ HumanReadable.sizeFormat(tilesData.sbd?.stats?.Alloc) }}
</v-chip>
</v-col>
<v-col cols="4">{{ $t('main.info.threads') }}</v-col>
<v-col cols="8">
<v-chip density="compact" color="primary" variant="flat" v-if="tilesData.sbd?.stats?.NumGoroutine">
{{ tilesData.sbd?.stats?.NumGoroutine }}
</v-chip>
</v-col>
<v-col cols="4">{{ $t('main.info.uptime') }}</v-col>
<v-col cols="8">{{ HumanReadable.formatSecond(tilesData.sbd?.stats?.Uptime) }}</v-col>
<v-col cols="4">{{ $t('online') }}</v-col>
<v-col cols="8">
<template v-if="tilesData.sbd?.running">
<v-chip density="compact" color="primary" variant="flat" v-if="Data().onlines.user">
<v-tooltip activator="parent" location="top" :text="$t('pages.clients')" />
{{ Data().onlines.user?.length }}
</v-chip>
<v-chip density="compact" color="success" variant="flat" v-if="Data().onlines.inbound">
<v-tooltip activator="parent" location="top" :text="$t('pages.inbounds')" />
{{ Data().onlines.inbound?.length }}
</v-chip>
<v-chip density="compact" color="info" variant="flat" v-if="Data().onlines.outbound">
<v-tooltip activator="parent" location="top" :text="$t('pages.outbounds')" />
{{ Data().onlines.outbound?.length }}
</v-chip>
</template>
</v-col>
</v-row>
</template>
</v-card-text>
</v-card>
</v-col>
</v-row>
</v-responsive>
</v-container>
</template>
<script lang="ts" setup>
import HttpUtils from '@/plugins/httputil'
import { HumanReadable } from '@/plugins/utils'
import Data from '@/store/modules/data'
import Gauge from '@/components/tiles/Gauge.vue'
import History from '@/components/tiles/History.vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { i18n } from '@/locales'
import LogVue from '@/layouts/modals/Logs.vue'
const loading = ref(false)
const menu = ref(false)
const menuItems = [
{ title: i18n.global.t('main.gauges'), value: [
{ title: i18n.global.t('main.gauge.cpu'), value: "g-cpu" },
{ title: i18n.global.t('main.gauge.mem'), value: "g-mem" },
]
},
{ title: i18n.global.t('main.charts'), value: [
{ title: i18n.global.t('main.chart.cpu'), value: "h-cpu" },
{ title: i18n.global.t('main.chart.mem'), value: "h-mem" },
{ title: i18n.global.t('main.chart.net'), value: "h-net" },
{ title: i18n.global.t('main.chart.pnet'), value: "hp-net" },
]
},
{ title: i18n.global.t('main.infos'), value: [
{ title: i18n.global.t('main.info.sys'), value: "i-sys" },
{ title: i18n.global.t('main.info.sbd'), value: "i-sbd" },
]
},
]
const tilesData = ref(<any>{})
const reloadItems = computed({
get() { return Data().reloadItems },
set(v:string[]) {
if (Data().reloadItems.length == 0 && v.length>0) startTimer()
if (Data().reloadItems.length > 0 && v.length == 0) stopTimer()
Data().reloadItems = v
v.length>0 ? localStorage.setItem("reloadItems",v.join(',')) : localStorage.removeItem("reloadItems")
}
})
const reloadData = async () => {
const request = [...new Set(reloadItems.value.map(r => r.split('-')[1]))]
const data = await HttpUtils.get('api/status',{ r: request.join(',')})
if (data.success) {
tilesData.value = data.obj
}
}
let intervalId: NodeJS.Timeout | null = null
const startTimer = () => {
intervalId = setInterval(() => {
reloadData()
}, 2000)
}
const stopTimer = () => {
if (intervalId) {
clearInterval(intervalId);
intervalId = null
}
}
onMounted(() => {
if (Data().reloadItems.length != 0) {
reloadData()
startTimer()
}
})
onBeforeUnmount(() => {
stopTimer()
})
const logModal = ref({
visible: false,
})
const openLogs = () => {
logModal.value.visible = true
}
const closeLogs = () => {
logModal.value.visible = false
}
const restartSingbox = async () => {
loading.value = true
await HttpUtils.post('api/restartSb',{})
loading.value = false
}
</script>
-129
View File
@@ -1,129 +0,0 @@
<template>
<v-card :subtitle="$t('objects.multiplex')">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('mux.enable')" v-model="muxEnable" hide-details></v-switch>
</v-col>
<template v-if="mux.enabled">
<template v-if="direction=='out'">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="[ 'smux', 'yamux', 'h2mux']"
:label="$t('protocol')"
clearable
@click:clear="mux.protocol=undefined"
v-model="mux.protocol">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('mux.maxConn')"
hide-details
type="number"
min=0
v-model.number="max_connections">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('mux.minStr')"
hide-details
type="number"
min=0
v-model.number="min_streams">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('mux.maxStr')"
hide-details
type="number"
:min="min_streams"
v-model.number="max_streams">
</v-text-field>
</v-col>
</template>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('mux.padding')" v-model="mux.padding" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('mux.enableBrutal')" v-model="burtalEnable" hide-details></v-switch>
</v-col>
</template>
</v-row>
<v-row v-if="mux.brutal?.enabled">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('stats.upload')"
hide-details
type="number"
:suffix="$t('stats.Mbps')"
v-model.number="up_mbps">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('stats.download')"
hide-details
type="number"
:suffix="$t('stats.Mbps')"
min="0"
v-model.number="down_mbps">
</v-text-field>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import { oMultiplex } from '@/types/multiplex'
export default {
props: ['data', 'direction'],
data() {
return {}
},
computed: {
mux(): oMultiplex {
if (!Object.hasOwn(this.$props.data,"multiplex")) this.$props.data.multiplex = {}
return <oMultiplex> this.$props.data.multiplex
},
muxEnable: {
get(): boolean { return this.mux ? this.mux.enabled : false },
set(newValue:boolean) { this.$props.data.multiplex = newValue ? { enabled: newValue } : {} }
},
max_connections: {
get(): number { return this.mux.max_connections ? this.mux.max_connections : 0 },
set(newValue:number) { this.mux.max_connections = newValue > 0 ? newValue : undefined }
},
min_streams: {
get(): number { return this.mux.min_streams ? this.mux.min_streams : 0 },
set(newValue:number) { this.mux.min_streams = newValue > 0 ? newValue : undefined }
},
max_streams: {
get(): number { return this.mux.max_streams ? this.mux.max_streams : 0 },
set(newValue:number) { this.mux.max_streams = newValue > 0 ? newValue : undefined }
},
burtalEnable: {
get(): boolean { return this.mux.brutal ? this.mux.brutal.enabled : false },
set(newValue:boolean) { this.mux.brutal = newValue ? { enabled: newValue, up_mbps: 100, down_mbps: 100 } : undefined }
},
down_mbps: {
get() { return this.mux.brutal && this.mux.brutal.down_mbps ? this.mux.brutal.down_mbps : 0 },
set(newValue:any) {
if (this.mux.brutal){
this.mux.brutal.down_mbps = newValue.length != 0 ? newValue : 0
}
}
},
up_mbps: {
get() { return this.mux.brutal && this.mux.brutal.up_mbps ? this.mux.brutal.up_mbps : 0 },
set(newValue:any) {
if (this.mux.brutal){
this.mux.brutal.up_mbps = newValue.length != 0 ? newValue : 0
}
}
},
}
}
</script>
-29
View File
@@ -1,29 +0,0 @@
<template>
<v-select
hide-details
:label="$t('network')"
:items="networks"
v-model="Network">
</v-select>
</template>
<script lang="ts">
export default {
props: ['data'],
data() {
return {
networks: [
{ title: "TCP/UDP", value: '' },
{ title: "TCP", value: 'tcp' },
{ title: "UDP", value: 'udp' },
],
}
},
computed: {
Network: {
get():string { return this.$props.data.network?? '' },
set(v:string) { this.$props.data.network = v != '' ? v : undefined }
}
}
}
</script>
-123
View File
@@ -1,123 +0,0 @@
<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.out_json.version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="needNetwork">
<Network :data="inData.out_json" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="needUot">
<UoT :data="inData.out_json" />
</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.out_json.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.out_json.security">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inData.out_json.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.out_json.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.out_json.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.out_json.udp_relay_mode"
v-model="inData.out_json.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.out_json.udp_over_stream" hide-details></v-switch>
</v-col>
</template>
</v-row>
<Headers :data="inData.out_json" 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.out_json.packet_encoding != undefined ? this.$props.inData.out_json.packet_encoding : 'none'; },
set(v:string) { this.$props.inData.out_json.packet_encoding = v != "none" ? v : undefined }
},
},
components: { Network, UoT, Headers }
}
</script>
-382
View File
@@ -1,382 +0,0 @@
<template>
<v-card style="background-color: inherit;">
<v-row>
<v-col cols="12" v-if="optionInbound">
<v-combobox
v-model="rule.inbound"
:items="inTags"
:label="$t('pages.inbounds')"
multiple
chips
hide-details
></v-combobox>
</v-col>
<v-col cols="12" v-if="optionClient">
<v-combobox
v-model="rule.auth_user"
:items="clients"
:label="$t('pages.clients')"
multiple
chips
hide-details
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionIPver">
<v-select
hide-details
:label="$t('rule.ipVer')"
:items="[4,6]"
v-model.number="rule.ip_version">
</v-select>
</v-col>
<v-col cols="12" sm="6" v-if="optionProtocol">
<v-combobox
v-model="rule.protocol"
:items="['http','tls', 'quic', 'stun', 'dns']"
:label="$t('protocol')"
multiple
chips
hide-details
></v-combobox>
</v-col>
</v-row>
<v-row v-if="optionDomain">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="domainKeys"
@update:model-value="updateDomainOption($event)"
v-model="domainOption">
</v-select>
</v-col>
<v-col cols="12" sm="6" v-if="rule.domain != undefined">
<v-text-field
:label="$t('rule.domain') + ' ' + $t('commaSeparated')"
hide-details
v-model="domain"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.domain_suffix != undefined">
<v-text-field
:label="$t('rule.domainSufix') + ' ' + $t('commaSeparated')"
hide-details
v-model="domain_suffix"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.domain_keyword != undefined">
<v-text-field
:label="$t('rule.domainKw') + ' ' + $t('commaSeparated')"
hide-details
v-model="domain_keyword"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.domain_regex != undefined">
<v-text-field
:label="$t('rule.domainRgx') + ' ' + $t('commaSeparated')"
hide-details
v-model="domain_regex"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.ip_cidr != undefined">
<v-text-field
:label="$t('rule.ip') + ' ' + $t('commaSeparated')"
hide-details
v-model="ip_cidr"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.ip_is_private != undefined">
<v-switch v-model="rule.ip_is_private" color="primary" :label="$t('rule.privateIp')" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="optionPort">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="portKeys"
@update:model-value="updatePortOption($event)"
v-model="portOption">
</v-select>
</v-col>
<v-col cols="12" sm="6" v-if="rule.port != undefined">
<v-text-field
:label="$t('rule.port') + ' ' + $t('commaSeparated')"
hide-details
v-model="port"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.port_range != undefined">
<v-text-field
:label="$t('rule.portRange') + ' ' + $t('commaSeparated')"
hide-details
v-model="port_range"></v-text-field>
</v-col>
</v-row>
<v-row v-if="optionSrcIP">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="srcIPKeys"
@update:model-value="updateSrcIPOption($event)"
v-model="srcIPOption">
</v-select>
</v-col>
<v-col cols="12" sm="6" v-if="rule.source_ip_cidr != undefined">
<v-text-field
:label="$t('rule.srcCidr') + ' ' + $t('commaSeparated')"
hide-details
v-model="source_ip_cidr"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.source_ip_is_private != undefined">
<v-switch v-model="rule.source_ip_is_private" color="primary" :label="$t('rule.srcPrivateIp')" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="optionSrcPort">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="srcPortKeys"
@update:model-value="updateSrcPortOption($event)"
v-model="srcPortOption">
</v-select>
</v-col>
<v-col cols="12" sm="6" v-if="rule.source_port != undefined">
<v-text-field
:label="$t('rule.srcPort') + ' ' + $t('commaSeparated')"
hide-details
v-model="source_port"></v-text-field>
</v-col>
<v-col cols="12" sm="6" v-if="rule.source_port_range != undefined">
<v-text-field
:label="$t('rule.srcPortRange') + ' ' + $t('commaSeparated')"
hide-details
v-model="source_port_range"></v-text-field>
</v-col>
</v-row>
<v-row v-if="optionRuleSet">
<v-col cols="12" sm="6">
<v-combobox
v-model="rule.rule_set"
:items="rsTags"
:label="$t('rule.ruleset')"
multiple
chips
hide-details
></v-combobox>
</v-col>
<v-col cols="12" sm="6">
<v-switch v-model="rule.rule_set_ipcidr_match_source" color="primary" :label="$t('rule.rulesetMatchSrc')" hide-details></v-switch>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('rule.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionInbound" color="primary" :label="$t('pages.inbounds')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionClient" color="primary" :label="$t('pages.clients')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionIPver" color="primary" :label="$t('rule.ipVer')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionProtocol" color="primary" :label="$t('protocol')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionDomain" color="primary" :label="$t('rule.domainRules')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionPort" color="primary" :label="$t('in.port')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionSrcIP" color="primary" :label="$t('rule.srcIpRules')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionSrcPort" color="primary" :label="$t('rule.srcPortRules')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionRuleSet" color="primary" :label="$t('rule.ruleset')" 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: ['rule', 'clients', 'inTags', 'rsTags', 'deleteable'],
data() {
return {
menu: false,
domainKeys: ['domain', 'domain_suffix', 'domain_keyword', 'domain_regex', 'ip_cidr', 'ip_is_private'],
portKeys: ['port', 'port_range'],
srcIPKeys: ['source_ip_cidr', 'source_ip_is_private'],
srcPortKeys: ['source_port', 'source_port_range'],
domainOption: 'domain',
portOption: 'port',
srcIPOption: 'source_ip_cidr',
srcPortOption: 'source_port',
}
},
methods: {
updateDomainOption(option:string) {
this.domainKeys.forEach(k => delete this.$props.rule[k])
this.$props.rule[option] = option == 'ip_is_private' ? false : []
},
updatePortOption(option:string) {
this.portKeys.forEach(k => delete this.$props.rule[k])
this.$props.rule[option] = []
},
updateSrcIPOption(option:string) {
this.srcIPKeys.forEach(k => delete this.$props.rule[k])
this.$props.rule[option] = option == 'source_ip_is_private' ? false : []
},
updateSrcPortOption(option:string) {
this.srcPortKeys.forEach(k => delete this.$props.rule[k])
this.$props.rule[option] = []
},
},
computed: {
optionInbound: {
get() { return this.$props.rule.inbound != undefined },
set(v:boolean) { this.$props.rule.inbound = v ? [] : undefined }
},
optionClient: {
get() { return this.$props.rule.auth_user != undefined },
set(v:boolean) { this.$props.rule.auth_user = v ? [] : undefined }
},
optionIPver: {
get() { return this.$props.rule.ip_version != undefined },
set(v:boolean) { this.$props.rule.ip_version = v ? 4 : undefined }
},
optionProtocol: {
get() { return this.$props.rule.protocol != undefined },
set(v:boolean) { this.$props.rule.protocol = v ? ['http'] : undefined }
},
optionDomain: {
get() { return Object.keys(this.$props.rule).some(r => this.domainKeys.includes(r)) },
set(v:boolean) {
if (v) {
this.$props.rule.domain = []
} else {
this.domainKeys.forEach(k => delete this.$props.rule[k])
}
this.domainOption = 'domain'
}
},
optionPort: {
get() { return Object.keys(this.$props.rule).some(r => this.portKeys.includes(r)) },
set(v:boolean) {
if (v) {
this.$props.rule.port = []
} else {
this.portKeys.forEach(k => delete this.$props.rule[k])
}
this.portOption = 'port'
}
},
optionSrcIP: {
get() { return Object.keys(this.$props.rule).some(r => this.srcIPKeys.includes(r)) },
set(v:boolean) {
if (v) {
this.$props.rule.source_ip_cidr = []
} else {
this.srcIPKeys.forEach(k => delete this.$props.rule[k])
}
this.srcIPOption = 'source_ip_cidr'
}
},
optionSrcPort: {
get() { return Object.keys(this.$props.rule).some(r => this.srcPortKeys.includes(r)) },
set(v:boolean) {
if (v) {
this.$props.rule.source_port = []
} else {
this.srcPortKeys.forEach(k => delete this.$props.rule[k])
}
this.srcPortOption = 'source_port'
}
},
optionRuleSet: {
get() { return this.$props.rule.rule_set != undefined },
set(v:boolean) {
if (v) {
this.$props.rule.rule_set = []
this.$props.rule.rule_set_ipcidr_match_source = false
} else {
delete this.$props.rule.rule_set
delete this.$props.rule.rule_set_ipcidr_match_source
}
}
},
domain: {
get() { return this.$props.rule.domain?.join(',') },
set(v:string) { this.$props.rule.domain = v.length>0 ? v.split(',') : [] }
},
domain_suffix: {
get() { return this.$props.rule.domain_suffix?.join(',') },
set(v:string) { this.$props.rule.domain_suffix = v.length>0 ? v.split(',') : [] }
},
domain_keyword: {
get() { return this.$props.rule.domain_keyword?.join(',') },
set(v:string) { this.$props.rule.domain_keyword = v.length>0 ? v.split(',') : [] }
},
domain_regex: {
get() { return this.$props.rule.domain_regex?.join(',') },
set(v:string) { this.$props.rule.domain_regex = v.length>0 ? v.split(',') : [] }
},
ip_cidr: {
get() { return this.$props.rule.ip_cidr?.join(',') },
set(v:string) { this.$props.rule.ip_cidr = v.length>0 ? v.split(',') : [] }
},
port: {
get() { return this.$props.rule.port?.join(',') },
set(v:string) {
if(!v.endsWith(',')) {
this.$props.rule.port = v.length > 0 ? v.split(',').map(str => parseInt(str, 10)) : []
}
}
},
port_range: {
get() { return this.$props.rule.port_range?.join(',') },
set(v:string) { this.$props.rule.port_range = v.length>0 ? v.split(',') : [] }
},
source_ip_cidr: {
get() { return this.$props.rule.source_ip_cidr?.join(',') },
set(v:string) { this.$props.rule.source_ip_cidr = v.length>0 ? v.split(',') : [] }
},
source_port: {
get() { return this.$props.rule.source_port?.join(',') },
set(v:string) {
if(!v.endsWith(',')) {
this.$props.rule.source_port = v.length > 0 ? v.split(',').map(str => parseInt(str, 10)) : []
}
}
},
source_port_range: {
get() { return this.$props.rule.source_port_range?.join(',') },
set(v:string) { this.$props.rule.source_port_range = v.length>0 ? v.split(',') : [] }
},
},
mounted() {
const ruleKeys = Object.keys(this.$props.rule)
if (this.optionDomain) {
const enabledOption = this.domainKeys.filter(k => ruleKeys.includes(k))
this.domainOption = enabledOption.length>0 ? enabledOption[0] : 'domain'
}
if (this.optionPort) {
const enabledOption = this.portKeys.filter(k => ruleKeys.includes(k))
this.portOption = enabledOption.length>0 ? enabledOption[0] : 'port'
}
if (this.optionSrcIP) {
const enabledOption = this.srcIPKeys.filter(k => ruleKeys.includes(k))
this.srcIPOption = enabledOption.length>0 ? enabledOption[0] : 'source_ip_cidr'
}
if (this.optionSrcPort) {
const enabledOption = this.srcPortKeys.filter(k => ruleKeys.includes(k))
this.srcPortOption = enabledOption.length>0 ? enabledOption[0] : 'source_port'
}
}
}
</script>
-452
View File
@@ -1,452 +0,0 @@
<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>
-51
View File
@@ -1,51 +0,0 @@
<template>
<v-card :subtitle="$t('objects.transport')">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('transport.enable')" v-model="tpEnable" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="tpEnable">
<v-select
hide-details
:label="$t('type')"
:items="Object.keys(trspTypes).map((key,index) => ({title: key, value: Object.values(trspTypes)[index]}))"
v-model="transportType">
</v-select>
</v-col>
</v-row>
<Http v-if="Transport.type == trspTypes.HTTP" :transport="Transport" />
<WebSocket v-if="Transport.type == trspTypes.WebSocket" :transport="Transport" />
<GRPC v-if="Transport.type == trspTypes.gRPC" :transport="Transport" />
<HttpUpgrade v-if="Transport.type == trspTypes.HTTPUpgrade" :transport="Transport" />
</v-card>
</template>
<script lang="ts">
import { TrspTypes, Transport } from '@/types/transport'
import Http from './transports/Http.vue'
import WebSocket from './transports/WebSocket.vue'
import GRPC from './transports/gRPC.vue'
import HttpUpgrade from './transports/HttpUpgrade.vue'
export default {
props: ['data'],
data() {
return {
trspTypes: TrspTypes
}
},
computed: {
Transport() {
return <Transport>this.$props.data.transport
},
tpEnable: {
get() { return Object.hasOwn(this.$props.data.transport, 'type') },
set(newValue: boolean) { this.$props.data.transport = newValue ? { type: 'http' } : {} }
},
transportType: {
get() { return this.Transport.type },
set(newValue: string) { this.$props.data.transport = { type: newValue } }
}
},
components: { Http, WebSocket, GRPC, HttpUpgrade }
}
</script>
-29
View File
@@ -1,29 +0,0 @@
<template>
<v-select
hide-details
label="UDP over TCP"
:items="versions"
v-model="udp_over_tcp">
</v-select>
</template>
<script lang="ts">
export default {
props: ['data'],
data() {
return {
versions: [
{ title: this.$t('disable'), value: 0 },
{ title: "1", value: 1 },
{ title: "2", value: 2 },
],
}
},
computed: {
udp_over_tcp: {
get():number { return this.$props.data.udp_over_tcp?.version?? 0 },
set(v:number) { this.$props.data.udp_over_tcp = v > 0 ? { enabled: true, version: v } : undefined }
}
}
}
</script>
-35
View File
@@ -1,35 +0,0 @@
<template>
<v-card :subtitle="$t('pages.clients')">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch
v-model="hasUser"
@change="() => {inbound.users = hasUser? [] : undefined}"
color="primary"
:label="$t('in.clients')"
hide-details></v-switch>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
export default {
props: ['inbound'],
data() {
return {
hasUser: false,
}
},
computed: {
cardTitle() {
this.hasUser = Object.hasOwn(this.$props.inbound,'users')
return this.$props.inbound?.type.toUpperCase()
},
},
mounted() {
this.hasUser = Object.hasOwn(this.$props.inbound,'users')
}
}
</script>
-78
View File
@@ -1,78 +0,0 @@
<template>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.addr')"
hide-details
v-model="address">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.port')"
type="number"
min="0"
hide-details
v-model="port">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="KeepAlive"
type="number"
min="0"
hide-details
v-model="data.persistent_keepalive_interval">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6">
<v-text-field v-model="data.public_key" :label="$t('types.wg.pubKey')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-text-field v-model="data.pre_shared_key" :label="$t('types.wg.psk')" hide-details></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6">
<v-text-field v-model="allowed_ips" :label="$t('types.wg.allowedIp') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6">
<v-text-field v-model="reserved" :label="'Reserved ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col>
</v-row>
</template>
<script lang="ts">
import { KeepAlive } from 'vue';
export default {
props: ['data'],
data() {
return {}
},
computed: {
allowed_ips: {
get() { return this.$props.data.allowed_ips?.join(',') },
set(v:string) { this.$props.data.allowed_ips = v.length > 0 ? v.split(',') : undefined }
},
reserved: {
get() { return this.$props.data.reserved?.join(',') },
set(v:string) {
if(!v.endsWith(',')) {
this.$props.data.reserved = v.length > 0 ? v.split(',').map(str => parseInt(str, 10)) : undefined
}
}
},
address: {
get() { return this.$props.data.address },
set(v:string) { this.$props.data.address = v.length > 0 ? v : undefined }
},
port: {
get() { return this.$props.data.port },
set(v:number) { this.$props.data.port = v > 0 ? v : undefined }
}
}
}
</script>
-38
View File
@@ -1,38 +0,0 @@
<template>
<Notivue v-slot="item">
<NotivueSwipe :item="item">
<Notification
:item="item"
:theme="theme"
:dir="direction"
:icons="outlinedIcons"
:hideClose="true"
@click="item.clear"
/>
</NotivueSwipe>
</Notivue>
</template>
<script lang="ts" setup>
import { Notivue, Notification, NotivueSwipe, outlinedIcons, pastelTheme, darkTheme } from 'notivue'
import { computed } from 'vue'
import { useTheme } from 'vuetify'
import vuetify from '@/plugins/vuetify';
const Theme = useTheme()
const theme = computed(() =>{
return Theme.global.name.value == "light" ? pastelTheme : darkTheme
})
const direction = computed(() => {
return vuetify.locale.isRtl ? 'rtl' : 'ltr'
})
</script>
<style>
:root {
--nv-z: 10020;
}
</style>
@@ -1,43 +0,0 @@
<template>
<v-card subtitle="Direct">
<v-row>
<v-col cols="12" sm="6" md="4">
<Network :data="data" />
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.direct.overrideAddr')"
hide-details
v-model="data.override_address">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.direct.overridePort')"
type="number"
min="0"
hide-details
v-model.number="override_port">
</v-text-field>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
export default {
props: ['data'],
data() {
return {}
},
computed: {
override_port: {
get() { return this.$props.data.override_port ? this.$props.data.override_port : ''; },
set(newValue: any) { this.$props.data.override_port = newValue.length == 0 || newValue == 0 ? undefined : parseInt(newValue); }
},
},
components: { Network }
}
</script>
@@ -1,50 +0,0 @@
<template>
<v-card subtitle="HTTP">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.un')"
hide-details
v-model="username">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.pw')"
hide-details
v-model="password">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('transport.path')"
hide-details
v-model="data.path">
</v-text-field>
</v-col>
</v-row>
<Headers :data="data" />
</v-card>
</template>
<script lang="ts">
import Headers from '@/components/Headers.vue';
export default {
props: ['data'],
data() {
return {}
},
computed: {
username: {
get(): string { return this.data.username?.length > 0 ? this.data.username : '' },
set(v:string) { this.data.username = v.length > 0 ? v : undefined },
},
password: {
get(): string { return this.data.password?.length > 0 ? this.data.password : '' },
set(v:string) { this.data.password = v.length > 0 ? v : undefined },
},
},
components: { Headers }
}
</script>
@@ -1,159 +0,0 @@
<template>
<v-card subtitle="Hysteria">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('stats.upload')"
hide-details
type="number"
:suffix="$t('stats.Mbps')"
v-model.number="up_mbps">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('stats.download')"
hide-details
type="number"
:suffix="$t('stats.Mbps')"
min="0"
v-model.number="down_mbps">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.hy.obfs')"
hide-details
v-model="data.obfs">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="direction=='out'">
<v-text-field
:label="$t('types.hy.auth')"
hide-details
v-model="data.auth_str">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="direction=='out'">
<Network :data="data" />
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.disable_mtu_discovery" color="primary" label="Disable MTU discovery" hide-details></v-switch>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="data.recv_window_conn != undefined">
<v-text-field
label="Recv window conn"
hide-details
type="number"
min="0"
v-model.number="data.recv_window_conn">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.recv_window != undefined">
<v-text-field
label="Recv window"
hide-details
type="number"
min="0"
v-model.number="data.recv_window">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.recv_window_client != undefined">
<v-text-field
label="Recv window client"
hide-details
type="number"
min="0"
v-model.number="data.recv_window_client">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.max_conn_client != undefined">
<v-text-field
label="Max conn client"
hide-details
type="number"
min="0"
v-model.number="data.max_conn_client">
</v-text-field>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.hy.hyOptions') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionRsvConn" color="primary" label="Recv window conn" hide-details></v-switch>
</v-list-item>
<v-list-item v-if="direction=='out'">
<v-switch v-model="optionRsvWin" color="primary" label="Recv window" hide-details></v-switch>
</v-list-item>
<v-list-item v-if="direction=='in'">
<v-switch v-model="optionRsvClnt" color="primary" label="Recv window client" hide-details></v-switch>
</v-list-item>
<v-list-item v-if="direction=='in'">
<v-switch v-model="optionMaxConn" color="primary" label="Max conn client" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
export default {
props: ['direction','data'],
data() {
return {
menu: false,
}
},
computed: {
optionRsvConn: {
get(): boolean { return this.$props.data.recv_window_conn != undefined },
set(v:boolean) { this.$props.data.recv_window_conn = v ? 15728640 : undefined }
},
optionRsvWin: {
get(): boolean { return this.$props.data.recv_window != undefined },
set(v:boolean) { this.$props.data.recv_window = v ? 67108864 : undefined }
},
optionRsvClnt: {
get(): boolean { return this.$props.data.recv_window_client != undefined },
set(v:boolean) { this.$props.data.recv_window_client = v ? 67108864 : undefined }
},
optionMaxConn: {
get(): boolean { return this.$props.data.max_conn_client != undefined },
set(v:boolean) { this.$props.data.max_conn_client = v ? 1024 : undefined }
},
down_mbps: {
get() { return this.$props.data.down_mbps ? this.$props.data.down_mbps : 0 },
set(newValue:any) {
if (newValue.length != 0 ){
this.$props.data.down_mbps = newValue
this.$props.data.down = "" + newValue + " Mbps"
} else {
this.$props.data.down_mbps = 0
this.$props.data.down = "0 Mbps"
}
}
},
up_mbps: {
get() { return this.$props.data.up_mbps ? this.$props.data.up_mbps : 0 },
set(newValue:number) { this.$props.data.up_mbps = newValue > 0 ? newValue : 0 }
},
},
components: { Network }
}
</script>
@@ -1,206 +0,0 @@
<template>
<v-card subtitle="Hysteria2">
<v-row v-if="!data.ignore_client_bandwidth">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('stats.upload')"
hide-details
type="number"
:suffix="$t('stats.Mbps')"
min="0"
v-model.number="up_mbps">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('stats.download')"
hide-details
type="number"
:suffix="$t('stats.Mbps')"
min="0"
v-model.number="down_mbps">
</v-text-field>
</v-col>
</v-row>
<template v-if="direction == 'in'">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.ignore_client_bandwidth" color="primary" :label="$t('types.hy.ignoreBw')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.obfs != undefined">
<v-text-field
:label="$t('types.hy.obfs')"
hide-details
v-model="data.obfs.password">
</v-text-field>
</v-col>
</v-row>
<v-card subtitle="Hysteria2 Masquerade" v-if="data.masquerade != undefined">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select v-model="masqueradeType" hide-details :label="$t('type')" :items="masqTypes"></v-select>
</v-col>
<v-col cols="12" sm="8" v-if="masqueradeType == ''">
<v-text-field
label="HTTP3 server on auth fails"
placeholder="file:///var/www | http://127.0.0.1:8080"
v-model="data.masquerade"
hide-details>
</v-text-field>
</v-col>
<v-col cols="12" sm="8" v-if="masqueradeType == 'file'">
<v-text-field
label="File server root directory"
placeholder="/var/www"
v-model="data.masquerade.directory"
hide-details>
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="masqueradeType == 'string'">
<v-text-field
label="HTTP Code"
type="number"
min="100"
max="599"
v-model.number="data.masquerade.status_code"
hide-details>
</v-text-field>
</v-col>
</v-row>
<v-row v-if="masqueradeType == 'proxy'">
<v-col cols="12" sm="6">
<v-text-field
label="Target URL"
placeholder="http://example.com:8080"
v-model="data.masquerade.url"
hide-details>
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Rewrite Host"
placeholder="example.com"
v-model="data.masquerade.rewrite_host"
hide-details>
</v-text-field>
</v-col>
</v-row>
<template v-if="masqueradeType == 'string'">
<v-row>
<v-col cols="12" sm="8">
<v-text-field
label="Content"
v-model="data.masquerade.content"
hide-details>
</v-text-field>
</v-col>
</v-row>
<Headers :data="data.masquerade" />
</template>
</v-card>
</template>
<template v-else>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.pw')"
hide-details
v-model="data.password">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<Network :data="data" />
</v-col>
<v-col cols="12" sm="8" v-if="optionMPort">
<v-text-field
:label="$t('rule.portRange') + ' ' + $t('commaSeparated')"
v-model="server_ports">
</v-text-field>
</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('types.hy.hy2Options') }}</v-btn>
</template>
<v-card>
<v-list>
<template v-if="direction == 'in'">
<v-list-item>
<v-switch v-model="optionObfs" color="primary" :label="$t('types.hy.obfs')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionMasq" color="primary" label="Masquerade" hide-details></v-switch>
</v-list-item>
</template>
<template v-else>
<v-list-item>
<v-switch v-model="optionMPort" color="primary" :label="$t('rule.portRange')" hide-details></v-switch>
</v-list-item>
</template>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
import Headers from '@/components/Headers.vue'
import { i18n } from '@/locales'
export default {
props: ['direction', 'data'],
data() {
return {
menu: false,
masqTypes: [
{ title: i18n.global.t('rule.simple'), value: '' },
{ title: "File server", value: "file" },
{ title: "Reverse Proxy", value: "proxy" },
{ title: "Fixed response", value: "string" },
]
}
},
computed: {
down_mbps: {
get() { return this.$props.data.down_mbps?? 0 },
set(v:number) { this.$props.data.down_mbps = v>0 ? v : undefined }
},
up_mbps: {
get() { return this.$props.data.up_mbps?? 0 },
set(v:number) { this.$props.data.up_mbps = v>0 ? v : undefined }
},
server_ports: {
get() { return this.$props.data.server_ports?.join(',')?? [] },
set(v:string) { this.$props.data.server_ports = v.length > 0 ? v.split(',') : undefined }
},
masqueradeType: {
get() { return typeof this.$props.data.masquerade === 'object' ? this.$props.data.masquerade.type?? '' : '' },
set(v:string) {
if (v == '') {
this.$props.data.masquerade = ''
} else {
this.$props.data.masquerade = { type: v }
}
}
},
optionObfs: {
get(): boolean { return this.$props.data.obfs != undefined },
set(v:boolean) { this.$props.data.obfs = v ? { type: "salamander", password: "" } : undefined }
},
optionMasq: {
get(): boolean { return this.$props.data.masquerade != undefined },
set(v:boolean) { this.$props.data.masquerade = v ? "" : undefined }
},
optionMPort: {
get(): boolean { return this.$props.data.server_ports != undefined },
set(v:boolean) { this.$props.data.server_ports = v ? [] : undefined }
}
},
components: { Network, Headers }
}
</script>
@@ -1,22 +0,0 @@
<template>
<v-card subtitle="Naive">
<v-row>
<v-col cols="12" sm="6" md="4">
<Network :data="inbound" />
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
export default {
props: ['inbound'],
data() {
return {
}
},
components: { Network }
}
</script>
@@ -1,44 +0,0 @@
<template>
<v-card subtitle="ShadowTls">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="[1,2,3]"
:label="$t('version')"
v-model="version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.version > 1">
<v-text-field
:label="$t('types.pw')"
hide-details
v-model="data.password">
</v-text-field>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
export default {
props: ['data'],
data() {
return {}
},
computed: {
version: {
get() { return this.$props.data.version ?? 3 },
set(v: number) {
this.$props.data.version = v
if (v==1) {
delete this.$props.data.password
} else if (this.$props.data.password === undefined ) {
this.$props.data.password = ""
}
}
},
}
}
</script>
@@ -1,46 +0,0 @@
<template>
<v-card subtitle="Selector">
<v-row>
<v-col cols="12" sm="6">
<v-combobox
v-model="data.outbounds"
:items="tags"
:label="$t('pages.outbounds')"
multiple
@update:model-value="updateDefault"
chips
hide-details
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-combobox
v-model="data.default"
:items="data.outbounds"
:label="$t('types.lb.defaultOut')"
clearable
hide-details
></v-combobox>
</v-col>
<v-col cols="12" sm="6">
<v-switch v-model="data.interrupt_exist_connections" color="primary" :label="$t('types.lb.interruptConn')" hide-details></v-switch>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
export default {
props: ['data','tags'],
data() {
return {}
},
methods: {
updateDefault() {
if (!this.$props.data.outbounds?.includes(this.$props.data.default)) {
delete this.$props.data.default
}
}
},
}
</script>
@@ -1,153 +0,0 @@
<template>
<v-card subtitle="ShadowTls">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="[1,2,3]"
:label="$t('version')"
v-model="version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.password != undefined">
<v-text-field
:label="$t('types.pw')"
hide-details
v-model="data.password">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.shdwTls.hs')"
hide-details
v-model="Inbound.handshake.server">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.port')"
type="number"
min="0"
hide-details
v-model.number="server_port">
</v-text-field>
</v-col>
</v-row>
<Dial :dial="Inbound.handshake" :outTags="outTags" />
<v-row v-if="Inbound.handshake_for_server_name != undefined">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.shdwTls.addHS')"
hide-details
append-icon="mdi-plus"
@click:append="addHandshakeServer()"
v-model="handshake_server">
</v-text-field>
</v-col>
</v-row>
<v-card
v-for="(value, key) in Inbound.handshake_for_server_name"
border
density="compact"
style="margin: 5px;"
color="background">
<v-card-title>
<v-row>
<v-col>{{ key }}</v-col>
<v-spacer></v-spacer>
<v-col>
<v-btn @click="Inbound.handshake_for_server_name ? delete Inbound.handshake_for_server_name[key] : null"
icon="mdi-delete"
></v-btn>
</v-col>
</v-row>
</v-card-title>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.shdwTls.hs')"
hide-details
v-model="value.server">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.port')"
type="number"
min="0"
hide-details
v-model.number="value.server_port">
</v-text-field>
</v-col>
</v-row>
<Dial :dial="value" :outTags="outTags" />
</v-card>
</v-card>
</template>
<script lang="ts">
import { ShadowTLS } from '@/types/inbounds'
import Dial from '../Dial.vue'
export default {
props: ['data', 'outTags'],
data() {
return {
handshake_server: ''
}
},
methods: {
addHandshakeServer() {
this.data.handshake_for_server_name[this.handshake_server] = {}
// Clear the input field after adding the server
this.handshake_server = ''
}
},
mounted() {
this.version = this.Inbound.version
},
computed: {
version: {
get() { this.version = this.Inbound.version; return this.Inbound.version; },
set(newValue: any) {
switch (newValue) {
case 1:
delete this.Inbound.password
delete this.Inbound.users
delete this.Inbound.handshake_for_server_name
break;
case 2:
if (!this.Inbound.password) {
this.Inbound.password = ""
}
delete this.Inbound.users
if (!this.Inbound.handshake_for_server_name) {
this.Inbound.handshake_for_server_name = {}
}
break;
case 3:
delete this.Inbound.password
if (!Object.hasOwn(this.Inbound, 'users')) {
this.Inbound.users = []
}
if (!this.Inbound.handshake_for_server_name) {
this.Inbound.handshake_for_server_name = {}
}
break;
}
this.Inbound.version = newValue;
}
},
Inbound(): ShadowTLS {
return <ShadowTLS>this.$props.data;
},
server_port: {
get() { return this.Inbound.handshake.server_port ? this.Inbound.handshake.server_port : 443; },
set(newValue: any) { this.Inbound.handshake.server_port = newValue.length == 0 || newValue == 0 ? 443 : parseInt(newValue); }
},
},
components: { Dial }
}
</script>
@@ -1,67 +0,0 @@
<template>
<v-card subtitle="Shadowsocks">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('in.ssMethod')"
:items="ssMethods"
@update:model-value="direction == 'in' ? changeMethod($event) : undefined"
v-model="data.method">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<Network :data="data" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="direction == 'out'">
<UoT :data="data" />
</v-col>
</v-row>
<v-row v-if="data.method.startsWith('2022') || direction == 'out'">
<v-col cols="12" sm="8">
<v-text-field
v-model="data.password"
:label="$t('types.pw')"
hide-details
:append-inner-icon="direction == 'in' ? 'mdi-refresh' : undefined"
@click:append-inner="changeMethod(data.method)">
</v-text-field>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
import UoT from '@/components/UoT.vue';
import RandomUtil from '@/plugins/randomUtil';
export default {
props: ['direction','data'],
data() {
return {
ssMethods: [
"none",
"aes-128-gcm",
"aes-192-gcm",
"aes-256-gcm",
"chacha20-ietf-poly1305",
"xchacha20-ietf-poly1305",
"2022-blake3-aes-128-gcm",
"2022-blake3-aes-256-gcm",
"2022-blake3-chacha20-poly1305"
]
}
},
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 }
}
</script>
@@ -1,59 +0,0 @@
<template>
<v-card subtitle="SOCKS">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.un')"
hide-details
v-model="username">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.pw')"
hide-details
v-model="password">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="['4','4a','5']"
:label="$t('version')"
v-model="data.version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<Network :data="data" />
</v-col>
<v-col cols="12" sm="6" md="4">
<UoT :data="data" />
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
import UoT from '@/components/UoT.vue';
export default {
props: ['data'],
data() {
return {}
},
computed: {
username: {
get(): string { return this.data.username?.length > 0 ? this.data.username : '' },
set(v:string) { this.data.username = v.length > 0 ? v : undefined },
},
password: {
get(): string { return this.data.password?.length > 0 ? this.data.password : '' },
set(v:string) { this.data.password = v.length > 0 ? v : undefined },
},
},
components: { Network, UoT }
}
</script>
-151
View File
@@ -1,151 +0,0 @@
<template>
<v-card subtitle="SSH">
<template v-if="optionKey">
<v-row>
<v-col cols="auto">
<v-btn-toggle v-model="usePath"
class="rounded-xl"
density="compact"
variant="outlined"
shaped
mandatory>
<v-btn
@click="data.private_key=undefined; data.private_key_path=''"
>{{ $t('tls.usePath') }}</v-btn>
<v-btn
@click="data.private_key_path=undefined; data.private_key=''"
>{{ $t('tls.useText') }}</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-row v-if="usePath == 0">
<v-col cols="12" sm="6">
<v-text-field
:label="$t('tls.keyPath')"
hide-details
v-model="data.private_key_path">
</v-text-field>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12" sm="6">
<v-textarea
:label="$t('tls.key')"
hide-details
v-model="data.private_key">
</v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6">
<v-text-field
:label="$t('types.ssh.passphrase')"
hide-details
v-model="data.private_key_passphrase">
</v-text-field>
</v-col>
</v-row>
</template>
<template v-else>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="data.user" :label="$t('types.un')" hide-details></v-text-field>
</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-row>
</template>
<v-row v-if="optionHostKey">
<v-col cols="12" sm="6">
<v-textarea
:label="$t('types.ssh.hostKey')"
hide-details
v-model="host_key">
</v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="data.host_key_algorithms != undefined">
<v-text-field v-model="algorithms" :label="$t('types.ssh.algorithm') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.client_version != undefined">
<v-text-field v-model="data.client_version" :label="$t('types.ssh.clientVer')" hide-details></v-text-field>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.ssh.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionKey" color="primary" label="SSH Key" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionHostKey" color="primary" :label="$t('types.ssh.hostKey')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionAlgorithms" color="primary" :label="$t('types.ssh.algorithm')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionVer" color="primary" :label="$t('types.ssh.clientVer')" 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: ['data'],
data() {
return {
menu: false,
usePath: 0,
}
},
computed: {
optionKey: {
get(): boolean { return this.data.private_key != undefined || this.data.private_key_path != undefined },
set(v:boolean) {
this.usePath = 0
if (v) {
this.$props.data.private_key_path = ""
delete this.$props.data.user
delete this.$props.data.password
} else {
delete this.$props.data.private_key_path
delete this.$props.data.private_key
delete this.$props.data.private_key_passphrase
}
}
},
optionHostKey: {
get(): boolean { return this.data.host_key != undefined },
set(v:boolean) { this.data.host_key = v ? '' : undefined }
},
optionAlgorithms: {
get(): boolean { return this.data.host_key_algorithms != undefined },
set(v:boolean) { this.data.host_key_algorithms = v ? [] : undefined }
},
optionVer: {
get(): boolean { return this.data.client_version != undefined },
set(v:boolean) { this.data.client_version = v ? 'SSH-2.0-OpenSSH_7.4p1' : undefined }
},
host_key: {
get(): string { return this.$props.data.host_key ? this.$props.data.host_key.join('\n') : '' },
set(v:string) { this.$props.data.host_key = v.split('\n') }
},
algorithms: {
get() { return this.$props.data.host_key_algorithms ? this.$props.data.host_key_algorithms.join(',') : '' },
set(v:string) { this.$props.data.host_key_algorithms = v.length > 0 ? v.split(',') : undefined }
},
},
}
</script>
@@ -1,22 +0,0 @@
<template>
<v-card subtitle="TProxy">
<v-row>
<v-col cols="12" sm="6" md="4">
<Network :data="inbound" />
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
export default {
props: ['inbound'],
data() {
return {
}
},
components: { Network }
}
</script>
-33
View File
@@ -1,33 +0,0 @@
<template>
<v-card subtitle="Tor">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="data.executable_path" :label="$t('types.tor.execPath')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="data.data_directory" :label="$t('types.tor.dataDir')" hide-details></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="extra_args" :label="$t('types.tor.extArgs') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
export default {
props: ['data'],
data() {
return {}
},
computed: {
extra_args: {
get() { return this.$props.data.extra_args?.join(',') },
set(v:string) { this.$props.data.extra_args = v.length > 0 ? v.split(',') : undefined }
},
},
}
</script>
@@ -1,24 +0,0 @@
<template>
<v-card subtitle="Trojan">
<v-row>
<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">
<Network :data="data" />
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
export default {
props: ['data'],
data() {
return {}
},
components: { Network }
}
</script>
@@ -1,89 +0,0 @@
<template>
<v-card subtitle="TUIC">
<v-row v-if="direction == 'out'">
<v-col cols="12" sm="6">
<v-text-field v-model="data.uuid" label="UUID" hide-details></v-text-field>
</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">
<Network :data="data" />
</v-col>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
label="UDP Relay Mode"
:items="['native', 'quic']"
clearable
@click:clear="delete data.udp_relay_mode"
v-model="data.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="data.udp_over_stream" hide-details></v-switch>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('types.tuic.congControl')"
:items="congestion_controls"
v-model="data.congestion_control">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="Zero-RTT Handshake" v-model="data.zero_rtt_handshake" hide-details></v-switch>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="direction == 'in'">
<v-text-field
:label="$t('types.tuic.authTimeout')"
hide-details
type="number"
:suffix="$t('date.s')"
min="1"
v-model.number="auth_timeout">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('types.tuic.hb')"
hide-details
type="number"
:suffix="$t('date.s')"
min="1"
v-model.number="heartbeat">
</v-text-field>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
export default {
props: ['direction', 'data'],
data() {
return {
congestion_controls: [
"cubic","new_reno", "bbr"
]
}
},
computed: {
auth_timeout: {
get() { return this.$props.data.auth_timeout ? parseInt(this.$props.data.auth_timeout.replace('s','')) : '' },
set(newValue:number) { this.$props.data.auth_timeout = newValue ? newValue + 's' : '' }
},
heartbeat: {
get() { return this.$props.data.heartbeat ? parseInt(this.$props.data.heartbeat.replace('s','')) : '' },
set(newValue:number) { this.$props.data.heartbeat = newValue ? newValue + 's' : '' }
}
},
components: { Network }
}
</script>
-62
View File
@@ -1,62 +0,0 @@
<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>
@@ -1,121 +0,0 @@
<template>
<v-card subtitle="URL Test">
<v-row>
<v-col cols="12" sm="6">
<v-combobox
v-model="data.outbounds"
:items="tags"
:label="$t('pages.outbounds')"
multiple
chips
hide-details
></v-combobox>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" v-if="optionUrl">
<v-text-field v-model="data.url" :label="$t('types.lb.testUrl')" hide-details></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="optionInterval">
<v-text-field
:label="$t('types.lb.interval')"
hide-details
type="number"
min="3"
:suffix="$t('date.s')"
v-model.number="interval"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionTolerance">
<v-text-field
:label="$t('types.lb.tolerance')"
hide-details
type="number"
min="0"
:suffix="$t('date.ms')"
v-model.number="tolerance"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionIdle">
<v-text-field
:label="$t('transport.idleTimeout')"
hide-details
type="number"
min="0"
:suffix="$t('date.m')"
v-model.number="idle_timeout"></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6">
<v-switch v-model="data.interrupt_exist_connections" color="primary" :label="$t('types.lb.interruptConn')" hide-details></v-switch>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.lb.urlTestOptions') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionUrl" color="primary" :label="$t('types.lb.testUrl')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionInterval" color="primary" :label="$t('types.lb.interval')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionTolerance" color="primary" :label="$t('types.lb.tolerance')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionIdle" color="primary" :label="$t('transport.idleTimeout')" 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: ['data', 'tags'],
data() {
return {
menu: false,
}
},
computed: {
optionUrl: {
get(): boolean { return this.$props.data.url != undefined },
set(v:boolean) { this.$props.data.url = v ? 'https://www.gstatic.com/generate_204' : undefined }
},
optionInterval: {
get(): boolean { return this.$props.data.interval != undefined },
set(v:boolean) { this.$props.data.interval = v ? '3s' : undefined }
},
optionTolerance: {
get(): boolean { return this.$props.data.tolerance != undefined },
set(v:boolean) { this.$props.data.tolerance = v ? 50 : undefined }
},
optionIdle: {
get(): boolean { return this.$props.data.idle_timeout != undefined },
set(v:boolean) { this.$props.data.idle_timeout = v ? '30m' : undefined }
},
interval: {
get() { return this.$props.data.interval ? parseInt(this.$props.data.interval.replace('s','')) : 3 },
set(v:number) { this.$props.data.interval = v > 0 ? v + 's' : '3s' }
},
tolerance: {
get() { return this.$props.data.tolerance ? parseInt(this.$props.data.tolerance) : 0 },
set(v:number) { this.$props.data.tolerance = v > 0 ? v : 0 }
},
idle_timeout: {
get() { return this.$props.data.idle_timeout ? parseInt(this.$props.data.idle_timeout.replace('m','')) : 30 },
set(v:number) { this.$props.data.idle_timeout = v > 0 ? v + 'm' : '0m' }
}
},
}
</script>
@@ -1,48 +0,0 @@
<template>
<v-card subtitle="VLESS">
<v-row>
<v-col cols="12" sm="6">
<v-text-field v-model="data.uuid" label="UUID" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('types.vless.flow')"
:items="['','xtls-rprx-vision']"
v-model="data.flow">
</v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('types.vless.udpEnc')"
:items="['none','packetaddr','xudp']"
v-model="packet_encoding">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<Network :data="data" />
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
export default {
props: ['data'],
data() {
return {}
},
computed: {
packet_encoding: {
get() { return this.$props.data.packet_encoding != undefined ? this.$props.data.packet_encoding : 'none'; },
set(newValue:string) { this.$props.data.packet_encoding = newValue != "none" ? newValue : undefined }
},
},
components: { Network }
}
</script>
@@ -1,72 +0,0 @@
<template>
<v-card subtitle="VMESS">
<v-row>
<v-col cols="12" sm="6">
<v-text-field v-model="data.uuid" label="UUID" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Alter ID"
hide-details
type="number"
min=0
v-model.number="data.alter_id">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('types.vmess.security')"
:items="securities"
v-model="data.security">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('types.vless.udpEnc')"
:items="['none','packetaddr','xudp']"
v-model="packet_encoding">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<Network :data="data" />
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.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="data.authenticated_length" color="primary" :label="$t('types.vmess.authLen')" hide-details></v-switch>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
export default {
props: ['data'],
data() {
return {
securities: [
"auto",
"none",
"zero",
"aes-128-gcm",
"aes-128-ctr",
"chacha20-poly1305",
]
}
},
computed: {
packet_encoding: {
get() { return this.$props.data.packet_encoding != undefined ? this.$props.data.packet_encoding : 'none'; },
set(newValue:string) { this.$props.data.packet_encoding = newValue != "none" ? newValue : undefined }
},
},
components: { Network }
}
</script>
@@ -1,169 +0,0 @@
<template>
<v-card subtitle="Wireguard">
<v-row>
<v-col cols="12" sm="8">
<v-text-field
v-model="data.private_key"
:label="$t('types.wg.privKey')"
append-icon="mdi-key-star"
@click:append="newKey()"
hide-details>
</v-text-field>
</v-col>
<v-col cols="12" sm="8">
<v-text-field v-model="address" :label="$t('types.wg.localIp') + ' ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('in.port')"
hide-details
type="number"
min=1
v-model.number="data.listen_port">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.udp_timeout != undefined">
<v-text-field
label="UDP Timeout"
hide-details
type="number"
min=0
:suffix="$t('date.m')"
v-model.number="udp_timeout">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="data.workers != undefined">
<v-text-field
:label="$t('types.wg.worker')"
hide-details
type="number"
min=1
v-model.number="data.workers">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.mtu != undefined">
<v-text-field
label="MTU"
hide-details
type="number"
min=0
v-model.number="data.mtu">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.system" color="primary" :label="$t('types.wg.sysIf')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.name != undefined">
<v-text-field
:label="$t('types.wg.ifName')"
hide-details
v-model="data.name">
</v-text-field>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.wg.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionUdp" color="primary" label="UDP Timeout" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionWorker" color="primary" :label="$t('types.wg.worker')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionMtu" color="primary" label="MTU" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionInterface" color="primary" :label="$t('types.wg.ifName')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
<v-card v-if="data.peers != undefined">
<v-card-subtitle>
{{ $t('types.wg.peers') }} <v-icon @click="addPeer" icon="mdi-plus" />
</v-card-subtitle>
<template v-for="(p, index) in data.peers">
<v-card style="margin-top: 1rem;">
<v-card-subtitle>
{{ $t('types.wg.peer') + ' ' + (index+1) }} <v-icon icon="mdi-delete" @click="data.peers.splice(index,1)" v-if="data.peers.length > 1" />
</v-card-subtitle>
<Peer :data="p" />
</v-card>
</template>
</v-card>
</template>
<script lang="ts">
import Peer from '@/components/WgPeer.vue'
export default {
props: ['data'],
emits: ["newWgKey"],
data() {
return {
menu: false,
}
},
methods: {
addPeer() {
this.$props.data.peers.push(this.$props.data.peers[0])
},
newKey() {
this.$emit('newWgKey')
},
},
computed: {
optionUdp: {
get(): boolean { return this.$props.data.udp_timeout != undefined },
set(v:boolean) { this.$props.data.udp_timeout = v ? "5m" : undefined }
},
optionRsrv: {
get(): boolean { return this.$props.data.reserved != undefined },
set(v:boolean) { this.$props.data.reserved = v ? [0,0,0] : undefined }
},
optionWorker: {
get(): boolean { return this.$props.data.workers != undefined },
set(v:boolean) { this.$props.data.workers = v ? 2 : undefined }
},
optionMtu: {
get(): boolean { return this.$props.data.mtu != undefined },
set(v:boolean) { this.$props.data.mtu = v ? 1408 : undefined }
},
optionInterface: {
get(): boolean { return this.$props.data.name != undefined },
set(v:boolean) { this.$props.data.name = v ? "" : undefined }
},
address: {
get() { return this.$props.data.address?.join(',') },
set(v:string) { this.$props.data.address = v.length > 0 ? v.split(',') : undefined }
},
reserved: {
get() { return this.$props.data.reserved?.join(',') },
set(v:string) {
if(!v.endsWith(',')) {
this.$props.data.reserved = v.length > 0 ? v.split(',').map(str => parseInt(str, 10)) : []
}
}
},
udp_timeout: {
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' }
}
},
components: { Peer }
}
</script>
-110
View File
@@ -1,110 +0,0 @@
<script lang="ts" setup>
import { HumanReadable } from '@/plugins/utils';
import { computed } from 'vue';
const props = defineProps({
tilesData: <any>{},
type: String
})
const data = computed(() => {
const d = props.tilesData
if (!d.mem && !d.cpu) return { percent: 0, text: '-' }
switch (props.type) {
case 'g-cpu':
return { percent: d.cpu, text: Math.ceil(d.cpu) + "%" }
case 'g-mem':
const curr = HumanReadable.sizeFormat(d.mem.current,0).split(' ')
const total = HumanReadable.sizeFormat(d.mem.total,0).split(' ')
if (curr[1] == total[1]) curr[1] = ''
return {
percent: Math.ceil(d.mem.current*100/d.mem.total),
text: curr[0] + "<sup>" + (curr[1]?? ' ') + "</sup>/" + total[0] + "<sup>" + (total[1]?? '') + "</sup>"
}
}
return { percent: 0, text: '-'}
})
const cssTransformRotateValue = computed(() => {
const percentageAsFraction = data.value.percent / 100
const halfPercentage = percentageAsFraction / 2
return `${halfPercentage}turn`
})
const gaugeColor = computed(() => {
if (data.value.percent > 90) return 'error'
if (data.value.percent > 70) return 'warning'
return 'primary'
})
</script>
<template>
<div class="gauge__outer">
<div class="gauge__inner">
<div
class="gauge__fill"
:style="{
transform: `rotate(${cssTransformRotateValue})`,
background: `rgb(var(--v-theme-${gaugeColor}))`
}">
</div>
<div class="gauge__cover"><span dir="ltr" v-html="data.text"></span></div>
</div>
</div>
</template>
<style scoped>
.gauge__outer {
width: 100%;
max-width: 250px;
}
.gauge__inner {
width: 100%;
height: 0;
padding-bottom: 50%;
background: rgb(var(--v-theme-surface));
position: relative;
border-top-left-radius: 100% 200%;
border-top-right-radius: 100% 200%;
overflow: hidden;
}
.gauge__fill {
position: absolute;
top: 100%;
left: 0;
width: inherit;
height: 100%;
background: rgb(var(--v-theme-primary));
transform-origin: center top;
transform: rotate(0turn);
transition: transform 0.2s ease-out;
}
.gauge__cover {
width: 75%;
height: 150%;
background: rgb(var(--v-theme-background));
position: absolute;
top: 25%;
left: 50%;
transform: translateX(-50%);
border-radius: 50%;
/* Text */
display: flex;
align-items: center;
justify-content: center;
padding-bottom: 25%;
box-sizing: border-box;
font-family: 'Lexend', sans-serif;
font-weight: bold;
font-size: 32px;
}
sup {
font-size: 16px;
}
</style>
-200
View File
@@ -1,200 +0,0 @@
<template>
<Line v-if="loaded" :data="data" :options="<any>options" />
</template>
<script lang="ts">
import { ref } from 'vue'
import { Line } from 'vue-chartjs'
import {
Chart as ChartJS,
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Filler,
} from 'chart.js'
import { HumanReadable } from '@/plugins/utils'
ChartJS.register(
CategoryScale,
LinearScale,
PointElement,
LineElement,
Title,
Filler
)
ChartJS.defaults.font.family = 'Vazirmatn'
export default {
components: {
Line
},
props: ['tilesData','type'],
data() {
return {
loaded: false,
labels: new Array(20).fill(''),
oldValues: <any>{},
options1: {
animation: false,
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index',
},
plugins: {
tooltip: {
enabled: false
},
legend: {
display: false,
}
},
scales: {
y: {
min: 0,
max: 100,
grid: {
color: () => { return this.$vuetify.theme.current.colors.secondary },
},
beginAtZero: true,
ticks: {
beginAtZero: true,
steps: 10,
stepValue: 5,
max: 100
}
}
}
},
optionsNet: {
animation: false,
responsive: true,
maintainAspectRatio: false,
interaction: {
intersect: false,
mode: 'index',
},
plugins: {
tooltip: {
enabled: false
},
legend: {
display: false,
}
},
scales: {
y: {
grid: {
color: () => { return this.$vuetify.theme.current.colors.secondary },
},
beginAtZero: true,
ticks: {
callback: (label:any, index: number) => { return parseInt(label).toString() },
count: 10
}
}
}
},
data: ref(<any>{})
}
},
computed: {
options() {
switch (this.$props.type){
case "h-net":
this.optionsNet.scales.y.ticks.callback = (label:any, index: number) => {
return label == 0 ? "0" : HumanReadable.sizeFormat(label,0)
}
return this.optionsNet
case "hp-net":
this.optionsNet.scales.y.ticks.callback = (label:any, index: number) => {
return label == 0 ? "0" : HumanReadable.packetFormat(label,0)
}
return this.optionsNet
}
return this.options1
}
},
methods: {
updateData1(value1: number) {
const newData = <number[]>[]
if (this.data.datasets){
newData.push(...this.data.datasets[0].data,value1)
}
if (newData.length>20) newData.shift()
this.data = {
labels: this.labels,
datasets: [
{
label: '',
backgroundColor: 'rgba(255, 165, 0, 0.2)',
borderColor: 'rgba(255, 165, 0,0.8)',
fill: true,
data: newData
}
],
}
this.loaded = true
},
updateData2(value1: number, value2:number) {
const newData1 = <number[]>[]
const newData2 = <number[]>[]
if (this.data.datasets){
newData1.push(...this.data.datasets[0].data,value1)
newData2.push(...this.data.datasets[1].data,value2)
}
if (newData1.length>20) newData1.shift()
if (newData2.length>20) newData2.shift()
this.data = {
labels: this.labels,
datasets: [
{
label: '',
backgroundColor: 'rgba(255, 165, 0, 0.2)',
borderColor: 'rgba(255, 165, 0,0.8)',
fill: true,
data: newData1
},
{
label: '',
backgroundColor: 'rgba(0, 128, 0, 0.1)',
borderColor: 'rgba(0, 128, 0,0.8)',
fill: true,
data: newData2
}
],
}
this.loaded = true
}
},
watch: {
tilesData(v:any) {
switch (this.$props.type) {
case 'h-cpu':
this.updateData1(v.cpu)
break
case 'h-mem':
this.updateData1(v.mem.current*100/v.mem.total)
break
case 'h-net':
if (this.oldValues.sent) {
const downSpeed = (v.net.recv-this.oldValues.recv)/2 // Each 2 sec
const upSpeed = (v.net.sent-this.oldValues.sent)/2 // Each 2 sec
this.updateData2(upSpeed,downSpeed)
}
this.oldValues = v.net
break
case 'hp-net':
if (this.oldValues.psent) {
const downSpeed = (v.net.precv-this.oldValues.precv)/2 // Each 2 sec
const upSpeed = (v.net.psent-this.oldValues.psent)/2 // Each 2 sec
this.updateData2(upSpeed,downSpeed)
}
this.oldValues = v.net
break
}
}
}
}
</script>
-252
View File
@@ -1,252 +0,0 @@
<template>
<v-card subtitle="ACME" style="background-color: inherit;">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
</v-col>
<v-col cols="12" md="8" v-if="enabled">
<v-text-field
:label="$t('rule.domain') + ' ' + $t('commaSeparated')"
hide-details
v-model="domains">
</v-text-field>
</v-col>
</v-row>
<template v-if="enabled">
<v-row>
<v-col cols="12" sm="6" md="4" v-if="optionDir">
<v-text-field
:label="$t('tls.acme.dataDir')"
hide-details
v-model="acme.data_directory">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionDefault">
<v-combobox
v-model="acme.default_server_name"
:items="acme.domain"
:label="$t('tls.acme.defaultDomain')"
hide-details
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionEmail">
<v-text-field
:label="$t('email')"
hide-details
v-model="acme.email">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="optionChallenge">
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.acme.httpChallenge')" v-model="acme.disable_http_challenge" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.acme.tlsChallenge')" v-model="acme.disable_tls_alpn_challenge" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="optionPorts">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.acme.altHport')"
hide-details
type="number"
min=1
max="65532"
v-model.number="acme.alternative_http_port">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.acme.altTport')"
hide-details
type="number"
min=1
max="65532"
v-model.number="acme.alternative_tls_port">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="optionProvider">
<v-col cols="12" sm="6" md="4">
<v-select
v-model="caProvider"
:items="providerList"
:label="$t('tls.acme.caProvider')"
hide-details
></v-select>
</v-col>
<v-col cols="12" md="8" v-if="caProvider == ''">
<v-text-field
:label="$t('tls.acme.customCa')"
hide-details
v-model="acme.provider">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="acme.external_account != undefined">
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Key ID"
hide-details
v-model="acme.external_account.key_id">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="MAC Key"
hide-details
v-model="acme.external_account.mac_key">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="acme.dns01_challenge != undefined">
<v-col cols="12" sm="6" md="4">
<v-select
:label="$t('tls.acme.dns01Provider')"
hide-details
:items="dnsProviders.map(d => d.provider)"
@update:model-value="acme.dns01_challenge = { provider: $event }"
v-model="acme.dns01_challenge.provider">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4"
v-for="item in dnsProviders.filter(d => d.provider == acme.dns01_challenge?.provider)[0]?.params"
:key="item">
<v-text-field
:label="item"
hide-details
v-model="acme.dns01_challenge[item]">
</v-text-field>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.acme.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionDir" color="primary" :label="$t('tls.acme.dataDir')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionDefault" color="primary" :label="$t('tls.acme.defaultDomain')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionEmail" color="primary" :label="$t('email')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionChallenge" color="primary" :label="$t('tls.acme.disableChallenges')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionPorts" color="primary" :label="$t('tls.acme.altPorts')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionProvider" color="primary" :label="$t('tls.acme.caProvider')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionExt" color="primary" :label="$t('tls.acme.extAcc')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionDns01" color="primary" :label="$t('tls.acme.dns01')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</template>
</v-card>
</template>
<script lang="ts">
import { acme } from '@/types/tls'
export default {
props: ['tls'],
data() {
return {
menu: false,
providerList: [
{ title: "Let's Encrypt", value: "letsencrypt" },
{ title: "ZeroSSL", value: "zerossl" },
{ title: "Custom", value: "" }
],
dnsProviders: [
{ provider: "cloudflare", params: [ "api_token" ] },
{ provider: "alidns", params: [ "access_key_id","access_key_secret","region_id" ] }
]
}
},
computed: {
acme() {
return <acme>this.$props.tls.acme
},
enabled: {
get() { return this.acme != undefined },
set(v: boolean) { this.$props.tls.acme = v ? { domain: [] } : undefined }
},
domains: {
get() { return this.acme?.domain ? this.acme.domain.join(',') : "" },
set(v: string) {
if(!v.endsWith(',')) {
this.acme.domain = v.length > 0 ? v.split(',') : []
}
}
},
caProvider: {
get() { return this.acme?.provider && ['letsencrypt','zerossl'].includes(this.acme.provider) ? this.acme?.provider : '' },
set(v: string) { this.acme.provider = ['letsencrypt','zerossl'].includes(v) ? v : 'https://' }
},
optionDir: {
get(): boolean { return this.acme?.data_directory != undefined },
set(v:boolean) { this.acme.data_directory = v ? '' : undefined }
},
optionDefault: {
get(): boolean { return this.acme?.default_server_name != undefined },
set(v:boolean) { this.acme.default_server_name = v ? this.domains.length>0 ? this.domains[0] : '' : undefined }
},
optionEmail: {
get(): boolean { return this.acme?.email != undefined },
set(v:boolean) { this.acme.email = v ? '' : undefined }
},
optionChallenge: {
get(): boolean { return this.acme?.disable_http_challenge != undefined || this.acme?.disable_tls_alpn_challenge != undefined },
set(v:boolean) {
if (v) {
this.acme.disable_http_challenge = false
this.acme.disable_tls_alpn_challenge = false
} else {
delete this.acme.disable_http_challenge
delete this.acme.disable_tls_alpn_challenge
}
}
},
optionPorts: {
get(): boolean { return this.acme?.alternative_http_port != undefined || this.acme?.alternative_tls_port != undefined },
set(v:boolean) {
if (v) {
this.acme.alternative_http_port = 80
this.acme.alternative_tls_port = 443
} else {
delete this.acme.alternative_http_port
delete this.acme.alternative_tls_port
}
}
},
optionProvider: {
get(): boolean { return this.acme?.provider != undefined },
set(v:boolean) { this.acme.provider = v ? 'letsencrypt' : undefined }
},
optionExt: {
get(): boolean { return this.acme?.external_account != undefined },
set(v:boolean) { this.acme.external_account = v ? { key_id: '', mac_key: '' } : undefined }
},
optionDns01: {
get(): boolean { return this.acme?.dns01_challenge != undefined },
set(v:boolean) { this.acme.dns01_challenge = v ? { provider: 'cloudflare' } : undefined }
},
}
}
</script>
-163
View File
@@ -1,163 +0,0 @@
<template>
<v-card subtitle="ECH" style="background-color: inherit;">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
</v-col>
</v-row>
<template v-if="enabled">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="Post-Quantum Schemes" v-model="ech.pq_signature_schemes_enabled" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="Disable Adaptive Size" v-model="ech.dynamic_record_sizing_disabled" hide-details></v-switch>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-btn-toggle v-model="useEchPath"
class="rounded-xl"
density="compact"
variant="outlined"
shaped
mandatory>
<v-btn
@click="delete ech.key"
>{{ $t('tls.usePath') }}</v-btn>
<v-btn
@click="delete ech.key_path"
>{{ $t('tls.useText') }}</v-btn>
</v-btn-toggle>
</v-col>
<v-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-if="useEchPath == 0">
<v-col cols="12">
<v-text-field
:label="$t('tls.keyPath')"
hide-details
v-model="ech.key_path">
</v-text-field>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12">
<v-textarea
:label="$t('tls.key')"
hide-details
v-model="echKeyText">
</v-textarea>
</v-col>
</v-row>
<v-row>
<v-col cols="12">
<v-textarea
:label="$t('tls.cert')"
hide-details
v-model="echConfigText">
</v-textarea>
</v-col>
</v-row>
</template>
</v-card>
</template>
<script lang="ts">
import { i18n } from '@/locales'
import HttpUtils from '@/plugins/httputil'
import { ech } from '@/types/tls'
import { push } from 'notivue'
export default {
props: ['iTls','oTls'],
data() {
return {
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.iTls.ech.pq_signature_schemes_enabled?? false
})
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: {
ech() {
return <ech>this.$props.iTls.ech
},
enabled: {
get() { return this.ech?.enabled?? false },
set(v: boolean) {
this.$props.iTls.ech = v ? { enabled: true } : undefined
this.$props.oTls.ech = v ? {} : undefined
}
},
echKeyText: {
get(): string { return this.ech?.key ? this.ech.key.join('\n') : '' },
set(newValue:string) { this.ech.key = newValue.split('\n') }
},
echConfigText: {
get(): string { return this.oTls.ech?.config ? this.oTls.ech.config.join('\n') : '' },
set(newValue:string) { this.oTls.ech.config = newValue.split('\n') }
},
}
}
</script>
-26
View File
@@ -1,26 +0,0 @@
<template>
<v-card :subtitle="$t('objects.tls')">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('template')"
:items="tlsItems"
v-model="inbound.tls_id">
</v-select>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import { i18n } from '@/locales'
export default {
props: ['inbound', 'tlsConfigs'],
computed: {
tlsItems(): any[] {
return [ { title: i18n.global.t('none'), value: 0 }, ...this.$props.tlsConfigs?.map((t:any) => { return { title: t.name, value: t.id } } )]
}
}
}
</script>
-336
View File
@@ -1,336 +0,0 @@
<template>
<v-card :subtitle="$t('objects.tls')">
<v-row v-if="tlsOptional">
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.enable')" v-model="tlsEnable" hide-details></v-switch>
</v-col>
</v-row>
<template v-if="tls.enabled">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.disableSni')" v-model="disable_sni" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.insecure')" v-model="insecure" hide-details></v-switch>
</v-col>
</v-row>
<template v-if="optionCert">
<v-row>
<v-col cols="auto">
<v-btn-toggle v-model="usePath"
class="rounded-xl"
density="compact"
variant="outlined"
shaped
mandatory>
<v-btn
@click="tls.certificate=undefined; tls.certificate_path=''"
>{{ $t('tls.usePath') }}</v-btn>
<v-btn
@click="tls.certificate_path=undefined; tls.certificate=''"
>{{ $t('tls.useText') }}</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-row v-if="usePath == 0">
<v-col cols="12" sm="6">
<v-text-field
:label="$t('tls.certPath')"
hide-details
v-model="tls.certificate_path">
</v-text-field>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12" sm="6">
<v-textarea
:label="$t('tls.cert')"
hide-details
v-model="tls.certificate">
</v-textarea>
</v-col>
</v-row>
</template>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="tls.server_name != undefined">
<v-text-field
label="SNI"
hide-details
v-model="tls.server_name">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="tls.alpn">
<v-select
hide-details
label="ALPN"
multiple
:items="alpn"
v-model="tls.alpn">
</v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="tls.min_version">
<v-select
hide-details
:label="$t('tls.minVer')"
:items="tlsVersions"
v-model="tls.min_version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="tls.max_version">
<v-select
hide-details
:label="$t('tls.maxVer')"
:items="tlsVersions"
v-model="tls.max_version">
</v-select>
</v-col>
</v-row>
<v-row v-if="tls.cipher_suites != undefined">
<v-col cols="12" md="8">
<v-select
hide-details
:label="$t('tls.cs')"
multiple
:items="cipher_suites"
v-model="tls.cipher_suites">
</v-select>
</v-col>
</v-row>
<v-row v-if="tls.utls != undefined">
<v-col cols="12" md="6">
<v-select
hide-details
label="Fingerprint"
:items="fingerprints"
v-model="tls.utls.fingerprint">
</v-select>
</v-col>
</v-row>
<v-row v-if="tls.reality != undefined">
<v-col cols="12" md="6">
<v-text-field
:label="$t('tls.pubKey')"
hide-details
v-model="tls.reality.public_key">
</v-text-field>
</v-col>
<v-col cols="12" md="4">
<v-text-field
label="Short ID"
hide-details
v-model="tls.reality.short_id">
</v-text-field>
</v-col>
</v-row>
<template v-if="tls.ech != undefined">
<v-row>
<v-col class="v-card-subtitle">ECH</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="Post-Quantum Schemes" v-model="tls.ech.pq_signature_schemes_enabled" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="Disable Adaptive Size" v-model="tls.ech.dynamic_record_sizing_disabled" hide-details></v-switch>
</v-col>
</v-row>
<v-row>
<v-col cols="auto">
<v-btn-toggle v-model="useEchPath"
class="rounded-xl"
density="compact"
variant="outlined"
shaped
mandatory>
<v-btn
@click="delete tls.ech?.config"
>{{ $t('tls.usePath') }}</v-btn>
<v-btn
@click="delete tls.ech?.config_path"
>{{ $t('tls.useText') }}</v-btn>
</v-btn-toggle>
</v-col>
</v-row>
<v-row v-if="useEchPath == 0">
<v-col cols="12" sm="6">
<v-text-field
:label="$t('tls.certPath')"
hide-details
v-model="tls.ech.config_path">
</v-text-field>
</v-col>
</v-row>
<v-row v-else>
<v-col cols="12" sm="6">
<v-textarea
:label="$t('tls.cert')"
hide-details
v-model="echConfigText">
</v-textarea>
</v-col>
</v-row>
</template>
</template>
<v-card-actions v-if="tls.enabled">
<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('tls.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionCert" color="primary" :label="$t('tls.cert')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionSNI" color="primary" label="SNI" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionALPN" color="primary" label="ALPN" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionMinV" color="primary" :label="$t('tls.minVer')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionMaxV" color="primary" :label="$t('tls.maxVer')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionCS" color="primary" :label="$t('tls.cs')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionFP" color="primary" label="UTLS" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionReality" color="primary" label="Reality" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionEch" color="primary" label="ECH" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-card-actions>
</v-card>
</template>
<script lang="ts">
import { oTls, defaultOutTls } from '@/types/tls'
export default {
props: ['outbound'],
data() {
return {
menu: false,
usePath: this.$props.outbound?.tls?.certificate? 1:0,
useEchPath: this.$props.outbound?.tls.ech?.config? 1:0,
defaults: defaultOutTls,
alpn: [
{ title: "H3", value: 'h3' },
{ title: "H2", value: 'h2' },
{ title: "Http/1.1", value: 'http/1.1' },
],
tlsVersions: [ '1.0', '1.1', '1.2', '1.3' ],
cipher_suites: [
{ title: "RSA-AES128-CBC-SHA", value: "TLS_RSA_WITH_AES_128_CBC_SHA" },
{ title: "RSA-AES256-CBC-SHA", value: "TLS_RSA_WITH_AES_256_CBC_SHA" },
{ title: "RSA-AES128-GCM-SHA256", value: "TLS_RSA_WITH_AES_128_GCM_SHA256" },
{ title: "RSA-AES256-GCM-SHA384", value: "TLS_RSA_WITH_AES_256_GCM_SHA384" },
{ title: "AES128-GCM-SHA256", value: "TLS_AES_128_GCM_SHA256" },
{ title: "AES256-GCM-SHA384", value: "TLS_AES_256_GCM_SHA384" },
{ title: "CHACHA20-POLY1305-SHA256", value: "TLS_CHACHA20_POLY1305_SHA256" },
{ title: "ECDHE-ECDSA-AES128-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_128_CBC_SHA" },
{ title: "ECDHE-ECDSA-AES256-CBC-SHA", value: "TLS_ECDHE_ECDSA_WITH_AES_256_CBC_SHA" },
{ title: "ECDHE-RSA-AES128-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_128_CBC_SHA" },
{ title: "ECDHE-RSA-AES256-CBC-SHA", value: "TLS_ECDHE_RSA_WITH_AES_256_CBC_SHA" },
{ title: "ECDHE-ECDSA-AES128-GCM-SHA256", value: "TLS_ECDHE_ECDSA_WITH_AES_128_GCM_SHA256" },
{ title: "ECDHE-ECDSA-AES256-GCM-SHA384", value: "TLS_ECDHE_ECDSA_WITH_AES_256_GCM_SHA384" },
{ title: "ECDHE-RSA-AES128-GCM-SHA256", value: "TLS_ECDHE_RSA_WITH_AES_128_GCM_SHA256" },
{ title: "ECDHE-RSA-AES256-GCM-SHA384", value: "TLS_ECDHE_RSA_WITH_AES_256_GCM_SHA384" },
{ title: "ECDHE-ECDSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_ECDSA_WITH_CHACHA20_POLY1305_SHA256" },
{ title: "ECDHE-RSA-CHACHA20-POLY1305-SHA256", value: "TLS_ECDHE_RSA_WITH_CHACHA20_POLY1305_SHA256" }
],
fingerprints: [
{ title: "Chrome", value: "chrome" },
{ title: "Firefox", value: "firefox" },
{ title: "Microsoft Edge", value: "edge" },
{ title: "Apple Safari", value: "safari" },
{ title: "360", value: "360" },
{ title: "QQ", value: "qq" },
{ title: "Apple IOS", value: "ios" },
{ title: "Android", value: "android" },
{ title: "Random", value: "random" },
{ title: "Randomized", value: "randomized" },
]
}
},
computed: {
tls(): oTls {
return <oTls> this.$props.outbound.tls
},
tlsEnable: {
get() { return Object.hasOwn(this.tls, 'enabled') ? this.tls.enabled : false },
set(newValue: boolean) { this.$props.outbound.tls = newValue ? { enabled: true } : { enabled: false } }
},
disable_sni: {
get() { return this.tls.disable_sni ?? false },
set(newValue: boolean) { this.$props.outbound.tls.disable_sni = newValue ? true : undefined }
},
insecure: {
get() { return this.tls.insecure ?? false },
set(newValue: boolean) { this.$props.outbound.tls.insecure = newValue ? true : undefined }
},
tlsOptional(): boolean {
return !['hysteria','hysteria2','tuic','shadowtls'].includes(this.$props.outbound.type)
},
echConfigText: {
get(): string { return this.tls.ech?.config ? this.tls.ech.config.join('\n') : '' },
set(newValue:string) { if (this.tls.ech) this.tls.ech.config = newValue.split('\n') }
},
optionCert: {
get(): boolean { return this.tls.certificate != undefined || this.tls.certificate_path != undefined },
set(v:boolean) {
this.usePath = 0
if (v) {
this.$props.outbound.tls.certificate_path = ""
} else {
delete this.$props.outbound.tls.certificate_path
delete this.$props.outbound.tls.certificate
}
}
},
optionSNI: {
get(): boolean { return this.tls.server_name != undefined },
set(v:boolean) { this.$props.outbound.tls.server_name = v ? '' : undefined }
},
optionALPN: {
get(): boolean { return this.tls.alpn != undefined },
set(v:boolean) { this.$props.outbound.tls.alpn = v ? defaultOutTls.alpn : undefined }
},
optionMinV: {
get(): boolean { return this.tls.min_version != undefined },
set(v:boolean) { this.$props.outbound.tls.min_version = v ? defaultOutTls.min_version : undefined }
},
optionMaxV: {
get(): boolean { return this.tls.max_version != undefined },
set(v:boolean) { this.$props.outbound.tls.max_version = v ? defaultOutTls.max_version : undefined }
},
optionCS: {
get(): boolean { return this.tls.cipher_suites != undefined },
set(v:boolean) { this.$props.outbound.tls.cipher_suites = v ? defaultOutTls.cipher_suites : undefined }
},
optionFP: {
get(): boolean { return this.tls.utls != undefined },
set(v:boolean) { this.$props.outbound.tls.utls = v ? defaultOutTls.utls : undefined }
},
optionReality: {
get(): boolean { return this.tls.reality != undefined },
set(v:boolean) { this.$props.outbound.tls.reality = v ? defaultOutTls.reality : undefined }
},
optionEch: {
get(): boolean { return this.tls.ech != undefined },
set(v:boolean) { this.$props.outbound.tls.ech = v ? defaultOutTls.ech : undefined }
}
}
}
</script>
@@ -1,82 +0,0 @@
<template>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('transport.hosts')"
hide-details
v-model="hosts">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('transport.path')"
hide-details
v-model="transport.path">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-select
:label="$t('transport.httpMethod')"
hide-details
clearable
@click:clear="delete transport.method"
:items="methodList"
v-model="transport.method">
</v-select>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('transport.idleTimeout')"
hide-details
type="number"
suffix="s"
min="1"
v-model.number="idle_timeout">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('transport.pingTimeout')"
hide-details
type="number"
suffix="s"
min="1"
v-model.number="ping_timeout">
</v-text-field>
</v-col>
</v-row>
<Headers :data="transport" />
</template>
<script lang="ts">
import { HTTP } from '../../types/transport'
import Headers from '../Headers.vue'
export default {
props: ['transport'],
data() {
return {
methodList: ['POST', 'GET', 'PUT', 'PATCH', 'DELETE']
}
},
computed: {
Http(): HTTP {
return <HTTP> this.$props.transport?? {}
},
hosts: {
get() { return this.Http.host ? this.Http.host.join(',') : '' },
set(newValue:string) { this.$props.transport.host = newValue.length>0 ? newValue.split(',') : [] }
},
idle_timeout: {
get() { return this.Http.idle_timeout ? parseInt(this.Http.idle_timeout.replace('s','')) : '' },
set(newValue:number) { this.$props.transport.idle_timeout = newValue ? newValue + 's' : '' }
},
ping_timeout: {
get() { return this.Http.ping_timeout ? parseInt(this.Http.ping_timeout.replace('s','')) : '' },
set(newValue:number) { this.$props.transport.ping_timeout = newValue ? newValue + 's' : '' }
}
},
components: { Headers }
}
</script>

Some files were not shown because too many files have changed in this diff Show More