Compare commits

...

77 Commits

Author SHA1 Message Date
Ho3ein
4ce53920fe v1.3.4 2023-05-01 02:29:24 +03:30
MHSanaei
5100bbba52 simplify ssl cert 2023-04-30 00:57:15 +03:30
MHSanaei
f93d912644 primary type for setDefaultCert 2023-04-29 22:51:33 +03:30
MHSanaei
ce8551b8c4 [darkmode] better colors + add sec to calendar
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 22:38:41 +03:30
MHSanaei
d26c21d900 [feature] inbounds auto refresh option
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 22:33:45 +03:30
MHSanaei
f5f9347661 Merge branch 'main' of https://github.com/MHSanaei/3x-ui 2023-04-29 22:28:55 +03:30
MHSanaei
a0f5875cb3 [darkmode] fix UTLS - cipherSuites 2023-04-29 22:28:50 +03:30
Ho3ein
3055c68615 Postman Collection thanks to @mehdikhody #303
thanks to @mehdikhody #303
2023-04-29 20:18:24 +03:30
MHSanaei
c3ed8051f3 [feature] add sniffing DestOverride options #298
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 18:47:44 +03:30
MHSanaei
d2cdc51c54 [feature] add quic to sniffingObject
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-29 16:26:39 +03:30
MHSanaei
ee896662f5 remove favicon from web root
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-28 18:49:32 +03:30
MHSanaei
177bd036a3 [bug] fix GetClientTrafficByEmail
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-28 18:40:33 +03:30
MHSanaei
d03e049320 v1.3.3 2023-04-28 01:03:59 +03:30
MHSanaei
957d9e24fb Revert "grpc.WithInsecure is deprecated"
This reverts commit 0b896d9c31.
2023-04-28 00:47:56 +03:30
MHSanaei
865e47e9a6 Update check_client_ip_job.go 2023-04-28 00:30:49 +03:30
MHSanaei
607c5d3598 [feature] add grpc multiMode 2023-04-28 00:15:06 +03:30
MHSanaei
8879541999 dark mode - default 2023-04-27 23:48:58 +03:30
MHSanaei
0b896d9c31 grpc.WithInsecure is deprecated 2023-04-27 23:48:22 +03:30
MHSanaei
6f4a2809e2 tls for ss - remove unused 2023-04-27 19:25:48 +03:30
MHSanaei
103a26edb6 [migrate] remove orphaned traffics
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-27 19:05:36 +03:30
MHSanaei
7419592626 v1.3.2 2023-04-26 13:00:51 +03:30
MHSanaei
edabfad559 fix expiry time
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-26 12:57:49 +03:30
MHSanaei
2849cc0b2e v1.3.1 2023-04-26 02:12:38 +03:30
MHSanaei
46eb174af1 remove unused 2023-04-26 02:11:11 +03:30
MHSanaei
f8878208ca upgrade sonic to 1.8.8 2023-04-26 02:10:25 +03:30
MHSanaei
e126095949 bug fix 2023-04-26 02:09:56 +03:30
MHSanaei
df2f292b68 v1.3.0 2023-04-25 21:46:24 +03:30
MHSanaei
9f5ba0cf93 [bug] vision-udp443 only for client + Translation
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 21:43:29 +03:30
MHSanaei
b5c5539501 add hostname to page title
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 20:29:13 +03:30
MHSanaei
252afe47c0 [api] support for delete depleted clients
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 18:46:09 +03:30
MHSanaei
379451135d [feature] delete depleted clients
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 18:43:37 +03:30
MHSanaei
bc06dbab21 [migration] add fix for omitted traffics
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 18:36:06 +03:30
MHSanaei
6a71ea7f5e [feature] reset traffics of all client
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 17:23:38 +03:30
MHSanaei
942b9862d8 [feature] add login session timeout
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 15:00:21 +03:30
MHSanaei
ae55fdc38a fix bug in http link without host
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 14:39:09 +03:30
MHSanaei
cc3ff61ae2 update by client id
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-25 14:38:35 +03:30
MHSanaei
045717010a Revert "bug fix - h2 sub"
This reverts commit cd5ad78f56.
2023-04-25 14:21:57 +03:30
MHSanaei
aae32d9211 Merge branch 'main' of https://github.com/MHSanaei/3x-ui 2023-04-25 01:19:15 +03:30
MHSanaei
cd5ad78f56 bug fix - h2 sub 2023-04-25 01:19:07 +03:30
Ho3ein
b450aacebd Update README.md 2023-04-25 00:25:55 +03:30
Ho3ein
640068b279 Update README.md 2023-04-25 00:25:40 +03:30
MHSanaei
04d85af40e undo changes 2023-04-24 16:55:29 +03:30
MHSanaei
fca882ee31 sub - tg to inbound 2023-04-24 16:45:22 +03:30
MHSanaei
2832106bc6 v1.2.8 2023-04-24 16:04:01 +03:30
MHSanaei
bf24838939 remove useless 2023-04-24 15:31:25 +03:30
MHSanaei
16e3107d23 Better client delete + api
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-24 15:07:11 +03:30
MHSanaei
262e3c0985 Add database migration
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-24 14:13:25 +03:30
MHSanaei
2b460bac1a Optimize database
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-24 14:04:05 +03:30
MHSanaei
d1c4eb9b4c Update setting.html 2023-04-24 02:48:17 +03:30
MHSanaei
dfa3d39ab3 fix flow view 2023-04-24 02:48:02 +03:30
Ho3ein
2b54d0344e v1.2.7 2023-04-21 19:15:36 +03:30
MHSanaei
f817f922fe add getClientTraffics api
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-21 19:06:59 +03:30
MHSanaei
55f7fcd1b3 typo thanks to @firefoxOnFire 2023-04-21 19:03:23 +03:30
MHSanaei
b0f974a94d secret token thanks to @HarlyquinForest 2023-04-21 19:00:14 +03:30
MHSanaei
6bebde4105 Merge branch 'main' of https://github.com/MHSanaei/3x-ui 2023-04-21 15:34:19 +03:30
Ho3ein
fa4a63c958 Merge pull request #273 from ehsaninuc/patch-1
Update translate.en_US.toml
2023-04-21 02:08:06 +03:30
Ho3ein
11def0a753 Merge pull request #272 from hamid-gh98/main
update iran.dat from xray zip
2023-04-21 02:02:32 +03:30
Ehsan Soltani Azad
513f87550a Update translate.en_US.toml 2023-04-21 01:41:03 +03:30
Hamidreza Ghavami
81838b504c update iran.dat from xray zip 2023-04-21 02:14:00 +04:30
MHSanaei
4980744793 add reset traffic in edit
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-20 15:55:51 +03:30
MHSanaei
5ff6f4094e fix userinfo header
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-20 15:51:38 +03:30
MHSanaei
bbce1eb3f7 fix enabletgbot cli
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-20 15:49:48 +03:30
MHSanaei
204f73a692 main.go enhancements
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-20 15:49:24 +03:30
MHSanaei
641a7d3e57 install warp to menu 2023-04-19 23:18:26 +03:30
Ho3ein
3f7e819a9b Update README.md 2023-04-19 15:41:17 +03:30
Ho3ein
834d003ab6 v1.2.6 2023-04-19 15:40:47 +03:30
MHSanaei
c627227893 bug fixed 2023-04-19 15:37:24 +03:30
MHSanaei
2a8725a7a5 mistake 2023-04-19 13:26:35 +03:30
Ho3ein
ca2d1bb901 Update README.md 2023-04-19 11:59:07 +03:30
Ho3ein
fa19649286 Merge pull request #269 from hamid-gh98/main
Add description for config groups
2023-04-19 11:56:46 +03:30
Ho3ein
56d75f5293 v1.2.5 2023-04-19 11:56:16 +03:30
MHSanaei
e1132a3f41 bug fix
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-19 11:55:38 +03:30
MHSanaei
4d479102ad reality link bug fix
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2023-04-19 11:55:31 +03:30
Hamidreza Ghavami
e90c575bfd add description for config groups 2023-04-19 04:02:53 +04:30
Hamidreza
9d02f455cc Merge pull request #268 from hamid-gh98/main
update README.md
2023-04-18 22:41:07 +03:30
Hamidreza Ghavami
13f67f595c update README.md 2023-04-18 23:40:08 +04:30
MHSanaei
25741dcb08 Update README.md 2023-04-18 22:34:43 +03:30
53 changed files with 1256 additions and 689 deletions

View File

@@ -20,10 +20,10 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
## 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`:
To install your desired version you can add the version to the end of install command. Example for ver `v1.3.3`:
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.0.9
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.3.3
```
# SSL
@@ -33,6 +33,8 @@ apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
or you can use x-ui menu then number '16' (Apply for an SSL Certificate)
# Default settings
@@ -79,13 +81,19 @@ XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
If you want to use routing to WARP follow steps as below:
1. Install WARP on **socks proxy mode**:
1. If you already installed warp, you can uninstall using below command:
```sh
warp u
```
2. Install WARP on **socks proxy mode**:
```sh
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
```
2. Turn on the config you need in panel or [Copy and paste this file to Xray Configuration](./media/configs/traffic+block-ads+warp.json)
3. Turn on the config you need in panel or [Copy and paste this file to Xray Configuration](./media/configs/traffic+block-ads+warp.json)
Config Features:
@@ -128,6 +136,7 @@ Reference syntax:
- 0 \*/10 \* \* \* \* //Notify at the first second of each 10 minutes
- @hourly // hourly notification
- @daily // Daily notification (00:00 in the morning)
- @weekly // weekly notification
- @every 8h // notify every 8 hours
# Telegram Bot Features
@@ -154,22 +163,25 @@ Reference syntax:
| :----: | ---------------------------------- | ------------------------------------------- |
| `GET` | `"/list"` | Get all inbounds |
| `GET` | `"/get/:id"` | Get inbound with inbound.id |
| `GET` | `"/getClientTraffics/:email"` | Get Client Traffics with email |
| `POST` | `"/add"` | Add inbound |
| `POST` | `"/del/:id"` | Delete Inbound |
| `POST` | `"/update/:id"` | Update Inbound |
| `POST` | `"/clientIps/:email"` | Client Ip address |
| `POST` | `"/clearClientIps/:email"` | Clear Client Ip address |
| `POST` | `"/addClient/"` | Add Client to inbound |
| `POST` | `"/delClient/:email"` | Delete Client |
| `POST` | `"/updateClient/:index"` | Update Client |
| `POST` | `"/addClient"` | Add Client to inbound |
| `POST` | `"/:id/delClient/:clientId"` | Delete Client by UID/Password as clientId |
| `POST` | `"/updateClient/:clientId"` | Update Client by UID/Password as clientId |
| `POST` | `"/:id/resetClientTraffic/:email"` | Reset Client's Traffic |
| `POST` | `"/resetAllTraffics"` | Reset traffics of all inbounds |
| `POST` | `"/resetAllClientTraffics/:id"` | Reset traffics of all clients in an inbound |
| `POST` | `"/delDepletedClients/:id"` | Delete inbound depleted clients (-1: all) |
- [Postman Collection](https://gist.github.com/mehdikhody/9a862801a2e41f6b5fb6bbc7e1326044)
# A Special Thanks To
- [alireza0](https://github.com/alireza0/)
- [FranzKafkaYu](https://github.com/FranzKafkaYu)
# Suggestion System
@@ -180,11 +192,8 @@ Reference syntax:
# Buy Me a Coffee
[![](https://img.shields.io/badge/Wallet-USDT__TRC20-green.svg)](#)
- Tron USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
```
TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC
```
# Pictures

View File

@@ -1 +1 @@
1.2.4
1.3.4

View File

@@ -27,8 +27,9 @@ func initUser() error {
}
if count == 0 {
user := &model.User{
Username: "admin",
Password: "admin",
Username: "admin",
Password: "admin",
LoginSecret: "",
}
return db.Create(user).Error
}

View File

@@ -18,9 +18,10 @@ const (
)
type User struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username"`
Password string `json:"password"`
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
Username string `json:"username"`
Password string `json:"password"`
LoginSecret string `json:"loginSecret"`
}
type Inbound struct {

2
go.mod
View File

@@ -24,7 +24,7 @@ require (
require (
github.com/BurntSushi/toml v1.2.1 // indirect
github.com/bytedance/sonic v1.8.7 // indirect
github.com/bytedance/sonic v1.8.8 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 // indirect
github.com/gin-contrib/sse v0.1.0 // indirect
github.com/go-ole/go-ole v1.2.6 // indirect

4
go.sum
View File

@@ -9,8 +9,8 @@ github.com/boj/redistore v0.0.0-20180917114910-cd5dcc76aeff/go.mod h1:+RTT1BOk5P
github.com/bradfitz/gomemcache v0.0.0-20190913173617-a41fca850d0b/go.mod h1:H0wQNHz2YrLsuXOZozoeDmnHXkNCRmMW0gwFWDfEZDA=
github.com/bradleypeabody/gorilla-sessions-memcache v0.0.0-20181103040241-659414f458e1/go.mod h1:dkChI7Tbtx7H1Tj7TqGSZMOeGpMP5gLHtjroHd4agiI=
github.com/bytedance/sonic v1.5.0/go.mod h1:ED5hyg4y6t3/9Ku1R6dU/4KyJ48DZ4jPhfY1O2AihPM=
github.com/bytedance/sonic v1.8.7 h1:d3sry5vGgVq/OpgozRUNP6xBsSo0mtNdwliApw+SAMQ=
github.com/bytedance/sonic v1.8.7/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/bytedance/sonic v1.8.8 h1:Kj4AYbZSeENfyXicsYppYKO0K2YWab+i2UTSY7Ukz9Q=
github.com/bytedance/sonic v1.8.8/go.mod h1:i736AoUSYt75HyZLoJW9ERYxcy6eaN6h4BZXU064P/U=
github.com/chenzhuoyu/base64x v0.0.0-20211019084208-fb5309c8db06/go.mod h1:DH46F32mSOjUmXrMHnKwZdA8wcEefY7UVqBKYGjpdQY=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311 h1:qSGYFH7+jGhDF8vLC+iwCD4WpbV1EBDSzWkJODFLams=
github.com/chenzhuoyu/base64x v0.0.0-20221115062448-fe3a3abad311/go.mod h1:b583jCggY9gE99b6G5LEC39OIiVsWj+R97kbl5odCEk=

View File

@@ -23,23 +23,14 @@ else
fi
echo "The OS release is: $release"
arch=$(arch)
if [[ $arch == "x86_64" || $arch == "x64" || $arch == "amd64" ]]; then
arch="amd64"
elif [[ $arch == "aarch64" || $arch == "arm64" ]]; then
arch="arm64"
else
arch="amd64"
echo -e "${red} Failed to check system arch, will use default arch: ${arch}${plain}"
fi
echo "arch: ${arch}"
if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then
echo "x-ui dosen't support 32-bit(x86) system, please use 64 bit operating system(x86_64) instead, if there is something wrong, please get in touch with me!"
exit -1
fi
arch3xui() {
case "$(uname -m)" in
x86_64 | x64 | amd64 ) echo 'amd64' ;;
armv8 | arm64 | aarch64 ) echo 'arm64' ;;
* ) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
esac
}
echo "arch: $(arch3xui)"
os_version=""
os_version=$(grep -i version_id /etc/os-release | cut -d \" -f2 | cut -d . -f1)
@@ -79,9 +70,10 @@ install_base() {
#This function will be called when user installed x-ui out of sercurity
config_after_install() {
/usr/local/x-ui/x-ui migrate
echo -e "${yellow}Install/update finished! For security it's recommended to modify panel settings ${plain}"
read -p "Do you want to continue with the modification [y/n]? ": config_confirm
if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
read -p "Please set up your username:" config_account
echo -e "${yellow}Your username will be:${config_account}${plain}"
read -p "Please set up your password:" config_password
@@ -122,18 +114,18 @@ install_x-ui() {
exit 1
fi
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-$(arch3xui).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${last_version}/x-ui-linux-$(arch3xui).tar.gz
if [[ $? -ne 0 ]]; then
echo -e "${red}Downloading x-ui failed, please be sure that your server can access Github ${plain}"
exit 1
fi
else
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-$(arch3xui).tar.gz"
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-$(arch3xui).tar.gz ${url}
if [[ $? -ne 0 ]]; then
echo -e "${red}Download x-ui $1 failed,please check the version exists${plain}"
echo -e "${red}Download x-ui $1 failed,please check the version exists ${plain}"
exit 1
fi
fi
@@ -142,10 +134,10 @@ install_x-ui() {
rm /usr/local/x-ui/ -rf
fi
tar zxvf x-ui-linux-${arch}.tar.gz
rm x-ui-linux-${arch}.tar.gz -f
tar zxvf x-ui-linux-$(arch3xui).tar.gz
rm x-ui-linux-$(arch3xui).tar.gz -f
cd x-ui
chmod +x x-ui bin/xray-linux-${arch}
chmod +x x-ui bin/xray-linux-$(arch3xui)
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
chmod +x /usr/local/x-ui/x-ui.sh

View File

@@ -1,8 +1,9 @@
package logger
import (
"github.com/op/go-logging"
"os"
"github.com/op/go-logging"
)
var logger *logging.Logger

45
main.go
View File

@@ -51,8 +51,8 @@ func runWebServer() {
}
sigCh := make(chan os.Signal, 1)
//信号量捕获处理
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
// Trap shutdown signals
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM)
for {
sig := <-sigCh
@@ -204,6 +204,37 @@ func updateSetting(port int, username string, password string) {
}
}
func migrateDb() {
inboundService := service.InboundService{}
err := database.InitDB(config.GetDBPath())
if err != nil {
log.Fatal(err)
}
fmt.Println("Start migrating database...")
inboundService.MigrationRequirements()
inboundService.RemoveOrphanedTraffics()
fmt.Println("Migration done!")
}
func removeSecret() {
err := database.InitDB(config.GetDBPath())
if err != nil {
fmt.Println(err)
return
}
userService := service.UserService{}
err = userService.RemoveUserSecret()
if err != nil {
fmt.Println(err)
}
settingService := service.SettingService{}
err = settingService.SetSecretStatus(false)
if err != nil {
fmt.Println(err)
}
}
func main() {
if len(os.Args) < 2 {
runWebServer()
@@ -229,6 +260,7 @@ func main() {
var tgbotRuntime string
var reset bool
var show bool
var remove_secret bool
settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
settingCmd.BoolVar(&show, "show", false, "show current settings")
settingCmd.IntVar(&port, "port", 0, "set panel port")
@@ -246,6 +278,7 @@ func main() {
fmt.Println("Commands:")
fmt.Println(" run run web panel")
fmt.Println(" v2-ui migrate form v2-ui")
fmt.Println(" migrate migrate form other/old x-ui")
fmt.Println(" setting set settings")
}
@@ -263,6 +296,8 @@ func main() {
return
}
runWebServer()
case "migrate":
migrateDb()
case "v2-ui":
err := v2uiCmd.Parse(os.Args[2:])
if err != nil {
@@ -290,6 +325,12 @@ func main() {
if (tgbottoken != "") || (tgbotchatid != "") || (tgbotRuntime != "") {
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
}
if remove_secret {
removeSecret()
}
if enabletgbot {
updateTgbotEnableSts(enabletgbot)
}
default:
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
fmt.Println()

View File

@@ -212,6 +212,7 @@
.ant-card-dark .ant-modal-close,
.ant-card-dark i,
.ant-card-dark .ant-select-dropdown-menu-item,
.ant-card-dark .ant-calendar-day-select,
.ant-card-dark .ant-calendar-month-select,
.ant-card-dark .ant-calendar-year-select,
.ant-card-dark .ant-calendar-date,
@@ -226,7 +227,7 @@
.ant-card-dark .ant-calendar-date:hover,
.ant-card-dark .ant-select-dropdown-menu-item-active,
.ant-card-dark li.ant-calendar-time-picker-select-option-selected {
background-color: #004488;
background-color: #11314d;
}
.ant-card-dark tbody .ant-table-expanded-row,
@@ -243,7 +244,7 @@
.ant-card-dark .ant-select-selection,
.ant-card-dark .ant-calendar-picker-clear {
color: hsla(0,0%,100%,.65);
background-color: #2e3b52;
background-color: #193752;
}
.ant-card-dark .ant-select-disabled .ant-select-selection {
@@ -264,12 +265,19 @@
.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 {
.ant-card-dark .ant-modal-header {
color: hsla(0,0%,100%,.65);
background-color: #222a37;
}
.ant-card-dark .ant-calendar-selected-day .ant-calendar-date {
background-color: #1668dc;
}
.ant-card-dark .ant-calendar-time-picker-select li:hover {
background: #1668dc;
}
.client-table-header {
background-color: #f0f2f5;
}

View File

@@ -3,6 +3,7 @@ class User {
constructor() {
this.username = "";
this.password = "";
this.LoginSecret = "";
}
}
@@ -171,6 +172,7 @@ class AllSetting {
this.webCertFile = "";
this.webKeyFile = "";
this.webBasePath = "/";
this.sessionMaxAge = "";
this.expireDiff = "";
this.trafficDiff = "";
this.tgBotEnable = false;
@@ -180,6 +182,7 @@ class AllSetting {
this.tgBotBackup = false;
this.tgCpu = "";
this.xrayTemplateConfig = "";
this.secretEnable = false;
this.timeLocation = "Asia/Tehran";

View File

@@ -28,20 +28,6 @@ const SSMethods = {
BLAKE3_CHACHA20_POLY1305: '2022-blake3-chacha20-poly1305',
};
const RULE_IP = {
PRIVATE: 'geoip:private',
CN: 'geoip:cn',
};
const RULE_DOMAIN = {
ADS: 'geosite:category-ads',
ADS_ALL: 'geosite:category-ads-all',
CN: 'geosite:cn',
GOOGLE: 'geosite:google',
FACEBOOK: 'geosite:facebook',
SPEEDTEST: 'geosite:speedtest',
};
const XTLS_FLOW_CONTROL = {
ORIGIN: "xtls-rprx-origin",
DIRECT: "xtls-rprx-direct",
@@ -93,21 +79,26 @@ const UTLS_FINGERPRINT = {
};
const ALPN_OPTION = {
H3: "h3",
H2: "h2",
HTTP1: "http/1.1",
H2: "h2",
H3: "h3",
};
const SNIFFING_OPTION = {
HTTP: "http",
TLS: "tls",
QUIC: "quic",
};
Object.freeze(Protocols);
Object.freeze(VmessMethods);
Object.freeze(SSMethods);
Object.freeze(RULE_IP);
Object.freeze(RULE_DOMAIN);
Object.freeze(XTLS_FLOW_CONTROL);
Object.freeze(TLS_FLOW_CONTROL);
Object.freeze(TLS_VERSION_OPTION);
Object.freeze(TLS_CIPHER_OPTION);
Object.freeze(ALPN_OPTION);
Object.freeze(SNIFFING_OPTION);
class XrayCommonClass {
@@ -456,18 +447,26 @@ class QuicStreamSettings extends XrayCommonClass {
}
class GrpcStreamSettings extends XrayCommonClass {
constructor(serviceName="") {
constructor(
serviceName="",
multiMode=false
) {
super();
this.serviceName = serviceName;
this.multiMode = multiMode;
}
static fromJson(json={}) {
return new GrpcStreamSettings(json.serviceName);
return new GrpcStreamSettings(
json.serviceName,
json.multiMode
);
}
toJson() {
return {
serviceName: this.serviceName,
multiMode: this.multiMode
}
}
}
@@ -853,6 +852,7 @@ class StreamSettings extends XrayCommonClass {
}
static fromJson(json={}) {
return new StreamSettings(
json.network,
json.security,
@@ -887,7 +887,7 @@ class StreamSettings extends XrayCommonClass {
}
class Sniffing extends XrayCommonClass {
constructor(enabled=true, destOverride=['http', 'tls']) {
constructor(enabled=true, destOverride=['http', 'tls', 'quic']) {
super();
this.enabled = enabled;
this.destOverride = destOverride;
@@ -897,7 +897,7 @@ class Sniffing extends XrayCommonClass {
let destOverride = ObjectUtil.clone(json.destOverride);
if (!ObjectUtil.isEmpty(destOverride) && !ObjectUtil.isArrEmpty(destOverride)) {
if (ObjectUtil.isEmpty(destOverride[0])) {
destOverride = ['http', 'tls'];
destOverride = ['http', 'tls', 'quic'];
}
}
return new Sniffing(
@@ -1159,6 +1159,7 @@ class Inbound extends XrayCommonClass {
case Protocols.VMESS:
case Protocols.VLESS:
case Protocols.TROJAN:
case Protocols.SHADOWSOCKS:
break;
default:
return false;
@@ -1260,50 +1261,6 @@ class Inbound extends XrayCommonClass {
if (this.protocol !== Protocols.VMESS) {
return '';
}
let network = this.stream.network;
let type = 'none';
let host = '';
let path = '';
if (network === 'tcp') {
let tcp = this.stream.tcp;
type = tcp.type;
if (type === 'http') {
let request = tcp.request;
path = request.path.join(',');
let index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
host = request.headers[index].value;
}
}
} else if (network === 'kcp') {
let kcp = this.stream.kcp;
type = kcp.type;
path = kcp.seed;
} else if (network === 'ws') {
let ws = this.stream.ws;
path = ws.path;
let index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
host = ws.headers[index].value;
}
} else if (network === 'http') {
network = 'h2';
path = this.stream.http.path;
host = this.stream.http.host.join(',');
} else if (network === 'quic') {
type = this.stream.quic.type;
host = this.stream.quic.security;
path = this.stream.quic.key;
} else if (network === 'grpc') {
path = this.stream.grpc.serviceName;
}
if (this.stream.security === 'tls') {
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
address = this.stream.tls.server;
}
}
let obj = {
v: '2',
ps: remark,
@@ -1311,16 +1268,66 @@ class Inbound extends XrayCommonClass {
port: this.port,
id: this.settings.vmesses[clientIndex].id,
aid: this.settings.vmesses[clientIndex].alterId,
net: network,
type: type,
host: host,
path: path,
net: this.stream.network,
type: 'none',
tls: this.stream.security,
sni: this.stream.tls.settings.serverName,
fp: this.stream.tls.settings.fingerprint,
alpn: this.stream.tls.alpn.join(','),
allowInsecure: this.stream.tls.settings.allowInsecure,
};
let network = this.stream.network;
if (network === 'tcp') {
let tcp = this.stream.tcp;
obj.type = tcp.type;
if (tcp.type === 'http') {
let request = tcp.request;
obj.path = request.path.join(',');
let index = request.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
obj.host = request.headers[index].value;
}
}
} else if (network === 'kcp') {
let kcp = this.stream.kcp;
obj.type = kcp.type;
obj.path = kcp.seed;
} else if (network === 'ws') {
let ws = this.stream.ws;
obj.path = ws.path;
let index = ws.headers.findIndex(header => header.name.toLowerCase() === 'host');
if (index >= 0) {
obj.host = ws.headers[index].value;
}
} else if (network === 'http') {
obj.net = 'h2';
obj.path = this.stream.http.path;
obj.host = this.stream.http.host.join(',');
} else if (network === 'quic') {
obj.type = this.stream.quic.type;
obj.host = this.stream.quic.security;
obj.path = this.stream.quic.key;
} else if (network === 'grpc') {
obj.path = this.stream.grpc.serviceName;
if (this.stream.grpc.multiMode){
obj.type = 'multi'
}
}
if (this.stream.security === 'tls') {
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
obj.add = this.stream.tls.server;
}
if (!ObjectUtil.isEmpty(this.stream.tls.settings.serverName)){
obj.sni = this.stream.tls.settings.serverName;
}
if (!ObjectUtil.isEmpty(this.stream.tls.settings.fingerprint)){
obj.fp = this.stream.tls.settings.fingerprint;
}
if (this.stream.tls.alpn.length>0){
obj.alpn = this.stream.tls.alpn.join(',');
}
if (this.stream.tls.settings.allowInsecure){
obj.allowInsecure = this.stream.tls.settings.allowInsecure;
}
}
return 'vmess://' + base64(JSON.stringify(obj, null, 2));
}
@@ -1373,6 +1380,9 @@ class Inbound extends XrayCommonClass {
case "grpc":
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
if(grpc.multiMode){
params.set("mode", "multi");
}
break;
}
@@ -1408,19 +1418,17 @@ class Inbound extends XrayCommonClass {
if (this.reality) {
params.set("security", "reality");
params.set("fp", this.stream.reality.settings.fingerprint);
params.set("pbk", this.stream.reality.settings.publicKey);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(",")[0]);
}
if (this.stream.network === 'tcp') {
if (this.stream.network === 'tcp' && !ObjectUtil.isEmpty(this.settings.vlesses[clientIndex].flow)) {
params.set("flow", this.settings.vlesses[clientIndex].flow);
}
if (this.stream.reality.shortIds != "") {
if (this.stream.reality.shortIds.length > 0) {
params.set("sid", this.stream.reality.shortIds.split(",")[0]);
}
if (this.stream.reality.settings.fingerprint != "") {
params.set("fp", this.stream.reality.settings.fingerprint);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}
@@ -1492,6 +1500,9 @@ class Inbound extends XrayCommonClass {
case "grpc":
const grpc = this.stream.grpc;
params.set("serviceName", grpc.serviceName);
if(grpc.multiMode){
params.set("mode", "multi");
}
break;
}
@@ -1512,19 +1523,14 @@ class Inbound extends XrayCommonClass {
if (this.reality) {
params.set("security", "reality");
params.set("fp", this.stream.reality.settings.fingerprint);
params.set("pbk", this.stream.reality.settings.publicKey);
if (!ObjectUtil.isArrEmpty(this.stream.reality.serverNames)) {
params.set("sni", this.stream.reality.serverNames.split(",")[0]);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}
if (this.stream.reality.shortIds != "") {
if (this.stream.reality.shortIds.length > 0) {
params.set("sid", this.stream.reality.shortIds.split(",")[0]);
}
if (this.stream.reality.settings.fingerprint != "") {
params.set("fp", this.stream.reality.settings.fingerprint);
}
if (!ObjectUtil.isEmpty(this.stream.reality.settings.serverName)) {
address = this.stream.reality.settings.serverName;
}
@@ -1536,7 +1542,7 @@ class Inbound extends XrayCommonClass {
if(this.stream.xtls.settings.allowInsecure){
params.set("allowInsecure", "1");
}
if (!ObjectUtil.isEmpty(this.stream.tls.server)) {
if (!ObjectUtil.isEmpty(this.stream.xtls.server)) {
address = this.stream.xtls.server;
}
params.set("flow", this.settings.trojans[clientIndex].flow);

View File

@@ -19,17 +19,19 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
g.GET("/list", a.getAllInbounds)
g.GET("/get/:id", a.getSingleInbound)
g.GET("/getClientTraffics/:email", a.getClientTraffics)
g.POST("/add", a.addInbound)
g.POST("/del/:id", a.delInbound)
g.POST("/update/:id", a.updateInbound)
g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/addClient/", a.addInboundClient)
g.POST("/delClient/:email", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient)
g.POST("/addClient", a.addInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
a.inboundController = NewInboundController(g)
}
@@ -39,6 +41,9 @@ func (a *APIController) getAllInbounds(c *gin.Context) {
func (a *APIController) getSingleInbound(c *gin.Context) {
a.inboundController.getInbound(c)
}
func (a *APIController) getClientTraffics(c *gin.Context) {
a.inboundController.getClientTraffics(c)
}
func (a *APIController) addInbound(c *gin.Context) {
a.inboundController.addInbound(c)
}
@@ -74,3 +79,6 @@ func (a *APIController) resetAllTraffics(c *gin.Context) {
func (a *APIController) resetAllClientTraffics(c *gin.Context) {
a.inboundController.resetAllClientTraffics(c)
}
func (a *APIController) delDepletedClients(c *gin.Context) {
a.inboundController.delDepletedClients(c)
}

View File

@@ -1,9 +1,10 @@
package controller
import (
"github.com/gin-gonic/gin"
"net/http"
"x-ui/web/session"
"github.com/gin-gonic/gin"
)
type BaseController struct {

View File

@@ -34,11 +34,12 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
g.POST("/clientIps/:email", a.getClientIps)
g.POST("/clearClientIps/:email", a.clearClientIps)
g.POST("/addClient", a.addInboundClient)
g.POST("/delClient/:email", a.delInboundClient)
g.POST("/updateClient/:index", a.updateInboundClient)
g.POST("/:id/delClient/:clientId", a.delInboundClient)
g.POST("/updateClient/:clientId", a.updateInboundClient)
g.POST("/:id/resetClientTraffic/:email", a.resetClientTraffic)
g.POST("/resetAllTraffics", a.resetAllTraffics)
g.POST("/resetAllClientTraffics/:id", a.resetAllClientTraffics)
g.POST("/delDepletedClients/:id", a.delDepletedClients)
}
@@ -78,6 +79,16 @@ func (a *InboundController) getInbound(c *gin.Context) {
jsonObj(c, inbound, nil)
}
func (a *InboundController) getClientTraffics(c *gin.Context) {
email := c.Param("email")
clientTraffics, err := a.inboundService.GetClientTrafficByEmail(email)
if err != nil {
jsonMsg(c, "Error getting traffics", err)
return
}
jsonObj(c, clientTraffics, nil)
}
func (a *InboundController) addInbound(c *gin.Context) {
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
@@ -145,7 +156,7 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
err := a.inboundService.ClearClientIps(email)
if err != nil {
jsonMsg(c, "修改", err)
jsonMsg(c, "Revise", err)
return
}
jsonMsg(c, "Log Cleared", nil)
@@ -160,7 +171,7 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
err = a.inboundService.AddInboundClient(data)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client(s) added", nil)
@@ -170,17 +181,16 @@ func (a *InboundController) addInboundClient(c *gin.Context) {
}
func (a *InboundController) delInboundClient(c *gin.Context) {
email := c.Param("email")
inbound := &model.Inbound{}
err := c.ShouldBind(inbound)
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
clientId := c.Param("clientId")
err = a.inboundService.DelInboundClient(inbound, email)
err = a.inboundService.DelInboundClient(id, clientId)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client deleted", nil)
@@ -190,22 +200,18 @@ func (a *InboundController) delInboundClient(c *gin.Context) {
}
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
}
clientId := c.Param("clientId")
inbound := &model.Inbound{}
err = c.ShouldBind(inbound)
err := c.ShouldBind(inbound)
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
err = a.inboundService.UpdateInboundClient(inbound, index)
err = a.inboundService.UpdateInboundClient(inbound, clientId)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "Client updated", nil)
@@ -224,7 +230,7 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
err = a.inboundService.ResetClientTraffic(id, email)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "traffic reseted", nil)
@@ -236,7 +242,7 @@ func (a *InboundController) resetClientTraffic(c *gin.Context) {
func (a *InboundController) resetAllTraffics(c *gin.Context) {
err := a.inboundService.ResetAllTraffics()
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "All traffics reseted", nil)
@@ -251,8 +257,22 @@ func (a *InboundController) resetAllClientTraffics(c *gin.Context) {
err = a.inboundService.ResetAllClientTraffics(id)
if err != nil {
jsonMsg(c, "something worng!", err)
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "All traffics of client reseted", nil)
}
func (a *InboundController) delDepletedClients(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id"))
if err != nil {
jsonMsg(c, I18n(c, "pages.inbounds.revise"), err)
return
}
err = a.inboundService.DelDepletedClients(id)
if err != nil {
jsonMsg(c, "Something went wrong!", err)
return
}
jsonMsg(c, "All delpeted clients are deleted", nil)
}

View File

@@ -11,15 +11,17 @@ import (
)
type LoginForm struct {
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
Username string `json:"username" form:"username"`
Password string `json:"password" form:"password"`
LoginSecret string `json:"loginSecret" form:"loginSecret"`
}
type IndexController struct {
BaseController
userService service.UserService
tgbot service.Tgbot
settingService service.SettingService
userService service.UserService
tgbot service.Tgbot
}
func NewIndexController(g *gin.RouterGroup) *IndexController {
@@ -32,6 +34,7 @@ func (a *IndexController) initRouter(g *gin.RouterGroup) {
g.GET("/", a.index)
g.POST("/login", a.login)
g.GET("/logout", a.logout)
g.POST("/getSecretStatus", a.getSecretStatus)
}
func (a *IndexController) index(c *gin.Context) {
@@ -57,7 +60,7 @@ func (a *IndexController) login(c *gin.Context) {
pureJsonMsg(c, false, I18n(c, "pages.login.toasts.emptyPassword"))
return
}
user := a.userService.CheckUser(form.Username, form.Password)
user := a.userService.CheckUser(form.Username, form.Password, form.LoginSecret)
timeStr := time.Now().Format("2006-01-02 15:04:05")
if user == nil {
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
@@ -69,6 +72,16 @@ func (a *IndexController) login(c *gin.Context) {
a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
}
sessionMaxAge, err := a.settingService.GetSessionMaxAge()
if err != nil {
logger.Infof("Unable to get session's max age from DB")
}
err = session.SetMaxAge(c, sessionMaxAge*60)
if err != nil {
logger.Infof("Unable to set session's max age")
}
err = session.SetLoginUser(c, user)
logger.Info("user", user.Id, "login success")
jsonMsg(c, I18n(c, "pages.login.toasts.successLogin"), err)
@@ -82,3 +95,11 @@ func (a *IndexController) logout(c *gin.Context) {
session.ClearSession(c)
c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
}
func (a *IndexController) getSecretStatus(c *gin.Context) {
status, err := a.settingService.GetSecretStatus()
if err == nil {
jsonObj(c, status, nil)
}
}

View File

@@ -17,6 +17,10 @@ type updateUserForm struct {
NewPassword string `json:"newPassword" form:"newPassword"`
}
type updateSecretForm struct {
LoginSecret string `json:"loginSecret" form:"loginSecret"`
}
type SettingController struct {
settingService service.SettingService
userService service.UserService
@@ -38,6 +42,8 @@ func (a *SettingController) initRouter(g *gin.RouterGroup) {
g.POST("/updateUser", a.updateUser)
g.POST("/restartPanel", a.restartPanel)
g.GET("/getDefaultJsonConfig", a.getDefaultJsonConfig)
g.POST("/updateUserSecret", a.updateSecret)
g.POST("/getUserSecret", a.getUserSecret)
}
func (a *SettingController) getAllSetting(c *gin.Context) {
@@ -128,3 +134,25 @@ func (a *SettingController) restartPanel(c *gin.Context) {
err := a.panelService.RestartPanel(time.Second * 3)
jsonMsg(c, I18n(c, "pages.setting.restartPanel"), err)
}
func (a *SettingController) updateSecret(c *gin.Context) {
form := &updateSecretForm{}
err := c.ShouldBind(form)
if err != nil {
jsonMsg(c, I18n(c, "pages.setting.toasts.modifySetting"), err)
}
user := session.GetLoginUser(c)
err = a.userService.UpdateUserSecret(user.Id, form.LoginSecret)
if err == nil {
user.LoginSecret = form.LoginSecret
session.SetLoginUser(c, user)
}
jsonMsg(c, I18n(c, "pages.setting.toasts.modifyUser"), err)
}
func (a *SettingController) getUserSecret(c *gin.Context) {
loginUser := session.GetLoginUser(c)
user := a.userService.GetUserSecret(loginUser.Id)
if user != nil {
jsonObj(c, user, nil)
}
}

View File

@@ -39,7 +39,7 @@ func (a *SUBController) subs(c *gin.Context) {
}
// Add subscription-userinfo
c.Writer.Header().Set("subscription-userinfo", header)
c.Writer.Header().Set("Subscription-Userinfo", header)
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
}

View File

@@ -1,24 +1,16 @@
package controller
import (
"github.com/gin-gonic/gin"
"net"
"net/http"
"strings"
"x-ui/config"
"x-ui/logger"
"x-ui/web/entity"
"github.com/gin-gonic/gin"
)
func getUriId(c *gin.Context) int64 {
s := struct {
Id int64 `uri:"id"`
}{}
_ = c.BindUri(&s)
return s.Id
}
func getRemoteIp(c *gin.Context) string {
value := c.GetHeader("X-Forwarded-For")
if value != "" {
@@ -75,6 +67,7 @@ func html(c *gin.Context, name string, title string, data gin.H) {
data = gin.H{}
}
data["title"] = title
data["host"] = strings.Split(c.Request.Host, ":")[0]
data["request_uri"] = c.Request.RequestURI
data["base_path"] = c.GetString("base_path")
c.HTML(http.StatusOK, name, getContext(data))
@@ -84,10 +77,8 @@ func getContext(h gin.H) gin.H {
a := gin.H{
"cur_ver": config.GetVersion(),
}
if h != nil {
for key, value := range h {
a[key] = value
}
for key, value := range h {
a[key] = value
}
return a
}

View File

@@ -32,6 +32,7 @@ type AllSetting struct {
WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
WebBasePath string `json:"webBasePath" form:"webBasePath"`
SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"`
ExpireDiff int `json:"expireDiff" form:"expireDiff"`
TrafficDiff int `json:"trafficDiff" form:"trafficDiff"`
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
@@ -42,6 +43,7 @@ type AllSetting struct {
TgCpu int `json:"tgCpu" form:"tgCpu"`
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
TimeLocation string `json:"timeLocation" form:"timeLocation"`
SecretEnable bool `json:"secretEnable" form:"secretEnable"`
}
func (s *AllSetting) CheckValid() error {

View File

@@ -2,8 +2,9 @@ package global
import (
"context"
"github.com/robfig/cron/v3"
_ "unsafe"
"github.com/robfig/cron/v3"
)
var webServer WebServer

View File

@@ -7,12 +7,13 @@
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
<link rel="shortcut icon" href="/favicon.ico" type="image/x-icon">
<link rel=icon type=”image/x-icon” href="{{ .base_path }}assets/favicon.ico">
<link rel="shortcut icon" type="image/x-icon" href="{{ .base_path }}assets/favicon.ico">
<style>
[v-cloak] {
display: none;
}
</style>
<title>{{ i18n .title}}</title>
<title>{{ .host }}-{{ i18n .title}}</title>
</head>
{{end}}

View File

@@ -57,6 +57,11 @@
<a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
</a-input>
</a-form-item>
<a-form-item v-if="secretEnable">
<a-input type="text" placeholder='{{ i18n "secretToken" }}' v-model.trim="user.loginSecret" @keydown.enter.native="login">
<a-icon slot="prefix" type="key" style="color: rgba(0,0,0,.25)"/>
</a-input>
</a-form-item>
<a-form-item>
<a-button block @click="login" :loading="loading">{{ i18n "login" }}</a-button>
</a-form-item>
@@ -98,10 +103,12 @@
data: {
loading: false,
user: new User(),
secretEnable: false,
lang : ""
},
created(){
this.lang = getLang();
this.secretEnable = this.getSecretStatus();
},
methods: {
async login() {
@@ -111,6 +118,15 @@
if (msg.success) {
location.href = basePath + 'xui/';
}
},
async getSecretStatus() {
this.loading= true;
const msg = await HttpUtil.post('/getSecretStatus');
this.loading = false;
if (msg.success){
this.secretEnable = msg.obj;
return msg.obj;
}
}
}
});

View File

@@ -46,13 +46,13 @@
<a-input type="number" v-model.number="clientsBulkModal.limitIp" min="0" style="width: 70px;" ></a-input>
</a-form-item>
<a-form-item v-if="clientsBulkModal.inbound.xtls" label="Flow">
<a-select v-model="clientsBulkModal.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<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-if="clientsBulkModal.inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="clientsBulkModal.flow" style="width: 150px">
<a-select v-model="clientsBulkModal.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<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>
@@ -91,7 +91,7 @@
<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"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="clientsBulkModal.expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>

View File

@@ -17,13 +17,14 @@
inbound: new Inbound(),
clients: [],
clientStats: [],
oldClientId: "",
index: null,
clientIps: null,
isExpired: false,
delayedStart: false,
ok() {
if(clientModal.isEdit){
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.index);
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
} else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
}
@@ -39,12 +40,13 @@
this.index = index === null ? this.clients.length : index;
this.isExpired = isEdit ? this.inbound.isExpiry(this.index) : false;
this.delayedStart = false;
if (!isEdit){
this.addClient(this.inbound.protocol, this.clients);
} else {
if (isEdit){
if (this.clients[index].expiryTime < 0){
this.delayedStart = true;
}
this.oldClientId = this.dbInbound.protocol == "trojan" ? this.clients[index].password : this.clients[index].id;
} else {
this.addClient(this.inbound.protocol, this.clients);
}
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm;
@@ -144,6 +146,24 @@
}
document.getElementById("clientIPs").value = ""
},
resetClientTraffic(email,dbInboundId,iconElement) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetTraffic"}}',
content: '{{ i18n "pages.inbounds.resetTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/xui/inbound/' + dbInboundId + '/resetClientTraffic/'+ email);
if (msg.success) {
this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0;
}
iconElement.disabled = false;
},
})
},
},
});
</script>

View File

@@ -66,7 +66,7 @@
const siderDrawer = {
visible: false,
collapsed: false,
isDarkTheme: localStorage.getItem("dark-mode") === 'true' ? true : false,
isDarkTheme: localStorage.getItem("dark-mode") === 'false' ? false : true,
show() {
this.visible = true;
},

View File

@@ -9,7 +9,7 @@
<a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
</template>
<template v-else-if="type === 'number'">
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)"></a-input>
<a-input type="number" :value="value" @input="$emit('input', $event.target.value)" :min="min"></a-input>
</template>
<template v-else-if="type === 'textarea'">
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
@@ -25,7 +25,7 @@
{{define "component/setting"}}
<script>
Vue.component('setting-list-item', {
props: ["type", "title", "desc", "value"],
props: ["type", "title", "desc", "value", "min"],
template: `{{template "component/settingListItem"}}`,
});
</script>

View File

@@ -69,13 +69,13 @@
</a-form>
</a-form-item>
<a-form-item v-if="inbound.xtls" label="Flow">
<a-select v-model="client.flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="client.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<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 v-model="client.flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<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>
@@ -98,6 +98,10 @@
[[ sizeFormat(clientStats.down) ]]
([[ sizeFormat(clientStats.up + clientStats.down) ]])
</a-tag>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)" v-if="client.email.length > 0"></a-icon>
</a-tooltip>
</template>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
@@ -116,7 +120,7 @@
<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"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
: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>

View File

@@ -49,7 +49,7 @@
<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"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
</a-form-item>

View File

@@ -18,6 +18,12 @@
</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 label="Subscription" v-if="client.email">
<a-input v-model.trim="client.subId"></a-input>
</a-form-item>
<a-form-item label="Telegram Username" v-if="client.email">
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
@@ -59,7 +65,7 @@
<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"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>

View File

@@ -18,6 +18,12 @@
</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="Subscription" v-if="client.email">
<a-input v-model.trim="client.subId"></a-input>
</a-form-item>
<a-form-item label="Telegram Username" v-if="client.email">
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
@@ -32,13 +38,13 @@
<a-input type="number" v-model.number="client.limitIp" min="0" style="width: 70px;" ></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" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<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>
</a-form-item>
<a-form-item v-else-if="inbound.canEnableTlsFlow()" label="Flow" layout="inline">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 150px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select v-model="inbound.settings.vlesses[index].flow" style="width: 200px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<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>
@@ -65,7 +71,7 @@
<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"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>

View File

@@ -21,6 +21,12 @@
</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 label="Subscription" v-if="client.email">
<a-input v-model.trim="client.subId"></a-input>
</a-form-item>
<a-form-item label="Telegram Username" v-if="client.email">
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
@@ -56,7 +62,7 @@
<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"
<a-date-picker :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''"
v-model="client._expiryTime" style="width: 170px;"></a-date-picker>
</a-form-item>

View File

@@ -12,5 +12,10 @@
</span>
<a-switch v-model="inbound.sniffing.enabled"></a-switch>
</a-form-item>
<a-form-item>
<a-checkbox-group v-model="inbound.sniffing.destOverride" v-if="inbound.sniffing.enabled">
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
</a-form>
{{end}}

View File

@@ -3,5 +3,8 @@
<a-form-item label="ServiceName">
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
</a-form-item>
<a-form-item label="Multi Mode">
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
</a-form-item>
</a-form>
{{end}}

View File

@@ -37,7 +37,7 @@
<a-input v-model.trim="inbound.stream.tls.server" style="width: 250px"></a-input>
</a-form-item>
<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" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option value="">auto</a-select-option>
<a-select-option v-for="key in TLS_CIPHER_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
@@ -56,14 +56,15 @@
<a-input v-model.trim="inbound.stream.tls.settings.serverName" style="width: 250px"></a-input>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 170px">
<a-select v-model="inbound.stream.tls.settings.fingerprint"
style="width: 170px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<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="Alpn">
<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 v-for="key,value in ALPN_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="Allow insecure">
@@ -82,7 +83,7 @@
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.tls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
<a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
@@ -120,7 +121,7 @@
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
<a-input v-model.trim="inbound.stream.xtls.certs[0].keyFile" style="width:300px;"></a-input>
</a-form-item>
<a-button @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
<a-button type="primary" icon="import" @click="setDefaultCertData">{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
@@ -142,7 +143,8 @@
<a-input type="number" v-model.number="inbound.stream.reality.xver" :min="0" style="width: 60px"></a-input>
</a-form-item>
<a-form-item label="uTLS" >
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 135px">
<a-select v-model="inbound.stream.reality.settings.fingerprint"
style="width: 135px" :dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>

View File

@@ -41,6 +41,7 @@
<template v-if="inbound.isGrpc">
<tr><td>grpc serviceName</td><td><a-tag color="green">[[ inbound.serviceName ]]</a-tag></td></tr>
<tr><td>grpc multiMode</td><td><a-tag color="green">[[ inbound.stream.grpc.multiMode ]]</a-tag></td></tr>
</template>
</table>
</td></tr>

View File

@@ -41,19 +41,19 @@
<a-col :xs="24" :sm="24" :lg="12">
{{ i18n "clients" }}:
<a-tag color="green">[[ total.clients ]]</a-tag>
<a-popover title="{{ i18n "disabled" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "disabled" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content">
<p v-for="clientEmail in total.deactive">[[ clientEmail ]]</p>
</template>
<a-tag v-if="total.deactive.length">[[ total.deactive.length ]]</a-tag>
</a-popover>
<a-popover title="{{ i18n "depleted" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "depleted" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content">
<p v-for="clientEmail in total.depleted">[[ clientEmail ]]</p>
</template>
<a-tag color="red" v-if="total.depleted.length">[[ total.depleted.length ]]</a-tag>
</a-popover>
<a-popover title="{{ i18n "depletingSoon" }}" :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<a-popover title='{{ i18n "depletingSoon" }}' :overlay-class-name="siderDrawer.isDarkTheme ? 'ant-dark' : ''">
<template slot="content">
<p v-for="clientEmail in total.expiring">[[ clientEmail ]]</p>
</template>
@@ -66,11 +66,44 @@
<transition name="list" appear>
<a-card hoverable :class="siderDrawer.isDarkTheme ? darkClass : ''">
<div slot="title">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-button type="primary" icon="export" @click="exportAllLinks">{{ i18n "pages.inbounds.export" }}</a-button>
<a-button type="primary" icon="reload" @click="resetAllTraffic">{{ i18n "pages.inbounds.resetAllTraffic" }}</a-button>
<a-row>
<a-col :xs="24" :sm="24" :lg="12">
<a-button type="primary" icon="plus" @click="openAddInbound">{{ i18n "pages.inbounds.addInbound" }}</a-button>
<a-dropdown :trigger="['click']">
<a-button type="primary" icon="menu">{{ i18n "pages.inbounds.generalActions" }}</a-button>
<a-menu slot="overlay" @click="a => generalActions(a)" :theme="siderDrawer.theme">
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }}
</a-menu-item>
<a-menu-item key="resetInbounds">
<a-icon type="reload"></a-icon>
{{ i18n "pages.inbounds.resetAllTraffic" }}
</a-menu-item>
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
</a-menu-item>
<a-menu-item key="delDepletedClients">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</a-menu>
</a-dropdown>
</a-col>
<a-col :xs="24" :sm="24" :lg="12" style="text-align: right;">
<a-select v-model="refreshInterval"
v-if="isRefreshEnabled"
@change="changeRefreshInterval"
:dropdown-class-name="siderDrawer.isDarkTheme ? 'ant-card-dark' : ''">
<a-select-option v-for="key in [5,10,30,60]" :value="key*1000">[[ key ]]s</a-select-option>
</a-select>
<a-icon type="sync" :spin="isRefreshEnabled"></a-icon>
<a-switch v-model="isRefreshEnabled" @change="toggleRefresh"></a-switch>
</a-col>
</a-row>
</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"
:data-source="searchedInbounds"
:loading="spinning" :scroll="{ x: 1300 }"
@@ -78,7 +111,7 @@
style="margin-top: 20px"
@change="() => getDBInbounds()">
<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: 22px" @click="openEditInbound(dbInbound.id);"></a-icon>
<a-dropdown :trigger="['click']">
<a @click="e => e.preventDefault()">{{ i18n "pages.inbounds.operate" }}</a>
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)" :theme="siderDrawer.theme">
@@ -101,12 +134,16 @@
</a-menu-item>
<a-menu-item key="resetClients">
<a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics"}}
{{ i18n "pages.inbounds.resetInboundClientTraffics"}}
</a-menu-item>
<a-menu-item key="export">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}}
</a-menu-item>
<a-menu-item key="delDepletedClients">
<a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item>
</template>
<template v-else>
<a-menu-item key="showInfo">
@@ -240,7 +277,7 @@
}, {
title: '{{ i18n "pages.inbounds.protocol" }}',
align: 'left',
width: 80,
width: 90,
scopedSlots: { customRender: 'protocol' },
}, {
title: '{{ i18n "clients" }}',
@@ -292,25 +329,22 @@
defaultCert: '',
defaultKey: '',
clientCount: {},
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
},
methods: {
loading(spinning=true) {
this.spinning = spinning;
},
async getDBInbounds() {
this.loading();
const msg = await HttpUtil.post('/xui/inbound/list');
this.loading(false);
if (!msg.success) {
return;
}
this.setInbounds(msg.obj);
this.searchKey = '';
},
async getDefaultSettings() {
this.loading();
const msg = await HttpUtil.post('/xui/setting/defaultSettings');
this.loading(false);
if (!msg.success) {
return;
}
@@ -322,17 +356,16 @@
setInbounds(dbInbounds) {
this.inbounds.splice(0);
this.dbInbounds.splice(0);
this.searchedInbounds.splice(0);
for (const inbound of dbInbounds) {
const dbInbound = new DBInbound(inbound);
to_inbound = dbInbound.toInbound()
this.inbounds.push(to_inbound);
this.dbInbounds.push(dbInbound);
this.searchedInbounds.push(dbInbound);
if([Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN].includes(inbound.protocol) ){
this.clientCount[inbound.id] = this.getClientCounts(inbound,to_inbound);
}
}
this.searchInbounds(this.searchKey);
},
getClientCounts(dbInbound,inbound){
let clientCount = 0,active = [], deactive = [], depleted = [], expiring = [];
@@ -390,6 +423,22 @@
});
}
},
generalActions(action){
switch (action.key) {
case "export":
this.exportAllLinks();
break;
case "resetInbounds":
this.resetAllTraffic();
break;
case "resetClients":
this.resetAllClientTraffics(-1);
break;
case "delDepletedClients":
this.delDepletedClients(-1)
break;
}
},
clickAction(action, dbInbound) {
switch (action.key) {
case "qrcode":
@@ -422,11 +471,14 @@
case "delete":
this.delInbound(dbInbound.id);
break;
case "delDepletedClients":
this.delDepletedClients(dbInbound.id)
break;
}
},
openCloneInbound(dbInbound) {
this.$confirm({
title: '{{ i18n "pages.inbounds.cloneInbound"}} ' + dbInbound.remark,
title: '{{ i18n "pages.inbounds.cloneInbound"}}' + dbInbound.remark,
content: '{{ i18n "pages.inbounds.cloneInboundContent"}}',
okText: '{{ i18n "pages.inbounds.cloneInboundOk"}}',
cancelText: '{{ i18n "cancel" }}',
@@ -561,9 +613,9 @@
okText: '{{ i18n "pages.client.submitEdit"}}',
dbInbound: dbInbound,
index: index,
confirm: async (client, dbInboundId, index) => {
confirm: async (client, dbInboundId, clientId) => {
clientModal.loading();
await this.updateClient(client, dbInboundId, index);
await this.updateClient(client, dbInboundId, clientId);
clientModal.close();
},
isEdit: true
@@ -580,12 +632,12 @@
};
await this.submit(`/xui/inbound/addClient`, data);
},
async updateClient(client, dbInboundId, index) {
async updateClient(client, dbInboundId, clientId) {
const data = {
id: dbInboundId,
settings: '{"clients": [' + client.toString() +']}',
};
await this.submit(`/xui/inbound/updateClient/${index}`, data);
await this.submit(`/xui/inbound/updateClient/${clientId}`, data);
},
resetTraffic(dbInboundId) {
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
@@ -615,22 +667,14 @@
},
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(),
};
clientId = dbInbound.protocol == "trojan" ? client.password : client.id;
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),
onOk: () => this.submit(`/xui/inbound/${dbInboundId}/delClient/${clientId}`),
});
},
getClients(protocol, clientSettings) {
@@ -658,8 +702,9 @@
inbound = dbInbound.toInbound();
clients = this.getClients(dbInbound.protocol, inbound.settings);
index = this.findIndexOfClient(clients, client);
clients[index].enable = ! clients[index].enable
await this.updateClient(inbound, dbInbound, index);
clients[index].enable = !clients[index].enable;
clientId = dbInbound.protocol == "trojan" ? clients[index].password : clients[index].id;
await this.updateClient(clients[index],dbInboundId, clientId);
this.loading(false);
},
async submit(url, data) {
@@ -699,14 +744,24 @@
},
resetAllClientTraffics(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
title: dbInboundId>0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficTitle"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficTitle"}}',
content: dbInboundId>0 ? '{{ i18n "pages.inbounds.resetInboundClientTrafficContent"}}' : '{{ i18n "pages.inbounds.resetAllClientTrafficContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/resetAllClientTraffics/' + dbInboundId),
})
},
delDepletedClients(dbInboundId) {
this.$confirm({
title: '{{ i18n "pages.inbounds.delDepletedClientsTitle"}}',
content: '{{ i18n "pages.inbounds.delDepletedClientsContent"}}',
class: siderDrawer.isDarkTheme ? darkClass : '',
okText: '{{ i18n "reset"}}',
cancelText: '{{ i18n "cancel"}}',
onOk: () => this.submit('/xui/inbound/delDepletedClients/' + dbInboundId),
})
},
isExpiry(dbInbound, index) {
return dbInbound.toInbound().isExpiry(index)
},
@@ -743,6 +798,25 @@
}
txtModal.show('{{ i18n "pages.inbounds.export"}}',copyText,'All-Inbounds');
},
async startDataRefreshLoop() {
while (this.isRefreshEnabled) {
try {
await this.getDBInbounds();
} catch (e) {
console.error(e);
}
await PromiseUtil.sleep(this.refreshInterval);
}
},
toggleRefresh() {
localStorage.setItem("isRefreshEnabled", this.isRefreshEnabled);
if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
},
changeRefreshInterval(){
localStorage.setItem("refreshInterval", this.refreshInterval);
},
},
watch: {
searchKey: debounce(function (newVal) {
@@ -750,8 +824,15 @@
}, 500)
},
mounted() {
this.loading();
this.getDefaultSettings();
this.getDBInbounds();
if (this.isRefreshEnabled) {
this.startDataRefreshLoop();
}
else {
this.getDBInbounds();
}
this.loading(false);
},
computed: {
total() {
@@ -778,7 +859,6 @@
}
},
});
</script>
{{template "inboundModal"}}

View File

@@ -45,6 +45,7 @@
<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.panelUrlPath"}}' desc='{{ i18n "pages.setting.panelUrlPathDesc"}}' v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.sessionMaxAge" }}' desc='{{ i18n "pages.setting.sessionMaxAgeDesc" }}' v-model="allSetting.sessionMaxAge" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.expireTimeDiff" }}' desc='{{ i18n "pages.setting.expireTimeDiffDesc" }}' v-model="allSetting.expireDiff" :min="0"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.setting.trafficDiff" }}' desc='{{ i18n "pages.setting.trafficDiffDesc" }}' v-model="allSetting.trafficDiff" :min="0"></setting-list-item>
<a-list-item>
@@ -91,8 +92,39 @@
<a-button type="primary" @click="updateUser">{{ i18n "confirm" }}</a-button>
</a-form-item>
</a-form>
<a-form :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65); padding: 20px;': 'background: white; padding: 20px;'">
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.setting.loginSecurity" }}' description='{{ i18n "pages.setting.loginSecurityDesc" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-switch @change="toggleToken(allSetting.secretEnable)" v-model="allSetting.secretEnable"></a-switch>
</template>
</a-col>
</a-row>
</a-list-item>
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.setting.secretToken" }}' description='{{ i18n "pages.setting.secretTokenDesc" }}'/>
</a-col>
<a-col :lg="24" :xl="12">
<svg
@click="getNewSecret"
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>
<template>
<a-textarea type="text" id='token' :disabled="!allSetting.secretEnable" v-model="user.loginSecret"></a-textarea>
</template>
</a-col>
</a-row>
</a-list-item>
<a-button type="primary" @click="updateSecret">{{ i18n "confirm" }}</a-button>
</a-form>
</a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.setting.xrayConfiguration"}}'>
<a-list item-layout="horizontal" :style="siderDrawer.isDarkTheme ? 'color: hsla(0,0%,100%,.65);': 'background: white;'">
<a-divider>{{ i18n "pages.setting.actions"}}</a-divider>
@@ -103,12 +135,24 @@
<a-divider>{{ i18n "pages.setting.basicTemplate"}}</a-divider>
<a-collapse>
<a-collapse-panel header='{{ i18n "pages.setting.generalConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.setting.generalConfigsDesc" }}
</h2>
</a-row>
<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>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigAds"}}' desc='{{ i18n "pages.setting.xrayConfigAdsDesc"}}' v-model="AdsSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigPorn"}}' desc='{{ i18n "pages.setting.xrayConfigPornDesc"}}' v-model="PornSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.setting.countryConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.setting.countryConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigIRIp"}}' desc='{{ i18n "pages.setting.xrayConfigIRIpDesc"}}' v-model="IRIpSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigIRDomain"}}' desc='{{ i18n "pages.setting.xrayConfigIRDomainDesc"}}' v-model="IRDomainSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigChinaIp"}}' desc='{{ i18n "pages.setting.xrayConfigChinaIpDesc"}}' v-model="ChinaIpSettings"></setting-list-item>
@@ -117,10 +161,22 @@
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigRussiaDomain"}}' desc='{{ i18n "pages.setting.xrayConfigRussiaDomainDesc"}}' v-model="RussiaDomainSettings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.setting.ipv4Configs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.setting.ipv4ConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigGoogleIPv4"}}' desc='{{ i18n "pages.setting.xrayConfigGoogleIPv4Desc"}}' v-model="GoogleIPv4Settings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigNetflixIPv4"}}' desc='{{ i18n "pages.setting.xrayConfigNetflixIPv4Desc"}}' v-model="NetflixIPv4Settings"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.setting.warpConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12">
<h2 style="color: inherit; font-weight: bold; font-size: 18px; padding: 10px 20px; border-bottom: 2px solid;">
<a-icon type="warning" style="color: inherit; font-size: 24px;"></a-icon>
{{ i18n "pages.setting.warpConfigsDesc" }}
</h2>
</a-row>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigGoogleWARP"}}' desc='{{ i18n "pages.setting.xrayConfigGoogleWARPDesc"}}' v-model="GoogleWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigOpenAIWARP"}}' desc='{{ i18n "pages.setting.xrayConfigOpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.setting.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.setting.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
@@ -181,7 +237,7 @@
oldAllSetting: new AllSetting(),
allSetting: new AllSetting(),
saveBtnDisable: true,
user: {},
user: new User(),
lang: getLang(),
ipv4Settings: {
tag: "IPv4",
@@ -238,8 +294,9 @@
}
},
methods: {
loading(spinning = true) {
this.spinning = spinning;
loading(spinning = true , obj) {
if(obj == null)
this.spinning = spinning;
},
async getAllSetting() {
this.loading(true);
@@ -250,6 +307,7 @@
this.allSetting = new AllSetting(msg.obj);
this.saveBtnDisable = true;
}
await this.getUserSecret();
},
async updateAllSetting() {
this.loading(true);
@@ -286,6 +344,41 @@
location.reload();
}
},
async getUserSecret(){
const user_msg = await HttpUtil.post("/xui/setting/getUserSecret", this.user);
if (user_msg.success){
this.user = user_msg.obj;
}
this.loading(false);
},
async updateSecret(){
this.loading(true);
const msg = await HttpUtil.post("/xui/setting/updateUserSecret", this.user);
if (msg.success){
this.user = msg.obj;
}
this.loading(false);
await this.updateAllSetting();
},
async getNewSecret(){
this.loading(true);
await PromiseUtil.sleep(1000);
var chars = 'abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ1234567890';
var string = '';
var len = 64;
for(var ii=0; ii<len; ii++){
string += chars[Math.floor(Math.random() * chars.length)];
}
this.user.loginSecret = string;
document.getElementById('token').value =this.user.loginSecret;
this.loading(false);
},
async toggleToken(value){
if(value)
this.getNewSecret();
else
this.user.loginSecret = "";
},
async resetXrayConfigToDefault() {
this.loading(true);
const msg = await HttpUtil.get("/xui/setting/getDefaultJsonConfig");

View File

@@ -4,23 +4,22 @@ import (
"encoding/json"
"os"
"regexp"
ss "strings"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/web/service"
"x-ui/xray"
// "strconv"
"github.com/go-cmd/cmd"
"net"
"sort"
"strings"
"time"
"github.com/go-cmd/cmd"
)
type CheckClientIpJob struct {
xrayService service.XrayService
inboundService service.InboundService
xrayService service.XrayService
}
var job *CheckClientIpJob
@@ -36,7 +35,7 @@ func (j *CheckClientIpJob) Run() {
processLogFile()
// disAllowedIps = []string{"192.168.1.183","192.168.1.197"}
blockedIps := []byte(ss.Join(disAllowedIps, ","))
blockedIps := []byte(strings.Join(disAllowedIps, ","))
err := os.WriteFile(xray.GetBlockedIPsPath(), blockedIps, 0755)
checkError(err)
@@ -58,7 +57,7 @@ func processLogFile() {
checkError(err)
}
lines := ss.Split(string(data), "\n")
lines := strings.Split(string(data), "\n")
for _, line := range lines {
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
emailRegx, _ := regexp.Compile(`email:.+`)
@@ -74,7 +73,7 @@ func processLogFile() {
if matchesEmail == "" {
continue
}
matchesEmail = ss.Split(matchesEmail, "email: ")[1]
matchesEmail = strings.Split(matchesEmail, "email: ")[1]
if InboundClientIps[matchesEmail] != nil {
if contains(InboundClientIps[matchesEmail], ip) {
@@ -92,14 +91,12 @@ func processLogFile() {
for clientEmail, ips := range InboundClientIps {
inboundClientIps, err := GetInboundClientIps(clientEmail)
sort.Sort(sort.StringSlice(ips))
sort.Strings(ips)
if err != nil {
addInboundClientIps(clientEmail, ips)
} else {
updateInboundClientIps(inboundClientIps, clientEmail, ips)
}
}
// check if inbound connection is more than limited ip and drop connection
@@ -202,6 +199,8 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
json.Unmarshal([]byte(inbound.Settings), &settings)
clients := settings["clients"]
var disAllowedIps []string // initialize the slice
for _, client := range clients {
if client.Email == clientEmail {
@@ -214,7 +213,7 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
}
}
logger.Debug("disAllowedIps ", disAllowedIps)
sort.Sort(sort.StringSlice(disAllowedIps))
sort.Strings(disAllowedIps)
db := database.GetDB()
err = db.Save(inboundClientIps).Error
@@ -223,6 +222,7 @@ func updateInboundClientIps(inboundClientIps *model.InboundClientIps, clientEmai
}
return nil
}
func DisableInbound(id int) error {
db := database.GetDB()
result := db.Model(model.Inbound{}).

View File

@@ -3,6 +3,7 @@ package service
import (
"encoding/json"
"fmt"
"strings"
"time"
"x-ui/database"
"x-ui/database/model"
@@ -266,11 +267,18 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) error {
if err != nil {
return err
}
existEmail, err := s.checkEmailsExistForClients(clients)
var settings map[string]interface{}
err = json.Unmarshal([]byte(data.Settings), &settings)
if err != nil {
return err
}
interfaceClients := settings["clients"].([]interface{})
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return err
}
if existEmail != "" {
return common.NewError("Duplicate email:", existEmail)
}
@@ -280,21 +288,18 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) error {
return err
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return err
}
oldClients := settings["clients"].([]interface{})
var newClients []interface{}
for _, client := range clients {
newClients = append(newClients, client)
}
oldClients := oldSettings["clients"].([]interface{})
oldClients = append(oldClients, interfaceClients...)
settings["clients"] = append(oldClients, newClients...)
oldSettings["clients"] = oldClients
newSettings, err := json.MarshalIndent(settings, "", " ")
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil {
return err
}
@@ -310,37 +315,73 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) error {
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)
func (s *InboundService) DelInboundClient(inboundId int, clientId string) error {
oldInbound, err := s.GetInbound(inboundId)
if err != nil {
logger.Error("Load Old Data Error")
return err
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
if err != nil {
return err
}
oldInbound.Settings = inbound.Settings
email := ""
client_key := "id"
if oldInbound.Protocol == "trojan" {
client_key = "password"
}
inerfaceClients := settings["clients"].([]interface{})
var newClients []interface{}
for _, client := range inerfaceClients {
c := client.(map[string]interface{})
c_id := c[client_key].(string)
if c_id == clientId {
email = c["email"].(string)
} else {
newClients = append(newClients, client)
}
}
settings["clients"] = newClients
newSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return err
}
oldInbound.Settings = string(newSettings)
db := database.GetDB()
err = s.DelClientStat(db, email)
if err != nil {
logger.Error("Delete stats Data Error")
return err
}
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(data *model.Inbound, index int) error {
func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId string) error {
clients, err := s.getClients(data)
if err != nil {
return err
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(data.Settings), &settings)
if err != nil {
return err
}
inerfaceClients := settings["clients"].([]interface{})
oldInbound, err := s.GetInbound(data.Id)
if err != nil {
return err
@@ -351,7 +392,23 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
return err
}
if len(clients[0].Email) > 0 && clients[0].Email != oldClients[index].Email {
oldEmail := ""
clientIndex := 0
for index, oldClient := range oldClients {
oldClientId := ""
if oldInbound.Protocol == "trojan" {
oldClientId = oldClient.Password
} else {
oldClientId = oldClient.ID
}
if clientId == oldClientId {
oldEmail = oldClient.Email
clientIndex = index
break
}
}
if len(clients[0].Email) > 0 && clients[0].Email != oldEmail {
existEmail, err := s.checkEmailsExistForClients(clients)
if err != nil {
return err
@@ -361,20 +418,16 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
}
}
var settings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &settings)
var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return err
}
settingsClients := oldSettings["clients"].([]interface{})
settingsClients[clientIndex] = inerfaceClients[0]
oldSettings["clients"] = settingsClients
settingsClients := settings["clients"].([]interface{})
var newClients []interface{}
newClients = append(newClients, clients[0])
settingsClients[index] = newClients[0]
settings["clients"] = settingsClients
newSettings, err := json.MarshalIndent(settings, "", " ")
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil {
return err
}
@@ -383,12 +436,12 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
db := database.GetDB()
if len(clients[0].Email) > 0 {
if len(oldClients[index].Email) > 0 {
err = s.UpdateClientStat(oldClients[index].Email, &clients[0])
if len(oldEmail) > 0 {
err = s.UpdateClientStat(oldEmail, &clients[0])
if err != nil {
return err
}
err = s.UpdateClientIPs(db, oldClients[index].Email, clients[index].Email)
err = s.UpdateClientIPs(db, oldEmail, clients[0].Email)
if err != nil {
return err
}
@@ -396,11 +449,11 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
s.AddClientStat(data.Id, &clients[0])
}
} else {
err = s.DelClientStat(db, oldClients[index].Email)
err = s.DelClientStat(db, oldEmail)
if err != nil {
return err
}
err = s.DelClientIPs(db, oldClients[index].Email)
err = s.DelClientIPs(db, oldEmail)
if err != nil {
return err
}
@@ -408,45 +461,35 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, index int) err
return db.Save(oldInbound).Error
}
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
func (s *InboundService) AddTraffic(traffics []*xray.Traffic) error {
if len(traffics) == 0 {
return nil
}
db := database.GetDB()
db = db.Model(model.Inbound{})
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
for _, traffic := range traffics {
if traffic.IsInbound {
err = tx.Where("tag = ?", traffic.Tag).
UpdateColumns(map[string]interface{}{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down)}).Error
if err != nil {
return
// Update traffics in a single transaction
err := database.GetDB().Transaction(func(tx *gorm.DB) error {
for _, traffic := range traffics {
if traffic.IsInbound {
update := tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
Updates(map[string]interface{}{
"up": gorm.Expr("up + ?", traffic.Up),
"down": gorm.Expr("down + ?", traffic.Down),
})
if update.Error != nil {
return update.Error
}
}
}
}
return
return nil
})
return err
}
func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err error) {
if len(traffics) == 0 {
return nil
}
traffics, err = s.adjustTraffics(traffics)
if err != nil {
return err
}
db := database.GetDB()
db = db.Model(xray.ClientTraffic{})
tx := db.Begin()
defer func() {
@@ -457,7 +500,32 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
}
}()
err = tx.Save(traffics).Error
emails := make([]string, 0, len(traffics))
for _, traffic := range traffics {
emails = append(emails, traffic.Email)
}
dbClientTraffics := make([]*xray.ClientTraffic, 0, len(traffics))
err = db.Model(xray.ClientTraffic{}).Where("email IN (?)", emails).Find(&dbClientTraffics).Error
if err != nil {
return err
}
dbClientTraffics, err = s.adjustTraffics(tx, dbClientTraffics)
if err != nil {
return err
}
for dbTraffic_index := range dbClientTraffics {
for traffic_index := range traffics {
if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
break
}
}
}
err = tx.Save(dbClientTraffics).Error
if err != nil {
logger.Warning("AddClientTraffic update data ", err)
}
@@ -465,81 +533,56 @@ func (s *InboundService) AddClientTraffic(traffics []*xray.ClientTraffic) (err e
return nil
}
func (s *InboundService) adjustTraffics(traffics []*xray.ClientTraffic) (full_traffics []*xray.ClientTraffic, err error) {
db := database.GetDB()
dbInbound := db.Model(model.Inbound{})
txInbound := dbInbound.Begin()
defer func() {
if err != nil {
txInbound.Rollback()
} else {
txInbound.Commit()
func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.ClientTraffic) ([]*xray.ClientTraffic, error) {
inboundIds := make([]int, 0, len(dbClientTraffics))
for _, dbClientTraffic := range dbClientTraffics {
if dbClientTraffic.ExpiryTime < 0 {
inboundIds = append(inboundIds, dbClientTraffic.InboundId)
}
}()
for _, traffic := range traffics {
inbound := &model.Inbound{}
client_traffic := &xray.ClientTraffic{}
err := db.Model(xray.ClientTraffic{}).Where("email = ?", traffic.Email).First(client_traffic).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err, traffic.Email)
}
continue
}
client_traffic.Up += traffic.Up
client_traffic.Down += traffic.Down
err = txInbound.Where("id=?", client_traffic.InboundId).First(inbound).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err, traffic.Email)
}
continue
}
// get clients
clients, err := s.getClients(inbound)
needUpdate := false
if err == nil {
for client_index, client := range clients {
if traffic.Email == client.Email {
if client.ExpiryTime < 0 {
clients[client_index].ExpiryTime = (time.Now().Unix() * 1000) - client.ExpiryTime
needUpdate = true
}
client_traffic.ExpiryTime = client.ExpiryTime
client_traffic.Total = client.TotalGB
break
}
}
}
if needUpdate {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbound.Settings), &settings)
// Convert clients to []interface to update clients in settings
var clientsInterface []interface{}
for _, c := range clients {
clientsInterface = append(clientsInterface, interface{}(c))
}
settings["clients"] = clientsInterface
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}
err = txInbound.Where("id=?", inbound.Id).Update("settings", string(modifiedSettings)).Error
if err != nil {
return nil, err
}
}
full_traffics = append(full_traffics, client_traffic)
}
return full_traffics, nil
if len(inboundIds) > 0 {
var inbounds []*model.Inbound
err := tx.Model(model.Inbound{}).Where("id IN (?)", inboundIds).Find(&inbounds).Error
if err != nil {
return nil, err
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
if ok {
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
for traffic_index := range dbClientTraffics {
if dbClientTraffics[traffic_index].ExpiryTime < 0 && c["email"] == dbClientTraffics[traffic_index].Email {
oldExpiryTime := c["expiryTime"].(float64)
newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime)
c["expiryTime"] = newExpiryTime
dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime
break
}
}
newClients = append(newClients, interface{}(c))
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}
inbounds[inbound_index].Settings = string(modifiedSettings)
}
}
err = tx.Save(inbounds).Error
if err != nil {
logger.Warning("AddClientTraffic update inbounds ", err)
logger.Error(inbounds)
}
}
return dbClientTraffics, nil
}
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
@@ -639,8 +682,15 @@ func (s *InboundService) ResetClientTraffic(id int, clientEmail string) error {
func (s *InboundService) ResetAllClientTraffics(id int) error {
db := database.GetDB()
whereText := "inbound_id "
if id == -1 {
whereText += " > ?"
} else {
whereText += " = ?"
}
result := db.Model(xray.ClientTraffic{}).
Where("inbound_id = ?", id).
Where(whereText, id).
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
err := result.Error
@@ -666,6 +716,84 @@ func (s *InboundService) ResetAllTraffics() error {
return nil
}
func (s *InboundService) DelDepletedClients(id int) (err error) {
db := database.GetDB()
tx := db.Begin()
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
whereText := "inbound_id "
if id < 0 {
whereText += "> ?"
} else {
whereText += "= ?"
}
depletedClients := []xray.ClientTraffic{}
err = db.Model(xray.ClientTraffic{}).Where(whereText+" and enable = ?", id, false).Select("inbound_id, GROUP_CONCAT(email) as email").Group("inbound_id").Find(&depletedClients).Error
if err != nil {
return err
}
for _, depletedClient := range depletedClients {
emails := strings.Split(depletedClient.Email, ",")
oldInbound, err := s.GetInbound(depletedClient.InboundId)
if err != nil {
return err
}
var oldSettings map[string]interface{}
err = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
if err != nil {
return err
}
oldClients := oldSettings["clients"].([]interface{})
var newClients []interface{}
for _, client := range oldClients {
deplete := false
c := client.(map[string]interface{})
for _, email := range emails {
if email == c["email"].(string) {
deplete = true
break
}
}
if !deplete {
newClients = append(newClients, client)
}
}
if len(newClients) > 0 {
oldSettings["clients"] = newClients
newSettings, err := json.MarshalIndent(oldSettings, "", " ")
if err != nil {
return err
}
oldInbound.Settings = string(newSettings)
err = tx.Save(oldInbound).Error
if err != nil {
return err
}
} else {
// Delete inbound if no client remains
s.DelInbound(depletedClient.InboundId)
}
}
err = tx.Where(whereText+" and enable = ?", id, false).Delete(xray.ClientTraffic{}).Error
if err != nil {
return err
}
return nil
}
func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTraffic, error) {
db := database.GetDB()
var inbounds []*model.Inbound
@@ -696,18 +824,20 @@ func (s *InboundService) GetClientTrafficTgBot(tguname string) ([]*xray.ClientTr
return traffics, err
}
func (s *InboundService) GetClientTrafficByEmail(email string) (traffic []*xray.ClientTraffic, err error) {
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
err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
if err != nil {
if err == gorm.ErrRecordNotFound {
logger.Warning(err)
return nil, err
}
logger.Warning(err)
return nil, err
}
return traffics, err
if len(traffics) > 0 {
return traffics[0], nil
}
return nil, nil
}
func (s *InboundService) SearchClientTraffic(query string) (traffic *xray.ClientTraffic, err error) {
@@ -781,3 +911,64 @@ func (s *InboundService) SearchInbounds(query string) ([]*model.Inbound, error)
}
return inbounds, nil
}
func (s *InboundService) MigrationRequirements() {
db := database.GetDB()
// Fix inbounds based problems
var inbounds []*model.Inbound
err := db.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
if err != nil && err != gorm.ErrRecordNotFound {
return
}
for inbound_index := range inbounds {
settings := map[string]interface{}{}
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
clients, ok := settings["clients"].([]interface{})
if ok {
// Fix Clinet configuration problems
var newClients []interface{}
for client_index := range clients {
c := clients[client_index].(map[string]interface{})
// Add email='' if it is not exists
if _, ok := c["email"]; !ok {
c["email"] = ""
}
// Remove "flow": "xtls-rprx-direct"
if _, ok := c["flow"]; ok {
if c["flow"] == "xtls-rprx-direct" {
c["flow"] = ""
}
}
newClients = append(newClients, interface{}(c))
}
settings["clients"] = newClients
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return
}
inbounds[inbound_index].Settings = string(modifiedSettings)
}
// Add client traffic row for all clients which has email
modelClients, err := s.getClients(inbounds[inbound_index])
if err != nil {
return
}
for _, modelClient := range modelClients {
if len(modelClient.Email) > 0 {
var count int64
db.Model(xray.ClientTraffic{}).Where("email = ?", modelClient.Email).Count(&count)
if count == 0 {
s.AddClientStat(inbounds[inbound_index].Id, &modelClient)
}
}
}
}
db.Save(inbounds)
// Remove orphaned traffics
db.Where("inbound_id = 0").Delete(xray.ClientTraffic{})
}

View File

@@ -323,6 +323,10 @@ func (s *ServerService) UpdateXray(version string) error {
if err != nil {
return err
}
err = copyZipFile("iran.dat", xray.GetIranPath())
if err != nil {
return err
}
return nil

View File

@@ -29,6 +29,7 @@ var defaultValueMap = map[string]string{
"webKeyFile": "",
"secret": random.Seq(32),
"webBasePath": "/",
"sessionMaxAge": "0",
"expireDiff": "0",
"trafficDiff": "0",
"timeLocation": "Asia/Tehran",
@@ -38,6 +39,7 @@ var defaultValueMap = map[string]string{
"tgRunTime": "@daily",
"tgBotBackup": "false",
"tgCpu": "0",
"secretEnable": "false",
}
type SettingService struct {
@@ -129,7 +131,13 @@ func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
func (s *SettingService) ResetSettings() error {
db := database.GetDB()
return db.Where("1 = 1").Delete(model.Setting{}).Error
err := db.Where("1 = 1").Delete(model.Setting{}).Error
if err != nil {
return err
}
return db.Model(model.User{}).
Where("1 = 1").
Update("login_secret", "").Error
}
func (s *SettingService) getSetting(key string) (*model.Setting, error) {
@@ -244,18 +252,10 @@ 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) 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) {
return s.getInt("webPort")
}
@@ -276,16 +276,20 @@ func (s *SettingService) GetExpireDiff() (int, error) {
return s.getInt("expireDiff")
}
func (s *SettingService) SetExpireDiff(value int) error {
return s.setInt("expireDiff", value)
}
func (s *SettingService) GetTrafficDiff() (int, error) {
return s.getInt("trafficDiff")
}
func (s *SettingService) SetgetTrafficDiff(value int) error {
return s.setInt("trafficDiff", value)
func (s *SettingService) GetSessionMaxAge() (int, error) {
return s.getInt("sessionMaxAge")
}
func (s *SettingService) GetSecretStatus() (bool, error) {
return s.getBool("secretEnable")
}
func (s *SettingService) SetSecretStatus(value bool) error {
return s.setBool("secretEnable", value)
}
func (s *SettingService) GetSecret() ([]byte, error) {

View File

@@ -66,13 +66,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
}
}
}
header = fmt.Sprintf("upload=%d;download=%d", traffic.Up, traffic.Down)
if traffic.Total > 0 {
header = header + fmt.Sprintf(";total=%d", traffic.Total)
}
if traffic.ExpiryTime > 0 {
header = header + fmt.Sprintf(";expire=%d", traffic.ExpiryTime)
}
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
return result, header, nil
}
@@ -108,80 +102,89 @@ func (s *SubService) getLink(inbound *model.Inbound, email string) string {
}
func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
address := s.address
if inbound.Protocol != model.VMess {
return ""
}
obj := map[string]interface{}{
"v": "2",
"ps": email,
"add": s.address,
"port": inbound.Port,
"type": "none",
}
var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
network, _ := stream["network"].(string)
typeStr := "none"
host := ""
path := ""
sni := ""
fp := ""
var alpn []string
allowInsecure := false
obj["net"] = network
switch network {
case "tcp":
tcp, _ := stream["tcpSettings"].(map[string]interface{})
header, _ := tcp["header"].(map[string]interface{})
typeStr, _ = header["type"].(string)
typeStr, _ := header["type"].(string)
obj["type"] = typeStr
if typeStr == "http" {
request := header["request"].(map[string]interface{})
requestPath, _ := request["path"].([]interface{})
path = requestPath[0].(string)
obj["path"] = requestPath[0].(string)
headers, _ := request["headers"].(map[string]interface{})
host = searchHost(headers)
obj["host"] = searchHost(headers)
}
case "kcp":
kcp, _ := stream["kcpSettings"].(map[string]interface{})
header, _ := kcp["header"].(map[string]interface{})
typeStr, _ = header["type"].(string)
path, _ = kcp["seed"].(string)
obj["type"], _ = header["type"].(string)
obj["path"], _ = kcp["seed"].(string)
case "ws":
ws, _ := stream["wsSettings"].(map[string]interface{})
path = ws["path"].(string)
obj["path"] = ws["path"].(string)
headers, _ := ws["headers"].(map[string]interface{})
host = searchHost(headers)
obj["host"] = searchHost(headers)
case "http":
network = "h2"
obj["net"] = "h2"
http, _ := stream["httpSettings"].(map[string]interface{})
path, _ = http["path"].(string)
host = searchHost(http)
obj["path"], _ = http["path"].(string)
obj["host"] = searchHost(http)
case "quic":
quic, _ := stream["quicSettings"].(map[string]interface{})
header := quic["header"].(map[string]interface{})
typeStr, _ = header["type"].(string)
host, _ = quic["security"].(string)
path, _ = quic["key"].(string)
obj["type"], _ = header["type"].(string)
obj["host"], _ = quic["security"].(string)
obj["path"], _ = quic["key"].(string)
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
path = grpc["serviceName"].(string)
obj["path"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
obj["type"] = "multi"
}
}
security, _ := stream["security"].(string)
obj["tls"] = security
if security == "tls" {
tlsSetting, _ := stream["tlsSettings"].(map[string]interface{})
alpns, _ := tlsSetting["alpn"].([]interface{})
for _, a := range alpns {
alpn = append(alpn, a.(string))
if len(alpns) > 0 {
var alpn []string
for _, a := range alpns {
alpn = append(alpn, a.(string))
}
obj["alpn"] = strings.Join(alpn, ",")
}
tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil {
if sniValue, ok := searchKey(tlsSettings, "serverName"); ok {
sni, _ = sniValue.(string)
obj["sni"], _ = sniValue.(string)
}
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
fp, _ = fpValue.(string)
obj["fp"], _ = fpValue.(string)
}
if insecure, ok := searchKey(tlsSettings, "allowInsecure"); ok {
allowInsecure, _ = insecure.(bool)
obj["allowInsecure"], _ = insecure.(bool)
}
}
serverName, _ := tlsSetting["serverName"].(string)
if serverName != "" {
address = serverName
obj["add"] = serverName
}
}
@@ -193,24 +196,9 @@ func (s *SubService) genVmessLink(inbound *model.Inbound, email string) string {
break
}
}
obj["id"] = clients[clientIndex].ID
obj["aid"] = clients[clientIndex].AlterIds
obj := map[string]interface{}{
"v": "2",
"ps": email,
"add": address,
"port": inbound.Port,
"id": clients[clientIndex].ID,
"aid": clients[clientIndex].AlterIds,
"net": network,
"type": typeStr,
"host": host,
"path": path,
"tls": security,
"sni": sni,
"fp": fp,
"alpn": strings.Join(alpn, ","),
"allowInsecure": allowInsecure,
}
jsonStr, _ := json.MarshalIndent(obj, "", " ")
return "vmess://" + base64.StdEncoding.EncodeToString(jsonStr)
}
@@ -272,6 +260,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
}
security, _ := stream["security"].(string)
@@ -313,21 +304,29 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
if security == "reality" {
params["security"] = "reality"
realitySettings, _ := stream["realitySettings"].(map[string]interface{})
if realitySettings != nil {
if sniValue, ok := searchKey(realitySettings, "serverNames"); ok {
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
realitySettings, _ := searchKey(realitySetting, "settings")
if realitySetting != nil {
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
sNames, _ := sniValue.([]interface{})
params["sni"], _ = sNames[0].(string)
}
if pbkValue, ok := searchKey(realitySettings, "publicKey"); ok {
params["pbk"], _ = pbkValue.(string)
}
if sidValue, ok := searchKey(realitySettings, "shortIds"); ok {
if sidValue, ok := searchKey(realitySetting, "shortIds"); ok {
shortIds, _ := sidValue.([]interface{})
params["sid"], _ = shortIds[0].(string)
}
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
params["fp"] = fp
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
}
}
}
@@ -350,9 +349,6 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
if xtlsSetting != nil {
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
@@ -445,6 +441,9 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
case "grpc":
grpc, _ := stream["grpcSettings"].(map[string]interface{})
params["serviceName"] = grpc["serviceName"].(string)
if grpc["multiMode"].(bool) {
params["mode"] = "multi"
}
}
security, _ := stream["security"].(string)
@@ -482,9 +481,10 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if security == "reality" {
params["security"] = "reality"
realitySettings, _ := stream["realitySettings"].(map[string]interface{})
if realitySettings != nil {
if sniValue, ok := searchKey(realitySettings, "serverNames"); ok {
realitySetting, _ := stream["realitySettings"].(map[string]interface{})
realitySettings, _ := searchKey(realitySetting, "settings")
if realitySetting != nil {
if sniValue, ok := searchKey(realitySetting, "serverNames"); ok {
sNames, _ := sniValue.([]interface{})
params["sni"], _ = sNames[0].(string)
}
@@ -496,7 +496,14 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
params["sid"], _ = shortIds[0].(string)
}
if fpValue, ok := searchKey(realitySettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
if fp, ok := fpValue.(string); ok && len(fp) > 0 {
params["fp"] = fp
}
}
if serverName, ok := searchKey(realitySettings, "serverName"); ok {
if sname, ok := serverName.(string); ok && len(sname) > 0 {
address = sname
}
}
}
@@ -519,9 +526,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
xtlsSettings, _ := searchKey(xtlsSetting, "settings")
if xtlsSetting != nil {
if sniValue, ok := searchKey(xtlsSettings, "serverName"); ok {
params["sni"], _ = sniValue.(string)
}
if fpValue, ok := searchKey(xtlsSettings, "fingerprint"); ok {
params["fp"], _ = fpValue.(string)
}
@@ -586,7 +590,11 @@ func searchHost(headers interface{}) string {
switch v.(type) {
case []interface{}:
hosts, _ := v.([]interface{})
return hosts[0].(string)
if len(hosts) > 0 {
return hosts[0].(string)
} else {
return ""
}
case interface{}:
return v.(string)
}

View File

@@ -404,38 +404,36 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string) {
}
func (t *Tgbot) searchClient(chatId int64, email string) {
traffics, err := t.inboundService.GetClientTrafficByEmail(email)
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil {
logger.Warning(err)
msg := "❌ Something went wrong!"
t.SendMsgToTgbot(chatId, msg)
return
}
if len(traffics) == 0 {
if traffic == nil {
msg := "No result!"
t.SendMsgToTgbot(chatId, msg)
return
}
for _, traffic := range traffics {
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
} 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)
expiryTime := ""
if traffic.ExpiryTime == 0 {
expiryTime = "♾Unlimited"
} else if traffic.ExpiryTime < 0 {
expiryTime = fmt.Sprintf("%d days", traffic.ExpiryTime/-86400000)
} 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) {

View File

@@ -25,12 +25,12 @@ func (s *UserService) GetFirstUser() (*model.User, error) {
return user, nil
}
func (s *UserService) CheckUser(username string, password string) *model.User {
func (s *UserService) CheckUser(username string, password string, secret string) *model.User {
db := database.GetDB()
user := &model.User{}
err := db.Model(model.User{}).
Where("username = ? and password = ?", username, password).
Where("username = ? and password = ? and login_secret = ?", username, password, secret).
First(user).
Error
if err == gorm.ErrRecordNotFound {
@@ -50,6 +50,35 @@ func (s *UserService) UpdateUser(id int, username string, password string) error
Error
}
func (s *UserService) UpdateUserSecret(id int, secret string) error {
db := database.GetDB()
return db.Model(model.User{}).
Where("id = ?", id).
Update("login_secret", secret).
Error
}
func (s *UserService) RemoveUserSecret() error {
db := database.GetDB()
return db.Model(model.User{}).
Where("1 = 1").
Update("login_secret", "").
Error
}
func (s *UserService) GetUserSecret(id int) *model.User {
db := database.GetDB()
user := &model.User{}
err := db.Model(model.User{}).
Where("id = ?", id).
First(user).
Error
if err == gorm.ErrRecordNotFound {
return nil
}
return user
}
func (s *UserService) UpdateFirstUser(username string, password string) error {
if username == "" {
return errors.New("username can not be empty")

View File

@@ -69,7 +69,6 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
}
s.inboundService.DisableInvalidClients()
s.inboundService.RemoveOrphanedTraffics()
inbounds, err := s.inboundService.GetAllInbounds()
if err != nil {
@@ -119,12 +118,15 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if key != "email" && key != "id" && key != "password" && key != "flow" && key != "alterId" {
delete(c, key)
}
if c["flow"] == "xtls-rprx-vision-udp443" {
c["flow"] = "xtls-rprx-vision"
}
}
final_clients = append(final_clients, interface{}(c))
}
settings["clients"] = final_clients
modifiedSettings, err := json.Marshal(settings)
modifiedSettings, err := json.MarshalIndent(settings, "", " ")
if err != nil {
return nil, err
}

View File

@@ -2,9 +2,10 @@ package session
import (
"encoding/gob"
"x-ui/database/model"
"github.com/gin-contrib/sessions"
"github.com/gin-gonic/gin"
"x-ui/database/model"
)
const (
@@ -21,6 +22,15 @@ func SetLoginUser(c *gin.Context, user *model.User) error {
return s.Save()
}
func SetMaxAge(c *gin.Context, maxAge int) error {
s := sessions.Default(c)
s.Options(sessions.Options{
Path: "/",
MaxAge: maxAge,
})
return s.Save()
}
func GetLoginUser(c *gin.Context) *model.User {
s := sessions.Default(c)
obj := s.Get(loginUser)

View File

@@ -26,7 +26,7 @@
"edit" = "Edit"
"delete" = "Delete"
"reset" = "Reset"
"copySuccess" = "Copy successfully"
"copySuccess" = "Copied successfully"
"sure" = "Sure"
"encryption" = "Encryption"
"transmission" = "Transmission"
@@ -40,7 +40,7 @@
"depletingSoon" = "Depleting soon"
"domainName" = "Domain name"
"additional" = "Alter"
"monitor" = "Listen IP"
"monitor" = "Listening IP"
"certificate" = "Certificate"
"fail" = "Fail"
"success" = " Success"
@@ -48,12 +48,13 @@
"install" = "Install"
"clients" = "Clients"
"usage" = "Usage"
"secretToken" = "Secret token"
[menu]
"dashboard" = "System Status"
"inbounds" = "Inbounds"
"setting" = "Panel Setting"
"logout" = "LogOut"
"logout" = "Logout"
"link" = "Other"
[pages.login]
@@ -61,7 +62,7 @@
"loginAgain" = "The login time limit has expired, please log in again"
[pages.login.toasts]
"invalidFormData" = "Input Data Format Is Invalid"
"invalidFormData" = "Input Data Format is Invalid"
"emptyUsername" = "Please Enter Username"
"emptyPassword" = "Please Enter Password"
"wrongUsernameOrPassword" = "Invalid username or password"
@@ -75,17 +76,17 @@
"stopXray" = "Stop"
"restartXray" = "Restart"
"xraySwitch" = "Switch Version"
"xraySwitchClick" = "Click on the version you want to switch"
"xraySwitchClickDesk" = "Please choose carefully, older versions may have incompatible configurations"
"xraySwitchClick" = "Choose the version you want to switch to."
"xraySwitchClickDesk" = "Choose wisely, as older versions may not be compatible with current configurations."
"operationHours" = "Operation Hours"
"operationHoursDesc" = "The running time of the system since it was started"
"operationHoursDesc" = "System uptime: time since startup."
"systemLoad" = "System Load"
"connectionCount" = "Connection Count"
"connectionCountDesc" = "The total number of connections for all network cards"
"connectionCount" = "Number of connections"
"connectionCountDesc" = "Total connections across all network cards"
"upSpeed" = "Total upload speed for all network cards"
"downSpeed" = "Total download speed for all network cards"
"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 data across all network cards since system startup"
"xraySwitchVersionDialog" = "Switch xray version"
"xraySwitchVersionDialogDesc" = "Whether to switch the xray version to"
"dontRefreshh" = "Installation is in progress, please do not refresh this page"
@@ -95,7 +96,7 @@
"totalDownUp" = "Total uploads/downloads"
"totalUsage" = "Total usage"
"inboundCount" = "Number of inbound"
"operate" = "Actions"
"operate" = "Menu"
"enable" = "Enable"
"remark" = "Remark"
"protocol" = "Protocol"
@@ -106,12 +107,13 @@
"expireDate" = "Expire date"
"resetTraffic" = "Reset traffic"
"addInbound" = "Add Inbound"
"generalActions" = "General Actions"
"addTo" = "Create"
"revise" = "Update"
"modifyInbound" = "Modify InBound"
"deleteInbound" = "Delete Inbound"
"deleteInboundContent" = "Are you sure you want to delete inbound?"
"resetTrafficContent" = "Are you sure you want to reset traffic?"
"deleteInboundContent" = "Confirm deletion of inbound?"
"resetTrafficContent" = "Confirm traffic reset?"
"copyLink" = "Copy Link"
"address" = "Address"
"network" = "Network"
@@ -121,8 +123,8 @@
"monitorDesc" = "Leave blank by default"
"meansNoLimit" = "Means no limit"
"totalFlow" = "Total flow"
"leaveBlankToNeverExpire" = "Leave blank to never expire"
"noRecommendKeepDefault" = "There are no special requirements to keep the default"
"leaveBlankToNeverExpire" = "Leave blank to set no expiration"
"noRecommendKeepDefault" = "No special requirements to maintain default settings"
"certificatePath" = "Certificate file path"
"certificateContent" = "Certificate file content"
"publicKeyPath" = "Public key path"
@@ -134,7 +136,7 @@
"export" = "Export links"
"Clone" = "Clone"
"cloneInbound" = "Create"
"cloneInboundContent" = "All items of this inbound except Port, Listening IP, Clients will be applied to the clone"
"cloneInboundContent" = "All settings of this inbound, except for Port, Listening IP, and Clients, will be applied to the clone"
"cloneInboundOk" = "Creating a clone from"
"resetAllTraffic" = "Reset All Inbounds Traffic"
"resetAllTrafficTitle" = "Reset all inbounds traffic"
@@ -142,17 +144,23 @@
"resetAllTrafficOkText" = "Confirm"
"resetAllTrafficCancelText" = "Cancel"
"IPLimit" = "IP Limit"
"IPLimitDesc" = "disable inbound if more than entered count (0 for disable limit ip)"
"resetAllClientTraffics" = "Reset Clients Traffic"
"IPLimitDesc" = "Disable inbound if the count exceeds the entered value (Enter 0 to disable IP limit)"
"resetInboundClientTraffics" = "Reset Clients Traffic"
"resetInboundClientTrafficTitle" = "Reset all clients traffic"
"resetInboundClientTrafficContent" = "Are you sure to reset all traffics of this inbound's clients ?"
"resetAllClientTraffics" = "Reset All Clients Traffic"
"resetAllClientTrafficTitle" = "Reset all clients traffic"
"resetAllClientTrafficContent" = "Are you sure to reset all traffics of this inbound's clients ?"
"resetAllClientTrafficContent" = "Are you sure to reset all traffics of all clients ?"
"delDepletedClients" = "Delete depleted clients"
"delDepletedClientsTitle" = "Delete depleted clients"
"delDepletedClientsContent" = "Are you sure to delete all depleted clients ?"
"Email" = "Email"
"EmailDesc" = "The Email Must Be Completely Unique"
"EmailDesc" = "Please provide a unique email address"
"IPLimitlog" = "IP Log"
"IPLimitlogDesc" = "IPs history Log (before enabling inbound after it has been disabled by IP limit, you should clear the log)"
"IPLimitlogclear" = "Clear The Log"
"setDefaultCert" = "Set cert from panel"
"XTLSdec" = "Xray core needs to be 1.7.5 and below"
"XTLSdec" = "Xray core needs to be 1.7.5"
"Realitydec" = "Xray core needs to be 1.8.0 and above"
[pages.client]
@@ -221,9 +229,13 @@
"advancedTemplate" = "Advanced Template parts"
"completeTemplate" = "Complete Template of Xray configuration"
"generalConfigs" = "General Configs"
"generalConfigsDesc" = "This options will prevent users from connecting to specific protocols and websites."
"countryConfigs" = "Country Configs"
"countryConfigsDesc" = "This options will prevent users from connecting to specific country domains."
"ipv4Configs" = "IPv4 Configs"
"ipv4ConfigsDesc" = "This options will be route to target domains only via IPv4."
"warpConfigs" = "WARP Configs"
"warpConfigsDesc" = "Caution: Before using this options, Install WARP in socks5 proxy mode on your server by following the steps on the panel's GitHub. WARP will route traffic to websites through Cloudflare servers."
"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"
@@ -232,7 +244,7 @@
"xrayConfigPrivateIpDesc" = "Change the configuration template to avoid connecting with private IP ranges, restart the panel to take effect"
"xrayConfigAds" = "Block Ads"
"xrayConfigAdsDesc" = "Change the configuration template to block Ads, restart the panel to take effect"
"xrayConfigPorn" = "Ban Porn websites to connect"
"xrayConfigPorn" = "Block Porn Websites"
"xrayConfigPornDesc" = "Change the configuration template to avoid connecting to Porn websites, restart the panel to take effect"
"xrayConfigIRIp" = "Ban Iran IP ranges to connect"
"xrayConfigIRIpDesc" = "Change the configuration template to avoid connecting with Iran IP ranges, restart the panel to take effect"
@@ -276,6 +288,8 @@
"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"
"sessionMaxAge" = "Session maximum age"
"sessionMaxAgeDesc" = "The time that you can stay login (unit: minute)"
"expireTimeDiff" = "Exhaustion time threshold"
"expireTimeDiffDesc" = "Detect exhaustion before expiration (unit:day)"
"trafficDiff" = "Exhaustion traffic threshold"
@@ -284,6 +298,10 @@
"tgNotifyCpuDesc" = "This telegram bot will send you a notification if CPU usage is more than this percentage (unit:%)"
"timeZonee" = "Time Zone"
"timeZoneDesc" = "The scheduled task runs according to the time in the time zone, and restarts the panel to take effect"
"loginSecurity" = "Login security"
"loginSecurityDesc" = "Toggle additional step in user login page"
"secretToken" = "Secret Token"
"secretTokenDesc" = "Copy this secret token and keep it in a safe place; without this you won't be able to login. This can not be recovered from x-ui command tool neither"
[pages.setting.toasts]
"modifySetting" = "Modify setting"

View File

@@ -48,6 +48,7 @@
"install" = "نصب"
"clients" = "کاربران"
"usage" = "استفاده"
"secretToken" = "توکن امنیتی"
[menu]
"dashboard" = "وضعیت سیستم"
@@ -95,7 +96,7 @@
"totalDownUp" = "جمع آپلود/دانلود"
"totalUsage" = "جمع کل"
"inboundCount" = "تعداد سرویس ها"
"operate" = "عملیات"
"operate" = "فهرست"
"enable" = "فعال"
"remark" = "نام"
"protocol" = "پروتکل"
@@ -106,6 +107,7 @@
"expireDate" = "تاریخ انقضا"
"resetTraffic" = "ریست ترافیک"
"addInbound" = "اضافه کردن سرویس"
"generalActions" = "عملیات کلی"
"addTo" = "اضافه کردن"
"revise" = "ویرایش"
"modifyInbound" = "ویرایش سرویس"
@@ -139,9 +141,15 @@
"resetAllTraffic" = "ریست ترافیک کل سرویس ها"
"resetAllTrafficTitle" = "ریست ترافیک کل سرویس ها"
"resetAllTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک سرویس ها را ریست کنید؟"
"resetInboundClientTraffics" = "ریست ترافیک کاربران"
"resetInboundClientTrafficTitle" = "ریست ترافیک کل کاربران"
"resetInboundClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
"resetAllClientTraffics" = "ریست ترافیک کاربران"
"resetAllClientTrafficTitle" = "ریست ترافیک کل کاربران"
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران این سرویس را ریست کنید؟"
"resetAllClientTrafficContent" = "آیا مطمئن هستید که میخواهید تمام ترافیک کاربران را ریست کنید؟"
"delDepletedClients" = "حذف کاربران منقضی"
"delDepletedClientsTitle" = "حذف کاربران منقضی"
"delDepletedClientsContent" = "آیا مطمئن هستید مه میخواهید تمامی کاربران منقضی شده را حذف کنید؟"
"IPLimit" = "محدودیت ای پی"
"IPLimitDesc" = "غیرفعال کردن ورودی در صورت بیش از تعداد وارد شده (0 برای غیرفعال کردن محدودیت ای پی )"
"Email" = "ایمیل"
@@ -150,7 +158,7 @@
"IPLimitlogDesc" = "گزارش سابقه ای پی (قبل از فعال کردن ورودی پس از غیرفعال شدن توسط محدودیت ای پی، باید گزارش را پاک کنید)"
"IPLimitlogclear" = "پاک کردن گزارش ها"
"setDefaultCert" = "استفاده از گواهی پنل"
"XTLSdec" = "هسته Xray باید 1.7.5 و کمتر باشد"
"XTLSdec" = "هسته Xray باید 1.7.5 باشد"
"Realitydec" = "هسته Xray باید 1.8.0 و بالاتر باشد"
[pages.client]
@@ -219,9 +227,13 @@
"advancedTemplate" = "بخش های پیشرفته الگو"
"completeTemplate" = "الگوی کامل تنظیمات ایکس ری"
"generalConfigs" = "تنظیمات عمومی"
"generalConfigsDesc" = "این گزینه ها از اتصال کاربران به پروتکل ها و وب سایت های خاص جلوگیری می کند."
"countryConfigs" = "تنظیمات برای کشورها"
"countryConfigsDesc" = "این گزینه از اتصال کاربران به دامنه های کشوری خاص جلوگیری می کند."
"ipv4Configs" = "تنظیمات برای IPv4"
"ipv4ConfigsDesc" = "این گزینه فقط از طریق آیپی ورژن 4 به دامنه های هدف هدایت می شود."
"warpConfigs" = "تنظیمات برای WARP"
"warpConfigsDesc" = "هشدار: قبل از استفاده از این گزینه، WARP را در حالت پراکسی socks5 با دنبال کردن مراحل در GitHub پنل روی سرور خود نصب کنید. WARP ترافیک را از طریق سرورهای Cloudflare به وب سایت ها هدایت می کند."
"xrayConfigTemplate" = "تنظیمات الگو ایکس ری"
"xrayConfigTemplateDesc" = "فایل پیکربندی ایکس ری نهایی بر اساس این الگو ایجاد میشود. لطفاً این را تغییر ندهید مگر اینکه دقیقاً بدانید که چه کاری انجام می دهید! پنل را مجدداً راه اندازی کنید تا اعمال شود"
"xrayConfigTorrent" = "فیلتر کردن بیت تورنت"
@@ -274,6 +286,8 @@
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید . پنل را مجدداً راه اندازی کنید تا اعمال شود"
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
"tgNotifyBackupDesc" = "ارسال کپی فایل پایگاه داده به همراه گزارش دوره ای"
"sessionMaxAge" = "بیشینه زمان جلسه وب"
"sessionMaxAgeDesc" = "بیشینه زمانی که میتوانید لاگین بمانید (واحد: دقیقه)"
"expireTimeDiff" = "آستانه زمان باقی مانده"
"expireTimeDiffDesc" = "فاصله زمانی هشدار تا رسیدن به زمان انقضا (واحد: روز)"
"trafficDiff" = "آستانه ترافیک باقی مانده"
@@ -282,6 +296,10 @@
"tgNotifyCpuDesc" = "این ربات تلگرام در صورت استفاده پردازنده بیشتر از این درصد برای شما پیام ارسال می کند.(واحد: درصد)"
"timeZonee" = "منظقه زمانی"
"timeZoneDesc" = "وظایف برنامه ریزی شده بر اساس این منطقه زمانی اجرا می شوند. پنل را مجدداً راه اندازی می کند تا اعمال شود"
"loginSecurity" = "لاگین ایمن"
"loginSecurityDesc" = "افزودن یک مرحله دیگر به فرآیند لاگین"
"secretToken" = "توکن امنیتی"
"secretTokenDesc" = "این کد امنیتی را نزد خود در این جای امن نگه داری، بدون این کد امکان ورود به پنل را نخواهید داشت. امکان بازیابی آن وجود ندارد!"
[pages.setting.toasts]
"modifySetting" = "ویرایش تنظیمات"

View File

@@ -48,6 +48,7 @@
"install" = "安装"
"clients" = "客户端"
"usage" = "用法"
"secretToken" = "秘密令牌"
[menu]
"dashboard" = "系统状态"
@@ -95,7 +96,7 @@
"totalDownUp" = "总上传 / 下载"
"totalUsage" = "总用量"
"inboundCount" = "入站数量"
"operate" = "操作"
"operate" = "菜单"
"enable" = "启用"
"remark" = "备注"
"protocol" = "协议"
@@ -106,6 +107,7 @@
"expireDate" = "到期时间"
"resetTraffic" = "重置流量"
"addInbound" = "添加入"
"generalActions" = "通用操作"
"addTo" = "添加"
"revise" = "修改"
"modifyInbound" = "修改入站"
@@ -139,9 +141,15 @@
"resetAllTraffic" = "重置所有入站流量"
"resetAllTrafficTitle" = "重置所有入站流量"
"resetAllTrafficContent" = "您确定要重置所有入站流量吗?"
"resetAllClientTraffics" = "重置客户端流量"
"resetInboundClientTraffics" = "重置客户端流量"
"resetInboundClientTrafficTitle" = "重置所有客户端流量"
"resetInboundClientTrafficContent" = "您确定要重置此入站客户端的所有流量吗?"
"resetAllClientTraffics" = "重置所有客户端流量"
"resetAllClientTrafficTitle" = "重置所有客户端流量"
"resetAllClientTrafficContent" = "确定要重置此入站客户端的所有流量吗?"
"resetAllClientTrafficContent" = "确定要重置所有客户端的所有流量吗?"
"delDepletedClients" = "删除耗尽的客户端"
"delDepletedClientsTitle" = "删除耗尽的客户"
"delDepletedClientsContent" = "你确定要删除所有耗尽的客户端吗?"
"IPLimit" = "IP限制"
"IPLimitDesc" = "如果超过输入的计数则禁用入站0 表示禁用限制 ip"
"Email" = "电子邮件"
@@ -150,7 +158,7 @@
"IPLimitlogDesc" = "IP 历史日志 通过IP限制禁用inbound之前需要清空日志"
"IPLimitlogclear" = "清除日志"
"setDefaultCert" = "从面板设置证书"
"XTLSdec" = "Xray核心需要1.7.5及以下版本"
"XTLSdec" = "Xray核心需要1.7.5"
"Realitydec" = "Xray核心需要1.8.0及以上版本"
[pages.client]
@@ -219,9 +227,13 @@
"advancedTemplate" = "高级模板部件"
"completeTemplate" = "Xray 配置的完整模板"
"generalConfigs" = "一般配置"
"generalConfigsDesc" = "此选项将阻止用户连接到特定协议和网站。"
"countryConfigs" = "国家配置"
"countryConfigsDesc" = "此选项将阻止用户连接到特定国家/地区的域。"
"ipv4Configs" = "IPv4 配置"
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域。"
"warpConfigs" = "WARP 配置"
"warpConfigsDesc" = "警告:在使用此选项之前,请按照面板 GitHub 上的步骤在您的服务器上以 socks5 代理模式安装 WARP。 WARP 将通过 Cloudflare 服务器将流量路由到网站。"
"xrayConfigTemplate" = "xray 配置模板"
"xrayConfigTemplateDesc" = "以该模型为基础生成最终的xray配置文件重新启动面板生成效率"
"xrayConfigTorrent" = "禁止使用 bittorrent"
@@ -274,6 +286,8 @@
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份"
"tgNotifyBackupDesc" = "正在发送数据库备份文件和报告通知。重启面板生效"
"sessionMaxAge" = "会话最大年龄"
"sessionMaxAgeDesc" = "您可以保持登录状态的时间(单位:分钟)"
"expireTimeDiff" = "耗尽时间阈值"
"expireTimeDiffDesc" = "到期前检测耗尽(单位:天)"
"trafficDiff" = "耗尽流量阈值"
@@ -282,6 +296,10 @@
"tgNotifyCpuDesc" = "如果 CPU 使用率超过此百分比(单位:%),此 talegram bot 将向您发送通知"
"timeZonee" = "时区"
"timeZoneDesc" = "定时任务按照该时区的时间运行,重启面板生效"
"loginSecurity" = "登录安全"
"loginSecurityDesc" = "在用户登录页面中切换附加步骤"
"secretToken" = "秘密令牌"
"secretTokenDesc" = "复制此秘密令牌并将其保存在安全的地方;没有这个你将无法登录。这也无法从 x-ui 命令工具中恢复"
[pages.setting.toasts]
"modifySetting" = "修改设置"

View File

@@ -33,9 +33,6 @@ import (
//go:embed assets/*
var assetsFS embed.FS
//go:embed assets/favicon.ico
var favicon []byte
//go:embed html/*
var htmlFS embed.FS
@@ -161,11 +158,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default()
// Add favicon
engine.GET("/favicon.ico", func(c *gin.Context) {
c.Data(200, "image/x-icon", favicon)
})
secret, err := s.settingService.GetSecret()
if err != nil {
return nil, err

154
x-ui.sh
View File

@@ -58,14 +58,14 @@ fi
confirm() {
if [[ $# > 1 ]]; then
echo && read -p "$1 [Default$2]: " temp
if [[ x"${temp}" == x"" ]]; then
echo && read -p "$1 [Default $2]: " temp
if [[ "${temp}" == "" ]]; then
temp=$2
fi
else
read -p "$1 [y/n]: " temp
fi
if [[ x"${temp}" == x"y" || x"${temp}" == x"Y" ]]; then
if [[ "${temp}" == "y" || "${temp}" == "Y" ]]; then
return 0
else
return 1
@@ -139,15 +139,23 @@ uninstall() {
}
reset_user() {
confirm "Reset your username and password to admin?" "n"
confirm "Are you sure to reset the username and password of the panel?" "n"
if [[ $? != 0 ]]; then
if [[ $# == 0 ]]; then
show_menu
fi
return 0
fi
/usr/local/x-ui/x-ui setting -username admin -password admin
echo -e "Username and password have been reset to ${green}admin${plain}, Please restart the panel now."
read -rp "Please set the login username [default is a random username]: " config_account
[[ -z $config_account ]] && config_account=$(date +%s%N | md5sum | cut -c 1-8)
read -rp "Please set the login password [default is a random password]: " config_password
[[ -z $config_password ]] && config_password=$(date +%s%N | md5sum | cut -c 1-8)
/usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password} >/dev/null 2>&1
/usr/local/x-ui/x-ui setting -remove_secret >/dev/null 2>&1
echo -e "Panel login username has been reset to: ${green} ${config_account} ${plain}"
echo -e "Panel login password has been reset to: ${green} ${config_password} ${plain}"
echo -e "${yellow} Panel login secret token disabled ${plain}"
echo -e "${green} Please use the new login username and password to access the X-UI panel. Also remember them! ${plain}"
confirm_restart
}
@@ -334,7 +342,7 @@ check_status() {
return 2
fi
temp=$(systemctl status x-ui | grep Active | awk '{print $3}' | cut -d "(" -f2 | cut -d ")" -f1)
if [[ x"${temp}" == x"running" ]]; then
if [[ "${temp}" == "running" ]]; then
return 0
else
return 1
@@ -343,7 +351,7 @@ check_status() {
check_enabled() {
temp=$(systemctl is-enabled x-ui)
if [[ x"${temp}" == x"enabled" ]]; then
if [[ "${temp}" == "enabled" ]]; then
return 0
else
return 1
@@ -423,32 +431,6 @@ show_xray_status() {
fi
}
#this will be an entrance for ssl cert issue
#here we can provide two different methods to issue cert
#first.standalone mode second.DNS API mode
ssl_cert_issue() {
local method=""
echo -E ""
LOGD "********Usage********"
LOGI "this shell script will use acme to help issue certs."
LOGI "here we provide two methods for issuing certs:"
LOGI "method 1:acme standalone mode,need to keep port:80 open"
LOGI "method 2:acme DNS API mode,need provide Cloudflare Global API Key"
LOGI "recommend method 2 first,if it fails,you can try method 1."
LOGI "certs will be installed in /root/cert directory"
read -p "please choose which method do you want,type 1 or 2": method
LOGI "you choosed method:${method}"
if [ "${method}" == "1" ]; then
ssl_cert_issue_standalone
elif [ "${method}" == "2" ]; then
ssl_cert_issue_by_cloudflare
else
LOGE "invalid input,please check it..."
exit 1
fi
}
open_ports() {
if ! command -v ufw &> /dev/null
then
@@ -536,7 +518,7 @@ install_acme() {
}
#method for standalone mode
ssl_cert_issue_standalone() {
ssl_cert_issue() {
#check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
echo "acme.sh could not be found. we will install it"
@@ -547,7 +529,7 @@ ssl_cert_issue_standalone() {
fi
fi
#install socat second
if [[ x"${release}" == x"centos" ]]; then
if [[ "${release}" == "centos" ]] || [[ "${release}" == "fedora" ]] ; then
yum install socat -y
else
apt install socat -y
@@ -561,7 +543,7 @@ ssl_cert_issue_standalone() {
#get the domain here,and we need verify it
local domain=""
read -p "please input your domain:" domain
read -p "Please enter your domain name:" domain
LOGD "your domain is:${domain},check it..."
#here we need to judge whether there exists cert already
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
@@ -628,97 +610,9 @@ ssl_cert_issue_standalone() {
}
#method for DNS API mode
ssl_cert_issue_by_cloudflare() {
echo -E ""
LOGD "******Preconditions******"
LOGI "1.need Cloudflare account associated email"
LOGI "2.need Cloudflare Global API Key"
LOGI "3.your domain use Cloudflare as resolver"
confirm "I have confirmed all these info above[y/n]" "y"
if [ $? -eq 0 ]; then
install_acme
if [ $? -ne 0 ]; then
LOGE "install acme failed,please check logs"
exit 1
fi
CF_Domain=""
CF_GlobalKey=""
CF_AccountEmail=""
LOGD "please input your domain:"
read -p "Input your domain here:" CF_Domain
LOGD "your domain is:${CF_Domain},check it..."
#here we need to judge whether there exists cert already
local currentCert=$(~/.acme.sh/acme.sh --list | tail -1 | awk '{print $1}')
if [ ${currentCert} == ${CF_Domain} ]; then
local certInfo=$(~/.acme.sh/acme.sh --list)
LOGE "system already have certs here,can not issue again,current certs details:"
LOGI "$certInfo"
exit 1
else
LOGI "your domain is ready for issuing cert now..."
fi
#create a directory for install cert
certPath="/root/cert/${CF_Domain}"
if [ ! -d "$certPath" ]; then
mkdir -p "$certPath"
else
rm -rf "$certPath"
mkdir -p "$certPath"
fi
LOGD "please inout your cloudflare global API key:"
read -p "Input your key here:" CF_GlobalKey
LOGD "your cloudflare global API key is:${CF_GlobalKey}"
LOGD "please input your cloudflare account email:"
read -p "Input your email here:" CF_AccountEmail
LOGD "your cloudflare account email:${CF_AccountEmail}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
if [ $? -ne 0 ]; then
LOGE "change the default CA to Lets'Encrypt failed,exit"
exit 1
fi
export CF_Key="${CF_GlobalKey}"
export CF_Email=${CF_AccountEmail}
~/.acme.sh/acme.sh --issue --dns dns_cf -d ${CF_Domain} -d *.${CF_Domain} --log
if [ $? -ne 0 ]; then
LOGE "issue cert failed,exit"
rm -rf ~/.acme.sh/${CF_Domain}
exit 1
else
LOGI "Certificate issued Successfully, Installing..."
fi
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} \
--key-file /root/cert/${CF_Domain}/privkey.pem \
--fullchain-file /root/cert/${CF_Domain}/fullchain.pem
if [ $? -ne 0 ]; then
LOGE "install cert failed,exit"
rm -rf ~/.acme.sh/${CF_Domain}
exit 1
else
LOGI "Certificate installed Successfully,Turning on automatic updates..."
fi
~/.acme.sh/acme.sh --upgrade --auto-upgrade
if [ $? -ne 0 ]; then
LOGE "auto renew failed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
exit 1
else
LOGI "auto renew succeed, certs details:"
ls -lah cert/*
chmod 755 $certPath/*
fi
else
show_menu
fi
}
google_recaptcha() {
curl -O https://raw.githubusercontent.com/jinwyp/one_click_script/master/install_kernel.sh && chmod +x ./install_kernel.sh && ./install_kernel.sh
warp_fixchatgpt() {
curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash
echo ""
before_show_menu
}
@@ -781,7 +675,7 @@ show_menu() {
${green}2.${plain} Update x-ui
${green}3.${plain} Uninstall x-ui
————————————————
${green}4.${plain} Reset Username And Password
${green}4.${plain} Reset Username & Password & Secret Token
${green}5.${plain} Reset Panel Settings
${green}6.${plain} Change Panel Port
${green}7.${plain} View Current Panel Settings
@@ -799,7 +693,7 @@ show_menu() {
${green}16.${plain} Apply for an SSL Certificate
${green}17.${plain} Update Geo Files
${green}18.${plain} Active Firewall and open ports
${green}19.${plain} Fixing Google reCAPTCHA
${green}19.${plain} Install WARP
${green}20.${plain} Speedtest by Ookla
"
show_status
@@ -864,7 +758,7 @@ show_menu() {
open_ports
;;
19)
google_recaptcha
warp_fixchatgpt
;;
20)
run_speedtest

View File

@@ -45,6 +45,10 @@ func GetGeoipPath() string {
return config.GetBinFolderPath() + "/geoip.dat"
}
func GetIranPath() string {
return config.GetBinFolderPath() + "/iran.dat"
}
func GetBlockedIPsPath() string {
return config.GetBinFolderPath() + "/blockedIPs"
}