The response has been limited to 50k tokens of the smallest files in the repo. You can remove this limitation by removing the max tokens filter.
├── .github
    ├── ISSUE_TEMPLATE
    │   └── ------.md
    └── workflows
    │   └── release.yml
├── .gitignore
├── Dockerfile
├── LICENSE
├── README.md
├── bin
    ├── geoip.dat
    ├── geosite.dat
    ├── xray-linux-amd64
    └── xray-linux-arm64
├── config
    ├── config.go
    ├── name
    └── version
├── database
    ├── db.go
    └── model
    │   └── model.go
├── go.mod
├── go.sum
├── install.sh
├── logger
    └── logger.go
├── main.go
├── media
    ├── 2022-04-04_141259.png
    ├── 2022-04-17_110907.png
    ├── 2022-04-17_111321.png
    ├── 2022-04-17_111705.png
    ├── 2022-04-17_111910.png
    ├── bda84fbc2ede834deaba1c173a932223.png
    └── d13ffd6a73f938d1037d0708e31433bf.png
├── util
    ├── common
    │   ├── err.go
    │   ├── format.go
    │   ├── multi_error.go
    │   └── stringUtil.go
    ├── context.go
    ├── json_util
    │   └── json.go
    ├── random
    │   └── random.go
    ├── reflect_util
    │   └── reflect.go
    └── sys
    │   ├── a.s
    │   ├── psutil.go
    │   ├── sys_darwin.go
    │   └── sys_linux.go
├── v2ui
    ├── db.go
    ├── models.go
    └── v2ui.go
├── web
    ├── assets
    │   ├── ant-design-vue@1.7.2
    │   │   ├── antd-with-locales.min.js
    │   │   ├── antd.less
    │   │   ├── antd.min.css
    │   │   └── antd.min.js
    │   ├── axios
    │   │   └── axios.min.js
    │   ├── base64
    │   │   └── base64.min.js
    │   ├── clipboard
    │   │   └── clipboard.min.js
    │   ├── css
    │   │   └── custom.css
    │   ├── element-ui@2.15.0
    │   │   └── theme-chalk
    │   │   │   └── display.css
    │   ├── js
    │   │   ├── axios-init.js
    │   │   ├── model
    │   │   │   ├── models.js
    │   │   │   └── xray.js
    │   │   └── util
    │   │   │   ├── common.js
    │   │   │   ├── date-util.js
    │   │   │   └── utils.js
    │   ├── moment
    │   │   └── moment.min.js
    │   ├── qrcode
    │   │   └── qrious.min.js
    │   ├── qs
    │   │   └── qs.min.js
    │   ├── uri
    │   │   └── URI.min.js
    │   └── vue@2.6.12
    │   │   ├── vue.common.dev.js
    │   │   ├── vue.common.js
    │   │   ├── vue.common.prod.js
    │   │   ├── vue.esm.browser.min.js
    │   │   ├── vue.esm.js
    │   │   ├── vue.min.js
    │   │   ├── vue.runtime.common.dev.js
    │   │   ├── vue.runtime.common.js
    │   │   ├── vue.runtime.common.prod.js
    │   │   ├── vue.runtime.esm.js
    │   │   ├── vue.runtime.js
    │   │   └── vue.runtime.min.js
    ├── controller
    │   ├── base.go
    │   ├── inbound.go
    │   ├── index.go
    │   ├── server.go
    │   ├── setting.go
    │   ├── util.go
    │   └── xui.go
    ├── entity
    │   └── entity.go
    ├── global
    │   └── global.go
    ├── html
    │   ├── common
    │   │   ├── head.html
    │   │   ├── js.html
    │   │   ├── prompt_modal.html
    │   │   ├── qrcode_modal.html
    │   │   └── text_modal.html
    │   ├── login.html
    │   └── xui
    │   │   ├── common_sider.html
    │   │   ├── component
    │   │       ├── inbound_info.html
    │   │       └── setting.html
    │   │   ├── form
    │   │       ├── inbound.html
    │   │       ├── protocol
    │   │       │   ├── dokodemo.html
    │   │       │   ├── http.html
    │   │       │   ├── shadowsocks.html
    │   │       │   ├── socks.html
    │   │       │   ├── trojan.html
    │   │       │   ├── vless.html
    │   │       │   └── vmess.html
    │   │       ├── sniffing.html
    │   │       ├── stream
    │   │       │   ├── stream_grpc.html
    │   │       │   ├── stream_http.html
    │   │       │   ├── stream_kcp.html
    │   │       │   ├── stream_quic.html
    │   │       │   ├── stream_settings.html
    │   │       │   ├── stream_tcp.html
    │   │       │   └── stream_ws.html
    │   │       └── tls_settings.html
    │   │   ├── inbound_info_modal.html
    │   │   ├── inbound_modal.html
    │   │   ├── inbounds.html
    │   │   ├── index.html
    │   │   └── setting.html
    ├── job
    │   ├── check_inbound_job.go
    │   ├── check_xray_running_job.go
    │   ├── stats_notify_job.go
    │   └── xray_traffic_job.go
    ├── network
    │   ├── auto_https_listener.go
    │   └── autp_https_conn.go
    ├── service
    │   ├── config.json
    │   ├── inbound.go
    │   ├── panel.go
    │   ├── server.go
    │   ├── setting.go
    │   ├── user.go
    │   └── xray.go
    ├── session
    │   └── session.go
    ├── translation
    │   ├── translate.en_US.toml
    │   ├── translate.zh_Hans.toml
    │   └── translate.zh_Hant.toml
    └── web.go
├── x-ui.service
├── x-ui.sh
└── xray
    ├── config.go
    ├── inbound.go
    ├── process.go
    └── traffic.go


/.github/ISSUE_TEMPLATE/------.md:
--------------------------------------------------------------------------------
 1 | ---
 2 | name: 提问题点这里
 3 | about: issue 模板
 4 | title: ''
 5 | labels: ''
 6 | assignees: ''
 7 | 
 8 | ---
 9 | 
10 | 任何由于自己的配置错误导致的情况,请自行解决,issues 只用于解决面板自身的 bug
11 | 
12 | 如果你确定面板的功能实现有 bug,请尽可能提供更多更精确的描述信息、复现方法与复现结果等等,而不是草草一句话了事,这对于问题的解决没有帮助
13 | 
14 | 提问的艺术: https://github.com/ryanhanwu/How-To-Ask-Questions-The-Smart-Way/blob/master/README-zh_CN.md
15 | 


--------------------------------------------------------------------------------
/.github/workflows/release.yml:
--------------------------------------------------------------------------------
  1 | name: Release X-ui
  2 | on:
  3 |   push:
  4 |     tags:
  5 |       - 0.*
  6 |   workflow_dispatch:
  7 | 
  8 | jobs:
  9 |   release:
 10 |     runs-on: ubuntu-latest
 11 |     outputs:
 12 |       upload_url: ${{ steps.create_release.outputs.upload_url }}
 13 |     steps:
 14 |       - name: Create Release
 15 |         id: create_release
 16 |         uses: actions/create-release@v1
 17 |         env:
 18 |           GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
 19 |         with:
 20 |           tag_name: ${{ github.ref }}
 21 |           release_name: ${{ github.ref }}
 22 |           draft: true
 23 |           prerelease: true
 24 |   linuxamd64build:
 25 |     name: build x-ui amd64 version
 26 |     needs: release
 27 |     runs-on: ubuntu-latest
 28 |     steps:
 29 |       - uses: actions/checkout@v2
 30 |       - name: Set up Go
 31 |         uses: actions/setup-go@v2
 32 |         with:
 33 |           go-version: 1.18
 34 |       - name: build linux amd64 version
 35 |         run: |
 36 |           CGO_ENABLED=1 GOOS=linux GOARCH=amd64 go build -o xui-release -v main.go
 37 |           mkdir x-ui
 38 |           cp xui-release x-ui/xui-release
 39 |           cp x-ui.service x-ui/x-ui.service
 40 |           cp x-ui.sh x-ui/x-ui.sh
 41 |           cd x-ui
 42 |           mv xui-release x-ui
 43 |           mkdir bin
 44 |           cd bin
 45 |           wget https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-64.zip
 46 |           unzip Xray-linux-64.zip
 47 |           rm -f Xray-linux-64.zip geoip.dat geosite.dat
 48 |           wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
 49 |           wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
 50 |           mv xray xray-linux-amd64
 51 |           cd ..
 52 |           cd ..
 53 |       - name: package
 54 |         run: tar -zcvf x-ui-linux-amd64.tar.gz x-ui
 55 |       - name: upload
 56 |         uses: actions/upload-release-asset@v1
 57 |         env:
 58 |           GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
 59 |         with:
 60 |           upload_url: ${{ needs.release.outputs.upload_url }}
 61 |           asset_path: x-ui-linux-amd64.tar.gz
 62 |           asset_name: x-ui-linux-amd64.tar.gz
 63 |           asset_content_type: application/gzip
 64 |   linuxarm64build:
 65 |     name: build x-ui arm64 version
 66 |     needs: release
 67 |     runs-on: ubuntu-latest
 68 |     steps:
 69 |       - uses: actions/checkout@v2
 70 |       - name: Set up Go
 71 |         uses: actions/setup-go@v2
 72 |         with:
 73 |           go-version: 1.18
 74 |       - name: build linux arm64 version
 75 |         run: |
 76 |           sudo apt-get update
 77 |           sudo apt install gcc-aarch64-linux-gnu
 78 |           CGO_ENABLED=1 GOOS=linux GOARCH=arm64 CC=aarch64-linux-gnu-gcc go build -o xui-release -v main.go
 79 |           mkdir x-ui
 80 |           cp xui-release x-ui/xui-release
 81 |           cp x-ui.service x-ui/x-ui.service
 82 |           cp x-ui.sh x-ui/x-ui.sh
 83 |           cd x-ui
 84 |           mv xui-release x-ui
 85 |           mkdir bin
 86 |           cd bin
 87 |           wget https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-arm64-v8a.zip
 88 |           unzip Xray-linux-arm64-v8a.zip
 89 |           rm -f Xray-linux-arm64-v8a.zip geoip.dat geosite.dat
 90 |           wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
 91 |           wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
 92 |           mv xray xray-linux-arm64
 93 |           cd ..
 94 |           cd ..
 95 |       - name: package
 96 |         run: tar -zcvf x-ui-linux-arm64.tar.gz x-ui
 97 |       - name: upload
 98 |         uses: actions/upload-release-asset@v1
 99 |         env:
100 |           GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
101 |         with:
102 |           upload_url: ${{ needs.release.outputs.upload_url }}
103 |           asset_path: x-ui-linux-arm64.tar.gz
104 |           asset_name: x-ui-linux-arm64.tar.gz
105 |           asset_content_type: application/gzip
106 |   linuxs390xbuild:
107 |     name: build x-ui s390x version
108 |     needs: release
109 |     runs-on: ubuntu-latest
110 |     steps:
111 |       - uses: actions/checkout@v2
112 |       - name: Set up Go
113 |         uses: actions/setup-go@v2
114 |         with:
115 |           go-version: 1.18
116 |       - name: build linux s390x version
117 |         run: |
118 |           sudo apt-get update
119 |           sudo apt install gcc-s390x-linux-gnu -y
120 |           CGO_ENABLED=1 GOOS=linux GOARCH=s390x CC=s390x-linux-gnu-gcc go build -o xui-release -v main.go
121 |           mkdir x-ui
122 |           cp xui-release x-ui/xui-release
123 |           cp x-ui.service x-ui/x-ui.service
124 |           cp x-ui.sh x-ui/x-ui.sh
125 |           cd x-ui
126 |           mv xui-release x-ui
127 |           mkdir bin
128 |           cd bin
129 |           wget https://github.com/XTLS/Xray-core/releases/latest/download/Xray-linux-s390x.zip
130 |           unzip Xray-linux-s390x.zip
131 |           rm -f Xray-linux-s390x.zip geoip.dat geosite.dat
132 |           wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
133 |           wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
134 |           mv xray xray-linux-s390x
135 |           cd ..
136 |           cd ..
137 |       - name: package
138 |         run: tar -zcvf x-ui-linux-s390x.tar.gz x-ui
139 |       - name: upload
140 |         uses: actions/upload-release-asset@v1
141 |         env:
142 |           GITHUB_TOKEN: ${{ secrets.GAYHUB_TOKEN }}
143 |         with:
144 |           upload_url: ${{ needs.release.outputs.upload_url }}
145 |           asset_path: x-ui-linux-s390x.tar.gz
146 |           asset_name: x-ui-linux-s390x.tar.gz
147 |           asset_content_type: application/gzip


--------------------------------------------------------------------------------
/.gitignore:
--------------------------------------------------------------------------------
 1 | .idea
 2 | tmp
 3 | bin/xray-darwin-arm64
 4 | bin/config.json
 5 | dist/
 6 | x-ui-*.tar.gz
 7 | /x-ui
 8 | /release.sh
 9 | .sync*
10 | main
11 | 


--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
 1 | FROM golang:latest AS builder
 2 | WORKDIR /root
 3 | COPY . .
 4 | RUN go build main.go
 5 | 
 6 | 
 7 | FROM debian:11-slim
 8 | RUN apt-get update && apt-get install -y --no-install-recommends -y ca-certificates \
 9 |     && apt-get clean && rm -rf /var/lib/apt/lists/* /tmp/* /var/tmp/*
10 | WORKDIR /root
11 | COPY --from=builder  /root/main /root/x-ui
12 | COPY bin/. /root/bin/.
13 | VOLUME [ "/etc/x-ui" ]
14 | CMD [ "./x-ui" ]
15 | 


--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
  1 | # x-ui
  2 | 
  3 | 支持多协议多用户的 xray 面板
  4 | 
  5 | # 功能介绍
  6 | 
  7 | - 系统状态监控
  8 | - 支持多用户多协议,网页可视化操作
  9 | - 支持的协议:vmess、vless、trojan、shadowsocks、dokodemo-door、socks、http
 10 | - 支持配置更多传输配置
 11 | - 流量统计,限制流量,限制到期时间
 12 | - 可自定义 xray 配置模板
 13 | - 支持 https 访问面板(自备域名 + ssl 证书)
 14 | - 支持一键SSL证书申请且自动续签
 15 | - 更多高级配置项,详见面板
 16 | 
 17 | # 安装&升级
 18 | 
 19 | ```
 20 | bash <(curl -Ls https://raw.githubusercontent.com/vaxilu/x-ui/master/install.sh)
 21 | ```
 22 | 
 23 | ## 手动安装&升级
 24 | 
 25 | 1. 首先从 https://github.com/vaxilu/x-ui/releases 下载最新的压缩包,一般选择 `amd64`架构
 26 | 2. 然后将这个压缩包上传到服务器的 `/root/`目录下,并使用 `root`用户登录服务器
 27 | 
 28 | > 如果你的服务器 cpu 架构不是 `amd64`,自行将命令中的 `amd64`替换为其他架构
 29 | 
 30 | ```
 31 | cd /root/
 32 | rm x-ui/ /usr/local/x-ui/ /usr/bin/x-ui -rf
 33 | tar zxvf x-ui-linux-amd64.tar.gz
 34 | chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh
 35 | cp x-ui/x-ui.sh /usr/bin/x-ui
 36 | cp -f x-ui/x-ui.service /etc/systemd/system/
 37 | mv x-ui/ /usr/local/
 38 | systemctl daemon-reload
 39 | systemctl enable x-ui
 40 | systemctl restart x-ui
 41 | ```
 42 | 
 43 | ## 使用docker安装
 44 | 
 45 | > 此 docker 教程与 docker 镜像由[Chasing66](https://github.com/Chasing66)提供
 46 | 
 47 | 1. 安装docker
 48 | 
 49 | ```shell
 50 | curl -fsSL https://get.docker.com | sh
 51 | ```
 52 | 
 53 | 2. 安装x-ui
 54 | 
 55 | ```shell
 56 | mkdir x-ui && cd x-ui
 57 | docker run -itd --network=host \
 58 |     -v $PWD/db/:/etc/x-ui/ \
 59 |     -v $PWD/cert/:/root/cert/ \
 60 |     --name x-ui --restart=unless-stopped \
 61 |     enwaiax/x-ui:latest
 62 | ```
 63 | 
 64 | > Build 自己的镜像
 65 | 
 66 | ```shell
 67 | docker build -t x-ui .
 68 | ```
 69 | 
 70 | ## SSL证书申请
 71 | 
 72 | > 此功能与教程由[FranzKafkaYu](https://github.com/FranzKafkaYu)提供
 73 | 
 74 | 脚本内置SSL证书申请功能,使用该脚本申请证书,需满足以下条件:
 75 | 
 76 | - 知晓Cloudflare 注册邮箱
 77 | - 知晓Cloudflare Global API Key
 78 | - 域名已通过cloudflare进行解析到当前服务器
 79 | 
 80 | 获取Cloudflare Global API Key的方法:
 81 |     ![](media/bda84fbc2ede834deaba1c173a932223.png)
 82 |     ![](media/d13ffd6a73f938d1037d0708e31433bf.png)
 83 | 
 84 | 使用时只需输入 `域名`, `邮箱`, `API KEY`即可,示意图如下:
 85 |         ![](media/2022-04-04_141259.png)
 86 | 
 87 | 注意事项:
 88 | 
 89 | - 该脚本使用DNS API进行证书申请
 90 | - 默认使用Let'sEncrypt作为CA方
 91 | - 证书安装目录为/root/cert目录
 92 | - 本脚本申请证书均为泛域名证书
 93 | 
 94 | ## Tg机器人使用(开发中,暂不可使用)
 95 | 
 96 | > 此功能与教程由[FranzKafkaYu](https://github.com/FranzKafkaYu)提供
 97 | 
 98 | X-UI支持通过Tg机器人实现每日流量通知,面板登录提醒等功能,使用Tg机器人,需要自行申请
 99 | 具体申请教程可以参考[博客链接](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html)
100 | 使用说明:在面板后台设置机器人相关参数,具体包括
101 | 
102 | - Tg机器人Token
103 | - Tg机器人ChatId
104 | - Tg机器人周期运行时间,采用crontab语法  
105 | 
106 | 参考语法:
107 | - 30 * * * * * //每一分的第30s进行通知
108 | - @hourly      //每小时通知
109 | - @daily       //每天通知(凌晨零点整)
110 | - @every 8h    //每8小时通知  
111 | 
112 | TG通知内容:
113 | - 节点流量使用
114 | - 面板登录提醒
115 | - 节点到期提醒
116 | - 流量预警提醒  
117 | 
118 | 更多功能规划中...
119 | ## 建议系统
120 | 
121 | - CentOS 7+
122 | - Ubuntu 16+
123 | - Debian 8+
124 | 
125 | # 常见问题
126 | 
127 | ## 从 v2-ui 迁移
128 | 
129 | 首先在安装了 v2-ui 的服务器上安装最新版 x-ui,然后使用以下命令进行迁移,将迁移本机 v2-ui 的 `所有 inbound 账号数据`至 x-ui,`面板设置和用户名密码不会迁移`
130 | 
131 | > 迁移成功后请 `关闭 v2-ui`并且 `重启 x-ui`,否则 v2-ui 的 inbound 会与 x-ui 的 inbound 会产生 `端口冲突`
132 | 
133 | ```
134 | x-ui v2-ui
135 | ```
136 | 
137 | ## issue 关闭
138 | 
139 | 各种小白问题看得血压很高
140 | 
141 | ## Stargazers over time
142 | 
143 | [![Stargazers over time](https://starchart.cc/vaxilu/x-ui.svg)](https://starchart.cc/vaxilu/x-ui)
144 | 


--------------------------------------------------------------------------------
/bin/geoip.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaxilu/x-ui/9c1be8c57a53953b47ee7c09a93554e73816f907/bin/geoip.dat


--------------------------------------------------------------------------------
/bin/geosite.dat:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaxilu/x-ui/9c1be8c57a53953b47ee7c09a93554e73816f907/bin/geosite.dat


--------------------------------------------------------------------------------
/bin/xray-linux-amd64:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaxilu/x-ui/9c1be8c57a53953b47ee7c09a93554e73816f907/bin/xray-linux-amd64


--------------------------------------------------------------------------------
/bin/xray-linux-arm64:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaxilu/x-ui/9c1be8c57a53953b47ee7c09a93554e73816f907/bin/xray-linux-arm64


--------------------------------------------------------------------------------
/config/config.go:
--------------------------------------------------------------------------------
 1 | package config
 2 | 
 3 | import (
 4 | 	_ "embed"
 5 | 	"fmt"
 6 | 	"os"
 7 | 	"strings"
 8 | )
 9 | 
10 | //go:embed version
11 | var version string
12 | 
13 | //go:embed name
14 | var name string
15 | 
16 | type LogLevel string
17 | 
18 | const (
19 | 	Debug LogLevel = "debug"
20 | 	Info  LogLevel = "info"
21 | 	Warn  LogLevel = "warn"
22 | 	Error LogLevel = "error"
23 | )
24 | 
25 | func GetVersion() string {
26 | 	return strings.TrimSpace(version)
27 | }
28 | 
29 | func GetName() string {
30 | 	return strings.TrimSpace(name)
31 | }
32 | 
33 | func GetLogLevel() LogLevel {
34 | 	if IsDebug() {
35 | 		return Debug
36 | 	}
37 | 	logLevel := os.Getenv("XUI_LOG_LEVEL")
38 | 	if logLevel == "" {
39 | 		return Info
40 | 	}
41 | 	return LogLevel(logLevel)
42 | }
43 | 
44 | func IsDebug() bool {
45 | 	return os.Getenv("XUI_DEBUG") == "true"
46 | }
47 | 
48 | func GetDBPath() string {
49 | 	return fmt.Sprintf("/etc/%s/%s.db", GetName(), GetName())
50 | }
51 | 


--------------------------------------------------------------------------------
/config/name:
--------------------------------------------------------------------------------
1 | x-ui


--------------------------------------------------------------------------------
/config/version:
--------------------------------------------------------------------------------
1 | 0.3.2


--------------------------------------------------------------------------------
/database/db.go:
--------------------------------------------------------------------------------
 1 | package database
 2 | 
 3 | import (
 4 | 	"gorm.io/driver/sqlite"
 5 | 	"gorm.io/gorm"
 6 | 	"gorm.io/gorm/logger"
 7 | 	"io/fs"
 8 | 	"os"
 9 | 	"path"
10 | 	"x-ui/config"
11 | 	"x-ui/database/model"
12 | )
13 | 
14 | var db *gorm.DB
15 | 
16 | func initUser() error {
17 | 	err := db.AutoMigrate(&model.User{})
18 | 	if err != nil {
19 | 		return err
20 | 	}
21 | 	var count int64
22 | 	err = db.Model(&model.User{}).Count(&count).Error
23 | 	if err != nil {
24 | 		return err
25 | 	}
26 | 	if count == 0 {
27 | 		user := &model.User{
28 | 			Username: "admin",
29 | 			Password: "admin",
30 | 		}
31 | 		return db.Create(user).Error
32 | 	}
33 | 	return nil
34 | }
35 | 
36 | func initInbound() error {
37 | 	return db.AutoMigrate(&model.Inbound{})
38 | }
39 | 
40 | func initSetting() error {
41 | 	return db.AutoMigrate(&model.Setting{})
42 | }
43 | 
44 | func InitDB(dbPath string) error {
45 | 	dir := path.Dir(dbPath)
46 | 	err := os.MkdirAll(dir, fs.ModeDir)
47 | 	if err != nil {
48 | 		return err
49 | 	}
50 | 
51 | 	var gormLogger logger.Interface
52 | 
53 | 	if config.IsDebug() {
54 | 		gormLogger = logger.Default
55 | 	} else {
56 | 		gormLogger = logger.Discard
57 | 	}
58 | 
59 | 	c := &gorm.Config{
60 | 		Logger: gormLogger,
61 | 	}
62 | 	db, err = gorm.Open(sqlite.Open(dbPath), c)
63 | 	if err != nil {
64 | 		return err
65 | 	}
66 | 
67 | 	err = initUser()
68 | 	if err != nil {
69 | 		return err
70 | 	}
71 | 	err = initInbound()
72 | 	if err != nil {
73 | 		return err
74 | 	}
75 | 	err = initSetting()
76 | 	if err != nil {
77 | 		return err
78 | 	}
79 | 
80 | 	return nil
81 | }
82 | 
83 | func GetDB() *gorm.DB {
84 | 	return db
85 | }
86 | 
87 | func IsNotFound(err error) bool {
88 | 	return err == gorm.ErrRecordNotFound
89 | }
90 | 


--------------------------------------------------------------------------------
/database/model/model.go:
--------------------------------------------------------------------------------
 1 | package model
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"x-ui/util/json_util"
 6 | 	"x-ui/xray"
 7 | )
 8 | 
 9 | type Protocol string
10 | 
11 | const (
12 | 	VMess       Protocol = "vmess"
13 | 	VLESS       Protocol = "vless"
14 | 	Dokodemo    Protocol = "Dokodemo-door"
15 | 	Http        Protocol = "http"
16 | 	Trojan      Protocol = "trojan"
17 | 	Shadowsocks Protocol = "shadowsocks"
18 | )
19 | 
20 | type User struct {
21 | 	Id       int    `json:"id" gorm:"primaryKey;autoIncrement"`
22 | 	Username string `json:"username"`
23 | 	Password string `json:"password"`
24 | }
25 | 
26 | type Inbound struct {
27 | 	Id         int    `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
28 | 	UserId     int    `json:"-"`
29 | 	Up         int64  `json:"up" form:"up"`
30 | 	Down       int64  `json:"down" form:"down"`
31 | 	Total      int64  `json:"total" form:"total"`
32 | 	Remark     string `json:"remark" form:"remark"`
33 | 	Enable     bool   `json:"enable" form:"enable"`
34 | 	ExpiryTime int64  `json:"expiryTime" form:"expiryTime"`
35 | 
36 | 	// config part
37 | 	Listen         string   `json:"listen" form:"listen"`
38 | 	Port           int      `json:"port" form:"port" gorm:"unique"`
39 | 	Protocol       Protocol `json:"protocol" form:"protocol"`
40 | 	Settings       string   `json:"settings" form:"settings"`
41 | 	StreamSettings string   `json:"streamSettings" form:"streamSettings"`
42 | 	Tag            string   `json:"tag" form:"tag" gorm:"unique"`
43 | 	Sniffing       string   `json:"sniffing" form:"sniffing"`
44 | }
45 | 
46 | func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
47 | 	listen := i.Listen
48 | 	if listen != "" {
49 | 		listen = fmt.Sprintf("\"%v\"", listen)
50 | 	}
51 | 	return &xray.InboundConfig{
52 | 		Listen:         json_util.RawMessage(listen),
53 | 		Port:           i.Port,
54 | 		Protocol:       string(i.Protocol),
55 | 		Settings:       json_util.RawMessage(i.Settings),
56 | 		StreamSettings: json_util.RawMessage(i.StreamSettings),
57 | 		Tag:            i.Tag,
58 | 		Sniffing:       json_util.RawMessage(i.Sniffing),
59 | 	}
60 | }
61 | 
62 | type Setting struct {
63 | 	Id    int    `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
64 | 	Key   string `json:"key" form:"key"`
65 | 	Value string `json:"value" form:"value"`
66 | }
67 | 


--------------------------------------------------------------------------------
/go.mod:
--------------------------------------------------------------------------------
 1 | module x-ui
 2 | 
 3 | go 1.16
 4 | 
 5 | require (
 6 | 	github.com/BurntSushi/toml v0.3.1
 7 | 	github.com/StackExchange/wmi v0.0.0-20210224194228-fe8f1750fd46 // indirect
 8 | 	github.com/Workiva/go-datastructures v1.0.53
 9 | 	github.com/gin-contrib/sessions v0.0.3
10 | 	github.com/gin-gonic/gin v1.7.1
11 | 	github.com/go-ole/go-ole v1.2.5 // indirect
12 | 	github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1
13 | 	github.com/nicksnyder/go-i18n/v2 v2.1.2
14 | 	github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
15 | 	github.com/robfig/cron/v3 v3.0.1
16 | 	github.com/shirou/gopsutil v3.21.3+incompatible
17 | 	github.com/tklauser/go-sysconf v0.3.5 // indirect
18 | 	github.com/xtls/xray-core v1.4.2
19 | 	go.uber.org/atomic v1.7.0
20 | 	golang.org/x/sys v0.0.0-20210511113859-b0526f3d8744 // indirect
21 | 	golang.org/x/text v0.3.6
22 | 	google.golang.org/grpc v1.38.0
23 | 	gopkg.in/yaml.v2 v2.4.0 // indirect
24 | 	gorm.io/driver/sqlite v1.1.4
25 | 	gorm.io/gorm v1.21.9
26 | )
27 | 


--------------------------------------------------------------------------------
/install.sh:
--------------------------------------------------------------------------------
  1 | #!/bin/bash
  2 | 
  3 | red='\033[0;31m'
  4 | green='\033[0;32m'
  5 | yellow='\033[0;33m'
  6 | plain='\033[0m'
  7 | 
  8 | cur_dir=$(pwd)
  9 | 
 10 | # check root
 11 | [[ $EUID -ne 0 ]] && echo -e "${red}错误:${plain} 必须使用root用户运行此脚本!\n" && exit 1
 12 | 
 13 | # check os
 14 | if [[ -f /etc/redhat-release ]]; then
 15 |     release="centos"
 16 | elif cat /etc/issue | grep -Eqi "debian"; then
 17 |     release="debian"
 18 | elif cat /etc/issue | grep -Eqi "ubuntu"; then
 19 |     release="ubuntu"
 20 | elif cat /etc/issue | grep -Eqi "centos|red hat|redhat"; then
 21 |     release="centos"
 22 | elif cat /proc/version | grep -Eqi "debian"; then
 23 |     release="debian"
 24 | elif cat /proc/version | grep -Eqi "ubuntu"; then
 25 |     release="ubuntu"
 26 | elif cat /proc/version | grep -Eqi "centos|red hat|redhat"; then
 27 |     release="centos"
 28 | else
 29 |     echo -e "${red}未检测到系统版本,请联系脚本作者!${plain}\n" && exit 1
 30 | fi
 31 | 
 32 | arch=$(arch)
 33 | 
 34 | if [[ $arch == "x86_64" || $arch == "x64" || $arch == "amd64" ]]; then
 35 |     arch="amd64"
 36 | elif [[ $arch == "aarch64" || $arch == "arm64" ]]; then
 37 |     arch="arm64"
 38 | elif [[ $arch == "s390x" ]]; then
 39 |     arch="s390x"
 40 | else
 41 |     arch="amd64"
 42 |     echo -e "${red}检测架构失败,使用默认架构: ${arch}${plain}"
 43 | fi
 44 | 
 45 | echo "架构: ${arch}"
 46 | 
 47 | if [ $(getconf WORD_BIT) != '32' ] && [ $(getconf LONG_BIT) != '64' ]; then
 48 |     echo "本软件不支持 32 位系统(x86),请使用 64 位系统(x86_64),如果检测有误,请联系作者"
 49 |     exit -1
 50 | fi
 51 | 
 52 | os_version=""
 53 | 
 54 | # os version
 55 | if [[ -f /etc/os-release ]]; then
 56 |     os_version=$(awk -F'[= ."]' '/VERSION_ID/{print $3}' /etc/os-release)
 57 | fi
 58 | if [[ -z "$os_version" && -f /etc/lsb-release ]]; then
 59 |     os_version=$(awk -F'[= ."]+' '/DISTRIB_RELEASE/{print $2}' /etc/lsb-release)
 60 | fi
 61 | 
 62 | if [[ x"${release}" == x"centos" ]]; then
 63 |     if [[ ${os_version} -le 6 ]]; then
 64 |         echo -e "${red}请使用 CentOS 7 或更高版本的系统!${plain}\n" && exit 1
 65 |     fi
 66 | elif [[ x"${release}" == x"ubuntu" ]]; then
 67 |     if [[ ${os_version} -lt 16 ]]; then
 68 |         echo -e "${red}请使用 Ubuntu 16 或更高版本的系统!${plain}\n" && exit 1
 69 |     fi
 70 | elif [[ x"${release}" == x"debian" ]]; then
 71 |     if [[ ${os_version} -lt 8 ]]; then
 72 |         echo -e "${red}请使用 Debian 8 或更高版本的系统!${plain}\n" && exit 1
 73 |     fi
 74 | fi
 75 | 
 76 | install_base() {
 77 |     if [[ x"${release}" == x"centos" ]]; then
 78 |         yum install wget curl tar -y
 79 |     else
 80 |         apt install wget curl tar -y
 81 |     fi
 82 | }
 83 | 
 84 | #This function will be called when user installed x-ui out of sercurity
 85 | config_after_install() {
 86 |     echo -e "${yellow}出于安全考虑,安装/更新完成后需要强制修改端口与账户密码${plain}"
 87 |     read -p "确认是否继续?[y/n]": config_confirm
 88 |     if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then
 89 |         read -p "请设置您的账户名:" config_account
 90 |         echo -e "${yellow}您的账户名将设定为:${config_account}${plain}"
 91 |         read -p "请设置您的账户密码:" config_password
 92 |         echo -e "${yellow}您的账户密码将设定为:${config_password}${plain}"
 93 |         read -p "请设置面板访问端口:" config_port
 94 |         echo -e "${yellow}您的面板访问端口将设定为:${config_port}${plain}"
 95 |         echo -e "${yellow}确认设定,设定中${plain}"
 96 |         /usr/local/x-ui/x-ui setting -username ${config_account} -password ${config_password}
 97 |         echo -e "${yellow}账户密码设定完成${plain}"
 98 |         /usr/local/x-ui/x-ui setting -port ${config_port}
 99 |         echo -e "${yellow}面板端口设定完成${plain}"
100 |     else
101 |         echo -e "${red}已取消,所有设置项均为默认设置,请及时修改${plain}"
102 |     fi
103 | }
104 | 
105 | install_x-ui() {
106 |     systemctl stop x-ui
107 |     cd /usr/local/
108 | 
109 |     if [ $# == 0 ]; then
110 |         last_version=$(curl -Ls "https://api.github.com/repos/vaxilu/x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
111 |         if [[ ! -n "$last_version" ]]; then
112 |             echo -e "${red}检测 x-ui 版本失败,可能是超出 Github API 限制,请稍后再试,或手动指定 x-ui 版本安装${plain}"
113 |             exit 1
114 |         fi
115 |         echo -e "检测到 x-ui 最新版本:${last_version},开始安装"
116 |         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
117 |         if [[ $? -ne 0 ]]; then
118 |             echo -e "${red}下载 x-ui 失败,请确保你的服务器能够下载 Github 的文件${plain}"
119 |             exit 1
120 |         fi
121 |     else
122 |         last_version=$1
123 |         url="https://github.com/vaxilu/x-ui/releases/download/${last_version}/x-ui-linux-${arch}.tar.gz"
124 |         echo -e "开始安装 x-ui v$1"
125 |         wget -N --no-check-certificate -O /usr/local/x-ui-linux-${arch}.tar.gz ${url}
126 |         if [[ $? -ne 0 ]]; then
127 |             echo -e "${red}下载 x-ui v$1 失败,请确保此版本存在${plain}"
128 |             exit 1
129 |         fi
130 |     fi
131 | 
132 |     if [[ -e /usr/local/x-ui/ ]]; then
133 |         rm /usr/local/x-ui/ -rf
134 |     fi
135 | 
136 |     tar zxvf x-ui-linux-${arch}.tar.gz
137 |     rm x-ui-linux-${arch}.tar.gz -f
138 |     cd x-ui
139 |     chmod +x x-ui bin/xray-linux-${arch}
140 |     cp -f x-ui.service /etc/systemd/system/
141 |     wget --no-check-certificate -O /usr/bin/x-ui https://raw.githubusercontent.com/vaxilu/x-ui/main/x-ui.sh
142 |     chmod +x /usr/local/x-ui/x-ui.sh
143 |     chmod +x /usr/bin/x-ui
144 |     config_after_install
145 |     #echo -e "如果是全新安装,默认网页端口为 ${green}54321${plain},用户名和密码默认都是 ${green}admin${plain}"
146 |     #echo -e "请自行确保此端口没有被其他程序占用,${yellow}并且确保 54321 端口已放行${plain}"
147 |     #    echo -e "若想将 54321 修改为其它端口,输入 x-ui 命令进行修改,同样也要确保你修改的端口也是放行的"
148 |     #echo -e ""
149 |     #echo -e "如果是更新面板,则按你之前的方式访问面板"
150 |     #echo -e ""
151 |     systemctl daemon-reload
152 |     systemctl enable x-ui
153 |     systemctl start x-ui
154 |     echo -e "${green}x-ui v${last_version}${plain} 安装完成,面板已启动,"
155 |     echo -e ""
156 |     echo -e "x-ui 管理脚本使用方法: "
157 |     echo -e "----------------------------------------------"
158 |     echo -e "x-ui              - 显示管理菜单 (功能更多)"
159 |     echo -e "x-ui start        - 启动 x-ui 面板"
160 |     echo -e "x-ui stop         - 停止 x-ui 面板"
161 |     echo -e "x-ui restart      - 重启 x-ui 面板"
162 |     echo -e "x-ui status       - 查看 x-ui 状态"
163 |     echo -e "x-ui enable       - 设置 x-ui 开机自启"
164 |     echo -e "x-ui disable      - 取消 x-ui 开机自启"
165 |     echo -e "x-ui log          - 查看 x-ui 日志"
166 |     echo -e "x-ui v2-ui        - 迁移本机器的 v2-ui 账号数据至 x-ui"
167 |     echo -e "x-ui update       - 更新 x-ui 面板"
168 |     echo -e "x-ui install      - 安装 x-ui 面板"
169 |     echo -e "x-ui uninstall    - 卸载 x-ui 面板"
170 |     echo -e "----------------------------------------------"
171 | }
172 | 
173 | echo -e "${green}开始安装${plain}"
174 | install_base
175 | install_x-ui $1
176 | 


--------------------------------------------------------------------------------
/logger/logger.go:
--------------------------------------------------------------------------------
 1 | package logger
 2 | 
 3 | import (
 4 | 	"github.com/op/go-logging"
 5 | 	"os"
 6 | )
 7 | 
 8 | var logger *logging.Logger
 9 | 
10 | func init() {
11 | 	InitLogger(logging.INFO)
12 | }
13 | 
14 | func InitLogger(level logging.Level) {
15 | 	format := logging.MustStringFormatter(
16 | 		`%{time:2006/01/02 15:04:05} %{level} - %{message}`,
17 | 	)
18 | 	newLogger := logging.MustGetLogger("x-ui")
19 | 	backend := logging.NewLogBackend(os.Stderr, "", 0)
20 | 	backendFormatter := logging.NewBackendFormatter(backend, format)
21 | 	backendLeveled := logging.AddModuleLevel(backendFormatter)
22 | 	backendLeveled.SetLevel(level, "")
23 | 	newLogger.SetBackend(backendLeveled)
24 | 
25 | 	logger = newLogger
26 | }
27 | 
28 | func Debug(args ...interface{}) {
29 | 	logger.Debug(args...)
30 | }
31 | 
32 | func Debugf(format string, args ...interface{}) {
33 | 	logger.Debugf(format, args...)
34 | }
35 | 
36 | func Info(args ...interface{}) {
37 | 	logger.Info(args...)
38 | }
39 | 
40 | func Infof(format string, args ...interface{}) {
41 | 	logger.Infof(format, args...)
42 | }
43 | 
44 | func Warning(args ...interface{}) {
45 | 	logger.Warning(args...)
46 | }
47 | 
48 | func Warningf(format string, args ...interface{}) {
49 | 	logger.Warningf(format, args...)
50 | }
51 | 
52 | func Error(args ...interface{}) {
53 | 	logger.Error(args...)
54 | }
55 | 
56 | func Errorf(format string, args ...interface{}) {
57 | 	logger.Errorf(format, args...)
58 | }
59 | 


--------------------------------------------------------------------------------
/main.go:
--------------------------------------------------------------------------------
  1 | package main
  2 | 
  3 | import (
  4 | 	"flag"
  5 | 	"fmt"
  6 | 	"log"
  7 | 	"os"
  8 | 	"os/signal"
  9 | 	"syscall"
 10 | 	_ "unsafe"
 11 | 	"x-ui/config"
 12 | 	"x-ui/database"
 13 | 	"x-ui/logger"
 14 | 	"x-ui/v2ui"
 15 | 	"x-ui/web"
 16 | 	"x-ui/web/global"
 17 | 	"x-ui/web/service"
 18 | 
 19 | 	"github.com/op/go-logging"
 20 | )
 21 | 
 22 | func runWebServer() {
 23 | 	log.Printf("%v %v", config.GetName(), config.GetVersion())
 24 | 
 25 | 	switch config.GetLogLevel() {
 26 | 	case config.Debug:
 27 | 		logger.InitLogger(logging.DEBUG)
 28 | 	case config.Info:
 29 | 		logger.InitLogger(logging.INFO)
 30 | 	case config.Warn:
 31 | 		logger.InitLogger(logging.WARNING)
 32 | 	case config.Error:
 33 | 		logger.InitLogger(logging.ERROR)
 34 | 	default:
 35 | 		log.Fatal("unknown log level:", config.GetLogLevel())
 36 | 	}
 37 | 
 38 | 	err := database.InitDB(config.GetDBPath())
 39 | 	if err != nil {
 40 | 		log.Fatal(err)
 41 | 	}
 42 | 
 43 | 	var server *web.Server
 44 | 
 45 | 	server = web.NewServer()
 46 | 	global.SetWebServer(server)
 47 | 	err = server.Start()
 48 | 	if err != nil {
 49 | 		log.Println(err)
 50 | 		return
 51 | 	}
 52 | 
 53 | 	sigCh := make(chan os.Signal, 1)
 54 | 	//信号量捕获处理
 55 | 	signal.Notify(sigCh, syscall.SIGHUP, syscall.SIGTERM, syscall.SIGKILL)
 56 | 	for {
 57 | 		sig := <-sigCh
 58 | 
 59 | 		switch sig {
 60 | 		case syscall.SIGHUP:
 61 | 			err := server.Stop()
 62 | 			if err != nil {
 63 | 				logger.Warning("stop server err:", err)
 64 | 			}
 65 | 			server = web.NewServer()
 66 | 			global.SetWebServer(server)
 67 | 			err = server.Start()
 68 | 			if err != nil {
 69 | 				log.Println(err)
 70 | 				return
 71 | 			}
 72 | 		default:
 73 | 			server.Stop()
 74 | 			return
 75 | 		}
 76 | 	}
 77 | }
 78 | 
 79 | func resetSetting() {
 80 | 	err := database.InitDB(config.GetDBPath())
 81 | 	if err != nil {
 82 | 		fmt.Println(err)
 83 | 		return
 84 | 	}
 85 | 
 86 | 	settingService := service.SettingService{}
 87 | 	err = settingService.ResetSettings()
 88 | 	if err != nil {
 89 | 		fmt.Println("reset setting failed:", err)
 90 | 	} else {
 91 | 		fmt.Println("reset setting success")
 92 | 	}
 93 | }
 94 | 
 95 | func showSetting(show bool) {
 96 | 	if show {
 97 | 		settingService := service.SettingService{}
 98 | 		port, err := settingService.GetPort()
 99 | 		if err != nil {
100 | 			fmt.Println("get current port fialed,error info:", err)
101 | 		}
102 | 		userService := service.UserService{}
103 | 		userModel, err := userService.GetFirstUser()
104 | 		if err != nil {
105 | 			fmt.Println("get current user info failed,error info:", err)
106 | 		}
107 | 		username := userModel.Username
108 | 		userpasswd := userModel.Password
109 | 		if (username == "") || (userpasswd == "") {
110 | 			fmt.Println("current username or password is empty")
111 | 		}
112 | 		fmt.Println("current pannel settings as follows:")
113 | 		fmt.Println("username:", username)
114 | 		fmt.Println("userpasswd:", userpasswd)
115 | 		fmt.Println("port:", port)
116 | 	}
117 | }
118 | 
119 | func updateTgbotEnableSts(status bool) {
120 | 	settingService := service.SettingService{}
121 | 	currentTgSts, err := settingService.GetTgbotenabled()
122 | 	if err != nil {
123 | 		fmt.Println(err)
124 | 		return
125 | 	}
126 | 	logger.Infof("current enabletgbot status[%v],need update to status[%v]", currentTgSts, status)
127 | 	if currentTgSts != status {
128 | 		err := settingService.SetTgbotenabled(status)
129 | 		if err != nil {
130 | 			fmt.Println(err)
131 | 			return
132 | 		} else {
133 | 			logger.Infof("SetTgbotenabled[%v] success", status)
134 | 		}
135 | 	}
136 | 	return
137 | }
138 | 
139 | func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string) {
140 | 	err := database.InitDB(config.GetDBPath())
141 | 	if err != nil {
142 | 		fmt.Println(err)
143 | 		return
144 | 	}
145 | 
146 | 	settingService := service.SettingService{}
147 | 
148 | 	if tgBotToken != "" {
149 | 		err := settingService.SetTgBotToken(tgBotToken)
150 | 		if err != nil {
151 | 			fmt.Println(err)
152 | 			return
153 | 		} else {
154 | 			logger.Info("updateTgbotSetting tgBotToken success")
155 | 		}
156 | 	}
157 | 
158 | 	if tgBotRuntime != "" {
159 | 		err := settingService.SetTgbotRuntime(tgBotRuntime)
160 | 		if err != nil {
161 | 			fmt.Println(err)
162 | 			return
163 | 		} else {
164 | 			logger.Infof("updateTgbotSetting tgBotRuntime[%s] success", tgBotRuntime)
165 | 		}
166 | 	}
167 | 
168 | 	if tgBotChatid != 0 {
169 | 		err := settingService.SetTgBotChatId(tgBotChatid)
170 | 		if err != nil {
171 | 			fmt.Println(err)
172 | 			return
173 | 		} else {
174 | 			logger.Info("updateTgbotSetting tgBotChatid success")
175 | 		}
176 | 	}
177 | }
178 | 
179 | func updateSetting(port int, username string, password string) {
180 | 	err := database.InitDB(config.GetDBPath())
181 | 	if err != nil {
182 | 		fmt.Println(err)
183 | 		return
184 | 	}
185 | 
186 | 	settingService := service.SettingService{}
187 | 
188 | 	if port > 0 {
189 | 		err := settingService.SetPort(port)
190 | 		if err != nil {
191 | 			fmt.Println("set port failed:", err)
192 | 		} else {
193 | 			fmt.Printf("set port %v success", port)
194 | 		}
195 | 	}
196 | 	if username != "" || password != "" {
197 | 		userService := service.UserService{}
198 | 		err := userService.UpdateFirstUser(username, password)
199 | 		if err != nil {
200 | 			fmt.Println("set username and password failed:", err)
201 | 		} else {
202 | 			fmt.Println("set username and password success")
203 | 		}
204 | 	}
205 | }
206 | 
207 | func main() {
208 | 	if len(os.Args) < 2 {
209 | 		runWebServer()
210 | 		return
211 | 	}
212 | 
213 | 	var showVersion bool
214 | 	flag.BoolVar(&showVersion, "v", false, "show version")
215 | 
216 | 	runCmd := flag.NewFlagSet("run", flag.ExitOnError)
217 | 
218 | 	v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError)
219 | 	var dbPath string
220 | 	v2uiCmd.StringVar(&dbPath, "db", "/etc/v2-ui/v2-ui.db", "set v2-ui db file path")
221 | 
222 | 	settingCmd := flag.NewFlagSet("setting", flag.ExitOnError)
223 | 	var port int
224 | 	var username string
225 | 	var password string
226 | 	var tgbottoken string
227 | 	var tgbotchatid int
228 | 	var enabletgbot bool
229 | 	var tgbotRuntime string
230 | 	var reset bool
231 | 	var show bool
232 | 	settingCmd.BoolVar(&reset, "reset", false, "reset all settings")
233 | 	settingCmd.BoolVar(&show, "show", false, "show current settings")
234 | 	settingCmd.IntVar(&port, "port", 0, "set panel port")
235 | 	settingCmd.StringVar(&username, "username", "", "set login username")
236 | 	settingCmd.StringVar(&password, "password", "", "set login password")
237 | 	settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token")
238 | 	settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time")
239 | 	settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id")
240 | 	settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify")
241 | 
242 | 	oldUsage := flag.Usage
243 | 	flag.Usage = func() {
244 | 		oldUsage()
245 | 		fmt.Println()
246 | 		fmt.Println("Commands:")
247 | 		fmt.Println("    run            run web panel")
248 | 		fmt.Println("    v2-ui          migrate form v2-ui")
249 | 		fmt.Println("    setting        set settings")
250 | 	}
251 | 
252 | 	flag.Parse()
253 | 	if showVersion {
254 | 		fmt.Println(config.GetVersion())
255 | 		return
256 | 	}
257 | 
258 | 	switch os.Args[1] {
259 | 	case "run":
260 | 		err := runCmd.Parse(os.Args[2:])
261 | 		if err != nil {
262 | 			fmt.Println(err)
263 | 			return
264 | 		}
265 | 		runWebServer()
266 | 	case "v2-ui":
267 | 		err := v2uiCmd.Parse(os.Args[2:])
268 | 		if err != nil {
269 | 			fmt.Println(err)
270 | 			return
271 | 		}
272 | 		err = v2ui.MigrateFromV2UI(dbPath)
273 | 		if err != nil {
274 | 			fmt.Println("migrate from v2-ui failed:", err)
275 | 		}
276 | 	case "setting":
277 | 		err := settingCmd.Parse(os.Args[2:])
278 | 		if err != nil {
279 | 			fmt.Println(err)
280 | 			return
281 | 		}
282 | 		if reset {
283 | 			resetSetting()
284 | 		} else {
285 | 			updateSetting(port, username, password)
286 | 		}
287 | 		if show {
288 | 			showSetting(show)
289 | 		}
290 | 		if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") {
291 | 			updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime)
292 | 		}
293 | 	default:
294 | 		fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands")
295 | 		fmt.Println()
296 | 		runCmd.Usage()
297 | 		fmt.Println()
298 | 		v2uiCmd.Usage()
299 | 		fmt.Println()
300 | 		settingCmd.Usage()
301 | 	}
302 | }
303 | 


--------------------------------------------------------------------------------
/media/2022-04-04_141259.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaxilu/x-ui/9c1be8c57a53953b47ee7c09a93554e73816f907/media/2022-04-04_141259.png


--------------------------------------------------------------------------------
/media/2022-04-17_110907.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaxilu/x-ui/9c1be8c57a53953b47ee7c09a93554e73816f907/media/2022-04-17_110907.png


--------------------------------------------------------------------------------
/media/2022-04-17_111321.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaxilu/x-ui/9c1be8c57a53953b47ee7c09a93554e73816f907/media/2022-04-17_111321.png


--------------------------------------------------------------------------------
/media/2022-04-17_111705.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaxilu/x-ui/9c1be8c57a53953b47ee7c09a93554e73816f907/media/2022-04-17_111705.png


--------------------------------------------------------------------------------
/media/2022-04-17_111910.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaxilu/x-ui/9c1be8c57a53953b47ee7c09a93554e73816f907/media/2022-04-17_111910.png


--------------------------------------------------------------------------------
/media/bda84fbc2ede834deaba1c173a932223.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaxilu/x-ui/9c1be8c57a53953b47ee7c09a93554e73816f907/media/bda84fbc2ede834deaba1c173a932223.png


--------------------------------------------------------------------------------
/media/d13ffd6a73f938d1037d0708e31433bf.png:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaxilu/x-ui/9c1be8c57a53953b47ee7c09a93554e73816f907/media/d13ffd6a73f938d1037d0708e31433bf.png


--------------------------------------------------------------------------------
/util/common/err.go:
--------------------------------------------------------------------------------
 1 | package common
 2 | 
 3 | import (
 4 | 	"errors"
 5 | 	"fmt"
 6 | 	"x-ui/logger"
 7 | )
 8 | 
 9 | var CtxDone = errors.New("context done")
10 | 
11 | func NewErrorf(format string, a ...interface{}) error {
12 | 	msg := fmt.Sprintf(format, a...)
13 | 	return errors.New(msg)
14 | }
15 | 
16 | func NewError(a ...interface{}) error {
17 | 	msg := fmt.Sprintln(a...)
18 | 	return errors.New(msg)
19 | }
20 | 
21 | func Recover(msg string) interface{} {
22 | 	panicErr := recover()
23 | 	if panicErr != nil {
24 | 		if msg != "" {
25 | 			logger.Error(msg, "panic:", panicErr)
26 | 		}
27 | 	}
28 | 	return panicErr
29 | }
30 | 


--------------------------------------------------------------------------------
/util/common/format.go:
--------------------------------------------------------------------------------
 1 | package common
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | )
 6 | 
 7 | func FormatTraffic(trafficBytes int64) (size string) {
 8 | 	if trafficBytes < 1024 {
 9 | 		return fmt.Sprintf("%.2fB", float64(trafficBytes)/float64(1))
10 | 	} else if trafficBytes < (1024 * 1024) {
11 | 		return fmt.Sprintf("%.2fKB", float64(trafficBytes)/float64(1024))
12 | 	} else if trafficBytes < (1024 * 1024 * 1024) {
13 | 		return fmt.Sprintf("%.2fMB", float64(trafficBytes)/float64(1024*1024))
14 | 	} else if trafficBytes < (1024 * 1024 * 1024 * 1024) {
15 | 		return fmt.Sprintf("%.2fGB", float64(trafficBytes)/float64(1024*1024*1024))
16 | 	} else if trafficBytes < (1024 * 1024 * 1024 * 1024 * 1024) {
17 | 		return fmt.Sprintf("%.2fTB", float64(trafficBytes)/float64(1024*1024*1024*1024))
18 | 	} else {
19 | 		return fmt.Sprintf("%.2fEB", float64(trafficBytes)/float64(1024*1024*1024*1024*1024))
20 | 	}
21 | }
22 | 


--------------------------------------------------------------------------------
/util/common/multi_error.go:
--------------------------------------------------------------------------------
 1 | package common
 2 | 
 3 | import (
 4 | 	"strings"
 5 | )
 6 | 
 7 | type multiError []error
 8 | 
 9 | func (e multiError) Error() string {
10 | 	var r strings.Builder
11 | 	r.WriteString("multierr: ")
12 | 	for _, err := range e {
13 | 		r.WriteString(err.Error())
14 | 		r.WriteString(" | ")
15 | 	}
16 | 	return r.String()
17 | }
18 | 
19 | func Combine(maybeError ...error) error {
20 | 	var errs multiError
21 | 	for _, err := range maybeError {
22 | 		if err != nil {
23 | 			errs = append(errs, err)
24 | 		}
25 | 	}
26 | 	if len(errs) == 0 {
27 | 		return nil
28 | 	}
29 | 	return errs
30 | }
31 | 


--------------------------------------------------------------------------------
/util/common/stringUtil.go:
--------------------------------------------------------------------------------
 1 | package common
 2 | 
 3 | import "sort"
 4 | 
 5 | func IsSubString(target string, str_array []string) bool {
 6 | 	sort.Strings(str_array)
 7 | 	index := sort.SearchStrings(str_array, target)
 8 | 	return index < len(str_array) && str_array[index] == target
 9 | }
10 | 


--------------------------------------------------------------------------------
/util/context.go:
--------------------------------------------------------------------------------
 1 | package util
 2 | 
 3 | import "context"
 4 | 
 5 | func IsDone(ctx context.Context) bool {
 6 | 	select {
 7 | 	case <-ctx.Done():
 8 | 		return true
 9 | 	default:
10 | 		return false
11 | 	}
12 | }
13 | 


--------------------------------------------------------------------------------
/util/json_util/json.go:
--------------------------------------------------------------------------------
 1 | package json_util
 2 | 
 3 | import (
 4 | 	"errors"
 5 | )
 6 | 
 7 | type RawMessage []byte
 8 | 
 9 | // MarshalJSON 自定义 json.RawMessage 默认行为
10 | func (m RawMessage) MarshalJSON() ([]byte, error) {
11 | 	if len(m) == 0 {
12 | 		return []byte("null"), nil
13 | 	}
14 | 	return m, nil
15 | }
16 | 
17 | // UnmarshalJSON sets *m to a copy of data.
18 | func (m *RawMessage) UnmarshalJSON(data []byte) error {
19 | 	if m == nil {
20 | 		return errors.New("json.RawMessage: UnmarshalJSON on nil pointer")
21 | 	}
22 | 	*m = append((*m)[0:0], data...)
23 | 	return nil
24 | }
25 | 


--------------------------------------------------------------------------------
/util/random/random.go:
--------------------------------------------------------------------------------
 1 | package random
 2 | 
 3 | import (
 4 | 	"math/rand"
 5 | 	"time"
 6 | )
 7 | 
 8 | var numSeq [10]rune
 9 | var lowerSeq [26]rune
10 | var upperSeq [26]rune
11 | var numLowerSeq [36]rune
12 | var numUpperSeq [36]rune
13 | var allSeq [62]rune
14 | 
15 | func init() {
16 | 	rand.Seed(time.Now().UnixNano())
17 | 
18 | 	for i := 0; i < 10; i++ {
19 | 		numSeq[i] = rune('0' + i)
20 | 	}
21 | 	for i := 0; i < 26; i++ {
22 | 		lowerSeq[i] = rune('a' + i)
23 | 		upperSeq[i] = rune('A' + i)
24 | 	}
25 | 
26 | 	copy(numLowerSeq[:], numSeq[:])
27 | 	copy(numLowerSeq[len(numSeq):], lowerSeq[:])
28 | 
29 | 	copy(numUpperSeq[:], numSeq[:])
30 | 	copy(numUpperSeq[len(numSeq):], upperSeq[:])
31 | 
32 | 	copy(allSeq[:], numSeq[:])
33 | 	copy(allSeq[len(numSeq):], lowerSeq[:])
34 | 	copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:])
35 | }
36 | 
37 | func Seq(n int) string {
38 | 	runes := make([]rune, n)
39 | 	for i := 0; i < n; i++ {
40 | 		runes[i] = allSeq[rand.Intn(len(allSeq))]
41 | 	}
42 | 	return string(runes)
43 | }
44 | 


--------------------------------------------------------------------------------
/util/reflect_util/reflect.go:
--------------------------------------------------------------------------------
 1 | package reflect_util
 2 | 
 3 | import "reflect"
 4 | 
 5 | func GetFields(t reflect.Type) []reflect.StructField {
 6 | 	num := t.NumField()
 7 | 	fields := make([]reflect.StructField, 0, num)
 8 | 	for i := 0; i < num; i++ {
 9 | 		fields = append(fields, t.Field(i))
10 | 	}
11 | 	return fields
12 | }
13 | 
14 | func GetFieldValues(v reflect.Value) []reflect.Value {
15 | 	num := v.NumField()
16 | 	fields := make([]reflect.Value, 0, num)
17 | 	for i := 0; i < num; i++ {
18 | 		fields = append(fields, v.Field(i))
19 | 	}
20 | 	return fields
21 | }
22 | 


--------------------------------------------------------------------------------
/util/sys/a.s:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/vaxilu/x-ui/9c1be8c57a53953b47ee7c09a93554e73816f907/util/sys/a.s


--------------------------------------------------------------------------------
/util/sys/psutil.go:
--------------------------------------------------------------------------------
1 | package sys
2 | 
3 | import (
4 | 	_ "unsafe"
5 | )
6 | 
7 | //go:linkname HostProc github.com/shirou/gopsutil/internal/common.HostProc
8 | func HostProc(combineWith ...string) string
9 | 


--------------------------------------------------------------------------------
/util/sys/sys_darwin.go:
--------------------------------------------------------------------------------
 1 | // +build darwin
 2 | 
 3 | package sys
 4 | 
 5 | import (
 6 | 	"github.com/shirou/gopsutil/net"
 7 | )
 8 | 
 9 | func GetTCPCount() (int, error) {
10 | 	stats, err := net.Connections("tcp")
11 | 	if err != nil {
12 | 		return 0, err
13 | 	}
14 | 	return len(stats), nil
15 | }
16 | 
17 | func GetUDPCount() (int, error) {
18 | 	stats, err := net.Connections("udp")
19 | 	if err != nil {
20 | 		return 0, err
21 | 	}
22 | 	return len(stats), nil
23 | }
24 | 


--------------------------------------------------------------------------------
/util/sys/sys_linux.go:
--------------------------------------------------------------------------------
 1 | // +build linux
 2 | 
 3 | package sys
 4 | 
 5 | import (
 6 | 	"bytes"
 7 | 	"fmt"
 8 | 	"io"
 9 | 	"os"
10 | )
11 | 
12 | func getLinesNum(filename string) (int, error) {
13 | 	file, err := os.Open(filename)
14 | 	if err != nil {
15 | 		return 0, err
16 | 	}
17 | 	defer file.Close()
18 | 
19 | 	sum := 0
20 | 	buf := make([]byte, 8192)
21 | 	for {
22 | 		n, err := file.Read(buf)
23 | 
24 | 		var buffPosition int
25 | 		for {
26 | 			i := bytes.IndexByte(buf[buffPosition:], '\n')
27 | 			if i < 0 || n == buffPosition {
28 | 				break
29 | 			}
30 | 			buffPosition += i + 1
31 | 			sum++
32 | 		}
33 | 
34 | 		if err == io.EOF {
35 | 			return sum, nil
36 | 		} else if err != nil {
37 | 			return sum, err
38 | 		}
39 | 	}
40 | }
41 | 
42 | func GetTCPCount() (int, error) {
43 | 	root := HostProc()
44 | 
45 | 	tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root))
46 | 	if err != nil {
47 | 		return tcp4, err
48 | 	}
49 | 	tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root))
50 | 	if err != nil {
51 | 		return tcp4 + tcp6, nil
52 | 	}
53 | 
54 | 	return tcp4 + tcp6, nil
55 | }
56 | 
57 | func GetUDPCount() (int, error) {
58 | 	root := HostProc()
59 | 
60 | 	udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root))
61 | 	if err != nil {
62 | 		return udp4, err
63 | 	}
64 | 	udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root))
65 | 	if err != nil {
66 | 		return udp4 + udp6, nil
67 | 	}
68 | 
69 | 	return udp4 + udp6, nil
70 | }
71 | 


--------------------------------------------------------------------------------
/v2ui/db.go:
--------------------------------------------------------------------------------
 1 | package v2ui
 2 | 
 3 | import (
 4 | 	"gorm.io/driver/sqlite"
 5 | 	"gorm.io/gorm"
 6 | 	"gorm.io/gorm/logger"
 7 | )
 8 | 
 9 | var v2db *gorm.DB
10 | 
11 | func initDB(dbPath string) error {
12 | 	c := &gorm.Config{
13 | 		Logger: logger.Discard,
14 | 	}
15 | 	var err error
16 | 	v2db, err = gorm.Open(sqlite.Open(dbPath), c)
17 | 	if err != nil {
18 | 		return err
19 | 	}
20 | 
21 | 	return nil
22 | }
23 | 
24 | func getV2Inbounds() ([]*V2Inbound, error) {
25 | 	inbounds := make([]*V2Inbound, 0)
26 | 	err := v2db.Model(V2Inbound{}).Find(&inbounds).Error
27 | 	return inbounds, err
28 | }
29 | 


--------------------------------------------------------------------------------
/v2ui/models.go:
--------------------------------------------------------------------------------
 1 | package v2ui
 2 | 
 3 | import "x-ui/database/model"
 4 | 
 5 | type V2Inbound struct {
 6 | 	Id             int `gorm:"primaryKey;autoIncrement"`
 7 | 	Port           int `gorm:"unique"`
 8 | 	Listen         string
 9 | 	Protocol       string
10 | 	Settings       string
11 | 	StreamSettings string
12 | 	Tag            string `gorm:"unique"`
13 | 	Sniffing       string
14 | 	Remark         string
15 | 	Up             int64
16 | 	Down           int64
17 | 	Enable         bool
18 | }
19 | 
20 | func (i *V2Inbound) TableName() string {
21 | 	return "inbound"
22 | }
23 | 
24 | func (i *V2Inbound) ToInbound(userId int) *model.Inbound {
25 | 	return &model.Inbound{
26 | 		UserId:         userId,
27 | 		Up:             i.Up,
28 | 		Down:           i.Down,
29 | 		Total:          0,
30 | 		Remark:         i.Remark,
31 | 		Enable:         i.Enable,
32 | 		ExpiryTime:     0,
33 | 		Listen:         i.Listen,
34 | 		Port:           i.Port,
35 | 		Protocol:       model.Protocol(i.Protocol),
36 | 		Settings:       i.Settings,
37 | 		StreamSettings: i.StreamSettings,
38 | 		Tag:            i.Tag,
39 | 		Sniffing:       i.Sniffing,
40 | 	}
41 | }
42 | 


--------------------------------------------------------------------------------
/v2ui/v2ui.go:
--------------------------------------------------------------------------------
 1 | package v2ui
 2 | 
 3 | import (
 4 | 	"fmt"
 5 | 	"x-ui/config"
 6 | 	"x-ui/database"
 7 | 	"x-ui/database/model"
 8 | 	"x-ui/util/common"
 9 | 	"x-ui/web/service"
10 | )
11 | 
12 | func MigrateFromV2UI(dbPath string) error {
13 | 	err := initDB(dbPath)
14 | 	if err != nil {
15 | 		return common.NewError("init v2-ui database failed:", err)
16 | 	}
17 | 	err = database.InitDB(config.GetDBPath())
18 | 	if err != nil {
19 | 		return common.NewError("init x-ui database failed:", err)
20 | 	}
21 | 
22 | 	v2Inbounds, err := getV2Inbounds()
23 | 	if err != nil {
24 | 		return common.NewError("get v2-ui inbounds failed:", err)
25 | 	}
26 | 	if len(v2Inbounds) == 0 {
27 | 		fmt.Println("migrate v2-ui inbounds success: 0")
28 | 		return nil
29 | 	}
30 | 
31 | 	userService := service.UserService{}
32 | 	user, err := userService.GetFirstUser()
33 | 	if err != nil {
34 | 		return common.NewError("get x-ui user failed:", err)
35 | 	}
36 | 
37 | 	inbounds := make([]*model.Inbound, 0)
38 | 	for _, v2inbound := range v2Inbounds {
39 | 		inbounds = append(inbounds, v2inbound.ToInbound(user.Id))
40 | 	}
41 | 
42 | 	inboundService := service.InboundService{}
43 | 	err = inboundService.AddInbounds(inbounds)
44 | 	if err != nil {
45 | 		return common.NewError("add x-ui inbounds failed:", err)
46 | 	}
47 | 
48 | 	fmt.Println("migrate v2-ui inbounds success:", len(inbounds))
49 | 
50 | 	return nil
51 | }
52 | 


--------------------------------------------------------------------------------
/web/assets/ant-design-vue@1.7.2/antd.less:
--------------------------------------------------------------------------------
1 | @import "../lib/style/index.less";
2 | @import "../lib/style/components.less";


--------------------------------------------------------------------------------
/web/assets/base64/base64.min.js:
--------------------------------------------------------------------------------
1 | (function(global,factory){typeof exports==="object"&&typeof module!=="undefined"?module.exports=factory(global):typeof define==="function"&&define.amd?define(factory):factory(global)})(typeof self!=="undefined"?self:typeof window!=="undefined"?window:typeof global!=="undefined"?global:this,function(global){"use strict";var _Base64=global.Base64;var version="2.5.0";var buffer;if(typeof module!=="undefined"&&module.exports){try{buffer=eval("require('buffer').Buffer")}catch(err){buffer=undefined}}var b64chars="ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";var b64tab=function(bin){var t={};for(var i=0,l=bin.length;i<l;i++)t[bin.charAt(i)]=i;return t}(b64chars);var fromCharCode=String.fromCharCode;var cb_utob=function(c){if(c.length<2){var cc=c.charCodeAt(0);return cc<128?c:cc<2048?fromCharCode(192|cc>>>6)+fromCharCode(128|cc&63):fromCharCode(224|cc>>>12&15)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}else{var cc=65536+(c.charCodeAt(0)-55296)*1024+(c.charCodeAt(1)-56320);return fromCharCode(240|cc>>>18&7)+fromCharCode(128|cc>>>12&63)+fromCharCode(128|cc>>>6&63)+fromCharCode(128|cc&63)}};var re_utob=/[\uD800-\uDBFF][\uDC00-\uDFFFF]|[^\x00-\x7F]/g;var utob=function(u){return u.replace(re_utob,cb_utob)};var cb_encode=function(ccc){var padlen=[0,2,1][ccc.length%3],ord=ccc.charCodeAt(0)<<16|(ccc.length>1?ccc.charCodeAt(1):0)<<8|(ccc.length>2?ccc.charCodeAt(2):0),chars=[b64chars.charAt(ord>>>18),b64chars.charAt(ord>>>12&63),padlen>=2?"=":b64chars.charAt(ord>>>6&63),padlen>=1?"=":b64chars.charAt(ord&63)];return chars.join("")};var btoa=global.btoa?function(b){return global.btoa(b)}:function(b){return b.replace(/[\s\S]{1,3}/g,cb_encode)};var _encode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(u){return(u.constructor===buffer.constructor?u:buffer.from(u)).toString("base64")}:function(u){return(u.constructor===buffer.constructor?u:new buffer(u)).toString("base64")}:function(u){return btoa(utob(u))};var encode=function(u,urisafe){return!urisafe?_encode(String(u)):_encode(String(u)).replace(/[+\/]/g,function(m0){return m0=="+"?"-":"_"}).replace(/=/g,"")};var encodeURI=function(u){return encode(u,true)};var re_btou=new RegExp(["[À-ß][€-¿]","[à-ï][€-¿]{2}","[ð-÷][€-¿]{3}"].join("|"),"g");var cb_btou=function(cccc){switch(cccc.length){case 4:var cp=(7&cccc.charCodeAt(0))<<18|(63&cccc.charCodeAt(1))<<12|(63&cccc.charCodeAt(2))<<6|63&cccc.charCodeAt(3),offset=cp-65536;return fromCharCode((offset>>>10)+55296)+fromCharCode((offset&1023)+56320);case 3:return fromCharCode((15&cccc.charCodeAt(0))<<12|(63&cccc.charCodeAt(1))<<6|63&cccc.charCodeAt(2));default:return fromCharCode((31&cccc.charCodeAt(0))<<6|63&cccc.charCodeAt(1))}};var btou=function(b){return b.replace(re_btou,cb_btou)};var cb_decode=function(cccc){var len=cccc.length,padlen=len%4,n=(len>0?b64tab[cccc.charAt(0)]<<18:0)|(len>1?b64tab[cccc.charAt(1)]<<12:0)|(len>2?b64tab[cccc.charAt(2)]<<6:0)|(len>3?b64tab[cccc.charAt(3)]:0),chars=[fromCharCode(n>>>16),fromCharCode(n>>>8&255),fromCharCode(n&255)];chars.length-=[0,0,2,1][padlen];return chars.join("")};var _atob=global.atob?function(a){return global.atob(a)}:function(a){return a.replace(/\S{1,4}/g,cb_decode)};var atob=function(a){return _atob(String(a).replace(/[^A-Za-z0-9\+\/]/g,""))};var _decode=buffer?buffer.from&&Uint8Array&&buffer.from!==Uint8Array.from?function(a){return(a.constructor===buffer.constructor?a:buffer.from(a,"base64")).toString()}:function(a){return(a.constructor===buffer.constructor?a:new buffer(a,"base64")).toString()}:function(a){return btou(_atob(a))};var decode=function(a){return _decode(String(a).replace(/[-_]/g,function(m0){return m0=="-"?"+":"/"}).replace(/[^A-Za-z0-9\+\/]/g,""))};var noConflict=function(){var Base64=global.Base64;global.Base64=_Base64;return Base64};global.Base64={VERSION:version,atob:atob,btoa:btoa,fromBase64:decode,toBase64:encode,utob:utob,encode:encode,encodeURI:encodeURI,btou:btou,decode:decode,noConflict:noConflict,__buffer__:buffer};if(typeof Object.defineProperty==="function"){var noEnum=function(v){return{value:v,enumerable:false,writable:true,configurable:true}};global.Base64.extendString=function(){Object.defineProperty(String.prototype,"fromBase64",noEnum(function(){return decode(this)}));Object.defineProperty(String.prototype,"toBase64",noEnum(function(urisafe){return encode(this,urisafe)}));Object.defineProperty(String.prototype,"toBase64URI",noEnum(function(){return encode(this,true)}))}}if(global["Meteor"]){Base64=global.Base64}if(typeof module!=="undefined"&&module.exports){module.exports.Base64=global.Base64}else if(typeof define==="function"&&define.amd){define([],function(){return global.Base64})}return{Base64:global.Base64}});


--------------------------------------------------------------------------------
/web/assets/clipboard/clipboard.min.js:
--------------------------------------------------------------------------------
1 | /*!
2 |  * clipboard.js v2.0.0
3 |  * https://zenorocha.github.io/clipboard.js
4 |  * 
5 |  * Licensed MIT © Zeno Rocha
6 |  */
7 | !function(t,e){"object"==typeof exports&&"object"==typeof module?module.exports=e():"function"==typeof define&&define.amd?define([],e):"object"==typeof exports?exports.ClipboardJS=e():t.ClipboardJS=e()}(this,function(){return function(t){function e(o){if(n[o])return n[o].exports;var r=n[o]={i:o,l:!1,exports:{}};return t[o].call(r.exports,r,r.exports,e),r.l=!0,r.exports}var n={};return e.m=t,e.c=n,e.i=function(t){return t},e.d=function(t,n,o){e.o(t,n)||Object.defineProperty(t,n,{configurable:!1,enumerable:!0,get:o})},e.n=function(t){var n=t&&t.__esModule?function(){return t.default}:function(){return t};return e.d(n,"a",n),n},e.o=function(t,e){return Object.prototype.hasOwnProperty.call(t,e)},e.p="",e(e.s=3)}([function(t,e,n){var o,r,i;!function(a,c){r=[t,n(7)],o=c,void 0!==(i="function"==typeof o?o.apply(e,r):o)&&(t.exports=i)}(0,function(t,e){"use strict";function n(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}var o=function(t){return t&&t.__esModule?t:{default:t}}(e),r="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},i=function(){function t(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}return function(e,n,o){return n&&t(e.prototype,n),o&&t(e,o),e}}(),a=function(){function t(e){n(this,t),this.resolveOptions(e),this.initSelection()}return i(t,[{key:"resolveOptions",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.action=t.action,this.container=t.container,this.emitter=t.emitter,this.target=t.target,this.text=t.text,this.trigger=t.trigger,this.selectedText=""}},{key:"initSelection",value:function(){this.text?this.selectFake():this.target&&this.selectTarget()}},{key:"selectFake",value:function(){var t=this,e="rtl"==document.documentElement.getAttribute("dir");this.removeFake(),this.fakeHandlerCallback=function(){return t.removeFake()},this.fakeHandler=this.container.addEventListener("click",this.fakeHandlerCallback)||!0,this.fakeElem=document.createElement("textarea"),this.fakeElem.style.fontSize="12pt",this.fakeElem.style.border="0",this.fakeElem.style.padding="0",this.fakeElem.style.margin="0",this.fakeElem.style.position="absolute",this.fakeElem.style[e?"right":"left"]="-9999px";var n=window.pageYOffset||document.documentElement.scrollTop;this.fakeElem.style.top=n+"px",this.fakeElem.setAttribute("readonly",""),this.fakeElem.value=this.text,this.container.appendChild(this.fakeElem),this.selectedText=(0,o.default)(this.fakeElem),this.copyText()}},{key:"removeFake",value:function(){this.fakeHandler&&(this.container.removeEventListener("click",this.fakeHandlerCallback),this.fakeHandler=null,this.fakeHandlerCallback=null),this.fakeElem&&(this.container.removeChild(this.fakeElem),this.fakeElem=null)}},{key:"selectTarget",value:function(){this.selectedText=(0,o.default)(this.target),this.copyText()}},{key:"copyText",value:function(){var t=void 0;try{t=document.execCommand(this.action)}catch(e){t=!1}this.handleResult(t)}},{key:"handleResult",value:function(t){this.emitter.emit(t?"success":"error",{action:this.action,text:this.selectedText,trigger:this.trigger,clearSelection:this.clearSelection.bind(this)})}},{key:"clearSelection",value:function(){this.trigger&&this.trigger.focus(),window.getSelection().removeAllRanges()}},{key:"destroy",value:function(){this.removeFake()}},{key:"action",set:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:"copy";if(this._action=t,"copy"!==this._action&&"cut"!==this._action)throw new Error('Invalid "action" value, use either "copy" or "cut"')},get:function(){return this._action}},{key:"target",set:function(t){if(void 0!==t){if(!t||"object"!==(void 0===t?"undefined":r(t))||1!==t.nodeType)throw new Error('Invalid "target" value, use a valid Element');if("copy"===this.action&&t.hasAttribute("disabled"))throw new Error('Invalid "target" attribute. Please use "readonly" instead of "disabled" attribute');if("cut"===this.action&&(t.hasAttribute("readonly")||t.hasAttribute("disabled")))throw new Error('Invalid "target" attribute. You can\'t cut text from elements with "readonly" or "disabled" attributes');this._target=t}},get:function(){return this._target}}]),t}();t.exports=a})},function(t,e,n){function o(t,e,n){if(!t&&!e&&!n)throw new Error("Missing required arguments");if(!c.string(e))throw new TypeError("Second argument must be a String");if(!c.fn(n))throw new TypeError("Third argument must be a Function");if(c.node(t))return r(t,e,n);if(c.nodeList(t))return i(t,e,n);if(c.string(t))return a(t,e,n);throw new TypeError("First argument must be a String, HTMLElement, HTMLCollection, or NodeList")}function r(t,e,n){return t.addEventListener(e,n),{destroy:function(){t.removeEventListener(e,n)}}}function i(t,e,n){return Array.prototype.forEach.call(t,function(t){t.addEventListener(e,n)}),{destroy:function(){Array.prototype.forEach.call(t,function(t){t.removeEventListener(e,n)})}}}function a(t,e,n){return u(document.body,t,e,n)}var c=n(6),u=n(5);t.exports=o},function(t,e){function n(){}n.prototype={on:function(t,e,n){var o=this.e||(this.e={});return(o[t]||(o[t]=[])).push({fn:e,ctx:n}),this},once:function(t,e,n){function o(){r.off(t,o),e.apply(n,arguments)}var r=this;return o._=e,this.on(t,o,n)},emit:function(t){var e=[].slice.call(arguments,1),n=((this.e||(this.e={}))[t]||[]).slice(),o=0,r=n.length;for(o;o<r;o++)n[o].fn.apply(n[o].ctx,e);return this},off:function(t,e){var n=this.e||(this.e={}),o=n[t],r=[];if(o&&e)for(var i=0,a=o.length;i<a;i++)o[i].fn!==e&&o[i].fn._!==e&&r.push(o[i]);return r.length?n[t]=r:delete n[t],this}},t.exports=n},function(t,e,n){var o,r,i;!function(a,c){r=[t,n(0),n(2),n(1)],o=c,void 0!==(i="function"==typeof o?o.apply(e,r):o)&&(t.exports=i)}(0,function(t,e,n,o){"use strict";function r(t){return t&&t.__esModule?t:{default:t}}function i(t,e){if(!(t instanceof e))throw new TypeError("Cannot call a class as a function")}function a(t,e){if(!t)throw new ReferenceError("this hasn't been initialised - super() hasn't been called");return!e||"object"!=typeof e&&"function"!=typeof e?t:e}function c(t,e){if("function"!=typeof e&&null!==e)throw new TypeError("Super expression must either be null or a function, not "+typeof e);t.prototype=Object.create(e&&e.prototype,{constructor:{value:t,enumerable:!1,writable:!0,configurable:!0}}),e&&(Object.setPrototypeOf?Object.setPrototypeOf(t,e):t.__proto__=e)}function u(t,e){var n="data-clipboard-"+t;if(e.hasAttribute(n))return e.getAttribute(n)}var l=r(e),s=r(n),f=r(o),d="function"==typeof Symbol&&"symbol"==typeof Symbol.iterator?function(t){return typeof t}:function(t){return t&&"function"==typeof Symbol&&t.constructor===Symbol&&t!==Symbol.prototype?"symbol":typeof t},h=function(){function t(t,e){for(var n=0;n<e.length;n++){var o=e[n];o.enumerable=o.enumerable||!1,o.configurable=!0,"value"in o&&(o.writable=!0),Object.defineProperty(t,o.key,o)}}return function(e,n,o){return n&&t(e.prototype,n),o&&t(e,o),e}}(),p=function(t){function e(t,n){i(this,e);var o=a(this,(e.__proto__||Object.getPrototypeOf(e)).call(this));return o.resolveOptions(n),o.listenClick(t),o}return c(e,t),h(e,[{key:"resolveOptions",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:{};this.action="function"==typeof t.action?t.action:this.defaultAction,this.target="function"==typeof t.target?t.target:this.defaultTarget,this.text="function"==typeof t.text?t.text:this.defaultText,this.container="object"===d(t.container)?t.container:document.body}},{key:"listenClick",value:function(t){var e=this;this.listener=(0,f.default)(t,"click",function(t){return e.onClick(t)})}},{key:"onClick",value:function(t){var e=t.delegateTarget||t.currentTarget;this.clipboardAction&&(this.clipboardAction=null),this.clipboardAction=new l.default({action:this.action(e),target:this.target(e),text:this.text(e),container:this.container,trigger:e,emitter:this})}},{key:"defaultAction",value:function(t){return u("action",t)}},{key:"defaultTarget",value:function(t){var e=u("target",t);if(e)return document.querySelector(e)}},{key:"defaultText",value:function(t){return u("text",t)}},{key:"destroy",value:function(){this.listener.destroy(),this.clipboardAction&&(this.clipboardAction.destroy(),this.clipboardAction=null)}}],[{key:"isSupported",value:function(){var t=arguments.length>0&&void 0!==arguments[0]?arguments[0]:["copy","cut"],e="string"==typeof t?[t]:t,n=!!document.queryCommandSupported;return e.forEach(function(t){n=n&&!!document.queryCommandSupported(t)}),n}}]),e}(s.default);t.exports=p})},function(t,e){function n(t,e){for(;t&&t.nodeType!==o;){if("function"==typeof t.matches&&t.matches(e))return t;t=t.parentNode}}var o=9;if("undefined"!=typeof Element&&!Element.prototype.matches){var r=Element.prototype;r.matches=r.matchesSelector||r.mozMatchesSelector||r.msMatchesSelector||r.oMatchesSelector||r.webkitMatchesSelector}t.exports=n},function(t,e,n){function o(t,e,n,o,r){var a=i.apply(this,arguments);return t.addEventListener(n,a,r),{destroy:function(){t.removeEventListener(n,a,r)}}}function r(t,e,n,r,i){return"function"==typeof t.addEventListener?o.apply(null,arguments):"function"==typeof n?o.bind(null,document).apply(null,arguments):("string"==typeof t&&(t=document.querySelectorAll(t)),Array.prototype.map.call(t,function(t){return o(t,e,n,r,i)}))}function i(t,e,n,o){return function(n){n.delegateTarget=a(n.target,e),n.delegateTarget&&o.call(t,n)}}var a=n(4);t.exports=r},function(t,e){e.node=function(t){return void 0!==t&&t instanceof HTMLElement&&1===t.nodeType},e.nodeList=function(t){var n=Object.prototype.toString.call(t);return void 0!==t&&("[object NodeList]"===n||"[object HTMLCollection]"===n)&&"length"in t&&(0===t.length||e.node(t[0]))},e.string=function(t){return"string"==typeof t||t instanceof String},e.fn=function(t){return"[object Function]"===Object.prototype.toString.call(t)}},function(t,e){function n(t){var e;if("SELECT"===t.nodeName)t.focus(),e=t.value;else if("INPUT"===t.nodeName||"TEXTAREA"===t.nodeName){var n=t.hasAttribute("readonly");n||t.setAttribute("readonly",""),t.select(),t.setSelectionRange(0,t.value.length),n||t.removeAttribute("readonly"),e=t.value}else{t.hasAttribute("contenteditable")&&t.focus();var o=window.getSelection(),r=document.createRange();r.selectNodeContents(t),o.removeAllRanges(),o.addRange(r),e=o.toString()}return e}t.exports=n}])});


--------------------------------------------------------------------------------
/web/assets/css/custom.css:
--------------------------------------------------------------------------------
  1 | #app {
  2 |     height: 100%;
  3 | }
  4 | 
  5 | .ant-space {
  6 |     width: 100%;
  7 | }
  8 | 
  9 | .ant-layout-sider-zero-width-trigger {
 10 |     display: none;
 11 | }
 12 | 
 13 | .ant-card {
 14 |     border-radius: 30px;
 15 | }
 16 | 
 17 | .ant-card-hoverable {
 18 |     cursor: auto;
 19 | }
 20 | 
 21 | .ant-card+.ant-card {
 22 |     margin-top: 20px;
 23 | }
 24 | 
 25 | .drawer-handle {
 26 |     position: absolute;
 27 |     top: 72px;
 28 |     width: 41px;
 29 |     height: 40px;
 30 |     cursor: pointer;
 31 |     z-index: 0;
 32 |     text-align: center;
 33 |     line-height: 40px;
 34 |     font-size: 16px;
 35 |     display: flex;
 36 |     justify-content: center;
 37 |     align-items: center;
 38 |     background: #fff;
 39 |     right: -40px;
 40 |     box-shadow: 2px 0 8px rgba(0, 0, 0, 0.15);
 41 |     border-radius: 0 4px 4px 0;
 42 | }
 43 | 
 44 | @media (min-width: 769px) {
 45 |     .drawer-handle {
 46 |         display: none;
 47 |     }
 48 | }
 49 | 
 50 | .fade-in-enter, .fade-in-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active, .fade-in-linear-enter, .fade-in-linear-leave, .fade-in-linear-leave-active {
 51 |     opacity: 0
 52 | }
 53 | 
 54 | .fade-in-linear-enter-active, .fade-in-linear-leave-active {
 55 |     -webkit-transition: opacity .2s linear;
 56 |     transition: opacity .2s linear
 57 | }
 58 | 
 59 | .fade-in-linear-enter-active, .fade-in-linear-leave-active {
 60 |     -webkit-transition: opacity .2s linear;
 61 |     transition: opacity .2s linear
 62 | }
 63 | 
 64 | .fade-in-enter-active, .fade-in-leave-active {
 65 |     -webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
 66 |     transition: all .3s cubic-bezier(.55, 0, .1, 1)
 67 | }
 68 | 
 69 | .zoom-in-center-enter-active, .zoom-in-center-leave-active {
 70 |     -webkit-transition: all .3s cubic-bezier(.55, 0, .1, 1);
 71 |     transition: all .3s cubic-bezier(.55, 0, .1, 1)
 72 | }
 73 | 
 74 | .zoom-in-center-enter, .zoom-in-center-leave-active {
 75 |     opacity: 0;
 76 |     -webkit-transform: scaleX(0);
 77 |     transform: scaleX(0)
 78 | }
 79 | 
 80 | .zoom-in-top-enter-active, .zoom-in-top-leave-active {
 81 |     opacity: 1;
 82 |     -webkit-transform: scaleY(1);
 83 |     transform: scaleY(1);
 84 |     -webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
 85 |     transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
 86 |     transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
 87 |     transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
 88 |     -webkit-transform-origin: center top;
 89 |     transform-origin: center top
 90 | }
 91 | 
 92 | .zoom-in-top-enter, .zoom-in-top-leave-active {
 93 |     opacity: 0;
 94 |     -webkit-transform: scaleY(0);
 95 |     transform: scaleY(0)
 96 | }
 97 | 
 98 | .zoom-in-bottom-enter-active, .zoom-in-bottom-leave-active {
 99 |     opacity: 1;
100 |     -webkit-transform: scaleY(1);
101 |     transform: scaleY(1);
102 |     -webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
103 |     transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
104 |     transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
105 |     transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
106 |     -webkit-transform-origin: center bottom;
107 |     transform-origin: center bottom
108 | }
109 | 
110 | .zoom-in-bottom-enter, .zoom-in-bottom-leave-active {
111 |     opacity: 0;
112 |     -webkit-transform: scaleY(0);
113 |     transform: scaleY(0)
114 | }
115 | 
116 | .zoom-in-left-enter-active, .zoom-in-left-leave-active {
117 |     opacity: 1;
118 |     -webkit-transform: scale(1, 1);
119 |     transform: scale(1, 1);
120 |     -webkit-transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
121 |     transition: opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
122 |     transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1);
123 |     transition: transform .3s cubic-bezier(.23, 1, .32, 1), opacity .3s cubic-bezier(.23, 1, .32, 1), -webkit-transform .3s cubic-bezier(.23, 1, .32, 1);
124 |     -webkit-transform-origin: top left;
125 |     transform-origin: top left
126 | }
127 | 
128 | .zoom-in-left-enter, .zoom-in-left-leave-active {
129 |     opacity: 0;
130 |     -webkit-transform: scale(.45, .45);
131 |     transform: scale(.45, .45)
132 | }
133 | 
134 | .list-enter-active, .list-leave-active {
135 |     -webkit-transition: all .3s;
136 |     transition: all .3s
137 | }
138 | 
139 | .list-enter, .list-leave-active {
140 |     opacity: 0;
141 |     -webkit-transform: translateY(-30px);
142 |     transform: translateY(-30px)
143 | }
144 | 
145 | .ant-progress-inner {
146 |     background-color: #EBEEF5;
147 | }


--------------------------------------------------------------------------------
/web/assets/element-ui@2.15.0/theme-chalk/display.css:
--------------------------------------------------------------------------------
1 | @media only screen and (max-width:767px){.hidden-xs-only{display:none!important}}@media only screen and (min-width:768px){.hidden-sm-and-up{display:none!important}}@media only screen and (min-width:768px) and (max-width:991px){.hidden-sm-only{display:none!important}}@media only screen and (max-width:991px){.hidden-sm-and-down{display:none!important}}@media only screen and (min-width:992px){.hidden-md-and-up{display:none!important}}@media only screen and (min-width:992px) and (max-width:1199px){.hidden-md-only{display:none!important}}@media only screen and (max-width:1199px){.hidden-md-and-down{display:none!important}}@media only screen and (min-width:1200px){.hidden-lg-and-up{display:none!important}}@media only screen and (min-width:1200px) and (max-width:1919px){.hidden-lg-only{display:none!important}}@media only screen and (max-width:1919px){.hidden-lg-and-down{display:none!important}}@media only screen and (min-width:1920px){.hidden-xl-only{display:none!important}}


--------------------------------------------------------------------------------
/web/assets/js/axios-init.js:
--------------------------------------------------------------------------------
 1 | axios.defaults.headers.post['Content-Type'] = 'application/x-www-form-urlencoded; charset=UTF-8';
 2 | axios.defaults.headers.common['X-Requested-With'] = 'XMLHttpRequest';
 3 | 
 4 | axios.interceptors.request.use(
 5 |     config => {
 6 |         config.data = Qs.stringify(config.data, {
 7 |             arrayFormat: 'repeat'
 8 |         });
 9 |         return config;
10 |     },
11 |     error => Promise.reject(error)
12 | );


--------------------------------------------------------------------------------
/web/assets/js/model/models.js:
--------------------------------------------------------------------------------
  1 | class User {
  2 | 
  3 |     constructor() {
  4 |         this.username = "";
  5 |         this.password = "";
  6 |     }
  7 | }
  8 | 
  9 | class Msg {
 10 | 
 11 |     constructor(success, msg, obj) {
 12 |         this.success = false;
 13 |         this.msg = "";
 14 |         this.obj = null;
 15 | 
 16 |         if (success != null) {
 17 |             this.success = success;
 18 |         }
 19 |         if (msg != null) {
 20 |             this.msg = msg;
 21 |         }
 22 |         if (obj != null) {
 23 |             this.obj = obj;
 24 |         }
 25 |     }
 26 | }
 27 | 
 28 | class DBInbound {
 29 | 
 30 |     constructor(data) {
 31 |         this.id = 0;
 32 |         this.userId = 0;
 33 |         this.up = 0;
 34 |         this.down = 0;
 35 |         this.total = 0;
 36 |         this.remark = "";
 37 |         this.enable = true;
 38 |         this.expiryTime = 0;
 39 | 
 40 |         this.listen = "";
 41 |         this.port = 0;
 42 |         this.protocol = "";
 43 |         this.settings = "";
 44 |         this.streamSettings = "";
 45 |         this.tag = "";
 46 |         this.sniffing = "";
 47 | 
 48 |         if (data == null) {
 49 |             return;
 50 |         }
 51 |         ObjectUtil.cloneProps(this, data);
 52 |     }
 53 | 
 54 |     get totalGB() {
 55 |         return toFixed(this.total / ONE_GB, 2);
 56 |     }
 57 | 
 58 |     set totalGB(gb) {
 59 |         this.total = toFixed(gb * ONE_GB, 0);
 60 |     }
 61 | 
 62 |     get isVMess() {
 63 |         return this.protocol === Protocols.VMESS;
 64 |     }
 65 | 
 66 |     get isVLess() {
 67 |         return this.protocol === Protocols.VLESS;
 68 |     }
 69 | 
 70 |     get isTrojan() {
 71 |         return this.protocol === Protocols.TROJAN;
 72 |     }
 73 | 
 74 |     get isSS() {
 75 |         return this.protocol === Protocols.SHADOWSOCKS;
 76 |     }
 77 | 
 78 |     get isSocks() {
 79 |         return this.protocol === Protocols.SOCKS;
 80 |     }
 81 | 
 82 |     get isHTTP() {
 83 |         return this.protocol === Protocols.HTTP;
 84 |     }
 85 | 
 86 |     get address() {
 87 |         let address = location.hostname;
 88 |         if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") {
 89 |             address = this.listen;
 90 |         }
 91 |         return address;
 92 |     }
 93 | 
 94 |     get _expiryTime() {
 95 |         if (this.expiryTime === 0) {
 96 |             return null;
 97 |         }
 98 |         return moment(this.expiryTime);
 99 |     }
100 | 
101 |     set _expiryTime(t) {
102 |         if (t == null) {
103 |             this.expiryTime = 0;
104 |         } else {
105 |             this.expiryTime = t.valueOf();
106 |         }
107 |     }
108 | 
109 |     get isExpiry() {
110 |         return this.expiryTime < new Date().getTime();
111 |     }
112 | 
113 |     toInbound() {
114 |         let settings = {};
115 |         if (!ObjectUtil.isEmpty(this.settings)) {
116 |             settings = JSON.parse(this.settings);
117 |         }
118 | 
119 |         let streamSettings = {};
120 |         if (!ObjectUtil.isEmpty(this.streamSettings)) {
121 |             streamSettings = JSON.parse(this.streamSettings);
122 |         }
123 | 
124 |         let sniffing = {};
125 |         if (!ObjectUtil.isEmpty(this.sniffing)) {
126 |             sniffing = JSON.parse(this.sniffing);
127 |         }
128 |         const config = {
129 |             port: this.port,
130 |             listen: this.listen,
131 |             protocol: this.protocol,
132 |             settings: settings,
133 |             streamSettings: streamSettings,
134 |             tag: this.tag,
135 |             sniffing: sniffing,
136 |         };
137 |         return Inbound.fromJson(config);
138 |     }
139 | 
140 |     hasLink() {
141 |         switch (this.protocol) {
142 |             case Protocols.VMESS:
143 |             case Protocols.VLESS:
144 |             case Protocols.TROJAN:
145 |             case Protocols.SHADOWSOCKS:
146 |                 return true;
147 |             default:
148 |                 return false;
149 |         }
150 |     }
151 | 
152 |     genLink() {
153 |         const inbound = this.toInbound();
154 |         return inbound.genLink(this.address, this.remark);
155 |     }
156 | }
157 | 
158 | class AllSetting {
159 | 
160 |     constructor(data) {
161 |         this.webListen = "";
162 |         this.webPort = 54321;
163 |         this.webCertFile = "";
164 |         this.webKeyFile = "";
165 |         this.webBasePath = "/";
166 |         this.tgBotEnable = false;
167 |         this.tgBotToken = "";
168 |         this.tgBotChatId = 0;
169 |         this.tgRunTime = "";
170 |         this.xrayTemplateConfig = "";
171 | 
172 |         this.timeLocation = "Asia/Shanghai";
173 | 
174 |         if (data == null) {
175 |             return
176 |         }
177 |         ObjectUtil.cloneProps(this, data);
178 |     }
179 | 
180 |     equals(other) {
181 |         return ObjectUtil.equals(this, other);
182 |     }
183 | }


--------------------------------------------------------------------------------
/web/assets/js/util/common.js:
--------------------------------------------------------------------------------
 1 | const ONE_KB = 1024;
 2 | const ONE_MB = ONE_KB * 1024;
 3 | const ONE_GB = ONE_MB * 1024;
 4 | const ONE_TB = ONE_GB * 1024;
 5 | const ONE_PB = ONE_TB * 1024;
 6 | 
 7 | function sizeFormat(size) {
 8 |     if (size < ONE_KB) {
 9 |         return size.toFixed(0) + " B";
10 |     } else if (size < ONE_MB) {
11 |         return (size / ONE_KB).toFixed(2) + " KB";
12 |     } else if (size < ONE_GB) {
13 |         return (size / ONE_MB).toFixed(2) + " MB";
14 |     } else if (size < ONE_TB) {
15 |         return (size / ONE_GB).toFixed(2) + " GB";
16 |     } else if (size < ONE_PB) {
17 |         return (size / ONE_TB).toFixed(2) + " TB";
18 |     } else {
19 |         return (size / ONE_PB).toFixed(2) + " PB";
20 |     }
21 | }
22 | 
23 | function base64(str) {
24 |     return Base64.encode(str);
25 | }
26 | 
27 | function safeBase64(str) {
28 |     return base64(str)
29 |         .replace(/\+/g, '-')
30 |         .replace(/=/g, '')
31 |         .replace(/\//g, '_');
32 | }
33 | 
34 | function formatSecond(second) {
35 |     if (second < 60) {
36 |         return second.toFixed(0) + ' 秒';
37 |     } else if (second < 3600) {
38 |         return (second / 60).toFixed(0) + ' 分钟';
39 |     } else if (second < 3600 * 24) {
40 |         return (second / 3600).toFixed(0) + ' 小时';
41 |     } else {
42 |         return (second / 3600 / 24).toFixed(0) + ' 天';
43 |     }
44 | }
45 | 
46 | function addZero(num) {
47 |     if (num < 10) {
48 |         return "0" + num;
49 |     } else {
50 |         return num;
51 |     }
52 | }
53 | 
54 | function toFixed(num, n) {
55 |     n = Math.pow(10, n);
56 |     return Math.round(num * n) / n;
57 | }


--------------------------------------------------------------------------------
/web/assets/js/util/date-util.js:
--------------------------------------------------------------------------------
  1 | const oneMinute = 1000 * 60; // 一分钟的毫秒数
  2 | const oneHour = oneMinute * 60; // 一小时的毫秒数
  3 | const oneDay = oneHour * 24; // 一天的毫秒数
  4 | const oneWeek = oneDay * 7; // 一星期的毫秒数
  5 | const oneMonth = oneDay * 30; // 一个月的毫秒数
  6 | 
  7 | /**
  8 |  * 按天数减少
  9 |  *
 10 |  * @param days 要减少的天数
 11 |  */
 12 | Date.prototype.minusDays = function (days) {
 13 |     return this.minusMillis(oneDay * days);
 14 | };
 15 | 
 16 | /**
 17 |  * 按天数增加
 18 |  *
 19 |  * @param days 要增加的天数
 20 |  */
 21 | Date.prototype.plusDays = function (days) {
 22 |     return this.plusMillis(oneDay * days);
 23 | };
 24 | 
 25 | /**
 26 |  * 按小时减少
 27 |  *
 28 |  * @param hours 要减少的小时数
 29 |  */
 30 | Date.prototype.minusHours = function (hours) {
 31 |     return this.minusMillis(oneHour * hours);
 32 | };
 33 | 
 34 | /**
 35 |  * 按小时增加
 36 |  *
 37 |  * @param hours 要增加的小时数
 38 |  */
 39 | Date.prototype.plusHours = function (hours) {
 40 |     return this.plusMillis(oneHour * hours);
 41 | };
 42 | 
 43 | /**
 44 |  * 按分钟减少
 45 |  *
 46 |  * @param minutes 要减少的分钟数
 47 |  */
 48 | Date.prototype.minusMinutes = function (minutes) {
 49 |     return this.minusMillis(oneMinute * minutes);
 50 | };
 51 | 
 52 | /**
 53 |  * 按分钟增加
 54 |  *
 55 |  * @param minutes 要增加的分钟数
 56 |  */
 57 | Date.prototype.plusMinutes = function (minutes) {
 58 |     return this.plusMillis(oneMinute * minutes);
 59 | };
 60 | 
 61 | /**
 62 |  * 按毫秒减少
 63 |  *
 64 |  * @param millis 要减少的毫秒数
 65 |  */
 66 | Date.prototype.minusMillis = function(millis) {
 67 |     let time = this.getTime() - millis;
 68 |     let newDate = new Date();
 69 |     newDate.setTime(time);
 70 |     return newDate;
 71 | };
 72 | 
 73 | /**
 74 |  * 按毫秒增加
 75 |  *
 76 |  * @param millis 要增加的毫秒数
 77 |  */
 78 | Date.prototype.plusMillis = function(millis) {
 79 |     let time = this.getTime() + millis;
 80 |     let newDate = new Date();
 81 |     newDate.setTime(time);
 82 |     return newDate;
 83 | };
 84 | 
 85 | /**
 86 |  * 设置时间为当天的 00:00:00.000
 87 |  */
 88 | Date.prototype.setMinTime = function () {
 89 |     this.setHours(0);
 90 |     this.setMinutes(0);
 91 |     this.setSeconds(0);
 92 |     this.setMilliseconds(0);
 93 |     return this;
 94 | };
 95 | 
 96 | /**
 97 |  * 设置时间为当天的 23:59:59.999
 98 |  */
 99 | Date.prototype.setMaxTime = function () {
100 |     this.setHours(23);
101 |     this.setMinutes(59);
102 |     this.setSeconds(59);
103 |     this.setMilliseconds(999);
104 |     return this;
105 | };
106 | 
107 | /**
108 |  * 格式化日期
109 |  */
110 | Date.prototype.formatDate = function () {
111 |     return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate());
112 | };
113 | 
114 | /**
115 |  * 格式化时间
116 |  */
117 | Date.prototype.formatTime = function () {
118 |     return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds());
119 | };
120 | 
121 | /**
122 |  * 格式化日期加时间
123 |  *
124 |  * @param split 日期和时间之间的分隔符,默认是一个空格
125 |  */
126 | Date.prototype.formatDateTime = function (split = ' ') {
127 |     return this.formatDate() + split + this.formatTime();
128 | };
129 | 
130 | class DateUtil {
131 | 
132 |     // 字符串转 Date 对象
133 |     static parseDate(str) {
134 |         return new Date(str.replace(/-/g, '/'));
135 |     }
136 | 
137 |     static formatMillis(millis) {
138 |         return moment(millis).format('YYYY-M-D H:m:s')
139 |     }
140 | 
141 |     static firstDayOfMonth() {
142 |         const date = new Date();
143 |         date.setDate(1);
144 |         date.setMinTime();
145 |         return date;
146 |     }
147 | }


--------------------------------------------------------------------------------
/web/assets/js/util/utils.js:
--------------------------------------------------------------------------------
  1 | class HttpUtil {
  2 |     static _handleMsg(msg) {
  3 |         if (!(msg instanceof Msg)) {
  4 |             return;
  5 |         }
  6 |         if (msg.msg === "") {
  7 |             return;
  8 |         }
  9 |         if (msg.success) {
 10 |             Vue.prototype.$message.success(msg.msg);
 11 |         } else {
 12 |             Vue.prototype.$message.error(msg.msg);
 13 |         }
 14 |     }
 15 | 
 16 |     static _respToMsg(resp) {
 17 |         const data = resp.data;
 18 |         if (data == null) {
 19 |             return new Msg(true);
 20 |         } else if (typeof data === 'object') {
 21 |             if (data.hasOwnProperty('success')) {
 22 |                 return new Msg(data.success, data.msg, data.obj);
 23 |             } else {
 24 |                 return data;
 25 |             }
 26 |         } else {
 27 |             return new Msg(false, 'unknown data:', data);
 28 |         }
 29 |     }
 30 | 
 31 |     static async get(url, data, options) {
 32 |         let msg;
 33 |         try {
 34 |             const resp = await axios.get(url, data, options);
 35 |             msg = this._respToMsg(resp);
 36 |         } catch (e) {
 37 |             msg = new Msg(false, e.toString());
 38 |         }
 39 |         this._handleMsg(msg);
 40 |         return msg;
 41 |     }
 42 | 
 43 |     static async post(url, data, options) {
 44 |         let msg;
 45 |         try {
 46 |             const resp = await axios.post(url, data, options);
 47 |             msg = this._respToMsg(resp);
 48 |         } catch (e) {
 49 |             msg = new Msg(false, e.toString());
 50 |         }
 51 |         this._handleMsg(msg);
 52 |         return msg;
 53 |     }
 54 | 
 55 |     static async postWithModal(url, data, modal) {
 56 |         if (modal) {
 57 |             modal.loading(true);
 58 |         }
 59 |         const msg = await this.post(url, data);
 60 |         if (modal) {
 61 |             modal.loading(false);
 62 |             if (msg instanceof Msg && msg.success) {
 63 |                 modal.close();
 64 |             }
 65 |         }
 66 |         return msg;
 67 |     }
 68 | }
 69 | 
 70 | class PromiseUtil {
 71 | 
 72 |     static async sleep(timeout) {
 73 |         await new Promise(resolve => {
 74 |             setTimeout(resolve, timeout)
 75 |         });
 76 |     }
 77 | 
 78 | }
 79 | 
 80 | const seq = [
 81 |     'a', 'b', 'c', 'd', 'e', 'f', 'g',
 82 |     'h', 'i', 'j', 'k', 'l', 'm', 'n',
 83 |     'o', 'p', 'q', 'r', 's', 't',
 84 |     'u', 'v', 'w', 'x', 'y', 'z',
 85 |     '0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
 86 |     'A', 'B', 'C', 'D', 'E', 'F', 'G',
 87 |     'H', 'I', 'J', 'K', 'L', 'M', 'N',
 88 |     'O', 'P', 'Q', 'R', 'S', 'T',
 89 |     'U', 'V', 'W', 'X', 'Y', 'Z'
 90 | ];
 91 | 
 92 | class RandomUtil {
 93 | 
 94 |     static randomIntRange(min, max) {
 95 |         return parseInt(Math.random() * (max - min) + min, 10);
 96 |     }
 97 | 
 98 |     static randomInt(n) {
 99 |         return this.randomIntRange(0, n);
100 |     }
101 | 
102 |     static randomSeq(count) {
103 |         let str = '';
104 |         for (let i = 0; i < count; ++i) {
105 |             str += seq[this.randomInt(62)];
106 |         }
107 |         return str;
108 |     }
109 | 
110 |     static randomLowerAndNum(count) {
111 |         let str = '';
112 |         for (let i = 0; i < count; ++i) {
113 |             str += seq[this.randomInt(36)];
114 |         }
115 |         return str;
116 |     }
117 | 
118 |     static randomMTSecret() {
119 |         let str = '';
120 |         for (let i = 0; i < 32; ++i) {
121 |             let index = this.randomInt(16);
122 |             if (index <= 9) {
123 |                 str += index;
124 |             } else {
125 |                 str += seq[index - 10];
126 |             }
127 |         }
128 |         return str;
129 |     }
130 | 
131 |     static randomUUID() {
132 |         let d = new Date().getTime();
133 |         return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function (c) {
134 |             let r = (d + Math.random() * 16) % 16 | 0;
135 |             d = Math.floor(d / 16);
136 |             return (c === 'x' ? r : (r & 0x7 | 0x8)).toString(16);
137 |         });
138 |     }
139 | }
140 | 
141 | class ObjectUtil {
142 | 
143 |     static getPropIgnoreCase(obj, prop) {
144 |         for (const name in obj) {
145 |             if (!obj.hasOwnProperty(name)) {
146 |                 continue;
147 |             }
148 |             if (name.toLowerCase() === prop.toLowerCase()) {
149 |                 return obj[name];
150 |             }
151 |         }
152 |         return undefined;
153 |     }
154 | 
155 |     static deepSearch(obj, key) {
156 |         if (obj instanceof Array) {
157 |             for (let i = 0; i < obj.length; ++i) {
158 |                 if (this.deepSearch(obj[i], key)) {
159 |                     return true;
160 |                 }
161 |             }
162 |         } else if (obj instanceof Object) {
163 |             for (let name in obj) {
164 |                 if (!obj.hasOwnProperty(name)) {
165 |                     continue;
166 |                 }
167 |                 if (this.deepSearch(obj[name], key)) {
168 |                     return true;
169 |                 }
170 |             }
171 |         } else {
172 |             return obj.toString().indexOf(key) >= 0;
173 |         }
174 |         return false;
175 |     }
176 | 
177 |     static isEmpty(obj) {
178 |         return obj === null || obj === undefined || obj === '';
179 |     }
180 | 
181 |     static isArrEmpty(arr) {
182 |         return !this.isEmpty(arr) && arr.length === 0;
183 |     }
184 | 
185 |     static copyArr(dest, src) {
186 |         dest.splice(0);
187 |         for (const item of src) {
188 |             dest.push(item);
189 |         }
190 |     }
191 | 
192 |     static clone(obj) {
193 |         let newObj;
194 |         if (obj instanceof Array) {
195 |             newObj = [];
196 |             this.copyArr(newObj, obj);
197 |         } else if (obj instanceof Object) {
198 |             newObj = {};
199 |             for (const key of Object.keys(obj)) {
200 |                 newObj[key] = obj[key];
201 |             }
202 |         } else {
203 |             newObj = obj;
204 |         }
205 |         return newObj;
206 |     }
207 | 
208 |     static deepClone(obj) {
209 |         let newObj;
210 |         if (obj instanceof Array) {
211 |             newObj = [];
212 |             for (const item of obj) {
213 |                 newObj.push(this.deepClone(item));
214 |             }
215 |         } else if (obj instanceof Object) {
216 |             newObj = {};
217 |             for (const key of Object.keys(obj)) {
218 |                 newObj[key] = this.deepClone(obj[key]);
219 |             }
220 |         } else {
221 |             newObj = obj;
222 |         }
223 |         return newObj;
224 |     }
225 | 
226 |     static cloneProps(dest, src, ...ignoreProps) {
227 |         if (dest == null || src == null) {
228 |             return;
229 |         }
230 |         const ignoreEmpty = this.isArrEmpty(ignoreProps);
231 |         for (const key of Object.keys(src)) {
232 |             if (!src.hasOwnProperty(key)) {
233 |                 continue;
234 |             } else if (!dest.hasOwnProperty(key)) {
235 |                 continue;
236 |             } else if (src[key] === undefined) {
237 |                 continue;
238 |             }
239 |             if (ignoreEmpty) {
240 |                 dest[key] = src[key];
241 |             } else {
242 |                 let ignore = false;
243 |                 for (let i = 0; i < ignoreProps.length; ++i) {
244 |                     if (key === ignoreProps[i]) {
245 |                         ignore = true;
246 |                         break;
247 |                     }
248 |                 }
249 |                 if (!ignore) {
250 |                     dest[key] = src[key];
251 |                 }
252 |             }
253 |         }
254 |     }
255 | 
256 |     static delProps(obj, ...props) {
257 |         for (const prop of props) {
258 |             if (prop in obj) {
259 |                 delete obj[prop];
260 |             }
261 |         }
262 |     }
263 | 
264 |     static execute(func, ...args) {
265 |         if (!this.isEmpty(func) && typeof func === 'function') {
266 |             func(...args);
267 |         }
268 |     }
269 | 
270 |     static orDefault(obj, defaultValue) {
271 |         if (obj == null) {
272 |             return defaultValue;
273 |         }
274 |         return obj;
275 |     }
276 | 
277 |     static equals(a, b) {
278 |         for (const key in a) {
279 |             if (!a.hasOwnProperty(key)) {
280 |                 continue;
281 |             }
282 |             if (!b.hasOwnProperty(key)) {
283 |                 return false;
284 |             } else if (a[key] !== b[key]) {
285 |                 return false;
286 |             }
287 |         }
288 |         return true;
289 |     }
290 | 
291 | }
292 | 


--------------------------------------------------------------------------------
/web/assets/qs/qs.min.js:
--------------------------------------------------------------------------------
1 | !function(e){if("object"==typeof exports&&"undefined"!=typeof module)module.exports=e();else if("function"==typeof define&&define.amd)define([],e);else{("undefined"!=typeof window?window:"undefined"!=typeof global?global:"undefined"!=typeof self?self:this).Qs=e()}}(function(){return function i(a,l,c){function f(r,e){if(!l[r]){if(!a[r]){var t="function"==typeof require&&require;if(!e&&t)return t(r,!0);if(s)return s(r,!0);var o=new Error("Cannot find module '"+r+"'");throw o.code="MODULE_NOT_FOUND",o}var n=l[r]={exports:{}};a[r][0].call(n.exports,function(e){return f(a[r][1][e]||e)},n,n.exports,i,a,l,c)}return l[r].exports}for(var s="function"==typeof require&&require,e=0;e<c.length;e++)f(c[e]);return f}({1:[function(e,r,t){"use strict";var o=String.prototype.replace,n=/%20/g;r.exports={default:"RFC3986",formatters:{RFC1738:function(e){return o.call(e,n,"+")},RFC3986:function(e){return e}},RFC1738:"RFC1738",RFC3986:"RFC3986"}},{}],2:[function(e,r,t){"use strict";var o=e("./stringify"),n=e("./parse"),i=e("./formats");r.exports={formats:i,parse:n,stringify:o}},{"./formats":1,"./parse":3,"./stringify":4}],3:[function(e,r,t){"use strict";var f=e("./utils"),p=Object.prototype.hasOwnProperty,d={allowDots:!1,allowPrototypes:!1,arrayLimit:20,decoder:f.decode,delimiter:"&",depth:5,parameterLimit:1e3,plainObjects:!1,strictNullHandling:!1},s=function(e,r,t){if(e){var o=t.allowDots?e.replace(/\.([^.[]+)/g,"[$1]"):e,n=/(\[[^[\]]*])/g,i=/(\[[^[\]]*])/.exec(o),a=i?o.slice(0,i.index):o,l=[];if(a){if(!t.plainObjects&&p.call(Object.prototype,a)&&!t.allowPrototypes)return;l.push(a)}for(var c=0;null!==(i=n.exec(o))&&c<t.depth;){if(c+=1,!t.plainObjects&&p.call(Object.prototype,i[1].slice(1,-1))&&!t.allowPrototypes)return;l.push(i[1])}return i&&l.push("["+o.slice(i.index)+"]"),function(e,r,t){for(var o=r,n=e.length-1;0<=n;--n){var i,a=e[n];if("[]"===a)i=(i=[]).concat(o);else{i=t.plainObjects?Object.create(null):{};var l="["===a.charAt(0)&&"]"===a.charAt(a.length-1)?a.slice(1,-1):a,c=parseInt(l,10);!isNaN(c)&&a!==l&&String(c)===l&&0<=c&&t.parseArrays&&c<=t.arrayLimit?(i=[])[c]=o:i[l]=o}o=i}return o}(l,r,t)}};r.exports=function(e,r){var t=r?f.assign({},r):{};if(null!==t.decoder&&void 0!==t.decoder&&"function"!=typeof t.decoder)throw new TypeError("Decoder has to be a function.");if(t.ignoreQueryPrefix=!0===t.ignoreQueryPrefix,t.delimiter="string"==typeof t.delimiter||f.isRegExp(t.delimiter)?t.delimiter:d.delimiter,t.depth="number"==typeof t.depth?t.depth:d.depth,t.arrayLimit="number"==typeof t.arrayLimit?t.arrayLimit:d.arrayLimit,t.parseArrays=!1!==t.parseArrays,t.decoder="function"==typeof t.decoder?t.decoder:d.decoder,t.allowDots="boolean"==typeof t.allowDots?t.allowDots:d.allowDots,t.plainObjects="boolean"==typeof t.plainObjects?t.plainObjects:d.plainObjects,t.allowPrototypes="boolean"==typeof t.allowPrototypes?t.allowPrototypes:d.allowPrototypes,t.parameterLimit="number"==typeof t.parameterLimit?t.parameterLimit:d.parameterLimit,t.strictNullHandling="boolean"==typeof t.strictNullHandling?t.strictNullHandling:d.strictNullHandling,""===e||null==e)return t.plainObjects?Object.create(null):{};for(var o="string"==typeof e?function(e,r){for(var t={},o=r.ignoreQueryPrefix?e.replace(/^\?/,""):e,n=r.parameterLimit===1/0?void 0:r.parameterLimit,i=o.split(r.delimiter,n),a=0;a<i.length;++a){var l,c,f=i[a],s=f.indexOf("]="),u=-1===s?f.indexOf("="):s+1;-1===u?(l=r.decoder(f,d.decoder),c=r.strictNullHandling?null:""):(l=r.decoder(f.slice(0,u),d.decoder),c=r.decoder(f.slice(u+1),d.decoder)),p.call(t,l)?t[l]=[].concat(t[l]).concat(c):t[l]=c}return t}(e,t):e,n=t.plainObjects?Object.create(null):{},i=Object.keys(o),a=0;a<i.length;++a){var l=i[a],c=s(l,o[l],t);n=f.merge(n,c,t)}return f.compact(n)}},{"./utils":5}],4:[function(e,r,t){"use strict";var A=e("./utils"),x=e("./formats"),N={brackets:function(e){return e+"[]"},indices:function(e,r){return e+"["+r+"]"},repeat:function(e){return e}},o=Date.prototype.toISOString,D={delimiter:"&",encode:!0,encoder:A.encode,encodeValuesOnly:!1,serializeDate:function(e){return o.call(e)},skipNulls:!1,strictNullHandling:!1},P=function e(r,t,o,n,i,a,l,c,f,s,u,p){var d=r;if("function"==typeof l)d=l(t,d);else if(d instanceof Date)d=s(d);else if(null===d){if(n)return a&&!p?a(t,D.encoder):t;d=""}if("string"==typeof d||"number"==typeof d||"boolean"==typeof d||A.isBuffer(d))return a?[u(p?t:a(t,D.encoder))+"="+u(a(d,D.encoder))]:[u(t)+"="+u(String(d))];var y,b=[];if(void 0===d)return b;if(Array.isArray(l))y=l;else{var m=Object.keys(d);y=c?m.sort(c):m}for(var g=0;g<y.length;++g){var v=y[g];i&&null===d[v]||(b=Array.isArray(d)?b.concat(e(d[v],o(t,v),o,n,i,a,l,c,f,s,u,p)):b.concat(e(d[v],t+(f?"."+v:"["+v+"]"),o,n,i,a,l,c,f,s,u,p)))}return b};r.exports=function(e,r){var t=e,o=r?A.assign({},r):{};if(null!==o.encoder&&void 0!==o.encoder&&"function"!=typeof o.encoder)throw new TypeError("Encoder has to be a function.");var n=void 0===o.delimiter?D.delimiter:o.delimiter,i="boolean"==typeof o.strictNullHandling?o.strictNullHandling:D.strictNullHandling,a="boolean"==typeof o.skipNulls?o.skipNulls:D.skipNulls,l="boolean"==typeof o.encode?o.encode:D.encode,c="function"==typeof o.encoder?o.encoder:D.encoder,f="function"==typeof o.sort?o.sort:null,s=void 0!==o.allowDots&&o.allowDots,u="function"==typeof o.serializeDate?o.serializeDate:D.serializeDate,p="boolean"==typeof o.encodeValuesOnly?o.encodeValuesOnly:D.encodeValuesOnly;if(void 0===o.format)o.format=x.default;else if(!Object.prototype.hasOwnProperty.call(x.formatters,o.format))throw new TypeError("Unknown format option provided.");var d,y,b=x.formatters[o.format];"function"==typeof o.filter?t=(y=o.filter)("",t):Array.isArray(o.filter)&&(d=y=o.filter);var m,g=[];if("object"!=typeof t||null===t)return"";m=o.arrayFormat in N?o.arrayFormat:"indices"in o?o.indices?"indices":"repeat":"indices";var v=N[m];d||(d=Object.keys(t)),f&&d.sort(f);for(var h=0;h<d.length;++h){var j=d[h];a&&null===t[j]||(g=g.concat(P(t[j],j,v,i,a,l?c:null,y,f,s,u,b,p)))}var O=g.join(n),w=!0===o.addQueryPrefix?"?":"";return 0<O.length?w+O:""}},{"./formats":1,"./utils":5}],5:[function(e,r,t){"use strict";var a=Object.prototype.hasOwnProperty,i=function(){for(var e=[],r=0;r<256;++r)e.push("%"+((r<16?"0":"")+r.toString(16)).toUpperCase());return e}(),l=function(e,r){for(var t=r&&r.plainObjects?Object.create(null):{},o=0;o<e.length;++o)void 0!==e[o]&&(t[o]=e[o]);return t};r.exports={arrayToObject:l,assign:function(e,t){return Object.keys(t).reduce(function(e,r){return e[r]=t[r],e},e)},compact:function(e){for(var r=[{obj:{o:e},prop:"o"}],t=[],o=0;o<r.length;++o)for(var n=r[o],i=n.obj[n.prop],a=Object.keys(i),l=0;l<a.length;++l){var c=a[l],f=i[c];"object"==typeof f&&null!==f&&-1===t.indexOf(f)&&(r.push({obj:i,prop:c}),t.push(f))}return function(e){for(var r;e.length;){var t=e.pop();if(r=t.obj[t.prop],Array.isArray(r)){for(var o=[],n=0;n<r.length;++n)void 0!==r[n]&&o.push(r[n]);t.obj[t.prop]=o}}return r}(r)},decode:function(r){try{return decodeURIComponent(r.replace(/\+/g," "))}catch(e){return r}},encode:function(e){if(0===e.length)return e;for(var r="string"==typeof e?e:String(e),t="",o=0;o<r.length;++o){var n=r.charCodeAt(o);45===n||46===n||95===n||126===n||48<=n&&n<=57||65<=n&&n<=90||97<=n&&n<=122?t+=r.charAt(o):n<128?t+=i[n]:n<2048?t+=i[192|n>>6]+i[128|63&n]:n<55296||57344<=n?t+=i[224|n>>12]+i[128|n>>6&63]+i[128|63&n]:(o+=1,n=65536+((1023&n)<<10|1023&r.charCodeAt(o)),t+=i[240|n>>18]+i[128|n>>12&63]+i[128|n>>6&63]+i[128|63&n])}return t},isBuffer:function(e){return null!=e&&!!(e.constructor&&e.constructor.isBuffer&&e.constructor.isBuffer(e))},isRegExp:function(e){return"[object RegExp]"===Object.prototype.toString.call(e)},merge:function o(t,n,i){if(!n)return t;if("object"!=typeof n){if(Array.isArray(t))t.push(n);else{if("object"!=typeof t)return[t,n];(i.plainObjects||i.allowPrototypes||!a.call(Object.prototype,n))&&(t[n]=!0)}return t}if("object"!=typeof t)return[t].concat(n);var e=t;return Array.isArray(t)&&!Array.isArray(n)&&(e=l(t,i)),Array.isArray(t)&&Array.isArray(n)?(n.forEach(function(e,r){a.call(t,r)?t[r]&&"object"==typeof t[r]?t[r]=o(t[r],e,i):t.push(e):t[r]=e}),t):Object.keys(n).reduce(function(e,r){var t=n[r];return a.call(e,r)?e[r]=o(e[r],t,i):e[r]=t,e},e)}}},{}]},{},[2])(2)});


--------------------------------------------------------------------------------
/web/assets/vue@2.6.12/vue.common.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 |   module.exports = require('./vue.common.prod.js')
3 | } else {
4 |   module.exports = require('./vue.common.dev.js')
5 | }
6 | 


--------------------------------------------------------------------------------
/web/assets/vue@2.6.12/vue.runtime.common.js:
--------------------------------------------------------------------------------
1 | if (process.env.NODE_ENV === 'production') {
2 |   module.exports = require('./vue.runtime.common.prod.js')
3 | } else {
4 |   module.exports = require('./vue.runtime.common.dev.js')
5 | }
6 | 


--------------------------------------------------------------------------------
/web/controller/base.go:
--------------------------------------------------------------------------------
 1 | package controller
 2 | 
 3 | import (
 4 | 	"github.com/gin-gonic/gin"
 5 | 	"net/http"
 6 | 	"x-ui/web/session"
 7 | )
 8 | 
 9 | type BaseController struct {
10 | }
11 | 
12 | func (a *BaseController) checkLogin(c *gin.Context) {
13 | 	if !session.IsLogin(c) {
14 | 		if isAjax(c) {
15 | 			pureJsonMsg(c, false, "登录时效已过,请重新登录")
16 | 		} else {
17 | 			c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
18 | 		}
19 | 		c.Abort()
20 | 	} else {
21 | 		c.Next()
22 | 	}
23 | }
24 | 


--------------------------------------------------------------------------------
/web/controller/inbound.go:
--------------------------------------------------------------------------------
  1 | package controller
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"github.com/gin-gonic/gin"
  6 | 	"strconv"
  7 | 	"x-ui/database/model"
  8 | 	"x-ui/logger"
  9 | 	"x-ui/web/global"
 10 | 	"x-ui/web/service"
 11 | 	"x-ui/web/session"
 12 | )
 13 | 
 14 | type InboundController struct {
 15 | 	inboundService service.InboundService
 16 | 	xrayService    service.XrayService
 17 | }
 18 | 
 19 | func NewInboundController(g *gin.RouterGroup) *InboundController {
 20 | 	a := &InboundController{}
 21 | 	a.initRouter(g)
 22 | 	a.startTask()
 23 | 	return a
 24 | }
 25 | 
 26 | func (a *InboundController) initRouter(g *gin.RouterGroup) {
 27 | 	g = g.Group("/inbound")
 28 | 
 29 | 	g.POST("/list", a.getInbounds)
 30 | 	g.POST("/add", a.addInbound)
 31 | 	g.POST("/del/:id", a.delInbound)
 32 | 	g.POST("/update/:id", a.updateInbound)
 33 | }
 34 | 
 35 | func (a *InboundController) startTask() {
 36 | 	webServer := global.GetWebServer()
 37 | 	c := webServer.GetCron()
 38 | 	c.AddFunc("@every 10s", func() {
 39 | 		if a.xrayService.IsNeedRestartAndSetFalse() {
 40 | 			err := a.xrayService.RestartXray(false)
 41 | 			if err != nil {
 42 | 				logger.Error("restart xray failed:", err)
 43 | 			}
 44 | 		}
 45 | 	})
 46 | }
 47 | 
 48 | func (a *InboundController) getInbounds(c *gin.Context) {
 49 | 	user := session.GetLoginUser(c)
 50 | 	inbounds, err := a.inboundService.GetInbounds(user.Id)
 51 | 	if err != nil {
 52 | 		jsonMsg(c, "获取", err)
 53 | 		return
 54 | 	}
 55 | 	jsonObj(c, inbounds, nil)
 56 | }
 57 | 
 58 | func (a *InboundController) addInbound(c *gin.Context) {
 59 | 	inbound := &model.Inbound{}
 60 | 	err := c.ShouldBind(inbound)
 61 | 	if err != nil {
 62 | 		jsonMsg(c, "添加", err)
 63 | 		return
 64 | 	}
 65 | 	user := session.GetLoginUser(c)
 66 | 	inbound.UserId = user.Id
 67 | 	inbound.Enable = true
 68 | 	inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
 69 | 	err = a.inboundService.AddInbound(inbound)
 70 | 	jsonMsg(c, "添加", err)
 71 | 	if err == nil {
 72 | 		a.xrayService.SetToNeedRestart()
 73 | 	}
 74 | }
 75 | 
 76 | func (a *InboundController) delInbound(c *gin.Context) {
 77 | 	id, err := strconv.Atoi(c.Param("id"))
 78 | 	if err != nil {
 79 | 		jsonMsg(c, "删除", err)
 80 | 		return
 81 | 	}
 82 | 	err = a.inboundService.DelInbound(id)
 83 | 	jsonMsg(c, "删除", err)
 84 | 	if err == nil {
 85 | 		a.xrayService.SetToNeedRestart()
 86 | 	}
 87 | }
 88 | 
 89 | func (a *InboundController) updateInbound(c *gin.Context) {
 90 | 	id, err := strconv.Atoi(c.Param("id"))
 91 | 	if err != nil {
 92 | 		jsonMsg(c, "修改", err)
 93 | 		return
 94 | 	}
 95 | 	inbound := &model.Inbound{
 96 | 		Id: id,
 97 | 	}
 98 | 	err = c.ShouldBind(inbound)
 99 | 	if err != nil {
100 | 		jsonMsg(c, "修改", err)
101 | 		return
102 | 	}
103 | 	err = a.inboundService.UpdateInbound(inbound)
104 | 	jsonMsg(c, "修改", err)
105 | 	if err == nil {
106 | 		a.xrayService.SetToNeedRestart()
107 | 	}
108 | }
109 | 


--------------------------------------------------------------------------------
/web/controller/index.go:
--------------------------------------------------------------------------------
 1 | package controller
 2 | 
 3 | import (
 4 | 	"net/http"
 5 | 	"time"
 6 | 	"x-ui/logger"
 7 | 	"x-ui/web/job"
 8 | 	"x-ui/web/service"
 9 | 	"x-ui/web/session"
10 | 
11 | 	"github.com/gin-gonic/gin"
12 | )
13 | 
14 | type LoginForm struct {
15 | 	Username string `json:"username" form:"username"`
16 | 	Password string `json:"password" form:"password"`
17 | }
18 | 
19 | type IndexController struct {
20 | 	BaseController
21 | 
22 | 	userService service.UserService
23 | }
24 | 
25 | func NewIndexController(g *gin.RouterGroup) *IndexController {
26 | 	a := &IndexController{}
27 | 	a.initRouter(g)
28 | 	return a
29 | }
30 | 
31 | func (a *IndexController) initRouter(g *gin.RouterGroup) {
32 | 	g.GET("/", a.index)
33 | 	g.POST("/login", a.login)
34 | 	g.GET("/logout", a.logout)
35 | }
36 | 
37 | func (a *IndexController) index(c *gin.Context) {
38 | 	if session.IsLogin(c) {
39 | 		c.Redirect(http.StatusTemporaryRedirect, "xui/")
40 | 		return
41 | 	}
42 | 	html(c, "login.html", "登录", nil)
43 | }
44 | 
45 | func (a *IndexController) login(c *gin.Context) {
46 | 	var form LoginForm
47 | 	err := c.ShouldBind(&form)
48 | 	if err != nil {
49 | 		pureJsonMsg(c, false, "数据格式错误")
50 | 		return
51 | 	}
52 | 	if form.Username == "" {
53 | 		pureJsonMsg(c, false, "请输入用户名")
54 | 		return
55 | 	}
56 | 	if form.Password == "" {
57 | 		pureJsonMsg(c, false, "请输入密码")
58 | 		return
59 | 	}
60 | 	user := a.userService.CheckUser(form.Username, form.Password)
61 | 	timeStr := time.Now().Format("2006-01-02 15:04:05")
62 | 	if user == nil {
63 | 		job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0)
64 | 		logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password)
65 | 		pureJsonMsg(c, false, "用户名或密码错误")
66 | 		return
67 | 	} else {
68 | 		logger.Infof("%s login success,Ip Address:%s\n", form.Username, getRemoteIp(c))
69 | 		job.NewStatsNotifyJob().UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1)
70 | 	}
71 | 
72 | 	err = session.SetLoginUser(c, user)
73 | 	logger.Info("user", user.Id, "login success")
74 | 	jsonMsg(c, "登录", err)
75 | }
76 | 
77 | func (a *IndexController) logout(c *gin.Context) {
78 | 	user := session.GetLoginUser(c)
79 | 	if user != nil {
80 | 		logger.Info("user", user.Id, "logout")
81 | 	}
82 | 	session.ClearSession(c)
83 | 	c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path"))
84 | }
85 | 


--------------------------------------------------------------------------------
/web/controller/server.go:
--------------------------------------------------------------------------------
 1 | package controller
 2 | 
 3 | import (
 4 | 	"github.com/gin-gonic/gin"
 5 | 	"time"
 6 | 	"x-ui/web/global"
 7 | 	"x-ui/web/service"
 8 | )
 9 | 
10 | type ServerController struct {
11 | 	BaseController
12 | 
13 | 	serverService service.ServerService
14 | 
15 | 	lastStatus        *service.Status
16 | 	lastGetStatusTime time.Time
17 | 
18 | 	lastVersions        []string
19 | 	lastGetVersionsTime time.Time
20 | }
21 | 
22 | func NewServerController(g *gin.RouterGroup) *ServerController {
23 | 	a := &ServerController{
24 | 		lastGetStatusTime: time.Now(),
25 | 	}
26 | 	a.initRouter(g)
27 | 	a.startTask()
28 | 	return a
29 | }
30 | 
31 | func (a *ServerController) initRouter(g *gin.RouterGroup) {
32 | 	g = g.Group("/server")
33 | 
34 | 	g.Use(a.checkLogin)
35 | 	g.POST("/status", a.status)
36 | 	g.POST("/getXrayVersion", a.getXrayVersion)
37 | 	g.POST("/installXray/:version", a.installXray)
38 | }
39 | 
40 | func (a *ServerController) refreshStatus() {
41 | 	a.lastStatus = a.serverService.GetStatus(a.lastStatus)
42 | }
43 | 
44 | func (a *ServerController) startTask() {
45 | 	webServer := global.GetWebServer()
46 | 	c := webServer.GetCron()
47 | 	c.AddFunc("@every 2s", func() {
48 | 		now := time.Now()
49 | 		if now.Sub(a.lastGetStatusTime) > time.Minute*3 {
50 | 			return
51 | 		}
52 | 		a.refreshStatus()
53 | 	})
54 | }
55 | 
56 | func (a *ServerController) status(c *gin.Context) {
57 | 	a.lastGetStatusTime = time.Now()
58 | 
59 | 	jsonObj(c, a.lastStatus, nil)
60 | }
61 | 
62 | func (a *ServerController) getXrayVersion(c *gin.Context) {
63 | 	now := time.Now()
64 | 	if now.Sub(a.lastGetVersionsTime) <= time.Minute {
65 | 		jsonObj(c, a.lastVersions, nil)
66 | 		return
67 | 	}
68 | 
69 | 	versions, err := a.serverService.GetXrayVersions()
70 | 	if err != nil {
71 | 		jsonMsg(c, "获取版本", err)
72 | 		return
73 | 	}
74 | 
75 | 	a.lastVersions = versions
76 | 	a.lastGetVersionsTime = time.Now()
77 | 
78 | 	jsonObj(c, versions, nil)
79 | }
80 | 
81 | func (a *ServerController) installXray(c *gin.Context) {
82 | 	version := c.Param("version")
83 | 	err := a.serverService.UpdateXray(version)
84 | 	jsonMsg(c, "安装 xray", err)
85 | }
86 | 


--------------------------------------------------------------------------------
/web/controller/setting.go:
--------------------------------------------------------------------------------
 1 | package controller
 2 | 
 3 | import (
 4 | 	"errors"
 5 | 	"github.com/gin-gonic/gin"
 6 | 	"time"
 7 | 	"x-ui/web/entity"
 8 | 	"x-ui/web/service"
 9 | 	"x-ui/web/session"
10 | )
11 | 
12 | type updateUserForm struct {
13 | 	OldUsername string `json:"oldUsername" form:"oldUsername"`
14 | 	OldPassword string `json:"oldPassword" form:"oldPassword"`
15 | 	NewUsername string `json:"newUsername" form:"newUsername"`
16 | 	NewPassword string `json:"newPassword" form:"newPassword"`
17 | }
18 | 
19 | type SettingController struct {
20 | 	settingService service.SettingService
21 | 	userService    service.UserService
22 | 	panelService   service.PanelService
23 | }
24 | 
25 | func NewSettingController(g *gin.RouterGroup) *SettingController {
26 | 	a := &SettingController{}
27 | 	a.initRouter(g)
28 | 	return a
29 | }
30 | 
31 | func (a *SettingController) initRouter(g *gin.RouterGroup) {
32 | 	g = g.Group("/setting")
33 | 
34 | 	g.POST("/all", a.getAllSetting)
35 | 	g.POST("/update", a.updateSetting)
36 | 	g.POST("/updateUser", a.updateUser)
37 | 	g.POST("/restartPanel", a.restartPanel)
38 | }
39 | 
40 | func (a *SettingController) getAllSetting(c *gin.Context) {
41 | 	allSetting, err := a.settingService.GetAllSetting()
42 | 	if err != nil {
43 | 		jsonMsg(c, "获取设置", err)
44 | 		return
45 | 	}
46 | 	jsonObj(c, allSetting, nil)
47 | }
48 | 
49 | func (a *SettingController) updateSetting(c *gin.Context) {
50 | 	allSetting := &entity.AllSetting{}
51 | 	err := c.ShouldBind(allSetting)
52 | 	if err != nil {
53 | 		jsonMsg(c, "修改设置", err)
54 | 		return
55 | 	}
56 | 	err = a.settingService.UpdateAllSetting(allSetting)
57 | 	jsonMsg(c, "修改设置", err)
58 | }
59 | 
60 | func (a *SettingController) updateUser(c *gin.Context) {
61 | 	form := &updateUserForm{}
62 | 	err := c.ShouldBind(form)
63 | 	if err != nil {
64 | 		jsonMsg(c, "修改用户", err)
65 | 		return
66 | 	}
67 | 	user := session.GetLoginUser(c)
68 | 	if user.Username != form.OldUsername || user.Password != form.OldPassword {
69 | 		jsonMsg(c, "修改用户", errors.New("原用户名或原密码错误"))
70 | 		return
71 | 	}
72 | 	if form.NewUsername == "" || form.NewPassword == "" {
73 | 		jsonMsg(c, "修改用户", errors.New("新用户名和新密码不能为空"))
74 | 		return
75 | 	}
76 | 	err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword)
77 | 	if err == nil {
78 | 		user.Username = form.NewUsername
79 | 		user.Password = form.NewPassword
80 | 		session.SetLoginUser(c, user)
81 | 	}
82 | 	jsonMsg(c, "修改用户", err)
83 | }
84 | 
85 | func (a *SettingController) restartPanel(c *gin.Context) {
86 | 	err := a.panelService.RestartPanel(time.Second * 3)
87 | 	jsonMsg(c, "重启面板", err)
88 | }
89 | 


--------------------------------------------------------------------------------
/web/controller/util.go:
--------------------------------------------------------------------------------
 1 | package controller
 2 | 
 3 | import (
 4 | 	"github.com/gin-gonic/gin"
 5 | 	"net"
 6 | 	"net/http"
 7 | 	"strings"
 8 | 	"x-ui/config"
 9 | 	"x-ui/logger"
10 | 	"x-ui/web/entity"
11 | )
12 | 
13 | func getUriId(c *gin.Context) int64 {
14 | 	s := struct {
15 | 		Id int64 `uri:"id"`
16 | 	}{}
17 | 
18 | 	_ = c.BindUri(&s)
19 | 	return s.Id
20 | }
21 | 
22 | func getRemoteIp(c *gin.Context) string {
23 | 	value := c.GetHeader("X-Forwarded-For")
24 | 	if value != "" {
25 | 		ips := strings.Split(value, ",")
26 | 		return ips[0]
27 | 	} else {
28 | 		addr := c.Request.RemoteAddr
29 | 		ip, _, _ := net.SplitHostPort(addr)
30 | 		return ip
31 | 	}
32 | }
33 | 
34 | func jsonMsg(c *gin.Context, msg string, err error) {
35 | 	jsonMsgObj(c, msg, nil, err)
36 | }
37 | 
38 | func jsonObj(c *gin.Context, obj interface{}, err error) {
39 | 	jsonMsgObj(c, "", obj, err)
40 | }
41 | 
42 | func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) {
43 | 	m := entity.Msg{
44 | 		Obj: obj,
45 | 	}
46 | 	if err == nil {
47 | 		m.Success = true
48 | 		if msg != "" {
49 | 			m.Msg = msg + "成功"
50 | 		}
51 | 	} else {
52 | 		m.Success = false
53 | 		m.Msg = msg + "失败: " + err.Error()
54 | 		logger.Warning(msg+"失败: ", err)
55 | 	}
56 | 	c.JSON(http.StatusOK, m)
57 | }
58 | 
59 | func pureJsonMsg(c *gin.Context, success bool, msg string) {
60 | 	if success {
61 | 		c.JSON(http.StatusOK, entity.Msg{
62 | 			Success: true,
63 | 			Msg:     msg,
64 | 		})
65 | 	} else {
66 | 		c.JSON(http.StatusOK, entity.Msg{
67 | 			Success: false,
68 | 			Msg:     msg,
69 | 		})
70 | 	}
71 | }
72 | 
73 | func html(c *gin.Context, name string, title string, data gin.H) {
74 | 	if data == nil {
75 | 		data = gin.H{}
76 | 	}
77 | 	data["title"] = title
78 | 	data["request_uri"] = c.Request.RequestURI
79 | 	data["base_path"] = c.GetString("base_path")
80 | 	c.HTML(http.StatusOK, name, getContext(data))
81 | }
82 | 
83 | func getContext(h gin.H) gin.H {
84 | 	a := gin.H{
85 | 		"cur_ver": config.GetVersion(),
86 | 	}
87 | 	if h != nil {
88 | 		for key, value := range h {
89 | 			a[key] = value
90 | 		}
91 | 	}
92 | 	return a
93 | }
94 | 
95 | func isAjax(c *gin.Context) bool {
96 | 	return c.GetHeader("X-Requested-With") == "XMLHttpRequest"
97 | }
98 | 


--------------------------------------------------------------------------------
/web/controller/xui.go:
--------------------------------------------------------------------------------
 1 | package controller
 2 | 
 3 | import (
 4 | 	"github.com/gin-gonic/gin"
 5 | )
 6 | 
 7 | type XUIController struct {
 8 | 	BaseController
 9 | 
10 | 	inboundController *InboundController
11 | 	settingController *SettingController
12 | }
13 | 
14 | func NewXUIController(g *gin.RouterGroup) *XUIController {
15 | 	a := &XUIController{}
16 | 	a.initRouter(g)
17 | 	return a
18 | }
19 | 
20 | func (a *XUIController) initRouter(g *gin.RouterGroup) {
21 | 	g = g.Group("/xui")
22 | 	g.Use(a.checkLogin)
23 | 
24 | 	g.GET("/", a.index)
25 | 	g.GET("/inbounds", a.inbounds)
26 | 	g.GET("/setting", a.setting)
27 | 
28 | 	a.inboundController = NewInboundController(g)
29 | 	a.settingController = NewSettingController(g)
30 | }
31 | 
32 | func (a *XUIController) index(c *gin.Context) {
33 | 	html(c, "index.html", "系统状态", nil)
34 | }
35 | 
36 | func (a *XUIController) inbounds(c *gin.Context) {
37 | 	html(c, "inbounds.html", "入站列表", nil)
38 | }
39 | 
40 | func (a *XUIController) setting(c *gin.Context) {
41 | 	html(c, "setting.html", "设置", nil)
42 | }
43 | 


--------------------------------------------------------------------------------
/web/entity/entity.go:
--------------------------------------------------------------------------------
 1 | package entity
 2 | 
 3 | import (
 4 | 	"crypto/tls"
 5 | 	"encoding/json"
 6 | 	"net"
 7 | 	"strings"
 8 | 	"time"
 9 | 	"x-ui/util/common"
10 | 	"x-ui/xray"
11 | )
12 | 
13 | type Msg struct {
14 | 	Success bool        `json:"success"`
15 | 	Msg     string      `json:"msg"`
16 | 	Obj     interface{} `json:"obj"`
17 | }
18 | 
19 | type Pager struct {
20 | 	Current  int         `json:"current"`
21 | 	PageSize int         `json:"page_size"`
22 | 	Total    int         `json:"total"`
23 | 	OrderBy  string      `json:"order_by"`
24 | 	Desc     bool        `json:"desc"`
25 | 	Key      string      `json:"key"`
26 | 	List     interface{} `json:"list"`
27 | }
28 | 
29 | type AllSetting struct {
30 | 	WebListen          string `json:"webListen" form:"webListen"`
31 | 	WebPort            int    `json:"webPort" form:"webPort"`
32 | 	WebCertFile        string `json:"webCertFile" form:"webCertFile"`
33 | 	WebKeyFile         string `json:"webKeyFile" form:"webKeyFile"`
34 | 	WebBasePath        string `json:"webBasePath" form:"webBasePath"`
35 | 	TgBotEnable        bool   `json:"tgBotEnable" form:"tgBotEnable"`
36 | 	TgBotToken         string `json:"tgBotToken" form:"tgBotToken"`
37 | 	TgBotChatId        int    `json:"tgBotChatId" form:"tgBotChatId"`
38 | 	TgRunTime          string `json:"tgRunTime" form:"tgRunTime"`
39 | 	XrayTemplateConfig string `json:"xrayTemplateConfig" form:"xrayTemplateConfig"`
40 | 
41 | 	TimeLocation string `json:"timeLocation" form:"timeLocation"`
42 | }
43 | 
44 | func (s *AllSetting) CheckValid() error {
45 | 	if s.WebListen != "" {
46 | 		ip := net.ParseIP(s.WebListen)
47 | 		if ip == nil {
48 | 			return common.NewError("web listen is not valid ip:", s.WebListen)
49 | 		}
50 | 	}
51 | 
52 | 	if s.WebPort <= 0 || s.WebPort > 65535 {
53 | 		return common.NewError("web port is not a valid port:", s.WebPort)
54 | 	}
55 | 
56 | 	if s.WebCertFile != "" || s.WebKeyFile != "" {
57 | 		_, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile)
58 | 		if err != nil {
59 | 			return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.WebCertFile, s.WebKeyFile, err)
60 | 		}
61 | 	}
62 | 
63 | 	if !strings.HasPrefix(s.WebBasePath, "/") {
64 | 		s.WebBasePath = "/" + s.WebBasePath
65 | 	}
66 | 	if !strings.HasSuffix(s.WebBasePath, "/") {
67 | 		s.WebBasePath += "/"
68 | 	}
69 | 
70 | 	xrayConfig := &xray.Config{}
71 | 	err := json.Unmarshal([]byte(s.XrayTemplateConfig), xrayConfig)
72 | 	if err != nil {
73 | 		return common.NewError("xray template config invalid:", err)
74 | 	}
75 | 
76 | 	_, err = time.LoadLocation(s.TimeLocation)
77 | 	if err != nil {
78 | 		return common.NewError("time location not exist:", s.TimeLocation)
79 | 	}
80 | 
81 | 	return nil
82 | }
83 | 


--------------------------------------------------------------------------------
/web/global/global.go:
--------------------------------------------------------------------------------
 1 | package global
 2 | 
 3 | import (
 4 | 	"context"
 5 | 	"github.com/robfig/cron/v3"
 6 | 	_ "unsafe"
 7 | )
 8 | 
 9 | var webServer WebServer
10 | 
11 | type WebServer interface {
12 | 	GetCron() *cron.Cron
13 | 	GetCtx() context.Context
14 | }
15 | 
16 | func SetWebServer(s WebServer) {
17 | 	webServer = s
18 | }
19 | 
20 | func GetWebServer() WebServer {
21 | 	return webServer
22 | }
23 | 


--------------------------------------------------------------------------------
/web/html/common/head.html:
--------------------------------------------------------------------------------
 1 | {{define "head"}}
 2 | <head>
 3 |     <meta charset="UTF-8">
 4 |     <meta name="renderer" content="webkit">
 5 |     <meta http-equiv="X-UA-Compatible" content="IE=edge,chrome=1">
 6 |     <meta name="viewport" content="width=device-width, initial-scale=1.0">
 7 |     <link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.css">
 8 |     <link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
 9 |     <link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
10 |     <style>
11 |         [v-cloak] {
12 |             display: none;
13 |         }
14 |     </style>
15 |     <title>{{.title}}</title>
16 | </head>
17 | {{end}}


--------------------------------------------------------------------------------
/web/html/common/js.html:
--------------------------------------------------------------------------------
 1 | {{define "js"}}
 2 | <script src="{{ .base_path }}assets/vue@2.6.12/vue.min.js"></script>
 3 | <script src="{{ .base_path }}assets/moment/moment.min.js"></script>
 4 | <script src="{{ .base_path }}assets/ant-design-vue@1.7.2/antd.min.js"></script>
 5 | <script src="{{ .base_path }}assets/base64/base64.min.js"></script>
 6 | <script src="{{ .base_path }}assets/axios/axios.min.js"></script>
 7 | <script src="{{ .base_path }}assets/qs/qs.min.js"></script>
 8 | <script src="{{ .base_path }}assets/qrcode/qrious.min.js"></script>
 9 | <script src="{{ .base_path }}assets/clipboard/clipboard.min.js"></script>
10 | <script src="{{ .base_path }}assets/uri/URI.min.js"></script>
11 | <script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
12 | <script src="{{ .base_path }}assets/js/util/common.js?{{ .cur_ver }}"></script>
13 | <script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
14 | <script src="{{ .base_path }}assets/js/util/utils.js?{{ .cur_ver }}"></script>
15 | <script src="{{ .base_path }}assets/js/model/xray.js?{{ .cur_ver }}"></script>
16 | <script src="{{ .base_path }}assets/js/model/models.js?{{ .cur_ver }}"></script>
17 | <script>
18 |     const basePath = '{{ .base_path }}';
19 |     axios.defaults.baseURL = basePath;
20 | </script>
21 | {{end}}


--------------------------------------------------------------------------------
/web/html/common/prompt_modal.html:
--------------------------------------------------------------------------------
 1 | {{define "promptModal"}}
 2 | <a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
 3 |          :closable="true" @ok="promptModal.ok" :mask-closable="false"
 4 |          :ok-text="promptModal.okText" cancel-text="取消">
 5 |     <a-input id="prompt-modal-input" :type="promptModal.type"
 6 |              v-model="promptModal.value"
 7 |              :autosize="{minRows: 10, maxRows: 20}"
 8 |              @keydown.enter.native="promptModal.keyEnter"
 9 |              @keydown.ctrl.83="promptModal.ctrlS"></a-input>
10 | </a-modal>
11 | 
12 | <script>
13 | 
14 |     const promptModal = {
15 |         title: '',
16 |         type: '',
17 |         value: '',
18 |         okText: '确定',
19 |         visible: false,
20 |         keyEnter(e) {
21 |             if (this.type !== 'textarea') {
22 |                 e.preventDefault();
23 |                 this.ok();
24 |             }
25 |         },
26 |         ctrlS(e) {
27 |             if (this.type === 'textarea') {
28 |                 e.preventDefault();
29 |                 promptModal.confirm(promptModal.value);
30 |             }
31 |         },
32 |         ok() {
33 |             promptModal.close();
34 |             promptModal.confirm(promptModal.value);
35 |         },
36 |         confirm() {},
37 |         open({
38 |                 title='',
39 |                 type='text',
40 |                 value='',
41 |                 okText='确定',
42 |                 confirm=() => {},
43 |             }) {
44 |             this.title = title;
45 |             this.type = type;
46 |             this.value = value;
47 |             this.okText = okText;
48 |             this.confirm = confirm;
49 |             this.visible = true;
50 |             promptModalApp.$nextTick(() => {
51 |                 document.querySelector('#prompt-modal-input').focus();
52 |             });
53 |         },
54 |         close() {
55 |             this.visible = false;
56 |         }
57 |     };
58 | 
59 |     const promptModalApp = new Vue({
60 |         el: '#prompt-modal',
61 |         data: {
62 |             promptModal: promptModal,
63 |         },
64 |     });
65 | 
66 | </script>
67 | {{end}}


--------------------------------------------------------------------------------
/web/html/common/qrcode_modal.html:
--------------------------------------------------------------------------------
 1 | {{define "qrcodeModal"}}
 2 | <a-modal id="qrcode-modal" v-model="qrModal.visible" :title="qrModal.title"
 3 |          :closable="true" width="300px" :ok-text="qrModal.okText"
 4 |          cancel-text='{{ i18n "close" }}' :ok-button-props="{attrs:{id:'qr-modal-ok-btn'}}">
 5 |     <canvas id="qrCode" style="width: 100%; height: 100%;"></canvas>
 6 | </a-modal>
 7 | 
 8 | <script>
 9 | 
10 |     const qrModal = {
11 |         title: '',
12 |         content: '',
13 |         okText: '',
14 |         copyText: '',
15 |         qrcode: null,
16 |         clipboard: null,
17 |         visible: false,
18 |         show: function (title='', content='', okText='{{ i18n "copy" }}', copyText='') {
19 |             this.title = title;
20 |             this.content = content;
21 |             this.okText = okText;
22 |             if (ObjectUtil.isEmpty(copyText)) {
23 |                 this.copyText = content;
24 |             } else {
25 |                 this.copyText = copyText;
26 |             }
27 |             this.visible = true;
28 |             qrModalApp.$nextTick(() => {
29 |                 if (this.clipboard === null) {
30 |                     this.clipboard = new ClipboardJS('#qr-modal-ok-btn', {
31 |                         text: () => this.copyText,
32 |                     });
33 |                     this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
34 |                 }
35 |                 if (this.qrcode === null) {
36 |                     this.qrcode = new QRious({
37 |                         element: document.querySelector('#qrCode'),
38 |                         size: 260,
39 |                         value: content,
40 |                     });
41 |                 } else {
42 |                     this.qrcode.value = content;
43 |                 }
44 |             });
45 |         },
46 |         close: function () {
47 |             this.visible = false;
48 |         },
49 |     };
50 | 
51 |     const qrModalApp = new Vue({
52 |         el: '#qrcode-modal',
53 |         data: {
54 |             qrModal: qrModal,
55 |         },
56 |     });
57 | 
58 | </script>
59 | {{end}}


--------------------------------------------------------------------------------
/web/html/common/text_modal.html:
--------------------------------------------------------------------------------
 1 | {{define "textModal"}}
 2 | <a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
 3 |          :closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
 4 |          :ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
 5 |     <a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
 6 |               @click="downloader.download(txtModal.fileName, txtModal.content)">
 7 |         {{ i18n "download" }} [[ txtModal.fileName ]]
 8 |     </a-button>
 9 |     <a-input type="textarea" v-model="txtModal.content"
10 |              :autosize="{ minRows: 10, maxRows: 20}"></a-input>
11 | </a-modal>
12 | 
13 | <script>
14 | 
15 |     const txtModal = {
16 |         title: '',
17 |         content: '',
18 |         fileName: '',
19 |         qrcode: null,
20 |         clipboard: null,
21 |         visible: false,
22 |         show: function (title='', content='', fileName='') {
23 |             this.title = title;
24 |             this.content = content;
25 |             this.fileName = fileName;
26 |             this.visible = true;
27 |             textModalApp.$nextTick(() => {
28 |                 if (this.clipboard === null) {
29 |                     this.clipboard = new ClipboardJS('#txt-modal-ok-btn', {
30 |                         text: () => this.content,
31 |                     });
32 |                     this.clipboard.on('success', () => app.$message.success('{{ i18n "copied" }}'));
33 |                 }
34 |                 if (this.qrcode === null) {
35 |                     this.qrcode = new QRious({
36 |                         element: document.querySelector('#qrCode'),
37 |                         size: 260,
38 |                         value: content,
39 |                     });
40 |                 } else {
41 |                     this.qrcode.value = content;
42 |                 }
43 |             });
44 |         },
45 |         close: function () {
46 |             this.visible = false;
47 |         },
48 |     };
49 | 
50 |     const textModalApp = new Vue({
51 |         el: '#text-modal',
52 |         data: {
53 |             txtModal: txtModal,
54 |         },
55 |     });
56 | 
57 | </script>
58 | {{end}}


--------------------------------------------------------------------------------
/web/html/login.html:
--------------------------------------------------------------------------------
 1 | <!DOCTYPE html>
 2 | <html lang="en">
 3 | {{template "head" .}}
 4 | <style>
 5 | 
 6 |     #app {
 7 |         padding-top: 100px;
 8 |     }
 9 | 
10 |     h1 {
11 |         text-align: center;
12 |         color: #fff;
13 |         margin: 20px 0 50px 0;
14 |     }
15 | 
16 |     .ant-btn, .ant-input {
17 |         height: 50px;
18 |         border-radius: 30px;
19 |     }
20 | 
21 |     .ant-input-affix-wrapper .ant-input-prefix {
22 |         left: 23px;
23 |     }
24 | 
25 |     .ant-input-affix-wrapper .ant-input:not(:first-child) {
26 |         padding-left: 50px;
27 |     }
28 | 
29 | </style>
30 | <body>
31 | <a-layout id="app" v-cloak>
32 |     <transition name="list" appear>
33 |         <a-layout-content>
34 |             <a-row type="flex" justify="center">
35 |                 <a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
36 |                     <h1>{{ .title }}</h1>
37 |                 </a-col>
38 |             </a-row>
39 |             <a-row type="flex" justify="center">
40 |                 <a-col :xs="22" :sm="20" :md="16" :lg="12" :xl="8">
41 |                     <a-form>
42 |                         <a-form-item>
43 |                             <a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
44 |                                      @keydown.enter.native="login" autofocus>
45 |                                 <a-icon slot="prefix" type="user" style="color: rgba(0,0,0,.25)"/>
46 |                             </a-input>
47 |                         </a-form-item>
48 |                         <a-form-item>
49 |                             <a-input type="password" v-model.trim="user.password"
50 |                                      placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
51 |                                 <a-icon slot="prefix" type="lock" style="color: rgba(0,0,0,.25)"/>
52 |                             </a-input>
53 |                         </a-form-item>
54 |                         <a-form-item>
55 |                             <a-button block @click="login" :loading="loading">{{ i18n "login" }}</a-button>
56 |                         </a-form-item>
57 |                     </a-form>
58 |                 </a-col>
59 |             </a-row>
60 |         </a-layout-content>
61 |     </transition>
62 | </a-layout>
63 | {{template "js" .}}
64 | <script>
65 |     const leftColor = RandomUtil.randomIntRange(0x222222, 0xFFFFFF / 2).toString(16);
66 |     const rightColor = RandomUtil.randomIntRange(0xFFFFFF / 2, 0xDDDDDD).toString(16);
67 |     const deg = RandomUtil.randomIntRange(0, 360);
68 |     const background = `linear-gradient(${deg}deg, #${leftColor} 10%, #${rightColor} 100%)`;
69 |     document.querySelector('#app').style.background = background;
70 |     const app = new Vue({
71 |         delimiters: ['[[', ']]'],
72 |         el: '#app',
73 |         data: {
74 |             loading: false,
75 |             user: new User(),
76 |         },
77 |         methods: {
78 |             async login() {
79 |                 this.loading = true;
80 |                 const msg = await HttpUtil.post('/login', this.user);
81 |                 this.loading = false;
82 |                 if (msg.success) {
83 |                     location.href = basePath + 'xui/';
84 |                 }
85 |             }
86 |         }
87 |     });
88 | </script>
89 | </body>
90 | </html>


--------------------------------------------------------------------------------
/web/html/xui/common_sider.html:
--------------------------------------------------------------------------------
 1 | {{define "menuItems"}}
 2 | <a-menu-item key="{{ .base_path }}xui/">
 3 |     <a-icon type="dashboard"></a-icon>
 4 |     <span>系统状态</span>
 5 | </a-menu-item>
 6 | <a-menu-item key="{{ .base_path }}xui/inbounds">
 7 |     <a-icon type="user"></a-icon>
 8 |     <span>入站列表</span>
 9 | </a-menu-item>
10 | <a-menu-item key="{{ .base_path }}xui/setting">
11 |     <a-icon type="setting"></a-icon>
12 |     <span>面板设置</span>
13 | </a-menu-item>
14 | <!--<a-menu-item key="{{ .base_path }}xui/clients">-->
15 | <!--    <a-icon type="laptop"></a-icon>-->
16 | <!--    <span>客户端</span>-->
17 | <!--</a-menu-item>-->
18 | <a-sub-menu>
19 |     <template slot="title">
20 |         <a-icon type="link"></a-icon>
21 |         <span>其他</span>
22 |     </template>
23 |     <a-menu-item key="https://github.com/vaxilu/x-ui/">
24 |         <a-icon type="github"></a-icon>
25 |         <span>Github</span>
26 |     </a-menu-item>
27 | </a-sub-menu>
28 | <a-menu-item key="{{ .base_path }}logout">
29 |     <a-icon type="logout"></a-icon>
30 |     <span>退出登录</span>
31 | </a-menu-item>
32 | {{end}}
33 | 
34 | 
35 | {{define "commonSider"}}
36 | <a-layout-sider id="sider" collapsible breakpoint="md" collapsed-width="0">
37 |     <a-menu theme="dark" mode="inline" :selected-keys="['{{ .request_uri }}']"
38 |             @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
39 |         {{template "menuItems" .}}
40 |     </a-menu>
41 | </a-layout-sider>
42 | <a-drawer id="sider-drawer" placement="left" :closable="false"
43 |           @close="siderDrawer.close()"
44 |           :visible="siderDrawer.visible" :wrap-style="{ padding: 0 }">
45 |     <div class="drawer-handle" @click="siderDrawer.change()" slot="handle">
46 |         <a-icon :type="siderDrawer.visible ? 'close' : 'menu-fold'"></a-icon>
47 |     </div>
48 |     <a-menu theme="light" mode="inline" :selected-keys="['{{ .request_uri }}']"
49 |         @click="({key}) => key.startsWith('http') ? window.open(key) : location.href = key">
50 |         {{template "menuItems" .}}
51 |     </a-menu>
52 | </a-drawer>
53 | <script>
54 | 
55 |     const siderDrawer = {
56 |         visible: false,
57 |         show() {
58 |             this.visible = true;
59 |         },
60 |         close() {
61 |             this.visible = false;
62 |         },
63 |         change() {
64 |             this.visible = !this.visible;
65 |         }
66 |     };
67 | 
68 | </script>
69 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/component/inbound_info.html:
--------------------------------------------------------------------------------
 1 | {{define "inboundInfoStream"}}
 2 | <p>传输: <a-tag color="green">[[ inbound.network ]]</a-tag></p>
 3 | 
 4 | <template v-if="inbound.isTcp || inbound.isWs || inbound.isH2">
 5 |     <p v-if="inbound.host">host: <a-tag color="green">[[ inbound.host ]]</a-tag></p>
 6 |     <p v-else>host: <a-tag color="orange">无</a-tag></p>
 7 | 
 8 |     <p v-if="inbound.path">path: <a-tag color="green">[[ inbound.path ]]</a-tag></p>
 9 |     <p v-else>path: <a-tag color="orange">无</a-tag></p>
10 | </template>
11 | 
12 | <template v-if="inbound.isQuic">
13 |     <p>quic 加密: <a-tag color="green">[[ inbound.quicSecurity ]]</a-tag></p>
14 |     <p>quic 密码: <a-tag color="green">[[ inbound.quicKey ]]</a-tag></p>
15 |     <p>quic 伪装: <a-tag color="green">[[ inbound.quicType ]]</a-tag></p>
16 | </template>
17 | 
18 | <template v-if="inbound.isKcp">
19 |     <p>kcp 加密: <a-tag color="green">[[ inbound.kcpType ]]</a-tag></p>
20 |     <p>kcp 密码: <a-tag color="green">[[ inbound.kcpSeed ]]</a-tag></p>
21 | </template>
22 | 
23 | <template v-if="inbound.isGrpc">
24 |     <p>grpc serviceName: <a-tag color="green">[[ inbound.serviceName ]]</a-tag></p>
25 | </template>
26 | 
27 | <template v-if="inbound.tls || inbound.xtls">
28 |     <p v-if="inbound.tls">tls: <a-tag color="green">开启</a-tag></p>
29 |     <p v-if="inbound.xtls">xtls: <a-tag color="green">开启</a-tag></p>
30 | </template>
31 | <template v-else>
32 |     <p>tls: <a-tag color="red">关闭</a-tag></p>
33 | </template>
34 | <p v-if="inbound.tls">
35 |     tls域名: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
36 | </p>
37 | <p v-if="inbound.xtls">
38 |     xtls域名: <a-tag :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : "无" ]]</a-tag>
39 | </p>
40 | {{end}}
41 | 
42 | 
43 | {{define "component/inboundInfoComponent"}}
44 | <div>
45 |     <p>协议: <a-tag color="green">[[ dbInbound.protocol ]]</a-tag></p>
46 |     <p>地址: <a-tag color="blue">[[ dbInbound.address ]]</a-tag></p>
47 |     <p>端口: <a-tag color="green">[[ dbInbound.port ]]</a-tag></p>
48 | 
49 |     <template v-if="dbInbound.isVMess">
50 |         <p>uuid: <a-tag color="green">[[ inbound.uuid ]]</a-tag></p>
51 |         <p>alterId: <a-tag color="green">[[ inbound.alterId ]]</a-tag></p>
52 |     </template>
53 | 
54 |     <template v-if="dbInbound.isVLess">
55 |         <p>uuid: <a-tag color="green">[[ inbound.uuid ]]</a-tag></p>
56 |         <p v-if="inbound.isXTls">flow: <a-tag color="green">[[ inbound.flow ]]</a-tag></p>
57 |     </template>
58 | 
59 |     <template v-if="dbInbound.isTrojan">
60 |         <p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
61 |     </template>
62 | 
63 |     <template v-if="dbInbound.isSS">
64 |         <p>加密: <a-tag color="green">[[ inbound.method ]]</a-tag></p>
65 |         <p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
66 |     </template>
67 | 
68 |     <template v-if="dbInbound.isSocks">
69 |         <p>用户名: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
70 |         <p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
71 |     </template>
72 | 
73 |     <template v-if="dbInbound.isHTTP">
74 |         <p>用户名: <a-tag color="green">[[ inbound.username ]]</a-tag></p>
75 |         <p>密码: <a-tag color="green">[[ inbound.password ]]</a-tag></p>
76 |     </template>
77 | 
78 |     <template v-if="dbInbound.isVMess || dbInbound.isVLess || dbInbound.isTrojan || dbInbound.isSS">
79 |         {{template "inboundInfoStream"}}
80 |     </template>
81 | </div>
82 | {{end}}
83 | 
84 | {{define "component/inboundInfo"}}
85 | <script>
86 |     Vue.component('inbound-info', {
87 |         delimiters: ['[[', ']]'],
88 |         props: ["dbInbound", "inbound"],
89 |         template: `{{template "component/inboundInfoComponent"}}`,
90 |     });
91 | </script>
92 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/component/setting.html:
--------------------------------------------------------------------------------
 1 | {{define "component/settingListItem"}}
 2 | <a-list-item style="padding: 20px">
 3 |     <a-row>
 4 |         <a-col :lg="24" :xl="12">
 5 |             <a-list-item-meta :title="title" :description="desc"/>
 6 |         </a-col>
 7 |         <a-col :lg="24" :xl="12">
 8 |             <template v-if="type === 'text'">
 9 |                 <a-input :value="value" @input="$emit('input', $event.target.value)"></a-input>
10 |             </template>
11 |             <template v-else-if="type === 'number'">
12 |                 <a-input type="number" :value="value" @input="$emit('input', $event.target.value)"></a-input>
13 |             </template>
14 |             <template v-else-if="type === 'textarea'">
15 |                 <a-textarea :value="value" @input="$emit('input', $event.target.value)" :auto-size="{ minRows: 10, maxRows: 10 }"></a-textarea>
16 |             </template>
17 |             <template v-else-if="type === 'switch'">
18 |                 <a-switch :checked="value" @change="value => $emit('input', value)"></a-switch>
19 |             </template>
20 |         </a-col>
21 |     </a-row>
22 | </a-list-item>
23 | {{end}}
24 | 
25 | {{define "component/setting"}}
26 | <script>
27 |     Vue.component('setting-list-item', {
28 |         props: ["type", "title", "desc", "value"],
29 |         template: `{{template "component/settingListItem"}}`,
30 |     });
31 | </script>
32 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/inbound.html:
--------------------------------------------------------------------------------
  1 | {{define "form/inbound"}}
  2 | <!-- base -->
  3 | <a-form layout="inline">
  4 |     <a-form-item label='{{ i18n "remark" }}'>
  5 |         <a-input v-model.trim="dbInbound.remark"></a-input>
  6 |     </a-form-item>
  7 |     <a-form-item label='{{ i18n "enable" }}'>
  8 |         <a-switch v-model="dbInbound.enable"></a-switch>
  9 |     </a-form-item>
 10 |     <a-form-item label='{{ i18n "protocol" }}'>
 11 |         <a-select v-model="inbound.protocol" style="width: 160px;">
 12 |             <a-select-option v-for="p in Protocols" :key="p" :value="p">[[ p ]]</a-select-option>
 13 |         </a-select>
 14 |     </a-form-item>
 15 |     <a-form-item>
 16 |         <span slot="label">
 17 |             监听 IP
 18 |             <a-tooltip>
 19 |                 <template slot="title">
 20 |                     默认留空即可
 21 |                 </template>
 22 |                 <a-icon type="question-circle" theme="filled"></a-icon>
 23 |             </a-tooltip>
 24 |         </span>
 25 |         <a-input v-model.trim="inbound.listen"></a-input>
 26 |     </a-form-item>
 27 |     <a-form-item label="端口">
 28 |         <a-input type="number" v-model.number="inbound.port"></a-input>
 29 |     </a-form-item>
 30 |     <a-form-item>
 31 |         <span slot="label">
 32 |             总流量(GB)
 33 |             <a-tooltip>
 34 |                 <template slot="title">
 35 |                     0 表示不限制
 36 |                 </template>
 37 |                 <a-icon type="question-circle" theme="filled"></a-icon>
 38 |             </a-tooltip>
 39 |         </span>
 40 |         <a-input-number v-model="dbInbound.totalGB" :min="0"></a-input-number>
 41 |     </a-form-item>
 42 |     <a-form-item>
 43 |         <span slot="label">
 44 |             到期时间
 45 |             <a-tooltip>
 46 |                 <template slot="title">
 47 |                     留空则永不到期
 48 |                 </template>
 49 |                 <a-icon type="question-circle" theme="filled"></a-icon>
 50 |             </a-tooltip>
 51 |         </span>
 52 |         <a-date-picker :show-time="{ format: 'HH:mm' }" format="YYYY-MM-DD HH:mm"
 53 |                        v-model="dbInbound._expiryTime" style="width: 300px;"></a-date-picker>
 54 |     </a-form-item>
 55 | </a-form>
 56 | 
 57 | <!-- vmess settings -->
 58 | <template v-if="inbound.protocol === Protocols.VMESS">
 59 |     {{template "form/vmess"}}
 60 | </template>
 61 | 
 62 | <!-- vless settings -->
 63 | <template v-if="inbound.protocol === Protocols.VLESS">
 64 |     {{template "form/vless"}}
 65 | </template>
 66 | 
 67 | <!-- trojan settings -->
 68 | <template v-if="inbound.protocol === Protocols.TROJAN">
 69 |     {{template "form/trojan"}}
 70 | </template>
 71 | 
 72 | <!-- shadowsocks -->
 73 | <template v-if="inbound.protocol === Protocols.SHADOWSOCKS">
 74 |     {{template "form/shadowsocks"}}
 75 | </template>
 76 | 
 77 | <!-- dokodemo-door -->
 78 | <template v-if="inbound.protocol === Protocols.DOKODEMO">
 79 |     {{template "form/dokodemo"}}
 80 | </template>
 81 | 
 82 | <!-- socks -->
 83 | <template v-if="inbound.protocol === Protocols.SOCKS">
 84 |     {{template "form/socks"}}
 85 | </template>
 86 | 
 87 | <!-- http -->
 88 | <template v-if="inbound.protocol === Protocols.HTTP">
 89 |     {{template "form/http"}}
 90 | </template>
 91 | 
 92 | <!-- stream settings -->
 93 | <template v-if="inbound.canEnableStream()">
 94 |     {{template "form/streamSettings"}}
 95 | </template>
 96 | 
 97 | <!-- tls settings -->
 98 | <template v-if="inbound.canEnableTls()">
 99 |     {{template "form/tlsSettings"}}
100 | </template>
101 | 
102 | <!-- sniffing -->
103 | <template v-if="inbound.canSniffing()">
104 |     {{template "form/sniffing"}}
105 | </template>
106 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/protocol/dokodemo.html:
--------------------------------------------------------------------------------
 1 | {{define "form/dokodemo"}}
 2 | <a-form layout="inline">
 3 |     <a-form-item label="目标地址">
 4 |         <a-input v-model.trim="inbound.settings.address"></a-input>
 5 |     </a-form-item>
 6 |     <a-form-item label="目标端口">
 7 |         <a-input type="number" v-model.number="inbound.settings.port"></a-input>
 8 |     </a-form-item>
 9 |     <a-form-item label="网络">
10 |         <a-select v-model="inbound.settings.network" style="width: 100px;">
11 |             <a-select-option value="tcp,udp">tcp+udp</a-select-option>
12 |             <a-select-option value="tcp">tcp</a-select-option>
13 |             <a-select-option value="udp">udp</a-select-option>
14 |         </a-select>
15 |     </a-form-item>
16 | </a-form>
17 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/protocol/http.html:
--------------------------------------------------------------------------------
 1 | {{define "form/http"}}
 2 | <a-form layout="inline">
 3 |     <a-form-item label="用户名">
 4 |         <a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
 5 |     </a-form-item>
 6 |     <a-form-item label="密码">
 7 |         <a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
 8 |     </a-form-item>
 9 | </a-form>
10 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/protocol/shadowsocks.html:
--------------------------------------------------------------------------------
 1 | {{define "form/shadowsocks"}}
 2 | <a-form layout="inline">
 3 |     <a-form-item label="加密">
 4 |         <a-select v-model="inbound.settings.method" style="width: 165px;">
 5 |             <a-select-option v-for="method in SSMethods" :value="method">[[ method ]]</a-select-option>
 6 |         </a-select>
 7 |     </a-form-item>
 8 |     <a-form-item label="密码">
 9 |         <a-input v-model.trim="inbound.settings.password"></a-input>
10 |     </a-form-item>
11 |     <a-form-item label="网络">
12 |         <a-select v-model="inbound.settings.network" style="width: 100px;">
13 |             <a-select-option value="tcp,udp">tcp+udp</a-select-option>
14 |             <a-select-option value="tcp">tcp</a-select-option>
15 |             <a-select-option value="udp">udp</a-select-option>
16 |         </a-select>
17 |     </a-form-item>
18 | </a-form>
19 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/protocol/socks.html:
--------------------------------------------------------------------------------
 1 | {{define "form/socks"}}
 2 | <a-form layout="inline">
 3 |     <a-form-item label="密码认证">
 4 |         <a-switch :checked="inbound.settings.auth === 'password'"
 5 |                   @change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
 6 |     </a-form-item>
 7 |     <template v-if="inbound.settings.auth === 'password'">
 8 |         <a-form-item label="用户名">
 9 |             <a-input v-model.trim="inbound.settings.accounts[0].user"></a-input>
10 |         </a-form-item>
11 |         <a-form-item label="密码">
12 |             <a-input v-model.trim="inbound.settings.accounts[0].pass"></a-input>
13 |         </a-form-item>
14 |     </template>
15 |     <a-form-item label="启用 udp">
16 |         <a-switch v-model="inbound.settings.udp"></a-switch>
17 |     </a-form-item>
18 |     <a-form-item v-if="inbound.settings.udp"
19 |                  label="IP">
20 |         <a-input v-model.trim="inbound.settings.ip"></a-input>
21 |     </a-form-item>
22 | </a-form>
23 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/protocol/trojan.html:
--------------------------------------------------------------------------------
 1 | {{define "form/trojan"}}
 2 | <a-form layout="inline">
 3 |     <a-form-item label="密码">
 4 |         <a-input v-model.trim="inbound.settings.clients[0].password"></a-input>
 5 |     </a-form-item>
 6 |     <a-form-item v-if="inbound.xtls" label="flow">
 7 |         <a-select v-model="inbound.settings.clients[0].flow" style="width: 150px">
 8 |             <a-select-option value="">无</a-select-option>
 9 |             <a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
10 |         </a-select>
11 |     </a-form-item>
12 | </a-form>
13 | 
14 | <a-form layout="inline">
15 |     <a-form-item label="fallbacks">
16 |         <a-row>
17 |             <a-button type="primary" size="small"
18 |                       @click="inbound.settings.addTrojanFallback()">
19 |                 +
20 |             </a-button>
21 |         </a-row>
22 |     </a-form-item>
23 | </a-form>
24 | 
25 | <!-- trojan fallbacks -->
26 | <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
27 |     <a-divider>
28 |         fallback[[ index + 1 ]]
29 |         <a-icon type="delete" @click="() => inbound.settings.delTrojanFallback(index)"
30 |                 style="color: rgb(255, 77, 79);cursor: pointer;"/>
31 |     </a-divider>
32 |     <a-form-item label="name">
33 |         <a-input v-model="fallback.name"></a-input>
34 |     </a-form-item>
35 |     <a-form-item label="alpn">
36 |         <a-input v-model="fallback.alpn"></a-input>
37 |     </a-form-item>
38 |     <a-form-item label="path">
39 |         <a-input v-model="fallback.path"></a-input>
40 |     </a-form-item>
41 |     <a-form-item label="dest">
42 |         <a-input v-model="fallback.dest"></a-input>
43 |     </a-form-item>
44 |     <a-form-item label="xver">
45 |         <a-input type="number" v-model.number="fallback.xver"></a-input>
46 |     </a-form-item>
47 |     <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
48 | </a-form>
49 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/protocol/vless.html:
--------------------------------------------------------------------------------
 1 | {{define "form/vless"}}
 2 | <a-form layout="inline">
 3 |     <a-form-item label="id">
 4 |         <a-input v-model.trim="inbound.settings.vlesses[0].id"></a-input>
 5 |     </a-form-item>
 6 |     <a-form-item v-if="inbound.xtls" label="flow">
 7 |         <a-select v-model="inbound.settings.vlesses[0].flow" style="width: 150px">
 8 |             <a-select-option value="">无</a-select-option>
 9 |             <a-select-option v-for="key in FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
10 |         </a-select>
11 |     </a-form-item>
12 | </a-form>
13 | 
14 | <a-form layout="inline">
15 |     <a-form-item label="fallbacks">
16 |         <a-row>
17 |             <a-button type="primary" size="small"
18 |                       @click="inbound.settings.addFallback()">
19 |                 +
20 |             </a-button>
21 |         </a-row>
22 |     </a-form-item>
23 | </a-form>
24 | 
25 | <!-- vless fallbacks -->
26 | <a-form v-for="(fallback, index) in inbound.settings.fallbacks" layout="inline">
27 |     <a-divider>
28 |         fallback[[ index + 1 ]]
29 |         <a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
30 |                 style="color: rgb(255, 77, 79);cursor: pointer;"/>
31 |     </a-divider>
32 |     <a-form-item label="name">
33 |         <a-input v-model="fallback.name"></a-input>
34 |     </a-form-item>
35 |     <a-form-item label="alpn">
36 |         <a-input v-model="fallback.alpn"></a-input>
37 |     </a-form-item>
38 |     <a-form-item label="path">
39 |         <a-input v-model="fallback.path"></a-input>
40 |     </a-form-item>
41 |     <a-form-item label="dest">
42 |         <a-input v-model="fallback.dest"></a-input>
43 |     </a-form-item>
44 |     <a-form-item label="xver">
45 |         <a-input type="number" v-model.number="fallback.xver"></a-input>
46 |     </a-form-item>
47 |     <a-divider v-if="inbound.settings.fallbacks.length - 1 === index"/>
48 | </a-form>
49 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/protocol/vmess.html:
--------------------------------------------------------------------------------
 1 | {{define "form/vmess"}}
 2 | <a-form layout="inline">
 3 |     <a-form-item label="id">
 4 |         <a-input v-model.trim="inbound.settings.vmesses[0].id"></a-input>
 5 |     </a-form-item>
 6 |     <a-form-item label="额外 ID">
 7 |         <a-input type="number" v-model.number="inbound.settings.vmesses[0].alterId"></a-input>
 8 |     </a-form-item>
 9 |     <a-form-item label="禁用不安全加密">
10 |         <a-switch v-model.number="inbound.settings.disableInsecure"></a-switch>
11 |     </a-form-item>
12 | </a-form>
13 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/sniffing.html:
--------------------------------------------------------------------------------
 1 | {{define "form/sniffing"}}
 2 | <a-form layout="inline">
 3 |   <a-form-item>
 4 |             <span slot="label">
 5 |                 sniffing
 6 |                 <a-tooltip>
 7 |                     <template slot="title">
 8 |                         没有特殊需求保持默认即可
 9 |                     </template>
10 |                     <a-icon type="question-circle" theme="filled"></a-icon>
11 |                 </a-tooltip>
12 |             </span>
13 |     <a-switch v-model="inbound.sniffing.enabled"></a-switch>
14 |   </a-form-item>
15 | </a-form>
16 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_grpc.html:
--------------------------------------------------------------------------------
1 | {{define "form/streamGRPC"}}
2 | <a-form layout="inline">
3 |     <a-form-item label="serviceName">
4 |         <a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
5 |     </a-form-item>
6 | </a-form>
7 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_http.html:
--------------------------------------------------------------------------------
 1 | {{define "form/streamHTTP"}}
 2 | <a-form layout="inline">
 3 |     <a-form-item label="路径">
 4 |         <a-input v-model.trim="inbound.stream.http.path"></a-input>
 5 |     </a-form-item>
 6 |     <a-form-item label="host">
 7 |         <a-row v-for="(host, index) in inbound.stream.http.host">
 8 |             <a-input v-model.trim="inbound.stream.http.host[index]"></a-input>
 9 |         </a-row>
10 |     </a-form-item>
11 | </a-form>
12 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_kcp.html:
--------------------------------------------------------------------------------
 1 | {{define "form/streamKCP"}}
 2 | <a-form layout="inline">
 3 |     <a-form-item label="伪装">
 4 |         <a-select v-model="inbound.stream.kcp.type" style="width: 280px;">
 5 |             <a-select-option value="none">none(not camouflage)</a-select-option>
 6 |             <a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
 7 |             <a-select-option value="utp">utp(camouflage BT download)</a-select-option>
 8 |             <a-select-option value="wechat-video">wechat-video(camouflage WeChat video)</a-select-option>
 9 |             <a-select-option value="dtls">dtls(camouflage DTLS 1.2 packages)</a-select-option>
10 |             <a-select-option value="wireguard">wireguard(camouflage wireguard packages)</a-select-option>
11 |         </a-select>
12 |     </a-form-item>
13 |     <a-form-item label="密码">
14 |         <a-input v-model.number="inbound.stream.kcp.seed"></a-input>
15 |     </a-form-item>
16 |     <a-form-item label="mtu">
17 |         <a-input type="number" v-model.number="inbound.stream.kcp.mtu"></a-input>
18 |     </a-form-item>
19 |     <a-form-item label="tti (ms)">
20 |         <a-input type="number" v-model.number="inbound.stream.kcp.tti"></a-input>
21 |     </a-form-item>
22 |     <a-form-item label="uplink capacity (MB/S)">
23 |         <a-input type="number" v-model.number="inbound.stream.kcp.upCap"></a-input>
24 |     </a-form-item>
25 |     <a-form-item label="downlink capacity (MB/S)">
26 |         <a-input type="number" v-model.number="inbound.stream.kcp.downCap"></a-input>
27 |     </a-form-item>
28 |     <a-form-item label="congestion">
29 |         <a-switch v-model="inbound.stream.kcp.congestion"></a-switch>
30 |     </a-form-item>
31 |     <a-form-item label="read buffer size (MB)">
32 |         <a-input type="number" v-model.number="inbound.stream.kcp.readBuffer"></a-input>
33 |     </a-form-item>
34 |     <a-form-item label="write buffer size (MB)">
35 |         <a-input type="number" v-model.number="inbound.stream.kcp.writeBuffer"></a-input>
36 |     </a-form-item>
37 | </a-form>
38 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_quic.html:
--------------------------------------------------------------------------------
 1 | {{define "form/streamQUIC"}}
 2 | <a-form layout="inline">
 3 |     <a-form-item label="加密">
 4 |         <a-select v-model="inbound.stream.quic.security" style="width: 165px;">
 5 |             <a-select-option value="none">none</a-select-option>
 6 |             <a-select-option value="aes-128-gcm">aes-128-gcm</a-select-option>
 7 |             <a-select-option value="chacha20-poly1305">chacha20-poly1305</a-select-option>
 8 |         </a-select>
 9 |     </a-form-item>
10 |     <a-form-item label="密码">
11 |         <a-input v-model.trim="inbound.stream.quic.key"></a-input>
12 |     </a-form-item>
13 |     <a-form-item label="伪装">
14 |         <a-select v-model="inbound.stream.quic.type" style="width: 280px;">
15 |             <a-select-option value="none">none(not camouflage)</a-select-option>
16 |             <a-select-option value="srtp">srtp(camouflage video call)</a-select-option>
17 |             <a-select-option value="utp">utp(camouflage BT download)</a-select-option>
18 |             <a-select-option value="wechat-video">wechat-video(camouflage WeChat video)</a-select-option>
19 |             <a-select-option value="dtls">dtls(camouflage DTLS 1.2 packages)</a-select-option>
20 |             <a-select-option value="wireguard">wireguard(camouflage wireguard packages)</a-select-option>
21 |         </a-select>
22 |     </a-form-item>
23 | </a-form>
24 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_settings.html:
--------------------------------------------------------------------------------
 1 | {{define "form/streamSettings"}}
 2 | <!-- select stream network -->
 3 | <a-form layout="inline">
 4 |     <a-form-item label="传输">
 5 |         <a-select v-model="inbound.stream.network" @change="streamNetworkChange">
 6 |             <a-select-option value="tcp">tcp</a-select-option>
 7 |             <a-select-option value="kcp">kcp</a-select-option>
 8 |             <a-select-option value="ws">ws</a-select-option>
 9 |             <a-select-option value="http">http</a-select-option>
10 |             <a-select-option value="quic">quic</a-select-option>
11 |             <a-select-option value="grpc">grpc</a-select-option>
12 |         </a-select>
13 |     </a-form-item>
14 | </a-form>
15 | 
16 | <!-- tcp -->
17 | <template v-if="inbound.stream.network === 'tcp'">
18 |     {{template "form/streamTCP"}}
19 | </template>
20 | 
21 | <!-- kcp -->
22 | <template v-if="inbound.stream.network === 'kcp'">
23 |     {{template "form/streamKCP"}}
24 | </template>
25 | 
26 | <!-- ws -->
27 | <template v-if="inbound.stream.network === 'ws'">
28 |     {{template "form/streamWS"}}
29 | </template>
30 | 
31 | <!-- http -->
32 | <template v-if="inbound.stream.network === 'http'">
33 |     {{template "form/streamHTTP"}}
34 | </template>
35 | 
36 | <!-- quic -->
37 | <template v-if="inbound.stream.network === 'quic'">
38 |     {{template "form/streamQUIC"}}
39 | </template>
40 | 
41 | <!-- grpc -->
42 | <template v-if="inbound.stream.network === 'grpc'">
43 |     {{template "form/streamGRPC"}}
44 | </template>
45 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_tcp.html:
--------------------------------------------------------------------------------
 1 | {{define "form/streamTCP"}}
 2 | <!-- tcp type -->
 3 | <a-form layout="inline">
 4 |     <a-form-item label="acceptProxyProtocol">
 5 |         <a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
 6 |     </a-form-item>
 7 |     <a-form-item label="http 伪装">
 8 |         <a-switch
 9 |                 :checked="inbound.stream.tcp.type === 'http'"
10 |                 @change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'">
11 |         </a-switch>
12 |     </a-form-item>
13 | </a-form>
14 | 
15 | <!-- tcp request -->
16 | <a-form v-if="inbound.stream.tcp.type === 'http'"
17 |         layout="inline">
18 |     <a-form-item label="请求版本">
19 |         <a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
20 |     </a-form-item>
21 |     <a-form-item label="请求方法">
22 |         <a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
23 |     </a-form-item>
24 |     <a-form-item label="请求路径">
25 |         <a-row v-for="(path, index) in inbound.stream.tcp.request.path">
26 |             <a-input v-model.trim="inbound.stream.tcp.request.path[index]"></a-input>
27 |         </a-row>
28 |     </a-form-item>
29 |     <a-form-item label="请求头">
30 |         <a-row>
31 |             <a-button size="small"
32 |                       @click="inbound.stream.tcp.request.addHeader('Host', 'xxx.com')">
33 |                 +
34 |             </a-button>
35 |         </a-row>
36 |         <a-input-group v-for="(header, index) in inbound.stream.tcp.request.headers">
37 |             <a-input style="width: 50%" v-model.trim="header.name"
38 |                      addon-before="名称"></a-input>
39 |             <a-input style="width: 50%" v-model.trim="header.value"
40 |                      addon-before="值">
41 |                 <template slot="addonAfter">
42 |                     <a-button size="small"
43 |                               @click="inbound.stream.tcp.request.removeHeader(index)">
44 |                         -
45 |                     </a-button>
46 |                 </template>
47 |             </a-input>
48 |         </a-input-group>
49 |     </a-form-item>
50 | </a-form>
51 | 
52 | <!-- tcp response -->
53 | <a-form v-if="inbound.stream.tcp.type === 'http'"
54 |         layout="inline">
55 |     <a-form-item label="响应版本">
56 |         <a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
57 |     </a-form-item>
58 |     <a-form-item label="响应状态">
59 |         <a-input v-model.trim="inbound.stream.tcp.response.status"></a-input>
60 |     </a-form-item>
61 |     <a-form-item label="响应状态说明">
62 |         <a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
63 |     </a-form-item>
64 |     <a-form-item label="响应头">
65 |         <a-row>
66 |             <a-button size="small"
67 |                       @click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')">
68 |                 +
69 |             </a-button>
70 |         </a-row>
71 |         <a-input-group v-for="(header, index) in inbound.stream.tcp.response.headers">
72 |             <a-input style="width: 50%" v-model.trim="header.name"
73 |                      addon-before="名称"></a-input>
74 |             <a-input style="width: 50%" v-model.trim="header.value"
75 |                      addon-before="值">
76 |                 <template slot="addonAfter">
77 |                     <a-button size="small"
78 |                               @click="inbound.stream.tcp.response.removeHeader(index)">
79 |                         -
80 |                     </a-button>
81 |                 </template>
82 |             </a-input>
83 |         </a-input-group>
84 |     </a-form-item>
85 | </a-form>
86 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/stream/stream_ws.html:
--------------------------------------------------------------------------------
 1 | {{define "form/streamWS"}}
 2 | <a-form layout="inline">
 3 |     <a-form-item label="acceptProxyProtocol">
 4 |         <a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
 5 |     </a-form-item>
 6 | </a-form>
 7 | <a-form layout="inline">
 8 |     <a-form-item label="路径">
 9 |         <a-input v-model.trim="inbound.stream.ws.path"></a-input>
10 |     </a-form-item>
11 |     <a-form-item label="请求头">
12 |         <a-row>
13 |             <a-button size="small"
14 |                       @click="inbound.stream.ws.addHeader('Host', '')">
15 |                 +
16 |             </a-button>
17 |         </a-row>
18 |         <a-input-group v-for="(header, index) in inbound.stream.ws.headers">
19 |             <a-input style="width: 50%" v-model.trim="header.name"
20 |                      addon-before="名称"></a-input>
21 |             <a-input style="width: 50%" v-model.trim="header.value"
22 |                      addon-before="值">
23 |                 <template slot="addonAfter">
24 |                     <a-button size="small"
25 |                               @click="inbound.stream.ws.removeHeader(index)">
26 |                         -
27 |                     </a-button>
28 |                 </template>
29 |             </a-input>
30 |         </a-input-group>
31 |     </a-form-item>
32 | </a-form>
33 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/form/tls_settings.html:
--------------------------------------------------------------------------------
 1 | {{define "form/tlsSettings"}}
 2 | <!-- tls enable -->
 3 | <a-form layout="inline" v-if="inbound.canSetTls()">
 4 |     <a-form-item label="tls">
 5 |         <a-switch v-model="inbound.tls">
 6 |         </a-switch>
 7 |     </a-form-item>
 8 |     <a-form-item v-if="inbound.canEnableXTls()" label="xtls">
 9 |         <a-switch v-model="inbound.xtls"></a-switch>
10 |     </a-form-item>
11 | </a-form>
12 | 
13 | <!-- tls settings -->
14 | <a-form v-if="inbound.tls || inbound.xtls"
15 |         layout="inline">
16 |     <a-form-item label="域名">
17 |         <a-input v-model.trim="inbound.stream.tls.server"></a-input>
18 |     </a-form-item>
19 |     <a-form-item label="alpn" placeholder="http/1.1,h2">
20 |         <a-input v-model.trim="inbound.stream.tls.alpn"></a-input>
21 |     </a-form-item>
22 |     <a-form-item label="证书">
23 |         <a-radio-group v-model="inbound.stream.tls.certs[0].useFile"
24 |                        button-style="solid">
25 |             <a-radio-button :value="true">certificate file path</a-radio-button>
26 |             <a-radio-button :value="false">certificate file content</a-radio-button>
27 |         </a-radio-group>
28 |     </a-form-item>
29 |     <template v-if="inbound.stream.tls.certs[0].useFile">
30 |         <a-form-item label="公钥文件路径">
31 |             <a-input v-model.trim="inbound.stream.tls.certs[0].certFile"></a-input>
32 |         </a-form-item>
33 |         <a-form-item label="密钥文件路径">
34 |             <a-input v-model.trim="inbound.stream.tls.certs[0].keyFile"></a-input>
35 |         </a-form-item>
36 |     </template>
37 |     <template v-else>
38 |         <a-form-item label="公钥内容">
39 |             <a-input type="textarea" :rows="2"
40 |                      v-model="inbound.stream.tls.certs[0].cert"></a-input>
41 |         </a-form-item>
42 |         <a-form-item label="密钥内容">
43 |             <a-input type="textarea" :rows="2"
44 |                      v-model="inbound.stream.tls.certs[0].key"></a-input>
45 |         </a-form-item>
46 |     </template>
47 | </a-form>
48 | <a-form layout="inline" v-else-if = "inbound.stream.network === 'tcp' ">
49 |     <a-form-item label="tcp-acceptProxyProtocol">
50 |         <a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
51 |     </a-form-item>
52 | </a-form>
53 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/inbound_info_modal.html:
--------------------------------------------------------------------------------
 1 | {{define "inboundInfoModal"}}
 2 | {{template "component/inboundInfo"}}
 3 | <a-modal id="inbound-info-modal" v-model="infoModal.visible" title="详细信息" @ok="infoModal.ok"
 4 |          :closable="true" :mask-closable="true"
 5 |          ok-text="复制链接" cancel-text='{{ i18n "close" }}' :ok-button-props="infoModal.okBtnPros">
 6 |     <inbound-info :db-inbound="dbInbound" :inbound="inbound"></inbound-info>
 7 | </a-modal>
 8 | <script>
 9 | 
10 |     const infoModal = {
11 |         visible: false,
12 |         inbound: new Inbound(),
13 |         dbInbound: new DBInbound(),
14 |         clipboard: null,
15 |         okBtnPros: {
16 |             attrs: {
17 |                 id: "inbound-info-modal-ok-btn",
18 |                 style: "",
19 |             },
20 |         },
21 |         show(dbInbound) {
22 |             this.inbound = dbInbound.toInbound();
23 |             this.dbInbound = new DBInbound(dbInbound);
24 |             this.visible = true;
25 | 
26 |             if (dbInbound.hasLink()) {
27 |                 this.okBtnPros.attrs.style = "";
28 |             } else {
29 |                 this.okBtnPros.attrs.style = "display: none";
30 |             }
31 | 
32 |             if (this.clipboard == null) {
33 |                 infoModalApp.$nextTick(() => {
34 |                     this.clipboard = new ClipboardJS(`#${this.okBtnPros.attrs.id}`, {
35 |                         text: () => this.dbInbound.genLink(),
36 |                     });
37 |                     this.clipboard.on('success', () => app.$message.success('复制成功'));
38 |                 });
39 |             }
40 |         },
41 |         close() {
42 |             infoModal.visible = false;
43 |         },
44 |     };
45 | 
46 |     const infoModalApp = new Vue({
47 |         delimiters: ['[[', ']]'],
48 |         el: '#inbound-info-modal',
49 |         data: {
50 |             infoModal,
51 |             get dbInbound() {
52 |                 return this.infoModal.dbInbound;
53 |             },
54 |             get inbound() {
55 |                 return this.infoModal.inbound;
56 |             }
57 |         },
58 |     });
59 | 
60 | </script>
61 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/inbound_modal.html:
--------------------------------------------------------------------------------
 1 | {{define "inboundModal"}}
 2 | <a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" @ok="inModal.ok"
 3 |          :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
 4 |          :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
 5 |     {{template "form/inbound"}}
 6 | </a-modal>
 7 | <script>
 8 | 
 9 |     const inModal = {
10 |         title: '',
11 |         visible: false,
12 |         confirmLoading: false,
13 |         okText: '确定',
14 |         confirm: null,
15 |         inbound: new Inbound(),
16 |         dbInbound: new DBInbound(),
17 |         ok() {
18 |             ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
19 |         },
20 |         show({ title='', okText='确定', inbound=null, dbInbound=null, confirm=(inbound, dbInbound)=>{} }) {
21 |             this.title = title;
22 |             this.okText = okText;
23 |             if (inbound) {
24 |                 this.inbound = Inbound.fromJson(inbound.toJson());
25 |             } else {
26 |                 this.inbound = new Inbound();
27 |             }
28 |             if (dbInbound) {
29 |                 this.dbInbound = new DBInbound(dbInbound);
30 |             } else {
31 |                 this.dbInbound = new DBInbound();
32 |             }
33 |             this.confirm = confirm;
34 |             this.visible = true;
35 |         },
36 |         close() {
37 |             inModal.visible = false;
38 |             inModal.loading(false);
39 |         },
40 |         loading(loading) {
41 |             inModal.confirmLoading = loading;
42 |         },
43 |     };
44 | 
45 |     const protocols = {
46 |         VMESS: Protocols.VMESS,
47 |         VLESS: Protocols.VLESS,
48 |         TROJAN: Protocols.TROJAN,
49 |         SHADOWSOCKS: Protocols.SHADOWSOCKS,
50 |         DOKODEMO: Protocols.DOKODEMO,
51 |         SOCKS: Protocols.SOCKS,
52 |         HTTP: Protocols.HTTP,
53 |     };
54 | 
55 |     new Vue({
56 |         delimiters: ['[[', ']]'],
57 |         el: '#inbound-modal',
58 |         data: {
59 |             inModal: inModal,
60 |             Protocols: protocols,
61 |             SSMethods: SSMethods,
62 |             get inbound() {
63 |                 return inModal.inbound;
64 |             },
65 |             get dbInbound() {
66 |                 return inModal.dbInbound;
67 |             }
68 |         },
69 |         methods: {
70 |             streamNetworkChange(oldValue) {
71 |                 if (oldValue === 'kcp') {
72 |                     this.inModal.inbound.tls = false;
73 |                 }
74 |             }
75 |         }
76 |     });
77 | 
78 | </script>
79 | {{end}}


--------------------------------------------------------------------------------
/web/html/xui/setting.html:
--------------------------------------------------------------------------------
  1 | <!DOCTYPE html>
  2 | <html lang="en">
  3 | {{template "head" .}}
  4 | <style>
  5 |     @media (min-width: 769px) {
  6 |         .ant-layout-content {
  7 |             margin: 24px 16px;
  8 |         }
  9 |     }
 10 | 
 11 |     .ant-col-sm-24 {
 12 |         margin-top: 10px;
 13 |     }
 14 | 
 15 |     .ant-tabs-bar {
 16 |         margin: 0;
 17 |     }
 18 | 
 19 |     .ant-list-item {
 20 |         display: block;
 21 |     }
 22 | 
 23 |     .ant-tabs-top-bar {
 24 |         background: white;
 25 |     }
 26 | </style>
 27 | <body>
 28 | <a-layout id="app" v-cloak>
 29 |     {{ template "commonSider" . }}
 30 |     <a-layout id="content-layout">
 31 |         <a-layout-content>
 32 |             <a-spin :spinning="spinning" :delay="500" tip="loading">
 33 |                 <a-space direction="vertical">
 34 |                     <a-space direction="horizontal">
 35 |                         <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">保存配置</a-button>
 36 |                         <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">重启面板</a-button>
 37 |                     </a-space>
 38 |                     <a-tabs default-active-key="1">
 39 |                         <a-tab-pane key="1" tab="面板配置">
 40 |                             <a-list item-layout="horizontal" style="background: white">
 41 |                                 <setting-list-item type="text" title="面板监听 IP" desc="默认留空监听所有 IP,重启面板生效" v-model="allSetting.webListen"></setting-list-item>
 42 |                                 <setting-list-item type="number" title="面板监听端口" desc="重启面板生效" v-model.number="allSetting.webPort"></setting-list-item>
 43 |                                 <setting-list-item type="text" title="面板证书公钥文件路径" desc="填写一个 '/' 开头的绝对路径,重启面板生效" v-model="allSetting.webCertFile"></setting-list-item>
 44 |                                 <setting-list-item type="text" title="面板证书密钥文件路径" desc="填写一个 '/' 开头的绝对路径,重启面板生效" v-model="allSetting.webKeyFile"></setting-list-item>
 45 |                                 <setting-list-item type="text" title="面板 url 根路径" desc="必须以 '/' 开头,以 '/' 结尾,重启面板生效" v-model="allSetting.webBasePath"></setting-list-item>
 46 |                             </a-list>
 47 |                         </a-tab-pane>
 48 |                         <a-tab-pane key="2" tab="用户设置">
 49 |                             <a-form style="background: white; padding: 20px">
 50 |                                 <a-form-item label="原用户名">
 51 |                                     <a-input v-model="user.oldUsername" style="max-width: 300px"></a-input>
 52 |                                 </a-form-item>
 53 |                                 <a-form-item label="原密码">
 54 |                                     <a-input type="password" v-model="user.oldPassword"
 55 |                                              style="max-width: 300px"></a-input>
 56 |                                 </a-form-item>
 57 |                                 <a-form-item label="新用户名">
 58 |                                     <a-input v-model="user.newUsername" style="max-width: 300px"></a-input>
 59 |                                 </a-form-item>
 60 |                                 <a-form-item label="新密码">
 61 |                                     <a-input type="password" v-model="user.newPassword"
 62 |                                              style="max-width: 300px"></a-input>
 63 |                                 </a-form-item>
 64 |                                 <a-form-item>
 65 |                                     <a-button type="primary" @click="updateUser">修改</a-button>
 66 |                                 </a-form-item>
 67 |                             </a-form>
 68 |                         </a-tab-pane>
 69 |                         <a-tab-pane key="3" tab="xray 相关设置">
 70 |                             <a-list item-layout="horizontal" style="background: white">
 71 |                                 <setting-list-item type="textarea" title="xray 配置模版" desc="以该模版为基础生成最终的 xray 配置文件,重启面板生效" v-model="allSetting.xrayTemplateConfig"></setting-list-item>
 72 |                             </a-list>
 73 |                         </a-tab-pane>
 74 |                         <a-tab-pane key="4" tab="TG提醒相关设置">
 75 |                             <a-list item-layout="horizontal" style="background: white">
 76 |                                 <setting-list-item type="switch" title="启用电报机器人" desc="重启面板生效"  v-model="allSetting.tgBotEnable"></setting-list-item>
 77 |                                 <setting-list-item type="text" title="电报机器人TOKEN" desc="重启面板生效"  v-model="allSetting.tgBotToken"></setting-list-item>
 78 |                                 <setting-list-item type="number" title="电报机器人ChatId" desc="重启面板生效"  v-model.number="allSetting.tgBotChatId"></setting-list-item>
 79 |                                 <setting-list-item type="text" title="电报机器人通知时间" desc="采用Crontab定时格式,重启面板生效"  v-model="allSetting.tgRunTime"></setting-list-item>
 80 |                             </a-list>
 81 |                         </a-tab-pane>
 82 |                         <a-tab-pane key="5" tab="其他设置">
 83 |                             <a-list item-layout="horizontal" style="background: white">
 84 |                                 <setting-list-item type="text" title="时区" desc="定时任务按照该时区的时间运行,重启面板生效" v-model="allSetting.timeLocation"></setting-list-item>
 85 |                             </a-list>
 86 |                         </a-tab-pane>
 87 |                     </a-tabs>
 88 |                 </a-space>
 89 |             </a-spin>
 90 |         </a-layout-content>
 91 |     </a-layout>
 92 | </a-layout>
 93 | {{template "js" .}}
 94 | {{template "component/setting"}}
 95 | <script>
 96 | 
 97 |     const app = new Vue({
 98 |         delimiters: ['[[', ']]'],
 99 |         el: '#app',
100 |         data: {
101 |             siderDrawer,
102 |             spinning: false,
103 |             oldAllSetting: new AllSetting(),
104 |             allSetting: new AllSetting(),
105 |             saveBtnDisable: true,
106 |             user: {},
107 |         },
108 |         methods: {
109 |             loading(spinning = true) {
110 |                 this.spinning = spinning;
111 |             },
112 |             async getAllSetting() {
113 |                 this.loading(true);
114 |                 const msg = await HttpUtil.post("/xui/setting/all");
115 |                 this.loading(false);
116 |                 if (msg.success) {
117 |                     this.oldAllSetting = new AllSetting(msg.obj);
118 |                     this.allSetting = new AllSetting(msg.obj);
119 |                     this.saveBtnDisable = true;
120 |                 }
121 |             },
122 |             async updateAllSetting() {
123 |                 this.loading(true);
124 |                 const msg = await HttpUtil.post("/xui/setting/update", this.allSetting);
125 |                 this.loading(false);
126 |                 if (msg.success) {
127 |                     await this.getAllSetting();
128 |                 }
129 |             },
130 |             async updateUser() {
131 |                 this.loading(true);
132 |                 const msg = await HttpUtil.post("/xui/setting/updateUser", this.user);
133 |                 this.loading(false);
134 |                 if (msg.success) {
135 |                     this.user = {};
136 |                 }
137 |             },
138 |             async restartPanel() {
139 |                 await new Promise(resolve => {
140 |                     this.$confirm({
141 |                         title: '重启面板',
142 |                         content: '确定要重启面板吗?点击确定将于 3 秒后重启,若重启后无法访问面板,请前往服务器查看面板日志信息',
143 |                         okText: '确定',
144 |                         cancelText: '取消',
145 |                         onOk: () => resolve(),
146 |                     });
147 |                 });
148 |                 this.loading(true);
149 |                 const msg = await HttpUtil.post("/xui/setting/restartPanel");
150 |                 this.loading(false);
151 |                 if (msg.success) {
152 |                     this.loading(true);
153 |                     await PromiseUtil.sleep(5000);
154 |                     location.reload();
155 |                 }
156 |             }
157 |         },
158 |         async mounted() {
159 |             await this.getAllSetting();
160 |             while (true) {
161 |                 await PromiseUtil.sleep(1000);
162 |                 this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
163 |             }
164 |         },
165 |     });
166 | 
167 | </script>
168 | </body>
169 | </html>


--------------------------------------------------------------------------------
/web/job/check_inbound_job.go:
--------------------------------------------------------------------------------
 1 | package job
 2 | 
 3 | import (
 4 | 	"x-ui/logger"
 5 | 	"x-ui/web/service"
 6 | )
 7 | 
 8 | type CheckInboundJob struct {
 9 | 	xrayService    service.XrayService
10 | 	inboundService service.InboundService
11 | }
12 | 
13 | func NewCheckInboundJob() *CheckInboundJob {
14 | 	return new(CheckInboundJob)
15 | }
16 | 
17 | func (j *CheckInboundJob) Run() {
18 | 	count, err := j.inboundService.DisableInvalidInbounds()
19 | 	if err != nil {
20 | 		logger.Warning("disable invalid inbounds err:", err)
21 | 	} else if count > 0 {
22 | 		logger.Debugf("disabled %v inbounds", count)
23 | 		j.xrayService.SetToNeedRestart()
24 | 	}
25 | }
26 | 


--------------------------------------------------------------------------------
/web/job/check_xray_running_job.go:
--------------------------------------------------------------------------------
 1 | package job
 2 | 
 3 | import "x-ui/web/service"
 4 | 
 5 | type CheckXrayRunningJob struct {
 6 | 	xrayService service.XrayService
 7 | 
 8 | 	checkTime int
 9 | }
10 | 
11 | func NewCheckXrayRunningJob() *CheckXrayRunningJob {
12 | 	return new(CheckXrayRunningJob)
13 | }
14 | 
15 | func (j *CheckXrayRunningJob) Run() {
16 | 	if j.xrayService.IsXrayRunning() {
17 | 		j.checkTime = 0
18 | 		return
19 | 	}
20 | 	j.checkTime++
21 | 	if j.checkTime < 2 {
22 | 		return
23 | 	}
24 | 	j.xrayService.SetToNeedRestart()
25 | }
26 | 


--------------------------------------------------------------------------------
/web/job/stats_notify_job.go:
--------------------------------------------------------------------------------
  1 | package job
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"net"
  6 | 	"os"
  7 | 
  8 | 	"time"
  9 | 
 10 | 	"x-ui/logger"
 11 | 	"x-ui/util/common"
 12 | 	"x-ui/web/service"
 13 | 
 14 | 	tgbotapi "github.com/go-telegram-bot-api/telegram-bot-api/v5"
 15 | )
 16 | 
 17 | type LoginStatus byte
 18 | 
 19 | const (
 20 | 	LoginSuccess LoginStatus = 1
 21 | 	LoginFail    LoginStatus = 0
 22 | )
 23 | 
 24 | type StatsNotifyJob struct {
 25 | 	enable         bool
 26 | 	xrayService    service.XrayService
 27 | 	inboundService service.InboundService
 28 | 	settingService service.SettingService
 29 | }
 30 | 
 31 | func NewStatsNotifyJob() *StatsNotifyJob {
 32 | 	return new(StatsNotifyJob)
 33 | }
 34 | 
 35 | func (j *StatsNotifyJob) SendMsgToTgbot(msg string) {
 36 | 	//Telegram bot basic info
 37 | 	tgBottoken, err := j.settingService.GetTgBotToken()
 38 | 	if err != nil {
 39 | 		logger.Warning("sendMsgToTgbot failed,GetTgBotToken fail:", err)
 40 | 		return
 41 | 	}
 42 | 	tgBotid, err := j.settingService.GetTgBotChatId()
 43 | 	if err != nil {
 44 | 		logger.Warning("sendMsgToTgbot failed,GetTgBotChatId fail:", err)
 45 | 		return
 46 | 	}
 47 | 
 48 | 	bot, err := tgbotapi.NewBotAPI(tgBottoken)
 49 | 	if err != nil {
 50 | 		fmt.Println("get tgbot error:", err)
 51 | 		return
 52 | 	}
 53 | 	bot.Debug = true
 54 | 	fmt.Printf("Authorized on account %s", bot.Self.UserName)
 55 | 	info := tgbotapi.NewMessage(int64(tgBotid), msg)
 56 | 	//msg.ReplyToMessageID = int(tgBotid)
 57 | 	bot.Send(info)
 58 | }
 59 | 
 60 | //Here run is a interface method of Job interface
 61 | func (j *StatsNotifyJob) Run() {
 62 | 	if !j.xrayService.IsXrayRunning() {
 63 | 		return
 64 | 	}
 65 | 	var info string
 66 | 	//get hostname
 67 | 	name, err := os.Hostname()
 68 | 	if err != nil {
 69 | 		fmt.Println("get hostname error:", err)
 70 | 		return
 71 | 	}
 72 | 	info = fmt.Sprintf("主机名称:%s\r\n", name)
 73 | 	//get ip address
 74 | 	var ip string
 75 | 	netInterfaces, err := net.Interfaces()
 76 | 	if err != nil {
 77 | 		fmt.Println("net.Interfaces failed, err:", err.Error())
 78 | 		return
 79 | 	}
 80 | 
 81 | 	for i := 0; i < len(netInterfaces); i++ {
 82 | 		if (netInterfaces[i].Flags & net.FlagUp) != 0 {
 83 | 			addrs, _ := netInterfaces[i].Addrs()
 84 | 
 85 | 			for _, address := range addrs {
 86 | 				if ipnet, ok := address.(*net.IPNet); ok && !ipnet.IP.IsLoopback() {
 87 | 					if ipnet.IP.To4() != nil {
 88 | 						ip = ipnet.IP.String()
 89 | 						break
 90 | 					} else {
 91 | 						ip = ipnet.IP.String()
 92 | 						break
 93 | 					}
 94 | 				}
 95 | 			}
 96 | 		}
 97 | 	}
 98 | 	info += fmt.Sprintf("IP地址:%s\r\n \r\n", ip)
 99 | 
100 | 	//get traffic
101 | 	inbouds, err := j.inboundService.GetAllInbounds()
102 | 	if err != nil {
103 | 		logger.Warning("StatsNotifyJob run failed:", err)
104 | 		return
105 | 	}
106 | 	//NOTE:If there no any sessions here,need to notify here
107 | 	//TODO:分节点推送,自动转化格式
108 | 	for _, inbound := range inbouds {
109 | 		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)))
110 | 		if inbound.ExpiryTime == 0 {
111 | 			info += fmt.Sprintf("到期时间:无限期\r\n \r\n")
112 | 		} else {
113 | 			info += fmt.Sprintf("到期时间:%s\r\n \r\n", time.Unix((inbound.ExpiryTime/1000), 0).Format("2006-01-02 15:04:05"))
114 | 		}
115 | 	}
116 | 	j.SendMsgToTgbot(info)
117 | }
118 | 
119 | func (j *StatsNotifyJob) UserLoginNotify(username string, ip string, time string, status LoginStatus) {
120 | 	if username == "" || ip == "" || time == "" {
121 | 		logger.Warning("UserLoginNotify failed,invalid info")
122 | 		return
123 | 	}
124 | 	var msg string
125 | 	//get hostname
126 | 	name, err := os.Hostname()
127 | 	if err != nil {
128 | 		fmt.Println("get hostname error:", err)
129 | 		return
130 | 	}
131 | 	if status == LoginSuccess {
132 | 		msg = fmt.Sprintf("面板登录成功提醒\r\n主机名称:%s\r\n", name)
133 | 	} else if status == LoginFail {
134 | 		msg = fmt.Sprintf("面板登录失败提醒\r\n主机名称:%s\r\n", name)
135 | 	}
136 | 	msg += fmt.Sprintf("时间:%s\r\n", time)
137 | 	msg += fmt.Sprintf("用户:%s\r\n", username)
138 | 	msg += fmt.Sprintf("IP:%s\r\n", ip)
139 | 	j.SendMsgToTgbot(msg)
140 | }
141 | 


--------------------------------------------------------------------------------
/web/job/xray_traffic_job.go:
--------------------------------------------------------------------------------
 1 | package job
 2 | 
 3 | import (
 4 | 	"x-ui/logger"
 5 | 	"x-ui/web/service"
 6 | )
 7 | 
 8 | type XrayTrafficJob struct {
 9 | 	xrayService    service.XrayService
10 | 	inboundService service.InboundService
11 | }
12 | 
13 | func NewXrayTrafficJob() *XrayTrafficJob {
14 | 	return new(XrayTrafficJob)
15 | }
16 | 
17 | func (j *XrayTrafficJob) Run() {
18 | 	if !j.xrayService.IsXrayRunning() {
19 | 		return
20 | 	}
21 | 	traffics, err := j.xrayService.GetXrayTraffic()
22 | 	if err != nil {
23 | 		logger.Warning("get xray traffic failed:", err)
24 | 		return
25 | 	}
26 | 	err = j.inboundService.AddTraffic(traffics)
27 | 	if err != nil {
28 | 		logger.Warning("add traffic failed:", err)
29 | 	}
30 | }
31 | 


--------------------------------------------------------------------------------
/web/network/auto_https_listener.go:
--------------------------------------------------------------------------------
 1 | package network
 2 | 
 3 | import "net"
 4 | 
 5 | type AutoHttpsListener struct {
 6 | 	net.Listener
 7 | }
 8 | 
 9 | func NewAutoHttpsListener(listener net.Listener) net.Listener {
10 | 	return &AutoHttpsListener{
11 | 		Listener: listener,
12 | 	}
13 | }
14 | 
15 | func (l *AutoHttpsListener) Accept() (net.Conn, error) {
16 | 	conn, err := l.Listener.Accept()
17 | 	if err != nil {
18 | 		return nil, err
19 | 	}
20 | 	return NewAutoHttpsConn(conn), nil
21 | }
22 | 


--------------------------------------------------------------------------------
/web/network/autp_https_conn.go:
--------------------------------------------------------------------------------
 1 | package network
 2 | 
 3 | import (
 4 | 	"bufio"
 5 | 	"bytes"
 6 | 	"fmt"
 7 | 	"net"
 8 | 	"net/http"
 9 | 	"sync"
10 | )
11 | 
12 | type AutoHttpsConn struct {
13 | 	net.Conn
14 | 
15 | 	firstBuf []byte
16 | 	bufStart int
17 | 
18 | 	readRequestOnce sync.Once
19 | }
20 | 
21 | func NewAutoHttpsConn(conn net.Conn) net.Conn {
22 | 	return &AutoHttpsConn{
23 | 		Conn: conn,
24 | 	}
25 | }
26 | 
27 | func (c *AutoHttpsConn) readRequest() bool {
28 | 	c.firstBuf = make([]byte, 2048)
29 | 	n, err := c.Conn.Read(c.firstBuf)
30 | 	c.firstBuf = c.firstBuf[:n]
31 | 	if err != nil {
32 | 		return false
33 | 	}
34 | 	reader := bytes.NewReader(c.firstBuf)
35 | 	bufReader := bufio.NewReader(reader)
36 | 	request, err := http.ReadRequest(bufReader)
37 | 	if err != nil {
38 | 		return false
39 | 	}
40 | 	resp := http.Response{
41 | 		Header: http.Header{},
42 | 	}
43 | 	resp.StatusCode = http.StatusTemporaryRedirect
44 | 	location := fmt.Sprintf("https://%v%v", request.Host, request.RequestURI)
45 | 	resp.Header.Set("Location", location)
46 | 	resp.Write(c.Conn)
47 | 	c.Close()
48 | 	c.firstBuf = nil
49 | 	return true
50 | }
51 | 
52 | func (c *AutoHttpsConn) Read(buf []byte) (int, error) {
53 | 	c.readRequestOnce.Do(func() {
54 | 		c.readRequest()
55 | 	})
56 | 
57 | 	if c.firstBuf != nil {
58 | 		n := copy(buf, c.firstBuf[c.bufStart:])
59 | 		c.bufStart += n
60 | 		if c.bufStart >= len(c.firstBuf) {
61 | 			c.firstBuf = nil
62 | 		}
63 | 		return n, nil
64 | 	}
65 | 
66 | 	return c.Conn.Read(buf)
67 | }
68 | 


--------------------------------------------------------------------------------
/web/service/config.json:
--------------------------------------------------------------------------------
 1 | {
 2 |   "api": {
 3 |     "services": [
 4 |       "HandlerService",
 5 |       "LoggerService",
 6 |       "StatsService"
 7 |     ],
 8 |     "tag": "api"
 9 |   },
10 |   "inbounds": [
11 |     {
12 |       "listen": "127.0.0.1",
13 |       "port": 62789,
14 |       "protocol": "dokodemo-door",
15 |       "settings": {
16 |         "address": "127.0.0.1"
17 |       },
18 |       "tag": "api"
19 |     }
20 |   ],
21 |   "outbounds": [
22 |     {
23 |       "protocol": "freedom",
24 |       "settings": {}
25 |     },
26 |     {
27 |       "protocol": "blackhole",
28 |       "settings": {},
29 |       "tag": "blocked"
30 |     }
31 |   ],
32 |   "policy": {
33 |     "system": {
34 |       "statsInboundDownlink": true,
35 |       "statsInboundUplink": true
36 |     }
37 |   },
38 |   "routing": {
39 |     "rules": [
40 |       {
41 |         "inboundTag": [
42 |           "api"
43 |         ],
44 |         "outboundTag": "api",
45 |         "type": "field"
46 |       },
47 |       {
48 |         "ip": [
49 |           "geoip:private"
50 |         ],
51 |         "outboundTag": "blocked",
52 |         "type": "field"
53 |       },
54 |       {
55 |         "outboundTag": "blocked",
56 |         "protocol": [
57 |           "bittorrent"
58 |         ],
59 |         "type": "field"
60 |       }
61 |     ]
62 |   },
63 |   "stats": {}
64 | }


--------------------------------------------------------------------------------
/web/service/inbound.go:
--------------------------------------------------------------------------------
  1 | package service
  2 | 
  3 | import (
  4 | 	"fmt"
  5 | 	"time"
  6 | 	"x-ui/database"
  7 | 	"x-ui/database/model"
  8 | 	"x-ui/util/common"
  9 | 	"x-ui/xray"
 10 | 
 11 | 	"gorm.io/gorm"
 12 | )
 13 | 
 14 | type InboundService struct {
 15 | }
 16 | 
 17 | func (s *InboundService) GetInbounds(userId int) ([]*model.Inbound, error) {
 18 | 	db := database.GetDB()
 19 | 	var inbounds []*model.Inbound
 20 | 	err := db.Model(model.Inbound{}).Where("user_id = ?", userId).Find(&inbounds).Error
 21 | 	if err != nil && err != gorm.ErrRecordNotFound {
 22 | 		return nil, err
 23 | 	}
 24 | 	return inbounds, nil
 25 | }
 26 | 
 27 | func (s *InboundService) GetAllInbounds() ([]*model.Inbound, error) {
 28 | 	db := database.GetDB()
 29 | 	var inbounds []*model.Inbound
 30 | 	err := db.Model(model.Inbound{}).Find(&inbounds).Error
 31 | 	if err != nil && err != gorm.ErrRecordNotFound {
 32 | 		return nil, err
 33 | 	}
 34 | 	return inbounds, nil
 35 | }
 36 | 
 37 | func (s *InboundService) checkPortExist(port int, ignoreId int) (bool, error) {
 38 | 	db := database.GetDB()
 39 | 	db = db.Model(model.Inbound{}).Where("port = ?", port)
 40 | 	if ignoreId > 0 {
 41 | 		db = db.Where("id != ?", ignoreId)
 42 | 	}
 43 | 	var count int64
 44 | 	err := db.Count(&count).Error
 45 | 	if err != nil {
 46 | 		return false, err
 47 | 	}
 48 | 	return count > 0, nil
 49 | }
 50 | 
 51 | func (s *InboundService) AddInbound(inbound *model.Inbound) error {
 52 | 	exist, err := s.checkPortExist(inbound.Port, 0)
 53 | 	if err != nil {
 54 | 		return err
 55 | 	}
 56 | 	if exist {
 57 | 		return common.NewError("端口已存在:", inbound.Port)
 58 | 	}
 59 | 	db := database.GetDB()
 60 | 	return db.Save(inbound).Error
 61 | }
 62 | 
 63 | func (s *InboundService) AddInbounds(inbounds []*model.Inbound) error {
 64 | 	for _, inbound := range inbounds {
 65 | 		exist, err := s.checkPortExist(inbound.Port, 0)
 66 | 		if err != nil {
 67 | 			return err
 68 | 		}
 69 | 		if exist {
 70 | 			return common.NewError("端口已存在:", inbound.Port)
 71 | 		}
 72 | 	}
 73 | 
 74 | 	db := database.GetDB()
 75 | 	tx := db.Begin()
 76 | 	var err error
 77 | 	defer func() {
 78 | 		if err == nil {
 79 | 			tx.Commit()
 80 | 		} else {
 81 | 			tx.Rollback()
 82 | 		}
 83 | 	}()
 84 | 
 85 | 	for _, inbound := range inbounds {
 86 | 		err = tx.Save(inbound).Error
 87 | 		if err != nil {
 88 | 			return err
 89 | 		}
 90 | 	}
 91 | 
 92 | 	return nil
 93 | }
 94 | 
 95 | func (s *InboundService) DelInbound(id int) error {
 96 | 	db := database.GetDB()
 97 | 	return db.Delete(model.Inbound{}, id).Error
 98 | }
 99 | 
100 | func (s *InboundService) GetInbound(id int) (*model.Inbound, error) {
101 | 	db := database.GetDB()
102 | 	inbound := &model.Inbound{}
103 | 	err := db.Model(model.Inbound{}).First(inbound, id).Error
104 | 	if err != nil {
105 | 		return nil, err
106 | 	}
107 | 	return inbound, nil
108 | }
109 | 
110 | func (s *InboundService) UpdateInbound(inbound *model.Inbound) error {
111 | 	exist, err := s.checkPortExist(inbound.Port, inbound.Id)
112 | 	if err != nil {
113 | 		return err
114 | 	}
115 | 	if exist {
116 | 		return common.NewError("端口已存在:", inbound.Port)
117 | 	}
118 | 
119 | 	oldInbound, err := s.GetInbound(inbound.Id)
120 | 	if err != nil {
121 | 		return err
122 | 	}
123 | 	oldInbound.Up = inbound.Up
124 | 	oldInbound.Down = inbound.Down
125 | 	oldInbound.Total = inbound.Total
126 | 	oldInbound.Remark = inbound.Remark
127 | 	oldInbound.Enable = inbound.Enable
128 | 	oldInbound.ExpiryTime = inbound.ExpiryTime
129 | 	oldInbound.Listen = inbound.Listen
130 | 	oldInbound.Port = inbound.Port
131 | 	oldInbound.Protocol = inbound.Protocol
132 | 	oldInbound.Settings = inbound.Settings
133 | 	oldInbound.StreamSettings = inbound.StreamSettings
134 | 	oldInbound.Sniffing = inbound.Sniffing
135 | 	oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
136 | 
137 | 	db := database.GetDB()
138 | 	return db.Save(oldInbound).Error
139 | }
140 | 
141 | func (s *InboundService) AddTraffic(traffics []*xray.Traffic) (err error) {
142 | 	if len(traffics) == 0 {
143 | 		return nil
144 | 	}
145 | 	db := database.GetDB()
146 | 	db = db.Model(model.Inbound{})
147 | 	tx := db.Begin()
148 | 	defer func() {
149 | 		if err != nil {
150 | 			tx.Rollback()
151 | 		} else {
152 | 			tx.Commit()
153 | 		}
154 | 	}()
155 | 	for _, traffic := range traffics {
156 | 		if traffic.IsInbound {
157 | 			err = tx.Where("tag = ?", traffic.Tag).
158 | 				UpdateColumn("up", gorm.Expr("up + ?", traffic.Up)).
159 | 				UpdateColumn("down", gorm.Expr("down + ?", traffic.Down)).
160 | 				Error
161 | 			if err != nil {
162 | 				return
163 | 			}
164 | 		}
165 | 	}
166 | 	return
167 | }
168 | 
169 | func (s *InboundService) DisableInvalidInbounds() (int64, error) {
170 | 	db := database.GetDB()
171 | 	now := time.Now().Unix() * 1000
172 | 	result := db.Model(model.Inbound{}).
173 | 		Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
174 | 		Update("enable", false)
175 | 	err := result.Error
176 | 	count := result.RowsAffected
177 | 	return count, err
178 | }
179 | 


--------------------------------------------------------------------------------
/web/service/panel.go:
--------------------------------------------------------------------------------
 1 | package service
 2 | 
 3 | import (
 4 | 	"os"
 5 | 	"syscall"
 6 | 	"time"
 7 | 	"x-ui/logger"
 8 | )
 9 | 
10 | type PanelService struct {
11 | }
12 | 
13 | func (s *PanelService) RestartPanel(delay time.Duration) error {
14 | 	p, err := os.FindProcess(syscall.Getpid())
15 | 	if err != nil {
16 | 		return err
17 | 	}
18 | 	go func() {
19 | 		time.Sleep(delay)
20 | 		err := p.Signal(syscall.SIGHUP)
21 | 		if err != nil {
22 | 			logger.Error("send signal SIGHUP failed:", err)
23 | 		}
24 | 	}()
25 | 	return nil
26 | }
27 | 


--------------------------------------------------------------------------------
/web/service/server.go:
--------------------------------------------------------------------------------
  1 | package service
  2 | 
  3 | import (
  4 | 	"archive/zip"
  5 | 	"bytes"
  6 | 	"encoding/json"
  7 | 	"fmt"
  8 | 	"github.com/shirou/gopsutil/cpu"
  9 | 	"github.com/shirou/gopsutil/disk"
 10 | 	"github.com/shirou/gopsutil/host"
 11 | 	"github.com/shirou/gopsutil/load"
 12 | 	"github.com/shirou/gopsutil/mem"
 13 | 	"github.com/shirou/gopsutil/net"
 14 | 	"io"
 15 | 	"io/fs"
 16 | 	"net/http"
 17 | 	"os"
 18 | 	"runtime"
 19 | 	"time"
 20 | 	"x-ui/logger"
 21 | 	"x-ui/util/sys"
 22 | 	"x-ui/xray"
 23 | )
 24 | 
 25 | type ProcessState string
 26 | 
 27 | const (
 28 | 	Running ProcessState = "running"
 29 | 	Stop    ProcessState = "stop"
 30 | 	Error   ProcessState = "error"
 31 | )
 32 | 
 33 | type Status struct {
 34 | 	T   time.Time `json:"-"`
 35 | 	Cpu float64   `json:"cpu"`
 36 | 	Mem struct {
 37 | 		Current uint64 `json:"current"`
 38 | 		Total   uint64 `json:"total"`
 39 | 	} `json:"mem"`
 40 | 	Swap struct {
 41 | 		Current uint64 `json:"current"`
 42 | 		Total   uint64 `json:"total"`
 43 | 	} `json:"swap"`
 44 | 	Disk struct {
 45 | 		Current uint64 `json:"current"`
 46 | 		Total   uint64 `json:"total"`
 47 | 	} `json:"disk"`
 48 | 	Xray struct {
 49 | 		State    ProcessState `json:"state"`
 50 | 		ErrorMsg string       `json:"errorMsg"`
 51 | 		Version  string       `json:"version"`
 52 | 	} `json:"xray"`
 53 | 	Uptime   uint64    `json:"uptime"`
 54 | 	Loads    []float64 `json:"loads"`
 55 | 	TcpCount int       `json:"tcpCount"`
 56 | 	UdpCount int       `json:"udpCount"`
 57 | 	NetIO    struct {
 58 | 		Up   uint64 `json:"up"`
 59 | 		Down uint64 `json:"down"`
 60 | 	} `json:"netIO"`
 61 | 	NetTraffic struct {
 62 | 		Sent uint64 `json:"sent"`
 63 | 		Recv uint64 `json:"recv"`
 64 | 	} `json:"netTraffic"`
 65 | }
 66 | 
 67 | type Release struct {
 68 | 	TagName string `json:"tag_name"`
 69 | }
 70 | 
 71 | type ServerService struct {
 72 | 	xrayService XrayService
 73 | }
 74 | 
 75 | func (s *ServerService) GetStatus(lastStatus *Status) *Status {
 76 | 	now := time.Now()
 77 | 	status := &Status{
 78 | 		T: now,
 79 | 	}
 80 | 
 81 | 	percents, err := cpu.Percent(0, false)
 82 | 	if err != nil {
 83 | 		logger.Warning("get cpu percent failed:", err)
 84 | 	} else {
 85 | 		status.Cpu = percents[0]
 86 | 	}
 87 | 
 88 | 	upTime, err := host.Uptime()
 89 | 	if err != nil {
 90 | 		logger.Warning("get uptime failed:", err)
 91 | 	} else {
 92 | 		status.Uptime = upTime
 93 | 	}
 94 | 
 95 | 	memInfo, err := mem.VirtualMemory()
 96 | 	if err != nil {
 97 | 		logger.Warning("get virtual memory failed:", err)
 98 | 	} else {
 99 | 		status.Mem.Current = memInfo.Used
100 | 		status.Mem.Total = memInfo.Total
101 | 	}
102 | 
103 | 	swapInfo, err := mem.SwapMemory()
104 | 	if err != nil {
105 | 		logger.Warning("get swap memory failed:", err)
106 | 	} else {
107 | 		status.Swap.Current = swapInfo.Used
108 | 		status.Swap.Total = swapInfo.Total
109 | 	}
110 | 
111 | 	distInfo, err := disk.Usage("/")
112 | 	if err != nil {
113 | 		logger.Warning("get dist usage failed:", err)
114 | 	} else {
115 | 		status.Disk.Current = distInfo.Used
116 | 		status.Disk.Total = distInfo.Total
117 | 	}
118 | 
119 | 	avgState, err := load.Avg()
120 | 	if err != nil {
121 | 		logger.Warning("get load avg failed:", err)
122 | 	} else {
123 | 		status.Loads = []float64{avgState.Load1, avgState.Load5, avgState.Load15}
124 | 	}
125 | 
126 | 	ioStats, err := net.IOCounters(false)
127 | 	if err != nil {
128 | 		logger.Warning("get io counters failed:", err)
129 | 	} else if len(ioStats) > 0 {
130 | 		ioStat := ioStats[0]
131 | 		status.NetTraffic.Sent = ioStat.BytesSent
132 | 		status.NetTraffic.Recv = ioStat.BytesRecv
133 | 
134 | 		if lastStatus != nil {
135 | 			duration := now.Sub(lastStatus.T)
136 | 			seconds := float64(duration) / float64(time.Second)
137 | 			up := uint64(float64(status.NetTraffic.Sent-lastStatus.NetTraffic.Sent) / seconds)
138 | 			down := uint64(float64(status.NetTraffic.Recv-lastStatus.NetTraffic.Recv) / seconds)
139 | 			status.NetIO.Up = up
140 | 			status.NetIO.Down = down
141 | 		}
142 | 	} else {
143 | 		logger.Warning("can not find io counters")
144 | 	}
145 | 
146 | 	status.TcpCount, err = sys.GetTCPCount()
147 | 	if err != nil {
148 | 		logger.Warning("get tcp connections failed:", err)
149 | 	}
150 | 
151 | 	status.UdpCount, err = sys.GetUDPCount()
152 | 	if err != nil {
153 | 		logger.Warning("get udp connections failed:", err)
154 | 	}
155 | 
156 | 	if s.xrayService.IsXrayRunning() {
157 | 		status.Xray.State = Running
158 | 		status.Xray.ErrorMsg = ""
159 | 	} else {
160 | 		err := s.xrayService.GetXrayErr()
161 | 		if err != nil {
162 | 			status.Xray.State = Error
163 | 		} else {
164 | 			status.Xray.State = Stop
165 | 		}
166 | 		status.Xray.ErrorMsg = s.xrayService.GetXrayResult()
167 | 	}
168 | 	status.Xray.Version = s.xrayService.GetXrayVersion()
169 | 
170 | 	return status
171 | }
172 | 
173 | func (s *ServerService) GetXrayVersions() ([]string, error) {
174 | 	url := "https://api.github.com/repos/XTLS/Xray-core/releases"
175 | 	resp, err := http.Get(url)
176 | 	if err != nil {
177 | 		return nil, err
178 | 	}
179 | 
180 | 	defer resp.Body.Close()
181 | 	buffer := bytes.NewBuffer(make([]byte, 8192))
182 | 	buffer.Reset()
183 | 	_, err = buffer.ReadFrom(resp.Body)
184 | 	if err != nil {
185 | 		return nil, err
186 | 	}
187 | 
188 | 	releases := make([]Release, 0)
189 | 	err = json.Unmarshal(buffer.Bytes(), &releases)
190 | 	if err != nil {
191 | 		return nil, err
192 | 	}
193 | 	versions := make([]string, 0, len(releases))
194 | 	for _, release := range releases {
195 | 		versions = append(versions, release.TagName)
196 | 	}
197 | 	return versions, nil
198 | }
199 | 
200 | func (s *ServerService) downloadXRay(version string) (string, error) {
201 | 	osName := runtime.GOOS
202 | 	arch := runtime.GOARCH
203 | 
204 | 	switch osName {
205 | 	case "darwin":
206 | 		osName = "macos"
207 | 	}
208 | 
209 | 	switch arch {
210 | 	case "amd64":
211 | 		arch = "64"
212 | 	case "arm64":
213 | 		arch = "arm64-v8a"
214 | 	}
215 | 
216 | 	fileName := fmt.Sprintf("Xray-%s-%s.zip", osName, arch)
217 | 	url := fmt.Sprintf("https://github.com/XTLS/Xray-core/releases/download/%s/%s", version, fileName)
218 | 	resp, err := http.Get(url)
219 | 	if err != nil {
220 | 		return "", err
221 | 	}
222 | 	defer resp.Body.Close()
223 | 
224 | 	os.Remove(fileName)
225 | 	file, err := os.Create(fileName)
226 | 	if err != nil {
227 | 		return "", err
228 | 	}
229 | 	defer file.Close()
230 | 
231 | 	_, err = io.Copy(file, resp.Body)
232 | 	if err != nil {
233 | 		return "", err
234 | 	}
235 | 
236 | 	return fileName, nil
237 | }
238 | 
239 | func (s *ServerService) UpdateXray(version string) error {
240 | 	zipFileName, err := s.downloadXRay(version)
241 | 	if err != nil {
242 | 		return err
243 | 	}
244 | 
245 | 	zipFile, err := os.Open(zipFileName)
246 | 	if err != nil {
247 | 		return err
248 | 	}
249 | 	defer func() {
250 | 		zipFile.Close()
251 | 		os.Remove(zipFileName)
252 | 	}()
253 | 
254 | 	stat, err := zipFile.Stat()
255 | 	if err != nil {
256 | 		return err
257 | 	}
258 | 	reader, err := zip.NewReader(zipFile, stat.Size())
259 | 	if err != nil {
260 | 		return err
261 | 	}
262 | 
263 | 	s.xrayService.StopXray()
264 | 	defer func() {
265 | 		err := s.xrayService.RestartXray(true)
266 | 		if err != nil {
267 | 			logger.Error("start xray failed:", err)
268 | 		}
269 | 	}()
270 | 
271 | 	copyZipFile := func(zipName string, fileName string) error {
272 | 		zipFile, err := reader.Open(zipName)
273 | 		if err != nil {
274 | 			return err
275 | 		}
276 | 		os.Remove(fileName)
277 | 		file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm)
278 | 		if err != nil {
279 | 			return err
280 | 		}
281 | 		defer file.Close()
282 | 		_, err = io.Copy(file, zipFile)
283 | 		return err
284 | 	}
285 | 
286 | 	err = copyZipFile("xray", xray.GetBinaryPath())
287 | 	if err != nil {
288 | 		return err
289 | 	}
290 | 	err = copyZipFile("geosite.dat", xray.GetGeositePath())
291 | 	if err != nil {
292 | 		return err
293 | 	}
294 | 	err = copyZipFile("geoip.dat", xray.GetGeoipPath())
295 | 	if err != nil {
296 | 		return err
297 | 	}
298 | 
299 | 	return nil
300 | 
301 | }
302 | 


--------------------------------------------------------------------------------
/web/service/setting.go:
--------------------------------------------------------------------------------
  1 | package service
  2 | 
  3 | import (
  4 | 	_ "embed"
  5 | 	"errors"
  6 | 	"fmt"
  7 | 	"reflect"
  8 | 	"strconv"
  9 | 	"strings"
 10 | 	"time"
 11 | 	"x-ui/database"
 12 | 	"x-ui/database/model"
 13 | 	"x-ui/logger"
 14 | 	"x-ui/util/common"
 15 | 	"x-ui/util/random"
 16 | 	"x-ui/util/reflect_util"
 17 | 	"x-ui/web/entity"
 18 | )
 19 | 
 20 | //go:embed config.json
 21 | var xrayTemplateConfig string
 22 | 
 23 | var defaultValueMap = map[string]string{
 24 | 	"xrayTemplateConfig": xrayTemplateConfig,
 25 | 	"webListen":          "",
 26 | 	"webPort":            "54321",
 27 | 	"webCertFile":        "",
 28 | 	"webKeyFile":         "",
 29 | 	"secret":             random.Seq(32),
 30 | 	"webBasePath":        "/",
 31 | 	"timeLocation":       "Asia/Shanghai",
 32 | 	"tgBotEnable":        "false",
 33 | 	"tgBotToken":         "",
 34 | 	"tgBotChatId":        "0",
 35 | 	"tgRunTime":          "",
 36 | }
 37 | 
 38 | type SettingService struct {
 39 | }
 40 | 
 41 | func (s *SettingService) GetAllSetting() (*entity.AllSetting, error) {
 42 | 	db := database.GetDB()
 43 | 	settings := make([]*model.Setting, 0)
 44 | 	err := db.Model(model.Setting{}).Find(&settings).Error
 45 | 	if err != nil {
 46 | 		return nil, err
 47 | 	}
 48 | 	allSetting := &entity.AllSetting{}
 49 | 	t := reflect.TypeOf(allSetting).Elem()
 50 | 	v := reflect.ValueOf(allSetting).Elem()
 51 | 	fields := reflect_util.GetFields(t)
 52 | 
 53 | 	setSetting := func(key, value string) (err error) {
 54 | 		defer func() {
 55 | 			panicErr := recover()
 56 | 			if panicErr != nil {
 57 | 				err = errors.New(fmt.Sprint(panicErr))
 58 | 			}
 59 | 		}()
 60 | 
 61 | 		var found bool
 62 | 		var field reflect.StructField
 63 | 		for _, f := range fields {
 64 | 			if f.Tag.Get("json") == key {
 65 | 				field = f
 66 | 				found = true
 67 | 				break
 68 | 			}
 69 | 		}
 70 | 
 71 | 		if !found {
 72 | 			// 有些设置自动生成,不需要返回到前端给用户修改
 73 | 			return nil
 74 | 		}
 75 | 
 76 | 		fieldV := v.FieldByName(field.Name)
 77 | 		switch t := fieldV.Interface().(type) {
 78 | 		case int:
 79 | 			n, err := strconv.ParseInt(value, 10, 64)
 80 | 			if err != nil {
 81 | 				return err
 82 | 			}
 83 | 			fieldV.SetInt(n)
 84 | 		case string:
 85 | 			fieldV.SetString(value)
 86 | 		case bool:
 87 | 			fieldV.SetBool(value == "true")
 88 | 		default:
 89 | 			return common.NewErrorf("unknown field %v type %v", key, t)
 90 | 		}
 91 | 		return
 92 | 	}
 93 | 
 94 | 	keyMap := map[string]bool{}
 95 | 	for _, setting := range settings {
 96 | 		err := setSetting(setting.Key, setting.Value)
 97 | 		if err != nil {
 98 | 			return nil, err
 99 | 		}
100 | 		keyMap[setting.Key] = true
101 | 	}
102 | 
103 | 	for key, value := range defaultValueMap {
104 | 		if keyMap[key] {
105 | 			continue
106 | 		}
107 | 		err := setSetting(key, value)
108 | 		if err != nil {
109 | 			return nil, err
110 | 		}
111 | 	}
112 | 
113 | 	return allSetting, nil
114 | }
115 | 
116 | func (s *SettingService) ResetSettings() error {
117 | 	db := database.GetDB()
118 | 	return db.Where("1 = 1").Delete(model.Setting{}).Error
119 | }
120 | 
121 | func (s *SettingService) getSetting(key string) (*model.Setting, error) {
122 | 	db := database.GetDB()
123 | 	setting := &model.Setting{}
124 | 	err := db.Model(model.Setting{}).Where("key = ?", key).First(setting).Error
125 | 	if err != nil {
126 | 		return nil, err
127 | 	}
128 | 	return setting, nil
129 | }
130 | 
131 | func (s *SettingService) saveSetting(key string, value string) error {
132 | 	setting, err := s.getSetting(key)
133 | 	db := database.GetDB()
134 | 	if database.IsNotFound(err) {
135 | 		return db.Create(&model.Setting{
136 | 			Key:   key,
137 | 			Value: value,
138 | 		}).Error
139 | 	} else if err != nil {
140 | 		return err
141 | 	}
142 | 	setting.Key = key
143 | 	setting.Value = value
144 | 	return db.Save(setting).Error
145 | }
146 | 
147 | func (s *SettingService) getString(key string) (string, error) {
148 | 	setting, err := s.getSetting(key)
149 | 	if database.IsNotFound(err) {
150 | 		value, ok := defaultValueMap[key]
151 | 		if !ok {
152 | 			return "", common.NewErrorf("key <%v> not in defaultValueMap", key)
153 | 		}
154 | 		return value, nil
155 | 	} else if err != nil {
156 | 		return "", err
157 | 	}
158 | 	return setting.Value, nil
159 | }
160 | 
161 | func (s *SettingService) setString(key string, value string) error {
162 | 	return s.saveSetting(key, value)
163 | }
164 | 
165 | func (s *SettingService) getBool(key string) (bool, error) {
166 | 	str, err := s.getString(key)
167 | 	if err != nil {
168 | 		return false, err
169 | 	}
170 | 	return strconv.ParseBool(str)
171 | }
172 | 
173 | func (s *SettingService) setBool(key string, value bool) error {
174 | 	return s.setString(key, strconv.FormatBool(value))
175 | }
176 | 
177 | func (s *SettingService) getInt(key string) (int, error) {
178 | 	str, err := s.getString(key)
179 | 	if err != nil {
180 | 		return 0, err
181 | 	}
182 | 	return strconv.Atoi(str)
183 | }
184 | 
185 | func (s *SettingService) setInt(key string, value int) error {
186 | 	return s.setString(key, strconv.Itoa(value))
187 | }
188 | 
189 | func (s *SettingService) GetXrayConfigTemplate() (string, error) {
190 | 	return s.getString("xrayTemplateConfig")
191 | }
192 | 
193 | func (s *SettingService) GetListen() (string, error) {
194 | 	return s.getString("webListen")
195 | }
196 | 
197 | func (s *SettingService) GetTgBotToken() (string, error) {
198 | 	return s.getString("tgBotToken")
199 | }
200 | 
201 | func (s *SettingService) SetTgBotToken(token string) error {
202 | 	return s.setString("tgBotToken", token)
203 | }
204 | 
205 | func (s *SettingService) GetTgBotChatId() (int, error) {
206 | 	return s.getInt("tgBotChatId")
207 | }
208 | 
209 | func (s *SettingService) SetTgBotChatId(chatId int) error {
210 | 	return s.setInt("tgBotChatId", chatId)
211 | }
212 | 
213 | func (s *SettingService) SetTgbotenabled(value bool) error {
214 | 	return s.setBool("tgBotEnable", value)
215 | }
216 | 
217 | func (s *SettingService) GetTgbotenabled() (bool, error) {
218 | 	return s.getBool("tgBotEnable")
219 | }
220 | 
221 | func (s *SettingService) SetTgbotRuntime(time string) error {
222 | 	return s.setString("tgRunTime", time)
223 | }
224 | 
225 | func (s *SettingService) GetTgbotRuntime() (string, error) {
226 | 	return s.getString("tgRunTime")
227 | }
228 | 
229 | func (s *SettingService) GetPort() (int, error) {
230 | 	return s.getInt("webPort")
231 | }
232 | 
233 | func (s *SettingService) SetPort(port int) error {
234 | 	return s.setInt("webPort", port)
235 | }
236 | 
237 | func (s *SettingService) GetCertFile() (string, error) {
238 | 	return s.getString("webCertFile")
239 | }
240 | 
241 | func (s *SettingService) GetKeyFile() (string, error) {
242 | 	return s.getString("webKeyFile")
243 | }
244 | 
245 | func (s *SettingService) GetSecret() ([]byte, error) {
246 | 	secret, err := s.getString("secret")
247 | 	if secret == defaultValueMap["secret"] {
248 | 		err := s.saveSetting("secret", secret)
249 | 		if err != nil {
250 | 			logger.Warning("save secret failed:", err)
251 | 		}
252 | 	}
253 | 	return []byte(secret), err
254 | }
255 | 
256 | func (s *SettingService) GetBasePath() (string, error) {
257 | 	basePath, err := s.getString("webBasePath")
258 | 	if err != nil {
259 | 		return "", err
260 | 	}
261 | 	if !strings.HasPrefix(basePath, "/") {
262 | 		basePath = "/" + basePath
263 | 	}
264 | 	if !strings.HasSuffix(basePath, "/") {
265 | 		basePath += "/"
266 | 	}
267 | 	return basePath, nil
268 | }
269 | 
270 | func (s *SettingService) GetTimeLocation() (*time.Location, error) {
271 | 	l, err := s.getString("timeLocation")
272 | 	if err != nil {
273 | 		return nil, err
274 | 	}
275 | 	location, err := time.LoadLocation(l)
276 | 	if err != nil {
277 | 		defaultLocation := defaultValueMap["timeLocation"]
278 | 		logger.Errorf("location <%v> not exist, using default location: %v", l, defaultLocation)
279 | 		return time.LoadLocation(defaultLocation)
280 | 	}
281 | 	return location, nil
282 | }
283 | 
284 | func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {
285 | 	if err := allSetting.CheckValid(); err != nil {
286 | 		return err
287 | 	}
288 | 
289 | 	v := reflect.ValueOf(allSetting).Elem()
290 | 	t := reflect.TypeOf(allSetting).Elem()
291 | 	fields := reflect_util.GetFields(t)
292 | 	errs := make([]error, 0)
293 | 	for _, field := range fields {
294 | 		key := field.Tag.Get("json")
295 | 		fieldV := v.FieldByName(field.Name)
296 | 		value := fmt.Sprint(fieldV.Interface())
297 | 		err := s.saveSetting(key, value)
298 | 		if err != nil {
299 | 			errs = append(errs, err)
300 | 		}
301 | 	}
302 | 	return common.Combine(errs...)
303 | }
304 | 


--------------------------------------------------------------------------------
/web/service/user.go:
--------------------------------------------------------------------------------
 1 | package service
 2 | 
 3 | import (
 4 | 	"errors"
 5 | 	"x-ui/database"
 6 | 	"x-ui/database/model"
 7 | 	"x-ui/logger"
 8 | 
 9 | 	"gorm.io/gorm"
10 | )
11 | 
12 | type UserService struct {
13 | }
14 | 
15 | func (s *UserService) GetFirstUser() (*model.User, error) {
16 | 	db := database.GetDB()
17 | 
18 | 	user := &model.User{}
19 | 	err := db.Model(model.User{}).
20 | 		First(user).
21 | 		Error
22 | 	if err != nil {
23 | 		return nil, err
24 | 	}
25 | 	return user, nil
26 | }
27 | 
28 | func (s *UserService) CheckUser(username string, password string) *model.User {
29 | 	db := database.GetDB()
30 | 
31 | 	user := &model.User{}
32 | 	err := db.Model(model.User{}).
33 | 		Where("username = ? and password = ?", username, password).
34 | 		First(user).
35 | 		Error
36 | 	if err == gorm.ErrRecordNotFound {
37 | 		return nil
38 | 	} else if err != nil {
39 | 		logger.Warning("check user err:", err)
40 | 		return nil
41 | 	}
42 | 	return user
43 | }
44 | 
45 | func (s *UserService) UpdateUser(id int, username string, password string) error {
46 | 	db := database.GetDB()
47 | 	return db.Model(model.User{}).
48 | 		Where("id = ?", id).
49 | 		Update("username", username).
50 | 		Update("password", password).
51 | 		Error
52 | }
53 | 
54 | func (s *UserService) UpdateFirstUser(username string, password string) error {
55 | 	if username == "" {
56 | 		return errors.New("username can not be empty")
57 | 	} else if password == "" {
58 | 		return errors.New("password can not be empty")
59 | 	}
60 | 	db := database.GetDB()
61 | 	user := &model.User{}
62 | 	err := db.Model(model.User{}).First(user).Error
63 | 	if database.IsNotFound(err) {
64 | 		user.Username = username
65 | 		user.Password = password
66 | 		return db.Model(model.User{}).Create(user).Error
67 | 	} else if err != nil {
68 | 		return err
69 | 	}
70 | 	user.Username = username
71 | 	user.Password = password
72 | 	return db.Save(user).Error
73 | }
74 | 


--------------------------------------------------------------------------------
/web/service/xray.go:
--------------------------------------------------------------------------------
  1 | package service
  2 | 
  3 | import (
  4 | 	"encoding/json"
  5 | 	"errors"
  6 | 	"sync"
  7 | 	"x-ui/logger"
  8 | 	"x-ui/xray"
  9 | 
 10 | 	"go.uber.org/atomic"
 11 | )
 12 | 
 13 | var p *xray.Process
 14 | var lock sync.Mutex
 15 | var isNeedXrayRestart atomic.Bool
 16 | var result string
 17 | 
 18 | type XrayService struct {
 19 | 	inboundService InboundService
 20 | 	settingService SettingService
 21 | }
 22 | 
 23 | func (s *XrayService) IsXrayRunning() bool {
 24 | 	return p != nil && p.IsRunning()
 25 | }
 26 | 
 27 | func (s *XrayService) GetXrayErr() error {
 28 | 	if p == nil {
 29 | 		return nil
 30 | 	}
 31 | 	return p.GetErr()
 32 | }
 33 | 
 34 | func (s *XrayService) GetXrayResult() string {
 35 | 	if result != "" {
 36 | 		return result
 37 | 	}
 38 | 	if s.IsXrayRunning() {
 39 | 		return ""
 40 | 	}
 41 | 	if p == nil {
 42 | 		return ""
 43 | 	}
 44 | 	result = p.GetResult()
 45 | 	return result
 46 | }
 47 | 
 48 | func (s *XrayService) GetXrayVersion() string {
 49 | 	if p == nil {
 50 | 		return "Unknown"
 51 | 	}
 52 | 	return p.GetVersion()
 53 | }
 54 | 
 55 | func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
 56 | 	templateConfig, err := s.settingService.GetXrayConfigTemplate()
 57 | 	if err != nil {
 58 | 		return nil, err
 59 | 	}
 60 | 
 61 | 	xrayConfig := &xray.Config{}
 62 | 	err = json.Unmarshal([]byte(templateConfig), xrayConfig)
 63 | 	if err != nil {
 64 | 		return nil, err
 65 | 	}
 66 | 
 67 | 	inbounds, err := s.inboundService.GetAllInbounds()
 68 | 	if err != nil {
 69 | 		return nil, err
 70 | 	}
 71 | 	for _, inbound := range inbounds {
 72 | 		if !inbound.Enable {
 73 | 			continue
 74 | 		}
 75 | 		inboundConfig := inbound.GenXrayInboundConfig()
 76 | 		xrayConfig.InboundConfigs = append(xrayConfig.InboundConfigs, *inboundConfig)
 77 | 	}
 78 | 	return xrayConfig, nil
 79 | }
 80 | 
 81 | func (s *XrayService) GetXrayTraffic() ([]*xray.Traffic, error) {
 82 | 	if !s.IsXrayRunning() {
 83 | 		return nil, errors.New("xray is not running")
 84 | 	}
 85 | 	return p.GetTraffic(true)
 86 | }
 87 | 
 88 | func (s *XrayService) RestartXray(isForce bool) error {
 89 | 	lock.Lock()
 90 | 	defer lock.Unlock()
 91 | 	logger.Debug("restart xray, force:", isForce)
 92 | 
 93 | 	xrayConfig, err := s.GetXrayConfig()
 94 | 	if err != nil {
 95 | 		return err
 96 | 	}
 97 | 
 98 | 	if p != nil && p.IsRunning() {
 99 | 		if !isForce && p.GetConfig().Equals(xrayConfig) {
100 | 			logger.Debug("not need to restart xray")
101 | 			return nil
102 | 		}
103 | 		p.Stop()
104 | 	}
105 | 
106 | 	p = xray.NewProcess(xrayConfig)
107 | 	result = ""
108 | 	return p.Start()
109 | }
110 | 
111 | func (s *XrayService) StopXray() error {
112 | 	lock.Lock()
113 | 	defer lock.Unlock()
114 | 	logger.Debug("stop xray")
115 | 	if s.IsXrayRunning() {
116 | 		return p.Stop()
117 | 	}
118 | 	return errors.New("xray is not running")
119 | }
120 | 
121 | func (s *XrayService) SetToNeedRestart() {
122 | 	isNeedXrayRestart.Store(true)
123 | }
124 | 
125 | func (s *XrayService) IsNeedRestartAndSetFalse() bool {
126 | 	return isNeedXrayRestart.CAS(true, false)
127 | }
128 | 


--------------------------------------------------------------------------------
/web/session/session.go:
--------------------------------------------------------------------------------
 1 | package session
 2 | 
 3 | import (
 4 | 	"encoding/gob"
 5 | 	"github.com/gin-contrib/sessions"
 6 | 	"github.com/gin-gonic/gin"
 7 | 	"x-ui/database/model"
 8 | )
 9 | 
10 | const (
11 | 	loginUser = "LOGIN_USER"
12 | )
13 | 
14 | func init() {
15 | 	gob.Register(model.User{})
16 | }
17 | 
18 | func SetLoginUser(c *gin.Context, user *model.User) error {
19 | 	s := sessions.Default(c)
20 | 	s.Set(loginUser, user)
21 | 	return s.Save()
22 | }
23 | 
24 | func GetLoginUser(c *gin.Context) *model.User {
25 | 	s := sessions.Default(c)
26 | 	obj := s.Get(loginUser)
27 | 	if obj == nil {
28 | 		return nil
29 | 	}
30 | 	user := obj.(model.User)
31 | 	return &user
32 | }
33 | 
34 | func IsLogin(c *gin.Context) bool {
35 | 	return GetLoginUser(c) != nil
36 | }
37 | 
38 | func ClearSession(c *gin.Context) {
39 | 	s := sessions.Default(c)
40 | 	s.Clear()
41 | 	s.Options(sessions.Options{
42 | 		Path:   "/",
43 | 		MaxAge: -1,
44 | 	})
45 | 	s.Save()
46 | }
47 | 


--------------------------------------------------------------------------------
/web/translation/translate.en_US.toml:
--------------------------------------------------------------------------------
 1 | "username" = "username"
 2 | "password" = "password"
 3 | "login" = "login"
 4 | "confirm" = "confirm"
 5 | "cancel" = "cancel"
 6 | "close" = "close"
 7 | "copy" = "copy"
 8 | "copied" = "copied"
 9 | "download" = "download"
10 | "remark" = "remark"
11 | "enable" = "enable"
12 | "protocol" = "protocol"


--------------------------------------------------------------------------------
/web/translation/translate.zh_Hans.toml:
--------------------------------------------------------------------------------
 1 | "username" = "用户名"
 2 | "password" = "密码"
 3 | "login" = "登录"
 4 | "confirm" = "确定"
 5 | "cancel" = "取消"
 6 | "close" = "关闭"
 7 | "copy" = "复制"
 8 | "copied" = "已复制"
 9 | "download" = "下载"
10 | "remark" = "备注"
11 | "enable" = "启用"
12 | "protocol" = "协议"


--------------------------------------------------------------------------------
/web/translation/translate.zh_Hant.toml:
--------------------------------------------------------------------------------
 1 | "username" = "用戶名"
 2 | "password" = "密碼"
 3 | "login" = "登錄"
 4 | "confirm" = "確定"
 5 | "cancel" = "取消"
 6 | "close" = "關閉"
 7 | "copy" = "複製"
 8 | "copied" = "已複製"
 9 | "download" = "下載"
10 | "remark" = "備註"
11 | "enable" = "啟用"
12 | "protocol" = "協議"


--------------------------------------------------------------------------------
/x-ui.service:
--------------------------------------------------------------------------------
 1 | [Unit]
 2 | Description=x-ui Service
 3 | After=network.target
 4 | Wants=network.target
 5 | 
 6 | [Service]
 7 | Type=simple
 8 | WorkingDirectory=/usr/local/x-ui/
 9 | ExecStart=/usr/local/x-ui/x-ui
10 | 
11 | [Install]
12 | WantedBy=multi-user.target


--------------------------------------------------------------------------------
/xray/config.go:
--------------------------------------------------------------------------------
 1 | package xray
 2 | 
 3 | import (
 4 | 	"bytes"
 5 | 	"x-ui/util/json_util"
 6 | )
 7 | 
 8 | type Config struct {
 9 | 	LogConfig       json_util.RawMessage `json:"log"`
10 | 	RouterConfig    json_util.RawMessage `json:"routing"`
11 | 	DNSConfig       json_util.RawMessage `json:"dns"`
12 | 	InboundConfigs  []InboundConfig      `json:"inbounds"`
13 | 	OutboundConfigs json_util.RawMessage `json:"outbounds"`
14 | 	Transport       json_util.RawMessage `json:"transport"`
15 | 	Policy          json_util.RawMessage `json:"policy"`
16 | 	API             json_util.RawMessage `json:"api"`
17 | 	Stats           json_util.RawMessage `json:"stats"`
18 | 	Reverse         json_util.RawMessage `json:"reverse"`
19 | 	FakeDNS         json_util.RawMessage `json:"fakeDns"`
20 | }
21 | 
22 | func (c *Config) Equals(other *Config) bool {
23 | 	if len(c.InboundConfigs) != len(other.InboundConfigs) {
24 | 		return false
25 | 	}
26 | 	for i, inbound := range c.InboundConfigs {
27 | 		if !inbound.Equals(&other.InboundConfigs[i]) {
28 | 			return false
29 | 		}
30 | 	}
31 | 	if !bytes.Equal(c.LogConfig, other.LogConfig) {
32 | 		return false
33 | 	}
34 | 	if !bytes.Equal(c.RouterConfig, other.RouterConfig) {
35 | 		return false
36 | 	}
37 | 	if !bytes.Equal(c.DNSConfig, other.DNSConfig) {
38 | 		return false
39 | 	}
40 | 	if !bytes.Equal(c.OutboundConfigs, other.OutboundConfigs) {
41 | 		return false
42 | 	}
43 | 	if !bytes.Equal(c.Transport, other.Transport) {
44 | 		return false
45 | 	}
46 | 	if !bytes.Equal(c.Policy, other.Policy) {
47 | 		return false
48 | 	}
49 | 	if !bytes.Equal(c.API, other.API) {
50 | 		return false
51 | 	}
52 | 	if !bytes.Equal(c.Stats, other.Stats) {
53 | 		return false
54 | 	}
55 | 	if !bytes.Equal(c.Reverse, other.Reverse) {
56 | 		return false
57 | 	}
58 | 	if !bytes.Equal(c.FakeDNS, other.FakeDNS) {
59 | 		return false
60 | 	}
61 | 	return true
62 | }
63 | 


--------------------------------------------------------------------------------
/xray/inbound.go:
--------------------------------------------------------------------------------
 1 | package xray
 2 | 
 3 | import (
 4 | 	"bytes"
 5 | 	"x-ui/util/json_util"
 6 | )
 7 | 
 8 | type InboundConfig struct {
 9 | 	Listen         json_util.RawMessage `json:"listen"` // listen 不能为空字符串
10 | 	Port           int                  `json:"port"`
11 | 	Protocol       string               `json:"protocol"`
12 | 	Settings       json_util.RawMessage `json:"settings"`
13 | 	StreamSettings json_util.RawMessage `json:"streamSettings"`
14 | 	Tag            string               `json:"tag"`
15 | 	Sniffing       json_util.RawMessage `json:"sniffing"`
16 | }
17 | 
18 | func (c *InboundConfig) Equals(other *InboundConfig) bool {
19 | 	if !bytes.Equal(c.Listen, other.Listen) {
20 | 		return false
21 | 	}
22 | 	if c.Port != other.Port {
23 | 		return false
24 | 	}
25 | 	if c.Protocol != other.Protocol {
26 | 		return false
27 | 	}
28 | 	if !bytes.Equal(c.Settings, other.Settings) {
29 | 		return false
30 | 	}
31 | 	if !bytes.Equal(c.StreamSettings, other.StreamSettings) {
32 | 		return false
33 | 	}
34 | 	if c.Tag != other.Tag {
35 | 		return false
36 | 	}
37 | 	if !bytes.Equal(c.Sniffing, other.Sniffing) {
38 | 		return false
39 | 	}
40 | 	return true
41 | }
42 | 


--------------------------------------------------------------------------------
/xray/process.go:
--------------------------------------------------------------------------------
  1 | package xray
  2 | 
  3 | import (
  4 | 	"bufio"
  5 | 	"bytes"
  6 | 	"context"
  7 | 	"encoding/json"
  8 | 	"errors"
  9 | 	"fmt"
 10 | 	"io/fs"
 11 | 	"os"
 12 | 	"os/exec"
 13 | 	"regexp"
 14 | 	"runtime"
 15 | 	"strings"
 16 | 	"time"
 17 | 	"x-ui/util/common"
 18 | 
 19 | 	"github.com/Workiva/go-datastructures/queue"
 20 | 	statsservice "github.com/xtls/xray-core/app/stats/command"
 21 | 	"google.golang.org/grpc"
 22 | )
 23 | 
 24 | var trafficRegex = regexp.MustCompile("(inbound|outbound)>>>([^>]+)>>>traffic>>>(downlink|uplink)")
 25 | 
 26 | func GetBinaryName() string {
 27 | 	return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH)
 28 | }
 29 | 
 30 | func GetBinaryPath() string {
 31 | 	return "bin/" + GetBinaryName()
 32 | }
 33 | 
 34 | func GetConfigPath() string {
 35 | 	return "bin/config.json"
 36 | }
 37 | 
 38 | func GetGeositePath() string {
 39 | 	return "bin/geosite.dat"
 40 | }
 41 | 
 42 | func GetGeoipPath() string {
 43 | 	return "bin/geoip.dat"
 44 | }
 45 | 
 46 | func stopProcess(p *Process) {
 47 | 	p.Stop()
 48 | }
 49 | 
 50 | type Process struct {
 51 | 	*process
 52 | }
 53 | 
 54 | func NewProcess(xrayConfig *Config) *Process {
 55 | 	p := &Process{newProcess(xrayConfig)}
 56 | 	runtime.SetFinalizer(p, stopProcess)
 57 | 	return p
 58 | }
 59 | 
 60 | type process struct {
 61 | 	cmd *exec.Cmd
 62 | 
 63 | 	version string
 64 | 	apiPort int
 65 | 
 66 | 	config  *Config
 67 | 	lines   *queue.Queue
 68 | 	exitErr error
 69 | }
 70 | 
 71 | func newProcess(config *Config) *process {
 72 | 	return &process{
 73 | 		version: "Unknown",
 74 | 		config:  config,
 75 | 		lines:   queue.New(100),
 76 | 	}
 77 | }
 78 | 
 79 | func (p *process) IsRunning() bool {
 80 | 	if p.cmd == nil || p.cmd.Process == nil {
 81 | 		return false
 82 | 	}
 83 | 	if p.cmd.ProcessState == nil {
 84 | 		return true
 85 | 	}
 86 | 	return false
 87 | }
 88 | 
 89 | func (p *process) GetErr() error {
 90 | 	return p.exitErr
 91 | }
 92 | 
 93 | func (p *process) GetResult() string {
 94 | 	if p.lines.Empty() && p.exitErr != nil {
 95 | 		return p.exitErr.Error()
 96 | 	}
 97 | 	items, _ := p.lines.TakeUntil(func(item interface{}) bool {
 98 | 		return true
 99 | 	})
100 | 	lines := make([]string, 0, len(items))
101 | 	for _, item := range items {
102 | 		lines = append(lines, item.(string))
103 | 	}
104 | 	return strings.Join(lines, "\n")
105 | }
106 | 
107 | func (p *process) GetVersion() string {
108 | 	return p.version
109 | }
110 | 
111 | func (p *Process) GetAPIPort() int {
112 | 	return p.apiPort
113 | }
114 | 
115 | func (p *Process) GetConfig() *Config {
116 | 	return p.config
117 | }
118 | 
119 | func (p *process) refreshAPIPort() {
120 | 	for _, inbound := range p.config.InboundConfigs {
121 | 		if inbound.Tag == "api" {
122 | 			p.apiPort = inbound.Port
123 | 			break
124 | 		}
125 | 	}
126 | }
127 | 
128 | func (p *process) refreshVersion() {
129 | 	cmd := exec.Command(GetBinaryPath(), "-version")
130 | 	data, err := cmd.Output()
131 | 	if err != nil {
132 | 		p.version = "Unknown"
133 | 	} else {
134 | 		datas := bytes.Split(data, []byte(" "))
135 | 		if len(datas) <= 1 {
136 | 			p.version = "Unknown"
137 | 		} else {
138 | 			p.version = string(datas[1])
139 | 		}
140 | 	}
141 | }
142 | 
143 | func (p *process) Start() (err error) {
144 | 	if p.IsRunning() {
145 | 		return errors.New("xray is already running")
146 | 	}
147 | 
148 | 	defer func() {
149 | 		if err != nil {
150 | 			p.exitErr = err
151 | 		}
152 | 	}()
153 | 
154 | 	data, err := json.MarshalIndent(p.config, "", "  ")
155 | 	if err != nil {
156 | 		return common.NewErrorf("生成 xray 配置文件失败: %v", err)
157 | 	}
158 | 	configPath := GetConfigPath()
159 | 	err = os.WriteFile(configPath, data, fs.ModePerm)
160 | 	if err != nil {
161 | 		return common.NewErrorf("写入配置文件失败: %v", err)
162 | 	}
163 | 
164 | 	cmd := exec.Command(GetBinaryPath(), "-c", configPath)
165 | 	p.cmd = cmd
166 | 
167 | 	stdReader, err := cmd.StdoutPipe()
168 | 	if err != nil {
169 | 		return err
170 | 	}
171 | 	errReader, err := cmd.StderrPipe()
172 | 	if err != nil {
173 | 		return err
174 | 	}
175 | 
176 | 	go func() {
177 | 		defer func() {
178 | 			common.Recover("")
179 | 			stdReader.Close()
180 | 		}()
181 | 		reader := bufio.NewReaderSize(stdReader, 8192)
182 | 		for {
183 | 			line, _, err := reader.ReadLine()
184 | 			if err != nil {
185 | 				return
186 | 			}
187 | 			if p.lines.Len() >= 100 {
188 | 				p.lines.Get(1)
189 | 			}
190 | 			p.lines.Put(string(line))
191 | 		}
192 | 	}()
193 | 
194 | 	go func() {
195 | 		defer func() {
196 | 			common.Recover("")
197 | 			errReader.Close()
198 | 		}()
199 | 		reader := bufio.NewReaderSize(errReader, 8192)
200 | 		for {
201 | 			line, _, err := reader.ReadLine()
202 | 			if err != nil {
203 | 				return
204 | 			}
205 | 			if p.lines.Len() >= 100 {
206 | 				p.lines.Get(1)
207 | 			}
208 | 			p.lines.Put(string(line))
209 | 		}
210 | 	}()
211 | 
212 | 	go func() {
213 | 		err := cmd.Run()
214 | 		if err != nil {
215 | 			p.exitErr = err
216 | 		}
217 | 	}()
218 | 
219 | 	p.refreshVersion()
220 | 	p.refreshAPIPort()
221 | 
222 | 	return nil
223 | }
224 | 
225 | func (p *process) Stop() error {
226 | 	if !p.IsRunning() {
227 | 		return errors.New("xray is not running")
228 | 	}
229 | 	return p.cmd.Process.Kill()
230 | }
231 | 
232 | func (p *process) GetTraffic(reset bool) ([]*Traffic, error) {
233 | 	if p.apiPort == 0 {
234 | 		return nil, common.NewError("xray api port wrong:", p.apiPort)
235 | 	}
236 | 	conn, err := grpc.Dial(fmt.Sprintf("127.0.0.1:%v", p.apiPort), grpc.WithInsecure())
237 | 	if err != nil {
238 | 		return nil, err
239 | 	}
240 | 	defer conn.Close()
241 | 
242 | 	client := statsservice.NewStatsServiceClient(conn)
243 | 	ctx, cancel := context.WithTimeout(context.Background(), time.Second*10)
244 | 	defer cancel()
245 | 	request := &statsservice.QueryStatsRequest{
246 | 		Reset_: reset,
247 | 	}
248 | 	resp, err := client.QueryStats(ctx, request)
249 | 	if err != nil {
250 | 		return nil, err
251 | 	}
252 | 	tagTrafficMap := map[string]*Traffic{}
253 | 	traffics := make([]*Traffic, 0)
254 | 	for _, stat := range resp.GetStat() {
255 | 		matchs := trafficRegex.FindStringSubmatch(stat.Name)
256 | 		isInbound := matchs[1] == "inbound"
257 | 		tag := matchs[2]
258 | 		isDown := matchs[3] == "downlink"
259 | 		if tag == "api" {
260 | 			continue
261 | 		}
262 | 		traffic, ok := tagTrafficMap[tag]
263 | 		if !ok {
264 | 			traffic = &Traffic{
265 | 				IsInbound: isInbound,
266 | 				Tag:       tag,
267 | 			}
268 | 			tagTrafficMap[tag] = traffic
269 | 			traffics = append(traffics, traffic)
270 | 		}
271 | 		if isDown {
272 | 			traffic.Down = stat.Value
273 | 		} else {
274 | 			traffic.Up = stat.Value
275 | 		}
276 | 	}
277 | 
278 | 	return traffics, nil
279 | }
280 | 


--------------------------------------------------------------------------------
/xray/traffic.go:
--------------------------------------------------------------------------------
1 | package xray
2 | 
3 | type Traffic struct {
4 | 	IsInbound bool
5 | 	Tag       string
6 | 	Up        int64
7 | 	Down      int64
8 | }
9 | 


--------------------------------------------------------------------------------