├── .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 |  82 |  83 | 84 | 使用时只需输入 `域名`, `邮箱`, `API KEY`即可,示意图如下: 85 |  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 | [](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 | --------------------------------------------------------------------------------