Compare commits

..

139 Commits

Author SHA1 Message Date
Alireza Ahmadi 96564f1f86 v1.0.0 2024-07-04 22:04:44 +02:00
Alireza Ahmadi cf6b61fe96 fix change detection 2024-07-04 11:29:46 +02:00
Alireza Ahmadi e1aaa3d748 fix sing-box client dns 2024-07-04 11:29:02 +02:00
Alireza Ahmadi 209561497a fix tls and ech auto generate 2024-07-01 00:30:13 +02:00
Alireza Ahmadi 60b374e5d4 upate readme: dev guide #111 2024-06-30 23:29:00 +02:00
Alireza Ahmadi ea8538148f update frontend 2024-06-30 23:28:00 +02:00
Alireza Ahmadi b3a2078ed6 client filter by opacity 2024-06-30 22:32:35 +02:00
Alireza Ahmadi f169064fbc small fixes 2024-06-30 22:32:19 +02:00
Alireza Ahmadi 7d441723ba update qr-code with singbox 2024-06-30 22:30:34 +02:00
Alireza Ahmadi a41140190f clone TLS 2024-06-30 22:29:44 +02:00
Alireza Ahmadi bb5cd91bc9 update translation 2024-06-30 22:29:30 +02:00
Alireza Ahmadi 5b6f6daaa8 add outbound by link #156 2024-06-28 18:17:45 +02:00
Alireza Ahmadi ba06ad598d fix reality utls #173 2024-06-28 16:09:42 +02:00
Alireza Ahmadi 69725ee5af option button better color 2024-06-28 15:59:22 +02:00
Alireza Ahmadi 0d36b811dc omit migration on first install #171 2024-06-28 15:58:15 +02:00
Alireza Ahmadi 6672a2721f subjson and multidomain 2024-06-28 15:55:37 +02:00
Alireza Ahmadi 6b24506ddd Merge pull request #165 from alireza0/dependabot/github_actions/docker/build-push-action-6
Bump docker/build-push-action from 5 to 6
2024-06-18 12:21:14 +02:00
dependabot[bot] 3298fd4e0d Bump docker/build-push-action from 5 to 6
Bumps [docker/build-push-action](https://github.com/docker/build-push-action) from 5 to 6.
- [Release notes](https://github.com/docker/build-push-action/releases)
- [Commits](https://github.com/docker/build-push-action/compare/v5...v6)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-06-17 16:22:20 +00:00
Alireza Ahmadi 6dc7c93030 filter clients #69 2024-06-15 23:26:06 +02:00
Alireza Ahmadi 7c127f07bb small changes 2024-06-15 23:22:23 +02:00
Alireza Ahmadi b5a2dd18f5 add dns resolver #159 2024-06-15 22:10:58 +02:00
Alireza Ahmadi 53ed86c373 better traffic chart #120 2024-06-15 21:53:45 +02:00
Alireza Ahmadi ccbd591b39 cn locales to vuetify standard 2024-06-15 20:11:39 +02:00
Alireza Ahmadi f5792c9d82 show/reset client traffics #129 2024-06-15 12:45:13 +02:00
Alireza Ahmadi 4a2ac30a95 Merge pull request #161 from leic4u/patch-1
Update zhcn.ts
2024-06-15 10:32:48 +02:00
Alireza Ahmadi 45b03d8472 move notification to bottom 2024-06-14 01:05:44 +02:00
Alireza Ahmadi db3270feaa small fixes 2024-06-13 20:16:29 +02:00
Alireza Ahmadi b144aecb6a auto generate cert keys 2024-06-13 20:16:04 +02:00
leic4u 474e5156bb Update zhcn.ts
Updated Simplified Chinese translation
2024-06-13 00:44:50 +08:00
Alireza Ahmadi d057076251 fix client inbounds #154 2024-06-10 17:19:32 +02:00
Alireza Ahmadi 29cc6fd3e3 using migration in docker #153 2024-06-10 16:33:55 +02:00
Alireza Ahmadi dd0f770c1a v0.0.5 2024-06-09 23:03:03 +02:00
Alireza Ahmadi 8e369535bf add migration to install script 2024-06-09 22:51:05 +02:00
Alireza Ahmadi 6a5e0a940b admin page enhancements 2024-06-09 22:48:49 +02:00
Alireza Ahmadi 3b24819309 change snackbar to nitivue 2024-06-09 22:48:32 +02:00
Alireza Ahmadi b5920cdc07 small fixes 2024-06-08 20:55:55 +02:00
Alireza Ahmadi cf620962bb alert on core error 2024-06-08 20:55:42 +02:00
Alireza Ahmadi 17f1126c23 [out] remove proxy from direct 2024-06-08 19:38:24 +02:00
Alireza Ahmadi 16203fdece update frontend 2024-06-08 17:51:24 +02:00
Alireza Ahmadi 12fe21906e show changes feature 2024-06-08 17:49:03 +02:00
Alireza Ahmadi 1b9d5e9378 update + translate log modal 2024-06-08 17:47:32 +02:00
Alireza Ahmadi c152a977c6 fix QrCode 2024-06-08 17:44:22 +02:00
Alireza Ahmadi 341baf69de [log] display logs 2024-06-06 23:01:48 +02:00
Alireza Ahmadi dedd4b3ee3 fix tls modal initiate 2024-06-06 22:08:33 +02:00
Alireza Ahmadi bfbf9777e9 db migration 2024-06-06 22:08:13 +02:00
Alireza Ahmadi 2cabf0aefb [refactor] string values to json 2024-06-06 22:07:26 +02:00
Alireza Ahmadi c994f4b24a add tls 2024-06-06 08:24:08 +02:00
Alireza Ahmadi f136229539 Create FUNDING.yml 2024-06-01 23:54:39 +02:00
Alireza Ahmadi 40fbb22b74 fix transmision in user links 2024-05-29 23:29:43 +02:00
Alireza Ahmadi 9547038164 avoid db check in updates 2024-05-29 23:27:21 +02:00
Alireza Ahmadi aca870e78f fix toggle enable client 2024-05-26 11:31:25 +02:00
Alireza Ahmadi dbf01c2086 fix edit client name #140 2024-05-26 11:31:05 +02:00
Alireza Ahmadi c3debcec5a restart core after crach #132 2024-05-26 10:35:08 +02:00
Alireza Ahmadi c179bf8a37 fix copy to clipboard #132 2024-05-26 10:32:58 +02:00
Alireza Ahmadi 21add1f3ce v0.0.4 2024-05-23 18:52:55 +02:00
Alireza Ahmadi 9968f3885f small fixes 2024-05-23 18:38:50 +02:00
Alireza Ahmadi 2ac13ef8f4 sing-box v1.8.14 2024-05-23 12:01:36 +02:00
Alireza Ahmadi 4900c14295 fix session key 2024-05-23 12:01:04 +02:00
Alireza Ahmadi 55a6d78114 avoid duplicate api call 2024-05-23 12:00:37 +02:00
Alireza Ahmadi caa115bbe3 http transmition interoperability with xray links 2024-05-23 12:00:13 +02:00
Alireza Ahmadi e3be3be9d9 fix users config in non-user based protocols 2024-05-23 11:59:28 +02:00
Alireza Ahmadi 988675a7a7 fix editing in/out tag 2024-05-23 11:57:51 +02:00
Alireza Ahmadi 458f0c20da fix numbers in settings 2024-05-23 11:56:55 +02:00
Alireza Ahmadi f8fbc3c329 fix typo in outbound port 2024-05-23 11:56:31 +02:00
Alireza Ahmadi 89bc3b5b23 fix gauge jumping on update 2024-05-23 11:55:33 +02:00
Alireza Ahmadi edfe0c86e7 [hy2] optional masquerade 2024-05-23 11:55:05 +02:00
Alireza Ahmadi 6865c8b49d fix panel ssl config 2024-05-23 11:54:27 +02:00
Alireza Ahmadi 07947c9665 update frontend 2024-05-23 11:53:27 +02:00
Alireza Ahmadi 09616b6fac update github workflow 2024-05-22 17:51:50 +02:00
Alireza Ahmadi 15105710bc update docker 2024-05-22 17:51:18 +02:00
Alireza Ahmadi c88fb45e27 v0.0.3 2024-05-15 02:08:49 +02:00
Alireza Ahmadi 4f7a90b7dd fix empty ruleset 2024-05-15 02:04:10 +02:00
Alireza Ahmadi cc45e8de20 sing-box v1.8.13 2024-05-15 01:47:35 +02:00
Alireza Ahmadi 5805e397af small fixes 2024-05-15 01:46:19 +02:00
Alireza Ahmadi db36756515 fix tcp type in links #114 2024-05-15 01:45:50 +02:00
Alireza Ahmadi 609132a5b1 avoid duplicate data 2024-05-15 01:44:42 +02:00
Alireza Ahmadi c0aef193ea random client name #109 2024-05-15 01:41:22 +02:00
Alireza Ahmadi 7d39252fec translations #106 2024-05-15 00:08:50 +02:00
Alireza Ahmadi b4fcec9477 small tls fixes 2024-05-14 18:48:39 +02:00
Alireza Ahmadi 8e2023ee66 load detour tags 2024-05-13 15:32:17 +02:00
Alireza Ahmadi 8b6cd88625 basic route configs 2024-05-13 14:26:39 +02:00
Alireza Ahmadi 8272285fe5 Rule and Ruleset modals 2024-05-13 00:05:27 +02:00
Alireza Ahmadi cb606828ff use tags in outbounds 2024-05-13 00:05:27 +02:00
Alireza Ahmadi 70c986662f fix outbound port 2024-05-13 00:05:27 +02:00
Alireza Ahmadi bf7b42ec20 small adjustments 2024-05-09 23:27:37 +02:00
Alireza Ahmadi 43d1aecec7 Outbound modal 2024-05-09 23:27:08 +02:00
Alireza Ahmadi 9a02e6593c optional store traffic time 2024-04-25 21:10:02 +02:00
Alireza Ahmadi abb869c75b add client description 2024-04-25 18:33:07 +02:00
Alireza Ahmadi fa922291ea fix chinese datapicker #105 2024-04-23 19:42:09 +02:00
dependabot[bot] ae7fa7285f Bump golang.org/x/net from 0.21.0 to 0.23.0 in /backend (#103)
Bumps [golang.org/x/net](https://github.com/golang/net) from 0.21.0 to 0.23.0.
- [Commits](https://github.com/golang/net/compare/v0.21.0...v0.23.0)

---
updated-dependencies:
- dependency-name: golang.org/x/net
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
Co-authored-by: dependabot[bot] <49699333+dependabot[bot]@users.noreply.github.com>
2024-04-20 13:42:25 +02:00
Alireza Ahmadi a0e437a549 update readme 2024-04-18 23:09:08 +02:00
Alireza Ahmadi 3d263d500f ultimate docker solution 2024-04-18 22:34:38 +02:00
Alireza Ahmadi c1c05c4863 fix zero inbounds in clients 2024-04-16 23:26:29 +02:00
Alireza Ahmadi af2861f2c9 fix calendar dark theme #86 2024-04-16 23:24:46 +02:00
Alireza Ahmadi 39ad029e20 exclude removed inboundTags #85 2024-04-16 23:19:33 +02:00
Alireza Ahmadi f545bcb30c avoid multiple submit #85 2024-04-16 22:53:21 +02:00
Alireza Ahmadi 819d74cf10 fix hysteria2 up-down change #92 2024-04-16 15:18:30 +02:00
Alireza Ahmadi 68ed8d120f Add Vietnamese Lang #62
Co-Authored-By:  vuong2023 <124447749+vuong2023@users.noreply.github.com>
2024-04-16 14:38:17 +02:00
debian-go 5b804eb149 Added Traditional Chinese language pack (#83)
* Create zhtw.ts

* Update index.ts

* Update README.md for Added Traditional Chinese language pack
2024-04-16 12:43:50 +02:00
vuong2023 6a6d7d7c1a Create vi.ts (#62)
add: Add Vietnamese language option
2024-04-16 12:43:41 +02:00
Trần Nguyễn Tuấn Anh 5726e64b9f workflows: Optimize and reduce binary size (#76)
* workflows: Optimize and reduce binary size

Signed-off-by: dopaemon <polarisdp@gmail.com>

* workflows: add tags osusergo

Signed-off-by: dopaemon <polarisdp@gmail.com>

---------

Signed-off-by: dopaemon <polarisdp@gmail.com>
2024-04-16 12:42:21 +02:00
Alireza Ahmadi 52db4e5332 fix tls cipher_suits 2024-04-16 12:39:22 +02:00
Alireza Ahmadi b673fd9032 Merge pull request #97 from alireza0/dependabot/npm_and_yarn/frontend/vite-4.5.3
Bump vite from 4.5.2 to 4.5.3 in /frontend
2024-04-07 22:22:01 +02:00
dependabot[bot] e80519eac0 Bump vite from 4.5.2 to 4.5.3 in /frontend
Bumps [vite](https://github.com/vitejs/vite/tree/HEAD/packages/vite) from 4.5.2 to 4.5.3.
- [Release notes](https://github.com/vitejs/vite/releases)
- [Changelog](https://github.com/vitejs/vite/blob/v4.5.3/packages/vite/CHANGELOG.md)
- [Commits](https://github.com/vitejs/vite/commits/v4.5.3/packages/vite)

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

Signed-off-by: dependabot[bot] <support@github.com>
2024-04-04 01:51:48 +00:00
Alireza Ahmadi 5371227fb6 Merge pull request #80 from alireza0/dependabot/npm_and_yarn/frontend/follow-redirects-1.15.6
Bump follow-redirects from 1.15.5 to 1.15.6 in /frontend
2024-03-17 02:07:50 +01:00
dependabot[bot] b9a7cefc7c Bump follow-redirects from 1.15.5 to 1.15.6 in /frontend
Bumps [follow-redirects](https://github.com/follow-redirects/follow-redirects) from 1.15.5 to 1.15.6.
- [Release notes](https://github.com/follow-redirects/follow-redirects/releases)
- [Commits](https://github.com/follow-redirects/follow-redirects/compare/v1.15.5...v1.15.6)

---
updated-dependencies:
- dependency-name: follow-redirects
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-17 00:15:35 +00:00
Alireza Ahmadi 13b750f965 Merge pull request #77 from alireza0/dependabot/go_modules/backend/google.golang.org/protobuf-1.33.0
Bump google.golang.org/protobuf from 1.32.0 to 1.33.0 in /backend
2024-03-14 12:37:30 +01:00
dependabot[bot] 4ced272d9a Bump google.golang.org/protobuf from 1.32.0 to 1.33.0 in /backend
Bumps google.golang.org/protobuf from 1.32.0 to 1.33.0.

---
updated-dependencies:
- dependency-name: google.golang.org/protobuf
  dependency-type: indirect
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-03-13 23:36:02 +00:00
Alireza Ahmadi 8b0cd4b89d [bug] fix udp timeout #56 2024-03-04 11:35:23 +01:00
Alireza Ahmadi de43f9779c v0.0.2 2024-03-01 23:20:53 +01:00
MisakaNo の 小破站 d39df72896 Optimize for Simplified Chinese translation (#48)
* feat: Optimize for Simplified Chinese translation

* fix: fix language pack index for zhcn
2024-03-01 18:42:27 +01:00
Alireza Ahmadi cfa5f38177 center align tiles contents 2024-03-01 17:23:45 +01:00
Alireza Ahmadi abcf0bca99 using sing-box 1.8.7 2024-03-01 17:18:43 +01:00
Shahin 5de5d6c3d0 Added Simplified Chinese language pack (#47)
* Added Simplified Chinese language pack

* [i18n] completing ch lang

---------

Co-authored-by: jiulingyun <server@jiulingyun.cn>
2024-03-01 15:54:14 +01:00
Alireza Ahmadi 944f671d96 default db location 2024-03-01 14:36:26 +01:00
Alireza Ahmadi 00ee69e541 [cli] add bbr 2024-03-01 13:39:24 +01:00
Alireza Ahmadi f5b609204e tiny fixes 2024-03-01 13:33:15 +01:00
Alireza Ahmadi edc1038623 add/fix releases 2024-03-01 13:09:22 +01:00
Alireza Ahmadi c687a42d7e [cli] add s-ui menu
Co-Authored-By: Shellgate <128194280+shellgate@users.noreply.github.com>
2024-03-01 10:14:24 +01:00
Alireza Ahmadi bf1759ceda [cmd] initiate 2024-02-29 23:28:29 +01:00
Alireza Ahmadi e404342e2c [setting] add webURI 2024-02-29 23:27:32 +01:00
Alireza Ahmadi 4a3917ea5d vless flow only in tls-tcp 2024-02-28 10:33:09 +01:00
Alireza Ahmadi 037dbbd7a0 load html file as template for production 2024-02-27 23:11:55 +01:00
Alireza Ahmadi 541e54c9a4 upgrade to go 1.22 and dependencies 2024-02-27 22:06:07 +01:00
Alireza Ahmadi 2cc78da07f fix alpn 2024-02-27 21:17:00 +01:00
Alireza Ahmadi 63dc779f68 dev mode requirements 2024-02-27 19:55:00 +01:00
Alireza Ahmadi 9ba611649c styling small changes 2024-02-27 19:53:57 +01:00
Alireza Ahmadi b0817e9c05 change credentials #39 2024-02-27 19:53:12 +01:00
Alireza Ahmadi 8a2407fbd3 random name for changable assets 2024-02-27 11:35:50 +01:00
Alireza Ahmadi 204e0d30e1 fix vless inbound flow 2024-02-27 11:34:18 +01:00
Alireza Ahmadi 19a6053098 auto logout 2024-02-27 01:03:00 +01:00
Alireza Ahmadi e91d8038ad change cookies 2024-02-27 01:01:44 +01:00
Alireza Ahmadi 840b7ba1b1 add ws headers host 2024-02-26 21:59:34 +01:00
Alireza Ahmadi 233c27be23 show usage for infinite clients as well #5 2024-02-26 19:09:26 +01:00
Alireza Ahmadi c54d9c15bc add base url 2024-02-26 19:01:01 +01:00
Alireza Ahmadi 30c9ed6aa7 Merge pull request #37 from alireza0/dependabot/github_actions/svenstaro/upload-release-action-2.9.0
Bump svenstaro/upload-release-action from 2.7.0 to 2.9.0
2024-02-22 19:14:09 +01:00
dependabot[bot] 1bdf4e9c4c Bump svenstaro/upload-release-action from 2.7.0 to 2.9.0
Bumps [svenstaro/upload-release-action](https://github.com/svenstaro/upload-release-action) from 2.7.0 to 2.9.0.
- [Release notes](https://github.com/svenstaro/upload-release-action/releases)
- [Changelog](https://github.com/svenstaro/upload-release-action/blob/master/CHANGELOG.md)
- [Commits](https://github.com/svenstaro/upload-release-action/compare/2.7.0...2.9.0)

---
updated-dependencies:
- dependency-name: svenstaro/upload-release-action
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2024-02-22 17:03:39 +00:00
Alireza Ahmadi 2f0d7a8502 update readme: uninstall 2024-02-20 17:21:17 +01:00
Alireza Ahmadi 99b8cbc75f Update README default info 2024-02-13 08:35:06 +01:00
146 changed files with 13447 additions and 4004 deletions
+1
View File
@@ -0,0 +1 @@
github: alireza0
+57
View File
@@ -0,0 +1,57 @@
name: Sing-box Docker Image CI
on:
workflow_dispatch:
jobs:
build:
runs-on: ubuntu-latest
steps:
- uses: actions/checkout@v4
- name: Get latest release
id: get_release
run: |
latest_release=$(curl -Ls "https://api.github.com/repos/sagernet/sing-box/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
echo "latest_release: $latest_release"
echo "latest_release=$latest_release" >> $GITHUB_OUTPUT
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
alireza7/s-ui-singbox
ghcr.io/alireza0/s-ui-singbox
tags: |
type=sha
type=pep440,pattern=${{ steps.get_release.outputs.latest_release }}
- name: Set up QEMU
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Docker Hub
uses: docker/login-action@v3
with:
username: ${{ secrets.DOCKER_HUB_USERNAME }}
password: ${{ secrets.DOCKER_HUB_TOKEN }}
- name: Login to GHCR
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push
uses: docker/build-push-action@v6
with:
context: core/
push: true
build-args: SINGBOX_VER=${{ steps.get_release.outputs.latest_release }}
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/386
tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }}
+2 -4
View File
@@ -11,8 +11,6 @@ jobs:
steps: steps:
- uses: actions/checkout@v4 - uses: actions/checkout@v4
with:
submodules: true
- name: Docker meta - name: Docker meta
id: meta id: meta
@@ -46,10 +44,10 @@ jobs:
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Build and push - name: Build and push
uses: docker/build-push-action@v5 uses: docker/build-push-action@v6
with: with:
context: . context: .
push: true push: true
platforms: linux/amd64,linux/arm64/v8 platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/386
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}
+30 -16
View File
@@ -10,16 +10,21 @@ jobs:
build: build:
strategy: strategy:
matrix: matrix:
platform: [amd64, arm64, arm] platform:
- amd64
- arm64
- armv7
- 386
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.1.1
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5.0.0 uses: actions/setup-go@v5
with: with:
go-version: '1.21' cache: false
go-version: '1.22'
- name: Setup Node.js - name: Setup Node.js
uses: actions/setup-node@v4 uses: actions/setup-node@v4
@@ -27,13 +32,15 @@ jobs:
node-version: '20' node-version: '20'
registry-url: 'https://registry.npmjs.org' registry-url: 'https://registry.npmjs.org'
- name: Install dependencies for arm64 and arm - name: Install dependencies
if: matrix.platform == 'arm64' || matrix.platform == 'arm'
run: | run: |
sudo apt-get update sudo apt-get update
if [ "${{ matrix.platform }}" == "arm64" ]; then
sudo apt install gcc-aarch64-linux-gnu sudo apt install gcc-aarch64-linux-gnu
if [ "${{ matrix.platform }}" == "arm" ]; then elif [ "${{ matrix.platform }}" == "armv7" ]; then
sudo apt install gcc-arm-linux-gnueabihf sudo apt install gcc-arm-linux-gnueabihf
elif [ "${{ matrix.platform }}" == "386" ]; then
sudo apt install gcc-i686-linux-gnu
fi fi
- name: Build frontend - name: Build frontend
@@ -44,26 +51,33 @@ jobs:
cd .. cd ..
mv frontend/dist backend/web/html mv frontend/dist backend/web/html
- name: Set evironments - name: Build s-ui & singbox
run: | run: |
export CGO_ENABLED=1 export CGO_ENABLED=1
export GOOS=linux export GOOS=linux
export GOARCH=${{ matrix.platform }} export GOARCH=${{ matrix.platform }}
if [ "${{ matrix.platform }}" == "arm64" ]; then if [ "${{ matrix.platform }}" == "arm64" ]; then
export GOARCH=arm64
export CC=aarch64-linux-gnu-gcc export CC=aarch64-linux-gnu-gcc
elif [ "${{ matrix.platform }}" == "arm" ]; then elif [ "${{ matrix.platform }}" == "armv7" ]; then
export GOARCH=arm
export GOARM=7
export CC=arm-linux-gnueabihf-gcc export CC=arm-linux-gnueabihf-gcc
elif [ "${{ matrix.platform }}" == "386" ]; then
export GOARCH=386
export CC=i686-linux-gnu-gcc
fi fi
- name: Build sing-box #### Build Sing-Box
run: | export VERSION=v1.9.3
git clone -b v1.8.5 https://github.com/SagerNet/sing-box git clone -b $VERSION https://github.com/SagerNet/sing-box
cd sing-box cd sing-box
go build -tags with_v2ray_api,with_clash_api,with_grpc,with_quic,with_ech -o sing-box ./cmd/sing-box go build -tags with_quic,with_grpc,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_v2ray_api,with_clash_api,with_gvisor \
-v -trimpath -ldflags "-X 'github.com/sagernet/sing-box/constant.Version=${VERSION}' -s -w -buildid=" \
-o sing-box ./cmd/sing-box
cd .. cd ..
- name: Build s-ui ### Build s-ui
run: |
cd backend cd backend
go build -o ../sui main.go go build -o ../sui main.go
cd .. cd ..
@@ -74,13 +88,13 @@ jobs:
cp sing-box.service s-ui/ cp sing-box.service s-ui/
mkdir s-ui/bin mkdir s-ui/bin
cp sing-box/sing-box s-ui/bin/ cp sing-box/sing-box s-ui/bin/
cp runSingbox.sh s-ui/bin/ cp core/runSingbox.sh s-ui/bin/
- name: Package - name: Package
run: tar -zcvf s-ui-linux-${{ matrix.platform }}.tar.gz s-ui run: tar -zcvf s-ui-linux-${{ matrix.platform }}.tar.gz s-ui
- name: Upload - name: Upload
uses: svenstaro/upload-release-action@2.7.0 uses: svenstaro/upload-release-action@v2
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }} tag: ${{ github.ref }}
+6 -5
View File
@@ -1,19 +1,20 @@
FROM node:alpine as front-builder FROM --platform=$BUILDPLATFORM node:alpine as front-builder
WORKDIR /app WORKDIR /app
COPY frontend/ ./ COPY frontend/ ./
RUN npm install && npm run build RUN npm install && npm run build
FROM golang:1.21-alpine AS backend-builder FROM golang:1.22-alpine AS backend-builder
WORKDIR /app WORKDIR /app
ARG TARGETARCH ARG TARGETARCH
ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE" ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE"
ENV CGO_ENABLED=1 ENV CGO_ENABLED=1
RUN apk --no-cache --update add build-base gcc wget unzip ENV GOARCH=$TARGETARCH
RUN apk update && apk --no-cache --update add build-base gcc wget unzip
COPY backend/ ./ COPY backend/ ./
COPY --from=front-builder /app/dist/ /app/web/html/ COPY --from=front-builder /app/dist/ /app/web/html/
RUN go build -o sui main.go RUN go build -ldflags="-w -s" -o sui main.go
FROM alpine FROM --platform=$TARGETPLATFORM alpine
LABEL org.opencontainers.image.authors="alireza7@gmail.com" LABEL org.opencontainers.image.authors="alireza7@gmail.com"
ENV TZ=Asia/Tehran ENV TZ=Asia/Tehran
WORKDIR /app WORKDIR /app
+87 -4
View File
@@ -2,7 +2,8 @@
**An Advanced Web Panel • Built on SagerNet/Sing-Box** **An Advanced Web Panel • Built on SagerNet/Sing-Box**
![](https://img.shields.io/github/v/release/alireza0/s-ui.svg) ![](https://img.shields.io/github/v/release/alireza0/s-ui.svg)
![](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)
[![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)
@@ -22,10 +23,17 @@
| Multi-Client/Inbound | :heavy_check_mark: | | Multi-Client/Inbound | :heavy_check_mark: |
| Advanced Traffic Routing Interface | :heavy_check_mark: | | Advanced Traffic Routing Interface | :heavy_check_mark: |
| Client & Traffic & System Status | :heavy_check_mark: | | Client & Traffic & System Status | :heavy_check_mark: |
| Subscription Service (link + info) | :heavy_check_mark: | | Subscription Service (link/json + info)| :heavy_check_mark: |
| Dark/Light Theme | :heavy_check_mark: | | Dark/Light Theme | :heavy_check_mark: |
## Default Installation Informarion
- Panel Port: 2095
- Panel Path: /app/
- Subscription Port: 2096
- Subscription Path: /sub/
- User/Passowrd: admin
## Install & Upgrade to Latest Version ## Install & Upgrade to Latest Version
```sh ```sh
@@ -40,6 +48,19 @@ bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.s
bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh) 0.0.1 bash <(curl -Ls https://raw.githubusercontent.com/alireza0/s-ui/master/install.sh) 0.0.1
``` ```
## Uninstall S-UI
```sh
systemctl disable sing-box --now
systemctl disable s-ui --now
rm -f /etc/systemd/system/s-ui.service
rm -f /etc/systemd/system/sing-box.service
systemctl daemon-reload
rm -fr /usr/local/s-ui
```
## Install using Docker ## Install using Docker
<details> <details>
@@ -55,10 +76,20 @@ curl -fsSL https://get.docker.com | sh
**Step 2:** Install S-UI **Step 2:** Install S-UI
> Docker compose method
```shell
mkdir s-ui && cd s-ui
wget -q https://raw.githubusercontent.com/alireza0/s-ui/main/docker-compose.yml
docker compose up -d
```
> Use docker for s-ui only
```shell ```shell
mkdir s-ui && cd s-ui mkdir s-ui && cd s-ui
docker run -itd \ docker run -itd \
-p 2095:2095 -p 443:443 -p 80:80 \ -p 2095:2095 -p 2096:2096 -p 443:443 -p 80:80 \
-v $PWD/db/:/usr/local/s-ui/db/ \ -v $PWD/db/:/usr/local/s-ui/db/ \
-v $PWD/cert/:/root/cert/ \ -v $PWD/cert/:/root/cert/ \
--name s-ui --restart=unless-stopped \ --name s-ui --restart=unless-stopped \
@@ -73,10 +104,63 @@ docker build -t s-ui .
</details> </details>
## Manual run + contribution
<details>
<summary>Click for details</summary>
### Build and run whole project
```shell
./runSUI.sh
```
### - Frontend
Frontend codes are in `frontend` folder in the root of repository.
To run it localy for instant developement you can use (apply automatic changes on file save):
```shell
cd frontend
npm run dev
```
> By this command it will run a `vite` web server on separate port `3000`, with backend proxy to `http://localhost:2095`. You can change it in `frontend/vite.config.mts`.
To build fronend:
```shell
cd frontend
npm run build
```
### - Backend
Backend codes are in `backend` folder in the root of repository.
> Please build fronend once before!
To build backend:
```shell
cd backend
# remove old frontend compiled files
rm -fr web/html/*
# apply new frontend compiled files
cp -R ../frontend/dist/ web/html/
# build
go build -o ../sui main.go
```
To run backend (from root folder of repository):
```shell
./sui
```
</details>
## Languages ## Languages
- English - English
- Farsi - Farsi
- Vietnamese
- Chinese (Simplified)
- Chinese (Traditional)
## Features ## Features
@@ -135,5 +219,4 @@ certbot certonly --standalone --register-unsafely-without-email --non-interactiv
</details> </details>
## Stargazers over Time ## Stargazers over Time
[![Stargazers over time](https://starchart.cc/alireza0/s-ui.svg)](https://starchart.cc/alireza0/s-ui) [![Stargazers over time](https://starchart.cc/alireza0/s-ui.svg)](https://starchart.cc/alireza0/s-ui)
+77 -7
View File
@@ -1,9 +1,9 @@
package api package api
import ( import (
"fmt"
"s-ui/logger" "s-ui/logger"
"s-ui/service" "s-ui/service"
"s-ui/util"
"strconv" "strconv"
"strings" "strings"
@@ -15,6 +15,8 @@ type APIHandler struct {
service.UserService service.UserService
service.ConfigService service.ConfigService
service.ClientService service.ClientService
service.TlsService
service.InDataService
service.PanelService service.PanelService
service.StatsService service.StatsService
service.ServerService service.ServerService
@@ -27,7 +29,8 @@ func NewAPIHandler(g *gin.RouterGroup) {
func (a *APIHandler) initRouter(g *gin.RouterGroup) { func (a *APIHandler) initRouter(g *gin.RouterGroup) {
g.Use(func(c *gin.Context) { g.Use(func(c *gin.Context) {
if c.Request.URL.Path != "/api/login" && c.Request.URL.Path != "/api/logout" { path := c.Request.URL.Path
if !strings.HasSuffix(path, "login") && !strings.HasSuffix(path, "logout") {
checkLogin(c) checkLogin(c)
} }
}) })
@@ -62,9 +65,26 @@ func (a *APIHandler) postHandler(c *gin.Context) {
} }
err = SetLoginUser(c, loginUser) err = SetLoginUser(c, loginUser)
if err == nil {
logger.Info("user ", loginUser, " login success") logger.Info("user ", loginUser, " login success")
} else {
logger.Warning("login failed: ", err)
}
jsonMsg(c, "", nil) 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": case "save":
loginUser := GetLoginUser(c) loginUser := GetLoginUser(c)
data := map[string]string{} data := map[string]string{}
@@ -76,6 +96,10 @@ func (a *APIHandler) postHandler(c *gin.Context) {
case "restartApp": case "restartApp":
err = a.PanelService.RestartPanel(3) err = a.PanelService.RestartPanel(3)
jsonMsg(c, "restartApp", err) jsonMsg(c, "restartApp", err)
case "linkConvert":
link := c.Request.FormValue("link")
result, _, err := util.GetOutbound(link, 0)
jsonObj(c, result, err)
default: default:
jsonMsg(c, "API call", nil) jsonMsg(c, "API call", nil)
} }
@@ -99,6 +123,13 @@ func (a *APIHandler) getHandler(c *gin.Context) {
return return
} }
jsonObj(c, data, nil) jsonObj(c, data, nil)
case "users":
users, err := a.UserService.GetUsers()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonObj(c, *users, nil)
case "setting": case "setting":
data, err := a.SettingService.GetAllSetting() data, err := a.SettingService.GetAllSetting()
if err != nil { if err != nil {
@@ -126,19 +157,45 @@ func (a *APIHandler) getHandler(c *gin.Context) {
case "onlines": case "onlines":
onlines, err := a.StatsService.GetOnlines() onlines, err := a.StatsService.GetOnlines()
jsonObj(c, onlines, err) jsonObj(c, onlines, err)
case "logs":
service := c.Query("s")
count := c.Query("c")
level := c.Query("l")
logs := a.ServerService.GetLogs(service, count, level)
jsonObj(c, logs, nil)
case "changes":
actor := c.Query("a")
chngKey := c.Query("k")
count := c.Query("c")
changes := a.ConfigService.GetChanges(actor, chngKey, count)
jsonObj(c, changes, nil)
case "keypairs":
kType := c.Query("k")
options := c.Query("o")
keypair := a.ServerService.GenKeypair(kType, options)
jsonObj(c, keypair, nil)
default: default:
jsonMsg(c, "API call", nil) jsonMsg(c, "API call", nil)
} }
} }
func (a *APIHandler) loadData(c *gin.Context) (string, error) { func (a *APIHandler) loadData(c *gin.Context) (interface{}, error) {
var data string data := make(map[string]interface{}, 0)
lu := c.Query("lu") lu := c.Query("lu")
isUpdated, err := a.ConfigService.CheckChnages(lu) isUpdated, err := a.ConfigService.CheckChanges(lu)
if err != nil { if err != nil {
return "", err return "", err
} }
onlines, err := a.StatsService.GetOnlines() onlines, err := a.StatsService.GetOnlines()
sysInfo := a.ServerService.GetSingboxInfo()
if sysInfo["running"] == false {
logs := a.ServerService.GetLogs("sing-box", "1", "debug")
if len(logs) > 0 {
data["lastLog"] = logs[0]
}
}
if err != nil { if err != nil {
return "", err return "", err
} }
@@ -151,13 +208,26 @@ func (a *APIHandler) loadData(c *gin.Context) (string, error) {
if err != nil { if err != nil {
return "", err return "", err
} }
tlsConfigs, err := a.TlsService.GetAll()
if err != nil {
return "", err
}
inData, err := a.InDataService.GetAll()
if err != nil {
return "", err
}
subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0]) subURI, err := a.SettingService.GetFinalSubURI(strings.Split(c.Request.Host, ":")[0])
if err != nil { if err != nil {
return "", err return "", err
} }
data = fmt.Sprintf(`{"config": %s,"clients": %s,"subURI": "%s", "onlines": %s}`, string(*config), clients, subURI, onlines) data["config"] = *config
data["clients"] = clients
data["tls"] = tlsConfigs
data["inData"] = inData
data["subURI"] = subURI
data["onlines"] = onlines
} else { } else {
data = fmt.Sprintf(`{"onlines": %s}`, onlines) data["onlines"] = onlines
} }
return data, nil return data, nil
+1 -1
View File
@@ -4,7 +4,7 @@ import (
"encoding/gob" "encoding/gob"
"s-ui/database/model" "s-ui/database/model"
"github.com/gin-contrib/sessions" sessions "github.com/Calidity/gin-sessions"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
+1 -1
View File
@@ -69,7 +69,7 @@ func pureJsonMsg(c *gin.Context, success bool, msg string) {
func checkLogin(c *gin.Context) { func checkLogin(c *gin.Context) {
if !IsLogin(c) { if !IsLogin(c) {
if c.GetHeader("X-Requested-With") == "XMLHttpRequest" { if c.GetHeader("X-Requested-With") == "XMLHttpRequest" {
pureJsonMsg(c, false, "Not authorized") pureJsonMsg(c, false, "Invalid login")
} else { } else {
c.Redirect(http.StatusTemporaryRedirect, "/login") c.Redirect(http.StatusTemporaryRedirect, "/login")
} }
+7 -1
View File
@@ -51,7 +51,13 @@ func (a *APP) Start() error {
if err != nil { if err != nil {
return err return err
} }
err = a.cronJob.Start(loc)
trafficAge, err := a.SettingService.GetTrafficAge()
if err != nil {
return err
}
err = a.cronJob.Start(loc, trafficAge)
if err != nil { if err != nil {
return err return err
} }
+63
View File
@@ -0,0 +1,63 @@
package cmd
import (
"fmt"
"s-ui/config"
"s-ui/database"
"s-ui/service"
)
func resetAdmin() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
userService := service.UserService{}
err = userService.UpdateFirstUser("admin", "admin")
if err != nil {
fmt.Println("reset admin credentials failed:", err)
} else {
fmt.Println("reset admin credentials success")
}
}
func updateAdmin(username string, password string) {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
if username != "" || password != "" {
userService := service.UserService{}
err := userService.UpdateFirstUser(username, password)
if err != nil {
fmt.Println("reset admin credentials failed:", err)
} else {
fmt.Println("reset admin credentials success")
}
}
}
func showAdmin() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
userService := service.UserService{}
userModel, err := userService.GetFirstUser()
if err != nil {
fmt.Println("get current user info failed,error info:", err)
}
username := userModel.Username
userpasswd := userModel.Password
if (username == "") || (userpasswd == "") {
fmt.Println("current username or password is empty")
}
fmt.Println("First admin credentials:")
fmt.Println("\tUsername:\t", username)
fmt.Println("\tPassword:\t", userpasswd)
}
+96
View File
@@ -0,0 +1,96 @@
package cmd
import (
"flag"
"fmt"
"os"
"s-ui/config"
)
func ParseCmd() {
var showVersion bool
flag.BoolVar(&showVersion, "v", false, "show version")
adminCmd := flag.NewFlagSet("admin", flag.ExitOnError)
settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
var username string
var password string
var port int
var path string
var subPort int
var subPath string
var reset bool
var show bool
settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
settingCmd.BoolVar(&show, "show", false, "show current settings")
settingCmd.IntVar(&port, "port", 0, "set panel port")
settingCmd.StringVar(&path, "path", "", "set panel path")
settingCmd.IntVar(&subPort, "subPort", 0, "set sub port")
settingCmd.StringVar(&subPath, "subPath", "", "set sub path")
adminCmd.BoolVar(&show, "show", false, "show first admin credentials")
adminCmd.BoolVar(&reset, "reset", false, "reset first admin credentials")
adminCmd.StringVar(&username, "username", "", "set login username")
adminCmd.StringVar(&password, "password", "", "set login password")
oldUsage := flag.Usage
flag.Usage = func() {
oldUsage()
fmt.Println()
fmt.Println("Commands:")
fmt.Println(" admin set/reset/show first admin credentials")
fmt.Println(" migrate migrate form older version")
fmt.Println(" setting set/reset/show settings")
fmt.Println()
adminCmd.Usage()
fmt.Println()
settingCmd.Usage()
}
flag.Parse()
if showVersion {
fmt.Println(config.GetVersion())
return
}
switch os.Args[1] {
case "admin":
err := adminCmd.Parse(os.Args[2:])
if err != nil {
fmt.Println(err)
return
}
switch {
case show:
showAdmin()
case reset:
resetAdmin()
default:
updateAdmin(username, password)
showAdmin()
}
case "migrate":
migrateDb()
case "setting":
err := settingCmd.Parse(os.Args[2:])
if err != nil {
fmt.Println(err)
return
}
switch {
case show:
showSetting()
case reset:
resetSetting()
default:
updateSetting(port, path, subPort, subPath)
showSetting()
}
default:
fmt.Println("Invalid subcommands")
flag.Usage()
}
}
+105
View File
@@ -0,0 +1,105 @@
package cmd
import (
"encoding/json"
"fmt"
"log"
"os"
"s-ui/config"
"s-ui/database"
"s-ui/database/model"
"strings"
"gorm.io/gorm"
)
func migrateDb() {
// void running on first install
path := config.GetDBPath()
_, err := os.Stat(path)
if err != nil {
return
}
err = database.OpenDB(path)
if err != nil {
log.Fatal(err)
}
db := database.GetDB()
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
fmt.Println("Start migrating database...")
err = migrateClientSchema(tx)
if err != nil {
log.Fatal(err)
}
err = changesObj(tx)
if err != nil {
log.Fatal(err)
}
fmt.Println("Migration done!")
}
func migrateClientSchema(db *gorm.DB) error {
rows, err := db.Raw("PRAGMA table_info(clients)").Rows()
if err != nil {
fmt.Println(err)
return err
}
defer rows.Close()
for rows.Next() {
var (
cid int
cname string
ctype string
notnull int
dfltValue interface{}
pk int
)
rows.Scan(&cid, &cname, &ctype, &notnull, &dfltValue, &pk)
if cname == "config" || cname == "inbounds" || cname == "links" {
if ctype == "text" {
fmt.Printf("Column %s has type TEXT\n", cname)
oldData := make([]struct {
Id uint
Data string
}, 0)
db.Model(model.Client{}).Select("id", cname+" as data").Scan(&oldData)
for _, data := range oldData {
var newData []byte
switch cname {
case "inbounds":
inbounds := strings.Split(data.Data, ",")
newData, _ = json.MarshalIndent(inbounds, " ", " ")
case "config":
jsonData := map[string]interface{}{}
json.Unmarshal([]byte(data.Data), &jsonData)
newData, _ = json.MarshalIndent(jsonData, " ", " ")
case "links":
jsonData := make([]interface{}, 0)
json.Unmarshal([]byte(data.Data), &jsonData)
newData, _ = json.MarshalIndent(jsonData, " ", " ")
}
err = db.Model(model.Client{}).Where("id = ?", data.Id).UpdateColumn(cname, newData).Error
if err != nil {
return err
}
}
}
}
}
db.AutoMigrate(model.Client{})
return nil
}
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
}
+105
View File
@@ -0,0 +1,105 @@
package cmd
import (
"fmt"
"s-ui/config"
"s-ui/database"
"s-ui/service"
)
func resetSetting() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
settingService := service.SettingService{}
err = settingService.ResetSettings()
if err != nil {
fmt.Println("reset setting failed:", err)
} else {
fmt.Println("reset setting success")
}
}
func updateSetting(port int, path string, subPort int, subPath string) {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
settingService := service.SettingService{}
if port > 0 {
err := settingService.SetPort(port)
if err != nil {
fmt.Println("set port failed:", err)
} else {
fmt.Println("set port success")
}
}
if path != "" {
err := settingService.SetWebPath(path)
if err != nil {
fmt.Println("set path failed:", err)
} else {
fmt.Println("set path success")
}
}
if subPort > 0 {
err := settingService.SetSubPort(subPort)
if err != nil {
fmt.Println("set sub port failed:", err)
} else {
fmt.Println("set sub port success")
}
}
if subPath != "" {
err := settingService.SetSubPath(subPath)
if err != nil {
fmt.Println("set sub path failed:", err)
} else {
fmt.Println("set sub path success")
}
}
}
func showSetting() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
settingService := service.SettingService{}
allSetting, err := settingService.GetAllSetting()
if err != nil {
fmt.Println("get current port failed,error info:", err)
}
fmt.Println("Current panel settings:")
fmt.Println("\tPanel port:\t", (*allSetting)["webPort"])
fmt.Println("\tPanel path:\t", (*allSetting)["webPath"])
if (*allSetting)["webListen"] != "" {
fmt.Println("\tPanel IP:\t", (*allSetting)["webListen"])
}
if (*allSetting)["webDomain"] != "" {
fmt.Println("\tPanel Domain:\t", (*allSetting)["webDomain"])
}
if (*allSetting)["webURI"] != "" {
fmt.Println("\tPanel URI:\t", (*allSetting)["webURI"])
}
fmt.Println()
fmt.Println("Current subscription settings:")
fmt.Println("\tSub port:\t", (*allSetting)["subPort"])
fmt.Println("\tSub path:\t", (*allSetting)["subPath"])
if (*allSetting)["subListen"] != "" {
fmt.Println("\tSub IP:\t", (*allSetting)["subListen"])
}
if (*allSetting)["subDomain"] != "" {
fmt.Println("\tSub Domain:\t", (*allSetting)["subDomain"])
}
if (*allSetting)["subURI"] != "" {
fmt.Println("\tSub URI:\t", (*allSetting)["subURI"])
}
}
+5 -1
View File
@@ -59,7 +59,7 @@ func GetBinFolderPath() string {
func GetDBFolderPath() string { func GetDBFolderPath() string {
dbFolderPath := os.Getenv("SUI_DB_FOLDER") dbFolderPath := os.Getenv("SUI_DB_FOLDER")
if dbFolderPath == "" { if dbFolderPath == "" {
dbFolderPath = "db" dbFolderPath = "/usr/local/s-ui/db"
} }
return dbFolderPath return dbFolderPath
} }
@@ -69,6 +69,10 @@ func GetDBPath() string {
} }
func GetDefaultConfig() string { func GetDefaultConfig() string {
apiEnv := GetEnvApi()
if len(apiEnv) > 0 {
return strings.Replace(defaultConfig, "127.0.0.1:1080", apiEnv, 1)
}
return defaultConfig return defaultConfig
} }
+1 -1
View File
@@ -1 +1 @@
0.0.1 1.0.0
+2 -2
View File
@@ -14,7 +14,7 @@ func NewCronJob() *CronJob {
return &CronJob{} return &CronJob{}
} }
func (c *CronJob) Start(loc *time.Location) error { func (c *CronJob) Start(loc *time.Location, trafficAge int) error {
c.cron = cron.New(cron.WithLocation(loc), cron.WithSeconds()) c.cron = cron.New(cron.WithLocation(loc), cron.WithSeconds())
c.cron.Start() c.cron.Start()
@@ -24,7 +24,7 @@ func (c *CronJob) Start(loc *time.Location) error {
// Start expiry job // Start expiry job
c.cron.AddJob("@every 1m", NewDepleteJob()) c.cron.AddJob("@every 1m", NewDepleteJob())
// Start deleting old stats // Start deleting old stats
c.cron.AddJob("@daily", NewDelStatsJob()) c.cron.AddJob("@daily", NewDelStatsJob(trafficAge))
}() }()
return nil return nil
+7 -3
View File
@@ -7,16 +7,20 @@ import (
type DelStatsJob struct { type DelStatsJob struct {
service.StatsService service.StatsService
trafficAge int
} }
func NewDelStatsJob() *DelStatsJob { func NewDelStatsJob(ta int) *DelStatsJob {
return &DelStatsJob{} return &DelStatsJob{
trafficAge: ta,
}
} }
func (s *DelStatsJob) Run() { func (s *DelStatsJob) Run() {
err := s.StatsService.DelOldStats(30) err := s.StatsService.DelOldStats(s.trafficAge)
if err != nil { if err != nil {
logger.Warning("Deleting old statistics failed: ", err) logger.Warning("Deleting old statistics failed: ", err)
return return
} }
logger.Debug("Stats older than ", s.trafficAge, " days were deleted")
} }
+8 -1
View File
@@ -29,7 +29,7 @@ func initUser() error {
return nil return nil
} }
func InitDB(dbPath string) error { func OpenDB(dbPath string) error {
dir := path.Dir(dbPath) dir := path.Dir(dbPath)
err := os.MkdirAll(dir, 01740) err := os.MkdirAll(dir, 01740)
if err != nil { if err != nil {
@@ -48,12 +48,19 @@ func InitDB(dbPath string) error {
Logger: gormLogger, Logger: gormLogger,
} }
db, err = gorm.Open(sqlite.Open(dbPath), c) db, err = gorm.Open(sqlite.Open(dbPath), c)
return err
}
func InitDB(dbPath string) error {
err := OpenDB(dbPath)
if err != nil { if err != nil {
return err return err
} }
err = db.AutoMigrate( err = db.AutoMigrate(
&model.Setting{}, &model.Setting{},
&model.Tls{},
&model.InboundData{},
&model.User{}, &model.User{},
&model.Stats{}, &model.Stats{},
&model.Client{}, &model.Client{},
+19 -3
View File
@@ -8,6 +8,21 @@ type Setting struct {
Value string `json:"value" form:"value"` Value string `json:"value" form:"value"`
} }
type Tls struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Name string `json:"name" form:"name"`
Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
Server json.RawMessage `json:"server" form:"server"`
Client json.RawMessage `json:"client" form:"client"`
}
type InboundData struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Tag string `json:"tag" form:"tag"`
Addrs json.RawMessage `json:"addrs" form:"addrs"`
OutJson json.RawMessage `json:"outJson" form:"outJson"`
}
type User struct { type User struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username" form:"username"` Username string `json:"username" form:"username"`
@@ -19,13 +34,14 @@ type Client struct {
Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` Id uint `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Enable bool `json:"enable" form:"enable"` Enable bool `json:"enable" form:"enable"`
Name string `json:"name" form:"name"` Name string `json:"name" form:"name"`
Config string `json:"config" form:"config"` Config json.RawMessage `json:"config" form:"config"`
Inbounds string `json:"inbounds" form:"inbounds"` Inbounds json.RawMessage `json:"inbounds" form:"inbounds"`
Links string `json:"links" form:"links"` Links json.RawMessage `json:"links" form:"links"`
Volume int64 `json:"volume" form:"volume"` Volume int64 `json:"volume" form:"volume"`
Expiry int64 `json:"expiry" form:"expiry"` Expiry int64 `json:"expiry" form:"expiry"`
Down int64 `json:"down" form:"down"` Down int64 `json:"down" form:"down"`
Up int64 `json:"up" form:"up"` Up int64 `json:"up" form:"up"`
Desc string `json:"desc" from:"desc"`
} }
type Stats struct { type Stats struct {
+18 -21
View File
@@ -1,23 +1,23 @@
module s-ui module s-ui
go 1.21.5 go 1.22.0
require ( require (
github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/gzip v0.0.6
github.com/gin-contrib/sessions v0.0.5
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/v2fly/v2ray-core/v5 v5.13.0
gorm.io/driver/sqlite v1.5.5 gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.7 gorm.io/gorm v1.25.7
) )
require ( require (
github.com/adrg/xdg v0.4.0 // indirect github.com/adrg/xdg v0.4.0 // indirect
github.com/bytedance/sonic v1.10.2 // indirect github.com/bytedance/sonic v1.11.1 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect
github.com/gabriel-vasile/mimetype v1.4.3 // indirect github.com/gabriel-vasile/mimetype v1.4.3 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/goccy/go-json v0.10.2 // indirect github.com/goccy/go-json v0.10.2 // indirect
@@ -26,42 +26,39 @@ require (
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.2 // indirect github.com/gorilla/sessions v1.2.2 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/cpuid/v2 v2.2.7 // indirect
github.com/leodido/go-urn v1.4.0 // indirect github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 // indirect github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a // indirect
github.com/mattn/go-isatty v0.0.20 // indirect github.com/mattn/go-isatty v0.0.20 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect github.com/modern-go/reflect2 v1.0.2 // indirect
github.com/pires/go-proxyproto v0.7.0 // indirect github.com/pires/go-proxyproto v0.7.0 // indirect
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/shoenig/go-m1cpu v0.1.6 // indirect github.com/shoenig/go-m1cpu v0.1.6 // indirect
github.com/tklauser/go-sysconf v0.3.12 // indirect github.com/tklauser/go-sysconf v0.3.13 // indirect
github.com/tklauser/numcpus v0.6.1 // indirect github.com/tklauser/numcpus v0.7.0 // indirect
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
github.com/ugorji/go/codec v1.2.12 // indirect github.com/ugorji/go/codec v1.2.12 // indirect
github.com/yusufpapurcu/wmi v1.2.3 // indirect github.com/yusufpapurcu/wmi v1.2.4 // indirect
golang.org/x/arch v0.7.0 // indirect golang.org/x/arch v0.7.0 // indirect
golang.org/x/crypto v0.18.0 // indirect golang.org/x/crypto v0.21.0 // indirect
golang.org/x/mod v0.14.0 // indirect golang.org/x/net v0.23.0 // indirect
golang.org/x/net v0.20.0 // indirect golang.org/x/sys v0.18.0 // indirect
golang.org/x/sync v0.6.0 // indirect
golang.org/x/sys v0.16.0 // indirect
golang.org/x/text v0.14.0 // indirect golang.org/x/text v0.14.0 // indirect
golang.org/x/tools v0.17.0 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 // indirect google.golang.org/protobuf v1.33.0 // indirect
google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )
require ( require (
github.com/Calidity/gin-sessions v1.3.1
github.com/gin-contrib/sse v0.1.0 // indirect github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-playground/validator/v10 v10.17.0 // indirect github.com/go-playground/validator/v10 v10.18.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect github.com/mattn/go-sqlite3 v2.0.3+incompatible // indirect
github.com/pelletier/go-toml/v2 v2.1.1 // indirect github.com/pelletier/go-toml/v2 v2.1.1 // indirect
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.24.1 github.com/shirou/gopsutil/v3 v3.24.1
github.com/v2fly/v2ray-core/v5 v5.13.0 google.golang.org/grpc v1.62.0
google.golang.org/grpc v1.61.0
) )
+38 -29
View File
@@ -1,3 +1,5 @@
github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE=
github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns=
github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls= github.com/adrg/xdg v0.4.0 h1:RzRqFcjH4nE5C6oTAxhBtoE2IRyjBSa62SCbyPidvls=
github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E= github.com/adrg/xdg v0.4.0/go.mod h1:N6ag73EX4wyxeaoeHctc1mas01KZgsj5tYiAIwqJE/E=
github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY= github.com/aead/cmac v0.0.0-20160719120800-7af84192f0b1 h1:+JkXLHME8vLJafGhOH4aoV2Iu8bR55nU6iKMVfYVLjY=
@@ -12,8 +14,8 @@ github.com/bufbuild/protocompile v0.6.0 h1:Uu7WiSQ6Yj9DbkdnOe7U4mNKp58y9WDMKDn28
github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE= github.com/bufbuild/protocompile v0.6.0/go.mod h1:YNP35qEYoYGme7QMtz5SBCoN4kL4g12jTtjuzRNdjpE=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM= github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM= github.com/bytedance/sonic v1.10.0-rc/go.mod h1:ElCzW+ufi8qKqNW0FY314xriJhyJhuoJ3gFZdAHF7NM=
github.com/bytedance/sonic v1.10.2 h1:GQebETVBxYB7JGWJtLBi07OVzWwt+8dWA00gEVW2ZFE= github.com/bytedance/sonic v1.11.1 h1:JC0+6c9FoWYYxakaoa+c5QTtJeiSZNeByOBhXtAFSn4=
github.com/bytedance/sonic v1.10.2/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4= github.com/bytedance/sonic v1.11.1/go.mod h1:iZcSUejdk5aukTND/Eu/ivjQuEL0Cu9/rf50Hi0u/g4=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY= github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk= github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0= github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d h1:77cEq6EriyTZ0g/qfRdp61a3Uu/AWrgIq2s0ClJV1g0=
@@ -37,8 +39,6 @@ github.com/gaukas/godicttls v0.0.4 h1:NlRaXb3J6hAnTmWdsEKb9bcSBD6BvcIjdGdeb0zfXb
github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI= github.com/gaukas/godicttls v0.0.4/go.mod h1:l6EenT4TLWgTdwslVb4sEMOCf7Bv0JAK67deKr9/NCI=
github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4= github.com/gin-contrib/gzip v0.0.6 h1:NjcunTcGAj5CO1gn4N8jHOSIeRFHIbn51z6K+xaN4d4=
github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk= github.com/gin-contrib/gzip v0.0.6/go.mod h1:QOJlmV2xmayAjkNS2Y8NQsMneuRShOU/kjovCXNuzzk=
github.com/gin-contrib/sessions v0.0.5 h1:CATtfHmLMQrMNpJRgzjWXD7worTh7g7ritsQfmF+0jE=
github.com/gin-contrib/sessions v0.0.5/go.mod h1:vYAuaUPqie3WUSsft6HUlCjlwwoJQs97miaG2+7neKY=
github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE= github.com/gin-contrib/sse v0.1.0 h1:Y/yl/+YNO8GZSjAhjMsSuLt29uWRFHdHYUb5lYOV9qE=
github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI= github.com/gin-contrib/sse v0.1.0/go.mod h1:RHrZQHXnP2xjPF+u1gW/2HnVO7nvIa9PG3Gm+fLHvGI=
github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk= github.com/gin-gonic/gin v1.8.1/go.mod h1:ji8BvRH1azfM+SYow9zQ6SZMvR8qOMZHmsCuWR9tTTk=
@@ -48,8 +48,9 @@ github.com/go-chi/chi/v5 v5.0.10 h1:rLz5avzKpjqxrYwXNfmjkrYYXOyLJd37pz53UFHC6vk=
github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8= github.com/go-chi/chi/v5 v5.0.10/go.mod h1:DslCQbL2OYiznFReuXYUmQ2hGd1aDpCnlMNITLSKoi8=
github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4= github.com/go-chi/render v1.0.3 h1:AsXqd2a1/INaIfUSKq3G5uA8weYx20FOsM7uSoCyyt4=
github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0= github.com/go-chi/render v1.0.3/go.mod h1:/gr3hVkmYR0YlEy3LxCuVRFzEu9Ruok+gFqbIofjao0=
github.com/go-ole/go-ole v1.2.6 h1:/Fpf6oFPoeFik9ty7siob0G6Ke8QvQEuVcuChpwXzpY=
github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0= github.com/go-ole/go-ole v1.2.6/go.mod h1:pprOEPIfldk/42T2oK7lQ4v4JSDwmV0As9GaiUsvbm0=
github.com/go-ole/go-ole v1.3.0 h1:Dt6ye7+vXGIKZ7Xtk4s6/xVdGDQynvom7xCFEdWr6uE=
github.com/go-ole/go-ole v1.3.0/go.mod h1:5LS6F96DhAwUc7C+1HLexzMXY1xGRSryjyPPKW6zv78=
github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.0.1/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s= github.com/go-playground/assert/v2 v2.2.0 h1:JvknZsQTYeFEAhQwI4qEt9cyV5ONwRHC+lYKSsYSR8s=
github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4= github.com/go-playground/assert/v2 v2.2.0/go.mod h1:VDjEfimB/XKnb+ZQfWdccd7VUvScMdVu0Titje2rxJ4=
@@ -60,8 +61,8 @@ github.com/go-playground/universal-translator v0.18.0/go.mod h1:UvRDBj+xPUEGrFYl
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos= github.com/go-playground/validator/v10 v10.10.0/go.mod h1:74x4gJWsvQexRdW8Pn3dXSGrTK4nAUsbPlLADvpJkos=
github.com/go-playground/validator/v10 v10.17.0 h1:SmVVlfAOtlZncTxRuinDPomC2DkXJ4E5T9gDA0AIH74= github.com/go-playground/validator/v10 v10.18.0 h1:BvolUXjp4zuvkZ5YN5t7ebzbhlUtPsPm2S9NAZ5nl9U=
github.com/go-playground/validator/v10 v10.17.0/go.mod h1:9iXMNT7sEkjXb0I+enO7QXmzG6QCsPWY4zveKFVRSyU= github.com/go-playground/validator/v10 v10.18.0/go.mod h1:dbuPbCMFw/DrkbEynArYaCwl3amGuJotoKCe95atGMM=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 h1:tfuBGBXKqDEevZMzYi5KSi8KkcZtzBcTgAUUtapy0OI=
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls= github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572/go.mod h1:9Pwr4B2jHnOSGXyyzV8ROjYa2ojvAY6HCGYYfMoC3Ls=
github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I= github.com/goccy/go-json v0.9.7/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MGFi0w8I=
@@ -105,8 +106,8 @@ github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHm
github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I= github.com/klauspost/compress v1.16.7 h1:2mk3MPGNzKyxErAw8YaohYh69+pa4sIQSC0fPGCFR9I=
github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE= github.com/klauspost/compress v1.16.7/go.mod h1:ntbaceVETuRiXiv4DpjP66DpAtAGkEQskQzEyD//IeE=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
github.com/klauspost/reedsolomon v1.11.7 h1:9uaHU0slncktTEEg4+7Vl7q7XUNMBUOK4R9gnKhMjAU= github.com/klauspost/reedsolomon v1.11.7 h1:9uaHU0slncktTEEg4+7Vl7q7XUNMBUOK4R9gnKhMjAU=
github.com/klauspost/reedsolomon v1.11.7/go.mod h1:4bXRN+cVzMdml6ti7qLouuYi32KHJ5MGv0Qd8a47h6A= github.com/klauspost/reedsolomon v1.11.7/go.mod h1:4bXRN+cVzMdml6ti7qLouuYi32KHJ5MGv0Qd8a47h6A=
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M= github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
@@ -121,8 +122,9 @@ github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY= github.com/leodido/go-urn v1.2.1/go.mod h1:zt4jvISO2HfUBqxjfIshjdMTYS56ZS/qv49ictyFfxY=
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ= github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI= github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0 h1:6E+4a0GO5zZEnZ81pIr0yLvtUWk2if982qA3F3QD6H4=
github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I= github.com/lufia/plan9stats v0.0.0-20211012122336-39d0f177ccd0/go.mod h1:zJYVVT2jmtg6P3p1VtQj7WsuWi/y4VnjVBn7F8KPB3I=
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a h1:3Bm7EwfUQUvhNeKIkUct/gl9eod1TcXuj8stxvi/GoI=
github.com/lufia/plan9stats v0.0.0-20240226150601-1dcf7310316a/go.mod h1:ilwx/Dta8jXAgpFYFvSWEMwxmbWXyiUHkd5FwyKhb5k=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40 h1:EnfXoSqDfSNJv0VBNqY/88RNnhSGYkrHaO0mmFGbVsc=
github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg= github.com/lunixbochs/struc v0.0.0-20200707160740-784aaebc1d40/go.mod h1:vy1vK6wD6j7xX6O6hXe621WabdtNkou2h7uRtTfRMyg=
github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94= github.com/mattn/go-isatty v0.0.14/go.mod h1:7GGIvUiUoEMVVmxf/4nioHXj79iQHKdU27kJ6hsGG94=
@@ -165,8 +167,9 @@ github.com/pires/go-proxyproto v0.7.0/go.mod h1:Vz/1JPY/OACxWGQNIRY2BeyDmpoaWmEP
github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA= github.com/pkg/diff v0.0.0-20210226163009-20ebb0f2a09e/go.mod h1:pJLUxLENpZxwdsKMEsNbx1VGcRFpLqf3715MtcvvzbA=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c h1:ncq/mPwQF4JjgDlrVEn3C11VoGHZN7m8qihwgMEtzYw=
github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs= github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k= github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw= github.com/quic-go/quic-go v0.40.0 h1:GYd1iznlKm7dpHD7pOVpUvItgMPo/jrMgDWZhMCecqw=
@@ -188,6 +191,7 @@ github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5
github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU= github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k= github.com/shoenig/test v0.6.4/go.mod h1:byHiCGXqrVaflBLAMq/srcZIHynQPQgeyvkvXnjqq0k=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw= github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
@@ -200,10 +204,12 @@ github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4= github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk= github.com/stretchr/testify v1.8.4 h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo= github.com/stretchr/testify v1.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
github.com/tklauser/go-sysconf v0.3.12 h1:0QaGUFOdQaIVdPgfITYzaTegZvdCjmYO52cSFAEVmqU=
github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI= github.com/tklauser/go-sysconf v0.3.12/go.mod h1:Ho14jnntGE1fpdOqQEEaiKRpvIavV0hSfmBq8nJbHYI=
github.com/tklauser/numcpus v0.6.1 h1:ng9scYS7az0Bk4OZLvrNXNSAO2Pxr1XXRAPyjhIx+Fk= github.com/tklauser/go-sysconf v0.3.13 h1:GBUpcahXSpR2xN01jhkNAbTLRk2Yzgggk8IM08lq3r4=
github.com/tklauser/go-sysconf v0.3.13/go.mod h1:zwleP4Q4OehZHGn4CYZDipCgg9usW5IJePewFCGVEa0=
github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY= github.com/tklauser/numcpus v0.6.1/go.mod h1:1XfjsgE2zo8GVw7POkMbHENHzVg3GzmoZ9fESEdAacY=
github.com/tklauser/numcpus v0.7.0 h1:yjuerZP127QG9m5Zh/mSO4wqurYil27tHrqwRoRjpr4=
github.com/tklauser/numcpus v0.7.0/go.mod h1:bb6dMVcj8A42tSE7i32fsIUCbQNllK5iDguyOZRUzAY=
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI= github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08= github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M= github.com/ugorji/go v1.2.7/go.mod h1:nF9osbDWLy6bDVv/Rtoh6QgnvNDpmCalQV5urGCCS6M=
@@ -224,8 +230,9 @@ github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432 h1:I/ATawgO2Rer
github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432/go.mod h1:QN7Go2ftTVfx0aCTh9RXHV8pkpi0FtmbwQw40dy61wQ= github.com/xiaokangwang/VLite v0.0.0-20220418190619-cff95160a432/go.mod h1:QN7Go2ftTVfx0aCTh9RXHV8pkpi0FtmbwQw40dy61wQ=
github.com/xtaci/smux v1.5.24 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY= github.com/xtaci/smux v1.5.24 h1:77emW9dtnOxxOQ5ltR+8BbsX1kzcOxQ5gB+aaV9hXOY=
github.com/xtaci/smux v1.5.24/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY= github.com/xtaci/smux v1.5.24/go.mod h1:OMlQbT5vcgl2gb49mFkYo6SMf+zP3rcjcwQz7ZU7IGY=
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.starlark.net v0.0.0-20230612165344-9532f5667272 h1:2/wtqS591wZyD2OsClsVBKRPEvBsQt/Js+fsCiYhwu8= go.starlark.net v0.0.0-20230612165344-9532f5667272 h1:2/wtqS591wZyD2OsClsVBKRPEvBsQt/Js+fsCiYhwu8=
go.starlark.net v0.0.0-20230612165344-9532f5667272/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds= go.starlark.net v0.0.0-20230612165344-9532f5667272/go.mod h1:jxU+3+j+71eXOW14274+SmmuW82qJzl6iZSeqEtTGds=
go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo= go.uber.org/mock v0.3.0 h1:3mUxI1No2/60yUYax92Pt8eNOEecx2D3lcXZh2NEZJo=
@@ -236,15 +243,15 @@ golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUu
golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc= golang.org/x/arch v0.7.0 h1:pskyeJh/3AmoQ8CPE95vxHLqp1G1GfGNXTmcl9NEKTc=
golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys= golang.org/x/arch v0.7.0/go.mod h1:FEVrYAQjsQXMVJ1nsMoVVXPZg6p2JE2mx8psSWTDQys=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc= golang.org/x/crypto v0.21.0 h1:X31++rzVUdKhX5sWmSOFZxx8UW/ldWx55cbf08iNAMA=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg= golang.org/x/crypto v0.21.0/go.mod h1:0BP7YvVV9gBbVKyeTG0Gyn+gZm94bibOW5BjDEYAOMs=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY= golang.org/x/exp v0.0.0-20230725093048-515e97ebf090 h1:Di6/M8l0O2lCLc6VVRWhgCiApHV8MnQurBnFSHsQtNY=
golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc= golang.org/x/exp v0.0.0-20230725093048-515e97ebf090/go.mod h1:FXUEEKJgO7OQYeo8N01OfiKP8RXMtf6e8aTskBGqWdc=
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0= golang.org/x/mod v0.13.0 h1:I/DsJXRlw/8l/0c24sM9yb0T4z9liZTduXvdAWYiysY=
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c= golang.org/x/mod v0.13.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo= golang.org/x/net v0.23.0 h1:7EYJ93RZ9vYSZAIb2x3lnuvqO5zneoD6IvWjuhfxjTs=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY= golang.org/x/net v0.23.0/go.mod h1:JKghWKKOSdJwpW2GEx0Ja7fmaKnMsbu+MWVZTokSYmg=
golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ= golang.org/x/sync v0.6.0 h1:5BMeUDZ7vkXGfEr1x9B4bRcTH4lpkTkpdh0T/J+qjbQ=
golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk= golang.org/x/sync v0.6.0/go.mod h1:Czt+wKu1gCyEFDUtn0jG5QVvpJ6rzVqr5aXyt9drQfk=
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
@@ -254,12 +261,14 @@ golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210806184541-e5e7981a1069/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211025201205-69cdffdb9359/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.18.0 h1:DBdB3niSjOA/O0blCZBqDefyWNYveAYMNF1Wum0DYQ4=
golang.org/x/sys v0.18.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
@@ -268,18 +277,18 @@ golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.3.0/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ= golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
golang.org/x/tools v0.17.0 h1:FvmRgNOcs3kOa+T20R1uhfP9F6HgG2mfxDv1vrx1Htc= golang.org/x/tools v0.14.0 h1:jvNa2pY0M4r62jkRQ6RwEZZyPcymeL9XZMLBbV7U2nc=
golang.org/x/tools v0.17.0/go.mod h1:xsh6VxdV005rRVaS6SSAf9oiAqljS7UZUacMZ8Bnsps= golang.org/x/tools v0.14.0/go.mod h1:uYBEerGOWcJyEORxN+Ek8+TT266gXkNlHdJBwexUsBg=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014 h1:FSL3lRCkhaPFxqi0s9o+V4UI2WTzAVOvkgbd4kVV4Wg= google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240205150955-31a09d347014/go.mod h1:SaPjaZGWb0lPqs6Ittu0spdfrOArqji4ZdeP5IC/9N4= google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0= google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs= google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
google.golang.org/protobuf v1.32.0 h1:pPC6BG5ex8PDFnkbrGU3EixyhKcQ2aDuBS36lqK/C7I= google.golang.org/protobuf v1.33.0 h1:uNO2rsAINq/JlFpSdYEKIZ0uKD/R9cpdv0T+yoGwGmI=
google.golang.org/protobuf v1.32.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos= google.golang.org/protobuf v1.33.0/go.mod h1:c6P6GXX6sHbq/GpV6MGZEdwhWPcYBgnhAHhKbcUYpos=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v1.0.0-20180628173108-788fd7840127/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
+11 -1
View File
@@ -5,10 +5,11 @@ import (
"os" "os"
"os/signal" "os/signal"
"s-ui/app" "s-ui/app"
"s-ui/cmd"
"syscall" "syscall"
) )
func main() { func runApp() {
app := app.NewApp() app := app.NewApp()
err := app.Init() err := app.Init()
@@ -36,3 +37,12 @@ func main() {
} }
} }
} }
func main() {
if len(os.Args) < 2 {
runApp()
return
} else {
cmd.ParseCmd()
}
}
+9 -11
View File
@@ -5,7 +5,6 @@ import (
"s-ui/database" "s-ui/database"
"s-ui/database/model" "s-ui/database/model"
"s-ui/logger" "s-ui/logger"
"strings"
"time" "time"
"gorm.io/gorm" "gorm.io/gorm"
@@ -14,18 +13,14 @@ import (
type ClientService struct { type ClientService struct {
} }
func (s *ClientService) GetAll() (string, error) { func (s *ClientService) GetAll() ([]model.Client, error) {
db := database.GetDB() db := database.GetDB()
clients := []model.Client{} clients := []model.Client{}
err := db.Model(model.Client{}).Scan(&clients).Error err := db.Model(model.Client{}).Scan(&clients).Error
if err != nil { if err != nil {
return "", err return nil, err
} }
data, err := json.Marshal(clients) return clients, nil
if err != nil {
return "", err
}
return string(data), nil
} }
func (s *ClientService) Save(tx *gorm.DB, changes []model.Changes) error { func (s *ClientService) Save(tx *gorm.DB, changes []model.Changes) error {
@@ -62,18 +57,20 @@ func (s *ClientService) DepleteClients() ([]string, []string, error) {
return nil, nil, err return nil, nil, err
} }
dt := time.Now().Unix()
var users, inbounds []string var users, inbounds []string
for _, client := range clients { for _, client := range clients {
logger.Debug("Client ", client.Name, " is going to be disabled") logger.Debug("Client ", client.Name, " is going to be disabled")
users = append(users, client.Name) users = append(users, client.Name)
userInbounds := strings.Split(client.Inbounds, ",") var userInbounds []string
json.Unmarshal(client.Inbounds, &userInbounds)
inbounds = append(inbounds, userInbounds...) inbounds = append(inbounds, userInbounds...)
changes = append(changes, model.Changes{ changes = append(changes, model.Changes{
DateTime: time.Now().Unix(), DateTime: dt,
Actor: "DepleteJob", Actor: "DepleteJob",
Key: "clients", Key: "clients",
Action: "disable", Action: "disable",
Obj: json.RawMessage(client.Name), Obj: json.RawMessage("\"" + client.Name + "\""),
}) })
} }
@@ -87,6 +84,7 @@ func (s *ClientService) DepleteClients() ([]string, []string, error) {
if err != nil { if err != nil {
return nil, nil, err return nil, nil, err
} }
LastUpdate = dt
} }
return users, inbounds, nil return users, inbounds, nil
+99 -40
View File
@@ -6,14 +6,19 @@ import (
"s-ui/config" "s-ui/config"
"s-ui/database" "s-ui/database"
"s-ui/database/model" "s-ui/database/model"
"s-ui/logger"
"s-ui/singbox" "s-ui/singbox"
"strconv"
"time" "time"
) )
var ApiAddr string var ApiAddr string
var LastUpdate int64
type ConfigService struct { type ConfigService struct {
ClientService ClientService
TlsService
InDataService
singbox.Controller singbox.Controller
SettingService SettingService
} }
@@ -51,27 +56,50 @@ func (s *ConfigService) InitConfig() error {
return err return err
} }
} }
return s.RefreshApiAddr(&data) var singboxConfig SingBoxConfig
err = json.Unmarshal(data, &singboxConfig)
if err != nil {
return err
} }
func (s *ConfigService) GetConfig() (*[]byte, error) { return s.RefreshApiAddr(&singboxConfig)
}
func (s *ConfigService) GetConfig() (*SingBoxConfig, error) {
configPath := config.GetBinFolderPath() configPath := config.GetBinFolderPath()
data, err := os.ReadFile(configPath + "/config.json") data, err := os.ReadFile(configPath + "/config.json")
if err != nil { if err != nil {
return nil, err return nil, err
} }
return &data, nil singboxConfig := SingBoxConfig{}
err = json.Unmarshal(data, &singboxConfig)
if err != nil {
return nil, err
}
return &singboxConfig, nil
} }
func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string) error { func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string) error {
var err error var err error
var clientChanges, settingChanges, configChanges []model.Changes var clientChanges, tlsChanges, inChanges, settingChanges, configChanges []model.Changes
if _, ok := changes["clients"]; ok { if _, ok := changes["clients"]; ok {
err = json.Unmarshal([]byte(changes["clients"]), &clientChanges) err = json.Unmarshal([]byte(changes["clients"]), &clientChanges)
if err != nil { if err != nil {
return err return err
} }
} }
if _, ok := changes["tls"]; ok {
err = json.Unmarshal([]byte(changes["tls"]), &tlsChanges)
if err != nil {
return err
}
}
if _, ok := changes["inData"]; ok {
err = json.Unmarshal([]byte(changes["inData"]), &inChanges)
if err != nil {
return err
}
}
if _, ok := changes["settings"]; ok { if _, ok := changes["settings"]; ok {
err = json.Unmarshal([]byte(changes["settings"]), &settingChanges) err = json.Unmarshal([]byte(changes["settings"]), &settingChanges)
if err != nil { if err != nil {
@@ -101,6 +129,18 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
return err return err
} }
} }
if len(tlsChanges) > 0 {
err = s.TlsService.Save(tx, tlsChanges)
if err != nil {
return err
}
}
if len(inChanges) > 0 {
err = s.InDataService.Save(tx, inChanges)
if err != nil {
return err
}
}
if len(settingChanges) > 0 { if len(settingChanges) > 0 {
err = s.SettingService.Save(tx, settingChanges) err = s.SettingService.Save(tx, settingChanges)
if err != nil { if err != nil {
@@ -112,11 +152,7 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
if err != nil { if err != nil {
return err return err
} }
newConfig := SingBoxConfig{} newConfig := *singboxConfig
err = json.Unmarshal(*singboxConfig, &newConfig)
if err != nil {
return err
}
for _, change := range configChanges { for _, change := range configChanges {
rawObject := change.Obj rawObject := change.Obj
switch change.Key { switch change.Key {
@@ -146,18 +182,15 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
case "outbounds": case "outbounds":
if change.Action == "edit" { if change.Action == "edit" {
newConfig.Outbounds[change.Index] = rawObject newConfig.Outbounds[change.Index] = rawObject
} else if change.Action == "del" {
newConfig.Outbounds = append(newConfig.Outbounds[:change.Index], newConfig.Outbounds[change.Index+1:]...)
} else { } else {
newConfig.Outbounds = append(newConfig.Outbounds, rawObject) newConfig.Outbounds = append(newConfig.Outbounds, rawObject)
} }
} }
} }
// Save to config.json err = s.Save(&newConfig)
data, err := json.MarshalIndent(newConfig, "", " ")
if err != nil {
return err
}
err = s.Save(&data)
if err != nil { if err != nil {
return err return err
} }
@@ -165,7 +198,11 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
// Log changes // Log changes
dt := time.Now().Unix() dt := time.Now().Unix()
allChanges := append(append(clientChanges, settingChanges...), configChanges...) allChanges := append(clientChanges, settingChanges...)
allChanges = append(allChanges, configChanges...)
allChanges = append(allChanges, tlsChanges...)
allChanges = append(allChanges, inChanges...)
if len(allChanges) > 0 {
for index := range allChanges { for index := range allChanges {
allChanges[index].DateTime = dt allChanges[index].DateTime = dt
allChanges[index].Actor = loginUser allChanges[index].Actor = loginUser
@@ -174,21 +211,32 @@ func (s *ConfigService) SaveChanges(changes map[string]string, loginUser string)
if err != nil { if err != nil {
return err return err
} }
}
LastUpdate = dt
return nil return nil
} }
func (s *ConfigService) CheckChnages(lu string) (bool, error) { func (s *ConfigService) CheckChanges(lu string) (bool, error) {
if lu == "" { if lu == "" {
return true, nil return true, nil
} }
if LastUpdate == 0 {
db := database.GetDB() db := database.GetDB()
var count int64 var count int64
err := db.Model(model.Changes{}).Where("date_time > " + lu).Count(&count).Error err := db.Model(model.Changes{}).Where("date_time > " + lu).Count(&count).Error
if err == nil {
LastUpdate = time.Now().Unix()
}
return count > 0, err return count > 0, err
} else {
intLu, err := strconv.ParseInt(lu, 10, 64)
return LastUpdate > intLu, err
}
} }
func (s *ConfigService) Save(data *[]byte) error { func (s *ConfigService) Save(singboxConfig *SingBoxConfig) error {
configPath := config.GetBinFolderPath() configPath := config.GetBinFolderPath()
_, err := os.Stat(configPath + "/config.json") _, err := os.Stat(configPath + "/config.json")
if os.IsNotExist(err) { if os.IsNotExist(err) {
@@ -200,35 +248,36 @@ func (s *ConfigService) Save(data *[]byte) error {
return err return err
} }
err = os.WriteFile(configPath+"/config.json", *data, 0764) data, err := json.MarshalIndent(singboxConfig, " ", " ")
if err != nil { if err != nil {
return err return err
} }
s.RefreshApiAddr(data) err = os.WriteFile(configPath+"/config.json", data, 0764)
if err != nil {
return err
}
s.RefreshApiAddr(singboxConfig)
s.Controller.Restart() s.Controller.Restart()
return nil return nil
} }
func (s *ConfigService) RefreshApiAddr(data *[]byte) error { func (s *ConfigService) RefreshApiAddr(singboxConfig *SingBoxConfig) error {
Env_API := config.GetEnvApi() Env_API := config.GetEnvApi()
if len(Env_API) > 0 { if len(Env_API) > 0 {
ApiAddr = Env_API ApiAddr = Env_API
} else { } else {
var err error var err error
if data == nil { if singboxConfig == nil {
data, err = s.GetConfig() singboxConfig, err = s.GetConfig()
if err != nil { if err != nil {
return err return err
} }
}
singboxConfig := SingBoxConfig{}
err = json.Unmarshal(*data, &singboxConfig)
if err != nil {
return err
} }
var experimental struct { var experimental struct {
V2rayApi struct { V2rayApi struct {
Listen string `json:"listen"` Listen string `json:"listen"`
@@ -255,12 +304,7 @@ func (s *ConfigService) DepleteClients() error {
if err != nil { if err != nil {
return err return err
} }
newConfig := SingBoxConfig{} for inbound_index, inbound := range singboxConfig.Inbounds {
err = json.Unmarshal(*singboxConfig, &newConfig)
if err != nil {
return err
}
for inbound_index, inbound := range newConfig.Inbounds {
var inboundJson map[string]interface{} var inboundJson map[string]interface{}
json.Unmarshal(inbound, &inboundJson) json.Unmarshal(inbound, &inboundJson)
if s.contains(inbounds, inboundJson["tag"].(string)) { if s.contains(inbounds, inboundJson["tag"].(string)) {
@@ -299,13 +343,10 @@ func (s *ConfigService) DepleteClients() error {
if err != nil { if err != nil {
return err return err
} }
newConfig.Inbounds[inbound_index] = modifiedInbound singboxConfig.Inbounds[inbound_index] = modifiedInbound
} }
modifiedConfig, err := json.MarshalIndent(newConfig, "", " ")
if err != nil { err = s.Save(singboxConfig)
return err
}
err = s.Save(&modifiedConfig)
if err != nil { if err != nil {
return err return err
} }
@@ -320,3 +361,21 @@ func (s *ConfigService) contains(slice []string, item string) bool {
} }
return false return false
} }
func (s *ConfigService) GetChanges(actor string, chngKey string, count string) []model.Changes {
c, _ := strconv.Atoi(count)
whereString := "`id`>0"
if len(actor) > 0 {
whereString += " and `actor`='" + actor + "'"
}
if len(chngKey) > 0 {
whereString += " and `key`='" + chngKey + "'"
}
db := database.GetDB()
var chngs []model.Changes
err := db.Model(model.Changes{}).Where(whereString).Order("`id` desc").Limit(c).Scan(&chngs).Error
if err != nil {
logger.Warning(err)
}
return chngs
}
+46
View File
@@ -0,0 +1,46 @@
package service
import (
"encoding/json"
"s-ui/database"
"s-ui/database/model"
"gorm.io/gorm"
)
type InDataService struct {
}
func (s *InDataService) GetAll() ([]model.InboundData, error) {
db := database.GetDB()
inData := []model.InboundData{}
err := db.Model(model.InboundData{}).Scan(&inData).Error
if err != nil {
return nil, err
}
return inData, nil
}
func (s *InDataService) Save(tx *gorm.DB, changes []model.Changes) error {
var err error
for _, change := range changes {
inData := model.InboundData{}
err = json.Unmarshal(change.Obj, &inData)
if err != nil {
return err
}
switch change.Action {
case "new":
err = tx.Create(&inData).Error
case "del":
err = tx.Where("id = ?", change.Index).Delete(model.InboundData{}).Error
default:
err = tx.Save(inData).Error
}
if err != nil {
return err
}
}
return err
}
+47 -3
View File
@@ -1,10 +1,13 @@
package service package service
import ( import (
"bytes"
"os" "os"
"os/exec"
"runtime" "runtime"
"s-ui/config" "s-ui/config"
"s-ui/logger" "s-ui/logger"
"strconv"
"strings" "strings"
"github.com/shirou/gopsutil/v3/cpu" "github.com/shirou/gopsutil/v3/cpu"
@@ -89,12 +92,12 @@ func (s *ServerService) GetNetInfo() map[string]interface{} {
func (s *ServerService) GetSingboxInfo() map[string]interface{} { func (s *ServerService) GetSingboxInfo() map[string]interface{} {
info := make(map[string]interface{}, 0) info := make(map[string]interface{}, 0)
if s.SingBoxService.IsRunning() { sysStats, err := s.SingBoxService.GetSysStats()
if err == nil {
info["running"] = true info["running"] = true
sysStats, _ := s.SingBoxService.GetSysStats()
info["stats"] = sysStats info["stats"] = sysStats
} else { } else {
info["running"] = false info["running"] = s.SingBoxService.IsRunning()
} }
return info return info
} }
@@ -135,3 +138,44 @@ func (s *ServerService) GetSystemInfo() map[string]interface{} {
return info return info
} }
func (s *ServerService) GetLogs(service string, count string, level string) []string {
c, _ := strconv.Atoi(count)
var lines []string
if service == "sing-box" {
cmdArgs := []string{"journalctl", "-u", service, "--no-pager", "-n", count, "-p", level}
// Run the command
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return []string{"Failed to run journalctl command!"}
}
lines = strings.Split(out.String(), "\n")
} else {
lines = logger.GetLogs(c, level)
}
return lines
}
func (s *ServerService) GenKeypair(keyType string, options string) []string {
if len(keyType) == 0 {
return []string{"No keypair to generate"}
}
sbExec := s.GetBinaryPath()
cmdArgs := []string{"generate", keyType + "-keypair"}
if keyType == "tls" || keyType == "ech" {
cmdArgs = append(cmdArgs, options)
}
// Run the command
cmd := exec.Command(sbExec, cmdArgs...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return []string{"Failed to generate keypair"}
}
return strings.Split(out.String(), "\n")
}
+78 -12
View File
@@ -18,10 +18,13 @@ var defaultValueMap = map[string]string{
"webListen": "", "webListen": "",
"webDomain": "", "webDomain": "",
"webPort": "2095", "webPort": "2095",
"webSecret": common.Random(32), "secret": common.Random(32),
"webCertFile": "", "webCertFile": "",
"webKeyFile": "", "webKeyFile": "",
"webPath": "/app/",
"webURI": "",
"sessionMaxAge": "0", "sessionMaxAge": "0",
"trafficAge": "30",
"timeLocation": "Asia/Tehran", "timeLocation": "Asia/Tehran",
"subListen": "", "subListen": "",
"subPort": "2096", "subPort": "2096",
@@ -33,6 +36,7 @@ var defaultValueMap = map[string]string{
"subEncode": "true", "subEncode": "true",
"subShowInfo": "false", "subShowInfo": "false",
"subURI": "", "subURI": "",
"subJsonExt": "",
} }
type SettingService struct { type SettingService struct {
@@ -62,11 +66,16 @@ func (s *SettingService) GetAllSetting() (*map[string]string, error) {
} }
// Due to security principles // Due to security principles
delete(allSetting, "webSecret") delete(allSetting, "secret")
return &allSetting, nil return &allSetting, nil
} }
func (s *SettingService) ResetSettings() error {
db := database.GetDB()
return db.Where("1 = 1").Delete(model.Setting{}).Error
}
func (s *SettingService) getSetting(key string) (*model.Setting, error) { func (s *SettingService) getSetting(key string) (*model.Setting, error) {
db := database.GetDB() db := database.GetDB()
setting := &model.Setting{} setting := &model.Setting{}
@@ -119,9 +128,9 @@ func (s *SettingService) getBool(key string) (bool, error) {
return strconv.ParseBool(str) return strconv.ParseBool(str)
} }
func (s *SettingService) setBool(key string, value bool) error { // func (s *SettingService) setBool(key string, value bool) error {
return s.setString(key, strconv.FormatBool(value)) // return s.setString(key, strconv.FormatBool(value))
} // }
func (s *SettingService) getInt(key string) (int, error) { func (s *SettingService) getInt(key string) (int, error) {
str, err := s.getString(key) str, err := s.getString(key)
@@ -158,12 +167,36 @@ func (s *SettingService) GetKeyFile() (string, error) {
return s.getString("webKeyFile") return s.getString("webKeyFile")
} }
func (s *SettingService) GetSecret() ([]byte, error) { func (s *SettingService) GetWebPath() (string, error) {
secret, err := s.getString("webSecret") webPath, err := s.getString("webPath")
if secret == defaultValueMap["webSecret"] {
err := s.saveSetting("webSecret", secret)
if err != nil { if err != nil {
logger.Warning("save webSecret failed:", err) return "", err
}
if !strings.HasPrefix(webPath, "/") {
webPath = "/" + webPath
}
if !strings.HasSuffix(webPath, "/") {
webPath += "/"
}
return webPath, nil
}
func (s *SettingService) SetWebPath(webPath string) error {
if !strings.HasPrefix(webPath, "/") {
webPath = "/" + webPath
}
if !strings.HasSuffix(webPath, "/") {
webPath += "/"
}
return s.setString("webPath", webPath)
}
func (s *SettingService) GetSecret() ([]byte, error) {
secret, err := s.getString("secret")
if secret == defaultValueMap["secret"] {
err := s.saveSetting("secret", secret)
if err != nil {
logger.Warning("save secret failed:", err)
} }
} }
return []byte(secret), err return []byte(secret), err
@@ -173,6 +206,10 @@ func (s *SettingService) GetSessionMaxAge() (int, error) {
return s.getInt("sessionMaxAge") return s.getInt("sessionMaxAge")
} }
func (s *SettingService) GetTrafficAge() (int, error) {
return s.getInt("trafficAge")
}
func (s *SettingService) GetTimeLocation() (*time.Location, error) { func (s *SettingService) GetTimeLocation() (*time.Location, error) {
l, err := s.getString("timeLocation") l, err := s.getString("timeLocation")
if err != nil { if err != nil {
@@ -195,6 +232,10 @@ func (s *SettingService) GetSubPort() (int, error) {
return s.getInt("subPort") return s.getInt("subPort")
} }
func (s *SettingService) SetSubPort(subPort int) error {
return s.setInt("subPort", subPort)
}
func (s *SettingService) GetSubPath() (string, error) { func (s *SettingService) GetSubPath() (string, error) {
subPath, err := s.getString("subPath") subPath, err := s.getString("subPath")
if err != nil { if err != nil {
@@ -209,6 +250,16 @@ func (s *SettingService) GetSubPath() (string, error) {
return subPath, nil return subPath, nil
} }
func (s *SettingService) SetSubPath(subPath string) error {
if !strings.HasPrefix(subPath, "/") {
subPath = "/" + subPath
}
if !strings.HasSuffix(subPath, "/") {
subPath += "/"
}
return s.setString("subPath", subPath)
}
func (s *SettingService) GetSubDomain() (string, error) { func (s *SettingService) GetSubDomain() (string, error) {
return s.getString("subDomain") return s.getString("subDomain")
} }
@@ -268,16 +319,27 @@ func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) error {
json.Unmarshal(change.Obj, &obj) json.Unmarshal(change.Obj, &obj)
// Secure file existance check // Secure file existance check
if key == "webCertFile" || if obj != "" && (key == "webCertFile" ||
key == "webKeyFile" || key == "webKeyFile" ||
key == "subCertFile" || key == "subCertFile" ||
key == "subKeyFile" { key == "subKeyFile") {
err = s.fileExists(obj) err = s.fileExists(obj)
if err != nil { if err != nil {
return common.NewError(" -> ", obj, " is not exists") return common.NewError(" -> ", obj, " is not exists")
} }
} }
// Correct Pathes start and ends with `/`
if key == "webPath" ||
key == "subPath" {
if !strings.HasPrefix(obj, "/") {
obj = "/" + obj
}
if !strings.HasSuffix(obj, "/") {
obj += "/"
}
}
err = tx.Model(model.Setting{}).Where("key = ?", key).Update("value", obj).Error err = tx.Model(model.Setting{}).Where("key = ?", key).Update("value", obj).Error
if err != nil { if err != nil {
return err return err
@@ -286,6 +348,10 @@ func (s *SettingService) Save(tx *gorm.DB, changes []model.Changes) error {
return err return err
} }
func (s *SettingService) GetSubJsonExt() (string, error) {
return s.getString("subJsonExt")
}
func (s *SettingService) fileExists(path string) error { func (s *SettingService) fileExists(path string) error {
_, err := os.Stat(path) _, err := os.Stat(path)
return err return err
+4 -1
View File
@@ -26,7 +26,10 @@ func (s *SingBoxService) GetStats() error {
} }
func (s *SingBoxService) GetSysStats() (*map[string]interface{}, error) { func (s *SingBoxService) GetSysStats() (*map[string]interface{}, error) {
s.V2rayAPI.Init(ApiAddr) err := s.V2rayAPI.Init(ApiAddr)
if err != nil {
return nil, err
}
defer s.V2rayAPI.Close() defer s.V2rayAPI.Close()
resp, err := s.V2rayAPI.GetSysStats() resp, err := s.V2rayAPI.GetSysStats()
if err != nil { if err != nil {
+2 -7
View File
@@ -1,7 +1,6 @@
package service package service
import ( import (
"encoding/json"
"s-ui/database" "s-ui/database"
"s-ui/database/model" "s-ui/database/model"
"time" "time"
@@ -86,12 +85,8 @@ func (s *StatsService) GetStats(resorce string, tag string, limit int) ([]model.
return result, nil return result, nil
} }
func (s *StatsService) GetOnlines() (string, error) { func (s *StatsService) GetOnlines() (onlines, error) {
onlines, err := json.Marshal(onlineResources) return *onlineResources, nil
if err != nil {
return "", err
}
return string(onlines), nil
} }
func (s *StatsService) DelOldStats(days int) error { func (s *StatsService) DelOldStats(days int) error {
oldTime := time.Now().AddDate(0, 0, -(days)).Unix() oldTime := time.Now().AddDate(0, 0, -(days)).Unix()
+46
View File
@@ -0,0 +1,46 @@
package service
import (
"encoding/json"
"s-ui/database"
"s-ui/database/model"
"gorm.io/gorm"
)
type TlsService struct {
}
func (s *TlsService) GetAll() ([]model.Tls, error) {
db := database.GetDB()
tlsConfig := []model.Tls{}
err := db.Model(model.Tls{}).Scan(&tlsConfig).Error
if err != nil {
return nil, err
}
return tlsConfig, nil
}
func (s *TlsService) Save(tx *gorm.DB, changes []model.Changes) error {
var err error
for _, change := range changes {
tlsConfig := model.Tls{}
err = json.Unmarshal(change.Obj, &tlsConfig)
if err != nil {
return err
}
switch change.Action {
case "new":
err = tx.Create(&tlsConfig).Error
case "del":
err = tx.Where("id = ?", change.Index).Delete(model.Tls{}).Error
default:
err = tx.Save(tlsConfig).Error
}
if err != nil {
return err
}
}
return err
}
+57 -1
View File
@@ -13,6 +13,40 @@ import (
type UserService struct { type UserService struct {
} }
func (s *UserService) GetFirstUser() (*model.User, error) {
db := database.GetDB()
user := &model.User{}
err := db.Model(model.User{}).
First(user).
Error
if err != nil {
return nil, err
}
return user, nil
}
func (s *UserService) UpdateFirstUser(username string, password string) error {
if username == "" {
return common.NewError("username can not be empty")
} else if password == "" {
return common.NewError("password can not be empty")
}
db := database.GetDB()
user := &model.User{}
err := db.Model(model.User{}).First(user).Error
if database.IsNotFound(err) {
user.Username = username
user.Password = password
return db.Model(model.User{}).Create(user).Error
} else if err != nil {
return err
}
user.Username = username
user.Password = password
return db.Save(user).Error
}
func (s *UserService) Login(username string, password string, remoteIP string) (string, error) { func (s *UserService) Login(username string, password string, remoteIP string) (string, error) {
user := s.CheckUser(username, password, remoteIP) user := s.CheckUser(username, password, remoteIP)
if user == nil { if user == nil {
@@ -36,7 +70,7 @@ func (s *UserService) CheckUser(username string, password string, remoteIP strin
return nil return nil
} }
lastLoginTxt := time.Now().Format("2006-01-02 15:04:05") + "-" + remoteIP lastLoginTxt := time.Now().Format("2006-01-02 15:04:05") + " " + remoteIP
err = db.Model(model.User{}). err = db.Model(model.User{}).
Where("username = ?", username). Where("username = ?", username).
Update("last_logins", &lastLoginTxt).Error Update("last_logins", &lastLoginTxt).Error
@@ -45,3 +79,25 @@ func (s *UserService) CheckUser(username string, password string, remoteIP strin
} }
return user return user
} }
func (s *UserService) GetUsers() (*[]model.User, error) {
var users []model.User
db := database.GetDB()
err := db.Model(model.User{}).Select("id,username,last_logins").Scan(&users).Error
if err != nil {
return nil, err
}
return &users, nil
}
func (s *UserService) ChangePass(id string, oldPass string, newUser string, newPass string) error {
db := database.GetDB()
user := &model.User{}
err := db.Model(model.User{}).Where("id = ? AND password = ?", id, oldPass).First(user).Error
if err != nil || database.IsNotFound(err) {
return err
}
user.Username = newUser
user.Password = newPass
return db.Save(user).Error
}
+243
View File
@@ -0,0 +1,243 @@
package sub
import (
"encoding/json"
"fmt"
"s-ui/database"
"s-ui/database/model"
"s-ui/service"
"s-ui/util"
)
const defaultJson = `
{
"inbounds": [
{
"type": "tun",
"inet4_address": "172.19.0.1/30",
"mtu": 9000,
"auto_route": true,
"strict_route": false,
"sniff": true,
"endpoint_independent_nat": false,
"stack": "system",
"platform": {
"http_proxy": {
"enabled": true,
"server": "127.0.0.1",
"server_port": 2080
}
}
},
{
"type": "mixed",
"listen": "127.0.0.1",
"listen_port": 2080,
"sniff": true,
"users": []
}
]
}
`
type JsonService struct {
service.SettingService
LinkService
}
func (j *JsonService) GetJson(subId string, format string) (*string, error) {
var jsonConfig map[string]interface{}
client, inDatas, err := j.getData(subId)
if err != nil {
return nil, err
}
outbounds, outTags, err := j.getOutbounds(client.Config, inDatas)
if err != nil {
return nil, err
}
links := j.LinkService.GetLinks(&client.Links, "external", "")
for index, link := range links {
json, tag, err := util.GetOutbound(link, index)
if err == nil && len(tag) > 0 {
*outbounds = append(*outbounds, *json)
*outTags = append(*outTags, tag)
}
}
j.addDefaultOutbounds(outbounds, outTags)
err = json.Unmarshal([]byte(defaultJson), &jsonConfig)
if err != nil {
return nil, err
}
jsonConfig["outbounds"] = outbounds
// Add other objects from settings
j.addOthers(&jsonConfig)
result, _ := json.MarshalIndent(jsonConfig, " ", " ")
resultStr := string(result)
return &resultStr, nil
}
func (j *JsonService) getData(subId string) (*model.Client, *[]model.InboundData, error) {
db := database.GetDB()
client := &model.Client{}
err := db.Model(model.Client{}).Where("enable = true and name = ?", subId).First(client).Error
if err != nil {
return nil, nil, err
}
var inbounds []string
err = json.Unmarshal(client.Inbounds, &inbounds)
if err != nil {
return nil, nil, err
}
inDatas := &[]model.InboundData{}
err = db.Model(model.InboundData{}).Where("tag in ?", inbounds).Find(&inDatas).Error
if err != nil {
return nil, nil, err
}
return client, inDatas, nil
}
func (j *JsonService) getOutbounds(clientConfig json.RawMessage, inDatas *[]model.InboundData) (*[]map[string]interface{}, *[]string, error) {
var outbounds []map[string]interface{}
var configs map[string]interface{}
var outTags []string
err := json.Unmarshal(clientConfig, &configs)
if err != nil {
return nil, nil, err
}
for _, inData := range *inDatas {
if len(inData.OutJson) < 5 {
continue
}
var outbound map[string]interface{}
err = json.Unmarshal(inData.OutJson, &outbound)
if err != nil {
return nil, nil, err
}
protocol, _ := outbound["type"].(string)
config, _ := configs[protocol].(map[string]interface{})
for key, value := range config {
if key != "alterId" && key != "name" && key != "username" {
outbound[key] = value
}
}
var addrs []map[string]interface{}
err = json.Unmarshal(inData.Addrs, &addrs)
if err != nil {
return nil, nil, err
}
tag := outbound["tag"].(string)
if len(addrs) == 0 {
outTags = append(outTags, tag)
outbounds = append(outbounds, outbound)
} else {
for index, addr := range addrs {
// Copy original config
newOut := make(map[string]interface{}, len(outbound))
for key, value := range outbound {
newOut[key] = value
}
// Change and push copied config
newOut["server"], _ = addr["server"].(string)
port, _ := addr["server_port"].(float64)
newOut["server_port"] = int(port)
remark, _ := addr["remark"].(string)
newTag := fmt.Sprintf("%d.%s%s", index+1, tag, remark)
outTags = append(outTags, newTag)
newOut["tag"] = newTag
outbounds = append(outbounds, newOut)
}
}
}
return &outbounds, &outTags, nil
}
func (j *JsonService) addDefaultOutbounds(outbounds *[]map[string]interface{}, outTags *[]string) {
outbound := []map[string]interface{}{
{
"outbounds": append([]string{"auto", "direct"}, *outTags...),
"tag": "proxy",
"type": "selector",
},
{
"tag": "auto",
"type": "urltest",
"outbounds": outTags,
"url": "http://www.gstatic.com/generate_204",
"interval": "10m",
"tolerance": 50,
},
{
"type": "direct",
"tag": "direct",
},
{
"type": "dns",
"tag": "dns-out",
},
{
"type": "block",
"tag": "block",
},
}
*outbounds = append(outbound, *outbounds...)
}
func (j *JsonService) addOthers(jsonConfig *map[string]interface{}) error {
rules := []interface{}{
map[string]interface{}{
"clash_mode": "Direct",
"outbound": "direct",
},
map[string]interface{}{
"clash_mode": "Global",
"outbound": "proxy",
},
}
route := map[string]interface{}{
"auto_detect_interface": true,
"final": "proxy",
"rules": rules,
}
othersStr, err := j.SettingService.GetSubJsonExt()
if err != nil {
return err
}
if len(othersStr) == 0 {
(*jsonConfig)["route"] = route
return nil
}
var othersJson map[string]interface{}
err = json.Unmarshal([]byte(othersStr), &othersJson)
if err != nil {
return err
}
if _, ok := othersJson["log"]; ok {
(*jsonConfig)["log"] = othersJson["log"]
}
if _, ok := othersJson["dns"]; ok {
(*jsonConfig)["dns"] = othersJson["dns"]
}
if _, ok := othersJson["experimental"]; ok {
(*jsonConfig)["experimental"] = othersJson["lexperimentalog"]
}
if _, ok := othersJson["rule_set"]; ok {
route["rule_set"] = othersJson["rule_set"]
}
if settingRules, ok := othersJson["rules"].([]interface{}); ok {
route["rules"] = append(rules, settingRules...)
}
(*jsonConfig)["route"] = route
return nil
}
+100
View File
@@ -0,0 +1,100 @@
package sub
import (
"crypto/tls"
"encoding/json"
"io"
"net/http"
"s-ui/logger"
"s-ui/util"
"strings"
)
type Link struct {
Type string `json:"type"`
Remark string `json:"remark"`
Uri string `json:"uri"`
}
type LinkService struct {
}
func (s *LinkService) GetLinks(linkJson *json.RawMessage, types string, clientInfo string) []string {
links := []Link{}
var result []string
err := json.Unmarshal(*linkJson, &links)
if err != nil {
return nil
}
for _, link := range links {
switch link.Type {
case "external":
result = append(result, link.Uri)
case "sub":
result = append(result, s.getExternalSub(link.Uri)...)
case "local":
if types == "all" {
result = append(result, s.addClientInfo(link.Uri, clientInfo))
}
}
}
return result
}
func (s *LinkService) addClientInfo(uri string, clientInfo string) string {
protocol := strings.Split(uri, "://")
if len(protocol) < 2 {
return uri
}
switch protocol[0] {
case "vmess":
var vmessJson map[string]interface{}
config, err := util.B64StrToByte(protocol[1])
if err != nil {
logger.Warning("sub: Error decoding vmess content:", err)
return uri
}
err = json.Unmarshal(config, &vmessJson)
if err != nil {
logger.Warning("sub: Error decoding vmess content:", err)
return uri
}
vmessJson["ps"] = vmessJson["ps"].(string) + clientInfo
result, err := json.MarshalIndent(vmessJson, "", " ")
if err != nil {
logger.Warning("sub: Error decoding vmess + clientInfo content:", err)
return uri
}
return "vmess://" + util.ByteToB64Str(result)
default:
return uri + clientInfo
}
}
func (s *LinkService) getExternalSub(url string) []string {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
// Make the HTTP request
response, err := client.Get(url)
if err != nil {
logger.Warning("sub: Error making HTTP request:", err)
return nil
}
defer response.Body.Close()
// Read the response body
body, err := io.ReadAll(response.Body)
if err != nil {
logger.Warning("sub: Error reading response body:", err)
return nil
}
// Convert if the content is Base64 encoded
links := util.StrOrBase64Encoded(string(body))
return strings.Split(links, "\n")
}
+12
View File
@@ -10,6 +10,7 @@ import (
type SubHandler struct { type SubHandler struct {
service.SettingService service.SettingService
SubService SubService
JsonService
} }
func NewSubHandler(g *gin.RouterGroup) { func NewSubHandler(g *gin.RouterGroup) {
@@ -23,6 +24,16 @@ func (s *SubHandler) initRouter(g *gin.RouterGroup) {
func (s *SubHandler) subs(c *gin.Context) { func (s *SubHandler) subs(c *gin.Context) {
subId := c.Param("subid") subId := c.Param("subid")
format, isFormat := c.GetQuery("format")
if isFormat {
result, err := s.JsonService.GetJson(subId, format)
if err != nil || result == nil {
logger.Error(err)
c.String(400, "Error!")
} else {
c.String(200, *result)
}
} else {
result, headers, err := s.SubService.GetSubs(subId) result, headers, err := s.SubService.GetSubs(subId)
if err != nil || result == nil { if err != nil || result == nil {
logger.Error(err) logger.Error(err)
@@ -37,3 +48,4 @@ func (s *SubHandler) subs(c *gin.Context) {
c.String(200, *result) c.String(200, *result)
} }
} }
}
+3 -102
View File
@@ -1,15 +1,10 @@
package sub package sub
import ( import (
"crypto/tls"
"encoding/base64" "encoding/base64"
"encoding/json"
"fmt" "fmt"
"io"
"net/http"
"s-ui/database" "s-ui/database"
"s-ui/database/model" "s-ui/database/model"
"s-ui/logger"
"s-ui/service" "s-ui/service"
"strings" "strings"
"time" "time"
@@ -17,12 +12,7 @@ import (
type SubService struct { type SubService struct {
service.SettingService service.SettingService
} LinkService
type Link struct {
Type string `json:"type"`
Remark string `json:"remark"`
Uri string `json:"uri"`
} }
func (s *SubService) GetSubs(subId string) (*string, []string, error) { func (s *SubService) GetSubs(subId string) (*string, []string, error) {
@@ -35,29 +25,14 @@ func (s *SubService) GetSubs(subId string) (*string, []string, error) {
return nil, nil, err return nil, nil, err
} }
links := []Link{}
err = json.Unmarshal([]byte(client.Links), &links)
if err != nil {
return nil, nil, err
}
clientInfo := "" clientInfo := ""
subShowInfo, _ := s.SettingService.GetSubShowInfo() subShowInfo, _ := s.SettingService.GetSubShowInfo()
if subShowInfo { if subShowInfo {
clientInfo = s.getClientInfo(client) clientInfo = s.getClientInfo(client)
} }
var result string linksArray := s.LinkService.GetLinks(&client.Links, "all", clientInfo)
for _, link := range links { result := strings.Join(linksArray, "\n")
switch link.Type {
case "external":
result += fmt.Sprintln(link.Uri)
case "sub":
result += s.getExternalSub(link.Uri)
case "local":
result += fmt.Sprintln(s.addClientInfo(link.Uri, clientInfo))
}
}
var headers []string var headers []string
updateInterval, _ := s.SettingService.GetSubUpdates() updateInterval, _ := s.SettingService.GetSubUpdates()
@@ -90,80 +65,6 @@ func (s *SubService) getClientInfo(c *model.Client) string {
} }
} }
func (s *SubService) addClientInfo(uri string, clientInfo string) string {
protocol := strings.Split(uri, "://")
if len(protocol) < 2 {
return uri
}
switch protocol[0] {
case "vmess":
var vmessJson map[string]interface{}
config, err := base64.StdEncoding.DecodeString(protocol[1])
if err != nil {
logger.Warning("sub: Error decoding vmess content:", err)
return uri
}
err = json.Unmarshal(config, &vmessJson)
if err != nil {
logger.Warning("sub: Error decoding vmess content:", err)
return uri
}
vmessJson["ps"] = vmessJson["ps"].(string) + clientInfo
result, err := json.MarshalIndent(vmessJson, "", " ")
if err != nil {
logger.Warning("sub: Error decoding vmess + clientInfo content:", err)
return uri
}
return "vmess://" + base64.StdEncoding.EncodeToString(result)
default:
return uri + clientInfo
}
}
func (s *SubService) getExternalSub(url string) string {
tr := &http.Transport{
TLSClientConfig: &tls.Config{InsecureSkipVerify: true},
}
client := &http.Client{Transport: tr}
// Make the HTTP request
response, err := client.Get(url)
if err != nil {
logger.Warning("sub: Error making HTTP request:", err)
return ""
}
defer response.Body.Close()
// Read the response body
body, err := io.ReadAll(response.Body)
if err != nil {
logger.Warning("sub: Error reading response body:", err)
return ""
}
// Check if the content is Base64 encoded
isBase64 := s.isBase64Encoded(string(body))
if isBase64 {
// Decode Base64 content
decodedText, err := base64.StdEncoding.DecodeString(string(body))
if err != nil {
logger.Warning("sub: Error decoding Base64 content:", err)
return ""
}
return string(decodedText)
} else {
return string(body)
}
}
// Function to check if a string is Base64 encoded
func (s *SubService) isBase64Encoded(str string) bool {
_, err := base64.StdEncoding.DecodeString(str)
return err == nil
}
func (s *SubService) formatTraffic(trafficBytes int64) string { func (s *SubService) formatTraffic(trafficBytes int64) string {
if trafficBytes < 1024 { if trafficBytes < 1024 {
return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1)) return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1))
+20
View File
@@ -0,0 +1,20 @@
package util
import "encoding/base64"
// Function to return decoded bytes if a string is Base64 encoded
func StrOrBase64Encoded(str string) string {
decoded, err := base64.StdEncoding.DecodeString(str)
if err == nil {
return string(decoded)
}
return str
}
func B64StrToByte(str string) ([]byte, error) {
return base64.StdEncoding.DecodeString(str)
}
func ByteToB64Str(b []byte) string {
return base64.StdEncoding.EncodeToString(b)
}
+16 -3
View File
@@ -1,13 +1,26 @@
package common package common
import "math/rand" import (
"math/rand"
"time"
)
var allSeq [62]rune var (
allSeq []rune
rnd = rand.New(rand.NewSource(time.Now().UnixNano()))
)
func init() {
chars := "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
for _, char := range chars {
allSeq = append(allSeq, char)
}
}
func Random(n int) string { func Random(n int) string {
runes := make([]rune, n) runes := make([]rune, n)
for i := 0; i < n; i++ { for i := 0; i < n; i++ {
runes[i] = allSeq[rand.Intn(len(allSeq))] runes[i] = allSeq[rnd.Intn(len(allSeq))]
} }
return string(runes) return string(runes)
} }
+473
View File
@@ -0,0 +1,473 @@
package util
import (
"encoding/json"
"fmt"
"net"
"net/url"
"s-ui/util/common"
"strconv"
"strings"
)
func GetOutbound(uri string, i int) (*map[string]interface{}, string, error) {
u, err := url.Parse(uri)
if err == nil {
switch u.Scheme {
case "vmess":
return vmess(u.Host, i)
case "vless":
return vless(u, i)
case "trojan":
return trojan(u, i)
case "hy", "hysteria":
return hy(u, i)
case "hy2", "hysteria2":
return hy2(u, i)
case "tuic":
return tuic(u, i)
case "ss", "shadowsocks":
return ss(u, i)
}
}
return nil, "", common.NewError("Unsupported link format")
}
func vmess(data string, i int) (*map[string]interface{}, string, error) {
dataByte, err := B64StrToByte(data)
if err != nil {
return nil, "", err
}
var dataJson map[string]interface{}
err = json.Unmarshal(dataByte, &dataJson)
if err != nil {
return nil, "", err
}
transport := map[string]interface{}{}
tp_net, _ := dataJson["net"].(string)
tp_type, _ := dataJson["type"].(string)
tp_host, _ := dataJson["host"].(string)
tp_path, _ := dataJson["path"].(string)
switch strings.ToLower(tp_net) {
case "tcp", "":
if tp_type == "http" {
transport["type"] = tp_type
if len(tp_host) > 0 {
transport["host"] = strings.Split(tp_host, ",")
}
transport["path"] = tp_path
}
case "http", "h2":
transport["type"] = "http"
if len(tp_host) > 0 {
transport["host"] = strings.Split(tp_host, ",")
}
transport["path"] = tp_path
case "ws":
transport["type"] = tp_net
transport["path"] = tp_path
transport["early_data_header_name"] = "Sec-WebSocket-Protocol"
if len(tp_host) > 0 {
transport["headers"] = map[string]interface{}{
"Host": tp_host,
}
}
case "quic":
transport["type"] = tp_net
case "grpc":
transport["type"] = tp_net
transport["service_name"] = tp_path
case "httpupgrade":
transport["type"] = tp_net
transport["path"] = tp_path
transport["host"] = tp_host
default:
return nil, "", common.NewError("Invalid vmess")
}
tls := map[string]interface{}{}
vmess_tls, _ := dataJson["tls"].(string)
if vmess_tls == "tls" {
tls["enabled"] = true
tls_sni, _ := dataJson["sni"].(string)
tls_alpn, _ := dataJson["alpn"].(string)
_, tls_insecure := dataJson["allowInsecure"]
tls_fp, _ := dataJson["fp"].(string)
if len(tls_sni) > 0 {
tls["server_name"] = tls_sni
}
if len(tls_alpn) > 0 {
tls["alpn"] = strings.Split(tls_alpn, ",")
}
if tls_insecure {
tls["insecure"] = true
}
if len(tls_fp) > 0 {
tls["utls"] = map[string]interface{}{
"enabled": true,
"fingerprint": tls_fp,
}
}
}
tag, _ := dataJson["ps"].(string)
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, tag)
}
alter_id, ok := dataJson["aid"].(int)
if !ok {
alter_id = 0
}
vmess := map[string]interface{}{
"type": "vmess",
"tag": tag,
"server": dataJson["add"],
"server_port": dataJson["port"],
"uuid": dataJson["id"],
"security": "auto",
"alter_id": alter_id,
"tls": tls,
"transport": transport,
}
return &vmess, tag, err
}
func vless(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
security := query.Get("security")
host, portStr, _ := net.SplitHostPort(u.Host)
port := 80
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
} else {
if security == "tls" || security == "reality" {
port = 443
}
}
tp_type := query.Get("type")
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
vless := map[string]interface{}{
"type": "vless",
"tag": tag,
"server": host,
"server_port": port,
"uuid": u.User.Username(),
"flow": query.Get("flow"),
"tls": getTls(security, &query),
"transport": getTransport(tp_type, &query),
}
return &vless, tag, nil
}
func trojan(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
security := query.Get("security")
host, portStr, _ := net.SplitHostPort(u.Host)
port := 80
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
} else {
if security == "tls" || security == "reality" {
port = 443
}
}
tp_type := query.Get("type")
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
trojan := map[string]interface{}{
"type": "trojan",
"tag": tag,
"server": host,
"server_port": port,
"password": u.User.Username(),
"tls": getTls(security, &query),
"transport": getTransport(tp_type, &query),
}
return &trojan, tag, nil
}
func hy(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
}
tls := map[string]interface{}{
"enabled": true,
"server_name": query.Get("peer"),
}
alpn := query.Get("alpn")
insecure := query.Get("insecure")
if len(alpn) > 0 {
tls["alpn"] = strings.Split(alpn, ",")
}
if insecure == "1" || insecure == "true" {
tls["insecure"] = true
}
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
hy := map[string]interface{}{
"type": "hysteria",
"tag": tag,
"server": host,
"server_port": port,
"obfs": query.Get("obfsParam"),
"auth_str": query.Get("auth"),
"tls": tls,
}
down, _ := strconv.Atoi(query.Get("downmbps"))
up, _ := strconv.Atoi(query.Get("upmbps"))
recv_window_conn, _ := strconv.Atoi(query.Get("recv_window_conn"))
recv_window, _ := strconv.Atoi(query.Get("recv_window"))
if down > 0 {
hy["down_mbps"] = down
}
if up > 0 {
hy["up_mbps"] = up
}
if recv_window_conn > 0 {
hy["recv_window_conn"] = recv_window_conn
}
if recv_window > 0 {
hy["recv_window"] = recv_window
}
return &hy, tag, nil
}
func hy2(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
}
tls := map[string]interface{}{
"enabled": true,
"server_name": query.Get("sni"),
}
alpn := query.Get("alpn")
insecure := query.Get("insecure")
if len(alpn) > 0 {
tls["alpn"] = strings.Split(alpn, ",")
}
if insecure == "1" || insecure == "true" {
tls["insecure"] = true
}
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
hy2 := map[string]interface{}{
"type": "hysteria2",
"tag": tag,
"server": host,
"server_port": port,
"password": u.User.Username(),
"tls": tls,
}
down, _ := strconv.Atoi(query.Get("downmbps"))
up, _ := strconv.Atoi(query.Get("upmbps"))
obfs := query.Get("obfs")
if down > 0 {
hy2["down_mbps"] = down
}
if up > 0 {
hy2["up_mbps"] = up
}
if obfs == "salamander" {
hy2["obfs"] = map[string]interface{}{
"type": "salamander",
"password": query.Get("obfs-password"),
}
}
return &hy2, tag, nil
}
func tuic(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
}
tls := map[string]interface{}{
"enabled": true,
"server_name": query.Get("sni"),
}
alpn := query.Get("alpn")
insecure := query.Get("allow_insecure")
disable_sni := query.Get("disable_sni")
if len(alpn) > 0 {
tls["alpn"] = strings.Split(alpn, ",")
}
if insecure == "1" || insecure == "true" {
tls["insecure"] = true
}
if disable_sni == "1" || disable_sni == "true" {
tls["disable_sni"] = true
}
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
password, _ := u.User.Password()
tuic := map[string]interface{}{
"type": "tuic",
"tag": tag,
"server": host,
"server_port": port,
"uuid": u.User.Username(),
"password": password,
"congestion_control": query.Get("congestion_control"),
"udp_relay_mode": query.Get("udp_relay_mode"),
"tls": tls,
}
return &tuic, tag, nil
}
func ss(u *url.URL, i int) (*map[string]interface{}, string, error) {
query, _ := url.ParseQuery(u.RawQuery)
host, portStr, _ := net.SplitHostPort(u.Host)
port := 443
if len(portStr) > 0 {
port, _ = strconv.Atoi(portStr)
}
method := u.User.Username()
password, ok := u.User.Password()
if !ok {
decrypted := StrOrBase64Encoded(method)
decrypted_arr := strings.Split(decrypted, ":")
if len(decrypted_arr) > 1 {
method = decrypted_arr[0]
password = strings.Join(decrypted_arr[1:], ":")
} else {
return nil, "", common.NewError("Unsupported shadowsocks")
}
}
tag := u.Fragment
if i > 0 {
tag = fmt.Sprintf("%d.%s", i, u.Fragment)
}
ss := map[string]interface{}{
"type": "shadowsocks",
"tag": tag,
"server": host,
"server_port": port,
"method": method,
"password": password,
}
v2ray_type := query.Get("type")
if len(v2ray_type) > 0 {
pl_arr := []string{}
host_header := query.Get("host")
if query.Get("security") == "tls" {
pl_arr = append(pl_arr, "tls")
}
if v2ray_type == "quic" {
pl_arr = append(pl_arr, "mode=quic")
}
if len(host_header) > 0 {
pl_arr = append(pl_arr, "host="+host_header)
}
ss["plugin"] = "v2ray-plugin"
ss["plugin_opts"] = strings.Join(pl_arr, ";")
}
plugin := query.Get("plugin")
if len(plugin) > 0 {
pl_arr := strings.Split(plugin, ";")
if len(pl_arr) > 0 {
ss["plugin"] = pl_arr[0]
ss["plugin_opts"] = strings.Join(pl_arr[1:], ";")
}
}
return &ss, tag, nil
}
func getTransport(tp_type string, q *url.Values) *map[string]interface{} {
transport := map[string]interface{}{}
tp_host := q.Get("host")
tp_path := q.Get("path")
switch strings.ToLower(tp_type) {
case "tcp", "":
if q.Get("headerType") == "http" {
transport["type"] = "http"
if len(tp_host) > 0 {
transport["host"] = strings.Split(tp_host, ",")
}
transport["path"] = tp_path
}
case "http", "h2":
transport["type"] = "http"
if len(tp_host) > 0 {
transport["host"] = strings.Split(tp_host, ",")
}
transport["path"] = tp_path
case "ws":
transport["type"] = "ws"
transport["path"] = tp_path
if len(tp_host) > 0 {
transport["headers"] = map[string]interface{}{
"Host": tp_host,
}
}
case "quic":
transport["type"] = "quic"
case "grpc":
transport["type"] = "grpc"
transport["service_name"] = q.Get("serviceName")
case "httpupgrade":
transport["type"] = "httpupgrade"
transport["path"] = tp_path
transport["host"] = tp_host
}
return &transport
}
func getTls(security string, q *url.Values) *map[string]interface{} {
tls := map[string]interface{}{}
tls_fp := q.Get("fp")
tls_sni := q.Get("sni")
tls_insecure := q.Get("allowInsecure")
tls_alpn := q.Get("alpn")
switch security {
case "tls":
tls["enabled"] = true
case "reality":
tls["enabled"] = true
tls["reality"] = map[string]interface{}{
"enabled": true,
"public_key": q.Get("pbk"),
"short_id": q.Get("sid"),
}
}
if len(tls_sni) > 0 {
tls["server_name"] = tls_sni
}
if len(tls_alpn) > 0 {
tls["alpn"] = strings.Split(tls_alpn, ",")
}
if tls_insecure == "1" || tls_insecure == "true" {
tls["insecure"] = true
}
if len(tls_fp) > 0 {
tls["utls"] = map[string]interface{}{
"enabled": true,
"fingerprint": tls_fp,
}
}
return &tls
}
+31 -13
View File
@@ -4,6 +4,7 @@ import (
"context" "context"
"crypto/tls" "crypto/tls"
"embed" "embed"
"html/template"
"io" "io"
"io/fs" "io/fs"
"net" "net"
@@ -17,9 +18,9 @@ import (
"strconv" "strconv"
"strings" "strings"
sessions "github.com/Calidity/gin-sessions"
"github.com/Calidity/gin-sessions/cookie"
"github.com/gin-contrib/gzip" "github.com/gin-contrib/gzip"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
@@ -53,6 +54,19 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default() engine := gin.Default()
// Load the HTML template
t := template.New("").Funcs(engine.FuncMap)
template, err := t.ParseFS(content, "html/index.html")
if err != nil {
return nil, err
}
engine.SetHTMLTemplate(template)
base_url, err := s.settingService.GetWebPath()
if err != nil {
return nil, err
}
webDomain, err := s.settingService.GetWebDomain() webDomain, err := s.settingService.GetWebDomain()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -68,10 +82,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
} }
engine.Use(gzip.Gzip(gzip.DefaultCompression)) engine.Use(gzip.Gzip(gzip.DefaultCompression))
assetsBasePath := "/assets/" assetsBasePath := base_url + "assets/"
store := cookie.NewStore(secret) store := cookie.NewStore(secret)
engine.Use(sessions.Sessions("session", store)) engine.Use(sessions.Sessions("s-ui", store))
engine.Use(func(c *gin.Context) { engine.Use(func(c *gin.Context) {
uri := c.Request.RequestURI uri := c.Request.RequestURI
if strings.HasPrefix(uri, assetsBasePath) { if strings.HasPrefix(uri, assetsBasePath) {
@@ -87,26 +102,29 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine.StaticFS(assetsBasePath, http.FS(assetsFS)) engine.StaticFS(assetsBasePath, http.FS(assetsFS))
group_api := engine.Group("/api") group_api := engine.Group(base_url + "api")
api.NewAPIHandler(group_api) api.NewAPIHandler(group_api)
// Serve index.html as the entry point // Serve index.html as the entry point
// Handle all other routes by serving index.html // Handle all other routes by serving index.html
engine.NoRoute(func(c *gin.Context) { engine.NoRoute(func(c *gin.Context) {
if c.Request.URL.Path != "/login" && !api.IsLogin(c) { if c.Request.URL.Path == strings.TrimSuffix(base_url, "/") {
c.Redirect(http.StatusTemporaryRedirect, "/login") c.Redirect(http.StatusTemporaryRedirect, base_url)
return return
} }
if c.Request.URL.Path == "/login" && api.IsLogin(c) { if !strings.HasPrefix(c.Request.URL.Path, base_url) {
c.Redirect(http.StatusTemporaryRedirect, "/") c.String(404, "")
return return
} }
data, err := content.ReadFile("html/index.html") if c.Request.URL.Path != base_url+"login" && !api.IsLogin(c) {
if err != nil { c.Redirect(http.StatusTemporaryRedirect, base_url+"login")
c.String(http.StatusInternalServerError, "Internal Server Error")
return return
} }
c.Data(http.StatusOK, "text/html", data) if c.Request.URL.Path == base_url+"login" && api.IsLogin(c) {
c.Redirect(http.StatusTemporaryRedirect, base_url)
return
}
c.HTML(http.StatusOK, "index.html", gin.H{"BASE_URL": base_url})
}) })
return engine, nil return engine, nil
+28
View File
@@ -0,0 +1,28 @@
FROM --platform=$BUILDPLATFORM golang:1.22-alpine AS singbox-builder
LABEL maintainer="Alireza <alireza7@gmail.com>"
WORKDIR /app
ARG TARGETOS TARGETARCH
ARG SINGBOX_VER=v1.9.3
ARG SINGBOX_TAGS="with_quic,with_grpc,with_wireguard,with_ech,with_utls,with_reality_server,with_acme,with_v2ray_api,with_clash_api,with_gvisor"
ARG GOPROXY=""
ENV GOPROXY ${GOPROXY}
ENV CGO_ENABLED=0
ENV GOOS=$TARGETOS
ENV GOARCH=$TARGETARCH
RUN apk --no-cache --update add build-base gcc wget unzip git
RUN set -ex \
&& git clone --depth 1 --branch $SINGBOX_VER https://github.com/SagerNet/sing-box.git \
&& cd sing-box \
&& go build -v -trimpath -tags \
$SINGBOX_TAGS \
-ldflags "-X \"github.com/sagernet/sing-box/constant.Version=$SINGBOX_VER\" -s -w -buildid=" \
./cmd/sing-box
FROM --platform=$TARGETPLATFORM alpine
LABEL maintainer="Alireza <alireza7@gmail.com>"
ENV TZ=Asia/Tehran
WORKDIR /app
RUN apk add --no-cache --update ca-certificates tzdata bash
COPY --from=singbox-builder /app/sing-box/sing-box .
COPY runSingbox.sh .
ENTRYPOINT [ "./runSingbox.sh" ]
+8
View File
@@ -42,4 +42,12 @@ do
;; ;;
esac esac
fi fi
# Check if sin-box crashed
if ! kill -0 $tokill > /dev/null 2>&1; then
if [ "$signal" != "stop" ]; then
echo "Sing-Box with PID $tokill crashed. Breaking the loop..."
break
fi
fi
done done
+45
View File
@@ -0,0 +1,45 @@
---
services:
s-ui:
image: alireza7/s-ui
container_name: s-ui
hostname: "S-UI docker"
volumes:
- "singbox:/app/bin"
- "$PWD/db:/app/db"
- "$PWD/cert:/app/cert"
environment:
SINGBOX_API: "sing-box:1080"
SUI_DB_FOLDER: "db"
tty: true
restart: unless-stopped
ports:
- "2095:2095"
- "2096:2096"
networks:
- s-ui
entrypoint: "./sui migrate && ./sui"
sing-box:
image: alireza7/s-ui-singbox
container_name: sing-box
volumes:
- "singbox:/app/"
- "$PWD/cert:/cert"
networks:
- s-ui
ports:
- "443:443"
- "1443:1443"
- "2443:2443"
- "3443:3443"
restart: unless-stopped
depends_on:
- s-ui
networks:
s-ui:
driver: bridge
volumes:
singbox:
+6
View File
@@ -5,6 +5,12 @@
<meta charset="UTF-8" /> <meta charset="UTF-8" />
<link rel="icon" href="assets/favicon.ico" /> <link rel="icon" href="assets/favicon.ico" />
<meta name="viewport" content="width=device-width, initial-scale=1.0" /> <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> <title>S-UI</title>
</head> </head>
+1155 -2620
View File
File diff suppressed because it is too large Load Diff
+24 -24
View File
@@ -1,42 +1,42 @@
{ {
"name": "frontend", "name": "frontend",
"version": "0.0.0", "version": "1.0.0",
"private": true, "private": true,
"scripts": { "scripts": {
"dev": "vite", "dev": "vite --host",
"build": "vue-tsc --noEmit && vite build", "build": "vue-tsc --noEmit && vite build",
"preview": "vite preview", "preview": "vite preview",
"lint": "eslint . --fix --ignore-path .gitignore" "lint": "eslint . --fix --ignore-path .gitignore"
}, },
"dependencies": { "dependencies": {
"@mdi/font": "7.0.96", "@mdi/font": "7.4.47",
"axios": "^1.6.5", "axios": "^1.7.2",
"chart.js": "^4.4.1", "chart.js": "^4.4.3",
"clipboard": "^2.0.11", "clipboard": "^2.0.11",
"core-js": "^3.29.0", "core-js": "^3.37.1",
"moment": "^2.30.1",
"notivue": "^2.4.4",
"pinia": "^2.1.7", "pinia": "^2.1.7",
"qrcode.vue": "^3.4.1", "qrcode.vue": "^3.4.1",
"roboto-fontface": "*", "roboto-fontface": "^0.10.0",
"vue": "^3.2.0", "vue": "^3.4.31",
"vue-chartjs": "^5.3.0", "vue-chartjs": "^5.3.1",
"vue-i18n": "^9.8.0", "vue-i18n": "^9.13.1",
"vue-router": "^4.0.0", "vue-router": "^4.4.0",
"vue3-persian-datetime-picker": "^1.2.2", "vue3-persian-datetime-picker": "^1.2.2",
"vuetify": "^3.0.0" "vuetify": "^3.6.10"
}, },
"devDependencies": { "devDependencies": {
"@babel/types": "^7.21.4", "@babel/types": "^7.24.7",
"@types/node": "^18.15.0", "@types/node": "^20.14.9",
"@vitejs/plugin-vue": "^4.0.0", "@vitejs/plugin-vue": "^5.0.5",
"@vue/eslint-config-typescript": "^11.0.0", "eslint-plugin-vue": "^9.26.0",
"eslint": "^8.22.0",
"eslint-plugin-vue": "^9.3.0",
"material-design-icons-iconfont": "^6.7.0", "material-design-icons-iconfont": "^6.7.0",
"sass": "^1.60.0", "sass": "^1.77.6",
"typescript": "^5.0.0", "typescript": "^5.5.2",
"unplugin-fonts": "^1.0.3", "unplugin-fonts": "^1.1.1",
"vite": "^4.2.0", "vite": "^5.3.2",
"vite-plugin-vuetify": "^1.0.0", "vite-plugin-vuetify": "^2.0.3",
"vue-tsc": "^1.2.0" "vue-tsc": "^2.0.22"
} }
} }
+114
View File
@@ -0,0 +1,114 @@
<template>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.addr')"
hide-details
required
v-model="addr.server">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.port')"
hide-details
type="number"
required
v-model.number="addr.server_port"></v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionRemark">
<v-text-field
:label="$t('in.remark')"
hide-details
v-model="addr.remark">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4" v-if="optionTLS">
<v-switch
:label="$t('tls.enable')"
color="primary"
hide-details
@update:model-value="updateTls($event)"
v-model="addr.tls" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionSNI">
<v-text-field
label="SNI"
hide-details
v-model="addr.server_name">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionInsecure">
<v-switch
:label="$t('tls.insecure')"
hide-details
color="primary"
v-model="addr.insecure" />
</v-col>
</v-row>
<v-row>
<v-spacer></v-spacer>
<v-col cols="auto" align="end" justify="center">
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details variant="tonal">{{ $t('in.mdOption') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionRemark" color="primary" :label="$t('in.remark')" hide-details></v-switch>
</v-list-item>
<v-list-item v-if="hasTls">
<v-switch v-model="optionTLS" color="primary" :label="$t('objects.tls')" hide-details></v-switch>
</v-list-item>
<v-list-item v-if="addr.tls">
<v-switch v-model="optionSNI" color="primary" label="SNI" hide-details></v-switch>
</v-list-item>
<v-list-item v-if="addr.tls">
<v-switch v-model="optionInsecure" color="primary" :label="$t('tls.insecure')" hide-details></v-switch>
</v-list-item>
</v-list>
</v-card>
</v-menu>
</v-col>
</v-row>
</template>
<script lang="ts">
export default {
props: ['addr', 'hasTls'],
data() {
return {
menu: false
}
},
computed: {
optionTLS: {
get(): boolean { return this.$props.addr.tls != undefined },
set(v:boolean) { this.$props.addr.tls = v ? true : undefined; this.updateTls(v) }
},
optionSNI: {
get(): boolean { return this.$props.addr.server_name != undefined },
set(v:boolean) { this.$props.addr.server_name = v ? '' : undefined }
},
optionRemark: {
get(): boolean { return this.$props.addr.remark != undefined },
set(v:boolean) { this.$props.addr.remark = v ? '' : undefined }
},
optionInsecure: {
get(): boolean { return this.$props.addr.insecure != undefined },
set(v:boolean) { this.$props.addr.insecure = v ? false : undefined }
}
},
methods: {
updateTls(v:boolean) {
if (!v) {
delete this.$props.addr.insecure
delete this.$props.addr.server_name
}
}
}
}
</script>
+21 -7
View File
@@ -10,7 +10,7 @@
<DatePicker <DatePicker
v-model="Input" v-model="Input"
@input="Input=$event" @input="Input=$event"
:locale="$i18n.locale" :locale="locale"
element="expiry" element="expiry"
compact-time compact-time
type="datetime"> type="datetime">
@@ -42,6 +42,9 @@
<script lang="ts"> <script lang="ts">
import DatePicker from 'vue3-persian-datetime-picker' import DatePicker from 'vue3-persian-datetime-picker'
import { i18n } from '@/locales' import { i18n } from '@/locales'
import 'moment/locale/vi'
import 'moment/locale/zh-cn'
import 'moment/locale/zh-tw'
export default { export default {
props: ['expiry'], props: ['expiry'],
@@ -54,10 +57,21 @@ export default {
}, },
components: { DatePicker }, components: { DatePicker },
computed: { computed: {
locale() {
const l = i18n.global.locale.value
switch (l) {
case "zhHans":
return "zh-cn"
case "zhHant":
return "zh-tw"
default:
return l
}
},
dateFormatted() { dateFormatted() {
if (this.expDate == 0) return i18n.global.t('unlimited') if (this.expDate == 0) return i18n.global.t('unlimited')
const date = new Date(this.expDate*1000) const date = new Date(this.expDate*1000)
return date.toLocaleString(i18n.global.locale.value) return date.toLocaleString(this.locale)
}, },
expDate() { expDate() {
return parseInt(this.expiry?? 0) return parseInt(this.expiry?? 0)
@@ -99,18 +113,18 @@ export default {
<style> <style>
.vpd-addon-list, .vpd-addon-list,
.vpd-addon-list-item { .vpd-addon-list-item {
background-color: rgb(var(--v-theme-background)); background-color: rgb(var(--v-theme-background)) !important;
border-color: rgb(var(--v-theme-background)); border-color: rgb(var(--v-theme-background)) !important;
} }
.vpd-content { .vpd-content {
background-color: rgb(var(--v-theme-background)); background-color: rgb(var(--v-theme-background)) !important;
} }
.vpd-addon-list-item.vpd-selected, .vpd-addon-list-item.vpd-selected,
.vpd-addon-list-item:hover { .vpd-addon-list-item:hover {
background-color: rgb(var(--v-theme-primary)); background-color: rgb(var(--v-theme-primary)) !important;
} }
.vpd-close-addon { .vpd-close-addon {
color: rgb(var(--v-theme-on-surface)); color: rgb(var(--v-theme-on-surface)) !important;
background-color: transparent; background-color: transparent;
} }
.vpd-controls { .vpd-controls {
+27 -28
View File
@@ -1,15 +1,17 @@
<template> <template>
<v-card subtitle="Dial" style="background-color: inherit;"> <v-card :subtitle="$t('objects.dial')" style="background-color: inherit;">
<v-row> <v-row>
<v-col cols="12" sm="6" md="4" v-if="optionDetour"> <v-col cols="12" sm="6" md="4" v-if="optionDetour">
<v-text-field <v-select
label="Forward to Outbound tag"
hide-details hide-details
v-model="dial.detour"></v-text-field> :label="$t('dial.detourText')"
:items="outTags"
v-model="dial.detour">
</v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4" v-if="optionBind"> <v-col cols="12" sm="6" md="4" v-if="optionBind">
<v-text-field <v-text-field
label="Bind to Network Interface" :label="$t('dial.bindIf')"
hide-details hide-details
v-model="dial.bind_interface"></v-text-field> v-model="dial.bind_interface"></v-text-field>
</v-col> </v-col>
@@ -17,13 +19,13 @@
<v-row> <v-row>
<v-col cols="12" sm="6" md="4" v-if="optionIPV4"> <v-col cols="12" sm="6" md="4" v-if="optionIPV4">
<v-text-field <v-text-field
label="Bind to IPv4" :label="$t('dial.bindIp4')"
hide-details hide-details
v-model="dial.inet4_bind_address"></v-text-field> v-model="dial.inet4_bind_address"></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4" v-if="optionIPV6"> <v-col cols="12" sm="6" md="4" v-if="optionIPV6">
<v-text-field <v-text-field
label="Bind to IPv6" :label="$t('dial.bindIp6')"
hide-details hide-details
v-model="dial.inet6_bind_address"></v-text-field> v-model="dial.inet6_bind_address"></v-text-field>
</v-col> </v-col>
@@ -38,7 +40,7 @@
v-model.number="routingMark"></v-text-field> v-model.number="routingMark"></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4" v-if="optionRA"> <v-col cols="12" sm="6" md="4" v-if="optionRA">
<v-switch v-model="dial.reuse_addr" color="primary" label="Reuse listener address" hide-details></v-switch> <v-switch v-model="dial.reuse_addr" color="primary" :label="$t('dial.reuseAddr')" hide-details></v-switch>
</v-col> </v-col>
</v-row> </v-row>
<v-row v-if="optionTCP"> <v-row v-if="optionTCP">
@@ -55,11 +57,11 @@
</v-col> </v-col>
<v-col cols="12" sm="6" md="4" v-if="optionCT"> <v-col cols="12" sm="6" md="4" v-if="optionCT">
<v-text-field <v-text-field
label="Connection Timeout" :label="$t('dial.connTimeout')"
hide-details hide-details
type="number" type="number"
min="1" min="1"
suffix="s" :suffix="$t('date.s')"
v-model.number="connectTimeout"></v-text-field> v-model.number="connectTimeout"></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -67,22 +69,19 @@
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-select <v-select
hide-details hide-details
clearable :label="$t('listen.domainStrategy')"
@click:clear="delete dial.domain_strategy"
width="100"
label="Domain to IP Strategy"
:items="['prefer_ipv4','prefer_ipv6','ipv4_only','ipv6_only']" :items="['prefer_ipv4','prefer_ipv6','ipv4_only','ipv6_only']"
v-model="dial.domain_strategy"> v-model="dial.domain_strategy">
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Fallback Timeout" :label="$t('dial.fbTimeout')"
hide-details hide-details
type="number" type="number"
min="50" min="50"
step="50" step="50"
suffix="ms" :suffix="$t('date.ms')"
v-model.number="fallbackDelay"></v-text-field> v-model.number="fallbackDelay"></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -90,39 +89,39 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>Dial Options</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('dial.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
<v-list-item> <v-list-item>
<v-switch v-model="optionDetour" color="primary" label="Detour" hide-details></v-switch> <v-switch v-model="optionDetour" color="primary" :label="$t('listen.detour')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionBind" color="primary" label="Bind Interface" hide-details></v-switch> <v-switch v-model="optionBind" color="primary" :label="$t('dial.bindIf')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionIPV4" color="primary" label="Bind to IPv4" hide-details></v-switch> <v-switch v-model="optionIPV4" color="primary" :label="$t('dial.bindIp4')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionIPV6" color="primary" label="Bind to IPv6" hide-details></v-switch> <v-switch v-model="optionIPV6" color="primary" :label="$t('dial.bindIp6')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionRM" color="primary" label="Routing Mark" hide-details></v-switch> <v-switch v-model="optionRM" color="primary" label="Routing Mark" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionRA" color="primary" label="Reuse Address" hide-details></v-switch> <v-switch v-model="optionRA" color="primary" :label="$t('dial.reuseAddr')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionTCP" color="primary" label="TCP Options" hide-details></v-switch> <v-switch v-model="optionTCP" color="primary" :label="$t('listen.tcpOptions')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionUDP" color="primary" label="UDP Options" hide-details></v-switch> <v-switch v-model="optionUDP" color="primary" :label="$t('listen.udpOptions')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionCT" color="primary" label="Connection Timeout" hide-details></v-switch> <v-switch v-model="optionCT" color="primary" :label="$t('dial.connTimeout')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionDS" color="primary" label="Domain Strategy" hide-details></v-switch> <v-switch v-model="optionDS" color="primary" :label="$t('listen.domainStrategy')" hide-details></v-switch>
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-card> </v-card>
@@ -133,7 +132,7 @@
<script lang="ts"> <script lang="ts">
export default { export default {
props: ['dial'], props: ['dial', 'outTags'],
data() { data() {
return { return {
menu: false menu: false
@@ -154,7 +153,7 @@ export default {
}, },
optionDetour: { optionDetour: {
get(): boolean { return this.$props.dial.detour != undefined }, get(): boolean { return this.$props.dial.detour != undefined },
set(v:boolean) { v ? this.$props.dial.detour = '' : delete this.$props.dial.detour } set(v:boolean) { v ? this.$props.dial.detour = this.outTags[0]?? '' : delete this.$props.dial.detour }
}, },
optionBind: { optionBind: {
get(): boolean { return this.$props.dial.bind_interface != undefined }, get(): boolean { return this.$props.dial.bind_interface != undefined },
+99
View File
@@ -0,0 +1,99 @@
<template>
<v-card>
<v-card-subtitle>
{{ $t('objects.headers') }}
<v-icon @click="add_header" icon="mdi-plus"></v-icon>
</v-card-subtitle>
<v-row v-for="(header, index) in hdrs">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('objects.key')"
hide-details
@input="update_key(index,$event.target.value)"
v-model="header.name">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('objects.value')"
hide-details
@input="update_value(index,$event.target.value)"
append-icon="mdi-delete"
@click:append="del_header(index)"
v-model="header.value">
</v-text-field>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
type Header = {
name: string
value: string
}
export default {
props: ['data'],
data() {
return {}
},
methods: {
add_header() {
this.hdrs = [...this.hdrs, {name: "Host", value: ""}]
},
del_header(i:number) {
let h = this.hdrs
h.splice(i,1)
this.hdrs = h
},
update_key(i:number,k:string) {
let h = this.hdrs
h[i].name = k
this.hdrs = h
},
update_value(i:number,v:string) {
let h = this.hdrs
h[i].value = v
this.hdrs = h
},
},
computed: {
hdrs: {
get() :Header[] {
let headers: Header[] = []
const h = this.$props.data.headers
if (h) {
Object.keys(h).forEach(key => {
if (Array.isArray(h[key])){
h[key].forEach((v:string) => headers.push({ name: key, value: v }))
} else {
headers.push({ name: key, value: h[key] })
}
})
}
return headers
},
set(v:Header[]) {
if (v.length>0) {
let headers:any = {}
v.forEach((h:Header) => {
if (headers[h.name]) {
if (Array.isArray(headers[h.name])) {
headers[h.name].push(h.value)
} else {
headers[h.name] = [headers[h.name], h.value]
}
} else {
headers[h.name] = h.value
}
})
this.$props.data.headers = headers
} else {
this.$props.data.headers = undefined
}
}
}
}
}
</script>
-75
View File
@@ -1,75 +0,0 @@
<template>
<v-card :subtitle="$t('in.multiplex')">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="Enable Multiplex" v-model="muxEnable" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="mux.enabled">
<v-switch color="primary" label="Reject Non-Padded" v-model="mux.padding" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="mux.enabled">
<v-switch color="primary" label="Enable Brutal" v-model="burtalEnable" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="mux.brutal?.enabled">
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Uplink Bandwidth"
hide-details
type="number"
suffix="Mbps"
v-model.number="up_mbps">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Downlink Bandwidth"
hide-details
type="number"
suffix="Mbps"
min="0"
v-model.number="down_mbps">
</v-text-field>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import { iMultiplex } from '@/types/inMultiplex'
export default {
props: ['inbound'],
data() {
return {}
},
computed: {
mux(): iMultiplex {
return <iMultiplex> this.$props.inbound.multiplex
},
muxEnable: {
get(): boolean { return this.$props.inbound.multiplex ? this.mux.enabled : false },
set(newValue:boolean) { this.$props.inbound.multiplex = newValue ? { enabled: newValue } : {} }
},
burtalEnable: {
get(): boolean { return this.mux.brutal ? this.mux.brutal.enabled : false },
set(newValue:boolean) { this.mux.brutal = { enabled: newValue, up_mbps: 100, down_mbps: 100 } }
},
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>
+20 -19
View File
@@ -1,5 +1,5 @@
<template> <template>
<v-card subtitle="Listen"> <v-card :subtitle="$t('objects.listen')">
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
@@ -20,27 +20,29 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6" md="4" v-if="optionDetour"> <v-col cols="12" sm="6" md="4" v-if="optionDetour">
<v-text-field <v-select
label="Forward to Inbound tag" :label="$t('listen.detourText')"
hide-details hide-details
v-model="inbound.detour"></v-text-field> :items="inTags"
v-model="inbound.detour">
</v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-switch v-model="inbound.sniff" color="primary" :label="$t('in.sniffing')" hide-details></v-switch> <v-switch v-model="inbound.sniff" color="primary" :label="$t('listen.sniffing')" hide-details></v-switch>
</v-col> </v-col>
</v-row> </v-row>
<v-row v-if="inbound.sniff"> <v-row v-if="inbound.sniff">
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-switch v-model="inbound.sniff_override_destination" color="primary" label="Override Sniffed Domain" hide-details></v-switch> <v-switch v-model="inbound.sniff_override_destination" color="primary" :label="$t('listen.sniffingOverride')" hide-details></v-switch>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Sniffing Timeout" :label="$t('listen.sniffingTimeout')"
hide-details hide-details
type="number" type="number"
min="50" min="50"
step="50" step="50"
suffix="ms" :suffix="$t('date.ms')"
v-model.number="sniffTimeout"></v-text-field> v-model.number="sniffTimeout"></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -62,7 +64,7 @@
hide-details hide-details
type="number" type="number"
min="1" min="1"
suffix="Min" :suffix="$t('date.m')"
v-model.number="udpTimeout"></v-text-field> v-model.number="udpTimeout"></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -70,8 +72,7 @@
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-select <v-select
hide-details hide-details
width="100" :label="$t('listen.domainStrategy')"
label="Domain to IP Strategy"
:items="['prefer_ipv4','prefer_ipv6','ipv4_only','ipv6_only']" :items="['prefer_ipv4','prefer_ipv6','ipv4_only','ipv6_only']"
v-model="inbound.domain_strategy"> v-model="inbound.domain_strategy">
</v-select> </v-select>
@@ -81,21 +82,21 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>Listen Options</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('listen.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
<v-list-item> <v-list-item>
<v-switch v-model="optionTCP" color="primary" label="TCP Options" hide-details></v-switch> <v-switch v-model="optionDetour" color="primary" :label="$t('listen.detour')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionUDP" color="primary" label="UDP Options" hide-details></v-switch> <v-switch v-model="optionTCP" color="primary" :label="$t('listen.tcpOptions')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionDetour" color="primary" label="Detour" hide-details></v-switch> <v-switch v-model="optionUDP" color="primary" :label="$t('listen.udpOptions')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionDS" color="primary" label="Domain Strategy" hide-details></v-switch> <v-switch v-model="optionDS" color="primary" :label="$t('listen.domainStrategy')" hide-details></v-switch>
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-card> </v-card>
@@ -106,7 +107,7 @@
<script lang="ts"> <script lang="ts">
export default { export default {
props: ['inbound'], props: ['inbound', 'inTags'],
data() { data() {
return { return {
menu: false menu: false
@@ -138,12 +139,12 @@ export default {
}, },
set(v:boolean) { set(v:boolean) {
this.$props.inbound.udp_fragment = v ? false : undefined this.$props.inbound.udp_fragment = v ? false : undefined
this.$props.inbound.udp_timeout = v ? false : undefined this.$props.inbound.udp_timeout = v ? '5m' : undefined
} }
}, },
optionDetour: { optionDetour: {
get(): boolean { return this.$props.inbound.detour != undefined }, get(): boolean { return this.$props.inbound.detour != undefined },
set(v:boolean) { this.$props.inbound.detour = v ? '' : undefined } set(v:boolean) { this.$props.inbound.detour = v ? this.inTags[0]?? '' : undefined }
}, },
optionDS: { optionDS: {
get(): boolean { return this.$props.inbound.domain_strategy != undefined }, get(): boolean { return this.$props.inbound.domain_strategy != undefined },
+39 -5
View File
@@ -1,4 +1,10 @@
<template> <template>
<LogVue
v-model="logModal.visible"
:visible="logModal.visible"
:logType="logModal.logType"
@close="closeLogs"
/>
<v-container class="fill-height"> <v-container class="fill-height">
<v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" > <v-responsive :class="reloadItems.length>0 ? 'fill-height text-center' : 'align-center'" >
<v-row class="d-flex align-center justify-center"> <v-row class="d-flex align-center justify-center">
@@ -10,7 +16,7 @@
<v-col cols="auto"> <v-col cols="auto">
<v-dialog v-model="menu" :close-on-content-click="false" transition="scale-transition" max-width="800"> <v-dialog v-model="menu" :close-on-content-click="false" transition="scale-transition" max-width="800">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" variant="tonal">{{ $t('main.tiles') }} <v-icon icon="mdi-star-plus" /></v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('main.tiles') }} <v-icon icon="mdi-star-plus" /></v-btn>
</template> </template>
<v-card rounded="xl"> <v-card rounded="xl">
<v-card-title> <v-card-title>
@@ -45,9 +51,9 @@
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6" md="3" v-for="i in reloadItems" :key="i"> <v-col cols="12" sm="6" md="3" v-for="i in reloadItems" :key="i">
<v-card class="rounded-lg" variant="outlined" height="200px" <v-card class="rounded-lg" variant="outlined" height="210px"
:title="menuItems.flatMap(cat => cat.value).find(m => m.value == i)?.title"> :title="menuItems.flatMap(cat => cat.value).find(m => m.value == i)?.title">
<v-card-text style="padding: 0 16px;"> <v-card-text style="padding: 0 16px;" align="center" justify="center">
<Gauge :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'g'" /> <Gauge :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'g'" />
<History :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'h'" /> <History :tilesData="tilesData" :type="i" v-if="i.charAt(0) == 'h'" />
<template v-if="i == 'i-sys'"> <template v-if="i == 'i-sys'">
@@ -80,13 +86,19 @@
</v-col> </v-col>
<v-col cols="3">S-UI</v-col> <v-col cols="3">S-UI</v-col>
<v-col cols="9"> <v-col cols="9">
<v-chip density="compact" color="primary" variant="flat"> <v-chip density="compact" color="blue">
<v-tooltip activator="parent" location="top"> <v-tooltip activator="parent" location="top">
{{ $t('main.info.threads') }}: {{ tilesData.sys?.appThreads }}<br /> {{ $t('main.info.threads') }}: {{ tilesData.sys?.appThreads }}<br />
{{ $t('main.info.memory') }}: {{ HumanReadable.sizeFormat(tilesData.sys?.appMem) }} {{ $t('main.info.memory') }}: {{ HumanReadable.sizeFormat(tilesData.sys?.appMem) }}
</v-tooltip> </v-tooltip>
v{{ tilesData.sys?.appVersion }} v{{ tilesData.sys?.appVersion }}
</v-chip> </v-chip>
<v-chip density="compact" color="transparent" style="cursor: pointer;" @click="openLogs('s-ui')">
<v-tooltip activator="parent" location="top">
{{ $t('basic.log.title') + " - S-UI" }}
</v-tooltip>
<v-icon icon="mdi-list-box-outline" color="blue" />
</v-chip>
</v-col> </v-col>
<v-col cols="3">{{ $t('main.info.uptime') }}</v-col> <v-col cols="3">{{ $t('main.info.uptime') }}</v-col>
<v-col cols="9">{{ HumanReadable.formatSecond(tilesData.uptime) }}</v-col> <v-col cols="9">{{ HumanReadable.formatSecond(tilesData.uptime) }}</v-col>
@@ -98,6 +110,12 @@
<v-col cols="8"> <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="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="error" variant="flat" v-else>{{ $t('no') }}</v-chip>
<v-chip density="compact" color="transparent" style="cursor: pointer;" @click="openLogs('sing-box')">
<v-tooltip activator="parent" location="top">
{{ $t('basic.log.title') + " - Sing-Box" }}
</v-tooltip>
<v-icon icon="mdi-list-box-outline" :color="tilesData.sbd?.running ? 'success': 'error'" />
</v-chip>
</v-col> </v-col>
<v-col cols="4">{{ $t('main.info.memory') }}</v-col> <v-col cols="4">{{ $t('main.info.memory') }}</v-col>
<v-col cols="8"> <v-col cols="8">
@@ -148,6 +166,7 @@ import Gauge from '@/components/tiles/Gauge.vue'
import History from '@/components/tiles/History.vue' import History from '@/components/tiles/History.vue'
import { computed, onBeforeUnmount, onMounted, ref } from 'vue' import { computed, onBeforeUnmount, onMounted, ref } from 'vue'
import { i18n } from '@/locales' import { i18n } from '@/locales'
import LogVue from '@/layouts/modals/Logs.vue'
const menu = ref(false) const menu = ref(false)
const menuItems = [ const menuItems = [
@@ -184,7 +203,7 @@ const reloadItems = computed({
const reloadData = async () => { const reloadData = async () => {
const request = [...new Set(reloadItems.value.map(r => r.split('-')[1]))] const request = [...new Set(reloadItems.value.map(r => r.split('-')[1]))]
const data = await HttpUtils.get('/api/status',{ r: request.join(',')}) const data = await HttpUtils.get('api/status',{ r: request.join(',')})
if (data.success) { if (data.success) {
tilesData.value = data.obj tilesData.value = data.obj
} }
@@ -215,4 +234,19 @@ onMounted(() => {
onBeforeUnmount(() => { onBeforeUnmount(() => {
stopTimer() stopTimer()
}) })
const logModal = ref({
visible: false,
logType: "s-ui"
})
const openLogs = (logType: string) => {
logModal.value.logType = logType
logModal.value.visible = true
}
const closeLogs = () => {
logModal.value.logType = "s-ui"
logModal.value.visible = false
}
</script> </script>
+128
View File
@@ -0,0 +1,128 @@
<template>
<v-card :subtitle="$t('objects.multiplex')">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('mux.enable')" v-model="muxEnable" hide-details></v-switch>
</v-col>
<template v-if="mux.enabled">
<template v-if="direction=='out'">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:items="[ 'smux', 'yamux', 'h2mux']"
:label="$t('protocol')"
clearable
@click:clear="mux.protocol=undefined"
v-model="mux.protocol">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('mux.maxConn')"
hide-details
type="number"
min=0
v-model.number="max_connections">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('mux.minStr')"
hide-details
type="number"
min=0
v-model.number="min_streams">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('mux.maxStr')"
hide-details
type="number"
:min="min_streams"
v-model.number="max_streams">
</v-text-field>
</v-col>
</template>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('mux.padding')" v-model="mux.padding" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('mux.enableBrutal')" v-model="burtalEnable" hide-details></v-switch>
</v-col>
</template>
</v-row>
<v-row v-if="mux.brutal?.enabled">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('stats.upload')"
hide-details
type="number"
:suffix="$t('stats.Mbps')"
v-model.number="up_mbps">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('stats.download')"
hide-details
type="number"
:suffix="$t('stats.Mbps')"
min="0"
v-model.number="down_mbps">
</v-text-field>
</v-col>
</v-row>
</v-card>
</template>
<script lang="ts">
import { oMultiplex } from '@/types/multiplex'
export default {
props: ['data', 'direction'],
data() {
return {}
},
computed: {
mux(): oMultiplex {
return <oMultiplex> this.$props.data.multiplex
},
muxEnable: {
get(): boolean { return this.mux ? this.mux.enabled : false },
set(newValue:boolean) { this.$props.data.multiplex = newValue ? { enabled: newValue } : {} }
},
max_connections: {
get(): number { return this.mux.max_connections ? this.mux.max_connections : 0 },
set(newValue:number) { this.mux.max_connections = newValue > 0 ? newValue : undefined }
},
min_streams: {
get(): number { return this.mux.min_streams ? this.mux.min_streams : 0 },
set(newValue:number) { this.mux.min_streams = newValue > 0 ? newValue : undefined }
},
max_streams: {
get(): number { return this.mux.max_streams ? this.mux.max_streams : 0 },
set(newValue:number) { this.mux.max_streams = newValue > 0 ? newValue : undefined }
},
burtalEnable: {
get(): boolean { return this.mux.brutal ? this.mux.brutal.enabled : false },
set(newValue:boolean) { this.mux.brutal = newValue ? { enabled: newValue, up_mbps: 100, down_mbps: 100 } : undefined }
},
down_mbps: {
get() { return this.mux.brutal && this.mux.brutal.down_mbps ? this.mux.brutal.down_mbps : 0 },
set(newValue:any) {
if (this.mux.brutal){
this.mux.brutal.down_mbps = newValue.length != 0 ? newValue : 0
}
}
},
up_mbps: {
get() { return this.mux.brutal && this.mux.brutal.up_mbps ? this.mux.brutal.up_mbps : 0 },
set(newValue:any) {
if (this.mux.brutal){
this.mux.brutal.up_mbps = newValue.length != 0 ? newValue : 0
}
}
},
}
}
</script>
+3 -3
View File
@@ -9,7 +9,7 @@
<script lang="ts"> <script lang="ts">
export default { export default {
props: ['inbound'], props: ['data'],
data() { data() {
return { return {
networks: [ networks: [
@@ -21,8 +21,8 @@ export default {
}, },
computed: { computed: {
Network: { Network: {
get():string { return this.$props.inbound.network?? '' }, get():string { return this.$props.data.network?? '' },
set(v:string) { this.$props.inbound.network = v != '' ? v : undefined } set(v:string) { this.$props.data.network = v != '' ? v : undefined }
} }
} }
} }
+123
View File
@@ -0,0 +1,123 @@
<template>
<v-card :subtitle="type">
<v-row>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.SOCKS">
<v-select
hide-details
:items="['4','4a','5']"
:label="$t('version')"
v-model="inData.outJson.version">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="needNetwork">
<Network :data="inData.outJson" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="needUot">
<UoT :data="inData.outJson" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.HTTP">
<v-text-field
:label="$t('transport.path')"
hide-details
v-model="inData.outJson.path">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.VMess || type == inTypes.VLESS">
<v-select
hide-details
:label="$t('types.vless.udpEnc')"
:items="['none','packetaddr','xudp']"
v-model="packet_encoding">
</v-select>
</v-col>
<template v-if="type == inTypes.VMess">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('types.vmess.security')"
:items="vmessSecurities"
v-model="inData.outJson.security">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inData.outJson.global_padding" color="primary" :label="$t('types.vmess.globalPadding')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="inData.outJson.authenticated_length" color="primary" :label="$t('types.vmess.authLen')" hide-details></v-switch>
</v-col>
</template>
<v-col cols="12" sm="6" md="4" v-if="type == inTypes.Hysteria">
<v-text-field
label="Recv window"
hide-details
type="number"
min="0"
v-model.number="inData.outJson.recv_window">
</v-text-field>
</v-col>
<template v-if="type == inTypes.TUIC">
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
label="UDP Relay Mode"
:items="['native', 'quic']"
clearable
@click:clear="delete inData.outJson.udp_relay_mode"
v-model="inData.outJson.udp_relay_mode">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="UDP Over Stream" v-model="inData.outJson.udp_over_stream" hide-details></v-switch>
</v-col>
</template>
</v-row>
<Headers :data="inData.outJson" v-if="type == inTypes.HTTP" />
</v-card>
</template>
<script lang="ts">
import { InTypes } from '@/types/inbounds'
import Network from './Network.vue'
import UoT from './UoT.vue'
import Headers from './Headers.vue'
export default {
props: ['inData', 'type'],
data() {
return {
inTypes: InTypes,
vmessSecurities: [
"auto",
"none",
"zero",
"aes-128-gcm",
"aes-128-ctr",
"chacha20-poly1305",
],
haveNetwork: [
InTypes.SOCKS,
InTypes.Shadowsocks,
InTypes.VMess,
InTypes.Trojan,
InTypes.Hysteria,
InTypes.VLESS,
InTypes.TUIC,
InTypes.Hysteria2,
],
havUoT: [
InTypes.SOCKS,
InTypes.Shadowsocks,
],
}
},
computed: {
needNetwork():boolean { return this.haveNetwork.includes(this.$props.type) },
needUot():boolean { return this.havUoT.includes(this.$props.type) },
packet_encoding: {
get() { return this.$props.inData.outJson.packet_encoding != undefined ? this.$props.inData.outJson.packet_encoding : 'none'; },
set(v:string) { this.$props.inData.outJson.packet_encoding = v != "none" ? v : undefined }
},
},
components: { Network, UoT, Headers }
}
</script>
+382
View File
@@ -0,0 +1,382 @@
<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.srcIp') + ' ' + $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>
+368
View File
@@ -0,0 +1,368 @@
<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>
<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="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
},
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
}
}
},
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()
}
},
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>
+6 -7
View File
@@ -1,5 +1,5 @@
<template> <template>
<v-card :subtitle="$t('in.transport')"> <v-card :subtitle="$t('objects.transport')">
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('transport.enable')" v-model="tpEnable" hide-details></v-switch> <v-switch color="primary" :label="$t('transport.enable')" v-model="tpEnable" hide-details></v-switch>
@@ -7,7 +7,6 @@
<v-col cols="12" sm="6" md="4" v-if="tpEnable"> <v-col cols="12" sm="6" md="4" v-if="tpEnable">
<v-select <v-select
hide-details hide-details
width="100"
:label="$t('type')" :label="$t('type')"
:items="Object.keys(trspTypes).map((key,index) => ({title: key, value: Object.values(trspTypes)[index]}))" :items="Object.keys(trspTypes).map((key,index) => ({title: key, value: Object.values(trspTypes)[index]}))"
v-model="transportType"> v-model="transportType">
@@ -28,7 +27,7 @@ import WebSocket from './transports/WebSocket.vue'
import GRPC from './transports/gRPC.vue' import GRPC from './transports/gRPC.vue'
import HttpUpgrade from './transports/HttpUpgrade.vue' import HttpUpgrade from './transports/HttpUpgrade.vue'
export default { export default {
props: ['inbound'], props: ['data'],
data() { data() {
return { return {
trspTypes: TrspTypes trspTypes: TrspTypes
@@ -36,15 +35,15 @@ export default {
}, },
computed: { computed: {
Transport() { Transport() {
return <Transport>this.$props.inbound.transport return <Transport>this.$props.data.transport
}, },
tpEnable: { tpEnable: {
get() { return Object.hasOwn(this.$props.inbound.transport, 'type') }, get() { return Object.hasOwn(this.$props.data.transport, 'type') },
set(newValue: boolean) { this.$props.inbound.transport = newValue ? { type: 'http' } : {} } set(newValue: boolean) { this.$props.data.transport = newValue ? { type: 'http' } : {} }
}, },
transportType: { transportType: {
get() { return this.Transport.type }, get() { return this.Transport.type },
set(newValue: string) { this.$props.inbound.transport = { type: newValue } } set(newValue: string) { this.$props.data.transport = { type: newValue } }
} }
}, },
components: { Http, WebSocket, GRPC, HttpUpgrade } components: { Http, WebSocket, GRPC, HttpUpgrade }
+29
View File
@@ -0,0 +1,29 @@
<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>
+2 -2
View File
@@ -1,5 +1,5 @@
<template> <template>
<v-card subtitle="Clients"> <v-card :subtitle="$t('pages.clients')">
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-switch <v-switch
@@ -16,7 +16,7 @@
<script lang="ts"> <script lang="ts">
export default { export default {
props: ['inbound', 'id'], props: ['inbound'],
data() { data() {
return { return {
hasUser: false, hasUser: false,
+59
View File
@@ -0,0 +1,59 @@
<template>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.addr')"
hide-details
v-model="data.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="data.server_port">
</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">
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
}
}
},
}
}
</script>
+31 -11
View File
@@ -1,18 +1,38 @@
<template> <template>
<v-snackbar <Notivue v-slot="item">
v-model="sb.showMsg" <NotivueSwipe :item="item">
location="top" <Notification
:color="snackbar.color" :item="item"
:timeout="snackbar.timeout"> :theme="theme"
{{ snackbar.message }} :dir="direction"
</v-snackbar> :icons="outlinedIcons"
:hideClose="true"
@click="item.clear"
/>
</NotivueSwipe>
</Notivue>
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { ref } from 'vue' import { Notivue, Notification, NotivueSwipe, outlinedIcons, pastelTheme, darkTheme } from 'notivue'
import Message from '@/store/modules/message' import { computed } from 'vue'
import { useTheme } from 'vuetify'
import vuetify from '@/plugins/vuetify';
const sb = Message() const Theme = useTheme()
const snackbar = ref(sb.snackbar) const theme = computed(() =>{
return Theme.global.name.value == "light" ? pastelTheme : darkTheme
})
const direction = computed(() => {
return vuetify.locale.isRtl ? 'rtl' : 'ltr'
})
</script> </script>
<style>
:root {
--nv-z: 10020;
}
</style>
+9 -9
View File
@@ -1,23 +1,23 @@
<template> <template>
<v-card subtitle="Direct"> <v-card subtitle="Direct">
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4" v-if="direction == 'in'">
<Network :inbound="inbound" /> <Network :data="data" />
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Override Address" :label="$t('types.direct.overrideAddr')"
hide-details hide-details
v-model="inbound.override_address"> v-model="data.override_address">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Override Port" :label="$t('types.direct.overridePort')"
type="number" type="number"
min="0" min="0"
hide-details hide-details
v-model="override_port"> v-model.number="override_port">
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -28,14 +28,14 @@
import Network from '@/components/Network.vue' import Network from '@/components/Network.vue'
export default { export default {
props: ['inbound'], props: ['direction','data'],
data() { data() {
return {} return {}
}, },
computed: { computed: {
override_port: { override_port: {
get() { return this.$props.inbound.override_port ? this.$props.inbound.override_port : ''; }, get() { return this.$props.data.override_port ? this.$props.data.override_port : ''; },
set(newValue: any) { this.$props.inbound.override_port = newValue.length == 0 || newValue == 0 ? undefined : parseInt(newValue); } set(newValue: any) { this.$props.data.override_port = newValue.length == 0 || newValue == 0 ? undefined : parseInt(newValue); }
}, },
}, },
components: { Network } components: { Network }
@@ -0,0 +1,50 @@
<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>
+110 -14
View File
@@ -3,19 +3,19 @@
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Uplink Limit" :label="$t('stats.upload')"
hide-details hide-details
type="number" type="number"
suffix="Mbps" :suffix="$t('stats.Mbps')"
v-model.number="up_mbps"> v-model.number="up_mbps">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Downlink Limit" :label="$t('stats.download')"
hide-details hide-details
type="number" type="number"
suffix="Mbps" :suffix="$t('stats.Mbps')"
min="0" min="0"
v-model.number="down_mbps"> v-model.number="down_mbps">
</v-text-field> </v-text-field>
@@ -24,40 +24,136 @@
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="obfs Password" :label="$t('types.hy.obfs')"
hide-details hide-details
v-model="inbound.obfs"> 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-text-field>
</v-col> </v-col>
</v-row> </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> </v-card>
</template> </template>
<script lang="ts"> <script lang="ts">
import Network from '@/components/Network.vue'
export default { export default {
props: ['inbound'], props: ['direction','data'],
data() { data() {
return { return {
menu: false,
} }
}, },
computed: { 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: { down_mbps: {
get() { return this.$props.inbound.down_mbps ? this.$props.inbound.down_mbps : 0 }, get() { return this.$props.data.down_mbps ? this.$props.data.down_mbps : 0 },
set(newValue:any) { set(newValue:any) {
if (newValue.length != 0 ){ if (newValue.length != 0 ){
this.$props.inbound.down_mbps = newValue this.$props.data.down_mbps = newValue
this.$props.inbound.down = "" + newValue + " Mbps" this.$props.data.down = "" + newValue + " Mbps"
} else { } else {
this.$props.inbound.down_mbps = 0 this.$props.data.down_mbps = 0
this.$props.inbound.down = "0 Mbps" this.$props.data.down = "0 Mbps"
} }
} }
}, },
up_mbps: { up_mbps: {
get() { return this.$props.inbound.up_mbps ? this.$props.inbound.up_mbps : 0 }, get() { return this.$props.data.up_mbps ? this.$props.data.up_mbps : 0 },
set(newValue:number) { this.$props.inbound.up_mbps = newValue > 0 ? newValue : 0 } set(newValue:number) { this.$props.data.up_mbps = newValue > 0 ? newValue : 0 }
}, },
}, },
components: { Network }
} }
</script> </script>
+45 -27
View File
@@ -1,43 +1,57 @@
<template> <template>
<v-card subtitle="Hysteria2"> <v-card subtitle="Hysteria2">
<v-row> <v-row v-if="direction == 'in'">
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4" v-if="data.masquerade != undefined">
<v-text-field <v-text-field
label="Masquerade" label="HTTP3 server on auth fail"
hide-details hide-details
v-model="hysteria2.masquerade"></v-text-field> v-model="data.masquerade">
</v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-switch v-model="hysteria2.ignore_client_bandwidth" color="primary" label="Ignore Client Bandwidth" hide-details></v-switch> <v-switch v-model="data.ignore_client_bandwidth" color="primary" :label="$t('types.hy.ignoreBw')" hide-details></v-switch>
</v-col> </v-col>
</v-row> </v-row>
<v-row v-if="!hysteria2.ignore_client_bandwidth"> <v-row v-else>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Uplink Limit" :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-row>
<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 hide-details
type="number" type="number"
suffix="Mbps" :suffix="$t('stats.Mbps')"
min="0"
v-model.number="up_mbps"> v-model.number="up_mbps">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Downlink Limit" :label="$t('stats.download')"
hide-details hide-details
type="number" type="number"
suffix="Mbps" :suffix="$t('stats.Mbps')"
min="0" min="0"
v-model.number="down_mbps"> v-model.number="down_mbps">
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row v-if="hysteria2.obfs"> <v-row v-if="data.obfs != undefined">
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="obfs Password" :label="$t('types.hy.obfs')"
hide-details hide-details
v-model="hysteria2.obfs.password"> v-model="data.obfs.password">
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
@@ -45,12 +59,15 @@
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start"> <v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>Options</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('types.hy.hy2Options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
<v-list-item> <v-list-item>
<v-switch v-model="optionObfs" color="primary" label="Obfs" hide-details></v-switch> <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> </v-list-item>
</v-list> </v-list>
</v-card> </v-card>
@@ -60,32 +77,33 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { Hysteria2, createInbound } from '@/types/inbounds' import Network from '@/components/Network.vue'
export default { export default {
props: ['inbound'], props: ['direction', 'data'],
data() { data() {
return { return {
menu: false, menu: false,
hysteria2: <Hysteria2> createInbound("hysteria2",{ "tag": "" }),
} }
}, },
computed: { computed: {
down_mbps: { down_mbps: {
get() { return this.hysteria2.down_mbps ? this.hysteria2.down_mbps : 0 }, get() { return this.$props.data.down_mbps?? 0 },
set(newValue:any) { this.hysteria2.down_mbps = newValue.length == 0 ? undefined : this.hysteria2.down_mbps } set(newValue:number) { this.$props.data.down_mbps = newValue>0 ? newValue : undefined }
}, },
up_mbps: { up_mbps: {
get() { return this.hysteria2.up_mbps ? this.hysteria2.up_mbps : 0 }, get() { return this.$props.data.up_mbps?? 0 },
set(newValue:any) { this.hysteria2.up_mbps = newValue.length == 0 ? undefined : this.hysteria2.up_mbps } set(newValue:number) { this.$props.data.up_mbps = newValue>0 ? newValue : undefined }
}, },
optionObfs: { optionObfs: {
get(): boolean { return this.hysteria2.obfs != undefined }, get(): boolean { return this.$props.data.obfs != undefined },
set(v:boolean) { this.$props.inbound.obfs = v ? { type: "salamander", password: ""} : 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 }
} }
}, },
mounted() { components: { Network }
this.hysteria2 = <Hysteria2> this.$props.inbound
}
} }
</script> </script>
+1 -1
View File
@@ -2,7 +2,7 @@
<v-card subtitle="Naive"> <v-card subtitle="Naive">
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<Network :inbound="inbound" /> <Network :data="inbound" />
</v-col> </v-col>
</v-row> </v-row>
</v-card> </v-card>
@@ -0,0 +1,44 @@
<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>
@@ -0,0 +1,46 @@
<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>
+14 -14
View File
@@ -5,29 +5,29 @@
<v-select <v-select
hide-details hide-details
:items="[1,2,3]" :items="[1,2,3]"
label="Version" :label="$t('version')"
v-model="version"> v-model="version">
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4" v-if="inbound.password != undefined"> <v-col cols="12" sm="6" md="4" v-if="data.password != undefined">
<v-text-field <v-text-field
label="Password" :label="$t('types.pw')"
hide-details hide-details
v-model="inbound.password"> v-model="data.password">
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Handshake Server" :label="$t('types.shdwTls.hs')"
hide-details hide-details
v-model="Inbound.handshake.server"> v-model="Inbound.handshake.server">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Server Port" :label="$t('out.port')"
type="number" type="number"
min="0" min="0"
hide-details hide-details
@@ -35,11 +35,11 @@
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<Dial :dial="Inbound.handshake" /> <Dial :dial="Inbound.handshake" :outTags="outTags" />
<v-row v-if="Inbound.handshake_for_server_name != undefined"> <v-row v-if="Inbound.handshake_for_server_name != undefined">
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Add Hanshake Server" :label="$t('types.shdwTls.addHS')"
hide-details hide-details
append-icon="mdi-plus" append-icon="mdi-plus"
@click:append="addHandshakeServer()" @click:append="addHandshakeServer()"
@@ -67,14 +67,14 @@
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Handshake Server" :label="$t('types.shdwTls.hs')"
hide-details hide-details
v-model="value.server"> v-model="value.server">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Server Port" :label="$t('out.port')"
type="number" type="number"
min="0" min="0"
hide-details hide-details
@@ -82,7 +82,7 @@
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<Dial :dial="value" /> <Dial :dial="value" :outTags="outTags" />
</v-card> </v-card>
</v-card> </v-card>
</template> </template>
@@ -92,7 +92,7 @@ import { ShadowTLS } from '@/types/inbounds'
import Dial from '../Dial.vue' import Dial from '../Dial.vue'
export default { export default {
props: ['inbound'], props: ['data', 'outTags'],
data() { data() {
return { return {
handshake_server: '' handshake_server: ''
@@ -100,7 +100,7 @@ export default {
}, },
methods: { methods: {
addHandshakeServer() { addHandshakeServer() {
this.inbound.handshake_for_server_name[this.handshake_server] = {} this.data.handshake_for_server_name[this.handshake_server] = {}
// Clear the input field after adding the server // Clear the input field after adding the server
this.handshake_server = '' this.handshake_server = ''
} }
@@ -141,7 +141,7 @@ export default {
} }
}, },
Inbound(): ShadowTLS { Inbound(): ShadowTLS {
return <ShadowTLS>this.$props.inbound; return <ShadowTLS>this.$props.data;
}, },
server_port: { server_port: {
get() { return this.Inbound.handshake.server_port ? this.Inbound.handshake.server_port : 443; }, get() { return this.Inbound.handshake.server_port ? this.Inbound.handshake.server_port : 443; },
@@ -4,16 +4,19 @@
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-select <v-select
hide-details hide-details
label="Method" :label="$t('in.ssMethod')"
:items="ssMethods" :items="ssMethods"
v-model="inbound.method"> v-model="data.method">
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field v-model="inbound.password" label="Password" hide-details></v-text-field> <v-text-field v-model="data.password" :label="$t('types.pw')" hide-details></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<Network :inbound="inbound" /> <Network :data="data" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="direction == 'out'">
<UoT :data="data" />
</v-col> </v-col>
</v-row> </v-row>
</v-card> </v-card>
@@ -21,9 +24,10 @@
<script lang="ts"> <script lang="ts">
import Network from '@/components/Network.vue' import Network from '@/components/Network.vue'
import UoT from '@/components/UoT.vue';
export default { export default {
props: ['inbound'], props: ['direction','data'],
data() { data() {
return { return {
ssMethods: [ ssMethods: [
@@ -39,6 +43,6 @@ export default {
] ]
} }
}, },
components: { Network } components: { Network, UoT }
} }
</script> </script>
@@ -0,0 +1,59 @@
<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
@@ -0,0 +1,151 @@
<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 -1
View File
@@ -2,7 +2,7 @@
<v-card subtitle="TProxy"> <v-card subtitle="TProxy">
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<Network :inbound="inbound" /> <Network :data="inbound" />
</v-col> </v-col>
</v-row> </v-row>
</v-card> </v-card>
+33
View File
@@ -0,0 +1,33 @@
<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>
@@ -0,0 +1,24 @@
<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>
+42 -19
View File
@@ -1,35 +1,59 @@
<template> <template>
<v-card subtitle="TUIC"> <v-card subtitle="TUIC">
<v-row> <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-col cols="12" sm="6" md="4">
<v-select <v-select
hide-details hide-details
label="Congestion Control" label="UDP Relay Mode"
:items="congestion_controls" :items="['native', 'quic']"
v-model="inbound.congestion_control"> clearable
@click:clear="delete data.udp_relay_mode"
v-model="data.udp_relay_mode">
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-switch color="primary" label="Zero-RTT Handshake" v-model="inbound.zero_rtt_handshake" hide-details></v-switch> <v-switch color="primary" label="UDP Over Stream" v-model="data.udp_over_stream" hide-details></v-switch>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<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 <v-text-field
label="Authentication Timeout" :label="$t('types.tuic.authTimeout')"
hide-details hide-details
type="number" type="number"
suffix="s" :suffix="$t('date.s')"
min="1" min="1"
v-model.number="auth_timeout"> v-model.number="auth_timeout">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Heartbeat" :label="$t('types.tuic.hb')"
hide-details hide-details
type="number" type="number"
suffix="s" :suffix="$t('date.s')"
min="1" min="1"
v-model.number="heartbeat"> v-model.number="heartbeat">
</v-text-field> </v-text-field>
@@ -39,9 +63,10 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { TUIC } from '@/types/inbounds' import Network from '@/components/Network.vue'
export default { export default {
props: ['inbound'], props: ['direction', 'data'],
data() { data() {
return { return {
congestion_controls: [ congestion_controls: [
@@ -50,17 +75,15 @@ export default {
} }
}, },
computed: { computed: {
Inbound(): TUIC {
return <TUIC> this.$props.inbound
},
auth_timeout: { auth_timeout: {
get() { return this.Inbound.auth_timeout ? parseInt(this.Inbound.auth_timeout.replace('s','')) : '' }, get() { return this.$props.data.auth_timeout ? parseInt(this.$props.data.auth_timeout.replace('s','')) : '' },
set(newValue:number) { this.$props.inbound.auth_timeout = newValue ? newValue + 's' : '' } set(newValue:number) { this.$props.data.auth_timeout = newValue ? newValue + 's' : '' }
}, },
heartbeat: { heartbeat: {
get() { return this.Inbound.heartbeat ? parseInt(this.Inbound.heartbeat.replace('s','')) : '' }, get() { return this.$props.data.heartbeat ? parseInt(this.$props.data.heartbeat.replace('s','')) : '' },
set(newValue:number) { this.$props.inbound.heartbeat = newValue ? newValue + 's' : '' } set(newValue:number) { this.$props.data.heartbeat = newValue ? newValue + 's' : '' }
}
} }
},
components: { Network }
} }
</script> </script>
@@ -0,0 +1,121 @@
<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>
@@ -0,0 +1,48 @@
<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>
@@ -0,0 +1,72 @@
<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>
@@ -0,0 +1,163 @@
<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')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="8">
<v-text-field v-model="data.peer_public_key" :label="$t('types.wg.pubKey')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="8" v-if="data.pre_shared_key != undefined">
<v-text-field v-model="data.pre_shared_key" :label="$t('types.wg.psk')" hide-details></v-text-field>
</v-col>
<v-col cols="12" sm="8">
<v-text-field v-model="local_ips" :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-if="data.reserved != undefined">
<v-text-field v-model="reserved" :label="'Reserved ' + $t('commaSeparated')" hide-details></v-text-field>
</v-col>
<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">
<Network :data="data" />
</v-col>
<v-col cols="12" sm="6" md="4" v-if="data.interface_name != undefined">
<v-text-field
:label="$t('types.wg.ifName')"
hide-details
v-model.number="data.interface_name">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.system_interface" color="primary" :label="$t('types.wg.sysIf')" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch v-model="data.gso" color="primary" :label="$t('types.wg.gso')" 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.wg.options') }}</v-btn>
</template>
<v-card>
<v-list>
<v-list-item>
<v-switch v-model="optionPsk" color="primary" :label="$t('types.wg.psk')" hide-details></v-switch>
</v-list-item>
<v-list-item>
<v-switch v-model="optionRsrv" color="primary" label="Reserved" 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-item>
<v-switch v-model="optionPeers" color="primary" :label="$t('types.wg.multiPeer')" 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-card-subtitle>
<Peer :data="p" />
</v-card>
</template>
</v-card>
</template>
<script lang="ts">
import Network from '@/components/Network.vue'
import Peer from '@/components/WgPeer.vue'
import { WgPeer } from '@/types/outbounds'
export default {
props: ['data'],
data() {
return {
menu: false,
}
},
methods: {
addPeer() {
this.$props.data.peers.push({server: '', port: ''})
}
},
computed: {
optionPsk: {
get(): boolean { return this.$props.data.pre_shared_key != undefined },
set(v:boolean) { this.$props.data.pre_shared_key = v ? "" : 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.interface_name != undefined },
set(v:boolean) { this.$props.data.interface_name = v ? "" : undefined }
},
optionPeers: {
get(): boolean { return this.$props.data.peers != undefined },
set(v:boolean) { this.$props.data.peers = v ? <WgPeer[]>[] : undefined }
},
local_ips: {
get() { return this.$props.data.local_address?.join(',') },
set(v:string) { this.$props.data.local_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)) : []
}
}
},
},
components: { Network, Peer }
}
</script>
+1 -2
View File
@@ -49,8 +49,7 @@ const gaugeColor = computed(() => {
background: `rgb(var(--v-theme-${gaugeColor}))` background: `rgb(var(--v-theme-${gaugeColor}))`
}"> }">
</div> </div>
<span class="gauge__cover" dir="ltr" v-html="data.text"> <div class="gauge__cover"><span dir="ltr" v-html="data.text"></span></div>
</span>
</div> </div>
</div> </div>
</template> </template>
+252
View File
@@ -0,0 +1,252 @@
<template>
<v-card subtitle="ACME" style="background-color: inherit;">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('enable')" v-model="enabled" hide-details></v-switch>
</v-col>
<v-col cols="12" md="8" v-if="enabled">
<v-text-field
:label="$t('rule.domain') + ' ' + $t('commaSeparated')"
hide-details
v-model="domains">
</v-text-field>
</v-col>
</v-row>
<template v-if="enabled">
<v-row>
<v-col cols="12" sm="6" md="4" v-if="optionDir">
<v-text-field
:label="$t('tls.acme.dataDir')"
hide-details
v-model="acme.data_directory">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionDefault">
<v-combobox
v-model="acme.default_server_name"
:items="acme.domain"
:label="$t('tls.acme.defaultDomain')"
hide-details
></v-combobox>
</v-col>
<v-col cols="12" sm="6" md="4" v-if="optionEmail">
<v-text-field
:label="$t('email')"
hide-details
v-model="acme.email">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="optionChallenge">
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.acme.httpChallenge')" v-model="acme.disable_http_challenge" hide-details></v-switch>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-switch color="primary" :label="$t('tls.acme.tlsChallenge')" v-model="acme.disable_tls_alpn_challenge" hide-details></v-switch>
</v-col>
</v-row>
<v-row v-if="optionPorts">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.acme.altHport')"
hide-details
type="number"
min=1
max="65532"
v-model.number="acme.alternative_http_port">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('tls.acme.altTport')"
hide-details
type="number"
min=1
max="65532"
v-model.number="acme.alternative_tls_port">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="optionProvider">
<v-col cols="12" sm="6" md="4">
<v-select
v-model="caProvider"
:items="providerList"
:label="$t('tls.acme.caProvider')"
hide-details
></v-select>
</v-col>
<v-col cols="12" md="8" v-if="caProvider == ''">
<v-text-field
:label="$t('tls.acme.customCa')"
hide-details
v-model="acme.provider">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="acme.external_account != undefined">
<v-col cols="12" sm="6" md="4">
<v-text-field
label="Key ID"
hide-details
v-model="acme.external_account.key_id">
</v-text-field>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
label="MAC Key"
hide-details
v-model="acme.external_account.mac_key">
</v-text-field>
</v-col>
</v-row>
<v-row v-if="acme.dns01_challenge != undefined">
<v-col cols="12" sm="6" md="4">
<v-select
:label="$t('tls.acme.dns01Provider')"
hide-details
:items="dnsProviders.map(d => d.provider)"
@update:model-value="acme.dns01_challenge = { provider: $event }"
v-model="acme.dns01_challenge.provider">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4"
v-for="item in dnsProviders.filter(d => d.provider == acme.dns01_challenge?.provider)[0]?.params"
:key="item">
<v-text-field
:label="item"
hide-details
v-model="acme.dns01_challenge[item]">
</v-text-field>
</v-col>
</v-row>
<v-card-actions>
<v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start">
<template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details 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/inTls'
export default {
props: ['tls'],
data() {
return {
menu: false,
providerList: [
{ title: "Let's Encrypt", value: "letsencrypt" },
{ title: "ZeroSSL", value: "zerossl" },
{ title: "Custom", value: "" }
],
dnsProviders: [
{ provider: "cloudflare", params: [ "api_token" ] },
{ provider: "alidns", params: [ "access_key_id","access_key_secret","region_id" ] }
]
}
},
computed: {
acme() {
return <acme>this.$props.tls.acme
},
enabled: {
get() { return this.acme != undefined },
set(v: boolean) { this.$props.tls.acme = v ? { domain: [] } : undefined }
},
domains: {
get() { return this.acme?.domain ? this.acme.domain.join(',') : "" },
set(v: string) {
if(!v.endsWith(',')) {
this.acme.domain = v.length > 0 ? v.split(',') : []
}
}
},
caProvider: {
get() { return this.acme?.provider && ['letsencrypt','zerossl'].includes(this.acme.provider) ? this.acme?.provider : '' },
set(v: string) { this.acme.provider = ['letsencrypt','zerossl'].includes(v) ? v : 'https://' }
},
optionDir: {
get(): boolean { return this.acme?.data_directory != undefined },
set(v:boolean) { this.acme.data_directory = v ? '' : undefined }
},
optionDefault: {
get(): boolean { return this.acme?.default_server_name != undefined },
set(v:boolean) { this.acme.default_server_name = v ? this.domains.length>0 ? this.domains[0] : '' : undefined }
},
optionEmail: {
get(): boolean { return this.acme?.email != undefined },
set(v:boolean) { this.acme.email = v ? '' : undefined }
},
optionChallenge: {
get(): boolean { return this.acme?.disable_http_challenge != undefined || this.acme?.disable_tls_alpn_challenge != undefined },
set(v:boolean) {
if (v) {
this.acme.disable_http_challenge = false
this.acme.disable_tls_alpn_challenge = false
} else {
delete this.acme.disable_http_challenge
delete this.acme.disable_tls_alpn_challenge
}
}
},
optionPorts: {
get(): boolean { return this.acme?.alternative_http_port != undefined || this.acme?.alternative_tls_port != undefined },
set(v:boolean) {
if (v) {
this.acme.alternative_http_port = 80
this.acme.alternative_tls_port = 443
} else {
delete this.acme.alternative_http_port
delete this.acme.alternative_tls_port
}
}
},
optionProvider: {
get(): boolean { return this.acme?.provider != undefined },
set(v:boolean) { this.acme.provider = v ? 'letsencrypt' : undefined }
},
optionExt: {
get(): boolean { return this.acme?.external_account != undefined },
set(v:boolean) { this.acme.external_account = v ? { key_id: '', mac_key: '' } : undefined }
},
optionDns01: {
get(): boolean { return this.acme?.dns01_challenge != undefined },
set(v:boolean) { this.acme.dns01_challenge = v ? { provider: 'cloudflare' } : undefined }
},
}
}
</script>
+160
View File
@@ -0,0 +1,160 @@
<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/inTls'
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.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>
@@ -1,11 +1,20 @@
<template> <template>
<v-card :subtitle="$t('in.tls')"> <v-card :subtitle="$t('objects.tls')">
<v-row v-if="tlsOptional"> <v-row>
<v-col cols="auto"> <v-col cols="12" sm="6" md="4" v-if="tlsOptional">
<v-switch color="primary" :label="$t('tls.enable')" v-model="tlsEnable" hide-details></v-switch> <v-switch color="primary" :label="$t('tls.enable')" v-model="tlsEnable" hide-details></v-switch>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4" v-if="tls.enabled">
<v-select
hide-details
:label="$t('template')"
:items="tlsItems"
@update:model-value="changeTlsItem($event)"
v-model="tlsId">
</v-select>
</v-col>
</v-row> </v-row>
<template v-if="tls.enabled"> <template v-if="tls.enabled && tlsId == 0">
<v-row> <v-row>
<v-col cols="auto"> <v-col cols="auto">
<v-btn-toggle v-model="usePath" <v-btn-toggle v-model="usePath"
@@ -77,7 +86,7 @@
<v-col cols="12" sm="6" md="4" v-if="tls.min_version"> <v-col cols="12" sm="6" md="4" v-if="tls.min_version">
<v-select <v-select
hide-details hide-details
label="Minimum Version" :label="$t('tls.minVer')"
:items="tlsVersions" :items="tlsVersions"
v-model="tls.min_version"> v-model="tls.min_version">
</v-select> </v-select>
@@ -85,7 +94,7 @@
<v-col cols="12" sm="6" md="4" v-if="tls.max_version"> <v-col cols="12" sm="6" md="4" v-if="tls.max_version">
<v-select <v-select
hide-details hide-details
label="Maximum Version" :label="$t('tls.maxVer')"
:items="tlsVersions" :items="tlsVersions"
v-model="tls.max_version"> v-model="tls.max_version">
</v-select> </v-select>
@@ -95,7 +104,7 @@
<v-col cols="12" md="8" v-if="tls.cipher_suites != undefined"> <v-col cols="12" md="8" v-if="tls.cipher_suites != undefined">
<v-select <v-select
hide-details hide-details
label="Cipher Suites" :label="$t('tls.cs')"
multiple multiple
:items="cipher_suites" :items="cipher_suites"
v-model="tls.cipher_suites"> v-model="tls.cipher_suites">
@@ -103,11 +112,11 @@
</v-col> </v-col>
</v-row> </v-row>
</template> </template>
<v-card-actions> <v-card-actions v-if="tls.enabled && tlsId == 0">
<v-spacer></v-spacer> <v-spacer></v-spacer>
<v-menu v-model="menu" :close-on-content-click="false" location="start" v-if="tls.enabled"> <v-menu v-model="menu" :close-on-content-click="false" location="start" v-if="tls.enabled">
<template v-slot:activator="{ props }"> <template v-slot:activator="{ props }">
<v-btn v-bind="props" hide-details>TLS Options</v-btn> <v-btn v-bind="props" hide-details variant="tonal">{{ $t('tls.options') }}</v-btn>
</template> </template>
<v-card> <v-card>
<v-list> <v-list>
@@ -118,13 +127,13 @@
<v-switch v-model="optionALPN" color="primary" label="ALPN" hide-details></v-switch> <v-switch v-model="optionALPN" color="primary" label="ALPN" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionMinV" color="primary" label="Min Version" hide-details></v-switch> <v-switch v-model="optionMinV" color="primary" :label="$t('tls.minVer')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionMaxV" color="primary" label="Max Version" hide-details></v-switch> <v-switch v-model="optionMaxV" color="primary" :label="$t('tls.maxVer')" hide-details></v-switch>
</v-list-item> </v-list-item>
<v-list-item> <v-list-item>
<v-switch v-model="optionCS" color="primary" label="Cipher Suites" hide-details></v-switch> <v-switch v-model="optionCS" color="primary" :label="$t('tls.cs')" hide-details></v-switch>
</v-list-item> </v-list-item>
</v-list> </v-list>
</v-card> </v-card>
@@ -134,22 +143,22 @@
</template> </template>
<script lang="ts"> <script lang="ts">
import { i18n } from '@/locales'
import { iTls, defaultInTls } from '@/types/inTls' import { iTls, defaultInTls } from '@/types/inTls'
export default { export default {
props: ['inbound'], props: ['inbound', 'tlsConfigs', 'tls_id'],
data() { data() {
return { return {
menu: false, menu: false,
usePath: 0, usePath: this.$props.inbound.tls.key == undefined ? 0 : 1,
defaults: defaultInTls, defaults: defaultInTls,
alpn: [ alpn: [
{ title: "H3", value: 'HTTP/3' }, { title: "H3", value: 'h3' },
{ title: "H2", value: 'HTTP/2' }, { title: "H2", value: 'h2' },
{ title: "Http1.1", value: 'HTTP/1.1' }, { title: "Http/1.1", value: 'http/1.1' },
], ],
tlsVersions: [ '1.0', '1.1', '1.2', '1.3' ], tlsVersions: [ '1.0', '1.1', '1.2', '1.3' ],
cipher_suites: [ cipher_suites: [
{ title: "Automatic", value: "" },
{ title: "RSA-AES128-CBC-SHA", value: "TLS_RSA_WITH_AES_128_CBC_SHA" }, { 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-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-AES128-GCM-SHA256", value: "TLS_RSA_WITH_AES_128_GCM_SHA256" },
@@ -174,9 +183,19 @@ export default {
tls(): iTls { tls(): iTls {
return <iTls> this.$props.inbound.tls return <iTls> this.$props.inbound.tls
}, },
tlsItems(): any[] {
return [ { title: i18n.global.t('none'), value: 0 }, ...this.$props.tlsConfigs?.map((t:any) => { return { title: t.name, value: t.id } } )]
},
tlsId: {
get() { return this.tls_id.value?? 0 },
set(newValue: boolean) { this.$props.tls_id.value = newValue }
},
tlsEnable: { tlsEnable: {
get() { return Object.hasOwn(this.$props.inbound.tls, 'enabled') ? this.tls.enabled : false }, get() { return this.tls.enabled?? false },
set(newValue: boolean) { this.$props.inbound.tls = newValue ? { enabled: true } : {} } set(newValue: boolean) {
this.$props.inbound.tls = newValue ? { enabled: true } : {}
this.$props.tls_id.value = 0
}
}, },
tlsOptional(): boolean { tlsOptional(): boolean {
return !['hysteria','hysteria2','tuic','naive'].includes(this.$props.inbound.type) return !['hysteria','hysteria2','tuic','naive'].includes(this.$props.inbound.type)
@@ -191,23 +210,33 @@ export default {
}, },
optionSNI: { optionSNI: {
get(): boolean { return this.tls.server_name != undefined }, get(): boolean { return this.tls.server_name != undefined },
set(v:boolean) { this.$props.inbound.tls.server_name = v ? '' : undefined } set(v:boolean) { this.tls.server_name = v ? '' : undefined }
}, },
optionALPN: { optionALPN: {
get(): boolean { return this.tls.alpn != undefined }, get(): boolean { return this.tls.alpn != undefined },
set(v:boolean) { this.$props.inbound.tls.alpn = v ? defaultInTls.alpn : undefined } set(v:boolean) { this.tls.alpn = v ? defaultInTls.alpn : undefined }
}, },
optionMinV: { optionMinV: {
get(): boolean { return this.tls.min_version != undefined }, get(): boolean { return this.tls.min_version != undefined },
set(v:boolean) { this.$props.inbound.tls.min_version = v ? defaultInTls.min_version : undefined } set(v:boolean) { this.tls.min_version = v ? defaultInTls.min_version : undefined }
}, },
optionMaxV: { optionMaxV: {
get(): boolean { return this.tls.max_version != undefined }, get(): boolean { return this.tls.max_version != undefined },
set(v:boolean) { this.$props.inbound.tls.max_version = v ? defaultInTls.max_version : undefined } set(v:boolean) { this.tls.max_version = v ? defaultInTls.max_version : undefined }
}, },
optionCS: { optionCS: {
get(): boolean { return this.tls.cipher_suites != undefined }, get(): boolean { return this.tls.cipher_suites != undefined },
set(v:boolean) { this.$props.inbound.tls.cipher_suites = v ? defaultInTls.cipher_suites : undefined } set(v:boolean) { this.tls.cipher_suites = v ? defaultInTls.cipher_suites : undefined }
}
},
methods: {
changeTlsItem(id: number){
if (id>0) {
const tlsConfig = this.$props.tlsConfigs?.findLast((t:any) => t.id == id)
if (tlsConfig) this.$props.inbound.tls = tlsConfig.server
} else {
this.$props.inbound.tls = { enabled: this.tls.enabled }
}
} }
} }
} }
+341
View File
@@ -0,0 +1,341 @@
<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/outTls'
export default {
props: ['outbound'],
data() {
return {
menu: false,
usePath: 0,
useEchPath: 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: "Chrome PSK", value: "chrome_psk" },
{ title: "Chrome PSK Shuffle", value: "chrome_psk_shuffle" },
{ title: "Chrome Padding PSK Shuffle", value: "chrome_padding_psk_shuffle" },
{ title: "Chrome Post-Quantum", value: "chrome_pq" },
{ title: "Chrome Post-Quantum PSK", value: "chrome_pq_psk" },
{ 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 } : {} }
},
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>
+13 -6
View File
@@ -15,17 +15,20 @@
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-select
label="Method" :label="$t('transport.httpMethod')"
hide-details hide-details
clearable
@click:clear="delete transport.method"
:items="methodList"
v-model="transport.method"> v-model="transport.method">
</v-text-field> </v-select>
</v-col> </v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Idle Timeout" :label="$t('transport.idleTimeout')"
hide-details hide-details
type="number" type="number"
suffix="s" suffix="s"
@@ -35,7 +38,7 @@
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Ping Timeout" :label="$t('transport.pingTimeout')"
hide-details hide-details
type="number" type="number"
suffix="s" suffix="s"
@@ -44,14 +47,17 @@
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<Headers :data="transport" />
</template> </template>
<script lang="ts"> <script lang="ts">
import { HTTP } from '../../types/transport' import { HTTP } from '../../types/transport'
import Headers from '../Headers.vue'
export default { export default {
props: ['transport'], props: ['transport'],
data() { data() {
return { return {
methodList: ['POST', 'GET', 'PUT', 'PATCH', 'DELETE']
} }
}, },
computed: { computed: {
@@ -70,6 +76,7 @@ export default {
get() { return this.Http.ping_timeout ? parseInt(this.Http.ping_timeout.replace('s','')) : '' }, get() { return this.Http.ping_timeout ? parseInt(this.Http.ping_timeout.replace('s','')) : '' },
set(newValue:number) { this.$props.transport.ping_timeout = newValue ? newValue + 's' : '' } set(newValue:number) { this.$props.transport.ping_timeout = newValue ? newValue + 's' : '' }
} }
} },
components: { Headers }
} }
</script> </script>
@@ -15,14 +15,17 @@
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<Headers :data="transport" />
</template> </template>
<script lang="ts"> <script lang="ts">
import Headers from '../Headers.vue';
export default { export default {
props: ['transport'], props: ['transport'],
data() { data() {
return { return {
} }
} },
components: { Headers }
} }
</script> </script>
@@ -7,6 +7,15 @@
v-model="transport.path"> v-model="transport.path">
</v-text-field> </v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('transport.host')"
hide-details
v-model="host">
</v-text-field>
</v-col>
</v-row>
<v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Max Early Data" label="Max Early Data"
@@ -24,10 +33,12 @@
</v-text-field> </v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<Headers :data="transport" />
</template> </template>
<script lang="ts"> <script lang="ts">
import { WebSocket } from '../../types/transport' import { WebSocket } from '../../types/transport'
import Headers from '../Headers.vue'
export default { export default {
props: ['transport'], props: ['transport'],
data() { data() {
@@ -42,10 +53,17 @@ export default {
get() { return this.WS.max_early_data ? this.WS.max_early_data : '' }, get() { return this.WS.max_early_data ? this.WS.max_early_data : '' },
set(newValue:number) { this.$props.transport.max_early_data = newValue != 0 ? newValue : undefined } set(newValue:number) { this.$props.transport.max_early_data = newValue != 0 ? newValue : undefined }
}, },
host: {
get() { return this.WS.headers?.Host ? this.WS.headers.Host : '' },
set(newValue:string) {
this.$props.transport.headers = newValue != "" ? { Host: newValue } : undefined
}
},
}, },
mounted() { mounted() {
this.WS.early_data_header_name = 'Sec-WebSocket-Protocol' this.WS.early_data_header_name ??= 'Sec-WebSocket-Protocol'
this.WS.path = '/' this.WS.path ??= '/'
} },
components: { Headers }
} }
</script> </script>
+4 -4
View File
@@ -2,7 +2,7 @@
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Service Name" :label="$t('transport.grpcServiceName')"
hide-details hide-details
v-model="transport.service_name"> v-model="transport.service_name">
</v-text-field> </v-text-field>
@@ -11,7 +11,7 @@
<v-switch <v-switch
color="primary" color="primary"
v-model="transport.permit_without_stream" v-model="transport.permit_without_stream"
label="Permit Without Stream" :label="$t('transport.grpcPws')"
hide-details> hide-details>
</v-switch> </v-switch>
</v-col> </v-col>
@@ -19,7 +19,7 @@
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Idle Timeout" :label="$t('transport.idleTimeout')"
hide-details hide-details
type="number" type="number"
suffix="s" suffix="s"
@@ -29,7 +29,7 @@
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field <v-text-field
label="Ping Timeout" :label="$t('transport.pingTimeout')"
hide-details hide-details
type="number" type="number"
suffix="s" suffix="s"
+4 -3
View File
@@ -1,6 +1,7 @@
<template> <template>
<v-app-bar :elevation="5"> <v-app-bar :elevation="5">
<v-icon v-if="isMobile" icon="mdi-menu" @click="$emit('toggleDrawer')" /> <v-icon v-if="isMobile" icon="mdi-menu" @click="$emit('toggleDrawer')" />
<span v-else style="width: 24px"></span>
<v-app-bar-title :text="$t(<string>$router.currentRoute.value.name)" class="align-center text-center " /> <v-app-bar-title :text="$t(<string>$router.currentRoute.value.name)" class="align-center text-center " />
<v-btn prepend-icon="mdi-content-save" v-if="stateChange" :text="$t('actions.save')" @click="saveChanges"></v-btn> <v-btn prepend-icon="mdi-content-save" v-if="stateChange" :text="$t('actions.save')" @click="saveChanges"></v-btn>
<v-icon icon="mdi-theme-light-dark" @click="toggleTheme()" style="margin: 0 10px;"></v-icon> <v-icon icon="mdi-theme-light-dark" @click="toggleTheme()" style="margin: 0 10px;"></v-icon>
@@ -8,7 +9,7 @@
</template> </template>
<script lang="ts" setup> <script lang="ts" setup>
import { computed, onMounted, ref,watch } from "vue" import { computed, ref } from "vue"
import { useTheme } from "vuetify" import { useTheme } from "vuetify"
import { FindDiff } from "@/plugins/utils" import { FindDiff } from "@/plugins/utils"
import Data from "@/store/modules/data" import Data from "@/store/modules/data"
@@ -31,11 +32,11 @@ const saveChanges = () => {
} }
const oldData = computed((): any => { const oldData = computed((): any => {
return {config: store.oldData.config, clients: store.oldData.clients} return {config: store.oldData.config, clients: store.oldData.clients, tls: store.oldData.tlsConfigs, inData: store.oldData.inData}
}) })
const newData = computed((): any => { const newData = computed((): any => {
return {config: store.config, clients: store.clients} return {config: store.config, clients: store.clients, tls: store.tlsConfigs, inData: store.inData}
}) })
const stateChange = computed((): any => { const stateChange = computed((): any => {
+8
View File
@@ -25,3 +25,11 @@ const isMobile = computed( ():boolean =>{
return smAndDown.value return smAndDown.value
}) })
</script> </script>
<style>
.v-card-subtitle {
text-align: center;
border-bottom: 1px solid gray;
min-height: 20px;
}
</style>
+6 -7
View File
@@ -32,7 +32,7 @@
</v-list-item> </v-list-item>
</v-list> </v-list>
<template v-slot:append> <template v-slot:append>
<v-list-item prepend-icon="mdi-logout" :title="$t('menu.logout')" @click="logout"></v-list-item> <v-list-item prepend-icon="mdi-logout" :title="$t('menu.logout')" @click="Logout"></v-list-item>
</template> </template>
</v-navigation-drawer> </v-navigation-drawer>
</template> </template>
@@ -40,7 +40,7 @@
<script lang="ts" setup> <script lang="ts" setup>
import { computed } from 'vue' import { computed } from 'vue'
import router from '@/router' import router from '@/router'
import HttpUtil from '@/plugins/httputil' import { logout } from '@/plugins/httputil'
const props = defineProps(['isMobile','displayDrawer']) const props = defineProps(['isMobile','displayDrawer'])
@@ -54,14 +54,13 @@ const menu = [
{ title: 'pages.clients', icon: 'mdi-account-multiple', path: '/clients' }, { title: 'pages.clients', icon: 'mdi-account-multiple', path: '/clients' },
{ title: 'pages.outbounds', icon: 'mdi-cloud-upload', path: '/outbounds' }, { title: 'pages.outbounds', icon: 'mdi-cloud-upload', path: '/outbounds' },
{ title: 'pages.rules', icon: 'mdi-routes', path: '/rules' }, { title: 'pages.rules', icon: 'mdi-routes', path: '/rules' },
{ title: 'pages.tls', icon: 'mdi-certificate', path: '/tls' },
{ title: 'pages.basics', icon: 'mdi-application-cog', path: '/basics' }, { title: 'pages.basics', icon: 'mdi-application-cog', path: '/basics' },
{ title: 'pages.admins', icon: 'mdi-account-tie', path: '/admins' },
{ title: 'pages.settings', icon: 'mdi-cog', path: '/settings' }, { title: 'pages.settings', icon: 'mdi-cog', path: '/settings' },
] ]
const logout = async () => { const Logout = async () => {
const response = await HttpUtil.get('/api/logout') logout()
if(response.success){
router.push('/login')
}
} }
</script> </script>
+97
View File
@@ -0,0 +1,97 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="400">
<v-card class="rounded-lg">
<v-card-title>
{{ $t('admin.changeCred') + " " + user.username }}
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row>
<v-col>
<v-text-field v-model="newData.oldPass" :label="$t('admin.oldPass')" :rules="passwordRules" type="password" required></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field v-model="newData.newUsername" :label="$t('admin.newUname')" :rules="usernameRules" required></v-text-field>
</v-col>
</v-row>
<v-row>
<v-col>
<v-text-field v-model="newData.newPass" :label="$t('admin.newPass')" :rules="passwordRules" type="password" required></v-text-field>
</v-col>
</v-row>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
variant="outlined"
@click="closeModal"
>
{{ $t('actions.close') }}
</v-btn>
<v-btn
color="blue-darken-1"
variant="tonal"
@click="saveChanges"
>
{{ $t('actions.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import { i18n } from '@/locales'
export default {
props: ['visible', 'user'],
data() {
return {
newData: {
id: 0,
oldPass: "",
newUsername: "",
newPass: ""
},
usernameRules: [
(value: string) => {
if (value?.length > 0) return true
return i18n.global.t('login.unRules')
},
],
passwordRules: [
(value: string) => {
if (value?.length > 0) return true
return i18n.global.t('login.pwRules')
},
]
}
},
methods: {
resetData() {
this.newData.id = this.$props.user.id
this.newData.oldPass = ""
this.newData.newUsername = ""
this.newData.newPass = ""
},
closeModal() {
this.resetData() // reset
this.$emit('close')
},
saveChanges() {
if (this.newData.oldPass == '' || this.newData.newUsername == '' || this.newData.newPass == '') return
this.$emit('save', this.newData)
},
},
watch: {
visible(newValue) {
if (newValue) {
this.resetData()
}
},
},
}
</script>
+145
View File
@@ -0,0 +1,145 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="90%" max-width="800" :loading="loading">
<v-card class="rounded-lg">
<v-card-title>
<v-row>
<v-col>{{ $t('admin.changes') }}</v-col>
<v-spacer></v-spacer>
<v-col cols="auto"><v-icon icon="mdi-close-box" @click="$emit('close')" /></v-col>
</v-row>
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row>
<v-col cols="12" sm="4" md="3">
<v-select
hide-details
:label="$t('admin.actor')"
:items="['', 'DepleteJob', ...admins]"
v-model="user"
@update:model-value="loadData">
</v-select>
</v-col>
<v-col cols="12" sm="4" md="3">
<v-select
hide-details
:label="$t('admin.key')"
:items="['', 'inbounds', 'outbounds', 'clients', 'route', 'tls', 'experimental']"
v-model="key"
@update:model-value="loadData">
</v-select>
</v-col>
<v-col cols="6" sm="4" md="3">
<v-select
hide-details
:label="$t('count')"
:items="[10,20,30,50,100]"
v-model.number="chngCount"
@update:model-value="loadData">
</v-select>
</v-col>
<v-col cols="auto" align="center" justify="center">
<v-btn
icon="mdi-refresh"
variant="tonal"
:loading="loading"
@click="loadData">
<v-icon />
</v-btn>
</v-col>
</v-row>
<v-data-table
:headers="changesHeaders"
:items="changes"
item-value="id"
density="compact"
show-expand
items-per-page="10"
>
<template v-slot:item.dateTime="{ value }">
<v-chip variant="text" dir="ltr" density="compact">
{{ dateFormatted(value) }}
</v-chip>
</template>
<template v-slot:item.action="{ value }">
<v-chip density="compact">
{{ $t('actions.' + value) }}
</v-chip>
</template>
<template v-slot:expanded-row="{ columns, item }">
<tr>
<td :colspan="columns.length">
<v-card dir="ltr" v-if="item.index>0">Index: {{ item.index }}</v-card>
<v-card style="background-color: background" dir="ltr"><pre>{{ item.obj }}</pre></v-card>
</td>
</tr>
</template>
</v-data-table>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import { i18n } from '@/locales'
import HttpUtils from '@/plugins/httputil'
export default {
props: ['admins', 'actor', 'visible'],
data() {
return {
loading: false,
changes: <any[]>[],
user: '',
key: '',
chngCount: 10,
expanded: [],
changesHeaders: [
{ title: 'ID', key: 'id' },
{ title: i18n.global.t('admin.date') + '-' + i18n.global.t('admin.time'), key: 'dateTime' },
{ title: i18n.global.t('admin.actor'), key: 'Actor' },
{ title: i18n.global.t('admin.key'), key: 'key' },
{ title: i18n.global.t('admin.action'), key: 'action' },
],
}
},
methods: {
async loadData() {
this.loading = true
const data = await HttpUtils.get('api/changes',{ a: this.user, k: this.key, c: this.chngCount })
if (data.success) {
this.changes = data.obj?? []
this.loading = false
}
},
dateFormatted(dt: number): string {
const date = new Date(dt*1000)
return date.toLocaleString(this.locale)
},
},
computed: {
locale() {
const l = i18n.global.locale.value
switch (l) {
case "zhHans":
return "zh-cn"
case "zhHant":
return "zh-tw"
default:
return l
}
},
},
watch: {
visible(newValue) {
this.changes = []
this.user = this.$props.actor
this.key = ''
this.chngCount = 10
if (newValue) {
this.loadData()
}
},
},
}
</script>
+53 -13
View File
@@ -5,7 +5,7 @@
{{ $t('actions.' + title) + " " + $t('objects.client') }} {{ $t('actions.' + title) + " " + $t('objects.client') }}
</v-card-title> </v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-text style="padding: 0 16px;"> <v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-container style="padding: 0;"> <v-container style="padding: 0;">
<v-tabs <v-tabs
v-model="tab" v-model="tab"
@@ -26,6 +26,9 @@
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field v-model="client.name" :label="$t('client.name')" hide-details></v-text-field> <v-text-field v-model="client.name" :label="$t('client.name')" hide-details></v-text-field>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="client.desc" :label="$t('client.desc')" hide-details></v-text-field>
</v-col>
</v-row> </v-row>
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
@@ -35,6 +38,33 @@
<DatePick :expiry="expDate" @submit="setDate" /> <DatePick :expiry="expDate" @submit="setDate" />
</v-col> </v-col>
</v-row> </v-row>
<v-row v-if="index != -1">
<v-col cols="12" sm="6" md="4" class="d-flex flex-column">
<div class="d-flex justify-space-between align-center">
<div>
{{ $t('stats.usage') }}: {{ total }}<sup dir="ltr" v-if="percent>0">({{ percent }}%)</sup>
</div>
<v-btn density="compact" variant="text" icon="mdi-restore" @click="client.up=0;client.down=0">
<v-tooltip activator="parent" location="top">
{{ $t('reset') }}
</v-tooltip>
<v-icon />
</v-btn>
</div>
<v-progress-linear
v-model="percent"
:color="percentColor"
v-if="client.volume>0"
bottom
>
</v-progress-linear>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-icon icon="mdi-upload" color="orange" /><span class="text-orange">{{ up }}</span>
/
<v-icon icon="mdi-download" color="success" /><span class="text-success">{{ down }}</span>
</v-col>
</v-row>
<v-row> <v-row>
<v-col> <v-col>
<v-combobox <v-combobox
@@ -139,6 +169,7 @@
<v-btn <v-btn
color="blue-darken-1" color="blue-darken-1"
variant="tonal" variant="tonal"
:loading="loading"
@click="saveChanges" @click="saveChanges"
> >
{{ $t('actions.save') }} {{ $t('actions.save') }}
@@ -152,6 +183,7 @@
import { Link } from '@/plugins/link' import { Link } from '@/plugins/link'
import { createClient, randomConfigs, updateConfigs } from '@/types/clients' import { createClient, randomConfigs, updateConfigs } from '@/types/clients'
import DatePick from '@/components/DateTime.vue' import DatePick from '@/components/DateTime.vue'
import { HumanReadable } from '@/plugins/utils'
export default { export default {
props: ['visible', 'data', 'index', 'inboundTags', 'stats'], props: ['visible', 'data', 'index', 'inboundTags', 'stats'],
@@ -160,6 +192,7 @@ export default {
return { return {
client: createClient(), client: createClient(),
title: "add", title: "add",
loading: false,
clientStats: false, clientStats: false,
tab: "t1", tab: "t1",
clientConfig: <any>[], clientConfig: <any>[],
@@ -174,7 +207,7 @@ export default {
const newData = JSON.parse(this.$props.data) const newData = JSON.parse(this.$props.data)
this.client = createClient(newData) this.client = createClient(newData)
this.title = "edit" this.title = "edit"
this.clientConfig = JSON.parse(this.client.config) this.clientConfig = this.client.config
} }
else { else {
this.client = createClient() this.client = createClient()
@@ -182,10 +215,9 @@ export default {
this.clientConfig = randomConfigs('client') this.clientConfig = randomConfigs('client')
} }
this.clientStats = this.$props.stats this.clientStats = this.$props.stats
const allLinks = <Link[]>JSON.parse(this.client.links) this.links = this.client.links.filter(l => l.type == 'local')
this.links = allLinks.filter(l => l.type == 'local') this.extLinks = this.client.links.filter(l => l.type == 'external')
this.extLinks = allLinks.filter(l => l.type == 'external') this.subLinks = this.client.links.filter(l => l.type == 'sub')
this.subLinks = allLinks.filter(l => l.type == 'sub')
this.tab = "t1" this.tab = "t1"
}, },
closeModal() { closeModal() {
@@ -193,12 +225,14 @@ export default {
this.$emit('close') this.$emit('close')
}, },
saveChanges() { saveChanges() {
this.client.config = updateConfigs(JSON.stringify(this.clientConfig), this.client.name) this.loading = true
this.client.links = JSON.stringify([ this.client.config = updateConfigs(this.clientConfig, this.client.name)
this.client.links = [
...this.links, ...this.links,
...this.extLinks.filter(l => l.uri != ''), ...this.extLinks.filter(l => l.uri != ''),
...this.subLinks.filter(l => l.uri != '')]) ...this.subLinks.filter(l => l.uri != '')]
this.$emit('save', this.client, this.clientStats) this.$emit('save', this.client, this.clientStats)
this.loading = false
}, },
setDate(newDate:number){ setDate(newDate:number){
this.client.expiry = newDate this.client.expiry = newDate
@@ -206,8 +240,8 @@ export default {
}, },
computed: { computed: {
clientInbounds: { clientInbounds: {
get() { return this.client.inbounds == "" ? [] : this.client.inbounds.split(',') }, get() { return this.client.inbounds.length>0 ? this.client.inbounds.filter(i => this.inboundTags.includes(i)) : [] },
set(newValue:string[]) { this.client.inbounds = newValue.length == 0 ? "" : newValue.join(',') } set(newValue:string[]) { this.client.inbounds = newValue.length == 0 ? [] : newValue }
}, },
expDate: { expDate: {
get() { return this.client.expiry}, get() { return this.client.expiry},
@@ -216,10 +250,16 @@ export default {
Volume: { Volume: {
get() { return this.client.volume == 0 ? 0 : (this.client.volume / (1024 ** 3)) }, get() { return this.client.volume == 0 ? 0 : (this.client.volume / (1024 ** 3)) },
set(v:number) { this.client.volume = v > 0 ? v*(1024 ** 3) : 0 } set(v:number) { this.client.volume = v > 0 ? v*(1024 ** 3) : 0 }
} },
up() :string { return HumanReadable.sizeFormat(this.client.up) },
down() :string { return HumanReadable.sizeFormat(this.client.down) },
total() :string { return HumanReadable.sizeFormat(this.client.down + this.client.up) },
percent() :number { return this.client.volume>0 ? Math.round((this.client.up + this.client.down) *100 / this.client.volume) : 0 },
percentColor() :string { return (this.client.up+this.client.down) >= this.client.volume ? 'error' : this.percent>90 ? 'warning' : 'success' },
}, },
watch: { watch: {
visible(newValue) { if (newValue) { visible(newValue) {
if (newValue) {
this.updateData() this.updateData()
} }
}, },
+104 -31
View File
@@ -5,12 +5,12 @@
{{ $t('actions.' + title) + " " + $t('objects.inbound') }} {{ $t('actions.' + title) + " " + $t('objects.inbound') }}
</v-card-title> </v-card-title>
<v-divider></v-divider> <v-divider></v-divider>
<v-card-text> <v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-container style="padding: 0;">
<v-row> <v-row>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-select <v-select
hide-details hide-details
width="100"
:label="$t('type')" :label="$t('type')"
:items="Object.keys(inTypes).map((key,index) => ({title: key, value: Object.values(inTypes)[index]}))" :items="Object.keys(inTypes).map((key,index) => ({title: key, value: Object.values(inTypes)[index]}))"
v-model="inbound.type" v-model="inbound.type"
@@ -18,23 +18,51 @@
</v-select> </v-select>
</v-col> </v-col>
<v-col cols="12" sm="6" md="4"> <v-col cols="12" sm="6" md="4">
<v-text-field v-model="inbound.tag" :label="$t('in.tag')" hide-details></v-text-field> <v-text-field v-model="inbound.tag" :label="$t('objects.tag')" hide-details></v-text-field>
</v-col> </v-col>
</v-row> </v-row>
<Listen :inbound="inbound" /> <v-tabs
<Direct v-if="inbound.type == inTypes.Direct" :inbound="inbound" /> v-if="HasInData.includes(inbound.type)"
<Shadowsocks v-if="inbound.type == inTypes.Shadowsocks" :inbound="inbound" /> v-model="side"
<Hysteria v-if="inbound.type == inTypes.Hysteria" :inbound="inbound" /> density="compact"
<Hysteria2 v-if="inbound.type == inTypes.Hysteria2" :inbound="inbound" /> fixed-tabs
align-tabs="center"
>
<v-tab value="s">{{ $t('in.sSide') }}</v-tab>
<v-tab value="c">{{ $t('in.cSide') }}</v-tab>
</v-tabs>
<v-window v-model="side" style="margin-top: 10px;">
<v-window-item value="s">
<Listen :inbound="inbound" :inTags="inTags" />
<Direct v-if="inbound.type == inTypes.Direct" direction="in" :data="inbound" />
<Shadowsocks v-if="inbound.type == inTypes.Shadowsocks" direction="in" :data="inbound" />
<Hysteria v-if="inbound.type == inTypes.Hysteria" direction="in" :data="inbound" />
<Hysteria2 v-if="inbound.type == inTypes.Hysteria2" direction="in" :data="inbound" />
<Naive v-if="inbound.type == inTypes.Naive" :inbound="inbound" /> <Naive v-if="inbound.type == inTypes.Naive" :inbound="inbound" />
<ShadowTls v-if="inbound.type == inTypes.ShadowTLS" :inbound="inbound" /> <ShadowTls v-if="inbound.type == inTypes.ShadowTLS" direction="in" :data="inbound" :outTags="outTags" />
<Tuic v-if="inbound.type == inTypes.TUIC" :inbound="inbound" /> <Tuic v-if="inbound.type == inTypes.TUIC" direction="in" :data="inbound" />
<TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" /> <TProxy v-if="inbound.type == inTypes.TProxy" :inbound="inbound" />
<Transport v-if="Object.hasOwn(inbound,'transport')" :inbound="inbound" /> <Transport v-if="Object.hasOwn(inbound,'transport')" :data="inbound" />
<Users v-if="HasOptionalUser.includes(inbound.type)" :inbound="inbound" :id="id" /> <Users v-if="HasOptionalUser.includes(inbound.type)" :inbound="inbound" />
<InTls v-if="Object.hasOwn(inbound,'tls')" :inbound="inbound" /> <InTls v-if="Object.hasOwn(inbound,'tls')" :inbound="inbound" :tlsConfigs="tlsConfigs" :tls_id="tls_id" />
<InMulitiplex v-if="Object.hasOwn(inbound,'multiplex')" :inbound="inbound" /> <Multiplex v-if="Object.hasOwn(inbound,'multiplex')" direction="in" :data="inbound" />
<v-switch v-model="inboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch> <v-switch v-model="inboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-window-item>
<v-window-item value="c">
<OutJsonVue :inData="inData" :type="inbound.type" />
<v-card>
<v-card-subtitle>{{ $t('in.multiDomain') }}
<v-icon @click="add_addr" icon="mdi-plus"></v-icon>
</v-card-subtitle>
<template v-for="addr,index in inData.addrs">
{{ $t('in.addr') }} #{{ (index+1) }} <v-icon icon="mdi-delete" @click="inData.addrs.splice(index,1)" />
<v-divider></v-divider>
<AddrVue :addr="addr" :hasTls="Object.hasOwn(inbound,'tls')" />
</template>
</v-card>
</v-window-item>
</v-window>
</v-container>
</v-card-text> </v-card-text>
<v-card-actions> <v-card-actions>
<v-spacer></v-spacer> <v-spacer></v-spacer>
@@ -48,6 +76,7 @@
<v-btn <v-btn
color="blue-darken-1" color="blue-darken-1"
variant="text" variant="text"
:loading="loading"
@click="saveChanges" @click="saveChanges"
> >
{{ $t('actions.save') }} {{ $t('actions.save') }}
@@ -57,9 +86,11 @@
</v-dialog> </v-dialog>
</template> </template>
<script lang="ts"> <script lang="ts">
import { InTypes, createInbound } from '@/types/inbounds' import { InTypes, createInbound } from '@/types/inbounds'
import { Addr, InData } from '@/plugins/inData'
import RandomUtil from '@/plugins/randomUtil'
import Listen from '@/components/Listen.vue' import Listen from '@/components/Listen.vue'
import Direct from '@/components/protocols/Direct.vue' import Direct from '@/components/protocols/Direct.vue'
import Users from '@/components/Users.vue' import Users from '@/components/Users.vue'
@@ -69,47 +100,92 @@ import Hysteria2 from '@/components/protocols/Hysteria2.vue'
import Naive from '@/components/protocols/Naive.vue' import Naive from '@/components/protocols/Naive.vue'
import ShadowTls from '@/components/protocols/ShadowTls.vue' import ShadowTls from '@/components/protocols/ShadowTls.vue'
import Tuic from '@/components/protocols/Tuic.vue' import Tuic from '@/components/protocols/Tuic.vue'
import InTls from '@/components/InTLS.vue' import InTls from '@/components/tls/InTLS.vue'
import TProxy from '@/components/protocols/TProxy.vue' import TProxy from '@/components/protocols/TProxy.vue'
import RandomUtil from '@/plugins/randomUtil' import Multiplex from '@/components/Multiplex.vue'
import InMulitiplex from '@/components/InMulitiplex.vue'
import Transport from '@/components/Transport.vue' import Transport from '@/components/Transport.vue'
import AddrVue from '@/components/Addr.vue'
import OutJsonVue from '@/components/OutJson.vue'
export default { export default {
props: ['visible', 'data', 'id', 'stats'], props: ['visible', 'data', 'cData', 'index', 'stats', 'inTags', 'outTags', 'tlsConfigs'],
emits: ['close', 'save'], emits: ['close', 'save'],
data() { data() {
return { return {
inbound: createInbound("direct",{ "tag": "" }), inbound: createInbound("direct",{ "tag": "" }),
inData: <InData>{},
title: "add", title: "add",
loading: false,
side: "s",
inTypes: InTypes, inTypes: InTypes,
inboundStats: false, inboundStats: false,
tls_id: { value: 0 },
HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks], HasOptionalUser: [InTypes.Mixed,InTypes.SOCKS,InTypes.HTTP,InTypes.Shadowsocks],
HasInData: [
InTypes.SOCKS,
InTypes.HTTP,
InTypes.Shadowsocks,
InTypes.VMess,
InTypes.ShadowTLS,
InTypes.Trojan,
InTypes.Hysteria,
InTypes.VLESS,
InTypes.TUIC,
InTypes.Hysteria2,
InTypes.Naive,
]
} }
}, },
methods: { methods: {
updateData() { updateData() {
if (this.$props.id != -1) { if (this.$props.index != -1) {
const newData = JSON.parse(this.$props.data) const newData = JSON.parse(this.$props.data)
this.inbound = createInbound(newData.type, newData) this.inbound = createInbound(newData.type, newData)
this.tls_id.value = this.$props.tlsConfigs?.findLast((t:any) => t.inbounds?.includes(this.inbound.tag))?.id?? 0
if (this.HasInData.includes(this.inbound.type)){
this.inData = this.$props.cData?.length> 0 ? <InData>JSON.parse(this.$props.cData) : <InData>{id: 0, tag: this.inbound.tag, addrs: [], outJson: {}}
} else {
this.inData = <InData>{id: -1}
}
this.title = "edit" this.title = "edit"
} }
else { else {
const port = RandomUtil.randomIntRange(10000, 60000) const port = RandomUtil.randomIntRange(10000, 60000)
this.inbound = createInbound("mixed",{ tag: "in-"+port ,listen: "::", listen_port: port }) this.inbound = createInbound("direct",{ tag: "direct-"+port ,listen: "::", listen_port: port })
this.tls_id.value = 0
if (this.HasInData.includes(this.inbound.type)){
this.inData = <InData>{id: 0, tag: this.inbound.tag, addrs: [], outJson: {}}
} else {
this.inData = <InData>{id: -1}
}
this.title = "add" this.title = "add"
} }
this.inboundStats = this.$props.stats this.inboundStats = this.$props.stats
this.side = "s"
}, },
changeType() { changeType() {
const prevConfig = { tag: this.inbound.tag ,listen: this.inbound.listen, listen_port: this.inbound.listen_port } // Tag change only in add outbound
const tag = this.$props.index != -1 ? this.inbound.tag : this.inbound.type + "-" + this.inbound.listen_port
// Use previous data
const prevConfig = { tag: tag ,listen: this.inbound.listen, listen_port: this.inbound.listen_port }
this.inbound = createInbound(this.inbound.type, prevConfig) this.inbound = createInbound(this.inbound.type, prevConfig)
if (this.HasInData.includes(this.inbound.type)){
this.inData = <InData>{id: 0, tag: this.inbound.tag, addrs: [], outJson: {}}
} else {
this.inData = <InData>{id: -1}
}
this.side = "s"
},
add_addr() {
this.inData.addrs.push(<Addr>{ server: location.hostname, server_port: this.inbound.listen_port })
}, },
closeModal() { closeModal() {
this.updateData() // reset this.updateData() // reset
this.$emit('close') this.$emit('close')
}, },
saveChanges() { saveChanges() {
this.$emit('save', this.inbound, this.inboundStats) this.loading = true
this.$emit('save', this.inbound, this.inboundStats, this.tls_id.value, this.inData)
this.loading = false
}, },
}, },
watch: { watch: {
@@ -119,13 +195,10 @@ export default {
} }
}, },
}, },
components: { Listen, InTls, Hysteria2, Naive, Direct, Shadowsocks, Users, Hysteria, ShadowTls, TProxy, InMulitiplex, Tuic, Transport } components: {
Listen, InTls, Hysteria2, Naive, Direct, Shadowsocks,
Users, Hysteria, ShadowTls, TProxy, Multiplex, Tuic, Transport,
AddrVue, OutJsonVue
}
} }
</script> </script>
<style>
.v-card-subtitle {
text-align: center;
border-bottom: 1px solid gray;
}
</style>
+90
View File
@@ -0,0 +1,90 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="90%" max-width="1200" :loading="loading">
<v-card class="rounded-lg">
<v-card-title>
<v-row>
<v-col>{{ $t('basic.log.title') + " - " + (logType == 's-ui'? "S-UI" : "Sing-Box") }}</v-col>
<v-spacer></v-spacer>
<v-col cols="auto">
<v-icon icon="mdi-close" @click="$emit('close')" />
</v-col>
</v-row>
</v-card-title>
<v-divider></v-divider>
<v-card-text>
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('basic.log.level')"
:items="logLevels"
v-model="logLevel"
@update:model-value="loadData">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('count')"
:items="[10,20,30,50,100]"
v-model.number="logCount"
@update:model-value="loadData">
</v-select>
</v-col>
<v-col cols="auto" align="center" justify="center">
<v-btn
icon="mdi-refresh"
variant="tonal"
:loading="loading"
@click="loadData">
<v-icon />
</v-btn>
</v-col>
</v-row>
<v-card style="background-color: background" dir="ltr" v-html="lines.join('<br />')"></v-card>
</v-card-text>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import HttpUtils from '@/plugins/httputil';
export default {
props: ['logType', 'visible'],
data() {
return {
loading: false,
lines: [],
logLevel: 'info',
logLevels: [
{ title: 'DEBUG', value: 'debug' },
{ title: 'INFO', value: 'info' },
{ title: 'WARNING', value: 'warning' },
{ title: 'ERROR', value: 'err' },
],
logCount: 10,
}
},
methods: {
async loadData() {
this.loading = true
const data = await HttpUtils.get('api/logs',{ s: this.$props.logType, c: this.logCount, l: this.logLevel })
if (data.success) {
this.lines = data.obj?? []
this.loading = false
}
}
},
watch: {
visible(newValue) {
this.lines = []
this.logLevel = 'info'
this.logCount = 10
if (newValue) {
this.loadData()
}
},
},
}
</script>
+204
View File
@@ -0,0 +1,204 @@
<template>
<v-dialog transition="dialog-bottom-transition" width="800">
<v-card class="rounded-lg">
<v-card-title>
{{ $t('actions.' + title) + " " + $t('objects.outbound') }}
</v-card-title>
<v-divider></v-divider>
<v-card-text style="padding: 0 16px; overflow-y: scroll;">
<v-container style="padding: 0;">
<v-tabs
v-model="tab"
align-tabs="center"
>
<v-tab value="t1">{{ $t('client.basics') }}</v-tab>
<v-tab value="t2">{{ $t('client.external') }}</v-tab>
</v-tabs>
<v-window v-model="tab">
<v-window-item value="t1">
<v-row>
<v-col cols="12" sm="6" md="4">
<v-select
hide-details
:label="$t('type')"
:items="Object.keys(outTypes).map((key,index) => ({title: key, value: Object.values(outTypes)[index]}))"
v-model="outbound.type"
@update:modelValue="changeType">
</v-select>
</v-col>
<v-col cols="12" sm="6" md="4">
<v-text-field v-model="outbound.tag" :label="$t('objects.tag')" hide-details></v-text-field>
</v-col>
</v-row>
<v-row v-if="!NoServer.includes(outbound.type)">
<v-col cols="12" sm="6" md="4">
<v-text-field
:label="$t('out.addr')"
hide-details
v-model="outbound.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="outbound.server_port">
</v-text-field>
</v-col>
</v-row>
<Direct v-if="outbound.type == outTypes.Direct" direction="out" :data="outbound" />
<Socks v-if="outbound.type == outTypes.SOCKS" :data="outbound" />
<Http v-if="outbound.type == outTypes.HTTP" :data="outbound" />
<Shadowsocks v-if="outbound.type == outTypes.Shadowsocks" direction="out" :data="outbound" />
<Vmess v-if="outbound.type == outTypes.VMess" :data="outbound" />
<Trojan v-if="outbound.type == outTypes.Trojan" :data="outbound" />
<Wireguard v-if="outbound.type == outTypes.Wireguard" :data="outbound" />
<Hysteria v-if="outbound.type == outTypes.Hysteria" direction="out" :data="outbound" />
<ShadowTls v-if="outbound.type == outTypes.ShadowTLS" :data="outbound" />
<Vless v-if="outbound.type == outTypes.VLESS" :data="outbound" />
<Tuic v-if="outbound.type == outTypes.TUIC" direction="out" :data="outbound" />
<Hysteria2 v-if="outbound.type == outTypes.Hysteria2" direction="out" :data="outbound" />
<Tor v-if="outbound.type == outTypes.Tor" :data="outbound" />
<Ssh v-if="outbound.type == outTypes.SSH" :data="outbound" />
<Selector v-if="outbound.type == outTypes.Selector" :data="outbound" :tags="tags" />
<UrlTest v-if="outbound.type == outTypes.URLTest" :data="outbound" :tags="tags" />
<Transport v-if="Object.hasOwn(outbound,'transport')" :data="outbound" />
<OutTLS v-if="Object.hasOwn(outbound,'tls')" :outbound="outbound" />
<Multiplex v-if="Object.hasOwn(outbound,'multiplex')" direction="out" :data="outbound" />
<Dial v-if="!NoDial.includes(outbound.type)" :dial="outbound" :outTags="tags" />
<v-switch v-model="outboundStats" color="primary" :label="$t('stats.enable')" hide-details></v-switch>
</v-window-item>
<v-window-item value="t2">
<v-row>
<v-col cols="12">
<v-text-field v-model="link" :label="$t('client.external')" hide-details />
</v-col>
<v-col cols="12" align="center">
<v-btn hide-details variant="tonal" :loading="loading" @click="linkConvert">{{ $t('submit') }}</v-btn>
</v-col>
</v-row>
</v-window-item>
</v-window>
</v-container>
</v-card-text>
<v-card-actions>
<v-spacer></v-spacer>
<v-btn
color="blue-darken-1"
variant="text"
@click="closeModal"
>
{{ $t('actions.close') }}
</v-btn>
<v-btn
color="blue-darken-1"
variant="text"
:loading="loading"
@click="saveChanges"
>
{{ $t('actions.save') }}
</v-btn>
</v-card-actions>
</v-card>
</v-dialog>
</template>
<script lang="ts">
import { OutTypes, createOutbound } from '@/types/outbounds'
import RandomUtil from '@/plugins/randomUtil'
import Dial from '@/components/Dial.vue'
import Multiplex from '@/components/Multiplex.vue'
import Transport from '@/components/Transport.vue'
import OutTLS from '@/components/tls/OutTLS.vue'
import Direct from '@/components/protocols/Direct.vue'
import Socks from '@/components/protocols/Socks.vue'
import Http from '@/components/protocols/Http.vue'
import Shadowsocks from '@/components/protocols/Shadowsocks.vue'
import Vmess from '@/components/protocols/Vmess.vue'
import Trojan from '@/components/protocols/Trojan.vue'
import Wireguard from '@/components/protocols/Wireguard.vue'
import Hysteria from '@/components/protocols/Hysteria.vue'
import ShadowTls from '@/components/protocols/OutShadowTls.vue'
import Vless from '@/components/protocols/Vless.vue'
import Tuic from '@/components/protocols/Tuic.vue'
import Hysteria2 from '@/components/protocols/Hysteria2.vue'
import Tor from '@/components/protocols/Tor.vue'
import Ssh from '@/components/protocols/Ssh.vue'
import Selector from '@/components/protocols/Selector.vue'
import UrlTest from '@/components/protocols/UrlTest.vue'
import HttpUtils from '@/plugins/httputil'
export default {
props: ['visible', 'data', 'id', 'stats', 'tags'],
emits: ['close', 'save'],
data() {
return {
outbound: createOutbound("direct",{ "tag": "" }),
title: "add",
tab: "t1",
link: "",
loading: false,
outTypes: OutTypes,
outboundStats: false,
NoDial: [OutTypes.Block, OutTypes.DNS, OutTypes.Selector, OutTypes.URLTest],
NoServer: [OutTypes.Direct, OutTypes.Block, OutTypes.DNS, OutTypes.Selector, OutTypes.URLTest, OutTypes.Tor],
}
},
methods: {
updateData() {
if (this.$props.id != -1) {
const newData = JSON.parse(this.$props.data)
this.outbound = createOutbound(newData.type, newData)
this.title = "edit"
}
else {
this.outbound = createOutbound("direct",{ tag: "direct-" + RandomUtil.randomSeq(3) })
this.title = "add"
}
this.tab = "t1"
this.outboundStats = this.$props.stats
},
changeType() {
// Tag change only in add outbound
const tag = this.$props.id != -1 ? this.outbound.tag : this.outbound.type + "-" + RandomUtil.randomSeq(3)
// Use previous data
const prevConfig = { tag: tag ,listen: this.outbound.listen, listen_port: this.outbound.listen_port }
this.outbound = createOutbound(this.outbound.type, prevConfig)
},
closeModal() {
this.updateData() // reset
this.$emit('close')
},
saveChanges() {
this.loading = true
this.$emit('save', this.outbound, this.outboundStats)
this.loading = false
},
async linkConvert() {
if (this.link.length>0){
this.loading = true
const msg = await HttpUtils.post('api/linkConvert', { link: this.link })
this.loading = false
if (msg.success) {
this.outbound = msg.obj
this.tab = "t1"
this.link = ""
}
}
}
},
watch: {
visible(newValue) {
if (newValue) {
this.updateData()
}
},
},
components: { Dial, Multiplex, Transport, OutTLS,
Direct, Socks, Http, Shadowsocks, Vmess, Trojan,
Wireguard, Hysteria, ShadowTls, Vless, Tuic,
Hysteria2, Tor, Ssh, Selector, UrlTest }
}
</script>

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