├── .github ├── ISSUE_TEMPLATE │ ├── bug_report.md │ ├── feature_request.md │ └── question-template.md ├── dependabot.yml └── workflows │ ├── docker.yml │ └── release.yml ├── .gitignore ├── DockerInitFiles.sh ├── Dockerfile ├── LICENSE ├── README.md ├── config ├── config.go ├── name └── version ├── database ├── db.go └── model │ └── model.go ├── docker-compose.yml ├── go.mod ├── go.sum ├── install.sh ├── logger └── logger.go ├── main.go ├── media ├── inbounds-dark.png ├── inbounds.png ├── outbounds.png ├── rules.png └── warp.png ├── sub ├── default.json ├── sub.go ├── subController.go ├── subJsonService.go └── subService.go ├── util ├── common │ ├── err.go │ ├── format.go │ └── multi_error.go ├── json_util │ └── json.go ├── random │ └── random.go ├── reflect_util │ └── reflect.go └── sys │ ├── psutil.go │ ├── sys_darwin.go │ ├── sys_linux.go │ └── sys_windows.go ├── web ├── assets │ ├── Vazirmatn-UI-NL-Regular.woff2 │ ├── ant-design-vue@1.7.8 │ │ ├── antd-with-locales.min.js │ │ ├── antd.less │ │ ├── antd.min.css │ │ ├── antd.min.js │ │ └── antd.min.js.map │ ├── axios │ │ └── axios.min.js │ ├── base64 │ │ └── base64.min.js │ ├── clipboard │ │ └── clipboard.min.js │ ├── codemirror │ │ ├── codemirror.js │ │ ├── codemirror.min.css │ │ ├── fold │ │ │ ├── brace-fold.js │ │ │ ├── foldcode.js │ │ │ ├── foldgutter.css │ │ │ └── foldgutter.js │ │ ├── hint │ │ │ └── javascript-hint.js │ │ ├── javascript.js │ │ ├── jshint.js │ │ ├── jsonlint.js │ │ ├── lint │ │ │ ├── javascript-lint.js │ │ │ ├── lint.css │ │ │ └── lint.js │ │ └── xq.min.css │ ├── css │ │ └── custom.css │ ├── element-ui@2.15.0 │ │ └── theme-chalk │ │ │ └── display.css │ ├── js │ │ ├── axios-init.js │ │ ├── langs.js │ │ ├── model │ │ │ ├── dbinbound.js │ │ │ ├── outbound.js │ │ │ ├── setting.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 │ │ ├── vue.common.dev.js │ │ ├── vue.common.js │ │ ├── vue.common.prod.js │ │ ├── vue.esm.browser.js │ │ ├── vue.esm.browser.min.js │ │ ├── vue.esm.js │ │ ├── vue.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 │ │ └── vue.runtime.mjs ├── controller │ ├── api.go │ ├── base.go │ ├── inbound.go │ ├── index.go │ ├── server.go │ ├── setting.go │ ├── util.go │ ├── xray_setting.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 │ │ ├── client_bulk_modal.html │ │ ├── client_modal.html │ │ ├── common_sider.html │ │ ├── component │ │ ├── password.html │ │ ├── setting.html │ │ └── themeSwitch.html │ │ ├── dns_modal.html │ │ ├── fakedns_modal.html │ │ ├── form │ │ ├── client.html │ │ ├── inbound.html │ │ ├── outbound.html │ │ ├── protocol │ │ │ ├── dokodemo.html │ │ │ ├── http.html │ │ │ ├── shadowsocks.html │ │ │ ├── socks.html │ │ │ ├── trojan.html │ │ │ ├── vless.html │ │ │ ├── vmess.html │ │ │ └── wireguard.html │ │ ├── sniffing.html │ │ ├── stream │ │ │ ├── external_proxy.html │ │ │ ├── stream_grpc.html │ │ │ ├── stream_http.html │ │ │ ├── stream_httpupgrade.html │ │ │ ├── stream_kcp.html │ │ │ ├── stream_quic.html │ │ │ ├── stream_settings.html │ │ │ ├── stream_sockopt.html │ │ │ ├── stream_splithttp.html │ │ │ ├── stream_tcp.html │ │ │ └── stream_ws.html │ │ └── tls_settings.html │ │ ├── inbound_client_table.html │ │ ├── inbound_info_modal.html │ │ ├── inbound_modal.html │ │ ├── inbounds.html │ │ ├── index.html │ │ ├── settings.html │ │ ├── warp_modal.html │ │ ├── xray.html │ │ ├── xray_balancer_modal.html │ │ ├── xray_outbound_modal.html │ │ ├── xray_reverse_modal.html │ │ └── xray_rule_modal.html ├── job │ ├── check_cpu_usage.go │ ├── check_xray_running_job.go │ ├── stats_notify_job.go │ └── xray_traffic_job.go ├── locale │ └── locale.go ├── middleware │ └── domainValidator.go ├── network │ ├── auto_https_conn.go │ └── auto_https_listener.go ├── service │ ├── config.json │ ├── inbound.go │ ├── panel.go │ ├── server.go │ ├── setting.go │ ├── tgbot.go │ ├── user.go │ ├── xray.go │ └── xray_setting.go ├── session │ └── session.go ├── translation │ ├── translate.en_US.toml │ ├── translate.fa_IR.toml │ ├── translate.ru_RU.toml │ ├── translate.vi_VN.toml │ └── translate.zh_Hans.toml └── web.go ├── x-ui.service ├── x-ui.sh └── xray ├── api.go ├── client_traffic.go ├── config.go ├── inbound.go ├── log_writer.go ├── process.go └── traffic.go /.github/ISSUE_TEMPLATE/bug_report.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Bug report 3 | about: Create a report to help us improve 4 | title: '' 5 | labels: bug 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Describe the bug** 11 | A clear and concise description of what the bug is. 12 | 13 | **To Reproduce** 14 | Steps to reproduce the behavior: 15 | 1. Go to '...' 16 | 2. Click on '....' 17 | 3. Scroll down to '....' 18 | 4. See error 19 | 20 | **Expected behavior** 21 | A clear and concise description of what you expected to happen. 22 | 23 | **Screenshots** 24 | If applicable, add screenshots to help explain your problem. 25 | 26 | **Desktop (please complete the following information):** 27 | - OS: [e.g. iOS] 28 | - Browser [e.g. chrome, safari] 29 | - Version [e.g. 22] 30 | 31 | **Smartphone (please complete the following information):** 32 | - Device: [e.g. iPhone6] 33 | - OS: [e.g. iOS8.1] 34 | - Browser [e.g. stock browser, safari] 35 | - Version [e.g. 22] 36 | 37 | **Additional context** 38 | Add any other context about the problem here. 39 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/feature_request.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Feature request 3 | about: Suggest an idea for this project 4 | title: '' 5 | labels: enhancement 6 | assignees: '' 7 | 8 | --- 9 | 10 | **Is your feature request related to a problem? Please describe.** 11 | A clear and concise description of what the problem is. Ex. I'm always frustrated when [...] 12 | 13 | **Describe the solution you'd like** 14 | A clear and concise description of what you want to happen. 15 | 16 | **Describe alternatives you've considered** 17 | A clear and concise description of any alternative solutions or features you've considered. 18 | 19 | **Additional context** 20 | Add any other context or screenshots about the feature request here. 21 | -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE/question-template.md: -------------------------------------------------------------------------------- 1 | --- 2 | name: Question template 3 | about: Ask if it is not clear that it is a bug 4 | title: '' 5 | labels: question 6 | assignees: '' 7 | 8 | --- 9 | 10 | 11 | -------------------------------------------------------------------------------- /.github/dependabot.yml: -------------------------------------------------------------------------------- 1 | version: 2 2 | updates: 3 | - package-ecosystem: "gomod" 4 | directory: "/" 5 | schedule: 6 | interval: "daily" 7 | - package-ecosystem: "github-actions" 8 | directory: "/" 9 | schedule: 10 | interval: "daily" -------------------------------------------------------------------------------- /.github/workflows/docker.yml: -------------------------------------------------------------------------------- 1 | name: Docker Image CI 2 | on: 3 | push: 4 | tags: 5 | - "*" 6 | workflow_dispatch: 7 | 8 | jobs: 9 | build: 10 | runs-on: ubuntu-latest 11 | 12 | steps: 13 | - uses: actions/checkout@v4 14 | with: 15 | submodules: true 16 | 17 | - name: Docker meta 18 | id: meta 19 | uses: docker/metadata-action@v5 20 | with: 21 | images: | 22 | alireza7/x-ui 23 | ghcr.io/alireza0/x-ui 24 | tags: | 25 | type=ref,event=branch 26 | type=ref,event=tag 27 | type=pep440,pattern={{version}} 28 | 29 | - name: Set up QEMU 30 | uses: docker/setup-qemu-action@v3 31 | 32 | - name: Set up Docker Buildx 33 | uses: docker/setup-buildx-action@v3 34 | 35 | - name: Login to Docker Hub 36 | uses: docker/login-action@v3 37 | with: 38 | username: ${{ secrets.DOCKER_HUB_USERNAME }} 39 | password: ${{ secrets.DOCKER_HUB_TOKEN }} 40 | 41 | - name: Login to GHCR 42 | uses: docker/login-action@v3 43 | with: 44 | registry: ghcr.io 45 | username: ${{ github.repository_owner }} 46 | password: ${{ secrets.GITHUB_TOKEN }} 47 | 48 | - name: Build and push 49 | uses: docker/build-push-action@v6 50 | with: 51 | context: . 52 | push: true 53 | platforms: linux/amd64,linux/arm64/v8, linux/arm/v7, linux/386 54 | tags: ${{ steps.meta.outputs.tags }} 55 | labels: ${{ steps.meta.outputs.labels }} 56 | -------------------------------------------------------------------------------- /.github/workflows/release.yml: -------------------------------------------------------------------------------- 1 | name: Release X-UI 2 | 3 | on: 4 | push: 5 | tags: 6 | - "*" 7 | workflow_dispatch: 8 | 9 | jobs: 10 | build: 11 | strategy: 12 | matrix: 13 | platform: 14 | - amd64 15 | - arm64 16 | - armv7 17 | - 386 18 | runs-on: ubuntu-20.04 19 | steps: 20 | - name: Checkout repository 21 | uses: actions/checkout@v4 22 | 23 | - name: Setup Go 24 | uses: actions/setup-go@v5 25 | with: 26 | go-version: '1.22' 27 | 28 | - name: Install dependencies 29 | run: | 30 | sudo apt-get update 31 | if [ "${{ matrix.platform }}" == "arm64" ]; then 32 | sudo apt install gcc-aarch64-linux-gnu 33 | elif [ "${{ matrix.platform }}" == "armv7" ]; then 34 | sudo apt install gcc-arm-linux-gnueabihf 35 | elif [ "${{ matrix.platform }}" == "386" ]; then 36 | sudo apt install gcc-i686-linux-gnu 37 | fi 38 | 39 | - name: Build x-ui 40 | run: | 41 | export CGO_ENABLED=1 42 | export GOOS=linux 43 | export GOARCH=${{ matrix.platform }} 44 | if [ "${{ matrix.platform }}" == "arm64" ]; then 45 | export GOARCH=arm64 46 | export CC=aarch64-linux-gnu-gcc 47 | elif [ "${{ matrix.platform }}" == "armv7" ]; then 48 | export GOARCH=arm 49 | export GOARM=7 50 | export CC=arm-linux-gnueabihf-gcc 51 | elif [ "${{ matrix.platform }}" == "386" ]; then 52 | export GOARCH=386 53 | export CC=i686-linux-gnu-gcc 54 | fi 55 | go build -o xui-release -v main.go 56 | 57 | mkdir x-ui 58 | cp xui-release x-ui/ 59 | cp x-ui.service x-ui/ 60 | cp x-ui.sh x-ui/ 61 | mv x-ui/xui-release x-ui/x-ui 62 | mkdir x-ui/bin 63 | cd x-ui/bin 64 | 65 | # Download dependencies 66 | Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v1.8.15/" 67 | if [ "${{ matrix.platform }}" == "amd64" ]; then 68 | wget ${Xray_URL}Xray-linux-64.zip 69 | unzip Xray-linux-64.zip 70 | rm -f Xray-linux-64.zip 71 | elif [ "${{ matrix.platform }}" == "arm64" ]; then 72 | wget ${Xray_URL}Xray-linux-arm64-v8a.zip 73 | unzip Xray-linux-arm64-v8a.zip 74 | rm -f Xray-linux-arm64-v8a.zip 75 | elif [ "${{ matrix.platform }}" == "armv7" ]; then 76 | wget ${Xray_URL}Xray-linux-arm32-v7a.zip 77 | unzip Xray-linux-arm32-v7a.zip 78 | rm -f Xray-linux-arm32-v7a.zip 79 | elif [ "${{ matrix.platform }}" == "386" ]; then 80 | wget ${Xray_URL}Xray-linux-32.zip 81 | unzip Xray-linux-32.zip 82 | rm -f Xray-linux-32.zip 83 | fi 84 | rm -f geoip.dat geosite.dat 85 | wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat 86 | wget https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat 87 | wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat 88 | wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat 89 | mv xray xray-linux-${{ matrix.platform }} 90 | cd ../.. 91 | 92 | - name: Package 93 | run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui 94 | 95 | - name: Upload files to GH release 96 | uses: svenstaro/upload-release-action@v2 97 | with: 98 | repo_token: ${{ secrets.GITHUB_TOKEN }} 99 | tag: ${{ github.ref }} 100 | file: x-ui-linux-${{ matrix.platform }}.tar.gz 101 | asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz 102 | prerelease: true 103 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .idea 2 | .vscode 3 | .cache 4 | .sync* 5 | *.tar.gz 6 | *.log 7 | access.log 8 | error.log 9 | tmp 10 | main 11 | backup/ 12 | bin/ 13 | dist/ 14 | release/ 15 | /release.sh 16 | /x-ui 17 | .DS_Store -------------------------------------------------------------------------------- /DockerInitFiles.sh: -------------------------------------------------------------------------------- 1 | #!/bin/sh 2 | case $1 in 3 | amd64) 4 | ARCH="64" 5 | FNAME="amd64" 6 | ;; 7 | i386) 8 | ARCH="32" 9 | FNAME="i386" 10 | ;; 11 | armv8 | arm64 | aarch64) 12 | ARCH="arm64-v8a" 13 | FNAME="arm64" 14 | ;; 15 | armv7 | arm | arm32) 16 | ARCH="arm32-v7a" 17 | FNAME="arm32" 18 | ;; 19 | *) 20 | ARCH="64" 21 | FNAME="amd64" 22 | ;; 23 | esac 24 | mkdir -p build/bin 25 | cd build/bin 26 | wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.15/Xray-linux-${ARCH}.zip" 27 | unzip "Xray-linux-${ARCH}.zip" 28 | rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat LICENSE README.md 29 | mv xray "xray-linux-${FNAME}" 30 | wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" 31 | wget "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" 32 | wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat 33 | wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat 34 | cd ../../ 35 | -------------------------------------------------------------------------------- /Dockerfile: -------------------------------------------------------------------------------- 1 | FROM golang:1.22-alpine AS builder 2 | WORKDIR /app 3 | ARG TARGETARCH 4 | RUN apk --no-cache --update add build-base gcc wget unzip 5 | COPY . . 6 | ENV CGO_ENABLED=1 7 | ENV CGO_CFLAGS="-D_LARGEFILE64_SOURCE" 8 | RUN go build -o build/x-ui main.go 9 | RUN ./DockerInitFiles.sh "$TARGETARCH" 10 | 11 | FROM alpine 12 | LABEL org.opencontainers.image.authors="alireza7@gmail.com" 13 | ENV TZ=Asia/Tehran 14 | WORKDIR /app 15 | 16 | RUN apk add ca-certificates tzdata 17 | 18 | COPY --from=builder /app/build/ /app/ 19 | VOLUME [ "/etc/x-ui" ] 20 | CMD [ "./x-ui" ] 21 | -------------------------------------------------------------------------------- /config/config.go: -------------------------------------------------------------------------------- 1 | package config 2 | 3 | import ( 4 | _ "embed" 5 | "fmt" 6 | "os" 7 | "strings" 8 | ) 9 | 10 | //go:embed version 11 | var version string 12 | 13 | //go:embed name 14 | var name string 15 | 16 | type LogLevel string 17 | 18 | const ( 19 | Debug LogLevel = "debug" 20 | Info LogLevel = "info" 21 | Warn LogLevel = "warn" 22 | Error LogLevel = "error" 23 | ) 24 | 25 | func GetVersion() string { 26 | return strings.TrimSpace(version) 27 | } 28 | 29 | func GetName() string { 30 | return strings.TrimSpace(name) 31 | } 32 | 33 | func GetLogLevel() LogLevel { 34 | if IsDebug() { 35 | return Debug 36 | } 37 | logLevel := os.Getenv("XUI_LOG_LEVEL") 38 | if logLevel == "" { 39 | return Info 40 | } 41 | return LogLevel(logLevel) 42 | } 43 | 44 | func IsDebug() bool { 45 | return os.Getenv("XUI_DEBUG") == "true" 46 | } 47 | 48 | func GetBinFolderPath() string { 49 | binFolderPath := os.Getenv("XUI_BIN_FOLDER") 50 | if binFolderPath == "" { 51 | binFolderPath = "bin" 52 | } 53 | return binFolderPath 54 | } 55 | 56 | func GetDBFolderPath() string { 57 | dbFolderPath := os.Getenv("XUI_DB_FOLDER") 58 | if dbFolderPath == "" { 59 | dbFolderPath = "/etc/x-ui" 60 | } 61 | return dbFolderPath 62 | } 63 | 64 | func GetDBPath() string { 65 | return fmt.Sprintf("%s/%s.db", GetDBFolderPath(), GetName()) 66 | } 67 | -------------------------------------------------------------------------------- /config/name: -------------------------------------------------------------------------------- 1 | x-ui -------------------------------------------------------------------------------- /config/version: -------------------------------------------------------------------------------- 1 | 1.8.4 -------------------------------------------------------------------------------- /database/db.go: -------------------------------------------------------------------------------- 1 | package database 2 | 3 | import ( 4 | "bytes" 5 | "io" 6 | "io/fs" 7 | "os" 8 | "path" 9 | 10 | "x-ui/config" 11 | "x-ui/database/model" 12 | "x-ui/xray" 13 | 14 | "gorm.io/driver/sqlite" 15 | "gorm.io/gorm" 16 | "gorm.io/gorm/logger" 17 | ) 18 | 19 | var db *gorm.DB 20 | 21 | func initUser() error { 22 | err := db.AutoMigrate(&model.User{}) 23 | if err != nil { 24 | return err 25 | } 26 | var count int64 27 | err = db.Model(&model.User{}).Count(&count).Error 28 | if err != nil { 29 | return err 30 | } 31 | if count == 0 { 32 | user := &model.User{ 33 | Username: "admin", 34 | Password: "admin", 35 | } 36 | return db.Create(user).Error 37 | } 38 | return nil 39 | } 40 | 41 | func initInbound() error { 42 | return db.AutoMigrate(&model.Inbound{}) 43 | } 44 | 45 | func initSetting() error { 46 | return db.AutoMigrate(&model.Setting{}) 47 | } 48 | 49 | func initClientTraffic() error { 50 | return db.AutoMigrate(&xray.ClientTraffic{}) 51 | } 52 | 53 | func InitDB(dbPath string) error { 54 | dir := path.Dir(dbPath) 55 | err := os.MkdirAll(dir, fs.ModeDir) 56 | if err != nil { 57 | return err 58 | } 59 | 60 | var gormLogger logger.Interface 61 | 62 | if config.IsDebug() { 63 | gormLogger = logger.Default 64 | } else { 65 | gormLogger = logger.Discard 66 | } 67 | 68 | c := &gorm.Config{ 69 | Logger: gormLogger, 70 | } 71 | db, err = gorm.Open(sqlite.Open(dbPath), c) 72 | if err != nil { 73 | return err 74 | } 75 | 76 | err = initUser() 77 | if err != nil { 78 | return err 79 | } 80 | err = initInbound() 81 | if err != nil { 82 | return err 83 | } 84 | err = initSetting() 85 | if err != nil { 86 | return err 87 | } 88 | 89 | err = initClientTraffic() 90 | if err != nil { 91 | return err 92 | } 93 | 94 | return nil 95 | } 96 | 97 | func GetDB() *gorm.DB { 98 | return db 99 | } 100 | 101 | func IsNotFound(err error) bool { 102 | return err == gorm.ErrRecordNotFound 103 | } 104 | 105 | func IsSQLiteDB(file io.Reader) (bool, error) { 106 | signature := []byte("SQLite format 3\x00") 107 | buf := make([]byte, len(signature)) 108 | _, err := file.Read(buf) 109 | if err != nil { 110 | return false, err 111 | } 112 | return bytes.Equal(buf, signature), nil 113 | } 114 | 115 | func Checkpoint() error { 116 | // Update WAL 117 | err := db.Exec("PRAGMA wal_checkpoint;").Error 118 | if err != nil { 119 | return err 120 | } 121 | return nil 122 | } 123 | -------------------------------------------------------------------------------- /database/model/model.go: -------------------------------------------------------------------------------- 1 | package model 2 | 3 | import ( 4 | "fmt" 5 | 6 | "x-ui/util/json_util" 7 | "x-ui/xray" 8 | ) 9 | 10 | type Protocol string 11 | 12 | const ( 13 | VMess Protocol = "vmess" 14 | VLESS Protocol = "vless" 15 | Dokodemo Protocol = "Dokodemo-door" 16 | Http Protocol = "http" 17 | Trojan Protocol = "trojan" 18 | Shadowsocks Protocol = "shadowsocks" 19 | ) 20 | 21 | type User struct { 22 | Id int `json:"id" gorm:"primaryKey;autoIncrement"` 23 | Username string `json:"username"` 24 | Password string `json:"password"` 25 | } 26 | 27 | type Inbound struct { 28 | Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` 29 | UserId int `json:"-"` 30 | Up int64 `json:"up" form:"up"` 31 | Down int64 `json:"down" form:"down"` 32 | Total int64 `json:"total" form:"total"` 33 | Remark string `json:"remark" form:"remark"` 34 | Enable bool `json:"enable" form:"enable"` 35 | ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` 36 | ClientStats []xray.ClientTraffic `gorm:"foreignKey:InboundId;references:Id" json:"clientStats" form:"clientStats"` 37 | 38 | // config part 39 | Listen string `json:"listen" form:"listen"` 40 | Port int `json:"port" form:"port"` 41 | Protocol Protocol `json:"protocol" form:"protocol"` 42 | Settings string `json:"settings" form:"settings"` 43 | StreamSettings string `json:"streamSettings" form:"streamSettings"` 44 | Tag string `json:"tag" form:"tag" gorm:"unique"` 45 | Sniffing string `json:"sniffing" form:"sniffing"` 46 | } 47 | 48 | func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig { 49 | listen := i.Listen 50 | if listen != "" { 51 | listen = fmt.Sprintf("\"%v\"", listen) 52 | } 53 | return &xray.InboundConfig{ 54 | Listen: json_util.RawMessage(listen), 55 | Port: i.Port, 56 | Protocol: string(i.Protocol), 57 | Settings: json_util.RawMessage(i.Settings), 58 | StreamSettings: json_util.RawMessage(i.StreamSettings), 59 | Tag: i.Tag, 60 | Sniffing: json_util.RawMessage(i.Sniffing), 61 | } 62 | } 63 | 64 | type Setting struct { 65 | Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` 66 | Key string `json:"key" form:"key"` 67 | Value string `json:"value" form:"value"` 68 | } 69 | 70 | type Client struct { 71 | ID string `json:"id"` 72 | Password string `json:"password"` 73 | Flow string `json:"flow"` 74 | Email string `json:"email"` 75 | TotalGB int64 `json:"totalGB" form:"totalGB"` 76 | ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` 77 | Enable bool `json:"enable" form:"enable"` 78 | TgID string `json:"tgId" form:"tgId"` 79 | SubID string `json:"subId" form:"subId"` 80 | Reset int `json:"reset" form:"reset"` 81 | } 82 | -------------------------------------------------------------------------------- /docker-compose.yml: -------------------------------------------------------------------------------- 1 | --- 2 | version: "3" 3 | 4 | services: 5 | xui: 6 | image: alireza7/x-ui 7 | container_name: x-ui 8 | hostname: yourhostname 9 | volumes: 10 | - $PWD/db/:/etc/x-ui/ 11 | - $PWD/cert/:/root/cert/ 12 | environment: 13 | XRAY_VMESS_AEAD_FORCED: "false" 14 | tty: true 15 | network_mode: host 16 | restart: unless-stopped 17 | -------------------------------------------------------------------------------- /go.mod: -------------------------------------------------------------------------------- 1 | module x-ui 2 | 3 | go 1.22.0 4 | 5 | require ( 6 | github.com/Calidity/gin-sessions v1.3.1 7 | github.com/gin-gonic/gin v1.10.0 8 | github.com/go-telegram-bot-api/telegram-bot-api/v5 v5.5.1 9 | github.com/goccy/go-json v0.10.3 10 | github.com/nicksnyder/go-i18n/v2 v2.4.0 11 | github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 12 | github.com/pelletier/go-toml/v2 v2.2.2 13 | github.com/robfig/cron/v3 v3.0.1 14 | github.com/xtls/xray-core v1.8.15 15 | go.uber.org/atomic v1.11.0 16 | golang.org/x/text v0.16.0 17 | google.golang.org/grpc v1.64.0 18 | gorm.io/driver/sqlite v1.5.6 19 | gorm.io/gorm v1.25.10 20 | ) 21 | 22 | require ( 23 | github.com/bytedance/sonic/loader v0.1.1 // indirect 24 | github.com/cloudwego/base64x v0.1.4 // indirect 25 | github.com/cloudwego/iasm v0.2.0 // indirect 26 | github.com/go-task/slim-sprig/v3 v3.0.0 // indirect 27 | ) 28 | 29 | require ( 30 | github.com/andybalholm/brotli v1.1.0 // indirect 31 | github.com/bytedance/sonic v1.11.6 // indirect 32 | github.com/cloudflare/circl v1.3.9 // indirect 33 | github.com/dgryski/go-metro v0.0.0-20211217172704-adc40b04c140 // indirect 34 | github.com/francoispqt/gojay v1.2.13 // indirect 35 | github.com/gabriel-vasile/mimetype v1.4.3 // indirect 36 | github.com/gin-contrib/gzip v1.0.1 37 | github.com/gin-contrib/sse v0.1.0 // indirect 38 | github.com/go-ole/go-ole v1.3.0 // indirect 39 | github.com/go-playground/locales v0.14.1 // indirect 40 | github.com/go-playground/universal-translator v0.18.1 // indirect 41 | github.com/go-playground/validator/v10 v10.20.0 // indirect 42 | github.com/google/btree v1.1.2 // indirect 43 | github.com/google/pprof v0.0.0-20240528025155-186aa0362fba // indirect 44 | github.com/gorilla/context v1.1.2 // indirect 45 | github.com/gorilla/securecookie v1.1.2 // indirect 46 | github.com/gorilla/sessions v1.2.2 // indirect 47 | github.com/gorilla/websocket v1.5.3 // indirect 48 | github.com/jinzhu/inflection v1.0.0 // indirect 49 | github.com/jinzhu/now v1.1.5 // indirect 50 | github.com/json-iterator/go v1.1.12 // indirect 51 | github.com/klauspost/compress v1.17.8 // indirect 52 | github.com/klauspost/cpuid/v2 v2.2.7 // indirect 53 | github.com/leodido/go-urn v1.4.0 // indirect 54 | github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect 55 | github.com/mattn/go-isatty v0.0.20 // indirect 56 | github.com/mattn/go-sqlite3 v1.14.22 // indirect 57 | github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect 58 | github.com/modern-go/reflect2 v1.0.2 // indirect 59 | github.com/onsi/ginkgo/v2 v2.19.0 // indirect 60 | github.com/pires/go-proxyproto v0.7.0 // indirect 61 | github.com/power-devops/perfstat v0.0.0-20240219145905-2259734c190a // indirect 62 | github.com/quic-go/quic-go v0.45.0 // indirect 63 | github.com/refraction-networking/utls v1.6.6 // indirect 64 | github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect 65 | github.com/sagernet/sing v0.4.1 // indirect 66 | github.com/sagernet/sing-shadowsocks v0.2.6 // indirect 67 | github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect 68 | github.com/shirou/gopsutil/v4 v4.24.5 69 | github.com/shoenig/go-m1cpu v0.1.6 // indirect 70 | github.com/tklauser/go-sysconf v0.3.13 // indirect 71 | github.com/tklauser/numcpus v0.7.0 // indirect 72 | github.com/twitchyliquid64/golang-asm v0.15.1 // indirect 73 | github.com/ugorji/go/codec v1.2.12 // indirect 74 | github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect 75 | github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 // indirect 76 | github.com/vishvananda/netns v0.0.4 // indirect 77 | github.com/xtls/reality v0.0.0-20240429224917-ecc4401070cc // indirect 78 | github.com/yusufpapurcu/wmi v1.2.4 // indirect 79 | go.uber.org/mock v0.4.0 // indirect 80 | go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect 81 | golang.org/x/arch v0.8.0 // indirect 82 | golang.org/x/crypto v0.24.0 // indirect 83 | golang.org/x/exp v0.0.0-20240531132922-fd00a4e0eefc // indirect 84 | golang.org/x/mod v0.18.0 // indirect 85 | golang.org/x/net v0.26.0 // indirect 86 | golang.org/x/sys v0.21.0 // indirect 87 | golang.org/x/time v0.5.0 // indirect 88 | golang.org/x/tools v0.22.0 // indirect 89 | golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect 90 | golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect 91 | google.golang.org/genproto/googleapis/rpc v0.0.0-20240528184218-531527333157 // indirect 92 | google.golang.org/protobuf v1.34.2 // indirect 93 | gopkg.in/yaml.v3 v3.0.1 // indirect 94 | gvisor.dev/gvisor v0.0.0-20231202080848-1f7806d17489 // indirect 95 | lukechampine.com/blake3 v1.3.0 // indirect 96 | ) 97 | -------------------------------------------------------------------------------- /logger/logger.go: -------------------------------------------------------------------------------- 1 | package logger 2 | 3 | import ( 4 | "fmt" 5 | "os" 6 | "time" 7 | 8 | "github.com/op/go-logging" 9 | ) 10 | 11 | var ( 12 | logger *logging.Logger 13 | logBuffer []struct { 14 | time string 15 | level logging.Level 16 | log string 17 | } 18 | ) 19 | 20 | func init() { 21 | InitLogger(logging.INFO) 22 | } 23 | 24 | func InitLogger(level logging.Level) { 25 | newLogger := logging.MustGetLogger("x-ui") 26 | var err error 27 | var backend logging.Backend 28 | var format logging.Formatter 29 | ppid := os.Getppid() 30 | 31 | backend, err = logging.NewSyslogBackend("") 32 | if err != nil { 33 | println(err) 34 | backend = logging.NewLogBackend(os.Stderr, "", 0) 35 | } 36 | if ppid > 0 && err != nil { 37 | format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`) 38 | } else { 39 | format = logging.MustStringFormatter(`%{level} - %{message}`) 40 | } 41 | 42 | backendFormatter := logging.NewBackendFormatter(backend, format) 43 | backendLeveled := logging.AddModuleLevel(backendFormatter) 44 | backendLeveled.SetLevel(level, "x-ui") 45 | newLogger.SetBackend(backendLeveled) 46 | 47 | logger = newLogger 48 | } 49 | 50 | func Debug(args ...interface{}) { 51 | logger.Debug(args...) 52 | addToBuffer("DEBUG", fmt.Sprint(args...)) 53 | } 54 | 55 | func Debugf(format string, args ...interface{}) { 56 | logger.Debugf(format, args...) 57 | addToBuffer("DEBUG", fmt.Sprintf(format, args...)) 58 | } 59 | 60 | func Info(args ...interface{}) { 61 | logger.Info(args...) 62 | addToBuffer("INFO", fmt.Sprint(args...)) 63 | } 64 | 65 | func Infof(format string, args ...interface{}) { 66 | logger.Infof(format, args...) 67 | addToBuffer("INFO", fmt.Sprintf(format, args...)) 68 | } 69 | 70 | func Warning(args ...interface{}) { 71 | logger.Warning(args...) 72 | addToBuffer("WARNING", fmt.Sprint(args...)) 73 | } 74 | 75 | func Warningf(format string, args ...interface{}) { 76 | logger.Warningf(format, args...) 77 | addToBuffer("WARNING", fmt.Sprintf(format, args...)) 78 | } 79 | 80 | func Error(args ...interface{}) { 81 | logger.Error(args...) 82 | addToBuffer("ERROR", fmt.Sprint(args...)) 83 | } 84 | 85 | func Errorf(format string, args ...interface{}) { 86 | logger.Errorf(format, args...) 87 | addToBuffer("ERROR", fmt.Sprintf(format, args...)) 88 | } 89 | 90 | func addToBuffer(level string, newLog string) { 91 | t := time.Now() 92 | if len(logBuffer) >= 10240 { 93 | logBuffer = logBuffer[1:] 94 | } 95 | 96 | logLevel, _ := logging.LogLevel(level) 97 | logBuffer = append(logBuffer, struct { 98 | time string 99 | level logging.Level 100 | log string 101 | }{ 102 | time: t.Format("2006/01/02 15:04:05"), 103 | level: logLevel, 104 | log: newLog, 105 | }) 106 | } 107 | 108 | func GetLogs(c int, level string) []string { 109 | var output []string 110 | logLevel, _ := logging.LogLevel(level) 111 | 112 | for i := len(logBuffer) - 1; i >= 0 && len(output) <= c; i-- { 113 | if logBuffer[i].level <= logLevel { 114 | output = append(output, fmt.Sprintf("%s %s - %s", logBuffer[i].time, logBuffer[i].level, logBuffer[i].log)) 115 | } 116 | } 117 | return output 118 | } 119 | -------------------------------------------------------------------------------- /media/inbounds-dark.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xDontwait/x-ui/54e446a34b0f0f8e390dcf92af8389be1edc2007/media/inbounds-dark.png -------------------------------------------------------------------------------- /media/inbounds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xDontwait/x-ui/54e446a34b0f0f8e390dcf92af8389be1edc2007/media/inbounds.png -------------------------------------------------------------------------------- /media/outbounds.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xDontwait/x-ui/54e446a34b0f0f8e390dcf92af8389be1edc2007/media/outbounds.png -------------------------------------------------------------------------------- /media/rules.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xDontwait/x-ui/54e446a34b0f0f8e390dcf92af8389be1edc2007/media/rules.png -------------------------------------------------------------------------------- /media/warp.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xDontwait/x-ui/54e446a34b0f0f8e390dcf92af8389be1edc2007/media/warp.png -------------------------------------------------------------------------------- /sub/default.json: -------------------------------------------------------------------------------- 1 | { 2 | "remarks": "", 3 | "dns": { 4 | "tag": "dns_out", 5 | "queryStrategy": "UseIP", 6 | "servers": [ 7 | { 8 | "address": "8.8.8.8", 9 | "skipFallback": false 10 | } 11 | ] 12 | }, 13 | "inbounds": [ 14 | { 15 | "port": 10808, 16 | "protocol": "socks", 17 | "settings": { 18 | "auth": "noauth", 19 | "udp": true, 20 | "userLevel": 8 21 | }, 22 | "sniffing": { 23 | "destOverride": [ 24 | "http", 25 | "tls", 26 | "fakedns" 27 | ], 28 | "enabled": true 29 | }, 30 | "tag": "socks" 31 | }, 32 | { 33 | "port": 10809, 34 | "protocol": "http", 35 | "settings": { 36 | "userLevel": 8 37 | }, 38 | "tag": "http" 39 | } 40 | ], 41 | "log": { 42 | "loglevel": "warning" 43 | }, 44 | "outbounds": [ 45 | { 46 | "tag": "direct", 47 | "protocol": "freedom", 48 | "settings": { 49 | "domainStrategy": "UseIP" 50 | } 51 | }, 52 | { 53 | "tag": "block", 54 | "protocol": "blackhole", 55 | "settings": { 56 | "response": { 57 | "type": "http" 58 | } 59 | } 60 | } 61 | ], 62 | "policy": { 63 | "levels": { 64 | "8": { 65 | "connIdle": 300, 66 | "downlinkOnly": 1, 67 | "handshake": 4, 68 | "uplinkOnly": 1 69 | } 70 | }, 71 | "system": { 72 | "statsOutboundUplink": true, 73 | "statsOutboundDownlink": true 74 | } 75 | }, 76 | "routing": { 77 | "domainStrategy": "IPIfNonMatch", 78 | "rules": [ 79 | { 80 | "type": "field", 81 | "network": "tcp,udp", 82 | "outboundTag": "proxy" 83 | } 84 | ] 85 | }, 86 | "stats": {} 87 | } -------------------------------------------------------------------------------- /sub/sub.go: -------------------------------------------------------------------------------- 1 | package sub 2 | 3 | import ( 4 | "context" 5 | "crypto/tls" 6 | "io" 7 | "net" 8 | "net/http" 9 | "strconv" 10 | 11 | "x-ui/config" 12 | "x-ui/logger" 13 | "x-ui/util/common" 14 | "x-ui/web/middleware" 15 | "x-ui/web/network" 16 | "x-ui/web/service" 17 | 18 | "github.com/gin-gonic/gin" 19 | ) 20 | 21 | type Server struct { 22 | httpServer *http.Server 23 | listener net.Listener 24 | 25 | sub *SUBController 26 | settingService service.SettingService 27 | 28 | ctx context.Context 29 | cancel context.CancelFunc 30 | } 31 | 32 | func NewServer() *Server { 33 | ctx, cancel := context.WithCancel(context.Background()) 34 | return &Server{ 35 | ctx: ctx, 36 | cancel: cancel, 37 | } 38 | } 39 | 40 | func (s *Server) initRouter() (*gin.Engine, error) { 41 | if config.IsDebug() { 42 | gin.SetMode(gin.DebugMode) 43 | } else { 44 | gin.DefaultWriter = io.Discard 45 | gin.DefaultErrorWriter = io.Discard 46 | gin.SetMode(gin.ReleaseMode) 47 | } 48 | 49 | engine := gin.Default() 50 | 51 | subDomain, err := s.settingService.GetSubDomain() 52 | if err != nil { 53 | return nil, err 54 | } 55 | 56 | if subDomain != "" { 57 | engine.Use(middleware.DomainValidatorMiddleware(subDomain)) 58 | } 59 | 60 | LinksPath, err := s.settingService.GetSubPath() 61 | if err != nil { 62 | return nil, err 63 | } 64 | 65 | JsonPath, err := s.settingService.GetSubJsonPath() 66 | if err != nil { 67 | return nil, err 68 | } 69 | 70 | Encrypt, err := s.settingService.GetSubEncrypt() 71 | if err != nil { 72 | return nil, err 73 | } 74 | 75 | ShowInfo, err := s.settingService.GetSubShowInfo() 76 | if err != nil { 77 | return nil, err 78 | } 79 | 80 | RemarkModel, err := s.settingService.GetRemarkModel() 81 | if err != nil { 82 | RemarkModel = "-ieo" 83 | } 84 | 85 | SubUpdates, err := s.settingService.GetSubUpdates() 86 | if err != nil { 87 | SubUpdates = "10" 88 | } 89 | 90 | SubJsonFragment, err := s.settingService.GetSubJsonFragment() 91 | if err != nil { 92 | SubJsonFragment = "" 93 | } 94 | 95 | SubJsonMux, err := s.settingService.GetSubJsonMux() 96 | if err != nil { 97 | SubJsonMux = "" 98 | } 99 | 100 | SubJsonRules, err := s.settingService.GetSubJsonRules() 101 | if err != nil { 102 | SubJsonRules = "" 103 | } 104 | 105 | g := engine.Group("/") 106 | 107 | s.sub = NewSUBController( 108 | g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates, 109 | SubJsonFragment, SubJsonMux, SubJsonRules) 110 | 111 | return engine, nil 112 | } 113 | 114 | func (s *Server) Start() (err error) { 115 | // This is an anonymous function, no function name 116 | defer func() { 117 | if err != nil { 118 | s.Stop() 119 | } 120 | }() 121 | 122 | subEnable, err := s.settingService.GetSubEnable() 123 | if err != nil { 124 | return err 125 | } 126 | if !subEnable { 127 | return nil 128 | } 129 | 130 | engine, err := s.initRouter() 131 | if err != nil { 132 | return err 133 | } 134 | 135 | certFile, err := s.settingService.GetSubCertFile() 136 | if err != nil { 137 | return err 138 | } 139 | keyFile, err := s.settingService.GetSubKeyFile() 140 | if err != nil { 141 | return err 142 | } 143 | listen, err := s.settingService.GetSubListen() 144 | if err != nil { 145 | return err 146 | } 147 | port, err := s.settingService.GetSubPort() 148 | if err != nil { 149 | return err 150 | } 151 | 152 | listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) 153 | listener, err := net.Listen("tcp", listenAddr) 154 | if err != nil { 155 | return err 156 | } 157 | 158 | if certFile != "" || keyFile != "" { 159 | cert, err := tls.LoadX509KeyPair(certFile, keyFile) 160 | if err == nil { 161 | c := &tls.Config{ 162 | Certificates: []tls.Certificate{cert}, 163 | } 164 | listener = network.NewAutoHttpsListener(listener) 165 | listener = tls.NewListener(listener, c) 166 | logger.Info("sub server run https on", listener.Addr()) 167 | } else { 168 | logger.Error("error in loading certificates: ", err) 169 | logger.Info("sub server run http on", listener.Addr()) 170 | } 171 | } else { 172 | logger.Info("sub server run http on", listener.Addr()) 173 | } 174 | s.listener = listener 175 | 176 | s.httpServer = &http.Server{ 177 | Handler: engine, 178 | } 179 | 180 | go func() { 181 | s.httpServer.Serve(listener) 182 | }() 183 | 184 | return nil 185 | } 186 | 187 | func (s *Server) Stop() error { 188 | s.cancel() 189 | 190 | var err1 error 191 | var err2 error 192 | if s.httpServer != nil { 193 | err1 = s.httpServer.Shutdown(s.ctx) 194 | } 195 | if s.listener != nil { 196 | err2 = s.listener.Close() 197 | } 198 | return common.Combine(err1, err2) 199 | } 200 | 201 | func (s *Server) GetCtx() context.Context { 202 | return s.ctx 203 | } 204 | -------------------------------------------------------------------------------- /sub/subController.go: -------------------------------------------------------------------------------- 1 | package sub 2 | 3 | import ( 4 | "encoding/base64" 5 | "net" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | type SUBController struct { 11 | subPath string 12 | subJsonPath string 13 | subEncrypt bool 14 | updateInterval string 15 | 16 | subService *SubService 17 | subJsonService *SubJsonService 18 | } 19 | 20 | func NewSUBController( 21 | g *gin.RouterGroup, 22 | subPath string, 23 | jsonPath string, 24 | encrypt bool, 25 | showInfo bool, 26 | rModel string, 27 | update string, 28 | jsonFragment string, 29 | jsonMux string, 30 | jsonRules string, 31 | ) *SUBController { 32 | sub := NewSubService(showInfo, rModel) 33 | a := &SUBController{ 34 | subPath: subPath, 35 | subJsonPath: jsonPath, 36 | subEncrypt: encrypt, 37 | updateInterval: update, 38 | 39 | subService: sub, 40 | subJsonService: NewSubJsonService(jsonFragment, jsonMux, jsonRules, sub), 41 | } 42 | a.initRouter(g) 43 | return a 44 | } 45 | 46 | func (a *SUBController) initRouter(g *gin.RouterGroup) { 47 | gLink := g.Group(a.subPath) 48 | gJson := g.Group(a.subJsonPath) 49 | 50 | gLink.GET(":subid", a.subs) 51 | 52 | gJson.GET(":subid", a.subJsons) 53 | } 54 | 55 | func (a *SUBController) subs(c *gin.Context) { 56 | subId := c.Param("subid") 57 | host, _, _ := net.SplitHostPort(c.Request.Host) 58 | subs, header, err := a.subService.GetSubs(subId, host) 59 | if err != nil || len(subs) == 0 { 60 | c.String(400, "Error!") 61 | } else { 62 | result := "" 63 | for _, sub := range subs { 64 | result += sub + "\n" 65 | } 66 | 67 | // Add headers 68 | c.Writer.Header().Set("Subscription-Userinfo", header) 69 | c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval) 70 | c.Writer.Header().Set("Profile-Title", subId) 71 | 72 | if a.subEncrypt { 73 | c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) 74 | } else { 75 | c.String(200, result) 76 | } 77 | } 78 | } 79 | 80 | func (a *SUBController) subJsons(c *gin.Context) { 81 | subId := c.Param("subid") 82 | host, _, _ := net.SplitHostPort(c.Request.Host) 83 | jsonSub, header, err := a.subJsonService.GetJson(subId, host) 84 | if err != nil || len(jsonSub) == 0 { 85 | c.String(400, "Error!") 86 | } else { 87 | 88 | // Add headers 89 | c.Writer.Header().Set("Subscription-Userinfo", header) 90 | c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval) 91 | c.Writer.Header().Set("Profile-Title", subId) 92 | 93 | c.String(200, jsonSub) 94 | } 95 | } 96 | -------------------------------------------------------------------------------- /util/common/err.go: -------------------------------------------------------------------------------- 1 | package common 2 | 3 | import ( 4 | "errors" 5 | "fmt" 6 | 7 | "x-ui/logger" 8 | ) 9 | 10 | func NewErrorf(format string, a ...interface{}) error { 11 | msg := fmt.Sprintf(format, a...) 12 | return errors.New(msg) 13 | } 14 | 15 | func NewError(a ...interface{}) error { 16 | msg := fmt.Sprintln(a...) 17 | return errors.New(msg) 18 | } 19 | 20 | func Recover(msg string) interface{} { 21 | panicErr := recover() 22 | if panicErr != nil { 23 | if msg != "" { 24 | logger.Error(msg, "panic:", panicErr) 25 | } 26 | } 27 | return panicErr 28 | } 29 | -------------------------------------------------------------------------------- /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/json_util/json.go: -------------------------------------------------------------------------------- 1 | package json_util 2 | 3 | import ( 4 | "errors" 5 | ) 6 | 7 | type RawMessage []byte 8 | 9 | // MarshalJSON: Customize json.RawMessage default behavior 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 ( 9 | numSeq [10]rune 10 | lowerSeq [26]rune 11 | upperSeq [26]rune 12 | numLowerSeq [36]rune 13 | numUpperSeq [36]rune 14 | allSeq [62]rune 15 | ) 16 | 17 | func init() { 18 | rand.Seed(time.Now().UnixNano()) 19 | 20 | for i := 0; i < 10; i++ { 21 | numSeq[i] = rune('0' + i) 22 | } 23 | for i := 0; i < 26; i++ { 24 | lowerSeq[i] = rune('a' + i) 25 | upperSeq[i] = rune('A' + i) 26 | } 27 | 28 | copy(numLowerSeq[:], numSeq[:]) 29 | copy(numLowerSeq[len(numSeq):], lowerSeq[:]) 30 | 31 | copy(numUpperSeq[:], numSeq[:]) 32 | copy(numUpperSeq[len(numSeq):], upperSeq[:]) 33 | 34 | copy(allSeq[:], numSeq[:]) 35 | copy(allSeq[len(numSeq):], lowerSeq[:]) 36 | copy(allSeq[len(numSeq)+len(lowerSeq):], upperSeq[:]) 37 | } 38 | 39 | func Seq(n int) string { 40 | runes := make([]rune, n) 41 | for i := 0; i < n; i++ { 42 | runes[i] = allSeq[rand.Intn(len(allSeq))] 43 | } 44 | return string(runes) 45 | } 46 | 47 | func Num(n int) int { 48 | return rand.Intn(n) 49 | } 50 | -------------------------------------------------------------------------------- /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/psutil.go: -------------------------------------------------------------------------------- 1 | package sys 2 | 3 | import ( 4 | _ "unsafe" 5 | ) 6 | 7 | //go:linkname HostProc github.com/shirou/gopsutil/v4/internal/common.HostProc 8 | func HostProc(combineWith ...string) string 9 | -------------------------------------------------------------------------------- /util/sys/sys_darwin.go: -------------------------------------------------------------------------------- 1 | //go:build darwin 2 | // +build darwin 3 | 4 | package sys 5 | 6 | import ( 7 | "github.com/shirou/gopsutil/v4/net" 8 | ) 9 | 10 | func GetTCPCount() (int, error) { 11 | stats, err := net.Connections("tcp") 12 | if err != nil { 13 | return 0, err 14 | } 15 | return len(stats), nil 16 | } 17 | 18 | func GetUDPCount() (int, error) { 19 | stats, err := net.Connections("udp") 20 | if err != nil { 21 | return 0, err 22 | } 23 | return len(stats), nil 24 | } 25 | -------------------------------------------------------------------------------- /util/sys/sys_linux.go: -------------------------------------------------------------------------------- 1 | //go:build linux 2 | // +build linux 3 | 4 | package sys 5 | 6 | import ( 7 | "bytes" 8 | "fmt" 9 | "io" 10 | "os" 11 | ) 12 | 13 | func getLinesNum(filename string) (int, error) { 14 | file, err := os.Open(filename) 15 | if err != nil { 16 | return 0, err 17 | } 18 | defer file.Close() 19 | 20 | sum := 0 21 | buf := make([]byte, 8192) 22 | for { 23 | n, err := file.Read(buf) 24 | 25 | var buffPosition int 26 | for { 27 | i := bytes.IndexByte(buf[buffPosition:], '\n') 28 | if i < 0 || n == buffPosition { 29 | break 30 | } 31 | buffPosition += i + 1 32 | sum++ 33 | } 34 | 35 | if err == io.EOF { 36 | return sum, nil 37 | } else if err != nil { 38 | return sum, err 39 | } 40 | } 41 | } 42 | 43 | func GetTCPCount() (int, error) { 44 | root := HostProc() 45 | 46 | tcp4, err := getLinesNum(fmt.Sprintf("%v/net/tcp", root)) 47 | if err != nil { 48 | return tcp4, err 49 | } 50 | tcp6, err := getLinesNum(fmt.Sprintf("%v/net/tcp6", root)) 51 | if err != nil { 52 | return tcp4 + tcp6, nil 53 | } 54 | 55 | return tcp4 + tcp6, nil 56 | } 57 | 58 | func GetUDPCount() (int, error) { 59 | root := HostProc() 60 | 61 | udp4, err := getLinesNum(fmt.Sprintf("%v/net/udp", root)) 62 | if err != nil { 63 | return udp4, err 64 | } 65 | udp6, err := getLinesNum(fmt.Sprintf("%v/net/udp6", root)) 66 | if err != nil { 67 | return udp4 + udp6, nil 68 | } 69 | 70 | return udp4 + udp6, nil 71 | } 72 | -------------------------------------------------------------------------------- /util/sys/sys_windows.go: -------------------------------------------------------------------------------- 1 | //go:build windows 2 | // +build windows 3 | 4 | package sys 5 | 6 | import ( 7 | "github.com/shirou/gopsutil/v4/net" 8 | ) 9 | 10 | func GetTCPCount() (int, error) { 11 | stats, err := net.Connections("tcp") 12 | if err != nil { 13 | return 0, err 14 | } 15 | return len(stats), nil 16 | } 17 | 18 | func GetUDPCount() (int, error) { 19 | stats, err := net.Connections("udp") 20 | if err != nil { 21 | return 0, err 22 | } 23 | return len(stats), nil 24 | } 25 | -------------------------------------------------------------------------------- /web/assets/Vazirmatn-UI-NL-Regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/0xDontwait/x-ui/54e446a34b0f0f8e390dcf92af8389be1edc2007/web/assets/Vazirmatn-UI-NL-Regular.woff2 -------------------------------------------------------------------------------- /web/assets/ant-design-vue@1.7.8/antd.less: -------------------------------------------------------------------------------- 1 | @import "../lib/style/index.less"; 2 | @import "../lib/style/components.less"; 3 | 4 | @blue-6: #0E49B5; 5 | @border-radius-base: 1rem; 6 | @progress-remaining-color: #EDEDED; -------------------------------------------------------------------------------- /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/codemirror/fold/brace-fold.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/5/LICENSE 3 | 4 | (function(mod) { 5 | if (typeof exports == "object" && typeof module == "object") // CommonJS 6 | mod(require("../../lib/codemirror")); 7 | else if (typeof define == "function" && define.amd) // AMD 8 | define(["../../lib/codemirror"], mod); 9 | else // Plain browser env 10 | mod(CodeMirror); 11 | })(function(CodeMirror) { 12 | "use strict"; 13 | 14 | function bracketFolding(pairs) { 15 | return function(cm, start) { 16 | var line = start.line, lineText = cm.getLine(line); 17 | 18 | function findOpening(pair) { 19 | var tokenType; 20 | for (var at = start.ch, pass = 0;;) { 21 | var found = at <= 0 ? -1 : lineText.lastIndexOf(pair[0], at - 1); 22 | if (found == -1) { 23 | if (pass == 1) break; 24 | pass = 1; 25 | at = lineText.length; 26 | continue; 27 | } 28 | if (pass == 1 && found < start.ch) break; 29 | tokenType = cm.getTokenTypeAt(CodeMirror.Pos(line, found + 1)); 30 | if (!/^(comment|string)/.test(tokenType)) return {ch: found + 1, tokenType: tokenType, pair: pair}; 31 | at = found - 1; 32 | } 33 | } 34 | 35 | function findRange(found) { 36 | var count = 1, lastLine = cm.lastLine(), end, startCh = found.ch, endCh 37 | outer: for (var i = line; i <= lastLine; ++i) { 38 | var text = cm.getLine(i), pos = i == line ? startCh : 0; 39 | for (;;) { 40 | var nextOpen = text.indexOf(found.pair[0], pos), nextClose = text.indexOf(found.pair[1], pos); 41 | if (nextOpen < 0) nextOpen = text.length; 42 | if (nextClose < 0) nextClose = text.length; 43 | pos = Math.min(nextOpen, nextClose); 44 | if (pos == text.length) break; 45 | if (cm.getTokenTypeAt(CodeMirror.Pos(i, pos + 1)) == found.tokenType) { 46 | if (pos == nextOpen) ++count; 47 | else if (!--count) { end = i; endCh = pos; break outer; } 48 | } 49 | ++pos; 50 | } 51 | } 52 | 53 | if (end == null || line == end) return null 54 | return {from: CodeMirror.Pos(line, startCh), 55 | to: CodeMirror.Pos(end, endCh)}; 56 | } 57 | 58 | var found = [] 59 | for (var i = 0; i < pairs.length; i++) { 60 | var open = findOpening(pairs[i]) 61 | if (open) found.push(open) 62 | } 63 | found.sort(function(a, b) { return a.ch - b.ch }) 64 | for (var i = 0; i < found.length; i++) { 65 | var range = findRange(found[i]) 66 | if (range) return range 67 | } 68 | return null 69 | } 70 | } 71 | 72 | CodeMirror.registerHelper("fold", "brace", bracketFolding([["{", "}"], ["[", "]"]])); 73 | 74 | CodeMirror.registerHelper("fold", "brace-paren", bracketFolding([["{", "}"], ["[", "]"], ["(", ")"]])); 75 | 76 | CodeMirror.registerHelper("fold", "import", function(cm, start) { 77 | function hasImport(line) { 78 | if (line < cm.firstLine() || line > cm.lastLine()) return null; 79 | var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); 80 | if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); 81 | if (start.type != "keyword" || start.string != "import") return null; 82 | // Now find closing semicolon, return its position 83 | for (var i = line, e = Math.min(cm.lastLine(), line + 10); i <= e; ++i) { 84 | var text = cm.getLine(i), semi = text.indexOf(";"); 85 | if (semi != -1) return {startCh: start.end, end: CodeMirror.Pos(i, semi)}; 86 | } 87 | } 88 | 89 | var startLine = start.line, has = hasImport(startLine), prev; 90 | if (!has || hasImport(startLine - 1) || ((prev = hasImport(startLine - 2)) && prev.end.line == startLine - 1)) 91 | return null; 92 | for (var end = has.end;;) { 93 | var next = hasImport(end.line + 1); 94 | if (next == null) break; 95 | end = next.end; 96 | } 97 | return {from: cm.clipPos(CodeMirror.Pos(startLine, has.startCh + 1)), to: end}; 98 | }); 99 | 100 | CodeMirror.registerHelper("fold", "include", function(cm, start) { 101 | function hasInclude(line) { 102 | if (line < cm.firstLine() || line > cm.lastLine()) return null; 103 | var start = cm.getTokenAt(CodeMirror.Pos(line, 1)); 104 | if (!/\S/.test(start.string)) start = cm.getTokenAt(CodeMirror.Pos(line, start.end + 1)); 105 | if (start.type == "meta" && start.string.slice(0, 8) == "#include") return start.start + 8; 106 | } 107 | 108 | var startLine = start.line, has = hasInclude(startLine); 109 | if (has == null || hasInclude(startLine - 1) != null) return null; 110 | for (var end = startLine;;) { 111 | var next = hasInclude(end + 1); 112 | if (next == null) break; 113 | ++end; 114 | } 115 | return {from: CodeMirror.Pos(startLine, has + 1), 116 | to: cm.clipPos(CodeMirror.Pos(end))}; 117 | }); 118 | 119 | }); 120 | -------------------------------------------------------------------------------- /web/assets/codemirror/fold/foldgutter.css: -------------------------------------------------------------------------------- 1 | .CodeMirror-foldmarker { 2 | color: blue; 3 | text-shadow: #b9f 1px 1px 2px, #b9f -1px -1px 2px, #b9f 1px -1px 2px, #b9f -1px 1px 2px; 4 | font-family: arial; 5 | line-height: .3; 6 | cursor: pointer; 7 | } 8 | .CodeMirror-foldgutter { 9 | width: .7em; 10 | } 11 | .CodeMirror-foldgutter-open, 12 | .CodeMirror-foldgutter-folded { 13 | cursor: pointer; 14 | } 15 | .CodeMirror-foldgutter-open:after { 16 | content: "\25BE"; 17 | } 18 | .CodeMirror-foldgutter-folded:after { 19 | content: "\25B8"; 20 | } 21 | -------------------------------------------------------------------------------- /web/assets/codemirror/lint/javascript-lint.js: -------------------------------------------------------------------------------- 1 | // CodeMirror, copyright (c) by Marijn Haverbeke and others 2 | // Distributed under an MIT license: https://codemirror.net/5/LICENSE 3 | 4 | // Depends on jshint.js from https://github.com/jshint/jshint 5 | 6 | (function(mod) { 7 | if (typeof exports == "object" && typeof module == "object") // CommonJS 8 | mod(require("../../lib/codemirror")); 9 | else if (typeof define == "function" && define.amd) // AMD 10 | define(["../../lib/codemirror"], mod); 11 | else // Plain browser env 12 | mod(CodeMirror); 13 | })(function(CodeMirror) { 14 | "use strict"; 15 | // declare global: JSHINT 16 | 17 | function validator(text, options) { 18 | if (!window.JSHINT) { 19 | if (window.console) { 20 | window.console.error("Error: window.JSHINT not defined, CodeMirror JavaScript linting cannot run."); 21 | } 22 | return []; 23 | } 24 | if (!options.indent) // JSHint error.character actually is a column index, this fixes underlining on lines using tabs for indentation 25 | options.indent = 1; // JSHint default value is 4 26 | JSHINT(text, options, options.globals); 27 | var errors = JSHINT.data().errors, result = []; 28 | if (errors) parseErrors(errors, result); 29 | return result; 30 | } 31 | 32 | CodeMirror.registerHelper("lint", "javascript", validator); 33 | 34 | function parseErrors(errors, output) { 35 | for ( var i = 0; i < errors.length; i++) { 36 | var error = errors[i]; 37 | if (error) { 38 | if (error.line <= 0) { 39 | if (window.console) { 40 | window.console.warn("Cannot display JSHint error (invalid line " + error.line + ")", error); 41 | } 42 | continue; 43 | } 44 | 45 | var start = error.character - 1, end = start + 1; 46 | if (error.evidence) { 47 | var index = error.evidence.substring(start).search(/.\b/); 48 | if (index > -1) { 49 | end += index; 50 | } 51 | } 52 | 53 | // Convert to format expected by validation service 54 | var hint = { 55 | message: error.reason, 56 | severity: error.code ? (error.code.startsWith('W') ? "warning" : "error") : "error", 57 | from: CodeMirror.Pos(error.line - 1, start), 58 | to: CodeMirror.Pos(error.line - 1, end) 59 | }; 60 | 61 | output.push(hint); 62 | } 63 | } 64 | } 65 | }); 66 | -------------------------------------------------------------------------------- /web/assets/codemirror/lint/lint.css: -------------------------------------------------------------------------------- 1 | /* The lint marker gutter */ 2 | .CodeMirror-lint-markers { 3 | width: 16px; 4 | } 5 | 6 | .CodeMirror-lint-tooltip { 7 | background-color: #ffd; 8 | border: 1px solid black; 9 | border-radius: 4px 4px 4px 4px; 10 | color: black; 11 | font-family: monospace; 12 | font-size: 10pt; 13 | overflow: hidden; 14 | padding: 2px 5px; 15 | position: fixed; 16 | white-space: pre; 17 | white-space: pre-wrap; 18 | z-index: 100; 19 | max-width: 600px; 20 | opacity: 0; 21 | transition: opacity .4s; 22 | -moz-transition: opacity .4s; 23 | -webkit-transition: opacity .4s; 24 | -o-transition: opacity .4s; 25 | -ms-transition: opacity .4s; 26 | } 27 | 28 | .CodeMirror-lint-mark { 29 | background-position: left bottom; 30 | background-repeat: repeat-x; 31 | } 32 | 33 | .CodeMirror-lint-mark-warning { 34 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJFhQXEbhTg7YAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAMklEQVQI12NkgIIvJ3QXMjAwdDN+OaEbysDA4MPAwNDNwMCwiOHLCd1zX07o6kBVGQEAKBANtobskNMAAAAASUVORK5CYII="); 35 | } 36 | 37 | .CodeMirror-lint-mark-error { 38 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAQAAAADCAYAAAC09K7GAAAAAXNSR0IArs4c6QAAAAZiS0dEAP8A/wD/oL2nkwAAAAlwSFlzAAALEwAACxMBAJqcGAAAAAd0SU1FB9sJDw4cOCW1/KIAAAAZdEVYdENvbW1lbnQAQ3JlYXRlZCB3aXRoIEdJTVBXgQ4XAAAAHElEQVQI12NggIL/DAz/GdA5/xkY/qPKMDAwAADLZwf5rvm+LQAAAABJRU5ErkJggg=="); 39 | } 40 | 41 | .CodeMirror-lint-marker { 42 | background-position: center center; 43 | background-repeat: no-repeat; 44 | cursor: pointer; 45 | display: inline-block; 46 | height: 16px; 47 | width: 16px; 48 | vertical-align: middle; 49 | position: relative; 50 | } 51 | 52 | .CodeMirror-lint-message { 53 | padding-left: 18px; 54 | background-position: top left; 55 | background-repeat: no-repeat; 56 | } 57 | 58 | .CodeMirror-lint-marker-warning, .CodeMirror-lint-message-warning { 59 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAANlBMVEX/uwDvrwD/uwD/uwD/uwD/uwD/uwD/uwD/uwD6twD/uwAAAADurwD2tQD7uAD+ugAAAAD/uwDhmeTRAAAADHRSTlMJ8mN1EYcbmiixgACm7WbuAAAAVklEQVR42n3PUQqAIBBFUU1LLc3u/jdbOJoW1P08DA9Gba8+YWJ6gNJoNYIBzAA2chBth5kLmG9YUoG0NHAUwFXwO9LuBQL1giCQb8gC9Oro2vp5rncCIY8L8uEx5ZkAAAAASUVORK5CYII="); 60 | } 61 | 62 | .CodeMirror-lint-marker-error, .CodeMirror-lint-message-error { 63 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAMAAAAoLQ9TAAAAHlBMVEW7AAC7AACxAAC7AAC7AAAAAAC4AAC5AAD///+7AAAUdclpAAAABnRSTlMXnORSiwCK0ZKSAAAATUlEQVR42mWPOQ7AQAgDuQLx/z8csYRmPRIFIwRGnosRrpamvkKi0FTIiMASR3hhKW+hAN6/tIWhu9PDWiTGNEkTtIOucA5Oyr9ckPgAWm0GPBog6v4AAAAASUVORK5CYII="); 64 | } 65 | 66 | .CodeMirror-lint-marker-multiple { 67 | background-image: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAcAAAAHCAMAAADzjKfhAAAACVBMVEUAAAAAAAC/v7914kyHAAAAAXRSTlMAQObYZgAAACNJREFUeNo1ioEJAAAIwmz/H90iFFSGJgFMe3gaLZ0od+9/AQZ0ADosbYraAAAAAElFTkSuQmCC"); 68 | background-repeat: no-repeat; 69 | background-position: right bottom; 70 | width: 100%; height: 100%; 71 | } 72 | 73 | .CodeMirror-lint-line-error { 74 | background-color: rgba(183, 76, 81, 0.08); 75 | } 76 | 77 | .CodeMirror-lint-line-warning { 78 | background-color: rgba(255, 211, 0, 0.1); 79 | } 80 | -------------------------------------------------------------------------------- /web/assets/codemirror/xq.min.css: -------------------------------------------------------------------------------- 1 | .cm-s-xq.CodeMirror{border-radius:1.5rem;border:1px solid #d9d9d9;height:auto}.cm-s-xq.CodeMirror:hover{background-color:#edf4fa;border-color:#2f67c2;transition:all .3s}.cm-s-xq .CodeMirror-gutters{border-right:1px solid #ddd;background-color:rgb(221 221 221 / 20%);white-space:nowrap}.cm-s-xq span.cm-keyword{line-height:1em;font-weight:bold;color:#5A5CAD}.cm-s-xq span.cm-atom{color:#7A316F;font-weight:bold}.cm-s-xq span.cm-number{color:#389E0D}.cm-s-xq span.cm-def{text-decoration:underline}.cm-s-xq span.cm-variable{color:black}.cm-s-xq span.cm-variable-2{color:black}.cm-s-xq span.cm-variable-3,.cm-s-xq span.cm-type{color:black}.cm-s-xq span.cm-property{color:#0e49b5}.cm-s-xq span.cm-operator{}.cm-s-xq span.cm-comment{color:#bbbbbb;font-style:italic}.cm-s-xq span.cm-string{}.cm-s-xq span.cm-meta{color:yellow}.cm-s-xq span.cm-qualifier{color:grey}.cm-s-xq span.cm-builtin{color:#7EA656}.cm-s-xq span.cm-bracket{color:#cc7}.cm-s-xq span.cm-tag{color:#3F7F7F}.cm-s-xq span.cm-attribute{color:#7F007F}.cm-s-xq span.cm-error{color:#e04141}.cm-s-xq .CodeMirror-activeline-background{background:#e8f2ff}.cm-s-xq .CodeMirror-matchingbracket{outline:1px solid grey;color:black!important;background:yellow}.dark .cm-s-xq.CodeMirror{background-color:#222D42;border-color:#2c3950;color:rgb(255 255 255 / 65%)}.dark .cm-s-xq.CodeMirror:hover{background-color:#0e2040;border-color:#0e49b5;transition:all .3s}.dark .cm-s-xq div.CodeMirror-selected{background:rgba(0,0,0,.5)}.dark .cm-s-xq .CodeMirror-line::selection,.dark .cm-s-xq .CodeMirror-line>span::selection,.dark .cm-s-xq .CodeMirror-line>span>span::selection{background:rgba(39,0,122,.99)}.dark .cm-s-xq .CodeMirror-line::-moz-selection,.dark .cm-s-xq .CodeMirror-line>span::-moz-selection,.dark .cm-s-xq .CodeMirror-line>span>span::-moz-selection{background:rgba(39,0,122,.99)}.dark .cm-s-xq .CodeMirror-gutters{background:rgb(0 0 0 / 30%);border-right:1px solid #2c3950}.dark .cm-s-xq .CodeMirror-guttermarker{color:#FFBD40}.dark .cm-s-xq .CodeMirror-guttermarker-subtle{color:rgb(255 255 255 / 70%)}.dark .cm-s-xq .CodeMirror-linenumber{color:rgb(255 255 255 / 50%)}.dark .cm-s-xq .CodeMirror-cursor{border-left:1px solid white}.dark .cm-s-xq span.cm-keyword{color:#FFBD40}.dark .cm-s-xq span.cm-atom{color:#c099ff}.dark .cm-s-xq span.cm-number{color:#9ccfd8}.dark .cm-s-xq span.cm-def{color:#FFF;text-decoration:underline}.dark .cm-s-xq span.cm-variable{color:#FFF}.dark .cm-s-xq span.cm-variable-2{color:#EEE}.dark .cm-s-xq span.cm-variable-3,.dark .cm-s-xq span.cm-type{color:#DDD}.dark .cm-s-xq span.cm-property{color:#f6c177}.dark .cm-s-xq span.cm-operator{}.dark .cm-s-xq span.cm-comment{color:gray}.dark .cm-s-xq span.cm-string{}.dark .cm-s-xq span.cm-meta{color:yellow}.dark .cm-s-xq span.cm-qualifier{color:#FFF700}.dark .cm-s-xq span.cm-builtin{color:#30a}.dark .cm-s-xq span.cm-bracket{color:#cc7}.dark .cm-s-xq span.cm-tag{color:#FFBD40}.dark .cm-s-xq span.cm-attribute{color:#FFF700}.dark .cm-s-xq span.cm-error{color:#e04141}.dark .cm-s-xq .CodeMirror-activeline-background{background:#27282E}.dark .cm-s-xq .CodeMirror-matchingbracket{outline:1px solid grey;color:white!important}.Line-Hover{transition:all .2s;}.Line-Hover:hover{background-color:rgb(4 48 143 / 5%)!important}.dark .Line-Hover:hover{background-color:rgb(0 0 0 / 20%)!important}.CodeMirror-foldmarker{color:#fc8800;text-shadow:#ffd8aa 1px 1px 2px,#ffd8aa -1px -1px 2px,#ffd8aa 1px -1px 2px,#ffd8aa -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer}.dark .CodeMirror-foldmarker{color:#ffffff;text-shadow:#bbb 1px 1px 2px,#bbb -1px -1px 2px,#bbb 1px -1px 2px,#bbb -1px 1px 2px;font-family:arial;line-height:.3;cursor:pointer} -------------------------------------------------------------------------------- /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 | if (config.data instanceof FormData) { 7 | config.headers['Content-Type'] = 'multipart/form-data'; 8 | } else { 9 | config.data = Qs.stringify(config.data, { 10 | arrayFormat: 'repeat', 11 | }); 12 | } 13 | return config; 14 | }, 15 | (error) => Promise.reject(error), 16 | ); 17 | 18 | axios.interceptors.response.use( 19 | (response) => response, 20 | (error) => { 21 | if (error.response) { 22 | const statusCode = error.response.status; 23 | // Check the status code 24 | if (statusCode === 401) { // Unauthorized 25 | return window.location.reload(); 26 | } 27 | } 28 | return Promise.reject(error); 29 | } 30 | ); 31 | -------------------------------------------------------------------------------- /web/assets/js/langs.js: -------------------------------------------------------------------------------- 1 | const supportLangs = [ 2 | { 3 | name: 'English', 4 | value: 'en-US', 5 | icon: '🇺🇸', 6 | }, 7 | { 8 | name: 'فارسی', 9 | value: 'fa-IR', 10 | icon: '🇮🇷', 11 | }, 12 | { 13 | name: '汉语', 14 | value: 'zh-Hans', 15 | icon: '🇨🇳', 16 | }, 17 | { 18 | name: 'Русский', 19 | value: 'ru-RU', 20 | icon: '🇷🇺', 21 | }, 22 | { 23 | name: 'Tiếng Việt', 24 | value: 'vi-VN', 25 | icon: '🇻🇳', 26 | }, 27 | ]; 28 | 29 | function getLang() { 30 | let lang = getCookie('lang'); 31 | 32 | if (!lang) { 33 | if (window.navigator) { 34 | lang = window.navigator.language || window.navigator.userLanguage; 35 | 36 | if (isSupportLang(lang)) { 37 | setCookie('lang', lang, 150); 38 | } else { 39 | setCookie('lang', 'en-US', 150); 40 | window.location.reload(); 41 | } 42 | } else { 43 | setCookie('lang', 'en-US', 150); 44 | window.location.reload(); 45 | } 46 | } 47 | 48 | return lang; 49 | } 50 | 51 | function setLang(lang) { 52 | if (!isSupportLang(lang)) { 53 | lang = 'en-US'; 54 | } 55 | 56 | setCookie('lang', lang, 150); 57 | window.location.reload(); 58 | } 59 | 60 | function isSupportLang(lang) { 61 | for (l of supportLangs) { 62 | if (l.value === lang) { 63 | return true; 64 | } 65 | } 66 | 67 | return false; 68 | } 69 | -------------------------------------------------------------------------------- /web/assets/js/model/dbinbound.js: -------------------------------------------------------------------------------- 1 | class DBInbound { 2 | 3 | constructor(data) { 4 | this.id = 0; 5 | this.userId = 0; 6 | this.up = 0; 7 | this.down = 0; 8 | this.total = 0; 9 | this.remark = ""; 10 | this.enable = true; 11 | this.expiryTime = 0; 12 | 13 | this.listen = ""; 14 | this.port = 0; 15 | this.protocol = ""; 16 | this.settings = ""; 17 | this.streamSettings = ""; 18 | this.tag = ""; 19 | this.sniffing = ""; 20 | this.clientStats = "" 21 | if (data == null) { 22 | return; 23 | } 24 | ObjectUtil.cloneProps(this, data); 25 | } 26 | 27 | get totalGB() { 28 | return toFixed(this.total / ONE_GB, 2); 29 | } 30 | 31 | set totalGB(gb) { 32 | this.total = toFixed(gb * ONE_GB, 0); 33 | } 34 | 35 | get isVMess() { 36 | return this.protocol === Protocols.VMESS; 37 | } 38 | 39 | get isVLess() { 40 | return this.protocol === Protocols.VLESS; 41 | } 42 | 43 | get isTrojan() { 44 | return this.protocol === Protocols.TROJAN; 45 | } 46 | 47 | get isSS() { 48 | return this.protocol === Protocols.SHADOWSOCKS; 49 | } 50 | 51 | get isSocks() { 52 | return this.protocol === Protocols.SOCKS; 53 | } 54 | 55 | get isHTTP() { 56 | return this.protocol === Protocols.HTTP; 57 | } 58 | 59 | get isWireguard() { 60 | return this.protocol === Protocols.WIREGUARD; 61 | } 62 | 63 | get address() { 64 | let address = location.hostname; 65 | if (!ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0") { 66 | address = this.listen; 67 | } 68 | return address; 69 | } 70 | 71 | get _expiryTime() { 72 | if (this.expiryTime === 0) { 73 | return null; 74 | } 75 | return moment(this.expiryTime); 76 | } 77 | 78 | set _expiryTime(t) { 79 | if (t == null) { 80 | this.expiryTime = 0; 81 | } else { 82 | this.expiryTime = t.valueOf(); 83 | } 84 | } 85 | 86 | get isExpiry() { 87 | return this.expiryTime < new Date().getTime(); 88 | } 89 | 90 | toInbound() { 91 | let settings = {}; 92 | if (!ObjectUtil.isEmpty(this.settings)) { 93 | settings = JSON.parse(this.settings); 94 | } 95 | 96 | let streamSettings = {}; 97 | if (!ObjectUtil.isEmpty(this.streamSettings)) { 98 | streamSettings = JSON.parse(this.streamSettings); 99 | } 100 | 101 | let sniffing = {}; 102 | if (!ObjectUtil.isEmpty(this.sniffing)) { 103 | sniffing = JSON.parse(this.sniffing); 104 | } 105 | 106 | const config = { 107 | port: this.port, 108 | listen: this.listen, 109 | protocol: this.protocol, 110 | settings: settings, 111 | streamSettings: streamSettings, 112 | tag: this.tag, 113 | sniffing: sniffing, 114 | clientStats: this.clientStats, 115 | }; 116 | return Inbound.fromJson(config); 117 | } 118 | 119 | isMultiUser() { 120 | switch (this.protocol) { 121 | case Protocols.VMESS: 122 | case Protocols.VLESS: 123 | case Protocols.TROJAN: 124 | return true; 125 | case Protocols.SHADOWSOCKS: 126 | return this.toInbound().isSSMultiUser; 127 | default: 128 | return false; 129 | } 130 | } 131 | 132 | hasLink() { 133 | switch (this.protocol) { 134 | case Protocols.VMESS: 135 | case Protocols.VLESS: 136 | case Protocols.TROJAN: 137 | case Protocols.SHADOWSOCKS: 138 | return true; 139 | default: 140 | return false; 141 | } 142 | } 143 | 144 | genInboundLinks(remarkModel) { 145 | const inbound = this.toInbound(); 146 | return inbound.genInboundLinks(this.remark,remarkModel); 147 | } 148 | } -------------------------------------------------------------------------------- /web/assets/js/model/setting.js: -------------------------------------------------------------------------------- 1 | class AllSetting { 2 | 3 | constructor(data) { 4 | this.webListen = ""; 5 | this.webDomain = ""; 6 | this.webPort = 54321; 7 | this.webCertFile = ""; 8 | this.webKeyFile = ""; 9 | this.webBasePath = "/"; 10 | this.sessionMaxAge = ""; 11 | this.pageSize = 0; 12 | this.expireDiff = ""; 13 | this.trafficDiff = ""; 14 | this.remarkModel = "-ieo"; 15 | this.tgBotEnable = false; 16 | this.tgBotToken = ""; 17 | this.tgBotChatId = ""; 18 | this.tgRunTime = "@daily"; 19 | this.tgBotBackup = false; 20 | this.tgBotLoginNotify = false; 21 | this.tgCpu = ""; 22 | this.tgLang = ""; 23 | this.subEnable = false; 24 | this.subListen = ""; 25 | this.subPort = "2096"; 26 | this.subPath = "/sub/"; 27 | this.subJsonPath = "/json/"; 28 | this.subDomain = ""; 29 | this.subCertFile = ""; 30 | this.subKeyFile = ""; 31 | this.subUpdates = 0; 32 | this.subEncrypt = true; 33 | this.subShowInfo = false; 34 | this.subURI = ""; 35 | this.subJsonURI = ""; 36 | this.subJsonFragment = ""; 37 | this.subJsonMux = ""; 38 | this.subJsonRules = ""; 39 | 40 | this.timeLocation = "Asia/Tehran"; 41 | 42 | if (data == null) { 43 | return 44 | } 45 | ObjectUtil.cloneProps(this, data); 46 | } 47 | 48 | equals(other) { 49 | return ObjectUtil.equals(this, other); 50 | } 51 | } -------------------------------------------------------------------------------- /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 cpuCoreFormat(cores) { 24 | if (cores === 1) { 25 | return "1 Core"; 26 | } else { 27 | return cores + " Cores"; 28 | } 29 | } 30 | 31 | function base64(str) { 32 | return Base64.encode(str); 33 | } 34 | 35 | function safeBase64(str) { 36 | return base64(str) 37 | .replace(/\+/g, '-') 38 | .replace(/=/g, '') 39 | .replace(/\//g, '_'); 40 | } 41 | 42 | function formatSecond(second) { 43 | if (second < 60) { 44 | return second.toFixed(0) + 's'; 45 | } else if (second < 3600) { 46 | return (second / 60).toFixed(0) + 'm'; 47 | } else if (second < 3600 * 24) { 48 | return (second / 3600).toFixed(0) + 'h'; 49 | } else { 50 | day = Math.floor(second / 3600 / 24); 51 | remain = ((second/3600) - (day*24)).toFixed(0); 52 | return day + 'd' + (remain > 0 ? ' ' + remain + 'h' : ''); 53 | } 54 | } 55 | 56 | function addZero(num) { 57 | if (num < 10) { 58 | return "0" + num; 59 | } else { 60 | return num; 61 | } 62 | } 63 | 64 | function toFixed(num, n) { 65 | n = Math.pow(10, n); 66 | return Math.floor(num * n) / n; 67 | } 68 | 69 | function debounce(fn, delay) { 70 | var timeoutID = null; 71 | return function () { 72 | clearTimeout(timeoutID); 73 | var args = arguments; 74 | var that = this; 75 | timeoutID = setTimeout(function () { 76 | fn.apply(that, args); 77 | }, delay); 78 | }; 79 | } 80 | 81 | function getCookie(cname) { 82 | let name = cname + "="; 83 | let decodedCookie = decodeURIComponent(document.cookie); 84 | let ca = decodedCookie.split(";"); 85 | for (let i = 0; i < ca.length; i++) { 86 | let c = ca[i]; 87 | while (c.charAt(0) == " ") { 88 | c = c.substring(1); 89 | } 90 | if (c.indexOf(name) == 0) { 91 | return c.substring(name.length, c.length); 92 | } 93 | } 94 | return ""; 95 | } 96 | 97 | function setCookie(cname, cvalue, exdays) { 98 | const d = new Date(); 99 | d.setTime(d.getTime() + exdays * 24 * 60 * 60 * 1000); 100 | let expires = "expires=" + d.toUTCString(); 101 | document.cookie = cname + "=" + cvalue + ";" + expires + ";path=/"; 102 | } 103 | 104 | function usageColor(data, threshold, total) { 105 | switch (true) { 106 | case data === null: 107 | return "green"; 108 | case total < 0: 109 | return "blue"; 110 | case total == 0: 111 | return "purple"; 112 | case data < total - threshold: 113 | return "blue"; 114 | case data < total: 115 | return "orange"; 116 | default: 117 | return "red"; 118 | } 119 | } 120 | 121 | function clientUsageColor(clientStats, trafficDiff) { 122 | switch (true) { 123 | case !clientStats || clientStats.total == 0: 124 | return "#7a316f"; // Purple 125 | case clientStats.up + clientStats.down < clientStats.total - trafficDiff: 126 | return "#0e49b5"; // Blue 127 | case clientStats.up + clientStats.down < clientStats.total: 128 | return "#f37b24"; // Orange 129 | default: 130 | return "#e04141"; // red 131 | } 132 | } 133 | 134 | function userExpiryColor(threshold, client, isDark = false) { 135 | if (!client.enable) { 136 | return isDark ? '#2c3950' : '#bcbcbc'; // Gray 137 | } 138 | now = new Date().getTime(), 139 | expiry = client.expiryTime; 140 | switch (true) { 141 | case expiry === null: 142 | return "#7a316f"; // Purple 143 | case expiry < 0: 144 | return "#0e49b5"; // Blue 145 | case expiry == 0: 146 | return "#7a316f"; // Purple 147 | case now < expiry - threshold: 148 | return "#0e49b5"; // Blue 149 | case now < expiry: 150 | return "#f37b24"; // Orange 151 | default: 152 | return "#e04141"; // red 153 | } 154 | } 155 | 156 | function doAllItemsExist(array1, array2) { 157 | for (let i = 0; i < array1.length; i++) { 158 | if (!array2.includes(array1[i])) { 159 | return false; 160 | } 161 | } 162 | return true; 163 | } 164 | 165 | function buildURL({ host, port, isTLS, base, path }) { 166 | if (!host || host.length === 0) host = window.location.hostname; 167 | if (!port || port.length === 0) port = window.location.port; 168 | 169 | if (isTLS === undefined) isTLS = window.location.protocol === "https:"; 170 | 171 | const protocol = isTLS ? "https:" : "http:"; 172 | 173 | port = String(port); 174 | if (port === "" || (isTLS && port === "443") || (!isTLS && port === "80")) { 175 | port = ""; 176 | } else { 177 | port = `:${port}`; 178 | } 179 | 180 | return `${protocol}//${host}${port}${base}${path}`; 181 | } 182 | -------------------------------------------------------------------------------- /web/assets/js/util/date-util.js: -------------------------------------------------------------------------------- 1 | const oneMinute = 1000 * 60; // 一The millise times of minutes 2 | const oneHour = oneMinute * 60; // 一Hours of millise times 3 | const oneDay = oneHour * 24; // 一Day's milliseconds 4 | const oneWeek = oneDay * 7; // 一Number of millise times on week 5 | const oneMonth = oneDay * 30; // 一Number of millise times a month 6 | 7 | /** 8 | * Decrease by day 9 | * 10 | * @param days A few days to reduce 11 | */ 12 | Date.prototype.minusDays = function (days) { 13 | return this.minusMillis(oneDay * days); 14 | }; 15 | 16 | /** 17 | * Increase by day 18 | * 19 | * @param days The number of days to be increased 20 | */ 21 | Date.prototype.plusDays = function (days) { 22 | return this.plusMillis(oneDay * days); 23 | }; 24 | 25 | /** 26 | * Reduced 27 | * 28 | * @param hours The number of hours to be reduced 29 | */ 30 | Date.prototype.minusHours = function (hours) { 31 | return this.minusMillis(oneHour * hours); 32 | }; 33 | 34 | /** 35 | * Increase 36 | * 37 | * @param hours Increase the number of hours 38 | */ 39 | Date.prototype.plusHours = function (hours) { 40 | return this.plusMillis(oneHour * hours); 41 | }; 42 | 43 | /** 44 | * Decrease by minute 45 | * 46 | * @param minutes The number of minutes to be reduced 47 | */ 48 | Date.prototype.minusMinutes = function (minutes) { 49 | return this.minusMillis(oneMinute * minutes); 50 | }; 51 | 52 | /** 53 | * Increase 54 | * 55 | * @param minutes The number of minutes to be increased 56 | */ 57 | Date.prototype.plusMinutes = function (minutes) { 58 | return this.plusMillis(oneMinute * minutes); 59 | }; 60 | 61 | /** 62 | * Decrease by millisecond 63 | * 64 | * @param millis Number of milliligues to be reduced 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 | * Add in milliseconds 75 | * 76 | * @param millis To increase the millimeter number 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 | * Setting time is the day 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 | * Setting time is the day 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 | * Formatting date 109 | */ 110 | Date.prototype.formatDate = function () { 111 | return this.getFullYear() + "-" + addZero(this.getMonth() + 1) + "-" + addZero(this.getDate()); 112 | }; 113 | 114 | /** 115 | * Formatting time 116 | */ 117 | Date.prototype.formatTime = function () { 118 | return addZero(this.getHours()) + ":" + addZero(this.getMinutes()) + ":" + addZero(this.getSeconds()); 119 | }; 120 | 121 | /** 122 | * Formatting date plus time 123 | * 124 | * @param split Division between date and time, the default is a space 125 | */ 126 | Date.prototype.formatDateTime = function (split = ' ') { 127 | return this.formatDate() + split + this.formatTime(); 128 | }; 129 | 130 | class DateUtil { 131 | // String string to date object 132 | static parseDate(str) { 133 | return new Date(str.replace(/-/g, '/')); 134 | } 135 | 136 | static formatMillis(millis) { 137 | return moment(millis).format('YYYY-M-D H:m:s'); 138 | } 139 | 140 | static firstDayOfMonth() { 141 | const date = new Date(); 142 | date.setDate(1); 143 | date.setMinTime(); 144 | return date; 145 | } 146 | } 147 | -------------------------------------------------------------------------------- /web/assets/vue/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/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/assets/vue/vue.runtime.mjs: -------------------------------------------------------------------------------- 1 | import Vue from './vue.runtime.common.js' 2 | export default Vue 3 | 4 | // this should be kept in sync with src/v3/index.ts 5 | export const { 6 | version, 7 | 8 | // refs 9 | ref, 10 | shallowRef, 11 | isRef, 12 | toRef, 13 | toRefs, 14 | unref, 15 | proxyRefs, 16 | customRef, 17 | triggerRef, 18 | computed, 19 | 20 | // reactive 21 | reactive, 22 | isReactive, 23 | isReadonly, 24 | isShallow, 25 | isProxy, 26 | shallowReactive, 27 | markRaw, 28 | toRaw, 29 | readonly, 30 | shallowReadonly, 31 | 32 | // watch 33 | watch, 34 | watchEffect, 35 | watchPostEffect, 36 | watchSyncEffect, 37 | 38 | // effectScope 39 | effectScope, 40 | onScopeDispose, 41 | getCurrentScope, 42 | 43 | // provide / inject 44 | provide, 45 | inject, 46 | 47 | // lifecycle 48 | onBeforeMount, 49 | onMounted, 50 | onBeforeUpdate, 51 | onUpdated, 52 | onBeforeUnmount, 53 | onUnmounted, 54 | onErrorCaptured, 55 | onActivated, 56 | onDeactivated, 57 | onServerPrefetch, 58 | onRenderTracked, 59 | onRenderTriggered, 60 | 61 | // v2 only 62 | set, 63 | del, 64 | 65 | // v3 compat 66 | h, 67 | getCurrentInstance, 68 | useSlots, 69 | useAttrs, 70 | mergeDefaults, 71 | nextTick, 72 | useCssModule, 73 | useCssVars, 74 | defineComponent, 75 | defineAsyncComponent 76 | } = Vue 77 | -------------------------------------------------------------------------------- /web/controller/api.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "x-ui/web/service" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type APIController struct { 10 | BaseController 11 | inboundController *InboundController 12 | Tgbot service.Tgbot 13 | } 14 | 15 | func NewAPIController(g *gin.RouterGroup) *APIController { 16 | a := &APIController{} 17 | a.initRouter(g) 18 | return a 19 | } 20 | 21 | func (a *APIController) initRouter(g *gin.RouterGroup) { 22 | g = g.Group("/xui/API/inbounds") 23 | g.Use(a.checkLogin) 24 | 25 | a.inboundController = NewInboundController(g) 26 | 27 | inboundRoutes := []struct { 28 | Method string 29 | Path string 30 | Handler gin.HandlerFunc 31 | }{ 32 | {"GET", "/createbackup", a.createBackup}, 33 | {"GET", "/", a.inboundController.getInbounds}, 34 | {"GET", "/get/:id", a.inboundController.getInbound}, 35 | {"GET", "/getClientTraffics/:email", a.inboundController.getClientTraffics}, 36 | {"POST", "/add", a.inboundController.addInbound}, 37 | {"POST", "/del/:id", a.inboundController.delInbound}, 38 | {"POST", "/update/:id", a.inboundController.updateInbound}, 39 | {"POST", "/addClient", a.inboundController.addInboundClient}, 40 | {"POST", "/:id/delClient/:clientId", a.inboundController.delInboundClient}, 41 | {"POST", "/updateClient/:clientId", a.inboundController.updateInboundClient}, 42 | {"POST", "/:id/resetClientTraffic/:email", a.inboundController.resetClientTraffic}, 43 | {"POST", "/resetAllTraffics", a.inboundController.resetAllTraffics}, 44 | {"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics}, 45 | {"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients}, 46 | {"POST", "/onlines", a.inboundController.onlines}, 47 | } 48 | 49 | for _, route := range inboundRoutes { 50 | g.Handle(route.Method, route.Path, route.Handler) 51 | } 52 | } 53 | 54 | func (a *APIController) createBackup(c *gin.Context) { 55 | a.Tgbot.SendBackupToAdmins() 56 | } 57 | -------------------------------------------------------------------------------- /web/controller/base.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | 6 | "x-ui/logger" 7 | "x-ui/web/locale" 8 | "x-ui/web/session" 9 | 10 | "github.com/gin-gonic/gin" 11 | ) 12 | 13 | type BaseController struct{} 14 | 15 | func (a *BaseController) checkLogin(c *gin.Context) { 16 | if !session.IsLogin(c) { 17 | if isAjax(c) { 18 | pureJsonMsg(c, http.StatusUnauthorized, false, I18nWeb(c, "pages.login.loginAgain")) 19 | } else { 20 | c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")) 21 | } 22 | c.Abort() 23 | } else { 24 | c.Next() 25 | } 26 | } 27 | 28 | func I18nWeb(c *gin.Context, name string, params ...string) string { 29 | anyfunc, funcExists := c.Get("I18n") 30 | if !funcExists { 31 | logger.Warning("I18n function not exists in gin context!") 32 | return "" 33 | } 34 | i18nFunc, _ := anyfunc.(func(i18nType locale.I18nType, key string, keyParams ...string) string) 35 | msg := i18nFunc(locale.Web, name, params...) 36 | return msg 37 | } 38 | -------------------------------------------------------------------------------- /web/controller/index.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net/http" 5 | "time" 6 | 7 | "x-ui/logger" 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 | settingService service.SettingService 23 | userService service.UserService 24 | tgbot service.Tgbot 25 | } 26 | 27 | func NewIndexController(g *gin.RouterGroup) *IndexController { 28 | a := &IndexController{} 29 | a.initRouter(g) 30 | return a 31 | } 32 | 33 | func (a *IndexController) initRouter(g *gin.RouterGroup) { 34 | g.GET("/", a.index) 35 | g.POST("/login", a.login) 36 | g.GET("/logout", a.logout) 37 | } 38 | 39 | func (a *IndexController) index(c *gin.Context) { 40 | if session.IsLogin(c) { 41 | c.Redirect(http.StatusTemporaryRedirect, "xui/") 42 | return 43 | } 44 | html(c, "login.html", "pages.login.title", nil) 45 | } 46 | 47 | func (a *IndexController) login(c *gin.Context) { 48 | var form LoginForm 49 | err := c.ShouldBind(&form) 50 | if err != nil { 51 | pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.invalidFormData")) 52 | return 53 | } 54 | if form.Username == "" { 55 | pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.emptyUsername")) 56 | return 57 | } 58 | if form.Password == "" { 59 | pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.emptyPassword")) 60 | return 61 | } 62 | 63 | user := a.userService.CheckUser(form.Username, form.Password) 64 | timeStr := time.Now().Format("2006-01-02 15:04:05") 65 | if user == nil { 66 | logger.Infof("wrong username or password: \"%s\" \"%s\"", form.Username, form.Password) 67 | a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 0) 68 | pureJsonMsg(c, http.StatusOK, false, I18nWeb(c, "pages.login.toasts.wrongUsernameOrPassword")) 69 | return 70 | } else { 71 | logger.Infof("%s login success ,Ip Address: %s\n", form.Username, getRemoteIp(c)) 72 | a.tgbot.UserLoginNotify(form.Username, getRemoteIp(c), timeStr, 1) 73 | } 74 | 75 | sessionMaxAge, err := a.settingService.GetSessionMaxAge() 76 | if err != nil { 77 | logger.Infof("Unable to get session's max age from DB") 78 | } 79 | 80 | if sessionMaxAge > 0 { 81 | err = session.SetMaxAge(c, sessionMaxAge*60) 82 | if err != nil { 83 | logger.Infof("Unable to set session's max age") 84 | } 85 | } 86 | 87 | err = session.SetLoginUser(c, user) 88 | logger.Info("user", user.Id, "login success") 89 | jsonMsg(c, I18nWeb(c, "pages.login.toasts.successLogin"), err) 90 | } 91 | 92 | func (a *IndexController) logout(c *gin.Context) { 93 | user := session.GetLoginUser(c) 94 | if user != nil { 95 | logger.Info("user", user.Id, "logout") 96 | } 97 | session.ClearSession(c) 98 | c.Redirect(http.StatusTemporaryRedirect, c.GetString("base_path")) 99 | } 100 | -------------------------------------------------------------------------------- /web/controller/server.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "fmt" 5 | "net/http" 6 | "regexp" 7 | "time" 8 | 9 | "x-ui/web/global" 10 | "x-ui/web/service" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | var filenameRegex = regexp.MustCompile(`^[a-zA-Z0-9_\-.]+$`) 16 | 17 | type ServerController struct { 18 | BaseController 19 | 20 | serverService service.ServerService 21 | 22 | lastStatus *service.Status 23 | lastGetStatusTime time.Time 24 | 25 | lastVersions []string 26 | lastGetVersionsTime time.Time 27 | } 28 | 29 | func NewServerController(g *gin.RouterGroup) *ServerController { 30 | a := &ServerController{ 31 | lastGetStatusTime: time.Now(), 32 | } 33 | a.initRouter(g) 34 | a.startTask() 35 | return a 36 | } 37 | 38 | func (a *ServerController) initRouter(g *gin.RouterGroup) { 39 | g = g.Group("/server") 40 | 41 | g.Use(a.checkLogin) 42 | g.POST("/status", a.status) 43 | g.POST("/getXrayVersion", a.getXrayVersion) 44 | g.POST("/stopXrayService", a.stopXrayService) 45 | g.POST("/restartXrayService", a.restartXrayService) 46 | g.POST("/installXray/:version", a.installXray) 47 | g.POST("/logs/:count", a.getLogs) 48 | g.POST("/getConfigJson", a.getConfigJson) 49 | g.GET("/getDb", a.getDb) 50 | g.POST("/importDB", a.importDB) 51 | g.POST("/getNewX25519Cert", a.getNewX25519Cert) 52 | } 53 | 54 | func (a *ServerController) refreshStatus() { 55 | a.lastStatus = a.serverService.GetStatus(a.lastStatus) 56 | } 57 | 58 | func (a *ServerController) startTask() { 59 | webServer := global.GetWebServer() 60 | c := webServer.GetCron() 61 | c.AddFunc("@every 2s", func() { 62 | now := time.Now() 63 | if now.Sub(a.lastGetStatusTime) > time.Minute*3 { 64 | return 65 | } 66 | a.refreshStatus() 67 | }) 68 | } 69 | 70 | func (a *ServerController) status(c *gin.Context) { 71 | a.lastGetStatusTime = time.Now() 72 | 73 | jsonObj(c, a.lastStatus, nil) 74 | } 75 | 76 | func (a *ServerController) getXrayVersion(c *gin.Context) { 77 | now := time.Now() 78 | if now.Sub(a.lastGetVersionsTime) <= time.Minute { 79 | jsonObj(c, a.lastVersions, nil) 80 | return 81 | } 82 | 83 | versions, err := a.serverService.GetXrayVersions() 84 | if err != nil { 85 | jsonMsg(c, I18nWeb(c, "getVersion"), err) 86 | return 87 | } 88 | 89 | a.lastVersions = versions 90 | a.lastGetVersionsTime = time.Now() 91 | 92 | jsonObj(c, versions, nil) 93 | } 94 | 95 | func (a *ServerController) installXray(c *gin.Context) { 96 | version := c.Param("version") 97 | err := a.serverService.UpdateXray(version) 98 | jsonMsg(c, I18nWeb(c, "install")+" xray", err) 99 | } 100 | 101 | func (a *ServerController) stopXrayService(c *gin.Context) { 102 | a.lastGetStatusTime = time.Now() 103 | err := a.serverService.StopXrayService() 104 | if err != nil { 105 | jsonMsg(c, "", err) 106 | return 107 | } 108 | jsonMsg(c, "Xray stoped", err) 109 | } 110 | 111 | func (a *ServerController) restartXrayService(c *gin.Context) { 112 | err := a.serverService.RestartXrayService() 113 | if err != nil { 114 | jsonMsg(c, "", err) 115 | return 116 | } 117 | jsonMsg(c, "Xray restarted", err) 118 | } 119 | 120 | func (a *ServerController) getLogs(c *gin.Context) { 121 | count := c.Param("count") 122 | level := c.PostForm("level") 123 | syslog := c.PostForm("syslog") 124 | logs := a.serverService.GetLogs(count, level, syslog) 125 | jsonObj(c, logs, nil) 126 | } 127 | 128 | func (a *ServerController) getConfigJson(c *gin.Context) { 129 | configJson, err := a.serverService.GetConfigJson() 130 | if err != nil { 131 | jsonMsg(c, "get config.json", err) 132 | return 133 | } 134 | jsonObj(c, configJson, nil) 135 | } 136 | 137 | func (a *ServerController) getDb(c *gin.Context) { 138 | db, err := a.serverService.GetDb() 139 | if err != nil { 140 | jsonMsg(c, "get Database", err) 141 | return 142 | } 143 | 144 | filename := "x-ui.db" 145 | 146 | if !filenameRegex.MatchString(filename) { 147 | c.AbortWithError(http.StatusBadRequest, fmt.Errorf("invalid filename")) 148 | return 149 | } 150 | 151 | // Set the headers for the response 152 | c.Header("Content-Type", "application/octet-stream") 153 | c.Header("Content-Disposition", "attachment; filename="+filename) 154 | 155 | // Write the file contents to the response 156 | c.Writer.Write(db) 157 | } 158 | 159 | func (a *ServerController) importDB(c *gin.Context) { 160 | // Get the file from the request body 161 | file, _, err := c.Request.FormFile("db") 162 | if err != nil { 163 | jsonMsg(c, "Error reading db file", err) 164 | return 165 | } 166 | defer file.Close() 167 | // Always restart Xray before return 168 | defer a.serverService.RestartXrayService() 169 | defer func() { 170 | a.lastGetStatusTime = time.Now() 171 | }() 172 | // Import it 173 | err = a.serverService.ImportDB(file) 174 | if err != nil { 175 | jsonMsg(c, "", err) 176 | return 177 | } 178 | jsonObj(c, "Import DB", nil) 179 | } 180 | 181 | func (a *ServerController) getNewX25519Cert(c *gin.Context) { 182 | cert, err := a.serverService.GetNewX25519Cert() 183 | if err != nil { 184 | jsonMsg(c, "get x25519 certificate", err) 185 | return 186 | } 187 | jsonObj(c, cert, nil) 188 | } 189 | -------------------------------------------------------------------------------- /web/controller/setting.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "errors" 5 | "time" 6 | 7 | "x-ui/web/entity" 8 | "x-ui/web/service" 9 | "x-ui/web/session" 10 | 11 | "github.com/gin-gonic/gin" 12 | ) 13 | 14 | type updateUserForm struct { 15 | OldUsername string `json:"oldUsername" form:"oldUsername"` 16 | OldPassword string `json:"oldPassword" form:"oldPassword"` 17 | NewUsername string `json:"newUsername" form:"newUsername"` 18 | NewPassword string `json:"newPassword" form:"newPassword"` 19 | } 20 | 21 | type SettingController struct { 22 | settingService service.SettingService 23 | userService service.UserService 24 | panelService service.PanelService 25 | } 26 | 27 | func NewSettingController(g *gin.RouterGroup) *SettingController { 28 | a := &SettingController{} 29 | a.initRouter(g) 30 | return a 31 | } 32 | 33 | func (a *SettingController) initRouter(g *gin.RouterGroup) { 34 | g = g.Group("/setting") 35 | 36 | g.POST("/all", a.getAllSetting) 37 | g.POST("/defaultSettings", a.getDefaultSettings) 38 | g.POST("/update", a.updateSetting) 39 | g.POST("/updateUser", a.updateUser) 40 | g.POST("/restartPanel", a.restartPanel) 41 | g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) 42 | } 43 | 44 | func (a *SettingController) getAllSetting(c *gin.Context) { 45 | allSetting, err := a.settingService.GetAllSetting() 46 | if err != nil { 47 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) 48 | return 49 | } 50 | jsonObj(c, allSetting, nil) 51 | } 52 | 53 | func (a *SettingController) getDefaultSettings(c *gin.Context) { 54 | result, err := a.settingService.GetDefaultSettings(c.Request.Host) 55 | if err != nil { 56 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) 57 | return 58 | } 59 | jsonObj(c, result, nil) 60 | } 61 | 62 | func (a *SettingController) updateSetting(c *gin.Context) { 63 | allSetting := &entity.AllSetting{} 64 | err := c.ShouldBind(allSetting) 65 | if err != nil { 66 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) 67 | return 68 | } 69 | err = a.settingService.UpdateAllSetting(allSetting) 70 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) 71 | } 72 | 73 | func (a *SettingController) updateUser(c *gin.Context) { 74 | form := &updateUserForm{} 75 | err := c.ShouldBind(form) 76 | if err != nil { 77 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) 78 | return 79 | } 80 | user := session.GetLoginUser(c) 81 | if user.Username != form.OldUsername || user.Password != form.OldPassword { 82 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.originalUserPassIncorrect"))) 83 | return 84 | } 85 | if form.NewUsername == "" || form.NewPassword == "" { 86 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), errors.New(I18nWeb(c, "pages.settings.toasts.userPassMustBeNotEmpty"))) 87 | return 88 | } 89 | err = a.userService.UpdateUser(user.Id, form.NewUsername, form.NewPassword) 90 | if err == nil { 91 | user.Username = form.NewUsername 92 | user.Password = form.NewPassword 93 | session.SetLoginUser(c, user) 94 | } 95 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifyUser"), err) 96 | } 97 | 98 | func (a *SettingController) restartPanel(c *gin.Context) { 99 | err := a.panelService.RestartPanel(time.Second * 3) 100 | jsonMsg(c, I18nWeb(c, "pages.settings.restartPanel"), err) 101 | } 102 | 103 | func (a *SettingController) getDefaultXrayConfig(c *gin.Context) { 104 | defaultJsonConfig, err := a.settingService.GetDefaultXrayConfig() 105 | if err != nil { 106 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) 107 | return 108 | } 109 | jsonObj(c, defaultJsonConfig, nil) 110 | } 111 | -------------------------------------------------------------------------------- /web/controller/util.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | "strings" 7 | 8 | "x-ui/config" 9 | "x-ui/logger" 10 | "x-ui/web/entity" 11 | 12 | "github.com/gin-gonic/gin" 13 | ) 14 | 15 | func getRemoteIp(c *gin.Context) string { 16 | value := c.GetHeader("X-Forwarded-For") 17 | if value != "" { 18 | ips := strings.Split(value, ",") 19 | return ips[0] 20 | } else { 21 | addr := c.Request.RemoteAddr 22 | ip, _, _ := net.SplitHostPort(addr) 23 | return ip 24 | } 25 | } 26 | 27 | func jsonMsg(c *gin.Context, msg string, err error) { 28 | jsonMsgObj(c, msg, nil, err) 29 | } 30 | 31 | func jsonObj(c *gin.Context, obj interface{}, err error) { 32 | jsonMsgObj(c, "", obj, err) 33 | } 34 | 35 | func jsonMsgObj(c *gin.Context, msg string, obj interface{}, err error) { 36 | m := entity.Msg{ 37 | Obj: obj, 38 | } 39 | if err == nil { 40 | m.Success = true 41 | if msg != "" { 42 | m.Msg = msg + I18nWeb(c, "success") 43 | } 44 | } else { 45 | m.Success = false 46 | m.Msg = msg + I18nWeb(c, "fail") + ": " + err.Error() 47 | logger.Warning(msg+I18nWeb(c, "fail")+": ", err) 48 | } 49 | c.JSON(http.StatusOK, m) 50 | } 51 | 52 | func pureJsonMsg(c *gin.Context, statusCode int, success bool, msg string) { 53 | c.JSON(statusCode, entity.Msg{ 54 | Success: success, 55 | Msg: msg, 56 | }) 57 | } 58 | 59 | func html(c *gin.Context, name string, title string, data gin.H) { 60 | if data == nil { 61 | data = gin.H{} 62 | } 63 | data["title"] = title 64 | data["host"], _, _ = net.SplitHostPort(c.Request.Host) 65 | data["request_uri"] = c.Request.RequestURI 66 | data["base_path"] = c.GetString("base_path") 67 | c.HTML(http.StatusOK, name, getContext(data)) 68 | } 69 | 70 | func getContext(h gin.H) gin.H { 71 | a := gin.H{ 72 | "cur_ver": config.GetVersion(), 73 | } 74 | for key, value := range h { 75 | a[key] = value 76 | } 77 | return a 78 | } 79 | 80 | func isAjax(c *gin.Context) bool { 81 | return c.GetHeader("X-Requested-With") == "XMLHttpRequest" 82 | } 83 | -------------------------------------------------------------------------------- /web/controller/xray_setting.go: -------------------------------------------------------------------------------- 1 | package controller 2 | 3 | import ( 4 | "x-ui/web/service" 5 | 6 | "github.com/gin-gonic/gin" 7 | ) 8 | 9 | type XraySettingController struct { 10 | XraySettingService service.XraySettingService 11 | SettingService service.SettingService 12 | InboundService service.InboundService 13 | XrayService service.XrayService 14 | } 15 | 16 | func NewXraySettingController(g *gin.RouterGroup) *XraySettingController { 17 | a := &XraySettingController{} 18 | a.initRouter(g) 19 | return a 20 | } 21 | 22 | func (a *XraySettingController) initRouter(g *gin.RouterGroup) { 23 | g = g.Group("/xray") 24 | 25 | g.POST("/", a.getXraySetting) 26 | g.POST("/update", a.updateSetting) 27 | g.GET("/getXrayResult", a.getXrayResult) 28 | g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) 29 | g.POST("/warp/:action", a.warp) 30 | } 31 | 32 | func (a *XraySettingController) getXraySetting(c *gin.Context) { 33 | xraySetting, err := a.SettingService.GetXrayConfigTemplate() 34 | if err != nil { 35 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) 36 | return 37 | } 38 | inboundTags, err := a.InboundService.GetInboundTags() 39 | if err != nil { 40 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) 41 | return 42 | } 43 | xrayResponse := "{ \"xraySetting\": " + xraySetting + ", \"inboundTags\": " + inboundTags + " }" 44 | jsonObj(c, xrayResponse, nil) 45 | } 46 | 47 | func (a *XraySettingController) updateSetting(c *gin.Context) { 48 | xraySetting := c.PostForm("xraySetting") 49 | err := a.XraySettingService.SaveXraySetting(xraySetting) 50 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.modifySettings"), err) 51 | } 52 | 53 | func (a *XraySettingController) getDefaultXrayConfig(c *gin.Context) { 54 | defaultJsonConfig, err := a.SettingService.GetDefaultXrayConfig() 55 | if err != nil { 56 | jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) 57 | return 58 | } 59 | jsonObj(c, defaultJsonConfig, nil) 60 | } 61 | 62 | func (a *XraySettingController) getXrayResult(c *gin.Context) { 63 | jsonObj(c, a.XrayService.GetXrayResult(), nil) 64 | } 65 | 66 | func (a *XraySettingController) warp(c *gin.Context) { 67 | action := c.Param("action") 68 | var resp string 69 | var err error 70 | switch action { 71 | case "data": 72 | resp, err = a.XraySettingService.GetWarp() 73 | case "config": 74 | resp, err = a.XraySettingService.GetWarpConfig() 75 | case "reg": 76 | skey := c.PostForm("privateKey") 77 | pkey := c.PostForm("publicKey") 78 | resp, err = a.XraySettingService.RegWarp(skey, pkey) 79 | case "license": 80 | license := c.PostForm("license") 81 | resp, err = a.XraySettingService.SetWarpLicence(license) 82 | } 83 | 84 | jsonObj(c, resp, err) 85 | } 86 | -------------------------------------------------------------------------------- /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 | xraySettingController *XraySettingController 13 | } 14 | 15 | func NewXUIController(g *gin.RouterGroup) *XUIController { 16 | a := &XUIController{} 17 | a.initRouter(g) 18 | return a 19 | } 20 | 21 | func (a *XUIController) initRouter(g *gin.RouterGroup) { 22 | g = g.Group("/xui") 23 | g.Use(a.checkLogin) 24 | 25 | g.GET("/", a.index) 26 | g.GET("/inbounds", a.inbounds) 27 | g.GET("/settings", a.settings) 28 | g.GET("/xray", a.xraySettings) 29 | 30 | a.inboundController = NewInboundController(g) 31 | a.settingController = NewSettingController(g) 32 | a.xraySettingController = NewXraySettingController(g) 33 | } 34 | 35 | func (a *XUIController) index(c *gin.Context) { 36 | html(c, "index.html", "pages.index.title", nil) 37 | } 38 | 39 | func (a *XUIController) inbounds(c *gin.Context) { 40 | html(c, "inbounds.html", "pages.inbounds.title", nil) 41 | } 42 | 43 | func (a *XUIController) settings(c *gin.Context) { 44 | html(c, "settings.html", "pages.settings.title", nil) 45 | } 46 | 47 | func (a *XUIController) xraySettings(c *gin.Context) { 48 | html(c, "xray.html", "pages.xray.title", nil) 49 | } 50 | -------------------------------------------------------------------------------- /web/entity/entity.go: -------------------------------------------------------------------------------- 1 | package entity 2 | 3 | import ( 4 | "crypto/tls" 5 | "net" 6 | "strings" 7 | "time" 8 | 9 | "x-ui/util/common" 10 | ) 11 | 12 | type Msg struct { 13 | Success bool `json:"success"` 14 | Msg string `json:"msg"` 15 | Obj interface{} `json:"obj"` 16 | } 17 | 18 | type AllSetting struct { 19 | WebListen string `json:"webListen" form:"webListen"` 20 | WebDomain string `json:"webDomain" form:"webDomain"` 21 | WebPort int `json:"webPort" form:"webPort"` 22 | WebCertFile string `json:"webCertFile" form:"webCertFile"` 23 | WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` 24 | WebBasePath string `json:"webBasePath" form:"webBasePath"` 25 | SessionMaxAge int `json:"sessionMaxAge" form:"sessionMaxAge"` 26 | PageSize int `json:"pageSize" form:"pageSize"` 27 | ExpireDiff int `json:"expireDiff" form:"expireDiff"` 28 | TrafficDiff int `json:"trafficDiff" form:"trafficDiff"` 29 | RemarkModel string `json:"remarkModel" form:"remarkModel"` 30 | TgBotEnable bool `json:"tgBotEnable" form:"tgBotEnable"` 31 | TgBotToken string `json:"tgBotToken" form:"tgBotToken"` 32 | TgBotChatId string `json:"tgBotChatId" form:"tgBotChatId"` 33 | TgRunTime string `json:"tgRunTime" form:"tgRunTime"` 34 | TgBotBackup bool `json:"tgBotBackup" form:"tgBotBackup"` 35 | TgBotLoginNotify bool `json:"tgBotLoginNotify" form:"tgBotLoginNotify"` 36 | TgCpu int `json:"tgCpu" form:"tgCpu"` 37 | TgLang string `json:"tgLang" form:"tgLang"` 38 | TimeLocation string `json:"timeLocation" form:"timeLocation"` 39 | SubEnable bool `json:"subEnable" form:"subEnable"` 40 | SubListen string `json:"subListen" form:"subListen"` 41 | SubPort int `json:"subPort" form:"subPort"` 42 | SubPath string `json:"subPath" form:"subPath"` 43 | SubDomain string `json:"subDomain" form:"subDomain"` 44 | SubCertFile string `json:"subCertFile" form:"subCertFile"` 45 | SubKeyFile string `json:"subKeyFile" form:"subKeyFile"` 46 | SubUpdates int `json:"subUpdates" form:"subUpdates"` 47 | SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"` 48 | SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"` 49 | SubURI string `json:"subURI" form:"subURI"` 50 | SubJsonPath string `json:"subJsonPath" form:"subJsonPath"` 51 | SubJsonURI string `json:"subJsonURI" form:"subJsonURI"` 52 | SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` 53 | SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` 54 | SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` 55 | } 56 | 57 | func (s *AllSetting) CheckValid() error { 58 | if s.WebListen != "" { 59 | ip := net.ParseIP(s.WebListen) 60 | if ip == nil { 61 | return common.NewError("web listen is not valid ip:", s.WebListen) 62 | } 63 | } 64 | 65 | if s.SubListen != "" { 66 | ip := net.ParseIP(s.SubListen) 67 | if ip == nil { 68 | return common.NewError("Sub listen is not valid ip:", s.SubListen) 69 | } 70 | } 71 | 72 | if s.WebPort <= 0 || s.WebPort > 65535 { 73 | return common.NewError("web port is not a valid port:", s.WebPort) 74 | } 75 | 76 | if s.SubPort <= 0 || s.SubPort > 65535 { 77 | return common.NewError("Sub port is not a valid port:", s.SubPort) 78 | } 79 | 80 | if s.SubPort == s.WebPort { 81 | return common.NewError("Sub and Web could not use same port:", s.SubPort) 82 | } 83 | 84 | if s.WebCertFile != "" || s.WebKeyFile != "" { 85 | _, err := tls.LoadX509KeyPair(s.WebCertFile, s.WebKeyFile) 86 | if err != nil { 87 | return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.WebCertFile, s.WebKeyFile, err) 88 | } 89 | } 90 | 91 | if s.SubCertFile != "" || s.SubKeyFile != "" { 92 | _, err := tls.LoadX509KeyPair(s.SubCertFile, s.SubKeyFile) 93 | if err != nil { 94 | return common.NewErrorf("cert file <%v> or key file <%v> invalid: %v", s.SubCertFile, s.SubKeyFile, err) 95 | } 96 | } 97 | 98 | if !strings.HasPrefix(s.WebBasePath, "/") { 99 | s.WebBasePath = "/" + s.WebBasePath 100 | } 101 | if !strings.HasSuffix(s.WebBasePath, "/") { 102 | s.WebBasePath += "/" 103 | } 104 | 105 | if !strings.HasPrefix(s.SubPath, "/") { 106 | s.SubPath = "/" + s.SubPath 107 | } 108 | if !strings.HasSuffix(s.SubPath, "/") { 109 | s.SubPath += "/" 110 | } 111 | 112 | if !strings.HasPrefix(s.SubJsonPath, "/") { 113 | s.SubJsonPath = "/" + s.SubJsonPath 114 | } 115 | if !strings.HasSuffix(s.SubJsonPath, "/") { 116 | s.SubJsonPath += "/" 117 | } 118 | 119 | _, err := time.LoadLocation(s.TimeLocation) 120 | if err != nil { 121 | return common.NewError("time location not exist:", s.TimeLocation) 122 | } 123 | 124 | return nil 125 | } 126 | -------------------------------------------------------------------------------- /web/global/global.go: -------------------------------------------------------------------------------- 1 | package global 2 | 3 | import ( 4 | "context" 5 | _ "unsafe" 6 | 7 | "github.com/robfig/cron/v3" 8 | ) 9 | 10 | var ( 11 | webServer WebServer 12 | subServer SubServer 13 | ) 14 | 15 | type WebServer interface { 16 | GetCron() *cron.Cron 17 | GetCtx() context.Context 18 | } 19 | 20 | type SubServer interface { 21 | GetCtx() context.Context 22 | } 23 | 24 | func SetWebServer(s WebServer) { 25 | webServer = s 26 | } 27 | 28 | func GetWebServer() WebServer { 29 | return webServer 30 | } 31 | 32 | func SetSubServer(s SubServer) { 33 | subServer = s 34 | } 35 | 36 | func GetSubServer() SubServer { 37 | return subServer 38 | } 39 | -------------------------------------------------------------------------------- /web/html/common/head.html: -------------------------------------------------------------------------------- 1 | {{define "head"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 29 | {{ .host }}-{{ i18n .title}} 30 | 31 |
32 | {{end}} -------------------------------------------------------------------------------- /web/html/common/js.html: -------------------------------------------------------------------------------- 1 | {{define "js"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 16 | {{end}} -------------------------------------------------------------------------------- /web/html/common/prompt_modal.html: -------------------------------------------------------------------------------- 1 | {{define "promptModal"}} 2 | 6 | 11 | 12 | 13 | 71 | {{end}} -------------------------------------------------------------------------------- /web/html/common/qrcode_modal.html: -------------------------------------------------------------------------------- 1 | {{define "qrcodeModal"}} 2 | 6 | {{ i18n "pages.inbounds.clickOnQRcode" }} 7 | 19 | {{ i18n "pages.inbounds.client" }} 20 | 26 | 27 | 28 | 110 | {{end}} -------------------------------------------------------------------------------- /web/html/common/text_modal.html: -------------------------------------------------------------------------------- 1 | {{define "textModal"}} 2 | 5 | 12 | 14 | 15 | 16 | 56 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/common_sider.html: -------------------------------------------------------------------------------- 1 | {{define "menuItems"}} 2 | 3 | 4 | {{ i18n "menu.dashboard"}} 5 | 6 | 7 | 8 | {{ i18n "menu.inbounds"}} 9 | 10 | 11 | 12 | {{ i18n "menu.settings"}} 13 | 14 | 15 | 16 | {{ i18n "menu.xray"}} 17 | 18 | 19 | 20 | {{ i18n "menu.logout"}} 21 | 22 | {{end}} 23 | 24 | 25 | {{define "commonSider"}} 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 35 | {{template "menuItems" .}} 36 | 37 | 38 | 42 |
43 | 44 |
45 | 46 | 47 | 48 | 49 | 50 | 51 | 53 | {{template "menuItems" .}} 54 | 55 |
56 | 71 | {{end}} 72 | -------------------------------------------------------------------------------- /web/html/xui/component/password.html: -------------------------------------------------------------------------------- 1 | {{define "component/passwordInput"}} 2 | 16 | {{end}} 17 | 18 | {{define "component/password"}} 19 | 35 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/component/setting.html: -------------------------------------------------------------------------------- 1 | {{define "component/settingListItem"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 17 | 20 | 23 | 24 | 25 | 26 | {{end}} 27 | 28 | {{define "component/setting"}} 29 | 35 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/component/themeSwitch.html: -------------------------------------------------------------------------------- 1 | {{define "component/themeSwitchTemplate"}} 2 | 7 | {{end}} 8 | 9 | {{define "component/themeSwitcher"}} 10 | 39 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/dns_modal.html: -------------------------------------------------------------------------------- 1 | {{define "dnsModal"}} 2 | 5 | 6 | 7 | 8 | 9 | 10 | + 11 | 16 | 17 | 18 | 22 | 23 | [[ l ]] 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 96 | {{end}} 97 | -------------------------------------------------------------------------------- /web/html/xui/fakedns_modal.html: -------------------------------------------------------------------------------- 1 | {{define "fakednsModal"}} 2 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 57 | {{end}} 58 | -------------------------------------------------------------------------------- /web/html/xui/form/inbound.html: -------------------------------------------------------------------------------- 1 | {{define "form/inbound"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | [[ p ]] 14 | 15 | 16 | 17 | 18 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 44 | GB 45 | 46 | 47 | 48 | 57 | 60 | 61 | 62 | 63 | 64 | 67 | 68 | 69 | 72 | 73 | 74 | 77 | 78 | 79 | 82 | 83 | 84 | 87 | 88 | 89 | 92 | 93 | 94 | 97 | 98 | 99 | 102 | 103 | 104 | 108 | 109 | 110 | 113 | 114 | 115 | 118 | {{end}} 119 | -------------------------------------------------------------------------------- /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 | 18 | 19 | 20 | 21 | 22 | 23 | {{end}} 24 | -------------------------------------------------------------------------------- /web/html/xui/form/protocol/http.html: -------------------------------------------------------------------------------- 1 | {{define "form/http"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 |
{{ i18n "username" }}{{ i18n "password" }}+
10 | 11 | 12 | 13 | 14 | 15 | 18 | 19 | 20 |
21 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/protocol/shadowsocks.html: -------------------------------------------------------------------------------- 1 | {{define "form/shadowsocks"}} 2 | 23 | 24 | 25 | 26 | [[ method_name ]] 27 | 28 | 29 | 30 | 39 | 40 | 41 | 42 | 43 | TCP+UDP 44 | TCP 45 | UDP 46 | 47 | 48 | 49 | 50 | {{end}} 51 | -------------------------------------------------------------------------------- /web/html/xui/form/protocol/socks.html: -------------------------------------------------------------------------------- 1 | {{define "form/socks"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 12 | 13 | 32 | 33 | {{end}} 34 | -------------------------------------------------------------------------------- /web/html/xui/form/protocol/trojan.html: -------------------------------------------------------------------------------- 1 | {{define "form/trojan"}} 2 | 3 | 4 | {{template "form/client"}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
{{ i18n "pages.inbounds.email" }}Password
[[ client.email ]][[ client.password ]]
19 |
20 |
21 | 54 | {{end}} 55 | -------------------------------------------------------------------------------- /web/html/xui/form/protocol/vless.html: -------------------------------------------------------------------------------- 1 | {{define "form/vless"}} 2 | 3 | 4 | {{template "form/client"}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 |
{{ i18n "pages.inbounds.email" }}FlowID
[[ client.email ]][[ client.flow ]][[ client.id ]]
21 |
22 |
23 | 56 | {{end}} 57 | -------------------------------------------------------------------------------- /web/html/xui/form/protocol/vmess.html: -------------------------------------------------------------------------------- 1 | {{define "form/vmess"}} 2 | 3 | 4 | {{template "form/client"}} 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 |
{{ i18n "pages.inbounds.email" }}ID
[[ client.email ]][[ client.id ]]
19 |
20 |
21 | {{end}} 22 | -------------------------------------------------------------------------------- /web/html/xui/form/protocol/wireguard.html: -------------------------------------------------------------------------------- 1 | {{define "form/wireguard"}} 2 | 3 | 4 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | + 28 | 29 | 30 | 31 | Peer [[ index + 1 ]] 32 | 34 | 35 | 36 | 45 | 46 | 47 | 48 | 51 | 52 | 53 | 54 | 63 | 64 | 65 | 66 | 69 | 74 | 75 | 76 | 77 | 78 | 79 | 80 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/sniffing.html: -------------------------------------------------------------------------------- 1 | {{define "form/sniffing"}} 2 | 3 | 4 | 5 | 6 | Sniffing 7 | 8 | 11 | 12 | 13 | 14 | 15 | 16 | 29 | 30 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/external_proxy.html: -------------------------------------------------------------------------------- 1 | {{define "form/externalProxy"}} 2 | 3 | 4 | 5 | 6 | + 7 | 8 | 9 | 18 | 19 | 20 | 21 | 22 | 23 | - 24 | 25 | 26 | {{end}} 27 | -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_grpc.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamGRPC"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | {{end}} 14 | -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_http.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamHTTP"}} 2 | 3 | 4 | 5 | 6 | 7 | 10 | 17 | 18 | 19 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_httpupgrade.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamHTTPUPGRADE"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | + 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | - 22 | 23 | 24 | 25 | 26 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_kcp.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamKCP"}} 2 | 3 | 4 | 5 | None 6 | SRTP 7 | uTP 8 | WeChat 9 | DTLS 1.2 10 | WireGuard 11 | DNS 12 | 13 | 14 | 15 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 44 | 45 | 46 | 47 | 48 | {{end}} 49 | -------------------------------------------------------------------------------- /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 | 20 | 21 | 22 | 23 | 24 | None 25 | SRTP 26 | uTP 27 | WeChat 28 | DTLS 1.2 29 | WireGuard 30 | 31 | 32 | 33 | {{end}} 34 | -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_settings.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamSettings"}} 2 | 3 | 4 | 5 | 7 | TCP 8 | mKCP 9 | WebSocket 10 | HTTP/2 11 | QUIC 12 | gRPC 13 | HttpUpgrade 14 | SplitHTTP 15 | 16 | 17 | 18 | 19 | 20 | 23 | 24 | 25 | 28 | 29 | 30 | 33 | 34 | 35 | 38 | 39 | 40 | 43 | 44 | 45 | 48 | 49 | 50 | 53 | 54 | 55 | 58 | 59 | 60 | 63 | {{end}} 64 | -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_sockopt.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamSockopt"}} 2 | 3 | 4 | 5 | 6 | 7 | 25 | 26 | {{end}} 27 | -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_splithttp.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamSplitHTTP"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | - 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | {{end}} 30 | -------------------------------------------------------------------------------- /web/html/xui/form/stream/stream_tcp.html: -------------------------------------------------------------------------------- 1 | {{define "form/streamTCP"}} 2 | 3 | 4 | 5 | 6 | 7 | 8 | 10 | 11 | 12 | 13 | 14 | 16 | 17 | {{ i18n "pages.inbounds.stream.general.request" }} 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 28 | 34 | 35 | 36 | + 37 | 38 | 39 | 40 | 42 | 43 | 44 | 46 | - 48 | 49 | 50 | 51 | 52 | 53 | {{ i18n "pages.inbounds.stream.general.response" }} 54 | 55 | 56 | 57 | 58 | 59 | 60 | 61 | 62 | 63 | 64 | + 66 | 67 | 68 | 69 | 71 | 72 | 73 | 75 | 78 | 79 | 80 | 81 | 82 | {{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 | + 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | - 22 | 23 | 24 | 25 | 26 | {{end}} 27 | -------------------------------------------------------------------------------- /web/html/xui/xray_balancer_modal.html: -------------------------------------------------------------------------------- 1 | {{define "balancerModal"}} 2 | 14 | 15 | 17 | 19 | 20 | 21 | 22 | Random 23 | Round Robin 24 | Least Load 25 | Least Ping 26 | 27 | 28 | 29 | 31 | [[ tag ]] 32 | 33 | 34 | 35 | 36 | 37 | 114 | {{end}} -------------------------------------------------------------------------------- /web/html/xui/xray_outbound_modal.html: -------------------------------------------------------------------------------- 1 | {{define "outModal"}} 2 | 6 | {{template "form/outbound"}} 7 | 8 | 127 | {{end}} 128 | -------------------------------------------------------------------------------- /web/job/check_cpu_usage.go: -------------------------------------------------------------------------------- 1 | package job 2 | 3 | import ( 4 | "strconv" 5 | "time" 6 | 7 | "x-ui/web/service" 8 | 9 | "github.com/shirou/gopsutil/v4/cpu" 10 | ) 11 | 12 | type CheckCpuJob struct { 13 | tgbotService service.Tgbot 14 | settingService service.SettingService 15 | } 16 | 17 | func NewCheckCpuJob() *CheckCpuJob { 18 | return new(CheckCpuJob) 19 | } 20 | 21 | // Here run is a interface method of Job interface 22 | func (j *CheckCpuJob) Run() { 23 | threshold, _ := j.settingService.GetTgCpu() 24 | 25 | // get latest status of server 26 | percent, err := cpu.Percent(1*time.Second, false) 27 | if err == nil && percent[0] > float64(threshold) { 28 | msg := j.tgbotService.I18nBot("tgbot.messages.cpuThreshold", 29 | "Percent=="+strconv.FormatFloat(percent[0], 'f', 2, 64), 30 | "Threshold=="+strconv.Itoa(threshold)) 31 | 32 | j.tgbotService.SendMsgToTgbotAdmins(msg) 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /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 | "x-ui/web/service" 5 | ) 6 | 7 | type LoginStatus byte 8 | 9 | const ( 10 | LoginSuccess LoginStatus = 1 11 | LoginFail LoginStatus = 0 12 | ) 13 | 14 | type StatsNotifyJob struct { 15 | xrayService service.XrayService 16 | tgbotService service.Tgbot 17 | } 18 | 19 | func NewStatsNotifyJob() *StatsNotifyJob { 20 | return new(StatsNotifyJob) 21 | } 22 | 23 | // Here run is a interface method of Job interface 24 | func (j *StatsNotifyJob) Run() { 25 | if !j.xrayService.IsXrayRunning() { 26 | return 27 | } 28 | j.tgbotService.SendReport() 29 | } 30 | -------------------------------------------------------------------------------- /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 | 22 | traffics, clientTraffics, err := j.xrayService.GetXrayTraffic() 23 | if err != nil { 24 | logger.Warning("get xray traffic failed:", err) 25 | return 26 | } 27 | err, needRestart := j.inboundService.AddTraffic(traffics, clientTraffics) 28 | if err != nil { 29 | logger.Warning("add traffic failed:", err) 30 | } 31 | if needRestart { 32 | j.xrayService.SetToNeedRestart() 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /web/locale/locale.go: -------------------------------------------------------------------------------- 1 | package locale 2 | 3 | import ( 4 | "embed" 5 | "io/fs" 6 | "strings" 7 | 8 | "x-ui/logger" 9 | 10 | "github.com/gin-gonic/gin" 11 | "github.com/nicksnyder/go-i18n/v2/i18n" 12 | "github.com/pelletier/go-toml/v2" 13 | "golang.org/x/text/language" 14 | ) 15 | 16 | var ( 17 | i18nBundle *i18n.Bundle 18 | LocalizerWeb *i18n.Localizer 19 | LocalizerBot *i18n.Localizer 20 | ) 21 | 22 | type I18nType string 23 | 24 | const ( 25 | Bot I18nType = "bot" 26 | Web I18nType = "web" 27 | ) 28 | 29 | type SettingService interface { 30 | GetTgLang() (string, error) 31 | } 32 | 33 | func InitLocalizer(i18nFS embed.FS, settingService SettingService) error { 34 | // set default bundle to english 35 | i18nBundle = i18n.NewBundle(language.MustParse("en-US")) 36 | i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal) 37 | 38 | // parse files 39 | if err := parseTranslationFiles(i18nFS, i18nBundle); err != nil { 40 | return err 41 | } 42 | 43 | // setup bot locale 44 | if err := initTGBotLocalizer(settingService); err != nil { 45 | return err 46 | } 47 | 48 | return nil 49 | } 50 | 51 | func createTemplateData(params []string, seperator ...string) map[string]interface{} { 52 | var sep string = "==" 53 | if len(seperator) > 0 { 54 | sep = seperator[0] 55 | } 56 | 57 | templateData := make(map[string]interface{}) 58 | for _, param := range params { 59 | parts := strings.SplitN(param, sep, 2) 60 | templateData[parts[0]] = parts[1] 61 | } 62 | 63 | return templateData 64 | } 65 | 66 | func I18n(i18nType I18nType, key string, params ...string) string { 67 | var localizer *i18n.Localizer 68 | 69 | switch i18nType { 70 | case "bot": 71 | localizer = LocalizerBot 72 | case "web": 73 | localizer = LocalizerWeb 74 | default: 75 | logger.Errorf("Invalid type for I18n: %s", i18nType) 76 | return "" 77 | } 78 | 79 | templateData := createTemplateData(params) 80 | 81 | msg, err := localizer.Localize(&i18n.LocalizeConfig{ 82 | MessageID: key, 83 | TemplateData: templateData, 84 | }) 85 | if err != nil { 86 | logger.Errorf("Failed to localize message: %v", err) 87 | return "" 88 | } 89 | 90 | return msg 91 | } 92 | 93 | func initTGBotLocalizer(settingService SettingService) error { 94 | botLang, err := settingService.GetTgLang() 95 | if err != nil { 96 | return err 97 | } 98 | 99 | LocalizerBot = i18n.NewLocalizer(i18nBundle, botLang) 100 | return nil 101 | } 102 | 103 | func LocalizerMiddleware() gin.HandlerFunc { 104 | return func(c *gin.Context) { 105 | var lang string 106 | 107 | if cookie, err := c.Request.Cookie("lang"); err == nil { 108 | lang = cookie.Value 109 | } else { 110 | lang = c.GetHeader("Accept-Language") 111 | } 112 | 113 | LocalizerWeb = i18n.NewLocalizer(i18nBundle, lang) 114 | 115 | c.Set("localizer", LocalizerWeb) 116 | c.Set("I18n", I18n) 117 | c.Next() 118 | } 119 | } 120 | 121 | func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error { 122 | err := fs.WalkDir(i18nFS, "translation", 123 | func(path string, d fs.DirEntry, err error) error { 124 | if err != nil { 125 | return err 126 | } 127 | 128 | if d.IsDir() { 129 | return nil 130 | } 131 | 132 | data, err := i18nFS.ReadFile(path) 133 | if err != nil { 134 | return err 135 | } 136 | 137 | _, err = i18nBundle.ParseMessageFileBytes(data, path) 138 | return err 139 | }) 140 | if err != nil { 141 | return err 142 | } 143 | 144 | return nil 145 | } 146 | -------------------------------------------------------------------------------- /web/middleware/domainValidator.go: -------------------------------------------------------------------------------- 1 | package middleware 2 | 3 | import ( 4 | "net" 5 | "net/http" 6 | 7 | "github.com/gin-gonic/gin" 8 | ) 9 | 10 | func DomainValidatorMiddleware(domain string) gin.HandlerFunc { 11 | return func(c *gin.Context) { 12 | host, _, _ := net.SplitHostPort(c.Request.Host) 13 | 14 | if host != domain { 15 | c.AbortWithStatus(http.StatusForbidden) 16 | return 17 | } 18 | 19 | c.Next() 20 | } 21 | } 22 | -------------------------------------------------------------------------------- /web/network/auto_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/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/service/config.json: -------------------------------------------------------------------------------- 1 | { 2 | "log": { 3 | "loglevel": "warning" 4 | }, 5 | "api": { 6 | "tag": "api", 7 | "services": ["HandlerService", "LoggerService", "StatsService"] 8 | }, 9 | "inbounds": [ 10 | { 11 | "tag": "api", 12 | "listen": "127.0.0.1", 13 | "port": 62789, 14 | "protocol": "dokodemo-door", 15 | "settings": { 16 | "address": "127.0.0.1" 17 | } 18 | } 19 | ], 20 | "outbounds": [ 21 | { 22 | "tag": "direct", 23 | "protocol": "freedom", 24 | "settings": { 25 | "domainStrategy": "UseIP" 26 | } 27 | }, 28 | { 29 | "tag": "blocked", 30 | "protocol": "blackhole", 31 | "settings": {} 32 | } 33 | ], 34 | "policy": { 35 | "levels": { 36 | "0": { 37 | "statsUserDownlink": true, 38 | "statsUserUplink": true 39 | } 40 | }, 41 | "system": { 42 | "statsInboundDownlink": true, 43 | "statsInboundUplink": true 44 | } 45 | }, 46 | "routing": { 47 | "domainStrategy": "AsIs", 48 | "rules": [ 49 | { 50 | "type": "field", 51 | "inboundTag": ["api"], 52 | "outboundTag": "api" 53 | }, 54 | { 55 | "type": "field", 56 | "outboundTag": "blocked", 57 | "ip": ["geoip:private"] 58 | }, 59 | { 60 | "type": "field", 61 | "outboundTag": "blocked", 62 | "protocol": ["bittorrent"] 63 | } 64 | ] 65 | }, 66 | "stats": {} 67 | } -------------------------------------------------------------------------------- /web/service/panel.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "os" 5 | "syscall" 6 | "time" 7 | 8 | "x-ui/logger" 9 | ) 10 | 11 | type PanelService struct{} 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/user.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "errors" 5 | 6 | "x-ui/database" 7 | "x-ui/database/model" 8 | "x-ui/logger" 9 | 10 | "gorm.io/gorm" 11 | ) 12 | 13 | type UserService struct{} 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 | Updates(map[string]interface{}{"username": username, "password": password}). 50 | Error 51 | } 52 | 53 | func (s *UserService) UpdateFirstUser(username string, password string) error { 54 | if username == "" { 55 | return errors.New("username can not be empty") 56 | } else if password == "" { 57 | return errors.New("password can not be empty") 58 | } 59 | db := database.GetDB() 60 | user := &model.User{} 61 | err := db.Model(model.User{}).First(user).Error 62 | if database.IsNotFound(err) { 63 | user.Username = username 64 | user.Password = password 65 | return db.Model(model.User{}).Create(user).Error 66 | } else if err != nil { 67 | return err 68 | } 69 | user.Username = username 70 | user.Password = password 71 | return db.Save(user).Error 72 | } 73 | -------------------------------------------------------------------------------- /web/service/xray_setting.go: -------------------------------------------------------------------------------- 1 | package service 2 | 3 | import ( 4 | "bytes" 5 | _ "embed" 6 | "encoding/json" 7 | "fmt" 8 | "net/http" 9 | "os" 10 | "time" 11 | 12 | "x-ui/util/common" 13 | "x-ui/xray" 14 | ) 15 | 16 | type XraySettingService struct { 17 | SettingService 18 | } 19 | 20 | func (s *XraySettingService) SaveXraySetting(newXraySettings string) error { 21 | if err := s.CheckXrayConfig(newXraySettings); err != nil { 22 | return err 23 | } 24 | return s.SettingService.saveSetting("xrayTemplateConfig", newXraySettings) 25 | } 26 | 27 | func (s *XraySettingService) CheckXrayConfig(XrayTemplateConfig string) error { 28 | xrayConfig := &xray.Config{} 29 | err := json.Unmarshal([]byte(XrayTemplateConfig), xrayConfig) 30 | if err != nil { 31 | return common.NewError("xray template config invalid:", err) 32 | } 33 | return nil 34 | } 35 | 36 | func (s *XraySettingService) GetWarpData() (string, error) { 37 | warp, err := s.SettingService.GetWarp() 38 | if err != nil { 39 | return "", err 40 | } 41 | return warp, nil 42 | } 43 | 44 | func (s *XraySettingService) GetWarpConfig() (string, error) { 45 | var warpData map[string]string 46 | warp, err := s.SettingService.GetWarp() 47 | if err != nil { 48 | return "", err 49 | } 50 | err = json.Unmarshal([]byte(warp), &warpData) 51 | if err != nil { 52 | return "", err 53 | } 54 | 55 | url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s", warpData["device_id"]) 56 | 57 | req, err := http.NewRequest("GET", url, nil) 58 | if err != nil { 59 | return "", err 60 | } 61 | req.Header.Set("Authorization", "Bearer "+warpData["access_token"]) 62 | 63 | client := &http.Client{} 64 | resp, err := client.Do(req) 65 | if err != nil { 66 | return "", err 67 | } 68 | defer resp.Body.Close() 69 | buffer := bytes.NewBuffer(make([]byte, 8192)) 70 | buffer.Reset() 71 | _, err = buffer.ReadFrom(resp.Body) 72 | if err != nil { 73 | return "", err 74 | } 75 | 76 | return buffer.String(), nil 77 | } 78 | 79 | func (s *XraySettingService) RegWarp(secretKey string, publicKey string) (string, error) { 80 | tos := time.Now().UTC().Format("2006-01-02T15:04:05.000Z") 81 | hostName, _ := os.Hostname() 82 | data := fmt.Sprintf(`{"key":"%s","tos":"%s","type": "PC","model": "x-ui", "name": "%s"}`, publicKey, tos, hostName) 83 | 84 | url := "https://api.cloudflareclient.com/v0a2158/reg" 85 | 86 | req, err := http.NewRequest("POST", url, bytes.NewBuffer([]byte(data))) 87 | if err != nil { 88 | return "", err 89 | } 90 | 91 | req.Header.Add("CF-Client-Version", "a-7.21-0721") 92 | req.Header.Add("Content-Type", "application/json") 93 | 94 | client := &http.Client{} 95 | resp, err := client.Do(req) 96 | if err != nil { 97 | return "", err 98 | } 99 | defer resp.Body.Close() 100 | buffer := bytes.NewBuffer(make([]byte, 8192)) 101 | buffer.Reset() 102 | _, err = buffer.ReadFrom(resp.Body) 103 | if err != nil { 104 | return "", err 105 | } 106 | 107 | var rspData map[string]interface{} 108 | err = json.Unmarshal(buffer.Bytes(), &rspData) 109 | if err != nil { 110 | return "", err 111 | } 112 | 113 | deviceId := rspData["id"].(string) 114 | token := rspData["token"].(string) 115 | license, ok := rspData["account"].(map[string]interface{})["license"].(string) 116 | if !ok { 117 | fmt.Println("Error accessing license value.") 118 | return "", err 119 | } 120 | 121 | warpData := fmt.Sprintf("{\n \"access_token\": \"%s\",\n \"device_id\": \"%s\",", token, deviceId) 122 | warpData += fmt.Sprintf("\n \"license_key\": \"%s\",\n \"private_key\": \"%s\"\n}", license, secretKey) 123 | 124 | s.SettingService.SetWarp(warpData) 125 | 126 | result := fmt.Sprintf("{\n \"data\": %s,\n \"config\": %s\n}", warpData, buffer.String()) 127 | 128 | return result, nil 129 | } 130 | 131 | func (s *XraySettingService) SetWarpLicence(license string) (string, error) { 132 | var warpData map[string]string 133 | warp, err := s.SettingService.GetWarp() 134 | if err != nil { 135 | return "", err 136 | } 137 | err = json.Unmarshal([]byte(warp), &warpData) 138 | if err != nil { 139 | return "", err 140 | } 141 | 142 | url := fmt.Sprintf("https://api.cloudflareclient.com/v0a2158/reg/%s/account", warpData["device_id"]) 143 | data := fmt.Sprintf(`{"license": "%s"}`, license) 144 | 145 | req, err := http.NewRequest("PUT", url, bytes.NewBuffer([]byte(data))) 146 | if err != nil { 147 | return "", err 148 | } 149 | req.Header.Set("Authorization", "Bearer "+warpData["access_token"]) 150 | 151 | client := &http.Client{} 152 | resp, err := client.Do(req) 153 | if err != nil { 154 | return "", err 155 | } 156 | defer resp.Body.Close() 157 | buffer := bytes.NewBuffer(make([]byte, 8192)) 158 | buffer.Reset() 159 | _, err = buffer.ReadFrom(resp.Body) 160 | if err != nil { 161 | return "", err 162 | } 163 | 164 | warpData["license_key"] = license 165 | newWarpData, err := json.MarshalIndent(warpData, "", " ") 166 | if err != nil { 167 | return "", err 168 | } 169 | s.SettingService.SetWarp(string(newWarpData)) 170 | println(string(newWarpData)) 171 | 172 | return string(newWarpData), nil 173 | } 174 | -------------------------------------------------------------------------------- /web/session/session.go: -------------------------------------------------------------------------------- 1 | package session 2 | 3 | import ( 4 | "encoding/gob" 5 | 6 | "x-ui/database/model" 7 | 8 | sessions "github.com/Calidity/gin-sessions" 9 | "github.com/gin-gonic/gin" 10 | ) 11 | 12 | const ( 13 | loginUser = "LOGIN_USER" 14 | ) 15 | 16 | func init() { 17 | gob.Register(model.User{}) 18 | } 19 | 20 | func SetLoginUser(c *gin.Context, user *model.User) error { 21 | s := sessions.Default(c) 22 | s.Set(loginUser, user) 23 | return s.Save() 24 | } 25 | 26 | func SetMaxAge(c *gin.Context, maxAge int) error { 27 | s := sessions.Default(c) 28 | s.Options(sessions.Options{ 29 | Path: "/", 30 | MaxAge: maxAge, 31 | }) 32 | return s.Save() 33 | } 34 | 35 | func GetLoginUser(c *gin.Context) *model.User { 36 | s := sessions.Default(c) 37 | obj := s.Get(loginUser) 38 | if obj == nil { 39 | return nil 40 | } 41 | user := obj.(model.User) 42 | return &user 43 | } 44 | 45 | func IsLogin(c *gin.Context) bool { 46 | return GetLoginUser(c) != nil 47 | } 48 | 49 | func ClearSession(c *gin.Context) { 50 | s := sessions.Default(c) 51 | s.Clear() 52 | s.Options(sessions.Options{ 53 | Path: "/", 54 | MaxAge: -1, 55 | }) 56 | s.Save() 57 | } 58 | -------------------------------------------------------------------------------- /x-ui.service: -------------------------------------------------------------------------------- 1 | [Unit] 2 | Description=x-ui Service 3 | After=network.target 4 | Wants=network.target 5 | 6 | [Service] 7 | Environment="XRAY_VMESS_AEAD_FORCED=false" 8 | Type=simple 9 | WorkingDirectory=/usr/local/x-ui/ 10 | ExecStart=/usr/local/x-ui/x-ui 11 | Restart=on-failure 12 | 13 | [Install] 14 | WantedBy=multi-user.target -------------------------------------------------------------------------------- /xray/client_traffic.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | type ClientTraffic struct { 4 | Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"` 5 | InboundId int `json:"inboundId" form:"inboundId"` 6 | Enable bool `json:"enable" form:"enable"` 7 | Email string `json:"email" form:"email" gorm:"unique"` 8 | Up int64 `json:"up" form:"up"` 9 | Down int64 `json:"down" form:"down"` 10 | ExpiryTime int64 `json:"expiryTime" form:"expiryTime"` 11 | Total int64 `json:"total" form:"total"` 12 | Reset int `json:"reset" form:"reset" gorm:"default:0"` 13 | } 14 | -------------------------------------------------------------------------------- /xray/config.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "bytes" 5 | 6 | "x-ui/util/json_util" 7 | ) 8 | 9 | type Config struct { 10 | LogConfig json_util.RawMessage `json:"log"` 11 | RouterConfig json_util.RawMessage `json:"routing"` 12 | DNSConfig json_util.RawMessage `json:"dns"` 13 | InboundConfigs []InboundConfig `json:"inbounds"` 14 | OutboundConfigs json_util.RawMessage `json:"outbounds"` 15 | Transport json_util.RawMessage `json:"transport"` 16 | Policy json_util.RawMessage `json:"policy"` 17 | API json_util.RawMessage `json:"api"` 18 | Stats json_util.RawMessage `json:"stats"` 19 | Reverse json_util.RawMessage `json:"reverse"` 20 | FakeDNS json_util.RawMessage `json:"fakedns"` 21 | Observatory json_util.RawMessage `json:"observatory"` 22 | BurstObservatory json_util.RawMessage `json:"burstObservatory"` 23 | } 24 | 25 | func (c *Config) Equals(other *Config) bool { 26 | if len(c.InboundConfigs) != len(other.InboundConfigs) { 27 | return false 28 | } 29 | for i, inbound := range c.InboundConfigs { 30 | if !inbound.Equals(&other.InboundConfigs[i]) { 31 | return false 32 | } 33 | } 34 | if !bytes.Equal(c.LogConfig, other.LogConfig) { 35 | return false 36 | } 37 | if !bytes.Equal(c.RouterConfig, other.RouterConfig) { 38 | return false 39 | } 40 | if !bytes.Equal(c.DNSConfig, other.DNSConfig) { 41 | return false 42 | } 43 | if !bytes.Equal(c.OutboundConfigs, other.OutboundConfigs) { 44 | return false 45 | } 46 | if !bytes.Equal(c.Transport, other.Transport) { 47 | return false 48 | } 49 | if !bytes.Equal(c.Policy, other.Policy) { 50 | return false 51 | } 52 | if !bytes.Equal(c.API, other.API) { 53 | return false 54 | } 55 | if !bytes.Equal(c.Stats, other.Stats) { 56 | return false 57 | } 58 | if !bytes.Equal(c.Reverse, other.Reverse) { 59 | return false 60 | } 61 | if !bytes.Equal(c.FakeDNS, other.FakeDNS) { 62 | return false 63 | } 64 | return true 65 | } 66 | -------------------------------------------------------------------------------- /xray/inbound.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "bytes" 5 | 6 | "x-ui/util/json_util" 7 | ) 8 | 9 | type InboundConfig struct { 10 | Listen json_util.RawMessage `json:"listen"` // listen cannot be an empty string 11 | Port int `json:"port"` 12 | Protocol string `json:"protocol"` 13 | Settings json_util.RawMessage `json:"settings"` 14 | StreamSettings json_util.RawMessage `json:"streamSettings"` 15 | Tag string `json:"tag"` 16 | Sniffing json_util.RawMessage `json:"sniffing"` 17 | } 18 | 19 | func (c *InboundConfig) Equals(other *InboundConfig) bool { 20 | if !bytes.Equal(c.Listen, other.Listen) { 21 | return false 22 | } 23 | if c.Port != other.Port { 24 | return false 25 | } 26 | if c.Protocol != other.Protocol { 27 | return false 28 | } 29 | if !bytes.Equal(c.Settings, other.Settings) { 30 | return false 31 | } 32 | if !bytes.Equal(c.StreamSettings, other.StreamSettings) { 33 | return false 34 | } 35 | if c.Tag != other.Tag { 36 | return false 37 | } 38 | if !bytes.Equal(c.Sniffing, other.Sniffing) { 39 | return false 40 | } 41 | return true 42 | } 43 | -------------------------------------------------------------------------------- /xray/log_writer.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "regexp" 5 | "strings" 6 | 7 | "x-ui/logger" 8 | ) 9 | 10 | func NewLogWriter() *LogWriter { 11 | return &LogWriter{} 12 | } 13 | 14 | type LogWriter struct { 15 | lastLine string 16 | } 17 | 18 | func (lw *LogWriter) Write(m []byte) (n int, err error) { 19 | regex := regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[([^\]]+)\] (.+)$`) 20 | 21 | // Convert the data to a string 22 | message := strings.TrimSpace(string(m)) 23 | 24 | messages := strings.Split(message, "\n") 25 | lw.lastLine = messages[len(messages)-1] 26 | 27 | for _, msg := range messages { 28 | matches := regex.FindStringSubmatch(msg) 29 | 30 | if len(matches) > 3 { 31 | level := matches[2] 32 | msgBody := matches[3] 33 | 34 | // Map the level to the appropriate logger function 35 | switch level { 36 | case "Debug": 37 | logger.Debug("XRAY: " + msgBody) 38 | case "Info": 39 | logger.Info("XRAY: " + msgBody) 40 | case "Warning": 41 | logger.Warning("XRAY: " + msgBody) 42 | case "Error": 43 | logger.Error("XRAY: " + msgBody) 44 | default: 45 | logger.Debug("XRAY: " + msg) 46 | } 47 | } else if msg != "" { 48 | logger.Debug("XRAY: " + msg) 49 | return len(m), nil 50 | } 51 | } 52 | 53 | return len(m), nil 54 | } 55 | -------------------------------------------------------------------------------- /xray/process.go: -------------------------------------------------------------------------------- 1 | package xray 2 | 3 | import ( 4 | "bytes" 5 | "encoding/json" 6 | "errors" 7 | "fmt" 8 | "io/fs" 9 | "os" 10 | "os/exec" 11 | "runtime" 12 | "syscall" 13 | "time" 14 | 15 | "x-ui/config" 16 | "x-ui/logger" 17 | "x-ui/util/common" 18 | ) 19 | 20 | func GetBinaryName() string { 21 | return fmt.Sprintf("xray-%s-%s", runtime.GOOS, runtime.GOARCH) 22 | } 23 | 24 | func GetBinaryPath() string { 25 | return config.GetBinFolderPath() + "/" + GetBinaryName() 26 | } 27 | 28 | func GetConfigPath() string { 29 | return config.GetBinFolderPath() + "/config.json" 30 | } 31 | 32 | func GetGeositePath() string { 33 | return config.GetBinFolderPath() + "/geosite.dat" 34 | } 35 | 36 | func GetGeoipPath() string { 37 | return config.GetBinFolderPath() + "/geoip.dat" 38 | } 39 | 40 | func stopProcess(p *Process) { 41 | p.Stop() 42 | } 43 | 44 | type Process struct { 45 | *process 46 | } 47 | 48 | func NewProcess(xrayConfig *Config) *Process { 49 | p := &Process{newProcess(xrayConfig)} 50 | runtime.SetFinalizer(p, stopProcess) 51 | return p 52 | } 53 | 54 | type process struct { 55 | cmd *exec.Cmd 56 | 57 | version string 58 | apiPort int 59 | 60 | onlineClients []string 61 | 62 | config *Config 63 | logWriter *LogWriter 64 | exitErr error 65 | startTime time.Time 66 | } 67 | 68 | func newProcess(config *Config) *process { 69 | return &process{ 70 | version: "Unknown", 71 | config: config, 72 | logWriter: NewLogWriter(), 73 | startTime: time.Now(), 74 | } 75 | } 76 | 77 | func (p *process) IsRunning() bool { 78 | if p.cmd == nil || p.cmd.Process == nil { 79 | return false 80 | } 81 | if p.cmd.ProcessState == nil { 82 | return true 83 | } 84 | return false 85 | } 86 | 87 | func (p *process) GetErr() error { 88 | return p.exitErr 89 | } 90 | 91 | func (p *process) GetResult() string { 92 | if len(p.logWriter.lastLine) == 0 && p.exitErr != nil { 93 | return p.exitErr.Error() 94 | } 95 | return p.logWriter.lastLine 96 | } 97 | 98 | func (p *process) GetVersion() string { 99 | return p.version 100 | } 101 | 102 | func (p *Process) GetAPIPort() int { 103 | return p.apiPort 104 | } 105 | 106 | func (p *Process) GetConfig() *Config { 107 | return p.config 108 | } 109 | 110 | func (p *Process) GetOnlineClients() []string { 111 | return p.onlineClients 112 | } 113 | 114 | func (p *Process) SetOnlineClients(users []string) { 115 | p.onlineClients = users 116 | } 117 | 118 | func (p *Process) GetUptime() uint64 { 119 | return uint64(time.Since(p.startTime).Seconds()) 120 | } 121 | 122 | func (p *process) refreshAPIPort() { 123 | for _, inbound := range p.config.InboundConfigs { 124 | if inbound.Tag == "api" { 125 | p.apiPort = inbound.Port 126 | break 127 | } 128 | } 129 | } 130 | 131 | func (p *process) refreshVersion() { 132 | cmd := exec.Command(GetBinaryPath(), "-version") 133 | data, err := cmd.Output() 134 | if err != nil { 135 | p.version = "Unknown" 136 | } else { 137 | datas := bytes.Split(data, []byte(" ")) 138 | if len(datas) <= 1 { 139 | p.version = "Unknown" 140 | } else { 141 | p.version = string(datas[1]) 142 | } 143 | } 144 | } 145 | 146 | func (p *process) Start() (err error) { 147 | if p.IsRunning() { 148 | return errors.New("xray is already running") 149 | } 150 | 151 | defer func() { 152 | if err != nil { 153 | logger.Error("Failure in running xray-core process: ", err) 154 | p.exitErr = err 155 | } 156 | }() 157 | 158 | data, err := json.MarshalIndent(p.config, "", " ") 159 | if err != nil { 160 | return common.NewErrorf("Failed to generate XRAY configuration files: %v", err) 161 | } 162 | configPath := GetConfigPath() 163 | err = os.WriteFile(configPath, data, fs.ModePerm) 164 | if err != nil { 165 | return common.NewErrorf("Write the configuration file failed: %v", err) 166 | } 167 | 168 | cmd := exec.Command(GetBinaryPath(), "-c", configPath) 169 | p.cmd = cmd 170 | 171 | cmd.Stdout = p.logWriter 172 | cmd.Stderr = p.logWriter 173 | 174 | go func() { 175 | err := cmd.Run() 176 | if err != nil { 177 | logger.Error("Failure in running xray-core: ", err) 178 | p.exitErr = err 179 | } 180 | }() 181 | 182 | p.refreshVersion() 183 | p.refreshAPIPort() 184 | 185 | return nil 186 | } 187 | 188 | func (p *process) Stop() error { 189 | if !p.IsRunning() { 190 | return errors.New("xray is not running") 191 | } 192 | return p.cmd.Process.Signal(syscall.SIGTERM) 193 | } 194 | -------------------------------------------------------------------------------- /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 | --------------------------------------------------------------------------------