diff --git a/README.md b/README.md index 75265783..06b4f347 100644 --- a/README.md +++ b/README.md @@ -23,7 +23,6 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russia | Subscription link + userInfo | :heavy_check_mark: | | Calculate expire date on first usage | :heavy_check_mark: | - **If you think this project is helpful to you, you may wish to give a** :star2: **Buy Me a Coffee :** @@ -31,10 +30,9 @@ xray panel supporting multi-protocol, **Multi-lang (English,Farsi,Chinese,Russia - Tron USDT (TRC20): `TYTq73Gj6dJ67qe58JVPD9zpjW2cc9XgVz` - Tezos (XTZ): tz2Wnh2SsY1eezXrcLChu6idWpgdHzUFQcts - # Install & Upgrade to latest version -``` +```sh bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) ``` @@ -42,21 +40,23 @@ bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.s To install your desired version you can add the version to the end of install command. Example for ver `0.5.2`: -``` +```sh bash <(curl -Ls https://raw.githubusercontent.com/alireza0/x-ui/master/install.sh) 0.5.2 ``` ## Manual install & upgrade -1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases , generally choose Architecture `amd64` +1. First download the latest compressed package from https://github.com/alireza0/x-ui/releases, generally choose Architecture `amd64` 2. Then upload the compressed package to the server's `/root/` directory and `root` rootlog in to the server with user > If your server cpu architecture is not `amd64` replace another architecture -``` +```sh +ARCH=$(uname -m) +[[ "${ARCH}" == "s390x" ]] && XUI_ARCH="s390x" || [[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64" cd /root/ rm x-ui/ /usr/local/x-ui/ /usr/bin/x-ui -rf -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/ @@ -207,13 +207,14 @@ 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 # Common problem @@ -226,7 +227,7 @@ First install the latest version of x-ui on the server where v2-ui is installed, > Please `Close v2-ui` and `restart x-ui`, otherwise the inbound of v2-ui will cause a `port conflict with the inbound of x-ui` -``` +```sh x-ui v2-ui ``` diff --git a/sub/sub.go b/sub/sub.go index be541ed2..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 { diff --git a/web/assets/js/model/models.js b/web/assets/js/model/models.js index b6fb49c9..2af44ada 100644 --- a/web/assets/js/model/models.js +++ b/web/assets/js/model/models.js @@ -166,6 +166,7 @@ class AllSetting { constructor(data) { this.webListen = ""; + this.webDomain = ""; this.webPort = 54321; this.webCertFile = ""; this.webKeyFile = ""; @@ -184,7 +185,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 43bf77ef..d994ae41 100644 --- a/web/assets/js/util/common.js +++ b/web/assets/js/util/common.js @@ -114,3 +114,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 8c1b993c..1f5c0bd8 100644 --- a/web/controller/inbound.go +++ b/web/controller/inbound.go @@ -63,6 +63,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 { diff --git a/web/controller/setting.go b/web/controller/setting.go index 643f9723..54de3a84 100644 --- a/web/controller/setting.go +++ b/web/controller/setting.go @@ -50,77 +50,42 @@ func (a *SettingController) getAllSetting(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 c9a90911..b171db77 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/html/common/qrcode_modal.html b/web/html/common/qrcode_modal.html index b1caeacc..64a7d2c9 100644 --- a/web/html/common/qrcode_modal.html +++ b/web/html/common/qrcode_modal.html @@ -66,8 +66,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', () => { @@ -75,29 +75,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 33937537..87bd8043 100644 --- a/web/html/xui/inbound_info_modal.html +++ b/web/html/xui/inbound_info_modal.html @@ -239,12 +239,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 6659b03e..76feeea2 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 08d0b112..88b4143a 100644 --- a/web/html/xui/inbounds.html +++ b/web/html/xui/inbounds.html @@ -307,7 +307,7 @@ { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 70, 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 ed9c4cfe..f10fa53e 100644 --- a/web/html/xui/settings.html +++ b/web/html/xui/settings.html @@ -67,6 +67,7 @@ + @@ -247,7 +248,32 @@ - + + +

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

+
+ + + + + + + + + + + + + + + + + +
+ @@ -260,7 +286,7 @@ - + @@ -315,9 +341,9 @@ + - @@ -431,7 +457,7 @@ this.loading(false); if (msg.success) { this.user = {}; - window.location.replace(basePath + "logout") + window.location.replace(basePath + "logout"); } }, async restartPanel() { @@ -450,12 +476,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 + "xui/settings"); + const { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting; + const isTLS = webCertFile !== "" || webKeyFile !== ""; + const url = buildURL({ host, port, isTLS, base, path: "xui/settings" }); + window.location.replace(url); } }, async resetXrayConfigToDefault() { 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/service/setting.go b/web/service/setting.go index 510944b0..1e6451db 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": "54321", "webCertFile": "", "webKeyFile": "", @@ -43,7 +44,7 @@ var defaultValueMap = map[string]string{ "subEnable": "false", "subListen": "", "subPort": "2096", - "subPath": "sub/", + "subPath": "/sub/", "subDomain": "", "subCertFile": "", "subKeyFile": "", @@ -209,6 +210,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 7bfdcc48..0859df29 100644 --- a/web/service/tgbot.go +++ b/web/service/tgbot.go @@ -64,13 +64,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 = tgbotapi.NewBotAPI(tgBottoken) @@ -134,9 +136,12 @@ func (t *Tgbot) OnReceive() { } func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin bool) { - msg := "" + msg, onlyMessage := "", false + + command, commandArgs := message.Command(), message.CommandArguments() + // Extract the command from the Message. - switch message.Command() { + switch command { case "help": msg += t.I18nBot("tgbot.commands.help") msg += t.I18nBot("tgbot.commands.pleaseChoose") @@ -147,26 +152,37 @@ func (t *Tgbot) answerCommand(message *tgbotapi.Message, chatId int64, isAdmin b } 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": - if len(message.CommandArguments()) > 1 { + onlyMessage = true + if len(commandArgs) > 1 { if isAdmin { - t.searchClient(chatId, message.CommandArguments()) + t.searchClient(chatId, commandArgs) } else { - t.searchForClient(chatId, message.CommandArguments()) + t.searchForClient(chatId, commandArgs) } } else { msg += t.I18nBot("tgbot.commands.usage") } case "inbound": + onlyMessage = true if isAdmin { - t.searchInbound(chatId, message.CommandArguments()) + t.searchInbound(chatId, commandArgs) } else { msg += t.I18nBot("tgbot.commands.unknown") } default: msg += t.I18nBot("tgbot.commands.unknown") } + + if onlyMessage { + t.SendMsgToTgbot(chatId, msg) + return + } t.SendAnswer(chatId, msg, isAdmin) } @@ -239,6 +255,7 @@ func (t *Tgbot) SendMsgToTgbot(tgid int64, msg string, replyMarkup ...tgbotapi.I if !isRunning { return } + if msg == "" { logger.Info("[tgbot] message is empty!") return diff --git a/web/translation/translate.en_US.toml b/web/translation/translate.en_US.toml index 6e1eb15c..2d13b724 100644 --- a/web/translation/translate.en_US.toml +++ b/web/translation/translate.en_US.toml @@ -160,7 +160,7 @@ "email" = "Email" "emailDesc" = "Please provide a unique email address." "setDefaultCert" = "Set cert from panel" -"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] @@ -212,6 +212,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" = "Port number for serving the panel." "publicKeyPath" = "Panel Certificate Public Key File Path" @@ -229,7 +231,7 @@ "telegramToken" = "Telegram Token" "telegramTokenDesc" = "The Token you have got from @BotFather" "telegramChatId" = "Telegram Admin ChatIDs" -"telegramChatIdDesc" = "Multi chatIDs separated by comma." +"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" @@ -360,6 +362,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 f5e64c6d..fbf17bae 100644 --- a/web/translation/translate.fa_IR.toml +++ b/web/translation/translate.fa_IR.toml @@ -159,7 +159,7 @@ "email" = "ایمیل" "emailDesc" = "ایمیل باید کاملا منحصر به فرد باشد" "setDefaultCert" = "استفاده از گواهی پنل" -"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)" +"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)" "subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید" [pages.client] @@ -211,6 +211,8 @@ "TGBotSettings" = "تنظیمات ربات تلگرام" "panelListeningIP" = "محدودیت آی پی پنل" "panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید" +"panelListeningDomain" = "محدودیت دامین پنل" +"panelListeningDomainDesc" = "برای استفاده از تمام دامنه‌ها و آی‌پی‌ها به طور پیش فرض خالی بگذارید" "panelPort" = "پورت پنل" "panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل" "publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل" @@ -228,7 +230,7 @@ "telegramToken" = "توکن تلگرام" "telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather" "telegramChatId" = "آی دی تلگرام مدیریت" -"telegramChatIdDesc" = "با استفاده از کاما میتونید چند آی دی را از هم جدا کنید" +"telegramChatIdDesc" = "از @userinfobot یا دستور '/id' در ربات برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. " "telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام" "telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید " "tgNotifyBackup" = "پشتیبان گیری از پایگاه داده" @@ -358,6 +360,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 82c6bad3..ac6ac609 100644 --- a/web/translation/translate.ru_RU.toml +++ b/web/translation/translate.ru_RU.toml @@ -160,7 +160,7 @@ "email" = "Email" "emailDesc" = "Пожалуйста, укажите уникальный Email" "setDefaultCert" = "Установить сертификат с панели" -"telegramDesc" = "используйте Telegram ID (вы можете получить его у @userinfobot)" +"telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)" "subscriptionDesc" = "вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигов" [pages.client] @@ -212,6 +212,8 @@ "TGBotSettings" = "Настройки Телеграм-бота" "panelListeningIP" = "IP-порт панели" "panelListeningIPDesc" = "Оставьте пустым для работы с любого IP. Перезагрузите панель для применения настроек" +"panelListeningDomain" = "Домен прослушивания панели" +"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы отслеживать все домены и IP-адреса" "panelPort" = "Порт панели" "panelPortDesc" = "Перезагрузите панель для применения настроек" "publicKeyPath" = "Путь к файлу публичного ключа сертификата панели" @@ -229,7 +231,7 @@ "telegramToken" = "Токен Телеграм-бота" "telegramTokenDesc" = "Перезагрузите панель для применения настроек" "telegramChatId" = "Телеграм-ID админа бота" -"telegramChatIdDesc" = "Если несколько Телеграм-ID, разделить запятой. Используйте @userinfobot, чтобы получить Телеграм-ID. Перезагрузите панель для применения настроек" +"telegramChatIdDesc" = "Множественные идентификаторы чата, разделенные запятыми. Чтобы получить свои идентификаторы чатов, используйте @userinfobot или команду '/id' в боте." "telegramNotifyTime" = "Частота уведомлений телеграм-бота" "telegramNotifyTimeDesc" = "Используйте формат Crontab. Перезагрузите панель для применения настроек" "tgNotifyBackup" = "Резервное копирование базы данных" @@ -359,6 +361,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 e3fe2922..a1d2e1a4 100644 --- a/web/translation/translate.zh_Hans.toml +++ b/web/translation/translate.zh_Hans.toml @@ -160,7 +160,7 @@ "email" = "电子邮件" "emailDesc" = "电子邮件必须完全唯" "setDefaultCert" = "从面板设置证书" -"telegramDesc" = "使用不带@的电报 ID 或聊天 ID(您可以在此处获取 @userinfobot)" +"telegramDesc" = "使用 Telegram ID,不包含 @ 符号或聊天 ID(可以在 @userinfobot 处获取,或在机器人中使用'/id'命令)" "subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称" [pages.client] @@ -212,6 +212,8 @@ "TGBotSettings" = "TG提醒相关设置" "panelListeningIP" = "面板监听 IP" "panelListeningIPDesc" = "默认留空监听所有 IP" +"panelListeningDomain" = "面板监听域名" +"panelListeningDomainDesc" = "默认情况下留空以监视所有域名和 IP 地址" "panelPort" = "面板监听端口" "panelPortDesc" = "重启面板生效" "publicKeyPath" = "面板证书公钥文件路径" @@ -229,7 +231,7 @@ "telegramToken" = "电报机器人TOKEN" "telegramTokenDesc" = "重启面板生效" "telegramChatId" = "以逗号分隔的多个 chatID" -"telegramChatIdDesc" = "重启面板生效" +"telegramChatIdDesc" = "多个聊天 ID 用逗号分隔。使用 @userinfobot 或在机器人中使用'/id'命令获取您的聊天 ID。" "telegramNotifyTime" = "电报机器人通知时间" "telegramNotifyTimeDesc" = "采用Crontab定时格式" "tgNotifyBackup" = "数据库备份" @@ -359,6 +361,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 a8c9a533..02e858bc 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" @@ -155,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