Compare commits
63 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
9c1be8c57a | ||
|
|
e5d08bb358 | ||
|
|
77d844460a | ||
|
|
d3507286d6 | ||
|
|
b616a5b138 | ||
|
|
178716025f | ||
|
|
580dca211e | ||
|
|
c06ccbac19 | ||
|
|
dfc6b56327 | ||
|
|
91c4cefb1c | ||
|
|
04ce724801 | ||
|
|
bdc0fd76bd | ||
|
|
72e206879a | ||
|
|
8791c8729c | ||
|
|
57363ee778 | ||
|
|
509f3641e4 | ||
|
|
9fef61b813 | ||
|
|
6428dee149 | ||
|
|
ebb9ee5827 | ||
|
|
534cc5d5a2 | ||
|
|
8373c5eb38 | ||
|
|
fd5cfd7853 | ||
|
|
cf10ff67e3 | ||
|
|
97020a82bc | ||
|
|
60ff4908cf | ||
|
|
b99c0fe230 | ||
|
|
f0badd3d96 | ||
|
|
98e050ef77 | ||
|
|
5d86891162 | ||
|
|
fe61444da2 | ||
|
|
9d76f90d85 | ||
|
|
a7d35ba5c5 | ||
|
|
21b82dceb2 | ||
|
|
0b6b73f895 | ||
|
|
f335c8d39a | ||
|
|
36d9a43421 | ||
|
|
5d118afb07 | ||
|
|
56c30d4d0c | ||
|
|
4c4a70e742 | ||
|
|
581e3b835a | ||
|
|
ccfeb28492 | ||
|
|
100bd29c92 | ||
|
|
6ed818cd67 | ||
|
|
698bf1d390 | ||
|
|
aeed8db419 | ||
|
|
10191887af | ||
|
|
b240b843c8 | ||
|
|
500b08b112 | ||
|
|
64c5ecce99 | ||
|
|
3e08b794be | ||
|
|
e473b7393e | ||
|
|
942ac7f562 | ||
|
|
292d5b89d4 | ||
|
|
f1057b1142 | ||
|
|
b8da50dde7 | ||
|
|
991e711ac1 | ||
|
|
1468984c92 | ||
|
|
e8b0d2c839 | ||
|
|
00f9de6f1c | ||
|
|
51f80b445d | ||
|
|
4bf10cd8b7 | ||
|
|
a02f03f5d6 | ||
|
|
7a660506ea |
14
.github/ISSUE_TEMPLATE/------.md
vendored
Normal file
@@ -0,0 +1,14 @@
|
||||
---
|
||||
name: 提问题点这里
|
||||
about: issue 模板
|
||||
title: ''
|
||||
labels: ''
|
||||
assignees: ''
|
||||
|
||||
---
|
||||
|
||||
任何由于自己的配置错误导致的情况,请自行解决,issues 只用于解决面板自身的 bug
|
||||
|
||||
如果你确定面板的功能实现有 bug,请尽可能提供更多更精确的描述信息、复现方法与复现结果等等,而不是草草一句话了事,这对于问题的解决没有帮助
|
||||
|
||||
提问的艺术: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md
|
||||
147
.github/workflows/release.yml
vendored
Normal file
@@ -0,0 +1,147 @@
|
||||
name: Release X-ui
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
- 0.*
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
release:
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
upload_url: ${{ steps.create_release.outputs.upload_url }}
|
||||
steps:
|
||||
- name: Create Release
|
||||
id: create_release
|
||||
uses: actions/create-release@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
|
||||
with:
|
||||
tag_name: ${{ github.ref }}
|
||||
release_name: ${{ github.ref }}
|
||||
draft: true
|
||||
prerelease: true
|
||||
linuxamd64build:
|
||||
name: build x-ui amd64 version
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.18
|
||||
- name: build linux amd64 version
|
||||
run: |
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
|
||||
mkdir x-ui
|
||||
cp xui-release x-ui/xui-release
|
||||
cp x-ui.service x-ui/x-ui.service
|
||||
cp x-ui.sh x-ui/x-ui.sh
|
||||
cd x-ui
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
rm -f Xray-linux-64.zip geoip.dat geosite.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
mv xray xray-linux-amd64
|
||||
cd ..
|
||||
cd ..
|
||||
- name: package
|
||||
run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui
|
||||
- name: upload
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: x-ui-linux-amd64.tar.gz
|
||||
asset_name: x-ui-linux-amd64.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
linuxarm64build:
|
||||
name: build x-ui arm64 version
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.18
|
||||
- name: build linux arm64 version
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt install gcc-aarch64-linux-gnu
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc go build -o xui-release -v main.go
|
||||
mkdir x-ui
|
||||
cp xui-release x-ui/xui-release
|
||||
cp x-ui.service x-ui/x-ui.service
|
||||
cp x-ui.sh x-ui/x-ui.sh
|
||||
cd x-ui
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
|
||||
unzip Xray-linux-arm64-v8a.zip
|
||||
rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
mv xray xray-linux-arm64
|
||||
cd ..
|
||||
cd ..
|
||||
- name: package
|
||||
run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui
|
||||
- name: upload
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: x-ui-linux-arm64.tar.gz
|
||||
asset_name: x-ui-linux-arm64.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
linuxs390xbuild:
|
||||
name: build x-ui s390x version
|
||||
needs: release
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v2
|
||||
- name: Set up Go
|
||||
uses: actions/setup-go@v2
|
||||
with:
|
||||
go-version: 1.18
|
||||
- name: build linux s390x version
|
||||
run: |
|
||||
sudo apt-get update
|
||||
sudo apt install gcc-s390x-linux-gnu -y
|
||||
CGO_ENABLED=1 GOOS=linux GOARCH=s390x CC=s390x-linux-gnu-gcc go build -o xui-release -v main.go
|
||||
mkdir x-ui
|
||||
cp xui-release x-ui/xui-release
|
||||
cp x-ui.service x-ui/x-ui.service
|
||||
cp x-ui.sh x-ui/x-ui.sh
|
||||
cd x-ui
|
||||
mv xui-release x-ui
|
||||
mkdir bin
|
||||
cd bin
|
||||
wget https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-s390x.zip
|
||||
unzip Xray-linux-s390x.zip
|
||||
rm -f Xray-linux-s390x.zip geoip.dat geosite.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
mv xray xray-linux-s390x
|
||||
cd ..
|
||||
cd ..
|
||||
- name: package
|
||||
run: tar -zcvf x-ui-linux-s390x.tar.gz x-ui
|
||||
- name: upload
|
||||
uses: actions/upload-release-asset@v1
|
||||
env:
|
||||
GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
|
||||
with:
|
||||
upload_url: ${{ needs.release.outputs.upload_url }}
|
||||
asset_path: x-ui-linux-s390x.tar.gz
|
||||
asset_name: x-ui-linux-s390x.tar.gz
|
||||
asset_content_type: application/gzip
|
||||
4
.gitignore
vendored
@@ -5,4 +5,6 @@ bin/config.json
|
||||
dist/
|
||||
x-ui-*.tar.gz
|
||||
/x-ui
|
||||
/release.sh
|
||||
/release.sh
|
||||
.sync*
|
||||
main
|
||||
|
||||
14
Dockerfile
Normal file
@@ -0,0 +1,14 @@
|
||||
FROM golang:latest AS builder
|
||||
WORKDIR /root
|
||||
COPY . .
|
||||
RUN go build main.go
|
||||
|
||||
|
||||
FROM debian:11-slim
|
||||
RUN apt-get update && apt-get install -y --no-install-recommends -y ca-certificates \
|
||||
&& apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
|
||||
WORKDIR /root
|
||||
COPY --from=builder /root/main /root/x-ui
|
||||
COPY bin/. /root/bin/.
|
||||
VOLUME [ "/etc/x-ui" ]
|
||||
CMD [ "./x-ui" ]
|
||||
124
README.md
@@ -1,45 +1,143 @@
|
||||
# x-ui
|
||||
|
||||
支持多协议多用户的 xray 面板
|
||||
|
||||
# 功能介绍
|
||||
|
||||
- 系统状态监控
|
||||
- 支持多用户多协议,网页可视化操作
|
||||
- 支持的协议:vmess、vless、trojan、shadowsocks、dokodemo-door、socks、http
|
||||
- 支持配置更多传输配置
|
||||
- 账号流量统计
|
||||
- 流量统计,限制流量,限制到期时间
|
||||
- 可自定义 xray 配置模板
|
||||
- 支持 https 访问面板(自备域名 + ssl 证书)
|
||||
- 支持一键SSL证书申请且自动续签
|
||||
- 更多高级配置项,详见面板
|
||||
|
||||
# 安装&升级
|
||||
## 测试版
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/sprov065/x-ui/master/install.sh) 0.1.0
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)
|
||||
```
|
||||
|
||||
## 手动安装&升级
|
||||
|
||||
1. 首先从 https://github.com/vaxilu/x-ui/releases 下载最新的压缩包,一般选择 `amd64`架构
|
||||
2. 然后将这个压缩包上传到服务器的 `/root/`目录下,并使用 `root`用户登录服务器
|
||||
|
||||
> 如果你的服务器 cpu 架构不是 `amd64`,自行将命令中的 `amd64`替换为其他架构
|
||||
|
||||
```
|
||||
cd /root/
|
||||
rm x-ui/ /usr/local/x-ui/ /usr/bin/x-ui -rf
|
||||
tar zxvf x-ui-linux-amd64.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
|
||||
```
|
||||
|
||||
## 使用docker安装
|
||||
|
||||
> 此 docker 教程与 docker 镜像由[Chasing66](https://github.com/Chasing66)提供
|
||||
|
||||
1. 安装docker
|
||||
|
||||
```shell
|
||||
curl -fsSL https://get.docker.com | sh
|
||||
```
|
||||
|
||||
2. 安装x-ui
|
||||
|
||||
```shell
|
||||
mkdir x-ui && cd x-ui
|
||||
docker run -itd --network=host \
|
||||
-v $PWD/db/:/etc/x-ui/ \
|
||||
-v $PWD/cert/:/root/cert/ \
|
||||
--name x-ui --restart=unless-stopped \
|
||||
enwaiax/x-ui:latest
|
||||
```
|
||||
|
||||
> Build 自己的镜像
|
||||
|
||||
```shell
|
||||
docker build -t x-ui .
|
||||
```
|
||||
|
||||
## SSL证书申请
|
||||
|
||||
> 此功能与教程由[FranzKafkaYu](https://github.com/FranzKafkaYu)提供
|
||||
|
||||
脚本内置SSL证书申请功能,使用该脚本申请证书,需满足以下条件:
|
||||
|
||||
- 知晓Cloudflare 注册邮箱
|
||||
- 知晓Cloudflare Global API Key
|
||||
- 域名已通过cloudflare进行解析到当前服务器
|
||||
|
||||
获取Cloudflare Global API Key的方法:
|
||||

|
||||

|
||||
|
||||
使用时只需输入 `域名`, `邮箱`, `API KEY`即可,示意图如下:
|
||||

|
||||
|
||||
注意事项:
|
||||
|
||||
- 该脚本使用DNS API进行证书申请
|
||||
- 默认使用Let'sEncrypt作为CA方
|
||||
- 证书安装目录为/root/cert目录
|
||||
- 本脚本申请证书均为泛域名证书
|
||||
|
||||
## Tg机器人使用(开发中,暂不可使用)
|
||||
|
||||
> 此功能与教程由[FranzKafkaYu](https://github.com/FranzKafkaYu)提供
|
||||
|
||||
X-UI支持通过Tg机器人实现每日流量通知,面板登录提醒等功能,使用Tg机器人,需要自行申请
|
||||
具体申请教程可以参考[博客链接](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
|
||||
使用说明:在面板后台设置机器人相关参数,具体包括
|
||||
|
||||
- Tg机器人Token
|
||||
- Tg机器人ChatId
|
||||
- Tg机器人周期运行时间,采用crontab语法
|
||||
|
||||
参考语法:
|
||||
- 30 * * * * * //每一分的第30s进行通知
|
||||
- @hourly //每小时通知
|
||||
- @daily //每天通知(凌晨零点整)
|
||||
- @every 8h //每8小时通知
|
||||
|
||||
TG通知内容:
|
||||
- 节点流量使用
|
||||
- 面板登录提醒
|
||||
- 节点到期提醒
|
||||
- 流量预警提醒
|
||||
|
||||
更多功能规划中...
|
||||
## 建议系统
|
||||
|
||||
- CentOS 7+
|
||||
- Ubuntu 16+
|
||||
- Debian 8+
|
||||
|
||||
# 常见问题
|
||||
## 与 v2-ui 关系
|
||||
x-ui 相当于 v2-ui 的加强版,未来会加入更多功能,待 x-ui 功能稳定后,v2-ui 将不再提供更新
|
||||
|
||||
x-ui 可与 v2-ui 并存,数据不互通,不影响对方的运行
|
||||
|
||||
## 从 v2-ui 迁移
|
||||
首先在安装了 v2-ui 的服务器上安装最新版 x-ui,然后使用以下命令进行迁移,将迁移本机 v2-ui 的`所有 inbound 账号数据`至 x-ui,`面板设置和用户名密码不会迁移`
|
||||
> 迁移成功后请`关闭 v2-ui` 并且`重启 x-ui`,否则 v2-ui 的 inbound 会与 x-ui 的 inbound 会产生`端口冲突`
|
||||
|
||||
首先在安装了 v2-ui 的服务器上安装最新版 x-ui,然后使用以下命令进行迁移,将迁移本机 v2-ui 的 `所有 inbound 账号数据`至 x-ui,`面板设置和用户名密码不会迁移`
|
||||
|
||||
> 迁移成功后请 `关闭 v2-ui`并且 `重启 x-ui`,否则 v2-ui 的 inbound 会与 x-ui 的 inbound 会产生 `端口冲突`
|
||||
|
||||
```
|
||||
x-ui v2-ui
|
||||
```
|
||||
|
||||
# Telegram
|
||||
群组:https://t.me/sprov_blog
|
||||
## issue 关闭
|
||||
|
||||
频道:https://t.me/sprov_channel
|
||||
各种小白问题看得血压很高
|
||||
|
||||
## Stargazers over time
|
||||
|
||||
[](https://starchart.cc/sprov065/x-ui)
|
||||
[](https://starchart.cc/vaxilu/x-ui)
|
||||
|
||||
@@ -1 +1 @@
|
||||
0.2.0
|
||||
0.3.2
|
||||
1
go.mod
@@ -9,6 +9,7 @@ require (
|
||||
github.com/gin-contrib/sessions v0.0.3
|
||||
github.com/gin-gonic/gin v1.7.1
|
||||
github.com/go-ole/go-ole v1.2.5 // indirect
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
|
||||
github.com/nicksnyder/go-i18n/v2 v2.1.2
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
|
||||
2
go.sum
@@ -70,6 +70,8 @@ github.com/go-playground/universal-translator v0.17.0 h1:icxd5fm+REJzpZx7ZfpaD87
|
||||
github.com/go-playground/universal-translator v0.17.0/go.mod h1:UkSxE5sNxxRwHyU+Scu5vgOQjsIJAF8j9muTVoKLVtA=
|
||||
github.com/go-playground/validator/v10 v10.4.1 h1:pH2c5ADXtd66mxoE0Zm9SUhxE20r7aM3F26W0hOn+GE=
|
||||
github.com/go-playground/validator/v10 v10.4.1/go.mod h1:nlOn6nFhuKACm19sB/8EGNn9GlaMV7XkbRSipzJ0Ii4=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 h1:wG8n/XJQ07TmjbITcGiUaOtXxdrINDz1b0J1w0SzqDc=
|
||||
github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1/go.mod h1:A2S0CWkNylc2phvKXWBBdD3K0iGnDBGbzRpISP2zBl8=
|
||||
github.com/gogo/protobuf v1.1.1/go.mod h1:r8qH/GZQm5c6nD/R0oafs1akxWv10x8SbQlK7atdtwQ=
|
||||
github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfUlaOkMMuAQtPIUF8ecWP5IEl/CR7VP2Q=
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
|
||||
66
install.sh
Normal file → Executable file
@@ -32,17 +32,19 @@ fi
|
||||
arch=$(arch)
|
||||
|
||||
if [[ $arch == "x86_64" || $arch == "x64" || $arch == "amd64" ]]; then
|
||||
arch="amd64"
|
||||
arch="amd64"
|
||||
elif [[ $arch == "aarch64" || $arch == "arm64" ]]; then
|
||||
arch="arm64"
|
||||
arch="arm64"
|
||||
elif [[ $arch == "s390x" ]]; then
|
||||
arch="s390x"
|
||||
else
|
||||
arch="amd64"
|
||||
echo -e "${red}检测架构失败,使用默认架构: ${arch}${plain}"
|
||||
arch="amd64"
|
||||
echo -e "${red}检测架构失败,使用默认架构: ${arch}${plain}"
|
||||
fi
|
||||
|
||||
echo "架构: ${arch}"
|
||||
|
||||
if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ] ; then
|
||||
if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then
|
||||
echo "本软件不支持 32 位系统(x86),请使用 64 位系统(x86_64),如果检测有误,请联系作者"
|
||||
exit -1
|
||||
fi
|
||||
@@ -79,28 +81,46 @@ install_base() {
|
||||
fi
|
||||
}
|
||||
|
||||
#This function will be called when user installed x-ui out of sercurity
|
||||
config_after_install() {
|
||||
echo -e "${yellow}出于安全考虑,安装/更新完成后需要强制修改端口与账户密码${plain}"
|
||||
read -p "确认是否继续?[y/n]": config_confirm
|
||||
if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then
|
||||
read -p "请设置您的账户名:" config_account
|
||||
echo -e "${yellow}您的账户名将设定为:${config_account}${plain}"
|
||||
read -p "请设置您的账户密码:" config_password
|
||||
echo -e "${yellow}您的账户密码将设定为:${config_password}${plain}"
|
||||
read -p "请设置面板访问端口:" config_port
|
||||
echo -e "${yellow}您的面板访问端口将设定为:${config_port}${plain}"
|
||||
echo -e "${yellow}确认设定,设定中${plain}"
|
||||
/usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password}
|
||||
echo -e "${yellow}账户密码设定完成${plain}"
|
||||
/usr/local/x-ui/x-ui setting -port ${config_port}
|
||||
echo -e "${yellow}面板端口设定完成${plain}"
|
||||
else
|
||||
echo -e "${red}已取消,所有设置项均为默认设置,请及时修改${plain}"
|
||||
fi
|
||||
}
|
||||
|
||||
install_x-ui() {
|
||||
systemctl stop x-ui
|
||||
cd /usr/local/
|
||||
if [[ -e /usr/local/x-ui/ ]]; then
|
||||
rm /usr/local/x-ui/ -rf
|
||||
fi
|
||||
|
||||
if [ $# == 0 ] ;then
|
||||
last_version=$(curl -Ls "https://api.github.com/repos/sprov065/x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [ $# == 0 ]; then
|
||||
last_version=$(curl -Ls "https://api.github.com/repos/vaxilu/x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$last_version" ]]; then
|
||||
echo -e "${red}检测 x-ui 版本失败,可能是超出 Github API 限制,请稍后再试,或手动指定 x-ui 版本安装${plain}"
|
||||
exit 1
|
||||
fi
|
||||
echo -e "检测到 x-ui 最新版本:${last_version},开始安装"
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/sprov065/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz https://github.com/vaxilu/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${red}下载 x-ui 失败,请确保你的服务器能够下载 Github 的文件${plain}"
|
||||
exit 1
|
||||
fi
|
||||
else
|
||||
last_version=$1
|
||||
url="https://github.com/sprov065/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
|
||||
url="https://github.com/vaxilu/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
|
||||
echo -e "开始安装 x-ui v$1"
|
||||
wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url}
|
||||
if [[ $? -ne 0 ]]; then
|
||||
@@ -109,24 +129,30 @@ install_x-ui() {
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e /usr/local/x-ui/ ]]; then
|
||||
rm /usr/local/x-ui/ -rf
|
||||
fi
|
||||
|
||||
tar zxvf x-ui-linux-${arch}.tar.gz
|
||||
rm x-ui-linux-${arch}.tar.gz -f
|
||||
cd x-ui
|
||||
chmod +x x-ui bin/xray-linux-${arch}
|
||||
cp -f x-ui.service /etc/systemd/system/
|
||||
wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/vaxilu/x-ui/main/x-ui.sh
|
||||
chmod +x /usr/local/x-ui/x-ui.sh
|
||||
chmod +x /usr/bin/x-ui
|
||||
config_after_install
|
||||
#echo -e "如果是全新安装,默认网页端口为 ${green}54321${plain},用户名和密码默认都是 ${green}admin${plain}"
|
||||
#echo -e "请自行确保此端口没有被其他程序占用,${yellow}并且确保 54321 端口已放行${plain}"
|
||||
# echo -e "若想将 54321 修改为其它端口,输入 x-ui 命令进行修改,同样也要确保你修改的端口也是放行的"
|
||||
#echo -e ""
|
||||
#echo -e "如果是更新面板,则按你之前的方式访问面板"
|
||||
#echo -e ""
|
||||
systemctl daemon-reload
|
||||
systemctl enable x-ui
|
||||
systemctl start x-ui
|
||||
echo -e "${green}x-ui v${last_version}${plain} 安装完成,面板已启动,"
|
||||
echo -e ""
|
||||
echo -e "如果是全新安装,默认网页端口为 ${green}54321${plain},用户名和密码默认都是 ${green}admin${plain}"
|
||||
echo -e "请自行确保此端口没有被其他程序占用,${yellow}并且确保 54321 端口已放行${plain}"
|
||||
# echo -e "若想将 54321 修改为其它端口,输入 x-ui 命令进行修改,同样也要确保你修改的端口也是放行的"
|
||||
echo -e ""
|
||||
echo -e "如果是更新面板,则按你之前的方式访问面板"
|
||||
echo -e ""
|
||||
curl -o /usr/bin/x-ui -Ls https://raw.githubusercontent.com/sprov065/x-ui/master/x-ui.sh
|
||||
chmod +x /usr/bin/x-ui
|
||||
echo -e "x-ui 管理脚本使用方法: "
|
||||
echo -e "----------------------------------------------"
|
||||
echo -e "x-ui - 显示管理菜单 (功能更多)"
|
||||
|
||||
@@ -7,16 +7,22 @@ import (
|
||||
|
||||
var logger *logging.Logger
|
||||
|
||||
func init() {
|
||||
InitLogger(logging.INFO)
|
||||
}
|
||||
|
||||
func InitLogger(level logging.Level) {
|
||||
format := logging.MustStringFormatter(
|
||||
`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
|
||||
)
|
||||
logger = logging.MustGetLogger("x-ui")
|
||||
newLogger := logging.MustGetLogger("x-ui")
|
||||
backend := logging.NewLogBackend(os.Stderr, "", 0)
|
||||
backendFormatter := logging.NewBackendFormatter(backend, format)
|
||||
backendLeveled := logging.AddModuleLevel(backendFormatter)
|
||||
backendLeveled.SetLevel(level, "")
|
||||
logger.SetBackend(backendLeveled)
|
||||
newLogger.SetBackend(backendLeveled)
|
||||
|
||||
logger = newLogger
|
||||
}
|
||||
|
||||
func Debug(args ...interface{}) {
|
||||
|
||||
106
main.go
@@ -3,7 +3,6 @@ package main
|
||||
import (
|
||||
"flag"
|
||||
"fmt"
|
||||
"github.com/op/go-logging"
|
||||
"log"
|
||||
"os"
|
||||
"os/signal"
|
||||
@@ -16,6 +15,8 @@ import (
|
||||
"x-ui/web"
|
||||
"x-ui/web/global"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
func runWebServer() {
|
||||
@@ -50,6 +51,7 @@ func runWebServer() {
|
||||
}
|
||||
|
||||
sigCh := make(chan os.Signal, 1)
|
||||
//信号量捕获处理
|
||||
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
|
||||
for {
|
||||
sig := <-sigCh
|
||||
@@ -90,6 +92,90 @@ func resetSetting() {
|
||||
}
|
||||
}
|
||||
|
||||
func showSetting(show bool) {
|
||||
if show {
|
||||
settingService := service.SettingService{}
|
||||
port, err := settingService.GetPort()
|
||||
if err != nil {
|
||||
fmt.Println("get current port fialed,error info:", err)
|
||||
}
|
||||
userService := service.UserService{}
|
||||
userModel, err := userService.GetFirstUser()
|
||||
if err != nil {
|
||||
fmt.Println("get current user info failed,error info:", err)
|
||||
}
|
||||
username := userModel.Username
|
||||
userpasswd := userModel.Password
|
||||
if (username == "") || (userpasswd == "") {
|
||||
fmt.Println("current username or password is empty")
|
||||
}
|
||||
fmt.Println("current pannel settings as follows:")
|
||||
fmt.Println("username:", username)
|
||||
fmt.Println("userpasswd:", userpasswd)
|
||||
fmt.Println("port:", port)
|
||||
}
|
||||
}
|
||||
|
||||
func updateTgbotEnableSts(status bool) {
|
||||
settingService := service.SettingService{}
|
||||
currentTgSts, err := settingService.GetTgbotenabled()
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
logger.Infof("current enabletgbot status[%v],need update to status[%v]", currentTgSts, status)
|
||||
if currentTgSts != status {
|
||||
err := settingService.SetTgbotenabled(status)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
} else {
|
||||
logger.Infof("SetTgbotenabled[%v] success", status)
|
||||
}
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
}
|
||||
|
||||
settingService := service.SettingService{}
|
||||
|
||||
if tgBotToken != "" {
|
||||
err := settingService.SetTgBotToken(tgBotToken)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
} else {
|
||||
logger.Info("updateTgbotSetting tgBotToken success")
|
||||
}
|
||||
}
|
||||
|
||||
if tgBotRuntime != "" {
|
||||
err := settingService.SetTgbotRuntime(tgBotRuntime)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
} else {
|
||||
logger.Infof("updateTgbotSetting tgBotRuntime[%s] success", tgBotRuntime)
|
||||
}
|
||||
}
|
||||
|
||||
if tgBotChatid != 0 {
|
||||
err := settingService.SetTgBotChatId(tgBotChatid)
|
||||
if err != nil {
|
||||
fmt.Println(err)
|
||||
return
|
||||
} else {
|
||||
logger.Info("updateTgbotSetting tgBotChatid success")
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func updateSetting(port int, username string, password string) {
|
||||
err := database.InitDB(config.GetDBPath())
|
||||
if err != nil {
|
||||
@@ -137,11 +223,21 @@ func main() {
|
||||
var port int
|
||||
var username string
|
||||
var password string
|
||||
var tgbottoken string
|
||||
var tgbotchatid int
|
||||
var enabletgbot bool
|
||||
var tgbotRuntime string
|
||||
var reset bool
|
||||
settingCmd.BoolVar(&reset, "reset", false, "reset all setting")
|
||||
var show bool
|
||||
settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
|
||||
settingCmd.BoolVar(&show, "show", false, "show current settings")
|
||||
settingCmd.IntVar(&port, "port", 0, "set panel port")
|
||||
settingCmd.StringVar(&username, "username", "", "set login username")
|
||||
settingCmd.StringVar(&password, "password", "", "set login password")
|
||||
settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
|
||||
settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
|
||||
settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id")
|
||||
settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
|
||||
|
||||
oldUsage := flag.Usage
|
||||
flag.Usage = func() {
|
||||
@@ -188,6 +284,12 @@ func main() {
|
||||
} else {
|
||||
updateSetting(port, username, password)
|
||||
}
|
||||
if show {
|
||||
showSetting(show)
|
||||
}
|
||||
if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") {
|
||||
updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
|
||||
}
|
||||
default:
|
||||
fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
|
||||
fmt.Println()
|
||||
|
||||
BIN
media/2022-04-04_141259.png
Normal file
|
After Width: | Height: | Size: 19 KiB |
BIN
media/2022-04-17_110907.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
media/2022-04-17_111321.png
Normal file
|
After Width: | Height: | Size: 20 KiB |
BIN
media/2022-04-17_111705.png
Normal file
|
After Width: | Height: | Size: 36 KiB |
BIN
media/2022-04-17_111910.png
Normal file
|
After Width: | Height: | Size: 30 KiB |
BIN
media/bda84fbc2ede834deaba1c173a932223.png
Normal file
|
After Width: | Height: | Size: 26 KiB |
BIN
media/d13ffd6a73f938d1037d0708e31433bf.png
Normal file
|
After Width: | Height: | Size: 23 KiB |
21
util/common/format.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package common
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
)
|
||||
|
||||
func FormatTraffic(trafficBytes int64) (size string) {
|
||||
if trafficBytes < 1024 {
|
||||
return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1))
|
||||
} else if trafficBytes < (1024 * 1024) {
|
||||
return fmt.Sprintf("%.2fKB", float64(trafficBytes)/float64(1024))
|
||||
} else if trafficBytes < (1024 * 1024 * 1024) {
|
||||
return fmt.Sprintf("%.2fMB", float64(trafficBytes)/float64(1024*1024))
|
||||
} else if trafficBytes < (1024 * 1024 * 1024 * 1024) {
|
||||
return fmt.Sprintf("%.2fGB", float64(trafficBytes)/float64(1024*1024*1024))
|
||||
} else if trafficBytes < (1024 * 1024 * 1024 * 1024 * 1024) {
|
||||
return fmt.Sprintf("%.2fTB", float64(trafficBytes)/float64(1024*1024*1024*1024))
|
||||
} else {
|
||||
return fmt.Sprintf("%.2fEB", float64(trafficBytes)/float64(1024*1024*1024*1024*1024))
|
||||
}
|
||||
}
|
||||
9
util/common/stringUtil.go
Normal file
@@ -0,0 +1,9 @@
|
||||
package common
|
||||
|
||||
import "sort"
|
||||
|
||||
func IsSubString(target string, str_array []string) bool {
|
||||
sort.Strings(str_array)
|
||||
index := sort.SearchStrings(str_array, target)
|
||||
return index < len(str_array) && str_array[index] == target
|
||||
}
|
||||
0
util/sys/a.s
Normal file
8
util/sys/psutil.go
Normal file
@@ -0,0 +1,8 @@
|
||||
package sys
|
||||
|
||||
import (
|
||||
_ "unsafe"
|
||||
)
|
||||
|
||||
//go:linkname HostProc github.com/shirou/gopsutil/internal/common.HostProc
|
||||
func HostProc(combineWith ...string) string
|
||||
23
util/sys/sys_darwin.go
Normal file
@@ -0,0 +1,23 @@
|
||||
// +build darwin
|
||||
|
||||
package sys
|
||||
|
||||
import (
|
||||
"github.com/shirou/gopsutil/net"
|
||||
)
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
stats, err := net.Connections("tcp")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(stats), nil
|
||||
}
|
||||
|
||||
func GetUDPCount() (int, error) {
|
||||
stats, err := net.Connections("udp")
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
return len(stats), nil
|
||||
}
|
||||
70
util/sys/sys_linux.go
Normal file
@@ -0,0 +1,70 @@
|
||||
// +build linux
|
||||
|
||||
package sys
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
)
|
||||
|
||||
func getLinesNum(filename string) (int, error) {
|
||||
file, err := os.Open(filename)
|
||||
if err != nil {
|
||||
return 0, err
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
sum := 0
|
||||
buf := make([]byte, 8192)
|
||||
for {
|
||||
n, err := file.Read(buf)
|
||||
|
||||
var buffPosition int
|
||||
for {
|
||||
i := bytes.IndexByte(buf[buffPosition:], '\n')
|
||||
if i < 0 || n == buffPosition {
|
||||
break
|
||||
}
|
||||
buffPosition += i + 1
|
||||
sum++
|
||||
}
|
||||
|
||||
if err == io.EOF {
|
||||
return sum, nil
|
||||
} else if err != nil {
|
||||
return sum, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func GetTCPCount() (int, error) {
|
||||
root := HostProc()
|
||||
|
||||
tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root))
|
||||
if err != nil {
|
||||
return tcp4, err
|
||||
}
|
||||
tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root))
|
||||
if err != nil {
|
||||
return tcp4 + tcp6, nil
|
||||
}
|
||||
|
||||
return tcp4 + tcp6, nil
|
||||
}
|
||||
|
||||
func GetUDPCount() (int, error) {
|
||||
root := HostProc()
|
||||
|
||||
udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root))
|
||||
if err != nil {
|
||||
return udp4, err
|
||||
}
|
||||
udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root))
|
||||
if err != nil {
|
||||
return udp4 + udp6, nil
|
||||
}
|
||||
|
||||
return udp4 + udp6, nil
|
||||
}
|
||||
@@ -1,14 +1,18 @@
|
||||
class User {
|
||||
username = "";
|
||||
password = "";
|
||||
|
||||
constructor() {
|
||||
this.username = "";
|
||||
this.password = "";
|
||||
}
|
||||
}
|
||||
|
||||
class Msg {
|
||||
success = false;
|
||||
msg = "";
|
||||
obj = null;
|
||||
|
||||
constructor(success, msg, obj) {
|
||||
this.success = false;
|
||||
this.msg = "";
|
||||
this.obj = null;
|
||||
|
||||
if (success != null) {
|
||||
this.success = success;
|
||||
}
|
||||
@@ -22,24 +26,25 @@ class Msg {
|
||||
}
|
||||
|
||||
class DBInbound {
|
||||
id = 0;
|
||||
userId = 0;
|
||||
up = 0;
|
||||
down = 0;
|
||||
total = 0;
|
||||
remark = "";
|
||||
enable = true;
|
||||
expiryTime = 0;
|
||||
|
||||
listen = "";
|
||||
port = 0;
|
||||
protocol = "";
|
||||
settings = "";
|
||||
streamSettings = "";
|
||||
tag = "";
|
||||
sniffing = "";
|
||||
|
||||
constructor(data) {
|
||||
this.id = 0;
|
||||
this.userId = 0;
|
||||
this.up = 0;
|
||||
this.down = 0;
|
||||
this.total = 0;
|
||||
this.remark = "";
|
||||
this.enable = true;
|
||||
this.expiryTime = 0;
|
||||
|
||||
this.listen = "";
|
||||
this.port = 0;
|
||||
this.protocol = "";
|
||||
this.settings = "";
|
||||
this.streamSettings = "";
|
||||
this.tag = "";
|
||||
this.sniffing = "";
|
||||
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
@@ -86,6 +91,25 @@ class DBInbound {
|
||||
return address;
|
||||
}
|
||||
|
||||
get _expiryTime() {
|
||||
if (this.expiryTime === 0) {
|
||||
return null;
|
||||
}
|
||||
return moment(this.expiryTime);
|
||||
}
|
||||
|
||||
set _expiryTime(t) {
|
||||
if (t == null) {
|
||||
this.expiryTime = 0;
|
||||
} else {
|
||||
this.expiryTime = t.valueOf();
|
||||
}
|
||||
}
|
||||
|
||||
get isExpiry() {
|
||||
return this.expiryTime < new Date().getTime();
|
||||
}
|
||||
|
||||
toInbound() {
|
||||
let settings = {};
|
||||
if (!ObjectUtil.isEmpty(this.settings)) {
|
||||
@@ -132,17 +156,21 @@ class DBInbound {
|
||||
}
|
||||
|
||||
class AllSetting {
|
||||
webListen = "";
|
||||
webPort = 54321;
|
||||
webCertFile = "";
|
||||
webKeyFile = "";
|
||||
webBasePath = "/";
|
||||
|
||||
xrayTemplateConfig = "";
|
||||
|
||||
timeLocation = "Asia/Shanghai";
|
||||
|
||||
constructor(data) {
|
||||
this.webListen = "";
|
||||
this.webPort = 54321;
|
||||
this.webCertFile = "";
|
||||
this.webKeyFile = "";
|
||||
this.webBasePath = "/";
|
||||
this.tgBotEnable = false;
|
||||
this.tgBotToken = "";
|
||||
this.tgBotChatId = 0;
|
||||
this.tgRunTime = "";
|
||||
this.xrayTemplateConfig = "";
|
||||
|
||||
this.timeLocation = "Asia/Shanghai";
|
||||
|
||||
if (data == null) {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -40,7 +40,7 @@ const RULE_DOMAIN = {
|
||||
SPEEDTEST: 'geosite:speedtest',
|
||||
};
|
||||
|
||||
const VLESS_FLOW = {
|
||||
const FLOW_CONTROL = {
|
||||
ORIGIN: "xtls-rprx-origin",
|
||||
DIRECT: "xtls-rprx-direct",
|
||||
};
|
||||
@@ -50,7 +50,7 @@ Object.freeze(VmessMethods);
|
||||
Object.freeze(SSMethods);
|
||||
Object.freeze(RULE_IP);
|
||||
Object.freeze(RULE_DOMAIN);
|
||||
Object.freeze(VLESS_FLOW);
|
||||
Object.freeze(FLOW_CONTROL);
|
||||
|
||||
class XrayCommonClass {
|
||||
|
||||
@@ -110,11 +110,13 @@ class XrayCommonClass {
|
||||
}
|
||||
|
||||
class TcpStreamSettings extends XrayCommonClass {
|
||||
constructor(type='none',
|
||||
constructor(acceptProxyProtocol=false,
|
||||
type='none',
|
||||
request=new TcpStreamSettings.TcpRequest(),
|
||||
response=new TcpStreamSettings.TcpResponse(),
|
||||
) {
|
||||
super();
|
||||
this.acceptProxyProtocol = acceptProxyProtocol;
|
||||
this.type = type;
|
||||
this.request = request;
|
||||
this.response = response;
|
||||
@@ -125,7 +127,7 @@ class TcpStreamSettings extends XrayCommonClass {
|
||||
if (!header) {
|
||||
header = {};
|
||||
}
|
||||
return new TcpStreamSettings(
|
||||
return new TcpStreamSettings(json.acceptProxyProtocol,
|
||||
header.type,
|
||||
TcpStreamSettings.TcpRequest.fromJson(header.request),
|
||||
TcpStreamSettings.TcpResponse.fromJson(header.response),
|
||||
@@ -134,6 +136,7 @@ class TcpStreamSettings extends XrayCommonClass {
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
acceptProxyProtocol: this.acceptProxyProtocol,
|
||||
header: {
|
||||
type: this.type,
|
||||
request: this.type === 'http' ? this.request.toJson() : undefined,
|
||||
@@ -293,8 +296,9 @@ class KcpStreamSettings extends XrayCommonClass {
|
||||
}
|
||||
|
||||
class WsStreamSettings extends XrayCommonClass {
|
||||
constructor(path='/', headers=[]) {
|
||||
constructor(acceptProxyProtocol=false, path='/', headers=[]) {
|
||||
super();
|
||||
this.acceptProxyProtocol = acceptProxyProtocol;
|
||||
this.path = path;
|
||||
this.headers = headers;
|
||||
}
|
||||
@@ -318,6 +322,7 @@ class WsStreamSettings extends XrayCommonClass {
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new WsStreamSettings(
|
||||
json.acceptProxyProtocol,
|
||||
json.path,
|
||||
XrayCommonClass.toHeaders(json.headers),
|
||||
);
|
||||
@@ -325,6 +330,7 @@ class WsStreamSettings extends XrayCommonClass {
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
acceptProxyProtocol: this.acceptProxyProtocol,
|
||||
path: this.path,
|
||||
headers: XrayCommonClass.toV2Headers(this.headers, false),
|
||||
};
|
||||
@@ -411,10 +417,11 @@ class GrpcStreamSettings extends XrayCommonClass {
|
||||
|
||||
class TlsStreamSettings extends XrayCommonClass {
|
||||
constructor(serverName='',
|
||||
certificates=[new TlsStreamSettings.Cert()]) {
|
||||
certificates=[new TlsStreamSettings.Cert()], alpn=[]) {
|
||||
super();
|
||||
this.server = serverName;
|
||||
this.certs = certificates;
|
||||
this.alpn = alpn;
|
||||
}
|
||||
|
||||
addCert(cert) {
|
||||
@@ -430,9 +437,11 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
if (!ObjectUtil.isEmpty(json.certificates)) {
|
||||
certs = json.certificates.map(cert => TlsStreamSettings.Cert.fromJson(cert));
|
||||
}
|
||||
|
||||
return new TlsStreamSettings(
|
||||
json.serverName,
|
||||
certs,
|
||||
json.alpn
|
||||
);
|
||||
}
|
||||
|
||||
@@ -440,6 +449,7 @@ class TlsStreamSettings extends XrayCommonClass {
|
||||
return {
|
||||
serverName: this.server,
|
||||
certificates: TlsStreamSettings.toJsonArray(this.certs),
|
||||
alpn: this.alpn
|
||||
};
|
||||
}
|
||||
}
|
||||
@@ -629,11 +639,7 @@ class Inbound extends XrayCommonClass {
|
||||
if (isTls) {
|
||||
this.stream.security = 'tls';
|
||||
} else {
|
||||
if (this.protocol === Protocols.TROJAN) {
|
||||
this.xtls = true;
|
||||
} else {
|
||||
this.stream.security = 'none';
|
||||
}
|
||||
this.stream.security = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -645,11 +651,7 @@ class Inbound extends XrayCommonClass {
|
||||
if (isXTls) {
|
||||
this.stream.security = 'xtls';
|
||||
} else {
|
||||
if (this.protocol === Protocols.TROJAN) {
|
||||
this.tls = true;
|
||||
} else {
|
||||
this.stream.security = 'none';
|
||||
}
|
||||
this.stream.security = 'none';
|
||||
}
|
||||
}
|
||||
|
||||
@@ -697,11 +699,13 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
// VLess
|
||||
// VLess & Trojan
|
||||
get flow() {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VLESS:
|
||||
return this.settings.vlesses[0].flow;
|
||||
case Protocols.TROJAN:
|
||||
return this.settings.clients[0].flow;
|
||||
default:
|
||||
return "";
|
||||
}
|
||||
@@ -1008,18 +1012,6 @@ class Inbound extends XrayCommonClass {
|
||||
params.set("flow", this.settings.vlesses[0].flow);
|
||||
}
|
||||
|
||||
for (const [key, value] of params) {
|
||||
switch (key) {
|
||||
case "host":
|
||||
case "path":
|
||||
case "seed":
|
||||
case "key":
|
||||
case "alpn":
|
||||
params.set(key, encodeURIComponent(value));
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
const link = `vless://${uuid}@${address}:${port}`;
|
||||
const url = new URL(link);
|
||||
for (const [key, value] of params) {
|
||||
@@ -1165,7 +1157,7 @@ Inbound.VmessSettings = class extends Inbound.Settings {
|
||||
}
|
||||
};
|
||||
Inbound.VmessSettings.Vmess = class extends XrayCommonClass {
|
||||
constructor(id=RandomUtil.randomUUID(), alterId=64) {
|
||||
constructor(id=RandomUtil.randomUUID(), alterId=0) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.alterId = alterId;
|
||||
@@ -1217,7 +1209,7 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
};
|
||||
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
|
||||
constructor(id=RandomUtil.randomUUID(), flow=VLESS_FLOW.DIRECT) {
|
||||
constructor(id=RandomUtil.randomUUID(), flow=FLOW_CONTROL.DIRECT) {
|
||||
super();
|
||||
this.id = id;
|
||||
this.flow = flow;
|
||||
@@ -1270,14 +1262,26 @@ Inbound.VLESSSettings.Fallback = class extends XrayCommonClass {
|
||||
};
|
||||
|
||||
Inbound.TrojanSettings = class extends Inbound.Settings {
|
||||
constructor(protocol, clients=[new Inbound.TrojanSettings.Client()]) {
|
||||
constructor(protocol,
|
||||
clients=[new Inbound.TrojanSettings.Client()],
|
||||
fallbacks=[],) {
|
||||
super(protocol);
|
||||
this.clients = clients;
|
||||
this.fallbacks = fallbacks;
|
||||
}
|
||||
|
||||
addTrojanFallback() {
|
||||
this.fallbacks.push(new Inbound.TrojanSettings.Fallback());
|
||||
}
|
||||
|
||||
delTrojanFallback(index) {
|
||||
this.fallbacks.splice(index, 1);
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
clients: Inbound.TrojanSettings.toJsonArray(this.clients),
|
||||
fallbacks: Inbound.TrojanSettings.toJsonArray(this.fallbacks),
|
||||
};
|
||||
}
|
||||
|
||||
@@ -1286,27 +1290,74 @@ Inbound.TrojanSettings = class extends Inbound.Settings {
|
||||
for (const c of json.clients) {
|
||||
clients.push(Inbound.TrojanSettings.Client.fromJson(c));
|
||||
}
|
||||
return new Inbound.TrojanSettings(Protocols.TROJAN, clients);
|
||||
return new Inbound.TrojanSettings(
|
||||
Protocols.TROJAN,
|
||||
clients,
|
||||
Inbound.TrojanSettings.Fallback.fromJson(json.fallbacks),);
|
||||
}
|
||||
};
|
||||
Inbound.TrojanSettings.Client = class extends XrayCommonClass {
|
||||
constructor(password=RandomUtil.randomSeq(10)) {
|
||||
constructor(password=RandomUtil.randomSeq(10), flow=FLOW_CONTROL.DIRECT) {
|
||||
super();
|
||||
this.password = password;
|
||||
this.flow = flow;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
password: this.password,
|
||||
flow: this.flow,
|
||||
};
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
return new Inbound.TrojanSettings.Client(json.password);
|
||||
return new Inbound.TrojanSettings.Client(
|
||||
json.password,
|
||||
json.flow,
|
||||
);
|
||||
}
|
||||
|
||||
};
|
||||
|
||||
Inbound.TrojanSettings.Fallback = class extends XrayCommonClass {
|
||||
constructor(name="", alpn='', path='', dest='', xver=0) {
|
||||
super();
|
||||
this.name = name;
|
||||
this.alpn = alpn;
|
||||
this.path = path;
|
||||
this.dest = dest;
|
||||
this.xver = xver;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
let xver = this.xver;
|
||||
if (!Number.isInteger(xver)) {
|
||||
xver = 0;
|
||||
}
|
||||
return {
|
||||
name: this.name,
|
||||
alpn: this.alpn,
|
||||
path: this.path,
|
||||
dest: this.dest,
|
||||
xver: xver,
|
||||
}
|
||||
}
|
||||
|
||||
static fromJson(json=[]) {
|
||||
const fallbacks = [];
|
||||
for (let fallback of json) {
|
||||
fallbacks.push(new Inbound.TrojanSettings.Fallback(
|
||||
fallback.name,
|
||||
fallback.alpn,
|
||||
fallback.path,
|
||||
fallback.dest,
|
||||
fallback.xver,
|
||||
))
|
||||
}
|
||||
return fallbacks;
|
||||
}
|
||||
};
|
||||
|
||||
Inbound.ShadowsocksSettings = class extends Inbound.Settings {
|
||||
constructor(protocol,
|
||||
method=SSMethods.AES_256_GCM,
|
||||
|
||||
@@ -77,18 +77,19 @@ class PromiseUtil {
|
||||
|
||||
}
|
||||
|
||||
const seq = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g',
|
||||
'h', 'i', 'j', 'k', 'l', 'm', 'n',
|
||||
'o', 'p', 'q', 'r', 's', 't',
|
||||
'u', 'v', 'w', 'x', 'y', 'z',
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G',
|
||||
'H', 'I', 'J', 'K', 'L', 'M', 'N',
|
||||
'O', 'P', 'Q', 'R', 'S', 'T',
|
||||
'U', 'V', 'W', 'X', 'Y', 'Z'
|
||||
];
|
||||
|
||||
class RandomUtil {
|
||||
static seq = [
|
||||
'a', 'b', 'c', 'd', 'e', 'f', 'g',
|
||||
'h', 'i', 'j', 'k', 'l', 'm', 'n',
|
||||
'o', 'p', 'q', 'r', 's', 't',
|
||||
'u', 'v', 'w', 'x', 'y', 'z',
|
||||
'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
|
||||
'A', 'B', 'C', 'D', 'E', 'F', 'G',
|
||||
'H', 'I', 'J', 'K', 'L', 'M', 'N',
|
||||
'O', 'P', 'Q', 'R', 'S', 'T',
|
||||
'U', 'V', 'W', 'X', 'Y', 'Z'
|
||||
];
|
||||
|
||||
static randomIntRange(min, max) {
|
||||
return parseInt(Math.random() * (max - min) + min, 10);
|
||||
@@ -101,7 +102,7 @@ class RandomUtil {
|
||||
static randomSeq(count) {
|
||||
let str = '';
|
||||
for (let i = 0; i < count; ++i) {
|
||||
str += this.seq[this.randomInt(62)];
|
||||
str += seq[this.randomInt(62)];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
@@ -109,7 +110,7 @@ class RandomUtil {
|
||||
static randomLowerAndNum(count) {
|
||||
let str = '';
|
||||
for (let i = 0; i < count; ++i) {
|
||||
str += this.seq[this.randomInt(36)];
|
||||
str += seq[this.randomInt(36)];
|
||||
}
|
||||
return str;
|
||||
}
|
||||
@@ -121,7 +122,7 @@ class RandomUtil {
|
||||
if (index <= 9) {
|
||||
str += index;
|
||||
} else {
|
||||
str += this.seq[index - 10];
|
||||
str += seq[index - 10];
|
||||
}
|
||||
}
|
||||
return str;
|
||||
|
||||
@@ -37,7 +37,7 @@ func (a *InboundController) startTask() {
|
||||
c := webServer.GetCron()
|
||||
c.AddFunc("@every 10s", func() {
|
||||
if a.xrayService.IsNeedRestartAndSetFalse() {
|
||||
err := a.xrayService.RestartXray()
|
||||
err := a.xrayService.RestartXray(false)
|
||||
if err != nil {
|
||||
logger.Error("restart xray failed:", err)
|
||||
}
|
||||
|
||||
@@ -1,11 +1,14 @@
|
||||
package controller
|
||||
|
||||
import (
|
||||
"github.com/gin-gonic/gin"
|
||||
"net/http"
|
||||
"time"
|
||||
"x-ui/logger"
|
||||
"x-ui/web/job"
|
||||
"x-ui/web/service"
|
||||
"x-ui/web/session"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type LoginForm struct {
|
||||
@@ -55,10 +58,15 @@ func (a *IndexController) login(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
user := a.userService.CheckUser(form.Username, form.Password)
|
||||
timeStr := time.Now().Format("2006-01-02 15:04:05")
|
||||
if user == nil {
|
||||
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
|
||||
logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
|
||||
pureJsonMsg(c, false, "用户名或密码错误")
|
||||
return
|
||||
} else {
|
||||
logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
|
||||
job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
|
||||
}
|
||||
|
||||
err = session.SetLoginUser(c, user)
|
||||
|
||||
@@ -27,12 +27,15 @@ type Pager struct {
|
||||
}
|
||||
|
||||
type AllSetting struct {
|
||||
WebListen string `json:"webListen" form:"webListen"`
|
||||
WebPort int `json:"webPort" form:"webPort"`
|
||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
||||
|
||||
WebListen string `json:"webListen" form:"webListen"`
|
||||
WebPort int `json:"webPort" form:"webPort"`
|
||||
WebCertFile string `json:"webCertFile" form:"webCertFile"`
|
||||
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`
|
||||
WebBasePath string `json:"webBasePath" form:"webBasePath"`
|
||||
TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"`
|
||||
TgBotToken string `json:"tgBotToken" form:"tgBotToken"`
|
||||
TgBotChatId int `json:"tgBotChatId" form:"tgBotChatId"`
|
||||
TgRunTime string `json:"tgRunTime" form:"tgRunTime"`
|
||||
XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
|
||||
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<a-icon type="link"></a-icon>
|
||||
<span>其他</span>
|
||||
</template>
|
||||
<a-menu-item key="https://github.com/sprov065/x-ui/">
|
||||
<a-menu-item key="https://github.com/vaxilu/x-ui/">
|
||||
<a-icon type="github"></a-icon>
|
||||
<span>Github</span>
|
||||
</a-menu-item>
|
||||
|
||||
@@ -14,6 +14,9 @@
|
||||
<template v-else-if="type === 'textarea'">
|
||||
<a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
|
||||
</template>
|
||||
<template v-else-if="type === 'switch'">
|
||||
<a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
|
||||
@@ -39,6 +39,19 @@
|
||||
</span>
|
||||
<a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<span slot="label">
|
||||
到期时间
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
留空则永不到期
|
||||
</template>
|
||||
<a-icon type="question-circle" theme="filled"></a-icon>
|
||||
</a-tooltip>
|
||||
</span>
|
||||
<a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
|
||||
v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vmess settings -->
|
||||
|
||||
@@ -3,5 +3,47 @@
|
||||
<a-form-item label="密码">
|
||||
<a-input v-model.trim="inbound.settings.clients[0].password"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="inbound.settings.clients[0].flow" style="width: 150px">
|
||||
<a-select-option value="">无</a-select-option>
|
||||
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="fallbacks">
|
||||
<a-row>
|
||||
<a-button type="primary" size="small"
|
||||
@click="inbound.settings.addTrojanFallback()">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- trojan fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
|
||||
<a-divider>
|
||||
fallback[[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
</a-divider>
|
||||
<a-form-item label="name">
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="alpn">
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="path">
|
||||
<a-input v-model="fallback.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="dest">
|
||||
<a-input v-model="fallback.dest"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="xver">
|
||||
<a-input type="number" v-model.number="fallback.xver"></a-input>
|
||||
</a-form-item>
|
||||
<a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -6,7 +6,7 @@
|
||||
<a-form-item v-if="inbound.xtls" label="flow">
|
||||
<a-select v-model="inbound.settings.vlesses[0].flow" style="width: 150px">
|
||||
<a-select-option value="">无</a-select-option>
|
||||
<a-select-option v-for="key in VLESS_FLOW" :value="key">[[ key ]]</a-select-option>
|
||||
<a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
@@ -1,6 +1,9 @@
|
||||
{{define "form/streamTCP"}}
|
||||
<!-- tcp type -->
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="acceptProxyProtocol">
|
||||
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
<a-form-item label="http 伪装">
|
||||
<a-switch
|
||||
:checked="inbound.stream.tcp.type === 'http'"
|
||||
|
||||
@@ -1,4 +1,9 @@
|
||||
{{define "form/streamWS"}}
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="acceptProxyProtocol">
|
||||
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-form layout="inline">
|
||||
<a-form-item label="路径">
|
||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||
|
||||
@@ -16,6 +16,9 @@
|
||||
<a-form-item label="域名">
|
||||
<a-input v-model.trim="inbound.stream.tls.server"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="alpn" placeholder="http/1.1,h2">
|
||||
<a-input v-model.trim="inbound.stream.tls.alpn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="证书">
|
||||
<a-radio-group v-model="inbound.stream.tls.certs[0].useFile"
|
||||
button-style="solid">
|
||||
@@ -42,4 +45,9 @@
|
||||
</a-form-item>
|
||||
</template>
|
||||
</a-form>
|
||||
<a-form layout="inline" v-else-if = "inbound.stream.network === 'tcp' ">
|
||||
<a-form-item label="tcp-acceptProxyProtocol">
|
||||
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -46,24 +46,44 @@
|
||||
<div slot="title">
|
||||
<a-button type="primary" icon="plus" @click="openAddInbound"></a-button>
|
||||
</div>
|
||||
<a-input v-model="searchKey" placeholder="search" autofocus style="max-width: 300px"></a-input>
|
||||
<!-- <a-input v-model="searchKey" placeholder="搜索" autofocus style="max-width: 300px"></a-input>-->
|
||||
<a-table :columns="columns" :row-key="dbInbound => dbInbound.id"
|
||||
:data-source="dbInbounds"
|
||||
:loading="spinning" :scroll="{ x: 1500 }"
|
||||
:pagination="false"
|
||||
style="margin-top: 20px"
|
||||
@change="() => getDBInbounds()">
|
||||
<template slot="action" slot-scope="text, dbInbound">
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a @click="e => e.preventDefault()">操作</a>
|
||||
<a-menu slot="overlay" @click="a => clickAction(a, dbInbound)">
|
||||
<a-menu-item v-if="dbInbound.hasLink()" key="qrcode">
|
||||
<a-icon type="qrcode"></a-icon>二维码
|
||||
</a-menu-item>
|
||||
<a-menu-item key="edit">
|
||||
<a-icon type="edit"></a-icon>编辑
|
||||
</a-menu-item>
|
||||
<a-menu-item key="resetTraffic">
|
||||
<a-icon type="retweet"></a-icon>重置流量
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delete">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon>删除
|
||||
</span>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</template>
|
||||
<template slot="protocol" slot-scope="text, dbInbound">
|
||||
<a-tag color="blue">[[ dbInbound.protocol ]]</a-tag>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, dbInbound">
|
||||
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]]</a-tag>
|
||||
<a-tag color="green">[[ sizeFormat(dbInbound.down) ]]</a-tag>
|
||||
<a-tag color="blue">[[ sizeFormat(dbInbound.up) ]] / [[ sizeFormat(dbInbound.down) ]]</a-tag>
|
||||
<template v-if="dbInbound.total > 0">
|
||||
<a-tag v-if="dbInbound.up + dbInbound.down < dbInbound.total" color="cyan">[[ sizeFormat(dbInbound.total) ]]</a-tag>
|
||||
<a-tag v-else color="red">[[ sizeFormat(dbInbound.total) ]]</a-tag>
|
||||
</template>
|
||||
<a-tag v-else color="cyan">无限制</a-tag>
|
||||
<a-tag v-else color="green">无限制</a-tag>
|
||||
</template>
|
||||
<template slot="settings" slot-scope="text, dbInbound">
|
||||
<a-button type="link" @click="showInfo(dbInbound)">查看</a-button>
|
||||
@@ -80,14 +100,15 @@
|
||||
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound)"></a-switch>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, dbInbound">
|
||||
<span v-if="dbInbound.expiryTime > 0" color="red">[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]</span>
|
||||
<span v-else>无限期</span>
|
||||
</template>
|
||||
<template slot="action" slot-scope="text, dbInbound">
|
||||
<a-button v-if="dbInbound.hasLink()" type="primary" icon="qrcode" @click="showQrcode(dbInbound)"></a-button>
|
||||
<a-button type="primary" icon="edit" @click="openEditInbound(dbInbound)"></a-button>
|
||||
<a-button icon="retweet" @click="resetTraffic(dbInbound)"></a-button>
|
||||
<a-button type="danger" icon="delete" @click="delInbound(dbInbound)"></a-button>
|
||||
<template v-if="dbInbound.expiryTime > 0">
|
||||
<a-tag v-if="dbInbound.isExpiry" color="red">
|
||||
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||
</a-tag>
|
||||
<a-tag v-else color="blue">
|
||||
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-tag v-else color="green">无限期</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-card>
|
||||
@@ -100,15 +121,25 @@
|
||||
<script>
|
||||
|
||||
const columns = [{
|
||||
title: "操作",
|
||||
align: 'center',
|
||||
width: 30,
|
||||
scopedSlots: { customRender: 'action' },
|
||||
}, {
|
||||
title: "启用",
|
||||
align: 'center',
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'enable' },
|
||||
}, {
|
||||
title: "id",
|
||||
align: 'center',
|
||||
dataIndex: "id",
|
||||
width: 60,
|
||||
width: 30,
|
||||
}, {
|
||||
title: "备注",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'remark' },
|
||||
width: 100,
|
||||
dataIndex: "remark",
|
||||
}, {
|
||||
title: "协议",
|
||||
align: 'center',
|
||||
@@ -122,12 +153,12 @@
|
||||
}, {
|
||||
title: "流量↑|↓",
|
||||
align: 'center',
|
||||
width: 80,
|
||||
width: 150,
|
||||
scopedSlots: { customRender: 'traffic' },
|
||||
}, {
|
||||
title: "详细信息",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
width: 40,
|
||||
scopedSlots: { customRender: 'settings' },
|
||||
}, {
|
||||
title: "传输配置",
|
||||
@@ -135,15 +166,10 @@
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'stream' },
|
||||
}, {
|
||||
title: "启用",
|
||||
title: "到期时间",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'enable' },
|
||||
}, {
|
||||
title: "操作",
|
||||
align: 'center',
|
||||
width: 60,
|
||||
scopedSlots: { customRender: 'action' },
|
||||
width: 80,
|
||||
scopedSlots: { customRender: 'expiryTime' },
|
||||
}];
|
||||
|
||||
const app = new Vue({
|
||||
@@ -190,6 +216,22 @@
|
||||
});
|
||||
}
|
||||
},
|
||||
clickAction(action, dbInbound) {
|
||||
switch (action.key) {
|
||||
case "qrcode":
|
||||
this.showQrcode(dbInbound);
|
||||
break;
|
||||
case "edit":
|
||||
this.openEditInbound(dbInbound);
|
||||
break;
|
||||
case "resetTraffic":
|
||||
this.resetTraffic(dbInbound);
|
||||
break;
|
||||
case "delete":
|
||||
this.delInbound(dbInbound);
|
||||
break;
|
||||
}
|
||||
},
|
||||
openAddInbound() {
|
||||
inModal.show({
|
||||
title: '添加入站',
|
||||
@@ -222,6 +264,7 @@
|
||||
total: dbInbound.total,
|
||||
remark: dbInbound.remark,
|
||||
enable: dbInbound.enable,
|
||||
expiryTime: dbInbound.expiryTime,
|
||||
|
||||
listen: inbound.listen,
|
||||
port: inbound.port,
|
||||
@@ -239,6 +282,7 @@
|
||||
total: dbInbound.total,
|
||||
remark: dbInbound.remark,
|
||||
enable: dbInbound.enable,
|
||||
expiryTime: dbInbound.expiryTime,
|
||||
|
||||
listen: inbound.listen,
|
||||
port: inbound.port,
|
||||
|
||||
@@ -188,8 +188,6 @@
|
||||
Object.freeze(State);
|
||||
|
||||
class CurTotal {
|
||||
current = 0
|
||||
total = 0
|
||||
|
||||
constructor(current, total) {
|
||||
this.current = current;
|
||||
@@ -216,19 +214,19 @@
|
||||
}
|
||||
|
||||
class Status {
|
||||
cpu = new CurTotal(0, 0);
|
||||
disk = new CurTotal(0, 0);
|
||||
loads = [0, 0, 0];
|
||||
mem = new CurTotal(0, 0);
|
||||
netIO = {up: 0, down: 0};
|
||||
netTraffic = {sent: 0, recv: 0};
|
||||
swap = new CurTotal(0, 0);
|
||||
tcpCount = 0;
|
||||
udpCount = 0;
|
||||
uptime = 0;
|
||||
xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
|
||||
|
||||
constructor(data) {
|
||||
this.cpu = new CurTotal(0, 0);
|
||||
this.disk = new CurTotal(0, 0);
|
||||
this.loads = [0, 0, 0];
|
||||
this.mem = new CurTotal(0, 0);
|
||||
this.netIO = {up: 0, down: 0};
|
||||
this.netTraffic = {sent: 0, recv: 0};
|
||||
this.swap = new CurTotal(0, 0);
|
||||
this.tcpCount = 0;
|
||||
this.udpCount = 0;
|
||||
this.uptime = 0;
|
||||
this.xray = {state: State.Stop, errorMsg: "", version: "", color: ""};
|
||||
|
||||
if (data == null) {
|
||||
return;
|
||||
}
|
||||
|
||||
@@ -71,7 +71,15 @@
|
||||
<setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件,重启面板生效" v-model="allSetting.xrayTemplateConfig"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="4" tab="其他设置">
|
||||
<a-tab-pane key="4" tab="TG提醒相关设置">
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<setting-list-item type="switch" title="启用电报机器人" desc="重启面板生效" v-model="allSetting.tgBotEnable"></setting-list-item>
|
||||
<setting-list-item type="text" title="电报机器人TOKEN" desc="重启面板生效" v-model="allSetting.tgBotToken"></setting-list-item>
|
||||
<setting-list-item type="number" title="电报机器人ChatId" desc="重启面板生效" v-model.number="allSetting.tgBotChatId"></setting-list-item>
|
||||
<setting-list-item type="text" title="电报机器人通知时间" desc="采用Crontab定时格式,重启面板生效" v-model="allSetting.tgRunTime"></setting-list-item>
|
||||
</a-list>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="5" tab="其他设置">
|
||||
<a-list item-layout="horizontal" style="background: white">
|
||||
<setting-list-item type="text" title="时区" desc="定时任务按照该时区的时间运行,重启面板生效" v-model="allSetting.timeLocation"></setting-list-item>
|
||||
</a-list>
|
||||
|
||||
25
web/job/check_inbound_job.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
)
|
||||
|
||||
type CheckInboundJob struct {
|
||||
xrayService service.XrayService
|
||||
inboundService service.InboundService
|
||||
}
|
||||
|
||||
func NewCheckInboundJob() *CheckInboundJob {
|
||||
return new(CheckInboundJob)
|
||||
}
|
||||
|
||||
func (j *CheckInboundJob) Run() {
|
||||
count, err := j.inboundService.DisableInvalidInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("disable invalid inbounds err:", err)
|
||||
} else if count > 0 {
|
||||
logger.Debugf("disabled %v inbounds", count)
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
25
web/job/check_xray_running_job.go
Normal file
@@ -0,0 +1,25 @@
|
||||
package job
|
||||
|
||||
import "x-ui/web/service"
|
||||
|
||||
type CheckXrayRunningJob struct {
|
||||
xrayService service.XrayService
|
||||
|
||||
checkTime int
|
||||
}
|
||||
|
||||
func NewCheckXrayRunningJob() *CheckXrayRunningJob {
|
||||
return new(CheckXrayRunningJob)
|
||||
}
|
||||
|
||||
func (j *CheckXrayRunningJob) Run() {
|
||||
if j.xrayService.IsXrayRunning() {
|
||||
j.checkTime = 0
|
||||
return
|
||||
}
|
||||
j.checkTime++
|
||||
if j.checkTime < 2 {
|
||||
return
|
||||
}
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
140
web/job/stats_notify_job.go
Normal file
@@ -0,0 +1,140 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"net"
|
||||
"os"
|
||||
|
||||
"time"
|
||||
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/service"
|
||||
|
||||
tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
|
||||
)
|
||||
|
||||
type LoginStatus byte
|
||||
|
||||
const (
|
||||
LoginSuccess LoginStatus = 1
|
||||
LoginFail LoginStatus = 0
|
||||
)
|
||||
|
||||
type StatsNotifyJob struct {
|
||||
enable bool
|
||||
xrayService service.XrayService
|
||||
inboundService service.InboundService
|
||||
settingService service.SettingService
|
||||
}
|
||||
|
||||
func NewStatsNotifyJob() *StatsNotifyJob {
|
||||
return new(StatsNotifyJob)
|
||||
}
|
||||
|
||||
func (j *StatsNotifyJob) SendMsgToTgbot(msg string) {
|
||||
//Telegram bot basic info
|
||||
tgBottoken, err := j.settingService.GetTgBotToken()
|
||||
if err != nil {
|
||||
logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
|
||||
return
|
||||
}
|
||||
tgBotid, err := j.settingService.GetTgBotChatId()
|
||||
if err != nil {
|
||||
logger.Warning("sendMsgToTgbot failed,GetTgBotChatId fail:", err)
|
||||
return
|
||||
}
|
||||
|
||||
bot, err := tgbotapi.NewBotAPI(tgBottoken)
|
||||
if err != nil {
|
||||
fmt.Println("get tgbot error:", err)
|
||||
return
|
||||
}
|
||||
bot.Debug = true
|
||||
fmt.Printf("Authorized on account %s", bot.Self.UserName)
|
||||
info := tgbotapi.NewMessage(int64(tgBotid), msg)
|
||||
//msg.ReplyToMessageID = int(tgBotid)
|
||||
bot.Send(info)
|
||||
}
|
||||
|
||||
//Here run is a interface method of Job interface
|
||||
func (j *StatsNotifyJob) Run() {
|
||||
if !j.xrayService.IsXrayRunning() {
|
||||
return
|
||||
}
|
||||
var info string
|
||||
//get hostname
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
fmt.Println("get hostname error:", err)
|
||||
return
|
||||
}
|
||||
info = fmt.Sprintf("主机名称:%s\r\n", name)
|
||||
//get ip address
|
||||
var ip string
|
||||
netInterfaces, err := net.Interfaces()
|
||||
if err != nil {
|
||||
fmt.Println("net.Interfaces failed, err:", err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
for i := 0; i < len(netInterfaces); i++ {
|
||||
if (netInterfaces[i].Flags & net.FlagUp) != 0 {
|
||||
addrs, _ := netInterfaces[i].Addrs()
|
||||
|
||||
for _, address := range addrs {
|
||||
if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
|
||||
if ipnet.IP.To4() != nil {
|
||||
ip = ipnet.IP.String()
|
||||
break
|
||||
} else {
|
||||
ip = ipnet.IP.String()
|
||||
break
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
info += fmt.Sprintf("IP地址:%s\r\n \r\n", ip)
|
||||
|
||||
//get traffic
|
||||
inbouds, err := j.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("StatsNotifyJob run failed:", err)
|
||||
return
|
||||
}
|
||||
//NOTE:If there no any sessions here,need to notify here
|
||||
//TODO:分节点推送,自动转化格式
|
||||
for _, inbound := range inbouds {
|
||||
info += fmt.Sprintf("节点名称:%s\r\n端口:%d\r\n上行流量↑:%s\r\n下行流量↓:%s\r\n总流量:%s\r\n", inbound.Remark, inbound.Port, common.FormatTraffic(inbound.Up), common.FormatTraffic(inbound.Down), common.FormatTraffic((inbound.Up + inbound.Down)))
|
||||
if inbound.ExpiryTime == 0 {
|
||||
info += fmt.Sprintf("到期时间:无限期\r\n \r\n")
|
||||
} else {
|
||||
info += fmt.Sprintf("到期时间:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
}
|
||||
j.SendMsgToTgbot(info)
|
||||
}
|
||||
|
||||
func (j *StatsNotifyJob) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
|
||||
if username == "" || ip == "" || time == "" {
|
||||
logger.Warning("UserLoginNotify failed,invalid info")
|
||||
return
|
||||
}
|
||||
var msg string
|
||||
//get hostname
|
||||
name, err := os.Hostname()
|
||||
if err != nil {
|
||||
fmt.Println("get hostname error:", err)
|
||||
return
|
||||
}
|
||||
if status == LoginSuccess {
|
||||
msg = fmt.Sprintf("面板登录成功提醒\r\n主机名称:%s\r\n", name)
|
||||
} else if status == LoginFail {
|
||||
msg = fmt.Sprintf("面板登录失败提醒\r\n主机名称:%s\r\n", name)
|
||||
}
|
||||
msg += fmt.Sprintf("时间:%s\r\n", time)
|
||||
msg += fmt.Sprintf("用户:%s\r\n", username)
|
||||
msg += fmt.Sprintf("IP:%s\r\n", ip)
|
||||
j.SendMsgToTgbot(msg)
|
||||
}
|
||||
30
web/job/xray_traffic_job.go
Normal file
@@ -0,0 +1,30 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"x-ui/logger"
|
||||
"x-ui/web/service"
|
||||
)
|
||||
|
||||
type XrayTrafficJob struct {
|
||||
xrayService service.XrayService
|
||||
inboundService service.InboundService
|
||||
}
|
||||
|
||||
func NewXrayTrafficJob() *XrayTrafficJob {
|
||||
return new(XrayTrafficJob)
|
||||
}
|
||||
|
||||
func (j *XrayTrafficJob) Run() {
|
||||
if !j.xrayService.IsXrayRunning() {
|
||||
return
|
||||
}
|
||||
traffics, err := j.xrayService.GetXrayTraffic()
|
||||
if err != nil {
|
||||
logger.Warning("get xray traffic failed:", err)
|
||||
return
|
||||
}
|
||||
err = j.inboundService.AddTraffic(traffics)
|
||||
if err != nil {
|
||||
logger.Warning("add traffic failed:", err)
|
||||
}
|
||||
}
|
||||
21
web/network/auto_https_listener.go
Normal file
@@ -0,0 +1,21 @@
|
||||
package network
|
||||
|
||||
import "net"
|
||||
|
||||
type AutoHttpsListener struct {
|
||||
net.Listener
|
||||
}
|
||||
|
||||
func NewAutoHttpsListener(listener net.Listener) net.Listener {
|
||||
return &AutoHttpsListener{
|
||||
Listener: listener,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *AutoHttpsListener) Accept() (net.Conn, error) {
|
||||
conn, err := l.Listener.Accept()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return NewAutoHttpsConn(conn), nil
|
||||
}
|
||||
67
web/network/autp_https_conn.go
Normal file
@@ -0,0 +1,67 @@
|
||||
package network
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"bytes"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/http"
|
||||
"sync"
|
||||
)
|
||||
|
||||
type AutoHttpsConn struct {
|
||||
net.Conn
|
||||
|
||||
firstBuf []byte
|
||||
bufStart int
|
||||
|
||||
readRequestOnce sync.Once
|
||||
}
|
||||
|
||||
func NewAutoHttpsConn(conn net.Conn) net.Conn {
|
||||
return &AutoHttpsConn{
|
||||
Conn: conn,
|
||||
}
|
||||
}
|
||||
|
||||
func (c *AutoHttpsConn) readRequest() bool {
|
||||
c.firstBuf = make([]byte, 2048)
|
||||
n, err := c.Conn.Read(c.firstBuf)
|
||||
c.firstBuf = c.firstBuf[:n]
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
reader := bytes.NewReader(c.firstBuf)
|
||||
bufReader := bufio.NewReader(reader)
|
||||
request, err := http.ReadRequest(bufReader)
|
||||
if err != nil {
|
||||
return false
|
||||
}
|
||||
resp := http.Response{
|
||||
Header: http.Header{},
|
||||
}
|
||||
resp.StatusCode = http.StatusTemporaryRedirect
|
||||
location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI)
|
||||
resp.Header.Set("Location", location)
|
||||
resp.Write(c.Conn)
|
||||
c.Close()
|
||||
c.firstBuf = nil
|
||||
return true
|
||||
}
|
||||
|
||||
func (c *AutoHttpsConn) Read(buf []byte) (int, error) {
|
||||
c.readRequestOnce.Do(func() {
|
||||
c.readRequest()
|
||||
})
|
||||
|
||||
if c.firstBuf != nil {
|
||||
n := copy(buf, c.firstBuf[c.bufStart:])
|
||||
c.bufStart += n
|
||||
if c.bufStart >= len(c.firstBuf) {
|
||||
c.firstBuf = nil
|
||||
}
|
||||
return n, nil
|
||||
}
|
||||
|
||||
return c.Conn.Read(buf)
|
||||
}
|
||||
@@ -2,11 +2,13 @@ package service
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"gorm.io/gorm"
|
||||
"time"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/util/common"
|
||||
"x-ui/xray"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type InboundService struct {
|
||||
@@ -166,8 +168,9 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
|
||||
|
||||
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
|
||||
db := database.GetDB()
|
||||
now := time.Now().Unix() * 1000
|
||||
result := db.Model(model.Inbound{}).
|
||||
Where("up + down >= total and total > 0 and enable = ?", true).
|
||||
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
|
||||
Update("enable", false)
|
||||
err := result.Error
|
||||
count := result.RowsAffected
|
||||
|
||||
@@ -18,6 +18,7 @@ import (
|
||||
"runtime"
|
||||
"time"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/sys"
|
||||
"x-ui/xray"
|
||||
)
|
||||
|
||||
@@ -142,18 +143,14 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||
logger.Warning("can not find io counters")
|
||||
}
|
||||
|
||||
tcpConnStats, err := net.Connections("tcp")
|
||||
status.TcpCount, err = sys.GetTCPCount()
|
||||
if err != nil {
|
||||
logger.Warning("get connections failed:", err)
|
||||
} else {
|
||||
status.TcpCount = len(tcpConnStats)
|
||||
logger.Warning("get tcp connections failed:", err)
|
||||
}
|
||||
|
||||
udpConnStats, err := net.Connections("udp")
|
||||
status.UdpCount, err = sys.GetUDPCount()
|
||||
if err != nil {
|
||||
logger.Warning("get connections failed:", err)
|
||||
} else {
|
||||
status.UdpCount = len(udpConnStats)
|
||||
logger.Warning("get udp connections failed:", err)
|
||||
}
|
||||
|
||||
if s.xrayService.IsXrayRunning() {
|
||||
@@ -265,7 +262,7 @@ func (s *ServerService) UpdateXray(version string) error {
|
||||
|
||||
s.xrayService.StopXray()
|
||||
defer func() {
|
||||
err := s.xrayService.RestartXray()
|
||||
err := s.xrayService.RestartXray(true)
|
||||
if err != nil {
|
||||
logger.Error("start xray failed:", err)
|
||||
}
|
||||
|
||||
@@ -29,6 +29,10 @@ var defaultValueMap = map[string]string{
|
||||
"secret": random.Seq(32),
|
||||
"webBasePath": "/",
|
||||
"timeLocation": "Asia/Shanghai",
|
||||
"tgBotEnable": "false",
|
||||
"tgBotToken": "",
|
||||
"tgBotChatId": "0",
|
||||
"tgRunTime": "",
|
||||
}
|
||||
|
||||
type SettingService struct {
|
||||
@@ -72,13 +76,15 @@ func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
|
||||
fieldV := v.FieldByName(field.Name)
|
||||
switch t := fieldV.Interface().(type) {
|
||||
case int:
|
||||
n, err := strconv.ParseInt(value, 10, 32)
|
||||
n, err := strconv.ParseInt(value, 10, 64)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
fieldV.SetInt(n)
|
||||
case string:
|
||||
fieldV.SetString(value)
|
||||
case bool:
|
||||
fieldV.SetBool(value == "true")
|
||||
default:
|
||||
return common.NewErrorf("unknown field %v type %v", key, t)
|
||||
}
|
||||
@@ -156,6 +162,18 @@ func (s *SettingService) setString(key string, value string) error {
|
||||
return s.saveSetting(key, value)
|
||||
}
|
||||
|
||||
func (s *SettingService) getBool(key string) (bool, error) {
|
||||
str, err := s.getString(key)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
return strconv.ParseBool(str)
|
||||
}
|
||||
|
||||
func (s *SettingService) setBool(key string, value bool) error {
|
||||
return s.setString(key, strconv.FormatBool(value))
|
||||
}
|
||||
|
||||
func (s *SettingService) getInt(key string) (int, error) {
|
||||
str, err := s.getString(key)
|
||||
if err != nil {
|
||||
@@ -176,6 +194,38 @@ func (s *SettingService) GetListen() (string, error) {
|
||||
return s.getString("webListen")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgBotToken() (string, error) {
|
||||
return s.getString("tgBotToken")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetTgBotToken(token string) error {
|
||||
return s.setString("tgBotToken", token)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgBotChatId() (int, error) {
|
||||
return s.getInt("tgBotChatId")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetTgBotChatId(chatId int) error {
|
||||
return s.setInt("tgBotChatId", chatId)
|
||||
}
|
||||
|
||||
func (s *SettingService) SetTgbotenabled(value bool) error {
|
||||
return s.setBool("tgBotEnable", value)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgbotenabled() (bool, error) {
|
||||
return s.getBool("tgBotEnable")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetTgbotRuntime(time string) error {
|
||||
return s.setString("tgRunTime", time)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetTgbotRuntime() (string, error) {
|
||||
return s.getString("tgRunTime")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetPort() (int, error) {
|
||||
return s.getInt("webPort")
|
||||
}
|
||||
|
||||
@@ -2,10 +2,11 @@ package service
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"gorm.io/gorm"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
|
||||
@@ -3,10 +3,11 @@ package service
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"go.uber.org/atomic"
|
||||
"sync"
|
||||
"x-ui/logger"
|
||||
"x-ui/xray"
|
||||
|
||||
"go.uber.org/atomic"
|
||||
)
|
||||
|
||||
var p *xray.Process
|
||||
@@ -84,18 +85,19 @@ func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, error) {
|
||||
return p.GetTraffic(true)
|
||||
}
|
||||
|
||||
func (s *XrayService) RestartXray() error {
|
||||
func (s *XrayService) RestartXray(isForce bool) error {
|
||||
lock.Lock()
|
||||
defer lock.Unlock()
|
||||
logger.Debug("restart xray")
|
||||
logger.Debug("restart xray, force:", isForce)
|
||||
|
||||
xrayConfig, err := s.GetXrayConfig()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if p != nil {
|
||||
if p.GetConfig().Equals(xrayConfig) {
|
||||
if p != nil && p.IsRunning() {
|
||||
if !isForce && p.GetConfig().Equals(xrayConfig) {
|
||||
logger.Debug("not need to restart xray")
|
||||
return nil
|
||||
}
|
||||
p.Stop()
|
||||
|
||||
98
web/web.go
@@ -4,13 +4,6 @@ import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"embed"
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/robfig/cron/v3"
|
||||
"golang.org/x/text/language"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
@@ -24,7 +17,17 @@ import (
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
"x-ui/web/controller"
|
||||
"x-ui/web/job"
|
||||
"x-ui/web/network"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/BurntSushi/toml"
|
||||
"github.com/gin-contrib/sessions"
|
||||
"github.com/gin-contrib/sessions/cookie"
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/nicksnyder/go-i18n/v2/i18n"
|
||||
"github.com/robfig/cron/v3"
|
||||
"golang.org/x/text/language"
|
||||
)
|
||||
|
||||
//go:embed assets/*
|
||||
@@ -277,56 +280,43 @@ func (s *Server) initI18n(engine *gin.Engine) error {
|
||||
}
|
||||
|
||||
func (s *Server) startTask() {
|
||||
err := s.xrayService.RestartXray()
|
||||
err := s.xrayService.RestartXray(true)
|
||||
if err != nil {
|
||||
logger.Warning("start xray failed:", err)
|
||||
}
|
||||
var checkTime = 0
|
||||
// 每 30 秒检查一次 xray 是否在运行
|
||||
s.cron.AddFunc("@every 30s", func() {
|
||||
if s.xrayService.IsXrayRunning() {
|
||||
checkTime = 0
|
||||
return
|
||||
}
|
||||
checkTime++
|
||||
if checkTime < 2 {
|
||||
return
|
||||
}
|
||||
s.xrayService.SetToNeedRestart()
|
||||
})
|
||||
s.cron.AddJob("@every 30s", job.NewCheckXrayRunningJob())
|
||||
|
||||
go func() {
|
||||
time.Sleep(time.Second * 5)
|
||||
// 每 10 秒统计一次流量,首次启动延迟 5 秒,与重启 xray 的时间错开
|
||||
s.cron.AddFunc("@every 10s", func() {
|
||||
if !s.xrayService.IsXrayRunning() {
|
||||
return
|
||||
}
|
||||
traffics, err := s.xrayService.GetXrayTraffic()
|
||||
if err != nil {
|
||||
logger.Warning("get xray traffic failed:", err)
|
||||
return
|
||||
}
|
||||
err = s.inboundService.AddTraffic(traffics)
|
||||
if err != nil {
|
||||
logger.Warning("add traffic failed:", err)
|
||||
}
|
||||
})
|
||||
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
|
||||
}()
|
||||
|
||||
// 每 30 秒检查一次 inbound 流量超出情况
|
||||
s.cron.AddFunc("@every 30s", func() {
|
||||
count, err := s.inboundService.DisableInvalidInbounds()
|
||||
if err != nil {
|
||||
logger.Warning("disable invalid inbounds err:", err)
|
||||
} else if count > 0 {
|
||||
logger.Debugf("disabled %v inbounds", count)
|
||||
s.xrayService.SetToNeedRestart()
|
||||
// 每 30 秒检查一次 inbound 流量超出和到期的情况
|
||||
s.cron.AddJob("@every 30s", job.NewCheckInboundJob())
|
||||
// 每一天提示一次流量情况,上海时间8点30
|
||||
var entry cron.EntryID
|
||||
isTgbotenabled, err := s.settingService.GetTgbotenabled()
|
||||
if (err == nil) && (isTgbotenabled) {
|
||||
runtime, err := s.settingService.GetTgbotRuntime()
|
||||
if err != nil || runtime == "" {
|
||||
logger.Errorf("Add NewStatsNotifyJob error[%s],Runtime[%s] invalid,wil run default", err, runtime)
|
||||
runtime = "@daily"
|
||||
}
|
||||
})
|
||||
logger.Infof("Tg notify enabled,run at %s", runtime)
|
||||
entry, err = s.cron.AddJob(runtime, job.NewStatsNotifyJob())
|
||||
if err != nil {
|
||||
logger.Warning("Add NewStatsNotifyJob error", err)
|
||||
return
|
||||
}
|
||||
} else {
|
||||
s.cron.Remove(entry)
|
||||
}
|
||||
}
|
||||
|
||||
func (s *Server) Start() (err error) {
|
||||
//这是一个匿名函数,没没有函数名
|
||||
defer func() {
|
||||
if err != nil {
|
||||
s.Stop()
|
||||
@@ -362,23 +352,23 @@ func (s *Server) Start() (err error) {
|
||||
return err
|
||||
}
|
||||
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
|
||||
var listener net.Listener
|
||||
listener, err := net.Listen("tcp", listenAddr)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if certFile != "" || keyFile != "" {
|
||||
var cert tls.Certificate
|
||||
cert, err = tls.LoadX509KeyPair(certFile, keyFile)
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
return err
|
||||
}
|
||||
c := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
listener, err = tls.Listen("tcp", listenAddr, c)
|
||||
} else {
|
||||
listener, err = net.Listen("tcp", listenAddr)
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
listener = network.NewAutoHttpsListener(listener)
|
||||
listener = tls.NewListener(listener, c)
|
||||
}
|
||||
|
||||
if certFile != "" || keyFile != "" {
|
||||
logger.Info("web server run https on", listener.Addr())
|
||||
} else {
|
||||
@@ -392,7 +382,9 @@ func (s *Server) Start() (err error) {
|
||||
Handler: engine,
|
||||
}
|
||||
|
||||
go s.httpServer.Serve(listener)
|
||||
go func() {
|
||||
s.httpServer.Serve(listener)
|
||||
}()
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
292
x-ui.sh
@@ -5,8 +5,20 @@ green='\033[0;32m'
|
||||
yellow='\033[0;33m'
|
||||
plain='\033[0m'
|
||||
|
||||
#Add some basic function here
|
||||
function LOGD() {
|
||||
echo -e "${yellow}[DEG] $* ${plain}"
|
||||
}
|
||||
|
||||
function LOGE() {
|
||||
echo -e "${red}[ERR] $* ${plain}"
|
||||
}
|
||||
|
||||
function LOGI() {
|
||||
echo -e "${green}[INF] $* ${plain}"
|
||||
}
|
||||
# check root
|
||||
[[ $EUID -ne 0 ]] && echo -e "${red}错误: ${plain} 必须使用root用户运行此脚本!\n" && exit 1
|
||||
[[ $EUID -ne 0 ]] && LOGE "错误: 必须使用root用户运行此脚本!\n" && exit 1
|
||||
|
||||
# check os
|
||||
if [[ -f /etc/redhat-release ]]; then
|
||||
@@ -24,7 +36,7 @@ elif cat /proc/version | grep -Eqi "ubuntu"; then
|
||||
elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then
|
||||
release="centos"
|
||||
else
|
||||
echo -e "${red}未检测到系统版本,请联系脚本作者!${plain}\n" && exit 1
|
||||
LOGE "未检测到系统版本,请联系脚本作者!\n" && exit 1
|
||||
fi
|
||||
|
||||
os_version=""
|
||||
@@ -39,15 +51,15 @@ fi
|
||||
|
||||
if [[ x"${release}" == x"centos" ]]; then
|
||||
if [[ ${os_version} -le 6 ]]; then
|
||||
echo -e "${red}请使用 CentOS 7 或更高版本的系统!${plain}\n" && exit 1
|
||||
LOGE "请使用 CentOS 7 或更高版本的系统!\n" && exit 1
|
||||
fi
|
||||
elif [[ x"${release}" == x"ubuntu" ]]; then
|
||||
if [[ ${os_version} -lt 16 ]]; then
|
||||
echo -e "${red}请使用 Ubuntu 16 或更高版本的系统!${plain}\n" && exit 1
|
||||
LOGE "请使用 Ubuntu 16 或更高版本的系统!\n" && exit 1
|
||||
fi
|
||||
elif [[ x"${release}" == x"debian" ]]; then
|
||||
if [[ ${os_version} -lt 8 ]]; then
|
||||
echo -e "${red}请使用 Debian 8 或更高版本的系统!${plain}\n" && exit 1
|
||||
LOGE "请使用 Debian 8 或更高版本的系统!\n" && exit 1
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -82,7 +94,7 @@ before_show_menu() {
|
||||
}
|
||||
|
||||
install() {
|
||||
bash <(curl -Ls https://blog.sprov.xyz/x-ui.sh)
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)
|
||||
if [[ $? == 0 ]]; then
|
||||
if [[ $# == 0 ]]; then
|
||||
start
|
||||
@@ -95,21 +107,21 @@ install() {
|
||||
update() {
|
||||
confirm "本功能会强制重装当前最新版,数据不会丢失,是否继续?" "n"
|
||||
if [[ $? != 0 ]]; then
|
||||
echo -e "${red}已取消${plain}"
|
||||
LOGE "已取消"
|
||||
if [[ $# == 0 ]]; then
|
||||
before_show_menu
|
||||
fi
|
||||
return 0
|
||||
fi
|
||||
bash <(curl -Ls https://blog.sprov.xyz/x-ui.sh)
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)
|
||||
if [[ $? == 0 ]]; then
|
||||
echo -e "${green}更新完成,已自动重启面板${plain}"
|
||||
LOGI "更新完成,已自动重启面板 "
|
||||
exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
confirm "确定要卸载面板吗,xray 也会卸载?" "n"
|
||||
confirm "确定要卸载面板吗,xray 也会卸载?" "n"
|
||||
if [[ $? != 0 ]]; then
|
||||
if [[ $# == 0 ]]; then
|
||||
show_menu
|
||||
@@ -127,9 +139,6 @@ uninstall() {
|
||||
echo ""
|
||||
echo -e "卸载成功,如果你想删除此脚本,则退出脚本后运行 ${green}rm /usr/bin/x-ui -f${plain} 进行删除"
|
||||
echo ""
|
||||
echo -e "Telegram 群组: ${green}https://t.me/sprov_blog${plain}"
|
||||
echo -e "Github issues: ${green}https://github.com/sprov065/x-ui/issues${plain}"
|
||||
echo -e "博客: ${green}https://blog.sprov.xyz/x-ui${plain}"
|
||||
|
||||
if [[ $# == 0 ]]; then
|
||||
before_show_menu
|
||||
@@ -162,10 +171,19 @@ reset_config() {
|
||||
confirm_restart
|
||||
}
|
||||
|
||||
check_config() {
|
||||
info=$(/usr/local/x-ui/x-ui setting -show true)
|
||||
if [[ $? != 0 ]]; then
|
||||
LOGE "get current settings error,please check logs"
|
||||
show_menu
|
||||
fi
|
||||
LOGI "${info}"
|
||||
}
|
||||
|
||||
set_port() {
|
||||
echo && echo -n -e "输入端口号[1-65535]: " && read port
|
||||
if [[ -z "${port}" ]]; then
|
||||
echo -e "${yellow}已取消${plain}"
|
||||
LOGD "已取消"
|
||||
before_show_menu
|
||||
else
|
||||
/usr/local/x-ui/x-ui setting -port ${port}
|
||||
@@ -178,15 +196,15 @@ start() {
|
||||
check_status
|
||||
if [[ $? == 0 ]]; then
|
||||
echo ""
|
||||
echo -e "${green}面板已运行,无需再次启动,如需重启请选择重启${plain}"
|
||||
LOGI "面板已运行,无需再次启动,如需重启请选择重启"
|
||||
else
|
||||
systemctl start x-ui
|
||||
sleep 2
|
||||
check_status
|
||||
if [[ $? == 0 ]]; then
|
||||
echo -e "${green}x-ui 启动成功${plain}"
|
||||
LOGI "x-ui 启动成功"
|
||||
else
|
||||
echo -e "${red}面板启动失败,可能是因为启动时间超过了两秒,请稍后查看日志信息${plain}"
|
||||
LOGE "面板启动失败,可能是因为启动时间超过了两秒,请稍后查看日志信息"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -199,15 +217,15 @@ stop() {
|
||||
check_status
|
||||
if [[ $? == 1 ]]; then
|
||||
echo ""
|
||||
echo -e "${green}面板已停止,无需再次停止${plain}"
|
||||
LOGI "面板已停止,无需再次停止"
|
||||
else
|
||||
systemctl stop x-ui
|
||||
sleep 2
|
||||
check_status
|
||||
if [[ $? == 1 ]]; then
|
||||
echo -e "${green}x-ui 与 xray 停止成功${plain}"
|
||||
LOGI "x-ui 与 xray 停止成功"
|
||||
else
|
||||
echo -e "${red}面板停止失败,可能是因为停止时间超过了两秒,请稍后查看日志信息${plain}"
|
||||
LOGE "面板停止失败,可能是因为停止时间超过了两秒,请稍后查看日志信息"
|
||||
fi
|
||||
fi
|
||||
|
||||
@@ -221,9 +239,9 @@ restart() {
|
||||
sleep 2
|
||||
check_status
|
||||
if [[ $? == 0 ]]; then
|
||||
echo -e "${green}x-ui 与 xray 重启成功${plain}"
|
||||
LOGI "x-ui 与 xray 重启成功"
|
||||
else
|
||||
echo -e "${red}面板重启失败,可能是因为启动时间超过了两秒,请稍后查看日志信息${plain}"
|
||||
LOGE "面板重启失败,可能是因为启动时间超过了两秒,请稍后查看日志信息"
|
||||
fi
|
||||
if [[ $# == 0 ]]; then
|
||||
before_show_menu
|
||||
@@ -240,9 +258,9 @@ status() {
|
||||
enable() {
|
||||
systemctl enable x-ui
|
||||
if [[ $? == 0 ]]; then
|
||||
echo -e "${green}x-ui 设置开机自启成功${plain}"
|
||||
LOGI "x-ui 设置开机自启成功"
|
||||
else
|
||||
echo -e "${red}x-ui 设置开机自启失败${plain}"
|
||||
LOGE "x-ui 设置开机自启失败"
|
||||
fi
|
||||
|
||||
if [[ $# == 0 ]]; then
|
||||
@@ -253,9 +271,9 @@ enable() {
|
||||
disable() {
|
||||
systemctl disable x-ui
|
||||
if [[ $? == 0 ]]; then
|
||||
echo -e "${green}x-ui 取消开机自启成功${plain}"
|
||||
LOGI "x-ui 取消开机自启成功"
|
||||
else
|
||||
echo -e "${red}x-ui 取消开机自启失败${plain}"
|
||||
LOGE "x-ui 取消开机自启失败"
|
||||
fi
|
||||
|
||||
if [[ $# == 0 ]]; then
|
||||
@@ -277,20 +295,21 @@ migrate_v2_ui() {
|
||||
}
|
||||
|
||||
install_bbr() {
|
||||
bash <(curl -L -s https://raw.githubusercontent.com/sprov065/blog/master/bbr.sh)
|
||||
# temporary workaround for installing bbr
|
||||
bash <(curl -L -s https://raw.githubusercontent.com/teddysun/across/master/bbr.sh)
|
||||
echo ""
|
||||
before_show_menu
|
||||
}
|
||||
|
||||
update_shell() {
|
||||
wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/sprov065/x-ui/raw/master/x-ui.sh
|
||||
wget -O /usr/bin/x-ui -N --no-check-certificate https://github.com/vaxilu/x-ui/raw/master/x-ui.sh
|
||||
if [[ $? != 0 ]]; then
|
||||
echo ""
|
||||
echo -e "${red}下载脚本失败,请检查本机能否连接 Github${plain}"
|
||||
LOGE "下载脚本失败,请检查本机能否连接 Github"
|
||||
before_show_menu
|
||||
else
|
||||
chmod +x /usr/bin/x-ui
|
||||
echo -e "${green}升级脚本成功,请重新运行脚本${plain}" && exit 0
|
||||
LOGI "升级脚本成功,请重新运行脚本" && exit 0
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -312,7 +331,7 @@ check_enabled() {
|
||||
if [[ x"${temp}" == x"enabled" ]]; then
|
||||
return 0
|
||||
else
|
||||
return 1;
|
||||
return 1
|
||||
fi
|
||||
}
|
||||
|
||||
@@ -320,7 +339,7 @@ check_uninstall() {
|
||||
check_status
|
||||
if [[ $? != 2 ]]; then
|
||||
echo ""
|
||||
echo -e "${red}面板已安装,请不要重复安装${plain}"
|
||||
LOGE "面板已安装,请不要重复安装"
|
||||
if [[ $# == 0 ]]; then
|
||||
before_show_menu
|
||||
fi
|
||||
@@ -334,7 +353,7 @@ check_install() {
|
||||
check_status
|
||||
if [[ $? == 2 ]]; then
|
||||
echo ""
|
||||
echo -e "${red}请先安装面板${plain}"
|
||||
LOGE "请先安装面板"
|
||||
if [[ $# == 0 ]]; then
|
||||
before_show_menu
|
||||
fi
|
||||
@@ -347,16 +366,17 @@ check_install() {
|
||||
show_status() {
|
||||
check_status
|
||||
case $? in
|
||||
0)
|
||||
echo -e "面板状态: ${green}已运行${plain}"
|
||||
show_enable_status
|
||||
;;
|
||||
1)
|
||||
echo -e "面板状态: ${yellow}未运行${plain}"
|
||||
show_enable_status
|
||||
;;
|
||||
2)
|
||||
echo -e "面板状态: ${red}未安装${plain}"
|
||||
0)
|
||||
echo -e "面板状态: ${green}已运行${plain}"
|
||||
show_enable_status
|
||||
;;
|
||||
1)
|
||||
echo -e "面板状态: ${yellow}未运行${plain}"
|
||||
show_enable_status
|
||||
;;
|
||||
2)
|
||||
echo -e "面板状态: ${red}未安装${plain}"
|
||||
;;
|
||||
esac
|
||||
show_xray_status
|
||||
}
|
||||
@@ -388,6 +408,81 @@ show_xray_status() {
|
||||
fi
|
||||
}
|
||||
|
||||
ssl_cert_issue() {
|
||||
echo -E ""
|
||||
LOGD "******使用说明******"
|
||||
LOGI "该脚本将使用Acme脚本申请证书,使用时需保证:"
|
||||
LOGI "1.知晓Cloudflare 注册邮箱"
|
||||
LOGI "2.知晓Cloudflare Global API Key"
|
||||
LOGI "3.域名已通过Cloudflare进行解析到当前服务器"
|
||||
LOGI "4.该脚本申请证书默认安装路径为/root/cert目录"
|
||||
confirm "我已确认以上内容[y/n]" "y"
|
||||
if [ $? -eq 0 ]; then
|
||||
cd ~
|
||||
LOGI "安装Acme脚本"
|
||||
curl https://get.acme.sh | sh
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "安装acme脚本失败"
|
||||
exit 1
|
||||
fi
|
||||
CF_Domain=""
|
||||
CF_GlobalKey=""
|
||||
CF_AccountEmail=""
|
||||
certPath=/root/cert
|
||||
if [ ! -d "$certPath" ]; then
|
||||
mkdir $certPath
|
||||
else
|
||||
rm -rf $certPath
|
||||
mkdir $certPath
|
||||
fi
|
||||
LOGD "请设置域名:"
|
||||
read -p "Input your domain here:" CF_Domain
|
||||
LOGD "你的域名设置为:${CF_Domain}"
|
||||
LOGD "请设置API密钥:"
|
||||
read -p "Input your key here:" CF_GlobalKey
|
||||
LOGD "你的API密钥为:${CF_GlobalKey}"
|
||||
LOGD "请设置注册邮箱:"
|
||||
read -p "Input your email here:" CF_AccountEmail
|
||||
LOGD "你的注册邮箱为:${CF_AccountEmail}"
|
||||
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "修改默认CA为Lets'Encrypt失败,脚本退出"
|
||||
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 "证书签发失败,脚本退出"
|
||||
exit 1
|
||||
else
|
||||
LOGI "证书签发成功,安装中..."
|
||||
fi
|
||||
~/.acme.sh/acme.sh --installcert -d ${CF_Domain} -d *.${CF_Domain} --ca-file /root/cert/ca.cer \
|
||||
--cert-file /root/cert/${CF_Domain}.cer --key-file /root/cert/${CF_Domain}.key \
|
||||
--fullchain-file /root/cert/fullchain.cer
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "证书安装失败,脚本退出"
|
||||
exit 1
|
||||
else
|
||||
LOGI "证书安装成功,开启自动更新..."
|
||||
fi
|
||||
~/.acme.sh/acme.sh --upgrade --auto-upgrade
|
||||
if [ $? -ne 0 ]; then
|
||||
LOGE "自动更新设置失败,脚本退出"
|
||||
ls -lah cert
|
||||
chmod 755 $certPath
|
||||
exit 1
|
||||
else
|
||||
LOGI "证书已安装且已开启自动更新,具体信息如下"
|
||||
ls -lah cert
|
||||
chmod 755 $certPath
|
||||
fi
|
||||
else
|
||||
show_menu
|
||||
fi
|
||||
}
|
||||
|
||||
show_usage() {
|
||||
echo "x-ui 管理脚本使用方法: "
|
||||
echo "------------------------------------------"
|
||||
@@ -409,7 +504,6 @@ show_usage() {
|
||||
show_menu() {
|
||||
echo -e "
|
||||
${green}x-ui 面板管理脚本${plain}
|
||||
--- https://blog.sprov.xyz/x-ui ---
|
||||
${green}0.${plain} 退出脚本
|
||||
————————————————
|
||||
${green}1.${plain} 安装 x-ui
|
||||
@@ -419,83 +513,117 @@ show_menu() {
|
||||
${green}4.${plain} 重置用户名密码
|
||||
${green}5.${plain} 重置面板设置
|
||||
${green}6.${plain} 设置面板端口
|
||||
${green}7.${plain} 查看当前面板设置
|
||||
————————————————
|
||||
${green}7.${plain} 启动 x-ui
|
||||
${green}8.${plain} 停止 x-ui
|
||||
${green}9.${plain} 重启 x-ui
|
||||
${green}10.${plain} 查看 x-ui 状态
|
||||
${green}11.${plain} 查看 x-ui 日志
|
||||
${green}8.${plain} 启动 x-ui
|
||||
${green}9.${plain} 停止 x-ui
|
||||
${green}10.${plain} 重启 x-ui
|
||||
${green}11.${plain} 查看 x-ui 状态
|
||||
${green}12.${plain} 查看 x-ui 日志
|
||||
————————————————
|
||||
${green}12.${plain} 设置 x-ui 开机自启
|
||||
${green}13.${plain} 取消 x-ui 开机自启
|
||||
${green}13.${plain} 设置 x-ui 开机自启
|
||||
${green}14.${plain} 取消 x-ui 开机自启
|
||||
————————————————
|
||||
${green}14.${plain} 一键安装 bbr (最新内核)
|
||||
${green}15.${plain} 一键安装 bbr (最新内核)
|
||||
${green}16.${plain} 一键申请SSL证书(acme申请)
|
||||
"
|
||||
show_status
|
||||
echo && read -p "请输入选择 [0-14]: " num
|
||||
echo && read -p "请输入选择 [0-16]: " num
|
||||
|
||||
case "${num}" in
|
||||
0) exit 0
|
||||
0)
|
||||
exit 0
|
||||
;;
|
||||
1) check_uninstall && install
|
||||
1)
|
||||
check_uninstall && install
|
||||
;;
|
||||
2) check_install && update
|
||||
2)
|
||||
check_install && update
|
||||
;;
|
||||
3) check_install && uninstall
|
||||
3)
|
||||
check_install && uninstall
|
||||
;;
|
||||
4) check_install && reset_user
|
||||
4)
|
||||
check_install && reset_user
|
||||
;;
|
||||
5) check_install && reset_config
|
||||
5)
|
||||
check_install && reset_config
|
||||
;;
|
||||
6) check_install && set_port
|
||||
6)
|
||||
check_install && set_port
|
||||
;;
|
||||
7) check_install && start
|
||||
7)
|
||||
check_install && check_config
|
||||
;;
|
||||
8) check_install && stop
|
||||
8)
|
||||
check_install && start
|
||||
;;
|
||||
9) check_install && restart
|
||||
9)
|
||||
check_install && stop
|
||||
;;
|
||||
10) check_install && status
|
||||
10)
|
||||
check_install && restart
|
||||
;;
|
||||
11) check_install && show_log
|
||||
11)
|
||||
check_install && status
|
||||
;;
|
||||
12) check_install && enable
|
||||
12)
|
||||
check_install && show_log
|
||||
;;
|
||||
13) check_install && disable
|
||||
13)
|
||||
check_install && enable
|
||||
;;
|
||||
14) install_bbr
|
||||
14)
|
||||
check_install && disable
|
||||
;;
|
||||
*) echo -e "${red}请输入正确的数字 [0-14]${plain}"
|
||||
15)
|
||||
install_bbr
|
||||
;;
|
||||
16)
|
||||
ssl_cert_issue
|
||||
;;
|
||||
*)
|
||||
LOGE "请输入正确的数字 [0-16]"
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
||||
if [[ $# > 0 ]]; then
|
||||
case $1 in
|
||||
"start") check_install 0 && start 0
|
||||
"start")
|
||||
check_install 0 && start 0
|
||||
;;
|
||||
"stop") check_install 0 && stop 0
|
||||
"stop")
|
||||
check_install 0 && stop 0
|
||||
;;
|
||||
"restart") check_install 0 && restart 0
|
||||
"restart")
|
||||
check_install 0 && restart 0
|
||||
;;
|
||||
"status") check_install 0 && status 0
|
||||
"status")
|
||||
check_install 0 && status 0
|
||||
;;
|
||||
"enable") check_install 0 && enable 0
|
||||
"enable")
|
||||
check_install 0 && enable 0
|
||||
;;
|
||||
"disable") check_install 0 && disable 0
|
||||
"disable")
|
||||
check_install 0 && disable 0
|
||||
;;
|
||||
"log") check_install 0 && show_log 0
|
||||
"log")
|
||||
check_install 0 && show_log 0
|
||||
;;
|
||||
"log") check_install 0 && migrate_v2_ui 0
|
||||
"v2-ui")
|
||||
check_install 0 && migrate_v2_ui 0
|
||||
;;
|
||||
"update") check_install 0 && update 0
|
||||
"update")
|
||||
check_install 0 && update 0
|
||||
;;
|
||||
"install") check_uninstall 0 && install 0
|
||||
"install")
|
||||
check_uninstall 0 && install 0
|
||||
;;
|
||||
"uninstall") check_install 0 && uninstall 0
|
||||
"uninstall")
|
||||
check_install 0 && uninstall 0
|
||||
;;
|
||||
*) show_usage
|
||||
*) show_usage ;;
|
||||
esac
|
||||
else
|
||||
show_menu
|
||||
|
||||
@@ -7,9 +7,6 @@ import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"github.com/Workiva/go-datastructures/queue"
|
||||
statsservice "github.com/xtls/xray-core/app/stats/command"
|
||||
"google.golang.org/grpc"
|
||||
"io/fs"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -18,6 +15,10 @@ import (
|
||||
"strings"
|
||||
"time"
|
||||
"x-ui/util/common"
|
||||
|
||||
"github.com/Workiva/go-datastructures/queue"
|
||||
statsservice "github.com/xtls/xray-core/app/stats/command"
|
||||
"google.golang.org/grpc"
|
||||
)
|
||||
|
||||
var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
|
||||
|
||||