diff --git a/README.md b/README.md index 56c4b344..9ba7e289 100644 --- a/README.md +++ b/README.md @@ -9,7 +9,6 @@ [![License](https://img.shields.io/badge/license-GPL%20V3-blue.svg?longCache=true)](https://www.gnu.org/licenses/gpl-3.0.en.html) 3x-ui panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russian)** - **If you think this project is helpful to you, you may wish to give a** :star2: **Buy Me a Coffee :** @@ -24,11 +23,12 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install. # Install custom version -To install your desired version you can add the version to the end of install command. Example for ver `v1.6.0`: +To install your desired version you can add the version to the end of install command. Example for ver `v1.6.1`: ``` -bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.6.0 +bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v1.6.1 ``` + # SSL ``` @@ -37,8 +37,8 @@ certbot certonly --standalone --agree-tos --register-unsafely-without-email -d y certbot renew --dry-run ``` -or you can use x-ui menu then number `16` (`SSL Certificate Management`) - +You also can use `x-ui` menu then select `16. SSL Certificate Management` + # Features - System Status Monitoring @@ -57,23 +57,26 @@ or you can use x-ui menu then number `16` (`SSL Certificate Management`) - Support export/import database from panel # Manual Install & Upgrade +
Click for Manual Install details - + 1. To download the latest version of the compressed package directly to your server, run the following command: ```sh -wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-amd64.tar.gz +ARCH=$(uname -m) +[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64" +wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz ``` -Note: If your server's CPU architecture is `arm64`, modify the URL by substituting `amd64` with your respective CPU architecture. - 2. Once the compressed package is downloaded, execute the following commands to install or upgrade x-ui: ```sh +ARCH=$(uname -m) +[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64" cd /root/ rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui -tar zxvf x-ui-linux-amd64.tar.gz +tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz chmod +x x-ui/x-ui x-ui/bin/xray-linux-* x-ui/x-ui.sh cp x-ui/x-ui.sh /usr/bin/x-ui cp -f x-ui/x-ui.service /etc/systemd/system/ @@ -82,14 +85,16 @@ systemctl daemon-reload systemctl enable x-ui systemctl restart x-ui ``` -Note: If your server's CPU architecture is `arm64`, modify the `amd64` in `tar zxvf x-ui-linux-amd64.tar.gz` with your respective CPU architecture. +
- + # Install with Docker +
Click for Docker details 1. Install Docker: + ```sh bash <(curl -sSL https://get.docker.com) ``` @@ -100,7 +105,7 @@ Note: If your server's CPU architecture is `arm64`, modify the `amd64` in `tar z git clone https://github.com/MHSanaei/3x-ui.git cd 3x-ui ``` - + 3. Start the Service ```sh @@ -119,12 +124,14 @@ Note: If your server's CPU architecture is `arm64`, modify the `amd64` in `tar z --name 3x-ui \ ghcr.io/mhsanaei/3x-ui:latest ``` +
- + # Default settings +
Click for Default settings details - + - Port: 2053 - username and password will be generated randomly if you skip to modify your own security(x-ui "7") - database path: /etc/x-ui/x-ui.db @@ -141,10 +148,10 @@ After you set ssl on settings
# Xray Configurations: - +
Click for Xray Configurations details - + **copy and paste to xray Configuration :** (you don't need to do this if you have a fresh install) - [traffic](./media/configs/traffic.json) @@ -152,14 +159,14 @@ After you set ssl on settings - [traffic + Block all Iran Domains](./media/configs/traffic+block-iran-domains.json) - [traffic + Block Ads + Use IPv4 for Google](./media/configs/traffic+block-ads+ipv4-google.json) - [traffic + Block Ads + Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP](./media/configs/traffic+block-ads+warp.json) - +
- + # [WARP Configuration](https://github.com/fscarmen/warp) (Optional) - +
Click for WARP Configuration details - + If you want to use routing to WARP follow steps as below: 1. If you already installed warp, you can uninstall using below command: @@ -171,7 +178,7 @@ If you want to use routing to WARP follow steps as below: 2. Install WARP on **socks proxy mode**: ```sh - curl -fsSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh | bash + bash <(curl -sSL https://gist.githubusercontent.com/hamid-gh98/dc5dd9b0cc5b0412af927b1ccdb294c7/raw/install_warp_proxy.sh) ``` 3. Turn on the config you need in panel or [Copy and paste this file to Xray Configuration](./media/configs/traffic+block-ads+warp.json) @@ -181,14 +188,14 @@ If you want to use routing to WARP follow steps as below: - Block Ads - Route Google + Netflix + Spotify + OpenAI (ChatGPT) to WARP - Fix Google 403 error - +
# Telegram Bot - +
Click for Telegram Bot details - + X-UI supports daily traffic notification, panel login reminder and other functions through the Tg robot. To use the Tg robot, you need to apply for the specific application tutorial. You can refer to the [blog](https://coderfan.net/how-to-use-telegram-bot-to-alarm-you-when-someone-login-into-your-vps.html) Set the robot-related parameters in the panel background, including: @@ -216,19 +223,21 @@ Reference syntax: - CPU threshold notification - Threshold for Expiration time and Traffic to report in advance - Support client report menu if client's telegram username added to the user's configurations -- Support telegram traffic report searched with UID (VMESS/VLESS) or Password (TROJAN) - anonymously +- Support telegram traffic report searched with UUID (VMESS/VLESS) or Password (TROJAN) - anonymously - Menu based bot - Search client by email ( only admin ) - Check all inbounds - Check server status - Check depleted users - Receive backup by request and in periodic reports +- Multi language bot
# API routes +
Click for API routes details - + - `/login` with `PUSH` user data: `{username: '', password: ''}` for login - `/panel/api/inbounds` base for following actions: @@ -261,6 +270,7 @@ Reference syntax:
# Environment Variables +
Click for Environment Variables details @@ -276,6 +286,7 @@ Example: ```sh XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go ``` +
# A Special Thanks To diff --git a/sub/sub.go b/sub/sub.go index f7353cc2..b642f7f2 100644 --- a/sub/sub.go +++ b/sub/sub.go @@ -7,10 +7,10 @@ import ( "net" "net/http" "strconv" - "strings" "x-ui/config" "x-ui/logger" "x-ui/util/common" + "x-ui/web/middleware" "x-ui/web/network" "x-ui/web/service" @@ -58,18 +58,7 @@ func (s *Server) initRouter() (*gin.Engine, error) { } if subDomain != "" { - validateDomain := func(c *gin.Context) { - host := strings.Split(c.Request.Host, ":")[0] - - if host != subDomain { - c.AbortWithStatus(http.StatusForbidden) - return - } - - c.Next() - } - - engine.Use(validateDomain) + engine.Use(middleware.DomainValidatorMiddleware(subDomain)) } g := engine.Group(subPath) @@ -116,11 +105,13 @@ func (s *Server) Start() (err error) { if err != nil { return err } + listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) listener, err := net.Listen("tcp", listenAddr) if err != nil { return err } + if certFile != "" || keyFile != "" { cert, err := tls.LoadX509KeyPair(certFile, keyFile) if err != nil { @@ -168,4 +159,4 @@ func (s *Server) Stop() error { func (s *Server) GetCtx() context.Context { return s.ctx -} \ No newline at end of file +} diff --git a/web/assets/js/model/models.js b/web/assets/js/model/models.js index 9a5dcc85..e1a766dc 100644 --- a/web/assets/js/model/models.js +++ b/web/assets/js/model/models.js @@ -168,6 +168,7 @@ class AllSetting { constructor(data) { this.webListen = ""; + this.webDomain = ""; this.webPort = 2053; this.webCertFile = ""; this.webKeyFile = ""; @@ -187,7 +188,7 @@ class AllSetting { this.subEnable = false; this.subListen = ""; this.subPort = "2096"; - this.subPath = "sub/"; + this.subPath = "/sub/"; this.subDomain = ""; this.subCertFile = ""; this.subKeyFile = ""; diff --git a/web/assets/js/util/common.js b/web/assets/js/util/common.js index 563bfd45..8e30bce7 100644 --- a/web/assets/js/util/common.js +++ b/web/assets/js/util/common.js @@ -135,3 +135,21 @@ function doAllItemsExist(array1, array2) { } return true; } + +function buildURL({ host, port, isTLS, base, path }) { + if (!host || host.length === 0) host = window.location.hostname; + if (!port || port.length === 0) port = window.location.port; + + if (isTLS === undefined) isTLS = window.location.protocol === "https:"; + + const protocol = isTLS ? "https:" : "http:"; + + port = String(port); + if (port === "" || (isTLS && port === "443") || (!isTLS && port === "80")) { + port = ""; + } else { + port = `:${port}`; + } + + return `${protocol}//${host}${port}${base}${path}`; +} diff --git a/web/controller/inbound.go b/web/controller/inbound.go index 5ce58d53..815f1788 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -65,6 +65,7 @@ func (a *InboundController) getInbounds(c *gin.Context) { } jsonObj(c, inbounds, nil) } + func (a *InboundController) getInbound(c *gin.Context) { id, err := strconv.Atoi(c.Param("id")) if err != nil { @@ -168,6 +169,7 @@ func (a *InboundController) clearClientIps(c *gin.Context) { } jsonMsg(c, "Log Cleared", nil) } + func (a *InboundController) addInboundClient(c *gin.Context) { data := &model.Inbound{} err := c.ShouldBind(data) diff --git a/web/controller/setting.go b/web/controller/setting.go index 0292c46a..cd509293 100644 --- a/web/controller/setting.go +++ b/web/controller/setting.go @@ -65,77 +65,42 @@ func (a *SettingController) getDefaultJsonConfig(c *gin.Context) { } func (a *SettingController) getDefaultSettings(c *gin.Context) { - expireDiff, err := a.settingService.GetExpireDiff() - if err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) - return + type settingFunc func() (interface{}, error) + + settings := map[string]settingFunc{ + "expireDiff": func() (interface{}, error) { return a.settingService.GetExpireDiff() }, + "trafficDiff": func() (interface{}, error) { return a.settingService.GetTrafficDiff() }, + "defaultCert": func() (interface{}, error) { return a.settingService.GetCertFile() }, + "defaultKey": func() (interface{}, error) { return a.settingService.GetKeyFile() }, + "tgBotEnable": func() (interface{}, error) { return a.settingService.GetTgbotenabled() }, + "subEnable": func() (interface{}, error) { return a.settingService.GetSubEnable() }, + "subPort": func() (interface{}, error) { return a.settingService.GetSubPort() }, + "subPath": func() (interface{}, error) { return a.settingService.GetSubPath() }, + "subDomain": func() (interface{}, error) { return a.settingService.GetSubDomain() }, + "subKeyFile": func() (interface{}, error) { return a.settingService.GetSubKeyFile() }, + "subCertFile": func() (interface{}, error) { return a.settingService.GetSubCertFile() }, } - trafficDiff, err := a.settingService.GetTrafficDiff() - if err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) - return - } - defaultCert, err := a.settingService.GetCertFile() - if err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) - return - } - defaultKey, err := a.settingService.GetKeyFile() - if err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) - return - } - tgBotEnable, err := a.settingService.GetTgbotenabled() - if err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) - return - } - subEnable, err := a.settingService.GetSubEnable() - if err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) - return - } - subPort, err := a.settingService.GetSubPort() - if err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) - return - } - subPath, err := a.settingService.GetSubPath() - if err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) - return - } - subDomain, err := a.settingService.GetSubDomain() - if err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) - return - } - subKeyFile, err := a.settingService.GetSubKeyFile() - if err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) - return - } - subCertFile, err := a.settingService.GetSubCertFile() - if err != nil { - jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) - return + + result := make(map[string]interface{}) + + for key, fn := range settings { + value, err := fn() + if err != nil { + jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) + return + } + result[key] = value } + subTLS := false - if subKeyFile != "" || subCertFile != "" { + if result["subKeyFile"] != "" || result["subCertFile"] != "" { subTLS = true } - result := map[string]interface{}{ - "expireDiff": expireDiff, - "trafficDiff": trafficDiff, - "defaultCert": defaultCert, - "defaultKey": defaultKey, - "tgBotEnable": tgBotEnable, - "subEnable": subEnable, - "subPort": subPort, - "subPath": subPath, - "subDomain": subDomain, - "subTLS": subTLS, - } + result["subTLS"] = subTLS + + delete(result, "subKeyFile") + delete(result, "subCertFile") + jsonObj(c, result, nil) } diff --git a/web/entity/entity.go b/web/entity/entity.go index 0bfbfd2a..d5e90108 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -28,6 +28,7 @@ type Pager struct { type AllSetting struct { WebListen string `json:"webListen" form:"webListen"` + WebDomain string `json:"webDomain" form:"webDomain"` WebPort int `json:"webPort" form:"webPort"` WebCertFile string `json:"webCertFile" form:"webCertFile"` WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` diff --git a/web/global/hashStorage.go b/web/global/hashStorage.go index 9dfea169..5d8135ee 100644 --- a/web/global/hashStorage.go +++ b/web/global/hashStorage.go @@ -18,7 +18,6 @@ type HashStorage struct { sync.RWMutex Data map[string]HashEntry Expiration time.Duration - } func NewHashStorage(expiration time.Duration) *HashStorage { @@ -46,7 +45,6 @@ func (h *HashStorage) SaveHash(query string) string { return md5HashString } - func (h *HashStorage) GetValue(hash string) (string, bool) { h.RLock() defer h.RUnlock() diff --git a/web/html/common/qrcode_modal.html b/web/html/common/qrcode_modal.html index e6b7b998..8edfa2de 100644 --- a/web/html/common/qrcode_modal.html +++ b/web/html/common/qrcode_modal.html @@ -68,8 +68,8 @@ qrModal: qrModal, }, methods: { - copyToClipboard(elmentId,content) { - this.qrModal.clipboard = new ClipboardJS('#'+elmentId, { + copyToClipboard(elmentId, content) { + this.qrModal.clipboard = new ClipboardJS('#' + elmentId, { text: () => content, }); this.qrModal.clipboard.on('success', () => { @@ -77,29 +77,25 @@ this.qrModal.clipboard.destroy(); }); }, - setQrCode(elmentId,content) { + setQrCode(elmentId, content) { new QRious({ - element: document.querySelector('#'+elmentId), - size: 260, - value: content, - }); + element: document.querySelector('#' + elmentId), + size: 260, + value: content, + }); }, genSubLink(subID) { - protocol = app.subSettings.tls ? "https://" : "http://"; - hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain; - subPort = app.subSettings.port; - port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort); - subPath = app.subSettings.path; - return protocol + hostName + port + subPath + subID; + const { domain: host, port, tls: isTLS, path: base } = app.subSettings; + return buildURL({ host, port, isTLS, base, path: subID }); } }, updated() { - if (qrModal.client.subId){ + if (qrModal.client && qrModal.client.subId) { qrModal.subId = qrModal.client.subId; - this.setQrCode("qrCode-sub",this.genSubLink(qrModal.subId)); + this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId)); } - qrModal.qrcodes.forEach((element,index) => { - this.setQrCode("qrCode-"+index, element.link); + qrModal.qrcodes.forEach((element, index) => { + this.setQrCode("qrCode-" + index, element.link); }); } }); diff --git a/web/html/xui/inbound_info_modal.html b/web/html/xui/inbound_info_modal.html index b7b3436b..23bd5af1 100644 --- a/web/html/xui/inbound_info_modal.html +++ b/web/html/xui/inbound_info_modal.html @@ -253,12 +253,8 @@ infoModal.visible = false; }, genSubLink(subID) { - protocol = app.subSettings.tls ? "https://" : "http://"; - hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain; - subPort = app.subSettings.port; - port = (subPort === 443 && app.subSettings.tls) || (subPort === 80 && !app.subSettings.tls) ? "" : ":" + String(subPort); - subPath = app.subSettings.path; - return protocol + hostName + port + subPath + subID; + const { domain: host, port, tls: isTLS, path: base } = app.subSettings; + return buildURL({ host, port, isTLS, base, path: subID }); } }; diff --git a/web/html/xui/inbound_modal.html b/web/html/xui/inbound_modal.html index 25e19473..11e6020c 100644 --- a/web/html/xui/inbound_modal.html +++ b/web/html/xui/inbound_modal.html @@ -96,7 +96,7 @@ set multiDomain(value) { if (value) { inModal.inbound.stream.tls.server = ""; - inModal.inbound.stream.tls.settings.domains = [{remark: "", domain: window.location.host.split(":")[0]}]; + inModal.inbound.stream.tls.settings.domains = [{ remark: "", domain: window.location.hostname }]; } else { inModal.inbound.stream.tls.server = ""; inModal.inbound.stream.tls.settings.domains = []; diff --git a/web/html/xui/inbounds.html b/web/html/xui/inbounds.html index 7b9ba207..329f0f46 100644 --- a/web/html/xui/inbounds.html +++ b/web/html/xui/inbounds.html @@ -311,7 +311,7 @@ { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, - { title: 'UID', width: 120, dataIndex: "id" }, + { title: 'UUID', width: 120, dataIndex: "id" }, ]; const innerTrojanColumns = [ diff --git a/web/html/xui/settings.html b/web/html/xui/settings.html index d78533a1..745959a2 100644 --- a/web/html/xui/settings.html +++ b/web/html/xui/settings.html @@ -91,6 +91,7 @@ + @@ -306,23 +307,37 @@ - - -

- - {{ i18n "pages.settings.templates.manualListsDesc" }} -

-
- - - - - - + + + + +

+ + {{ i18n "pages.settings.templates.manualListsDesc" }} +

+
+ + + + + + + + + + + + + + + + + +
- + @@ -335,7 +350,7 @@ - + @@ -391,9 +406,9 @@ + - @@ -522,7 +537,7 @@ this.loading(false); if (msg.success) { this.user = {}; - window.location.replace(basePath + "logout") + window.location.replace(basePath + "logout"); } }, async restartPanel() { @@ -541,12 +556,10 @@ if (msg.success) { this.loading(true); await PromiseUtil.sleep(5000); - let protocol = "http://"; - if (this.allSetting.webCertFile !== "") { - protocol = "https://"; - } - const { host } = window.location; - window.location.replace(protocol + host + this.allSetting.webBasePath + "panel/settings"); + const { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting; + const isTLS = webCertFile !== "" || webKeyFile !== ""; + const url = buildURL({ host, port, isTLS, base, path: "panel/settings" }); + window.location.replace(url); } }, async fetchUserSecret() { diff --git a/web/middleware/domainValidator.go b/web/middleware/domainValidator.go new file mode 100644 index 00000000..3adb0f0f --- /dev/null +++ b/web/middleware/domainValidator.go @@ -0,0 +1,21 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +func DomainValidatorMiddleware(domain string) gin.HandlerFunc { + return func(c *gin.Context) { + host := strings.Split(c.Request.Host, ":")[0] + + if host != domain { + c.AbortWithStatus(http.StatusForbidden) + return + } + + c.Next() + } +} diff --git a/web/middleware/redirect.go b/web/middleware/redirect.go new file mode 100644 index 00000000..e3dc8ada --- /dev/null +++ b/web/middleware/redirect.go @@ -0,0 +1,34 @@ +package middleware + +import ( + "net/http" + "strings" + + "github.com/gin-gonic/gin" +) + +func RedirectMiddleware(basePath string) gin.HandlerFunc { + return func(c *gin.Context) { + // Redirect from old '/xui' path to '/panel' + redirects := map[string]string{ + "panel/API": "panel/api", + "xui/API": "panel/api", + "xui": "panel", + } + + path := c.Request.URL.Path + for from, to := range redirects { + from, to = basePath+from, basePath+to + + if strings.HasPrefix(path, from) { + newPath := to + path[len(from):] + + c.Redirect(http.StatusMovedPermanently, newPath) + c.Abort() + return + } + } + + c.Next() + } +} diff --git a/web/service/inbound.go b/web/service/inbound.go index 6a182fcf..11522ad2 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -1185,6 +1185,7 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error) } return InboundClientIps.Ips, nil } + func (s *InboundService) ClearClientIps(clientEmail string) error { db := database.GetDB() diff --git a/web/service/setting.go b/web/service/setting.go index 593b23be..677ccbb2 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -24,6 +24,7 @@ var xrayTemplateConfig string var defaultValueMap = map[string]string{ "xrayTemplateConfig": xrayTemplateConfig, "webListen": "", + "webDomain": "", "webPort": "2053", "webCertFile": "", "webKeyFile": "", @@ -44,7 +45,7 @@ var defaultValueMap = map[string]string{ "subEnable": "false", "subListen": "", "subPort": "2096", - "subPath": "sub/", + "subPath": "/sub/", "subDomain": "", "subCertFile": "", "subKeyFile": "", @@ -225,6 +226,10 @@ func (s *SettingService) GetListen() (string, error) { return s.getString("webListen") } +func (s *SettingService) GetWebDomain() (string, error) { + return s.getString("webDomain") +} + func (s *SettingService) GetTgBotToken() (string, error) { return s.getString("tgBotToken") } diff --git a/web/service/tgbot.go b/web/service/tgbot.go index e0261775..d1a1e3fe 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -77,13 +77,15 @@ func (t *Tgbot) Start(i18nFS embed.FS) error { return err } - for _, adminId := range strings.Split(tgBotid, ",") { - id, err := strconv.Atoi(adminId) - if err != nil { - logger.Warning("Failed to get IDs from GetTgBotChatId:", err) - return err + if tgBotid != "" { + for _, adminId := range strings.Split(tgBotid, ",") { + id, err := strconv.Atoi(adminId) + if err != nil { + logger.Warning("Failed to get IDs from GetTgBotChatId:", err) + return err + } + adminIds = append(adminIds, int64(id)) } - adminIds = append(adminIds, int64(id)) } bot, err = telego.NewBot(tgBottoken) @@ -188,7 +190,7 @@ func (t *Tgbot) OnReceive() { } func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) { - msg := "" + msg, onlyMessage := "", false command, commandArgs := tu.ParseCommand(message.Text) @@ -204,8 +206,13 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo } msg += "\n\n" + t.I18nBot("tgbot.commands.pleaseChoose") case "status": + onlyMessage = true msg += t.I18nBot("tgbot.commands.status") + case "id": + onlyMessage = true + msg += t.I18nBot("tgbot.commands.getID", "ID=="+strconv.FormatInt(message.From.ID, 10)) case "usage": + onlyMessage = true if len(commandArgs) > 0 { if isAdmin { t.searchClient(chatId, commandArgs[0]) @@ -216,6 +223,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo msg += t.I18nBot("tgbot.commands.usage") } case "inbound": + onlyMessage = true if isAdmin && len(commandArgs) > 0 { t.searchInbound(chatId, commandArgs[0]) } else { @@ -224,6 +232,11 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo default: msg += t.I18nBot("tgbot.commands.unknown") } + + if onlyMessage { + t.SendMsgToTgbot(chatId, msg) + return + } t.SendAnswer(chatId, msg, isAdmin) } @@ -498,6 +511,7 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R if !isRunning { return } + if msg == "" { logger.Info("[tgbot] message is empty!") return @@ -723,7 +737,7 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string) output := "" output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) - if (traffic.Enable) { + if traffic.Enable { output += t.I18nBot("tgbot.messages.active") if flag { output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) @@ -791,6 +805,7 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ... output := "" output += t.I18nBot("tgbot.messages.email", "Email=="+email) output += t.I18nBot("tgbot.messages.TGUser", "TelegramID=="+tgId) + output += t.I18nBot("tgbot.messages.refreshedOn", "Time=="+time.Now().Format("2006-01-02 15:04:05")) inlineKeyboard := tu.InlineKeyboard( tu.InlineKeyboardRow( @@ -840,7 +855,7 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) { flag := false diff := traffic.ExpiryTime/1000 - now if traffic.ExpiryTime == 0 { - expiryTime = t.I18nBot("tgbot.unlimited") + expiryTime = t.I18nBot("tgbot.unlimited") } else if diff > 172800 || !traffic.Enable { expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") } else if traffic.ExpiryTime < 0 { @@ -860,7 +875,7 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) { output := "" output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) - if (traffic.Enable) { + if traffic.Enable { output += t.I18nBot("tgbot.messages.active") if flag { output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) @@ -918,7 +933,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) { t.SendMsgToTgbot(chatId, msg) return } - + now := time.Now().Unix() for _, inbound := range inbouds { info := "" @@ -958,7 +973,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) { output := "" output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) - if (traffic.Enable) { + if traffic.Enable { output += t.I18nBot("tgbot.messages.active") if flag { output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) @@ -998,7 +1013,7 @@ func (t *Tgbot) searchForClient(chatId int64, query string) { flag := false diff := traffic.ExpiryTime/1000 - now if traffic.ExpiryTime == 0 { - expiryTime = t.I18nBot("tgbot.unlimited") + expiryTime = t.I18nBot("tgbot.unlimited") } else if diff > 172800 || !traffic.Enable { expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05") } else if traffic.ExpiryTime < 0 { @@ -1018,7 +1033,7 @@ func (t *Tgbot) searchForClient(chatId int64, query string) { output := "" output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) - if (traffic.Enable) { + if traffic.Enable { output += t.I18nBot("tgbot.messages.active") if flag { output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) @@ -1117,7 +1132,7 @@ func (t *Tgbot) getExhausted() string { for _, traffic := range exhaustedClients { expiryTime := "" flag := false - diff := (traffic.ExpiryTime - now)/1000 + diff := (traffic.ExpiryTime - now) / 1000 if traffic.ExpiryTime == 0 { expiryTime = t.I18nBot("tgbot.unlimited") } else if diff > 172800 || !traffic.Enable { @@ -1138,7 +1153,7 @@ func (t *Tgbot) getExhausted() string { } output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) - if (traffic.Enable) { + if traffic.Enable { output += t.I18nBot("tgbot.messages.active") if flag { output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index a15bf44c..401a1265 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -168,7 +168,7 @@ "setDefaultCert" = "Set cert from panel" "xtlsDesc" = "Xray core needs to be 1.7.5" "realityDesc" = "Xray core needs to be 1.8.0 or higher." -"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot )" +"telegramDesc" = "use Telegram ID without @ or chat IDs ( you can get it here @userinfobot or use '/id' command in bot )" "subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations" [pages.client] @@ -221,6 +221,8 @@ "TGBotSettings" = "Telegram Bot Settings" "panelListeningIP" = "Panel Listening IP" "panelListeningIPDesc" = "Leave blank by default to monitor all IPs." +"panelListeningDomain" = "Panel Listening Domain" +"panelListeningDomainDesc" = "Leave blank by default to monitor all domains and IPs" "panelPort" = "Panel Port" "panelPortDesc" = "The port used to display this panel" "publicKeyPath" = "Panel Certificate Public Key File Path" @@ -238,7 +240,7 @@ "telegramToken" = "Telegram Token" "telegramTokenDesc" = "You must get the token from the manager of Telegram bots @botfather" "telegramChatId" = "Telegram Admin Chat IDs" -"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot to get your Chat IDs." +"telegramChatIdDesc" = "Multiple Chat IDs separated by comma. use @userinfobot or use '/id' command in bot to get your Chat IDs." "telegramNotifyTime" = "Telegram bot notification time" "telegramNotifyTimeDesc" = "Use Crontab timing format." "tgNotifyBackup" = "Database Backup" @@ -397,6 +399,7 @@ "welcome" = "🤖 Welcome to {{ .Hostname }} management bot.\r\n" "status" = "✅ Bot is ok!" "usage" = "❗ Please provide a text to search!" +"getID" = "🆔 Your ID: {{ .ID }}" "helpAdminCommands" = "Search for a client email:\r\n/usage [Email]\r\n \r\nSearch for inbounds (with client stats):\r\n/inbound [Remark]" "helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n/usage [UUID|Password]\r\n \r\nUse UUID for vmess/vless and Password for Trojan." diff --git a/web/translation/translate.fa_IR.toml b/web/translation/translate.fa_IR.toml index abaa989b..841483c7 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -168,7 +168,7 @@ "setDefaultCert" = "استفاده از گواهی پنل" "xtlsDesc" = "هسته Xray باید 1.7.5 باشد" "realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد" -"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)" +"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)" "subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید" [pages.client] @@ -221,6 +221,8 @@ "TGBotSettings" = "تنظیمات ربات تلگرام" "panelListeningIP" = "محدودیت آی پی پنل" "panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید" +"panelListeningDomain" = "محدودیت دامین پنل" +"panelListeningDomainDesc" = "برای استفاده از تمام دامنه‌ها و آی‌پی‌ها به طور پیش فرض خالی بگذارید" "panelPort" = "پورت پنل" "panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل" "publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل" @@ -238,7 +240,7 @@ "telegramToken" = "توکن تلگرام" "telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather" "telegramChatId" = "آی دی تلگرام مدیریت" -"telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. " +"telegramChatIdDesc" = "از @userinfobot یا دستور '/id' در ربات برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. " "telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام" "telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید " "tgNotifyBackup" = "پشتیبان گیری از پایگاه داده" @@ -397,6 +399,7 @@ "welcome" = "🤖 به ربات مدیریت {{ .Hostname }} خوش آمدید.\r\n" "status" = "✅ ربات در حالت عادی است!" "usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!" +"getID" = "🆔 شناسه شما: {{ .ID }}" "helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n/usage [ایمیل]\r\n \r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n/inbound [توضیح]" "helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n/usage [UUID|رمز عبور]\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید." diff --git a/web/translation/translate.ru_RU.toml b/web/translation/translate.ru_RU.toml index 3f0a18e6..45a73a97 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -168,7 +168,7 @@ "setDefaultCert" = "Установить сертификат с панели" "xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5" "realityDesc" = "Версия Xray должна быть не ниже 1.8.0" -"telegramDesc" = "Используйте Telegram ID без @ или ID пользователя (вы можете получить его у @userinfobot)" +"telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)" "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигураций" [pages.client] @@ -221,6 +221,8 @@ "TGBotSettings" = "Настройки Telegram бота" "panelListeningIP" = "IP адрес панели" "panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP" +"panelListeningDomain" = "Домен прослушивания панели" +"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы отслеживать все домены и IP-адреса" "panelPort" = "Порт панели" "panelPortDesc" = "Порт, используемый для отображения этой панели" "publicKeyPath" = "Путь к файлу публичного ключа сертификата панели" @@ -238,7 +240,7 @@ "telegramToken" = "Токен Telegram бота" "telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather" "telegramChatId" = "Telegram ID админа бота" -"telegramChatIdDesc" = "Несколько Telegram ID, разделённых запятой. Используйте @userinfobot, чтобы получить Telegram ID" +"telegramChatIdDesc" = "Множественные идентификаторы чата, разделенные запятыми. Чтобы получить свои идентификаторы чатов, используйте @userinfobot или команду '/id' в боте." "telegramNotifyTime" = "Частота уведомлений бота Telegram" "telegramNotifyTimeDesc" = "Используйте формат времени Crontab" "tgNotifyBackup" = "Резервное копирование базы данных" @@ -397,6 +399,7 @@ "welcome" = "🤖 Добро пожаловать в бота управления {{ .Hostname }}.\r\n" "status" = "✅ Бот работает нормально!" "usage" = "❗ Пожалуйста, укажите текст для поиска!" +"getID" = "🆔 Ваш ID: {{ .ID }}" "helpAdminCommands" = "Поиск по электронной почте клиента:\r\n/usage [Email]\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n/inbound [Remark]" "helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n/usage [UUID|Password]\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan." diff --git a/web/translation/translate.zh_Hans.toml b/web/translation/translate.zh_Hans.toml index 60f42abc..70c24df7 100644 --- a/web/translation/translate.zh_Hans.toml +++ b/web/translation/translate.zh_Hans.toml @@ -168,7 +168,7 @@ "setDefaultCert" = "从面板设置证书" "xtlsDesc" = "Xray核心需要1.7.5" "realityDesc" = "Xray核心需要1.8.0及以上版本" -"telegramDesc" = "使用不带@的电报 ID 或聊天 ID(您可以在此处获取 @userinfobot)" +"telegramDesc" = "使用 Telegram ID,不包含 @ 符号或聊天 ID(可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)" "subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称" [pages.client] @@ -221,6 +221,8 @@ "TGBotSettings" = "TG提醒相关设置" "panelListeningIP" = "面板监听 IP" "panelListeningIPDesc" = "默认留空监听所有 IP" +"panelListeningDomain" = "面板监听域名" +"panelListeningDomainDesc" = "默认情况下留空以监视所有域名和 IP 地址" "panelPort" = "面板监听端口" "panelPortDesc" = "重启面板生效" "publicKeyPath" = "面板证书公钥文件路径" @@ -238,7 +240,7 @@ "telegramToken" = "电报机器人TOKEN" "telegramTokenDesc" = "重启面板生效" "telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效" -"telegramChatIdDesc" = "多个聊天 ID 以逗号分隔。使用@userinfobot 获取您的聊天 ID。重新启动面板以应用更改。" +"telegramChatIdDesc" = "多个聊天 ID 用逗号分隔。使用 @userinfobot 或在机器人中使用'/id'命令获取您的聊天 ID。" "telegramNotifyTime" = "电报机器人通知时间" "telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效" "tgNotifyBackup" = "数据库备份" @@ -397,6 +399,7 @@ "welcome" = "🤖 欢迎来到{{ .Hostname }}管理机器人。\r\n" "status" = "✅ 机器人正常运行!" "usage" = "❗ 请输入要搜索的文本!" +"getID" = "🆔 您的ID为:{{ .ID }}" "helpAdminCommands" = "搜索客户端邮箱:\r\n/usage [Email]\r\n \r\n搜索入站连接(包含客户端统计信息):\r\n/inbound [Remark]" "helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n/usage [UUID|Password]\r\n \r\n对于vmess/vless,请使用UUID;对于Trojan,请使用密码。" diff --git a/web/web.go b/web/web.go index 543ecf8e..3d8f0242 100644 --- a/web/web.go +++ b/web/web.go @@ -19,6 +19,7 @@ import ( "x-ui/web/controller" "x-ui/web/job" "x-ui/web/locale" + "x-ui/web/middleware" "x-ui/web/network" "x-ui/web/service" @@ -144,28 +145,6 @@ func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template, return t, nil } -func redirectMiddleware(basePath string) gin.HandlerFunc { - return func(c *gin.Context) { - // Redirect from old '/xui' path to '/panel' - path := c.Request.URL.Path - redirects := map[string]string{ - "panel/API": "panel/api", - "xui/API": "panel/api", - "xui": "panel", - } - for from, to := range redirects { - from, to = basePath+from, basePath+to - if strings.HasPrefix(path, from) { - newPath := to + path[len(from):] - c.Redirect(http.StatusMovedPermanently, newPath) - c.Abort() - return - } - } - c.Next() - } -} - func (s *Server) initRouter() (*gin.Engine, error) { if config.IsDebug() { gin.SetMode(gin.DebugMode) @@ -177,6 +156,15 @@ func (s *Server) initRouter() (*gin.Engine, error) { engine := gin.Default() + webDomain, err := s.settingService.GetWebDomain() + if err != nil { + return nil, err + } + + if webDomain != "" { + engine.Use(middleware.DomainValidatorMiddleware(webDomain)) + } + secret, err := s.settingService.GetSecret() if err != nil { return nil, err @@ -233,7 +221,7 @@ func (s *Server) initRouter() (*gin.Engine, error) { } // Apply the redirect middleware (`/xui` to `/panel`) - engine.Use(redirectMiddleware(basePath)) + engine.Use(middleware.RedirectMiddleware(basePath)) g := engine.Group(basePath)