Merge pull request #545 from hamid-gh98/main

🔀 New Feature + Fix URLs + Some Improvements 🛠️🌐
This commit is contained in:
Ho3ein 2023-05-31 09:47:02 +03:30 committed by GitHub
commit 94fad02737
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
23 changed files with 274 additions and 206 deletions

View file

@ -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) [![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)** 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: **If you think this project is helpful to you, you may wish to give a** :star2:
**Buy Me a Coffee :** **Buy Me a Coffee :**
@ -24,11 +23,12 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
# Install custom version # 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 # SSL
``` ```
@ -37,7 +37,7 @@ certbot certonly --standalone --agree-tos --register-unsafely-without-email -d y
certbot renew --dry-run 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 # Features
@ -57,23 +57,26 @@ or you can use x-ui menu then number `16` (`SSL Certificate Management`)
- Support export/import database from panel - Support export/import database from panel
# Manual Install & Upgrade # Manual Install & Upgrade
<details> <details>
<summary>Click for Manual Install details</summary> <summary>Click for Manual Install details</summary>
1. To download the latest version of the compressed package directly to your server, run the following command: 1. To download the latest version of the compressed package directly to your server, run the following command:
```sh ```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: 2. Once the compressed package is downloaded, execute the following commands to install or upgrade x-ui:
```sh ```sh
ARCH=$(uname -m)
[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
cd /root/ cd /root/
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui 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 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 x-ui/x-ui.sh /usr/bin/x-ui
cp -f x-ui/x-ui.service /etc/systemd/system/ cp -f x-ui/x-ui.service /etc/systemd/system/
@ -82,14 +85,16 @@ systemctl daemon-reload
systemctl enable x-ui systemctl enable x-ui
systemctl restart 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.
</details> </details>
# Install with Docker # Install with Docker
<details> <details>
<summary>Click for Docker details</summary> <summary>Click for Docker details</summary>
1. Install Docker: 1. Install Docker:
```sh ```sh
bash <(curl -sSL https://get.docker.com) bash <(curl -sSL https://get.docker.com)
``` ```
@ -119,9 +124,11 @@ Note: If your server's CPU architecture is `arm64`, modify the `amd64` in `tar z
--name 3x-ui \ --name 3x-ui \
ghcr.io/mhsanaei/3x-ui:latest ghcr.io/mhsanaei/3x-ui:latest
``` ```
</details> </details>
# Default settings # Default settings
<details> <details>
<summary>Click for Default settings details</summary> <summary>Click for Default settings details</summary>
@ -171,7 +178,7 @@ If you want to use routing to WARP follow steps as below:
2. Install WARP on **socks proxy mode**: 2. Install WARP on **socks proxy mode**:
```sh ```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) 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)
@ -216,16 +223,18 @@ Reference syntax:
- CPU threshold notification - CPU threshold notification
- Threshold for Expiration time and Traffic to report in advance - 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 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 - Menu based bot
- Search client by email ( only admin ) - Search client by email ( only admin )
- Check all inbounds - Check all inbounds
- Check server status - Check server status
- Check depleted users - Check depleted users
- Receive backup by request and in periodic reports - Receive backup by request and in periodic reports
- Multi language bot
</details> </details>
# API routes # API routes
<details> <details>
<summary>Click for API routes details</summary> <summary>Click for API routes details</summary>
@ -261,6 +270,7 @@ Reference syntax:
</details> </details>
# Environment Variables # Environment Variables
<details> <details>
<summary>Click for Environment Variables details</summary> <summary>Click for Environment Variables details</summary>
@ -276,6 +286,7 @@ Example:
```sh ```sh
XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go XUI_BIN_FOLDER="bin" XUI_DB_FOLDER="/etc/x-ui" go build main.go
``` ```
</details> </details>
# A Special Thanks To # A Special Thanks To

View file

@ -7,10 +7,10 @@ import (
"net" "net"
"net/http" "net/http"
"strconv" "strconv"
"strings"
"x-ui/config" "x-ui/config"
"x-ui/logger" "x-ui/logger"
"x-ui/util/common" "x-ui/util/common"
"x-ui/web/middleware"
"x-ui/web/network" "x-ui/web/network"
"x-ui/web/service" "x-ui/web/service"
@ -58,18 +58,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
} }
if subDomain != "" { if subDomain != "" {
validateDomain := func(c *gin.Context) { engine.Use(middleware.DomainValidatorMiddleware(subDomain))
host := strings.Split(c.Request.Host, ":")[0]
if host != subDomain {
c.AbortWithStatus(http.StatusForbidden)
return
}
c.Next()
}
engine.Use(validateDomain)
} }
g := engine.Group(subPath) g := engine.Group(subPath)
@ -116,11 +105,13 @@ func (s *Server) Start() (err error) {
if err != nil { if err != nil {
return err return err
} }
listenAddr := net.JoinHostPort(listen, strconv.Itoa(port)) listenAddr := net.JoinHostPort(listen, strconv.Itoa(port))
listener, err := net.Listen("tcp", listenAddr) listener, err := net.Listen("tcp", listenAddr)
if err != nil { if err != nil {
return err return err
} }
if certFile != "" || keyFile != "" { if certFile != "" || keyFile != "" {
cert, err := tls.LoadX509KeyPair(certFile, keyFile) cert, err := tls.LoadX509KeyPair(certFile, keyFile)
if err != nil { if err != nil {

View file

@ -168,6 +168,7 @@ class AllSetting {
constructor(data) { constructor(data) {
this.webListen = ""; this.webListen = "";
this.webDomain = "";
this.webPort = 2053; this.webPort = 2053;
this.webCertFile = ""; this.webCertFile = "";
this.webKeyFile = ""; this.webKeyFile = "";
@ -187,7 +188,7 @@ class AllSetting {
this.subEnable = false; this.subEnable = false;
this.subListen = ""; this.subListen = "";
this.subPort = "2096"; this.subPort = "2096";
this.subPath = "sub/"; this.subPath = "/sub/";
this.subDomain = ""; this.subDomain = "";
this.subCertFile = ""; this.subCertFile = "";
this.subKeyFile = ""; this.subKeyFile = "";

View file

@ -135,3 +135,21 @@ function doAllItemsExist(array1, array2) {
} }
return true; 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}`;
}

View file

@ -65,6 +65,7 @@ func (a *InboundController) getInbounds(c *gin.Context) {
} }
jsonObj(c, inbounds, nil) jsonObj(c, inbounds, nil)
} }
func (a *InboundController) getInbound(c *gin.Context) { func (a *InboundController) getInbound(c *gin.Context) {
id, err := strconv.Atoi(c.Param("id")) id, err := strconv.Atoi(c.Param("id"))
if err != nil { if err != nil {
@ -168,6 +169,7 @@ func (a *InboundController) clearClientIps(c *gin.Context) {
} }
jsonMsg(c, "Log Cleared", nil) jsonMsg(c, "Log Cleared", nil)
} }
func (a *InboundController) addInboundClient(c *gin.Context) { func (a *InboundController) addInboundClient(c *gin.Context) {
data := &model.Inbound{} data := &model.Inbound{}
err := c.ShouldBind(data) err := c.ShouldBind(data)

View file

@ -65,77 +65,42 @@ func (a *SettingController) getDefaultJsonConfig(c *gin.Context) {
} }
func (a *SettingController) getDefaultSettings(c *gin.Context) { func (a *SettingController) getDefaultSettings(c *gin.Context) {
expireDiff, err := a.settingService.GetExpireDiff() 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() },
}
result := make(map[string]interface{})
for key, fn := range settings {
value, err := fn()
if err != nil { if err != nil {
jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err) jsonMsg(c, I18nWeb(c, "pages.settings.toasts.getSettings"), err)
return return
} }
trafficDiff, err := a.settingService.GetTrafficDiff() result[key] = value
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
} }
subTLS := false subTLS := false
if subKeyFile != "" || subCertFile != "" { if result["subKeyFile"] != "" || result["subCertFile"] != "" {
subTLS = true subTLS = true
} }
result := map[string]interface{}{ result["subTLS"] = subTLS
"expireDiff": expireDiff,
"trafficDiff": trafficDiff, delete(result, "subKeyFile")
"defaultCert": defaultCert, delete(result, "subCertFile")
"defaultKey": defaultKey,
"tgBotEnable": tgBotEnable,
"subEnable": subEnable,
"subPort": subPort,
"subPath": subPath,
"subDomain": subDomain,
"subTLS": subTLS,
}
jsonObj(c, result, nil) jsonObj(c, result, nil)
} }

View file

@ -28,6 +28,7 @@ type Pager struct {
type AllSetting struct { type AllSetting struct {
WebListen string `json:"webListen" form:"webListen"` WebListen string `json:"webListen" form:"webListen"`
WebDomain string `json:"webDomain" form:"webDomain"`
WebPort int `json:"webPort" form:"webPort"` WebPort int `json:"webPort" form:"webPort"`
WebCertFile string `json:"webCertFile" form:"webCertFile"` WebCertFile string `json:"webCertFile" form:"webCertFile"`
WebKeyFile string `json:"webKeyFile" form:"webKeyFile"` WebKeyFile string `json:"webKeyFile" form:"webKeyFile"`

View file

@ -18,7 +18,6 @@ type HashStorage struct {
sync.RWMutex sync.RWMutex
Data map[string]HashEntry Data map[string]HashEntry
Expiration time.Duration Expiration time.Duration
} }
func NewHashStorage(expiration time.Duration) *HashStorage { func NewHashStorage(expiration time.Duration) *HashStorage {
@ -46,7 +45,6 @@ func (h *HashStorage) SaveHash(query string) string {
return md5HashString return md5HashString
} }
func (h *HashStorage) GetValue(hash string) (string, bool) { func (h *HashStorage) GetValue(hash string) (string, bool) {
h.RLock() h.RLock()
defer h.RUnlock() defer h.RUnlock()

View file

@ -85,16 +85,12 @@
}); });
}, },
genSubLink(subID) { genSubLink(subID) {
protocol = app.subSettings.tls ? "https://" : "http://"; const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain; return buildURL({ host, port, isTLS, base, path: subID });
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;
} }
}, },
updated() { updated() {
if (qrModal.client.subId){ if (qrModal.client && qrModal.client.subId) {
qrModal.subId = qrModal.client.subId; qrModal.subId = qrModal.client.subId;
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId)); this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
} }

View file

@ -253,12 +253,8 @@
infoModal.visible = false; infoModal.visible = false;
}, },
genSubLink(subID) { genSubLink(subID) {
protocol = app.subSettings.tls ? "https://" : "http://"; const { domain: host, port, tls: isTLS, path: base } = app.subSettings;
hostName = app.subSettings.domain === "" ? window.location.hostname : app.subSettings.domain; return buildURL({ host, port, isTLS, base, path: subID });
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;
} }
}; };

View file

@ -96,7 +96,7 @@
set multiDomain(value) { set multiDomain(value) {
if (value) { if (value) {
inModal.inbound.stream.tls.server = ""; 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 { } else {
inModal.inbound.stream.tls.server = ""; inModal.inbound.stream.tls.server = "";
inModal.inbound.stream.tls.settings.domains = []; inModal.inbound.stream.tls.settings.domains = [];

View file

@ -311,7 +311,7 @@
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } }, { title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } }, { title: '{{ i18n "pages.inbounds.traffic" }}↑|↓', width: 120, scopedSlots: { customRender: 'traffic' } },
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } }, { title: '{{ i18n "pages.inbounds.expireDate" }}', width: 70, scopedSlots: { customRender: 'expiryTime' } },
{ title: 'UID', width: 120, dataIndex: "id" }, { title: 'UUID', width: 120, dataIndex: "id" },
]; ];
const innerTrojanColumns = [ const innerTrojanColumns = [

View file

@ -91,6 +91,7 @@
</a-row> </a-row>
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle"> <a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningIP"}}' desc='{{ i18n "pages.settings.panelListeningIPDesc"}}' v-model="allSetting.webListen"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.panelListeningDomain"}}' desc='{{ i18n "pages.settings.panelListeningDomainDesc"}}' v-model="allSetting.webDomain"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item> <setting-list-item type="number" title='{{ i18n "pages.settings.panelPort"}}' desc='{{ i18n "pages.settings.panelPortDesc"}}' v-model="allSetting.webPort" :min="0"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.publicKeyPath"}}' desc='{{ i18n "pages.settings.publicKeyPathDesc"}}' v-model="allSetting.webCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.privateKeyPath"}}' desc='{{ i18n "pages.settings.privateKeyPathDesc"}}' v-model="allSetting.webKeyFile"></setting-list-item>
@ -306,23 +307,37 @@
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigNetflixWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigNetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARP"}}' desc='{{ i18n "pages.settings.templates.xrayConfigSpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualLists"}}'> </a-collapse>
</a-tab-pane>
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.manualLists"}}' style="padding-top: 20px;">
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
<h2 class="collapse-title"> <h2 class="collapse-title">
<a-icon type="warning"></a-icon> <a-icon type="warning"></a-icon>
{{ i18n "pages.settings.templates.manualListsDesc" }} {{ i18n "pages.settings.templates.manualListsDesc" }}
</h2> </h2>
</a-row> </a-row>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedIPs"}}' v-model="manualBlockedIPs"></setting-list-item> <a-collapse>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualBlockedDomains"}}' v-model="manualBlockedDomains"></setting-list-item> <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedIPs"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectIPs"}}' v-model="manualDirectIPs"></setting-list-item> <setting-list-item type="textarea" v-model="manualBlockedIPs"></setting-list-item>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualDirectDomains"}}' v-model="manualDirectDomains"></setting-list-item> </a-collapse-panel>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualIPv4Domains"}}' v-model="manualIPv4Domains"></setting-list-item> <a-collapse-panel header='{{ i18n "pages.settings.templates.manualBlockedDomains"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.manualWARPDomains"}}' v-model="manualWARPDomains"></setting-list-item> <setting-list-item type="textarea" v-model="manualBlockedDomains"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectIPs"}}'>
<setting-list-item type="textarea" v-model="manualDirectIPs"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualDirectDomains"}}'>
<setting-list-item type="textarea" v-model="manualDirectDomains"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualIPv4Domains"}}'>
<setting-list-item type="textarea" v-model="manualIPv4Domains"></setting-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.settings.templates.manualWARPDomains"}}'>
<setting-list-item type="textarea" v-model="manualWARPDomains"></setting-list-item>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tpl-2" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;"> <a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.advancedTemplate"}}' style="padding-top: 20px;">
<a-collapse> <a-collapse>
<a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'> <a-collapse-panel header='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}'>
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item> <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigInbounds"}}' desc='{{ i18n "pages.settings.templates.xrayConfigInboundsDesc"}}' v-model="inboundSettings"></setting-list-item>
@ -335,7 +350,7 @@
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;"> <a-tab-pane key="tpl-4" tab='{{ i18n "pages.settings.templates.completeTemplate"}}' style="padding-top: 20px;">
<setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item> <setting-list-item type="textarea" title='{{ i18n "pages.settings.templates.xrayConfigTemplate"}}' desc='{{ i18n "pages.settings.templates.xrayConfigTemplateDesc"}}' v-model="allSetting.xrayTemplateConfig"></setting-list-item>
</a-tab-pane> </a-tab-pane>
</a-tabs> </a-tabs>
@ -391,9 +406,9 @@
<a-list item-layout="horizontal" :style="themeSwitcher.textStyle"> <a-list item-layout="horizontal" :style="themeSwitcher.textStyle">
<setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.subEnable"}}' desc='{{ i18n "pages.settings.subEnableDesc"}}' v-model="allSetting.subEnable"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.subListen"}}' desc='{{ i18n "pages.settings.subListenDesc"}}' v-model="allSetting.subListen"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item> <setting-list-item type="number" title='{{ i18n "pages.settings.subPort"}}' desc='{{ i18n "pages.settings.subPortDesc"}}' v-model.number="allSetting.subPort"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subPath"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subDomain"}}' desc='{{ i18n "pages.settings.subDomainDesc"}}' v-model="allSetting.subDomain"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.subCertPath"}}' desc='{{ i18n "pages.settings.subCertPathDesc"}}' v-model="allSetting.subCertFile"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item> <setting-list-item type="text" title='{{ i18n "pages.settings.subKeyPath"}}' desc='{{ i18n "pages.settings.subKeyPathDesc"}}' v-model="allSetting.subKeyFile"></setting-list-item>
<setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item> <setting-list-item type="number" title='{{ i18n "pages.settings.subUpdates"}}' desc='{{ i18n "pages.settings.subUpdatesDesc"}}' v-model="allSetting.subUpdates"></setting-list-item>
@ -522,7 +537,7 @@
this.loading(false); this.loading(false);
if (msg.success) { if (msg.success) {
this.user = {}; this.user = {};
window.location.replace(basePath + "logout") window.location.replace(basePath + "logout");
} }
}, },
async restartPanel() { async restartPanel() {
@ -541,12 +556,10 @@
if (msg.success) { if (msg.success) {
this.loading(true); this.loading(true);
await PromiseUtil.sleep(5000); await PromiseUtil.sleep(5000);
let protocol = "http://"; const { webCertFile, webKeyFile, webDomain: host, webPort: port, webBasePath: base } = this.allSetting;
if (this.allSetting.webCertFile !== "") { const isTLS = webCertFile !== "" || webKeyFile !== "";
protocol = "https://"; const url = buildURL({ host, port, isTLS, base, path: "panel/settings" });
} window.location.replace(url);
const { host } = window.location;
window.location.replace(protocol + host + this.allSetting.webBasePath + "panel/settings");
} }
}, },
async fetchUserSecret() { async fetchUserSecret() {

View file

@ -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()
}
}

View file

@ -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()
}
}

View file

@ -1185,6 +1185,7 @@ func (s *InboundService) GetInboundClientIps(clientEmail string) (string, error)
} }
return InboundClientIps.Ips, nil return InboundClientIps.Ips, nil
} }
func (s *InboundService) ClearClientIps(clientEmail string) error { func (s *InboundService) ClearClientIps(clientEmail string) error {
db := database.GetDB() db := database.GetDB()

View file

@ -24,6 +24,7 @@ var xrayTemplateConfig string
var defaultValueMap = map[string]string{ var defaultValueMap = map[string]string{
"xrayTemplateConfig": xrayTemplateConfig, "xrayTemplateConfig": xrayTemplateConfig,
"webListen": "", "webListen": "",
"webDomain": "",
"webPort": "2053", "webPort": "2053",
"webCertFile": "", "webCertFile": "",
"webKeyFile": "", "webKeyFile": "",
@ -44,7 +45,7 @@ var defaultValueMap = map[string]string{
"subEnable": "false", "subEnable": "false",
"subListen": "", "subListen": "",
"subPort": "2096", "subPort": "2096",
"subPath": "sub/", "subPath": "/sub/",
"subDomain": "", "subDomain": "",
"subCertFile": "", "subCertFile": "",
"subKeyFile": "", "subKeyFile": "",
@ -225,6 +226,10 @@ func (s *SettingService) GetListen() (string, error) {
return s.getString("webListen") return s.getString("webListen")
} }
func (s *SettingService) GetWebDomain() (string, error) {
return s.getString("webDomain")
}
func (s *SettingService) GetTgBotToken() (string, error) { func (s *SettingService) GetTgBotToken() (string, error) {
return s.getString("tgBotToken") return s.getString("tgBotToken")
} }

View file

@ -77,6 +77,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
return err return err
} }
if tgBotid != "" {
for _, adminId := range strings.Split(tgBotid, ",") { for _, adminId := range strings.Split(tgBotid, ",") {
id, err := strconv.Atoi(adminId) id, err := strconv.Atoi(adminId)
if err != nil { if err != nil {
@ -85,6 +86,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
} }
adminIds = append(adminIds, int64(id)) adminIds = append(adminIds, int64(id))
} }
}
bot, err = telego.NewBot(tgBottoken) bot, err = telego.NewBot(tgBottoken)
if err != nil { if err != nil {
@ -188,7 +190,7 @@ func (t *Tgbot) OnReceive() {
} }
func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) { func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin bool) {
msg := "" msg, onlyMessage := "", false
command, commandArgs := tu.ParseCommand(message.Text) 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") msg += "\n\n" + t.I18nBot("tgbot.commands.pleaseChoose")
case "status": case "status":
onlyMessage = true
msg += t.I18nBot("tgbot.commands.status") 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": case "usage":
onlyMessage = true
if len(commandArgs) > 0 { if len(commandArgs) > 0 {
if isAdmin { if isAdmin {
t.searchClient(chatId, commandArgs[0]) 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") msg += t.I18nBot("tgbot.commands.usage")
} }
case "inbound": case "inbound":
onlyMessage = true
if isAdmin && len(commandArgs) > 0 { if isAdmin && len(commandArgs) > 0 {
t.searchInbound(chatId, commandArgs[0]) t.searchInbound(chatId, commandArgs[0])
} else { } else {
@ -224,6 +232,11 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
default: default:
msg += t.I18nBot("tgbot.commands.unknown") msg += t.I18nBot("tgbot.commands.unknown")
} }
if onlyMessage {
t.SendMsgToTgbot(chatId, msg)
return
}
t.SendAnswer(chatId, msg, isAdmin) t.SendAnswer(chatId, msg, isAdmin)
} }
@ -498,6 +511,7 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
if !isRunning { if !isRunning {
return return
} }
if msg == "" { if msg == "" {
logger.Info("[tgbot] message is empty!") logger.Info("[tgbot] message is empty!")
return return
@ -723,7 +737,7 @@ func (t *Tgbot) getClientUsage(chatId int64, tgUserName string, tgUserID string)
output := "" output := ""
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
if (traffic.Enable) { if traffic.Enable {
output += t.I18nBot("tgbot.messages.active") output += t.I18nBot("tgbot.messages.active")
if flag { if flag {
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
@ -791,6 +805,7 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
output := "" output := ""
output += t.I18nBot("tgbot.messages.email", "Email=="+email) output += t.I18nBot("tgbot.messages.email", "Email=="+email)
output += t.I18nBot("tgbot.messages.TGUser", "TelegramID=="+tgId) 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( inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@ -860,7 +875,7 @@ func (t *Tgbot) searchClient(chatId int64, email string, messageID ...int) {
output := "" output := ""
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
if (traffic.Enable) { if traffic.Enable {
output += t.I18nBot("tgbot.messages.active") output += t.I18nBot("tgbot.messages.active")
if flag { if flag {
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
@ -958,7 +973,7 @@ func (t *Tgbot) searchInbound(chatId int64, remark string) {
output := "" output := ""
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
if (traffic.Enable) { if traffic.Enable {
output += t.I18nBot("tgbot.messages.active") output += t.I18nBot("tgbot.messages.active")
if flag { if flag {
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
@ -1018,7 +1033,7 @@ func (t *Tgbot) searchForClient(chatId int64, query string) {
output := "" output := ""
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
if (traffic.Enable) { if traffic.Enable {
output += t.I18nBot("tgbot.messages.active") output += t.I18nBot("tgbot.messages.active")
if flag { if flag {
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)
@ -1138,7 +1153,7 @@ func (t *Tgbot) getExhausted() string {
} }
output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email) output += t.I18nBot("tgbot.messages.email", "Email=="+traffic.Email)
if (traffic.Enable) { if traffic.Enable {
output += t.I18nBot("tgbot.messages.active") output += t.I18nBot("tgbot.messages.active")
if flag { if flag {
output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime) output += t.I18nBot("tgbot.messages.expireIn", "Time=="+expiryTime)

View file

@ -168,7 +168,7 @@
"setDefaultCert" = "Set cert from panel" "setDefaultCert" = "Set cert from panel"
"xtlsDesc" = "Xray core needs to be 1.7.5" "xtlsDesc" = "Xray core needs to be 1.7.5"
"realityDesc" = "Xray core needs to be 1.8.0 or higher." "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" "subscriptionDesc" = "you can find your sub link on Details, also you can use the same name for several configurations"
[pages.client] [pages.client]
@ -221,6 +221,8 @@
"TGBotSettings" = "Telegram Bot Settings" "TGBotSettings" = "Telegram Bot Settings"
"panelListeningIP" = "Panel Listening IP" "panelListeningIP" = "Panel Listening IP"
"panelListeningIPDesc" = "Leave blank by default to monitor all IPs." "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" "panelPort" = "Panel Port"
"panelPortDesc" = "The port used to display this panel" "panelPortDesc" = "The port used to display this panel"
"publicKeyPath" = "Panel Certificate Public Key File Path" "publicKeyPath" = "Panel Certificate Public Key File Path"
@ -238,7 +240,7 @@
"telegramToken" = "Telegram Token" "telegramToken" = "Telegram Token"
"telegramTokenDesc" = "You must get the token from the manager of Telegram bots @botfather" "telegramTokenDesc" = "You must get the token from the manager of Telegram bots @botfather"
"telegramChatId" = "Telegram Admin Chat IDs" "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" "telegramNotifyTime" = "Telegram bot notification time"
"telegramNotifyTimeDesc" = "Use Crontab timing format." "telegramNotifyTimeDesc" = "Use Crontab timing format."
"tgNotifyBackup" = "Database Backup" "tgNotifyBackup" = "Database Backup"
@ -397,6 +399,7 @@
"welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n" "welcome" = "🤖 Welcome to <b>{{ .Hostname }}</b> management bot.\r\n"
"status" = "✅ Bot is ok!" "status" = "✅ Bot is ok!"
"usage" = "❗ Please provide a text to search!" "usage" = "❗ Please provide a text to search!"
"getID" = "🆔 Your ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>" "helpAdminCommands" = "Search for a client email:\r\n<code>/usage [Email]</code>\r\n \r\nSearch for inbounds (with client stats):\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan." "helpClientCommands" = "To search for statistics, just use folowing command:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nUse UUID for vmess/vless and Password for Trojan."

View file

@ -168,7 +168,7 @@
"setDefaultCert" = "استفاده از گواهی پنل" "setDefaultCert" = "استفاده از گواهی پنل"
"xtlsDesc" = "هسته Xray باید 1.7.5 باشد" "xtlsDesc" = "هسته Xray باید 1.7.5 باشد"
"realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد" "realityDesc" = "هسته Xray باید 1.8.0 و بالاتر باشد"
"telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot)" "telegramDesc" = "از آیدی تلگرام بدون @ یا آیدی چت استفاده کنید (می توانید آن را از اینجا دریافت کنید @userinfobot یا در ربات دستور '/id' را وارد کنید)"
"subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید" "subscriptionDesc" = "می توانید ساب لینک خود را در جزئیات پیدا کنید، همچنین می توانید از همین نام برای چندین کانفیگ استفاده کنید"
[pages.client] [pages.client]
@ -221,6 +221,8 @@
"TGBotSettings" = "تنظیمات ربات تلگرام" "TGBotSettings" = "تنظیمات ربات تلگرام"
"panelListeningIP" = "محدودیت آی پی پنل" "panelListeningIP" = "محدودیت آی پی پنل"
"panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید" "panelListeningIPDesc" = "برای استفاده از تمام آی‌پیها به طور پیش فرض خالی بگذارید"
"panelListeningDomain" = "محدودیت دامین پنل"
"panelListeningDomainDesc" = "برای استفاده از تمام دامنه‌ها و آی‌پی‌ها به طور پیش فرض خالی بگذارید"
"panelPort" = "پورت پنل" "panelPort" = "پورت پنل"
"panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل" "panelPortDesc" = "پورت مورد استفاده برای نمایش این پنل"
"publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل" "publicKeyPath" = "مسیر فایل گواهی کلید عمومی پنل"
@ -238,7 +240,7 @@
"telegramToken" = "توکن تلگرام" "telegramToken" = "توکن تلگرام"
"telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather" "telegramTokenDesc" = "توکن را باید از مدیر بات های تلگرام دریافت کنید @botfather"
"telegramChatId" = "آی دی تلگرام مدیریت" "telegramChatId" = "آی دی تلگرام مدیریت"
"telegramChatIdDesc" = "از @userinfobot برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. " "telegramChatIdDesc" = "از @userinfobot یا دستور '/id' در ربات برای دریافت شناسه های چت خود استفاده کنید. با استفاده از کاما میتونید چند آی دی را از هم جدا کنید. "
"telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام" "telegramNotifyTime" = "مدت زمان نوتیفیکیشن ربات تلگرام"
"telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید " "telegramNotifyTimeDesc" = "از فرمت زمان بندی لینوکس استفاده کنید "
"tgNotifyBackup" = "پشتیبان گیری از پایگاه داده" "tgNotifyBackup" = "پشتیبان گیری از پایگاه داده"
@ -397,6 +399,7 @@
"welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n" "welcome" = "🤖 به ربات مدیریت <b>{{ .Hostname }}</b> خوش آمدید.\r\n"
"status" = "✅ ربات در حالت عادی است!" "status" = "✅ ربات در حالت عادی است!"
"usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!" "usage" = "❗ لطفاً یک متن برای جستجو وارد کنید!"
"getID" = "🆔 شناسه شما: <code>{{ .ID }}</code>"
"helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n \r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>" "helpAdminCommands" = "برای جستجوی ایمیل مشتری:\r\n<code>/usage [ایمیل]</code>\r\n \r\nبرای جستجوی ورودی‌ها (با آمار مشتری):\r\n<code>/inbound [توضیح]</code>"
"helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n<code>/usage [UUID|رمز عبور]</code>\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید." "helpClientCommands" = "برای جستجوی آمار، فقط از دستور زیر استفاده کنید:\r\n \r\n<code>/usage [UUID|رمز عبور]</code>\r\n \r\nاز UUID برای vmess/vless و از رمز عبور برای Trojan استفاده کنید."

View file

@ -168,7 +168,7 @@
"setDefaultCert" = "Установить сертификат с панели" "setDefaultCert" = "Установить сертификат с панели"
"xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5" "xtlsDesc" = "Версия Xray должна быть не ниже 1.7.5"
"realityDesc" = "Версия Xray должна быть не ниже 1.8.0" "realityDesc" = "Версия Xray должна быть не ниже 1.8.0"
"telegramDesc" = "Используйте Telegram ID без @ или ID пользователя (вы можете получить его у @userinfobot)" "telegramDesc" = "Используйте идентификатор Telegram без символа @ или идентификатора чата (можно получить его здесь @userinfobot или использовать команду '/id' в боте)"
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигураций" "subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе «Подробнее», также вы можете использовать одно и то же имя для нескольких конфигураций"
[pages.client] [pages.client]
@ -221,6 +221,8 @@
"TGBotSettings" = "Настройки Telegram бота" "TGBotSettings" = "Настройки Telegram бота"
"panelListeningIP" = "IP адрес панели" "panelListeningIP" = "IP адрес панели"
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP" "panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
"panelListeningDomain" = "Домен прослушивания панели"
"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы отслеживать все домены и IP-адреса"
"panelPort" = "Порт панели" "panelPort" = "Порт панели"
"panelPortDesc" = "Порт, используемый для отображения этой панели" "panelPortDesc" = "Порт, используемый для отображения этой панели"
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели" "publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
@ -238,7 +240,7 @@
"telegramToken" = "Токен Telegram бота" "telegramToken" = "Токен Telegram бота"
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather" "telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
"telegramChatId" = "Telegram ID админа бота" "telegramChatId" = "Telegram ID админа бота"
"telegramChatIdDesc" = "Несколько Telegram ID, разделённых запятой. Используйте @userinfobot, чтобы получить Telegram ID" "telegramChatIdDesc" = "Множественные идентификаторы чата, разделенные запятыми. Чтобы получить свои идентификаторы чатов, используйте @userinfobot или команду '/id' в боте."
"telegramNotifyTime" = "Частота уведомлений бота Telegram" "telegramNotifyTime" = "Частота уведомлений бота Telegram"
"telegramNotifyTimeDesc" = "Используйте формат времени Crontab" "telegramNotifyTimeDesc" = "Используйте формат времени Crontab"
"tgNotifyBackup" = "Резервное копирование базы данных" "tgNotifyBackup" = "Резервное копирование базы данных"
@ -397,6 +399,7 @@
"welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>.\r\n" "welcome" = "🤖 Добро пожаловать в бота управления <b>{{ .Hostname }}</b>.\r\n"
"status" = "✅ Бот работает нормально!" "status" = "✅ Бот работает нормально!"
"usage" = "❗ Пожалуйста, укажите текст для поиска!" "usage" = "❗ Пожалуйста, укажите текст для поиска!"
"getID" = "🆔 Ваш ID: <code>{{ .ID }}</code>"
"helpAdminCommands" = "Поиск по электронной почте клиента:\r\n<code>/usage [Email]</code>\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n<code>/inbound [Remark]</code>" "helpAdminCommands" = "Поиск по электронной почте клиента:\r\n<code>/usage [Email]</code>\r\n \r\nПоиск входящих соединений (со статистикой клиента):\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan." "helpClientCommands" = "Для получения статистики используйте следующую команду:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\nИспользуйте UUID для vmess/vless и пароль для Trojan."

View file

@ -168,7 +168,7 @@
"setDefaultCert" = "从面板设置证书" "setDefaultCert" = "从面板设置证书"
"xtlsDesc" = "Xray核心需要1.7.5" "xtlsDesc" = "Xray核心需要1.7.5"
"realityDesc" = "Xray核心需要1.8.0及以上版本" "realityDesc" = "Xray核心需要1.8.0及以上版本"
"telegramDesc" = "使用不带@的电报 ID 或聊天 ID您可以在此处获取 @userinfobot" "telegramDesc" = "使用 Telegram ID不包含 @ 符号或聊天 ID可以在 @userinfobot 处获取,或在机器人中使用'/id'命令"
"subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称" "subscriptionDesc" = "您可以在详细信息上找到您的子链接,也可以对多个配置使用相同的名称"
[pages.client] [pages.client]
@ -221,6 +221,8 @@
"TGBotSettings" = "TG提醒相关设置" "TGBotSettings" = "TG提醒相关设置"
"panelListeningIP" = "面板监听 IP" "panelListeningIP" = "面板监听 IP"
"panelListeningIPDesc" = "默认留空监听所有 IP" "panelListeningIPDesc" = "默认留空监听所有 IP"
"panelListeningDomain" = "面板监听域名"
"panelListeningDomainDesc" = "默认情况下留空以监视所有域名和 IP 地址"
"panelPort" = "面板监听端口" "panelPort" = "面板监听端口"
"panelPortDesc" = "重启面板生效" "panelPortDesc" = "重启面板生效"
"publicKeyPath" = "面板证书公钥文件路径" "publicKeyPath" = "面板证书公钥文件路径"
@ -238,7 +240,7 @@
"telegramToken" = "电报机器人TOKEN" "telegramToken" = "电报机器人TOKEN"
"telegramTokenDesc" = "重启面板生效" "telegramTokenDesc" = "重启面板生效"
"telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效" "telegramChatId" = "以逗号分隔的多个 chatID 重启面板生效"
"telegramChatIdDesc" = "多个聊天 ID 以逗号分隔。使用@userinfobot 获取您的聊天 ID。重新启动面板以应用更改。" "telegramChatIdDesc" = "多个聊天 ID 用逗号分隔。使用 @userinfobot 或在机器人中使用'/id'命令获取您的聊天 ID。"
"telegramNotifyTime" = "电报机器人通知时间" "telegramNotifyTime" = "电报机器人通知时间"
"telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效" "telegramNotifyTimeDesc" = "采用Crontab定时格式,重启面板生效"
"tgNotifyBackup" = "数据库备份" "tgNotifyBackup" = "数据库备份"
@ -397,6 +399,7 @@
"welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n" "welcome" = "🤖 欢迎来到<b>{{ .Hostname }}</b>管理机器人。\r\n"
"status" = "✅ 机器人正常运行!" "status" = "✅ 机器人正常运行!"
"usage" = "❗ 请输入要搜索的文本!" "usage" = "❗ 请输入要搜索的文本!"
"getID" = "🆔 您的ID为<code>{{ .ID }}</code>"
"helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n \r\n搜索入站连接包含客户端统计信息\r\n<code>/inbound [Remark]</code>" "helpAdminCommands" = "搜索客户端邮箱:\r\n<code>/usage [Email]</code>\r\n \r\n搜索入站连接包含客户端统计信息\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\n对于vmess/vless请使用UUID对于Trojan请使用密码。" "helpClientCommands" = "要搜索统计信息,请使用以下命令:\r\n \r\n<code>/usage [UUID|Password]</code>\r\n \r\n对于vmess/vless请使用UUID对于Trojan请使用密码。"

View file

@ -19,6 +19,7 @@ import (
"x-ui/web/controller" "x-ui/web/controller"
"x-ui/web/job" "x-ui/web/job"
"x-ui/web/locale" "x-ui/web/locale"
"x-ui/web/middleware"
"x-ui/web/network" "x-ui/web/network"
"x-ui/web/service" "x-ui/web/service"
@ -144,28 +145,6 @@ func (s *Server) getHtmlTemplate(funcMap template.FuncMap) (*template.Template,
return t, nil 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) { func (s *Server) initRouter() (*gin.Engine, error) {
if config.IsDebug() { if config.IsDebug() {
gin.SetMode(gin.DebugMode) gin.SetMode(gin.DebugMode)
@ -177,6 +156,15 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default() 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() secret, err := s.settingService.GetSecret()
if err != nil { if err != nil {
return nil, err return nil, err
@ -233,7 +221,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
} }
// Apply the redirect middleware (`/xui` to `/panel`) // Apply the redirect middleware (`/xui` to `/panel`)
engine.Use(redirectMiddleware(basePath)) engine.Use(middleware.RedirectMiddleware(basePath))
g := engine.Group(basePath) g := engine.Group(basePath)