73 Commits
0.0.1 ... main

Author SHA1 Message Date
vaxilu
9c1be8c57a Merge pull request #165 from KerryJi/feature/dev
fix fallback bug
2022-04-28 14:32:31 +08:00
kerry
e5d08bb358 add alpn into tlsSetting, fix fallback bug 2022-04-28 14:27:28 +08:00
kerry
77d844460a fix fallback bug 2022-04-27 23:27:32 +08:00
vaxilu
d3507286d6 Merge pull request #163 from FranzKafkaYu/develop
优化Tg提醒功能
2022-04-25 08:53:04 +08:00
FranzKafkayu
b616a5b138 1.optimize Tg bot notify info
2.fixed a bug when ParseIn twith  Tg bot chat id
2022-04-22 23:55:25 +08:00
vaxilu
178716025f Merge pull request #162 from FranzKafkaYu/develop
Delete Tg Bot Control Part in Shell Scripts
2022-04-20 22:00:55 +08:00
FranzKafkayu
580dca211e Delete Tg Bot Control Part in Shell Scripts 2022-04-20 21:54:19 +08:00
vaxilu
c06ccbac19 Merge branch 'main' of https://github.com/vaxilu/x-ui into main 2022-04-19 12:12:57 +08:00
vaxilu
dfc6b56327 网页端设置电报机器人开关 2022-04-19 12:12:37 +08:00
vaxilu
91c4cefb1c Update README.md 2022-04-19 11:38:13 +08:00
vaxilu
04ce724801 Merge pull request #161 from FranzKafkaYu/develop
Update Tg bot related function
2022-04-19 11:33:56 +08:00
vaxilu
bdc0fd76bd Update README.md 2022-04-17 14:42:58 +08:00
FranzKafkayu
72e206879a Update Tg bot related function 2022-04-17 11:21:45 +08:00
vaxilu
8791c8729c Update README.md 2022-04-14 10:29:27 +08:00
vaxilu
57363ee778 Merge pull request #157 from misakano7545/main
增加Actions CI自动编译,以及增加对s390x架构的VPS支持
2022-04-12 21:18:58 +08:00
Misaka No
509f3641e4 Update release.yml 2022-04-09 16:19:03 +08:00
Misaka No
9fef61b813 Update release.yml 2022-04-09 14:31:18 +08:00
Misaka No
6428dee149 Update release.yml 2022-04-09 14:30:20 +08:00
Misaka No
ebb9ee5827 Update release.yml 2022-04-09 11:48:26 +08:00
Misaka No
534cc5d5a2 Update release.yml 2022-04-09 11:38:04 +08:00
Misaka No
8373c5eb38 Update release.yml 2022-04-09 11:36:39 +08:00
Misaka No
fd5cfd7853 Update install.sh 2022-04-09 11:21:35 +08:00
Misaka No
cf10ff67e3 Create release.yml 2022-04-09 11:21:01 +08:00
vaxilu
97020a82bc Update x-ui.sh 2022-03-13 09:41:25 +08:00
vaxilu
60ff4908cf Merge pull request #148 from FranzKafkaYu/main
add ssl issue and require config after install to avoid default config
2022-03-13 09:36:41 +08:00
FranzKafkaYu
b99c0fe230 1.repair url in scripts 2022-03-12 03:18:20 -08:00
FranzKafkaYu
f0badd3d96 1.modified install.sh file format to unix 2022-03-12 03:01:58 -08:00
FranzKafkaYu
98e050ef77 1.require config after install first time to avoid default config
2.add ssl cert issue in x-ui.sh
2022-03-12 02:47:52 -08:00
vaxilu
5d86891162 Update README.md 2021-12-05 15:57:40 +08:00
vaxilu
fe61444da2 Merge pull request #138 from Chasing66/main
Add docker version
2021-12-05 15:53:40 +08:00
LuckyHunter
9d76f90d85 Add docker version 2021-12-05 15:42:43 +08:00
vaxilu
a7d35ba5c5 Update README.md 2021-11-22 20:05:30 +08:00
vaxilu
21b82dceb2 Update README.md 2021-08-25 17:56:57 +08:00
vaxilu
0b6b73f895 Update install.sh 2021-08-25 17:50:58 +08:00
vaxilu
f335c8d39a Update version 2021-08-25 17:16:19 +08:00
vaxilu
36d9a43421 Update common_sider.html 2021-08-25 17:06:23 +08:00
vaxilu
5d118afb07 Update x-ui.sh 2021-08-25 16:59:11 +08:00
vaxilu
56c30d4d0c Merge pull request #74 from jukrb0x/patch-1
fix: workaround for installing bbr
2021-08-25 16:55:31 +08:00
sprov
4c4a70e742 fix vless link 2021-08-25 09:42:20 +08:00
vaxilu
581e3b835a Merge pull request #64 from GSWXXN/main
add fallbacks feature & flow control mode for trojan
2021-08-25 08:46:27 +08:00
Jabriel
ccfeb28492 fix: workaround for installing bbr
The former link for installing bbr is not found anymore.
2021-08-25 04:21:28 +08:00
GSWXXN
100bd29c92 add flow control mode for trojan
Signed-off-by: GSWXXN <819269088@qq.com>
2021-08-22 16:20:22 +08:00
GSWXXN
6ed818cd67 add fallbacks feature for trojan
Signed-off-by: GSWXXN <819269088@qq.com>
2021-08-21 01:06:01 +08:00
vaxilu
698bf1d390 Merge pull request #60 from tshipenchko/patch-1
Change sprov065 to vaxilu
2021-08-17 19:52:04 +08:00
Łukasz Tshipenchko
aeed8db419 Change sprov065 to vaxilu 2021-08-15 14:55:17 +06:00
vaxilu
10191887af Update README.md 2021-08-10 14:24:53 +08:00
sprov
b240b843c8 Update version
0.3.1
2021-07-26 22:02:35 +08:00
sprov
500b08b112 Update inbound.go
fix expiry time
2021-07-26 22:02:08 +08:00
sprov
64c5ecce99 Update install.sh 2021-07-26 14:15:34 +08:00
sprov
3e08b794be Update README.md 2021-07-26 14:07:25 +08:00
sprov
e473b7393e Update README.md 2021-07-26 14:07:03 +08:00
sprov
942ac7f562 Update README.md 2021-07-26 13:41:38 +08:00
sprov
292d5b89d4 0.3.0
- 增加到期时间限制
 - 新增配置面板 https 访问后,http 自动跳转 https(同端口)
 - 降低获取系统连接数的 cpu 使用率
 - 优化界面
 - VMess 协议 alterId 默认改为 0
 - 修复旧版本 iOS 系统白屏问题
 - 修复重启面板后 xray 没有启动的问题
2021-07-26 13:29:29 +08:00
sprov
f1057b1142 Update issue templates 2021-06-24 12:47:43 +08:00
sprov
b8da50dde7 Update issue templates 2021-06-24 12:47:16 +08:00
sprov
991e711ac1 Update issue templates 2021-06-24 12:46:00 +08:00
sprov
1468984c92 修复inbounds备注不显示 2021-06-17 12:00:08 +08:00
sprov
e8b0d2c839 Merge branch 'main' of https://github.com/sprov065/x-ui into main 2021-06-17 11:53:35 +08:00
sprov
00f9de6f1c Update x-ui.sh 2021-06-17 11:53:26 +08:00
sprov
51f80b445d Update README.md 2021-06-17 11:19:51 +08:00
sprov
4bf10cd8b7 Merge branch 'main' of https://github.com/sprov065/x-ui into main 2021-06-17 11:15:12 +08:00
sprov
a02f03f5d6 Update x-ui.sh 2021-06-17 11:15:00 +08:00
sprov
7a660506ea Update README.md 2021-06-17 11:14:04 +08:00
sprov
f0356ad786 Update README.md 2021-06-17 11:11:56 +08:00
sprov
89677c4fe1 0.2.0
- 优化 ui 界面
 - 优化网站加载速度
 - 新增从 v2-ui 迁移账号数据的功能
2021-06-17 11:05:43 +08:00
sprov
d67dff5a4c Update README.md 2021-06-15 11:11:32 +08:00
sprov
e91daabb18 0.1.0
- 改进 ui 界面
 - 修复流量超出后账号不自动失效问题
 - 修复vless生成的链接不正确问题
 - 修复网页端重启面板功能问题
2021-06-15 11:10:39 +08:00
sprov
5cc4cf02ee Update README.md 2021-06-12 11:49:05 +08:00
sprov
1444a00630 Update README.md 2021-06-12 11:44:10 +08:00
sprov
ae4ea3e091 Update README.md 2021-06-12 11:39:34 +08:00
sprov
214b217f12 0.0.2
- 增加设置总流量功能,流量超出后自动禁用
 - 优化部分 ui 细节
 - 修复监听 ip 不为空导致无法启动 xray 的问题
 - 修复二维码链接没有包含 address 的问题
2021-06-12 11:26:35 +08:00
sprov
f6eb413597 fix inbound listen 2021-06-06 23:52:03 +08:00
sprov
e5788c6d8e install.sh 2021-06-06 23:41:30 +08:00
66 changed files with 2367 additions and 379 deletions

14
.github/ISSUE_TEMPLATE/------.md vendored Normal file
View 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
View 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
View File

@@ -5,4 +5,6 @@ bin/config.json
dist/
x-ui-*.tar.gz
/x-ui
/release.sh
/release.sh
.sync*
main

14
Dockerfile Normal file
View 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" ]

143
README.md
View File

@@ -1,2 +1,143 @@
# x-ui
a web panel based on xray-core
支持多协议多用户的 xray 面板
# 功能介绍
- 系统状态监控
- 支持多用户多协议,网页可视化操作
- 支持的协议vmess、vless、trojan、shadowsocks、dokodemo-door、socks、http
- 支持配置更多传输配置
- 流量统计,限制流量,限制到期时间
- 可自定义 xray 配置模板
- 支持 https 访问面板(自备域名 + ssl 证书)
- 支持一键SSL证书申请且自动续签
- 更多高级配置项,详见面板
# 安装&升级
```
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的方法:
![](media/bda84fbc2ede834deaba1c173a932223.png)
![](media/d13ffd6a73f938d1037d0708e31433bf.png)
使用时只需输入 `域名`, `邮箱`, `API KEY`即可,示意图如下:
![](media/2022-04-04_141259.png)
注意事项:
- 该脚本使用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 迁移
首先在安装了 v2-ui 的服务器上安装最新版 x-ui然后使用以下命令进行迁移将迁移本机 v2-ui 的 `所有 inbound 账号数据`至 x-ui`面板设置和用户名密码不会迁移`
> 迁移成功后请 `关闭 v2-ui`并且 `重启 x-ui`,否则 v2-ui 的 inbound 会与 x-ui 的 inbound 会产生 `端口冲突`
```
x-ui v2-ui
```
## issue 关闭
各种小白问题看得血压很高
## Stargazers over time
[![Stargazers over time](https://starchart.cc/vaxilu/x-ui.svg)](https://starchart.cc/vaxilu/x-ui)

View File

@@ -1,10 +1,18 @@
package config
import (
_ "embed"
"fmt"
"os"
"strings"
)
//go:embed version
var version string
//go:embed name
var name string
type LogLevel string
const (
@@ -15,11 +23,11 @@ const (
)
func GetVersion() string {
return "0.0.1"
return strings.TrimSpace(version)
}
func GetName() string {
return "x-ui"
return strings.TrimSpace(name)
}
func GetLogLevel() LogLevel {

1
config/name Normal file
View File

@@ -0,0 +1 @@
x-ui

1
config/version Normal file
View File

@@ -0,0 +1 @@
0.3.2

View File

@@ -26,8 +26,9 @@ type User struct {
type Inbound struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
UserId int `json:"-"`
Up int64 `json:"up"`
Down int64 `json:"down"`
Up int64 `json:"up" form:"up"`
Down int64 `json:"down" form:"down"`
Total int64 `json:"total" form:"total"`
Remark string `json:"remark" form:"remark"`
Enable bool `json:"enable" form:"enable"`
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
@@ -43,8 +44,12 @@ type Inbound struct {
}
func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
listen := i.Listen
if listen != "" {
listen = fmt.Sprintf("\"%v\"", listen)
}
return &xray.InboundConfig{
Listen: json_util.RawMessage(fmt.Sprintf("\"%s\"", i.Listen)),
Listen: json_util.RawMessage(listen),
Port: i.Port,
Protocol: string(i.Protocol),
Settings: json_util.RawMessage(i.Settings),

1
go.mod
View File

@@ -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
View File

@@ -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=

69
install.sh Normal file → Executable file
View 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-x-ui-linux-${arch}
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 - 显示管理菜单 (功能更多)"
@@ -137,6 +163,7 @@ install_x-ui() {
echo -e "x-ui enable - 设置 x-ui 开机自启"
echo -e "x-ui disable - 取消 x-ui 开机自启"
echo -e "x-ui log - 查看 x-ui 日志"
echo -e "x-ui v2-ui - 迁移本机器的 v2-ui 账号数据至 x-ui"
echo -e "x-ui update - 更新 x-ui 面板"
echo -e "x-ui install - 安装 x-ui 面板"
echo -e "x-ui uninstall - 卸载 x-ui 面板"

View File

@@ -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{}) {

130
main.go
View File

@@ -3,7 +3,6 @@ package main
import (
"flag"
"fmt"
"github.com/op/go-logging"
"log"
"os"
"os/signal"
@@ -16,10 +15,9 @@ import (
"x-ui/web"
"x-ui/web/global"
"x-ui/web/service"
)
// this function call global.setWebServer
func setWebServer(server global.WebServer)
"github.com/op/go-logging"
)
func runWebServer() {
log.Printf("%v %v", config.GetName(), config.GetVersion())
@@ -45,7 +43,7 @@ func runWebServer() {
var server *web.Server
server = web.NewServer()
setWebServer(server)
global.SetWebServer(server)
err = server.Start()
if err != nil {
log.Println(err)
@@ -53,21 +51,27 @@ func runWebServer() {
}
sigCh := make(chan os.Signal, 1)
signal.Notify(sigCh, syscall.SIGHUP)
//信号量捕获处理
signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
for {
sig := <-sigCh
if sig == syscall.SIGHUP {
server.Stop()
switch sig {
case syscall.SIGHUP:
err := server.Stop()
if err != nil {
logger.Warning("stop server err:", err)
}
server = web.NewServer()
setWebServer(server)
global.SetWebServer(server)
err = server.Start()
if err != nil {
log.Println(err)
return
}
} else {
continue
default:
server.Stop()
return
}
}
}
@@ -88,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 {
@@ -135,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() {
@@ -173,7 +271,7 @@ func main() {
}
err = v2ui.MigrateFromV2UI(dbPath)
if err != nil {
logger.Error("migrate from v2-ui failed:", err)
fmt.Println("migrate from v2-ui failed:", err)
}
case "setting":
err := settingCmd.Parse(os.Args[2:])
@@ -186,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

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

BIN
media/2022-04-17_110907.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
media/2022-04-17_111321.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

BIN
media/2022-04-17_111705.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 36 KiB

BIN
media/2022-04-17_111910.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 30 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 23 KiB

21
util/common/format.go Normal file
View 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))
}
}

View 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
}

View File

8
util/sys/psutil.go Normal file
View 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
View 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
View 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
}

28
v2ui/db.go Normal file
View File

@@ -0,0 +1,28 @@
package v2ui
import (
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
var v2db *gorm.DB
func initDB(dbPath string) error {
c := &gorm.Config{
Logger: logger.Discard,
}
var err error
v2db, err = gorm.Open(sqlite.Open(dbPath), c)
if err != nil {
return err
}
return nil
}
func getV2Inbounds() ([]*V2Inbound, error) {
inbounds := make([]*V2Inbound, 0)
err := v2db.Model(V2Inbound{}).Find(&inbounds).Error
return inbounds, err
}

41
v2ui/models.go Normal file
View File

@@ -0,0 +1,41 @@
package v2ui
import "x-ui/database/model"
type V2Inbound struct {
Id int `gorm:"primaryKey;autoIncrement"`
Port int `gorm:"unique"`
Listen string
Protocol string
Settings string
StreamSettings string
Tag string `gorm:"unique"`
Sniffing string
Remark string
Up int64
Down int64
Enable bool
}
func (i *V2Inbound) TableName() string {
return "inbound"
}
func (i *V2Inbound) ToInbound(userId int) *model.Inbound {
return &model.Inbound{
UserId: userId,
Up: i.Up,
Down: i.Down,
Total: 0,
Remark: i.Remark,
Enable: i.Enable,
ExpiryTime: 0,
Listen: i.Listen,
Port: i.Port,
Protocol: model.Protocol(i.Protocol),
Settings: i.Settings,
StreamSettings: i.StreamSettings,
Tag: i.Tag,
Sniffing: i.Sniffing,
}
}

View File

@@ -1,7 +1,51 @@
package v2ui
import "errors"
import (
"fmt"
"x-ui/config"
"x-ui/database"
"x-ui/database/model"
"x-ui/util/common"
"x-ui/web/service"
)
func MigrateFromV2UI(dbPath string) error {
return errors.New("not support right now")
err := initDB(dbPath)
if err != nil {
return common.NewError("init v2-ui database failed:", err)
}
err = database.InitDB(config.GetDBPath())
if err != nil {
return common.NewError("init x-ui database failed:", err)
}
v2Inbounds, err := getV2Inbounds()
if err != nil {
return common.NewError("get v2-ui inbounds failed:", err)
}
if len(v2Inbounds) == 0 {
fmt.Println("migrate v2-ui inbounds success: 0")
return nil
}
userService := service.UserService{}
user, err := userService.GetFirstUser()
if err != nil {
return common.NewError("get x-ui user failed:", err)
}
inbounds := make([]*model.Inbound, 0)
for _, v2inbound := range v2Inbounds {
inbounds = append(inbounds, v2inbound.ToInbound(user.Id))
}
inboundService := service.InboundService{}
err = inboundService.AddInbounds(inbounds)
if err != nil {
return common.NewError("add x-ui inbounds failed:", err)
}
fmt.Println("migrate v2-ui inbounds success:", len(inbounds))
return nil
}

File diff suppressed because one or more lines are too long

View File

@@ -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,29 +26,90 @@ class Msg {
}
class DBInbound {
id = 0;
userId = 0;
up = 0;
down = 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;
}
ObjectUtil.cloneProps(this, data);
}
get totalGB() {
return toFixed(this.total / ONE_GB, 2);
}
set totalGB(gb) {
this.total = toFixed(gb * ONE_GB, 0);
}
get isVMess() {
return this.protocol === Protocols.VMESS;
}
get isVLess() {
return this.protocol === Protocols.VLESS;
}
get isTrojan() {
return this.protocol === Protocols.TROJAN;
}
get isSS() {
return this.protocol === Protocols.SHADOWSOCKS;
}
get isSocks() {
return this.protocol === Protocols.SOCKS;
}
get isHTTP() {
return this.protocol === Protocols.HTTP;
}
get address() {
let address = location.hostname;
if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") {
address = this.listen;
}
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)) {
@@ -84,24 +149,28 @@ class DBInbound {
}
}
genLink(address = "") {
genLink() {
const inbound = this.toInbound();
return inbound.genLink(address, this.remark);
return inbound.genLink(this.address, this.remark);
}
}
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
}

View File

@@ -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,
@@ -168,6 +171,15 @@ TcpStreamSettings.TcpRequest = class extends XrayCommonClass {
this.headers.push({ name: name, value: value });
}
getHeader(name) {
for (const header of this.headers) {
if (header.name.toLowerCase() === name.toLowerCase()) {
return header.value;
}
}
return null;
}
removeHeader(index) {
this.headers.splice(index, 1);
}
@@ -284,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;
}
@@ -294,12 +307,22 @@ class WsStreamSettings extends XrayCommonClass {
this.headers.push({ name: name, value: value });
}
getHeader(name) {
for (const header of this.headers) {
if (header.name.toLowerCase() === name.toLowerCase()) {
return header.value;
}
}
return null;
}
removeHeader(index) {
this.headers.splice(index, 1);
}
static fromJson(json={}) {
return new WsStreamSettings(
json.acceptProxyProtocol,
json.path,
XrayCommonClass.toHeaders(json.headers),
);
@@ -307,6 +330,7 @@ class WsStreamSettings extends XrayCommonClass {
toJson() {
return {
acceptProxyProtocol: this.acceptProxyProtocol,
path: this.path,
headers: XrayCommonClass.toV2Headers(this.headers, false),
};
@@ -393,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) {
@@ -412,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
);
}
@@ -422,6 +449,7 @@ class TlsStreamSettings extends XrayCommonClass {
return {
serverName: this.server,
certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn
};
}
}
@@ -611,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';
}
}
@@ -627,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';
}
}
@@ -643,6 +663,153 @@ class Inbound extends XrayCommonClass {
this.stream.network = network;
}
get isTcp() {
return this.network === "tcp";
}
get isWs() {
return this.network === "ws";
}
get isKcp() {
return this.network === "kcp";
}
get isQuic() {
return this.network === "quic"
}
get isGrpc() {
return this.network === "grpc";
}
get isH2() {
return this.network === "http";
}
// VMess & VLess
get uuid() {
switch (this.protocol) {
case Protocols.VMESS:
return this.settings.vmesses[0].id;
case Protocols.VLESS:
return this.settings.vlesses[0].id;
default:
return "";
}
}
// 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 "";
}
}
// VMess
get alterId() {
switch (this.protocol) {
case Protocols.VMESS:
return this.settings.vmesses[0].alterId;
default:
return "";
}
}
// Socks & HTTP
get username() {
switch (this.protocol) {
case Protocols.SOCKS:
case Protocols.HTTP:
return this.settings.accounts[0].user;
default:
return "";
}
}
// Trojan & Shadowsocks & Socks & HTTP
get password() {
switch (this.protocol) {
case Protocols.TROJAN:
return this.settings.clients[0].password;
case Protocols.SHADOWSOCKS:
return this.settings.password;
case Protocols.SOCKS:
case Protocols.HTTP:
return this.settings.accounts[0].pass;
default:
return "";
}
}
// Shadowsocks
get method() {
switch (this.protocol) {
case Protocols.SHADOWSOCKS:
return this.settings.method;
default:
return "";
}
}
get serverName() {
if (this.stream.isTls || this.stream.isXTls) {
return this.stream.tls.server;
}
return "";
}
get host() {
if (this.isTcp) {
return this.stream.tcp.request.getHeader("Host");
} else if (this.isWs) {
return this.stream.ws.getHeader("Host");
} else if (this.isH2) {
return this.stream.http.host[0];
}
return null;
}
get path() {
if (this.isTcp) {
return this.stream.tcp.request.path[0];
} else if (this.isWs) {
return this.stream.ws.path;
} else if (this.isH2) {
return this.stream.http.path[0];
}
return null;
}
get quicSecurity() {
return this.stream.quic.security;
}
get quicKey() {
return this.stream.quic.key;
}
get quicType() {
return this.stream.quic.type;
}
get kcpType() {
return this.stream.kcp.type;
}
get kcpSeed() {
return this.stream.kcp.seed;
}
get serviceName() {
return this.stream.grpc.serviceName;
}
canEnableTls() {
switch (this.protocol) {
case Protocols.VMESS:
@@ -785,7 +952,7 @@ class Inbound extends XrayCommonClass {
const type = this.stream.network;
const params = new Map();
params.set("type", this.stream.network);
if (this.isXTls) {
if (this.xtls) {
params.set("security", "xtls");
} else {
params.set("security", this.stream.security);
@@ -841,22 +1008,10 @@ class Inbound extends XrayCommonClass {
}
}
if (this.isXTls) {
if (this.xtls) {
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) {
@@ -1002,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;
@@ -1054,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;
@@ -1107,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),
};
}
@@ -1123,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,

View File

@@ -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;
@@ -284,7 +285,7 @@ class ObjectUtil {
return false;
}
}
return true
return true;
}
}

View File

@@ -36,9 +36,8 @@ func (a *InboundController) startTask() {
webServer := global.GetWebServer()
c := webServer.GetCron()
c.AddFunc("@every 10s", func() {
if a.xrayService.IsNeedRestart() {
a.xrayService.SetIsNeedRestart(false)
err := a.xrayService.RestartXray()
if a.xrayService.IsNeedRestartAndSetFalse() {
err := a.xrayService.RestartXray(false)
if err != nil {
logger.Error("restart xray failed:", err)
}
@@ -70,7 +69,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
err = a.inboundService.AddInbound(inbound)
jsonMsg(c, "添加", err)
if err == nil {
a.xrayService.SetIsNeedRestart(true)
a.xrayService.SetToNeedRestart()
}
}
@@ -83,7 +82,7 @@ func (a *InboundController) delInbound(c *gin.Context) {
err = a.inboundService.DelInbound(id)
jsonMsg(c, "删除", err)
if err == nil {
a.xrayService.SetIsNeedRestart(true)
a.xrayService.SetToNeedRestart()
}
}
@@ -104,6 +103,6 @@ func (a *InboundController) updateInbound(c *gin.Context) {
err = a.inboundService.UpdateInbound(inbound)
jsonMsg(c, "修改", err)
if err == nil {
a.xrayService.SetIsNeedRestart(true)
a.xrayService.SetToNeedRestart()
}
}

View File

@@ -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)

View File

@@ -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"`

View File

@@ -13,8 +13,7 @@ type WebServer interface {
GetCtx() context.Context
}
//go:linkname setWebServer main.setWebServer
func setWebServer(s WebServer) {
func SetWebServer(s WebServer) {
webServer = s
}

View File

@@ -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>

View File

@@ -0,0 +1,92 @@
{{define "inboundInfoStream"}}
<p>传输: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
<template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
<p v-if="inbound.host">host: <a-tag color="green">[[ inbound.host ]]</a-tag></p>
<p v-else>host: <a-tag color="orange"></a-tag></p>
<p v-if="inbound.path">path: <a-tag color="green">[[ inbound.path ]]</a-tag></p>
<p v-else>path: <a-tag color="orange"></a-tag></p>
</template>
<template v-if="inbound.isQuic">
<p>quic 加密: <a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></p>
<p>quic 密码: <a-tag color="green">[[ inbound.quicKey ]]</a-tag></p>
<p>quic 伪装: <a-tag color="green">[[ inbound.quicType ]]</a-tag></p>
</template>
<template v-if="inbound.isKcp">
<p>kcp 加密: <a-tag color="green">[[ inbound.kcpType ]]</a-tag></p>
<p>kcp 密码: <a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></p>
</template>
<template v-if="inbound.isGrpc">
<p>grpc serviceName: <a-tag color="green">[[ inbound.serviceName ]]</a-tag></p>
</template>
<template v-if="inbound.tls || inbound.xtls">
<p v-if="inbound.tls">tls: <a-tag color="green">开启</a-tag></p>
<p v-if="inbound.xtls">xtls: <a-tag color="green">开启</a-tag></p>
</template>
<template v-else>
<p>tls: <a-tag color="red">关闭</a-tag></p>
</template>
<p v-if="inbound.tls">
tls域名: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
</p>
<p v-if="inbound.xtls">
xtls域名: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
</p>
{{end}}
{{define "component/inboundInfoComponent"}}
<div>
<p>协议: <a-tag color="green">[[ dbInbound.protocol ]]</a-tag></p>
<p>地址: <a-tag color="blue">[[ dbInbound.address ]]</a-tag></p>
<p>端口: <a-tag color="green">[[ dbInbound.port ]]</a-tag></p>
<template v-if="dbInbound.isVMess">
<p>uuid: <a-tag color="green">[[ inbound.uuid ]]</a-tag></p>
<p>alterId: <a-tag color="green">[[ inbound.alterId ]]</a-tag></p>
</template>
<template v-if="dbInbound.isVLess">
<p>uuid: <a-tag color="green">[[ inbound.uuid ]]</a-tag></p>
<p v-if="inbound.isXTls">flow: <a-tag color="green">[[ inbound.flow ]]</a-tag></p>
</template>
<template v-if="dbInbound.isTrojan">
<p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isSS">
<p>加密: <a-tag color="green">[[ inbound.method ]]</a-tag></p>
<p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isSocks">
<p>用户名: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
<p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isHTTP">
<p>用户名: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
<p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
</template>
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
{{template "inboundInfoStream"}}
</template>
</div>
{{end}}
{{define "component/inboundInfo"}}
<script>
Vue.component('inbound-info', {
delimiters: ['[[', ']]'],
props: ["dbInbound", "inbound"],
template: `{{template "component/inboundInfoComponent"}}`,
});
</script>
{{end}}

View File

@@ -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>

View File

@@ -27,6 +27,31 @@
<a-form-item label="端口">
<a-input type="number" v-model.number="inbound.port"></a-input>
</a-form-item>
<a-form-item>
<span slot="label">
总流量(GB)
<a-tooltip>
<template slot="title">
0 表示不限制
</template>
<a-icon type="question-circle" theme="filled"></a-icon>
</a-tooltip>
</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 -->

View File

@@ -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}}

View File

@@ -3,10 +3,10 @@
<a-form-item label="id">
<a-input v-model.trim="inbound.settings.vlesses[0].id"></a-input>
</a-form-item>
<a-form-item label="flow">
<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>

View File

@@ -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'"

View File

@@ -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>

View File

@@ -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}}

View File

@@ -0,0 +1,61 @@
{{define "inboundInfoModal"}}
{{template "component/inboundInfo"}}
<a-modal id="inbound-info-modal" v-model="infoModal.visible" title="详细信息" @ok="infoModal.ok"
:closable="true" :mask-closable="true"
ok-text="复制链接" cancel-text='{{ i18n "close" }}' :ok-button-props="infoModal.okBtnPros">
<inbound-info :db-inbound="dbInbound" :inbound="inbound"></inbound-info>
</a-modal>
<script>
const infoModal = {
visible: false,
inbound: new Inbound(),
dbInbound: new DBInbound(),
clipboard: null,
okBtnPros: {
attrs: {
id: "inbound-info-modal-ok-btn",
style: "",
},
},
show(dbInbound) {
this.inbound = dbInbound.toInbound();
this.dbInbound = new DBInbound(dbInbound);
this.visible = true;
if (dbInbound.hasLink()) {
this.okBtnPros.attrs.style = "";
} else {
this.okBtnPros.attrs.style = "display: none";
}
if (this.clipboard == null) {
infoModalApp.$nextTick(() => {
this.clipboard = new ClipboardJS(`#${this.okBtnPros.attrs.id}`, {
text: () => this.dbInbound.genLink(),
});
this.clipboard.on('success', () => app.$message.success('复制成功'));
});
}
},
close() {
infoModal.visible = false;
},
};
const infoModalApp = new Vue({
delimiters: ['[[', ']]'],
el: '#inbound-info-modal',
data: {
infoModal,
get dbInbound() {
return this.infoModal.dbInbound;
},
get inbound() {
return this.infoModal.inbound;
}
},
});
</script>
{{end}}

View File

@@ -27,15 +27,15 @@
<a-card hoverable style="margin-bottom: 20px;">
<a-row>
<a-col :xs="24" :sm="24" :lg="12">
upload / download
总上传 / 下载
<a-tag color="green">[[ sizeFormat(total.up) ]] / [[ sizeFormat(total.down) ]]</a-tag>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
total traffic
总用量
<a-tag color="green">[[ sizeFormat(total.up + total.down) ]]</a-tag>
</a-col>
<a-col :xs="24" :sm="24" :lg="12">
number of accounts
入站数量
<a-tag color="green">[[ dbInbounds.length ]]</a-tag>
</a-col>
</a-row>
@@ -46,37 +46,69 @@
<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="green">无限制</a-tag>
</template>
<template slot="settings" slot-scope="text, dbInbound">
<a-button type="link">查看</a-button>
<a-button type="link" @click="showInfo(dbInbound)">查看</a-button>
</template>
<template slot="streamSettings" slot-scope="text, dbInbound">
<a-button type="link">查看</a-button>
<template slot="stream" slot-scope="text, dbInbound, index">
<template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
<a-tag color="green">[[ inbounds[index].stream.network ]]</a-tag>
<a-tag v-if="inbounds[index].stream.isTls" color="blue">tls</a-tag>
<a-tag v-if="inbounds[index].stream.isXTls" color="blue">xtls</a-tag>
</template>
<template v-else></template>
</template>
<template slot="enable" slot-scope="text, dbInbound">
<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 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>
@@ -89,45 +121,55 @@
<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: "protocol",
title: "备注",
align: 'center',
width: 100,
dataIndex: "remark",
}, {
title: "协议",
align: 'center',
width: 60,
scopedSlots: { customRender: 'protocol' },
}, {
title: "port",
title: "端口",
align: 'center',
dataIndex: "port",
width: 60,
}, {
title: "traffic",
title: "流量↑|↓",
align: 'center',
width: 60,
width: 150,
scopedSlots: { customRender: 'traffic' },
// }, {
// title: "settings",
// align: 'center',
// width: 60,
// scopedSlots: { customRender: 'settings' },
// }, {
// title: "streamSettings",
// align: 'center',
// width: 60,
// scopedSlots: { customRender: 'streamSettings' },
}, {
title: "enable",
title: "详细信息",
align: 'center',
width: 40,
scopedSlots: { customRender: 'settings' },
}, {
title: "传输配置",
align: 'center',
width: 60,
scopedSlots: { customRender: 'enable' },
scopedSlots: { customRender: 'stream' },
}, {
title: "action",
title: "到期时间",
align: 'center',
width: 60,
scopedSlots: { customRender: 'action' },
width: 80,
scopedSlots: { customRender: 'expiryTime' },
}];
const app = new Vue({
@@ -136,6 +178,7 @@
data: {
siderDrawer,
spinning: false,
inbounds: [],
dbInbounds: [],
searchKey: '',
},
@@ -153,9 +196,12 @@
this.setInbounds(msg.obj);
},
setInbounds(dbInbounds) {
this.inbounds.splice(0);
this.dbInbounds.splice(0);
for (const inbound of dbInbounds) {
this.dbInbounds.push(new DBInbound(inbound));
const dbInbound = new DBInbound(inbound);
this.inbounds.push(dbInbound.toInbound());
this.dbInbounds.push(dbInbound);
}
},
searchInbounds(key) {
@@ -170,10 +216,26 @@
});
}
},
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: 'add account',
okText: 'add',
title: '添加入站',
okText: '添加',
confirm: async (inbound, dbInbound) => {
inModal.loading();
await this.addInbound(inbound, dbInbound);
@@ -184,8 +246,8 @@
openEditInbound(dbInbound) {
const inbound = dbInbound.toInbound();
inModal.show({
title: 'update account',
okText: 'update',
title: '修改入站',
okText: '修改',
inbound: inbound,
dbInbound: dbInbound,
confirm: async (inbound, dbInbound) => {
@@ -197,8 +259,12 @@
},
async addInbound(inbound, dbInbound) {
const data = {
up: dbInbound.up,
down: dbInbound.down,
total: dbInbound.total,
remark: dbInbound.remark,
enable: dbInbound.enable,
expiryTime: dbInbound.expiryTime,
listen: inbound.listen,
port: inbound.port,
@@ -211,8 +277,12 @@
},
async updateInbound(inbound, dbInbound) {
const data = {
up: dbInbound.up,
down: dbInbound.down,
total: dbInbound.total,
remark: dbInbound.remark,
enable: dbInbound.enable,
expiryTime: dbInbound.expiryTime,
listen: inbound.listen,
port: inbound.port,
@@ -223,23 +293,36 @@
};
await this.submit(`/xui/inbound/update/${dbInbound.id}`, data, inModal);
},
resetTraffic(dbInbound) {
this.$confirm({
title: '重置流量',
content: '确定要重置流量吗?',
okText: '重置',
cancelText: '取消',
onOk: () => {
const inbound = dbInbound.toInbound();
dbInbound.up = 0;
dbInbound.down = 0;
this.updateInbound(inbound, dbInbound);
},
});
},
delInbound(dbInbound) {
this.$confirm({
title: 'delete account',
content: 'Cannot be restored after deletion, confirm deletion?',
okText: 'delete',
cancelText: 'cancel',
title: '删除入站',
content: '确定要删除入站吗?',
okText: '删除',
cancelText: '取消',
onOk: () => this.submit('/xui/inbound/del/' + dbInbound.id),
});
},
showQrcode(dbInbound) {
let address = location.hostname;
if (!ObjectUtil.isEmpty(dbInbound.listen) || dbInbound.listen !== "0.0.0.0") {
address = dbInbound.listen;
}
const link = dbInbound.genLink(address);
const link = dbInbound.genLink();
qrModal.show('二维码', link);
},
showInfo(dbInbound) {
infoModal.show(dbInbound);
},
switchEnable(dbInbound) {
this.submit(`/xui/inbound/update/${dbInbound.id}`, dbInbound);
},
@@ -278,5 +361,6 @@
{{template "promptModal"}}
{{template "qrcodeModal"}}
{{template "textModal"}}
{{template "inboundInfoModal"}}
</body>
</html>

View File

@@ -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;
}

View File

@@ -38,11 +38,11 @@
<a-tabs default-active-key="1">
<a-tab-pane key="1" tab="面板配置">
<a-list item-layout="horizontal" style="background: white">
<setting-list-item type="text" title="面板监听 IP" desc="默认留空监听所有 IP" v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title="面板监听端口" v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title="面板证书公钥文件路径" desc="填写一个 '/' 开头的绝对路径" v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title="面板证书密钥文件路径" desc="填写一个 '/' 开头的绝对路径" v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title="面板 url 根路径" desc="必须以 '/' 开头,以 '/' 结尾" v-model="allSetting.webBasePath"></setting-list-item>
<setting-list-item type="text" title="面板监听 IP" desc="默认留空监听所有 IP,重启面板生效" v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="number" title="面板监听端口" desc="重启面板生效" v-model.number="allSetting.webPort"></setting-list-item>
<setting-list-item type="text" title="面板证书公钥文件路径" desc="填写一个 '/' 开头的绝对路径,重启面板生效" v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title="面板证书密钥文件路径" desc="填写一个 '/' 开头的绝对路径,重启面板生效" v-model="allSetting.webKeyFile"></setting-list-item>
<setting-list-item type="text" title="面板 url 根路径" desc="必须以 '/' 开头,以 '/' 结尾,重启面板生效" v-model="allSetting.webBasePath"></setting-list-item>
</a-list>
</a-tab-pane>
<a-tab-pane key="2" tab="用户设置">
@@ -68,12 +68,20 @@
</a-tab-pane>
<a-tab-pane key="3" tab="xray 相关设置">
<a-list item-layout="horizontal" style="background: white">
<setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件" v-model="allSetting.xrayTemplateConfig"></setting-list-item>
<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="text" title="时区" desc="定时任务按照该时区的时间运行" v-model="allSetting.timeLocation"></setting-list-item>
<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>
</a-tab-pane>
</a-tabs>

View 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()
}
}

View 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
View 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)
}

View 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)
}
}

View 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
}

View 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)
}

View File

@@ -2,10 +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 {
@@ -31,11 +34,64 @@ func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
return inbounds, nil
}
func (s *InboundService) checkPortExist(port int, ignoreId int) (bool, error) {
db := database.GetDB()
db = db.Model(model.Inbound{}).Where("port = ?", port)
if ignoreId > 0 {
db = db.Where("id != ?", ignoreId)
}
var count int64
err := db.Count(&count).Error
if err != nil {
return false, err
}
return count > 0, nil
}
func (s *InboundService) AddInbound(inbound *model.Inbound) error {
exist, err := s.checkPortExist(inbound.Port, 0)
if err != nil {
return err
}
if exist {
return common.NewError("端口已存在:", inbound.Port)
}
db := database.GetDB()
return db.Save(inbound).Error
}
func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
for _, inbound := range inbounds {
exist, err := s.checkPortExist(inbound.Port, 0)
if err != nil {
return err
}
if exist {
return common.NewError("端口已存在:", inbound.Port)
}
}
db := database.GetDB()
tx := db.Begin()
var err error
defer func() {
if err == nil {
tx.Commit()
} else {
tx.Rollback()
}
}()
for _, inbound := range inbounds {
err = tx.Save(inbound).Error
if err != nil {
return err
}
}
return nil
}
func (s *InboundService) DelInbound(id int) error {
db := database.GetDB()
return db.Delete(model.Inbound{}, id).Error
@@ -52,10 +108,21 @@ func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
}
func (s *InboundService) UpdateInbound(inbound *model.Inbound) error {
exist, err := s.checkPortExist(inbound.Port, inbound.Id)
if err != nil {
return err
}
if exist {
return common.NewError("端口已存在:", inbound.Port)
}
oldInbound, err := s.GetInbound(inbound.Id)
if err != nil {
return err
}
oldInbound.Up = inbound.Up
oldInbound.Down = inbound.Down
oldInbound.Total = inbound.Total
oldInbound.Remark = inbound.Remark
oldInbound.Enable = inbound.Enable
oldInbound.ExpiryTime = inbound.ExpiryTime
@@ -98,3 +165,14 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
}
return
}
func (s *InboundService) DisableInvalidInbounds() (int64, error) {
db := database.GetDB()
now := time.Now().Unix() * 1000
result := db.Model(model.Inbound{}).
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
return count, err
}

View File

@@ -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)
}

View File

@@ -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")
}

View File

@@ -2,15 +2,29 @@ package service
import (
"errors"
"gorm.io/gorm"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"gorm.io/gorm"
)
type UserService struct {
}
func (s *UserService) GetFirstUser() (*model.User, error) {
db := database.GetDB()
user := &model.User{}
err := db.Model(model.User{}).
First(user).
Error
if err != nil {
return nil, err
}
return user, nil
}
func (s *UserService) CheckUser(username string, password string) *model.User {
db := database.GetDB()

View File

@@ -3,20 +3,21 @@ 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
var lock sync.Mutex
var isNeedXrayRestart atomic.Bool
var result string
type XrayService struct {
inboundService InboundService
settingService SettingService
isNeedXrayRestart atomic.Bool
}
func (s *XrayService) IsXrayRunning() bool {
@@ -84,17 +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, 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()
@@ -108,16 +111,17 @@ func (s *XrayService) RestartXray() error {
func (s *XrayService) StopXray() error {
lock.Lock()
defer lock.Unlock()
logger.Debug("stop xray")
if s.IsXrayRunning() {
return p.Stop()
}
return errors.New("xray is not running")
}
func (s *XrayService) SetIsNeedRestart(needRestart bool) {
s.isNeedXrayRestart.Store(needRestart)
func (s *XrayService) SetToNeedRestart() {
isNeedXrayRestart.Store(true)
}
func (s *XrayService) IsNeedRestart() bool {
return s.isNeedXrayRestart.Load()
func (s *XrayService) IsNeedRestartAndSetFalse() bool {
return isNeedXrayRestart.CAS(true, false)
}

View File

@@ -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"
@@ -18,13 +11,23 @@ import (
"net/http"
"os"
"strconv"
"strings"
"time"
"x-ui/config"
"x-ui/logger"
"x-ui/util"
"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/*
@@ -36,16 +39,47 @@ var htmlFS embed.FS
//go:embed translation/*
var i18nFS embed.FS
var startTime = time.Now()
type wrapAssetsFS struct {
embed.FS
}
func (f *wrapAssetsFS) Open(name string) (fs.File, error) {
return f.FS.Open("assets/" + name)
file, err := f.FS.Open("assets/" + name)
if err != nil {
return nil, err
}
return &wrapAssetsFile{
File: file,
}, nil
}
type wrapAssetsFile struct {
fs.File
}
func (f *wrapAssetsFile) Stat() (fs.FileInfo, error) {
info, err := f.File.Stat()
if err != nil {
return nil, err
}
return &wrapAssetsFileInfo{
FileInfo: info,
}, nil
}
type wrapAssetsFileInfo struct {
fs.FileInfo
}
func (f *wrapAssetsFileInfo) ModTime() time.Time {
return startTime
}
type Server struct {
listener net.Listener
httpServer *http.Server
listener net.Listener
index *controller.IndexController
server *controller.ServerController
@@ -131,12 +165,19 @@ func (s *Server) initRouter() (*gin.Engine, error) {
if err != nil {
return nil, err
}
assetsBasePath := basePath + "assets/"
store := cookie.NewStore(secret)
engine.Use(sessions.Sessions("session", store))
engine.Use(func(c *gin.Context) {
c.Set("base_path", basePath)
})
engine.Use(func(c *gin.Context) {
uri := c.Request.RequestURI
if strings.HasPrefix(uri, assetsBasePath) {
c.Header("Cache-Control", "max-age=31536000")
}
})
err = s.initI18n(engine)
if err != nil {
return nil, err
@@ -239,43 +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
s.cron.AddFunc("@every 30s", func() {
if s.xrayService.IsXrayRunning() {
checkTime = 0
return
}
checkTime++
if checkTime < 2 {
return
}
s.xrayService.SetIsNeedRestart(true)
})
// 每 30 秒检查一次 xray 是否在运行
s.cron.AddJob("@every 30s", job.NewCheckXrayRunningJob())
go func() {
time.Sleep(time.Second * 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)
}
})
// 每 10 秒统计一次流量,首次启动延迟 5 秒,与重启 xray 的时间错开
s.cron.AddJob("@every 10s", job.NewXrayTrafficJob())
}()
// 每 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()
@@ -311,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 {
@@ -337,24 +378,32 @@ func (s *Server) Start() (err error) {
s.startTask()
go engine.RunListener(listener)
s.httpServer = &http.Server{
Handler: engine,
}
go func() {
s.httpServer.Serve(listener)
}()
return nil
}
func (s *Server) Stop() error {
if util.IsDone(s.ctx) {
// 防止 gc 后调用第二次 Stop
s.xrayService.StopXray()
}
s.cancel()
s.xrayService.StopXray()
if s.cron != nil {
s.cron.Stop()
}
if s.listener != nil {
return s.listener.Close()
var err1 error
var err2 error
if s.httpServer != nil {
err1 = s.httpServer.Shutdown(s.ctx)
}
return nil
if s.listener != nil {
err2 = s.listener.Close()
}
return common.Combine(err1, err2)
}
func (s *Server) GetCtx() context.Context {

12
x-ui.service Normal file
View File

@@ -0,0 +1,12 @@
[Unit]
Description=x-ui Service
After=network.target
Wants=network.target
[Service]
Type=simple
WorkingDirectory=/usr/local/x-ui/
ExecStart=/usr/local/x-ui/x-ui
[Install]
WantedBy=multi-user.target

299
x-ui.sh
View File

@@ -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
@@ -270,21 +288,28 @@ show_log() {
fi
}
migrate_v2_ui() {
/usr/local/x-ui/x-ui v2-ui
before_show_menu
}
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
}
@@ -306,7 +331,7 @@ check_enabled() {
if [[ x"${temp}" == x"enabled" ]]; then
return 0
else
return 1;
return 1
fi
}
@@ -314,7 +339,7 @@ check_uninstall() {
check_status
if [[ $? != 2 ]]; then
echo ""
echo -e "${red}面板已安装,请不要重复安装${plain}"
LOGE "面板已安装,请不要重复安装"
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -328,7 +353,7 @@ check_install() {
check_status
if [[ $? == 2 ]]; then
echo ""
echo -e "${red}请先安装面板${plain}"
LOGE "请先安装面板"
if [[ $# == 0 ]]; then
before_show_menu
fi
@@ -341,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
}
@@ -382,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 "------------------------------------------"
@@ -393,6 +494,7 @@ show_usage() {
echo "x-ui enable - 设置 x-ui 开机自启"
echo "x-ui disable - 取消 x-ui 开机自启"
echo "x-ui log - 查看 x-ui 日志"
echo "x-ui v2-ui - 迁移本机器的 v2-ui 账号数据至 x-ui"
echo "x-ui update - 更新 x-ui 面板"
echo "x-ui install - 安装 x-ui 面板"
echo "x-ui uninstall - 卸载 x-ui 面板"
@@ -402,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
@@ -412,81 +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
;;
"update") check_install 0 && update 0
"v2-ui")
check_install 0 && migrate_v2_ui 0
;;
"install") check_uninstall 0 && install 0
"update")
check_install 0 && update 0
;;
"uninstall") check_install 0 && uninstall 0
"install")
check_uninstall 0 && install 0
;;
*) show_usage
"uninstall")
check_install 0 && uninstall 0
;;
*) show_usage ;;
esac
else
show_menu

View File

@@ -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)")