├── .github └── workflows │ └── release.yml ├── .gitignore ├── Dockerfile ├── LICENSE ├── README.md ├── am_restart_x_ui.sh ├── 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_freebsd.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/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.GITHUB_TOKEN }} 19 | with: 20 | tag_name: ${{ github.ref }} 21 | release_name: ${{ github.ref }} 22 | draft: true 23 | prerelease: true 24 | freebsdamd64build: 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 freedbsd amd64 version 35 | run: | 36 | CGO_ENABLED=1 GOOS=freebsd 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-freebsd-64.zip 46 | unzip Xray-freebsd-64.zip 47 | rm -f Xray-freebsd-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-freebsd-amd64 51 | cd .. 52 | cd .. 53 | - name: package 54 | run: tar -zcvf x-ui-freebsd-amd64.tar.gz x-ui 55 | - name: upload 56 | uses: actions/upload-release-asset@v1 57 | env: 58 | GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }} 59 | with: 60 | upload_url: ${{ needs.release.outputs.upload_url }} 61 | asset_path: x-ui-freebsd-amd64.tar.gz 62 | asset_name: x-ui-freebsd-amd64.tar.gz 63 | asset_content_type: application/gzip 64 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .DS_Store 2 | node_modules 3 | /dist 4 | 5 | 6 | # local env files 7 | .env.local 8 | .env.*.local 9 | 10 | # Log files 11 | npm-debug.log* 12 | yarn-debug.log* 13 | yarn-error.log* 14 | pnpm-debug.log* 15 | 16 | # Editor directories and files 17 | .idea 18 | .vscode 19 | *.suo 20 | *.ntvs* 21 | *.njsproj 22 | *.sln 23 | *.sw? 24 | -------------------------------------------------------------------------------- /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" ] -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # [am-serv00-x-ui](https://github.com/amclubs/am-serv00-x-ui) 2 | 这是一个基于serv00免费服务器安装 x-ui for freebsd 支持多协议多用户的 xray 面板, 本版本支持FreeBSD非root安装 3 | 4 | # 5 | ▶️ **新人[YouTube](https://youtube.com/@am_clubs?sub_confirmation=1)** 需要您的支持,请务必帮我**点赞**、**关注**、**打开小铃铛**,***十分感谢!!!*** ✅ 6 |
🎁请 **follow** 我的[GitHub](https://github.com/amclubs)、给我所有项目一个 **Star** 星星(拜托了)!你的支持是我不断前进的动力! 💖 7 |
✅**解锁更多技能** [加入TG群【am_clubs】](https://t.me/am_clubs)、[YouTube频道【@am_clubs】](https://youtube.com/@am_clubs?sub_confirmation=1)、[【博客(国内)】](https://amclubss.com)、[【博客(国际)】](https://amclubs.blogspot.com) 8 |
✅点击观看教程[CLoudflare免费节点](https://www.youtube.com/playlist?list=PLGVQi7TjHKXbrY0Pk8gm3T7m8MZ-InquF) | [VPS搭建节点](https://www.youtube.com/playlist?list=PLGVQi7TjHKXaVlrHP9Du61CaEThYCQaiY) | [获取免费域名](https://www.youtube.com/playlist?list=PLGVQi7TjHKXZGODTvB8DEervrmHANQ1AR) | [免费VPN](https://www.youtube.com/playlist?list=PLGVQi7TjHKXY7V2JF-ShRSVwGANlZULdk) | [IPTV源](https://www.youtube.com/playlist?list=PLGVQi7TjHKXbkozDYVsDRJhbnNaEOC76w) | [Mac和Win工具](https://www.youtube.com/playlist?list=PLGVQi7TjHKXYBWu65yP8E08HxAu9LbCWm) | [AI分享](https://www.youtube.com/playlist?list=PLGVQi7TjHKXaodkM-mS-2Nwggwc5wRjqY) 9 | 10 | # 视频教程 11 | - [点击进入视频教程](https://youtu.be/YCq0pEpG2jE) 12 | - [免费域名视频教程](https://www.youtube.com/playlist?list=PLGVQi7TjHKXZGODTvB8DEervrmHANQ1AR) 13 | - [免费节点Cloudflare视频教程](https://www.youtube.com/playlist?list=PLGVQi7TjHKXbrY0Pk8gm3T7m8MZ-InquF) 14 | - [serv00所有视频教程](https://www.youtube.com/playlist?list=PLGVQi7TjHKXaVlrHP9Du61CaEThYCQaiY) 15 | 16 | # 功能介绍 17 | 18 | - 系统状态监控 19 | - 支持多用户多协议,网页可视化操作 20 | - 支持的协议:vmess、vless、trojan、shadowsocks、dokodemo-door、socks、http 21 | - 支持配置更多传输配置 22 | - 流量统计,限制流量,限制到期时间 23 | - 可自定义 xray 配置模板 24 | - 支持 https 访问面板(自备域名 + ssl 证书) 25 | - 更多高级配置项,详见面板 26 | 27 | # 安装&升级 28 | 在安装前,请先准备好用户名,密码和两个端口(面板访问端口和流量监控端口)! 29 | ``` 30 | wget -O x-ui.sh -N --no-check-certificate https://raw.githubusercontent.com/amclubs/am-serv00-x-ui/main/x-ui.sh && chmod +x x-ui.sh && ./x-ui.sh 31 | ``` 32 | 33 | ## 手动安装&升级 34 | 35 | 1. 首先从 https://github.com/amclubs/am-serv00-x-ui/releases 下载最新的压缩包,一般选择 `amd64`架构 36 | 2. 然后将这个压缩包上传到服务器的 `/home/[username]`目录下, 37 | 38 | > 如果你的服务器 cpu 架构不是 `amd64`,自行将命令中的 `amd64`替换为其他架构 39 | 40 | ``` 41 | cd ~ 42 | rm -rf ./x-ui 43 | tar zxvf x-ui-freebsd-amd64.tar.gz 44 | chmod +x x-ui/x-ui x-ui/bin/xray-freebsd-* x-ui/x-ui.sh 45 | cp x-ui/x-ui.sh ./x-ui.sh 46 | cd x-ui 47 | crontab -l > x-ui.cron 48 | echo "0 0 * * * cd $cur_dir/x-ui && cat /dev/null > x-ui.log" >> x-ui.cron 49 | echo "@reboot cd $cur_dir/x-ui && nohup ./x-ui run > ./x-ui.log 2>&1 &" >> x-ui.cron 50 | crontab x-ui.cron 51 | rm x-ui.cron 52 | nohup ./x-ui run > ./x-ui.log 2>&1 & 53 | ``` 54 | 55 | ## SSL证书申请 56 | 57 | 建议使用Cloudflare 15年证书 58 | 59 | ## Tg机器人使用(开发中,暂不可使用) 60 | 61 | 此功能未经测试! 62 | 63 | ## 建议系统 64 | 65 | - FreeBSD 14+ 66 | 67 | # 感谢 68 | [parentalclash](https://github.com/parentalclash/x-ui-freebsd)、[vaxilu](https://github.com/vaxilu/x-ui) 69 | 70 | # 71 |
72 |
[点击展开] 赞赏支持 ~🧧 73 | *我非常感谢您的赞赏和支持,它们将极大地激励我继续创新,持续产生有价值的工作。* 74 | 75 | - **USDT-TRC20:** `TWTxUyay6QJN3K4fs4kvJTT8Zfa2mWTwDD` 76 | - **TRX-TRC20:** `TWTxUyay6QJN3K4fs4kvJTT8Zfa2mWTwDD` 77 | 78 |
79 |
80 | TRC10/TRC20扫码支付 81 |
82 |
83 |
84 | 85 | # 86 | 免责声明: 87 | - 1、该项目设计和开发仅供学习、研究和安全测试目的。请于下载后 24 小时内删除, 不得用作任何商业用途, 文字、数据及图片均有所属版权, 如转载须注明来源。 88 | - 2、使用本程序必循遵守部署服务器所在地区的法律、所在国家和用户所在国家的法律法规。对任何人或团体使用该项目时产生的任何后果由使用者承担。 89 | - 3、作者不对使用该项目可能引起的任何直接或间接损害负责。作者保留随时更新免责声明的权利,且不另行通知。 90 | 91 | 92 | -------------------------------------------------------------------------------- /am_restart_x_ui.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | 3 | # 检查进程是否在运行 4 | pgrep -x "x-ui" > /dev/null 5 | 6 | # 如果没有运行,则启动 x-ui 7 | if [ $? -ne 0 ]; then 8 | /home/${USER}/x-ui/x-ui.sh restart 9 | fi 10 | -------------------------------------------------------------------------------- /bin/geoip.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-serv00-x-ui/7a8fd8c1e2b4546149c26948f749609b6ea42f80/bin/geoip.dat -------------------------------------------------------------------------------- /bin/geosite.dat: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-serv00-x-ui/7a8fd8c1e2b4546149c26948f749609b6ea42f80/bin/geosite.dat -------------------------------------------------------------------------------- /bin/xray-linux-amd64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-serv00-x-ui/7a8fd8c1e2b4546149c26948f749609b6ea42f80/bin/xray-linux-amd64 -------------------------------------------------------------------------------- /bin/xray-linux-arm64: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-serv00-x-ui/7a8fd8c1e2b4546149c26948f749609b6ea42f80/bin/xray-linux-arm64 -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "os" 7 | "strings" 8 | "path/filepath" 9 | ) 10 | 11 | //go:embed version 12 | var version string 13 | 14 | //go:embed name 15 | var name string 16 | 17 | type LogLevel string 18 | 19 | const ( 20 | Debug LogLevel = "debug" 21 | Info LogLevel = "info" 22 | Warn LogLevel = "warn" 23 | Error LogLevel = "error" 24 | ) 25 | 26 | func GetVersion() string { 27 | return strings.TrimSpace(version) 28 | } 29 | 30 | func GetName() string { 31 | return strings.TrimSpace(name) 32 | } 33 | 34 | func GetLogLevel() LogLevel { 35 | if IsDebug() { 36 | return Debug 37 | } 38 | logLevel := os.Getenv("XUI_LOG_LEVEL") 39 | if logLevel == "" { 40 | return Info 41 | } 42 | return LogLevel(logLevel) 43 | } 44 | 45 | func IsDebug() bool { 46 | return os.Getenv("XUI_DEBUG") == "true" 47 | } 48 | 49 | func GetDBPath() string { 50 | return fmt.Sprintf("%s/%s.db", GetExecPath(), GetName()) 51 | } 52 | 53 | func GetExecPath() string { 54 | path, err := os.Executable() 55 | if err != nil { 56 | fmt.Println("fail to get exec path:", err) 57 | return fmt.Sprintf("/etc/%s", GetName()) 58 | } 59 | return filepath.Dir(path) 60 | } 61 | -------------------------------------------------------------------------------- /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 | cd ~ 9 | cur_dir=$(pwd) 10 | 11 | uname_output=$(uname -a) 12 | 13 | # check os 14 | if echo "$uname_output" | grep -Eqi "freebsd"; then 15 | release="freebsd" 16 | else 17 | echo -e "${red}未检测到系统版本,请联系脚本作者!${plain}\n" && exit 1 18 | fi 19 | 20 | arch="none" 21 | 22 | if echo "$uname_output" | grep -Eqi 'x86_64|amd64|x64'; then 23 | arch="amd64" 24 | elif echo "$uname_output" | grep -Eqi 'aarch64|arm64'; then 25 | arch="arm64" 26 | else 27 | arch="amd64" 28 | echo -e "${red}检测架构失败,使用默认架构: ${arch}${plain}" 29 | fi 30 | 31 | echo "架构: ${arch}" 32 | 33 | #This function will be called when user installed x-ui out of sercurity 34 | config_after_install() { 35 | echo -e "${yellow}出于安全考虑,安装/更新完成后需要强制修改端口与账户密码${plain}" 36 | read -p "确认是否继续?[y/n]": config_confirm 37 | if [[ x"${config_confirm}" == x"y" || x"${config_confirm}" == x"Y" ]]; then 38 | read -p "请设置您的账户名:" config_account 39 | echo -e "${yellow}您的账户名将设定为:${config_account}${plain}" 40 | read -p "请设置您的账户密码:" config_password 41 | echo -e "${yellow}您的账户密码将设定为:${config_password}${plain}" 42 | read -p "请设置面板访问端口:" config_port 43 | echo -e "${yellow}您的面板访问端口将设定为:${config_port}${plain}" 44 | read -p "请设置面板流量监测端口:" config_traffic_port 45 | echo -e "${yellow}您的面板流量监测端口将设定为:${config_traffic_port}${plain}" 46 | echo -e "${yellow}确认设定,设定中${plain}" 47 | ./x-ui setting -username ${config_account} -password ${config_password} 48 | echo -e "${yellow}账户密码设定完成${plain}" 49 | ./x-ui setting -port ${config_port} 50 | echo -e "${yellow}面板访问端口设定完成${plain}" 51 | ./x-ui setting -trafficport ${config_traffic_port} 52 | echo -e "${yellow}面板流量监测端口设定完成${plain}" 53 | else 54 | echo -e "${red}已取消,所有设置项均为默认设置,请及时修改${plain}" 55 | echo -e "如果是全新安装,默认网页端口为 ${green}54321${plain},默认流量监测端口为 ${green}54322${plain},用户名和密码默认都是 ${green}admin${plain}" 56 | echo -e "请自行确保此端口没有被其他程序占用,${yellow}并且确保 54321 和 54322 端口已放行${plain}" 57 | echo -e "若想将 54321 和 54322 修改为其它端口,输入 x-ui 命令进行修改,同样也要确保你修改的端口也是放行的" 58 | fi 59 | } 60 | stop_x-ui() { 61 | # 设置你想要杀死的nohup进程的命令名 62 | xui_com="./x-ui run" 63 | xray_com="bin/xray-$release-$arch -c bin/config.json" 64 | 65 | # 使用pgrep查找进程ID 66 | PID=$(pgrep -f "$xray_com") 67 | 68 | # 检查是否找到了进程 69 | if [ ! -z "$PID" ]; then 70 | # 找到了进程,杀死它 71 | kill $PID 72 | 73 | # 可选:检查进程是否已经被杀死 74 | if kill -0 $PID > /dev/null 2>&1; then 75 | kill -9 $PID 76 | fi 77 | fi 78 | # 使用pgrep查找进程ID 79 | PID=$(pgrep -f "$xui_com") 80 | 81 | # 检查是否找到了进程 82 | if [ ! -z "$PID" ]; then 83 | # 找到了进程,杀死它 84 | kill $PID 85 | 86 | # 可选:检查进程是否已经被杀死 87 | if kill -0 $PID > /dev/null 2>&1; then 88 | kill -9 $PID 89 | fi 90 | fi 91 | 92 | } 93 | 94 | install_x-ui() { 95 | stop_x-ui 96 | 97 | if [ $# == 0 ]; then 98 | last_version=$(curl -Ls "https://api.github.com/repos/amclubs/am-serv00-x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/') 99 | wget -N --no-check-certificate -O x-ui-${release}-${arch}.tar.gz https://github.com/amclubs/am-serv00-x-ui/releases/latest/download/x-ui-${release}-${arch}.tar.gz 100 | if [[ $? -ne 0 ]]; then 101 | echo -e "${red}下载 x-ui 失败,请确保你的服务器能够下载 Github 的文件${plain}" 102 | exit 1 103 | fi 104 | else 105 | last_version=$1 106 | url="https://github.com/vaxilu/x-ui/releases/latest/download/x-ui-${release}-${arch}.tar.gz" 107 | echo -e "开始安装 x-ui v$1" 108 | wget -N --no-check-certificate -O x-ui-${release}-${arch}.tar.gz ${url} 109 | if [[ $? -ne 0 ]]; then 110 | echo -e "${red}下载 x-ui v$1 失败,请确保此版本存在${plain}" 111 | exit 1 112 | fi 113 | fi 114 | 115 | if [[ -e ./x-ui/ ]]; then 116 | rm ./x-ui/ -rf 117 | fi 118 | 119 | tar zxvf x-ui-${release}-${arch}.tar.gz 120 | rm -f x-ui-${release}-${arch}.tar.gz 121 | cd x-ui 122 | chmod +x x-ui bin/xray-${release}-${arch} 123 | #cp -f x-ui.service /etc/systemd/system/ 124 | cp x-ui.sh ../x-ui.sh 125 | chmod +x ../x-ui.sh 126 | chmod +x x-ui.sh 127 | config_after_install 128 | #echo -e "" 129 | #echo -e "如果是更新面板,则按你之前的方式访问面板" 130 | #echo -e "" 131 | crontab -l > x-ui.cron 132 | sed -i "" "/x-ui.log/d" x-ui.cron 133 | echo "0 0 * * * cd $cur_dir/x-ui && cat /dev/null > x-ui.log" >> x-ui.cron 134 | echo "@reboot cd $cur_dir/x-ui && nohup ./x-ui run > ./x-ui.log 2>&1 &" >> x-ui.cron 135 | crontab x-ui.cron 136 | rm x-ui.cron 137 | nohup ./x-ui run > ./x-ui.log 2>&1 & 138 | echo -e "${green}x-ui v${last_version}${plain} 安装完成,面板已启动," 139 | echo -e "" 140 | echo -e "x-ui 管理脚本使用方法: " 141 | echo -e "----------------------------------------------" 142 | echo -e "/home/${USER}/x-ui.sh - 显示管理菜单 (功能更多)" 143 | echo -e "/home/${USER}/x-ui.sh start - 启动 x-ui 面板" 144 | echo -e "/home/${USER}/x-ui.sh stop - 停止 x-ui 面板" 145 | echo -e "/home/${USER}/x-ui.sh restart - 重启 x-ui 面板" 146 | echo -e "/home/${USER}/x-ui.sh status - 查看 x-ui 状态" 147 | echo -e "/home/${USER}/x-ui.sh enable - 设置 x-ui 开机自启" 148 | echo -e "/home/${USER}/x-ui.sh disable - 取消 x-ui 开机自启" 149 | echo -e "/home/${USER}/x-ui.sh update - 更新 x-ui 面板" 150 | echo -e "/home/${USER}/x-ui.sh install - 安装 x-ui 面板" 151 | echo -e "/home/${USER}/x-ui.sh uninstall - 卸载 x-ui 面板" 152 | echo -e "----------------------------------------------" 153 | } 154 | 155 | echo -e "${green}开始安装${plain}" 156 | #install_base 157 | install_x-ui $1 158 | -------------------------------------------------------------------------------- /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 | path, err := settingService.GetBasePath() 103 | if err != nil { 104 | fmt.Println("get current path fialed,error info:", err) 105 | } 106 | userService := service.UserService{} 107 | userModel, err := userService.GetFirstUser() 108 | if err != nil { 109 | fmt.Println("get current user info failed,error info:", err) 110 | } 111 | username := userModel.Username 112 | userpasswd := userModel.Password 113 | if (username == "") || (userpasswd == "") { 114 | fmt.Println("current username or password is empty") 115 | } 116 | fmt.Println("current pannel settings as follows:") 117 | fmt.Println("username:", username) 118 | fmt.Println("userpasswd:", userpasswd) 119 | fmt.Println("port:", port) 120 | fmt.Println("path:", path) 121 | } 122 | } 123 | 124 | func updateTgbotEnableSts(status bool) { 125 | settingService := service.SettingService{} 126 | currentTgSts, err := settingService.GetTgbotenabled() 127 | if err != nil { 128 | fmt.Println(err) 129 | return 130 | } 131 | logger.Infof("current enabletgbot status[%v],need update to status[%v]", currentTgSts, status) 132 | if currentTgSts != status { 133 | err := settingService.SetTgbotenabled(status) 134 | if err != nil { 135 | fmt.Println(err) 136 | return 137 | } else { 138 | logger.Infof("SetTgbotenabled[%v] success", status) 139 | } 140 | } 141 | return 142 | } 143 | 144 | func updateTgbotSetting(tgBotToken string, tgBotChatid int, tgBotRuntime string) { 145 | err := database.InitDB(config.GetDBPath()) 146 | if err != nil { 147 | fmt.Println(err) 148 | return 149 | } 150 | 151 | settingService := service.SettingService{} 152 | 153 | if tgBotToken != "" { 154 | err := settingService.SetTgBotToken(tgBotToken) 155 | if err != nil { 156 | fmt.Println(err) 157 | return 158 | } else { 159 | logger.Info("updateTgbotSetting tgBotToken success") 160 | } 161 | } 162 | 163 | if tgBotRuntime != "" { 164 | err := settingService.SetTgbotRuntime(tgBotRuntime) 165 | if err != nil { 166 | fmt.Println(err) 167 | return 168 | } else { 169 | logger.Infof("updateTgbotSetting tgBotRuntime[%s] success", tgBotRuntime) 170 | } 171 | } 172 | 173 | if tgBotChatid != 0 { 174 | err := settingService.SetTgBotChatId(tgBotChatid) 175 | if err != nil { 176 | fmt.Println(err) 177 | return 178 | } else { 179 | logger.Info("updateTgbotSetting tgBotChatid success") 180 | } 181 | } 182 | } 183 | 184 | func updateSetting(port int, trafficport int, username string, password string) { 185 | err := database.InitDB(config.GetDBPath()) 186 | if err != nil { 187 | fmt.Println(err) 188 | return 189 | } 190 | 191 | settingService := service.SettingService{} 192 | 193 | if port > 0 { 194 | err := settingService.SetPort(port) 195 | if err != nil { 196 | fmt.Println("set port failed:", err) 197 | } else { 198 | fmt.Printf("set port %v success", port) 199 | } 200 | } 201 | if trafficport > 0 { 202 | err := settingService.SetTrafficPort(trafficport) 203 | if err != nil { 204 | fmt.Println("set traffic port failed:", err) 205 | } else { 206 | fmt.Printf("set traffic port %v success", port) 207 | } 208 | } 209 | if username != "" || password != "" { 210 | userService := service.UserService{} 211 | err := userService.UpdateFirstUser(username, password) 212 | if err != nil { 213 | fmt.Println("set username and password failed:", err) 214 | } else { 215 | fmt.Println("set username and password success") 216 | } 217 | } 218 | } 219 | 220 | func main() { 221 | if len(os.Args) < 2 { 222 | runWebServer() 223 | return 224 | } 225 | 226 | var showVersion bool 227 | flag.BoolVar(&showVersion, "v", false, "show version") 228 | 229 | runCmd := flag.NewFlagSet("run", flag.ExitOnError) 230 | 231 | v2uiCmd := flag.NewFlagSet("v2-ui", flag.ExitOnError) 232 | var dbPath string 233 | v2uiCmd.StringVar(&dbPath, "db", "/etc/v2-ui/v2-ui.db", "set v2-ui db file path") 234 | 235 | settingCmd := flag.NewFlagSet("setting", flag.ExitOnError) 236 | var port int 237 | var trafficport int 238 | var username string 239 | var password string 240 | var tgbottoken string 241 | var tgbotchatid int 242 | var enabletgbot bool 243 | var tgbotRuntime string 244 | var reset bool 245 | var show bool 246 | settingCmd.BoolVar(&reset, "reset", false, "reset all settings") 247 | settingCmd.BoolVar(&show, "show", false, "show current settings") 248 | settingCmd.IntVar(&port, "port", 0, "set panel port") 249 | settingCmd.IntVar(&trafficport, "trafficport", 0, "set traffic stats collection port") 250 | settingCmd.StringVar(&username, "username", "", "set login username") 251 | settingCmd.StringVar(&password, "password", "", "set login password") 252 | settingCmd.StringVar(&tgbottoken, "tgbottoken", "", "set telegrame bot token") 253 | settingCmd.StringVar(&tgbotRuntime, "tgbotRuntime", "", "set telegrame bot cron time") 254 | settingCmd.IntVar(&tgbotchatid, "tgbotchatid", 0, "set telegrame bot chat id") 255 | settingCmd.BoolVar(&enabletgbot, "enabletgbot", false, "enable telegram bot notify") 256 | 257 | oldUsage := flag.Usage 258 | flag.Usage = func() { 259 | oldUsage() 260 | fmt.Println() 261 | fmt.Println("Commands:") 262 | fmt.Println(" run run web panel") 263 | fmt.Println(" v2-ui migrate form v2-ui") 264 | fmt.Println(" setting set settings") 265 | } 266 | 267 | flag.Parse() 268 | if showVersion { 269 | fmt.Println(config.GetVersion()) 270 | return 271 | } 272 | 273 | switch os.Args[1] { 274 | case "run": 275 | err := runCmd.Parse(os.Args[2:]) 276 | if err != nil { 277 | fmt.Println(err) 278 | return 279 | } 280 | runWebServer() 281 | case "v2-ui": 282 | err := v2uiCmd.Parse(os.Args[2:]) 283 | if err != nil { 284 | fmt.Println(err) 285 | return 286 | } 287 | err = v2ui.MigrateFromV2UI(dbPath) 288 | if err != nil { 289 | fmt.Println("migrate from v2-ui failed:", err) 290 | } 291 | case "setting": 292 | err := settingCmd.Parse(os.Args[2:]) 293 | if err != nil { 294 | fmt.Println(err) 295 | return 296 | } 297 | if reset { 298 | resetSetting() 299 | } else { 300 | updateSetting(port, trafficport, username, password) 301 | } 302 | if show { 303 | showSetting(show) 304 | } 305 | if (tgbottoken != "") || (tgbotchatid != 0) || (tgbotRuntime != "") { 306 | updateTgbotSetting(tgbottoken, tgbotchatid, tgbotRuntime) 307 | } 308 | default: 309 | fmt.Println("except 'run' or 'v2-ui' or 'setting' subcommands") 310 | fmt.Println() 311 | runCmd.Usage() 312 | fmt.Println() 313 | v2uiCmd.Usage() 314 | fmt.Println() 315 | settingCmd.Usage() 316 | } 317 | } 318 | -------------------------------------------------------------------------------- /media/2022-04-04_141259.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-serv00-x-ui/7a8fd8c1e2b4546149c26948f749609b6ea42f80/media/2022-04-04_141259.png -------------------------------------------------------------------------------- /media/2022-04-17_110907.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-serv00-x-ui/7a8fd8c1e2b4546149c26948f749609b6ea42f80/media/2022-04-17_110907.png -------------------------------------------------------------------------------- /media/2022-04-17_111321.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-serv00-x-ui/7a8fd8c1e2b4546149c26948f749609b6ea42f80/media/2022-04-17_111321.png -------------------------------------------------------------------------------- /media/2022-04-17_111705.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-serv00-x-ui/7a8fd8c1e2b4546149c26948f749609b6ea42f80/media/2022-04-17_111705.png -------------------------------------------------------------------------------- /media/2022-04-17_111910.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-serv00-x-ui/7a8fd8c1e2b4546149c26948f749609b6ea42f80/media/2022-04-17_111910.png -------------------------------------------------------------------------------- /media/bda84fbc2ede834deaba1c173a932223.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-serv00-x-ui/7a8fd8c1e2b4546149c26948f749609b6ea42f80/media/bda84fbc2ede834deaba1c173a932223.png -------------------------------------------------------------------------------- /media/d13ffd6a73f938d1037d0708e31433bf.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/amclubs/am-serv00-x-ui/7a8fd8c1e2b4546149c26948f749609b6ea42f80/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/amclubs/am-serv00-x-ui/7a8fd8c1e2b4546149c26948f749609b6ea42f80/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_freebsd.go: -------------------------------------------------------------------------------- 1 | // +build freebsd 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>>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;n0&&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;o0&&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>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 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 15 | {{.title}} 16 | 17 | {{end}} -------------------------------------------------------------------------------- /web/html/common/js.html: -------------------------------------------------------------------------------- 1 | {{define "js"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 21 | {{end}} -------------------------------------------------------------------------------- /web/html/common/prompt_modal.html: -------------------------------------------------------------------------------- 1 | {{define "promptModal"}} 2 | 5 | 10 | 11 | 12 | 67 | {{end}} -------------------------------------------------------------------------------- /web/html/common/qrcode_modal.html: -------------------------------------------------------------------------------- 1 | {{define "qrcodeModal"}} 2 | 5 | 6 | 7 | 8 | 59 | {{end}} -------------------------------------------------------------------------------- /web/html/common/text_modal.html: -------------------------------------------------------------------------------- 1 | {{define "textModal"}} 2 | 5 | 7 | {{ i18n "download" }} [[ txtModal.fileName ]] 8 | 9 | 11 | 12 | 13 | 58 | {{end}} -------------------------------------------------------------------------------- /web/html/login.html: -------------------------------------------------------------------------------- 1 | 2 | 3 | {{template "head" .}} 4 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |

{{ .title }}

37 |
38 |
39 | 40 | 41 | 42 | 43 | 45 | 46 | 47 | 48 | 49 | 51 | 52 | 53 | 54 | 55 | {{ i18n "login" }} 56 | 57 | 58 | 59 | 60 |
61 |
62 |
63 | {{template "js" .}} 64 | 89 | 90 | -------------------------------------------------------------------------------- /web/html/xui/common_sider.html: -------------------------------------------------------------------------------- 1 | {{define "menuItems"}} 2 | 3 | 4 | 系统状态 5 | 6 | 7 | 8 | 入站列表 9 | 10 | 11 | 12 | 面板设置 13 | 14 | 15 | 16 | 17 | 18 | 19 | 23 | 24 | 25 | Github 26 | 27 | 28 | 29 | 30 | 退出登录 31 | 32 | {{end}} 33 | 34 | 35 | {{define "commonSider"}} 36 | 37 | 39 | {{template "menuItems" .}} 40 | 41 | 42 | 45 |
46 | 47 |
48 | 50 | {{template "menuItems" .}} 51 | 52 |
53 | 69 | {{end}} 70 | -------------------------------------------------------------------------------- /web/html/xui/component/inbound_info.html: -------------------------------------------------------------------------------- 1 | {{define "inboundInfoStream"}} 2 |

传输: [[ inbound.network ]]

3 | 4 | 11 | 12 | 17 | 18 | 22 | 23 | 26 | 27 | 31 | 34 |

35 | tls域名: [[ inbound.serverName ? inbound.serverName : "无" ]] 36 |

37 |

38 | xtls域名: [[ inbound.serverName ? inbound.serverName : "无" ]] 39 |

40 | {{end}} 41 | 42 | 43 | {{define "component/inboundInfoComponent"}} 44 |
45 |

协议: [[ dbInbound.protocol ]]

46 |

地址: [[ dbInbound.address ]]

47 |

端口: [[ dbInbound.port ]]

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