Compare commits

..

81 Commits

Author SHA1 Message Date
MHSanaei
27d020709e 1.1.2 2023-03-24 17:45:47 +03:30
MHSanaei
77be5cf7d8 Merge pull request #74 from MHSanaei/dev
Dev
2023-03-24 17:43:13 +03:30
MHSanaei
c9d768a086 Update index.html 2023-03-24 17:35:07 +03:30
MHSanaei
9c0718bc44 Revert "Add version and log"
This reverts commit 826c7264b5.
2023-03-24 17:14:26 +03:30
MHSanaei
826c7264b5 Add version and log
TGBOT: Add xray config to backup
[TGBOT] add seach inbound
2023-03-24 17:13:31 +03:30
MHSanaei
162349f8c8 [Bulk client] add option: without random email 2023-03-24 17:11:29 +03:30
MHSanaei
a6dfdcdd31 Add version and log 2023-03-24 17:08:30 +03:30
MHSanaei
03a6c131f9 [infoModal] better display 2023-03-24 16:54:21 +03:30
MHSanaei
8dad9a4338 [tgbot] fix admins input 2023-03-24 16:51:43 +03:30
MHSanaei
f0d4dbf838 [tgbot] fix exhausted report 2023-03-24 16:50:10 +03:30
MHSanaei
3152d5f191 Remove ugly-bugy qrCode footer 2023-03-24 16:47:14 +03:30
MHSanaei
17f64462d2 Merge pull request #73 from MHSanaei/TLS-enhancements
Tls enhancements
2023-03-24 16:42:08 +03:30
MHSanaei
bbec13c0da [tgbot] fix client search 2023-03-24 16:40:56 +03:30
MHSanaei
466ad1605b Merge pull request #72 from MHSanaei/dependabot/github_actions/actions/checkout-3.5.0
Bump actions/checkout from 3.4.0 to 3.5.0
2023-03-24 16:32:29 +03:30
dependabot[bot]
a068c350ee Bump actions/checkout from 3.4.0 to 3.5.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.4.0 to 3.5.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.4.0...v3.5.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-24 10:05:30 +00:00
Alireza Ahmadi
0605221628 Enable fallback in xtls as well 2023-03-23 20:14:51 +01:00
Alireza Ahmadi
3856c4d0f9 Fix input-group darck-theme 2023-03-23 12:52:49 +01:00
Alireza Ahmadi
557a9d020a Fix TLS-ALPN + allowInsecure 2023-03-23 11:38:16 +01:00
Alireza Ahmadi
14d7cb812e Default tls version 1.0-1.2 2023-03-23 10:37:13 +01:00
MHSanaei
c49a9e877c Update issue templates 2023-03-22 22:49:57 +03:30
MHSanaei
b08d653e02 Merge pull request #60 from MHSanaei/dependabot/go_modules/google.golang.org/grpc-1.54.0
Bump google.golang.org/grpc from 1.53.0 to 1.54.0
2023-03-22 16:29:07 +03:30
dependabot[bot]
0928e408c0 Bump google.golang.org/grpc from 1.53.0 to 1.54.0
Bumps [google.golang.org/grpc](https://github.com/grpc/grpc-go) from 1.53.0 to 1.54.0.
- [Release notes](https://github.com/grpc/grpc-go/releases)
- [Commits](https://github.com/grpc/grpc-go/compare/v1.53.0...v1.54.0)

---
updated-dependencies:
- dependency-name: google.golang.org/grpc
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-22 10:07:59 +00:00
MHSanaei
032ed73c0d custom version 2023-03-20 16:49:39 +03:30
MHSanaei
c5bbee354f Merge pull request #34 from MHSanaei/dev
i hate myself
2023-03-19 14:39:51 +03:30
MHSanaei
fe7ce3f74b Update xray.js 2023-03-19 14:39:19 +03:30
MHSanaei
63acd585ba Merge pull request #33 from MHSanaei/xray.js
Update xray.js
2023-03-19 14:15:05 +03:30
MHSanaei
9a1cf70451 Update xray.js 2023-03-19 14:13:19 +03:30
MHSanaei
1777f257a8 Update version 2023-03-19 14:00:52 +03:30
MHSanaei
b4997da51c Update install.sh 2023-03-19 12:44:46 +03:30
MHSanaei
4f9aff3043 Merge pull request #31 from MHSanaei/dev
xtls bug fixed
2023-03-19 12:41:25 +03:30
MHSanaei
e68317c6bd xtls bug fixed 2023-03-19 12:39:14 +03:30
MHSanaei
b7f476568b bug fixed 2023-03-18 20:23:29 +03:30
MHSanaei
40c2f5206b Update README.md 2023-03-18 00:53:00 +03:30
MHSanaei
b0a544a321 Add files via upload 2023-03-18 00:30:11 +03:30
MHSanaei
12828b6a15 Update xray.js 2023-03-18 00:01:20 +03:30
MHSanaei
8c6c9ace79 Merge pull request #29 from MHSanaei/ClientIPs-functions
Client ips functions
2023-03-17 23:07:52 +03:30
MHSanaei
f881c5347c Merge branch 'main' into ClientIPs-functions 2023-03-17 23:07:43 +03:30
MHSanaei
430338f045 Update trojan.html 2023-03-17 23:05:33 +03:30
MHSanaei
74b14657cf remove useless code 2023-03-17 23:03:21 +03:30
MHSanaei
07b35753d4 Merge branch 'ClientIPs-functions' of https://github.com/MHSanaei/3x-ui into ClientIPs-functions 2023-03-17 22:12:19 +03:30
Alireza Ahmadi
263df2abf5 Fix delete bug 2023-03-17 19:39:26 +01:00
Alireza Ahmadi
738a771b87 Fix delete bug 2023-03-17 19:29:08 +01:00
Alireza Ahmadi
025f559460 Fix clear IP affect in UI 2023-03-17 19:04:07 +01:00
Alireza Ahmadi
baaa814a51 Fix del/update affects on clientIPs 2023-03-17 19:02:56 +01:00
MHSanaei
d46e0e6925 Delete inbounds_client_row.html 2023-03-17 20:45:47 +03:30
MHSanaei
3cd3cba8c6 Merge pull request #28 from MHSanaei/dev
Update trojan.html
2023-03-17 20:38:35 +03:30
MHSanaei
366463bbb3 Update trojan.html 2023-03-17 20:38:01 +03:30
MHSanaei
a0d6f85837 1.1.0 2023-03-17 20:01:24 +03:30
MHSanaei
f25a7a571e Merge pull request #26 from MHSanaei/dev
alireza
2023-03-17 20:00:14 +03:30
MHSanaei
87e173b567 Merge branch 'main' into dev 2023-03-17 20:00:08 +03:30
MHSanaei
6fbcd28799 Merge tag 'main' into dev 2023-03-17 16:22:01 +00:00
MHSanaei
b9ffe62d69 1.1.0 2023-03-17 19:41:01 +03:30
MHSanaei
96786c9418 alireza 2023-03-17 19:37:49 +03:30
MHSanaei
0423d8d919 1.0.9 2023-03-17 02:36:28 +03:30
MHSanaei
688a68aba9 Merge pull request #25 from MHSanaei/dev
pack
2023-03-17 02:35:08 +03:30
MHSanaei
bc56e63737 pack1 2023-03-17 01:31:14 +03:30
MHSanaei
a3e5628961 Merge pull request #24 from MHSanaei/dependabot/github_actions/actions/checkout-3.4.0
Bump actions/checkout from 3.3.0 to 3.4.0
2023-03-16 11:21:35 +03:30
MHSanaei
74ff17eeed Merge pull request #23 from MHSanaei/dependabot/github_actions/actions/setup-go-4.0.0
Bump actions/setup-go from 3.5.0 to 4.0.0
2023-03-16 11:21:16 +03:30
dependabot[bot]
b1dca4dc24 Bump actions/checkout from 3.3.0 to 3.4.0
Bumps [actions/checkout](https://github.com/actions/checkout) from 3.3.0 to 3.4.0.
- [Release notes](https://github.com/actions/checkout/releases)
- [Changelog](https://github.com/actions/checkout/blob/main/CHANGELOG.md)
- [Commits](https://github.com/actions/checkout/compare/v3.3.0...v3.4.0)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-16 07:50:54 +00:00
dependabot[bot]
1a2b14b212 Bump actions/setup-go from 3.5.0 to 4.0.0
Bumps [actions/setup-go](https://github.com/actions/setup-go) from 3.5.0 to 4.0.0.
- [Release notes](https://github.com/actions/setup-go/releases)
- [Commits](https://github.com/actions/setup-go/compare/v3.5.0...v4.0.0)

---
updated-dependencies:
- dependency-name: actions/setup-go
  dependency-type: direct:production
  update-type: version-update:semver-major
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-16 07:50:51 +00:00
MHSanaei
20b30958cb Merge pull request #22 from MHSanaei/dependabot/go_modules/google.golang.org/protobuf-1.29.1
Bump google.golang.org/protobuf from 1.29.0 to 1.29.1
2023-03-16 00:53:32 +03:30
dependabot[bot]
d224c207f8 Bump google.golang.org/protobuf from 1.29.0 to 1.29.1
Bumps [google.golang.org/protobuf](https://github.com/protocolbuffers/protobuf-go) from 1.29.0 to 1.29.1.
- [Release notes](https://github.com/protocolbuffers/protobuf-go/releases)
- [Changelog](https://github.com/protocolbuffers/protobuf-go/blob/master/release.bash)
- [Commits](https://github.com/protocolbuffers/protobuf-go/compare/v1.29.0...v1.29.1)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-14 23:07:07 +00:00
MHSanaei
f2d7b23e50 Update install.sh 2023-03-11 19:49:36 +03:30
MHSanaei
a28a855eff !!!
stupid mistake
2023-03-11 19:08:43 +03:30
MHSanaei
68da556bd2 fedora and centos install fixed 2023-03-11 18:35:35 +03:30
MHSanaei
b77001973a go version 'stable' 2023-03-09 22:45:18 +03:30
MHSanaei
7d887024df Merge pull request #21 from MHSanaei/dependabot/go_modules/github.com/xtls/xray-core-1.8.0
Bump github.com/xtls/xray-core from 1.7.5 to 1.8.0
2023-03-09 22:41:05 +03:30
dependabot[bot]
8641ec8f13 Bump github.com/xtls/xray-core from 1.7.5 to 1.8.0
Bumps [github.com/xtls/xray-core](https://github.com/xtls/xray-core) from 1.7.5 to 1.8.0.
- [Release notes](https://github.com/xtls/xray-core/releases)
- [Commits](https://github.com/xtls/xray-core/compare/v1.7.5...v1.8.0)

---
updated-dependencies:
- dependency-name: github.com/xtls/xray-core
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-09 19:07:53 +00:00
MHSanaei
6337efb786 fedora + bbr
support fedora 29+ remove old bbr script and change it to easy way
2023-03-08 02:04:07 +03:30
MHSanaei
4d9852f68d Merge pull request #18 from MHSanaei/dependabot/go_modules/gorm.io/gorm-1.24.6
Bump gorm.io/gorm from 1.24.5 to 1.24.6
2023-03-06 19:44:10 +03:30
MHSanaei
750fc54787 Merge pull request #19 from MHSanaei/dependabot/go_modules/golang.org/x/text-0.8.0
Bump golang.org/x/text from 0.7.0 to 0.8.0
2023-03-06 19:43:57 +03:30
dependabot[bot]
a2cd6a534f Bump golang.org/x/text from 0.7.0 to 0.8.0
Bumps [golang.org/x/text](https://github.com/golang/text) from 0.7.0 to 0.8.0.
- [Release notes](https://github.com/golang/text/releases)
- [Commits](https://github.com/golang/text/compare/v0.7.0...v0.8.0)

---
updated-dependencies:
- dependency-name: golang.org/x/text
  dependency-type: direct:production
  update-type: version-update:semver-minor
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-06 10:19:18 +00:00
dependabot[bot]
529eb3088a Bump gorm.io/gorm from 1.24.5 to 1.24.6
Bumps [gorm.io/gorm](https://github.com/go-gorm/gorm) from 1.24.5 to 1.24.6.
- [Release notes](https://github.com/go-gorm/gorm/releases)
- [Commits](https://github.com/go-gorm/gorm/compare/v1.24.5...v1.24.6)

---
updated-dependencies:
- dependency-name: gorm.io/gorm
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-06 10:18:56 +00:00
MHSanaei
f0de1e5a62 Merge pull request #16 from MHSanaei/dependabot/go_modules/github.com/pelletier/go-toml/v2-2.0.7
Bump github.com/pelletier/go-toml/v2 from 2.0.6 to 2.0.7
2023-03-01 19:33:15 +03:30
MHSanaei
14c5546661 Merge pull request #15 from MHSanaei/dependabot/go_modules/github.com/shirou/gopsutil/v3-3.23.2
Bump github.com/shirou/gopsutil/v3 from 3.23.1 to 3.23.2
2023-03-01 19:32:49 +03:30
dependabot[bot]
e6a52dc2f6 Bump github.com/pelletier/go-toml/v2 from 2.0.6 to 2.0.7
Bumps [github.com/pelletier/go-toml/v2](https://github.com/pelletier/go-toml) from 2.0.6 to 2.0.7.
- [Release notes](https://github.com/pelletier/go-toml/releases)
- [Changelog](https://github.com/pelletier/go-toml/blob/v2/.goreleaser.yaml)
- [Commits](https://github.com/pelletier/go-toml/compare/v2.0.6...v2.0.7)

---
updated-dependencies:
- dependency-name: github.com/pelletier/go-toml/v2
  dependency-type: direct:production
  update-type: version-update:semver-patch
...

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-01 09:31:52 +00:00
dependabot[bot]
28c9e7fe9c Bump github.com/shirou/gopsutil/v3 from 3.23.1 to 3.23.2
Bumps [github.com/shirou/gopsutil/v3](https://github.com/shirou/gopsutil) from 3.23.1 to 3.23.2.
- [Release notes](https://github.com/shirou/gopsutil/releases)
- [Commits](https://github.com/shirou/gopsutil/compare/v3.23.1...v3.23.2)

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

Signed-off-by: dependabot[bot] <support@github.com>
2023-03-01 09:31:40 +00:00
MHSanaei
b37b12754f Merge pull request #14 from MHSanaei/dev
Update xray.js - JSON.parse
2023-03-01 00:44:23 +03:30
MHSanaei
c7ca4c29d5 Merge branch 'main' of https://github.com/MHSanaei/3x-ui 2023-03-01 00:42:11 +03:30
MHSanaei
44b74224ab Update xray.js 2023-03-01 00:42:08 +03:30
MHSanaei
7a4523dec1 Add files via upload 2023-03-01 00:24:25 +03:30
66 changed files with 3135 additions and 1376 deletions

24
.github/ISSUE_TEMPLATE/bug_report.md vendored Normal file
View File

@@ -0,0 +1,24 @@
---
name: Bug report
about: Create a report to help us improve
title: ''
labels: bug
assignees: ''
---
**Describe the 3x-ui bug**
A clear and concise description of what the bug is.
**Expected behavior**
A clear and concise description of what you expected to happen.
**Screenshots**
If applicable, add screenshots to help explain your problem.
**Server (please complete the following information):**
- OS: [e.g. iOS]
- Version [e.g. 22]
**Additional context**
Add any other context about the problem here.

View File

@@ -0,0 +1,20 @@
---
name: Feature request
about: Suggest an idea for this project
title: ''
labels: enhancement
assignees: ''
---
**Is your feature request related to a problem? Please describe.**
A clear and concise description of what the problem is. Ex. I'm always frustrated when [...]
**Describe the solution you'd like**
A clear and concise description of what you want to happen.
**Describe alternatives you've considered**
A clear and concise description of any alternative solutions or features you've considered.
**Additional context**
Add any other context or screenshots about the feature request here.

10
.github/ISSUE_TEMPLATE/question-.md vendored Normal file
View File

@@ -0,0 +1,10 @@
---
name: 'Question '
about: Describe this issue template's purpose here.
title: ''
labels: question
assignees: ''
---

View File

@@ -6,11 +6,11 @@ jobs:
name: build x-ui amd64 version name: build x-ui amd64 version
runs-on: ubuntu-20.04 runs-on: ubuntu-20.04
steps: steps:
- uses: actions/checkout@v3.3.0 - uses: actions/checkout@v3.5.0
- name: Set up Go - name: Set up Go
uses: actions/setup-go@v3.5.0 uses: actions/setup-go@v4.0.0
with: with:
go-version: '1.20.1' go-version: 'stable'
- name: build linux amd64 version - name: build linux amd64 version
run: | run: |
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go

View File

@@ -1,10 +1,11 @@
# 3x-ui # 3x-ui
![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg) ![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)
![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg) ![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui) ![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total) ![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-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)
> **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment** > **Disclaimer: This project is only for personal learning and communication, please do not use it for illegal purposes, please do not use it in a production environment**
xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)** xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
@@ -14,7 +15,11 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese)**
``` ```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
``` ```
## Install custom version
To install your desired version you can add the version to the end of install command. Example for ver `v1.0.9`:
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.0.9
```
# SSL # SSL
``` ```
apt-get install certbot -y apt-get install certbot -y
@@ -27,7 +32,7 @@ certbot renew --dry-run
# Default settings # Default settings
- Port: 2053 - Port: 2053
- username and password will be generated randomly you can see them after you install it (x-ui "7") - username and password will be generated randomly if you skip to modify your own security(x-ui "7")
- database path: /etc/x-ui/x-ui.db - database path: /etc/x-ui/x-ui.db
before you set ssl on settings before you set ssl on settings
@@ -39,28 +44,73 @@ After you set ssl on settings
# Enable Traffic For Users: # Enable Traffic For Users:
**copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install) **copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install)
- [for enable traffic](https://raw.githubusercontent.com/MHSanaei/3x-ui/main/media/for%20enable%20traffic.txt) - [for enable traffic](https://raw.githubusercontent.com/mhsanaei/3x-ui/main/media/for%20enable%20traffic.txt)
- [for enable traffic+block all iran ip address](https://raw.githubusercontent.com/MHSanaei/3x-ui/main/media/for%20enable%20traffic%2Bblock%20all%20iran%20ip.txt) - [for enable traffic+block all iran ip address](https://raw.githubusercontent.com/mhsanaei/3x-ui/main/media/for%20enable%20traffic%2Bblock%20all%20iran%20ip.txt)
# Features
- System Status Monitoring
- Search within all inbounds and clients
- Support Dark/Light theme UI
- Support multi-user multi-protocol, web page visualization operation
- Supported protocols: vmess, vless, trojan, shadowsocks, dokodemo-door, socks, http
- Support for configuring more transport configurations
- Traffic statistics, limit traffic, limit expiration time
- Customizable xray configuration templates
- Support https access panel (self-provided domain name + ssl certificate)
- Support one-click SSL certificate application and automatic renewal
- For more advanced configuration items, please refer to the panel
# Tg robot use
X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
Set the robot-related parameters in the panel background, including:
- Tg robot Token
- Tg robot ChatId
- Tg robot cycle runtime, in crontab syntax
- Tg robot Expiration threshold
- Tg robot Traffic threshold
- Tg robot Enable send backup in cycle runtime
- Tg robot Enable CPU usage alarm threshold
Reference syntax:
- @hourly // hourly notification
- @daily // Daily notification (00:00 in the morning)
- @every 8h // notify every 8 hours
# Telegram Bot Features
- Report periodic
- Login notification
- CPU threshold notification
- Threshold for Expiration time and Traffic to report in advance
- Support client report if client's telegram username is added to the end of `email` like 'test123@telegram_username'
- Support telegram traffic report searched with UID (VMESS/VLESS) or Password (TROJAN) - anonymously
- Menu based bot
- Search client by email ( only admin )
- Check all inbounds
- Check server status
- Check Exhausted users
- Receive backup by request and in periodic reports
# A Special Thanks To
- [alireza0](https://github.com/alireza0/)
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
# Suggestion System # Suggestion System
- Ubuntu 20.04+ - Ubuntu 20.04+
- Debian 10+ - Debian 10+
- CentOS 8+ - CentOS 8+
- Fedora 36+
# Pictures # Pictures
![1](https://raw.githubusercontent.com/MHSanaei/3x-ui/main/media/1.png) ![1](./media/1.png)
![2](https://raw.githubusercontent.com/MHSanaei/3x-ui/main/media/2.png) ![2](./media/2.png)
![3](https://raw.githubusercontent.com/MHSanaei/3x-ui/main/media/3.png) ![3](./media/3.png)
![4](https://raw.githubusercontent.com/MHSanaei/3x-ui/main/media/4.png) ![4](./media/4.png)
# A Special Thanks To
- [vaxilu](https://github.com/vaxilu/)
- [HexaSoftwareTech](https://github.com/HexaSoftwareTech/)
- [diditra](https://github.com/diditra/)
- [FranzKafkaYu](https://github.com/FranzKafkaYu)
- [alireza0](https://github.com/alireza0/)
## Stargazers over time ## Stargazers over time

View File

@@ -1 +1 @@
1.0.8 1.1.2

View File

@@ -73,10 +73,11 @@ type Setting struct {
type Client struct { type Client struct {
ID string `json:"id"` ID string `json:"id"`
Password string `json:"password"`
Flow string `json:"flow"`
AlterIds uint16 `json:"alterId"` AlterIds uint16 `json:"alterId"`
Email string `json:"email"` Email string `json:"email"`
LimitIP int `json:"limitIp"` LimitIP int `json:"limitIp"`
Security string `json:"security"`
TotalGB int64 `json:"totalGB" form:"totalGB"` TotalGB int64 `json:"totalGB" form:"totalGB"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
} }

24
go.mod
View File

@@ -10,15 +10,15 @@ require (
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
github.com/nicksnyder/go-i18n/v2 v2.2.1 github.com/nicksnyder/go-i18n/v2 v2.2.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.0.6 github.com/pelletier/go-toml/v2 v2.0.7
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.23.1 github.com/shirou/gopsutil/v3 v3.23.2
github.com/xtls/xray-core v1.7.5 github.com/xtls/xray-core v1.8.0
go.uber.org/atomic v1.10.0 go.uber.org/atomic v1.10.0
golang.org/x/text v0.7.0 golang.org/x/text v0.8.0
google.golang.org/grpc v1.53.0 google.golang.org/grpc v1.54.0
gorm.io/driver/sqlite v1.4.4 gorm.io/driver/sqlite v1.4.4
gorm.io/gorm v1.24.5 gorm.io/gorm v1.24.6
) )
require ( require (
@@ -31,7 +31,7 @@ require (
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.11.2 // indirect github.com/go-playground/validator/v10 v10.11.2 // indirect
github.com/goccy/go-json v0.10.0 // indirect github.com/goccy/go-json v0.10.0 // indirect
github.com/golang/protobuf v1.5.2 // indirect github.com/golang/protobuf v1.5.3 // indirect
github.com/gorilla/context v1.1.1 // indirect github.com/gorilla/context v1.1.1 // indirect
github.com/gorilla/securecookie v1.1.1 // indirect github.com/gorilla/securecookie v1.1.1 // indirect
github.com/gorilla/sessions v1.2.1 // indirect github.com/gorilla/sessions v1.2.1 // indirect
@@ -53,10 +53,10 @@ require (
github.com/ugorji/go/codec v1.2.10 // indirect github.com/ugorji/go/codec v1.2.10 // indirect
github.com/yusufpapurcu/wmi v1.2.2 // indirect github.com/yusufpapurcu/wmi v1.2.2 // indirect
golang.org/x/arch v0.2.0 // indirect golang.org/x/arch v0.2.0 // indirect
golang.org/x/crypto v0.6.0 // indirect golang.org/x/crypto v0.7.0 // indirect
golang.org/x/net v0.7.0 // indirect golang.org/x/net v0.8.0 // indirect
golang.org/x/sys v0.5.0 // indirect golang.org/x/sys v0.6.0 // indirect
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 // indirect google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 // indirect
google.golang.org/protobuf v1.28.1 // indirect google.golang.org/protobuf v1.29.1 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
) )

83
go.sum
View File

@@ -3,7 +3,7 @@ github.com/BurntSushi/toml v1.2.1 h1:9F2/+DoOYIOksmaJFPw1tGFy1eDnIJXg+UHjuD8lTak
github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.2.1/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig= github.com/Workiva/go-datastructures v1.0.53 h1:J6Y/52yX10Xc5JjXmGtWoSSxs3mZnGSaq37xZZh7Yig=
github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A= github.com/Workiva/go-datastructures v1.0.53/go.mod h1:1yZL+zfsztete+ePzZz/Zb1/t5BnDuE2Ya2MMGhzP6A=
github.com/andybalholm/brotli v1.0.4 h1:V7DdXeJtZscaqfNuAdSRuRFzuiKlHSC/Zh3zl9qY3JY= github.com/andybalholm/brotli v1.0.5 h1:8uQZIdzKmjc/iuPu7O2ioW48L81FgatrcpfFmiq/cCs=
github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw= github.com/antonlindstrom/pgstore v0.0.0-20200229204646-b08ebf1105e0/go.mod h1:2Ti6VUHVxpC0VSmTZzEvpzysnaGAfGBOoMIz5ykPyyw=
github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw= github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P97fT2CiHkbFQwkK3mjsFAP6zCYV2aXtjw=
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA= github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
@@ -52,8 +52,8 @@ github.com/goccy/go-json v0.10.0/go.mod h1:6MelG93GURQebXPDq3khkgXZkazVtN9CRI+MG
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc= github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw= github.com/golang/protobuf v1.3.3/go.mod h1:vzj43D7+SQXF/4pzW/hwtAqwc6iTitCiVSaWz5lYuqw=
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk= github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
github.com/golang/protobuf v1.5.2 h1:ROPKBNFfQgOUMifHyP+KYbvpjbdoFNs+aK7DXlji0Tw= github.com/golang/protobuf v1.5.3 h1:KhyjKVUg7Usr/dYsdSqoFveMYd5ko72D+zANwlG1mmg=
github.com/golang/protobuf v1.5.2/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY= github.com/golang/protobuf v1.5.3/go.mod h1:XVQd3VNwM+JqD3oG2Ue2ip4fOMUkwXdXDdiuN0vRsmY=
github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4= github.com/gomodule/redigo v2.0.0+incompatible/go.mod h1:B4C85qUVwatsJoIUNIfCRsp7qO0iAmpGFZ4EELWSbC4=
github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU= github.com/google/btree v1.1.2 h1:xf4v41cLI2Z6FxbKm+8Bu+m8ifhj15JuZ9sa0jZCMUU=
github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE= github.com/google/go-cmp v0.5.5/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/gNBxE=
@@ -61,7 +61,7 @@ github.com/google/go-cmp v0.5.6/go.mod h1:v8dTdLbMG2kIc/vJvl+f65V22dbkXbowE6jgT/
github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38= github.com/google/go-cmp v0.5.9 h1:O2Tfq5qg4qc4AmwVlvv0oLiVAGB7enBSJ2x2DqQFi38=
github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= github.com/google/go-cmp v0.5.9/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY=
github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg= github.com/google/gofuzz v1.0.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
github.com/google/pprof v0.0.0-20230207041349-798e818bf904 h1:4/hN5RUoecvl+RmJRE2YxKWtnnQls6rQjjW5oV7qg2U= github.com/google/pprof v0.0.0-20230228050547-1710fef4ab10 h1:CqYfpuYIjnlNxM3msdyPRKabhXZWbKjf3Q8BWROFBso=
github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8= github.com/gorilla/context v1.1.1 h1:AWwleXJkX/nhcU9bZSnZoi3h/qGYqQAGhq6zZe/aQW8=
github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg= github.com/gorilla/context v1.1.1/go.mod h1:kBGZzfjB9CEq2AlWe17Uuf7NDRt0dE0s8S51q0aT7Yg=
github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ= github.com/gorilla/securecookie v1.1.1 h1:miw7JPhV+b/lAHSXz4qd/nN9jRiAFV5FwjeKyCS8BvQ=
@@ -80,7 +80,7 @@ github.com/json-iterator/go v1.1.9/go.mod h1:KdQUCv79m/52Kvf8AW2vK1V8akMuk1QjK/u
github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM= github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnrnM=
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw= github.com/kidstuff/mongostore v0.0.0-20181113001930-e650cd85ee4b/go.mod h1:g2nVr8KZVXJSS97Jo8pJ0jgq29P6H7dG0oplUA86MQw=
github.com/klauspost/compress v1.15.15 h1:EF27CXIuDsYJ6mmvtBRlEuB2UVOqHG1tAXgZ7yIO+lw= github.com/klauspost/compress v1.16.0 h1:iULayQNOReoYUe+1qtKOqw9CwJv3aNQu8ivo7lw1HU4=
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.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk= github.com/klauspost/cpuid/v2 v2.2.4 h1:acbojRNwl3o09bUq+yDCtZFc1aiwaAAxtcn8YkZXnvk=
github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY= github.com/klauspost/cpuid/v2 v2.2.4/go.mod h1:RVVoqg1df56z8g3pUjL/3lE5UfnlrJX8tyFgg4nqhuY=
@@ -100,6 +100,7 @@ github.com/mattn/go-sqlite3 v1.14.15/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S
github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y= github.com/mattn/go-sqlite3 v1.14.16 h1:yOQRA0RpS5PFz/oikGwBEqvAWhWg5ufRz4ETLjwpU1Y=
github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg= github.com/mattn/go-sqlite3 v1.14.16/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc= github.com/memcachier/mc v2.0.1+incompatible/go.mod h1:7bkvFE61leUBvXz+yxsOnGBQSZpBSPIMUQSmmSHvuXc=
github.com/miekg/dns v1.1.51 h1:0+Xg7vObnhrz/4ZCZcZh7zPXlmU0aveS2HDBd0m0qSo=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q= github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@@ -108,12 +109,12 @@ github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9G
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA= github.com/nicksnyder/go-i18n/v2 v2.2.1 h1:aOzRCdwsJuoExfZhoiXHy4bjruwCMdt5otbYojM/PaA=
github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0= github.com/nicksnyder/go-i18n/v2 v2.2.1/go.mod h1:fF2++lPHlo+/kPaj3nB0uxtPwzlPm+BlgwGX7MkeGj0=
github.com/onsi/ginkgo/v2 v2.8.0 h1:pAM+oBNPrpXRs+E/8spkeGx9QgekbRVyr74EUvRVOUI= github.com/onsi/ginkgo/v2 v2.9.0 h1:Tugw2BKlNHTMfG+CheOITkYvk4LAh6MFOvikhGVnhE8=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk= github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8= github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml/v2 v2.0.6 h1:nrzqCb7j9cDFj2coyLNLaZuJTLjWjlaz6nvTvIwycIU= github.com/pelletier/go-toml/v2 v2.0.7 h1:muncTPStnKRos5dpVKULv2FVd4bMOhNePj9CjgDb8Us=
github.com/pelletier/go-toml/v2 v2.0.6/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek= github.com/pelletier/go-toml/v2 v2.0.7/go.mod h1:eumQOmlWiOPt5WriQQqoM5y18pDHwha2N+QD+EUNTek=
github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU= github.com/philhofer/fwd v1.1.1/go.mod h1:gk3iGcWd9+svBvR0sR+KPcfE+RNWozjowpeBVG3ZVNU=
github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8= github.com/pires/go-proxyproto v0.6.2 h1:KAZ7UteSOt6urjme6ZldyFm4wDe/z0ZUP0Yv0Dos0d8=
github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY= github.com/pires/go-proxyproto v0.6.2/go.mod h1:Odh9VFOZJCf9G8cLW5o435Xf1J95Jw9Gw5rnCjcwzAY=
@@ -124,21 +125,20 @@ github.com/power-devops/perfstat v0.0.0-20210106213030-5aafc221ea8c/go.mod h1:Om
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b h1:0LFwY6Q3gMACTjAbMZBjXAqTOzOwFaj2Ld6cjeQ7Rig=
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg= github.com/quasoft/memstore v0.0.0-20191010062613-2bce066d2b0b/go.mod h1:wTPjTepVu7uJBYgZ0SdWHQlIas582j6cn2jgk4DDdlg=
github.com/quic-go/qtls-go1-18 v0.2.0 h1:5ViXqBZ90wpUcZS0ge79rf029yx0dYB0McyPJwqqj7U= github.com/quic-go/qtls-go1-19 v0.2.1 h1:aJcKNMkH5ASEJB9FXNeZCyTEIHU1J7MmHyz1Q1TSG1A=
github.com/quic-go/qtls-go1-19 v0.2.0 h1:Cvn2WdhyViFUHoOqK52i51k4nDX8EwIh5VJiVM4nttk= github.com/quic-go/qtls-go1-20 v0.1.1 h1:KbChDlg82d3IHqaj2bn6GfKRj84Per2VGf5XV3wSwQk=
github.com/quic-go/qtls-go1-20 v0.1.0 h1:d1PK3ErFy9t7zxKsG3NXBJXZjp/kMLoIb3y/kV54oAI= github.com/quic-go/quic-go v0.33.0 h1:ItNoTDN/Fm/zBlq769lLJc8ECe9gYaW40veHCCco7y0=
github.com/quic-go/quic-go v0.32.0 h1:lY02md31s1JgPiiyfqJijpu/UX/Iun304FI3yUqX7tA= github.com/refraction-networking/utls v1.2.3-0.20230308205431-4f1df6c200db h1:ULRv/GPW5KYDafE0FACN2no+HTCyQLUtfyOIeyp3GNc=
github.com/refraction-networking/utls v1.2.2-0.20230207151345-a75a4b484849 h1:vNEcNapWFwnYJTBcVkHJa8VrdL40PNDLDbSGVY+ZV7I=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs= github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro= github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8= github.com/rogpeppe/go-internal v1.8.0 h1:FCbCCtXNOY3UtUuHUYaghJg4y7Fd14rXifAYUAtL9R8=
github.com/sagernet/sing v0.1.6 h1:Qy63OUfKpcqKjfd5rPmUlj0RGjHZSK/PJn0duyCCsRg= github.com/sagernet/sing v0.1.7 h1:g4vjr3q8SUlBZSx97Emz5OBfSMBxxW5Q8C2PfdoSo08=
github.com/sagernet/sing-shadowsocks v0.1.1-0.20230202035033-e3123545f2f7 h1:Plup6oEiyLzY3HDqQ+QsUBzgBGdVmcsgf3t8h940z9U= github.com/sagernet/sing-shadowsocks v0.1.1 h1:uFK2rlVeD/b1xhDwSMbUI2goWc6fOKxp+ZeKHZq6C9Q=
github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo= github.com/sagernet/wireguard-go v0.0.0-20221116151939-c99467f53f2c h1:vK2wyt9aWYHHvNLWniwijBu/n4pySypiKRhN32u/JGo=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U= github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
github.com/shirou/gopsutil/v3 v3.23.1 h1:a9KKO+kGLKEvcPIs4W62v0nu3sciVDOOOPUD0Hz7z/4= github.com/shirou/gopsutil/v3 v3.23.2 h1:PAWSuiAszn7IhPMBtXsbSCafej7PqUOvY6YywlQUExU=
github.com/shirou/gopsutil/v3 v3.23.1/go.mod h1:NN6mnm5/0k8jw4cBfCnJtr5L7ErOTg18tMNpgFkn0hA= github.com/shirou/gopsutil/v3 v3.23.2/go.mod h1:gv0aQw33GLo3pG8SiWKiQrbDzbRY1K80RyZJ7V4Th1M=
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=
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo= github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
@@ -148,8 +148,9 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.0/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg= github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
github.com/stretchr/testify v1.8.1 h1:w7B6lhMri9wdJUVmEZPGGhZzrYTPvgJArz7wNPgYKsk=
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.2 h1:+h33VjcLVPDHtOdpUCuF+7gSuG3yGIftsP1YvFihtJ8=
github.com/stretchr/testify v1.8.2/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg= github.com/tinylib/msgp v1.1.5/go.mod h1:eQsjooMTnV42mHu917E26IogZ2930nFyBQdofk10Udg=
github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM= github.com/tklauser/go-sysconf v0.3.11 h1:89WgdJhk5SNwJfu+GKyYveZ4IaJ7xAkecBo+KdJV0CM=
github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI= github.com/tklauser/go-sysconf v0.3.11/go.mod h1:GqXfhXY3kiPa0nAXPDIQIWzJbMCB7AmcWpGR8lSZfqI=
@@ -163,14 +164,14 @@ github.com/ugorji/go/codec v1.1.7/go.mod h1:Ax+UKWsSmolVDwsd+7N3ZtXu+yMGCf907BLY
github.com/ugorji/go/codec v1.2.10 h1:eimT6Lsr+2lzmSZxPhLFoOWFmQqwk0fllJJ5hEbTXtQ= github.com/ugorji/go/codec v1.2.10 h1:eimT6Lsr+2lzmSZxPhLFoOWFmQqwk0fllJJ5hEbTXtQ=
github.com/ugorji/go/codec v1.2.10/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg= github.com/ugorji/go/codec v1.2.10/go.mod h1:UNopzCgEMSXjBc6AOMqYvWC1ktqTAfzJZUZgYf6w6lg=
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
github.com/xtls/go v0.0.0-20230107031059-4610f88d00f3 h1:a3Y4WVjCxwoyO4E2xdNvq577tW8lkSBgyrA8E9+2NtM= github.com/xtls/reality v0.0.0-20230309125256-0d0713b108c8 h1:LLtLxEe3S0Ko+ckqt4t29RLskpNdOZfgjZCC2/Byr50=
github.com/xtls/xray-core v1.7.5 h1:Ukr3hXnOG2ciViQL7kfYRl9S3GVej2dkV7DzabmoLL4= github.com/xtls/xray-core v1.8.0 h1:/OD0sDv6YIBqvE+cVfnqlKrtbMs0Fm9IP5BR5d8Eu4k=
github.com/xtls/xray-core v1.7.5/go.mod h1:Mx1QzIDvSk4eZ8hKa3AYsSPfyZJNQXWVXTJxJRJ98wI= github.com/xtls/xray-core v1.8.0/go.mod h1:i9KWgbLyxg/NT+3+g4nE74Zp3DgTCP3X04YkSfsJeDI=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY= github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.starlark.net v0.0.0-20230128213706-3f75dec8e403 h1:jPeC7Exc+m8OBJUlWbBLh0O5UZPM7yU5W4adnhhbG4U= go.starlark.net v0.0.0-20230302034142-4b1e35fe2254 h1:Ss6D3hLXTM0KobyBYEAygXzFfGcjnmfEJOBgSbemCtg=
go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ= go.uber.org/atomic v1.10.0 h1:9qC72Qh0+3MqyJbAn8YU5xVq1frD8bn3JtD2oXtafVQ=
go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0= go.uber.org/atomic v1.10.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8= golang.org/x/arch v0.0.0-20210923205945-b76863e36670/go.mod h1:5om86z9Hs0C8fWVUuoMHwpExlXzs5Tkyp9hOrfG7pp8=
@@ -180,19 +181,19 @@ golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACk
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.6.0 h1:qfktjS5LUO+fFKeJXZ+ikTRijMmljikvG68fpMMruSc= golang.org/x/crypto v0.7.0 h1:AvwMYaRytfdeVt3u6mLaxYtErKYjxA2OXjJ1HHq6t3A=
golang.org/x/crypto v0.6.0/go.mod h1:OFC/31mSvZgRz0V1QTNCzfAI1aIRzbiufJtkMIlEp58= golang.org/x/crypto v0.7.0/go.mod h1:pYwdfH91IfpZVANVyUOhSIPZaFoJGxTFbZhFTx+dXZU=
golang.org/x/exp v0.0.0-20230206171751-46f607a40771 h1:xP7rWLUr1e1n2xkK5YB4LI0hPEy3LJC6Wk+D4pGlOJg= golang.org/x/exp v0.0.0-20230307190834-24139beb5833 h1:SChBja7BCQewoTAU7IgvucQKMIXrEpFxNMs0spT3/5s=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4= golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.7.0 h1:LapD9S96VoQRhi/GrNTqeBJFrUjs5UHCAtTlgwA5oZA= golang.org/x/mod v0.9.0 h1:KENHtAZL2y3NLMYZeHY9DW8HW8V+kQyJsY/V9JlKvCs=
golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190404232315-eb5bcb51f2a3/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s= golang.org/x/net v0.0.0-20190620200207-3b0461eec859/go.mod h1:z5CRVTTTmAJ677TzLLGU+0bjPO0LkuOLi4/5GtJWs/s=
golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU= golang.org/x/net v0.0.0-20201021035429-f5854403a974/go.mod h1:sp8m0HH+o8qH0wwXwYZr8TS3Oi6o0r6Gce1SSxlDquU=
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.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c= golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.7.0 h1:rJrUqqhjsgNp7KqAIc25s9pZnjU7TUcSY7HcVZjdn1g= golang.org/x/net v0.8.0 h1:Zrh2ngAOFYneWTAIAPethzeaQLuHwhuBkuV6ZiRnUaQ=
golang.org/x/net v0.7.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs= golang.org/x/net v0.8.0/go.mod h1:QVkue5JL9kW//ek3r6jTKnTFis1tRmNAW2P1shuFdJc=
golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20190423024810-112230192c58/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
@@ -210,9 +211,9 @@ golang.org/x/sys v0.0.0-20220704084225-05e143d24a9e/go.mod h1:oPkhp1MJrh7nUepCBc
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.4.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0 h1:MUK/U/4lj1t1oPg0HfuXDN/Z1wv31ZJ/YcPiGccS4DU=
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 h1:MVltZSvRTcU2ljQOhs94SXPftV6DCNnZViHeQps87pQ=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
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/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8= golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -220,26 +221,26 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
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.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ= golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.4.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.7.0 h1:4BRB4x83lYWy72KwLD/qYDuTu7q9PjSagHvijDw7cLo= golang.org/x/text v0.8.0 h1:57P1ETyNKtuIjB4SRd15iJxuhj8Gc416Y78H3qgMh68=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8= golang.org/x/text v0.8.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4= golang.org/x/time v0.3.0 h1:rg5rLMjNzMS1RkNLzCG38eapWhnYLFYXDXj2gOlr8j4=
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.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo= golang.org/x/tools v0.0.0-20191119224855-298f0cb1881e/go.mod h1:b+2E5dAYhXwXZwtnZ6UAqBI28+e2cm9otk0dWdXHAEo=
golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA= golang.org/x/tools v0.0.0-20201022035929-9cf592e881e9/go.mod h1:emZCQorbCU4vsT4fOWvOPXz4eW1wZW4PmDk9uLelYpA=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc= golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.5.0 h1:+bSpV5HIeWkuvgaMfI3UmKRThoTA5ODJTUd8T17NO+4= golang.org/x/tools v0.7.0 h1:W4OVu8VVOaIO0yzWMNdepAulS7YfoS3Zabrm8DOXXU4=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20191011141410-1b5146add898/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
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=
golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20200804184101-5ec99f83aff1/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923 h1:znp6mq/drrY+6khTAlJUDNFFcDGV2ENLYKpMq8SyCds= google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4 h1:DdoeryqhaXp1LtT/emMP1BRJPHHKFi5akj/nbx/zNTA=
google.golang.org/genproto v0.0.0-20230223222841-637eb2293923/go.mod h1:3Dl5ZL0q0isWJt+FVcfpQyirqemEuLAK/iFvg1UP1Hw= google.golang.org/genproto v0.0.0-20230306155012-7f2fa6fef1f4/go.mod h1:NWraEVixdDnqcqQ30jipen1STv2r/n24Wb7twVTGR4s=
google.golang.org/grpc v1.53.0 h1:LAv2ds7cmFV/XTS3XG1NneeENYrXGmorPxsBbptIjNc= google.golang.org/grpc v1.54.0 h1:EhTqbhiYeixwWQtAEZAxmV9MGqcjEU2mFx52xCzNyag=
google.golang.org/grpc v1.53.0/go.mod h1:OnIrk0ipVdj4N5d9IUoFUx72/VlD7+jUsHwZgwSMQpw= google.golang.org/grpc v1.54.0/go.mod h1:PUSEXI6iWghWaB6lXM4knEgpJNu2qUcKfDtNci3EC2g=
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.1 h1:d0NfwRgPtno5B1Wa6L2DAG+KivqkdutMf1UhdNx175w= google.golang.org/protobuf v1.29.1 h1:7QBf+IK2gx70Ap/hDsOmam3GE0v9HicjfEdAxE62UoM=
google.golang.org/protobuf v1.28.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.29.1/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
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-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI= gopkg.in/yaml.v2 v2.2.2/go.mod h1:hI93XBmqTisBFMUTm0b8Fm+jr3Dg1NNxqwp+5A1VGuI=
@@ -252,8 +253,8 @@ gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc= gorm.io/driver/sqlite v1.4.4 h1:gIufGoR0dQzjkyqDyYSCvsYR6fba1Gw5YKDqKeChxFc=
gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI= gorm.io/driver/sqlite v1.4.4/go.mod h1:0Aq3iPO+v9ZKbcdiz8gLWRw5VOPcBOPUQJFLq5e2ecI=
gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= gorm.io/gorm v1.24.0/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA=
gorm.io/gorm v1.24.5 h1:g6OPREKqqlWq4kh/3MCQbZKImeB9e6Xgc4zD+JgNZGE= gorm.io/gorm v1.24.6 h1:wy98aq9oFEetsc4CAbKD2SoBCdMzsbSIvSUUFJuHi5s=
gorm.io/gorm v1.24.5/go.mod h1:DVrVomtaYTbqs7gB/x2uVvqnXzv0nqjB396B8cG4dBA= gorm.io/gorm v1.24.6/go.mod h1:L4uxeKpfBml98NYqVqwAdmV1a2nBtAec/cf3fpucW/k=
gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4= gvisor.dev/gvisor v0.0.0-20220901235040-6ca97ef2ce1c h1:m5lcgWnL3OElQNVyp3qcncItJ2c0sQlSGjYK2+nJTA4=
lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0= lukechampine.com/blake3 v1.1.7 h1:GgRMhmdsuK8+ii6UZFDL8Nb+VyMwadAgcJyfYHxG6n0=
rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4= rsc.io/pdf v0.1.1/go.mod h1:n8OzWcQ6Sp37PL01nO98y4iUCRdTGarVfzxY20ICaU4=

View File

@@ -10,24 +10,18 @@ cur_dir=$(pwd)
# check root # check root
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error${plain} Please run this script with root privilege \n " && exit 1 [[ $EUID -ne 0 ]] && echo -e "${red}Fatal error${plain} Please run this script with root privilege \n " && exit 1
# check os # Check OS and set release variable
if [[ -f /etc/redhat-release ]]; then if [[ -f /etc/os-release ]]; then
release="centos" source /etc/os-release
elif cat /etc/issue | grep -Eqi "debian"; then release=$ID
release="debian" elif [[ -f /usr/lib/os-release ]]; then
elif cat /etc/issue | grep -Eqi "ubuntu"; then source /usr/lib/os-release
release="ubuntu" release=$ID
elif cat /etc/issue | grep -Eqi "centos|red hat|redhat"; then
release="centos"
elif cat /proc/version | grep -Eqi "debian"; then
release="debian"
elif cat /proc/version | grep -Eqi "ubuntu"; then
release="ubuntu"
elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then
release="centos"
else else
echo -e "${red} Check system OS failed, please contact the author! ${plain}\n" && exit 1 echo "Failed to check the system OS, please contact the author!" >&2
exit 1
fi fi
echo "The OS release is: $release"
arch=$(arch) arch=$(arch)
@@ -48,37 +42,38 @@ if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then
fi fi
os_version="" os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
# os version if [[ "${release}" == "centos" ]]; then
if [[ -f /etc/os-release ]]; then if [[ ${os_version} -lt 8 ]]; then
os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release)
fi
if [[ -z "$os_version" && -f /etc/lsb-release ]]; then
os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release)
fi
if [[ x"${release}" == x"centos" ]]; then
if [[ ${os_version} -le 7 ]]; then
echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1 echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi fi
elif [[ x"${release}" == x"ubuntu" ]]; then elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then if [[ ${os_version} -lt 20 ]]; then
echo -e "${red} Please use Ubuntu 20 or higher ${plain}\n" && exit 1 echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1
fi fi
elif [[ x"${release}" == x"debian" ]]; then
if [[ ${os_version} -lt 9 ]]; then elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then
echo -e "${red}please use Fedora 36 or higher version${plain}\n" && exit 1
fi
elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 10 ]]; then
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1 echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
fi fi
else
echo -e "${red}Failed to check the OS version, please contact the author!${plain}" && exit 1
fi fi
install_base() { install_base() {
if [[ x"${release}" == x"centos" ]]; then if [[ "${release}" == "centos" ]]; then
yum install wget curl tar -y yum install wget curl tar -y
else else
apt install wget curl tar -y apt install wget curl tar -y
fi fi
} }
#This function will be called when user installed x-ui out of sercurity #This function will be called when user installed x-ui out of sercurity
config_after_install() { config_after_install() {
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}" echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
@@ -103,9 +98,8 @@ config_after_install() {
/usr/local/x-ui/x-ui setting -username ${usernameTemp} -password ${passwordTemp} /usr/local/x-ui/x-ui setting -username ${usernameTemp} -password ${passwordTemp}
echo -e "this is a fresh installation,will generate random login info for security concerns:" echo -e "this is a fresh installation,will generate random login info for security concerns:"
echo -e "###############################################" echo -e "###############################################"
echo -e "${green}user name:${usernameTemp}${plain}" echo -e "${green}username:${usernameTemp}${plain}"
echo -e "${green}user password:${passwordTemp}${plain}" echo -e "${green}password:${passwordTemp}${plain}"
echo -e "${red}web port:${portTemp}${plain}"
echo -e "###############################################" echo -e "###############################################"
echo -e "${red}if you forgot your login info,you can type x-ui and then type 7 to check after installation${plain}" echo -e "${red}if you forgot your login info,you can type x-ui and then type 7 to check after installation${plain}"
else else
@@ -119,20 +113,20 @@ install_x-ui() {
cd /usr/local/ cd /usr/local/
if [ $# == 0 ]; then if [ $# == 0 ]; then
last_version=$(curl -Ls "https://api.github.com/repos/mhsanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') last_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
if [[ ! -n "$last_version" ]]; then if [[ ! -n "$last_version" ]]; then
echo -e "${red}Failed to fetch x-ui version, it maybe due to Github API restrictions, please try it later${plain}" echo -e "${red}Failed to fetch x-ui version, it maybe due to Github API restrictions, please try it later${plain}"
exit 1 exit 1
fi fi
echo -e "Got x-ui latest version: ${last_version}, beginning the installation..." echo -e "Got x-ui latest version: ${last_version}, beginning the installation..."
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/mhsanaei/3x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
echo -e "${red}Downloading x-ui failed, please be sure that your server can access Github ${plain}" echo -e "${red}Downloading x-ui failed, please be sure that your server can access Github ${plain}"
exit 1 exit 1
fi fi
else else
last_version=$1 last_version=$1
url="https://github.com/mhsanaei/3x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz" url="https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
echo -e "Begining to install x-ui $1" echo -e "Begining to install x-ui $1"
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url} wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url}
if [[ $? -ne 0 ]]; then if [[ $? -ne 0 ]]; then
@@ -150,7 +144,7 @@ install_x-ui() {
cd x-ui cd x-ui
chmod +x x-ui bin/xray-linux-${arch} chmod +x x-ui bin/xray-linux-${arch}
cp -f x-ui.service /etc/systemd/system/ cp -f x-ui.service /etc/systemd/system/
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/mhsanaei/3x-ui/main/x-ui.sh wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh
chmod +x /usr/local/x-ui/x-ui.sh chmod +x /usr/local/x-ui/x-ui.sh
chmod +x /usr/bin/x-ui chmod +x /usr/bin/x-ui
config_after_install config_after_install

10
main.go
View File

@@ -136,7 +136,7 @@ func updateTgbotEnableSts(status bool) {
return return
} }
func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string) { func updateTgbotSetting(tgBotToken string, tgBotChatid string, tgBotRuntime string) {
err := database.InitDB(config.GetDBPath()) err := database.InitDB(config.GetDBPath())
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@@ -165,7 +165,7 @@ func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string)
} }
} }
if tgBotChatid != 0 { if tgBotChatid != "" {
err := settingService.SetTgBotChatId(tgBotChatid) err := settingService.SetTgBotChatId(tgBotChatid)
if err != nil { if err != nil {
fmt.Println(err) fmt.Println(err)
@@ -224,7 +224,7 @@ func main() {
var username string var username string
var password string var password string
var tgbottoken string var tgbottoken string
var tgbotchatid int var tgbotchatid string
var enabletgbot bool var enabletgbot bool
var tgbotRuntime string var tgbotRuntime string
var reset bool var reset bool
@@ -236,7 +236,7 @@ func main() {
settingCmd.StringVar(&password, "password", "", "set login password") settingCmd.StringVar(&password, "password", "", "set login password")
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token") settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time") settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id") settingCmd.StringVar(&tgbotchatid, "tgbotchatid", "", "set telegrame bot chat id")
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify") settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
oldUsage := flag.Usage oldUsage := flag.Usage
@@ -287,7 +287,7 @@ func main() {
if show { if show {
showSetting(show) showSetting(show)
} }
if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") { if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime) updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
} }
default: default:

Binary file not shown.

Before

Width:  |  Height:  |  Size: 42 KiB

After

Width:  |  Height:  |  Size: 44 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 44 KiB

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 46 KiB

After

Width:  |  Height:  |  Size: 37 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 34 KiB

After

Width:  |  Height:  |  Size: 42 KiB

View File

@@ -1,3 +1,4 @@
//go:build darwin
// +build darwin // +build darwin
package sys package sys

View File

@@ -1,3 +1,4 @@
//go:build linux
// +build linux // +build linux
package sys package sys

File diff suppressed because one or more lines are too long

View File

@@ -150,3 +150,190 @@
color:rgb(255, 255, 255) !important; color:rgb(255, 255, 255) !important;
background-color: rgb(255, 127, 127); background-color: rgb(255, 127, 127);
} }
.ant-table-tbody>tr>td,
.ant-table-thead>tr>th{
padding:16px;
}
.ant-menu-dark,
.ant-menu-dark .ant-menu-sub,
.ant-layout-header,
.ant-layout-sider-dark,
.ant-layout-sider-zero-width-trigger,
.ant-dropdown-menu-dark,.ant-dropdown-menu-dark .ant-dropdown-menu,
.ant-menu-dark.ant-menu-horizontal>.ant-menu-item,.ant-menu-dark.ant-menu-horizontal>.ant-menu-submenu {
background:#161b22
}
.ant-card-dark {
color: hsla(0,0%,100%,.65);
background-color: #1a212a;
border-color:rgba(0,0,0,.09);
}
.ant-card-dark:hover {
border-color: #e8e8e8;
}
.ant-card-dark .ant-table-thead th {
color: hsla(0,0%,100%,.65);
background-color: #161b22;
}
.ant-card-dark .ant-table-tbody tr td,
.ant-card-dark .ant-modal-title {
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-collapse-content,
.ant-card-dark .ant-calendar,
.ant-card-dark .ant-table-placeholder,
.ant-card-dark .ant-input-group-addon {
color: hsla(0,0%,100%,.65);
background-color: #262f3d;
}
.ant-card-dark .ant-list-item-meta-title,
.ant-card-dark .ant-list-item-meta-description,
.ant-card-dark .ant-form-item-label>label,
.ant-card-dark .ant-form-item,
.ant-card-dark .ant-divider-inner-text,
.ant-card-dark .ant-modal-confirm-content,
.ant-card-dark .ant-modal-confirm-title,
.ant-card-dark .ant-progress-text,
.ant-card-dark .ant-modal-close,
.ant-card-dark i,
.ant-card-dark .ant-select-dropdown-menu-item,
.ant-card-dark .ant-calendar-month-select,
.ant-card-dark .ant-calendar-year-select,
.ant-card-dark .ant-calendar-date,
.ant-card-dark .ant-collapse>.ant-collapse-item>.ant-collapse-header,
.ant-card-dark .ant-empty-normal,
.ant-card-dark .ant-checkbox+span {
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-table-tbody>tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)>td,
.ant-card-dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled),
.ant-card-dark .ant-calendar-date:hover {
background-color: #004488;
}
.ant-card-dark tbody .ant-table-expanded-row {
color: hsla(0,0%,100%,.65);
background-color: #1a212a;
}
.ant-card-dark .ant-input,
.ant-card-dark .ant-input-number,
.ant-card-dark .ant-calendar-input,
.ant-card-dark .ant-select-dropdown-menu-item-selected,
.ant-card-dark .ant-select-selection {
color: hsla(0,0%,100%,.65);
background-color: #2e3b52;
}
.ant-card-dark .ant-collapse-item {
color: hsla(0,0%,100%,.65);
background-color: #161b22;
}
.ant-card-dark .ant-modal-content,
.ant-card-dark .ant-modal-body,
.ant-card-dark .ant-modal-header,
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
color: hsla(0,0%,100%,.65);
background-color: #222a37;
}
.client-table-header {
background-color: #f0f2f5;
}
.client-table-odd-row {
background-color: #fafafa;
}
.ant-card-dark .client-table-header {
background-color: #1a212a;
color: hsla(0,0%,100%,.65);
}
.ant-card-dark .client-table-odd-row {
color: hsla(0,0%,100%,.65);
background-color: #242c3a;
}
.ant-card-dark .ant-calendar-last-month-cell .ant-calendar-date,
.ant-card-dark .ant-calendar-next-month-btn-day .ant-calendar-date {
color: hsla(0,0%,100%,.30);
}
.ant-drawer-dark {
color: hsla(0,0%,100%,.65);
}
.ant-drawer-dark .ant-drawer-wrapper-body,
.ant-drawer-dark .drawer-handle {
background-color: #1a212a;
border: 1px solid hsla(0,0%,100%,.30);
}
.ant-card-dark .ant-tag-blue {
color: #3c9ae8;
background: #111d2c;
border-color: #15395b;
}
.ant-card-dark .ant-tag-green {
color: #6abe39;
background: #162312;
border-color: #274916;
}
.ant-card-dark .ant-tag-cyan {
color: #33bcb7;
background: #112123;
border-color: #144848;
}
.ant-card-dark .ant-tag-red {
color: #e84749;
background: #2a1215;
border-color: #58181c;
}
.ant-card-dark .ant-tag-orange {
color: #e89a3c;
background: #2b1d11;
border-color: #593815;
}
.ant-card-dark .ant-table-row-expand-icon,
.ant-card-dark .ant-checkbox-inner {
background: none;
}
.ant-card-dark .ant-switch-checked {
background-color: #0c61b0;
}
.ant-card-dark .ant-btn,
.ant-card-dark .ant-radio-button-wrapper {
color: hsla(0,0%,100%,.65);
background: none;
border: 1px solid hsla(0,0%,100%,.65);
}
.ant-card-dark .ant-radio-button-wrapper:hover {
color: #177ddc;
}
.ant-card-dark .ant-btn-primary {
color: hsla(0,0%,100%,.65);
background-color: #073763;
border-color: #1890ff;
text-shadow: 0 -1px 0 rgba(0,0,0,.12);
box-shadow: 0 2px 0 rgba(0,0,0,.045);
}

View File

@@ -4,7 +4,7 @@ supportLangs = [
value : "en-US", value : "en-US",
icon : "🇺🇸" icon : "🇺🇸"
}, },
{ {
name : "Farsi", name : "Farsi",
value : "fa_IR", value : "fa_IR",
icon : "🇮🇷" icon : "🇮🇷"

View File

@@ -36,7 +36,8 @@ class DBInbound {
this.remark = ""; this.remark = "";
this.enable = true; this.enable = true;
this.expiryTime = 0; this.expiryTime = 0;
this.iplimit = 0; this.limitIp = 0;
this.listen = ""; this.listen = "";
this.port = 0; this.port = 0;
this.protocol = ""; this.protocol = "";
@@ -109,10 +110,6 @@ class DBInbound {
get isExpiry() { get isExpiry() {
return this.expiryTime < new Date().getTime(); return this.expiryTime < new Date().getTime();
} }
get isDBInboundEmpty() {
const inbound = this.toInbound();
return inbound.isInboundEmpty();
}
toInbound() { toInbound() {
let settings = {}; let settings = {};
@@ -155,10 +152,11 @@ class DBInbound {
} }
} }
genLink(clientIndex = 0) { genLink(clientIndex) {
const inbound = this.toInbound(); const inbound = this.toInbound();
return inbound.genLink(this.address, this.remark, clientIndex); return inbound.genLink(this.address, this.remark, clientIndex);
} }
get genInboundLinks() { get genInboundLinks() {
const inbound = this.toInbound(); const inbound = this.toInbound();
return inbound.genInboundLinks(this.address, this.remark); return inbound.genInboundLinks(this.address, this.remark);
@@ -175,8 +173,12 @@ class AllSetting {
this.webBasePath = "/"; this.webBasePath = "/";
this.tgBotEnable = false; this.tgBotEnable = false;
this.tgBotToken = ""; this.tgBotToken = "";
this.tgBotChatId = 0; this.tgBotChatId = "";
this.tgRunTime = ""; this.tgRunTime = "@daily";
this.tgBotBackup = false;
this.tgExpireDiff = "";
this.tgTrafficDiff = "";
this.tgCpu = "";
this.xrayTemplateConfig = ""; this.xrayTemplateConfig = "";
this.timeLocation = "Asia/Tehran"; this.timeLocation = "Asia/Tehran";

View File

@@ -57,7 +57,7 @@ const TLS_VERSION_OPTION = {
TLS11: "1.1", TLS11: "1.1",
TLS12: "1.2", TLS12: "1.2",
TLS13: "1.3", TLS13: "1.3",
} };
const TLS_CIPHER_OPTION = { const TLS_CIPHER_OPTION = {
RSA_AES_128_CBC: "TLS_RSA_WITH_AES_128_CBC_SHA", RSA_AES_128_CBC: "TLS_RSA_WITH_AES_128_CBC_SHA",
@@ -92,6 +92,11 @@ const UTLS_FINGERPRINT = {
UTLS_RANDOMIZED: "randomized", UTLS_RANDOMIZED: "randomized",
}; };
const ALPN_OPTION = {
H2: "h2",
HTTP1: "http/1.1",
};
Object.freeze(Protocols); Object.freeze(Protocols);
Object.freeze(VmessMethods); Object.freeze(VmessMethods);
Object.freeze(SSMethods); Object.freeze(SSMethods);
@@ -102,6 +107,7 @@ Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(TLS_VERSION_OPTION); Object.freeze(TLS_VERSION_OPTION);
Object.freeze(TLS_CIPHER_OPTION); Object.freeze(TLS_CIPHER_OPTION);
Object.freeze(UTLS_FINGERPRINT); Object.freeze(UTLS_FINGERPRINT);
Object.freeze(ALPN_OPTION);
class XrayCommonClass { class XrayCommonClass {
@@ -469,9 +475,9 @@ class GrpcStreamSettings extends XrayCommonClass {
} }
class TlsStreamSettings extends XrayCommonClass { class TlsStreamSettings extends XrayCommonClass {
constructor(serverName = '', minVersion = TLS_VERSION_OPTION.TLS12, maxVersion = TLS_VERSION_OPTION.TLS13, constructor(serverName = '', minVersion = TLS_VERSION_OPTION.TLS10, maxVersion = TLS_VERSION_OPTION.TLS12,
cipherSuites = '', cipherSuites = '',
certificates = [new TlsStreamSettings.Cert()], alpn = ["h2", "http/1.1"]) { certificates = [new TlsStreamSettings.Cert()], alpn=[''] ,settings=[new TlsStreamSettings.Settings()]) {
super(); super();
this.server = serverName; this.server = serverName;
this.minVersion = minVersion; this.minVersion = minVersion;
@@ -479,6 +485,7 @@ class TlsStreamSettings extends XrayCommonClass {
this.cipherSuites = cipherSuites; this.cipherSuites = cipherSuites;
this.certs = certificates; this.certs = certificates;
this.alpn = alpn; this.alpn = alpn;
this.settings = settings;
} }
addCert(cert) { addCert(cert) {
@@ -491,9 +498,14 @@ class TlsStreamSettings extends XrayCommonClass {
static fromJson(json={}) { static fromJson(json={}) {
let certs; let certs;
let settings;
if (!ObjectUtil.isEmpty(json.certificates)) { if (!ObjectUtil.isEmpty(json.certificates)) {
certs = json.certificates.map(cert => TlsStreamSettings.Cert.fromJson(cert)); certs = json.certificates.map(cert => TlsStreamSettings.Cert.fromJson(cert));
} }
if (!ObjectUtil.isEmpty(json.settings)) {
let values = json.settings[0];
settings = [new TlsStreamSettings.Settings(values.allowInsecure , values.fingerprint, values.serverName)];
}
return new TlsStreamSettings( return new TlsStreamSettings(
json.serverName, json.serverName,
@@ -502,6 +514,7 @@ class TlsStreamSettings extends XrayCommonClass {
json.cipherSuites, json.cipherSuites,
certs, certs,
json.alpn, json.alpn,
settings,
); );
} }
@@ -512,7 +525,9 @@ class TlsStreamSettings extends XrayCommonClass {
maxVersion: this.maxVersion, maxVersion: this.maxVersion,
cipherSuites: this.cipherSuites, cipherSuites: this.cipherSuites,
certificates: TlsStreamSettings.toJsonArray(this.certs), certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn alpn: this.alpn,
settings: TlsStreamSettings.toJsonArray(this.settings),
}; };
} }
} }
@@ -558,6 +573,30 @@ TlsStreamSettings.Cert = class extends XrayCommonClass {
} }
}; };
TlsStreamSettings.Settings = class extends XrayCommonClass {
constructor(allowInsecure = false, fingerprint = '', serverName = '') {
super();
this.allowInsecure = allowInsecure;
this.fingerprint = fingerprint;
this.serverName = serverName;
}
static fromJson(json = {}) {
return new TlsStreamSettings.Settings(
json.allowInsecure,
json.fingerprint,
json.servername,
);
}
toJson() {
return {
allowInsecure: this.allowInsecure,
fingerprint: this.fingerprint,
serverName: this.serverName,
};
}
};
class StreamSettings extends XrayCommonClass { class StreamSettings extends XrayCommonClass {
constructor(network='tcp', constructor(network='tcp',
security='none', security='none',
@@ -593,12 +632,12 @@ class StreamSettings extends XrayCommonClass {
} }
} }
get isXTls() { get isXTLS() {
return this.security === "xtls"; return this.security === "xtls";
} }
set isXTls(isXTls) { set isXTLS(isXTLS) {
if (isXTls) { if (isXTLS) {
this.security = 'xtls'; this.security = 'xtls';
} else { } else {
this.security = 'none'; this.security = 'none';
@@ -608,7 +647,7 @@ class StreamSettings extends XrayCommonClass {
static fromJson(json={}) { static fromJson(json={}) {
let tls; let tls;
if (json.security === "xtls") { if (json.security === "xtls") {
tls = TlsStreamSettings.fromJson(json.xtlsSettings); tls = TlsStreamSettings.fromJson(json.XTLSSettings);
} else { } else {
tls = TlsStreamSettings.fromJson(json.tlsSettings); tls = TlsStreamSettings.fromJson(json.tlsSettings);
} }
@@ -631,7 +670,7 @@ class StreamSettings extends XrayCommonClass {
network: network, network: network,
security: this.security, security: this.security,
tlsSettings: this.isTls ? this.tls.toJson() : undefined, tlsSettings: this.isTls ? this.tls.toJson() : undefined,
xtlsSettings: this.isXTls ? this.tls.toJson() : undefined, XTLSSettings: this.isXTLS ? this.tls.toJson() : undefined,
tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined, tcpSettings: network === 'tcp' ? this.tcp.toJson() : undefined,
kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined, kcpSettings: network === 'kcp' ? this.kcp.toJson() : undefined,
wsSettings: network === 'ws' ? this.ws.toJson() : undefined, wsSettings: network === 'ws' ? this.ws.toJson() : undefined,
@@ -666,7 +705,7 @@ class Sniffing extends XrayCommonClass {
class Inbound extends XrayCommonClass { class Inbound extends XrayCommonClass {
constructor(port=RandomUtil.randomIntRange(10000, 60000), constructor(port=RandomUtil.randomIntRange(10000, 60000),
listen='', listen='',
protocol=Protocols.VMESS, protocol=Protocols.VLESS,
settings=null, settings=null,
streamSettings=new StreamSettings(), streamSettings=new StreamSettings(),
tag='', tag='',
@@ -710,12 +749,12 @@ class Inbound extends XrayCommonClass {
} }
} }
get xtls() { get XTLS() {
return this.stream.security === 'xtls'; return this.stream.security === 'xtls';
} }
set xtls(isXTls) { set XTLS(isXTLS) {
if (isXTls) { if (isXTLS) {
this.stream.security = 'xtls'; this.stream.security = 'xtls';
} else { } else {
this.stream.security = 'none'; this.stream.security = 'none';
@@ -754,18 +793,6 @@ class Inbound extends XrayCommonClass {
return this.network === "http"; return this.network === "http";
} }
isInboundEmpty() {
if (this.protocol == Protocols.VMESS && this.settings.vmesses.length == 0) {
return true;
} else if (this.protocol == Protocols.VLESS && this.settings.vlesses.length == 0) {
return true;
} else if (this.protocol == Protocols.TROJAN && this.settings.trojans.length == 0) {
return true;
} else {
return false;
}
}
// VMess & VLess // VMess & VLess
get uuid() { get uuid() {
switch (this.protocol) { switch (this.protocol) {
@@ -837,7 +864,7 @@ class Inbound extends XrayCommonClass {
} }
get serverName() { get serverName() {
if (this.stream.isTls || this.stream.isXTls) { if (this.stream.isTls || this.stream.isXTLS) {
return this.stream.tls.server; return this.stream.tls.server;
} }
return ""; return "";
@@ -931,7 +958,7 @@ class Inbound extends XrayCommonClass {
} }
} }
//this is used for xtls-rprx-vison //this is used for xtls-rprx-vision
canEnableTlsFlow() { canEnableTlsFlow() {
if ((this.stream.security === 'tls') && (this.network === "tcp")) { if ((this.stream.security === 'tls') && (this.network === "tcp")) {
switch (this.protocol) { switch (this.protocol) {
@@ -947,8 +974,9 @@ class Inbound extends XrayCommonClass {
canSetTls() { canSetTls() {
return this.canEnableTls(); return this.canEnableTls();
} }
canEnableXTls() { canEnableXTLS() {
switch (this.protocol) { switch (this.protocol) {
case Protocols.VLESS: case Protocols.VLESS:
case Protocols.TROJAN: case Protocols.TROJAN:
@@ -1053,6 +1081,10 @@ class Inbound extends XrayCommonClass {
host: host, host: host,
path: path, path: path,
tls: this.stream.security, tls: this.stream.security,
sni: this.stream.tls.settings[0]['serverName'],
fp: this.stream.tls.settings[0]['fingerprint'],
alpn: this.stream.tls.alpn.join(','),
allowInsecure: this.stream.tls.settings[0].allowInsecure,
}; };
return 'vmess://' + base64(JSON.stringify(obj, null, 2)); return 'vmess://' + base64(JSON.stringify(obj, null, 2));
} }
@@ -1064,11 +1096,6 @@ class Inbound extends XrayCommonClass {
const type = this.stream.network; const type = this.stream.network;
const params = new Map(); const params = new Map();
params.set("type", this.stream.network); params.set("type", this.stream.network);
if (this.xtls) {
params.set("security", "xtls");
} else {
params.set("security", this.stream.security);
}
switch (type) { switch (type) {
case "tcp": case "tcp":
const tcp = this.stream.tcp; const tcp = this.stream.tcp;
@@ -1114,20 +1141,35 @@ class Inbound extends XrayCommonClass {
break; break;
} }
if (this.stream.security === 'tls') { if (this.tls) {
params.set("security", "tls");
params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings[0].allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) { if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server; address = this.stream.tls.server;
params.set("sni", address); }
if (this.stream.tls.settings[0]['serverName'] !== ''){
params.set("sni", this.stream.tls.settings[0]['serverName']);
} }
if (this.settings.vlesses[clientIndex].flow === "xtls-rprx-vision") { if (type === "tcp" && this.settings.vlesses[clientIndex].flow.length > 0) {
params.set("flow", this.settings.vlesses[clientIndex].flow); params.set("flow", this.settings.vlesses[clientIndex].flow);
} }
params.set("fp", this.settings.vlesses[clientIndex].fingerprint);
}
if (this.xtls) {
params.set("flow", this.settings.vlesses[clientIndex].flow);
} }
if (this.XTLS) {
params.set("security", "tls");
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings[0].allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
}
params.set("flow", this.settings.vlesses[clientIndex].flow);
}
const link = `vless://${uuid}@${address}:${port}`; const link = `vless://${uuid}@${address}:${port}`;
const url = new URL(link); const url = new URL(link);
@@ -1138,7 +1180,7 @@ class Inbound extends XrayCommonClass {
return url.toString(); return url.toString();
} }
genSSLink(address = '', remark = '',clientIndex) { genSSLink(address = '', remark = '') {
let settings = this.settings; let settings = this.settings;
const server = this.stream.tls.server; const server = this.stream.tls.server;
if (!ObjectUtil.isEmpty(server)) { if (!ObjectUtil.isEmpty(server)) {
@@ -1157,12 +1199,7 @@ class Inbound extends XrayCommonClass {
const port = this.port; const port = this.port;
const type = this.stream.network; const type = this.stream.network;
const params = new Map(); const params = new Map();
params.set("type", this.stream.network); params.set("type", this.stream.network);
if (this.xtls) {
params.set("security", "xtls");
} else {
params.set("security", this.stream.security);
}
switch (type) { switch (type) {
case "tcp": case "tcp":
const tcp = this.stream.tcp; const tcp = this.stream.tcp;
@@ -1208,16 +1245,33 @@ class Inbound extends XrayCommonClass {
break; break;
} }
if (this.stream.security === 'tls') { if (this.tls) {
params.set("security", "tls");
params.set("fp" , this.stream.tls.settings[0]['fingerprint']);
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings[0].allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) { if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server; address = this.stream.tls.server;
params.set("sni", address); }
if (this.stream.tls.settings[0]['serverName'] !== ''){
params.set("sni", this.stream.tls.settings[0]['serverName']);
} }
params.set("flow", this.settings.trojans[clientIndex].flow);
}
if (this.xtls) {
params.set("flow", this.settings.trojans[clientIndex].flow);
} }
if (this.XTLS) {
params.set("security", "tls");
params.set("alpn", this.stream.tls.alpn);
if(this.stream.tls.settings[0].allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
}
params.set("flow", this.settings.trojans[clientIndex].flow);
}
const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`; const link = `trojan://${settings.trojans[clientIndex].password}@${address}:${this.port}#${encodeURIComponent(remark)}`;
const url = new URL(link); const url = new URL(link);
for (const [key, value] of params) { for (const [key, value] of params) {
@@ -1250,19 +1304,18 @@ class Inbound extends XrayCommonClass {
} }
genInboundLinks(address = '', remark = '') { genInboundLinks(address = '', remark = '') {
let link = ''; let link = '';
JSON.parse(this.settings) switch (this.protocol) {
switch (this.protocol) { case Protocols.VMESS:
case Protocols.VMESS: case Protocols.VLESS:
case Protocols.VLESS: case Protocols.TROJAN:
case Protocols.TROJAN: JSON.parse(this.settings).clients.forEach((_,index) => {
JSON.parse(this.settings).clients.forEach((client,index) => { link += this.genLink(address, remark, index) + '\r\n';
link += this.genLink(address, remark, index) + '\r\n'; });
}); return link;
return link; case Protocols.SHADOWSOCKS:
case Protocols.SHADOWSOCKS: return (this.genSSLink(address, remark) + '\r\n');
return (this.genSSLink(address, remark) + '\r\n'); default: return '';
default: return ''; }
}
} }
static fromJson(json={}) { static fromJson(json={}) {
@@ -1430,7 +1483,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
fallbacks=[],) { fallbacks=[],) {
super(protocol); super(protocol);
this.vlesses = vlesses; this.vlesses = vlesses;
this.decryption = decryption; this.decryption = 'none';
this.fallbacks = fallbacks; this.fallbacks = fallbacks;
} }
@@ -1446,7 +1499,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
return new Inbound.VLESSSettings( return new Inbound.VLESSSettings(
Protocols.VLESS, Protocols.VLESS,
json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)), json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
json.decryption, 'none',
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks), Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks),
); );
} }
@@ -1462,14 +1515,13 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
}; };
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass { Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, fingerprint = UTLS_FINGERPRINT.UTLS_CHROME, expiryTime='') { constructor(id=RandomUtil.randomUUID(), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime='') {
super(); super();
this.id = id; this.id = id;
this.flow = flow; this.flow = flow;
this.email = email; this.email = email;
this.limitIp = limitIp; this.limitIp = limitIp;
this.totalGB = totalGB; this.totalGB = totalGB;
this.fingerprint = fingerprint;
this.expiryTime = expiryTime; this.expiryTime = expiryTime;
} }
@@ -1481,7 +1533,6 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
json.email, json.email,
json.limitIp, json.limitIp,
json.totalGB, json.totalGB,
json.fingerprint,
json.expiryTime, json.expiryTime,
); );
@@ -1580,7 +1631,7 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
} }
}; };
Inbound.TrojanSettings.Trojan = class extends XrayCommonClass { Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
constructor(password=RandomUtil.randomSeq(10), flow ='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime='') { constructor(password=RandomUtil.randomSeq(10), flow='', email=RandomUtil.randomText(),limitIp=0, totalGB=0, expiryTime='') {
super(); super();
this.password = password; this.password = password;
this.flow = flow; this.flow = flow;
@@ -1707,11 +1758,12 @@ Inbound.ShadowsocksSettings = class extends Inbound.Settings {
}; };
Inbound.DokodemoSettings = class extends Inbound.Settings { Inbound.DokodemoSettings = class extends Inbound.Settings {
constructor(protocol, address, port, network='tcp,udp') { constructor(protocol, address, port, network='tcp,udp', followRedirect=false) {
super(protocol); super(protocol);
this.address = address; this.address = address;
this.port = port; this.port = port;
this.network = network; this.network = network;
this.followRedirect = followRedirect;
} }
static fromJson(json={}) { static fromJson(json={}) {
@@ -1720,6 +1772,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
json.address, json.address,
json.port, json.port,
json.network, json.network,
json.followRedirect,
); );
} }
@@ -1728,6 +1781,7 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
address: this.address, address: this.address,
port: this.port, port: this.port,
network: this.network, network: this.network,
followRedirect: this.followRedirect,
}; };
} }
}; };

View File

@@ -136,7 +136,7 @@ class RandomUtil {
return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16); return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16);
}); });
} }
static randomText() { static randomText() {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = ''; var string = '';

View File

@@ -33,7 +33,10 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/update/:id", a.updateInbound) g.POST("/update/:id", a.updateInbound)
g.POST("/clientIps/:email", a.getClientIps) g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps) g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/resetClientTraffic/:email", a.resetClientTraffic) g.POST("/addClient/", a.addInboundClient)
g.POST("/delClient/:email", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
} }
@@ -124,6 +127,7 @@ func (a *InboundController) updateInbound(c *gin.Context) {
a.xrayService.SetToNeedRestart() a.xrayService.SetToNeedRestart()
} }
} }
func (a *InboundController) getClientIps(c *gin.Context) { func (a *InboundController) getClientIps(c *gin.Context) {
email := c.Param("email") email := c.Param("email")
@@ -144,13 +148,85 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
} }
jsonMsg(c, "Log Cleared", nil) jsonMsg(c, "Log Cleared", nil)
} }
func (a *InboundController) addInboundClient(c *gin.Context) {
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
err = a.inboundService.AddInboundClient(inbound)
if err != nil {
jsonMsg(c, "something worng!", err)
return
}
jsonMsg(c, "Client added", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) delInboundClient(c *gin.Context) {
email := c.Param("email")
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
err = a.inboundService.DelInboundClient(inbound, email)
if err != nil {
jsonMsg(c, "something worng!", err)
return
}
jsonMsg(c, "Client deleted", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) updateInboundClient(c *gin.Context) {
index, err := strconv.Atoi(c.Param("index"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
inbound := &model.Inbound{}
err = c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
err = a.inboundService.UpdateInboundClient(inbound, index)
if err != nil {
jsonMsg(c, "something worng!", err)
return
}
jsonMsg(c, "Client updated", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
}
func (a *InboundController) resetClientTraffic(c *gin.Context) { func (a *InboundController) resetClientTraffic(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
email := c.Param("email") email := c.Param("email")
err := a.inboundService.ResetClientTraffic(email) err = a.inboundService.ResetClientTraffic(id, email)
if err != nil { if err != nil {
jsonMsg(c, "something worng!", err) jsonMsg(c, "something worng!", err)
return return
} }
jsonMsg(c, "traffic reseted", nil) jsonMsg(c, "traffic reseted", nil)
if err == nil {
a.xrayService.SetToNeedRestart()
}
} }

View File

@@ -4,7 +4,6 @@ import (
"net/http" "net/http"
"time" "time"
"x-ui/logger" "x-ui/logger"
"x-ui/web/job"
"x-ui/web/service" "x-ui/web/service"
"x-ui/web/session" "x-ui/web/session"
@@ -20,6 +19,7 @@ type IndexController struct {
BaseController BaseController
userService service.UserService userService service.UserService
tgbot service.Tgbot
} }
func NewIndexController(g *gin.RouterGroup) *IndexController { func NewIndexController(g *gin.RouterGroup) *IndexController {
@@ -60,13 +60,13 @@ func (a *IndexController) login(c *gin.Context) {
user := a.userService.CheckUser(form.Username, form.Password) user := a.userService.CheckUser(form.Username, form.Password)
timeStr := time.Now().Format("2006-01-02 15:04:05") timeStr := time.Now().Format("2006-01-02 15:04:05")
if user == nil { if user == nil {
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0) a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password) logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword")) pureJsonMsg(c, false, I18n(c, "pages.login.toasts.wrongUsernameOrPassword"))
return return
} else { } else {
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c)) logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1) a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
} }
err = session.SetLoginUser(c, user) err = session.SetLoginUser(c, user)

View File

@@ -1,10 +1,11 @@
package controller package controller
import ( import (
"github.com/gin-gonic/gin"
"time" "time"
"x-ui/web/global" "x-ui/web/global"
"x-ui/web/service" "x-ui/web/service"
"github.com/gin-gonic/gin"
) )
type ServerController struct { type ServerController struct {
@@ -34,7 +35,10 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
g.Use(a.checkLogin) g.Use(a.checkLogin)
g.POST("/status", a.status) g.POST("/status", a.status)
g.POST("/getXrayVersion", a.getXrayVersion) g.POST("/getXrayVersion", a.getXrayVersion)
g.POST("/stopXrayService", a.stopXrayService)
g.POST("/restartXrayService", a.restartXrayService)
g.POST("/installXray/:version", a.installXray) g.POST("/installXray/:version", a.installXray)
g.POST("/logs", a.getLogs)
} }
func (a *ServerController) refreshStatus() { func (a *ServerController) refreshStatus() {
@@ -83,3 +87,32 @@ func (a *ServerController) installXray(c *gin.Context) {
err := a.serverService.UpdateXray(version) err := a.serverService.UpdateXray(version)
jsonMsg(c, I18n(c, "install")+" xray", err) jsonMsg(c, I18n(c, "install")+" xray", err)
} }
func (a *ServerController) stopXrayService(c *gin.Context) {
a.lastGetStatusTime = time.Now()
err := a.serverService.StopXrayService()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonMsg(c, "Xray stoped", err)
}
func (a *ServerController) restartXrayService(c *gin.Context) {
err := a.serverService.RestartXrayService()
if err != nil {
jsonMsg(c, "", err)
return
}
jsonMsg(c, "Xray restarted", err)
}
func (a *ServerController) getLogs(c *gin.Context) {
logs, err := a.serverService.GetLogs()
if err != nil {
jsonMsg(c, I18n(c, "getLogs"), err)
return
}
jsonObj(c, logs, nil)
}

View File

@@ -2,11 +2,12 @@ package controller
import ( import (
"errors" "errors"
"github.com/gin-gonic/gin"
"time" "time"
"x-ui/web/entity" "x-ui/web/entity"
"x-ui/web/service" "x-ui/web/service"
"x-ui/web/session" "x-ui/web/session"
"github.com/gin-gonic/gin"
) )
type updateUserForm struct { type updateUserForm struct {

View File

@@ -34,11 +34,14 @@ type AllSetting struct {
WebBasePath string `json:"webBasePath" form:"webBasePath"` WebBasePath string `json:"webBasePath" form:"webBasePath"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
TgBotToken string `json:"tgBotToken" form:"tgBotToken"` TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
TgBotChatId int `json:"tgBotChatId" form:"tgBotChatId"` TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"`
TgRunTime string `json:"tgRunTime" form:"tgRunTime"` TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"`
TgExpireDiff int `json:"tgExpireDiff" form:"tgExpireDiff"`
TgTrafficDiff int `json:"tgTrafficDiff" form:"tgTrafficDiff"`
TgCpu int `json:"tgCpu" form:"tgCpu"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"` XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
} }
func (s *AllSetting) CheckValid() error { func (s *AllSetting) CheckValid() error {

View File

@@ -1,6 +1,7 @@
{{define "promptModal"}} {{define "promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title" <a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
:closable="true" @ok="promptModal.ok" :mask-closable="false" :closable="true" @ok="promptModal.ok" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'> :ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}'>
<a-input id="prompt-modal-input" :type="promptModal.type" <a-input id="prompt-modal-input" :type="promptModal.type"
v-model="promptModal.value" v-model="promptModal.value"

View File

@@ -1,8 +1,11 @@
{{define "qrcodeModal"}} {{define "qrcodeModal"}}
<a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title" <a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
:closable="true" width="300px" :ok-text="qrModal.okText" :closable="true"
cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}"> :class="siderDrawer.isDarkTheme ? darkClass : ''"
<canvas id="qrCode" style="width: 100%; height: 100%;"></canvas> :footer="null"
width="300px">
<a-tag color="green" style="margin-bottom: 10px;display: block;text-align: center;" >{{ i18n "pages.inbounds.clickOnQRcode" }}</a-tag>
<canvas @click="copyToClipboard()" id="qrCode" style="width: 100%; height: 100%;"></canvas>
</a-modal> </a-modal>
<script> <script>
@@ -12,17 +15,15 @@
content: '', content: '',
inbound: new Inbound(), inbound: new Inbound(),
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
okText: '',
copyText: '', copyText: '',
qrcode: null, qrcode: null,
clipboard: null, clipboard: null,
visible: false, visible: false,
show: function (title='', content='', dbInbound=new DBInbound(),okText='{{ i18n "copy" }}', copyText='') { show: function (title='', content='', dbInbound=new DBInbound(), copyText='') {
this.title = title; this.title = title;
this.content = content; this.content = content;
this.dbInbound = dbInbound; this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.okText = okText;
if (ObjectUtil.isEmpty(copyText)) { if (ObjectUtil.isEmpty(copyText)) {
this.copyText = content; this.copyText = content;
} else { } else {
@@ -30,12 +31,6 @@
} }
this.visible = true; this.visible = true;
qrModalApp.$nextTick(() => { qrModalApp.$nextTick(() => {
if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
text: () => this.copyText,
});
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
}
if (this.qrcode === null) { if (this.qrcode === null) {
this.qrcode = new QRious({ this.qrcode = new QRious({
element: document.querySelector('#qrCode'), element: document.querySelector('#qrCode'),
@@ -57,6 +52,17 @@
data: { data: {
qrModal: qrModal, qrModal: qrModal,
}, },
methods: {
copyToClipboard() {
this.qrModal.clipboard = new ClipboardJS('#qrCode', {
text: () => this.qrModal.copyText,
});
this.qrModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copied" }}')
this.qrModal.clipboard.destroy();
});
}
},
}); });
</script> </script>

View File

@@ -1,9 +1,10 @@
{{define "textModal"}} {{define "textModal"}}
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title" <a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}' :closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}"> :ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;" <a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
@click="downloader.download(txtModal.fileName, txtModal.content)"> :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :download="txtModal.fileName">
{{ i18n "download" }} [[ txtModal.fileName ]] {{ i18n "download" }} [[ txtModal.fileName ]]
</a-button> </a-button>
<a-input type="textarea" v-model="txtModal.content" <a-input type="textarea" v-model="txtModal.content"
@@ -31,15 +32,6 @@
}); });
this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}')); this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
} }
if (this.qrcode === null) {
this.qrcode = new QRious({
element: document.querySelector('#qrCode'),
size: 260,
value: content,
});
} else {
this.qrcode.value = content;
}
}); });
}, },
close: function () { close: function () {
@@ -48,6 +40,7 @@
}; };
const textModalApp = new Vue({ const textModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#text-modal', el: '#text-modal',
data: { data: {
txtModal: txtModal, txtModal: txtModal,

View File

@@ -39,7 +39,7 @@
<a-layout-content> <a-layout-content>
<a-row type="flex" justify="center"> <a-row type="flex" justify="center">
<a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8"> <a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
<h1>3x-ui {{ i18n "pages.login.title" }}</h1> <h1>{{ i18n "pages.login.title" }}</h1>
</a-col> </a-col>
</a-row> </a-row>
<a-row type="flex" justify="center"> <a-row type="flex" justify="center">

View File

@@ -0,0 +1,162 @@
{{define "clientsBulkModal"}}
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title" @ok="clientsBulkModal.ok"
:confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'>
<a-form layout="inline">
<a-form-item label='{{ i18n "pages.client.method" }}'>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid" style="width: 350px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</a-select-option>
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
<a-select-option :value="4">Random+Prefix+Num@Telegram Username</a-select-option>
<a-select-option :value="5">Prefix+Num+Postfix [ BE CAREFUL! ]</a-select-option>
</a-select>
</a-form-item><br />
<a-form-item v-if="clientsBulkModal.emailMethod>1">
<span slot="label">{{ i18n "pages.client.first" }}</span>
<a-input-number v-model="clientsBulkModal.firstNum" :min="1"></a-input-number>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod>1">
<span slot="label">{{ i18n "pages.client.last" }}</span>
<a-input-number v-model="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod>0">
<span slot="label">{{ i18n "pages.client.prefix" }}</span>
<a-input v-model="clientsBulkModal.emailPrefix" style="width: 120px"></a-input>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod>2">
<span slot="label" v-if="clientsBulkModal.emailMethod == 4">tg_uname</span>
<span slot="label" v-else>{{ i18n "pages.client.postfix" }}</span>
<a-input v-model="clientsBulkModal.emailPostfix" style="width: 120px"></a-input>
</a-form-item>
<a-form-item v-if="clientsBulkModal.emailMethod < 2">
<span slot="label">{{ i18n "pages.client.clientCount" }}</span>
<a-input-number v-model="clientsBulkModal.quantity" :min="1" :max="100"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>
</a-form>
</a-modal>
<script>
const clientsBulkModal = {
visible: false,
confirmLoading: false,
title: '',
okText: '',
confirm: null,
dbInbound: new DBInbound(),
inbound: new Inbound(),
clients: [],
quantity: 1,
totalGB: 0,
expiryTime: '',
emailMethod: 0,
firstNum: 1,
lastNum: 1,
emailPrefix: "",
emailPostfix: "",
ok() {
method=clientsBulkModal.emailMethod;
if(method>1){
start=clientsBulkModal.firstNum;
end=clientsBulkModal.lastNum + 1;
} else {
start=0;
end=clientsBulkModal.quantity;
}
prefix = (method>0 && clientsBulkModal.emailPrefix.length>0) ? clientsBulkModal.emailPrefix : "";
useNum=(method>1);
postfix = (method>2 && clientsBulkModal.emailPostfix.length>0) ? (method == 4 ? "@" : "") + clientsBulkModal.emailPostfix : "";
for (let i = start; i < end; i++) {
newClient = clientsBulkModal.newClient(clientsBulkModal.dbInbound.protocol);
if(method==5) newClient.email = "";
newClient.email += useNum ? prefix + i.toString() + postfix : prefix + postfix;
newClient._totalGB = clientsBulkModal.totalGB;
newClient._expiryTime = clientsBulkModal.expiryTime;
clientsBulkModal.clients.push(newClient);
}
ObjectUtil.execute(clientsBulkModal.confirm, clientsBulkModal.inbound, clientsBulkModal.dbInbound);
},
show({ title='', okText='{{ i18n "sure" }}', dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
this.visible = true;
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.quantity = 1;
this.totalGB = 0;
this.expiryTime = '';
this.emailMethod= 0;
this.firstNum= 1;
this.lastNum= 1;
this.emailPrefix= "";
this.emailPostfix= "";
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
},
getClients(protocol, clientSettings) {
switch(protocol){
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
default: return null;
}
},
newClient(protocol) {
switch (protocol) {
case Protocols.VMESS: return new Inbound.VmessSettings.Vmess();
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
default: return null;
}
},
close() {
clientsBulkModal.visible = false;
clientsBulkModal.loading(false);
},
loading(loading) {
clientsBulkModal.confirmLoading = loading;
},
};
const clientsBulkModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#client-bulk-modal',
data: {
clientsBulkModal,
get inbound() {
return this.clientsBulkModal.inbound;
},
},
});
</script>
{{end}}

View File

@@ -0,0 +1,133 @@
{{define "clientsModal"}}
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/client"}}
</a-modal>
<script>
const clientModal = {
visible: false,
confirmLoading: false,
title: '',
okText: '',
dbInbound: new DBInbound(),
inbound: new Inbound(),
clients: [],
clientStats: [],
index: null,
clientIps: null,
isExpired: false,
ok() {
ObjectUtil.execute(clientModal.confirm, clientModal.inbound, clientModal.dbInbound, clientModal.index);
},
show({ title='', okText='{{ i18n "sure" }}', index=null, dbInbound=null, confirm=(index, dbInbound)=>{}, isEdit=false }) {
this.visible = true;
this.title = title;
this.okText = okText;
this.isEdit = isEdit;
this.dbInbound = new DBInbound(dbInbound);
this.inbound = dbInbound.toInbound();
this.clients = this.getClients(this.inbound.protocol, this.inbound.settings);
this.index = index === null ? this.clients.length : index;
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
if (!isEdit){
this.addClient(this.inbound.protocol, this.clients);
}
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm;
},
getClients(protocol, clientSettings) {
switch(protocol){
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
default: return null;
}
},
addClient(protocol, clients) {
switch (protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.Vmess());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
default: return null;
}
},
close() {
clientModal.visible = false;
clientModal.loading(false);
},
loading(loading) {
clientModal.confirmLoading = loading;
},
};
const clientModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#client-modal',
data: {
clientModal,
get inbound() {
return this.clientModal.inbound;
},
get client() {
return this.clientModal.clients[this.clientModal.index];
},
get clientStats() {
return this.clientModal.clientStats;
},
get isEdit() {
return this.clientModal.isEdit;
},
get isTrafficExhausted() {
if(!clientStats) return false
if(clientStats.total == 0) return false
if(clientStats.up + clientStats.down < clientStats.total) return false
return true
},
get isExpiry() {
return this.clientModal.isExpired
},
get statsColor() {
if(!clientStats) return 'blue'
if(clientStats.total === 0) return 'blue'
else if(clientStats.total > 0 && (clientStats.down+clientStats.up) < clientStats.total) return 'cyan'
else return 'red'
}
},
methods: {
getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = '';
var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
client.email = string;
},
async getDBClientIps(email,event) {
const msg = await HttpUtil.post('/xui/inbound/clientIps/'+ email);
if (!msg.success) {
return;
}
try {
ips = JSON.parse(msg.obj)
ips = ips.join(",")
event.target.value = ips
} catch (error) {
// text
event.target.value = msg.obj
}
},
async clearDBClientIps(email) {
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email);
if (!msg.success) {
return;
}
document.getElementById("clientIPs").value = ""
},
},
});
</script>
{{end}}

View File

@@ -13,18 +13,18 @@
</a-menu-item> </a-menu-item>
<!--<a-menu-item key="{{ .base_path }}xui/clients">--> <!--<a-menu-item key="{{ .base_path }}xui/clients">-->
<!-- <a-icon type="laptop"></a-icon>--> <!-- <a-icon type="laptop"></a-icon>-->
<!-- <span>client</span>--> <!-- <span>Client</span>-->
<!--</a-menu-item>--> <!--</a-menu-item>-->
<a-sub-menu> <a-sub-menu>
<template slot="title"> <template slot="title">
<a-icon type="link"></a-icon> <a-icon type="link"></a-icon>
<span>{{ i18n "menu.link"}}</span> <span>{{ i18n "menu.link"}}</span>
</template> </template>
<a-menu-item key="https://github.com/mhsanaei/3x-ui/"> <a-menu-item key="https://github.com/mhsanaei/3x-ui/">
<a-icon type="github"></a-icon> <a-icon type="github"></a-icon>
<span>Github</span> <span>Github</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="https://t.me/xxxuiforever"> <a-menu-item key="https://t.me/panel3xui">
<a-icon type="usergroup-add"></a-icon> <a-icon type="usergroup-add"></a-icon>
<span>Telegram</span> <span>Telegram</span>
</a-menu-item> </a-menu-item>
@@ -37,27 +37,50 @@
{{define "commonSider"}} {{define "commonSider"}}
<a-layout-sider id="sider" collapsible breakpoint="md" collapsed-width="0"> <a-layout-sider :theme="siderDrawer.theme" id="sider" collapsible breakpoint="md" collapsed-width="0">
<a-menu theme="dark" mode="inline" :selected-keys="['{{ .request_uri }}']" <a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
<a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item>
</a-menu>
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key"> @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}} {{template "menuItems" .}}
</a-menu> </a-menu>
</a-layout-sider> </a-layout-sider>
<a-drawer id="sider-drawer" placement="left" :closable="false" <a-drawer id="sider-drawer" placement="left" :closable="false"
@close="siderDrawer.close()" @close="siderDrawer.close()"
:visible="siderDrawer.visible" :wrap-style="{ padding: 0 }"> :visible="siderDrawer.visible"
:wrap-class-name="siderDrawer.isDarkTheme ? 'ant-drawer-dark' : ''"
:wrap-style="{ padding: 0 }">
<div class="drawer-handle" @click="siderDrawer.change()" slot="handle"> <div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
<a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon> <a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
</div> </div>
<a-menu theme="light" mode="inline" :selected-keys="['{{ .request_uri }}']" <a-menu :theme="siderDrawer.theme" mode="inline" selected-keys="">
<a-menu-item mode="inline">
<a-icon type="bg-colors"></a-icon>
<a-switch :default-checked="siderDrawer.isDarkTheme"
checked-children="☀"
un-checked-children="🌙"
@change="siderDrawer.changeTheme()"></a-switch>
</a-menu-item>
</a-menu>
<a-menu :theme="siderDrawer.theme" mode="inline" :selected-keys="['{{ .request_uri }}']"
@click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key"> @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
{{template "menuItems" .}} {{template "menuItems" .}}
</a-menu> </a-menu>
</a-drawer> </a-drawer>
<script> <script>
const darkClass = "ant-card-dark";
const bgDarkStyle = "background-color: #242c3a";
const siderDrawer = { const siderDrawer = {
visible: false, visible: false,
collapsed: false,
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
show() { show() {
this.visible = true; this.visible = true;
}, },
@@ -66,6 +89,16 @@
}, },
change() { change() {
this.visible = !this.visible; this.visible = !this.visible;
},
toggleCollapsed() {
this.collapsed = !this.collapsed;
},
changeTheme() {
this.isDarkTheme = ! this.isDarkTheme;
localStorage.setItem("dark-mode", this.isDarkTheme);
},
get theme() {
return this.isDarkTheme ? 'dark' : 'light';
} }
}; };

View File

@@ -1,94 +0,0 @@
{{define "inboundInfoStream"}}
<p>{{ i18n "transmission" }}: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
<p v-if="inbound.host">host: <a-tag color="green">[[ inbound.host ]]</a-tag></p>
<p v-else>{{ i18n "host" }}: <a-tag color="orange">{{ i18n "none" }}</a-tag></p>
<p v-if="inbound.path">path: <a-tag color="green">[[ inbound.path ]]</a-tag></p>
<p v-else>{{ i18n "path" }}: <a-tag color="orange">{{ i18n "none" }}</a-tag></p>
</template>
<template v-if="inbound.isQuic">
<p>quic {{ i18n "encryption" }}: <a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></p>
<p>quic {{ i18n "password" }}: <a-tag color="green">[[ inbound.quicKey ]]</a-tag></p>
<p>quic {{ i18n "camouflage" }}: <a-tag color="green">[[ inbound.quicType ]]</a-tag></p>
</template>
<template v-if="inbound.isKcp">
<p>kcp {{ i18n "encryption" }}: <a-tag color="green">[[ inbound.kcpType ]]</a-tag></p>
<p>kcp {{ i18n "password" }}: <a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></p>
</template>
<template v-if="inbound.isGrpc">
<p>grpc serviceName: <a-tag color="green">[[ inbound.serviceName ]]</a-tag></p>
</template>
<template v-if="inbound.tls || inbound.xtls">
<p v-if="inbound.tls">tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag></p>
<p v-if="inbound.xtls">xtls: <a-tag color="green">{{ i18n "enabled" }}</a-tag></p>
</template>
<template v-else>
<p>tls: <a-tag color="red">{{ i18n "disabled" }}</a-tag></p>
</template>
<p v-if="inbound.tls">
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</p>
<p v-if="inbound.xtls">
xtls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
</p>
{{end}}
{{define "component/inboundInfoComponent"}}
<div>
<p>{{ i18n "protocol"}}: <a-tag color="green">[[ dbInbound.protocol ]]</a-tag></p>
<p>{{ i18n "pages.inbounds.address"}}: <a-tag color="blue">[[ dbInbound.address ]]</a-tag></p>
<p>{{ i18n "pages.inbounds.port"}}: <a-tag color="green">[[ dbInbound.port ]]</a-tag></p>
<template v-if="dbInbound.isVMess" v-for="(vmess, index) in inbound.settings.vmesses">
<p>uuid: <a-tag color="green">[[ vmess.id ]]</a-tag></p>
<p>alterId: <a-tag color="green">[[ vmess.alterId ]]</a-tag></p>
<a-divider style="height: 2px; background-color: #7e7e7e" />
</template>
<template v-if="dbInbound.isVLess" v-for="(vless, index) in inbound.settings.vlesses">
<p>uuid: <a-tag color="green">[[ vless.id ]]</a-tag></p>
<p v-if="inbound.isXTls">flow: <a-tag color="green">[[ vless.flow ]]</a-tag></p>
<a-divider style="height: 2px; background-color: #7e7e7e" />
</template>
<template v-if="dbInbound.isTrojan">
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isSS">
<p>{{ i18n "encryption"}}: <a-tag color="green">[[ inbound.method ]]</a-tag></p>
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isSocks">
<p>{{ i18n "username"}}: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isHTTP">
<p>{{ i18n "username"}}: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
<p>{{ i18n "password"}}: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
{{template "inboundInfoStream"}}
</template>
</div>
{{end}}
{{define "component/inboundInfo"}}
<script>
Vue.component('inbound-info', {
delimiters: ['[[', ']]'],
props: ["dbInbound", "inbound"],
template: `{{template "component/inboundInfoComponent"}}`,
});
</script>
{{end}}

View File

@@ -0,0 +1,110 @@
{{define "form/client"}}
<a-form layout="inline" v-if="client">
<template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
</template>
<a-form-item>
<span slot="label">
Email
<a-tooltip>
<template slot="title">
The Email Must Be Completely Unique
</template>
<a-icon type="sync" @click="getNewEmail(client)"></a-icon>
</a-tooltip>
</span>
<a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
</a-form-item>
<a-form-item label="Password" v-if="inbound.protocol === Protocols.TROJAN">
<a-input v-model.trim="client.password" style="width: 150px;" ></a-input>
</a-form-item>
<a-form-item label="ID" v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "additional" }} ID' v-if="inbound.protocol === Protocols.VMESS">
<a-input type="number" v-model.number="client.alterId" style="width: 70px;"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
IP Count Limit
<a-tooltip>
<template slot="title">
Disable inbound if more than entered count (0 for disable limit ip)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
</a-form-item>
<a-form-item v-if="client.email && client.limitIp > 0 && isEdit">
<span slot="label">
IP Log
<a-tooltip>
<template slot="title">
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">
Clear The Log
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(client.email)"></a-icon>
</span>
</a-tooltip>
</span>
<a-form layout="block">
<a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 2, maxRows: 10 }">
</a-textarea>
</a-form>
</a-form-item>
<a-form-item v-if="inbound.XTLS" label="Flow">
<a-select v-model="client.flow" style="width: 150px">
<a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="client.flow" style="width: 150px">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
<a-tooltip>
<template slot="title">
0 <span>{{ i18n "pages.inbounds.meansNoLimit" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-input-number v-model="client._totalGB":min="0" style="width: 70px;"></a-input-number>
<template v-if="isEdit && clientStats">
{{ i18n "usage" }}:
<a-tag :color="statsColor">
[[ sizeFormat(clientStats.up) ]] /
[[ sizeFormat(clientStats.down) ]]
([[ sizeFormat(clientStats.up + clientStats.down) ]])
</a-tag>
</template>
</a-form-item>
<a-form-item>
<span slot="label">
<span >{{ i18n "pages.inbounds.expireDate" }}</span>
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</span>
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
<a-tag color="red" v-if="isExpiry">Expired</a-tag>
</a-form-item>
</a-form>
{{end}}

View File

@@ -8,7 +8,7 @@
<a-switch v-model="dbInbound.enable"></a-switch> <a-switch v-model="dbInbound.enable"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "protocol" }}'> <a-form-item label='{{ i18n "protocol" }}'>
<a-select v-model="inbound.protocol" style="width: 160px;"> <a-select v-model="inbound.protocol" style="width: 160px;" :disabled="isEdit" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option> <a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -50,6 +50,7 @@
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker> v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item> </a-form-item>
</a-form> </a-form>

View File

@@ -7,11 +7,14 @@
<a-input type="number" v-model.number="inbound.settings.port"></a-input> <a-input type="number" v-model.number="inbound.settings.port"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network"}}'> <a-form-item label='{{ i18n "pages.inbounds.network"}}'>
<a-select v-model="inbound.settings.network" style="width: 100px;"> <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="tcp,udp">tcp+udp</a-select-option> <a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option> <a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option> <a-select-option value="udp">udp</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="FollowRedirect">
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,7 +1,7 @@
{{define "form/shadowsocks"}} {{define "form/shadowsocks"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "encryption" }}'> <a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="inbound.settings.method" style="width: 165px;"> <a-select v-model="inbound.settings.method" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option> <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
@@ -9,7 +9,7 @@
<a-input v-model.trim="inbound.settings.password"></a-input> <a-input v-model.trim="inbound.settings.password"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network" }}'> <a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select v-model="inbound.settings.network" style="width: 100px;"> <a-select v-model="inbound.settings.network" style="width: 100px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="tcp,udp">tcp+udp</a-select-option> <a-select-option value="tcp,udp">tcp+udp</a-select-option>
<a-select-option value="tcp">tcp</a-select-option> <a-select-option value="tcp">tcp</a-select-option>
<a-select-option value="udp">udp</a-select-option> <a-select-option value="udp">udp</a-select-option>

View File

@@ -1,6 +1,6 @@
{{define "form/socks"}} {{define "form/socks"}}
<a-form layout="inline"> <a-form layout="inline">
<!-- <a-form-item label="密码认证">--> <!-- <a-form-item label="Password authentication">-->
<a-form-item label='{{ i18n "password" }}'> <a-form-item label='{{ i18n "password" }}'>
<a-switch :checked="inbound.settings.auth === 'password'" <a-switch :checked="inbound.settings.auth === 'password'"
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch> @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>

View File

@@ -1,11 +1,7 @@
{{define "form/trojan"}} {{define "form/trojan"}}
<a-form layout="inline"> <a-form layout="inline">
<label>{{ i18n "clients"}} </label> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
<a-collapse activeKey="0" v-for="(trojan, index) in inbound.settings.trojans" <a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
:key="`trojan-${index}`">
<a-collapse-panel :class="getHeaderStyle(trojan.email)" :header="getHeaderText(trojan.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(trojan.email) + getDownStats(trojan.email)) > trojan.totalGB && trojan.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
@@ -14,15 +10,16 @@
<template slot="title"> <template slot="title">
The Email Must Be Completely Unique The Email Must Be Completely Unique
</template> </template>
<!--Renew Svg Icon--> <a-icon @click="getNewEmail(client)" type="sync"> </a-icon>
<svg
@click="getNewEmail(trojan)"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="trojan.email"></a-input> <a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
</a-form-item> </a-form-item>
<a-form-item> </a-form>
<a-form-item label="Password">
<a-input v-model.trim="client.password" style="width: 150px;"></a-input>
</a-form-item>
<a-form-item>
<span slot="label"> <span slot="label">
IP Count Limit IP Count Limit
<a-tooltip> <a-tooltip>
@@ -32,37 +29,10 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input type="number" v-model.number="trojan.limitIp" min="0" ></a-input> <a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="trojan.email && trojan.limitIp > 0 && isEdit"> <a-form-item v-if="inbound.XTLS" label="Flow">
<span slot="label"> <a-select v-model="client.flow" style="width: 150px">
IP log
<a-tooltip>
<template slot="title">
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">
clear the log
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(trojan.email,$event)"></a-icon>
</span>
</a-tooltip>
</span>
<a-form layout="block">
<a-textarea readonly @click="getDBClientIps(trojan.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 0.5, maxRows: 10 }">
</a-textarea>
</a-form>
</a-form-item>
</a-form>
<a-form-item label="Password">
<a-input v-model.trim="trojan.password"></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="trojan.flow" style="width: 150px">
<a-select-option value="">{{ i18n "none" }}</a-select-option> <a-select-option value="">{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
@@ -77,7 +47,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input-number v-model="trojan._totalGB" :min="0"></a-input-number> <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
@@ -90,72 +60,57 @@
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
v-model="trojan._expiryTime" style="width: 300px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item> </a-form-item>
<a-form layout="inline">
<a-tooltip v-if="trojan._totalGB > 0">
<template slot="title">
{{ i18n "pages.inbounds.resetTraffic" }}
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(trojan,$event)"></a-icon>
</span>
</a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(trojan.email)) ]] / [[ sizeFormat(getDownStats(trojan.email)) ]]</a-tag>
<a-tag v-if="trojan._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(trojan.email) + getDownStats(trojan.email)) ]]</a-tag>
<a-tag v-show="inbound.settings.trojans.length > 1" @click="removeClient(index, inbound.settings.trojans)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" class="mt-2 cursor-pointer">
<path fill="none" d="M0 0h24v24H0z" />
<path fill="#EC4899"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
/>
</svg>
</a-tag>
</a-form>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<a-tag @click="addClient(inbound.protocol, inbound.settings.trojans)"> <a-collapse v-else>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="ml-2 cursor-pointer"> <a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.trojans.length">
<path fill="none" d="M0 0h24v24H0z" /> <table width="100%">
<path fill="green" <tr class="client-table-header">
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z" <th v-for="col in Object.keys(inbound.settings.trojans[0]).slice(0, 3)">[[ col ]]</th>
/> </tr>
</svg> <tr v-for="(client, index) in inbound.settings.trojans" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
</a-tag> <td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp && (inbound.tls || inbound.xtls)">
<a-form layout="inline">
<a-form-item label="Fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addTrojanFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<a-form layout="inline"> <!-- trojan fallbacks -->
<a-form-item label="Fallbacks"> <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-row> <a-divider>
<a-button type="primary" size="small" fallback[[ index + 1 ]]
@click="inbound.settings.addTrojanFallback()"> <a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
+ style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-button> </a-divider>
</a-row> <a-form-item label="Name">
</a-form-item> <a-input v-model="fallback.name"></a-input>
</a-form> </a-form-item>
<a-form-item label="Alpn">
<!-- trojan fallbacks --> <a-input v-model="fallback.alpn"></a-input>
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline"> </a-form-item>
<a-divider> <a-form-item label="Path">
fallback[[ index + 1 ]] <a-input v-model="fallback.path"></a-input>
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)" </a-form-item>
style="color: rgb(255, 77, 79);cursor: pointer;"/> <a-form-item label="Dest">
</a-divider> <a-input v-model="fallback.dest"></a-input>
<a-form-item label="Name"> </a-form-item>
<a-input v-model="fallback.name"></a-input> <a-form-item label="xVer">
</a-form-item> <a-input type="number" v-model.number="fallback.xver"></a-input>
<a-form-item label="Alpn"> </a-form-item>
<a-input v-model="fallback.alpn"></a-input> <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form-item> </a-form>
<a-form-item label="Path"> </template>
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label="Dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xVer">
<a-input type="number" v-model.number="fallback.xver"></a-input>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>
{{end}} {{end}}

View File

@@ -1,12 +1,7 @@
{{define "form/vless"}} {{define "form/vless"}}
<a-form layout="inline"> <a-form layout="inline">
<label>{{ i18n "clients"}}</label> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse activeKey="0" v-for="(vless, index) in inbound.settings.vlesses" <a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
:key="`vless-${index}`">
<a-collapse-panel :class="getHeaderStyle(vless.email)" :header="getHeaderText(vless.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(vless.email) + getDownStats(vless.email)) > vless.totalGB && vless.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
@@ -15,15 +10,16 @@
<template slot="title"> <template slot="title">
The Email Must Be Completely Unique The Email Must Be Completely Unique
</template> </template>
<!--Renew Svg Icon--> <a-icon type="sync" @click="getNewEmail(client)"></a-icon>
<svg
@click="getNewEmail(vless)"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="vless.email"></a-input> <a-input v-model.trim="client.email" style="width: 150px;" ></a-input>
</a-form-item> </a-form-item>
<a-form-item> </a-form>
<a-form-item label="ID">
<a-input v-model.trim="client.id" style="width: 300px;" ></a-input>
</a-form-item>
<a-form-item>
<span slot="label"> <span slot="label">
IP Count Limit IP Count Limit
<a-tooltip> <a-tooltip>
@@ -33,37 +29,9 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input type="number" v-model.number="vless.limitIp" min="0" ></a-input> <a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="vless.email && vless.limitIp > 0 && isEdit"> <a-form-item v-if="inbound.XTLS" label="Flow">
<span slot="label">
IP log
<a-tooltip>
<template slot="title">
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">
clear the log
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(vless.email,$event)"></a-icon>
</span>
</a-tooltip>
</span>
<a-form layout="block">
<a-textarea readonly @click="getDBClientIps(vless.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 0.5, maxRows: 10 }">
</a-textarea>
</a-form>
</a-form-item>
</a-form>
<a-form-item label="ID">
<a-input v-model.trim="vless.id"></a-input>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px"> <a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px">
<a-select-option value="" selected>{{ i18n "none" }}</a-select-option> <a-select-option value="" selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in XTLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
@@ -75,11 +43,6 @@
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.tls" label="uTLS" layout="inline">
<a-select v-model="inbound.settings.vlesses[index].fingerprint" label="uTLS" style="width: 150px">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
@@ -90,7 +53,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input-number v-model="vless._totalGB" :min="0"></a-input-number> <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
@@ -103,73 +66,57 @@
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
v-model="vless._expiryTime" style="width: 300px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item> </a-form-item>
<a-form layout="inline">
<a-tooltip v-if="vless._totalGB > 0">
<template slot="title">
{{ i18n "pages.inbounds.resetTraffic" }}
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(vless,$event)"></a-icon>
</span>
</a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(vless.email)) ]] / [[ sizeFormat(getDownStats(vless.email)) ]]</a-tag>
<a-tag v-if="vless._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vless.email) + getDownStats(vless.email)) ]]</a-tag>
<a-tag v-show="inbound.settings.vlesses.length > 1" @click="removeClient(index, inbound.settings.vlesses)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" class="mt-2 cursor-pointer">
<path fill="none" d="M0 0h24v24H0z" />
<path fill="#EC4899"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
/>
</svg>
</a-tag>
</a-form>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<a-tag @click="addClient(inbound.protocol, inbound.settings.vlesses)"> <a-collapse v-else>
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="ml-2 cursor-pointer"> <a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vlesses.length">
<path fill="none" d="M0 0h24v24H0z" /> <table width="100%">
<path fill="green" <tr class="client-table-header">
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z" <th v-for="col in Object.keys(inbound.settings.vlesses[0]).slice(0, 3)">[[ col ]]</th>
/> </tr>
</svg> <tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
</a-tag> <td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</tr>
</table>
</a-collapse-panel>
</a-collapse>
<template v-if="inbound.isTcp && (inbound.tls || inbound.xtls)">
<a-form layout="inline">
<a-form-item label="Fallbacks">
<a-row>
<a-button type="primary" size="small"
@click="inbound.settings.addFallback()">
+
</a-button>
</a-row>
</a-form-item>
</a-form>
<a-form layout="inline"> <!-- vless fallbacks -->
<a-form-item label="Fallbacks"> <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
<a-row> <a-divider>
<a-button type="primary" size="small" fallback[[ index + 1 ]]
@click="inbound.settings.addFallback()"> <a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
+ style="color: rgb(255, 77, 79);cursor: pointer;"/>
</a-button> </a-divider>
</a-row> <a-form-item label="Name">
</a-form-item> <a-input v-model="fallback.name"></a-input>
</a-form> </a-form-item>
<a-form-item label="Alpn">
<!-- vless fallbacks --> <a-input v-model="fallback.alpn"></a-input>
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline"> </a-form-item>
<a-divider> <a-form-item label="Path">
fallback[[ index + 1 ]] <a-input v-model="fallback.path"></a-input>
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)" </a-form-item>
style="color: rgb(255, 77, 79);cursor: pointer;"/> <a-form-item label="Dest">
</a-divider> <a-input v-model="fallback.dest"></a-input>
<a-form-item label="Name"> </a-form-item>
<a-input v-model="fallback.name"></a-input> <a-form-item label="xVer">
</a-form-item> <a-input type="number" v-model.number="fallback.xver"></a-input>
<a-form-item label="Alpn"> </a-form-item>
<a-input v-model="fallback.alpn"></a-input> <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form-item> </a-form>
<a-form-item label="Path"> </template>
<a-input v-model="fallback.path"></a-input> {{end}}
</a-form-item>
<a-form-item label="Dest">
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label="xVer">
<a-input type="number" v-model.number="fallback.xver"></a-input>
</a-form-item>
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
</a-form>
{{end}}

View File

@@ -1,11 +1,7 @@
{{define "form/vmess"}} {{define "form/vmess"}}
<a-form layout="inline"> <a-form layout="inline">
<label>{{ i18n "clients"}}</label> <a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
<a-collapse activeKey="0" v-for="(vmess, index) in inbound.settings.vmesses" <a-collapse-panel header="{{ i18n "pages.inbounds.client" }}">
:key="`vmess-${index}`">
<a-collapse-panel :class="getHeaderStyle(vmess.email)" :header="getHeaderText(vmess.email)">
<a-tag v-if="isExpiry(index) || ((getUpStats(vmess.email) + getDownStats(vmess.email)) > vmess.totalGB && vmess.totalGB != 0)" color="red" style="margin-bottom: 10px;display: block;text-align: center;">Account is (Expired|Traffic Ended) And Disabled</a-tag>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
@@ -14,54 +10,30 @@
<template slot="title"> <template slot="title">
The Email Must Be Completely Unique The Email Must Be Completely Unique
</template> </template>
<!--Renew Svg Icon--> <a-icon type="sync" @click="getNewEmail(client)"></a-icon>
<svg
@click="getNewEmail(vmess)"
xmlns="http://www.w3.org/2000/svg" width="16" height="16" fill="currentColor" class="anticon anticon-question-circle" viewBox="0 0 16 16"> <path d="M11.534 7h3.932a.25.25 0 0 1 .192.41l-1.966 2.36a.25.25 0 0 1-.384 0l-1.966-2.36a.25.25 0 0 1 .192-.41zm-11 2h3.932a.25.25 0 0 0 .192-.41L2.692 6.23a.25.25 0 0 0-.384 0L.342 8.59A.25.25 0 0 0 .534 9z"/> <path fill-rule="evenodd" d="M8 3c-1.552 0-2.94.707-3.857 1.818a.5.5 0 1 1-.771-.636A6.002 6.002 0 0 1 13.917 7H12.9A5.002 5.002 0 0 0 8 3zM3.1 9a5.002 5.002 0 0 0 8.757 2.182.5.5 0 1 1 .771.636A6.002 6.002 0 0 1 2.083 9H3.1z"/> </svg>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input v-model.trim="vmess.email"></a-input> <a-input v-model.trim="client.email" style="width: 150px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item> </a-form>
<a-form-item label="ID">
<a-input v-model.trim="client.id" style="width: 300px;"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "additional" }} ID'>
<a-input type="number" v-model.number="client.alterId" style="width: 70px;"></a-input>
</a-form-item>
<a-form-item>
<span slot="label"> <span slot="label">
IP Count Limit IP Count Limit
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
disable inbound if more than entered count (0 for disable limit ip) Disable inbound if more than entered count (0 for disable limit ip)
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input type="number" v-model.number="vmess.limitIp" min="0" ></a-input> <a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item v-if="vmess.email && vmess.limitIp > 0 && isEdit">
<span slot="label">
IP Log
<a-tooltip>
<template slot="title">
IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">
clear the log
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="clearDBClientIps(vmess.email,$event)"></a-icon>
</span>
</a-tooltip>
</span>
<a-textarea readonly @click="getDBClientIps(vmess.email,$event)" placeholder="Click To Get IPs" :auto-size="{ minRows: 0.5, maxRows: 10 }">
</a-textarea>
</a-form-item>
</a-form>
<a-form-item label="ID">
<a-input v-model.trim="vmess.id"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "additional" }} ID'>
<a-input type="number" v-model.number="vmess.alterId"></a-input>
</a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
<span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB) <span >{{ i18n "pages.inbounds.totalFlow" }}</span>(GB)
@@ -72,7 +44,7 @@
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
</span> </span>
<a-input-number v-model="vmess._totalGB" :min="0"></a-input-number> <a-input-number v-model="client._totalGB" :min="0"></a-input-number>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
@@ -85,47 +57,26 @@
</a-tooltip> </a-tooltip>
</span> </span>
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm" <a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
v-model="vmess._expiryTime" style="width: 300px;"></a-date-picker> v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item> </a-form-item>
<a-form layout="inline">
<a-tooltip v-if="vmess._totalGB > 0">
<template slot="title">
{{ i18n "pages.inbounds.resetTraffic" }}
</template>
<span style="color: #FF4D4F">
<a-icon type="delete" @click="resetClientTraffic(vmess,$event)"></a-icon>
</span>
</a-tooltip>
<a-tag color="blue">[[ sizeFormat(getUpStats(vmess.email)) ]] / [[ sizeFormat(getDownStats(vmess.email)) ]]</a-tag>
<a-tag v-if="vmess._totalGB > 0" color="red">used : [[ sizeFormat(getUpStats(vmess.email) + getDownStats(vmess.email)) ]]</a-tag>
<a-tag v-show="inbound.settings.vmesses.length > 1" @click="removeClient(index, inbound.settings.vmesses)">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 22 22" width="22" height="22" class="mt-2 cursor-pointer">
<path fill="none" d="M0 0h24v24H0z" />
<path fill="#EC4899"
d="M12 22C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16zm0-9.414l2.828-2.829 1.415 1.415L13.414 12l2.829 2.828-1.415 1.415L12 13.414l-2.828 2.829-1.415-1.415L10.586 12 7.757 9.172l1.415-1.415L12 10.586z"
/>
</svg>
</a-tag>
</a-form>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
<a-collapse v-else>
<a-tag @click="addClient(inbound.protocol, inbound.settings.vmesses)"> <a-collapse-panel :header="'{{ i18n "pages.client.clientCount"}} : ' + inbound.settings.vmesses.length">
<svg xmlns="http://www.w3.org/2000/svg" viewBox="0 0 24 24" width="24" height="24" class="ml-2 cursor-pointer"> <table width="100%">
<path fill="none" d="M0 0h24v24H0z" /> <tr class="client-table-header">
<path fill="green" <th v-for="col in Object.keys(inbound.settings.vmesses[0]).slice(0, 3)">[[ col ]]</th>
d="M11 11V7h2v4h4v2h-4v4h-2v-4H7v-2h4zm1 11C6.477 22 2 17.523 2 12S6.477 2 12 2s10 4.477 10 10-4.477 10-10 10zm0-2a8 8 0 1 0 0-16 8 8 0 0 0 0 16z" </tr>
/> <tr v-for="(client, index) in inbound.settings.vmesses" :class="index % 2 == 1 ? 'client-table-odd-row' : ''">
</svg> <td v-for="col in Object.values(client).slice(0, 3)">[[ col ]]</td>
</a-tag> </tr>
</table>
</a-collapse-panel>
</a-collapse>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'> <a-form-item label='{{ i18n "pages.inbounds.disableInsecureEncryption" }}'>
<a-switch v-model.number="inbound.settings.disableInsecure"></a-switch> <a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
</a-form-item> </a-form-item>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -1,7 +1,7 @@
{{define "form/streamQUIC"}} {{define "form/streamQUIC"}}
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
<a-select v-model="inbound.stream.quic.security" style="width: 165px;"> <a-select v-model="inbound.stream.quic.security" style="width: 165px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="none">none</a-select-option> <a-select-option value="none">none</a-select-option>
<a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option> <a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
<a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option> <a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
@@ -11,7 +11,7 @@
<a-input v-model.trim="inbound.stream.quic.key"></a-input> <a-input v-model.trim="inbound.stream.quic.key"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "camouflage" }}'> <a-form-item label='{{ i18n "camouflage" }}'>
<a-select v-model="inbound.stream.quic.type" style="width: 280px;"> <a-select v-model="inbound.stream.quic.type" style="width: 280px;" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="none">nonenot camouflage</a-select-option> <a-select-option value="none">nonenot camouflage</a-select-option>
<a-select-option value="srtp">srtpcamouflage video call</a-select-option> <a-select-option value="srtp">srtpcamouflage video call</a-select-option>
<a-select-option value="utp">utpcamouflage BT download</a-select-option> <a-select-option value="utp">utpcamouflage BT download</a-select-option>

View File

@@ -2,7 +2,7 @@
<!-- select stream network --> <!-- select stream network -->
<a-form layout="inline"> <a-form layout="inline">
<a-form-item label='{{ i18n "transmission" }}'> <a-form-item label='{{ i18n "transmission" }}'>
<a-select v-model="inbound.stream.network" @change="streamNetworkChange"> <a-select v-model="inbound.stream.network" @change="streamNetworkChange" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="tcp">TCP</a-select-option> <a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="kcp">KCP</a-select-option> <a-select-option value="kcp">KCP</a-select-option>
<a-select-option value="ws">WS</a-select-option> <a-select-option value="ws">WS</a-select-option>

View File

@@ -5,22 +5,15 @@
<a-switch v-model="inbound.tls"> <a-switch v-model="inbound.tls">
</a-switch> </a-switch>
</a-form-item> </a-form-item>
<a-form-item v-if="inbound.canEnableXTls()" label="XTLS"> <a-form-item v-if="inbound.canEnableXTLS()" label="XTLS">
<a-switch v-model="inbound.xtls"></a-switch> <a-switch v-model="inbound.XTLS"></a-switch>
</a-form-item> </a-form-item>
</a-form> </a-form>
<!-- tls settings --> <!-- tls settings -->
<a-form v-if="inbound.tls || inbound.xtls"layout="inline"> <a-form v-if="inbound.tls || inbound.XTLS" layout="inline">
<a-form-item label="MinVersion"> <a-form-item label="SNI" placeholder="Server Name Indication" v-if="inbound.tls">
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px"> <a-input v-model.trim="inbound.stream.tls.settings[0].serverName"></a-input>
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="MaxVersion">
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item> </a-form-item>
<a-form-item label="CipherSuites"> <a-form-item label="CipherSuites">
<a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px"> <a-select v-model="inbound.stream.tls.cipherSuites" style="width: 300px">
@@ -28,11 +21,32 @@
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="MinVersion">
<a-select v-model="inbound.stream.tls.minVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="MaxVersion">
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 60px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="uTLS" v-if="inbound.tls" >
<a-select v-model="inbound.stream.tls.settings[0].fingerprint" style="width: 135px">
<a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "domainName" }}'> <a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="inbound.stream.tls.server"></a-input> <a-input v-model.trim="inbound.stream.tls.server"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="Alpn"> <a-form-item label="Alpn">
<a-input v-model.trim="inbound.stream.tls.alpn"></a-input> <a-checkbox-group v-model="inbound.stream.tls.alpn" style="width:200px">
<a-checkbox v-for="key in ALPN_OPTION" :value="key">[[ key ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="Allow insecure">
<a-switch v-model="inbound.stream.tls.settings[0].allowInsecure"></a-switch>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "certificate" }}'> <a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid"> <a-radio-group v-model="inbound.stream.tls.certs[0].useFile" button-style="solid">
@@ -42,18 +56,18 @@
</a-form-item> </a-form-item>
<template v-if="inbound.stream.tls.certs[0].useFile"> <template v-if="inbound.stream.tls.certs[0].useFile">
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'> <a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].certFile"></a-input> <a-input v-model.trim="inbound.stream.tls.certs[0].certFile" style="width:300px;"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'> <a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile"></a-input> <a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item> </a-form-item>
</template> </template>
<template v-else> <template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'> <a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
<a-input type="textarea" :rows="2" v-model="inbound.stream.tls.certs[0].cert"></a-input> <a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].cert"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'> <a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
<a-input type="textarea" :rows="2" v-model="inbound.stream.tls.certs[0].key"></a-input> <a-input type="textarea" :rows="3" style="width:300px;" v-model="inbound.stream.tls.certs[0].key"></a-input>
</a-form-item> </a-form-item>
</template> </template>
</a-form> </a-form>

View File

@@ -1,24 +1,32 @@
{{define "client_row"}} {{define "client_table"}}
<template slot="actions" slot-scope="text, client, index"> <template slot="actions" slot-scope="text, client, index">
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "qrCode" }}</template> <template slot="title">{{ i18n "qrCode" }}</template>
<a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon> <a-icon style="font-size: 24px;" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record,index);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.client.edit" }}</template>
<a-icon style="font-size: 24px;" type="edit" @click="openEditClient(record.id,client);"></a-icon>
</a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "info" }}</template> <template slot="title">{{ i18n "info" }}</template>
<a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon> <a-icon style="font-size: 24px;" type="info-circle" @click="showInfo(record,index);"></a-icon>
</a-tooltip> </a-tooltip>
<a-tooltip> <a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template> <template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record,$event)" v-if="client.email != ''"></a-icon> <a-icon style="font-size: 24px;" type="retweet" @click="resetClientTraffic(client,record.id)" v-if="client.email.length > 0"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title"><span style="color: #FF4D4F"> {{ i18n "delete"}}</span></template>
<a-icon style="font-size: 24px;" type="delete" v-if="isRemovable(record.id)" @click="delClient(record.id,client)"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<template slot="client" slot-scope="text, client"> <template slot="client" slot-scope="text, client">
[[ client.email ]] [[ client.email ]]
<a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "disabled" }}</a-tag> <a-tag v-if="!isClientEnabled(record, client.email)" color="red">{{ i18n "disabled" }}</a-tag>
</template> </template>
<template slot="traffic" slot-scope="text, client"> <template slot="traffic" slot-scope="text, client">
<a-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag> <a-tag color="blue">[[ sizeFormat(getUpStats(record, client.email)) ]] / [[ sizeFormat(getDownStats(record, client.email)) ]]</a-tag>
<template v-if="client._totalGB > 0"> <template v-if="client._totalGB > 0">
<a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]]GB</a-tag> <a-tag v-if="isTrafficExhausted(record, client.email)" color="red">[[client._totalGB]]GB</a-tag>
<a-tag v-else color="cyan">[[client._totalGB]]GB</a-tag> <a-tag v-else color="cyan">[[client._totalGB]]GB</a-tag>
@@ -33,4 +41,4 @@
</template> </template>
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</template> </template>
{{end}} {{end}}

View File

@@ -3,6 +3,7 @@
v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}' v-model="infoModal.visible" title='{{ i18n "pages.inbounds.details"}}'
:closable="true" :closable="true"
:mask-closable="true" :mask-closable="true"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:footer="null" :footer="null"
width="600px" width="600px"
> >
@@ -43,7 +44,7 @@
</template> </template>
</table> </table>
</td></tr> </td></tr>
<tr colspan="2"> <tr colspan="2" v-if="dbInbound.hasLink()">
<td v-if="inbound.tls"> <td v-if="inbound.tls">
tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br /> tls: <a-tag color="green">{{ i18n "enabled" }}</a-tag><br />
tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag> tls {{ i18n "domainName" }}: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
@@ -56,20 +57,18 @@
</td> </td>
</tr> </tr>
</table> </table>
<template v-if="infoModal.clientSettings">
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider> <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<table style="margin-bottom: 10px; width: 100%;"> <table style="margin-bottom: 10px;">
<tr><th>[[ Object.keys(infoModal.clientSettings)[0] ]]</th><th>[[ Object.keys(infoModal.clientSettings)[1] ]]</th><th>[[ Object.keys(infoModal.clientSettings)[2] ]]</th></tr> <tr v-for="col,index in Object.keys(infoModal.clientSettings).slice(0, 3)">
<tr> <td>[[ col ]]</td>
<td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[0] ]]</a-tag></td> <td><a-tag color="green">[[ infoModal.clientSettings[col] ]]</a-tag></td>
<td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[1] ]]</a-tag></td> </table>
<td><a-tag color="green">[[ Object.values(infoModal.clientSettings)[2] ]]</a-tag></td>
</tr>
</table>
<table style="margin-bottom: 10px; width: 100%;"> <table style="margin-bottom: 10px; width: 100%;">
<tr><th>{{ i18n "usage" }}</th><th>{{ i18n "pages.inbounds.totalFlow" }}</th><th>{{ i18n "pages.inbounds.expireDate" }}</th><th>{{ i18n "enable" }}</th></tr> <tr><th>{{ i18n "usage" }}</th><th>{{ i18n "pages.inbounds.totalFlow" }}</th><th>{{ i18n "pages.inbounds.expireDate" }}</th><th>{{ i18n "enable" }}</th></tr>
<tr> <tr>
<td> <td>
<a-tag :color="statsColor(infoModal.clientStats)"> <a-tag v-if="infoModal.clientStats" :color="statsColor(infoModal.clientStats)">
[[ sizeFormat(infoModal.clientStats['up']) ]] / [[ sizeFormat(infoModal.clientStats['up']) ]] /
[[ sizeFormat(infoModal.clientStats['down']) ]] [[ sizeFormat(infoModal.clientStats['down']) ]]
([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]]) ([[ sizeFormat(infoModal.clientStats['up'] + infoModal.clientStats['down']) ]])
@@ -88,11 +87,72 @@
<a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag> <a-tag v-else color="green">{{ i18n "indefinite" }}</a-tag>
</td> </td>
<td> <td>
<a-tag v-if="infoModal.clientStats.enable" color="blue">{{ i18n "enabled" }}</a-tag> <a-tag v-if="isEnable" color="blue">{{ i18n "enabled" }}</a-tag>
<a-tag v-else color="red">{{ i18n "disabled" }}</a-tag> <a-tag v-else color="red">{{ i18n "disabled" }}</a-tag>
</td> </td>
</tr> </tr>
</table> </table>
</template>
<template v-else>
<a-divider></a-divider>
<table v-if="inbound.protocol == Protocols.SHADOWSOCKS" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "encryption" }}</th>
<th>{{ i18n "password" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.method ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.password ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
</tr>
</table>
<table v-if="inbound.protocol == Protocols.DOKODEMO" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
<th>{{ i18n "pages.inbounds.network" }}</th>
<th>FollowRedirect</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.address ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.port ]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.network ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.followRedirect ]]</a-tag></td>
</tr>
</table>
</table>
<table v-if="inbound.protocol == Protocols.SOCKS" style="margin-bottom: 10px; width: 100%;">
<tr>
<th>{{ i18n "password" }} Auth</th>
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
<th>IP</th>
</tr><tr>
<td><a-tag color="green">[[ inbound.settings.auth ]]</a-tag></td>
<td><a-tag color="blue">[[ inbound.settings.udp]]</a-tag></td>
<td><a-tag color="green">[[ inbound.settings.ip ]]</a-tag></td>
</tr><tr v-if="inbound.settings.auth == 'password'">
<td> </td>
<td>{{ i18n "username" }}</td>
<td>{{ i18n "password" }}</td>
</tr><tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
</table>
<table v-if="inbound.protocol == Protocols.HTTP" style="margin-bottom: 10px; width: 100%;">
<tr>
<th> </th>
<th>{{ i18n "username" }}</th>
<th>{{ i18n "password" }}</th>
</tr><tr v-for="account,index in inbound.settings.accounts">
<td><a-tag color="green">[[ index ]]</a-tag></td>
<td><a-tag color="blue">[[ account.user ]]</a-tag></td>
<td><a-tag color="green">[[ account.pass ]]</a-tag></td>
</tr>
</table>
</table>
</template>
<div v-if="dbInbound.hasLink()"> <div v-if="dbInbound.hasLink()">
<a-divider>URL</a-divider> <a-divider>URL</a-divider>
<p>[[ infoModal.link ]]</p> <p>[[ infoModal.link ]]</p>
@@ -105,39 +165,31 @@
visible: false, visible: false,
inbound: new Inbound(), inbound: new Inbound(),
dbInbound: new DBInbound(), dbInbound: new DBInbound(),
settings: null,
clientSettings: new Inbound.Settings(), clientSettings: new Inbound.Settings(),
clientStats: [], clientStats: [],
upStats: 0, upStats: 0,
downStats: 0, downStats: 0,
clipboard: null, clipboard: null,
link: null, link: null,
index: 0, index: null,
isExpired: false, isExpired: false,
show(dbInbound, index=0) { show(dbInbound, index) {
this.index = index; this.index = index;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound); this.dbInbound = new DBInbound(dbInbound);
this.link = dbInbound.genLink(index); this.link = dbInbound.genLink(index);
this.clientSettings = Object.values(JSON.parse(this.inbound.settings).clients)[index]; this.settings = JSON.parse(this.inbound.settings);
this.clientStats = dbInbound.clientStats; this.clientSettings = this.settings.clients ? Object.values(this.settings.clients)[index] : null;
this.isExpired = this.inbound.isExpiry(index); this.isExpired = this.inbound.isExpiry(index);
if(dbInbound.clientStats.length > 0) this.clientStats = this.settings.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
{
for (const key in dbInbound.clientStats) {
if (Object.hasOwnProperty.call(dbInbound.clientStats, key)) {
if(dbInbound.clientStats[key]['email'] == this.clientSettings.email)
this.clientStats = dbInbound.clientStats[key];
}
}
}
this.visible = true; this.visible = true;
infoModalApp.$nextTick(() => { infoModalApp.$nextTick(() => {
if (this.clipboard === null) { if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#copy-url-link', { this.clipboard = new ClipboardJS('#copy-url-link', {
text: () => this.link, text: () => this.link,
}); });
this.clipboard.on('success', () => app.$message.success('{{ i18n "copySuccess" }}')); this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
} }
}); });
}, },
@@ -145,6 +197,7 @@
infoModal.visible = false; infoModal.visible = false;
}, },
}; };
const infoModalApp = new Vue({ const infoModalApp = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#inbound-info-modal', el: '#inbound-info-modal',
@@ -155,32 +208,33 @@
}, },
get inbound() { get inbound() {
return this.infoModal.inbound; return this.infoModal.inbound;
},
get isEnable() {
if(infoModal.clientStats){
return infoModal.clientStats.enable;
}
return infoModal.dbInbound.isEnable;
} }
}, },
methods: { methods: {
setQrCode(elmentId,index) {
content = infoModal.inbound.genLink(infoModal.dbInbound.address,infoModal.dbInbound.remark,index)
new QRious({
element: document.querySelector('#'+elmentId),
size: 260,
value: content,
});
},
copyTextToClipboard(elmentId,content) { copyTextToClipboard(elmentId,content) {
this.infoModal.clipboard = new ClipboardJS('#' + elmentId, { this.infoModal.clipboard = new ClipboardJS('#' + elmentId, {
text: () => content, text: () => content,
}); });
this.infoModal.clipboard.on('success', () => { this.infoModal.clipboard.on('success', () => {
app.$message.success('{{ i18n "copySuccess" }}') app.$message.success('{{ i18n "copied" }}')
this.infoModal.clipboard.destroy(); this.infoModal.clipboard.destroy();
}); });
}, },
statsColor(stats) { statsColor(stats) {
if(!stats) return 'blue'
if(stats['total'] === 0) return 'blue' if(stats['total'] === 0) return 'blue'
else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan' else if(stats['total'] > 0 && (stats['down']+stats['up']) < stats['total']) return 'cyan'
else return 'red' else return 'red'
} }
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,6 +1,7 @@
{{define "inboundModal"}} {{define "inboundModal"}}
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok" <a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'> :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}} {{template "form/inbound"}}
</a-modal> </a-modal>
@@ -88,101 +89,24 @@
removeClient(index, clients) { removeClient(index, clients) {
clients.splice(index, 1); clients.splice(index, 1);
}, },
async getDBClientIps(email,event) {
const msg = await HttpUtil.post('/xui/inbound/clientIps/'+ email);
if (!msg.success) {
return;
}
try {
ips = JSON.parse(msg.obj)
ips = ips.join(",")
event.target.value = ips
} catch (error) {
// text
event.target.value = msg.obj
}
},
async clearDBClientIps(email,event) {
const msg = await HttpUtil.post('/xui/inbound/clearClientIps/'+ email);
if (!msg.success) {
return;
}
event.target.value = ""
},
async resetClientTraffic(client,event) {
const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
if (!msg.success) {
return;
}
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == client.email){
clientStats[key]['up'] = 0
clientStats[key]['down'] = 0
}
}
}
}
},
isExpiry(index) { isExpiry(index) {
return this.inbound.isExpiry(index) return this.inbound.isExpiry(index)
}, },
getUpStats(email) {
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['up']
}
}
}
},
getDownStats(email) {
clientStats = this.inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['down']
}
}
}
},
isClientEnable(email) { isClientEnable(email) {
clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null clientStats = this.dbInbound.clientStats ? this.dbInbound.clientStats.find(stats => stats.email === email) : null
return clientStats ? clientStats['enable'] : true return clientStats ? clientStats['enable'] : true
}, },
getHeaderText(email) {
if(email == "")
return "Add Client"
return email + (this.isClientEnable(email) == true ? ' Active' : ' Deactive')
},
getHeaderStyle(email) {
return (this.isClientEnable(email) == true ? '' : 'deactive-client')
},
getNewEmail(client) { getNewEmail(client) {
var chars = 'abcdefghijklmnopqrstuvwxyz1234567890'; var chars = 'abcdefghijklmnopqrstuvwxyz1234567890';
var string = ''; var string = '';
var len = 7 + Math.floor(Math.random() * 5) var len = 6 + Math.floor(Math.random() * 5);
for(var ii=0; ii<len; ii++){ for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)]; string += chars[Math.floor(Math.random() * chars.length)];
} }
client.email = string client.email = string;
} }
}, },
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -15,7 +15,7 @@
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak>
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout"> <a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading"> <a-spin :spinning="spinning" :delay="500" tip="loading">
<transition name="list" appear> <transition name="list" appear>
@@ -24,30 +24,36 @@
</a-tag> </a-tag>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable style="margin-bottom: 20px;"> <a-card hoverable style="margin-bottom: 20px;" :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row> <a-row>
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.totalDownUp" }} {{ i18n "pages.inbounds.totalDownUp" }}:
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag> <a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
</a-col> </a-col>
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.totalUsage" }} {{ i18n "pages.inbounds.totalUsage" }}:
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag> <a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
</a-col> </a-col>
<a-col :xs="24" :sm="24" :lg="12"> <a-col :xs="24" :sm="24" :lg="12">
{{ i18n "pages.inbounds.inboundCount" }} {{ i18n "pages.inbounds.inboundCount" }}:
<a-tag color="green">[[ dbInbounds.length ]]</a-tag> <a-tag color="green">[[ dbInbounds.length ]]</a-tag>
</a-col> </a-col>
<a-col :xs="24" :sm="24" :lg="12">
{{ i18n "clients" }}:
<a-tag color="green">[[ total.clients ]]</a-tag>
<a-tag color="blue">{{ i18n "enabled" }} [[ total.active ]]</a-tag>
<a-tag color="red">{{ i18n "disabled" }} [[ total.deactive ]]</a-tag>
</a-col>
</a-row> </a-row>
</a-card> </a-card>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<div slot="title"> <div slot="title">
<a-button type="primary" @click="openAddInbound">Add Inbound</a-button> <a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-button type="primary" @click="exportAllLinks" class="copy-btn">Export Links</a-button> <a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
</div> </div>
<a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input> <a-input v-model.lazy="searchKey" placeholder="{{ i18n "search" }}" autofocus style="max-width: 300px"></a-input>
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id" <a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds" :data-source="searchedInbounds"
:loading="spinning" :scroll="{ x: 1300 }" :loading="spinning" :scroll="{ x: 1300 }"
@@ -57,8 +63,8 @@
<template slot="action" slot-scope="text, dbInbound"> <template slot="action" slot-scope="text, dbInbound">
<a-icon type="edit" style="font-size: 25px" @click="openEditInbound(dbInbound.id);"></a-icon> <a-icon type="edit" style="font-size: 25px" @click="openEditInbound(dbInbound.id);"></a-icon>
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a> <a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)"> <a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme" style="border: 1px solid rgba(255, 255, 255, 0.65);">
<a-menu-item v-if="dbInbound.isSS" key="qrcode"> <a-menu-item v-if="dbInbound.isSS" key="qrcode">
<a-icon type="qrcode"></a-icon> <a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }} {{ i18n "qrCode" }}
@@ -67,6 +73,26 @@
<a-icon type="edit"></a-icon> <a-icon type="edit"></a-icon>
{{ i18n "edit" }} {{ i18n "edit" }}
</a-menu-item> </a-menu-item>
<template v-if="dbInbound.isTrojan || dbInbound.isVLess || dbInbound.isVMess">
<a-menu-item key="addClient">
<a-icon type="user"></a-icon>
{{ i18n "pages.client.add"}}
</a-menu-item>
<a-menu-item key="addBulkClient">
<a-icon type="team"></a-icon>
{{ i18n "pages.client.bulk"}}
</a-menu-item>
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}}
</a-menu-item>
</template>
<template v-else>
<a-menu-item key="showInfo">
<a-icon type="info-circle"></a-icon>
{{ i18n "info"}}
</a-menu-item>
</template>
<a-menu-item key="resetTraffic"> <a-menu-item key="resetTraffic">
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }} <a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic" }}
</a-menu-item> </a-menu-item>
@@ -98,7 +124,7 @@
<template v-else>{{ i18n "none" }}</template> <template v-else>{{ i18n "none" }}</template>
</template> </template>
<template slot="enable" slot-scope="text, dbInbound"> <template slot="enable" slot-scope="text, dbInbound">
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch> <a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id)"></a-switch>
</template> </template>
<template slot="expiryTime" slot-scope="text, dbInbound"> <template slot="expiryTime" slot-scope="text, dbInbound">
<template v-if="dbInbound.expiryTime > 0"> <template v-if="dbInbound.expiryTime > 0">
@@ -119,7 +145,7 @@
:data-source="getInboundClients(record)" :data-source="getInboundClients(record)"
:pagination="false" :pagination="false"
> >
{{template "client_row"}} {{template "client_table"}}
</a-table> </a-table>
<a-table <a-table
v-else-if="record.protocol === Protocols.TROJAN" v-else-if="record.protocol === Protocols.TROJAN"
@@ -128,16 +154,7 @@
:data-source="getInboundClients(record)" :data-source="getInboundClients(record)"
:pagination="false" :pagination="false"
> >
{{template "client_row"}} {{template "client_table"}}
</a-table>
<a-table
v-else
:row-key="client => client.id"
:columns="innerOneColumns"
:data-source="record"
:pagination="false"
>
{{template "client_row"}}
</a-table> </a-table>
</template> </template>
</a-table> </a-table>
@@ -161,7 +178,7 @@
width: 40, width: 40,
scopedSlots: { customRender: 'enable' }, scopedSlots: { customRender: 'enable' },
}, { }, {
title: "Id", title: "ID",
align: 'center', align: 'center',
dataIndex: "id", dataIndex: "id",
width: 30, width: 30,
@@ -185,7 +202,7 @@
align: 'center', align: 'center',
width: 150, width: 150,
scopedSlots: { customRender: 'traffic' }, scopedSlots: { customRender: 'traffic' },
},{ }, {
title: '{{ i18n "pages.inbounds.transportConfig" }}', title: '{{ i18n "pages.inbounds.transportConfig" }}',
align: 'center', align: 'center',
width: 60, width: 60,
@@ -198,26 +215,20 @@
}]; }];
const innerColumns = [ const innerColumns = [
{ title: '', width: 70, scopedSlots: { customRender: 'actions' } }, { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'UID', width: 120, dataIndex: "id" }, { title: 'UID', width: 150, dataIndex: "id" },
]; ];
const innerTrojanColumns = [ const innerTrojanColumns = [
{ title: '', width: 70, scopedSlots: { customRender: 'actions' } }, { title: '{{ i18n "pages.inbounds.operate" }}', width: 70, scopedSlots: { customRender: 'actions' } },
{ title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 60, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 100, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'Password', width: 100, dataIndex: "password" }, { title: 'Password', width: 100, dataIndex: "password" },
]; ];
const innerOneColumns = [
{ title: '', width: 70, scopedSlots: { customRender: 'actions' } },
];
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
@@ -241,6 +252,7 @@
return; return;
} }
this.setInbounds(msg.obj); this.setInbounds(msg.obj);
this.searchKey = '';
}, },
setInbounds(dbInbounds) { setInbounds(dbInbounds) {
this.inbounds.splice(0); this.inbounds.splice(0);
@@ -281,14 +293,26 @@
case "qrcode": case "qrcode":
this.showQrcode(dbInbound); this.showQrcode(dbInbound);
break; break;
case "showInfo":
this.showInfo(dbInbound);
break;
case "edit": case "edit":
this.openEditInbound(dbInbound.id); this.openEditInbound(dbInbound.id);
break; break;
case "addClient":
this.openAddClient(dbInbound.id)
break;
case "addBulkClient":
this.openAddBulkClient(dbInbound.id)
break;
case "export":
this.inboundLinks(dbInbound.id);
break;
case "resetTraffic": case "resetTraffic":
this.resetTraffic(dbInbound); this.resetTraffic(dbInbound.id);
break; break;
case "delete": case "delete":
this.delInbound(dbInbound); this.delInbound(dbInbound.id);
break; break;
} }
}, },
@@ -305,8 +329,8 @@
isEdit: false isEdit: false
}); });
}, },
openEditInbound(dbInbound_id) { openEditInbound(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInbound_id); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
const inbound = dbInbound.toInbound(); const inbound = dbInbound.toInbound();
inModal.show({ inModal.show({
title: '{{ i18n "pages.inbounds.modifyInbound"}}', title: '{{ i18n "pages.inbounds.modifyInbound"}}',
@@ -335,9 +359,10 @@
port: inbound.port, port: inbound.port,
protocol: inbound.protocol, protocol: inbound.protocol,
settings: inbound.settings.toString(), settings: inbound.settings.toString(),
streamSettings: inbound.stream.toString(),
sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
}; };
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit('/xui/inbound/add', data, inModal); await this.submit('/xui/inbound/add', data, inModal);
}, },
async updateInbound(inbound, dbInbound) { async updateInbound(inbound, dbInbound) {
@@ -353,15 +378,80 @@
port: inbound.port, port: inbound.port,
protocol: inbound.protocol, protocol: inbound.protocol,
settings: inbound.settings.toString(), settings: inbound.settings.toString(),
streamSettings: inbound.stream.toString(),
sniffing: inbound.canSniffing() ? inbound.sniffing.toString() : '{}',
}; };
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal); await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
}, },
resetTraffic(dbInbound) { openAddClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientModal.show({
title: '{{ i18n "pages.client.add"}}',
okText: '{{ i18n "pages.client.submitAdd"}}',
dbInbound: dbInbound,
confirm: async (inbound, dbInbound, index) => {
clientModal.loading();
await this.addClient(inbound, dbInbound);
clientModal.close();
},
isEdit: false
});
},
openAddBulkClient(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clientsBulkModal.show({
title: '{{ i18n "pages.client.bulk"}} ' + dbInbound.remark,
okText: '{{ i18n "pages.client.bulk"}}',
dbInbound: dbInbound,
confirm: async (inbound, dbInbound) => {
clientsBulkModal.loading();
await this.addClient(inbound, dbInbound);
clientsBulkModal.close();
},
});
},
openEditClient(dbInboundId, client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
clients = this.getInboundClients(dbInbound);
index = this.findIndexOfClient(clients, client);
clientModal.show({
title: '{{ i18n "pages.client.edit"}}',
okText: '{{ i18n "pages.client.submitEdit"}}',
dbInbound: dbInbound,
index: index,
confirm: async (inbound, dbInbound, index) => {
clientModal.loading();
await this.updateClient(inbound, dbInbound, index);
clientModal.close();
},
isEdit: true
});
},
findIndexOfClient(clients,client) {
firstKey = Object.keys(client)[0];
return clients.findIndex(c => c[firstKey] === client[firstKey]);
},
async addClient(inbound, dbInbound) {
const data = {
id: dbInbound.id,
settings: inbound.settings.toString(),
};
await this.submit('/xui/inbound/addClient', data);
},
async updateClient(inbound, dbInbound, index) {
const data = {
id: dbInbound.id,
settings: inbound.settings.toString(),
};
await this.submit(`/xui/inbound/updateClient/${index}`, data);
},
resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => { onOk: () => {
@@ -372,27 +462,44 @@
}, },
}); });
}, },
exportAllLinks() { delInbound(dbInboundId) {
let copyText = '';
for (const dbInbound of this.dbInbounds) {
copyText += dbInbound.genInboundLinks
}
const clipboard = new ClipboardJS('.copy-btn', {
text: function () {
return copyText;
}
});
clipboard.on('success', () => { this.$message.success('Export Links succeed'); });
},
delInbound(dbInbound) {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}', title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/del/' + dbInbound.id), onOk: () => this.submit('/xui/inbound/del/' + dbInboundId),
}); });
}, },
delClient(dbInboundId,client) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
newDbInbound = new DBInbound(dbInbound);
inbound = newDbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client);
clients.splice(index, 1);
const data = {
id: dbInboundId,
settings: inbound.settings.toString(),
};
this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "delete"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delClient/' + client.email, data),
});
},
getClients(protocol, clientSettings) {
switch(protocol){
case Protocols.VMESS: return clientSettings.vmesses;
case Protocols.VLESS: return clientSettings.vlesses;
case Protocols.TROJAN: return clientSettings.trojans;
default: return null;
}
},
showQrcode(dbInbound, clientIndex) { showQrcode(dbInbound, clientIndex) {
const link = dbInbound.genLink(clientIndex); const link = dbInbound.genLink(clientIndex);
qrModal.show('{{ i18n "qrCode"}}', link, dbInbound); qrModal.show('{{ i18n "qrCode"}}', link, dbInbound);
@@ -400,11 +507,12 @@
showInfo(dbInbound, index) { showInfo(dbInbound, index) {
infoModal.show(dbInbound, index); infoModal.show(dbInbound, index);
}, },
switchEnable(dbInbound) { switchEnable(dbInboundId) {
this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound); dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
this.submit(`/xui/inbound/update/${dbInboundId}`, dbInbound);
}, },
async submit(url, data, modal) { async submit(url, data) {
const msg = await HttpUtil.postWithModal(url, data, modal); const msg = await HttpUtil.postWithModal(url, data);
if (msg.success) { if (msg.success) {
await this.getDBInbounds(); await this.getDBInbounds();
} }
@@ -418,93 +526,51 @@
return dbInbound.toInbound().settings.trojans return dbInbound.toInbound().settings.trojans
} }
}, },
resetClientTraffic(client,inbound,event) { resetClientTraffic(client,dbInboundId) {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}', title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}', content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}', okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: () => { onOk: () => this.submit('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ client.email),
this.resetClTraffic(client,inbound,event); })
},
});
},
async resetClTraffic(client,inbound,event) {
const msg = await HttpUtil.post('/xui/inbound/resetClientTraffic/'+ client.email);
if (!msg.success) {
return;
}
clientStats = inbound.clientStats
if(clientStats.length > 0)
{
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == client.email){
clientStats[key]['up'] = 0
clientStats[key]['down'] = 0
}
}
}
}
}, },
isExpiry(dbInbound, index) { isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index) return dbInbound.toInbound().isExpiry(index)
}, },
getUpStats(dbInbound, email) { getUpStats(dbInbound, email) {
clientStats = dbInbound.clientStats if(email.length == 0) return 0
if(clientStats.length > 0) clientStats = dbInbound.clientStats.find(stats => stats.email === email)
{ return clientStats ? clientStats.up : 0
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['up']
}
}
}
}, },
getDownStats(dbInbound, email) { getDownStats(dbInbound, email) {
clientStats = dbInbound.clientStats if(email.length == 0) return 0
if(clientStats.length > 0) clientStats = dbInbound.clientStats.find(stats => stats.email === email)
{ return clientStats ? clientStats.down : 0
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['down']
}
}
}
}, },
isTrafficExhausted(dbInbound, email) { isTrafficExhausted(dbInbound, email) {
clientStats = dbInbound.clientStats if(email.length == 0) return false
if(clientStats.length > 0) clientStats = dbInbound.clientStats.find(stats => stats.email === email)
{ return clientStats ? clientStats.down + clientStats.up > clientStats.total : false
for (const key in clientStats) {
if (Object.hasOwnProperty.call(clientStats, key)) {
if(clientStats[key]['email'] == email)
return clientStats[key]['down']+clientStats[key]['up'] > clientStats[key]['total']
}
}
}
}, },
isClientEnabled(dbInbound, email) { isClientEnabled(dbInbound, email) {
clientStats = dbInbound.clientStats clientStats = dbInbound.clientStats ? dbInbound.clientStats.find(stats => stats.email === email) : null
if(clientStats.length > 0) return clientStats ? clientStats['enable'] : true
{ },
for (const key in clientStats) { isRemovable(dbInbound_id){
if (Object.hasOwnProperty.call(clientStats, key)) { return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInbound_id)).length > 1
if(clientStats[key]['email'] == email) },
return clientStats[key]['enable'] inboundLinks(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
} txtModal.show('{{ i18n "pages.inbounds.export"}}',dbInbound.genInboundLinks,dbInbound.remark);
} },
} exportAllLinks() {
else{ let copyText = '';
return true for (const dbInbound of this.dbInbounds) {
copyText += dbInbound.genInboundLinks
} }
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
}, },
}, },
watch: { watch: {
@@ -518,13 +584,32 @@
computed: { computed: {
total() { total() {
let down = 0, up = 0; let down = 0, up = 0;
for (let i = 0; i < this.dbInbounds.length; ++i) { let clients = 0, active = 0, deactive = 0;
down += this.dbInbounds[i].down; this.dbInbounds.forEach(dbInbound => {
up += this.dbInbounds[i].up; down += dbInbound.down;
} up += dbInbound.up;
inbound = dbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
if(clients){
if(dbInbound.enable){
isClientEnable = false;
clients.forEach(client => {
isClientEnable = client.email == "" ? true: this.isClientEnabled(dbInbound,client.email);
isClientEnable ? active++ : deactive++;
});
} else {
deactive += clients.length;
}
} else {
dbInbound.enable ? active++ : deactive++;
}
});
return { return {
down: down, down: down,
up: up, up: up,
clients: active + deactive,
active: active,
deactive: deactive,
}; };
} }
}, },
@@ -537,5 +622,7 @@
{{template "qrcodeModal"}} {{template "qrcodeModal"}}
{{template "textModal"}} {{template "textModal"}}
{{template "inboundInfoModal"}} {{template "inboundInfoModal"}}
{{template "clientsModal"}}
{{template "clientsBulkModal"}}
</body> </body>
</html> </html>

View File

@@ -11,28 +11,34 @@
.ant-col-sm-24 { .ant-col-sm-24 {
margin-top: 10px; margin-top: 10px;
} }
.ant-card-dark h2 {
color: hsla(0,0%,100%,.65);
}
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak>
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout"> <a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/> <a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-card hoverable> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row> <a-row>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-row> <a-row>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color" :stroke-color="status.cpu.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.cpu.percent"></a-progress> :percent="status.cpu.percent"></a-progress>
<div>CPU</div> <div>CPU</div>
</a-col> </a-col>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color" :stroke-color="status.mem.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.mem.percent"></a-progress> :percent="status.mem.percent"></a-progress>
<div> <div>
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]] {{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
@@ -45,6 +51,7 @@
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.swap.color" :stroke-color="status.swap.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.swap.percent"></a-progress> :percent="status.swap.percent"></a-progress>
<div> <div>
Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]] Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
@@ -53,6 +60,7 @@
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.disk.color" :stroke-color="status.disk.color"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
:percent="status.disk.percent"></a-progress> :percent="status.disk.percent"></a-progress>
<div> <div>
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]] {{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
@@ -67,7 +75,7 @@
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "pages.index.xrayStatus" }}: {{ i18n "pages.index.xrayStatus" }}:
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag> <a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
<a-tooltip v-if="status.xray.state === State.Error"> <a-tooltip v-if="status.xray.state === State.Error">
@@ -76,14 +84,18 @@
</template> </template>
<a-icon type="question-circle" theme="filled"></a-icon> <a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip> </a-tooltip>
<a-tag color="green" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag> <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">[[ status.xray.version ]]</a-tag>
<a-tag color="blue" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch"}}</a-tag> <a-tag color="blue" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
<a-tag color="blue" style="cursor: pointer;" @click="openLogs">Logs</a-tag>
<a-tag color="green">3x-ui v{{ .cur_ver }}</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "pages.index.operationHours" }}: {{ i18n "pages.index.operationHours" }}:
<a-tag color="#87d068">[[ formatSecond(status.uptime) ]]</a-tag> <a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
{{ i18n "pages.index.operationHoursDesc" }} {{ i18n "pages.index.operationHoursDesc" }}
@@ -93,12 +105,12 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]] {{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
TCP / UDP {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]] TCP / UDP {{ i18n "pages.index.connectionCount" }}: [[ status.tcpCount ]] / [[ status.udpCount ]]
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
@@ -109,7 +121,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-icon type="arrow-up"></a-icon> <a-icon type="arrow-up"></a-icon>
@@ -135,7 +147,7 @@
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :md="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-icon type="cloud-upload"></a-icon> <a-icon type="cloud-upload"></a-icon>
@@ -166,7 +178,8 @@
</a-layout> </a-layout>
<a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}' <a-modal id="version-modal" v-model="versionModal.visible" title='{{ i18n "pages.index.xraySwitch" }}'
:closable="true" @ok="() => versionModal.visible = false" :closable="true" @ok="() => versionModal.visible = false"
ok-text='{{ i18n "confirm" }}' cancel-text='{{ i18n "cancel"}}'> :class="siderDrawer.isDarkTheme ? darkClass : ''"
footer="">
<h2>{{ i18n "pages.index.xraySwitchClick"}}</h2> <h2>{{ i18n "pages.index.xraySwitchClick"}}</h2>
<h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2> <h2>{{ i18n "pages.index.xraySwitchClickDesk"}}</h2>
<template v-for="version, index in versionModal.versions"> <template v-for="version, index in versionModal.versions">
@@ -176,6 +189,17 @@
</a-tag> </a-tag>
</template> </template>
</a-modal> </a-modal>
<a-modal id="log-modal" v-model="logModal.visible" title="X-UI logs"
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
:class="siderDrawer.isDarkTheme ? darkClass : ''"
width="800px"
footer="">
<table style="margin: 0px; width: 100%; background-color: black; color: hsla(0,0%,100%,.65);">
<tr v-for="log , index in logModal.logs">
<td style="vertical-align: top;">[[ index ]]</td><td>[[ log ]]</td>
</tr>
</table>
</a-modal>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
<script> <script>
@@ -269,6 +293,18 @@
}, },
}; };
const logModal = {
visible: false,
logs: '',
show(logs) {
this.visible = true;
this.logs = logs;
},
hide() {
this.visible = false;
},
};
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
@@ -276,6 +312,7 @@
siderDrawer, siderDrawer,
status: new Status(), status: new Status(),
versionModal, versionModal,
logModal,
spinning: false, spinning: false,
loadingTip: '{{ i18n "loading"}}', loadingTip: '{{ i18n "loading"}}',
}, },
@@ -307,6 +344,7 @@
title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}', title: '{{ i18n "pages.index.xraySwitchVersionDialog"}}',
content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`, content: '{{ i18n "pages.index.xraySwitchVersionDialogDesc"}}' + ` ${version}?`,
okText: '{{ i18n "confirm"}}', okText: '{{ i18n "confirm"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
cancelText: '{{ i18n "cancel"}}', cancelText: '{{ i18n "cancel"}}',
onOk: async () => { onOk: async () => {
versionModal.hide(); versionModal.hide();
@@ -316,6 +354,33 @@
}, },
}); });
}, },
//here add stop xray function
async stopXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/stopXrayService');
this.loading(false);
if (!msg.success) {
return;
}
},
//here add restart xray function
async restartXrayService() {
this.loading(true);
const msg = await HttpUtil.post('server/restartXrayService');
this.loading(false);
if (!msg.success) {
return;
}
},
async openLogs(){
this.loading(true);
const msg = await HttpUtil.post('server/logs');
this.loading(false);
if (!msg.success) {
return;
}
logModal.show(msg.obj);
}
}, },
async mounted() { async mounted() {
while (true) { while (true) {

View File

@@ -20,14 +20,14 @@
display: block; display: block;
} }
.ant-tabs-top-bar { :not(.ant-card-dark)>.ant-tabs-top-bar {
background: white; background: white;
} }
</style> </style>
<body> <body>
<a-layout id="app" v-cloak> <a-layout id="app" v-cloak>
{{ template "commonSider" . }} {{ template "commonSider" . }}
<a-layout id="content-layout"> <a-layout id="content-layout" :style="siderDrawer.isDarkTheme ? bgDarkStyle : ''">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip="loading"> <a-spin :spinning="spinning" :delay="500" tip="loading">
<a-space direction="vertical"> <a-space direction="vertical">
@@ -35,17 +35,17 @@
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.setting.save" }}</a-button> <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.setting.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.setting.restartPanel" }}</a-button> <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.setting.restartPanel" }}</a-button>
</a-space> </a-space>
<a-tabs default-active-key="1"> <a-tabs default-active-key="1" :class="siderDrawer.isDarkTheme ? darkClass : ''">
<a-tab-pane key="1" tab='{{ i18n "pages.setting.panelConfig"}}'> <a-tab-pane key="1" tab='{{ i18n "pages.setting.panelConfig"}}'>
<a-list item-layout="horizontal" style="background: white"> <a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.setting.panelListeningIP"}}' desc='{{ i18n "pages.setting.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item> <setting-list-item type="number" title='{{ i18n "pages.setting.panelPort"}}' desc='{{ i18n "pages.setting.panelPortDesc"}}' v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.setting.publicKeyPath"}}' desc='{{ i18n "pages.setting.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.setting.privateKeyPath"}}' desc='{{ i18n "pages.setting.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.setting.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<a-list-item> <a-list-item>
<a-row style="padding: 20px"> <a-row style="padding: 20px">
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<a-list-item-meta title="Language"/> <a-list-item-meta title="Language"/>
</a-col> </a-col>
@@ -56,9 +56,10 @@
ref="selectLang" ref="selectLang"
v-model="lang" v-model="lang"
@change="setLang(lang)" @change="setLang(lang)"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
style="width: 100%" style="width: 100%"
> >
<a-select-option :value="l.value" label="China" v-for="l in supportLangs" > <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span> <span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span> &nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option> </a-select-option>
@@ -71,7 +72,7 @@
</a-list> </a-list>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="2" tab='{{ i18n "pages.setting.userSetting"}}'> <a-tab-pane key="2" tab='{{ i18n "pages.setting.userSetting"}}'>
<a-form style="background: white; padding: 20px"> <a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'">
<a-form-item label='{{ i18n "pages.setting.oldUsername"}}'> <a-form-item label='{{ i18n "pages.setting.oldUsername"}}'>
<a-input v-model="user.oldUsername" style="max-width: 300px"></a-input> <a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
</a-form-item> </a-form-item>
@@ -87,26 +88,45 @@
style="max-width: 300px"></a-input> style="max-width: 300px"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<!-- <a-button type="primary" @click="updateUser">update</a-button>--> <!-- <a-button type="primary" @click="updateUser">Revise</a-button>-->
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button> <a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'> <a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
<a-list item-layout="horizontal" style="background: white"> <a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigTorrent"}}' desc='{{ i18n "pages.setting.xrayConfigTorrentDesc"}}' v-model="torrentSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPrivateIp"}}' desc='{{ i18n "pages.setting.xrayConfigPrivateIpDesc"}}' v-model="privateIpSettings"></setting-list-item>
<a-divider>{{ i18n "pages.setting.advancedTemplate"}}</a-divider>
<a-collapse>
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigInbounds"}}">
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigInbounds"}}' desc='{{ i18n "pages.setting.xrayConfigInboundsDesc"}}' v-model ="inboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigOutbounds"}}">
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigOutbounds"}}' desc='{{ i18n "pages.setting.xrayConfigOutboundsDesc"}}' v-model ="outboundSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header="{{ i18n "pages.setting.xrayConfigRoutings"}}">
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigRoutings"}}' desc='{{ i18n "pages.setting.xrayConfigRoutingsDesc"}}' v-model ="routingRuleSettings"></setting-list-item>
</a-collapse-panel>
</a-collapse>
<a-divider>{{ i18n "pages.setting.completeTemplate"}}</a-divider>
<setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item> <setting-list-item type="textarea" title='{{ i18n "pages.setting.xrayConfigTemplate"}}' desc='{{ i18n "pages.setting.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-list> </a-list>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="4" tab='{{ i18n "pages.setting.TGReminder"}}'> <a-tab-pane key="4" tab='{{ i18n "pages.setting.TGReminder"}}'>
<a-list item-layout="horizontal" style="background: white"> <a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="switch" title='{{ i18n "pages.setting.telegramBotEnable" }}' desc='{{ i18n "pages.setting.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.setting.telegramBotEnable" }}' desc='{{ i18n "pages.setting.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.setting.telegramToken"}}' desc='{{ i18n "pages.setting.telegramTokenDesc"}}' v-model="allSetting.tgBotToken"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model.number="allSetting.tgBotChatId"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.setting.telegramChatId"}}' desc='{{ i18n "pages.setting.telegramChatIdDesc"}}' v-model="allSetting.tgBotChatId"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.setting.telegramNotifyTime"}}' desc='{{ i18n "pages.setting.telegramNotifyTimeDesc"}}' v-model="allSetting.tgRunTime"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.tgNotifyBackup" }}' desc='{{ i18n "pages.setting.tgNotifyBackupDesc" }}' v-model="allSetting.tgBotBackup"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyExpireTimeDiff" }}' desc='{{ i18n "pages.setting.tgNotifyExpireTimeDiffDesc" }}' v-model="allSetting.tgExpireDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyTrafficDiff" }}' desc='{{ i18n "pages.setting.tgNotifyTrafficDiffDesc" }}' v-model="allSetting.tgTrafficDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.tgNotifyCpu" }}' desc='{{ i18n "pages.setting.tgNotifyCpuDesc" }}' v-model="allSetting.tgCpu" :min="0" :max="100"></setting-list-item>
</a-list> </a-list>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'> <a-tab-pane key="5" tab='{{ i18n "pages.setting.otherSetting"}}'>
<a-list item-layout="horizontal" style="background: white"> <a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<setting-list-item type="text" title='{{ i18n "pages.setting.timeZonee"}}' desc='{{ i18n "pages.setting.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.setting.timeZonee"}}' desc='{{ i18n "pages.setting.timeZoneDesc"}}' v-model="allSetting.timeLocation"></setting-list-item>
</a-list> </a-list>
</a-tab-pane> </a-tab-pane>
@@ -189,6 +209,102 @@
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting); this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
} }
}, },
computed: {
templateSettings: {
get: function () { return this.allSetting.xrayTemplateConfig ? JSON.parse(this.allSetting.xrayTemplateConfig) : null ; },
set: function (newValue) { this.allSetting.xrayTemplateConfig = JSON.stringify(newValue, null, 2) },
},
inboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.inbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.inbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
outboundSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.outbounds, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.outbounds = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.routing.rules = JSON.parse(newValue)
this.templateSettings = newTemplateSettings
},
},
torrentSettings: {
get: function () {
torrentFilter = false
if(this.templateSettings != null){
this.templateSettings.routing.rules.forEach(routingRule => {
if(routingRule.hasOwnProperty("protocol")){
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
torrentFilter = true
}
}
});
}
return torrentFilter
},
set: function (newValue) {
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
if (newValue){
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"protocol\": [\"bittorrent\"],\"type\": \"field\"}"))
}
else {
newTemplateSettings.routing.rules = [];
this.templateSettings.routing.rules.forEach(routingRule => {
if (routingRule.hasOwnProperty('protocol')){
if (routingRule.protocol[0] === "bittorrent" && routingRule.outboundTag == "blocked"){
return;
}
}
newTemplateSettings.routing.rules.push(routingRule);
});
}
this.templateSettings = newTemplateSettings
},
},
privateIpSettings: {
get: function () {
localIpFilter = false
if(this.templateSettings != null){
this.templateSettings.routing.rules.forEach(routingRule => {
if(routingRule.hasOwnProperty("ip")){
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
localIpFilter = true
}
}
});
}
return localIpFilter
},
set: function (newValue) {
newTemplateSettings = JSON.parse(this.allSetting.xrayTemplateConfig);
if (newValue){
newTemplateSettings.routing.rules.push(JSON.parse("{\"outboundTag\": \"blocked\",\"ip\": [\"geoip:private\"],\"type\": \"field\"}"))
}
else {
newTemplateSettings.routing.rules = [];
this.templateSettings.routing.rules.forEach(routingRule => {
if (routingRule.hasOwnProperty('ip')){
if (routingRule.ip[0] === "geoip:private" && routingRule.outboundTag == "blocked"){
return;
}
}
newTemplateSettings.routing.rules.push(routingRule);
});
}
this.templateSettings = newTemplateSettings
},
},
}
}); });
</script> </script>

View File

@@ -0,0 +1,30 @@
package job
import (
"fmt"
"time"
"x-ui/web/service"
"github.com/shirou/gopsutil/v3/cpu"
)
type CheckCpuJob struct {
tgbotService service.Tgbot
settingService service.SettingService
}
func NewCheckCpuJob() *CheckCpuJob {
return new(CheckCpuJob)
}
// Here run is a interface method of Job interface
func (j *CheckCpuJob) Run() {
threshold, _ := j.settingService.GetTgCpu()
// get latest status of server
percent, err := cpu.Percent(1*time.Second, false)
if err == nil && percent[0] > float64(threshold) {
msg := fmt.Sprintf("🔴 CPU usage %.2f%% is more than threshold %d%%", percent[0], threshold)
j.tgbotService.SendMsgToTgbotAdmins(msg)
}
}

View File

@@ -1,15 +1,7 @@
package job package job
import ( import (
"fmt"
"net"
"os"
"time"
"x-ui/logger"
"x-ui/util/common"
"x-ui/web/service" "x-ui/web/service"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
) )
type LoginStatus byte type LoginStatus byte
@@ -20,229 +12,18 @@ const (
) )
type StatsNotifyJob struct { type StatsNotifyJob struct {
enable bool xrayService service.XrayService
xrayService service.XrayService tgbotService service.Tgbot
inboundService service.InboundService
settingService service.SettingService
} }
func NewStatsNotifyJob() *StatsNotifyJob { func NewStatsNotifyJob() *StatsNotifyJob {
return new(StatsNotifyJob) return new(StatsNotifyJob)
} }
func (j *StatsNotifyJob) SendMsgToTgbot(msg string) {
//Telegram bot basic info
tgBottoken, err := j.settingService.GetTgBotToken()
if err != nil || tgBottoken == "" {
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
return
}
tgBotid, err := j.settingService.GetTgBotChatId()
if err != nil {
logger.Warning("sendMsgToTgbot failed,GetTgBotChatId fail:", err)
return
}
bot, err := tgbotapi.NewBotAPI(tgBottoken)
if err != nil {
fmt.Println("get tgbot error:", err)
return
}
bot.Debug = true
fmt.Printf("Authorized on account %s", bot.Self.UserName)
info := tgbotapi.NewMessage(int64(tgBotid), msg)
//msg.ReplyToMessageID = int(tgBotid)
bot.Send(info)
}
// Here run is a interface method of Job interface // Here run is a interface method of Job interface
func (j *StatsNotifyJob) Run() { func (j *StatsNotifyJob) Run() {
if !j.xrayService.IsXrayRunning() { if !j.xrayService.IsXrayRunning() {
return return
} }
var info string j.tgbotService.SendReport()
//get hostname
name, err := os.Hostname()
if err != nil {
fmt.Println("get hostname error:", err)
return
}
info = fmt.Sprintf("Hostname:%s\r\n", name)
//get ip address
var ip string
netInterfaces, err := net.Interfaces()
if err != nil {
fmt.Println("net.Interfaces failed, err:", err.Error())
return
}
for i := 0; i < len(netInterfaces); i++ {
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
addrs, _ := netInterfaces[i].Addrs()
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
ip = ipnet.IP.String()
break
} else {
ip = ipnet.IP.String()
break
}
}
}
}
}
info += fmt.Sprintf("IP:%s\r\n \r\n", ip)
// get traffic
inbouds, err := j.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("StatsNotifyJob run failed:", err)
return
}
// NOTE:If there no any sessions here,need to notify here
// TODO:Sub-node push, automatic conversion format
for _, inbound := range inbouds {
info += fmt.Sprintf("Node name:%s\r\nPort:%d\r\nUpload↑:%s\r\nDownload↓:%s\r\nTotal:%s\r\n", inbound.Remark, inbound.Port, common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down), common.FormatTraffic((inbound.Up + inbound.Down)))
if inbound.ExpiryTime == 0 {
info += fmt.Sprintf("Expire date:unlimited\r\n \r\n")
} else {
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
}
j.SendMsgToTgbot(info)
}
func (j *StatsNotifyJob) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
if username == "" || ip == "" || time == "" {
logger.Warning("UserLoginNotify failed,invalid info")
return
}
var msg string
// Get hostname
name, err := os.Hostname()
if err != nil {
fmt.Println("get hostname error:", err)
return
}
if status == LoginSuccess {
msg = fmt.Sprintf("Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
} else if status == LoginFail {
msg = fmt.Sprintf("Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
}
msg += fmt.Sprintf("Time:%s\r\n", time)
msg += fmt.Sprintf("Username:%s\r\n", username)
msg += fmt.Sprintf("IP:%s\r\n", ip)
j.SendMsgToTgbot(msg)
}
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "get_usage"),
),
)
func (j *StatsNotifyJob) OnReceive() *StatsNotifyJob {
tgBottoken, err := j.settingService.GetTgBotToken()
if err != nil || tgBottoken == "" {
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
return j
}
bot, err := tgbotapi.NewBotAPI(tgBottoken)
if err != nil {
fmt.Println("get tgbot error:", err)
return j
}
bot.Debug = false
u := tgbotapi.NewUpdate(0)
u.Timeout = 10
updates := bot.GetUpdatesChan(u)
for update := range updates {
if update.Message == nil {
if update.CallbackQuery != nil {
// Respond to the callback query, telling Telegram to show the user
// a message with the data received.
callback := tgbotapi.NewCallback(update.CallbackQuery.ID, update.CallbackQuery.Data)
if _, err := bot.Request(callback); err != nil {
logger.Warning(err)
}
// And finally, send a message containing the data received.
msg := tgbotapi.NewMessage(update.CallbackQuery.Message.Chat.ID, "")
switch update.CallbackQuery.Data {
case "get_usage":
msg.Text = "for get your usage send command like this : \n <code>/usage uuid | id</code> \n example : <code>/usage fc3239ed-8f3b-4151-ff51-b183d5182142</code>"
msg.ParseMode = "HTML"
}
if _, err := bot.Send(msg); err != nil {
logger.Warning(err)
}
}
continue
}
if !update.Message.IsCommand() { // ignore any non-command Messages
continue
}
// Create a new MessageConfig. We don't have text yet,
// so we leave it empty.
msg := tgbotapi.NewMessage(update.Message.Chat.ID, "")
// Extract the command from the Message.
switch update.Message.Command() {
case "help":
msg.Text = "What you need?"
msg.ReplyMarkup = numericKeyboard
case "start":
msg.Text = "Hi :) \n What you need?"
msg.ReplyMarkup = numericKeyboard
case "status":
msg.Text = "bot is ok."
case "usage":
msg.Text = j.getClientUsage(update.Message.CommandArguments())
default:
msg.Text = "I don't know that command, /help"
msg.ReplyMarkup = numericKeyboard
}
if _, err := bot.Send(msg); err != nil {
logger.Warning(err)
}
}
return j
}
func (j *StatsNotifyJob) getClientUsage(id string) string {
traffic, err := j.inboundService.GetClientTrafficById(id)
if err != nil {
logger.Warning(err)
return "something wrong!"
}
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = fmt.Sprintf("unlimited")
} else {
expiryTime = fmt.Sprintf("%s", time.Unix((traffic.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
total := ""
if traffic.Total == 0 {
total = fmt.Sprintf("unlimited")
} else {
total = fmt.Sprintf("%s", common.FormatTraffic((traffic.Total)))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Download↑: %s\r\n🔽 Upload↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
return output
} }

View File

@@ -28,11 +28,10 @@ func (j *XrayTrafficJob) Run() {
if err != nil { if err != nil {
logger.Warning("add traffic failed:", err) logger.Warning("add traffic failed:", err)
} }
err = j.inboundService.AddClientTraffic(clientTraffics) err = j.inboundService.AddClientTraffic(clientTraffics)
if err != nil { if err != nil {
logger.Warning("add client traffic failed:", err) logger.Warning("add client traffic failed:", err)
} }
} }

View File

@@ -54,7 +54,7 @@ func (s *InboundService) getClients(inbound *model.Inbound) ([]model.Client, err
settings := map[string][]model.Client{} settings := map[string][]model.Client{}
json.Unmarshal([]byte(inbound.Settings), &settings) json.Unmarshal([]byte(inbound.Settings), &settings)
if settings == nil { if settings == nil {
return nil, fmt.Errorf("Setting is null") return nil, fmt.Errorf("setting is null")
} }
clients := settings["clients"] clients := settings["clients"]
@@ -125,11 +125,18 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, err
return inbound, common.NewError("Duplicate email:", existEmail) return inbound, common.NewError("Duplicate email:", existEmail)
} }
clients, err := s.getClients(inbound)
if err != nil {
return inbound, err
}
db := database.GetDB() db := database.GetDB()
err = db.Save(inbound).Error err = db.Save(inbound).Error
if err == nil { if err == nil {
s.UpdateClientStat(inbound.Id, inbound.Settings) for _, client := range clients {
s.AddClientStat(inbound.Id, &client)
}
} }
return inbound, err return inbound, err
} }
@@ -168,6 +175,24 @@ func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
func (s *InboundService) DelInbound(id int) error { func (s *InboundService) DelInbound(id int) error {
db := database.GetDB() db := database.GetDB()
err := db.Where("inbound_id = ?", id).Delete(xray.ClientTraffic{}).Error
if err != nil {
return err
}
inbound, err := s.GetInbound(id)
if err != nil {
return err
}
clients, err := s.getClients(inbound)
if err != nil {
return err
}
for _, client := range clients {
err := s.DelClientIPs(db, client.Email)
if err != nil {
return err
}
}
return db.Delete(model.Inbound{}, id).Error return db.Delete(model.Inbound{}, id).Error
} }
@@ -216,11 +241,128 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
oldInbound.Sniffing = inbound.Sniffing oldInbound.Sniffing = inbound.Sniffing
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
s.UpdateClientStat(inbound.Id, inbound.Settings)
db := database.GetDB() db := database.GetDB()
return inbound, db.Save(oldInbound).Error return inbound, db.Save(oldInbound).Error
} }
func (s *InboundService) AddInboundClient(inbound *model.Inbound) error {
existEmail, err := s.checkEmailExistForInbound(inbound)
if err != nil {
return err
}
if existEmail != "" {
return common.NewError("Duplicate email:", existEmail)
}
clients, err := s.getClients(inbound)
if err != nil {
return err
}
oldInbound, err := s.GetInbound(inbound.Id)
if err != nil {
return err
}
oldClients, err := s.getClients(oldInbound)
if err != nil {
return err
}
oldInbound.Settings = inbound.Settings
if len(clients[len(clients)-1].Email) > 0 {
s.AddClientStat(inbound.Id, &clients[len(clients)-1])
}
for i := len(oldClients); i < len(clients); i++ {
if len(clients[i].Email) > 0 {
s.AddClientStat(inbound.Id, &clients[i])
}
}
db := database.GetDB()
return db.Save(oldInbound).Error
}
func (s *InboundService) DelInboundClient(inbound *model.Inbound, email string) error {
db := database.GetDB()
err := s.DelClientStat(db, email)
if err != nil {
logger.Error("Delete stats Data Error")
return err
}
oldInbound, err := s.GetInbound(inbound.Id)
if err != nil {
logger.Error("Load Old Data Error")
return err
}
oldInbound.Settings = inbound.Settings
err = s.DelClientIPs(db, email)
if err != nil {
logger.Error("Error in delete client IPs")
return err
}
return db.Save(oldInbound).Error
}
func (s *InboundService) UpdateInboundClient(inbound *model.Inbound, index int) error {
existEmail, err := s.checkEmailExistForInbound(inbound)
if err != nil {
return err
}
if existEmail != "" {
return common.NewError("Duplicate email:", existEmail)
}
clients, err := s.getClients(inbound)
if err != nil {
return err
}
oldInbound, err := s.GetInbound(inbound.Id)
if err != nil {
return err
}
oldClients, err := s.getClients(oldInbound)
if err != nil {
return err
}
oldInbound.Settings = inbound.Settings
db := database.GetDB()
if len(clients[index].Email) > 0 {
if len(oldClients[index].Email) > 0 {
err = s.UpdateClientStat(oldClients[index].Email, &clients[index])
if err != nil {
return err
}
err = s.UpdateClientIPs(db, oldClients[index].Email, clients[index].Email)
if err != nil {
return err
}
} else {
s.AddClientStat(inbound.Id, &clients[index])
}
} else {
err = s.DelClientStat(db, oldClients[index].Email)
if err != nil {
return err
}
err = s.DelClientIPs(db, oldClients[index].Email)
if err != nil {
return err
}
}
return db.Save(oldInbound).Error
}
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) { func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
if len(traffics) == 0 { if len(traffics) == 0 {
return nil return nil
@@ -275,14 +417,19 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
for _, traffic := range traffics { for _, traffic := range traffics {
inbound := &model.Inbound{} inbound := &model.Inbound{}
client := &xray.ClientTraffic{}
err := txInbound.Where("settings like ?", "%"+traffic.Email+"%").First(inbound).Error err := tx.Where("email = ?", traffic.Email).First(client).Error
traffic.InboundId = inbound.Id
if err != nil { if err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
// delete removed client record logger.Warning(err, traffic.Email)
clientErr := s.DelClientStat(tx, traffic.Email) }
logger.Warning(err, traffic.Email, clientErr) continue
}
err = txInbound.Where("id=?", client.InboundId).First(inbound).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err, traffic.Email)
} }
continue continue
@@ -297,7 +444,7 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
traffic.Total = client.TotalGB traffic.Total = client.TotalGB
} }
} }
if tx.Where("inbound_id = ?", inbound.Id).Where("email = ?", traffic.Email). if tx.Where("inbound_id = ? and email = ?", inbound.Id, traffic.Email).
UpdateColumns(map[string]interface{}{ UpdateColumns(map[string]interface{}{
"enable": true, "enable": true,
"expiry_time": traffic.ExpiryTime, "expiry_time": traffic.ExpiryTime,
@@ -336,68 +483,60 @@ func (s *InboundService) DisableInvalidClients() (int64, error) {
count := result.RowsAffected count := result.RowsAffected
return count, err return count, err
} }
func (s *InboundService) UpdateClientStat(inboundId int, inboundSettings string) error { func (s *InboundService) AddClientStat(inboundId int, client *model.Client) error {
db := database.GetDB() db := database.GetDB()
// get settings clients clientTraffic := xray.ClientTraffic{}
settings := map[string][]model.Client{} clientTraffic.InboundId = inboundId
json.Unmarshal([]byte(inboundSettings), &settings) clientTraffic.Email = client.Email
clients := settings["clients"] clientTraffic.Total = client.TotalGB
for _, client := range clients { clientTraffic.ExpiryTime = client.ExpiryTime
result := db.Model(xray.ClientTraffic{}). clientTraffic.Enable = true
Where("inbound_id = ? and email = ?", inboundId, client.Email). clientTraffic.Up = 0
Updates(map[string]interface{}{"enable": true, "total": client.TotalGB, "expiry_time": client.ExpiryTime}) clientTraffic.Down = 0
if result.RowsAffected == 0 { result := db.Create(&clientTraffic)
clientTraffic := xray.ClientTraffic{}
clientTraffic.InboundId = inboundId
clientTraffic.Email = client.Email
clientTraffic.Total = client.TotalGB
clientTraffic.ExpiryTime = client.ExpiryTime
clientTraffic.Enable = true
clientTraffic.Up = 0
clientTraffic.Down = 0
db.Create(&clientTraffic)
}
err := result.Error
if err != nil {
return err
}
}
return nil
}
func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error
}
func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {
db := database.GetDB()
InboundClientIps := &model.InboundClientIps{}
err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
if err != nil {
return "", err
}
return InboundClientIps.Ips, nil
}
func (s *InboundService) ClearClientIps(clientEmail string) (error) {
db := database.GetDB()
result := db.Model(model.InboundClientIps{}).
Where("client_email = ?", clientEmail).
Update("ips", "")
err := result.Error err := result.Error
if err != nil { if err != nil {
return err return err
} }
return nil return nil
} }
func (s *InboundService) ResetClientTraffic(clientEmail string) error { func (s *InboundService) UpdateClientStat(email string, client *model.Client) error {
db := database.GetDB() db := database.GetDB()
result := db.Model(xray.ClientTraffic{}). result := db.Model(xray.ClientTraffic{}).
Where("email = ?", clientEmail). Where("email = ?", email).
Updates(map[string]interface{}{"up": 0, "down": 0}) Updates(map[string]interface{}{
"enable": true,
"email": client.Email,
"total": client.TotalGB,
"expiry_time": client.ExpiryTime})
err := result.Error
if err != nil {
return err
}
return nil
}
func (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error {
return tx.Model(model.InboundClientIps{}).Where("client_email = ?", oldEmail).Update("client_email", newEmail).Error
}
func (s *InboundService) DelClientStat(tx *gorm.DB, email string) error {
return tx.Where("email = ?", email).Delete(xray.ClientTraffic{}).Error
}
func (s *InboundService) DelClientIPs(tx *gorm.DB, email string) error {
logger.Warning(email)
return tx.Where("client_email = ?", email).Delete(model.InboundClientIps{}).Error
}
func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
db := database.GetDB()
result := db.Model(xray.ClientTraffic{}).
Where("inbound_id = ? and email = ?", id, clientEmail).
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
err := result.Error err := result.Error
@@ -406,12 +545,40 @@ func (s *InboundService) ResetClientTraffic(clientEmail string) error {
} }
return nil return nil
} }
func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.ClientTraffic, err error) { func (s *InboundService) GetClientTrafficTgBot(tguname string) (traffic []*xray.ClientTraffic, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("email like ?", "%@"+tguname).Find(&traffics).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err)
return nil, err
}
}
return traffics, err
}
func (s *InboundService) GetClientTrafficByEmail(email string) (traffic []*xray.ClientTraffic, err error) {
db := database.GetDB()
var traffics []*xray.ClientTraffic
err = db.Model(xray.ClientTraffic{}).Where("email like ?", "%"+email+"%").Find(&traffics).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err)
return nil, err
}
}
return traffics, err
}
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
db := database.GetDB() db := database.GetDB()
inbound := &model.Inbound{} inbound := &model.Inbound{}
traffic = &xray.ClientTraffic{} traffic = &xray.ClientTraffic{}
err = db.Model(model.Inbound{}).Where("settings like ?", "%"+uuid+"%").First(inbound).Error err = db.Model(model.Inbound{}).Where("settings like ?", "%\""+query+"\"%").First(inbound).Error
if err != nil { if err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
logger.Warning(err) logger.Warning(err)
@@ -425,9 +592,17 @@ func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.Client
json.Unmarshal([]byte(inbound.Settings), &settings) json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"] clients := settings["clients"]
for _, client := range clients { for _, client := range clients {
if uuid == client.ID { if client.ID == query && client.Email != "" {
traffic.Email = client.Email traffic.Email = client.Email
break
} }
if client.Password == query && client.Email != "" {
traffic.Email = client.Email
break
}
}
if traffic.Email == "" {
return nil, err
} }
err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error err = db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(traffic).Error
if err != nil { if err != nil {
@@ -436,3 +611,36 @@ func (s *InboundService) GetClientTrafficById(uuid string) (traffic *xray.Client
} }
return traffic, err return traffic, err
} }
func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) {
db := database.GetDB()
InboundClientIps := &model.InboundClientIps{}
err := db.Model(model.InboundClientIps{}).Where("client_email = ?", clientEmail).First(InboundClientIps).Error
if err != nil {
return "", err
}
return InboundClientIps.Ips, nil
}
func (s *InboundService) ClearClientIps(clientEmail string) error {
db := database.GetDB()
result := db.Model(model.InboundClientIps{}).
Where("client_email = ?", clientEmail).
Update("ips", "")
err := result.Error
if err != nil {
return err
}
return nil
}
func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error) {
db := database.GetDB()
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Preload("ClientStats").Where("remark like ?", "%"+query+"%").Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return nil, err
}
return inbounds, nil
}

View File

@@ -9,7 +9,9 @@ import (
"io/fs" "io/fs"
"net/http" "net/http"
"os" "os"
"os/exec"
"runtime" "runtime"
"strings"
"time" "time"
"x-ui/logger" "x-ui/logger"
"x-ui/util/sys" "x-ui/util/sys"
@@ -143,7 +145,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
} else { } else {
logger.Warning("can not find io counters") logger.Warning("can not find io counters")
} }
status.TcpCount, err = sys.GetTCPCount() status.TcpCount, err = sys.GetTCPCount()
if err != nil { if err != nil {
logger.Warning("get tcp connections failed:", err) logger.Warning("get tcp connections failed:", err)
@@ -153,7 +155,7 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
if err != nil { if err != nil {
logger.Warning("get udp connections failed:", err) logger.Warning("get udp connections failed:", err)
} }
if s.xrayService.IsXrayRunning() { if s.xrayService.IsXrayRunning() {
status.Xray.State = Running status.Xray.State = Running
status.Xray.ErrorMsg = "" status.Xray.ErrorMsg = ""
@@ -198,6 +200,30 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
return versions, nil return versions, nil
} }
func (s *ServerService) StopXrayService() (string error) {
err := s.xrayService.StopXray()
if err != nil {
logger.Error("stop xray failed:", err)
return err
}
return nil
}
func (s *ServerService) RestartXrayService() (string error) {
s.xrayService.StopXray()
defer func() {
err := s.xrayService.RestartXray(true)
if err != nil {
logger.Error("start xray failed:", err)
}
}()
return nil
}
func (s *ServerService) downloadXRay(version string) (string, error) { func (s *ServerService) downloadXRay(version string) (string, error) {
osName := runtime.GOOS osName := runtime.GOOS
arch := runtime.GOARCH arch := runtime.GOARCH
@@ -300,3 +326,26 @@ func (s *ServerService) UpdateXray(version string) error {
return nil return nil
} }
func (s *ServerService) GetLogs() ([]string, error) {
// Define the journalctl command and its arguments
var cmdArgs []string
if runtime.GOOS == "linux" {
cmdArgs = []string{"journalctl", "-u", "x-ui", "--no-pager", "-n", "100"}
} else {
return []string{"Unsupported operating system"}, nil
}
// Run the command
cmd := exec.Command(cmdArgs[0], cmdArgs[1:]...)
var out bytes.Buffer
cmd.Stdout = &out
err := cmd.Run()
if err != nil {
return nil, err
}
lines := strings.Split(out.String(), "\n")
return lines, nil
}

View File

@@ -31,8 +31,12 @@ var defaultValueMap = map[string]string{
"timeLocation": "Asia/Tehran", "timeLocation": "Asia/Tehran",
"tgBotEnable": "false", "tgBotEnable": "false",
"tgBotToken": "", "tgBotToken": "",
"tgBotChatId": "0", "tgBotChatId": "",
"tgRunTime": "", "tgRunTime": "@daily",
"tgBotBackup": "false",
"tgExpireDiff": "0",
"tgTrafficDiff": "0",
"tgCpu": "0",
} }
type SettingService struct { type SettingService struct {
@@ -202,30 +206,62 @@ func (s *SettingService) SetTgBotToken(token string) error {
return s.setString("tgBotToken", token) return s.setString("tgBotToken", token)
} }
func (s *SettingService) GetTgBotChatId() (int, error) { func (s *SettingService) GetTgBotChatId() (string, error) {
return s.getInt("tgBotChatId") return s.getString("tgBotChatId")
} }
func (s *SettingService) SetTgBotChatId(chatId int) error { func (s *SettingService) SetTgBotChatId(chatIds string) error {
return s.setInt("tgBotChatId", chatId) return s.setString("tgBotChatId", chatIds)
}
func (s *SettingService) SetTgbotenabled(value bool) error {
return s.setBool("tgBotEnable", value)
} }
func (s *SettingService) GetTgbotenabled() (bool, error) { func (s *SettingService) GetTgbotenabled() (bool, error) {
return s.getBool("tgBotEnable") return s.getBool("tgBotEnable")
} }
func (s *SettingService) SetTgbotRuntime(time string) error { func (s *SettingService) SetTgbotenabled(value bool) error {
return s.setString("tgRunTime", time) return s.setBool("tgBotEnable", value)
} }
func (s *SettingService) GetTgbotRuntime() (string, error) { func (s *SettingService) GetTgbotRuntime() (string, error) {
return s.getString("tgRunTime") return s.getString("tgRunTime")
} }
func (s *SettingService) SetTgbotRuntime(time string) error {
return s.setString("tgRunTime", time)
}
func (s *SettingService) GetTgBotBackup() (bool, error) {
return s.getBool("tgBotBackup")
}
func (s *SettingService) SetTgBotBackup(value bool) error {
return s.setBool("tgBotBackup", value)
}
func (s *SettingService) GetTgExpireDiff() (int, error) {
return s.getInt("tgExpireDiff")
}
func (s *SettingService) SetTgExpireDiff(value int) error {
return s.setInt("tgExpireDiff", value)
}
func (s *SettingService) GetTgTrafficDiff() (int, error) {
return s.getInt("tgTrafficDiff")
}
func (s *SettingService) SetTgTrafficDiff(value int) error {
return s.setInt("tgTrafficDiff", value)
}
func (s *SettingService) GetTgCpu() (int, error) {
return s.getInt("tgCpu")
}
func (s *SettingService) SetTgCpu(value int) error {
return s.setInt("tgCpu", value)
}
func (s *SettingService) GetPort() (int, error) { func (s *SettingService) GetPort() (int, error) {
return s.getInt("webPort") return s.getInt("webPort")
} }

596
web/service/tgbot.go Normal file
View File

@@ -0,0 +1,596 @@
package service
import (
"fmt"
"net"
"os"
"strconv"
"strings"
"time"
"x-ui/config"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/common"
"x-ui/xray"
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
)
var bot *tgbotapi.BotAPI
var adminIds []int64
var isRunning bool
type LoginStatus byte
const (
LoginSuccess LoginStatus = 1
LoginFail LoginStatus = 0
)
type Tgbot struct {
inboundService InboundService
settingService SettingService
serverService ServerService
lastStatus *Status
}
func (t *Tgbot) NewTgbot() *Tgbot {
return new(Tgbot)
}
func (t *Tgbot) Start() error {
tgBottoken, err := t.settingService.GetTgBotToken()
if err != nil || tgBottoken == "" {
logger.Warning("Get TgBotToken failed:", err)
return err
}
tgBotid, err := t.settingService.GetTgBotChatId()
if err != nil {
logger.Warning("Get GetTgBotChatId failed:", err)
return err
}
for _, adminId := range strings.Split(tgBotid, ",") {
id, err := strconv.Atoi(adminId)
if err != nil {
logger.Warning("Failed to get IDs from GetTgBotChatId:", err)
return err
}
adminIds = append(adminIds, int64(id))
}
bot, err = tgbotapi.NewBotAPI(tgBottoken)
if err != nil {
fmt.Println("Get tgbot's api error:", err)
return err
}
bot.Debug = false
// listen for TG bot income messages
if !isRunning {
logger.Info("Starting Telegram receiver ...")
go t.OnReceive()
isRunning = true
}
return nil
}
func (t *Tgbot) IsRunnging() bool {
return isRunning
}
func (t *Tgbot) Stop() {
bot.StopReceivingUpdates()
logger.Info("Stop Telegram receiver ...")
isRunning = false
adminIds = nil
}
func (t *Tgbot) OnReceive() {
u := tgbotapi.NewUpdate(0)
u.Timeout = 10
updates := bot.GetUpdatesChan(u)
for update := range updates {
tgId := update.FromChat().ID
chatId := update.FromChat().ChatConfig().ChatID
isAdmin := checkAdmin(tgId)
if update.Message == nil {
if update.CallbackQuery != nil {
t.asnwerCallback(update.CallbackQuery, isAdmin)
}
} else {
if update.Message.IsCommand() {
t.answerCommand(update.Message, chatId, isAdmin)
}
}
}
}
func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin bool) {
msg := ""
// Extract the command from the Message.
switch message.Command() {
case "help":
msg = "This bot is providing you some specefic data from the server.\n\n Please choose:"
case "start":
msg = "Hello <i>" + message.From.FirstName + "</i> 👋"
if isAdmin {
hostname, _ := os.Hostname()
msg += "\nWelcome to <b>" + hostname + "</b> management bot"
}
msg += "\n\nI can do some magics for you, please choose:"
case "status":
msg = "bot is ok ✅"
case "usage":
if len(message.CommandArguments()) > 1 {
if isAdmin {
t.searchClient(chatId, message.CommandArguments())
} else {
t.searchForClient(chatId, message.CommandArguments())
}
} else {
msg = "❗Please provide a text for search!"
}
case "inbound":
if isAdmin {
t.searchInbound(chatId, message.CommandArguments())
} else {
msg = "❗ Unknown command"
}
default:
msg = "❗ Unknown command"
}
t.SendAnswer(chatId, msg, isAdmin)
}
func (t *Tgbot) asnwerCallback(callbackQuery *tgbotapi.CallbackQuery, isAdmin bool) {
// Respond to the callback query, telling Telegram to show the user
// a message with the data received.
callback := tgbotapi.NewCallback(callbackQuery.ID, callbackQuery.Data)
if _, err := bot.Request(callback); err != nil {
logger.Warning(err)
}
switch callbackQuery.Data {
case "get_usage":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getServerUsage())
case "inbounds":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getInboundUsages())
case "exhausted_soon":
t.SendMsgToTgbot(callbackQuery.From.ID, t.getExhausted())
case "get_backup":
t.sendBackup(callbackQuery.From.ID)
case "client_traffic":
t.getClientUsage(callbackQuery.From.ID, callbackQuery.From.UserName)
case "client_commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UID|Passowrd]</code>\r\n \r\nUse UID for vmess and vless and Password for Trojan.")
case "commands":
t.SendMsgToTgbot(callbackQuery.From.ID, "Search for a client email:\r\n<code>/usage email</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [remark]</code>")
}
}
func checkAdmin(tgId int64) bool {
for _, adminId := range adminIds {
if adminId == tgId {
return true
}
}
return false
}
func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
var numericKeyboard = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Server Usage", "get_usage"),
tgbotapi.NewInlineKeyboardButtonData("Get DB Backup", "get_backup"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Inbounds", "inbounds"),
tgbotapi.NewInlineKeyboardButtonData("Exhausted soon", "exhausted_soon"),
),
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Commands", "commands"),
),
)
var numericKeyboardClient = tgbotapi.NewInlineKeyboardMarkup(
tgbotapi.NewInlineKeyboardRow(
tgbotapi.NewInlineKeyboardButtonData("Get Usage", "client_traffic"),
tgbotapi.NewInlineKeyboardButtonData("Commands", "client_commands"),
),
)
msgConfig := tgbotapi.NewMessage(chatId, msg)
msgConfig.ParseMode = "HTML"
if isAdmin {
msgConfig.ReplyMarkup = numericKeyboard
} else {
msgConfig.ReplyMarkup = numericKeyboardClient
}
_, err := bot.Send(msgConfig)
if err != nil {
logger.Warning("Error sending telegram message :", err)
}
}
func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string) {
var allMessages []string
limit := 2000
// paging message if it is big
if len(msg) > limit {
messages := strings.Split(msg, "\r\n \r\n")
lastIndex := -1
for _, message := range messages {
if (len(allMessages) == 0) || (len(allMessages[lastIndex])+len(message) > limit) {
allMessages = append(allMessages, message)
lastIndex++
} else {
allMessages[lastIndex] += "\r\n \r\n" + message
}
}
} else {
allMessages = append(allMessages, msg)
}
for _, message := range allMessages {
info := tgbotapi.NewMessage(tgid, message)
info.ParseMode = "HTML"
_, err := bot.Send(info)
if err != nil {
logger.Warning("Error sending telegram message :", err)
}
time.Sleep(500 * time.Millisecond)
}
}
func (t *Tgbot) SendMsgToTgbotAdmins(msg string) {
for _, adminId := range adminIds {
t.SendMsgToTgbot(adminId, msg)
}
}
func (t *Tgbot) SendReport() {
runTime, err := t.settingService.GetTgbotRuntime()
if err == nil && len(runTime) > 0 {
t.SendMsgToTgbotAdmins("🕰 Scheduled reports: " + runTime + "\r\nDate-Time: " + time.Now().Format("2006-01-02 15:04:05"))
}
info := t.getServerUsage()
t.SendMsgToTgbotAdmins(info)
exhausted := t.getExhausted()
t.SendMsgToTgbotAdmins(exhausted)
backupEnable, err := t.settingService.GetTgBotBackup()
if err == nil && backupEnable {
for _, adminId := range adminIds {
t.sendBackup(int64(adminId))
}
}
}
func (t *Tgbot) getServerUsage() string {
var info string
//get hostname
name, err := os.Hostname()
if err != nil {
logger.Error("get hostname error:", err)
name = ""
}
info = fmt.Sprintf("💻 Hostname: %s\r\n", name)
info += fmt.Sprintf("🚀X-UI Version: %s\r\n", config.GetVersion())
//get ip address
var ip string
var ipv6 string
netInterfaces, err := net.Interfaces()
if err != nil {
logger.Error("net.Interfaces failed, err:", err.Error())
info += "🌐 IP: Unknown\r\n \r\n"
} else {
for i := 0; i < len(netInterfaces); i++ {
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
addrs, _ := netInterfaces[i].Addrs()
for _, address := range addrs {
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
if ipnet.IP.To4() != nil {
ip += ipnet.IP.String() + " "
} else if ipnet.IP.To16() != nil && !ipnet.IP.IsLinkLocalUnicast() {
ipv6 += ipnet.IP.String() + " "
}
}
}
}
}
info += fmt.Sprintf("🌐IP: %s\r\n🌐IPv6: %s\r\n", ip, ipv6)
}
// get latest status of server
t.lastStatus = t.serverService.GetStatus(t.lastStatus)
info += fmt.Sprintf("🔌Server Uptime: %d days\r\n", int(t.lastStatus.Uptime/86400))
info += fmt.Sprintf("📈Server Load: %.1f, %.1f, %.1f\r\n", t.lastStatus.Loads[0], t.lastStatus.Loads[1], t.lastStatus.Loads[2])
info += fmt.Sprintf("📋Server Memory: %s/%s\r\n", common.FormatTraffic(int64(t.lastStatus.Mem.Current)), common.FormatTraffic(int64(t.lastStatus.Mem.Total)))
info += fmt.Sprintf("🔹TcpCount: %d\r\n", t.lastStatus.TcpCount)
info += fmt.Sprintf("🔸UdpCount: %d\r\n", t.lastStatus.UdpCount)
info += fmt.Sprintf("🚦Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent+t.lastStatus.NetTraffic.Recv)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Sent)), common.FormatTraffic(int64(t.lastStatus.NetTraffic.Recv)))
info += fmt.Sprintf("Xray status: %s", t.lastStatus.Xray.State)
return info
}
func (t *Tgbot) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
if username == "" || ip == "" || time == "" {
logger.Warning("UserLoginNotify failed,invalid info")
return
}
var msg string
// Get hostname
name, err := os.Hostname()
if err != nil {
logger.Warning("get hostname error:", err)
return
}
if status == LoginSuccess {
msg = fmt.Sprintf("✅ Successfully logged-in to the panel\r\nHostname:%s\r\n", name)
} else if status == LoginFail {
msg = fmt.Sprintf("❗ Login to the panel was unsuccessful\r\nHostname:%s\r\n", name)
}
msg += fmt.Sprintf("⏰ Time:%s\r\n", time)
msg += fmt.Sprintf("🆔 Username:%s\r\n", username)
msg += fmt.Sprintf("🌐 IP:%s\r\n", ip)
t.SendMsgToTgbotAdmins(msg)
}
func (t *Tgbot) getInboundUsages() string {
info := ""
// get traffic
inbouds, err := t.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("GetAllInbounds run failed:", err)
info += "❌ Failed to get inbounds"
} else {
// NOTE:If there no any sessions here,need to notify here
// TODO:Sub-node push, automatic conversion format
for _, inbound := range inbouds {
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 {
info += "Expire date: ♾ Unlimited\r\n \r\n"
} else {
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
}
}
return info
}
func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserName)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if len(traffics) == 0 {
msg := "Your configuration is not found!\nPlease ask your Admin to use your telegram username in your configuration(s).\n\nYour username: <b>@" + tgUserName + "</b>"
t.SendMsgToTgbot(chatId, msg)
}
for _, traffic := range traffics {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
t.SendAnswer(chatId, "Please choose:", false)
}
func (t *Tgbot) searchClient(chatId int64, email string) {
traffics, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if len(traffics) == 0 {
msg := "No result!"
t.SendMsgToTgbot(chatId, msg)
return
}
for _, traffic := range traffics {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
}
func (t *Tgbot) searchInbound(chatId int64, remark string) {
inbouds, err := t.inboundService.SearchInbounds(remark)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
for _, inbound := range inbouds {
info := ""
info += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\n", inbound.Remark, inbound.Port)
info += fmt.Sprintf("Traffic: %s (↑%s,↓%s)\r\n", common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 {
info += "Expire date: ♾ Unlimited\r\n \r\n"
} else {
info += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
t.SendMsgToTgbot(chatId, info)
for _, traffic := range inbound.ClientStats {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
}
}
func (t *Tgbot) searchForClient(chatId int64, query string) {
traffic, err := t.inboundService.SearchClientTraffic(query)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if traffic == nil {
msg := "No result!"
t.SendMsgToTgbot(chatId, msg)
return
}
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output := fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire in: %s\r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
t.SendMsgToTgbot(chatId, output)
}
func (t *Tgbot) getExhausted() string {
trDiff := int64(0)
exDiff := int64(0)
now := time.Now().Unix() * 1000
var exhaustedInbounds []model.Inbound
var exhaustedClients []xray.ClientTraffic
var disabledInbounds []model.Inbound
var disabledClients []xray.ClientTraffic
output := ""
TrafficThreshold, err := t.settingService.GetTgTrafficDiff()
if err == nil && TrafficThreshold > 0 {
trDiff = int64(TrafficThreshold) * 1073741824
}
ExpireThreshold, err := t.settingService.GetTgExpireDiff()
if err == nil && ExpireThreshold > 0 {
exDiff = int64(ExpireThreshold) * 84600000
}
inbounds, err := t.inboundService.GetAllInbounds()
if err != nil {
logger.Warning("Unable to load Inbounds", err)
}
for _, inbound := range inbounds {
if inbound.Enable {
if (inbound.ExpiryTime > 0 && (inbound.ExpiryTime-now < exDiff)) ||
(inbound.Total > 0 && (inbound.Total-inbound.Up+inbound.Down < trDiff)) {
exhaustedInbounds = append(exhaustedInbounds, *inbound)
}
if len(inbound.ClientStats) > 0 {
for _, client := range inbound.ClientStats {
if client.Enable {
if (client.ExpiryTime > 0 && (client.ExpiryTime-now < exDiff)) ||
(client.Total > 0 && (client.Total-client.Up+client.Down < trDiff)) {
exhaustedClients = append(exhaustedClients, client)
}
} else {
disabledClients = append(disabledClients, client)
}
}
}
} else {
disabledInbounds = append(disabledInbounds, *inbound)
}
}
output += fmt.Sprintf("Exhausted Inbounds count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledInbounds), len(exhaustedInbounds))
if len(exhaustedInbounds) > 0 {
output += "Exhausted Inbounds:\r\n"
for _, inbound := range exhaustedInbounds {
output += fmt.Sprintf("📍Inbound:%s\r\nPort:%d\r\nTraffic: %s (↑%s,↓%s)\r\n", inbound.Remark, inbound.Port, common.FormatTraffic((inbound.Up + inbound.Down)), common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down))
if inbound.ExpiryTime == 0 {
output += "Expire date: ♾Unlimited\r\n \r\n"
} else {
output += fmt.Sprintf("Expire date:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
}
}
}
output += fmt.Sprintf("Exhausted Clients count:\r\n🛑 Disabled: %d\r\n🔜 Exhaust soon: %d\r\n \r\n", len(disabledClients), len(exhaustedClients))
if len(exhaustedClients) > 0 {
output += "Exhausted Clients:\r\n"
for _, traffic := range exhaustedClients {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else {
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
}
total := ""
if traffic.Total == 0 {
total = "♾Unlimited"
} else {
total = common.FormatTraffic((traffic.Total))
}
output += fmt.Sprintf("💡 Active: %t\r\n📧 Email: %s\r\n🔼 Upload↑: %s\r\n🔽 Download↓: %s\r\n🔄 Total: %s / %s\r\n📅 Expire date: %s\r\n \r\n",
traffic.Enable, traffic.Email, common.FormatTraffic(traffic.Up), common.FormatTraffic(traffic.Down), common.FormatTraffic((traffic.Up + traffic.Down)),
total, expiryTime)
}
}
return output
}
func (t *Tgbot) sendBackup(chatId int64) {
sendingTime := time.Now().Format("2006-01-02 15:04:05")
t.SendMsgToTgbot(chatId, "Backup time: "+sendingTime)
file := tgbotapi.FilePath(config.GetDBPath())
msg := tgbotapi.NewDocument(chatId, file)
_, err := bot.Send(msg)
if err != nil {
logger.Warning("Error in uploading backup: ", err)
}
file = tgbotapi.FilePath(xray.GetConfigPath())
msg = tgbotapi.NewDocument(chatId, file)
_, err = bot.Send(msg)
if err != nil {
logger.Warning("Error in uploading config.json: ", err)
}
}

View File

@@ -160,5 +160,5 @@ func (s *XrayService) SetToNeedRestart() {
} }
func (s *XrayService) IsNeedRestartAndSetFalse() bool { func (s *XrayService) IsNeedRestartAndSetFalse() bool {
return isNeedXrayRestart.CAS(true, false) return isNeedXrayRestart.CompareAndSwap(true, false)
} }

View File

@@ -1,5 +1,5 @@
"username" = "username" "username" = "Username"
"password" = "password" "password" = "Password"
"login" = "Login" "login" = "Login"
"confirm" = "Confirm" "confirm" = "Confirm"
"cancel" = "Cancel" "cancel" = "Cancel"
@@ -10,6 +10,8 @@
"remark" = "Remark" "remark" = "Remark"
"enable" = "Enable" "enable" = "Enable"
"protocol" = "Protocol" "protocol" = "Protocol"
"search" = "Search"
"loading" = "Loading" "loading" = "Loading"
"second" = "Second" "second" = "Second"
"minute" = "Minute" "minute" = "Minute"
@@ -20,6 +22,7 @@
"unlimited" = "Unlimited" "unlimited" = "Unlimited"
"none" = "None" "none" = "None"
"qrCode" = "QR Code" "qrCode" = "QR Code"
"info" = "More information"
"edit" = "Edit" "edit" = "Edit"
"delete" = "Delete" "delete" = "Delete"
"reset" = "Reset" "reset" = "Reset"
@@ -32,19 +35,16 @@
"camouflage" = "Camouflage" "camouflage" = "Camouflage"
"enabled" = "Enabled" "enabled" = "Enabled"
"disabled" = "Disabled" "disabled" = "Disabled"
"domainName" = "Domain Name" "domainName" = "Domain name"
"additional" = "Alter" "additional" = "Alter"
"monitor" = "Listen IP" "monitor" = "Listen IP"
"certificate" = "Certificate" "certificate" = "Certificat"
"fail" = "Fail" "fail" = "Fail"
"success" = "Success" "success" = " Success"
"getVersion" = "Get Version" "getVersion" = "Get version"
"install" = "Install" "install" = "Install"
"used" = "Used"
"clients" = "Clients" "clients" = "Clients"
"search" = "Search"
"usage" = "Usage" "usage" = "Usage"
"info" = "Details"
[menu] [menu]
"dashboard" = "System Status" "dashboard" = "System Status"
@@ -61,15 +61,16 @@
"invalidFormData" = "Input Data Format Is Invalid" "invalidFormData" = "Input Data Format Is Invalid"
"emptyUsername" = "Please Enter Username" "emptyUsername" = "Please Enter Username"
"emptyPassword" = "Please Enter Password" "emptyPassword" = "Please Enter Password"
"wrongUsernameOrPassword" = "invalid username or password" "wrongUsernameOrPassword" = "Invalid username or password"
"successLogin" = "Login" "successLogin" = "Login"
[pages.index] [pages.index]
"title" = "System Status" "title" = "System Status"
"memory" = "Memory" "memory" = "Memory"
"hard" = "Hard Disk" "hard" = "Hard Disk"
"xrayStatus" = "Xray Status" "xrayStatus" = "Xray Status"
"stopXray" = "Stop"
"restartXray" = "Restart"
"xraySwitch" = "Switch Version" "xraySwitch" = "Switch Version"
"xraySwitchClick" = "Click on the version you want to switch" "xraySwitchClick" = "Click on the version you want to switch"
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations" "xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
@@ -82,15 +83,15 @@
"downSpeed" = "Total download speed for all network cards" "downSpeed" = "Total download speed for all network cards"
"totalSent" = "Total upload traffic of all network cards since system startup" "totalSent" = "Total upload traffic of all network cards since system startup"
"totalReceive" = "Total download traffic of all network cards since system startup" "totalReceive" = "Total download traffic of all network cards since system startup"
"xraySwitchVersionDialog" = "switch xray version" "xraySwitchVersionDialog" = "Switch xray version"
"xraySwitchVersionDialogDesc" = "whether to switch the xray version to" "xraySwitchVersionDialogDesc" = "Whether to switch the xray version to"
"dontRefreshh" = "Installation is in progress, please do not refresh this page" "dontRefreshh" = "Installation is in progress, please do not refresh this page"
[pages.inbounds] [pages.inbounds]
"title" = "Inbounds" "title" = "Inbounds"
"totalDownUp" = "Total Uploads/Downloads" "totalDownUp" = "Total uploads/downloads"
"totalUsage" = "Total Usage" "totalUsage" = "Total usage"
"inboundCount" = "Number Of Inbound" "inboundCount" = "Number of inbound"
"operate" = "Actions" "operate" = "Actions"
"enable" = "Enable" "enable" = "Enable"
"remark" = "Remark" "remark" = "Remark"
@@ -99,11 +100,11 @@
"traffic" = "Traffic" "traffic" = "Traffic"
"details" = "Details" "details" = "Details"
"transportConfig" = "Transport" "transportConfig" = "Transport"
"expireDate" = "Expire Date" "expireDate" = "Expire date"
"resetTraffic" = "Reset Traffic" "resetTraffic" = "Reset traffic"
"addInbound" = "Add Inbound" "addInbound" = "Add Inbound"
"addTo" = "Add To" "addTo" = "Add To"
"revise" = "Save" "revise" = "Revise"
"modifyInbound" = "Modify InBound" "modifyInbound" = "Modify InBound"
"deleteInbound" = "Delete Inbound" "deleteInbound" = "Delete Inbound"
"deleteInboundContent" = "Are you sure you want to delete inbound?" "deleteInboundContent" = "Are you sure you want to delete inbound?"
@@ -112,44 +113,56 @@
"address" = "Address" "address" = "Address"
"network" = "Network" "network" = "Network"
"destinationPort" = "Destination port" "destinationPort" = "Destination port"
"targetAddress" = "Target Address" "targetAddress" = "Target address"
"disableInsecureEncryption" = "Disable insecure encryption" "disableInsecureEncryption" = "Disable insecure encryption"
"monitorDesc" = "Leave blank by default" "monitorDesc" = "Leave blank by default"
"meansNoLimit" = "Means No Limit" "meansNoLimit" = "Means no limit"
"totalFlow" = "Total Traffic" "totalFlow" = "Total flow"
"leaveBlankToNeverExpire" = "Leave blank to never expire" "leaveBlankToNeverExpire" = "Leave blank to never expire"
"noRecommendKeepDefault" = "There are no special requirements to keep the default" "noRecommendKeepDefault" = "There are no special requirements to keep the default"
"certificatePath" = "Certificate File Path" "certificatePath" = "Certificate file path"
"certificateContent" = "Certificate File Content" "certificateContent" = "Certificate file content"
"publicKeyPath" = "Public Key File Path" "publicKeyPath" = "Public key path"
"publicKeyContent" = "public Key Content" "publicKeyContent" = "Public key content"
"keyPath" = "Key File Path" "keyPath" = "Private Key path"
"keyContent" = "Key Content" "keyContent" = "Private Key content"
"clickOnQRcode" = "Click on QR Code to Copy"
"client" = "Client" "client" = "Client"
"uid" = "UID" "export" = "Export links"
[pages.client]
"add" = "Add client"
"edit" = "Edit client"
"submitAdd" = "Add client"
"submitEdit" = "Save changes"
"clientCount" = "Number of clients"
"bulk" = "Add bulk"
"method" = "Method"
"first" = "First"
"last" = "Last"
"prefix" = "Prefix"
"postfix" = "postfix"
[pages.inbounds.toasts] [pages.inbounds.toasts]
"obtain" = "Obtain" "obtain" = "Obtain"
[pages.inbounds.stream.general] [pages.inbounds.stream.general]
"requestHeader" = "Request Header" "requestHeader" = "Request header"
"name" = "Name" "name" = "Name"
"value" = "Value" "value" = "Value"
[pages.inbounds.stream.tcp] [pages.inbounds.stream.tcp]
"requestVersion" = "Request Version" "requestVersion" = "Request version"
"requestMethod" = "Request Method" "requestMethod" = "Request method"
"requestPath" = "Request Path" "requestPath" = "Request path"
"responseVersion" = "Response Version" "responseVersion" = "Response version"
"responseStatus" = "Response Status" "responseStatus" = "Response status"
"responseStatusDescription" = "Response Status Description" "responseStatusDescription" = "Response status description"
"responseHeader" = "Response Header" "responseHeader" = "Response header"
[pages.inbounds.stream.quic] [pages.inbounds.stream.quic]
"encryption" = "Encryption" "encryption" = "Encryption"
[pages.setting] [pages.setting]
"title" = "Setting" "title" = "Setting"
"save" = "Save" "save" = "Save"
@@ -157,7 +170,7 @@
"restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information" "restartPanelDesc" = "Are you sure you want to restart the panel? Click OK to restart after 3 seconds. If you cannot access the panel after restarting, please go to the server to view the panel log information"
"panelConfig" = "Panel Configuration" "panelConfig" = "Panel Configuration"
"userSetting" = "User Setting" "userSetting" = "User Setting"
"xrayConfiguration" = "Xray Configuration" "xrayConfiguration" = "xray Configuration"
"TGReminder" = "TG Reminder Related Settings" "TGReminder" = "TG Reminder Related Settings"
"otherSetting" = "Other Setting" "otherSetting" = "Other Setting"
"panelListeningIP" = "Panel listening IP" "panelListeningIP" = "Panel listening IP"
@@ -174,22 +187,42 @@
"currentPassword" = "Current Password" "currentPassword" = "Current Password"
"newUsername" = "New Username" "newUsername" = "New Username"
"newPassword" = "New Password" "newPassword" = "New Password"
"xrayConfigTemplate" = "xray Configuration Template" "advancedTemplate" = "Advanced template parts"
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect" "completeTemplate" = "Complete template of Xray configuration"
"xrayConfigTemplate" = "Xray Configuration Template"
"xrayConfigTemplateDesc" = "Generate the final xray configuration file based on this template, restart the panel to take effect."
"xrayConfigTorrent" = "Ban bittorrent usage"
"xrayConfigTorrentDesc" = "Change the configuration temlate to avoid using bittorrent by users, restart the panel to take effect"
"xrayConfigPrivateIp" = "Ban private ip range to connect"
"xrayConfigPrivateIpDesc" = "Change the configuration temlate to avoid connecting with private IP ranges, restart the panel to take effect"
"xrayConfigInbounds" = "Configuration of Inbounds"
"xrayConfigInboundsDesc" = "Change the configuration temlate to accept special clients, restart the panel to take effect"
"xrayConfigOutbounds" = "Configuration of Outbounds"
"xrayConfigOutboundsDesc" = "Change the configuration temlate to define outgoing ways for this server, restart the panel to take effect"
"xrayConfigRoutings" = "Configuration of Routing rules"
"xrayConfigRoutingsDesc" = "Change the configuration temlate to define Routing rules for this server, restart the panel to take effect"
"telegramBotEnable" = "Enable telegram bot" "telegramBotEnable" = "Enable telegram bot"
"telegramBotEnableDesc" = "Restart the panel to take effect" "telegramBotEnableDesc" = "Restart the panel to take effect"
"telegramToken" = "Telegram Token" "telegramToken" = "Telegram Token"
"telegramTokenDesc" = "Restart the panel to take effect" "telegramTokenDesc" = "Restart the panel to take effect"
"telegramChatId" = "Telegram ChatId" "telegramChatId" = "Telegram Admin ChatIds"
"telegramChatIdDesc" = "Restart the panel to take effect" "telegramChatIdDesc" = "Multi chatIDs separated by comma. Restart the panel to take effect"
"telegramNotifyTime" = "Telegram bot notification time" "telegramNotifyTime" = "Telegram bot notification time"
"telegramNotifyTimeDesc" = "Using Crontab timing format, restart the panel to take effect" "telegramNotifyTimeDesc" = "Using Crontab timing format. Restart the panel to take effect"
"tgNotifyBackup" = "Database backup"
"tgNotifyBackupDesc" = "Sending database backup file with report notification. Restart the panel to take effect"
"tgNotifyExpireTimeDiff" = "Remained time threshold"
"tgNotifyExpireTimeDiffDesc" = "This telegram bot will send you a notification before expiration (unit:day)"
"tgNotifyTrafficDiff" = "Remained traffic threshold"
"tgNotifyTrafficDiffDesc" = "This telegram bot will send you a notification before finishing traffic (unit:GB)"
"tgNotifyCpu" = "CPU percentage alert threshold"
"tgNotifyCpuDesc" = "This telegram bot will send you a notification if CPU usage is more than this percentage (unit:%)"
"timeZonee" = "Time Zone" "timeZonee" = "Time Zone"
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect" "timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"
[pages.setting.toasts] [pages.setting.toasts]
"modifySetting" = "modify setting" "modifySetting" = "Modify setting"
"getSetting" = "get setting" "getSetting" = "Get setting"
"modifyUser" = "modify user" "modifyUser" = "Modify user"
"originalUserPassIncorrect" = "The original user name or original password is incorrect" "originalUserPassIncorrect" = "The original user name or original password is incorrect"
"userPassMustBeNotEmpty" = "New username and new password cannot be empty" "userPassMustBeNotEmpty" = "New username and new password cannot be empty"

View File

@@ -10,6 +10,8 @@
"remark" = "نام" "remark" = "نام"
"enable" = "فعال" "enable" = "فعال"
"protocol" = "پروتکل" "protocol" = "پروتکل"
"search" = "جستجو"
"loading" = "در حال بروزرسانی..." "loading" = "در حال بروزرسانی..."
"second" = "ثانیه" "second" = "ثانیه"
"minute" = "دقیقه" "minute" = "دقیقه"
@@ -20,6 +22,7 @@
"unlimited" = "نامحدود" "unlimited" = "نامحدود"
"none" = "هیچ" "none" = "هیچ"
"qrCode" = "QR کد" "qrCode" = "QR کد"
"info" = "اطلاعات بیشتر"
"edit" = "ویرایش" "edit" = "ویرایش"
"delete" = "حذف" "delete" = "حذف"
"reset" = "ریست" "reset" = "ریست"
@@ -30,8 +33,8 @@
"host" = "آدرس" "host" = "آدرس"
"path" = "مسیر" "path" = "مسیر"
"camouflage" = "استتار" "camouflage" = "استتار"
"enabled" = "فعال شد" "enabled" = "فعال"
"disabled" = "غیرفعال شد" "disabled" = "غیرفعال"
"domainName" = "آدرس دامنه" "domainName" = "آدرس دامنه"
"additional" = "آی دی جایگزین" "additional" = "آی دی جایگزین"
"monitor" = "آی پی اتصال" "monitor" = "آی پی اتصال"
@@ -40,11 +43,8 @@
"success" = " موفق" "success" = " موفق"
"getVersion" = "دریافت ورژن" "getVersion" = "دریافت ورژن"
"install" = "نصب" "install" = "نصب"
"used" = "استفاده شده"
"clients" = "کاربران" "clients" = "کاربران"
"search" = "جستجو"
"usage" = "استفاده" "usage" = "استفاده"
"info" = "جزئیات"
[menu] [menu]
"dashboard" = "وضعیت سیستم" "dashboard" = "وضعیت سیستم"
@@ -64,18 +64,19 @@
"wrongUsernameOrPassword" = "نام کاربری و رمز عبور اشتباه میباشد" "wrongUsernameOrPassword" = "نام کاربری و رمز عبور اشتباه میباشد"
"successLogin" = "خوش آمدید" "successLogin" = "خوش آمدید"
[pages.index] [pages.index]
"title" = "وضعیت سیستم" "title" = "وضعیت سیستم"
"memory" = "حافظه رم" "memory" = "حافظه رم"
"hard" = "حافظه دیسک" "hard" = "حافظه دیسک"
"xrayStatus" = "وضعیت Xray" "xrayStatus" = "وضعیت Xray"
"stopXray" = "توقف"
"restartXray" = "شروع مجدد"
"xraySwitch" = "تغییر ورژن" "xraySwitch" = "تغییر ورژن"
"xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید" "xraySwitchClick" = "ورژن مورد نظر را انتخاب کنید"
"xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ." "xraySwitchClickDesk" = "لطفا با دقت انتخاب کنید ، در صورت انتخاب اشتباه امکان قطعی سیستم وجود دارد ."
"operationHours" = "ساعت فعال" "operationHours" = "ساعت فعال"
"operationHoursDesc" = "ساعت فعال بعد از شروع سیستم" "operationHoursDesc" = "ساعت فعال بعد از شروع سیستم"
"systemLoad" = "سرعت لود سیستم" "systemLoad" = "بار روی سیستم"
"connectionCount" = "تعداد کانکشن ها" "connectionCount" = "تعداد کانکشن ها"
"connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه" "connectionCountDesc" = "تعداد کانکشن ها برای کل شبکه"
"upSpeed" = "سرعت آپلود در حال حاضر سیستم" "upSpeed" = "سرعت آپلود در حال حاضر سیستم"
@@ -86,7 +87,6 @@
"xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین" "xraySwitchVersionDialogDesc" = "آیا از تغییر ورژن مطمئن هستین"
"dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید " "dontRefreshh" = "در حال نصب ، لطفا رفرش نکنید "
[pages.inbounds] [pages.inbounds]
"title" = "کاربران" "title" = "کاربران"
"totalDownUp" = "جمع آپلود/دانلود" "totalDownUp" = "جمع آپلود/دانلود"
@@ -104,7 +104,7 @@
"resetTraffic" = "ریست ترافیک" "resetTraffic" = "ریست ترافیک"
"addInbound" = "اضافه کردن سرویس" "addInbound" = "اضافه کردن سرویس"
"addTo" = "اضافه کردن" "addTo" = "اضافه کردن"
"revise" = "ذخیره" "revise" = "ویرایش"
"modifyInbound" = "ویرایش سرویس" "modifyInbound" = "ویرایش سرویس"
"deleteInbound" = "حذف سرویس" "deleteInbound" = "حذف سرویس"
"deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟" "deleteInboundContent" = "آیا مطمئن به حذف سرویس هستید ؟"
@@ -126,8 +126,22 @@
"publicKeyContent" = "محتوای Certificate.crt" "publicKeyContent" = "محتوای Certificate.crt"
"keyPath" = "مسیر فایل Private.key" "keyPath" = "مسیر فایل Private.key"
"keyContent" = "محتوای Private.key" "keyContent" = "محتوای Private.key"
"clickOnQRcode" = "برای کپی بر روی کد تصویری کلیک کنید"
"client" = "کاربر" "client" = "کاربر"
"uid" = "UID" "export" = "استخراج لینک‌ها"
[pages.client]
"add" = "کاربر جدید"
"edit" = "ویرایش کاربر"
"submitAdd" = "اضافه کردن"
"submitEdit" = "ذخیره تغییرات"
"clientCount" = "تعداد کاربران"
"bulk" = "انبوه سازی"
"method" = "روش"
"first" = "از"
"last" = "تا"
"prefix" = "پیشوند"
"postfix" = "پسوند"
[pages.inbounds.toasts] [pages.inbounds.toasts]
"obtain" = "Obtain" "obtain" = "Obtain"
@@ -149,7 +163,6 @@
[pages.inbounds.stream.quic] [pages.inbounds.stream.quic]
"encryption" = "رمزنگاری" "encryption" = "رمزنگاری"
[pages.setting] [pages.setting]
"title" = "تنظیمات" "title" = "تنظیمات"
"save" = "ذخیره" "save" = "ذخیره"
@@ -174,16 +187,36 @@
"currentPassword" = "رمز عبور فعلی" "currentPassword" = "رمز عبور فعلی"
"newUsername" = "نام کاربری جدید" "newUsername" = "نام کاربری جدید"
"newPassword" = "رمز عبور جدید" "newPassword" = "رمز عبور جدید"
"xrayConfigTemplate" = "تنظیمات قالب Xray" "advancedTemplate" = "بخش های پیشرفته الگو"
"xrayConfigTemplateDesc" = "فایل پیکربندی xray نهایی را بر اساس این الگو ایجاد کنید. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود" "completeTemplate" = "الگوی کامل تنظیمات ایکس ری"
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
"xrayConfigTorrentDesc" = "الگوی تنظیمات را برای فیلتر کردن پروتکل بیت تورنت برای کاربران تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigPrivateIp" = "جلوگیری از اتصال آی پی های نامعتبر"
"xrayConfigPrivateIpDesc" = "الگوی تنظیمات را برای فیلتر کردن اتصال آی پی های نامعتبر و بسته های سرگردان تغییر میدهد. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigInbounds" = "تنظیمات ورودی"
"xrayConfigInboundsDesc" = "میتوانید الگوی تنظیمات را برای ورودی های خاص تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigOutbounds" = "تنظیمات خروجی"
"xrayConfigOutboundsDesc" = "میتوانید الگوی تنظیمات را برای خروجی اینترنت تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigRoutings" = "تنظیمات قوانین مسیریابی"
"xrayConfigRoutingsDesc" = "میتوانید الگوی تنظیمات را برای مسیریابی تنظیم نمایید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramBotEnable" = "فعالسازی ربات تلگرام" "telegramBotEnable" = "فعالسازی ربات تلگرام"
"telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود" "telegramBotEnableDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramToken" = "توکن تلگرام" "telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود" "telegramTokenDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramChatId" = "آی دی تلگرام مدیریت . از ربات @getidsbot آی دی خود را دریافت کنید" "telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "پنل را مجدداً راه اندازی کنید تا اعمال شود" "telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. پنل را مجدداً راه اندازی کنید تا اعمال شود"
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام" "telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود" "telegramNotifyTimeDesc" = "از فرمت زمان بندی Crontab استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"tgNotifyExpireTimeDiff" = "آستانه زمان باقی مانده"
"tgNotifyExpireTimeDiffDesc" = "این ربات تلگرام قبل از انقضا برای شما پیام ارسال می کند (واحد: روز)"
"tgNotifyTrafficDiff" = "آستانه ترافیک باقی مانده"
"tgNotifyTrafficDiffDesc" = "این ربات تلگرام قبل از اتمام ترافیک برای شما پیام ارسال می کند (واحد: گیگابایت)"
"tgNotifyCpu" = "آستانه هشدار درصد پردازنده"
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
"timeZonee" = "منظقه زمانی" "timeZonee" = "منظقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود" "timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"

View File

@@ -10,6 +10,8 @@
"remark" = "备注" "remark" = "备注"
"enable" = "启用" "enable" = "启用"
"protocol" = "协议" "protocol" = "协议"
"search" = "搜尋"
"loading" = "加载中" "loading" = "加载中"
"second" = "秒" "second" = "秒"
"minute" = "分钟" "minute" = "分钟"
@@ -20,6 +22,7 @@
"unlimited" = "无限制" "unlimited" = "无限制"
"none" = "无" "none" = "无"
"qrCode" = "二维码" "qrCode" = "二维码"
"info" = "更多信息"
"edit" = "编辑" "edit" = "编辑"
"delete" = "删除" "delete" = "删除"
"reset" = "重置" "reset" = "重置"
@@ -40,11 +43,8 @@
"success" = "成功" "success" = "成功"
"getVersion" = "获取版本" "getVersion" = "获取版本"
"install" = "安装" "install" = "安装"
"used" = "用过的"
"clients" = "客户端" "clients" = "客户端"
"search" = "搜索"
"usage" = "用法" "usage" = "用法"
"info" = "细节"
[menu] [menu]
"dashboard" = "系统状态" "dashboard" = "系统状态"
@@ -69,6 +69,8 @@
"memory" = "内存" "memory" = "内存"
"hard" = "硬盘" "hard" = "硬盘"
"xrayStatus" = "xray 状态" "xrayStatus" = "xray 状态"
"stopXray" = "停止 Xray"
"restartXray" = "重启 Xray"
"xraySwitch" = "切换版本" "xraySwitch" = "切换版本"
"xraySwitchClick" = "点击你想切换的版本" "xraySwitchClick" = "点击你想切换的版本"
"xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容" "xraySwitchClickDesk" = "请谨慎选择,旧版本可能配置不兼容"
@@ -85,7 +87,6 @@
"xraySwitchVersionDialogDesc" = "是否切换 xray 版本至" "xraySwitchVersionDialogDesc" = "是否切换 xray 版本至"
"dontRefreshh" = "安装中,请不要刷新此页面" "dontRefreshh" = "安装中,请不要刷新此页面"
[pages.inbounds] [pages.inbounds]
"title" = "入站列表" "title" = "入站列表"
"totalDownUp" = "总上传 / 下载" "totalDownUp" = "总上传 / 下载"
@@ -125,9 +126,22 @@
"publicKeyContent" = "公钥内容" "publicKeyContent" = "公钥内容"
"keyPath" = "密钥文件路径" "keyPath" = "密钥文件路径"
"keyContent" = "密钥内容" "keyContent" = "密钥内容"
"clickOnQRcode" = "点击二维码复制"
"client" = "客户" "client" = "客户"
"uid" = "UID" "export" = "导出链接"
[pages.client]
"add" = "添加客户端"
"edit" = "编辑客户"
"submitAdd" = "添加客户端"
"submitEdit" = "保存修改"
"clientCount" = "客户数量"
"bulk" = "批量创建"
"method" = "方法"
"first" = "第一"
"last" = "最后"
"prefix" = "前缀"
"postfix" = "后缀"
[pages.inbounds.toasts] [pages.inbounds.toasts]
"obtain" = "获取" "obtain" = "获取"
@@ -149,7 +163,6 @@
[pages.inbounds.stream.quic] [pages.inbounds.stream.quic]
"encryption" = "加密" "encryption" = "加密"
[pages.setting] [pages.setting]
"title" = "设置" "title" = "设置"
"save" = "保存配置" "save" = "保存配置"
@@ -174,16 +187,36 @@
"currentPassword" = "原密码" "currentPassword" = "原密码"
"newUsername" = "新用户名" "newUsername" = "新用户名"
"newPassword" = "新密码" "newPassword" = "新密码"
"xrayConfigTemplate" = "xray 配置模版" "advancedTemplate" = "高级模板部件"
"xrayConfigTemplateDesc" = "以该模版为基础生成最终的 xray 配置文件,重启面板生效" "completeTemplate" = "Xray 配置的完整模板"
"xrayConfigTemplate" = "xray 配置模板"
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件重新启动面板生成效率"
"xrayConfigTorrent" = "禁止使用 bittorrent"
"xrayConfigTorrentDesc" = "更改配置模板避免用户使用bittorrent重启面板生效"
"xrayConfigPrivateIp" = "禁止私人 ip 范围连接"
"xrayConfigPrivateIpDesc" = "更改配置模板以避免连接私有 IP 范围,重启面板生效"
"xrayConfigInbounds" = "入站配置"
"xrayConfigInboundsDesc" = "更改配置模板接受特殊客户端,重启面板生效"
"xrayConfigOutbounds" = "出站配置"
"xrayConfigOutboundsDesc" = "更改配置模板定义此服务器的传出方式,重启面板生效"
"xrayConfigRoutings" = "路由规则配置"
"xrayConfigRoutingsDesc" = "更改配置模板为该服务器定义路由规则,重启面板生效"
"telegramBotEnable" = "启用电报机器人" "telegramBotEnable" = "启用电报机器人"
"telegramBotEnableDesc" = "重启面板生效" "telegramBotEnableDesc" = "重启面板生效"
"telegramToken" = "电报机器人TOKEN" "telegramToken" = "电报机器人TOKEN"
"telegramTokenDesc" = "重启面板生效" "telegramTokenDesc" = "重启面板生效"
"telegramChatId" = "电报机器人ChatId" "telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
"telegramChatIdDesc" = "重启面板生效" "telegramChatIdDesc" = "重启面板生效"
"telegramNotifyTime" = "电报机器人通知时间" "telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效" "telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
"tgNotifyExpireTimeDiff" = "剩余时间阈值"
"tgNotifyExpireTimeDiffDesc" = "这个 talegram bot 会在到期前给你发送通知(单位:天)"
"tgNotifyTrafficDiff" = "剩余流量阈值"
"tgNotifyTrafficDiffDesc" = "这个 talegram bot 会在流量结束前给你发送通知单位GB"
"tgNotifyCpu" = "CPU 百分比警报阈值"
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
"timeZonee" = "时区" "timeZonee" = "时区"
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效" "timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"

View File

@@ -21,11 +21,11 @@ import (
"x-ui/web/network" "x-ui/web/network"
"x-ui/web/service" "x-ui/web/service"
"github.com/pelletier/go-toml/v2"
"github.com/gin-contrib/sessions" "github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/cookie" "github.com/gin-contrib/sessions/cookie"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/nicksnyder/go-i18n/v2/i18n" "github.com/nicksnyder/go-i18n/v2/i18n"
"github.com/pelletier/go-toml/v2"
"github.com/robfig/cron/v3" "github.com/robfig/cron/v3"
"golang.org/x/text/language" "golang.org/x/text/language"
) )
@@ -88,7 +88,7 @@ type Server struct {
xrayService service.XrayService xrayService service.XrayService
settingService service.SettingService settingService service.SettingService
inboundService service.InboundService tgbotService service.Tgbot
cron *cron.Cron cron *cron.Cron
@@ -309,7 +309,7 @@ func (s *Server) startTask() {
// Check the inbound traffic every 30 seconds that the traffic exceeds and expires // Check the inbound traffic every 30 seconds that the traffic exceeds and expires
s.cron.AddJob("@every 30s", job.NewCheckInboundJob()) s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
// check client ips from log file every 10 sec // check client ips from log file every 10 sec
s.cron.AddJob("@every 10s", job.NewCheckClientIpJob()) s.cron.AddJob("@every 10s", job.NewCheckClientIpJob())
@@ -328,8 +328,13 @@ func (s *Server) startTask() {
logger.Warning("Add NewStatsNotifyJob error", err) logger.Warning("Add NewStatsNotifyJob error", err)
return return
} }
// listen for TG bot income messages
go job.NewStatsNotifyJob().OnReceive() // Check CPU load and alarm to TgBot if threshold passes
cpuThreshold, err := s.settingService.GetTgCpu()
if (err == nil) && (cpuThreshold > 0) {
s.cron.AddJob("@every 10s", job.NewCheckCpuJob())
}
} else { } else {
s.cron.Remove(entry) s.cron.Remove(entry)
} }
@@ -406,6 +411,12 @@ func (s *Server) Start() (err error) {
s.httpServer.Serve(listener) s.httpServer.Serve(listener)
}() }()
isTgbotenabled, err := s.settingService.GetTgbotenabled()
if (err == nil) && (isTgbotenabled) {
tgBot := s.tgbotService.NewTgbot()
tgBot.Start()
}
return nil return nil
} }
@@ -415,6 +426,9 @@ func (s *Server) Stop() error {
if s.cron != nil { if s.cron != nil {
s.cron.Stop() s.cron.Stop()
} }
if s.tgbotService.IsRunnging() {
s.tgbotService.Stop()
}
var err1 error var err1 error
var err2 error var err2 error
if s.httpServer != nil { if s.httpServer != nil {

118
x-ui.sh
View File

@@ -20,49 +20,45 @@ function LOGI() {
# check root # check root
[[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1 [[ $EUID -ne 0 ]] && LOGE "ERROR: You must be root to run this script! \n" && exit 1
# check os # Check OS and set release variable
if [[ -f /etc/redhat-release ]]; then if [[ -f /etc/os-release ]]; then
release="centos" source /etc/os-release
elif cat /etc/issue | grep -Eqi "debian"; then release=$ID
release="debian" elif [[ -f /usr/lib/os-release ]]; then
elif cat /etc/issue | grep -Eqi "ubuntu"; then source /usr/lib/os-release
release="ubuntu" release=$ID
elif cat /etc/issue | grep -Eqi "centos|red hat|redhat"; then
release="centos"
elif cat /proc/version | grep -Eqi "debian"; then
release="debian"
elif cat /proc/version | grep -Eqi "ubuntu"; then
release="ubuntu"
elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then
release="centos"
else else
LOGE "check system OS failed,please contact with author! \n" && exit 1 echo "Failed to check the system OS, please contact the author!" >&2
exit 1
fi fi
echo "The OS release is: $release"
os_version="" os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
# os version if [[ "${release}" == "centos" ]]; then
if [[ -f /etc/os-release ]]; then
os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release)
fi
if [[ -z "$os_version" && -f /etc/lsb-release ]]; then
os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release)
fi
if [[ x"${release}" == x"centos" ]]; then
if [[ ${os_version} -le 6 ]]; then
LOGE "please use CentOS 7 or higher version! \n" && exit 1
fi
elif [[ x"${release}" == x"ubuntu" ]]; then
if [[ ${os_version} -lt 16 ]]; then
LOGE "please use Ubuntu 16 or higher version\n" && exit 1
fi
elif [[ x"${release}" == x"debian" ]]; then
if [[ ${os_version} -lt 8 ]]; then if [[ ${os_version} -lt 8 ]]; then
LOGE "please use Debian 8 or higher version\n" && exit 1 echo -e "${red} Please use CentOS 8 or higher ${plain}\n" && exit 1
fi
elif [[ "${release}" == "ubuntu" ]]; then
if [[ ${os_version} -lt 20 ]]; then
echo -e "${red}please use Ubuntu 20 or higher version${plain}\n" && exit 1
fi
elif [[ "${release}" == "fedora" ]]; then
if [[ ${os_version} -lt 36 ]]; then
echo -e "${red}please use Fedora 36 or higher version${plain}\n" && exit 1
fi
elif [[ "${release}" == "debian" ]]; then
if [[ ${os_version} -lt 10 ]]; then
echo -e "${red} Please use Debian 10 or higher ${plain}\n" && exit 1
fi fi
fi fi
confirm() { confirm() {
if [[ $# > 1 ]]; then if [[ $# > 1 ]]; then
echo && read -p "$1 [Default$2]: " temp echo && read -p "$1 [Default$2]: " temp
@@ -94,7 +90,7 @@ before_show_menu() {
} }
install() { install() {
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/main/install.sh) bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh)
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
if [[ $# == 0 ]]; then if [[ $# == 0 ]]; then
start start
@@ -113,7 +109,7 @@ update() {
fi fi
return 0 return 0
fi fi
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/main/install.sh) bash <(curl -Ls https://raw.githubusercontent.com/MHSanaei/3x-ui/main/install.sh)
if [[ $? == 0 ]]; then if [[ $? == 0 ]]; then
LOGI "Update is complete, Panel has automatically restarted " LOGI "Update is complete, Panel has automatically restarted "
exit 0 exit 0
@@ -288,21 +284,45 @@ show_log() {
fi fi
} }
migrate_v2_ui() { enable_bbr() {
/usr/local/x-ui/x-ui v2-ui
before_show_menu if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then
} echo -e "${green}BBR is already enabled!${plain}"
exit 0
fi
# Check the OS and install necessary packages
if [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "ubuntu" ]]; then
sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "debian" ]]; then
sudo apt-get update && sudo apt-get install -yqq --no-install-recommends ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "fedora" ]]; then
sudo dnf -y update && sudo dnf -y install ca-certificates
elif [[ "$(cat /etc/os-release | grep -E '^ID=' | awk -F '=' '{print $2}')" == "centos" ]]; then
sudo yum -y update && sudo yum -y install ca-certificates
else
echo "Unsupported operating system. Please check the script and install the necessary packages manually."
exit 1
fi
# Enable BBR
echo "net.core.default_qdisc=fq" | sudo tee -a /etc/sysctl.conf
echo "net.ipv4.tcp_congestion_control=bbr" | sudo tee -a /etc/sysctl.conf
# Apply changes
sudo sysctl -p
# Verify that BBR is enabled
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "bbr" ]]; then
echo -e "${green}BBR has been enabled successfully.${plain}"
else
echo -e "${red}Failed to enable BBR. Please check your system configuration.${plain}"
fi
install_bbr() {
# temporary workaround for installing bbr
bash <(curl -L -s https://raw.githubusercontent.com/teddysun/across/master/bbr.sh)
echo ""
before_show_menu
} }
update_shell() { update_shell() {
wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/mhsanaei/3x-ui/raw/main/x-ui.sh wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/MHSanaei/3x-ui/raw/main/x-ui.sh
if [[ $? != 0 ]]; then if [[ $? != 0 ]]; then
echo "" echo ""
LOGE "Failed to download scriptPlease check whether the machine can connect Github" LOGE "Failed to download scriptPlease check whether the machine can connect Github"
@@ -657,8 +677,8 @@ show_menu() {
${green}11.${plain} Check x-ui Status ${green}11.${plain} Check x-ui Status
${green}12.${plain} Check x-ui Logs ${green}12.${plain} Check x-ui Logs
———————————————— ————————————————
${green}13.${plain} Enable x-ui On Sysyem Startup ${green}13.${plain} Enable x-ui On System Startup
${green}14.${plain} Disabel x-ui On Sysyem Startup ${green}14.${plain} Disabel x-ui On System Startup
———————————————— ————————————————
${green}15.${plain} Enable BBR ${green}15.${plain} Enable BBR
${green}16.${plain} Issuse Certs ${green}16.${plain} Issuse Certs
@@ -713,7 +733,7 @@ show_menu() {
check_install && disable check_install && disable
;; ;;
15) 15)
install_bbr enable_bbr
;; ;;
16) 16)
ssl_cert_issue ssl_cert_issue