Compare commits

...

18 Commits

Author SHA1 Message Date
MHSanaei
97489e743a v2.2.6 2024-03-13 01:45:25 +03:30
MHSanaei
c74efa1d43 fix bug - deleteBalancer 2024-03-13 01:31:37 +03:30
Alireza Ahmadi
18af7047f8 ip limit only if log is available 2024-03-12 21:53:32 +01:00
MHSanaei
e7ae846823 [ui] remove online animation
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2024-03-13 00:15:17 +03:30
MHSanaei
b042f01e58 limit mux to connection base outbounds
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2024-03-12 23:04:11 +03:30
MHSanaei
e43601ac08 support IPv6 address in Link
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2024-03-12 23:02:02 +03:30
Tara Rostami
bc29d7a252 Minor Fixes (#2041) 2024-03-12 20:57:50 +03:30
MHSanaei
dd21bb2db7 bug fix - remove secret 2024-03-12 20:45:44 +03:30
MHSanaei
4d07b99fe7 Xray Outbound - Authority gRPC 2024-03-12 20:45:33 +03:30
MHSanaei
8b5fe0b018 [subJson] add mux and direct
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2024-03-12 19:44:51 +03:30
MHSanaei
fc23af5db6 new - DNS obf type for kcp 2024-03-12 19:28:57 +03:30
MHSanaei
10f54cd937 [xray] add outbound sendThrough + warp typo fix
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2024-03-12 19:23:48 +03:30
Steven
b21758e6a6 Improved readme.zh (#2037) 2024-03-12 18:38:56 +03:30
MHSanaei
903db95783 minor changes
Co-Authored-By: somebodywashere <68244480+somebodywashere@users.noreply.github.com>
2024-03-12 17:51:45 +03:30
MHSanaei
6fdc07a2d0 fix sockopt in all protocols
Co-Authored-By: Alireza Ahmadi <alireza7@gmail.com>
2024-03-12 17:28:09 +03:30
MHSanaei
c9f245cb25 fix updateSecret 2024-03-12 16:20:48 +03:30
Steven
5dc95f8556 Fix unloaded photos in readme.zh (#2033)
* Update README.zh.md

* Update README.zh.md
2024-03-12 15:49:45 +03:30
Steven
d9d8545cca add Chinese README.zh (#2032)
* add readme-zh

* Update README.md

* Update README.zh.md
2024-03-12 15:12:24 +03:30
29 changed files with 838 additions and 125 deletions

View File

@@ -1,5 +1,7 @@
# 3X-UI
[English](/README.md) | [Chinese](/README.zh.md)
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
**An Advanced Web Panel • Built on Xray Core**
@@ -26,10 +28,10 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
## Install Custom Version
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.2.5`:
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.2.6`:
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.5
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.6
```
## SSL Certificate

473
README.zh.md Normal file
View File

@@ -0,0 +1,473 @@
# 3X-UI
[English](/README.md) | [Chinese](/README.zh.md)
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
**一个更好的面板 • 基于Xray Core构建**
[![](https://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](https://github.com/MHSanaei/3x-ui/releases)
[![](https://img.shields.io/github/actions/workflow/status/mhsanaei/3x-ui/release.yml.svg)](#)
[![GO Version](https://img.shields.io/github/go-mod/go-version/mhsanaei/3x-ui.svg)](#)
[![Downloads](https://img.shields.io/github/downloads/mhsanaei/3x-ui/total.svg)](#)
[![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html)
> **Disclaimer:** 此项目仅供个人学习交流,请不要用于非法目的,请不要在生产环境中使用。
**如果此项目对你有用,请给一个**:star2:
<p align="left"><a href="#"><img width="125" src="https://github.com/MHSanaei/3x-ui/assets/115543613/7aa895dd-048a-42e7-989b-afd41a74e2e1" alt="Image"></a></p>
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
## 安装 & 升级
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)
```
## 安装指定版本
要安装所需的版本,请将该版本添加到安装命令的末尾。 e.g., ver `v2.2.6`:
```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.6
```
## SSL 认证
<details>
<summary>点击查看 SSL 认证</summary>
### Cloudflare
管理脚本具有用于 Cloudflare 的内置 SSL 证书应用程序。若要使用此脚本申请证书,需要满足以下条件:
- Cloudflare 邮箱地址
- Cloudflare Global API Key
- 域名已通过 cloudflare 解析到当前服务器
**1:** 在终端中运行`x-ui` 选择 `Cloudflare SSL Certificate`.
### Certbot
```
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
***Tip:*** *管理脚本具有Certbot。使用 `x-ui` 命令, 选择 `SSL Certificate Management`.*
</details>
## 手动安装 & 升级
<details>
<summary>点击查看 手动安装 & 升级</summary>
#### 使用
1. 若要将最新版本的压缩包直接下载到服务器,请运行以下命令:
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
*) XUI_ARCH="amd64" ;;
esac
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
```
2. 下载压缩包后,执行以下命令安装或升级 x-ui
```sh
ARCH=$(uname -m)
case "${ARCH}" in
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
*) XUI_ARCH="amd64" ;;
esac
cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
cp x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/
mv x-ui/ /usr/local/
systemctl daemon-reload
systemctl enable x-ui
systemctl restart x-ui
```
</details>
## 通过Docker安装
<details>
<summary>点击查看 通过Docker安装</summary>
#### 使用
1. 安装Docker
```sh
bash <(curl -sSL https://get.docker.com)
```
2. 克隆仓库:
```sh
git clone https://github.com/MHSanaei/3x-ui.git
cd 3x-ui
```
3. 运行服务:
```sh
docker compose up -d
```
```sh
docker run -itd \
-e XRAY_VMESS_AEAD_FORCED=false \
-v $PWD/db/:/etc/x-ui/ \
-v $PWD/cert/:/root/cert/ \
--network=host \
--restart=unless-stopped \
--name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest
```
更新至最新版本
```sh
cd 3x-ui
docker compose down
docker compose pull 3x-ui
docker compose up -d
```
从Docker中删除3x-ui
```sh
docker stop 3x-ui
docker rm 3x-ui
cd --
rm -r 3x-ui
```
</details>
## 建议使用的操作系统
- Ubuntu 20.04+
- Debian 11+
- CentOS 8+
- Fedora 36+
- Arch Linux
- Manjaro
- Armbian
- AlmaLinux 9+
- Rockylinux 9+
## 支持的架构和设备
<details>
<summary>点击查看 支持的架构和设备</summary>
我们的平台提供与各种架构和设备的兼容性,确保在各种计算环境中的灵活性。以下是我们支持的关键架构:
- **amd64**: 这种流行的架构是个人计算机和服务器的标准,可以无缝地适应大多数现代操作系统。
- **x86 / i386**: 这种架构在台式机和笔记本电脑中被广泛采用,得到了众多操作系统和应用程序的广泛支持,包括但不限于 Windows、macOS 和 Linux 系统。
- **armv8 / arm64 / aarch64**: 这种架构专为智能手机和平板电脑等当代移动和嵌入式设备量身定制,以 Raspberry Pi 4、Raspberry Pi 3、Raspberry Pi Zero 2/Zero 2 W、Orange Pi 3 LTS 等设备为例。
- **armv7 / arm / arm32**: 作为较旧的移动和嵌入式设备的架构它仍然广泛用于Orange Pi Zero LTS、Orange Pi PC Plus、Raspberry Pi 2等设备。
- **armv6 / arm / arm32**: 这种架构面向非常老旧的嵌入式设备虽然不太普遍但仍在使用中。Raspberry Pi 1、Raspberry Pi Zero/Zero W 等设备都依赖于这种架构。
- **armv5 / arm / arm32**: 它是一种主要与早期嵌入式系统相关的旧架构,目前不太常见,但仍可能出现在早期 Raspberry Pi 版本和一些旧智能手机等传统设备中。
</details>
## Languages
- English英语
- Farsi伊朗语
- Chinese中文
- Russian俄语
- Vietnamese越南语
- Spanish西班牙语
- Indonesian (印度尼西亚语)
- Ukrainian乌克兰语
## Features
- 系统状态监控
- 在所有入站和客户端中搜索
- 深色/浅色主题
- 支持多用户和多协议
- 支持多种协议,包括 VMess、VLESS、Trojan、Shadowsocks、Dokodemo-door、Socks、HTTP、wireguard
- 支持 XTLS 原生协议,包括 RPRX-Direct、Vision、REALITY
- 流量统计、流量限制、过期时间限制
- 可自定义的 Xray配置模板
- 支持HTTPS访问面板自建域名+SSL证书
- 支持一键式SSL证书申请和自动续费
- 更多高级配置项目请参考面板
- 修复了 API 路由(用户设置将使用 API 创建)
- 支持通过面板中提供的不同项目更改配置。
- 支持从面板导出/导入数据库
## 默认设置
<details>
<summary>点击查看 默认设置</summary>
### 信息
- **端口:** 2053
- **用户名 & 密码:** It will be generated randomly if you skip modifying.
- **数据库路径:**
- /etc/x-ui/x-ui.db
- **Xray 配置路径:**
- /usr/local/x-ui/bin/config.json
- **面板链接无SSL**
- http://ip:2053/panel
- http://domain:2053/panel
- **面板链接有SSL**
- https://domain:2053/panel
</details>
## [WARP 配置](https://gitlab.com/fscarmen/warp)
<details>
<summary>点击查看 WARP 配置</summary>
#### 使用
如果要在 v2.1.0 之前使用 WARP 路由,请按照以下步骤操作:
**1.** 在 **SOCKS Proxy Mode** 模式中安装Wrap
```sh
bash <(curl -sSL https://raw.githubusercontent.com/hamid-gh98/x-ui-scripts/main/install_warp_proxy.sh)
```
**2.** 如果您已经安装了 warp您可以使用以下命令卸载
```sh
warp u
```
**3.** 在面板中打开您需要的配置
配置:
- Block Ads
- Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP
- Fix Google 403 error
</details>
## IP 限制
<details>
<summary>点击查看 IP 限制</summary>
#### 使用
**注意:** 使用 IP 隧道时IP 限制无法正常工作。
- 适用于最高 `v1.6.1`
- IP 限制 已被集成在面板中。
- 适用于 `v1.7.0` 以及更新的版本:
- 要使 IP 限制正常工作,您需要按照以下步骤安装 fail2ban 及其所需的文件:
1. 使用面板内置的 `x-ui` 指令
2. 选择 `IP Limit Management`.
3. 根据您的需要选择合适的选项。
- 确保您的 Xray 配置上有 ./access.log 。在 v2.1.3 之后,我们有一个选项。
```sh
"log": {
"access": "./access.log",
"dnsLog": false,
"loglevel": "warning"
},
```
</details>
## Telegram 机器人
<details>
<summary>点击查看 Telegram 机器人</summary>
#### 使用
Web 面板通过 Telegram Bot 支持每日流量、面板登录、数据库备份、系统状态、客户端信息等通知和功能。要使用机器人,您需要在面板中设置机器人相关参数,包括:
- 电报令牌
- 管理员聊天 ID
- 通知时间cron 语法)
- 到期日期通知
- 流量上限通知
- 数据库备份
- CPU 负载通知
**参考:**
- `30 \* \* \* \* \*` - 在每个点的 30 秒处通知
- `0 \*/10 \* \* \* \*` - 每 10 分钟的第一秒通知
- `@hourly` - 每小时通知
- `@daily` - 每天通知 (00:00)
- `@weekly` - 每周通知
- `@every 8h` - 每8小时通知
### Telegram Bot 功能
- 定期报告
- 登录通知
- CPU 阈值通知
- 提前报告的过期时间和流量阈值
- 如果将客户的电报用户名添加到用户的配置中,则支持客户端报告菜单
- 支持使用UUIDVMESS/VLESS或密码TROJAN搜索报文流量报告 - 匿名
- 基于菜单的机器人
- 通过电子邮件搜索客户端(仅限管理员)
- 检查所有入库
- 检查服务器状态
- 检查耗尽的用户
- 根据请求和定期报告接收备份
- 多语言机器人
### 注册 Telegram bot
- 与 [Botfather](https://t.me/BotFather) 对话:
![Botfather](./media/botfather.png)
- 使用 /newbot 创建新机器人你需要提供机器人名称以及用户名注意名称中末尾要包含“bot”
![创建机器人](./media/newbot.png)
- 启动您刚刚创建的机器人。可以在此处找到机器人的链接。
![令牌](./media/token.png)
- 输入您的面板并配置 Telegram 机器人设置,如下所示:
![面板设置](./media/panel-bot-config.png)
在输入字段编号 3 中输入机器人令牌。
在输入字段编号 4 中输入用户 ID。具有此 id 的 Telegram 帐户将是机器人管理员。 (您可以输入多个,只需将它们用“ ,”分开即可)
- 如何获取TG ID? 使用 [bot](https://t.me/useridinfobot) 启动机器人,它会给你 Telegram 用户 ID。
![用户 ID](./media/user-id.png)
</details>
## API 路由
<details>
<summary>点击查看 API 路由</summary>
#### 使用
- `/login` with `POST` user data: `{username: '', password: ''}` for login
- `/panel/api/inbounds` 以下操作的基础:
| 方法 | 路径 | 操作 |
| :----: | ---------------------------------- | ------------------------------------------- |
| `GET` | `"/list"` | 获取所有入站 |
| `GET` | `"/get/:id"` | 获取所有入站以及inbound.id |
| `GET` | `"/getClientTraffics/:email"` | 通过电子邮件获取客户端流量 |
| `GET` | `"/createbackup"` | Telegram 机器人向管理员发送备份 |
| `POST` | `"/add"` | 添加入站 |
| `POST` | `"/del/:id"` | 删除入站 |
| `POST` | `"/update/:id"` | 更新入站 |
| `POST` | `"/clientIps/:email"` | 客户端 IP 地址 |
| `POST` | `"/clearClientIps/:email"` | 清除客户端 IP 地址 |
| `POST` | `"/addClient"` | 将客户端添加到入站 |
| `POST` | `"/:id/delClient/:clientId"` | 通过 clientId\* 删除客户端 |
| `POST` | `"/updateClient/:clientId"` | 通过 clientId\* 更新客户端 |
| `POST` | `"/:id/resetClientTraffic/:email"` | 重置客户端的流量 |
| `POST` | `"/resetAllTraffics"` | 重置所有入站的流量 |
| `POST` | `"/resetAllClientTraffics/:id"` | 重置入站中所有客户端的流量 |
| `POST` | `"/delDepletedClients/:id"` | 删除入站耗尽的客户端 -1 all |
| `POST` | `"/onlines"` | 获取在线用户 电子邮件列表 |
\*- `clientId` 项应该使用下列数据
- `client.id` VMESS and VLESS
- `client.password` TROJAN
- `client.email` Shadowsocks
- [API 文档](https://documenter.getpostman.com/view/16802678/2s9YkgD5jm)
- [<img src="https://run.pstmn.io/button.svg" alt="Run In Postman" style="width: 128px; height: 32px;">](https://app.getpostman.com/run-collection/16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415?action=collection%2Ffork&source=rip_markdown&collection-url=entityId%3D16802678-1a4c9270-ac77-40ed-959a-7aa56dc4a415%26entityType%3Dcollection%26workspaceId%3D2cd38c01-c851-4a15-a972-f181c23359d9)
</details>
## 环境变量
<details>
<summary>点击查看 环境变量</summary>
#### Usage
| 变量 | Type | 默认 |
| -------------- | :--------------------------------------------: | :------------ |
| XUI_LOG_LEVEL | `"debug"` \| `"info"` \| `"warn"` \| `"error"` | `"info"` |
| XUI_DEBUG | `boolean` | `false` |
| XUI_BIN_FOLDER | `string` | `"bin"` |
| XUI_DB_FOLDER | `string` | `"/etc/x-ui"` |
| XUI_LOG_FOLDER | `string` | `"/var/log"` |
例子:
```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
```
</details>
## 预览
![1](./media/1.png)
![2](./media/2.png)
![3](./media/3.png)
![4](./media/4.png)
![5](./media/5.png)
![6](./media/6.png)
![7](./media/7.png)
## 特别感谢
- [alireza0](https://github.com/alireza0/)
## 致谢
- [Iran v2ray rules](https://github.com/chocolate4u/Iran-v2ray-rules) (License: **GPL-3.0**): _Enhanced v2ray/xray and v2ray/xray-clients routing rules with built-in Iranian domains and a focus on security and adblocking._
- [Vietnam Adblock rules](https://github.com/vuong2023/vn-v2ray-rules) (License: **GPL-3.0**): _A hosted domain hosted in Vietnam and blocklist with the most efficiency for Vietnamese._
## Star趋势
[![Stargazers over time](https://starchart.cc/MHSanaei/3x-ui.svg)](https://starchart.cc/MHSanaei/3x-ui)

View File

@@ -1 +1 @@
2.2.5
2.2.6

23
main.go
View File

@@ -244,21 +244,33 @@ func migrateDb() {
}
func removeSecret() {
err := database.InitDB(config.GetDBPath())
userService := service.UserService{}
secretExists, err := userService.CheckSecretExistence()
if err != nil {
fmt.Println(err)
fmt.Println("Error checking secret existence:", err)
return
}
userService := service.UserService{}
if !secretExists {
fmt.Println("No secret exists to remove.")
return
}
err = userService.RemoveUserSecret()
if err != nil {
fmt.Println(err)
fmt.Println("Error removing secret:", err)
return
}
settingService := service.SettingService{}
err = settingService.SetSecretStatus(false)
if err != nil {
fmt.Println(err)
fmt.Println("Error updating secret status:", err)
return
}
fmt.Println("Secret removed successfully.")
}
func main() {
@@ -285,6 +297,7 @@ func main() {
var remove_secret bool
settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
settingCmd.BoolVar(&show, "show", false, "show current settings")
settingCmd.BoolVar(&remove_secret, "remove_secret", false, "remove secret")
settingCmd.IntVar(&port, "port", 0, "set panel port")
settingCmd.StringVar(&username, "username", "", "set login username")
settingCmd.StringVar(&password, "password", "", "set login password")

View File

@@ -92,9 +92,21 @@ func (s *Server) initRouter() (*gin.Engine, error) {
SubJsonFragment = ""
}
SubJsonMux, err := s.settingService.GetSubJsonMux()
if err != nil {
SubJsonMux = ""
}
SubJsonRules, err := s.settingService.GetSubJsonRules()
if err != nil {
SubJsonRules = ""
}
g := engine.Group("/")
s.sub = NewSUBController(g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates, SubJsonFragment)
s.sub = NewSUBController(
g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates,
SubJsonFragment, SubJsonMux, SubJsonRules)
return engine, nil
}

View File

@@ -26,6 +26,8 @@ func NewSUBController(
rModel string,
update string,
jsonFragment string,
jsonMux string,
jsonRules string,
) *SUBController {
sub := NewSubService(showInfo, rModel)
a := &SUBController{
@@ -35,7 +37,7 @@ func NewSUBController(
updateInterval: update,
subService: sub,
subJsonService: NewSubJsonService(jsonFragment, sub),
subJsonService: NewSubJsonService(jsonFragment, jsonMux, jsonRules, sub),
}
a.initRouter(g)
return a

View File

@@ -21,12 +21,13 @@ type SubJsonService struct {
configJson map[string]interface{}
defaultOutbounds []json_util.RawMessage
fragment string
mux string
inboundService service.InboundService
SubService *SubService
}
func NewSubJsonService(fragment string, subService *SubService) *SubJsonService {
func NewSubJsonService(fragment string, mux string, rules string, subService *SubService) *SubJsonService {
var configJson map[string]interface{}
var defaultOutbounds []json_util.RawMessage
json.Unmarshal([]byte(defaultJson), &configJson)
@@ -37,6 +38,17 @@ func NewSubJsonService(fragment string, subService *SubService) *SubJsonService
}
}
if rules != "" {
var newRules []interface{}
routing, _ := configJson["routing"].(map[string]interface{})
defaultRules, _ := routing["rules"].([]interface{})
json.Unmarshal([]byte(rules), &newRules)
defaultRules = append(newRules, defaultRules...)
fmt.Printf("routing: %#v\n\nRules: %#v\n\n", routing, defaultRules)
routing["rules"] = defaultRules
configJson["routing"] = routing
}
if fragment != "" {
defaultOutbounds = append(defaultOutbounds, json_util.RawMessage(fragment))
}
@@ -45,6 +57,7 @@ func NewSubJsonService(fragment string, subService *SubService) *SubJsonService
configJson: configJson,
defaultOutbounds: defaultOutbounds,
fragment: fragment,
mux: mux,
SubService: subService,
}
}
@@ -277,6 +290,9 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
outbound.Protocol = string(inbound.Protocol)
outbound.Tag = "proxy"
if s.mux != "" {
outbound.Mux = json_util.RawMessage(s.mux)
}
outbound.StreamSettings = streamSettings
outbound.Settings = OutboundSettings{
Vnext: vnextData,
@@ -313,6 +329,9 @@ func (s *SubJsonService) genServer(inbound *model.Inbound, streamSettings json_u
outbound.Protocol = string(inbound.Protocol)
outbound.Tag = "proxy"
if s.mux != "" {
outbound.Mux = json_util.RawMessage(s.mux)
}
outbound.StreamSettings = streamSettings
outbound.Settings = OutboundSettings{
Servers: serverData,
@@ -326,7 +345,7 @@ type Outbound struct {
Protocol string `json:"protocol"`
Tag string `json:"tag"`
StreamSettings json_util.RawMessage `json:"streamSettings"`
Mux map[string]interface{} `json:"mux,omitempty"`
Mux json_util.RawMessage `json:"mux,omitempty"`
ProxySettings map[string]interface{} `json:"proxySettings,omitempty"`
Settings OutboundSettings `json:"settings,omitempty"`
}

View File

@@ -755,8 +755,8 @@ style attribute {
.dark .ant-btn-danger[disabled],
.dark .ant-calendar-ok-btn-disabled {
color: rgb(255 255 255 / 35%);
background-color: var(--dark-color-surface-300);
border-color: #42516c;
background-color: var(--dark-color-surface-200);
border-color: var(--dark-color-surface-300);
}
.dark

View File

@@ -257,20 +257,22 @@ class QuicStreamSettings extends CommonClass {
}
class GrpcStreamSettings extends CommonClass {
constructor(serviceName="", multiMode=false) {
constructor(serviceName="", multiMode=false, authority="") {
super();
this.serviceName = serviceName;
this.multiMode = multiMode;
this.authority = authority;
}
static fromJson(json={}) {
return new GrpcStreamSettings(json.serviceName, json.multiMode);
return new GrpcStreamSettings(json.serviceName, json.multiMode,json.authority);
}
toJson() {
return {
serviceName: this.serviceName,
multiMode: this.multiMode,
authority: this.authority
}
}
}
@@ -501,6 +503,7 @@ class Outbound extends CommonClass {
protocol=Protocols.VMess,
settings=null,
streamSettings = new StreamSettings(),
sendThrough,
mux = new Mux(),
) {
super();
@@ -508,6 +511,7 @@ class Outbound extends CommonClass {
this._protocol = protocol;
this.settings = settings == null ? Outbound.Settings.getSettings(protocol) : settings;
this.stream = streamSettings;
this.sendThrough = sendThrough;
this.mux = mux;
}
@@ -543,6 +547,10 @@ class Outbound extends CommonClass {
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol);
}
canEnableMux() {
return [Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks, Protocols.HTTP, Protocols.Socks].includes(this.protocol);
}
hasVnext() {
return [Protocols.VMess, Protocols.VLESS].includes(this.protocol);
}
@@ -573,16 +581,25 @@ class Outbound extends CommonClass {
json.protocol,
Outbound.Settings.fromJson(json.protocol, json.settings),
StreamSettings.fromJson(json.streamSettings),
json.sendThrough,
Mux.fromJson(json.mux),
)
}
toJson() {
var stream;
if (this.canEnableStream()) {
stream = this.stream.toJson();
} else {
if (this.stream?.sockopt)
stream = { sockopt: this.stream.sockopt.toJson() };
}
return {
tag: this.tag == '' ? undefined : this.tag,
protocol: this.protocol,
settings: this.settings instanceof CommonClass ? this.settings.toJson() : this.settings,
streamSettings: this.canEnableStream() ? this.stream.toJson() : undefined,
streamSettings: stream,
sendThrough: this.sendThrough != "" ? this.sendThrough : undefined,
mux: this.mux?.enabled ? this.mux : undefined,
};
}
@@ -695,7 +712,7 @@ class Outbound extends CommonClass {
let data = link.split('?');
if(data.length != 2) return null;
const regex = /([^@]+):\/\/([^@]+)@([^:]+):(\d+)\?(.*)$/;
const regex = /([^@]+):\/\/([^@]+)@(.+):(\d+)\?(.*)$/;
const match = link.match(regex);
if (!match) return null;

View File

@@ -35,9 +35,11 @@ class AllSetting {
this.subUpdates = 0;
this.subEncrypt = true;
this.subShowInfo = false;
this.subURI = '';
this.subJsonURI = '';
this.subJsonFragment = '';
this.subURI = "";
this.subJsonURI = "";
this.subJsonFragment = "";
this.subJsonMux = "";
this.subJsonRules = "";
this.timeLocation = "Asia/Tehran";

View File

@@ -52,6 +52,8 @@ type AllSetting struct {
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"`
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
Datepicker string `json:"datepicker" form:"datepicker"`
}

View File

@@ -63,7 +63,7 @@
</template>
<a-input v-model.trim="client.tgId"></a-input>
</a-form-item>
<a-form-item>
<a-form-item v-if="app.ipLimitEnable">
<template slot="label">
<a-tooltip>
<template slot="title">
@@ -75,13 +75,13 @@
</template>
<a-input-number v-model="client.limitIp" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="client.email && client.limitIp > 0 && isEdit">
<a-form-item v-if="app.ipLimitEnable && client.email && isEdit">
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.inbounds.IPLimitlogDesc" }}</span>
</template>
<span>{{ i18n "pages.inbounds.IPLimitlog" }}</span>
<span>{{ i18n "pages.inbounds.IPLimitlog" }} </span>
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>

View File

@@ -11,6 +11,9 @@
<a-form-item label='{{ i18n "pages.xray.outbound.tag" }}' has-feedback :validate-status="outModal.duplicateTag? 'warning' : 'success'">
<a-input v-model.trim="outbound.tag" @change="outModal.check()" placeholder='{{ i18n "pages.xray.outbound.tagDesc" }}'></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.outbound.sendThrough" }}'>
<a-input v-model="outbound.sendThrough"></a-input>
</a-form-item>
<!-- freedom settings-->
<template v-if="outbound.protocol === Protocols.Freedom">
@@ -252,6 +255,7 @@
<a-select-option value="wechat-video">WeChat</a-select-option>
<a-select-option value="dtls">DTLS 1.2</a-select-option>
<a-select-option value="wireguard">WireGuard</a-select-option>
<a-select-option value="dns">DNS</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
@@ -329,6 +333,9 @@
<a-form-item label='Service Name'>
<a-input v-model.trim="outbound.stream.grpc.serviceName"></a-input>
</a-form-item>
<a-form-item label="Authority">
<a-input v-model.trim="outbound.stream.grpc.authority"></a-input>
</a-form-item>
<a-form-item label='Multi Mode'>
<a-switch v-model="outbound.stream.grpc.multiMode"></a-switch>
</a-form-item>
@@ -422,21 +429,23 @@
</template>
<!-- mux settings -->
<a-form-item label="Mux">
<a-switch v-model="outbound.mux.enabled"></a-switch>
</a-form-item>
<template v-if="outbound.mux.enabled">
<a-form-item label="Concurrency">
<a-input-number v-model="outbound.mux.concurrency" :min="-1" :max="1024"></a-input-number>
</a-form-item>
<a-form-item label="xudp Concurrency">
<a-input-number v-model="outbound.mux.xudpConcurrency" :min="-1" :max="1024"></a-input-number>
</a-form-item>
<a-form-item label="xudp UDP 443">
<a-select v-model="outbound.mux.xudpProxyUDP443" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="c in ['reject', 'allow', 'skip']" :value="c">[[ c ]]</a-select-option>
</a-select>
<template v-if="outbound.canEnableMux()">
<a-form-item label="Mux">
<a-switch v-model="outbound.mux.enabled"></a-switch>
</a-form-item>
<template v-if="outbound.mux.enabled">
<a-form-item label="Concurrency">
<a-input-number v-model="outbound.mux.concurrency" :min="-1" :max="1024"></a-input-number>
</a-form-item>
<a-form-item label="xudp Concurrency">
<a-input-number v-model="outbound.mux.xudpConcurrency" :min="-1" :max="1024"></a-input-number>
</a-form-item>
<a-form-item label="xudp UDP 443">
<a-select v-model="outbound.mux.xudpProxyUDP443" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="c in ['reject', 'allow', 'skip']" :value="c">[[ c ]]</a-select-option>
</a-select>
</a-form-item>
</template>
</template>
</a-form>

View File

@@ -19,7 +19,7 @@
:overlay-class-name="themeSwitcher.currentTheme"
ok-text='{{ i18n "reset"}}'
cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: #008771' : 'color: #008771'"></a-icon>
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: var(--color-primary-100)' : 'color: var(--color-primary-100)'"></a-icon>
<a-icon style="font-size: 24px; cursor: pointer;" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
</a-popconfirm>
</a-tooltip>
@@ -54,7 +54,7 @@
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
</template>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'">
<a-badge :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'">
</a-badge>
</a-tooltip>
[[ client.email ]]
@@ -86,13 +86,12 @@
<td width="120px" v-else-if="client.totalGB > 0">
<a-progress :stroke-color="clientStatsColor(record, client.email)"
:show-info="false"
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
:status="isClientEnabled(record, client.email)? 'exception' : ''"
:percent="statsProgress(record, client.email)"/>
</td>
<td width="120px" v-else class="infinite-bar">
<a-progress
:show-info="false"
:status="isClientOnline(client.email)? 'active' : ''"
:percent="100"></a-progress>
</td>
<td width="60px">
@@ -117,7 +116,7 @@
</td>
<td width="120px" class="infinite-bar">
<a-progress :show-info="false"
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
:status="isClientEnabled(record, client.email)? 'exception' : ''"
:percent="expireProgress(client.expiryTime, client.reset)"/>
</td>
<td width="60px">[[ client.reset + "d" ]]</td>
@@ -202,14 +201,13 @@
</template>
<a-progress :stroke-color="clientStatsColor(record, client.email)"
:show-info="false"
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
:status="isClientEnabled(record, client.email)? 'exception' : ''"
:percent="statsProgress(record, client.email)"/>
</a-popover>
</td>
<td width="120px" v-else class="infinite-bar">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'"
:show-info="false"
:status="isClientOnline(client.email)? 'active' : ''"
:percent="100"></a-progress>
</td>
<td width="80px">
@@ -235,7 +233,7 @@
<span v-else>[[ DateUtil.formatMillis(client._expiryTime) ]]</span>
</template>
<a-progress :show-info="false"
:status="isClientOnline(client.email)? 'active' : isClientEnabled(record, client.email)? 'exception' : ''"
:status="isClientEnabled(record, client.email)? 'exception' : ''"
:percent="expireProgress(client.expiryTime, client.reset)"/>
</a-popover>
</td>

View File

@@ -36,13 +36,6 @@
.ant-collapse {
margin: 5px 0;
}
.online-animation .ant-badge-status-dot {
animation: 1.2s ease infinite normal none running onlineAnimation;
}
@keyframes onlineAnimation {
0%, 50%, 100% { transform: scale(1); opacity: 1; }
10% { transform: scale(1.5); opacity: .2; }
}
.info-large-tag {
max-width: 200px;
overflow: hidden;
@@ -578,6 +571,7 @@
datepicker: 'gregorian',
tgBotEnable: false,
showAlert: false,
ipLimitEnable: false,
pageSize: 0,
isMobile: window.innerWidth <= 768,
},
@@ -625,6 +619,7 @@
this.pageSize = pageSize;
this.remarkModel = remarkModel;
this.datepicker = datepicker;
this.ipLimitEnable = ipLimitEnable;
}
},
setInbounds(dbInbounds) {

View File

@@ -295,9 +295,11 @@
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subJsonPath"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subURI"}}' desc='{{ i18n "pages.settings.subURIDesc"}}' v-model="allSetting.subJsonURI" placeholder="(http|https)://domain[:port]/path/"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.fragment"}}' desc='{{ i18n "pages.settings.fragmentDesc"}}' v-model="fragment"></setting-list-item>
<setting-list-item type="switch" title='Mux' v-model="enableMux"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.directCountryConfigs"}}' desc='{{ i18n "pages.xray.directCountryConfigsDesc"}}' v-model="enableDirect"></setting-list-item>
</a-list>
<a-collapse v-if="fragment">
<a-collapse-panel header='{{ i18n "pages.settings.fragment"}}'>
<a-collapse v-if="fragment || enableMux || enableDirect">
<a-collapse-panel header='{{ i18n "pages.settings.fragment"}}' v-if="fragment">
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
@@ -318,6 +320,36 @@
<setting-list-item type="text" title='Length' v-model="fragmentLength" placeholder="100-200"></setting-list-item>
<setting-list-item type="text" title='Interval' v-model="fragmentInterval" placeholder="10-20"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='Mux' v-if="enableMux">
<setting-list-item type="number" title='Concurrency' v-model="muxConcurrency" :min="-1" :max="1024"></setting-list-item>
<setting-list-item type="number" title='xudp Concurrency' v-model="muxXudpConcurrency" :min="-1" :max="1024"></setting-list-item>
<a-list-item style="padding: 20px">
<a-row>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='xudp UDP 443'/>
</a-col>
<a-col :lg="24" :xl="12">
<a-select
v-model="muxXudpProxyUDP443"
style="width: 100%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p" :label="p" v-for="p in ['reject', 'allow', 'skip']">
[[ p ]]
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.directCountryConfigs"}}' v-if="enableDirect">
<a-list-item style="padding: 20px">
<a-checkbox-group
v-model="directCountries"
name="Countries"
:options="countryOptions"
/>
</a-list-item>
</a-collapse-panel>
</a-collapse>
</a-tab-pane>
</a-tabs>
@@ -367,6 +399,40 @@
}
}
},
defaultMux: {
enabled: true,
concurrency: 8,
xudpConcurrency: 16,
xudpProxyUDP443: "reject"
},
defaultRules: [
{
type: "field",
outboundTag: "direct",
domain: [
"geosite:category-ir",
"geosite:cn"
],
"enabled": true
},
{
type: "field",
outboundTag: "direct",
ip: [
"geoip:private",
"geoip:ir",
"geoip:cn"
],
enabled: true
},
],
countryOptions: [
{ label: 'Private IP/Domain', value: 'private' },
{ label: '🇮🇷 Iran', value: 'ir' },
{ label: '🇨🇳 China', value: 'cn' },
{ label: '🇷🇺 Russia', value: 'ru' },
{ label: '🇻🇳 Vietnam', value: 'vn' },
],
get remarkModel() {
rm = this.allSetting.remarkModel;
return rm.length>1 ? rm.substring(1).split('') : [];
@@ -425,7 +491,6 @@
this.loading(false);
if (msg.success) {
this.user = {};
window.location.replace(basePath + "logout");
}
},
async restartPanel() {
@@ -462,9 +527,8 @@
async updateSecret() {
this.loading(true);
const msg = await HttpUtil.post("/panel/setting/updateUserSecret", this.user);
if (msg.success) {
if (msg && msg.obj) {
this.user = msg.obj;
window.location.replace(basePath + "logout");
}
this.loading(false);
await this.updateAllSetting();
@@ -532,6 +596,61 @@
}
}
},
enableMux: {
get: function() { return this.allSetting?.subJsonMux != ""; },
set: function (v) {
this.allSetting.subJsonMux = v ? JSON.stringify(this.defaultMux) : "";
}
},
muxConcurrency: {
get: function() { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).concurrency : -1; },
set: function(v) {
newMux = JSON.parse(this.allSetting.subJsonMux);
newMux.concurrency = v;
this.allSetting.subJsonMux = JSON.stringify(newMux);
}
},
muxXudpConcurrency: {
get: function() { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpConcurrency : -1; },
set: function(v) {
newMux = JSON.parse(this.allSetting.subJsonMux);
newMux.xudpConcurrency = v;
this.allSetting.subJsonMux = JSON.stringify(newMux);
}
},
muxXudpProxyUDP443: {
get: function() { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpProxyUDP443 : "reject"; },
set: function(v) {
newMux = JSON.parse(this.allSetting.subJsonMux);
newMux.xudpProxyUDP443 = v;
this.allSetting.subJsonMux = JSON.stringify(newMux);
}
},
enableDirect: {
get: function() { return this.allSetting?.subJsonRules != ""; },
set: function (v) {
this.allSetting.subJsonRules = v ? JSON.stringify(this.defaultRules) : "";
}
},
directCountries: {
get: function() {
if (!this.enableDirect) return [];
rules = JSON.parse(this.allSetting.subJsonRules);
return Array.isArray(rules) ? rules[1].ip.map(d => d.replace("geoip:","")) : [];
},
set: function (v) {
rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return;
rules[0].domain = [];
rules[1].ip = [];
v.forEach(d => {
category = ["cn","private"].includes(d) ? "" : "category-";
rules[0].domain.push("geosite:"+category+d);
rules[1].ip.push("geoip:"+d);
});
this.allSetting.subJsonRules = JSON.stringify(rules);
}
},
confAlerts: {
get: function() {
if (!this.allSetting) return [];

View File

@@ -12,7 +12,7 @@
<td>[[ warpModal.warpData.access_token ]]</td>
</tr>
<tr>
<td>Devide ID</td>
<td>Device ID</td>
<td>[[ warpModal.warpData.device_id ]]</td>
</tr>
<tr class="client-table-odd-row">
@@ -24,19 +24,19 @@
<td>[[ warpModal.warpData.private_key ]]</td>
</tr>
</table>
<a-divider style="margin: 0;">{{ i18n "pages.settings.toasts.modifySettings" }}</a-divider>
<a-divider style="margin: 0;">{{ i18n "pages.xray.outbound.settings" }}</a-divider>
<a-collapse style="margin: 10px 0;">
<a-collapse-panel header='WARP/WARP+ License Key'>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="License Key">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Key">
<a-input v-model="warpPlus"></a-input>
<a-button @click="updateLicense(warpPlus)" :disabled="warpPlus.length<26" :loading="warpModal.confirmLoading">{{ i18n "pages.inbounds.update" }}</a-button>
</a-form-item>
</a-form>
</a-collapse-panel>
</a-collapse>
<a-divider style="margin: 0;">{{ i18n "pages.settings.toasts.getSettings" }}</a-divider>
<a-button icon="sync" @click="getConfig" style="margin-bottom: 10px;" :loading="warpModal.confirmLoading">{{ i18n "info" }}</a-button>
<a-divider style="margin: 0;">{{ i18n "pages.xray.outbound.accountInfo" }}</a-divider>
<a-button icon="sync" @click="getConfig" style="margin-top: 5px; margin-bottom: 10px;" :loading="warpModal.confirmLoading" type="primary">{{ i18n "info" }}</a-button>
<template v-if="!ObjectUtil.isEmpty(warpModal.warpConfig)">
<table style="width: 100%">
<tr class="client-table-odd-row">
@@ -48,7 +48,7 @@
<td>[[ warpModal.warpConfig.model ]]</td>
</tr>
<tr class="client-table-odd-row">
<td>Device Active</td>
<td>Device Enabled</td>
<td>[[ warpModal.warpConfig.enabled ]]</td>
</tr>
<template v-if="!ObjectUtil.isEmpty(warpModal.warpConfig.account)">
@@ -61,7 +61,7 @@
<td>[[ warpModal.warpConfig.account.role ]]</td>
</tr>
<tr>
<td>Premium Data</td>
<td>WARP+ Data</td>
<td>[[ sizeFormat(warpModal.warpConfig.account.premium_data) ]]</td>
</tr>
<tr class="client-table-odd-row">
@@ -74,16 +74,15 @@
</tr>
</template>
</table>
<a-divider style="margin: 10px 0;">WARP {{ i18n "pages.xray.rules.outbound" }}</a-divider>
<a-divider style="margin: 10px 0;">{{ i18n "pages.xray.outbound.outboundStatus" }}</a-divider>
<a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "status" }}'>
<template v-if="warpOutboundIndex>=0">
<a-tag color="green">{{ i18n "enabled" }}</a-tag>
<a-button @click="resetOutbound" :loading="warpModal.confirmLoading">{{ i18n "reset" }}</a-button>
<a-tag color="green" style="line-height: 31px;">{{ i18n "enabled" }}</a-tag>
<a-button @click="resetOutbound" :loading="warpModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button>
</template>
<template v-else>
<a-tag color="orange">{{ i18n "disabled" }}</a-tag>
<a-button @click="addOutbound" :loading="warpModal.confirmLoading">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
<a-tag color="orange" style="line-height: 31px;">{{ i18n "disabled" }}</a-tag>
<a-button @click="addOutbound" :loading="warpModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
</template>
</a-form-item>
</a-form>

View File

@@ -1182,7 +1182,7 @@
});
},
deleteBalancer(index) {
newTemplateSettings = this.templateSettings;
let newTemplateSettings = { ...this.templateSettings };
// Remove from balancers
const removedBalancer = this.balancersData.splice(index, 1)[0];
@@ -1192,9 +1192,12 @@
newTemplateSettings.routing.balancers.splice(realIndex, 1);
// Remove related routing rules
let rules = newTemplateSettings.routing.rules.filter((r) => !r.balancerTag || r.balancerTag !== removedBalancer.tag);
newTemplateSettings.routing.rules = rules;
newTemplateSettings.routing.rules.forEach((rule) => {
if (rule.balancerTag === removedBalancer.tag) {
delete rule.balancerTag;
}
});
// Update balancers property to an empty array if there are no more balancers
if (newTemplateSettings.routing.balancers.length === 0) {
delete newTemplateSettings.routing.balancers;

View File

@@ -1069,10 +1069,7 @@ func (s *InboundService) AddClientStat(tx *gorm.DB, inboundId int, client *model
clientTraffic.Reset = client.Reset
result := tx.Create(&clientTraffic)
err := result.Error
if err != nil {
return err
}
return nil
return err
}
func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *model.Client) error {
@@ -1085,12 +1082,8 @@ func (s *InboundService) UpdateClientStat(tx *gorm.DB, email string, client *mod
"expiry_time": client.ExpiryTime,
"reset": client.Reset,
})
err := result.Error
if err != nil {
return err
}
return nil
return err
}
func (s *InboundService) UpdateClientIPs(tx *gorm.DB, oldEmail string, newEmail string) error {
@@ -1215,10 +1208,7 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId string) err
}
inbound.Settings = string(modifiedSettings)
_, err = s.UpdateInboundClient(inbound, clientId)
if err != nil {
return err
}
return nil
return err
}
func (s *InboundService) checkIsEnabledByEmail(clientEmail string) (bool, error) {
@@ -1365,10 +1355,7 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
}
inbound.Settings = string(modifiedSettings)
_, err = s.UpdateInboundClient(inbound, clientId)
if err != nil {
return err
}
return nil
return err
}
func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry_time int64) error {
@@ -1425,10 +1412,7 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
}
inbound.Settings = string(modifiedSettings)
_, err = s.UpdateInboundClient(inbound, clientId)
if err != nil {
return err
}
return nil
return err
}
func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, totalGB int) error {
@@ -1488,10 +1472,7 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
}
inbound.Settings = string(modifiedSettings)
_, err = s.UpdateInboundClient(inbound, clientId)
if err != nil {
return err
}
return nil
return err
}
func (s *InboundService) ResetClientTrafficByEmail(clientEmail string) error {
@@ -1584,10 +1565,7 @@ func (s *InboundService) ResetAllClientTraffics(id int) error {
Updates(map[string]interface{}{"enable": true, "up": 0, "down": 0})
err := result.Error
if err != nil {
return err
}
return nil
return err
}
func (s *InboundService) ResetAllTraffics() error {
@@ -1598,10 +1576,7 @@ func (s *InboundService) ResetAllTraffics() error {
Updates(map[string]interface{}{"up": 0, "down": 0})
err := result.Error
if err != nil {
return err
}
return nil
return err
}
func (s *InboundService) DelDepletedClients(id int) (err error) {
@@ -1675,11 +1650,7 @@ func (s *InboundService) DelDepletedClients(id int) (err error) {
}
err = tx.Where(whereText+" and enable = ?", id, false).Delete(xray.ClientTraffic{}).Error
if err != nil {
return err
}
return nil
return err
}
func (s *InboundService) GetClientTrafficTgBot(tgId string) ([]*xray.ClientTraffic, error) {

View File

@@ -61,6 +61,8 @@ var defaultValueMap = map[string]string{
"subJsonPath": "/json/",
"subJsonURI": "",
"subJsonFragment": "",
"subJsonMux": "",
"subJsonRules": "",
"datepicker": "gregorian",
"warp": "",
}
@@ -437,6 +439,14 @@ func (s *SettingService) GetSubJsonFragment() (string, error) {
return s.getString("subJsonFragment")
}
func (s *SettingService) GetSubJsonMux() (string, error) {
return s.getString("subJsonMux")
}
func (s *SettingService) GetSubJsonRules() (string, error) {
return s.getString("subJsonRules")
}
func (s *SettingService) GetDatepicker() (string, error) {
return s.getString("datepicker")
}
@@ -449,6 +459,25 @@ func (s *SettingService) SetWarp(data string) error {
return s.setString("warp", data)
}
func (s *SettingService) GetIpLimitEnable() (bool, error) {
templateConfig, err := s.GetXrayConfigTemplate()
if err != nil {
return false, err
}
var xrayConfig map[string]interface{}
err = json.Unmarshal([]byte(templateConfig), &xrayConfig)
if err != nil {
return false, err
}
if logConfig, ok := xrayConfig["log"].(map[string]interface{}); ok {
if accessLogPath, ok := logConfig["access"].(string); ok {
return accessLogPath == "./access.log", nil
}
}
return false, nil
}
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
if err := allSetting.CheckValid(); err != nil {
return err
@@ -482,17 +511,18 @@ func (s *SettingService) GetDefaultXrayConfig() (interface{}, error) {
func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
type settingFunc func() (interface{}, error)
settings := map[string]settingFunc{
"expireDiff": func() (interface{}, error) { return s.GetExpireDiff() },
"trafficDiff": func() (interface{}, error) { return s.GetTrafficDiff() },
"pageSize": func() (interface{}, error) { return s.GetPageSize() },
"defaultCert": func() (interface{}, error) { return s.GetCertFile() },
"defaultKey": func() (interface{}, error) { return s.GetKeyFile() },
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() },
"subEnable": func() (interface{}, error) { return s.GetSubEnable() },
"subURI": func() (interface{}, error) { return s.GetSubURI() },
"subJsonURI": func() (interface{}, error) { return s.GetSubJsonURI() },
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
"datepicker": func() (interface{}, error) { return s.GetDatepicker() },
"expireDiff": func() (interface{}, error) { return s.GetExpireDiff() },
"trafficDiff": func() (interface{}, error) { return s.GetTrafficDiff() },
"pageSize": func() (interface{}, error) { return s.GetPageSize() },
"defaultCert": func() (interface{}, error) { return s.GetCertFile() },
"defaultKey": func() (interface{}, error) { return s.GetKeyFile() },
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() },
"subEnable": func() (interface{}, error) { return s.GetSubEnable() },
"subURI": func() (interface{}, error) { return s.GetSubURI() },
"subJsonURI": func() (interface{}, error) { return s.GetSubJsonURI() },
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
"datepicker": func() (interface{}, error) { return s.GetDatepicker() },
"ipLimitEnable": func() (interface{}, error) { return s.GetIpLimitEnable() },
}
result := make(map[string]interface{})

View File

@@ -79,6 +79,21 @@ func (s *UserService) GetUserSecret(id int) *model.User {
return user
}
func (s *UserService) CheckSecretExistence() (bool, error) {
db := database.GetDB()
var count int64
err := db.Model(model.User{}).
Where("login_secret IS NOT NULL").
Count(&count).
Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (s *UserService) UpdateFirstUser(username string, password string) error {
if username == "" {
return errors.New("username can not be empty")

View File

@@ -446,6 +446,10 @@
"bridge" = "Bridge"
"portal" = "Portal"
"intercon" = "Interconnection"
"settings" = "Settings"
"accountInfo" = "Account Information"
"outboundStatus" = "Outbound Status"
"sendThrough" = "Send Through"
[pages.xray.balancer]
"addBalancer" = "Add Balancer"

View File

@@ -446,6 +446,10 @@
"bridge" = "puente"
"portal" = "portal"
"intercon" = "Interconexión"
"settings" = "Configuración"
"accountInfo" = "Información de la cuenta"
"outboundStatus" = "Estado de salida"
"sendThrough" = "Enviar a través de"
[pages.xray.balancer]
"addBalancer" = "Agregar equilibrador"

View File

@@ -446,6 +446,10 @@
"bridge" = "پل"
"portal" = "پورتال"
"intercon" = "اتصال میانی"
"settings" = "تنظیمات"
"accountInfo" = "اطلاعات حساب"
"outboundStatus" = "وضعیت خروجی"
"sendThrough" = "ارسال با"
[pages.xray.balancer]
"addBalancer" = "افزودن بالانسر"

View File

@@ -446,6 +446,10 @@
"bridge" = "Jembatan"
"portal" = "Portal"
"intercon" = "Interkoneksi"
"settings" = "Pengaturan"
"accountInfo" = "Informasi Akun"
"outboundStatus" = "Status Keluar"
"sendThrough" = "Kirim Melalui"
[pages.xray.balancer]
"addBalancer" = "Tambahkan Penyeimbang"

View File

@@ -446,6 +446,10 @@
"bridge" = "Мост"
"portal" = "Портал"
"intercon" = "Соединение"
"settings" = "Настройки"
"accountInfo" = "Информация Об Учетной Записи"
"outboundStatus" = "Исходящий статус"
"sendThrough" = "Отправить через"
[pages.xray.balancer]
"addBalancer" = "Добавить балансир"

View File

@@ -446,6 +446,10 @@
"bridge" = "Міст"
"portal" = "Портал"
"intercon" = "Взаємозв'язок"
"settings" = "Налаштування"
"accountInfo" = "Інформація про обліковий запис"
"outboundStatus" = "Статус виходу"
"sendThrough" = "Надіслати через"
[pages.xray.balancer]
"addBalancer" = "Додати балансир"

View File

@@ -446,6 +446,10 @@
"bridge" = "Cầu"
"portal" = "Cổng thông tin"
"intercon" = "Kết nối"
"settings" = "cài đặt"
"accountInfo" = "Thông tin tài khoản"
"outboundStatus" = "Trạng thái đầu ra"
"sendThrough" = "Gửi qua"
[pages.xray.balancer]
"addBalancer" = "Thêm cân bằng"

View File

@@ -446,6 +446,10 @@
"bridge" = "Bridge"
"portal" = "Portal"
"intercon" = "互连"
"settings" = "设置"
"accountInfo" = "帐户信息"
"outboundStatus" = "出站状态"
"sendThrough" = "发送通过"
[pages.xray.balancer]
"addBalancer" = "添加负载均衡"