Compare commits

..

1 Commits

Author SHA1 Message Date
MHSanaei
f9c703ea44 remove multi protocol script 2024-01-27 19:07:17 +03:30
81 changed files with 838 additions and 4422 deletions

View File

@@ -1,4 +1,5 @@
name: Release 3X-UI for Docker name: Release 3X-UI for Docker
on: on:
push: push:
tags: tags:
@@ -6,12 +7,26 @@ on:
workflow_dispatch: workflow_dispatch:
jobs: jobs:
build_and_push: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
steps: steps:
- name: Check out the code - name: Checkout repository
uses: actions/checkout@v4.1.1 uses: actions/checkout@v4.1.1
with:
submodules: true
- name: Docker meta
id: meta
uses: docker/metadata-action@v5
with:
images: |
ghcr.io/mhsanaei/3x-ui
tags: |
type=ref,event=branch
type=ref,event=tag
type=pep440,pattern={{version}}
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0 uses: docker/setup-qemu-action@v3.0.0
@@ -22,20 +37,14 @@ jobs:
uses: docker/login-action@v3.0.0 uses: docker/login-action@v3.0.0
with: with:
registry: ghcr.io registry: ghcr.io
username: ${{ github.actor }} username: ${{ github.repository_owner }}
password: ${{ secrets.GITHUB_TOKEN }} password: ${{ secrets.GITHUB_TOKEN }}
- name: Docker meta - name: Build and push
id: meta
uses: docker/metadata-action@v5.5.1
with:
images: ghcr.io/${{ github.repository }}
- name: Build and push Docker image
uses: docker/build-push-action@v5.1.0 uses: docker/build-push-action@v5.1.0
with: with:
context: . context: .
push: ${{ github.event_name != 'pull_request' }} push: true
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386 platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
tags: ${{ steps.meta.outputs.tags }} tags: ${{ steps.meta.outputs.tags }}
labels: ${{ steps.meta.outputs.labels }} labels: ${{ steps.meta.outputs.labels }}

View File

@@ -25,7 +25,7 @@ jobs:
- name: Setup Go - name: Setup Go
uses: actions/setup-go@v5.0.0 uses: actions/setup-go@v5.0.0
with: with:
go-version: '1.22' go-version: '1.21'
- name: Install dependencies - name: Install dependencies
run: | run: |
@@ -116,11 +116,12 @@ jobs:
- name: Package - name: Package
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
- name: Upload files to GH release - name: Upload
uses: svenstaro/upload-release-action@2.9.0 uses: svenstaro/upload-release-action@2.7.0
with: with:
repo_token: ${{ secrets.GITHUB_TOKEN }} repo_token: ${{ secrets.GITHUB_TOKEN }}
tag: ${{ github.ref }} tag: ${{ github.ref }}
file: x-ui-linux-${{ matrix.platform }}.tar.gz file: x-ui-linux-${{ matrix.platform }}.tar.gz
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
prerelease: true prerelease: true
overwrite: true

View File

@@ -1,7 +1,7 @@
# ======================================================== # ========================================================
# Stage: Builder # Stage: Builder
# ======================================================== # ========================================================
FROM golang:1.22-alpine AS builder FROM golang:1.21-alpine AS builder
WORKDIR /app WORKDIR /app
ARG TARGETARCH ARG TARGETARCH
@@ -28,14 +28,12 @@ WORKDIR /app
RUN apk add --no-cache --update \ RUN apk add --no-cache --update \
ca-certificates \ ca-certificates \
tzdata \ tzdata \
fail2ban \ fail2ban
bash
COPY --from=builder /app/build/ /app/ COPY --from=builder /app/build/ /app/
COPY --from=builder /app/DockerEntrypoint.sh /app/ COPY --from=builder /app/DockerEntrypoint.sh /app/
COPY --from=builder /app/x-ui.sh /usr/bin/x-ui COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
# Configure fail2ban # Configure fail2ban
RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \ RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \
&& cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \ && cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \
@@ -49,5 +47,4 @@ RUN chmod +x \
/usr/bin/x-ui /usr/bin/x-ui
VOLUME [ "/etc/x-ui" ] VOLUME [ "/etc/x-ui" ]
CMD [ "./x-ui" ]
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ] ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]

110
README.md
View File

@@ -25,39 +25,11 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
## Install Custom Version ## Install Custom Version
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.2.0`: To install your desired version, add the version to the end of the installation command. e.g., ver `v2.1.2`:
``` ```
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.0 bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.1.2
``` ```
## SSL Certificate
<details>
<summary>Click for SSL Certificate</summary>
### Cloudflare
The Management script has a built-in SSL certificate application for Cloudflare. To use this script to apply for a certificate, you need the following:
- Cloudflare registered email
- Cloudflare Global API Key
- The domain name has been resolved to the current server through cloudflare
**1:** Run the`x-ui`command on the terminal, then choose `Cloudflare SSL Certificate`.
### Certbot
```
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
***Tip:*** *Certbot is also built into the Management script. You can run the `x-ui` command, then choose `SSL Certificate Management`.*
</details>
## Manual Install & Upgrade ## Manual Install & Upgrade
<details> <details>
@@ -69,17 +41,7 @@ certbot renew --dry-run
```sh ```sh
ARCH=$(uname -m) ARCH=$(uname -m)
case "${ARCH}" in [[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
*) XUI_ARCH="amd64" ;;
esac
wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI_ARCH}.tar.gz
``` ```
@@ -87,16 +49,7 @@ wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI
```sh ```sh
ARCH=$(uname -m) ARCH=$(uname -m)
case "${ARCH}" in [[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
x86_64 | x64 | amd64) XUI_ARCH="amd64" ;;
i*86 | x86) XUI_ARCH="386" ;;
armv8* | armv8 | arm64 | aarch64) XUI_ARCH="arm64" ;;
armv7* | armv7) XUI_ARCH="armv7" ;;
armv6* | armv6) XUI_ARCH="armv6" ;;
armv5* | armv5) XUI_ARCH="armv5" ;;
*) XUI_ARCH="amd64" ;;
esac
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-${XUI_ARCH}.tar.gz tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
@@ -183,25 +136,21 @@ remove 3x-ui from docker
- AlmaLinux 9+ - AlmaLinux 9+
- Rockylinux 9+ - Rockylinux 9+
## Supported Architectures and Devices ## Compatible Architectures & Devices
<details> Supports a variety of different architectures and devices. Here are some of the main architectures that we support:
<summary>Click for Supported Architectures and devices details</summary>
Our platform offers compatibility with a diverse range of architectures and devices, ensuring flexibility across various computing environments. The following are key architectures that we support: - **amd64**: This is the most common architecture for personal computers and servers. It supports most modern operating systems.
- **amd64**: This prevalent architecture is the standard for personal computers and servers, accommodating most modern operating systems seamlessly. - **x86 / i386**: This architecture is prevalent in desktop and laptop computers. It's widely supported by various operating systems and applications. (Ex: Most Windows, macOS, and Linux systems)
- **x86 / i386**: Widely adopted in desktop and laptop computers, this architecture enjoys broad support from numerous operating systems and applications, including but not limited to Windows, macOS, and Linux systems. - **armv8 / arm64 / aarch64**: This is the architecture for modern mobile and embedded devices, including smartphones and tablets. (Ex: Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS,...)
- **armv8 / arm64 / aarch64**: Tailored for contemporary mobile and embedded devices, such as smartphones and tablets, this architecture is exemplified by devices like Raspberry Pi 4, Raspberry Pi 3, Raspberry Pi Zero 2/Zero 2 W, Orange Pi 3 LTS, and more. - **armv7 / arm / arm32**: This is the architecture for older mobile and embedded devices. It is still widely used in many devices. (Ex: Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2,...)
- **armv7 / arm / arm32**: Serving as the architecture for older mobile and embedded devices, it remains widely utilized in devices like Orange Pi Zero LTS, Orange Pi PC Plus, Raspberry Pi 2, among others. - **armv6 / arm / arm32**: This is the architecture for very old embedded devices. While not as common as before, there are still some devices using this architecture. (Ex: Raspberry Pi 1, Raspberry Pi Zero/Zero W,...)
- **armv6 / arm / arm32**: Geared towards very old embedded devices, this architecture, while less prevalent, is still in use. Devices such as Raspberry Pi 1, Raspberry Pi Zero/Zero W, rely on this architecture. - **armv5 / arm / arm32**: This is an older architecture primarily used in early embedded systems. While it's less common today, some legacy devices may still rely on this architecture. (Ex: Early versions of Raspberry Pi, some older smartphones)
- **armv5 / arm / arm32**: An older architecture primarily associated with early embedded systems, it is less common today but may still be found in legacy devices like early Raspberry Pi versions and some older smartphones.
</details>
## Languages ## Languages
@@ -211,7 +160,6 @@ Our platform offers compatibility with a diverse range of architectures and devi
- Russian - Russian
- Vietnamese - Vietnamese
- Spanish - Spanish
- Indonesian
## Features ## Features
@@ -253,6 +201,34 @@ Our platform offers compatibility with a diverse range of architectures and devi
</details> </details>
## SSL Certificate
<details>
<summary>Click for SSL Certificate</summary>
### Cloudflare
The Management script has a built-in SSL certificate application for Cloudflare. To use this script to apply for a certificate, you need the following:
- Cloudflare registered email
- Cloudflare Global API Key
- The domain name has been resolved to the current server through cloudflare
**1:** Run the`x-ui`command on the terminal, then choose `Cloudflare SSL Certificate`.
### Certbot
```
apt-get install certbot -y
certbot certonly --standalone --agree-tos --register-unsafely-without-email -d yourdomain.com
certbot renew --dry-run
```
***Tip:*** *Certbot is also built into the Management script. You can run the `x-ui` command, then choose `SSL Certificate Management`.*
</details>
## [WARP Configuration](https://gitlab.com/fscarmen/warp) ## [WARP Configuration](https://gitlab.com/fscarmen/warp)
<details> <details>
@@ -305,13 +281,13 @@ If you want to use routing to WARP before v2.1.0 follow steps as below:
2. Select `IP Limit Management`. 2. Select `IP Limit Management`.
3. Choose the appropriate options based on your needs. 3. Choose the appropriate options based on your needs.
- make sure you have ./access.log on your Xray Configuration after v2.1.3 we have an option for it - make sure you have access.log on your Xray Configuration
```sh ```sh
"log": { "log": {
"loglevel": "warning",
"access": "./access.log", "access": "./access.log",
"dnsLog": false, "error": "./error.log"
"loglevel": "warning"
}, },
``` ```

View File

@@ -1 +1 @@
2.2.0 2.1.2

View File

@@ -21,7 +21,6 @@ var db *gorm.DB
var initializers = []func() error{ var initializers = []func() error{
initUser, initUser,
initInbound, initInbound,
initOutbound,
initSetting, initSetting,
initInboundClientIps, initInboundClientIps,
initClientTraffic, initClientTraffic,
@@ -52,10 +51,6 @@ func initInbound() error {
return db.AutoMigrate(&model.Inbound{}) return db.AutoMigrate(&model.Inbound{})
} }
func initOutbound() error {
return db.AutoMigrate(&model.OutboundTraffics{})
}
func initSetting() error { func initSetting() error {
return db.AutoMigrate(&model.Setting{}) return db.AutoMigrate(&model.Setting{})
} }

View File

@@ -44,15 +44,6 @@ type Inbound struct {
Tag string `json:"tag" form:"tag" gorm:"unique"` Tag string `json:"tag" form:"tag" gorm:"unique"`
Sniffing string `json:"sniffing" form:"sniffing"` Sniffing string `json:"sniffing" form:"sniffing"`
} }
type OutboundTraffics struct {
Id int `json:"id" form:"id" gorm:"primaryKey;autoIncrement"`
Tag string `json:"tag" form:"tag" gorm:"unique"`
Up int64 `json:"up" form:"up" gorm:"default:0"`
Down int64 `json:"down" form:"down" gorm:"default:0"`
Total int64 `json:"total" form:"total" gorm:"default:0"`
}
type InboundClientIps struct { type InboundClientIps struct {
Id int `json:"id" gorm:"primaryKey;autoIncrement"` Id int `json:"id" gorm:"primaryKey;autoIncrement"`
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"` ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`

30
go.mod
View File

@@ -1,29 +1,29 @@
module x-ui module x-ui
go 1.22.0 go 1.21.4
require ( require (
github.com/Calidity/gin-sessions v1.3.1 github.com/Calidity/gin-sessions v1.3.1
github.com/gin-contrib/gzip v0.0.6 github.com/gin-contrib/gzip v0.0.6
github.com/gin-gonic/gin v1.9.1 github.com/gin-gonic/gin v1.9.1
github.com/goccy/go-json v0.10.2 github.com/goccy/go-json v0.10.2
github.com/mymmrac/telego v0.29.1 github.com/mymmrac/telego v0.28.0
github.com/nicksnyder/go-i18n/v2 v2.4.0 github.com/nicksnyder/go-i18n/v2 v2.3.0
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.1.1 github.com/pelletier/go-toml/v2 v2.1.1
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.24.1 github.com/shirou/gopsutil/v3 v3.23.12
github.com/valyala/fasthttp v1.52.0 github.com/valyala/fasthttp v1.51.0
github.com/xtls/xray-core v1.8.7 github.com/xtls/xray-core v1.8.7
go.uber.org/atomic v1.11.0 go.uber.org/atomic v1.11.0
golang.org/x/text v0.14.0 golang.org/x/text v0.14.0
google.golang.org/grpc v1.62.0 google.golang.org/grpc v1.61.0
gorm.io/driver/sqlite v1.5.5 gorm.io/driver/sqlite v1.5.4
gorm.io/gorm v1.25.7 gorm.io/gorm v1.25.6
) )
require ( require (
github.com/andybalholm/brotli v1.1.0 // indirect github.com/andybalholm/brotli v1.0.6 // indirect
github.com/bytedance/sonic v1.10.2 // indirect github.com/bytedance/sonic v1.10.2 // indirect
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
github.com/chenzhuoyu/iasm v0.9.1 // indirect github.com/chenzhuoyu/iasm v0.9.1 // indirect
@@ -45,11 +45,10 @@ require (
github.com/gorilla/securecookie v1.1.2 // indirect github.com/gorilla/securecookie v1.1.2 // indirect
github.com/gorilla/sessions v1.2.2 // indirect github.com/gorilla/sessions v1.2.2 // indirect
github.com/gorilla/websocket v1.5.1 // indirect github.com/gorilla/websocket v1.5.1 // indirect
github.com/grbit/go-json v0.11.0 // indirect
github.com/jinzhu/inflection v1.0.0 // indirect github.com/jinzhu/inflection v1.0.0 // indirect
github.com/jinzhu/now v1.1.5 // indirect github.com/jinzhu/now v1.1.5 // indirect
github.com/json-iterator/go v1.1.12 // indirect github.com/json-iterator/go v1.1.12 // indirect
github.com/klauspost/compress v1.17.6 // indirect github.com/klauspost/compress v1.17.4 // indirect
github.com/klauspost/cpuid/v2 v2.2.6 // indirect github.com/klauspost/cpuid/v2 v2.2.6 // indirect
github.com/leodido/go-urn v1.2.4 // indirect github.com/leodido/go-urn v1.2.4 // indirect
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
@@ -76,7 +75,6 @@ require (
github.com/ugorji/go/codec v1.2.11 // indirect github.com/ugorji/go/codec v1.2.11 // indirect
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
github.com/valyala/bytebufferpool v1.0.0 // indirect github.com/valyala/bytebufferpool v1.0.0 // indirect
github.com/valyala/fastjson v1.6.4 // indirect
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 // indirect github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 // indirect
github.com/vishvananda/netns v0.0.4 // indirect github.com/vishvananda/netns v0.0.4 // indirect
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 // indirect github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 // indirect
@@ -84,16 +82,16 @@ require (
go.uber.org/mock v0.4.0 // indirect go.uber.org/mock v0.4.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.6.0 // indirect golang.org/x/arch v0.6.0 // indirect
golang.org/x/crypto v0.19.0 // indirect golang.org/x/crypto v0.18.0 // indirect
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
golang.org/x/mod v0.14.0 // indirect golang.org/x/mod v0.14.0 // indirect
golang.org/x/net v0.21.0 // indirect golang.org/x/net v0.20.0 // indirect
golang.org/x/sys v0.17.0 // indirect golang.org/x/sys v0.16.0 // indirect
golang.org/x/time v0.5.0 // indirect golang.org/x/time v0.5.0 // indirect
golang.org/x/tools v0.16.1 // indirect golang.org/x/tools v0.16.1 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
google.golang.org/protobuf v1.32.0 // indirect google.golang.org/protobuf v1.32.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b // indirect gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b // indirect

56
go.sum
View File

@@ -12,8 +12,8 @@ github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8
github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ=
github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE= github.com/Calidity/gin-sessions v1.3.1 h1:nF3dCBWa7TZ4j26iYLwGRmzZy9YODhWoOS3fmi+snyE=
github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns= github.com/Calidity/gin-sessions v1.3.1/go.mod h1:I0+QE6qkO50TeN/n6If6novvxHk4Isvr23U8EdvPdns=
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M= github.com/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY= github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c= github.com/anmitsu/go-shlex v0.0.0-20161002113705-648efa622239/go.mod h1:2FmKhYUyUczH0OGQWaF5ceTx0UBShxjsH6f8oGKYe2c=
github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q= github.com/beorn7/perks v0.0.0-20180321164747-3a771d992973/go.mod h1:Dwedo/Wpr24TaqPxmxbtue+5NUziq4I4S80YR8gNf3Q=
github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g= github.com/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
@@ -124,8 +124,6 @@ github.com/gorilla/sessions v1.2.2 h1:lqzMYz6bOfvn2WriPUjNByzeXIlVzURcPmgMczkmTj
github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ= github.com/gorilla/sessions v1.2.2/go.mod h1:ePLdVu+jbEgHH+KWw8I1z2wqd0BAdAQh/8LRvBeoNcQ=
github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY= github.com/gorilla/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY= github.com/gorilla/websocket v1.5.1/go.mod h1:x3kM2JMyaluk02fnUJpQuwD2dCS5NDG2ZHL0uE0tcaY=
github.com/grbit/go-json v0.11.0 h1:bAbyMdYrYl/OjYsSqLH99N2DyQ291mHy726Mx+sYrnc=
github.com/grbit/go-json v0.11.0/go.mod h1:IYpHsdybQ386+6g3VE6AXQ3uTGa5mquBme5/ZWmtzek=
github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA= github.com/gregjones/httpcache v0.0.0-20180305231024-9cad4c3443a7/go.mod h1:FecbI9+v66THATjSRHfNgh1IVFe/9kFxbXtjV0ctIMA=
github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw= github.com/grpc-ecosystem/grpc-gateway v1.5.0/go.mod h1:RSKVYQBd5MCa4OVpNdGskqpgL2+G+NZTnrVHpWWfpdw=
github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU= github.com/jellevandenhooff/dkim v0.0.0-20150330215556-f50fe3d243e1/go.mod h1:E0B/fFc00Y+Rasa88328GlI/XbtyysCtTHZS8h7IrBU=
@@ -138,8 +136,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo= github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU= github.com/jstemmer/go-junit-report v0.0.0-20190106144839-af01ea7f8024/go.mod h1:6v2b51hI/fHJwM22ozAgKL4VKDeJcHhJFhtBdhmNjmU=
github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck= github.com/kisielk/gotool v1.0.0/go.mod h1:XhKaO+MFFWcvkIS/tQcRk01m1F5IRFswLeQ+oQHNcck=
github.com/klauspost/compress v1.17.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI= github.com/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
github.com/klauspost/compress v1.17.6/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM= github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg= github.com/klauspost/cpuid/v2 v2.0.9/go.mod h1:FInQzS24/EEf25PyTYn52gqo7WaD8xa0213Md/qVLRg=
github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc= github.com/klauspost/cpuid/v2 v2.2.6 h1:ndNyv040zDGIDh8thGkXYjnFtiN02M1PVVF+JE/48xc=
github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws= github.com/klauspost/cpuid/v2 v2.2.6/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
@@ -177,12 +175,12 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJ
github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0= github.com/modern-go/reflect2 v1.0.1/go.mod h1:bx2lNnkwVCuqBIxFjflWJWanXIb3RllmbCylyMrvgv0=
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M= github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk= github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
github.com/mymmrac/telego v0.29.1 h1:nsNnK0mS18OL+unoDjDI6BVfafJBbT8Wtj7rCzEWoM8= github.com/mymmrac/telego v0.28.0 h1:DNXaYISeZw1J9oB81vCNdskLow8gCRRUJxufqLuH3XE=
github.com/mymmrac/telego v0.29.1/go.mod h1:ZLD1+L2TQRr97NPOCoN1V2w8y9kmFov33OfZ3qT8cF4= github.com/mymmrac/telego v0.28.0/go.mod h1:oRperySNzJq8dRTl24+uBF1Uy7tlQGIjid/JQtHDsZg=
github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo= github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJEU3ofeGjhHklVoIGuVj85JJwZ6kWPaJwCIxgnFmo=
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM= github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM= github.com/nicksnyder/go-i18n/v2 v2.3.0 h1:2NPsCsNFCVd7i+Su0xYsBrIhS3bE2XMv5gNTft2O+PQ=
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4= github.com/nicksnyder/go-i18n/v2 v2.3.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs= github.com/onsi/ginkgo/v2 v2.13.2 h1:Bi2gGVkfn6gQcjNjZJVO8Gf0FHzMPf2phUei9tejVMs=
github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM= github.com/onsi/ginkgo/v2 v2.13.2/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg= github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
@@ -232,8 +230,8 @@ github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee/go.mod h1:qwtSXrKuJ
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U= github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb h1:XfLJSPIOUX+osiMraVgIrMR27uMXnRJWGm1+GL8/63U=
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo= github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI= github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU= github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM= github.com/shoenig/go-m1cpu v0.1.6 h1:nxdKQNcEB6vzgA2E2bvzKIYRuNj7XNJ4S/aRSwKzFtM=
github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ= github.com/shoenig/go-m1cpu v0.1.6/go.mod h1:1JJMcUBvfNwpq05QDQVAnx3gUHr9IYF7GNg9SUEw2VQ=
github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU= github.com/shoenig/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
@@ -290,10 +288,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU= github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw= github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc= github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0= github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ= github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU= github.com/viant/assertly v0.4.8/go.mod h1:aGifi++jvCrUaklKEKT0BU95igDNaqkvz+49uaYMPRU=
github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM= github.com/viant/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 h1:tkMT5pTye+1NlKIXETU78NXw0fyjnaNHmJyyLyzw8+U= github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 h1:tkMT5pTye+1NlKIXETU78NXw0fyjnaNHmJyyLyzw8+U=
@@ -323,8 +319,8 @@ golang.org/x/crypto v0.0.0-20181030102418-4d3f4d9ffa16/go.mod h1:6SG95UA2DQfeDnf
golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190308221718-c2843e01d9a2/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w= golang.org/x/crypto v0.0.0-20190313024323-a1f597ede03a/go.mod h1:djNgcEr1/C05ACkg1iLfiJU5Ep61QUkGW8qpdssI0+w=
golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc= golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo= golang.org/x/crypto v0.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU= golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20190121172915-509febef88a4/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM= golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc h1:ao2WRsKSzW6KuUY9IWPwWahcHCgR0s52IfwutMfEbdM=
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI= golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
@@ -342,8 +338,8 @@ golang.org/x/net v0.0.0-20190108225652-1e06a53dbb7e/go.mod h1:mL1N/T3taQHkDXs73r
golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20190213061140-3a22650c66bd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg= golang.org/x/net v0.0.0-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg= golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4= golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44= golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181017192945-9dcd33a902f4/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@@ -373,9 +369,9 @@ golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.11.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.15.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.16.0 h1:xWw16ngr6ZMtmxDyKyIgsE93KNKz5HKmMa3b8ALHidU=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA= golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.17.0 h1:25cE3gD+tdBA7lp7QfhuV+rJiE9YXTcS3VG1SqssI/Y=
golang.org/x/sys v0.17.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@@ -411,14 +407,14 @@ google.golang.org/genproto v0.0.0-20180831171423-11092d34479b/go.mod h1:JiN7NxoA
google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc= google.golang.org/genproto v0.0.0-20181029155118-b69ba1387ce2/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg= google.golang.org/genproto v0.0.0-20181202183823-bd91e49a0898/go.mod h1:7Ep/1NZk928CDR8SjdVbjWNpdIf6nzjE3BTgJDr2Atg=
google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE= google.golang.org/genproto v0.0.0-20190306203927-b5d61aea6440/go.mod h1:VzzqZJRnGkLBvHegQrXjBqPurQTc5/KpmUdxsrq26oE=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80 h1:AjyfHzEPEFp/NpvfN5g+KDla3EMojjhRVZc1i7cj+oM= google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240123012728-ef4313101c80/go.mod h1:PAREbraiVEVGVdTZsVWjSbbTtSyGbAgIIvni8a8CD5s= google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw= google.golang.org/grpc v1.14.0/go.mod h1:yo6s7OP7yaDglbqo1J04qKzAhqBH6lvTonzMVmEdcZw=
google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio= google.golang.org/grpc v1.16.0/go.mod h1:0JHn/cJsOMiMfNA9+DeHDlAU7KAAB5GDlYFpa9MZMio=
google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs= google.golang.org/grpc v1.17.0/go.mod h1:6QZJwpn2B+Zp71q/5VxRsJ6NXXVCE5NRUHRo+f3cWCs=
google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c= google.golang.org/grpc v1.19.0/go.mod h1:mqu4LbDTu4XGKhr4mRzUsmM4RtVoemTSY81AxZiDr8c=
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk= google.golang.org/grpc v1.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE= google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw= google.golang.org/protobuf v1.26.0-rc.1/go.mod h1:jlhhOSvTdKEhbULTjvd4ARK9grFBp09yW+WbY/TyQbw=
google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc= google.golang.org/protobuf v1.26.0/go.mod h1:9q0QmTI4eRPtz6boOQmLYwt+qCgq0jsYwAQnmE0givc=
google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I= google.golang.org/protobuf v1.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
@@ -439,10 +435,10 @@ gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C
gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.0-20210107192922-496545a6307b/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E= gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE= gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A= gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A=
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8= gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o= grpc.go4.org v0.0.0-20170609214715-11d0a25b4919/go.mod h1:77eQGdRu53HpSqPFJFmuJdjuHRquDANNeA4x7B8WQ9o=
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b h1:yqkg3pTifuKukuWanp8spDsL4irJkHF5WI0J47hU87o= gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b h1:yqkg3pTifuKukuWanp8spDsL4irJkHF5WI0J47hU87o=
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk= gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=

View File

@@ -82,16 +82,16 @@ fi
install_base() { install_base() {
case "${release}" in case "${release}" in
centos | almalinux | rocky) centos | almalinux | rocky)
yum -y update && yum install -y -q wget curl tar tzdata yum -y update && yum install -y -q wget curl tar
;; ;;
fedora) fedora)
dnf -y update && dnf install -y -q wget curl tar tzdata dnf -y update && dnf install -y -q wget curl tar
;; ;;
arch | manjaro) arch | manjaro)
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata pacman -Syu && pacman -Syu --noconfirm wget curl tar
;; ;;
*) *)
apt-get update && apt install -y -q wget curl tar tzdata apt-get update && apt install -y -q wget curl tar
;; ;;
esac esac
} }

View File

@@ -65,16 +65,6 @@ func Infof(format string, args ...interface{}) {
addToBuffer("INFO", fmt.Sprintf(format, args...)) addToBuffer("INFO", fmt.Sprintf(format, args...))
} }
func Notice(args ...interface{}) {
logger.Notice(args...)
addToBuffer("NOTICE", fmt.Sprint(args...))
}
func Noticef(format string, args ...interface{}) {
logger.Noticef(format, args...)
addToBuffer("NOTICE", fmt.Sprintf(format, args...))
}
func Warning(args ...interface{}) { func Warning(args ...interface{}) {
logger.Warning(args...) logger.Warning(args...)
addToBuffer("WARNING", fmt.Sprint(args...)) addToBuffer("WARNING", fmt.Sprint(args...))

View File

@@ -1,105 +0,0 @@
{
"dns": {
"tag": "dns_out",
"queryStrategy": "UseIP",
"servers": [
{
"address": "8.8.8.8",
"skipFallback": false
}
]
},
"inbounds": [
{
"port": 10808,
"protocol": "socks",
"settings": {
"auth": "noauth",
"udp": true,
"userLevel": 8
},
"sniffing": {
"destOverride": [
"http",
"tls",
"fakedns"
],
"enabled": true
},
"tag": "socks"
},
{
"port": 10809,
"protocol": "http",
"settings": {
"userLevel": 8
},
"tag": "http"
}
],
"log": {
"loglevel": "warning"
},
"outbounds": [
{
"tag": "direct",
"protocol": "freedom",
"settings": {
"domainStrategy": "UseIP"
}
},
{
"tag": "block",
"protocol": "blackhole",
"settings": {
"response": {
"type": "http"
}
}
}
],
"policy": {
"levels": {
"8": {
"connIdle": 300,
"downlinkOnly": 1,
"handshake": 4,
"uplinkOnly": 1
}
},
"system": {
"statsOutboundUplink": true,
"statsOutboundDownlink": true
}
},
"routing": {
"domainStrategy": "AsIs",
"rules": [
{
"type": "field",
"network": "tcp,udp",
"balancerTag": "all"
}
],
"balancers": [
{
"tag": "all",
"selector": [
"proxy"
],
"strategy": {
"type": "leastPing"
}
}
]
},
"observatory": {
"probeInterval": "5m",
"probeURL": "https://api.github.com/_private/browser/stats",
"subjectSelector": [
"proxy"
],
"EnableConcurrency": true
},
"stats": {}
}

View File

@@ -47,6 +47,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine := gin.Default() engine := gin.Default()
subPath, err := s.settingService.GetSubPath()
if err != nil {
return nil, err
}
subDomain, err := s.settingService.GetSubDomain() subDomain, err := s.settingService.GetSubDomain()
if err != nil { if err != nil {
return nil, err return nil, err
@@ -56,44 +61,9 @@ func (s *Server) initRouter() (*gin.Engine, error) {
engine.Use(middleware.DomainValidatorMiddleware(subDomain)) engine.Use(middleware.DomainValidatorMiddleware(subDomain))
} }
LinksPath, err := s.settingService.GetSubPath() g := engine.Group(subPath)
if err != nil {
return nil, err
}
JsonPath, err := s.settingService.GetSubJsonPath() s.sub = NewSUBController(g)
if err != nil {
return nil, err
}
Encrypt, err := s.settingService.GetSubEncrypt()
if err != nil {
return nil, err
}
ShowInfo, err := s.settingService.GetSubShowInfo()
if err != nil {
return nil, err
}
RemarkModel, err := s.settingService.GetRemarkModel()
if err != nil {
RemarkModel = "-ieo"
}
SubUpdates, err := s.settingService.GetSubUpdates()
if err != nil {
SubUpdates = "10"
}
SubJsonFragment, err := s.settingService.GetSubJsonFragment()
if err != nil {
SubJsonFragment = ""
}
g := engine.Group("/")
s.sub = NewSUBController(g, LinksPath, JsonPath, Encrypt, ShowInfo, RemarkModel, SubUpdates, SubJsonFragment)
return engine, nil return engine, nil
} }

View File

@@ -3,57 +3,34 @@ package sub
import ( import (
"encoding/base64" "encoding/base64"
"strings" "strings"
"x-ui/web/service"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
type SUBController struct { type SUBController struct {
subPath string subService SubService
subJsonPath string settingService service.SettingService
subEncrypt bool
updateInterval string
subService *SubService
subJsonService *SubJsonService
} }
func NewSUBController( func NewSUBController(g *gin.RouterGroup) *SUBController {
g *gin.RouterGroup, a := &SUBController{}
subPath string,
jsonPath string,
encrypt bool,
showInfo bool,
rModel string,
update string,
jsonFragment string) *SUBController {
a := &SUBController{
subPath: subPath,
subJsonPath: jsonPath,
subEncrypt: encrypt,
updateInterval: update,
subService: NewSubService(showInfo, rModel),
subJsonService: NewSubJsonService(jsonFragment),
}
a.initRouter(g) a.initRouter(g)
return a return a
} }
func (a *SUBController) initRouter(g *gin.RouterGroup) { func (a *SUBController) initRouter(g *gin.RouterGroup) {
gLink := g.Group(a.subPath) g = g.Group("/")
gJson := g.Group(a.subJsonPath)
gLink.GET(":subid", a.subs) g.GET("/:subid", a.subs)
gJson.GET(":subid", a.subJsons)
} }
func (a *SUBController) subs(c *gin.Context) { func (a *SUBController) subs(c *gin.Context) {
println(c.Request.Header["User-Agent"][0]) subEncrypt, _ := a.settingService.GetSubEncrypt()
subShowInfo, _ := a.settingService.GetSubShowInfo()
subId := c.Param("subid") subId := c.Param("subid")
host := strings.Split(c.Request.Host, ":")[0] host := strings.Split(c.Request.Host, ":")[0]
subs, header, err := a.subService.GetSubs(subId, host) subs, headers, err := a.subService.GetSubs(subId, host, subShowInfo)
if err != nil || len(subs) == 0 { if err != nil || len(subs) == 0 {
c.String(400, "Error!") c.String(400, "Error!")
} else { } else {
@@ -63,32 +40,14 @@ func (a *SUBController) subs(c *gin.Context) {
} }
// Add headers // Add headers
c.Writer.Header().Set("Subscription-Userinfo", header) c.Writer.Header().Set("Subscription-Userinfo", headers[0])
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval) c.Writer.Header().Set("Profile-Update-Interval", headers[1])
c.Writer.Header().Set("Profile-Title", subId) c.Writer.Header().Set("Profile-Title", headers[2])
if a.subEncrypt { if subEncrypt {
c.String(200, base64.StdEncoding.EncodeToString([]byte(result))) c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
} else { } else {
c.String(200, result) c.String(200, result)
} }
} }
} }
func (a *SUBController) subJsons(c *gin.Context) {
println(c.Request.Header["User-Agent"][0])
subId := c.Param("subid")
host := strings.Split(c.Request.Host, ":")[0]
jsonSub, header, err := a.subJsonService.GetJson(subId, host)
if err != nil || len(jsonSub) == 0 {
c.String(400, "Error!")
} else {
// Add headers
c.Writer.Header().Set("Subscription-Userinfo", header)
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
c.Writer.Header().Set("Profile-Title", subId)
c.String(200, jsonSub)
}
}

View File

@@ -1,355 +0,0 @@
package sub
import (
_ "embed"
"encoding/json"
"fmt"
"strings"
"x-ui/database/model"
"x-ui/logger"
"x-ui/util/json_util"
"x-ui/util/random"
"x-ui/web/service"
"x-ui/xray"
)
//go:embed default.json
var defaultJson string
type SubJsonService struct {
fragmanet string
inboundService service.InboundService
SubService
}
func NewSubJsonService(fragment string) *SubJsonService {
return &SubJsonService{
fragmanet: fragment,
}
}
func (s *SubJsonService) GetJson(subId string, host string) (string, string, error) {
inbounds, err := s.SubService.getInboundsBySubId(subId)
if err != nil || len(inbounds) == 0 {
return "", "", err
}
var header string
var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic
var configJson map[string]interface{}
var defaultOutbounds []json_util.RawMessage
json.Unmarshal([]byte(defaultJson), &configJson)
if outboundSlices, ok := configJson["outbounds"].([]interface{}); ok {
for _, defaultOutbound := range outboundSlices {
jsonBytes, _ := json.Marshal(defaultOutbound)
defaultOutbounds = append(defaultOutbounds, jsonBytes)
}
}
outbounds := []json_util.RawMessage{}
startIndex := 0
// Prepare Inbounds
for _, inbound := range inbounds {
clients, err := s.inboundService.GetClients(inbound)
if err != nil {
logger.Error("SubJsonService - GetClients: Unable to get clients from inbound")
}
if clients == nil {
continue
}
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
if err == nil {
inbound.Listen = listen
inbound.Port = port
inbound.StreamSettings = streamSettings
}
}
var subClients []model.Client
for _, client := range clients {
if client.Enable && client.SubID == subId {
subClients = append(subClients, client)
clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email))
}
}
outbound := s.getOutbound(inbound, subClients, host, startIndex)
if outbound != nil {
outbounds = append(outbounds, outbound...)
startIndex += len(outbound)
}
}
if len(outbounds) == 0 {
return "", "", nil
}
// Prepare statistics
for index, clientTraffic := range clientTraffics {
if index == 0 {
traffic.Up = clientTraffic.Up
traffic.Down = clientTraffic.Down
traffic.Total = clientTraffic.Total
if clientTraffic.ExpiryTime > 0 {
traffic.ExpiryTime = clientTraffic.ExpiryTime
}
} else {
traffic.Up += clientTraffic.Up
traffic.Down += clientTraffic.Down
if traffic.Total == 0 || clientTraffic.Total == 0 {
traffic.Total = 0
} else {
traffic.Total += clientTraffic.Total
}
if clientTraffic.ExpiryTime != traffic.ExpiryTime {
traffic.ExpiryTime = 0
}
}
}
if s.fragmanet != "" {
outbounds = append(outbounds, json_util.RawMessage(s.fragmanet))
}
// Combile outbounds
outbounds = append(outbounds, defaultOutbounds...)
configJson["outbounds"] = outbounds
finalJson, _ := json.MarshalIndent(configJson, "", " ")
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
return string(finalJson), header, nil
}
func (s *SubJsonService) getOutbound(inbound *model.Inbound, clients []model.Client, host string, startIndex int) []json_util.RawMessage {
var newOutbounds []json_util.RawMessage
stream := s.streamData(inbound.StreamSettings)
externalProxies, ok := stream["externalProxy"].([]interface{})
if !ok || len(externalProxies) == 0 {
externalProxies = []interface{}{
map[string]interface{}{
"forceTls": "same",
"dest": host,
"port": float64(inbound.Port),
},
}
}
delete(stream, "externalProxy")
config_index := startIndex
for _, ep := range externalProxies {
extPrxy := ep.(map[string]interface{})
inbound.Listen = extPrxy["dest"].(string)
inbound.Port = int(extPrxy["port"].(float64))
newStream := stream
switch extPrxy["forceTls"].(string) {
case "tls":
if newStream["security"] != "tls" {
newStream["security"] = "tls"
newStream["tslSettings"] = map[string]interface{}{}
}
case "none":
if newStream["security"] != "none" {
newStream["security"] = "none"
delete(newStream, "tslSettings")
}
}
streamSettings, _ := json.MarshalIndent(newStream, "", " ")
inbound.StreamSettings = string(streamSettings)
for _, client := range clients {
inbound.Tag = fmt.Sprintf("proxy_%d", config_index)
switch inbound.Protocol {
case "vmess", "vless":
newOutbounds = append(newOutbounds, s.genVnext(inbound, client))
case "trojan", "shadowsocks":
newOutbounds = append(newOutbounds, s.genServer(inbound, client))
}
config_index += 1
}
}
return newOutbounds
}
func (s *SubJsonService) streamData(stream string) map[string]interface{} {
var streamSettings map[string]interface{}
json.Unmarshal([]byte(stream), &streamSettings)
security, _ := streamSettings["security"].(string)
if security == "tls" {
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]interface{}))
} else if security == "reality" {
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]interface{}))
}
delete(streamSettings, "sockopt")
if s.fragmanet != "" {
streamSettings["sockopt"] = json_util.RawMessage(`{"dialerProxy": "fragment", "tcpKeepAliveIdle": 100, "tcpNoDelay": true}`)
}
// remove proxy protocol
network, _ := streamSettings["network"].(string)
switch network {
case "tcp":
streamSettings["tcpSettings"] = s.removeAcceptProxy(streamSettings["tcpSettings"])
case "ws":
streamSettings["wsSettings"] = s.removeAcceptProxy(streamSettings["wsSettings"])
}
return streamSettings
}
func (s *SubJsonService) removeAcceptProxy(setting interface{}) map[string]interface{} {
netSettings, ok := setting.(map[string]interface{})
if ok {
delete(netSettings, "acceptProxyProtocol")
}
return netSettings
}
func (s *SubJsonService) tlsData(tData map[string]interface{}) map[string]interface{} {
tlsData := make(map[string]interface{}, 1)
tlsClientSettings := tData["settings"].(map[string]interface{})
tlsData["serverName"] = tData["serverName"]
tlsData["alpn"] = tData["alpn"]
if allowInsecure, ok := tlsClientSettings["allowInsecure"].(bool); ok {
tlsData["allowInsecure"] = allowInsecure
}
if fingerprint, ok := tlsClientSettings["fingerprint"].(string); ok {
tlsData["fingerprint"] = fingerprint
}
return tlsData
}
func (s *SubJsonService) realityData(rData map[string]interface{}) map[string]interface{} {
rltyData := make(map[string]interface{}, 1)
rltyClientSettings := rData["settings"].(map[string]interface{})
rltyData["show"] = false
rltyData["publicKey"] = rltyClientSettings["publicKey"]
rltyData["fingerprint"] = rltyClientSettings["fingerprint"]
// Set random data
rltyData["spiderX"] = "/" + random.Seq(15)
shortIds, ok := rData["shortIds"].([]interface{})
if ok && len(shortIds) > 0 {
rltyData["shortId"] = shortIds[random.Num(len(shortIds))].(string)
} else {
rltyData["shortId"] = ""
}
serverNames, ok := rData["serverNames"].([]interface{})
if ok && len(serverNames) > 0 {
rltyData["serverName"] = serverNames[random.Num(len(serverNames))].(string)
} else {
rltyData["serverName"] = ""
}
return rltyData
}
func (s *SubJsonService) genVnext(inbound *model.Inbound, client model.Client) json_util.RawMessage {
outbound := Outbound{}
usersData := make([]UserVnext, 1)
usersData[0].ID = client.ID
usersData[0].Level = 8
if inbound.Protocol == model.VLESS {
usersData[0].Flow = client.Flow
usersData[0].Encryption = "none"
}
vnextData := make([]VnextSetting, 1)
vnextData[0] = VnextSetting{
Address: inbound.Listen,
Port: inbound.Port,
Users: usersData,
}
outbound.Protocol = string(inbound.Protocol)
outbound.Tag = inbound.Tag
outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings)
outbound.Settings = OutboundSettings{
Vnext: vnextData,
}
result, _ := json.MarshalIndent(outbound, "", " ")
return result
}
func (s *SubJsonService) genServer(inbound *model.Inbound, client model.Client) json_util.RawMessage {
outbound := Outbound{}
serverData := make([]ServerSetting, 1)
serverData[0] = ServerSetting{
Address: inbound.Listen,
Port: inbound.Port,
Level: 8,
Password: client.Password,
}
if inbound.Protocol == model.Shadowsocks {
var inboundSettings map[string]interface{}
json.Unmarshal([]byte(inbound.Settings), &inboundSettings)
method, _ := inboundSettings["method"].(string)
serverData[0].Method = method
// server password in multi-user 2022 protocols
if strings.HasPrefix(method, "2022") {
if serverPassword, ok := inboundSettings["password"].(string); ok {
serverData[0].Password = fmt.Sprintf("%s:%s", serverPassword, client.Password)
}
}
}
outbound.Protocol = string(inbound.Protocol)
outbound.Tag = inbound.Tag
outbound.StreamSettings = json_util.RawMessage(inbound.StreamSettings)
outbound.Settings = OutboundSettings{
Servers: serverData,
}
result, _ := json.MarshalIndent(outbound, "", " ")
return result
}
type Outbound struct {
Protocol string `json:"protocol"`
Tag string `json:"tag"`
StreamSettings json_util.RawMessage `json:"streamSettings"`
Mux map[string]interface{} `json:"mux,omitempty"`
ProxySettings map[string]interface{} `json:"proxySettings,omitempty"`
Settings OutboundSettings `json:"settings,omitempty"`
}
type OutboundSettings struct {
Vnext []VnextSetting `json:"vnext,omitempty"`
Servers []ServerSetting `json:"servers,omitempty"`
}
type VnextSetting struct {
Address string `json:"address"`
Port int `json:"port"`
Users []UserVnext `json:"users"`
}
type UserVnext struct {
Encryption string `json:"encryption,omitempty"`
Flow string `json:"flow,omitempty"`
ID string `json:"id"`
Level int `json:"level"`
}
type ServerSetting struct {
Password string `json:"password"`
Level int `json:"level"`
Address string `json:"address"`
Port int `json:"port"`
Flow string `json:"flow,omitempty"`
Method string `json:"method,omitempty"`
}

View File

@@ -25,42 +25,47 @@ type SubService struct {
settingService service.SettingService settingService service.SettingService
} }
func NewSubService(showInfo bool, remarkModel string) *SubService { func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string, []string, error) {
return &SubService{
showInfo: showInfo,
remarkModel: remarkModel,
}
}
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
s.address = host s.address = host
s.showInfo = showInfo
var result []string var result []string
var header string var headers []string
var traffic xray.ClientTraffic var traffic xray.ClientTraffic
var clientTraffics []xray.ClientTraffic var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId) inbounds, err := s.getInboundsBySubId(subId)
if err != nil { if err != nil {
return nil, "", err return nil, nil, err
} }
s.remarkModel, err = s.settingService.GetRemarkModel()
s.datepicker, err = s.settingService.GetDatepicker()
if err != nil { if err != nil {
s.datepicker = "gregorian" s.remarkModel = "-ieo"
} }
s.datepicker, err = s.settingService.GetDatepicker()
if err != nil {
s.datepicker = "gregorian"
}
for _, inbound := range inbounds { for _, inbound := range inbounds {
clients, err := s.inboundService.GetClients(inbound) clients, err := s.inboundService.GetClients(inbound)
if err != nil { if err != nil {
logger.Error("SubService - GetClients: Unable to get clients from inbound") logger.Error("SubService - GetSub: Unable to get clients from inbound")
} }
if clients == nil { if clients == nil {
continue continue
} }
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' { if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings) fallbackMaster, err := s.getFallbackMaster(inbound.Listen)
if err == nil { if err == nil {
inbound.Listen = listen inbound.Listen = fallbackMaster.Listen
inbound.Port = port inbound.Port = fallbackMaster.Port
inbound.StreamSettings = streamSettings var stream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
var masterStream map[string]interface{}
json.Unmarshal([]byte(fallbackMaster.StreamSettings), &masterStream)
stream["security"] = masterStream["security"]
stream["tlsSettings"] = masterStream["tlsSettings"]
stream["externalProxy"] = masterStream["externalProxy"]
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
inbound.StreamSettings = string(modifiedStream)
} }
} }
for _, client := range clients { for _, client := range clients {
@@ -71,8 +76,6 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
} }
} }
} }
// Prepare statistics
for index, clientTraffic := range clientTraffics { for index, clientTraffic := range clientTraffics {
if index == 0 { if index == 0 {
traffic.Up = clientTraffic.Up traffic.Up = clientTraffic.Up
@@ -94,8 +97,11 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
} }
} }
} }
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000) headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
return result, header, nil updateInterval, _ := s.settingService.GetSubUpdates()
headers = append(headers, fmt.Sprintf("%d", updateInterval))
headers = append(headers, subId)
return result, headers, nil
} }
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) { func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
@@ -124,7 +130,7 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
return xray.ClientTraffic{} return xray.ClientTraffic{}
} }
func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) { func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
db := database.GetDB() db := database.GetDB()
var inbound *model.Inbound var inbound *model.Inbound
err := db.Model(model.Inbound{}). err := db.Model(model.Inbound{}).
@@ -132,19 +138,9 @@ func (s *SubService) getFallbackMaster(dest string, streamSettings string) (stri
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest). Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
Find(&inbound).Error Find(&inbound).Error
if err != nil { if err != nil {
return "", 0, "", err return nil, err
} }
return inbound, nil
var stream map[string]interface{}
json.Unmarshal([]byte(streamSettings), &stream)
var masterStream map[string]interface{}
json.Unmarshal([]byte(inbound.StreamSettings), &masterStream)
stream["security"] = masterStream["security"]
stream["tlsSettings"] = masterStream["tlsSettings"]
stream["externalProxy"] = masterStream["externalProxy"]
modifiedStream, _ := json.MarshalIndent(stream, "", " ")
return inbound.Listen, inbound.Port, string(modifiedStream), nil
} }
func (s *SubService) getLink(inbound *model.Inbound, email string) string { func (s *SubService) getLink(inbound *model.Inbound, email string) string {
@@ -582,7 +578,6 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok { if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
params["sni"], _ = sniValue.(string) params["sni"], _ = sniValue.(string)
} }
tlsSettings, _ := searchKey(tlsSetting, "settings") tlsSettings, _ := searchKey(tlsSetting, "settings")
if tlsSetting != nil { if tlsSetting != nil {
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok { if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {

View File

@@ -2,6 +2,7 @@ package random
import ( import (
"math/rand" "math/rand"
"time"
) )
var numSeq [10]rune var numSeq [10]rune
@@ -12,6 +13,8 @@ var numUpperSeq [36]rune
var allSeq [62]rune var allSeq [62]rune
func init() { func init() {
rand.Seed(time.Now().UnixNano())
for i := 0; i < 10; i++ { for i := 0; i < 10; i++ {
numSeq[i] = rune('0' + i) numSeq[i] = rune('0' + i)
} }
@@ -38,7 +41,3 @@ func Seq(n int) string {
} }
return string(runes) return string(runes)
} }
func Num(n int) int {
return rand.Intn(n)
}

View File

@@ -538,7 +538,7 @@
var on = function(emitter, type, f) { var on = function(emitter, type, f) {
if (emitter.addEventListener) { if (emitter.addEventListener) {
emitter.addEventListener(type, f, { passive: true }); emitter.addEventListener(type, f, false);
} else if (emitter.attachEvent) { } else if (emitter.attachEvent) {
emitter.attachEvent("on" + type, f); emitter.attachEvent("on" + type, f);
} else { } else {

View File

@@ -45,8 +45,8 @@ THE SOFTWARE.
.cm-s-xq .CodeMirror-activeline-background { background: #e8f2ff; } .cm-s-xq .CodeMirror-activeline-background { background: #e8f2ff; }
.cm-s-xq .CodeMirror-matchingbracket { outline:1px solid grey;color:black !important;background:yellow; } .cm-s-xq .CodeMirror-matchingbracket { outline:1px solid grey;color:black !important;background:yellow; }
.dark .cm-s-xq.CodeMirror { background-color: #000000; border-color: #25272a; color: rgb(255 255 255 / 85%); } .dark .cm-s-xq.CodeMirror { background-color: #222D42; border-color: #2c3950; color: rgb(255 255 255 / 65%); }
.dark .cm-s-xq.CodeMirror:hover { background-color: rgb(0 50 42 / 20%); border-color: #008771; transition: all .3s; } .dark .cm-s-xq.CodeMirror:hover { background-color: rgb(0 50 42 / 30%); border-color: #008771; transition: all .3s; }
.dark .cm-s-xq div.CodeMirror-selected { background: rgba(0, 0, 0, 0.5); } .dark .cm-s-xq div.CodeMirror-selected { background: rgba(0, 0, 0, 0.5); }
.dark .cm-s-xq .CodeMirror-line::selection, .dark .cm-s-xq .CodeMirror-line > span::selection, .dark .cm-s-xq .CodeMirror-line > span > span::selection { background: rgba(39, 0, 122, 0.99); } .dark .cm-s-xq .CodeMirror-line::selection, .dark .cm-s-xq .CodeMirror-line > span::selection, .dark .cm-s-xq .CodeMirror-line > span > span::selection { background: rgba(39, 0, 122, 0.99); }
.dark .cm-s-xq .CodeMirror-line::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 0, 122, 0.99); } .dark .cm-s-xq .CodeMirror-line::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span::-moz-selection, .dark .cm-s-xq .CodeMirror-line > span > span::-moz-selection { background: rgba(39, 0, 122, 0.99); }

View File

@@ -1,18 +1,3 @@
:root {
--dark-color-background: #21242a;
--dark-color-surface-100: #0c0e12;
--dark-color-surface-200: #222327;
--dark-color-surface-300: #32353b;
--dark-color-surface-400: rgba(255, 255, 255, 0.1);
--dark-color-surface-500: #3b404b;
--dark-color-surface-600: #505663;
--dark-color-text-primary: rgb(255 255 255 / 85%);
--dark-color-stroke: #202025;
--dark-color-btn-danger: #cd3838;
--dark-color-btn-danger-border: transparent;
--dark-color-btn-danger-hover: #e94b4b;
}
html, html,
body { body {
height: 100vh; height: 100vh;
@@ -517,13 +502,13 @@ style attribute {
.dark .ant-table, .dark .ant-table,
.dark .ant-collapse-content, .dark .ant-collapse-content,
.dark .ant-tabs { .dark .ant-tabs {
background-color: var(--dark-color-surface-100); background-color: #151f31;
color: var(--dark-color-text-primary); color: #ffffffa6;
} }
.dark .ant-card-hoverable:hover, .dark .ant-card-hoverable:hover,
.dark .ant-space-item > .ant-tabs:hover { .dark .ant-space-item > .ant-tabs:hover {
box-shadow: 0 2px 8px transparent; box-shadow: 0 1px 10px -1px rgb(154 175 238 / 80%);
} }
.dark > .ant-layout, .dark > .ant-layout,
@@ -533,8 +518,8 @@ style attribute {
.dark .ant-table-expanded-row:hover, .dark .ant-table-expanded-row:hover,
.dark .ant-table-expanded-row .ant-table-tbody, .dark .ant-table-expanded-row .ant-table-tbody,
.dark .ant-calendar { .dark .ant-calendar {
background-color: var(--dark-color-background); background-color: #101828;
color: var(--dark-color-text-primary); color: rgb(255 255 255 /65%);
} }
.dark .ant-table-expanded-row .ant-table-thead > tr:first-child > th { .dark .ant-table-expanded-row .ant-table-thead > tr:first-child > th {
@@ -543,7 +528,7 @@ style attribute {
.dark .ant-calendar, .dark .ant-calendar,
.dark .ant-card-bordered { .dark .ant-card-bordered {
border-color: var(--dark-color-background); border-color: #151f31;
} }
.dark .ant-table-bordered, .dark .ant-table-bordered,
@@ -555,7 +540,7 @@ style attribute {
.dark .ant-table-bordered .ant-table-thead > tr:not(:last-child) > th, .dark .ant-table-bordered .ant-table-thead > tr:not(:last-child) > th,
.dark .ant-table-bordered .ant-table-tbody > tr > td, .dark .ant-table-bordered .ant-table-tbody > tr > td,
.dark .ant-table-bordered .ant-table-thead > tr > th { .dark .ant-table-bordered .ant-table-thead > tr > th {
border-color: var(--dark-color-surface-400); border-color: #2c3950;
} }
.dark .ant-table-tbody > tr > td, .dark .ant-table-tbody > tr > td,
@@ -568,7 +553,7 @@ style attribute {
.dark .ant-popover-title, .dark .ant-popover-title,
.dark .ant-calendar-header, .dark .ant-calendar-header,
.dark .ant-calendar-input-wrap { .dark .ant-calendar-input-wrap {
border-bottom-color: var(--dark-color-surface-400); border-bottom-color: #2c3950;
} }
.dark .ant-modal-footer, .dark .ant-modal-footer,
@@ -576,7 +561,7 @@ style attribute {
.dark .ant-calendar-footer, .dark .ant-calendar-footer,
.dark .ant-divider-horizontal.ant-divider-with-text-center:before, .dark .ant-divider-horizontal.ant-divider-with-text-center:before,
.dark .ant-divider-horizontal.ant-divider-with-text-center:after { .dark .ant-divider-horizontal.ant-divider-with-text-center:after {
border-top-color: var(--dark-color-surface-300); border-top-color: #2c3950;
} }
.dark .ant-progress-text, .dark .ant-progress-text,
@@ -612,7 +597,7 @@ style attribute {
.dark .ant-calendar-year-panel-year, .dark .ant-calendar-year-panel-year,
.dark .ant-calendar-month-panel-month, .dark .ant-calendar-month-panel-month,
.dark .ant-calendar-decade-panel-decade { .dark .ant-calendar-decade-panel-decade {
color: rgba(255, 255, 255, 0.85); color: rgba(255, 255, 255, 0.65);
} }
.dark .ant-list-item-meta-description { .dark .ant-list-item-meta-description {
@@ -638,12 +623,13 @@ style attribute {
.dark .ant-select-dropdown li, .dark .ant-select-dropdown li,
.dark .ant-select-dropdown-menu-item, .dark .ant-select-dropdown-menu-item,
.dark .ant-divider:not(.ant-divider-with-text-center), .dark .ant-divider:not(.ant-divider-with-text-center),
.dark .ant-calendar-input,
.dark .client-table-header, .dark .client-table-header,
.dark .ant-select-selection--multiple .ant-select-selection__choice, .dark .ant-select-selection--multiple .ant-select-selection__choice,
.dark .ant-calendar-time-picker-inner { .dark .ant-calendar-time-picker-inner {
background-color: var(--dark-color-surface-200); background-color: #222d42;
border-color: var(--dark-color-surface-300); border-color: #2c3950;
color: rgba(255, 255, 255, 0.85); color: rgba(255, 255, 255, 0.65);
} }
.dark .ant-select-selection:hover, .dark .ant-select-selection:hover,
@@ -657,7 +643,7 @@ style attribute {
} }
.dark .ant-btn:not(.ant-btn-primary):not(.ant-btn-danger) { .dark .ant-btn:not(.ant-btn-primary):not(.ant-btn-danger) {
color: rgba(255, 255, 255, 0.85); color: rgba(255, 255, 255, 0.65);
background-color: rgb(10 117 87 / 30%); background-color: rgb(10 117 87 / 30%);
border: 1px solid #008771; border: 1px solid #008771;
} }
@@ -680,7 +666,7 @@ style attribute {
.dark .ant-btn-danger[disabled], .dark .ant-btn-danger[disabled],
.dark .ant-calendar-ok-btn-disabled { .dark .ant-calendar-ok-btn-disabled {
color: rgb(255 255 255 / 35%); color: rgb(255 255 255 / 35%);
background-color: var(--dark-color-surface-300); background-color: #2c3950;
border-color: #42516c; border-color: #42516c;
} }
@@ -689,7 +675,7 @@ style attribute {
> tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected) > tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)
> td, > td,
.dark .client-table-odd-row { .dark .client-table-odd-row {
background-color: rgb(89 89 89 / 15%); background-color: #00877122;
} }
.dark .ant-table-row-expand-icon { .dark .ant-table-row-expand-icon {
@@ -706,31 +692,31 @@ style attribute {
.dark .ant-switch:not(.ant-switch-checked), .dark .ant-switch:not(.ant-switch-checked),
.dark .ant-progress-line .ant-progress-inner { .dark .ant-progress-line .ant-progress-inner {
background-color: var(--dark-color-surface-500); background-color: #2c3950;
} }
.dark .ant-progress-circle-trail { .dark .ant-progress-circle-trail {
stroke: var(--dark-color-stroke) !important; stroke: #2c3950 !important;
} }
.ant-dropdown-menu-dark, .ant-dropdown-menu-dark,
.dark .ant-popover-inner { .dark .ant-popover-inner {
background-color: var(--dark-color-surface-500); background-color: #222d42;
} }
.dark > .ant-popover-content > .ant-popover-arrow { .dark > .ant-popover-content > .ant-popover-arrow {
border-color: var(--dark-color-surface-500); border-color: #222d42;
} }
.ant-dropdown-menu-dark .ant-dropdown-menu-item:hover, .ant-dropdown-menu-dark .ant-dropdown-menu-item:hover,
.dark .ant-select-dropdown-menu-item-selected, .dark .ant-select-dropdown-menu-item-selected,
.dark .ant-select-dropdown-menu-item:hover, .dark .ant-select-dropdown-menu-item:hover,
.dark .ant-calendar-time-picker-select-option-selected { .dark .ant-calendar-time-picker-select-option-selected {
background-color: var(--dark-color-surface-600); background-color: #313f5a;
} }
.ant-menu-dark .ant-menu-item:hover { .ant-menu-dark .ant-menu-item:hover {
background-color: var(--dark-color-surface-300); background-color: #2c3950;
} }
.dark .ant-alert-message { .dark .ant-alert-message {
@@ -738,9 +724,9 @@ style attribute {
} }
.dark .ant-tag { .dark .ant-tag {
color: rgba(255, 255, 255, 0.85); color: rgba(255, 255, 255, 0.65);
background-color: rgba(255, 255, 255, 0.08); background-color: #ffffff0a;
border-color: rgba(255, 255, 255, 0.15); border-color: #344461;
} }
.dark .ant-tag-blue { .dark .ant-tag-blue {
@@ -751,38 +737,38 @@ style attribute {
.dark .ant-tag-red, .dark .ant-tag-red,
.dark .ant-alert-error { .dark .ant-alert-error {
background-color: #2a1215; background-color: #291515;
border-color: #58181c; border-color: #5c2626;
color: #e84749; color: #e04141;
} }
.dark .ant-tag-orange, .dark .ant-tag-orange,
.dark .ant-alert-warning { .dark .ant-alert-warning {
background-color: #2b1d11; background-color: #312313;
border-color: #593815; border-color: #593914;
color: #e89a3c; color: #ffa031;
} }
.dark .ant-tag-green { .dark .ant-tag-green {
background-color: #112421; background-color: #112421;
border-color: #195544; border-color: #144840;
color: #59cbac; color: #33bca5;
} }
.dark .ant-tag-purple { .dark .ant-tag-purple {
background-color: #241121; background-color: #2c1e32;
border-color: #5a2969; border-color: #49394e;
color: #d686ca; color: #cfb9cc;
} }
.dark .ant-modal-content, .dark .ant-modal-content,
.dark .ant-modal-header { .dark .ant-modal-header {
background-color: #101113; background-color: #181f2c;
} }
.dark .ant-calendar-next-month-btn-day .ant-calendar-date, .dark .ant-calendar-next-month-btn-day .ant-calendar-date,
.dark .ant-calendar-last-month-cell .ant-calendar-date { .dark .ant-calendar-last-month-cell .ant-calendar-date {
color: var(--dark-color-surface-300); color: #2c3950;
} }
.dark .ant-calendar-selected-day .ant-calendar-date { .dark .ant-calendar-selected-day .ant-calendar-date {
@@ -792,7 +778,7 @@ style attribute {
.dark .ant-calendar-date:hover, .dark .ant-calendar-date:hover,
.dark .ant-calendar-time-picker-select li:hover { .dark .ant-calendar-time-picker-select li:hover {
background-color: var(--dark-color-surface-300); background-color: #313f5a;
color: #fff; color: #fff;
} }
@@ -810,7 +796,7 @@ style attribute {
} }
.dark .ant-calendar-time-picker-select { .dark .ant-calendar-time-picker-select {
border-right-color: var(--dark-color-surface-300); border-right-color: #2c3950;
} }
.has-warning .ant-input, .has-warning .ant-input,
@@ -971,7 +957,7 @@ li.ant-select-dropdown-menu-item:empty:after {
} }
.dark .ant-calendar-year-panel-header { .dark .ant-calendar-year-panel-header {
border-bottom: 1px solid var(--dark-color-surface-200); border-bottom: 1px solid #222d42;
} }
.dark .ant-calendar-year-panel-last-decade-cell .ant-calendar-year-panel-year, .dark .ant-calendar-year-panel-last-decade-cell .ant-calendar-year-panel-year,
@@ -982,7 +968,7 @@ li.ant-select-dropdown-menu-item:empty:after {
.dark .ant-calendar-year-panel-year:hover, .dark .ant-calendar-year-panel-year:hover,
.dark .ant-calendar-month-panel-month:hover, .dark .ant-calendar-month-panel-month:hover,
.dark .ant-calendar-decade-panel-decade:hover { .dark .ant-calendar-decade-panel-decade:hover {
background-color: var(--dark-color-surface-600); background-color: #222d42;
} }
.dark .ant-calendar-header a:hover { .dark .ant-calendar-header a:hover {
@@ -990,13 +976,13 @@ li.ant-select-dropdown-menu-item:empty:after {
} }
.dark .ant-calendar-month-panel-header { .dark .ant-calendar-month-panel-header {
background-color: var(--dark-color-background); background-color: #101828;
border-bottom: 1px solid var(--dark-color-surface-200); border-bottom: 1px solid #222d42;
} }
.dark .ant-calendar-year-panel, .dark .ant-calendar-year-panel,
.dark .ant-calendar table { .dark .ant-calendar table {
background-color: var(--dark-color-background); background-color: #101828;
} }
.dark .ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year, .dark .ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year,
@@ -1042,8 +1028,8 @@ li.ant-select-dropdown-menu-item:empty:after {
} }
.dark .ant-calendar-decade-panel-header { .dark .ant-calendar-decade-panel-header {
border-bottom: 1px solid var(--dark-color-surface-200); border-bottom: 1px solid #222d42;
background-color: var(--dark-color-background); background-color: #101828;
} }
.dark .ant-checkbox-inner { .dark .ant-checkbox-inner {
@@ -1057,25 +1043,19 @@ li.ant-select-dropdown-menu-item:empty:after {
} }
.dark .ant-calendar-input { .dark .ant-calendar-input {
background-color: var(--dark-color-background); background-color: #101828;
color: var(--dark-color-text-primary);
} }
.dark .ant-calendar-input::placeholder { .dark .ant-calendar-input::placeholder {
color: rgba(255, 255, 255, 0.25); color: rgba(255, 255, 255, 0.25);
} }
.ant-input-group.ant-input-group-compact-addon:not(:first-child):not(
:last-child
),
.ant-input-group.ant-input-group-compact-wrap:not(:first-child):not(
:last-child
),
.ant-input-group.ant-input-group-compact
> .ant-input:not(:first-child):not(:last-child),
.ant-input-number-handler,
.ant-input-number-handler-wrap { .ant-input-number-handler-wrap {
border-radius: 0; border-radius: 0;
}
.ant-input-number-handler {
border-radius: 0;
} }
.ant-input-number { .ant-input-number {
@@ -1109,15 +1089,15 @@ li.ant-select-dropdown-menu-item:empty:after {
> td, > td,
.ant-table-thead .ant-table-thead
> tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected) > tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)
> td, > td {
.ant-calendar-time-picker-select li:hover {
background-color: rgb(232 244 242); background-color: rgb(232 244 242);
} }
.dark .ant-dropdown-menu-item:hover,
.dark .ant-dropdown-menu-submenu-title:hover, .dark .ant-dropdown-menu-submenu-title:hover,
.dark .ant-select-dropdown-menu-item-active:not(.ant-select-dropdown-menu-item-disabled), .dark .ant-select-dropdown-menu-item-active:not(.ant-select-dropdown-menu-item-disabled),
.dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled) { .dark .ant-select-dropdown-menu-item:hover:not(.ant-select-dropdown-menu-item-disabled) {
background-color: var(--dark-color-surface-300); background-color: #313f5a;
} }
.ant-select-dropdown, .ant-select-dropdown,
@@ -1130,8 +1110,6 @@ li.ant-select-dropdown-menu-item:empty:after {
} }
.qr-bg { .qr-bg {
width: 100%;
height: 100%;
background-color: #fff; background-color: #fff;
display: flex; display: flex;
justify-content: center; justify-content: center;
@@ -1143,35 +1121,3 @@ li.ant-select-dropdown-menu-item:empty:after {
.ant-input-group-addon:not(:first-child):not(:last-child), .ant-input-group-wrap:not(:first-child):not(:last-child), .ant-input-group>.ant-input:not(:first-child):not(:last-child) { .ant-input-group-addon:not(:first-child):not(:last-child), .ant-input-group-wrap:not(:first-child):not(:last-child), .ant-input-group>.ant-input:not(:first-child):not(:last-child) {
border-radius: 0rem 1rem 1rem 0rem; border-radius: 0rem 1rem 1rem 0rem;
} }
.ant-tag {
margin-right: 6px;
}
b, strong {
font-weight: 500;
}
.ant-collapse>.ant-collapse-item>.ant-collapse-header {
padding: 10px 16px 10px 40px;
}
.dark .ant-message-notice-content {
background-color: #000000;
border: 1px solid #303134;
color: rgba(255, 255, 255, 0.85);
}
.ant-btn-danger {
background-color: var(--dark-color-btn-danger);
border-color: var(--dark-color-btn-danger-border);
}
.ant-btn-danger:focus, .ant-btn-danger:hover {
background-color: var(--dark-color-btn-danger-hover);
border-color: var(--dark-color-btn-danger-hover);
}
.dark .ant-alert-close-icon .anticon-close:hover {
color: rgb(255 255 255);
}

BIN
web/assets/favicon.ico Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

View File

@@ -29,11 +29,6 @@ const supportLangs = [
value: 'es-ES', value: 'es-ES',
icon: '🇪🇸', icon: '🇪🇸',
}, },
{
name: 'Indonesian',
value: 'id-ID',
icon: '🇮🇩',
},
]; ];
function getLang() { function getLang() {

View File

@@ -418,7 +418,7 @@ class Outbound extends CommonClass {
} }
canEnableTls() { canEnableTls() {
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false; if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.stream.network); return ["tcp", "ws", "http", "quic", "grpc"].includes(this.stream.network);
} }
@@ -861,13 +861,13 @@ Outbound.SocksSettings = class extends CommonClass {
} }
static fromJson(json={}) { static fromJson(json={}) {
let servers = json.servers; servers = json.servers;
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}]; if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
return new Outbound.SocksSettings( return new Outbound.SocksSettings(
servers[0].address, servers[0].address,
servers[0].port, servers[0].port,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user, ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass, ObjectUtil.isArrEmpty(servers[0].pass) ? '' : servers[0].users[0].pass,
); );
} }
@@ -891,13 +891,13 @@ Outbound.HttpSettings = class extends CommonClass {
} }
static fromJson(json={}) { static fromJson(json={}) {
let servers = json.servers; servers = json.servers;
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}]; if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
return new Outbound.HttpSettings( return new Outbound.HttpSettings(
servers[0].address, servers[0].address,
servers[0].port, servers[0].port,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user, ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass, ObjectUtil.isArrEmpty(servers[0].pass) ? '' : servers[0].users[0].pass,
); );
} }
@@ -914,8 +914,8 @@ Outbound.HttpSettings = class extends CommonClass {
Outbound.WireguardSettings = class extends CommonClass { Outbound.WireguardSettings = class extends CommonClass {
constructor( constructor(
mtu=1420, secretKey='', mtu=1420, secretKey=Wireguard.generateKeypair().privateKey,
address=[''], workers=2, domainStrategy='', reserved='', address=[''], workers=2, domainStrategy='ForceIPv6v4', reserved='',
peers=[new Outbound.WireguardSettings.Peer()], kernelMode=false) { peers=[new Outbound.WireguardSettings.Peer()], kernelMode=false) {
super(); super();
this.mtu = mtu; this.mtu = mtu;
@@ -957,7 +957,7 @@ Outbound.WireguardSettings = class extends CommonClass {
address: this.address ? this.address.split(",") : [], address: this.address ? this.address.split(",") : [],
workers: this.workers?? undefined, workers: this.workers?? undefined,
domainStrategy: WireguardDomainStrategy.includes(this.domainStrategy) ? this.domainStrategy : undefined, domainStrategy: WireguardDomainStrategy.includes(this.domainStrategy) ? this.domainStrategy : undefined,
reserved: this.reserved ? this.reserved.split(",").map(Number) : undefined, reserved: this.reserved ? this.reserved.split(",") : undefined,
peers: Outbound.WireguardSettings.Peer.toJsonArray(this.peers), peers: Outbound.WireguardSettings.Peer.toJsonArray(this.peers),
kernelMode: this.kernelMode, kernelMode: this.kernelMode,
}; };
@@ -965,7 +965,7 @@ Outbound.WireguardSettings = class extends CommonClass {
}; };
Outbound.WireguardSettings.Peer = class extends CommonClass { Outbound.WireguardSettings.Peer = class extends CommonClass {
constructor(publicKey='', psk='', allowedIPs=['0.0.0.0/0','::/0'], endpoint='', keepAlive=0) { constructor(publicKey=Wireguard.generateKeypair().publicKey, psk='', allowedIPs=['0.0.0.0/0','::/0'], endpoint='', keepAlive=0) {
super(); super();
this.publicKey = publicKey; this.publicKey = publicKey;
this.psk = psk; this.psk = psk;

View File

@@ -28,7 +28,6 @@ class AllSetting {
this.subListen = ""; this.subListen = "";
this.subPort = "2096"; this.subPort = "2096";
this.subPath = "/sub/"; this.subPath = "/sub/";
this.subJsonPath = "/json/";
this.subDomain = ""; this.subDomain = "";
this.subCertFile = ""; this.subCertFile = "";
this.subKeyFile = ""; this.subKeyFile = "";
@@ -36,8 +35,6 @@ class AllSetting {
this.subEncrypt = true; this.subEncrypt = true;
this.subShowInfo = false; this.subShowInfo = false;
this.subURI = ''; this.subURI = '';
this.subJsonURI = '';
this.subJsonFragment = '';
this.timeLocation = "Asia/Tehran"; this.timeLocation = "Asia/Tehran";

View File

@@ -1146,6 +1146,10 @@ class Inbound extends XrayCommonClass {
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol); return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
} }
canSniffing() {
return [Protocols.VMESS, Protocols.VLESS, Protocols.TROJAN, Protocols.SHADOWSOCKS].includes(this.protocol);
}
reset() { reset() {
this.port = RandomUtil.randomIntRange(10000, 60000); this.port = RandomUtil.randomIntRange(10000, 60000);
this.listen = ''; this.listen = '';
@@ -1530,28 +1534,6 @@ class Inbound extends XrayCommonClass {
return url.toString(); return url.toString();
} }
getWireguardLink(address, port, remark, peerId) {
let txt = `[Interface]\n`
txt += `PrivateKey = ${this.settings.peers[peerId].privateKey}\n`
txt += `Address = ${this.settings.peers[peerId].allowedIPs[0]}\n`
txt += `DNS = 1.1.1.1, 1.0.0.1\n`
if (this.settings.mtu) {
txt += `MTU = ${this.settings.mtu}\n`
}
txt += `\n# ${remark}\n`
txt += `[Peer]\n`
txt += `PublicKey = ${this.settings.pubKey}\n`
txt += `AllowedIPs = 0.0.0.0/0, ::/0\n`
txt += `Endpoint = ${address}:${port}`
if (this.settings.peers[peerId].psk) {
txt += `\nPresharedKey = ${this.settings.peers[peerId].psk}`
}
if (this.settings.peers[peerId].keepAlive) {
txt += `\nPersistentKeepalive = ${this.settings.peers[peerId].keepAlive}\n`
}
return txt;
}
genLink(address='', port=this.port, forceTls='same', remark='', client) { genLink(address='', port=this.port, forceTls='same', remark='', client) {
switch (this.protocol) { switch (this.protocol) {
case Protocols.VMESS: case Protocols.VMESS:
@@ -1575,7 +1557,7 @@ class Inbound extends XrayCommonClass {
const orderChars = remarkModel.slice(1); const orderChars = remarkModel.slice(1);
let orders = { let orders = {
'i': remark, 'i': remark,
'e': email, 'e': client ? client.email : '',
'o': '', 'o': '',
}; };
if(ObjectUtil.isArrEmpty(this.stream.externalProxy)){ if(ObjectUtil.isArrEmpty(this.stream.externalProxy)){
@@ -1598,7 +1580,6 @@ class Inbound extends XrayCommonClass {
} }
genInboundLinks(remark = '', remarkModel = '-ieo') { genInboundLinks(remark = '', remarkModel = '-ieo') {
let addr = !ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0" ? this.listen : location.hostname;
if(this.clients){ if(this.clients){
let links = []; let links = [];
this.clients.forEach((client) => { this.clients.forEach((client) => {
@@ -1608,14 +1589,7 @@ class Inbound extends XrayCommonClass {
}); });
return links.join('\r\n'); return links.join('\r\n');
} else { } else {
if(this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(addr, this.port, 'same', remark); if(this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(this.listen, this.port, 'same', remark);
if(this.protocol == Protocols.WIREGUARD) {
let links = [];
this.settings.peers.forEach((p,index) => {
links.push(this.getWireguardLink(addr,this.port,remark + remarkModel.charAt(0) + (index+1),index));
});
return links.join('\r\n');
}
return ''; return '';
} }
} }
@@ -2295,7 +2269,7 @@ Inbound.WireguardSettings = class extends XrayCommonClass {
} }
addPeer() { addPeer() {
this.peers.push(new Inbound.WireguardSettings.Peer(null,null,'',['10.0.0.' + (this.peers.length+2)])); this.peers.push(new Inbound.WireguardSettings.Peer());
} }
delPeer(index) { delPeer(index) {
@@ -2323,24 +2297,16 @@ Inbound.WireguardSettings = class extends XrayCommonClass {
}; };
Inbound.WireguardSettings.Peer = class extends XrayCommonClass { Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
constructor(privateKey, publicKey, psk='', allowedIPs=['10.0.0.2/32'], keepAlive=0) { constructor(publicKey=Wireguard.generateKeypair().publicKey, psk='', allowedIPs=['0.0.0.0/0','::/0'], keepAlive=0) {
super(); super();
this.privateKey = privateKey
this.publicKey = publicKey; this.publicKey = publicKey;
if (!this.publicKey){
[this.publicKey, this.privateKey] = Object.values(Wireguard.generateKeypair())
}
this.psk = psk; this.psk = psk;
allowedIPs.forEach((a,index) => {
if (a.length>0 && !a.includes('/')) allowedIPs[index] += '/32';
})
this.allowedIPs = allowedIPs; this.allowedIPs = allowedIPs;
this.keepAlive = keepAlive; this.keepAlive = keepAlive;
} }
static fromJson(json={}){ static fromJson(json={}){
return new Inbound.WireguardSettings.Peer( return new Inbound.WireguardSettings.Peer(
json.privateKey,
json.publicKey, json.publicKey,
json.preSharedKey, json.preSharedKey,
json.allowedIPs, json.allowedIPs,
@@ -2349,11 +2315,7 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
} }
toJson() { toJson() {
this.allowedIPs.forEach((a,index) => {
if (a.length>0 && !a.includes('/')) this.allowedIPs[index] += '/32';
});
return { return {
privateKey: this.privateKey,
publicKey: this.publicKey, publicKey: this.publicKey,
preSharedKey: this.psk.length>0 ? this.psk : undefined, preSharedKey: this.psk.length>0 ? this.psk : undefined,
allowedIPs: this.allowedIPs, allowedIPs: this.allowedIPs,

File diff suppressed because one or more lines are too long

View File

@@ -86,7 +86,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
user := session.GetLoginUser(c) user := session.GetLoginUser(c)
inbound.UserId = user.Id inbound.UserId = user.Id
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) inbound.Tag = fmt.Sprintf("inbound-0.0.0.0:%v", inbound.Port)
} else { } else {
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port) inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
} }
@@ -283,7 +283,7 @@ func (a *InboundController) importInbound(c *gin.Context) {
inbound.Id = 0 inbound.Id = 0
inbound.UserId = user.Id inbound.UserId = user.Id
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) inbound.Tag = fmt.Sprintf("inbound-0.0.0.0:%v", inbound.Port)
} else { } else {
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port) inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
} }

View File

@@ -10,7 +10,6 @@ type XraySettingController struct {
XraySettingService service.XraySettingService XraySettingService service.XraySettingService
SettingService service.SettingService SettingService service.SettingService
InboundService service.InboundService InboundService service.InboundService
OutboundService service.OutboundService
XrayService service.XrayService XrayService service.XrayService
} }
@@ -28,8 +27,6 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
g.GET("/getXrayResult", a.getXrayResult) g.GET("/getXrayResult", a.getXrayResult)
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig) g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
g.POST("/warp/:action", a.warp) g.POST("/warp/:action", a.warp)
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
} }
func (a *XraySettingController) getXraySetting(c *gin.Context) { func (a *XraySettingController) getXraySetting(c *gin.Context) {
@@ -81,27 +78,9 @@ func (a *XraySettingController) warp(c *gin.Context) {
resp, err = a.XraySettingService.RegWarp(skey, pkey) resp, err = a.XraySettingService.RegWarp(skey, pkey)
case "license": case "license":
license := c.PostForm("license") license := c.PostForm("license")
println(license)
resp, err = a.XraySettingService.SetWarpLicence(license) resp, err = a.XraySettingService.SetWarpLicence(license)
} }
jsonObj(c, resp, err) jsonObj(c, resp, err)
} }
func (a *XraySettingController) getOutboundsTraffic(c *gin.Context) {
outboundsTraffic, err := a.OutboundService.GetOutboundsTraffic()
if err != nil {
jsonMsg(c, "Error getting traffics", err)
return
}
jsonObj(c, outboundsTraffic, nil)
}
func (a *XraySettingController) resetOutboundsTraffic(c *gin.Context) {
tag := c.PostForm("tag")
err := a.OutboundService.ResetOutboundTraffic(tag)
if err != nil {
jsonMsg(c, "Error in reset outbound traffics", err)
return
}
jsonObj(c, "", nil)
}

View File

@@ -48,9 +48,6 @@ type AllSetting struct {
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"` SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"` SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
SubURI string `json:"subURI" form:"subURI"` SubURI string `json:"subURI" form:"subURI"`
SubJsonPath string `json:"subJsonPath" form:"subJsonPath"`
SubJsonURI string `json:"subJsonURI" form:"subJsonURI"`
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"`
Datepicker string `json:"datepicker" form:"datepicker"` Datepicker string `json:"datepicker" form:"datepicker"`
} }
@@ -108,13 +105,6 @@ func (s *AllSetting) CheckValid() error {
s.SubPath += "/" s.SubPath += "/"
} }
if !strings.HasPrefix(s.SubJsonPath, "/") {
s.SubJsonPath = "/" + s.SubJsonPath
}
if !strings.HasSuffix(s.SubJsonPath, "/") {
s.SubJsonPath += "/"
}
_, err := time.LoadLocation(s.TimeLocation) _, err := time.LoadLocation(s.TimeLocation)
if err != nil { if err != nil {
return common.NewError("time location not exist:", s.TimeLocation) return common.NewError("time location not exist:", s.TimeLocation)

View File

@@ -7,6 +7,8 @@
<link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.8/antd.min.css"> <link rel="stylesheet" href="{{ .base_path }}assets/ant-design-vue@1.7.8/antd.min.css">
<link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css"> <link rel="stylesheet" href="{{ .base_path }}assets/element-ui@2.15.0/theme-chalk/display.css">
<link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}"> <link rel="stylesheet" href="{{ .base_path }}assets/css/custom.css?{{ .cur_ver }}">
<link rel=”icon” type=”image/x-icon” href="{{ .base_path }}assets/favicon.ico">
<link rel="shortcut icon" type="image/x-icon" href="{{ .base_path }}assets/favicon.ico">
<style> <style>
[v-cloak] { [v-cloak] {
display: none; display: none;
@@ -28,5 +30,4 @@
</style> </style>
<title>{{ .host }}-{{ i18n .title}}</title> <title>{{ .host }}-{{ i18n .title}}</title>
</head> </head>
<div id="message"></div>
{{end}} {{end}}

View File

@@ -8,23 +8,13 @@
{{ i18n "pages.inbounds.clickOnQRcode" }} {{ i18n "pages.inbounds.clickOnQRcode" }}
</a-tag> </a-tag>
<template v-if="app.subSettings.enable && qrModal.subId"> <template v-if="app.subSettings.enable && qrModal.subId">
<a-divider>{{ i18n "pages.settings.subSettings"}}</a-divider> <a-divider>Subscription</a-divider>
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" <div class="qr-bg"><canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas></div>
id="qrCode-sub"
class="qr-bg">
</canvas>
<a-divider>{{ i18n "pages.settings.subSettings"}} Json</a-divider>
<canvas @click="copyToClipboard('qrCode-subJson',genSubJsonLink(qrModal.client.subId))"
id="qrCode-subJson"
style="width: 100%; height: 100%; display: flex; border-radius: 1rem;">
</canvas>
</template> </template>
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider> <a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
<template v-for="(row, index) in qrModal.qrcodes"> <template v-for="(row, index) in qrModal.qrcodes">
<a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag> <a-tag color="green" style="margin: 10px 0; display: block; text-align: center;">[[ row.remark ]]</a-tag>
<canvas @click="copyToClipboard('qrCode-'+index, row.link)" <div class="qr-bg"><canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas></div>
:id="'qrCode-'+index"
class="qr-bg"></canvas>
</template> </template>
</a-modal> </a-modal>
@@ -45,21 +35,12 @@
this.client = client; this.client = client;
this.subId = ''; this.subId = '';
this.qrcodes = []; this.qrcodes = [];
if (this.inbound.protocol == Protocols.WIREGUARD){ this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l,index) =>{ this.qrcodes.push({
this.qrcodes.push({ remark: l.remark,
remark: "Peer " + (index+1), link: l.link
link: l
});
}); });
} else { });
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
this.qrcodes.push({
remark: l.remark,
link: l.link
});
});
}
this.visible = true; this.visible = true;
}, },
close: function () { close: function () {
@@ -92,16 +73,12 @@
}, },
genSubLink(subID) { genSubLink(subID) {
return app.subSettings.subURI+subID; return app.subSettings.subURI+subID;
},
genSubJsonLink(subID) {
return app.subSettings.subJsonURI+subID;
} }
}, },
updated() { updated() {
if (qrModal.client && 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));
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
} }
qrModal.qrcodes.forEach((element, index) => { qrModal.qrcodes.forEach((element, index) => {
this.setQrCode("qrCode-" + index, element.link); this.setQrCode("qrCode-" + index, element.link);

View File

@@ -1,16 +1,15 @@
{{define "textModal"}} {{define "textModal"}}
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title" <a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
:closable="true" :closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.currentTheme"> :class="themeSwitcher.currentTheme"
<template slot="footer"> :ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" icon="download" <a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)" :href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
:download="txtModal.fileName">[[ txtModal.fileName ]] :download="txtModal.fileName">
</a-button> {{ i18n "download" }} [[ txtModal.fileName ]]
<a-button type="primary" id="copy-btn">{{ i18n "copy" }}</a-button> </a-button>
</template> <a-input type="textarea" v-model="txtModal.content"
<a-input style="overflow-y: auto;" type="textarea" v-model="txtModal.content" :autosize="{ minRows: 10, maxRows: 20}"></a-input>
:autosize="{ minRows: 10, maxRows: 20}"></a-input>
</a-modal> </a-modal>
<script> <script>
@@ -29,7 +28,7 @@
this.visible = true; this.visible = true;
textModalApp.$nextTick(() => { textModalApp.$nextTick(() => {
if (this.clipboard === null) { if (this.clipboard === null) {
this.clipboard = new ClipboardJS('#copy-btn', { this.clipboard = new ClipboardJS('#txt-modal-ok-btn', {
text: () => this.content, text: () => this.content,
}); });
this.clipboard.on('success', () => { this.clipboard.on('success', () => {

View File

@@ -2,14 +2,9 @@
<html lang="en"> <html lang="en">
{{template "head" .}} {{template "head" .}}
<style> <style>
html * {
-webkit-font-smoothing: antialiased;
-moz-osx-font-smoothing: grayscale;
}
h1 { h1 {
text-align: center; text-align: center;
/* margin: 20px 0 50px 0;*/ margin: 20px 0 50px 0;
height: 110px;
} }
.ant-btn, .ant-btn,
.ant-input { .ant-input {
@@ -36,9 +31,7 @@
} }
.title { .title {
font-size: 32px; font-size: 32px;
} font-weight: bold;
.title b {
font-weight: bold !important;
} }
#app { #app {
overflow: hidden; overflow: hidden;
@@ -71,10 +64,10 @@
background-color: #0f2d32; background-color: #0f2d32;
} }
.dark #login { .dark #login {
background-color: #101113; background-color: #151f31;
} }
.dark h1 { .dark h1 {
color: rgba(255, 255, 255); color: rgba(255, 255, 255, 0.85);
} }
.ant-form-item { .ant-form-item {
margin-bottom: 16px; margin-bottom: 16px;
@@ -199,7 +192,7 @@
z-index: -1; z-index: -1;
} }
.dark .waves-header { .dark .waves-header {
background-color: #0a2227; background-color: #101828;
} }
.waves-inner-header { .waves-inner-header {
height: 50vh; height: 50vh;
@@ -211,7 +204,7 @@
position: relative; position: relative;
width: 100%; width: 100%;
height: 15vh; height: 15vh;
margin-bottom: -8px; /*Fix for safari gap*/ margin-bottom: -5px; /*Fix for safari gap*/
min-height: 100px; min-height: 100px;
max-height: 150px; max-height: 150px;
} }
@@ -219,27 +212,23 @@
animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite; animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
} }
.dark .parallax > use { .dark .parallax > use {
fill: #0f2d32; fill: rgb(10 117 87 / 20%);
} }
.parallax > use:nth-child(1) { .parallax > use:nth-child(1) {
animation-delay: -2s; animation-delay: -2s;
animation-duration: 4s; animation-duration: 7s;
opacity: 0.2; opacity: 0.2;
} }
.parallax > use:nth-child(2) { .parallax > use:nth-child(2) {
animation-delay: -3s; animation-delay: -3s;
animation-duration: 7s; animation-duration: 10s;
opacity: 0.4; opacity: 0.4;
} }
.parallax > use:nth-child(3) { .parallax > use:nth-child(3) {
animation-delay: -4s; animation-delay: -4s;
animation-duration: 10s; animation-duration: 13s;
opacity: 0.6; opacity: 0.6;
} }
.parallax > use:nth-child(4) {
animation-delay: -5s;
animation-duration: 13s;
}
@keyframes move-forever { @keyframes move-forever {
0% { 0% {
transform: translate3d(-90px, 0, 0); transform: translate3d(-90px, 0, 0);
@@ -254,216 +243,89 @@
min-height: 40px; min-height: 40px;
} }
} }
.words-wrapper {
width: 100%;
display: inline-block;
position: relative;
text-align: center;
}
.words-wrapper b {
width: 100%;
display: inline-block;
position: absolute;
left: 0;
top: 0;
}
.words-wrapper b.is-visible {
position: relative;
}
.headline.zoom .words-wrapper {
-webkit-perspective: 300px;
-moz-perspective: 300px;
perspective: 300px;
}
.headline {
display: flex;
justify-content: center;
align-items: center;
}
.headline.zoom b {
opacity: 0;
}
.headline.zoom b.is-visible {
opacity: 1;
-webkit-animation: zoom-in 0.8s;
-moz-animation: zoom-in 0.8s;
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-in 0.8s;
}
.headline.zoom b.is-hidden {
-webkit-animation: zoom-out 0.8s;
-moz-animation: zoom-out 0.8s;
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-out 0.4s;
}
@-webkit-keyframes zoom-in {
0% {
opacity: 0;
-webkit-transform: translateZ(100px);
}
100% {
opacity: 1;
-webkit-transform: translateZ(0);
}
}
@-moz-keyframes zoom-in {
0% {
opacity: 0;
-moz-transform: translateZ(100px);
}
100% {
opacity: 1;
-moz-transform: translateZ(0);
}
}
@keyframes zoom-in {
0% {
opacity: 0;
-webkit-transform: translateZ(100px);
-moz-transform: translateZ(100px);
-ms-transform: translateZ(100px);
-o-transform: translateZ(100px);
transform: translateZ(100px);
}
100% {
opacity: 1;
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);
}
}
@-webkit-keyframes zoom-out {
0% {
opacity: 1;
-webkit-transform: translateZ(0);
}
100% {
opacity: 0;
-webkit-transform: translateZ(-100px);
}
}
@-moz-keyframes zoom-out {
0% {
opacity: 1;
-moz-transform: translateZ(0);
}
100% {
opacity: 0;
-moz-transform: translateZ(-100px);
}
}
@keyframes zoom-out {
0% {
opacity: 1;
-webkit-transform: translateZ(0);
-moz-transform: translateZ(0);
-ms-transform: translateZ(0);
-o-transform: translateZ(0);
transform: translateZ(0);
}
100% {
opacity: 0;
-webkit-transform: translateZ(-100px);
-moz-transform: translateZ(-100px);
-ms-transform: translateZ(-100px);
-o-transform: translateZ(-100px);
transform: translateZ(-100px);
}
}
</style> </style>
<body> <body>
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme"> <a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
<transition name="list" appear> <transition name="list" appear>
<a-layout-content class="under" style="min-height: 0;"> <a-layout-content class="under" style="min-height: 0;">
<div class="waves-header"> <div class="waves-header">
<div class="waves-inner-header"></div> <div class="waves-inner-header"></div>
<svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" <svg class="waves" xmlns="http://www.w3.org/2000/svg" xmlns:xlink="http://www.w3.org/1999/xlink" viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto">
viewBox="0 24 150 28" preserveAspectRatio="none" shape-rendering="auto"> <defs>
<defs> <path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" />
<path id="gentle-wave" d="M-160 44c30 0 58-18 88-18s 58 18 88 18 58-18 88-18 58 18 88 18 v44h-352z" /> </defs>
</defs> <g class="parallax">
<g class="parallax"> <use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="0" fill="rgba(0, 135, 113, 0.08)" /> <use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="3" fill="rgba(0, 135, 113, 0.08)" /> <use xlink:href="#gentle-wave" x="48" y="7" fill="rgba(0, 135, 113, 0.08)" />
<use xlink:href="#gentle-wave" x="48" y="5" fill="rgba(0, 135, 113, 0.08)" /> </g>
<use xlink:href="#gentle-wave" x="48" y="7" fill="#c7ebe2" /> </svg>
</g> </div>
</svg> <a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto;">
</div> <a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="8" :xxl="6" id="login" style="margin: 3rem 0;">
<a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto;"> <a-row type="flex" justify="center">
<a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="8" :xxl="6" id="login" style="margin: 3rem 0;"> <a-col>
<a-row type="flex" justify="center"> <h1 class="title">{{ i18n "pages.login.title" }}</h1>
<a-col style="width: 100%;"> </a-col>
<h1 class="title headline zoom"> </a-row>
<span class="words-wrapper"> <a-row type="flex" justify="center">
<b class="is-visible">{{ i18n "pages.login.title" }}</b> <a-col span="24">
<b>3X-UI</b> <a-form>
</span> <a-form-item>
</h1> <a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
@keydown.enter.native="login" autofocus>
<a-icon slot="prefix" type="user" style="font-size: 16px;"/>
</a-input>
</a-form-item>
<a-form-item>
<password-input icon="lock" v-model.trim="user.password"
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
</password-input>
</a-form-item>
<a-form-item v-if="secretEnable">
<password-input icon="key" v-model.trim="user.loginSecret"
placeholder='{{ i18n "secretToken" }}' @keydown.enter.native="login">
</password-input>
</a-input>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<div class="wave-btn-bg wave-btn-bg-cl" :style="loading ? { width: '54px' } : { display: 'inline-block' }">
<a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined">
[[ loading ? '' : '{{ i18n "login" }}' ]]
</a-button>
</div>
</a-row>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<a-col :span="24">
<a-select ref="selectLang" v-model="lang" @change="setLang(lang)" style="width: 150px;" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<a-col>
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>&nbsp;
</a-col>
<a-col>
<theme-switch />
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-col>
</a-row>
</a-col> </a-col>
</a-row> </a-row>
<a-row type="flex" justify="center"> </a-layout-content>
<a-col span="24"> </transition>
<a-form>
<a-form-item>
<a-input v-model.trim="user.username" placeholder='{{ i18n "username" }}'
@keydown.enter.native="login" autofocus>
<a-icon slot="prefix" type="user" style="font-size: 16px;" />
</a-input>
</a-form-item>
<a-form-item>
<password-input icon="lock" v-model.trim="user.password" placeholder='{{ i18n "password" }}'
@keydown.enter.native="login">
</password-input>
</a-form-item>
<a-form-item v-if="secretEnable">
<password-input icon="key" v-model.trim="user.loginSecret" placeholder='{{ i18n "secretToken" }}'
@keydown.enter.native="login">
</password-input>
</a-input>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<div class="wave-btn-bg wave-btn-bg-cl"
:style="loading ? { width: '52px' } : { display: 'inline-block' }">
<a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login"
:icon="loading ? 'poweroff' : undefined">
[[ loading ? '' : '{{ i18n "login" }}' ]]
</a-button>
</div>
</a-row>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<a-col :span="24">
<a-select ref="selectLang" v-model="lang" @change="setLang(lang)" style="width: 150px;"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<a-col>
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>&nbsp;
</a-col>
<a-col>
<theme-switch />
</a-col>
</a-row>
</a-form-item>
</a-form>
</a-col>
</a-row>
</a-col>
</a-row>
</a-layout-content>
</transition>
</a-layout> </a-layout>
{{template "js" .}} {{template "js" .}}
{{template "component/themeSwitcher" .}} {{template "component/themeSwitcher" .}}
@@ -510,42 +372,6 @@
}, },
}, },
}); });
document.addEventListener("DOMContentLoaded", function() {
var animationDelay = 2000;
initHeadline();
function initHeadline() {
animateHeadline(document.querySelectorAll('.headline'));
}
function animateHeadline(headlines) {
var duration = animationDelay;
headlines.forEach(function(headline) {
setTimeout(function() {
hideWord(headline.querySelector('.is-visible'));
}, duration);
});
}
function hideWord(word) {
var nextWord = takeNext(word);
switchWord(word, nextWord);
setTimeout(function() {
hideWord(nextWord);
}, animationDelay);
}
function takeNext(word) {
return (word.nextElementSibling) ? word.nextElementSibling : word.parentElement.firstElementChild;
}
function switchWord(oldWord, newWord) {
oldWord.classList.remove('is-visible');
oldWord.classList.add('is-hidden');
newWord.classList.remove('is-hidden');
newWord.classList.add('is-visible');
}
});
</script> </script>
</body> </body>
</html> </html>

View File

@@ -220,7 +220,7 @@
clientsBulkModal.visible = false; clientsBulkModal.visible = false;
clientsBulkModal.loading(false); clientsBulkModal.loading(false);
}, },
loading(loading=true) { loading(loading) {
clientsBulkModal.confirmLoading = loading; clientsBulkModal.confirmLoading = loading;
}, },
}; };

View File

@@ -72,7 +72,7 @@
clientModal.visible = false; clientModal.visible = false;
clientModal.loading(false); clientModal.loading(false);
}, },
loading(loading=true) { loading(loading) {
clientModal.confirmLoading = loading; clientModal.confirmLoading = loading;
}, },
}; };

View File

@@ -1,19 +1,19 @@
{{define "menuItems"}} {{define "menuItems"}}
<a-menu-item key="{{ .base_path }}panel/"> <a-menu-item key="{{ .base_path }}panel/">
<a-icon type="dashboard"></a-icon> <a-icon type="dashboard"></a-icon>
<span><b>{{ i18n "menu.dashboard"}}</b></span> <span>{{ i18n "menu.dashboard"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="{{ .base_path }}panel/inbounds"> <a-menu-item key="{{ .base_path }}panel/inbounds">
<a-icon type="user"></a-icon> <a-icon type="user"></a-icon>
<span><b>{{ i18n "menu.inbounds"}}</b></span> <span>{{ i18n "menu.inbounds"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="{{ .base_path }}panel/settings"> <a-menu-item key="{{ .base_path }}panel/settings">
<a-icon type="setting"></a-icon> <a-icon type="setting"></a-icon>
<span><b>{{ i18n "menu.settings"}}</b></span> <span>{{ i18n "menu.settings"}}</span>
</a-menu-item> </a-menu-item>
<a-menu-item key="{{ .base_path }}panel/xray"> <a-menu-item key="{{ .base_path }}panel/xray">
<a-icon type="tool"></a-icon> <a-icon type="tool"></a-icon>
<span><b>{{ i18n "menu.xray"}}</b></span> <span>{{ i18n "menu.xray"}}</span>
</a-menu-item> </a-menu-item>
<!--<a-menu-item key="{{ .base_path }}panel/clients">--> <!--<a-menu-item key="{{ .base_path }}panel/clients">-->
<!-- <a-icon type="laptop"></a-icon>--> <!-- <a-icon type="laptop"></a-icon>-->
@@ -21,7 +21,7 @@
<!--</a-menu-item>--> <!--</a-menu-item>-->
<a-menu-item key="{{ .base_path }}logout"> <a-menu-item key="{{ .base_path }}logout">
<a-icon type="logout"></a-icon> <a-icon type="logout"></a-icon>
<span><b>{{ i18n "menu.logout"}}</b></span> <span>{{ i18n "menu.logout"}}</span>
</a-menu-item> </a-menu-item>
{{end}} {{end}}

View File

@@ -5,7 +5,7 @@
@input="$emit('input', convertToGregorian($event.target.value)); jalaliDatepicker.hide();" @input="$emit('input', convertToGregorian($event.target.value)); jalaliDatepicker.hide();"
:placeholder="placeholder"> :placeholder="placeholder">
<template #addonAfter> <template #addonAfter>
<a-icon type="calendar" style="font-size: 14px; opacity: 0.5;"/> <a-icon type="calendar" style="font-size: 16px;"/>
</template> </template>
</a-input> </a-input>
</div> </div>

View File

@@ -21,7 +21,6 @@
this.isDarkTheme = !this.isDarkTheme; this.isDarkTheme = !this.isDarkTheme;
localStorage.setItem('dark-mode', this.isDarkTheme); localStorage.setItem('dark-mode', this.isDarkTheme);
document.querySelector('body').setAttribute('class', this.isDarkTheme ? 'dark' : 'light') document.querySelector('body').setAttribute('class', this.isDarkTheme ? 'dark' : 'light')
document.getElementById('message').className = themeSwitcher.currentTheme;
}, },
}; };
} }
@@ -32,10 +31,6 @@
props: [], props: [],
template: `{{template "component/themeSwitchTemplate"}}`, template: `{{template "component/themeSwitchTemplate"}}`,
data: () => ({ themeSwitcher }), data: () => ({ themeSwitcher }),
mounted() {
this.$message.config({getContainer: () => document.getElementById('message')});
document.getElementById('message').className = themeSwitcher.currentTheme;
}
}); });
</script> </script>
{{end}} {{end}}

View File

@@ -1,86 +0,0 @@
{{define "dnsModal"}}
<a-modal id="dns-modal" v-model="dnsModal.visible" :title="dnsModal.title" @ok="dnsModal.ok"
:closable="true" :mask-closable="false"
:ok-text="dnsModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.outbound.address" }}'>
<a-input v-model.trim="dnsModal.dnsServer.address"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.domains" }}'>
<a-button size="small" type="primary" @click="dnsModal.dnsServer.domains.push('')">+</a-button>
<template v-for="(domain, index) in dnsModal.dnsServer.domains">
<a-input v-model.trim="dnsModal.dnsServer.domains[index]">
<a-button size="small" slot="addonAfter" @click="dnsModal.dnsServer.domains.splice(index,1)">-</a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.dns.strategy" }}' v-if="isAdvanced">
<a-select
v-model="dnsModal.dnsServer.queryStrategy"
style="width: 100%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseIP', 'UseIPv4', 'UseIPv6']">
[[ l ]]
</a-select-option>
</a-select>
</a-form-item>
</a-form>
</a-modal>
<script>
const dnsModal = {
title: '',
visible: false,
okText: '{{ i18n "confirm" }}',
isEdit: false,
confirm: null,
dnsServer: {
address: "localhost",
domains: [],
queryStrategy: 'UseIP',
},
ok() {
domains = dnsModal.dnsServer.domains.filter(d => d.length>0);
dnsModal.dnsServer.domains = domains;
newDnsServer = domains.length > 0 ? dnsModal.dnsServer : dnsModal.dnsServer.address;
ObjectUtil.execute(dnsModal.confirm, newDnsServer);
},
show({ title='', okText='{{ i18n "confirm" }}', dnsServer, confirm=(dnsServer)=>{}, isEdit=false }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if(isEdit) {
if (typeof dnsServer == 'object'){
this.dnsServer = dnsServer;
} else {
this.dnsServer.address = dnsServer?? '';
}
} else {
this.dnsServer = {
address: "localhost",
domains: [],
queryStrategy: 'UseIP',
}
}
this.isEdit = isEdit;
},
close() {
dnsModal.visible = false;
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#dns-modal',
data: {
dnsModal: dnsModal,
},
computed: {
isAdvanced: {
get: function () { return dnsModal.dnsServer.domains.length>0 }
}
}
});
</script>
{{end}}

View File

@@ -1,57 +0,0 @@
{{define "fakednsModal"}}
<a-modal id="fakedns-modal" v-model="fakednsModal.visible" :title="fakednsModal.title" @ok="fakednsModal.ok"
:closable="true" :mask-closable="false"
:ok-text="fakednsModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.fakedns.ipPool" }}'>
<a-input v-model.trim="fakednsModal.fakeDns.ipPool"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.fakedns.poolSize" }}'>
<a-input-number style="width: 100%;" type="number" min="1" v-model.trim="fakednsModal.fakeDns.poolSize"></a-input-number>
</a-form-item>
</a-form>
</a-modal>
<script>
const fakednsModal = {
title: '',
visible: false,
okText: '{{ i18n "confirm" }}',
isEdit: false,
confirm: null,
fakeDns: {
ipPool: "198.18.0.0/16",
poolSize: 65535,
},
ok() {
ObjectUtil.execute(fakednsModal.confirm, fakednsModal.fakeDns);
},
show({ title='', okText='{{ i18n "confirm" }}', fakeDns, confirm=(fakeDns)=>{}, isEdit=false }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if(isEdit) {
this.fakeDns = fakeDns;
} else {
this.fakeDns = {
ipPool: "198.18.0.0/16",
poolSize: 65535,
}
}
this.isEdit = isEdit;
},
close() {
fakednsModal.visible = false;
},
};
new Vue({
delimiters: ['[[', ']]'],
el: '#fakedns-modal',
data: {
fakednsModal: fakednsModal,
}
});
</script>
{{end}}

View File

@@ -54,7 +54,7 @@
<a-icon type="question-circle"></a-icon> <a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-date-picker style="width: 100%;" v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss" <a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="dbInbound._expiryTime"></a-date-picker> v-model="dbInbound._expiryTime"></a-date-picker>
<persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}' <persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
@@ -114,7 +114,7 @@
</template> </template>
<!-- sniffing --> <!-- sniffing -->
<template> <template v-if="inbound.canSniffing()">
{{template "form/sniffing"}} {{template "form/sniffing"}}
</template> </template>
{{end}} {{end}}

View File

@@ -134,10 +134,28 @@
<a-form-item label='{{ i18n "pages.xray.wireguard.endpoint" }}'> <a-form-item label='{{ i18n "pages.xray.wireguard.endpoint" }}'>
<a-input v-model.trim="peer.endpoint"></a-input> <a-input v-model.trim="peer.endpoint"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'> <a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "pages.xray.wireguard.publicKey" }}
<a-icon @click="peer.publicKey = publicKey=Wireguard.generateKeypair().publicKey"type="sync"> </a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="peer.publicKey"></a-input> <a-input v-model.trim="peer.publicKey"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.xray.wireguard.psk" }}'> <a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "pages.xray.wireguard.psk" }}
<a-icon @click="peer.psk = publicKey=Wireguard.generateKeypair().publicKey"type="sync"> </a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="peer.psk"></a-input> <a-input v-model.trim="peer.psk"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
@@ -171,6 +189,7 @@
<a-form-item label='ID'> <a-form-item label='ID'>
<a-input v-model.trim="outbound.settings.id"></a-input> <a-input v-model.trim="outbound.settings.id"></a-input>
</a-form-item> </a-form-item>
<!-- vless settings --> <!-- vless settings -->
<template v-if="outbound.canEnableTlsFlow()"> <template v-if="outbound.canEnableTlsFlow()">
<a-form-item label='Flow'> <a-form-item label='Flow'>
@@ -193,23 +212,18 @@
<a-input v-model.trim="outbound.settings.pass"></a-input> <a-input v-model.trim="outbound.settings.pass"></a-input>
</a-form-item> </a-form-item>
</template> </template>
<!-- trojan/shadowsocks -->
<template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)"> <!-- shadowsocks -->
<a-form-item label='{{ i18n "password" }}'> <template v-if="outbound.protocol === Protocols.Shadowsocks">
<a-input v-model.trim="outbound.settings.password"></a-input>
</a-form-item>
</template>
<!-- shadowsocks -->
<template v-if="outbound.protocol === Protocols.Shadowsocks">
<a-form-item label='{{ i18n "encryption" }}'> <a-form-item label='{{ i18n "encryption" }}'>
<a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme"> <a-select v-model="outbound.settings.method" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="(method, method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option> <a-select-option v-for="(method,method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label='UDP over TCP'> <a-form-item label='UDP over TCP'>
<a-switch v-model="outbound.settings.uot"></a-switch> <a-switch v-model="outbound.settings.uot"></a-switch>
</a-form-item> </a-form-item>
</template> </template>
</template> </template>
<!-- stream settings --> <!-- stream settings -->
@@ -349,15 +363,13 @@
<a-input v-model.trim="outbound.stream.tls.serverName"></a-input> <a-input v-model.trim="outbound.stream.tls.serverName"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="uTLS"> <a-form-item label="uTLS">
<a-select v-model="outbound.stream.tls.fingerprint" <a-select v-model="outbound.stream.tls.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme">
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value=''>None</a-select-option> <a-select-option value=''>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item label="ALPN"> <a-form-item label="ALPN">
<a-select mode="multiple" <a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="outbound.stream.tls.alpn"> v-model="outbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option> <a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
</a-select> </a-select>
@@ -369,12 +381,11 @@
<!-- reality settings --> <!-- reality settings -->
<template v-if="outbound.stream.isReality"> <template v-if="outbound.stream.isReality">
<a-form-item label="SNI"> <a-form-item label='{{ i18n "domainName" }}'>
<a-input v-model.trim="outbound.stream.reality.serverName"></a-input> <a-input v-model.trim="outbound.stream.reality.serverName"></a-input>
</a-form-item> </a-form-item>
<a-form-item label="uTLS"> <a-form-item label="uTLS">
<a-select v-model="outbound.stream.reality.fingerprint" <a-select v-model="outbound.stream.reality.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme">
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option> <a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>

View File

@@ -38,16 +38,10 @@
<template slot="title"> <template slot="title">
<span>{{ i18n "reset" }}</span> <span>{{ i18n "reset" }}</span>
</template> </template>
{{ i18n "pages.xray.wireguard.secretKey" }} {{ i18n "pages.xray.wireguard.publicKey" }}
<a-icon @click="[peer.publicKey, peer.privateKey] = Object.values(Wireguard.generateKeypair())"type="sync"> </a-icon> <a-icon @click="peer.publicKey = publicKey=Wireguard.generateKeypair().publicKey"type="sync"> </a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="peer.privateKey"></a-input>
</a-form-item>
<a-form-item>
<template slot="label">
{{ i18n "pages.xray.wireguard.publicKey" }}
</template>
<a-input v-model.trim="peer.publicKey"></a-input> <a-input v-model.trim="peer.publicKey"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
@@ -57,7 +51,7 @@
<span>{{ i18n "reset" }}</span> <span>{{ i18n "reset" }}</span>
</template> </template>
{{ i18n "pages.xray.wireguard.psk" }} {{ i18n "pages.xray.wireguard.psk" }}
<a-icon @click="peer.psk = Wireguard.keyToBase64(Wireguard.generatePresharedKey())"type="sync"> </a-icon> <a-icon @click="peer.psk = publicKey=Wireguard.generateKeypair().publicKey"type="sync"> </a-icon>
</a-tooltip> </a-tooltip>
</template> </template>
<a-input v-model.trim="peer.psk"></a-input> <a-input v-model.trim="peer.psk"></a-input>

View File

@@ -1,6 +1,6 @@
{{define "form/sniffing"}} {{define "form/sniffing"}}
<a-divider style="margin:5px 0 0;"></a-divider> <a-divider style="margin:5px 0 0;"></a-divider>
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }"> <a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item> <a-form-item>
<span slot="label"> <span slot="label">
Sniffing Sniffing

View File

@@ -20,7 +20,7 @@
<a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65531"></a-input-number> <a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65531"></a-input-number>
</a-tooltip> </a-tooltip>
<a-input style="width: 20%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'></a-input> <a-input style="width: 20%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'></a-input>
<a-button style="width: 10%; margin: 0px; border-radius: 0 1rem 1rem 0;" @click="inbound.stream.externalProxy.splice(index, 1)">-</a-button> <a-button style="width: 10%; margin: 0px" @click="inbound.stream.externalProxy.splice(index, 1)">-</a-button>
</a-input-group> </a-input-group>
</a-form> </a-form>
{{end}} {{end}}

View File

@@ -33,7 +33,7 @@
</template> </template>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')">+</a-button> <a-button size="small" @click="inbound.stream.tcp.request.addHeader('host', '')">+</a-button>
</a-form-item> </a-form-item>
<a-form-item :wrapper-col="{span:24}"> <a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers"> <a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">

View File

@@ -7,7 +7,7 @@
<a-input v-model.trim="inbound.stream.ws.path"></a-input> <a-input v-model.trim="inbound.stream.ws.path"></a-input>
</a-form-item> </a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'> <a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button size="small" @click="inbound.stream.ws.addHeader('Host', '')">+</a-button> <a-button size="small" @click="inbound.stream.ws.addHeader()">+</a-button>
</a-form-item> </a-form-item>
<a-form-item :wrapper-col="{span:24}"> <a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers"> <a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">

View File

@@ -40,7 +40,7 @@
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch> <a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
</template> </template>
<template slot="online" slot-scope="text, client, index"> <template slot="online" slot-scope="text, client, index">
<template v-if="client.enable && isClientOnline(client.email)"> <template v-if="isClientOnline(client.email)">
<a-tag color="green">{{ i18n "online" }}</a-tag> <a-tag color="green">{{ i18n "online" }}</a-tag>
</template> </template>
<template v-else> <template v-else>
@@ -52,7 +52,7 @@
<template slot="title"> <template slot="title">
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template> <template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template> <template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template> <template v-else-if="isClientOnline(client.email)">{{ i18n "online" }}</template>
</template> </template>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"> <a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'">
</a-badge> </a-badge>

View File

@@ -166,7 +166,7 @@
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId"> <template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
<a-divider>Subscription URL</a-divider> <a-divider>Subscription URL</a-divider>
<a-row> <a-row>
<a-col :sx="24" :md="22">SUB: <a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col> <a-col :sx="24" :md="22"><a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
<a-col :sx="24" :md="2" style="text-align: right;"> <a-col :sx="24" :md="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'> <a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)"> <button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)">
@@ -175,24 +175,14 @@
</a-tooltip> </a-tooltip>
</a-col> </a-col>
</a-row> </a-row>
<a-row>
<a-col :sx="24" :md="22">JSON: <a :href="[[ infoModal.subJsonLink ]]" target="_blank">[[ infoModal.subJsonLink ]]</a></a-col>
<a-col :sx="24" :md="2" style="text-align: right; margin-top: 5px;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-subJson-link" @click="copyToClipboard('copy-subJson-link', infoModal.subJsonLink)">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</template> </template>
<template v-if="app.tgBotEnable && infoModal.clientSettings.tgId"> <template v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
<a-divider>Telegram ID</a-divider> <a-divider>Telegram ID</a-divider>
<a-row> <a-row>
<a-col :sx="24" :md="22">[[ infoModal.clientSettings.tgId ]]</a-col> <a-col :sx="24" :md="22"><a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></a-col>
<a-col :sx="24" :md="2" style="text-align: right;"> <a-col :sx="24" :md="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'> <a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary" id="copy-tg-link" @click="copyToClipboard('copy-tg-link', infoModal.clientSettings.tgId)"> <button class="ant-btn ant-btn-primary" id="copy-tg-link" @click="copyToClipboard('copy-tg-link', '@' + infoModal.clientSettings.tgId)">
<a-icon type="snippets"></a-icon> <a-icon type="snippets"></a-icon>
</button> </button>
</a-tooltip> </a-tooltip>
@@ -293,50 +283,24 @@
</tr> </tr>
<template v-for="(peer, index) in inbound.settings.peers"> <template v-for="(peer, index) in inbound.settings.peers">
<tr> <tr>
<td colspan="2"><a-divider>Peer [[ index + 1 ]]</a-divider></td> <td colspan="2"><a-tag>Peer [[ index + 1 ]]</a-tag></td>
</tr> </tr>
<tr class="client-table-odd-row"> <tr class="client-table-odd-row">
<td>{{ i18n "pages.xray.wireguard.secretKey" }}</td>
<td>[[ peer.privateKey ]]</td>
</tr>
<tr>
<td>{{ i18n "pages.xray.wireguard.publicKey" }}</td> <td>{{ i18n "pages.xray.wireguard.publicKey" }}</td>
<td>[[ peer.publicKey ]]</td> <td>[[ peer.publicKey ]]</td>
</tr> </tr>
<tr class="client-table-odd-row"> <tr>
<td>{{ i18n "pages.xray.wireguard.psk" }}</td> <td>{{ i18n "pages.xray.wireguard.psk" }}</td>
<td>[[ peer.psk ]]</td> <td>[[ peer.psk ]]</td>
</tr> </tr>
<tr> <tr class="client-table-odd-row">
<td>{{ i18n "pages.xray.wireguard.allowedIPs" }}</td> <td>{{ i18n "pages.xray.wireguard.allowedIPs" }}</td>
<td>[[ peer.allowedIPs.join(",") ]]</td> <td>[[ peer.allowedIPs.join(",") ]]</td>
</tr> </tr>
<tr class="client-table-odd-row"> <tr>
<td>Keep Alive</td> <td>Keep Alive</td>
<td>[[ peer.keepAlive ]]</td> <td>[[ peer.keepAlive ]]</td>
</tr> </tr>
<tr>
<td colspan="2">
<a-row>
<a-col :span="22" style="overflow-wrap: anywhere;">
<a-tag color="blue">Config</a-tag>
<div
v-html="infoModal.links[index].replaceAll(`\n`,`<br />`)"
style="border-radius: 1rem; padding: 0.5rem;"
class="client-table-odd-row"></div>
</a-col>
<a-col :span="2" style="text-align: right;">
<a-tooltip title='{{ i18n "copy" }}'>
<button class="ant-btn ant-btn-primary"
:id="'copy-url-link-'+index"
@click="copyToClipboard('copy-url-link-'+index, infoModal.links[index])">
<a-icon type="snippets"></a-icon>
</button>
</a-tooltip>
</a-col>
</a-row>
</td>
</tr>
</table> </table>
</template> </template>
</template> </template>
@@ -355,7 +319,7 @@
index: null, index: null,
isExpired: false, isExpired: false,
subLink: '', subLink: '',
subJsonLink: '', tgLink: '',
show(dbInbound, index) { show(dbInbound, index) {
this.index = index; this.index = index;
this.inbound = dbInbound.toInbound(); this.inbound = dbInbound.toInbound();
@@ -363,15 +327,13 @@
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null; this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index): this.dbInbound.isExpiry; this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index): this.dbInbound.isExpiry;
this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : []; this.clientStats = this.inbound.clients ? this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) : [];
if (this.inbound.protocol == Protocols.WIREGUARD){ this.links = this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, this.clientSettings);
this.links = this.inbound.genInboundLinks(dbInbound.remark).split('\r\n')
} else {
this.links = this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, this.clientSettings);
}
if (this.clientSettings) { if (this.clientSettings) {
if (this.clientSettings.subId) { if (this.clientSettings.subId) {
this.subLink = this.genSubLink(this.clientSettings.subId); this.subLink = this.genSubLink(this.clientSettings.subId);
this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId); }
if (this.clientSettings.tgId) {
this.tgLink = "https://t.me/" + this.clientSettings.tgId;
} }
} }
this.visible = true; this.visible = true;
@@ -381,9 +343,6 @@
}, },
genSubLink(subID) { genSubLink(subID) {
return app.subSettings.subURI+subID; return app.subSettings.subURI+subID;
},
genSubJsonLink(subID) {
return app.subSettings.subJsonURI+subID;
} }
}; };

View File

@@ -40,7 +40,7 @@
inModal.visible = false; inModal.visible = false;
inModal.loading(false); inModal.loading(false);
}, },
loading(loading=true) { loading(loading) {
inModal.confirmLoading = loading; inModal.confirmLoading = loading;
}, },
}; };

View File

@@ -56,13 +56,9 @@
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'> <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear> <transition name="list" appear>
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px" <a-tag v-if="false" color="red" style="margin-bottom: 10px">
message='{{ i18n "secAlertTitle" }}' Please go to the panel settings as soon as possible to modify the username and password, otherwise there may be a risk of leaking account information
color="red" </a-tag>
description='{{ i18n "secAlertSsl" }}'
show-icon closable
>
</a-alert>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-card hoverable> <a-card hoverable>
@@ -137,10 +133,6 @@
<a-icon type="export"></a-icon> <a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }} {{ i18n "pages.inbounds.export" }}
</a-menu-item> </a-menu-item>
<a-menu-item key="subs" v-if="subSettings.enable">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export" }} - {{ i18n "pages.settings.subSettings" }}
</a-menu-item>
<a-menu-item key="resetInbounds"> <a-menu-item key="resetInbounds">
<a-icon type="reload"></a-icon> <a-icon type="reload"></a-icon>
{{ i18n "pages.inbounds.resetAllTraffic" }} {{ i18n "pages.inbounds.resetAllTraffic" }}
@@ -149,7 +141,7 @@
<a-icon type="file-done"></a-icon> <a-icon type="file-done"></a-icon>
{{ i18n "pages.inbounds.resetAllClientTraffics" }} {{ i18n "pages.inbounds.resetAllClientTraffics" }}
</a-menu-item> </a-menu-item>
<a-menu-item key="delDepletedClients" style="color: #FF4D4F;"> <a-menu-item key="delDepletedClients">
<a-icon type="rest"></a-icon> <a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }} {{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item> </a-menu-item>
@@ -186,7 +178,7 @@
</a-radio-group> </a-radio-group>
</div> </div>
<a-back-top></a-back-top> <a-back-top></a-back-top>
<a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id" <a-table :columns="isMobile ? mobileColums : columns" :row-key="dbInbound => dbInbound.id"
:data-source="searchedInbounds" :data-source="searchedInbounds"
:scroll="isMobile ? {} : { x: 1000 }" :scroll="isMobile ? {} : { x: 1000 }"
:pagination=pagination(searchedInbounds) :pagination=pagination(searchedInbounds)
@@ -204,7 +196,7 @@
<a-icon type="edit"></a-icon> <a-icon type="edit"></a-icon>
{{ i18n "edit" }} {{ i18n "edit" }}
</a-menu-item> </a-menu-item>
<a-menu-item key="qrcode" v-if="(dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser) || dbInbound.isWireguard"> <a-menu-item key="qrcode" v-if="dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser">
<a-icon type="qrcode"></a-icon> <a-icon type="qrcode"></a-icon>
{{ i18n "qrCode" }} {{ i18n "qrCode" }}
</a-menu-item> </a-menu-item>
@@ -225,11 +217,7 @@
<a-icon type="export"></a-icon> <a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}} {{ i18n "pages.inbounds.export"}}
</a-menu-item> </a-menu-item>
<a-menu-item key="subs" v-if="subSettings.enable"> <a-menu-item key="delDepletedClients">
<a-icon type="export"></a-icon>
{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}
</a-menu-item>
<a-menu-item key="delDepletedClients" style="color: #FF4D4F;">
<a-icon type="rest"></a-icon> <a-icon type="rest"></a-icon>
{{ i18n "pages.inbounds.delDepletedClients" }} {{ i18n "pages.inbounds.delDepletedClients" }}
</a-menu-item> </a-menu-item>
@@ -508,7 +496,7 @@
scopedSlots: { customRender: 'expiryTime' }, scopedSlots: { customRender: 'expiryTime' },
}]; }];
const mobileColumns = [{ const mobileColums = [{
title: "ID", title: "ID",
align: 'right', align: 'right',
dataIndex: "id", dataIndex: "id",
@@ -571,13 +559,11 @@
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000, refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
subSettings: { subSettings: {
enable : false, enable : false,
subURI : '', subURI : ''
subJsonURI : '',
}, },
remarkModel: '-ieo', remarkModel: '-ieo',
datepicker: 'gregorian', datepicker: 'gregorian',
tgBotEnable: false, tgBotEnable: false,
showAlert: false,
pageSize: 0, pageSize: 0,
isMobile: window.innerWidth <= 768, isMobile: window.innerWidth <= 768,
}, },
@@ -592,7 +578,6 @@
this.refreshing = false; this.refreshing = false;
return; return;
} }
await this.getOnlineUsers(); await this.getOnlineUsers();
this.setInbounds(msg.obj); this.setInbounds(msg.obj);
setTimeout(() => { setTimeout(() => {
@@ -619,8 +604,7 @@
this.tgBotEnable = tgBotEnable; this.tgBotEnable = tgBotEnable;
this.subSettings = { this.subSettings = {
enable : subEnable, enable : subEnable,
subURI: subURI, subURI: subURI
subJsonURI: subJsonURI
}; };
this.pageSize = pageSize; this.pageSize = pageSize;
this.remarkModel = remarkModel; this.remarkModel = remarkModel;
@@ -658,12 +642,8 @@
clientCount = clients.length; clientCount = clients.length;
if (dbInbound.enable) { if (dbInbound.enable) {
clients.forEach(client => { clients.forEach(client => {
if (client.enable) { client.enable ? active.push(client.email) : deactive.push(client.email);
active.push(client.email); if(this.isClientOnline(client.email)) online.push(client.email);
if (this.isClientOnline(client.email)) online.push(client.email);
} else {
deactive.push(client.email);
}
}); });
clientStats.forEach(client => { clientStats.forEach(client => {
if (!client.enable) { if (!client.enable) {
@@ -688,7 +668,6 @@
online: online, online: online,
}; };
}, },
searchInbounds(key) { searchInbounds(key) {
if (ObjectUtil.isEmpty(key)) { if (ObjectUtil.isEmpty(key)) {
this.searchedInbounds = this.dbInbounds.slice(); this.searchedInbounds = this.dbInbounds.slice();
@@ -752,9 +731,6 @@
case "export": case "export":
this.exportAllLinks(); this.exportAllLinks();
break; break;
case "subs":
this.exportAllSubs();
break;
case "resetInbounds": case "resetInbounds":
this.resetAllTraffic(); this.resetAllTraffic();
break; break;
@@ -786,9 +762,6 @@
case "export": case "export":
this.inboundLinks(dbInbound.id); this.inboundLinks(dbInbound.id);
break; break;
case "subs":
this.exportSubs(dbInbound.id);
break;
case "clipboard": case "clipboard":
this.copyToClipboard(dbInbound.id); this.copyToClipboard(dbInbound.id);
break; break;
@@ -838,7 +811,7 @@
protocol: baseInbound.protocol, protocol: baseInbound.protocol,
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(), settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
streamSettings: baseInbound.stream.toString(), streamSettings: baseInbound.stream.toString(),
sniffing: baseInbound.sniffing.toString(), sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
}; };
await this.submit('/panel/inbound/add', data, inModal); await this.submit('/panel/inbound/add', data, inModal);
}, },
@@ -887,7 +860,7 @@
settings: inbound.settings.toString(), settings: inbound.settings.toString(),
}; };
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString(); if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
data.sniffing = inbound.sniffing.toString(); if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit('/panel/inbound/add', data, inModal); await this.submit('/panel/inbound/add', data, inModal);
}, },
@@ -906,7 +879,7 @@
settings: inbound.settings.toString(), settings: inbound.settings.toString(),
}; };
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString(); if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
data.sniffing = inbound.sniffing.toString(); if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal); await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
}, },
@@ -994,7 +967,7 @@
}, },
delInbound(dbInboundId) { delInbound(dbInboundId) {
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.deleteInbound"}}' + ' #' + dbInboundId, title: '{{ i18n "pages.inbounds.deleteInbound"}}',
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}', content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
@@ -1007,7 +980,7 @@
clientId = this.getClientId(dbInbound.protocol, client); clientId = this.getClientId(dbInbound.protocol, client);
if (confirmation){ if (confirmation){
this.$confirm({ this.$confirm({
title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email, title: '{{ i18n "pages.inbounds.deleteClient"}}',
content: '{{ i18n "pages.inbounds.deleteClientContent"}}', content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
class: themeSwitcher.currentTheme, class: themeSwitcher.currentTheme,
okText: '{{ i18n "delete"}}', okText: '{{ i18n "delete"}}',
@@ -1213,22 +1186,6 @@
newDbInbound = this.checkFallback(dbInbound); newDbInbound = this.checkFallback(dbInbound);
txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks(), newDbInbound.remark); txtModal.show('{{ i18n "pages.inbounds.export"}}', newDbInbound.genInboundLinks(), newDbInbound.remark);
}, },
exportSubs(dbInboundId) {
const dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
const clients = this.getInboundClients(dbInbound);
let subLinks = []
if (clients != null){
clients.forEach(c => {
if (c.subId && c.subId.length>0){
subLinks.push(this.subSettings.subURI + c.subId + "?name=" + c.subId)
}
})
}
txtModal.show(
'{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
[...new Set(subLinks)].join('\n'),
dbInbound.remark + "-Subs");
},
importInbound() { importInbound() {
promptModal.open({ promptModal.open({
title: '{{ i18n "pages.inbounds.importInbound" }}', title: '{{ i18n "pages.inbounds.importInbound" }}',
@@ -1241,23 +1198,6 @@
}, },
}); });
}, },
exportAllSubs() {
let subLinks = []
for (const dbInbound of this.dbInbounds) {
const clients = this.getInboundClients(dbInbound);
if (clients != null){
clients.forEach(c => {
if (c.subId && c.subId.length>0){
subLinks.push(this.subSettings.subURI + c.subId + "?name=" + c.subId)
}
})
}
}
txtModal.show(
'{{ i18n "pages.inbounds.export"}} - {{ i18n "pages.settings.subSettings" }}',
[...new Set(subLinks)].join('\r\n'),
'All-Inbounds-Subs');
},
exportAllLinks() { exportAllLinks() {
let copyText = []; let copyText = [];
for (const dbInbound of this.dbInbounds) { for (const dbInbound of this.dbInbounds) {
@@ -1298,7 +1238,7 @@
pagination(obj){ pagination(obj){
if (this.pageSize > 0 && obj.length>this.pageSize) { if (this.pageSize > 0 && obj.length>this.pageSize) {
// Set page options based on object size // Set page options based on object size
sizeOptions = []; sizeOptions = []
for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) { for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) {
sizeOptions.push(i.toString()); sizeOptions.push(i.toString());
} }
@@ -1311,8 +1251,8 @@
position: 'bottom', position: 'bottom',
pageSize: this.pageSize, pageSize: this.pageSize,
pageSizeOptions: sizeOptions pageSizeOptions: sizeOptions
}; }
return p; return p
} }
return false return false
}, },
@@ -1326,9 +1266,6 @@
}, 500) }, 500)
}, },
mounted() { mounted() {
if (window.location.protocol !== "https:") {
this.showAlert = true;
}
window.addEventListener('resize', this.onResize); window.addEventListener('resize', this.onResize);
this.onResize(); this.onResize();
this.loading(); this.loading();
@@ -1366,6 +1303,7 @@
} }
}, },
}); });
</script> </script>
{{template "inboundModal"}} {{template "inboundModal"}}
@@ -1375,5 +1313,6 @@
{{template "inboundInfoModal"}} {{template "inboundInfoModal"}}
{{template "clientsModal"}} {{template "clientsModal"}}
{{template "clientsBulkModal"}} {{template "clientsBulkModal"}}
</body> </body>
</html> </html>

View File

@@ -18,16 +18,6 @@
.ant-card-dark h2 { .ant-card-dark h2 {
color: hsla(0, 0%, 100%, .65); color: hsla(0, 0%, 100%, .65);
} }
.dark .ant-card-hoverable:hover,
.dark .ant-space-item > .ant-tabs:hover {
transform: scale(0.987);
outline-color: #40434d;
}
.dark .ant-card-bordered {
outline: 2px solid var(--dark-color-background);
}
</style> </style>
<body> <body>
@@ -36,15 +26,6 @@
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/> <a-spin :spinning="spinning" :delay="200" :tip="loadingTip"/>
<transition name="list" appear>
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
message='{{ i18n "secAlertTitle" }}'
color="red"
description='{{ i18n "secAlertSsl" }}'
show-icon closable
>
</a-alert>
</transition>
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-card hoverable> <a-card hoverable>
@@ -55,15 +36,15 @@
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.cpu.color" :stroke-color="status.cpu.color"
:percent="status.cpu.percent"></a-progress> :percent="status.cpu.percent"></a-progress>
<div><b>CPU:</b> [[ cpuCoreFormat(status.cpuCores) ]]</div> <div>CPU: [[ cpuCoreFormat(status.cpuCores) ]]</div>
<div><b>Speed:</b> [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div> <div>Speed: [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
</a-col> </a-col>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
<a-progress type="dashboard" status="normal" <a-progress type="dashboard" status="normal"
:stroke-color="status.mem.color" :stroke-color="status.mem.color"
:percent="status.mem.percent"></a-progress> :percent="status.mem.percent"></a-progress>
<div> <div>
<b>{{ i18n "pages.index.memory"}}:</b> [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]] {{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
@@ -75,7 +56,7 @@
:stroke-color="status.swap.color" :stroke-color="status.swap.color"
:percent="status.swap.percent"></a-progress> :percent="status.swap.percent"></a-progress>
<div> <div>
<b>Swap:</b> [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]] Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
</div> </div>
</a-col> </a-col>
<a-col :span="12" style="text-align: center"> <a-col :span="12" style="text-align: center">
@@ -83,7 +64,7 @@
:stroke-color="status.disk.color" :stroke-color="status.disk.color"
:percent="status.disk.percent"></a-progress> :percent="status.disk.percent"></a-progress>
<div> <div>
<b>{{ i18n "pages.index.hard"}}:</b> [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]] {{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
</div> </div>
</a-col> </a-col>
</a-row> </a-row>
@@ -94,25 +75,25 @@
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<a-row> <a-row>
<a-col :sm="24" :lg="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable>
<b>3X-UI:</b> 3X-UI <a href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a>
<a rel="noopener" href="https://github.com/MHSanaei/3x-ui/releases" target="_blank"><a-tag color="green">v{{ .cur_ver }}</a-tag></a> Xray <a-tag color="green" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
<a rel="noopener" href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@Panel3xui</a-tag></a> <a href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :lg="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable>
<b>{{ i18n "pages.index.operationHours" }}:</b> {{ i18n "menu.link" }}:
<a-tag color="green">Xray [[ formatSecond(status.appStats.uptime) ]]</a-tag> <a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag>
<a-tag color="green">OS [[ formatSecond(status.uptime) ]]</a-tag> <a-tag color="purple" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :lg="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable>
<b>{{ i18n "pages.index.xrayStatus" }}:</b> {{ i18n "pages.index.xrayStatus" }}:
<a-tag style="text-transform: capitalize;" :color="status.xray.color">[[ status.xray.state ]] <a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
</a-tag>
<a-popover v-if="status.xray.state === State.Error" <a-popover v-if="status.xray.state === State.Error"
:overlay-class-name="themeSwitcher.currentTheme"> :overlay-class-name="themeSwitcher.currentTheme">
<span slot="title" style="font-size: 12pt">An error occurred while running Xray <span slot="title" style="font-size: 12pt">An error occurred while running Xray
@@ -125,143 +106,137 @@
</a-popover> </a-popover>
<a-tag color="purple" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag> <a-tag color="purple" style="cursor: pointer;" @click="stopXrayService">{{ i18n "pages.index.stopXray" }}</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag> <a-tag color="purple" style="cursor: pointer;" @click="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag> <a-tag color="purple" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :lg="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable>
<b>{{ i18n "menu.link" }}:</b> {{ i18n "pages.index.operationHours" }}:
<a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</a-tag> Xray
<a-tag color="purple" style="cursor: pointer;" @click="openConfig">{{ i18n "pages.index.config" }}</a-tag> <a-tag color="green">[[ formatSecond(status.appStats.uptime) ]]</a-tag>
<a-tag color="purple" style="cursor: pointer;" @click="openBackup">{{ i18n "pages.index.backup" }}</a-tag> OS
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :lg="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable>
<b>{{ i18n "pages.index.systemLoad" }}:</b> {{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<a-tag color="green">
<a-tooltip> <a-tooltip>
[[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<template slot="title"> <template slot="title">
{{ i18n "pages.index.systemLoadDesc" }} {{ i18n "pages.index.systemLoadDesc" }}
</template> </template>
<a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :lg="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable>
<b>{{ i18n "usage"}}:</b> {{ i18n "usage"}}:
<a-tag color="green"> RAM [[ sizeFormat(status.appStats.mem) ]] -
RAM [[ sizeFormat(status.appStats.mem) ]] Threads [[ status.appStats.threads ]]
</a-tag> </a-tooltip>
<a-tag color="green">
Threads [[ status.appStats.threads ]]
</a-tag>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :lg="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable>
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-tag> <a-icon type="global"></a-icon>
IPv4:
<a-tooltip> <a-tooltip>
<a-icon type="global"></a-icon> IPv4
<template slot="title"> <template slot="title">
[[ status.publicIP.ipv4 ]] [[ status.publicIP.ipv4 ]]
</template> </template>
<a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</a-tag> </a-col>
</a-col> <a-col :span="12">
<a-col :span="12"> <a-icon type="global"></a-icon>
<a-tag> IPv6:
<a-tooltip> <a-tooltip>
<a-icon type="global"></a-icon> IPv6
<template slot="title"> <template slot="title">
[[ status.publicIP.ipv6 ]] [[ status.publicIP.ipv6 ]]
</template> </template>
<a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</a-tag>
</a-col> </a-col>
</a-row> </a-row>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :lg="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable>
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-tag> <a-icon type="swap"></a-icon>
TCP: [[ status.tcpCount ]]
<a-tooltip> <a-tooltip>
<a-icon type="swap"></a-icon> TCP: [[ status.tcpCount ]]
<template slot="title"> <template slot="title">
{{ i18n "pages.index.connectionTcpCountDesc" }} {{ i18n "pages.index.connectionTcpCountDesc" }}
</template> </template>
<a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</a-tag>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-tag> <a-icon type="swap"></a-icon>
UDP: [[ status.udpCount ]]
<a-tooltip> <a-tooltip>
<a-icon type="swap"></a-icon> UDP: [[ status.udpCount ]]
<template slot="title"> <template slot="title">
{{ i18n "pages.index.connectionUdpCountDesc" }} {{ i18n "pages.index.connectionUdpCountDesc" }}
</template> </template>
<a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</a-tag>
</a-col> </a-col>
</a-row> </a-row>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :lg="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable>
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-tag>
<a-tooltip>
<a-icon type="arrow-up"></a-icon> <a-icon type="arrow-up"></a-icon>
Up: [[ sizeFormat(status.netIO.up) ]]/s [[ sizeFormat(status.netIO.up) ]]/s
<a-tooltip>
<template slot="title"> <template slot="title">
{{ i18n "pages.index.upSpeed" }} {{ i18n "pages.index.upSpeed" }}
</template> </template>
<a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</a-tag>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-tag>
<a-tooltip>
<a-icon type="arrow-down"></a-icon> <a-icon type="arrow-down"></a-icon>
Down: [[ sizeFormat(status.netIO.down) ]]/s [[ sizeFormat(status.netIO.down) ]]/s
<a-tooltip>
<template slot="title"> <template slot="title">
{{ i18n "pages.index.downSpeed" }} {{ i18n "pages.index.downSpeed" }}
</template> </template>
<a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</a-tag>
</a-col> </a-col>
</a-row> </a-row>
</a-card> </a-card>
</a-col> </a-col>
<a-col :sm="24" :lg="12"> <a-col :sm="24" :md="12">
<a-card hoverable> <a-card hoverable>
<a-row> <a-row>
<a-col :span="12"> <a-col :span="12">
<a-tag> <a-icon type="cloud-upload"></a-icon>
[[ sizeFormat(status.netTraffic.sent) ]]
<a-tooltip> <a-tooltip>
<a-icon type="cloud-upload"></a-icon>
<template slot="title"> <template slot="title">
{{ i18n "pages.index.totalSent" }} {{ i18n "pages.index.totalSent" }}
</template> Out: [[ sizeFormat(status.netTraffic.sent) ]] </template>
<a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</a-tag>
</a-col> </a-col>
<a-col :span="12"> <a-col :span="12">
<a-tag> <a-icon type="cloud-download"></a-icon>
[[ sizeFormat(status.netTraffic.recv) ]]
<a-tooltip> <a-tooltip>
<a-icon type="cloud-download"></a-icon>
<template slot="title"> <template slot="title">
{{ i18n "pages.index.totalReceive" }} {{ i18n "pages.index.totalReceive" }}
</template> In: [[ sizeFormat(status.netTraffic.recv) ]] </template>
<a-icon type="question-circle"></a-icon>
</a-tooltip> </a-tooltip>
</a-tag>
</a-col> </a-col>
</a-row> </a-row>
</a-card> </a-card>
@@ -281,51 +256,51 @@
></a-alert> ></a-alert>
<template v-for="version, index in versionModal.versions"> <template v-for="version, index in versionModal.versions">
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'" <a-tag :color="index % 2 == 0 ? 'purple' : 'green'"
style="margin-right: 10px" @click="switchV2rayVersion(version)"> style="margin: 10px" @click="switchV2rayVersion(version)">
[[ version ]] [[ version ]]
</a-tag> </a-tag>
</template> </template>
</a-modal> </a-modal>
<a-modal id="log-modal" v-model="logModal.visible" <a-modal id="log-modal" v-model="logModal.visible" title="Logs"
:closable="true" @cancel="() => logModal.visible = false" :closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
:class="themeSwitcher.currentTheme" :class="themeSwitcher.currentTheme"
width="800px" footer=""> width="800px"
<template slot="title"> footer="">
{{ i18n "pages.index.logs" }}
<a-icon :spin="logModal.loading"
type="sync"
style="vertical-align: middle; margin-left: 10px;"
:disabled="logModal.loading"
@click="openLogs()">
</a-icon>
</template>
<a-form layout="inline"> <a-form layout="inline">
<a-form-item> <a-form-item label="Count">
<a-input-group compact> <a-select v-model="logModal.rows"
<a-select v-model="logModal.rows" style="width:70px;" style="width: 80px"
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme"> @change="openLogs()"
<a-select-option value="10">10</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="20">20</a-select-option> <a-select-option value="10">10</a-select-option>
<a-select-option value="50">50</a-select-option> <a-select-option value="20">20</a-select-option>
<a-select-option value="100">100</a-select-option> <a-select-option value="50">50</a-select-option>
</a-select> <a-select-option value="100">100</a-select-option>
<a-select v-model="logModal.level" style="width:100px;" </a-select>
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme"> </a-form-item>
<a-select-option value="debug">Debug</a-select-option> <a-form-item label="Log Level">
<a-select-option value="info">Info</a-select-option> <a-select v-model="logModal.level"
<a-select-option value="notice">Notice</a-select-option> style="width: 120px"
<a-select-option value="warning">Warning</a-select-option> @change="openLogs()"
<a-select-option value="err">Error</a-select-option> :dropdown-class-name="themeSwitcher.currentTheme">
</a-select> <a-select-option value="debug">Debug</a-select-option>
</a-input-group> <a-select-option value="info">Info</a-select-option>
<a-select-option value="notice">Notice</a-select-option>
<a-select-option value="warning">Warning</a-select-option>
<a-select-option value="err">Error</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="SysLog">
<a-checkbox v-model="logModal.syslog" @change="openLogs()"></a-checkbox>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox> <a-button class="ant-btn ant-btn-primary" :loading="logModal.loading" @click="openLogs()"><a-icon :spin="logModal.loading" type="sync"></a-icon> Reload</a-button>
</a-form-item> </a-form-item>
<a-form-item style="float: right;"> <a-form-item>
<a-button type="primary" icon="download" <a-button type="primary" style="margin-bottom: 10px;"
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs.join('\n'))" download="x-ui.log"> :href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs.join('\n'))" download="x-ui.log">
{{ i18n "download" }} x-ui.log
</a-button> </a-button>
</a-form-item> </a-form-item>
</a-form> </a-form>
@@ -333,8 +308,8 @@
</a-modal> </a-modal>
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title" <a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
:closable="true" footer="" :closable="true" :class="themeSwitcher.currentTheme"
:class="themeSwitcher.currentTheme"> @ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content" <a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
:message="backupModal.description" :message="backupModal.description"
show-icon show-icon
@@ -458,6 +433,7 @@
const logModal = { const logModal = {
visible: false, visible: false,
logs: [], logs: [],
formattedLogs: '',
rows: 20, rows: 20,
level: 'info', level: 'info',
syslog: false, syslog: false,
@@ -465,7 +441,7 @@
show(logs) { show(logs) {
this.visible = true; this.visible = true;
this.logs = logs; this.logs = logs;
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record..."; this.formattedLogs = logs.length > 0 ? this.formatLogs(logs) : "No Record...";
}, },
formatLogs(logs) { formatLogs(logs) {
let formattedLogs = ''; let formattedLogs = '';
@@ -543,7 +519,6 @@
backupModal, backupModal,
spinning: false, spinning: false,
loadingTip: '{{ i18n "loading"}}', loadingTip: '{{ i18n "loading"}}',
showAlert: false,
}, },
methods: { methods: {
loading(spinning, tip = '{{ i18n "loading"}}') { loading(spinning, tip = '{{ i18n "loading"}}') {
@@ -667,14 +642,14 @@
}, },
}, },
async mounted() { async mounted() {
if (window.location.protocol !== "https:") { let retries = 0;
this.showAlert = true; while (retries < 5) {
}
while (true) {
try { try {
await this.getStatus(); await this.getStatus();
retries = 0;
} catch (e) { } catch (e) {
console.error(e); console.error("Error occurred while fetching status:", e);
retries++;
} }
await PromiseUtil.sleep(2000); await PromiseUtil.sleep(2000);
} }

View File

@@ -75,43 +75,28 @@
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'> <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear>
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
message='{{ i18n "secAlertTitle" }}'
color="red"
description='{{ i18n "secAlertSsl" }}'
show-icon closable
>
</a-alert>
<a-alert type="error" v-if="confAlerts.length>0" style="margin-bottom: 10px"
message='{{ i18n "secAlertTitle" }}'
color="red"
show-icon closable
>
<template slot="description">
{{ i18n "secAlertConf" }}
<li v-for="a in confAlerts">- [[ a ]]</li>
</template>
</a-alert>
</transition>
<a-space direction="vertical"> <a-space direction="vertical">
<a-card hoverable style="margin-bottom: .5rem; overflow-x: hidden;"> <a-card hoverable style="margin-bottom: .5rem;">
<a-row style="display: flex; flex-wrap: wrap; align-items: center;"> <a-row>
<a-col :xs="24" :sm="10" style="padding: 4px;"> <a-col :xs="24" :sm="8" style="padding: 4px;">
<a-space direction="horizontal"> <a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button> <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button> <a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
</a-space> </a-space>
</a-col> </a-col>
<a-col :xs="24" :sm="14"> <a-col :xs="24" :sm="16">
<template> <template>
<div> <div>
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"> <template>
</a-back-top> <div>
<a-alert type="warning" style="float: right; width: fit-content" <a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
message='{{ i18n "pages.settings.infoDesc" }}' </a-back-top>
show-icon <a-alert type="warning" style="float: right; width: fit-content"
> message='{{ i18n "pages.settings.infoDesc" }}'
show-icon
>
</div>
</template>
</div> </div>
</template> </template>
</a-col> </a-col>
@@ -179,6 +164,7 @@
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<a-list-item-meta title="Language" /> <a-list-item-meta title="Language" />
</a-col> </a-col>
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<template> <template>
<a-select <a-select
@@ -248,6 +234,7 @@
<a-button type="primary" :loading="this.changeSecret" @click="updateSecret">{{ i18n "confirm" }}</a-button> <a-button type="primary" :loading="this.changeSecret" @click="updateSecret">{{ i18n "confirm" }}</a-button>
</a-form> </a-form>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'> <a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
<a-list item-layout="horizontal"> <a-list item-layout="horizontal">
<setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.settings.telegramBotEnable" }}' desc='{{ i18n "pages.settings.telegramBotEnableDesc" }}' v-model="allSetting.tgBotEnable"></setting-list-item>
@@ -263,13 +250,15 @@
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<a-list-item-meta title="Telegram Bot Language" /> <a-list-item-meta title="Telegram Bot Language" />
</a-col> </a-col>
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<template> <template>
<a-select <a-select
ref="selectBotLang" ref="selectBotLang"
v-model="allSetting.tgLang" v-model="allSetting.tgLang"
:dropdown-class-name="themeSwitcher.currentTheme" :dropdown-class-name="themeSwitcher.currentTheme"
style="width: 100%"> style="width: 100%"
>
<a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs"> <a-select-option :value="l.value" :label="l.value" v-for="l in supportLangs">
<span role="img" :aria-label="l.name" v-text="l.icon"></span> <span role="img" :aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span> &nbsp;&nbsp;<span v-text="l.name"></span>
@@ -296,17 +285,6 @@
<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>
</a-list> </a-list>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="5" tab='{{ i18n "pages.settings.subSettings" }} Json' v-if="allSetting.subEnable">
<a-list item-layout="horizontal">
<setting-list-item type="text" title='{{ i18n "pages.settings.subPath"}}' desc='{{ i18n "pages.settings.subPathDesc"}}' v-model="allSetting.subJsonPath"></setting-list-item>
<setting-list-item type="text" title='{{ i18n "pages.settings.subURI"}}' desc='{{ i18n "pages.settings.subURIDesc"}}' v-model="allSetting.subJsonURI" placeholder="(http|https)://domain[:port]/path/"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.settings.fragment"}}' desc='{{ i18n "pages.settings.fragmentDesc"}}' v-model="fragment"></setting-list-item>
<template v-if="fragment">
<setting-list-item type="text" title='length' v-model="fragmentLength" placeholder="100-200"></setting-list-item>
<setting-list-item type="text" title='Interval' v-model="fragmentInterval" placeholder="10-20"></setting-list-item>
</template>
</a-list>
</a-tab-pane>
</a-tabs> </a-tabs>
</a-space> </a-space>
</a-spin> </a-spin>
@@ -337,24 +315,6 @@
remarkSeparators: [' ','-','_','@',':','~','|',',','.','/'], remarkSeparators: [' ','-','_','@',':','~','|',',','.','/'],
datepickerList: [{name:'Gregorian (Standard)', value: 'gregorian'}, {name:'Jalalian (شمسی)', value: 'jalalian'}], datepickerList: [{name:'Gregorian (Standard)', value: 'gregorian'}, {name:'Jalalian (شمسی)', value: 'jalalian'}],
remarkSample: '', remarkSample: '',
defaultFragment: {
tag: "fragment",
protocol: "freedom",
settings: {
domainStrategy: "AsIs",
fragment: {
packets: "tlshello",
length: "100-200",
interval: "10-20"
}
},
streamSettings: {
sockopt: {
tcpKeepAliveIdle: 100,
tcpNoDelay: true
}
}
},
get remarkModel() { get remarkModel() {
rm = this.allSetting.remarkModel; rm = this.allSetting.remarkModel;
return rm.length>1 ? rm.substring(1).split('') : []; return rm.length>1 ? rm.substring(1).split('') : [];
@@ -483,60 +443,13 @@
} }
}, },
}, },
computed: {
fragment: {
get: function() { return this.allSetting?.subJsonFragment != ""; },
set: function (v) {
this.allSetting.subJsonFragment = v ? JSON.stringify(this.defaultFragment) : "";
}
},
fragmentLength: {
get: function() { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.length : ""; },
set: function(v) {
if (v != ""){
newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.settings.fragment.length = v;
this.allSetting.subJsonFragment = JSON.stringify(newFragment);
}
}
},
fragmentInterval: {
get: function() { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.interval : ""; },
set: function(v) {
if (v != ""){
newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.settings.fragment.interval = v;
this.allSetting.subJsonFragment = JSON.stringify(newFragment);
}
}
},
confAlerts: {
get: function() {
if (!this.allSetting) return [];
var alerts = []
if (this.allSetting.port == 54321) alerts.push('{{ i18n "pages.settings.panelPort"}}');
panelPath = window.location.pathname.split('/').length<4
if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "pages.settings.panelSettings"}} {{ i18n "pages.settings.panelUrlPath"}}');
if (this.allSetting.subEnable) {
subPath = this.allSetting.subURI.length >0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
if (subPath == '/sub/') alerts.push('{{ i18n "pages.settings.subSettings"}} {{ i18n "pages.settings.subPath"}}');
subJsonPath = this.allSetting.subJsonURI.length >0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
if (subJsonPath == '/json/') alerts.push('JSON {{ i18n "pages.settings.subPath"}}');
}
return alerts
}
}
},
async mounted() { async mounted() {
if (window.location.protocol !== "https:") {
this.showAlert = true;
}
await this.getAllSetting(); await this.getAllSetting();
while (true) { while (true) {
await PromiseUtil.sleep(1000); await PromiseUtil.sleep(600);
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting); this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
} }
} },
}); });
</script> </script>
</body> </body>

View File

@@ -108,7 +108,7 @@
this.visible = false; this.visible = false;
this.loading(false); this.loading(false);
}, },
loading(loading=true) { loading(loading) {
this.confirmLoading = loading; this.confirmLoading = loading;
}, },
async getData(){ async getData(){
@@ -140,7 +140,7 @@
mtu: 1420, mtu: 1420,
secretKey: warpModal.warpData.private_key, secretKey: warpModal.warpData.private_key,
address: Object.values(config.interface.addresses), address: Object.values(config.interface.addresses),
domainStrategy: 'ForceIP', domainStrategy: 'ForceIPv6v4',
peers: [{ peers: [{
publicKey: peer.public_key, publicKey: peer.public_key,
endpoint: peer.endpoint.host, endpoint: peer.endpoint.host,

View File

@@ -1,14 +1,14 @@
<!DOCTYPE html> <!DOCTYPE html>
<html lang="en"> <html lang="en">
{{template "head" .}} {{template "head" .}}
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.css?{{ .cur_ver }}"></script> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.css">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.css?{{ .cur_ver }}"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.css?{{ .cur_ver }}">
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css"> <link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
<script src="{{ .base_path }}assets/base64/base64.min.js"></script> <script src="{{ .base_path }}assets/base64/base64.min.js"></script>
<script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/js/model/outbound.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/codemirror/codemirror.js?{{ .cur_ver }}"></script> <script src="{{ .base_path }}assets/codemirror/codemirror.js"></script>
<script src="{{ .base_path }}assets/codemirror/javascript.js"></script> <script src="{{ .base_path }}assets/codemirror/javascript.js"></script>
<script src="{{ .base_path }}assets/codemirror/jshint.js"></script> <script src="{{ .base_path }}assets/codemirror/jshint.js"></script>
<script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script> <script src="{{ .base_path }}assets/codemirror/jsonlint.js"></script>
@@ -63,15 +63,6 @@
<a-layout id="content-layout"> <a-layout id="content-layout">
<a-layout-content> <a-layout-content>
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'> <a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear>
<a-alert type="error" v-if="showAlert" style="margin-bottom: 10px"
message='{{ i18n "secAlertTitle" }}'
color="red"
description='{{ i18n "secAlertSsl" }}'
show-icon closable
>
</a-alert>
</transition>
<a-space direction="vertical"> <a-space direction="vertical">
<a-card hoverable style="margin-bottom: .5rem;"> <a-card hoverable style="margin-bottom: .5rem;">
<a-row> <a-row>
@@ -108,7 +99,7 @@
:class="themeSwitcher.currentTheme"> :class="themeSwitcher.currentTheme">
<a-tab-pane key="tpl-1" tab='{{ i18n "pages.xray.basicTemplate"}}'> <a-tab-pane key="tpl-1" tab='{{ i18n "pages.xray.basicTemplate"}}'>
<a-space direction="horizontal" style="padding: 20px 20px"> <a-space direction="horizontal" style="padding: 20px 20px">
<a-button type="danger" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button> <a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
</a-space> </a-space>
<a-collapse> <a-collapse>
<a-collapse-panel header='{{ i18n "pages.xray.generalConfigs"}}'> <a-collapse-panel header='{{ i18n "pages.xray.generalConfigs"}}'>
@@ -123,12 +114,15 @@
<a-list-item> <a-list-item>
<a-row style="padding: 20px"> <a-row style="padding: 20px">
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.xray.FreedomStrategy" }}' <a-list-item-meta
description='{{ i18n "pages.xray.FreedomStrategyDesc" }}' /> title='{{ i18n "pages.xray.FreedomStrategy" }}'
description='{{ i18n "pages.xray.FreedomStrategyDesc" }}'/>
</a-col> </a-col>
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<template> <template>
<a-select v-model="freedomStrategy" :dropdown-class-name="themeSwitcher.currentTheme" <a-select
v-model="freedomStrategy"
:dropdown-class-name="themeSwitcher.currentTheme"
style="width: 100%"> style="width: 100%">
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option> <a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select> </a-select>
@@ -138,67 +132,22 @@
</a-list-item> </a-list-item>
<a-row style="padding: 20px"> <a-row style="padding: 20px">
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.xray.RoutingStrategy" }}' <a-list-item-meta
description='{{ i18n "pages.xray.RoutingStrategyDesc" }}' /> title='{{ i18n "pages.xray.RoutingStrategy" }}'
description='{{ i18n "pages.xray.RoutingStrategyDesc" }}'/>
</a-col> </a-col>
<a-col :lg="24" :xl="12"> <a-col :lg="24" :xl="12">
<a-select v-model="routingStrategy" :dropdown-class-name="themeSwitcher.currentTheme" <template>
<a-select
v-model="routingStrategy"
:dropdown-class-name="themeSwitcher.currentTheme"
style="width: 100%"> style="width: 100%">
<a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option> <a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option>
</a-select> </a-select>
</a-col>
</a-row>
</a-list-item>
</a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.logConfigs" }}'>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" style="text-align: center;">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled" style="color: #FFA031"></a-icon>
{{ i18n "pages.xray.logConfigsDesc" }}
</template>
</a-alert>
</a-row>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.xray.logLevel" }}'
description='{{ i18n "pages.xray.logLevelDesc" }}' />
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select v-model="setLogLevel" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
<a-select-option v-for="s in logLevel" :value="s">[[ s ]]</a-select-option>
</a-select>
</template> </template>
</a-col> </a-col>
</a-row> </a-row>
<a-row style="padding: 20px"> </a-list-item>
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.xray.accessLog" }}'
description='{{ i18n "pages.xray.accessLogDesc" }}' />
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select v-model="accessLog" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
<a-select-option v-for="s in access" :value="s">[[ s ]]</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.xray.errorLog" }}'
description='{{ i18n "pages.xray.errorLogDesc" }}' />
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select v-model="errorLog" :dropdown-class-name="themeSwitcher.currentTheme" style="width: 100%">
<a-select-option v-for="s in error" :value="s">[[ s ]]</a-select-option>
</a-select>
</template>
</a-col>
</a-row>
</a-list-item>
</a-collapse-panel> </a-collapse-panel>
<a-collapse-panel header='{{ i18n "pages.xray.blockConfigs"}}'> <a-collapse-panel header='{{ i18n "pages.xray.blockConfigs"}}'>
<a-row :xs="24" :sm="24" :lg="12"> <a-row :xs="24" :sm="24" :lg="12">
@@ -278,11 +227,8 @@
<setting-list-item type="switch" title='{{ i18n "pages.xray.OpenAIWARP"}}' desc='{{ i18n "pages.xray.OpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.xray.OpenAIWARP"}}' desc='{{ i18n "pages.xray.OpenAIWARPDesc"}}' v-model="OpenAIWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.NetflixWARP"}}' desc='{{ i18n "pages.xray.NetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.xray.NetflixWARP"}}' desc='{{ i18n "pages.xray.NetflixWARPDesc"}}' v-model="NetflixWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.SpotifyWARP"}}' desc='{{ i18n "pages.xray.SpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item> <setting-list-item type="switch" title='{{ i18n "pages.xray.SpotifyWARP"}}' desc='{{ i18n "pages.xray.SpotifyWARPDesc"}}' v-model="SpotifyWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.MetaWARP"}}' desc='{{ i18n "pages.xray.MetaWARPDesc"}}' v-model="MetaWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.AppleWARP"}}' desc='{{ i18n "pages.xray.AppleWARPDesc"}}' v-model="AppleWARPSettings"></setting-list-item>
<setting-list-item type="switch" title='{{ i18n "pages.xray.RedditWARP"}}' desc='{{ i18n "pages.xray.RedditWARPDesc"}}' v-model="RedditWARPSettings"></setting-list-item>
</template> </template>
<a-button v-else type="primary" icon="cloud" style="margin: 15px 20px;" @click="showWarp()">WARP</a-button> <a-button v-else style="margin: 10px 0;" @click="showWarp">WARP {{ i18n "pages.xray.rules.outbound" }}</a-button>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</a-tab-pane> </a-tab-pane>
@@ -347,14 +293,6 @@
[[ rule.outboundTag ]] [[ rule.outboundTag ]]
</a-popover> </a-popover>
</template> </template>
<template slot="balancer" slot-scope="text, rule, index">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content">
<p v-if="rule.balancerTag">Balancer Tag: [[ rule.balancerTag ]]</p>
</template>
[[ rule.balancerTag ]]
</a-popover>
</template>
<template slot="info" slot-scope="text, rule, index"> <template slot="info" slot-scope="text, rule, index">
<a-popover placement="bottomRight" <a-popover placement="bottomRight"
v-if="(rule.source+rule.sourcePort+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0" v-if="(rule.source+rule.sourcePort+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
@@ -393,10 +331,6 @@
<td>Port</td> <td>Port</td>
<td><a-tag color="green" v-for="r in rule.port.split(',')">[[ r ]]</a-tag></td> <td><a-tag color="green" v-for="r in rule.port.split(',')">[[ r ]]</a-tag></td>
</tr> </tr>
<tr v-if="rule.balancerTag">
<td>Balancer Tag</td>
<td><a-tag color="blue">[[ rule.balancerTag ]]</a-tag></td>
</tr>
</table> </table>
</template> </template>
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;"> <a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
@@ -407,17 +341,8 @@
</a-table> </a-table>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true"> <a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true">
<a-row> <a-button type="primary" icon="plus" @click="addOutbound()" style="margin-bottom: 10px;">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
<a-col :xs="12" :sm="12" :lg="12"> <a-button type="primary" @click="showWarp()" style="margin-bottom: 10px;">WARP</a-button>
<a-button type="primary" icon="plus" @click="addOutbound()" style="margin-bottom: 10px;">{{ i18n
"pages.xray.outbound.addOutbound" }}</a-button>
<a-button type="primary" icon="cloud" @click="showWarp()" style="margin-bottom: 10px;">WARP</a-button>
</a-col>
<a-col :xs="12" :sm="12" :lg="12" style="text-align: right;">
<a-icon type="sync" :spin="refreshing" @click="refreshOutboundTraffic()" style="margin: 0 5px;"></a-icon>
<a-icon type="retweet" @click="resetOutboundTraffic(-1)"></a-icon>
</a-col>
</a-row>
<a-table :columns="outboundColumns" bordered <a-table :columns="outboundColumns" bordered
:row-key="r => r.key" :row-key="r => r.key"
:data-source="outboundData" :data-source="outboundData"
@@ -430,19 +355,10 @@
<a-dropdown :trigger="['click']"> <a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon> <a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme"> <a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item v-if="index>0" @click="setFirstOutbound(index)">
<a-icon type="vertical-align-top"></a-icon>
{{ i18n "pages.xray.rules.first"}}
</a-menu-item>
<a-menu-item @click="editOutbound(index)"> <a-menu-item @click="editOutbound(index)">
<a-icon type="edit"></a-icon> <a-icon type="edit"></a-icon>
{{ i18n "edit" }} {{ i18n "edit" }}
</a-menu-item> </a-menu-item>
<a-menu-item @click="resetOutboundTraffic(index)">
<span>
<a-icon type="retweet"></a-icon> {{ i18n "pages.inbounds.resetTraffic"}}
</span>
</a-menu-item>
<a-menu-item @click="deleteOutbound(index)"> <a-menu-item @click="deleteOutbound(index)">
<span style="color: #FF4D4F"> <span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}} <a-icon type="delete"></a-icon> {{ i18n "delete"}}
@@ -462,14 +378,11 @@
<a-tag style="margin:0;" v-if="outbound.streamSettings.security=='reality'" color="green">reality</a-tag> <a-tag style="margin:0;" v-if="outbound.streamSettings.security=='reality'" color="green">reality</a-tag>
</template> </template>
</template> </template>
<template slot="traffic" slot-scope="text, outbound, index">
<a-tag color="green">[[ findOutboundTraffic(outbound) ]]</a-tag>
</template>
</a-table> </a-table>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tpl-4" tab='{{ i18n "pages.xray.outbound.reverse"}}' style="padding-top: 20px;" force-render="true"> <a-tab-pane key="tpl-4" tab='{{ i18n "pages.xray.outbound.reverse"}}' style="padding-top: 20px;" force-render="true">
<a-button type="primary" icon="plus" @click="addReverse()" style="margin-bottom: 10px;">{{ i18n "pages.xray.outbound.addReverse" }}</a-button> <a-button type="primary" icon="plus" @click="addReverse()" style="margin-bottom: 10px;">{{ i18n "pages.xray.outbound.addReverse" }}</a-button>
<a-table :columns="reverseColumns" bordered v-if="reverseData.length>0" <a-table :columns="reverseColumns" bordered
:row-key="r => r.key" :row-key="r => r.key"
:data-source="reverseData" :data-source="reverseData"
:scroll="isMobile ? {} : { x: 200 }" :scroll="isMobile ? {} : { x: 200 }"
@@ -495,123 +408,6 @@
</template> </template>
</a-table> </a-table>
</a-tab-pane> </a-tab-pane>
<a-tab-pane key="tpl-5" tab='{{ i18n "pages.xray.Balancers"}}' style="padding-top: 20px;" force-render="true">
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
message='{{ i18n "pages.xray.balancer.balancerDesc" }}' show-icon></a-alert>
<a-button type="primary" icon="plus" @click="addBalancer()" style="margin-bottom: 10px;">{{ i18n "pages.xray.balancer.addBalancer"}}</a-button>
<a-table :columns="balancerColumns" bordered v-if="balancersData.length>0"
:row-key="r => r.key"
:data-source="balancersData"
:scroll="isMobile ? {} : { x: 200 }"
:pagination="false"
:indent-size="0"
:style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
<template slot="action" slot-scope="text, balancer, index">
[[ index+1 ]]
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item @click="editBalancer(index)">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<a-menu-item @click="deleteBalancer(index)">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<template slot="strategy" slot-scope="text, balancer, index">
<a-tag style="margin:0;" v-if="balancer.strategy=='random'" color="purple">Random</a-tag>
<a-tag style="margin:0;" v-if="balancer.strategy=='roundRobin'" color="green">Round Robin</a-tag>
</template>
<template slot="selector" slot-scope="text, balancer, index">
<a-tag class="info-large-tag" style="margin:1;" v-for="sel in balancer.selector">[[ sel ]]</a-tag>
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="tpl-6" tab='DNS' style="padding-top: 20px;" force-render="true">
<setting-list-item type="switch" title='{{ i18n "pages.xray.dns.enable" }}' desc='{{ i18n "pages.xray.dns.enableDesc" }}' v-model="enableDNS"></setting-list-item>
<template v-if="enableDNS">
<a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta title='{{ i18n "pages.xray.dns.strategy" }}' description='{{ i18n "pages.xray.dns.strategyDesc" }}' />
</a-col>
<a-col :lg="24" :xl="12">
<a-select
v-model="dnsStrategy"
style="width: 100%"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseIP', 'UseIPv4', 'UseIPv6']">
[[ l ]]
</a-select-option>
</a-select>
</a-col>
</a-row>
</a-list-item>
<a-divider>DNS</a-divider>
<a-button type="primary" icon="plus" @click="addDNSServer()" style="margin-bottom: 10px;">{{ i18n "pages.xray.dns.add" }}</a-button>
<a-table :columns="dnsColumns" bordered v-if="dnsServers.length>0"
:row-key="r => r.key"
:data-source="dnsServers"
:scroll="isMobile ? {} : { x: 200 }"
:pagination="false"
:indent-size="0"
:style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
<template slot="action" slot-scope="text,dns,index">
[[ index+1 ]]
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more" style="font-size: 16px; text-decoration: bold;"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item @click="editDNSServer(index)">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<a-menu-item @click="deleteDNSServer(index)">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
<template slot="address" slot-scope="dns,index">
<span v-if="typeof dns == 'object'">[[ dns.address ]]</span>
<span v-else>[[ dns ]]</span>
</template>
<template slot="domain" slot-scope="dns,index">
<span v-if="typeof dns == 'object'">[[ dns.domains.join(",") ]]</span>
</template>
</a-table>
<a-divider>Fake DNS</a-divider>
<a-button type="primary" icon="plus" @click="addFakedns()" style="margin-bottom: 10px;">{{ i18n "pages.xray.fakedns.add" }}</a-button>
<a-table :columns="fakednsColumns" bordered v-if="fakeDns && fakeDns.length>0" :row-key="r => r.key"
:data-source="fakeDns" :scroll="isMobile ? {} : { x: 200 }" :pagination="false" :indent-size="0"
:style="isMobile ? 'padding: 5px 0' : 'margin-left: 1px;'">
<template slot="action" slot-scope="text,fakedns,index">
[[ index+1 ]]
<a-dropdown :trigger="['click']">
<a-icon @click="e => e.preventDefault()" type="more"
style="font-size: 16px; text-decoration: bold;"></a-icon>
<a-menu slot="overlay" :theme="themeSwitcher.currentTheme">
<a-menu-item @click="editFakedns(index)">
<a-icon type="edit"></a-icon>
{{ i18n "edit" }}
</a-menu-item>
<a-menu-item @click="deleteFakedns(index)">
<span style="color: #FF4D4F">
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
</span>
</a-menu-item>
</a-menu>
</a-dropdown>
</template>
</a-table>
</template>
</a-tab-pane>
<a-tab-pane key="tpl-advanced" tab='{{ i18n "pages.xray.advancedTemplate"}}' style="padding-top: 20px;" force-render="true"> <a-tab-pane key="tpl-advanced" tab='{{ i18n "pages.xray.advancedTemplate"}}' style="padding-top: 20px;" force-render="true">
<a-list-item-meta title='{{ i18n "pages.xray.Template"}}' description='{{ i18n "pages.xray.TemplateDesc"}}'></a-list-item-meta> <a-list-item-meta title='{{ i18n "pages.xray.Template"}}' description='{{ i18n "pages.xray.TemplateDesc"}}'></a-list-item-meta>
<a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" style="margin: 10px 0;" :size="isMobile ? 'small' : ''"> <a-radio-group v-model="advSettings" @change="changeCode" button-style="solid" style="margin: 10px 0;" :size="isMobile ? 'small' : ''">
@@ -634,9 +430,6 @@
{{template "ruleModal"}} {{template "ruleModal"}}
{{template "outModal"}} {{template "outModal"}}
{{template "reverseModal"}} {{template "reverseModal"}}
{{template "balancerModal"}}
{{template "dnsModal"}}
{{template "fakednsModal"}}
{{template "warpModal"}} {{template "warpModal"}}
<script> <script>
const rulesColumns = [ const rulesColumns = [
@@ -653,10 +446,9 @@
{ title: 'Domain', dataIndex: 'domain', align: 'center', width: 20, ellipsis: true }, { title: 'Domain', dataIndex: 'domain', align: 'center', width: 20, ellipsis: true },
{ title: 'Port', dataIndex: 'port', align: 'center', width: 10, ellipsis: true }]}, { title: 'Port', dataIndex: 'port', align: 'center', width: 10, ellipsis: true }]},
{ title: '{{ i18n "pages.xray.rules.inbound"}}', children: [ { title: '{{ i18n "pages.xray.rules.inbound"}}', children: [
{ title: 'Inbound Tag', dataIndex: 'inboundTag', align: 'center', width: 15, ellipsis: true }, { title: 'Inbound Tag', dataIndex: 'inboundTag', align: 'center', width: 20, ellipsis: true },
{ title: 'Client Email', dataIndex: 'user', align: 'center', width: 20, ellipsis: true }]}, { title: 'Client Email', dataIndex: 'user', align: 'center', width: 20, ellipsis: true }]},
{ title: '{{ i18n "pages.xray.rules.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 15 }, { title: '{{ i18n "pages.xray.rules.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 20 },
{ title: '{{ i18n "pages.xray.rules.balancer"}}', dataIndex: 'balancerTag', align: 'center', width: 15 },
]; ];
const rulesMobileColumns = [ const rulesMobileColumns = [
@@ -671,7 +463,6 @@
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 }, { title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
{ title: '{{ i18n "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } }, { title: '{{ i18n "protocol"}}', align: 'center', width: 50, scopedSlots: { customRender: 'protocol' } },
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } }, { title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
{ title: '{{ i18n "pages.inbounds.traffic" }}', align: 'center', width: 50, scopedSlots: { customRender: 'traffic' } },
]; ];
const reverseColumns = [ const reverseColumns = [
@@ -681,25 +472,6 @@
{ title: '{{ i18n "pages.xray.outbound.domain"}}', dataIndex: 'domain', align: 'center', width: 50 }, { title: '{{ i18n "pages.xray.outbound.domain"}}', dataIndex: 'domain', align: 'center', width: 50 },
]; ];
const dnsColumns = [
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
{ title: '{{ i18n "pages.xray.outbound.address"}}', align: 'center', width: 50, scopedSlots: { customRender: 'address' } },
{ title: '{{ i18n "pages.xray.dns.domains"}}', align: 'center', width: 50, scopedSlots: { customRender: 'domain' } },
];
const fakednsColumns = [
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
{ title: '{{ i18n "pages.xray.fakedns.ipPool"}}', dataIndex: 'ipPool', align: 'center', width: 50 },
{ title: '{{ i18n "pages.xray.fakedns.poolSize"}}', dataIndex: 'poolSize', align: 'center', width: 50 },
];
const balancerColumns = [
{ title: "#", align: 'center', width: 20, scopedSlots: { customRender: 'action' } },
{ title: '{{ i18n "pages.xray.balancer.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
{ title: '{{ i18n "pages.xray.balancer.balancerStrategy"}}', align: 'center', width: 50, scopedSlots: { customRender: 'strategy' }},
{ title: '{{ i18n "pages.xray.balancer.balancerSelectors"}}', align: 'center', width: 100, scopedSlots: { customRender: 'selector' }},
];
const app = new Vue({ const app = new Vue({
delimiters: ['[[', ']]'], delimiters: ['[[', ']]'],
el: '#app', el: '#app',
@@ -711,11 +483,8 @@
oldXraySetting: '', oldXraySetting: '',
xraySetting: '', xraySetting: '',
inboundTags: [], inboundTags: [],
outboundsTraffic: [],
saveBtnDisable: true, saveBtnDisable: true,
refreshing: false,
restartResult: '', restartResult: '',
showAlert: false,
isMobile: window.innerWidth <= 768, isMobile: window.innerWidth <= 768,
advSettings: 'xraySetting', advSettings: 'xraySetting',
cm: null, cm: null,
@@ -752,9 +521,6 @@
protocol: "freedom" protocol: "freedom"
}, },
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"], routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
logLevel: ["none" , "debug" , "info" , "warning", "error"],
access: ["none" , "./access.log" ],
error: ["none" , "./error.log" ],
settingsData: { settingsData: {
protocols: { protocols: {
bittorrent: ["bittorrent"], bittorrent: ["bittorrent"],
@@ -781,9 +547,6 @@
google: ["geosite:google"], google: ["geosite:google"],
spotify: ["geosite:spotify"], spotify: ["geosite:spotify"],
netflix: ["geosite:netflix"], netflix: ["geosite:netflix"],
meta: ["geosite:meta"],
apple: ["geosite:apple"],
reddit: ["geosite:reddit"],
cn: [ cn: [
"geosite:cn", "geosite:cn",
"regexp:.*\\.cn$" "regexp:.*\\.cn$"
@@ -818,12 +581,6 @@
loading(spinning = true) { loading(spinning = true) {
this.spinning = spinning; this.spinning = spinning;
}, },
async getOutboundsTraffic() {
const msg = await HttpUtil.get("/panel/xray/getOutboundsTraffic");
if (msg.success) {
this.outboundsTraffic = msg.obj;
}
},
async getXraySetting() { async getXraySetting() {
this.loading(true); this.loading(true);
const msg = await HttpUtil.post("/panel/xray/"); const msg = await HttpUtil.post("/panel/xray/");
@@ -1002,14 +759,6 @@
} }
return true; return true;
}, },
findOutboundTraffic(o) {
for (const otraffic of this.outboundsTraffic) {
if (otraffic.tag == o.tag) {
return sizeFormat(otraffic.up) + ' / ' + sizeFormat(otraffic.down);
}
}
return sizeFormat(0) + ' / ' + sizeFormat(0);
},
findOutboundAddress(o) { findOutboundAddress(o) {
serverObj = null; serverObj = null;
switch(o.protocol){ switch(o.protocol){
@@ -1067,126 +816,6 @@
outbounds.splice(index,1); outbounds.splice(index,1);
this.outboundSettings = JSON.stringify(outbounds); this.outboundSettings = JSON.stringify(outbounds);
}, },
setFirstOutbound(index){
outbounds = this.templateSettings.outbounds;
outbounds.splice(0, 0, outbounds.splice(index, 1)[0]);
this.outboundSettings = JSON.stringify(outbounds);
},
async refreshOutboundTraffic() {
if (!this.refreshing) {
this.refreshing = true;
await this.getOutboundsTraffic();
data = []
if (this.templateSettings != null) {
this.templateSettings.outbounds.forEach((o, index) => {
data.push({'key': index, ...o});
});
}
this.outboundData = data;
this.refreshing = false;
}
},
async resetOutboundTraffic(index) {
let tag = "-alltags-";
if (index >= 0) {
tag = this.outboundData[index].tag ? this.outboundData[index].tag : ""
}
const msg = await HttpUtil.post("/panel/xray/resetOutboundsTraffic", { tag: tag });
if (msg.success) {
await this.refreshOutboundTraffic();
}
},
addBalancer() {
balancerModal.show({
title: '{{ i18n "pages.xray.balancer.addBalancer"}}',
okText: '{{ i18n "pages.xray.balancer.addBalancer"}}',
balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),
balancer: {
tag: '',
strategy: 'random',
selector: []
},
confirm: (balancer) => {
balancerModal.loading();
newTemplateSettings = this.templateSettings;
if (newTemplateSettings.routing.balancers == undefined) {
newTemplateSettings.routing.balancers = [];
}
let tmpBalancer = {
'tag': balancer.tag,
'selector': balancer.selector
};
if (balancer.strategy == 'roundRobin') {
tmpBalancer.strategy = {
'type': balancer.strategy
};
}
newTemplateSettings.routing.balancers.push(tmpBalancer);
this.templateSettings = newTemplateSettings;
balancerModal.close();
},
isEdit: false
});
},
editBalancer(index) {
const oldTag = this.balancersData[index].tag;
balancerModal.show({
title: '{{ i18n "pages.xray.balancer.editBalancer"}}',
okText: '{{ i18n "sure" }}',
balancerTags: this.balancersData.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag),
balancer: this.balancersData[index],
confirm: (balancer) => {
balancerModal.loading();
newTemplateSettings = this.templateSettings;
let tmpBalancer = {
'tag': balancer.tag,
'selector': balancer.selector
};
if (balancer.strategy == 'roundRobin') {
tmpBalancer.strategy = {
'type': balancer.strategy
};
}
newTemplateSettings.routing.balancers[index] = tmpBalancer;
// change edited tag if used in rule section
if (oldTag != balancer.tag) {
newTemplateSettings.routing.rules.forEach((rule) => {
if (rule.balancerTag && rule.balancerTag == oldTag) {
rule.balancerTag = balancer.tag;
}
});
}
this.templateSettings = newTemplateSettings;
balancerModal.close();
},
isEdit: true
});
},
deleteBalancer(index) {
newTemplateSettings = this.templateSettings;
//remove from balancers
const oldTag = this.balancersData[index].tag;
this.balancersData.splice(index, 1);
// remove from settings
let realIndex = newTemplateSettings.routing.balancers.findIndex((b) => b.tag == oldTag);
newTemplateSettings.routing.balancers.splice(realIndex, 1);
// remove related routing rules
let rules = [];
newTemplateSettings.routing.rules.forEach((r) => {
if (!r.balancerTag || r.balancerTag != oldTag) {
rules.push(r);
}
});
newTemplateSettings.routing.rules = rules;
this.templateSettings = newTemplateSettings;
},
addReverse(){ addReverse(){
reverseModal.show({ reverseModal.show({
title: '{{ i18n "pages.xray.outbound.addReverse"}}', title: '{{ i18n "pages.xray.outbound.addReverse"}}',
@@ -1272,66 +901,6 @@
this.templateSettings = newTemplateSettings; this.templateSettings = newTemplateSettings;
}, },
addDNSServer(){
dnsModal.show({
title: '{{ i18n "pages.xray.dns.add" }}',
confirm: (dnsServer) => {
dnsServers = this.dnsServers;
dnsServers.push(dnsServer);
this.dnsServers = dnsServers;
dnsModal.close();
},
isEdit: false
});
},
editDNSServer(index){
dnsModal.show({
title: '{{ i18n "pages.xray.dns.edit" }} #' + (index+1),
dnsServer: this.dnsServers[index],
confirm: (dnsServer) => {
dnsServers = this.dnsServers;
dnsServers[index] = dnsServer;
this.dnsServers = dnsServers;
dnsModal.close();
},
isEdit: true
});
},
deleteDNSServer(index){
newDnsServers = this.dnsServers;
newDnsServers.splice(index,1);
this.dnsServers = newDnsServers;
},
addFakedns() {
fakednsModal.show({
title: '{{ i18n "pages.xray.fakedns.add" }}',
confirm: (item) => {
fakeDns = this.fakeDns?? [];
fakeDns.push(item);
this.fakeDns = fakeDns;
fakednsModal.close();
},
isEdit: false
});
},
editFakedns(index){
fakednsModal.show({
title: '{{ i18n "pages.xray.fakedns.edit" }} #' + (index+1),
fakeDns: this.fakeDns[index],
confirm: (item) => {
fakeDns = this.fakeDns;
fakeDns[index] = item;
this.fakeDns = fakeDns;
fakednsModal.close();
},
isEdit: true
});
},
deleteFakedns(index){
fakeDns = this.fakeDns;
fakeDns.splice(index,1);
this.fakeDns = fakeDns;
},
addRule(){ addRule(){
ruleModal.show({ ruleModal.show({
title: '{{ i18n "pages.xray.rules.add"}}', title: '{{ i18n "pages.xray.rules.add"}}',
@@ -1378,12 +947,8 @@
} }
}, },
async mounted() { async mounted() {
if (window.location.protocol !== "https:") {
this.showAlert = true;
}
await this.getXraySetting(); await this.getXraySetting();
await this.getXrayResult(); await this.getXrayResult();
await this.getOutboundsTraffic();
while (true) { while (true) {
await PromiseUtil.sleep(800); await PromiseUtil.sleep(800);
this.saveBtnDisable = this.oldXraySetting === this.xraySetting; this.saveBtnDisable = this.oldXraySetting === this.xraySetting;
@@ -1439,27 +1004,6 @@
return data; return data;
}, },
}, },
balancersData: {
get: function () {
data = []
if (this.templateSettings != null && this.templateSettings.routing != null && this.templateSettings.routing.balancers != null) {
this.templateSettings.routing.balancers.forEach((o, index) => {
let strategy = "random"
if (o.strategy && o.strategy.type == "roundRobin") {
strategy = o.strategy.type
}
data.push({
'key': index,
'tag': o.tag ? o.tag : "",
'strategy': strategy,
'selector': o.selector ? o.selector : []
});
});
}
return data;
}
},
routingRuleSettings: { routingRuleSettings: {
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; }, get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
set: function (newValue) { set: function (newValue) {
@@ -1521,39 +1065,6 @@
this.templateSettings = newTemplateSettings; this.templateSettings = newTemplateSettings;
} }
}, },
setLogLevel: {
get: function () {
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.loglevel) return "warning";
return this.templateSettings.log.loglevel;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.log.loglevel = newValue;
this.templateSettings = newTemplateSettings;
}
},
accessLog: {
get: function () {
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.access) return "";
return this.templateSettings.log.access;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.log.access = newValue;
this.templateSettings = newTemplateSettings;
}
},
errorLog: {
get: function () {
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.error) return "";
return this.templateSettings.log.error;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.log.error = newValue;
this.templateSettings = newTemplateSettings;
}
},
blockedIPs: { blockedIPs: {
get: function () { get: function () {
return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" }); return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
@@ -1676,14 +1187,14 @@
familyProtectSettings: { familyProtectSettings: {
get: function () { get: function () {
if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false; if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false;
return doAllItemsExist(this.settingsData.familyProtectDNS.servers, this.templateSettings.dns.servers); return doAllItemsExist(this.templateSettings.dns.servers, this.settingsData.familyProtectDNS.servers);
}, },
set: function (newValue) { set: function (newValue) {
newTemplateSettings = this.templateSettings; newTemplateSettings = this.templateSettings;
if (newValue) { if (newValue) {
newTemplateSettings.dns = this.settingsData.familyProtectDNS; newTemplateSettings.dns = this.settingsData.familyProtectDNS;
} else { } else {
newTemplateSettings.dns.servers = newTemplateSettings.dns?.servers?.filter(data => !this.settingsData.familyProtectDNS.servers.includes(data)) delete newTemplateSettings.dns;
} }
this.templateSettings = newTemplateSettings; this.templateSettings = newTemplateSettings;
}, },
@@ -1945,42 +1456,6 @@
} }
}, },
}, },
MetaWARPSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.meta, this.warpDomains);
},
set: function (newValue) {
if (newValue) {
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.meta];
} else {
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.meta.includes(data));
}
},
},
AppleWARPSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.apple, this.warpDomains);
},
set: function (newValue) {
if (newValue) {
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.apple];
} else {
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.apple.includes(data));
}
},
},
RedditWARPSettings: {
get: function () {
return doAllItemsExist(this.settingsData.domains.reddit, this.warpDomains);
},
set: function (newValue) {
if (newValue) {
this.warpDomains = [...this.warpDomains, ...this.settingsData.domains.reddit];
} else {
this.warpDomains = this.warpDomains.filter(data => !this.settingsData.domains.reddit.includes(data));
}
},
},
SpotifyWARPSettings: { SpotifyWARPSettings: {
get: function () { get: function () {
return doAllItemsExist(this.settingsData.domains.spotify, this.warpDomains); return doAllItemsExist(this.settingsData.domains.spotify, this.warpDomains);
@@ -1993,42 +1468,6 @@
} }
}, },
}, },
enableDNS: {
get: function () {
return this.templateSettings ? this.templateSettings.dns != null : false;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.dns = newValue ? { servers: [], queryStrategy: "UseIP" } : null;
this.templateSettings = newTemplateSettings;
}
},
dnsStrategy: {
get: function () {
return this.enableDNS ? this.templateSettings.dns.queryStrategy : null;
},
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.dns.queryStrategy = newValue;
this.templateSettings = newTemplateSettings;
}
},
dnsServers: {
get: function () { return this.enableDNS ? this.templateSettings.dns.servers : []; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.dns.servers = newValue;
this.templateSettings = newTemplateSettings;
}
},
fakeDns: {
get: function () { return this.templateSettings && this.templateSettings.fakedns ? this.templateSettings.fakedns : []; },
set: function (newValue) {
newTemplateSettings = this.templateSettings;
newTemplateSettings.fakedns = newValue.length >0 ? newValue : null;
this.templateSettings = newTemplateSettings;
}
}
}, },
}); });
</script> </script>

View File

@@ -1,113 +0,0 @@
{{define "balancerModal"}}
<a-modal
id="balancer-modal"
v-model="balancerModal.visible"
:title="balancerModal.title"
@ok="balancerModal.ok"
:confirm-loading="balancerModal.confirmLoading"
:ok-button-props="{ props: { disabled: !balancerModal.isValid } }"
:closable="true"
:mask-closable="false"
:ok-text="balancerModal.okText"
cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.balancer.tag" }}' has-feedback
:validate-status="balancerModal.duplicateTag? 'warning' : 'success'">
<a-input v-model.trim="balancerModal.balancer.tag" @change="balancerModal.check()"
placeholder='{{ i18n "pages.xray.balancer.tagDesc" }}'></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.balancer.balancerStrategy" }}'>
<a-select v-model="balancerModal.balancer.strategy" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="random">Random</a-select-option>
<a-select-option value="roundRobin">Round Robin</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.balancer.balancerSelectors" }}' has-feedback
:validate-status="balancerModal.emptySelector? 'warning' : 'success'">
<a-select v-model="balancerModal.balancer.selector" mode="tags" @change="balancerModal.checkSelector()"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in balancerModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
</a-select>
</a-form-item>
</table>
</a-form>
</a-modal>
<script>
const balancerModal = {
title: '',
visible: false,
confirmLoading: false,
okText: '{{ i18n "sure" }}',
isEdit: false,
confirm: null,
duplicateTag: false,
emptySelector: false,
balancer: {
tag: '',
strategy: 'random',
selector: []
},
outboundTags: [],
balancerTags:[],
ok() {
if (balancerModal.balancer.selector.length == 0) {
balancerModal.emptySelector = true;
return;
}
balancerModal.emptySelector = false;
ObjectUtil.execute(balancerModal.confirm, balancerModal.balancer);
},
show({ title = '', okText = '{{ i18n "sure" }}', balancerTags = [], balancer, confirm = (balancer) => { }, isEdit = false }) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if (isEdit) {
balancerModal.balancer = balancer;
} else {
balancerModal.balancer = {
tag: '',
strategy: 'random',
selector: []
};
}
this.balancerTags = balancerTags.filter((tag) => tag != balancer.tag);
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
this.isEdit = isEdit;
this.check();
this.checkSelector();
},
close() {
this.visible = false;
this.loading(false);
},
loading(loading=true) {
this.confirmLoading = loading;
},
check() {
if (this.balancer.tag == '' || this.balancerTags.includes(this.balancer.tag)) {
this.duplicateTag = true;
this.isValid = false;
} else {
this.duplicateTag = false;
this.isValid = true;
}
},
checkSelector() {
this.emptySelector = this.balancer.selector.length == 0;
}
};
new Vue({
delimiters: ['[[', ']]'],
el: '#balancer-modal',
data: {
balancerModal: balancerModal
},
methods: {
}
});
</script>
{{end}}

View File

@@ -42,7 +42,7 @@
outModal.visible = false; outModal.visible = false;
outModal.loading(false); outModal.loading(false);
}, },
loading(loading=true) { loading(loading) {
outModal.confirmLoading = loading; outModal.confirmLoading = loading;
}, },
check(){ check(){

View File

@@ -120,7 +120,7 @@
reverseModal.visible = false; reverseModal.visible = false;
reverseModal.loading(false); reverseModal.loading(false);
}, },
loading(loading=true) { loading(loading) {
reverseModal.confirmLoading = loading; reverseModal.confirmLoading = loading;
}, },
}; };
@@ -132,6 +132,8 @@
reverseModal: reverseModal, reverseModal: reverseModal,
reverseTypes: { bridge: '{{ i18n "pages.xray.outbound.bridge" }}', portal:'{{ i18n "pages.xray.outbound.portal" }}'}, reverseTypes: { bridge: '{{ i18n "pages.xray.outbound.bridge" }}', portal:'{{ i18n "pages.xray.outbound.portal" }}'},
}, },
methods: {
}
}); });
</script> </script>

View File

@@ -20,7 +20,7 @@
<a-input v-model.trim="ruleModal.rule.source"></a-input> <a-input v-model.trim="ruleModal.rule.source"></a-input>
</a-form-item> </a-form-item>
<a-form-item> <a-form-item>
<template slot="label"> <template slot="label">Source Port
<a-tooltip> <a-tooltip>
<template slot="title"> <template slot="title">
<span>{{ i18n "pages.xray.rules.useComma" }}</span> <span>{{ i18n "pages.xray.rules.useComma" }}</span>
@@ -107,19 +107,6 @@
<a-select-option v-for="tag in ruleModal.outboundTags" :value="tag">[[ tag ]]</a-select-option> <a-select-option v-for="tag in ruleModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
</a-select> </a-select>
</a-form-item> </a-form-item>
<a-form-item>
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "pages.xray.balancer.balancerDesc" }}</span>
</template>
Balancer Tag <a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-select v-model="ruleModal.rule.balancerTag" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in ruleModal.balancerTags" :value="tag">[[ tag ]]</a-select-option>
</a-select>
</a-form-item>
</table> </table>
</a-form> </a-form>
</a-modal> </a-modal>
@@ -146,12 +133,11 @@
protocol: [], protocol: [],
attrs: [], attrs: [],
outboundTag: "", outboundTag: "",
balancerTag: "",
}, },
inboundTags: [], inboundTags: [],
outboundTags: [], outboundTags: [],
users: [], users: [],
balancerTags: [], balancerTag: [],
ok() { ok() {
newRule = ruleModal.getResult(); newRule = ruleModal.getResult();
ObjectUtil.execute(ruleModal.confirm, newRule); ObjectUtil.execute(ruleModal.confirm, newRule);
@@ -174,7 +160,6 @@
this.rule.protocol = rule.protocol; this.rule.protocol = rule.protocol;
this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : []; this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : [];
this.rule.outboundTag = rule.outboundTag; this.rule.outboundTag = rule.outboundTag;
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
} else { } else {
this.rule = { this.rule = {
domainMatcher: "", domainMatcher: "",
@@ -189,29 +174,24 @@
protocol: [], protocol: [],
attrs: [], attrs: [],
outboundTag: "", outboundTag: "",
balancerTag: "",
} }
} }
this.isEdit = isEdit; this.isEdit = isEdit;
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag); this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
this.inboundTags.push(...app.inboundTags); this.inboundTags.push(...app.inboundTags);
this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)]; this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
if(app.templateSettings.reverse){ if(app.templateSettings.reverse){
if(app.templateSettings.reverse.bridges) { if(app.templateSettings.reverse.bridges) {
this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag)); this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));
} }
if(app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag)); if(app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
} }
if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
this.balancerTags = [ "", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
}
}, },
close() { close() {
ruleModal.visible = false; ruleModal.visible = false;
ruleModal.loading(false); ruleModal.loading(false);
}, },
loading(loading=true) { loading(loading) {
ruleModal.confirmLoading = loading; ruleModal.confirmLoading = loading;
}, },
getResult() { getResult() {
@@ -230,8 +210,7 @@
rule.inboundTag = value.inboundTag; rule.inboundTag = value.inboundTag;
rule.protocol = value.protocol; rule.protocol = value.protocol;
rule.attrs = Object.fromEntries(value.attrs); rule.attrs = Object.fromEntries(value.attrs);
rule.outboundTag = value.outboundTag == "" ? undefined : value.outboundTag; rule.outboundTag = value.outboundTag;
rule.balancerTag = value.balancerTag == "" ? undefined : value.balancerTag;
for (const [key, value] of Object.entries(rule)) { for (const [key, value] of Object.entries(rule)) {
if ( if (

View File

@@ -1,9 +1,7 @@
package job package job
import ( import (
"bufio"
"encoding/json" "encoding/json"
"io"
"log" "log"
"os" "os"
"os/exec" "os/exec"
@@ -14,7 +12,6 @@ import (
"x-ui/database" "x-ui/database"
"x-ui/database/model" "x-ui/database/model"
"x-ui/config"
"x-ui/logger" "x-ui/logger"
"x-ui/xray" "x-ui/xray"
) )
@@ -26,6 +23,7 @@ type CheckClientIpJob struct {
var job *CheckClientIpJob var job *CheckClientIpJob
var ipFiles = []string{ var ipFiles = []string{
xray.GetIPLimitLogPath(), xray.GetIPLimitLogPath(),
xray.GetIPLimitPrevLogPath(),
xray.GetIPLimitBannedLogPath(), xray.GetIPLimitBannedLogPath(),
xray.GetIPLimitBannedPrevLogPath(), xray.GetIPLimitBannedPrevLogPath(),
xray.GetAccessPersistentLogPath(), xray.GetAccessPersistentLogPath(),
@@ -39,10 +37,8 @@ func NewCheckClientIpJob() *CheckClientIpJob {
func (j *CheckClientIpJob) Run() { func (j *CheckClientIpJob) Run() {
// create files and dirs required for iplimit if not exists // create files required for iplimit if not exists
for i := 0; i < len(ipFiles); i++ { for i := 0; i < len(ipFiles); i++ {
err := os.MkdirAll(config.GetLogFolder(), 0770)
j.checkError(err)
file, err := os.OpenFile(ipFiles[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644) file, err := os.OpenFile(ipFiles[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
j.checkError(err) j.checkError(err)
defer file.Close() defer file.Close()
@@ -53,37 +49,6 @@ func (j *CheckClientIpJob) Run() {
j.checkFail2BanInstalled() j.checkFail2BanInstalled()
j.processLogFile() j.processLogFile()
} }
if !j.hasLimitIp() && xray.GetAccessLogPath() == "./access.log" {
go j.clearLogTime()
}
}
func (j *CheckClientIpJob) clearLogTime() {
for {
time.Sleep(time.Hour)
j.clearAccessLog()
}
}
func (j *CheckClientIpJob) clearAccessLog() {
accessLogPath := xray.GetAccessLogPath()
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
j.checkError(err)
defer logAccessP.Close()
// reopen the access log file for reading
file, err := os.Open(accessLogPath)
j.checkError(err)
defer file.Close()
// copy access log content to persistent file
_, err = io.Copy(logAccessP, file)
j.checkError(err)
// clean access log
err = os.Truncate(accessLogPath, 0)
j.checkError(err)
} }
func (j *CheckClientIpJob) hasLimitIp() bool { func (j *CheckClientIpJob) hasLimitIp() bool {
@@ -127,34 +92,24 @@ func (j *CheckClientIpJob) checkFail2BanInstalled() {
func (j *CheckClientIpJob) processLogFile() { func (j *CheckClientIpJob) processLogFile() {
accessLogPath := xray.GetAccessLogPath() accessLogPath := xray.GetAccessLogPath()
if accessLogPath == "none" {
logger.Warning("Access log is set to 'none' check your Xray Configs")
return
}
if accessLogPath == "" { if accessLogPath == "" {
logger.Warning("Access log doesn't exist in your Xray Configs") logger.Warning("access.log doesn't exist in your config.json")
return return
} }
file, err := os.Open(accessLogPath) data, err := os.ReadFile(accessLogPath)
j.checkError(err)
defer file.Close()
InboundClientIps := make(map[string][]string) InboundClientIps := make(map[string][]string)
j.checkError(err)
scanner := bufio.NewScanner(file) lines := strings.Split(string(data), "\n")
for scanner.Scan() { for _, line := range lines {
line := scanner.Text() ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
ipRegx, _ := regexp.Compile(`(\d+\.\d+\.\d+\.\d+).* accepted`)
emailRegx, _ := regexp.Compile(`email:.+`) emailRegx, _ := regexp.Compile(`email:.+`)
matches := ipRegx.FindStringSubmatch(line) matchesIp := ipRegx.FindString(line)
if len(matches) > 1 { if len(matchesIp) > 0 {
ip := matches[1] ip := string(matchesIp)
if ip == "127.0.0.1" { if ip == "127.0.0.1" || ip == "1.1.1.1" {
continue continue
} }
@@ -169,14 +124,13 @@ func (j *CheckClientIpJob) processLogFile() {
continue continue
} }
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip) InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
} else { } else {
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip) InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
} }
} }
} }
j.checkError(scanner.Err())
shouldCleanLog := false shouldCleanLog := false
for clientEmail, ips := range InboundClientIps { for clientEmail, ips := range InboundClientIps {
@@ -187,13 +141,27 @@ func (j *CheckClientIpJob) processLogFile() {
} else { } else {
shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips) shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
} }
} }
// added delay before cleaning logs to reduce chance of logging IP that already has been banned // added delay before cleaning logs to reduce chance of logging IP that already has been banned
time.Sleep(time.Second * 2) time.Sleep(time.Second * 2)
if shouldCleanLog { if shouldCleanLog {
j.clearAccessLog() // copy access log to persistent file
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
j.checkError(err)
input, err := os.ReadFile(accessLogPath)
j.checkError(err)
if _, err := logAccessP.Write(input); err != nil {
j.checkError(err)
}
defer logAccessP.Close()
// clean access log
if err := os.Truncate(xray.GetAccessLogPath(), 0); err != nil {
j.checkError(err)
}
} }
} }

View File

@@ -15,7 +15,7 @@ func NewClearLogsJob() *ClearLogsJob {
// Here Run is an interface method of the Job interface // Here Run is an interface method of the Job interface
func (j *ClearLogsJob) Run() { func (j *ClearLogsJob) Run() {
logFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()} logFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()}
logFilesPrev := []string{xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()} logFilesPrev := []string{xray.GetIPLimitPrevLogPath(), xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()}
// clear old previous logs // clear old previous logs
for i := 0; i < len(logFilesPrev); i++ { for i := 0; i < len(logFilesPrev); i++ {
@@ -26,26 +26,25 @@ func (j *ClearLogsJob) Run() {
// clear log files and copy to previous logs // clear log files and copy to previous logs
for i := 0; i < len(logFiles); i++ { for i := 0; i < len(logFiles); i++ {
if i > 0 {
// copy to previous logs
logFilePrev, err := os.OpenFile(logFilesPrev[i-1], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
if err != nil {
logger.Warning("clear logs job err:", err)
}
logFile, err := os.ReadFile(logFiles[i]) // copy to previous logs
if err != nil { logFilePrev, err := os.OpenFile(logFilesPrev[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
logger.Warning("clear logs job err:", err) if err != nil {
} logger.Warning("clear logs job err:", err)
_, err = logFilePrev.Write(logFile)
if err != nil {
logger.Warning("clear logs job err:", err)
}
defer logFilePrev.Close()
} }
err := os.Truncate(logFiles[i], 0) logFile, err := os.ReadFile(logFiles[i])
if err != nil {
logger.Warning("clear logs job err:", err)
}
_, err = logFilePrev.Write(logFile)
if err != nil {
logger.Warning("clear logs job err:", err)
}
defer logFilePrev.Close()
err = os.Truncate(logFiles[i], 0)
if err != nil { if err != nil {
logger.Warning("clear logs job err:", err) logger.Warning("clear logs job err:", err)
} }

View File

@@ -6,9 +6,8 @@ import (
) )
type XrayTrafficJob struct { type XrayTrafficJob struct {
xrayService service.XrayService xrayService service.XrayService
inboundService service.InboundService inboundService service.InboundService
outboundService service.OutboundService
} }
func NewXrayTrafficJob() *XrayTrafficJob { func NewXrayTrafficJob() *XrayTrafficJob {
@@ -25,15 +24,11 @@ func (j *XrayTrafficJob) Run() {
logger.Warning("get xray traffic failed:", err) logger.Warning("get xray traffic failed:", err)
return return
} }
err, needRestart0 := j.inboundService.AddTraffic(traffics, clientTraffics) err, needRestart := j.inboundService.AddTraffic(traffics, clientTraffics)
if err != nil { if err != nil {
logger.Warning("add inbound traffic failed:", err) logger.Warning("add traffic failed:", err)
} }
err, needRestart1 := j.outboundService.AddTraffic(traffics, clientTraffics) if needRestart {
if err != nil {
logger.Warning("add outbound traffic failed:", err)
}
if needRestart0 || needRestart1 {
j.xrayService.SetToNeedRestart() j.xrayService.SetToNeedRestart()
} }

View File

@@ -1,9 +1,7 @@
{ {
"log": { "log": {
"access": "none", "loglevel": "warning",
"dnsLog": false, "error": "./error.log"
"error": "./error.log",
"loglevel": "warning"
}, },
"api": { "api": {
"tag": "api", "tag": "api",
@@ -45,9 +43,7 @@
}, },
"system": { "system": {
"statsInboundDownlink": true, "statsInboundDownlink": true,
"statsInboundUplink": true, "statsInboundUplink": true
"statsOutboundDownlink": true,
"statsOutboundUplink": true
} }
}, },
"routing": { "routing": {

View File

@@ -312,7 +312,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
oldInbound.StreamSettings = inbound.StreamSettings oldInbound.StreamSettings = inbound.StreamSettings
oldInbound.Sniffing = inbound.Sniffing oldInbound.Sniffing = inbound.Sniffing
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" { if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port) oldInbound.Tag = fmt.Sprintf("inbound-0.0.0.0:%v", inbound.Port)
} else { } else {
oldInbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port) oldInbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
} }
@@ -682,7 +682,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
return needRestart, tx.Save(oldInbound).Error return needRestart, tx.Save(oldInbound).Error
} }
func (s *InboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) { func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
var err error var err error
db := database.GetDB() db := database.GetDB()
tx := db.Begin() tx := db.Begin()
@@ -694,7 +694,7 @@ func (s *InboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*
tx.Commit() tx.Commit()
} }
}() }()
err = s.addInboundTraffic(tx, traffics) err = s.addInboundTraffic(tx, inboundTraffics)
if err != nil { if err != nil {
return err, false return err, false
} }
@@ -1900,13 +1900,6 @@ func (s *InboundService) MigrationRequirements() {
newStream, _ := json.MarshalIndent(stream, " ", " ") newStream, _ := json.MarshalIndent(stream, " ", " ")
tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream) tx.Model(model.Inbound{}).Where("id = ?", ep.Id).Update("stream_settings", newStream)
} }
err = tx.Raw(`UPDATE inbounds
SET tag = REPLACE(tag, '0.0.0.0:', '')
WHERE INSTR(tag, '0.0.0.0:') > 0;`).Error
if err != nil {
return
}
} }
func (s *InboundService) MigrateDB() { func (s *InboundService) MigrateDB() {

View File

@@ -1,101 +0,0 @@
package service
import (
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
"x-ui/xray"
"gorm.io/gorm"
)
type OutboundService struct {
}
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
var err error
db := database.GetDB()
tx := db.Begin()
defer func() {
if err != nil {
tx.Rollback()
} else {
tx.Commit()
}
}()
err = s.addOutboundTraffic(tx, traffics)
if err != nil {
return err, false
}
return nil, false
}
func (s *OutboundService) addOutboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
if len(traffics) == 0 {
return nil
}
var err error
for _, traffic := range traffics {
if traffic.IsOutbound {
var outbound model.OutboundTraffics
err = tx.Model(&model.OutboundTraffics{}).Where("tag = ?", traffic.Tag).
FirstOrCreate(&outbound).Error
if err != nil {
return err
}
outbound.Tag = traffic.Tag
outbound.Up = outbound.Up + traffic.Up
outbound.Down = outbound.Down + traffic.Down
outbound.Total = outbound.Up + outbound.Down
err = tx.Save(&outbound).Error
if err != nil {
return err
}
}
}
return nil
}
func (s *OutboundService) GetOutboundsTraffic() ([]*model.OutboundTraffics, error) {
db := database.GetDB()
var traffics []*model.OutboundTraffics
err := db.Model(model.OutboundTraffics{}).Find(&traffics).Error
if err != nil {
logger.Warning(err)
return nil, err
}
return traffics, nil
}
func (s *OutboundService) ResetOutboundTraffic(tag string) error {
db := database.GetDB()
whereText := "tag "
if tag == "-alltags-" {
whereText += " <> ?"
} else {
whereText += " = ?"
}
result := db.Model(model.OutboundTraffics{}).
Where(whereText, tag).
Updates(map[string]interface{}{"up": 0, "down": 0, "total": 0})
err := result.Error
if err != nil {
return err
}
return nil
}

View File

@@ -57,9 +57,6 @@ var defaultValueMap = map[string]string{
"subEncrypt": "true", "subEncrypt": "true",
"subShowInfo": "true", "subShowInfo": "true",
"subURI": "", "subURI": "",
"subJsonPath": "/json/",
"subJsonURI": "",
"subJsonFragment": "",
"datepicker": "gregorian", "datepicker": "gregorian",
"warp": "", "warp": "",
} }
@@ -390,11 +387,17 @@ func (s *SettingService) GetSubPort() (int, error) {
} }
func (s *SettingService) GetSubPath() (string, error) { func (s *SettingService) GetSubPath() (string, error) {
return s.getString("subPath") subPath, err := s.getString("subPath")
} if err != nil {
return "", err
func (s *SettingService) GetSubJsonPath() (string, error) { }
return s.getString("subJsonPath") if !strings.HasPrefix(subPath, "/") {
subPath = "/" + subPath
}
if !strings.HasSuffix(subPath, "/") {
subPath += "/"
}
return subPath, nil
} }
func (s *SettingService) GetSubDomain() (string, error) { func (s *SettingService) GetSubDomain() (string, error) {
@@ -409,8 +412,8 @@ func (s *SettingService) GetSubKeyFile() (string, error) {
return s.getString("subKeyFile") return s.getString("subKeyFile")
} }
func (s *SettingService) GetSubUpdates() (string, error) { func (s *SettingService) GetSubUpdates() (int, error) {
return s.getString("subUpdates") return s.getInt("subUpdates")
} }
func (s *SettingService) GetSubEncrypt() (bool, error) { func (s *SettingService) GetSubEncrypt() (bool, error) {
@@ -429,14 +432,6 @@ func (s *SettingService) GetSubURI() (string, error) {
return s.getString("subURI") return s.getString("subURI")
} }
func (s *SettingService) GetSubJsonURI() (string, error) {
return s.getString("subJsonURI")
}
func (s *SettingService) GetSubJsonFragment() (string, error) {
return s.getString("subJsonFragment")
}
func (s *SettingService) GetDatepicker() (string, error) { func (s *SettingService) GetDatepicker() (string, error) {
return s.getString("datepicker") return s.getString("datepicker")
} }
@@ -489,7 +484,6 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() }, "tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() },
"subEnable": func() (interface{}, error) { return s.GetSubEnable() }, "subEnable": func() (interface{}, error) { return s.GetSubEnable() },
"subURI": func() (interface{}, error) { return s.GetSubURI() }, "subURI": func() (interface{}, error) { return s.GetSubURI() },
"subJsonURI": func() (interface{}, error) { return s.GetSubJsonURI() },
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() }, "remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
"datepicker": func() (interface{}, error) { return s.GetDatepicker() }, "datepicker": func() (interface{}, error) { return s.GetDatepicker() },
} }
@@ -504,11 +498,10 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
result[key] = value result[key] = value
} }
if result["subEnable"].(bool) && (result["subURI"].(string) == "" || result["subJsonURI"].(string) == "") { if result["subEnable"].(bool) && result["subURI"].(string) == "" {
subURI := "" subURI := ""
subPort, _ := s.GetSubPort() subPort, _ := s.GetSubPort()
subPath, _ := s.GetSubPath() subPath, _ := s.GetSubPath()
subJsonPath, _ := s.GetSubJsonPath()
subDomain, _ := s.GetSubDomain() subDomain, _ := s.GetSubDomain()
subKeyFile, _ := s.GetSubKeyFile() subKeyFile, _ := s.GetSubKeyFile()
subCertFile, _ := s.GetSubCertFile() subCertFile, _ := s.GetSubCertFile()
@@ -529,12 +522,12 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
} else { } else {
subURI += fmt.Sprintf("%s:%d", subDomain, subPort) subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
} }
if result["subURI"].(string) == "" { if subPath[0] == byte('/') {
result["subURI"] = subURI + subPath subURI += subPath
} } else {
if result["subJsonURI"].(string) == "" { subURI += "/" + subPath
result["subJsonURI"] = subURI + subJsonPath
} }
result["subURI"] = subURI
} }
return result, nil return result, nil

View File

@@ -6,9 +6,9 @@ import (
"net" "net"
"net/url" "net/url"
"os" "os"
"slices"
"strconv" "strconv"
"strings" "strings"
"slices"
"time" "time"
"x-ui/config" "x-ui/config"
"x-ui/database" "x-ui/database"
@@ -115,19 +115,14 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
} }
func (t *Tgbot) NewBot(token string, proxyUrl string) (*telego.Bot, error) { func (t *Tgbot) NewBot(token string, proxyUrl string) (*telego.Bot, error) {
if proxyUrl == "" { if proxyUrl == "" || !strings.HasPrefix(proxyUrl, "socks5://") {
// No proxy URL provided, use default instance logger.Warning("invalid socks5 url, start with default")
return telego.NewBot(token)
}
if !strings.HasPrefix(proxyUrl, "socks5://") {
logger.Warning("Invalid socks5 URL, starting with default")
return telego.NewBot(token) return telego.NewBot(token)
} }
_, err := url.Parse(proxyUrl) _, err := url.Parse(proxyUrl)
if err != nil { if err != nil {
logger.Warning("Can't parse proxy URL, using default instance for tgbot:", err) logger.Warning("cant parse proxy url, use default instance for tgbot:", err)
return telego.NewBot(token) return telego.NewBot(token)
} }
@@ -202,13 +197,9 @@ func (t *Tgbot) OnReceive() {
}, th.AnyCallbackQueryWithMessage()) }, th.AnyCallbackQueryWithMessage())
botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) { botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) {
if message.UsersShared != nil { if message.UserShared != nil {
if checkAdmin(message.From.ID) { if checkAdmin(message.From.ID) {
userIDsStr := "" err := t.inboundService.SetClientTelegramUserID(message.UserShared.RequestID, strconv.FormatInt(message.UserShared.UserID, 10))
for _, userID := range message.UsersShared.UserIDs {
userIDsStr += strconv.FormatInt(userID, 10) + " "
}
err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userIDsStr)
output := "" output := ""
if err != nil { if err != nil {
output += t.I18nBot("tgbot.messages.selectUserFailed") output += t.I18nBot("tgbot.messages.selectUserFailed")
@@ -269,7 +260,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
msg += t.I18nBot("tgbot.commands.unknown") msg += t.I18nBot("tgbot.commands.unknown")
} }
if msg != "" { if msg != ""{
if onlyMessage { if onlyMessage {
t.SendMsgToTgbot(chatId, msg) t.SendMsgToTgbot(chatId, msg)
return return
@@ -281,7 +272,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) { func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
chatId := callbackQuery.Message.GetChat().ID chatId := callbackQuery.Message.Chat.ID
if isAdmin { if isAdmin {
// get query from hash storage // get query from hash storage
@@ -300,22 +291,22 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
t.searchClient(chatId, email) t.searchClient(chatId, email)
case "client_refresh": case "client_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clientRefreshSuccess", "Email=="+email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clientRefreshSuccess", "Email=="+email))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
case "client_cancel": case "client_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
case "ips_refresh": case "ips_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.IpRefreshSuccess", "Email=="+email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.IpRefreshSuccess", "Email=="+email))
t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
case "ips_cancel": case "ips_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
case "tgid_refresh": case "tgid_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.TGIdRefreshSuccess", "Email=="+email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.TGIdRefreshSuccess", "Email=="+email))
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID()) t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
case "tgid_cancel": case "tgid_cancel":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID()) t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
case "reset_traffic": case "reset_traffic":
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@@ -325,13 +316,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmResetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic_c "+email)), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmResetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic_c "+email)),
), ),
) )
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
case "reset_traffic_c": case "reset_traffic_c":
err := t.inboundService.ResetClientTrafficByEmail(email) err := t.inboundService.ResetClientTrafficByEmail(email)
if err == nil { if err == nil {
t.xrayService.SetToNeedRestart() t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetTrafficSuccess", "Email=="+email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetTrafficSuccess", "Email=="+email))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
} else { } else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
} }
@@ -355,7 +346,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton("40 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 40")), tu.InlineKeyboardButton("40 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 40")),
), ),
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 50")), tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")),
tu.InlineKeyboardButton("60 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")), tu.InlineKeyboardButton("60 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")),
tu.InlineKeyboardButton("80 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 80")), tu.InlineKeyboardButton("80 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 80")),
), ),
@@ -365,7 +356,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton("200 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 200")), tu.InlineKeyboardButton("200 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 200")),
), ),
) )
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
case "limit_traffic_c": case "limit_traffic_c":
if len(dataArray) == 3 { if len(dataArray) == 3 {
limitTraffic, err := strconv.Atoi(dataArray[2]) limitTraffic, err := strconv.Atoi(dataArray[2])
@@ -374,13 +365,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if err == nil { if err == nil {
t.xrayService.SetToNeedRestart() t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.setTrafficLimitSuccess", "Email=="+email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.setTrafficLimitSuccess", "Email=="+email))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
return return
} }
} }
} }
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
case "limit_traffic_in": case "limit_traffic_in":
if len(dataArray) >= 3 { if len(dataArray) >= 3 {
oldInputNumber, err := strconv.Atoi(dataArray[2]) oldInputNumber, err := strconv.Atoi(dataArray[2])
@@ -436,12 +427,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" -1")), tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" -1")),
), ),
) )
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
return return
} }
} }
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
case "reset_exp": case "reset_exp":
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@@ -468,7 +459,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 12 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 365")), tu.InlineKeyboardButton(t.I18nBot("tgbot.add")+" 12 "+t.I18nBot("tgbot.months")).WithCallbackData(t.encodeQuery("reset_exp_c "+email+" 365")),
), ),
) )
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
case "reset_exp_c": case "reset_exp_c":
if len(dataArray) == 3 { if len(dataArray) == 3 {
days, err := strconv.Atoi(dataArray[2]) days, err := strconv.Atoi(dataArray[2])
@@ -503,13 +494,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if err == nil { if err == nil {
t.xrayService.SetToNeedRestart() t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.expireResetSuccess", "Email=="+email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.expireResetSuccess", "Email=="+email))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
return return
} }
} }
} }
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
case "reset_exp_in": case "reset_exp_in":
if len(dataArray) >= 3 { if len(dataArray) >= 3 {
oldInputNumber, err := strconv.Atoi(dataArray[2]) oldInputNumber, err := strconv.Atoi(dataArray[2])
@@ -565,12 +556,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" -1")), tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" -1")),
), ),
) )
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
return return
} }
} }
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
case "ip_limit": case "ip_limit":
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@@ -599,7 +590,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton("10").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 10")), tu.InlineKeyboardButton("10").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 10")),
), ),
) )
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
case "ip_limit_c": case "ip_limit_c":
if len(dataArray) == 3 { if len(dataArray) == 3 {
count, err := strconv.Atoi(dataArray[2]) count, err := strconv.Atoi(dataArray[2])
@@ -608,13 +599,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
if err == nil { if err == nil {
t.xrayService.SetToNeedRestart() t.xrayService.SetToNeedRestart()
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetIpSuccess", "Email=="+email, "Count=="+strconv.Itoa(count))) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetIpSuccess", "Email=="+email, "Count=="+strconv.Itoa(count)))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
return return
} }
} }
} }
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
case "ip_limit_in": case "ip_limit_in":
if len(dataArray) >= 3 { if len(dataArray) >= 3 {
oldInputNumber, err := strconv.Atoi(dataArray[2]) oldInputNumber, err := strconv.Atoi(dataArray[2])
@@ -670,12 +661,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" -1")), tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" -1")),
), ),
) )
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
return return
} }
} }
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
case "clear_ips": case "clear_ips":
inlineKeyboard := tu.InlineKeyboard( inlineKeyboard := tu.InlineKeyboard(
tu.InlineKeyboardRow( tu.InlineKeyboardRow(
@@ -685,12 +676,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmClearIps")).WithCallbackData(t.encodeQuery("clear_ips_c "+email)), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmClearIps")).WithCallbackData(t.encodeQuery("clear_ips_c "+email)),
), ),
) )
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
case "clear_ips_c": case "clear_ips_c":
err := t.inboundService.ClearClientIps(email) err := t.inboundService.ClearClientIps(email)
if err == nil { if err == nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clearIpSuccess", "Email=="+email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clearIpSuccess", "Email=="+email))
t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
} else { } else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
} }
@@ -709,7 +700,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmRemoveTGUser")).WithCallbackData(t.encodeQuery("tgid_remove_c "+email)), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmRemoveTGUser")).WithCallbackData(t.encodeQuery("tgid_remove_c "+email)),
), ),
) )
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
case "tgid_remove_c": case "tgid_remove_c":
traffic, err := t.inboundService.GetClientTrafficByEmail(email) traffic, err := t.inboundService.GetClientTrafficByEmail(email)
if err != nil || traffic == nil { if err != nil || traffic == nil {
@@ -719,7 +710,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
err = t.inboundService.SetClientTelegramUserID(traffic.Id, "") err = t.inboundService.SetClientTelegramUserID(traffic.Id, "")
if err == nil { if err == nil {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.removedTGUserSuccess", "Email=="+email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.removedTGUserSuccess", "Email=="+email))
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID()) t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
} else { } else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
} }
@@ -732,7 +723,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmToggle")).WithCallbackData(t.encodeQuery("toggle_enable_c "+email)), tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmToggle")).WithCallbackData(t.encodeQuery("toggle_enable_c "+email)),
), ),
) )
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard) t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
case "toggle_enable_c": case "toggle_enable_c":
enabled, err := t.inboundService.ToggleClientEnableByEmail(email) enabled, err := t.inboundService.ToggleClientEnableByEmail(email)
if err == nil { if err == nil {
@@ -742,7 +733,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
} else { } else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.disableSuccess", "Email=="+email)) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.disableSuccess", "Email=="+email))
} }
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID()) t.searchClient(chatId, email, callbackQuery.Message.MessageID)
} else { } else {
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
} }
@@ -778,7 +769,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
t.onlineClients(chatId) t.onlineClients(chatId)
case "onlines_refresh": case "onlines_refresh":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
t.onlineClients(chatId, callbackQuery.Message.GetMessageID()) t.onlineClients(chatId, callbackQuery.Message.MessageID)
case "commands": case "commands":
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands")) t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands"))
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands")) t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands"))
@@ -1031,7 +1022,7 @@ func (t *Tgbot) getInboundUsages() string {
} }
func (t *Tgbot) clientInfoMsg(traffic *xray.ClientTraffic, printEnabled bool, printOnline bool, printActive bool, func (t *Tgbot) clientInfoMsg(traffic *xray.ClientTraffic, printEnabled bool, printOnline bool, printActive bool,
printDate bool, printTraffic bool, printRefreshed bool) string { printDate bool, printTraffic bool, printRefreshed bool) string {
now := time.Now().Unix() now := time.Now().Unix()
expiryTime := "" expiryTime := ""
@@ -1219,13 +1210,13 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard) t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
} else { } else {
t.SendMsgToTgbot(chatId, output, inlineKeyboard) t.SendMsgToTgbot(chatId, output, inlineKeyboard)
requestUser := telego.KeyboardButtonRequestUsers{ requestUser := telego.KeyboardButtonRequestUser{
RequestID: int32(traffic.Id), RequestID: int32(traffic.Id),
UserIsBot: new(bool), UserIsBot: new(bool),
} }
keyboard := tu.Keyboard( keyboard := tu.Keyboard(
tu.KeyboardRow( tu.KeyboardRow(
tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUsers(&requestUser), tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUser(&requestUser),
), ),
tu.KeyboardRow( tu.KeyboardRow(
tu.KeyboardButton(t.I18nBot("tgbot.buttons.closeKeyboard")), tu.KeyboardButton(t.I18nBot("tgbot.buttons.closeKeyboard")),
@@ -1390,6 +1381,7 @@ func (t *Tgbot) getExhausted(chatId int64) {
output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients))) output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC)) output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC))
if exhaustedCC > 0 { if exhaustedCC > 0 {
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients")) output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients"))
var buttons []telego.InlineKeyboardButton var buttons []telego.InlineKeyboardButton
@@ -1499,6 +1491,7 @@ func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow( keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow(
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh")))) tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh"))))
if onlinesCount > 0 { if onlinesCount > 0 {
var buttons []telego.InlineKeyboardButton var buttons []telego.InlineKeyboardButton
for _, online := range onlines { for _, online := range onlines {
@@ -1572,44 +1565,30 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
file, err := os.Open(xray.GetIPLimitBannedPrevLogPath()) file, err := os.Open(xray.GetIPLimitBannedPrevLogPath())
if err == nil { if err == nil {
// Check if the file is non-empty before attempting to upload document := tu.Document(
fileInfo, _ := file.Stat() tu.ID(chatId),
if fileInfo.Size() > 0 { tu.File(file),
document := tu.Document( )
tu.ID(chatId), _, err = bot.SendDocument(document)
tu.File(file), if err != nil {
) logger.Error("Error in uploading backup: ", err)
_, err = bot.SendDocument(document)
if err != nil {
logger.Error("Error in uploading IPLimitBannedPrevLog: ", err)
}
} else {
logger.Warning("IPLimitBannedPrevLog file is empty, not uploading.")
} }
file.Close()
} else { } else {
logger.Error("Error in opening IPLimitBannedPrevLog file for backup: ", err) logger.Error("Error in opening db file for backup: ", err)
} }
file, err = os.Open(xray.GetIPLimitBannedLogPath()) file, err = os.Open(xray.GetIPLimitBannedLogPath())
if err == nil { if err == nil {
// Check if the file is non-empty before attempting to upload document := tu.Document(
fileInfo, _ := file.Stat() tu.ID(chatId),
if fileInfo.Size() > 0 { tu.File(file),
document := tu.Document( )
tu.ID(chatId), _, err = bot.SendDocument(document)
tu.File(file), if err != nil {
) logger.Error("Error in uploading config.json: ", err)
_, err = bot.SendDocument(document)
if err != nil {
logger.Error("Error in uploading IPLimitBannedLog: ", err)
}
} else {
logger.Warning("IPLimitBannedLog file is empty, not uploading.")
} }
file.Close()
} else { } else {
logger.Error("Error in opening IPLimitBannedLog file for backup: ", err) logger.Error("Error in opening config.json file for backup: ", err)
} }
} }

View File

@@ -95,7 +95,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
if !clientTraffic.Enable { if !clientTraffic.Enable {
clients = RemoveIndex(clients, index-indexDecrease) clients = RemoveIndex(clients, index-indexDecrease)
indexDecrease++ indexDecrease++
logger.Info("Remove Inbound User ", c["email"], " due the expire or traffic limit") logger.Info("Remove Inbound User", c["email"], "due the expire or traffic limit")
} }

View File

@@ -52,9 +52,6 @@
"secretToken" = "Secret Token" "secretToken" = "Secret Token"
"remained" = "Remained" "remained" = "Remained"
"security" = "Security" "security" = "Security"
"secAlertTitle" = "Security Alert"
"secAlertSsl" = "This connection is not secure. Please avoid entering sensitive information until TLS is activated for data protection."
"secAlertConf" = "Certain configurations have been identified as susceptible to attacks, prompting immediate action to reinforce security protocols and safeguard against potential security breaches."
[menu] [menu]
"dashboard" = "Overview" "dashboard" = "Overview"
@@ -79,7 +76,7 @@
"title" = "Overview" "title" = "Overview"
"memory" = "RAM" "memory" = "RAM"
"hard" = "Disk" "hard" = "Disk"
"xrayStatus" = "Xray" "xrayStatus" = "Status"
"stopXray" = "Stop" "stopXray" = "Stop"
"restartXray" = "Restart" "restartXray" = "Restart"
"xraySwitch" = "Version" "xraySwitch" = "Version"
@@ -305,8 +302,6 @@
"subShowInfoDesc" = "The remaining traffic and date will be displayed in the client apps." "subShowInfoDesc" = "The remaining traffic and date will be displayed in the client apps."
"subURI" = "Reverse Proxy URI" "subURI" = "Reverse Proxy URI"
"subURIDesc" = "The URI path of the subscription URL for use behind proxies." "subURIDesc" = "The URI path of the subscription URL for use behind proxies."
"fragment" = "Fragmentation"
"fragmentDesc" = "Enable fragmentation for TLS hello packet"
[pages.xray] [pages.xray]
"title" = "Xray Configs" "title" = "Xray Configs"
@@ -314,10 +309,8 @@
"restart" = "Restart Xray" "restart" = "Restart Xray"
"basicTemplate" = "Basics" "basicTemplate" = "Basics"
"advancedTemplate" = "Advanced" "advancedTemplate" = "Advanced"
"generalConfigs" = "General" "generalConfigs" = "General Strategy"
"generalConfigsDesc" = "These options will determine general adjustments." "generalConfigsDesc" = "These options will determine general strategy adjustments."
"logConfigs" = "Log"
"logConfigsDesc" = "Logs may affect your server's efficiency. It is recommended to enable it wisely only in case of your needs"
"blockConfigs" = "Protection Shield" "blockConfigs" = "Protection Shield"
"blockConfigsDesc" = "These options will block traffic based on specific requested protocols and websites." "blockConfigsDesc" = "These options will block traffic based on specific requested protocols and websites."
"blockCountryConfigs" = "Block Country" "blockCountryConfigs" = "Block Country"
@@ -388,12 +381,6 @@
"OpenAIWARPDesc" = "Routes traffic to ChatGPT via WARP." "OpenAIWARPDesc" = "Routes traffic to ChatGPT via WARP."
"NetflixWARP" = "Netflix" "NetflixWARP" = "Netflix"
"NetflixWARPDesc" = "Routes traffic to Netflix via WARP." "NetflixWARPDesc" = "Routes traffic to Netflix via WARP."
"MetaWARP" = "Meta"
"MetaWARPDesc" = "Routes traffic to Meta (Instagram, Facebook, WhatsApp, Threads,...) via WARP."
"AppleWARP" = "Apple"
"AppleWARPDesc" = "Routes traffic to Apple via WARP."
"RedditWARP" = "Reddit"
"RedditWARPDesc" = "Routes traffic to Reddit via WARP."
"SpotifyWARP" = "Spotify" "SpotifyWARP" = "Spotify"
"SpotifyWARPDesc" = "Routes traffic to Spotify via WARP." "SpotifyWARPDesc" = "Routes traffic to Spotify via WARP."
"IRWARP" = "Iran domains" "IRWARP" = "Iran domains"
@@ -401,17 +388,10 @@
"Inbounds" = "Inbounds" "Inbounds" = "Inbounds"
"InboundsDesc" = "Accepting the specific clients." "InboundsDesc" = "Accepting the specific clients."
"Outbounds" = "Outbounds" "Outbounds" = "Outbounds"
"Balancers" = "Balancers"
"OutboundsDesc" = "Set the outgoing traffic pathway." "OutboundsDesc" = "Set the outgoing traffic pathway."
"Routings" = "Routing Rules" "Routings" = "Routing Rules"
"RoutingsDesc" = "The priority of each rule is important!" "RoutingsDesc" = "The priority of each rule is important!"
"completeTemplate" = "All" "completeTemplate" = "All"
"logLevel" = "Log Level"
"logLevelDesc" = "The log level for error logs, indicating the information that needs to be recorded."
"accessLog" = "Access Log"
"accessLogDesc" = "The file path for the access log. The special value 'none' disabled access logs"
"errorLog" = "Error Log"
"errorLogDesc" = "The file path for the error log. The special value 'none' disabled error logs"
[pages.xray.rules] [pages.xray.rules]
"first" = "First" "first" = "First"
@@ -422,7 +402,6 @@
"dest" = "Destination" "dest" = "Destination"
"inbound" = "Inbound" "inbound" = "Inbound"
"outbound" = "Outbound" "outbound" = "Outbound"
"balancer" = "Balancer"
"info" = "Info" "info" = "Info"
"add" = "Add Rule" "add" = "Add Rule"
"edit" = "Edit Rule" "edit" = "Edit Rule"
@@ -443,15 +422,6 @@
"portal" = "Portal" "portal" = "Portal"
"intercon" = "Interconnection" "intercon" = "Interconnection"
[pages.xray.balancer]
"addBalancer" = "Add Balancer"
"editBalancer" = "Edit Balancer"
"balancerStrategy" = "Strategy"
"balancerSelectors" = "Selectors"
"tag" = "Tag"
"tagDesc" = "Unique Tag"
"balancerDesc" = "It is not possible to use balancerTag and outboundTag at the same time. If used at the same time, only outboundTag will work."
[pages.xray.wireguard] [pages.xray.wireguard]
"secretKey" = "Secret Key" "secretKey" = "Secret Key"
"publicKey" = "Public Key" "publicKey" = "Public Key"
@@ -460,21 +430,6 @@
"psk" = "PreShared Key" "psk" = "PreShared Key"
"domainStrategy" = "Domain Strategy" "domainStrategy" = "Domain Strategy"
[pages.xray.dns]
"enable" = "Enable DNS"
"enableDesc" = "Enable built-in DNS server"
"strategy" = "Query Strategy"
"strategyDesc" = "Overall strategy to resolve domain names"
"add" = "Add Server"
"edit" = "Edit Server"
"domains" = "Domains"
[pages.xray.fakedns]
"add" = "Add Fake DNS"
"edit" = "Edit Fake DNS"
"ipPool" = "IP Pool Subnet"
"poolSize" = "Pool Size"
[pages.settings.security] [pages.settings.security]
"admin" = "Admin" "admin" = "Admin"
"secret" = "Secret Token" "secret" = "Secret Token"
@@ -497,7 +452,7 @@
"wentWrong" = "❌ Something went wrong!" "wentWrong" = "❌ Something went wrong!"
"noIpRecord" = "❗ No IP Record!" "noIpRecord" = "❗ No IP Record!"
"noInbounds" = "❗ No inbound found!" "noInbounds" = "❗ No inbound found!"
"unlimited" = "♾ Unlimited(Reset)" "unlimited" = "♾ Unlimited"
"add" = "Add" "add" = "Add"
"month" = "Month" "month" = "Month"
"months" = "Months" "months" = "Months"

View File

@@ -20,7 +20,7 @@
"check" = "Verificar" "check" = "Verificar"
"indefinite" = "Indefinido" "indefinite" = "Indefinido"
"unlimited" = "Ilimitado" "unlimited" = "Ilimitado"
"none" = "None" "none" = "Ninguno"
"qrCode" = "Código QR" "qrCode" = "Código QR"
"info" = "Más Información" "info" = "Más Información"
"edit" = "Editar" "edit" = "Editar"
@@ -52,15 +52,12 @@
"secretToken" = "Token Secreto" "secretToken" = "Token Secreto"
"remained" = "Restante" "remained" = "Restante"
"security" = "Seguridad" "security" = "Seguridad"
"secAlertTitle" = "Alerta de seguridad"
"secAlertSsl" = "Esta conexión no es segura. Evite ingresar información confidencial hasta que TLS esté activado para la protección de datos."
"secAlertConf" = "Se han identificado ciertas configuraciones como susceptibles a ataques, lo que genera acciones inmediatas para reforzar los protocolos de seguridad y proteger contra posibles violaciones de seguridad."
[menu] [menu]
"dashboard" = "Estado del Sistema" "dashboard" = "Estado del Sistema"
"inbounds" = "Entradas" "inbounds" = "Entradas"
"settings" = "Configuraciones" "settings" = "Configuraciones"
"xray" = "Ajustes Xray" "xray" = "Configuración Xray"
"logout" = "Cerrar Sesión" "logout" = "Cerrar Sesión"
"link" = "Gestionar" "link" = "Gestionar"
@@ -79,7 +76,7 @@
"title" = "Estado del Sistema" "title" = "Estado del Sistema"
"memory" = "Memoria" "memory" = "Memoria"
"hard" = "Disco Duro" "hard" = "Disco Duro"
"xrayStatus" = "Xray" "xrayStatus" = "Estado de"
"stopXray" = "Detener" "stopXray" = "Detener"
"restartXray" = "Reiniciar" "restartXray" = "Reiniciar"
"xraySwitch" = "Versión" "xraySwitch" = "Versión"
@@ -305,8 +302,6 @@
"subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración." "subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración."
"subURI" = "URI de proxy inverso" "subURI" = "URI de proxy inverso"
"subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy" "subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy"
"fragment" = "Fragmentación"
"fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo TLS"
[pages.xray] [pages.xray]
"title" = "Xray Configuración" "title" = "Xray Configuración"
@@ -316,8 +311,6 @@
"advancedTemplate" = "Plantilla Avanzada" "advancedTemplate" = "Plantilla Avanzada"
"generalConfigs" = "Configuraciones Generales" "generalConfigs" = "Configuraciones Generales"
"generalConfigsDesc" = "Estas opciones proporcionarán ajustes generales." "generalConfigsDesc" = "Estas opciones proporcionarán ajustes generales."
"logConfigs" = "Registro"
"logConfigsDesc" = "Los registros pueden afectar la eficiencia de su servidor. Se recomienda habilitarlo sabiamente solo en caso de sus necesidades."
"blockConfigs" = "Configuraciones de Bloqueo" "blockConfigs" = "Configuraciones de Bloqueo"
"blockConfigsDesc" = "Estas opciones evitarán que los usuarios se conecten a protocolos y sitios web específicos." "blockConfigsDesc" = "Estas opciones evitarán que los usuarios se conecten a protocolos y sitios web específicos."
"blockCountryConfigs" = "Configuraciones de Bloqueo por País" "blockCountryConfigs" = "Configuraciones de Bloqueo por País"
@@ -382,36 +375,23 @@
"GoogleIPv4Desc" = "Agregar enrutamiento para que Google se conecte con IPv4." "GoogleIPv4Desc" = "Agregar enrutamiento para que Google se conecte con IPv4."
"NetflixIPv4" = "Usar IPv4 para Netflix" "NetflixIPv4" = "Usar IPv4 para Netflix"
"NetflixIPv4Desc" = "Agregar enrutamiento para que Netflix se conecte con IPv4." "NetflixIPv4Desc" = "Agregar enrutamiento para que Netflix se conecte con IPv4."
"GoogleWARP" = "Google" "GoogleWARP" = "Rutear Google a través de WARP."
"GoogleWARPDesc" = "Enruta el tráfico a Apple a través de WARP." "GoogleWARPDesc" = "Agregar enrutamiento para Google a través de WARP."
"OpenAIWARP" = "OpenAI (ChatGPT)" "OpenAIWARP" = "Rutear OpenAI (ChatGPT) a través de WARP."
"OpenAIWARPDesc" = "Enruta el tráfico a OpenAI (ChatGPT) a través de WARP." "OpenAIWARPDesc" = "Agregar enrutamiento para OpenAI (ChatGPT) a través de WARP."
"NetflixWARP" = "Netflix" "NetflixWARP" = "Rutear Netflix a través de WARP."
"NetflixWARPDesc" = "Enruta el tráfico a Netflix a través de WARP." "NetflixWARPDesc" = "Agregar enrutamiento para Netflix a través de WARP."
"MetaWARP" = "Meta" "SpotifyWARP" = "Rutear Spotify a través de WARP."
"MetaWARPDesc" = "Enruta el tráfico a Meta (Instagram, Facebook, WhatsApp, Threads,...) a través de WARP." "SpotifyWARPDesc" = "Agregar enrutamiento para Spotify a través de WARP."
"AppleWARP" = "Apple"
"AppleWARPDesc" = "Enruta el tráfico a Apple a través de WARP."
"RedditWARP" = "Reddit"
"RedditWARPDesc" = "Enruta el tráfico a Reddit a través de WARP."
"SpotifyWARP" = "Spotify"
"SpotifyWARPDesc" = "Enruta el tráfico a Spotify a través de WARP."
"IRWARP" = "Rutear dominios de Irán a través de WARP." "IRWARP" = "Rutear dominios de Irán a través de WARP."
"IRWARPDesc" = "Agregar enrutamiento para dominios de Irán a través de WARP." "IRWARPDesc" = "Agregar enrutamiento para dominios de Irán a través de WARP."
"Inbounds" = "Entrante" "Inbounds" = "Entrante"
"InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos." "InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos."
"Outbounds" = "Salidas" "Outbounds" = "Salidas"
"Balancers" = "Equilibradores"
"OutboundsDesc" = "Cambia la plantilla de configuración para definir formas de salida para este servidor." "OutboundsDesc" = "Cambia la plantilla de configuración para definir formas de salida para este servidor."
"Routings" = "Reglas de enrutamiento" "Routings" = "Reglas de enrutamiento"
"RoutingsDesc" = "¡La prioridad de cada regla es importante!" "RoutingsDesc" = "¡La prioridad de cada regla es importante!"
"completeTemplate" = "Todos" "completeTemplate" = "Todos"
"logLevel" = "Nivel de registro"
"logLevelDesc" = "El nivel de registro para registros de errores, que indica la información que debe registrarse."
"accessLog" = "Registro de acceso"
"accessLogDesc" = "La ruta del archivo para el registro de acceso. El valor especial 'ninguno' deshabilita los registros de acceso"
"errorLog" = "Registro de errores"
"errorLogDesc" = "La ruta del archivo para el registro de errores. El valor especial 'ninguno' deshabilitó los registros de errores"
[pages.xray.rules] [pages.xray.rules]
"first" = "Primero" "first" = "Primero"
@@ -422,7 +402,6 @@
"dest" = "Destino" "dest" = "Destino"
"inbound" = "Entrante" "inbound" = "Entrante"
"outbound" = "saliente" "outbound" = "saliente"
"balancer" = "Balancín"
"info" = "Información" "info" = "Información"
"add" = "Agregar regla" "add" = "Agregar regla"
"edit" = "Editar regla" "edit" = "Editar regla"
@@ -443,15 +422,6 @@
"portal" = "portal" "portal" = "portal"
"intercon" = "Interconexión" "intercon" = "Interconexión"
[pages.xray.balancer]
"addBalancer" = "Agregar equilibrador"
"editBalancer" = "Editar balanceador"
"balancerStrategy" = "Estrategia"
"balancerSelectors" = "Selectores"
"tag" = "Etiqueta"
"tagDesc" = "etiqueta única"
"balancerDesc" = "No es posible utilizar balancerTag y outboundTag al mismo tiempo. Si se utilizan al mismo tiempo, sólo funcionará outboundTag."
[pages.xray.wireguard] [pages.xray.wireguard]
"secretKey" = "Llave secreta" "secretKey" = "Llave secreta"
"publicKey" = "Llave pública" "publicKey" = "Llave pública"
@@ -460,21 +430,6 @@
"psk" = "Clave precompartida" "psk" = "Clave precompartida"
"domainStrategy" = "Estrategia de dominio" "domainStrategy" = "Estrategia de dominio"
[pages.xray.dns]
"enable" = "Habilitar DNS"
"enableDesc" = "Habilitar servidor DNS integrado"
"strategy" = "Estrategia de consulta"
"strategyDesc" = "Estrategia general para resolver nombres de dominio"
"add" = "Agregar servidor"
"edit" = "Editar servidor"
"domains" = "Dominios"
[pages.xray.fakedns]
"add" = "Agregar DNS falso"
"edit" = "Editar DNS falso"
"ipPool" = "Subred del grupo de IP"
"poolSize" = "Tamaño del grupo"
[pages.settings.security] [pages.settings.security]
"admin" = "Administrador" "admin" = "Administrador"
"secret" = "Token Secreto" "secret" = "Token Secreto"

View File

@@ -52,9 +52,6 @@
"secretToken" = "توکن امنیتی" "secretToken" = "توکن امنیتی"
"remained" = "باقی‌مانده" "remained" = "باقی‌مانده"
"security" = "امنیت" "security" = "امنیت"
"secAlertTitle" = "هشدار‌امنیتی"
"secAlertSsl" = "این‌اتصال‌امن نیست. لطفا‌ تازمانی‌که تی‌ال‌اس برای محافظت از‌ داده‌ها فعال نشده‌است، از وارد کردن اطلاعات حساس خودداری کنید"
"secAlertConf" = "پیکربندی‌های خاصی مستعد حملات سایبری شناسایی شده‌اند، اقدام فوری برای تقویت پروتکل‌های امنیتی و محافظت در برابر نقض‌های امنیتی لازم است"
[menu] [menu]
"dashboard" = "نمای کلی" "dashboard" = "نمای کلی"
@@ -79,7 +76,7 @@
"title" = "نمای کلی" "title" = "نمای کلی"
"memory" = "RAM" "memory" = "RAM"
"hard" = "Disk" "hard" = "Disk"
"xrayStatus" = "ایکس‌ری" "xrayStatus" = "وضعیت‌ایکس‌ری"
"stopXray" = "توقف" "stopXray" = "توقف"
"restartXray" = "شروع‌مجدد" "restartXray" = "شروع‌مجدد"
"xraySwitch" = "‌نسخه" "xraySwitch" = "‌نسخه"
@@ -305,8 +302,6 @@
"subShowInfoDesc" = "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد" "subShowInfoDesc" = "ترافیک و زمان باقی‌مانده را در برنامه‌های کاربری نمایش می‌دهد"
"subURI" = "پروکسی معکوس URI مسیر" "subURI" = "پروکسی معکوس URI مسیر"
"subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر" "subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسی‌ها تغییر می‌دهد URI مسیر"
"fragment" = "تکه‌تکه شدن"
"fragmentDesc" = "فعال کردن تکه تکه شدن برای بسته نخست تی‌ال‌اس"
[pages.xray] [pages.xray]
"title" = "پیکربندی ایکس‌ری" "title" = "پیکربندی ایکس‌ری"
@@ -316,8 +311,6 @@
"advancedTemplate" = "پیشرفته" "advancedTemplate" = "پیشرفته"
"generalConfigs" = "استراتژی‌ کلی" "generalConfigs" = "استراتژی‌ کلی"
"generalConfigsDesc" = "این گزینه‌ها استراتژی کلی ترافیک را تعیین می‌کنند" "generalConfigsDesc" = "این گزینه‌ها استراتژی کلی ترافیک را تعیین می‌کنند"
"logConfigs" = "گزارش"
"logConfigsDesc" = "گزارش‌ها ممکن است بر کارایی سرور شما تأثیر بگذارد. توصیه می شود فقط در صورت نیاز آن را عاقلانه فعال کنید"
"blockConfigs" = "سپر محافظ" "blockConfigs" = "سپر محافظ"
"blockConfigsDesc" = "این گزینه‌ها ترافیک را بر اساس پروتکل‌های درخواستی خاص، و وب سایت‌ها مسدود می‌کند" "blockConfigsDesc" = "این گزینه‌ها ترافیک را بر اساس پروتکل‌های درخواستی خاص، و وب سایت‌ها مسدود می‌کند"
"blockCountryConfigs" = "مسدودسازی کشور" "blockCountryConfigs" = "مسدودسازی کشور"
@@ -388,12 +381,6 @@
"OpenAIWARPDesc" = "ترافیک را از طریق وارپ به چت جی‌پی‌تی هدایت می‌کند" "OpenAIWARPDesc" = "ترافیک را از طریق وارپ به چت جی‌پی‌تی هدایت می‌کند"
"NetflixWARP" = "نتفلیکس" "NetflixWARP" = "نتفلیکس"
"NetflixWARPDesc" = "ترافیک را از طریق وارپ به نتفلیکس هدایت می‌کند" "NetflixWARPDesc" = "ترافیک را از طریق وارپ به نتفلیکس هدایت می‌کند"
"MetaWARP" = "متا"
"MetaWARPDesc" = "ترافیک را از طریق وارپ به متا (اینستاگرام، فیس بوک، واتساپ، تردز و...) هدایت می کند."
"AppleWARP" = "اپل"
"AppleWARPDesc" = " ترافیک را از طریق وارپ به اپل هدایت می‌کند"
"RedditWARP" = "ردیت"
"RedditWARPDesc" = " ترافیک را از طریق وارپ به ردیت هدایت می‌کند"
"SpotifyWARP" = "اسپاتیفای" "SpotifyWARP" = "اسپاتیفای"
"SpotifyWARPDesc" = " ترافیک را از طریق وارپ به اسپاتیفای هدایت می‌کند" "SpotifyWARPDesc" = " ترافیک را از طریق وارپ به اسپاتیفای هدایت می‌کند"
"IRWARP" = "دامنه‌های ایران" "IRWARP" = "دامنه‌های ایران"
@@ -401,17 +388,10 @@
"Inbounds" = "ورودی‌ها" "Inbounds" = "ورودی‌ها"
"InboundsDesc" = "پذیرش کلاینت خاص" "InboundsDesc" = "پذیرش کلاینت خاص"
"Outbounds" = "خروجی‌ها" "Outbounds" = "خروجی‌ها"
"Balancers" = "بالانسرها"
"OutboundsDesc" = "مسیر ترافیک خروجی را تنظیم کنید" "OutboundsDesc" = "مسیر ترافیک خروجی را تنظیم کنید"
"Routings" = "قوانین مسیریابی" "Routings" = "قوانین مسیریابی"
"RoutingsDesc" = "اولویت هر قانون مهم است" "RoutingsDesc" = "اولویت هر قانون مهم است"
"completeTemplate" = "کامل" "completeTemplate" = "کامل"
"logLevel" = "سطح گزارش"
"logLevelDesc" = "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند."
"accessLog" = "مسیر گزارش"
"accessLogDesc" = "مسیر فایل برای گزارش دسترسی. مقدار ویژه «هیچ» گزارش‌های دسترسی را غیرفعال میکند."
"errorLog" = "گزارش خطا"
"errorLogDesc" = "مسیر فایل برای ورود به سیستم خطا. مقدار ویژه «هیچ» گزارش های خطا را غیرفعال میکند"
[pages.xray.rules] [pages.xray.rules]
"first" = "اولین" "first" = "اولین"
@@ -422,7 +402,6 @@
"dest" = "مقصد" "dest" = "مقصد"
"inbound" = "ورودی" "inbound" = "ورودی"
"outbound" = "خروجی" "outbound" = "خروجی"
"balancer" = "بالانسر"
"info" = "اطلاعات" "info" = "اطلاعات"
"add" = "افزودن قانون" "add" = "افزودن قانون"
"edit" = "ویرایش قانون" "edit" = "ویرایش قانون"
@@ -443,15 +422,6 @@
"portal" = "پورتال" "portal" = "پورتال"
"intercon" = "اتصال میانی" "intercon" = "اتصال میانی"
[pages.xray.balancer]
"addBalancer" = "افزودن بالانسر"
"editBalancer" = "ویرایش بالانسر"
"balancerStrategy" = "استراتژی"
"balancerSelectors" = "انتخاب‌گرها"
"tag" = "برچسب"
"tagDesc" = "برچسب یگانه"
"balancerDesc" = "امکان استفاده همزمان balancerTag و outboundTag باهم وجود ندارد. درصورت استفاده همزمان فقط outboundTag عمل خواهد کرد."
[pages.xray.wireguard] [pages.xray.wireguard]
"secretKey" = "کلید شخصی" "secretKey" = "کلید شخصی"
"publicKey" = "کلید عمومی" "publicKey" = "کلید عمومی"
@@ -460,21 +430,6 @@
"psk" = "کلید مشترک" "psk" = "کلید مشترک"
"domainStrategy" = "استراتژی حل دامنه" "domainStrategy" = "استراتژی حل دامنه"
[pages.xray.dns]
"enable" = "فعال کردن حل دامنه"
"enableDesc" = "سرور حل دامنه داخلی را فعال کنید"
"strategy" = "استراتژی پرس‌وجو"
"strategyDesc" = "استراتژی کلی برای حل نام دامنه"
"add" = "افزودن سرور"
"edit" = "ویرایش سرور"
"domains" = "دامنه‌ها"
[pages.xray.fakedns]
"add" = "افزودن دی‌ان‌اس جعلی"
"edit" = "ویرایش دی‌ان‌اس جعلی"
"ipPool" = "زیرشبکه استخر آی‌پی"
"poolSize" = "اندازه استخر"
[pages.settings.security] [pages.settings.security]
"admin" = "مدیر" "admin" = "مدیر"
"secret" = "توکن مخفی" "secret" = "توکن مخفی"
@@ -497,7 +452,7 @@
"wentWrong" = "❌ مشکلی رخ داده است!" "wentWrong" = "❌ مشکلی رخ داده است!"
"noIpRecord" = "❗ رکورد IP یافت نشد!" "noIpRecord" = "❗ رکورد IP یافت نشد!"
"noInbounds" = "❗ هیچ ورودی یافت نشد!" "noInbounds" = "❗ هیچ ورودی یافت نشد!"
"unlimited" = "♾ - نامحدود(ریست)" "unlimited" = "♾ نامحدود"
"add" = "اضافه کردن" "add" = "اضافه کردن"
"month" = "ماه" "month" = "ماه"
"months" = "ماه‌ها" "months" = "ماه‌ها"

View File

@@ -1,621 +0,0 @@
"username" = "Nama Pengguna"
"password" = "Kata Sandi"
"login" = "Masuk"
"confirm" = "Konfirmasi"
"cancel" = "Batal"
"close" = "Tutup"
"copy" = "Salin"
"copied" = "Tersalin"
"download" = "Unduh"
"remark" = "Catatan"
"enable" = "Aktifkan"
"protocol" = "Protokol"
"search" = "Cari"
"filter" = "Filter"
"loading" = "Memuat..."
"second" = "Detik"
"minute" = "Menit"
"hour" = "Jam"
"day" = "Hari"
"check" = "Centang"
"indefinite" = "Tak Terbatas"
"unlimited" = "Tanpa Batas"
"none" = "None"
"qrCode" = "Kode QR"
"info" = "Informasi Lebih Lanjut"
"edit" = "Edit"
"delete" = "Hapus"
"reset" = "Reset"
"copySuccess" = "Berhasil Disalin"
"sure" = "Yakin"
"encryption" = "Enkripsi"
"transmission" = "Transmisi"
"host" = "Host"
"path" = "Jalur"
"camouflage" = "Obfuscation"
"status" = "Status"
"enabled" = "Aktif"
"disabled" = "Nonaktif"
"depleted" = "Habis"
"depletingSoon" = "Akan Habis"
"offline" = "Offline"
"online" = "Online"
"domainName" = "Nama Domain"
"monitor" = "IP Pemantauan"
"certificate" = "Sertifikat"
"fail" = "Gagal"
"success" = "Berhasil"
"getVersion" = "Dapatkan Versi"
"install" = "Instal"
"clients" = "Klien"
"usage" = "Penggunaan"
"secretToken" = "Token Rahasia"
"remained" = "Tersisa"
"security" = "Keamanan"
"secAlertTitle" = "Peringatan keamanan"
"secAlertSsl" = "Koneksi ini tidak aman. Harap hindari memasukkan informasi sensitif sampai TLS diaktifkan untuk perlindungan data."
"secAlertConf" = "Konfigurasi tertentu telah diidentifikasi rentan terhadap serangan, sehingga mendorong tindakan segera untuk memperkuat protokol keamanan dan melindungi dari potensi pelanggaran keamanan."
[menu]
"dashboard" = "Ikhtisar"
"inbounds" = "Masuk"
"settings" = "Pengaturan Panel"
"xray" = "Konfigurasi Xray"
"logout" = "Keluar"
"link" = "Kelola"
[pages.login]
"title" = "Selamat Datang"
"loginAgain" = "Sesi Anda telah berakhir, harap masuk kembali"
[pages.login.toasts]
"invalidFormData" = "Format data input tidak valid."
"emptyUsername" = "Nama Pengguna diperlukan"
"emptyPassword" = "Kata Sandi diperlukan"
"wrongUsernameOrPassword" = "Nama pengguna atau kata sandi tidak valid."
"successLogin" = "Login berhasil"
[pages.index]
"title" = "Ikhtisar"
"memory" = "RAM"
"hard" = "Disk"
"xrayStatus" = "Xray"
"stopXray" = "Stop"
"restartXray" = "Restart"
"xraySwitch" = "Versi"
"xraySwitchClick" = "Pilih versi yang ingin Anda pindah."
"xraySwitchClickDesk" = "Pilih dengan hati-hati, karena versi yang lebih lama mungkin tidak kompatibel dengan konfigurasi saat ini."
"operationHours" = "Waktu Aktif"
"systemLoad" = "Beban Sistem"
"systemLoadDesc" = "Rata-rata beban sistem selama 1, 5, dan 15 menit terakhir"
"connectionTcpCountDesc" = "Total koneksi TCP di seluruh sistem"
"connectionUdpCountDesc" = "Total koneksi UDP di seluruh sistem"
"connectionCount" = "Statistik Koneksi"
"upSpeed" = "Kecepatan unggah keseluruhan di seluruh sistem"
"downSpeed" = "Kecepatan unduh keseluruhan di seluruh sistem"
"totalSent" = "Total data terkirim di seluruh sistem sejak startup OS"
"totalReceive" = "Total data diterima di seluruh sistem sejak startup OS"
"xraySwitchVersionDialog" = "Ganti Versi Xray"
"xraySwitchVersionDialogDesc" = "Apakah Anda yakin ingin mengubah versi Xray menjadi"
"dontRefresh" = "Instalasi sedang berlangsung, harap jangan menyegarkan halaman ini"
"logs" = "Log"
"config" = "Konfigurasi"
"backup" = "Cadangan & Pulihkan"
"backupTitle" = "Cadangan & Pulihkan Database"
"backupDescription" = "Disarankan untuk membuat cadangan sebelum memulihkan database."
"exportDatabase" = "Cadangkan"
"importDatabase" = "Pulihkan"
[pages.inbounds]
"title" = "Masuk"
"totalDownUp" = "Total Terkirim/Diterima"
"totalUsage" = "Penggunaan Total"
"inboundCount" = "Total Masuk"
"operate" = "Menu"
"enable" = "Aktifkan"
"remark" = "Catatan"
"protocol" = "Protokol"
"port" = "Port"
"traffic" = "Traffic"
"details" = "Rincian"
"transportConfig" = "Transport"
"expireDate" = "Durasi"
"resetTraffic" = "Reset Traffic"
"addInbound" = "Tambahkan Masuk"
"generalActions" = "Tindakan Umum"
"create" = "Buat"
"update" = "Perbarui"
"modifyInbound" = "Ubah Masuk"
"deleteInbound" = "Hapus Masuk"
"deleteInboundContent" = "Apakah Anda yakin ingin menghapus masuk?"
"deleteClient" = "Hapus Klien"
"deleteClientContent" = "Apakah Anda yakin ingin menghapus klien?"
"resetTrafficContent" = "Apakah Anda yakin ingin mereset traffic?"
"copyLink" = "Salin URL"
"address" = "Alamat"
"network" = "Jaringan"
"destinationPort" = "Port Tujuan"
"targetAddress" = "Alamat Target"
"monitorDesc" = "Biarkan kosong untuk mendengarkan semua IP"
"meansNoLimit" = " = Unlimited. (unit: GB)"
"totalFlow" = "Total Aliran"
"leaveBlankToNeverExpire" = "Biarkan kosong untuk tidak pernah kedaluwarsa"
"noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default"
"certificatePath" = "Path Berkas"
"certificateContent" = "Konten Berkas"
"publicKeyPath" = "Path Kunci Publik"
"publicKeyContent" = "Konten Kunci Publik"
"keyPath" = "Path Kunci Privat"
"keyContent" = "Konten Kunci Privat"
"clickOnQRcode" = "Klik pada Kode QR untuk Menyalin"
"client" = "Klien"
"export" = "Ekspor Semua URL"
"clone" = "Duplikat"
"cloneInbound" = "Duplikat"
"cloneInboundContent" = "Semua pengaturan masuk ini, kecuali Port, Listening IP, dan Klien, akan diterapkan pada duplikat."
"cloneInboundOk" = "Duplikat"
"resetAllTraffic" = "Reset Semua Traffic Masuk"
"resetAllTrafficTitle" = "Reset Semua Traffic Masuk"
"resetAllTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua masuk?"
"resetInboundClientTraffics" = "Reset Traffic Klien Masuk"
"resetInboundClientTrafficTitle" = "Reset Traffic Klien Masuk"
"resetInboundClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic klien masuk ini?"
"resetAllClientTraffics" = "Reset Traffic Semua Klien"
"resetAllClientTrafficTitle" = "Reset Traffic Semua Klien"
"resetAllClientTrafficContent" = "Apakah Anda yakin ingin mereset traffic semua klien?"
"delDepletedClients" = "Hapus Klien Habis"
"delDepletedClientsTitle" = "Hapus Klien Habis"
"delDepletedClientsContent" = "Apakah Anda yakin ingin menghapus semua klien yang habis?"
"email" = "Email"
"emailDesc" = "Harap berikan alamat email yang unik."
"IPLimit" = "Batas IP"
"IPLimitDesc" = "Menonaktifkan masuk jika jumlah melebihi nilai yang ditetapkan. (0 = nonaktif)"
"IPLimitlog" = "Log IP"
"IPLimitlogDesc" = "Log histori IP. (untuk mengaktifkan masuk setelah menonaktifkan, hapus log)"
"IPLimitlogclear" = "Hapus Log"
"setDefaultCert" = "Atur Sertifikat dari Panel"
"xtlsDesc" = "Xray harus versi 1.7.5"
"realityDesc" = "Xray harus versi 1.8.0+"
"telegramDesc" = "Harap berikan ID Telegram atau obrolan tanpa menggunakan '@'. (dapatkan di sini @userinfobot) atau (gunakan perintah '/id' di bot)"
"subscriptionDesc" = "Untuk menemukan URL langganan Anda, buka 'Rincian'. Selain itu, Anda dapat menggunakan nama yang sama untuk beberapa klien."
"info" = "Info"
"same" = "Sama"
"inboundData" = "Data Masuk"
"exportInbound" = "Ekspor Masuk"
"import" = "Impor"
"importInbound" = "Impor Masuk"
[pages.client]
"add" = "Tambah Klien"
"edit" = "Edit Klien"
"submitAdd" = "Tambah Klien"
"submitEdit" = "Simpan Perubahan"
"clientCount" = "Jumlah Klien"
"bulk" = "Tambahkan Massal"
"method" = "Metode"
"first" = "Pertama"
"last" = "Terakhir"
"prefix" = "Awalan"
"postfix" = "Akhiran"
"delayedStart" = "Mulai saat Penggunaan Awal"
"expireDays" = "Durasi"
"days" = "Hari"
"renew" = "Perpanjang Otomatis"
"renewDesc" = "Perpanjangan otomatis setelah kedaluwarsa. (0 = nonaktif)(unit: hari)"
[pages.inbounds.toasts]
"obtain" = "Dapatkan"
[pages.inbounds.stream.general]
"request" = "Permintaan"
"response" = "Respons"
"name" = "Nama"
"value" = "Nilai"
[pages.inbounds.stream.tcp]
"version" = "Versi"
"method" = "Metode"
"path" = "Path"
"status" = "Status"
"statusDescription" = "Deskripsi Status"
"requestHeader" = "Header Permintaan"
"responseHeader" = "Header Respons"
[pages.inbounds.stream.quic]
"encryption" = "Enkripsi"
[pages.settings]
"title" = "Pengaturan Panel"
"save" = "Simpan"
"infoDesc" = "Setiap perubahan yang dibuat di sini perlu disimpan. Harap restart panel untuk menerapkan perubahan."
"restartPanel" = "Restart Panel"
"restartPanelDesc" = "Apakah Anda yakin ingin merestart panel? Jika Anda tidak dapat mengakses panel setelah merestart, lihat info log panel di server."
"actions" = "Tindakan"
"resetDefaultConfig" = "Reset ke Default"
"panelSettings" = "Umum"
"securitySettings" = "Otentikasi"
"TGBotSettings" = "Bot Telegram"
"panelListeningIP" = "IP Pendengar"
"panelListeningIPDesc" = "Alamat IP untuk panel web. (biarkan kosong untuk mendengarkan semua IP)"
"panelListeningDomain" = "Domain Pendengar"
"panelListeningDomainDesc" = "Nama domain untuk panel web. (biarkan kosong untuk mendengarkan semua domain dan IP)"
"panelPort" = "Port Pendengar"
"panelPortDesc" = "Nomor port untuk panel web. (harus menjadi port yang tidak digunakan)"
"publicKeyPath" = "Path Kunci Publik"
"publicKeyPathDesc" = "Path berkas kunci publik untuk panel web. (dimulai dengan /)"
"privateKeyPath" = "Path Kunci Privat"
"privateKeyPathDesc" = "Path berkas kunci privat untuk panel web. (dimulai dengan /)"
"panelUrlPath" = "URI Path"
"panelUrlPathDesc" = "URI path untuk panel web. (dimulai dengan / dan diakhiri dengan /)"
"pageSize" = "Ukuran Halaman"
"pageSizeDesc" = "Tentukan ukuran halaman untuk tabel masuk. (0 = nonaktif)"
"remarkModel" = "Model Catatan & Karakter Pemisah"
"datepicker" = "Jenis Kalender"
"datepickerPlaceholder" = "Pilih tanggal"
"datepickerDescription" = "Tugas terjadwal akan berjalan berdasarkan kalender ini."
"sampleRemark" = "Contoh Catatan"
"oldUsername" = "Username Saat Ini"
"currentPassword" = "Kata Sandi Saat Ini"
"newUsername" = "Username Baru"
"newPassword" = "Kata Sandi Baru"
"telegramBotEnable" = "Aktifkan Bot Telegram"
"telegramBotEnableDesc" = "Mengaktifkan bot Telegram."
"telegramToken" = "Token Telegram"
"telegramTokenDesc" = "Token bot Telegram yang diperoleh dari '@BotFather'."
"telegramProxy" = "Proxy SOCKS"
"telegramProxyDesc" = "Mengaktifkan proxy SOCKS5 untuk terhubung ke Telegram. (sesuaikan pengaturan sesuai panduan)"
"telegramChatId" = "ID Obrolan Admin"
"telegramChatIdDesc" = "ID Obrolan Admin Telegram. (dipisahkan koma)(dapatkan di sini @userinfobot) atau (gunakan perintah '/id' di bot)"
"telegramNotifyTime" = "Waktu Notifikasi"
"telegramNotifyTimeDesc" = "Waktu notifikasi bot Telegram yang diatur untuk laporan berkala. (gunakan format waktu crontab)"
"tgNotifyBackup" = "Cadangan Database"
"tgNotifyBackupDesc" = "Kirim berkas cadangan database dengan laporan."
"tgNotifyLogin" = "Notifikasi Login"
"tgNotifyLoginDesc" = "Dapatkan notifikasi tentang username, alamat IP, dan waktu setiap kali seseorang mencoba masuk ke panel web Anda."
"sessionMaxAge" = "Durasi Sesi"
"sessionMaxAgeDesc" = "Durasi di mana Anda dapat tetap masuk. (unit: menit)"
"expireTimeDiff" = "Notifikasi Tanggal Kedaluwarsa"
"expireTimeDiffDesc" = "Dapatkan notifikasi tentang tanggal kedaluwarsa saat mencapai ambang batas ini. (unit: hari)"
"trafficDiff" = "Notifikasi Batas Traffic"
"trafficDiffDesc" = "Dapatkan notifikasi tentang batas traffic saat mencapai ambang batas ini. (unit: GB)"
"tgNotifyCpu" = "Notifikasi Beban CPU"
"tgNotifyCpuDesc" = "Dapatkan notifikasi jika beban CPU melebihi ambang batas ini. (unit: %)"
"timeZone" = "Zone Waktu"
"timeZoneDesc" = "Tugas terjadwal akan berjalan berdasarkan zona waktu ini."
"subSettings" = "Langganan"
"subEnable" = "Aktifkan Layanan Langganan"
"subEnableDesc" = "Mengaktifkan layanan langganan."
"subListen" = "IP Pendengar"
"subListenDesc" = "Alamat IP untuk layanan langganan. (biarkan kosong untuk mendengarkan semua IP)"
"subPort" = "Port Pendengar"
"subPortDesc" = "Nomor port untuk layanan langganan. (harus menjadi port yang tidak digunakan)"
"subCertPath" = "Path Kunci Publik"
"subCertPathDesc" = "Path berkas kunci publik untuk layanan langganan. (dimulai dengan /)"
"subKeyPath" = "Path Kunci Privat"
"subKeyPathDesc" = "Path berkas kunci privat untuk layanan langganan. (dimulai dengan /)"
"subPath" = "URI Path"
"subPathDesc" = "URI path untuk layanan langganan. (dimulai dengan / dan diakhiri dengan /)"
"subDomain" = "Domain Pendengar"
"subDomainDesc" = "Nama domain untuk layanan langganan. (biarkan kosong untuk mendengarkan semua domain dan IP)"
"subUpdates" = "Interval Pembaruan"
"subUpdatesDesc" = "Interval pembaruan URL langganan dalam aplikasi klien. (unit: jam)"
"subEncrypt" = "Encode"
"subEncryptDesc" = "Konten yang dikembalikan dari layanan langganan akan dienkripsi Base64."
"subShowInfo" = "Tampilkan Info Penggunaan"
"subShowInfoDesc" = "Sisa traffic dan tanggal akan ditampilkan di aplikasi klien."
"subURI" = "URI Proxy Terbalik"
"subURIDesc" = "URI path URL langganan untuk penggunaan di belakang proxy."
"fragment" = "Fragmentasi"
"fragmentDesc" = "Aktifkan fragmentasi untuk paket hello TLS"
[pages.xray]
"title" = "Konfigurasi Xray"
"save" = "Simpan"
"restart" = "Restart Xray"
"basicTemplate" = "Dasar"
"advancedTemplate" = "Lanjutan"
"generalConfigs" = "Strategi Umum"
"generalConfigsDesc" = "Opsi ini akan menentukan penyesuaian strategi umum."
"logConfigs" = "Catatan"
"logConfigsDesc" = "Log dapat mempengaruhi efisiensi server Anda. Disarankan untuk mengaktifkannya dengan bijak hanya jika diperlukan"
"blockConfigs" = "Pelindung"
"blockConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan protokol dan situs web yang diminta."
"blockCountryConfigs" = "Blokir Negara"
"blockCountryConfigsDesc" = "Opsi ini akan memblokir lalu lintas berdasarkan negara yang diminta."
"directCountryConfigs" = "Langsung ke Negara"
"directCountryConfigsDesc" = "Opsi ini akan langsung meneruskan lalu lintas berdasarkan negara yang diminta."
"ipv4Configs" = "Pengalihan IPv4"
"ipv4ConfigsDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui IPv4."
"warpConfigs" = "Pengalihan WARP"
"warpConfigsDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui WARP."
"Template" = "Template Konfigurasi Xray Lanjutan"
"TemplateDesc" = "File konfigurasi Xray akhir akan dibuat berdasarkan template ini."
"FreedomStrategy" = "Strategi Protokol Freedom"
"FreedomStrategyDesc" = "Atur strategi output untuk jaringan dalam Protokol Freedom."
"RoutingStrategy" = "Strategi Pengalihan Keseluruhan"
"RoutingStrategyDesc" = "Atur strategi pengalihan lalu lintas keseluruhan untuk menyelesaikan semua permintaan."
"Torrent" = "Blokir Protokol BitTorrent"
"TorrentDesc" = "Memblokir protokol BitTorrent."
"PrivateIp" = "Blokir Koneksi ke IP Pribadi"
"PrivateIpDesc" = "Memblokir pembentukan koneksi ke rentang IP pribadi."
"Ads" = "Blokir Iklan"
"AdsDesc" = "Memblokir situs web periklanan."
"Family" = "Proteksi Keluarga"
"FamilyDesc" = "Memblokir konten dewasa dan situs web berbahaya."
"Security" = "Pelindung Keamanan"
"SecurityDesc" = "Memblokir situs web malware, phishing, dan penambang kripto."
"Speedtest" = "Blokir Speedtest"
"SpeedtestDesc" = "Memblokir pembentukan koneksi ke situs web speedtest."
"IRIp" = "Blokir Koneksi ke IP Iran"
"IRIpDesc" = "Memblokir pembentukan koneksi ke rentang IP Iran."
"IRDomain" = "Blokir Koneksi ke Domain Iran"
"IRDomainDesc" = "Memblokir pembentukan koneksi ke domain Iran."
"ChinaIp" = "Blokir Koneksi ke IP China"
"ChinaIpDesc" = "Memblokir pembentukan koneksi ke rentang IP China."
"ChinaDomain" = "Blokir Koneksi ke Domain China"
"ChinaDomainDesc" = "Memblokir pembentukan koneksi ke domain China."
"RussiaIp" = "Blokir Koneksi ke IP Rusia"
"RussiaIpDesc" = "Memblokir pembentukan koneksi ke rentang IP Rusia."
"RussiaDomain" = "Blokir Koneksi ke Domain Rusia"
"RussiaDomainDesc" = "Memblokir pembentukan koneksi ke domain Rusia."
"VNIp" = "Blokir Koneksi ke IP Vietnam"
"VNIpDesc" = "Memblokir pembentukan koneksi ke rentang IP Vietnam."
"VNDomain" = "Blokir Koneksi ke Domain Vietnam"
"VNDomainDesc" = "Memblokir pembentukan koneksi ke domain Vietnam."
"DirectIRIp" = "Koneksi Langsung ke IP Iran"
"DirectIRIpDesc" = "Membentuk koneksi langsung ke rentang IP Iran."
"DirectIRDomain" = "Koneksi Langsung ke Domain Iran"
"DirectIRDomainDesc" = "Membentuk koneksi langsung ke domain Iran."
"DirectChinaIp" = "Koneksi Langsung ke IP China"
"DirectChinaIpDesc" = "Membentuk koneksi langsung ke rentang IP China."
"DirectChinaDomain" = "Koneksi Langsung ke Domain China"
"DirectChinaDomainDesc" = "Membentuk koneksi langsung ke domain China."
"DirectRussiaIp" = "Koneksi Langsung ke IP Rusia"
"DirectRussiaIpDesc" = "Membentuk koneksi langsung ke rentang IP Rusia."
"DirectRussiaDomain" = "Koneksi Langsung ke Domain Rusia"
"DirectRussiaDomainDesc" = "Membentuk koneksi langsung ke domain Rusia."
"DirectVNIp" = "Koneksi Langsung ke IP Vietnam"
"DirectVNIpDesc" = "Membentuk koneksi langsung ke rentang IP Vietnam."
"DirectVNDomain" = "Koneksi Langsung ke Domain Vietnam"
"DirectVNDomainDesc" = "Membentuk koneksi langsung ke domain Vietnam."
"GoogleIPv4" = "Google"
"GoogleIPv4Desc" = "Rute lalu lintas ke Google melalui IPv4."
"NetflixIPv4" = "Netflix"
"NetflixIPv4Desc" = "Rute lalu lintas ke Netflix melalui IPv4."
"GoogleWARP" = "Google"
"GoogleWARPDesc" = "Tambahkan pengalihan untuk Google melalui WARP."
"OpenAIWARP" = "ChatGPT"
"OpenAIWARPDesc" = "Rute lalu lintas ke ChatGPT melalui WARP."
"NetflixWARP" = "Netflix"
"NetflixWARPDesc" = "Rute lalu lintas ke Netflix melalui WARP."
"MetaWARP" = "Meta"
"MetaWARPDesc" = "Merutekan lalu lintas ke Meta (Instagram, Facebook, WhatsApp, Threads,...) melalui WARP."
"AppleWARP" = "Apple"
"AppleWARPDesc" = "Merutekan lalu lintas ke Apple melalui WARP."
"RedditWARP" = "Reddit"
"RedditWARPDesc" = "Merutekan lalu lintas ke Reddit melalui WARP."
"SpotifyWARP" = "Spotify"
"SpotifyWARPDesc" = "Rute lalu lintas ke Spotify melalui WARP."
"IRWARP" = "Domain Iran"
"IRWARPDesc" = "Rute lalu lintas ke domain Iran melalui WARP."
"Inbounds" = "Masuk"
"InboundsDesc" = "Menerima klien tertentu."
"Outbounds" = "Keluar"
"Balancers" = "Penyeimbang"
"OutboundsDesc" = "Atur jalur lalu lintas keluar."
"Routings" = "Aturan Pengalihan"
"RoutingsDesc" = "Prioritas setiap aturan penting!"
"completeTemplate" = "Semua"
"logLevel" = "Tingkat Log"
"logLevelDesc" = "Tingkat log untuk log kesalahan, menunjukkan informasi yang perlu dicatat."
"accessLog" = "Log Akses"
"accessLogDesc" = "Jalur file untuk log akses. Nilai khusus 'tidak ada' menonaktifkan log akses"
"errorLog" = "Catatan eror"
"errorLogDesc" = "Jalur file untuk log kesalahan. Nilai khusus 'tidak ada' menonaktifkan log kesalahan"
[pages.xray.rules]
"first" = "Pertama"
"last" = "Terakhir"
"up" = "Naik"
"down" = "Turun"
"source" = "Sumber"
"dest" = "Tujuan"
"inbound" = "Masuk"
"outbound" = "Keluar"
"balancer" = "Pengimbang"
"info" = "Info"
"add" = "Tambahkan Aturan"
"edit" = "Edit Aturan"
"useComma" = "Item yang dipisahkan koma"
[pages.xray.outbound]
"addOutbound" = "Tambahkan Keluar"
"addReverse" = "Tambahkan Revers"
"editOutbound" = "Edit Keluar"
"editReverse" = "Edit Revers"
"tag" = "Tag"
"tagDesc" = "Tag Unik"
"address" = "Alamat"
"reverse" = "Revers"
"domain" = "Domain"
"type" = "Tipe"
"bridge" = "Jembatan"
"portal" = "Portal"
"intercon" = "Interkoneksi"
[pages.xray.balancer]
"addBalancer" = "Tambahkan Penyeimbang"
"editBalancer" = "Sunting Penyeimbang"
"balancerStrategy" = "Strategi"
"balancerSelectors" = "Penyeleksi"
"tag" = "Menandai"
"tagDesc" = "Label Unik"
"balancerDesc" = "BalancerTag dan outboundTag tidak dapat digunakan secara bersamaan. Jika digunakan secara bersamaan, hanya outboundTag yang akan berfungsi."
[pages.xray.wireguard]
"secretKey" = "Kunci Rahasia"
"publicKey" = "Kunci Publik"
"allowedIPs" = "IP yang Diizinkan"
"endpoint" = "Titik Akhir"
"psk" = "Kunci Pra-Bagi"
"domainStrategy" = "Strategi Domain"
[pages.xray.dns]
"enable" = "Aktifkan DNS"
"enableDesc" = "Aktifkan server DNS bawaan"
"strategy" = "Strategi Kueri"
"strategyDesc" = "Strategi keseluruhan untuk menyelesaikan nama domain"
"add" = "Tambahkan Server"
"edit" = "Sunting Server"
"domains" = "Domains"
[pages.xray.fakedns]
"add" = "Tambahkan DNS Palsu"
"edit" = "Edit DNS Palsu"
"ipPool" = "Subnet Kumpulan IP"
"poolSize" = "Ukuran Kolam"
[pages.settings.security]
"admin" = "Admin"
"secret" = "Token Rahasia"
"loginSecurity" = "Login Aman"
"loginSecurityDesc" = "Menambahkan lapisan otentikasi tambahan untuk memberikan keamanan lebih."
"secretToken" = "Token Rahasia"
"secretTokenDesc" = "Simpan token ini dengan aman di tempat yang aman. Token ini diperlukan untuk login dan tidak dapat dipulihkan."
[pages.settings.toasts]
"modifySettings" = "Ubah Pengaturan"
"getSettings" = "Dapatkan Pengaturan"
"modifyUser" = "Ubah Admin"
"originalUserPassIncorrect" = "Username atau password saat ini tidak valid"
"userPassMustBeNotEmpty" = "Username dan password baru tidak boleh kosong"
[tgbot]
"keyboardClosed" = "❌ Papan ketik kustom ditutup!"
"noResult" = "❗ Tidak ada hasil!"
"noQuery" = "❌ Permintaan tidak ditemukan! Harap gunakan perintah lagi!"
"wentWrong" = "❌ Ada yang salah!"
"noIpRecord" = "❗ Tidak ada Catatan IP!"
"noInbounds" = "❗ Tidak ada masuk ditemukan!"
"unlimited" = "♾ Tak terbatas"
"add" = "Tambah"
"month" = "Bulan"
"months" = "Bulan"
"day" = "Hari"
"days" = "Hari"
"hours" = "Jam"
"unknown" = "Tidak diketahui"
"inbounds" = "Masuk"
"clients" = "Klien"
"offline" = "🔴 Offline"
"online" = "🟢 Online"
[tgbot.commands]
"unknown" = "❗ Perintah tidak dikenal."
"pleaseChoose" = "👇 Harap pilih:\r\n"
"help" = "🤖 Selamat datang di bot ini! Ini dirancang untuk menyediakan data tertentu dari panel web dan memungkinkan Anda melakukan modifikasi sesuai kebutuhan.\r\n\r\n"
"start" = "👋 Halo <i>{{ .Firstname }}</i>.\r\n"
"welcome" = "🤖 Selamat datang di <b>{{.Hostname }}</b> bot managemen.\r\n"
"status" = "✅ Bot dalam keadaan baik!"
"usage" = "❗ Harap berikan teks untuk mencari!"
"getID" = "🆔 ID Anda:<code>{{.ID }}</code>"
"helpAdminCommands" = "Untuk mencari email klien:\r\n<code>/usage [Email]</code>\r\n\r\nUntuk mencari masuk (dengan statistik klien):\r\n<code>/inbound [Remark]</code>"
"helpClientCommands" = "Untuk mencari statistik, gunakan perintah berikut:\r\n\r\n<code>/usage [Email]</code>"
[tgbot.messages]
"cpuThreshold" = "🔴 Beban CPU {{ .Percent }}% melebihi batas {{ .Threshold }}%"
"selectUserFailed" = "❌ Kesalahan dalam pemilihan pengguna!"
"userSaved" = "✅ Pengguna Telegram tersimpan."
"loginSuccess" = "✅ Berhasil masuk ke panel.\r\n"
"loginFailed" = "❗️ Gagal masuk ke panel.\r\n"
"report" = "🕰 Laporan Terjadwal: {{ .RunTime }}\r\n"
"datetime" = "⏰ Tanggal & Waktu: {{ .DateTime }}\r\n"
"hostname" = "💻 Host: {{ .Hostname }}\r\n"
"version" = "🚀 Versi 3X-UI: {{ .Version }}\r\n"
"ipv6" = "🌐 IPv6: {{ .IPv6 }}\r\n"
"ipv4" = "🌐 IPv4: {{ .IPv4 }}\r\n"
"ip" = "🌐 IP: {{ .IP }}\r\n"
"ips" = "🔢 IP:\r\n{{ .IPs }}\r\n"
"serverUpTime" = "⏳ Waktu Aktif: {{ .UpTime }} {{ .Unit }}\r\n"
"serverLoad" = "📈 Beban Sistem: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n"
"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
"traffic" = "🚦 Lalu Lintas: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
"xrayStatus" = " Status: {{ .State }}\r\n"
"username" = "👤 Nama Pengguna: {{ .Username }}\r\n"
"time" = "⏰ Waktu: {{ .Time }}\r\n"
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
"port" = "🔌 Port: {{ .Port }}\r\n"
"expire" = "📅 Tanggal Kadaluarsa: {{ .Time }}\r\n"
"expireIn" = "📅 Kadaluarsa Dalam: {{ .Time }}\r\n"
"active" = "💡 Aktif: {{ .Enable }}\r\n"
"enabled" = "🚨 Diaktifkan: {{ .Enable }}\r\n"
"online" = "🌐 Status Koneksi: {{ .Status }}\r\n"
"email" = "📧 Email: {{ .Email }}\r\n"
"upload" = "🔼 Unggah: ↑{{ .Upload }}\r\n"
"download" = "🔽 Unduh: ↓{{ .Download }}\r\n"
"total" = "📊 Total: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
"TGUser" = "👤 Pengguna Telegram: {{ .TelegramID }}\r\n"
"exhaustedMsg" = "🚨 Habis {{ .Type }}:\r\n"
"exhaustedCount" = "🚨 Jumlah Habis {{ .Type }}:\r\n"
"onlinesCount" = "🌐 Klien Online: {{ .Count }}\r\n"
"disabled" = "🛑 Dinonaktifkan: {{ .Disabled }}\r\n"
"depleteSoon" = "🔜 Habis Sebentar: {{ .Deplete }}\r\n\r\n"
"backupTime" = "🗄 Waktu Backup: {{ .Time }}\r\n"
"refreshedOn" = "\r\n📋🔄 Diperbarui Pada: {{ .Time }}\r\n\r\n"
"yes" = "✅ Ya"
"no" = "❌ Tidak"
[tgbot.buttons]
"closeKeyboard" = "❌ Tutup Papan Ketik"
"cancel" = "❌ Batal"
"cancelReset" = "❌ Batal Reset"
"cancelIpLimit" = "❌ Batal Batas IP"
"confirmResetTraffic" = "✅ Konfirmasi Reset Lalu Lintas?"
"confirmClearIps" = "✅ Konfirmasi Hapus IPs?"
"confirmRemoveTGUser" = "✅ Konfirmasi Hapus Pengguna Telegram?"
"confirmToggle" = "✅ Konfirmasi Aktifkan/Nonaktifkan Pengguna?"
"dbBackup" = "Dapatkan Cadangan DB"
"serverUsage" = "Penggunaan Server"
"getInbounds" = "Dapatkan Inbounds"
"depleteSoon" = "Habis Sebentar"
"clientUsage" = "Dapatkan Penggunaan"
"onlines" = "Klien Online"
"commands" = "Perintah"
"refresh" = "🔄 Perbarui"
"clearIPs" = "❌ Hapus IPs"
"removeTGUser" = "❌ Hapus Pengguna Telegram"
"selectTGUser" = "👤 Pilih Pengguna Telegram"
"selectOneTGUser" = "👤 Pilih Pengguna Telegram:"
"resetTraffic" = "📈 Reset Lalu Lintas"
"resetExpire" = "📅 Ubah Tanggal Kadaluarsa"
"ipLog" = "🔢 Log IP"
"ipLimit" = "🔢 Batas IP"
"setTGUser" = "👤 Set Pengguna Telegram"
"toggle" = "🔘 Aktifkan / Nonaktifkan"
"custom" = "🔢 Kustom"
"confirmNumber" = "✅ Konfirmasi: {{ .Num }}"
"confirmNumberAdd" = "✅ Konfirmasi menambahkan: {{ .Num }}"
"limitTraffic" = "🚧 Batas Lalu Lintas"
"getBanLogs" = "Dapatkan Log Pemblokiran"
[tgbot.answers]
"successfulOperation" = "✅ Operasi berhasil!"
"errorOperation" = "❗ Kesalahan dalam operasi."
"getInboundsFailed" = "❌ Gagal mendapatkan inbounds."
"canceled" = "❌ {{ .Email }}: Operasi dibatalkan."
"clientRefreshSuccess" = "✅ {{ .Email }}: Klien diperbarui dengan berhasil."
"IpRefreshSuccess" = "✅ {{ .Email }}: IP diperbarui dengan berhasil."
"TGIdRefreshSuccess" = "✅ {{ .Email }}: Pengguna Telegram Klien diperbarui dengan berhasil."
"resetTrafficSuccess" = "✅ {{ .Email }}: Lalu lintas direset dengan berhasil."
"setTrafficLimitSuccess" = "✅ {{ .Email }}: Batas lalu lintas disimpan dengan berhasil."
"expireResetSuccess" = "✅ {{ .Email }}: Hari kadaluarsa direset dengan berhasil."
"resetIpSuccess" = "✅ {{ .Email }}: Batas IP {{ .Count }} disimpan dengan berhasil."
"clearIpSuccess" = "✅ {{ .Email }}: IP dihapus dengan berhasil."
"getIpLog" = "✅ {{ .Email }}: Dapatkan Log IP."
"getUserInfo" = "✅ {{ .Email }}: Dapatkan Info Pengguna Telegram."
"removedTGUserSuccess" = "✅ {{ .Email }}: Pengguna Telegram dihapus dengan berhasil."
"enableSuccess" = "✅ {{ .Email }}: Diaktifkan dengan berhasil."
"disableSuccess" = "✅ {{ .Email }}: Dinonaktifkan dengan berhasil."
"askToAddUserId" = "Konfigurasi Anda tidak ditemukan!\r\nSilakan minta admin Anda untuk menggunakan ID Telegram Anda dalam konfigurasi Anda.\r\n\r\nID Pengguna Anda: <code>{{ .TgUserID }}</code>"

View File

@@ -52,9 +52,6 @@
"secretToken" = "Секретный токен" "secretToken" = "Секретный токен"
"remained" = "остались" "remained" = "остались"
"security" = "Безопасность" "security" = "Безопасность"
"secAlertTitle" = "Предупреждение системы безопасности"
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных"
"secAlertConf" = "Некоторые конфигурации были определены как уязвимые для атак, что требует немедленных действий по усилению протоколов безопасности и защите от потенциальных нарушений безопасности."
[menu] [menu]
"dashboard" = "Статус системы" "dashboard" = "Статус системы"
@@ -79,7 +76,7 @@
"title" = "Статус системы" "title" = "Статус системы"
"memory" = "Память" "memory" = "Память"
"hard" = "Жесткий диск" "hard" = "Жесткий диск"
"xrayStatus" = "Xray" "xrayStatus" = "Статус"
"stopXray" = "Остановить" "stopXray" = "Остановить"
"restartXray" = "Перезапустить" "restartXray" = "Перезапустить"
"xraySwitch" = "Версия" "xraySwitch" = "Версия"
@@ -305,8 +302,6 @@
"subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации" "subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
"subURI" = "URI обратного прокси" "subURI" = "URI обратного прокси"
"subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами" "subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами"
"fragment" = "Фрагментация"
"fragmentDesc" = "Включить фрагментацию для пакета приветствия TLS"
[pages.xray] [pages.xray]
"title" = "Настройки Xray" "title" = "Настройки Xray"
@@ -316,8 +311,6 @@
"advancedTemplate" = "Расширенный шаблон" "advancedTemplate" = "Расширенный шаблон"
"generalConfigs" = "Основные настройки" "generalConfigs" = "Основные настройки"
"generalConfigsDesc" = "Эти параметры описывают общие настройки" "generalConfigsDesc" = "Эти параметры описывают общие настройки"
"logConfigs" = "Журнал"
"logConfigsDesc" = "Журналы могут повлиять на эффективность вашего сервера. Рекомендуется включать их с умом только в случае ваших нужд!"
"blockConfigs" = "Блокировка конфигураций" "blockConfigs" = "Блокировка конфигураций"
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам" "blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
"blockCountryConfigs" = "Конфигурации блокировки страны" "blockCountryConfigs" = "Конфигурации блокировки страны"
@@ -382,36 +375,23 @@
"GoogleIPv4Desc" = "Добавить маршрутизацию для Google для подключения к IPv4" "GoogleIPv4Desc" = "Добавить маршрутизацию для Google для подключения к IPv4"
"NetflixIPv4" = "Использовать IPv4 для Netflix" "NetflixIPv4" = "Использовать IPv4 для Netflix"
"NetflixIPv4Desc" = "Добавить маршрутизацию для Netflix для подключения к IPv4" "NetflixIPv4Desc" = "Добавить маршрутизацию для Netflix для подключения к IPv4"
"GoogleWARP" = "Google" "GoogleWARP" = "Маршрутизация Google через WARP"
"GoogleWARPDesc" = "Направляет трафик в Google через WARP." "GoogleWARPDesc" = "Добавить маршрутизацию для Google через WARP"
"OpenAIWARP" = "OpenAI (ChatGPT)" "OpenAIWARP" = "Маршрутизация OpenAI (ChatGPT) через WARP"
"OpenAIWARPDesc" = "Направляет трафик в OpenAI (ChatGPT) через WARP." "OpenAIWARPDesc" = "Добавить маршрутизацию для OpenAI (ChatGPT) через WARP"
"NetflixWARP" = "Netflix" "NetflixWARP" = "Маршрутизация Netflix через WARP"
"NetflixWARPDesc" = "Направляет трафик в Apple через WARP." "NetflixWARPDesc" = "Добавить маршрутизацию для Netflix через WARP"
"MetaWARP" = "Мета" "SpotifyWARP" = "Маршрутизация Spotify через WARP"
"MetaWARPDesc" = "Направляет трафик в Meta (Instagram, Facebook, WhatsApp, Threads...) через WARP." "SpotifyWARPDesc" = "Добавить маршрутизацию для Spotify через WARP"
"AppleWARP" = "Apple"
"AppleWARPDesc" = "Направляет трафик в Apple через WARP."
"RedditWARP" = "Reddit"
"RedditWARPDesc" = "Направляет трафик в Reddit через WARP."
"SpotifyWARP" = "Spotify"
"SpotifyWARPDesc" = "Направляет трафик в Spotify через WARP."
"IRWARP" = "Маршрутизация доменов Ирана через WARP" "IRWARP" = "Маршрутизация доменов Ирана через WARP"
"IRWARPDesc" = "Добавить маршрутизацию для доменов Ирана через WARP" "IRWARPDesc" = "Добавить маршрутизацию для доменов Ирана через WARP"
"Inbounds" = "Входящие" "Inbounds" = "Входящие"
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных пользователей" "InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных пользователей"
"Outbounds" = "Исходящие" "Outbounds" = "Исходящие"
"Balancers" = "Балансиры"
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера" "OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера"
"Routings" = "Правила маршрутизации" "Routings" = "Правила маршрутизации"
"RoutingsDesc" = "Важен приоритет каждого правила!" "RoutingsDesc" = "Важен приоритет каждого правила!"
"completeTemplate" = "Все" "completeTemplate" = "Все"
"logLevel" = "Уровень журнала"
"logLevelDesc" = "Уровень журнала для журналов ошибок, указывающий информацию, которую необходимо записать."
"accessLog" = "Журнал доступа"
"accessLogDesc" = "Путь к файлу журнала доступа. Специальное значение «none» отключило журналы доступа."
"errorLog" = "Журнал ошибок"
"errorLogDesc" = "Путь к файлу журнала ошибок. Специальное значение «none» отключает журналы ошибок."
[pages.xray.rules] [pages.xray.rules]
"first" = "Первый" "first" = "Первый"
@@ -422,7 +402,6 @@
"dest" = "Пункт назначения" "dest" = "Пункт назначения"
"inbound" = "Входящий" "inbound" = "Входящий"
"outbound" = "Исходящий" "outbound" = "Исходящий"
"balancer" = "балансир"
"info" = "Информация" "info" = "Информация"
"add" = "Добавить правило" "add" = "Добавить правило"
"edit" = "Редактировать правило" "edit" = "Редактировать правило"
@@ -443,15 +422,6 @@
"portal" = "Портал" "portal" = "Портал"
"intercon" = "Соединение" "intercon" = "Соединение"
[pages.xray.balancer]
"addBalancer" = "Добавить балансир"
"editBalancer" = "Редактировать балансир"
"balancerStrategy" = "Стратегия"
"balancerSelectors" = "Селекторы"
"tag" = "Тег"
"tagDesc" = "уникальный тег"
"balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag."
[pages.xray.wireguard] [pages.xray.wireguard]
"secretKey" = "Секретный ключ" "secretKey" = "Секретный ключ"
"publicKey" = "Открытый ключ" "publicKey" = "Открытый ключ"
@@ -460,21 +430,6 @@
"psk" = "Общий ключ" "psk" = "Общий ключ"
"domainStrategy" = "Стратегия домена" "domainStrategy" = "Стратегия домена"
[pages.xray.dns]
"enable" = "Включить DNS"
"enableDesc" = "Включить встроенный DNS-сервер"
"strategy" = "Стратегия запроса"
"strategyDesc" = "Общая стратегия разрешения доменных имен"
"add" = "Добавить сервер"
"edit" = "Редактировать сервер"
"domains" = "Домены"
[pages.xray.fakedns]
"add" = "Добавить поддельный DNS"
"edit" = "Редактировать поддельный DNS"
"ipPool" = "Подсеть пула IP"
"poolSize" = "Размер пула"
[pages.settings.security] [pages.settings.security]
"admin" = "Админ" "admin" = "Админ"
"secret" = "Секретный токен" "secret" = "Секретный токен"

View File

@@ -20,7 +20,7 @@
"check" = "Kiểm tra" "check" = "Kiểm tra"
"indefinite" = "Không xác định" "indefinite" = "Không xác định"
"unlimited" = "Không giới hạn" "unlimited" = "Không giới hạn"
"none" = "None" "none" = "Không có"
"qrCode" = "Mã QR" "qrCode" = "Mã QR"
"info" = "Thông tin thêm" "info" = "Thông tin thêm"
"edit" = "Chỉnh sửa" "edit" = "Chỉnh sửa"
@@ -52,9 +52,6 @@
"secretToken" = "Mã bí mật" "secretToken" = "Mã bí mật"
"remained" = "Còn lại" "remained" = "Còn lại"
"security" = "Bảo vệ" "security" = "Bảo vệ"
"secAlertTitle" = "Cảnh báo an ninh-Tiếng Việt by Ohoang7"
"secAlertSsl" = "Kết nối này không an toàn; Vui lòng không nhập thông tin nhạy cảm cho đến khi TLS được kích hoạt để bảo vệ dữ liệu của Bạn"
"secAlertConf" = "Một số cấu hình nhất định đã được xác định là dễ bị tấn công, thúc đẩy hành động ngay lập tức để củng cố các giao thức bảo mật và bảo vệ chống lại các vi phạm bảo mật tiềm ẩn."
[menu] [menu]
"dashboard" = "Trạng thái hệ thống" "dashboard" = "Trạng thái hệ thống"
@@ -79,7 +76,7 @@
"title" = "Trạng thái hệ thống" "title" = "Trạng thái hệ thống"
"memory" = "Ram" "memory" = "Ram"
"hard" = "Dung lượng" "hard" = "Dung lượng"
"xrayStatus" = "Xray" "xrayStatus" = "Trạng thái Xray"
"stopXray" = "Dừng lại" "stopXray" = "Dừng lại"
"restartXray" = "Khởi động lại" "restartXray" = "Khởi động lại"
"xraySwitch" = "Phiên bản" "xraySwitch" = "Phiên bản"
@@ -305,8 +302,6 @@
"subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình" "subShowInfoDesc" = "Hiển thị lưu lượng truy cập còn lại và ngày sau tên cấu hình"
"subURI" = "URI proxy trung gian" "subURI" = "URI proxy trung gian"
"subURIDesc" = "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian" "subURIDesc" = "Thay đổi URI cơ sở của URL gói đăng ký để sử dụng cho proxy trung gian"
"fragment" = "Sự phân mảnh"
"fragmentDesc" = "Kích hoạt phân mảnh cho gói TLS hello"
[pages.xray] [pages.xray]
"title" = "Cài đặt Xray" "title" = "Cài đặt Xray"
@@ -316,8 +311,6 @@
"advancedTemplate" = "Mẫu Nâng cao" "advancedTemplate" = "Mẫu Nâng cao"
"generalConfigs" = "Cấu hình Chung" "generalConfigs" = "Cấu hình Chung"
"generalConfigsDesc" = "Những tùy chọn này sẽ cung cấp điều chỉnh tổng quát." "generalConfigsDesc" = "Những tùy chọn này sẽ cung cấp điều chỉnh tổng quát."
"logConfigs" = "Nhật ký"
"logConfigsDesc" = "Nhật ký có thể ảnh hưởng đến hiệu suất máy chủ của bạn. Bạn chỉ nên kích hoạt nó một cách khôn ngoan trong trường hợp bạn cần"
"blockConfigs" = "Cấu hình Chặn" "blockConfigs" = "Cấu hình Chặn"
"blockConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các giao thức và trang web cụ thể." "blockConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các giao thức và trang web cụ thể."
"blockCountryConfigs" = "Cấu hình Chặn Quốc gia" "blockCountryConfigs" = "Cấu hình Chặn Quốc gia"
@@ -382,36 +375,23 @@
"GoogleIPv4Desc" = "Thêm định tuyến cho Google để kết nối qua IPv4." "GoogleIPv4Desc" = "Thêm định tuyến cho Google để kết nối qua IPv4."
"NetflixIPv4" = "Sử dụng IPv4 cho Netflix" "NetflixIPv4" = "Sử dụng IPv4 cho Netflix"
"NetflixIPv4Desc" = "Thêm định tuyến cho Netflix để kết nối qua IPv4." "NetflixIPv4Desc" = "Thêm định tuyến cho Netflix để kết nối qua IPv4."
"GoogleWARP" = "Google" "GoogleWARP" = "Định tuyến Google qua WARP."
"GoogleWARPDesc" = "Định tuyến lưu lượng truy cập tới Google thông qua WARP." "GoogleWARPDesc" = "Thêm định tuyến cho Google qua WARP."
"OpenAIWARP" = "OpenAI (ChatGPT)" "OpenAIWARP" = "Định tuyến OpenAI (ChatGPT) qua WARP."
"OpenAIWARPDesc" = "Định tuyến lưu lượng truy cập tới OpenAI (ChatGPT) thông qua WARP." "OpenAIWARPDesc" = "Thêm định tuyến cho OpenAI (ChatGPT) qua WARP."
"NetflixWARP" = "Netflix" "NetflixWARP" = "Định tuyến Netflix qua WARP."
"NetflixWARPDesc" = "Định tuyến lưu lượng truy cập tới Netflix thông qua WARP." "NetflixWARPDesc" = "Thêm định tuyến cho Netflix qua WARP."
"MetaWARP" = "Meta" "SpotifyWARP" = "Định tuyến Spotify qua WARP."
"MetaWARPDesc" = "Định tuyến lưu lượng truy cập tới Meta (Instagram, Facebook, WhatsApp, Threads,...) thông qua WARP." "SpotifyWARPDesc" = "Thêm định tuyến cho Spotify qua WARP."
"AppleWARP" = "Apple"
"AppleWARPDesc" = "Định tuyến lưu lượng truy cập tới Apple thông qua WARP."
"RedditWARP" = "Reddit"
"RedditWARPDesc" = "Định tuyến lưu lượng truy cập tới Reddit thông qua WARP."
"SpotifyWARP" = "Spotify"
"SpotifyWARPDesc" = "Định tuyến lưu lượng truy cập tới Spotify thông qua WARP."
"IRWARP" = "Định tuyến tên miền của Iran qua WARP." "IRWARP" = "Định tuyến tên miền của Iran qua WARP."
"IRWARPDesc" = "Thêm định tuyến cho các tên miền của Iran qua WARP." "IRWARPDesc" = "Thêm định tuyến cho các tên miền của Iran qua WARP."
"Inbounds" = "Đầu vào" "Inbounds" = "Đầu vào"
"InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể." "InboundsDesc" = "Thay đổi mẫu cấu hình để chấp nhận các máy khách cụ thể."
"Outbounds" = "Đầu ra" "Outbounds" = "Đầu ra"
"Balancers" = "Cân bằng"
"OutboundsDesc" = "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này." "OutboundsDesc" = "Thay đổi mẫu cấu hình để xác định các cách ra đi cho máy chủ này."
"Routings" = "Quy tắc định tuyến" "Routings" = "Quy tắc định tuyến"
"RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!" "RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!"
"completeTemplate" = "All" "completeTemplate" = "All"
"logLevel" = "Mức đăng nhập"
"logLevelDesc" = "Cấp độ nhật ký cho nhật ký lỗi, cho biết thông tin cần được ghi lại."
"accessLog" = "Nhật ký truy cập"
"accessLogDesc" = "Đường dẫn tệp cho nhật ký truy cập. Nhật ký truy cập bị vô hiệu hóa có giá trị đặc biệt 'không'"
"errorLog" = "Nhật ký lỗi"
"errorLogDesc" = "Đường dẫn tệp cho nhật ký lỗi. Nhật ký lỗi bị vô hiệu hóa có giá trị đặc biệt 'không'"
[pages.xray.rules] [pages.xray.rules]
"first" = "Đầu tiên" "first" = "Đầu tiên"
@@ -422,7 +402,6 @@
"dest" = "Đích" "dest" = "Đích"
"inbound" = "Vào" "inbound" = "Vào"
"outbound" = "Ra" "outbound" = "Ra"
"balancer" = "Cân bằng"
"info" = "Thông tin" "info" = "Thông tin"
"add" = "Thêm quy tắc" "add" = "Thêm quy tắc"
"edit" = "Chỉnh sửa quy tắc" "edit" = "Chỉnh sửa quy tắc"
@@ -443,15 +422,6 @@
"portal" = "Cổng thông tin" "portal" = "Cổng thông tin"
"intercon" = "Kết nối" "intercon" = "Kết nối"
[pages.xray.balancer]
"addBalancer" = "Thêm cân bằng"
"editBalancer" = "Chỉnh sửa cân bằng"
"balancerStrategy" = "Chiến lược"
"balancerSelectors" = "Bộ chọn"
"tag" = "Thẻ"
"tagDesc" = "thẻ duy nhất"
"balancerDesc" = "Không thể sử dụng balancerTag và outboundTag cùng một lúc. Nếu sử dụng cùng lúc thì chỉ outboundTag mới hoạt động."
[pages.xray.wireguard] [pages.xray.wireguard]
"secretKey" = "Khoá bí mật" "secretKey" = "Khoá bí mật"
"publicKey" = "Khóa công khai" "publicKey" = "Khóa công khai"
@@ -460,21 +430,6 @@
"psk" = "Khóa chia sẻ" "psk" = "Khóa chia sẻ"
"domainStrategy" = "Chiến lược tên miền" "domainStrategy" = "Chiến lược tên miền"
[pages.xray.dns]
"enable" = "Kích hoạt DNS"
"enableDesc" = "Kích hoạt máy chủ DNS tích hợp"
"strategy" = "Chiến lược truy vấn"
"strategyDesc" = "Chiến lược tổng thể để phân giải tên miền"
"add" = "Thêm máy chủ"
"edit" = "Chỉnh sửa máy chủ"
"domains" = "Tên miền"
[pages.xray.fakedns]
"add" = "Thêm DNS giả"
"edit" = "Chỉnh sửa DNS giả"
"ipPool" = "Mạng con nhóm IP"
"poolSize" = "Kích thước bể bơi"
[pages.settings.security] [pages.settings.security]
"admin" = "Quản trị viên" "admin" = "Quản trị viên"
"secret" = "Mã thông báo bí mật" "secret" = "Mã thông báo bí mật"

View File

@@ -52,9 +52,6 @@
"secretToken" = "安全密钥" "secretToken" = "安全密钥"
"remained" = "剩余" "remained" = "剩余"
"security" = "安全" "security" = "安全"
"secAlertTitle" = "安全警报"
"secAlertSsl" = "此连接不安全;在激活 TLS 进行数据保护之前,请勿输入敏感信息"
"secAlertConf" = "某些配置已被确定为容易受到攻击,促使立即采取行动以加强安全协议并防范潜在的安全漏洞。"
[menu] [menu]
"dashboard" = "系统状态" "dashboard" = "系统状态"
@@ -79,7 +76,7 @@
"title" = "系统状态" "title" = "系统状态"
"memory" = "内存" "memory" = "内存"
"hard" = "硬盘" "hard" = "硬盘"
"xrayStatus" = "Xray" "xrayStatus" = "状态"
"stopXray" = "停止" "stopXray" = "停止"
"restartXray" = "重启" "restartXray" = "重启"
"xraySwitch" = "版本" "xraySwitch" = "版本"
@@ -305,8 +302,6 @@
"subShowInfoDesc" = "在配置名称后显示剩余流量和日期" "subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
"subURI" = "反向代理 URI" "subURI" = "反向代理 URI"
"subURIDesc" = "更改订阅 URL 的基本 URI 以在代理后面使用" "subURIDesc" = "更改订阅 URL 的基本 URI 以在代理后面使用"
"fragment" = "碎片"
"fragmentDesc" = "启用 TLS hello 数据包分段"
[pages.xray] [pages.xray]
"title" = "Xray 设置" "title" = "Xray 设置"
@@ -316,8 +311,6 @@
"advancedTemplate" = "高级模板部件" "advancedTemplate" = "高级模板部件"
"generalConfigs" = "通用配置" "generalConfigs" = "通用配置"
"generalConfigsDesc" = "这些选项将提供一般调整" "generalConfigsDesc" = "这些选项将提供一般调整"
"logConfigs"="日志"
"logConfigsDesc" = "日志可能会影响您服务器的效率。建议仅在您需要时明智地启用它"
"blockConfigs" = "阻塞配置" "blockConfigs" = "阻塞配置"
"blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站" "blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站"
"blockCountryConfigs" = "阻止国家配置" "blockCountryConfigs" = "阻止国家配置"
@@ -382,36 +375,23 @@
"GoogleIPv4Desc" = "添加谷歌连接IPv4的路由" "GoogleIPv4Desc" = "添加谷歌连接IPv4的路由"
"NetflixIPv4" = "为 Netflix 使用 IPv4" "NetflixIPv4" = "为 Netflix 使用 IPv4"
"NetflixIPv4Desc" = "添加Netflix连接IPv4的路由" "NetflixIPv4Desc" = "添加Netflix连接IPv4的路由"
"GoogleWARP" = "Google" "GoogleWARP" = "将谷歌路由到 WARP"
"GoogleWARPDesc" = "通过 WARP 将流量路由到 Google。" "GoogleWARPDesc" = "为谷歌添加路由到WARP"
"OpenAIWARP" = "OpenAI (ChatGPT)" "OpenAIWARP" = "OpenAI (ChatGPT) 路由到 WARP"
"OpenAIWARPDesc" = "通过 WARP 将流量路由到 OpenAI (ChatGPT)。" "OpenAIWARPDesc" = "OpenAIChatGPT路由添加到WARP"
"NetflixWARP" = "Netflix" "NetflixWARP" = "Netflix 路由到 WARP"
"NetflixWARPDesc" = "通过 WARP 将流量路由到 Netflix。" "NetflixWARPDesc" = "为Netflix添加路由到WARP"
"MetaWARP"="Meta" "SpotifyWARP" = "将 Spotify 路由到 WARP"
"MetaWARPDesc" = "通过 WARP 将流量路由到 MetaInstagram、Facebook、WhatsApp、Threads..." "SpotifyWARPDesc" = "为Spotify添加路由到WARP"
"AppleWARP" = "Apple"
"AppleWARPDesc" = "通过 WARP 将流量路由到 Apple。"
"RedditWARP" = "Reddit"
"RedditWARPDesc" = "通过 WARP 将流量路由到 Reddit。"
"SpotifyWARP" = "Spotify"
"SpotifyWARPDesc" = "通过 WARP 将流量路由到 Spotify。"
"IRWARP" = "将伊朗域名路由到 WARP" "IRWARP" = "将伊朗域名路由到 WARP"
"IRWARPDesc" = "将伊朗域的路由添加到 WARP。 重启面板生效" "IRWARPDesc" = "将伊朗域的路由添加到 WARP。 重启面板生效"
"Inbounds" = "入站" "Inbounds" = "入站"
"InboundsDesc" = "更改配置模板接受特殊客户端" "InboundsDesc" = "更改配置模板接受特殊客户端"
"Outbounds" = "出站" "Outbounds" = "出站"
"Balancers" = "平衡器"
"OutboundsDesc" = "更改配置模板定义此服务器的传出方式" "OutboundsDesc" = "更改配置模板定义此服务器的传出方式"
"Routings" = "路由规则" "Routings" = "路由规则"
"RoutingsDesc" = "每条规则的优先级都很重要" "RoutingsDesc" = "每条规则的优先级都很重要"
"completeTemplate" = "全部" "completeTemplate" = "全部"
"logLevel" = "日志级别"
"logLevelDesc" = "错误日志的日志级别,表示需要记录的信息。"
"accessLog" = "访问日志"
"accessLogDesc" = "访问日志的文件路径。 特殊值“none”禁用访问日志"
"errorLog" = "错误日志"
"errorLogDesc" = "错误日志的文件路径。 特殊值“none”禁用错误日志"
[pages.xray.rules] [pages.xray.rules]
"first" = "第一个" "first" = "第一个"
@@ -422,7 +402,6 @@
"dest" = "目的地" "dest" = "目的地"
"inbound" = "入站" "inbound" = "入站"
"outbound" = "出站" "outbound" = "出站"
"balancer" = "平衡器"
"info" = "信息" "info" = "信息"
"add" = "添加规则" "add" = "添加规则"
"edit" = "编辑规则" "edit" = "编辑规则"
@@ -443,15 +422,6 @@
"portal" = "门户" "portal" = "门户"
"intercon" = "互连" "intercon" = "互连"
[pages.xray.balancer]
"addBalancer" = "添加平衡器"
"editBalancer" = "编辑平衡器"
"balancerStrategy" = "战略"
"balancerSelectors" = "选择器"
"tag" = "标签"
"tagDesc" = "唯一标记"
"balancerDesc" = "不能同时使用balancerTag和outboundTag。 如果同时使用则只有outboundTag起作用。"
[pages.xray.wireguard] [pages.xray.wireguard]
"secretKey" = "密钥" "secretKey" = "密钥"
"publicKey" = "公钥" "publicKey" = "公钥"
@@ -460,21 +430,6 @@
"psk" = "共享密钥" "psk" = "共享密钥"
"domainStrategy" = "域策略" "domainStrategy" = "域策略"
[pages.xray.dns]
"enable" = "启用 DNS"
"enableDesc" = "启用内置 DNS 服务器"
"strategy" = "查询策略"
"strategyDesc" = "解析域名的总体策略"
"add" = "添加服务器"
"edit" = "编辑服务器"
"domains" = "域"
[pages.xray.fakedns]
"add" = "添加假 DNS"
"edit" = "编辑假 DNS"
"ipPool" = "IP 池子网"
"poolSize" = "池大小"
[pages.settings.security] [pages.settings.security]
"admin" = "管理员" "admin" = "管理员"
"secret" = "密钥" "secret" = "密钥"

128
x-ui.sh
View File

@@ -150,12 +150,6 @@ custom_version() {
eval $install_command eval $install_command
} }
# Function to handle the deletion of the script file
delete_script() {
rm "$0" # Remove the script file itself
exit 1
}
uninstall() { uninstall() {
confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n" confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n"
if [[ $? != 0 ]]; then if [[ $? != 0 ]]; then
@@ -173,13 +167,12 @@ uninstall() {
rm /usr/local/x-ui/ -rf rm /usr/local/x-ui/ -rf
echo "" echo ""
echo -e "Uninstalled Successfully.\n" echo -e "Uninstalled Successfully, If you want to remove this script, then after exiting the script run ${green}rm /usr/bin/x-ui -f${plain} to delete it."
echo "If you need to install this panel again, you can use below command:"
echo -e "${green}bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh)${plain}"
echo "" echo ""
# Trap the SIGTERM signal
trap delete_script SIGTERM if [[ $# == 0 ]]; then
delete_script before_show_menu
fi
} }
reset_user() { reset_user() {
@@ -345,47 +338,6 @@ show_banlog() {
fi fi
} }
bbr_menu() {
echo -e "${green}\t1.${plain} Enable BBR"
echo -e "${green}\t2.${plain} Disable BBR"
echo -e "${green}\t0.${plain} Back to Main Menu"
read -p "Choose an option: " choice
case "$choice" in
0)
show_menu
;;
1)
enable_bbr
;;
2)
disable_bbr
;;
*) echo "Invalid choice" ;;
esac
}
disable_bbr() {
if ! grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf || ! grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then
echo -e "${yellow}BBR is not currently enabled.${plain}"
exit 0
fi
# Replace BBR with CUBIC configurations
sed -i 's/net.core.default_qdisc=fq/net.core.default_qdisc=pfifo_fast/' /etc/sysctl.conf
sed -i 's/net.ipv4.tcp_congestion_control=bbr/net.ipv4.tcp_congestion_control=cubic/' /etc/sysctl.conf
# Apply changes
sysctl -p
# Verify that BBR is replaced with CUBIC
if [[ $(sysctl net.ipv4.tcp_congestion_control | awk '{print $3}') == "cubic" ]]; then
echo -e "${green}BBR has been replaced with CUBIC successfully.${plain}"
else
echo -e "${red}Failed to replace BBR with CUBIC. Please check your system configuration.${plain}"
fi
}
enable_bbr() { enable_bbr() {
if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then if grep -q "net.core.default_qdisc=fq" /etc/sysctl.conf && grep -q "net.ipv4.tcp_congestion_control=bbr" /etc/sysctl.conf; then
echo -e "${green}BBR is already enabled!${plain}" echo -e "${green}BBR is already enabled!${plain}"
@@ -531,33 +483,6 @@ show_xray_status() {
fi fi
} }
firewall_menu() {
echo -e "${green}\t1.${plain} Install Firewall & open ports"
echo -e "${green}\t2.${plain} Allowed List"
echo -e "${green}\t3.${plain} Delete Ports from List"
echo -e "${green}\t4.${plain} Disable Firewall"
echo -e "${green}\t0.${plain} Back to Main Menu"
read -p "Choose an option: " choice
case "$choice" in
0)
show_menu
;;
1)
open_ports
;;
2)
sudo ufw status
;;
3)
delete_ports
;;
4)
sudo ufw disable
;;
*) echo "Invalid choice" ;;
esac
}
open_ports() { open_ports() {
if ! command -v ufw &>/dev/null; then if ! command -v ufw &>/dev/null; then
echo "ufw firewall is not installed. Installing now..." echo "ufw firewall is not installed. Installing now..."
@@ -610,37 +535,6 @@ open_ports() {
ufw status | grep $ports ufw status | grep $ports
} }
delete_ports() {
# Prompt the user to enter the ports they want to delete
read -p "Enter the ports you want to delete (e.g. 80,443,2053 or range 400-500): " ports
# Check if the input is valid
if ! [[ $ports =~ ^([0-9]+|[0-9]+-[0-9]+)(,([0-9]+|[0-9]+-[0-9]+))*$ ]]; then
echo "Error: Invalid input. Please enter a comma-separated list of ports or a range of ports (e.g. 80,443,2053 or 400-500)." >&2
exit 1
fi
# Delete the specified ports using ufw
IFS=',' read -ra PORT_LIST <<<"$ports"
for port in "${PORT_LIST[@]}"; do
if [[ $port == *-* ]]; then
# Split the range into start and end ports
start_port=$(echo $port | cut -d'-' -f1)
end_port=$(echo $port | cut -d'-' -f2)
# Loop through the range and delete each port
for ((i = start_port; i <= end_port; i++)); do
ufw delete allow $i
done
else
ufw delete allow "$port"
fi
done
# Confirm that the ports are deleted
echo "Deleted the specified ports:"
ufw status | grep $ports
}
update_geo() { update_geo() {
local defaultBinFolder="/usr/local/x-ui/bin" local defaultBinFolder="/usr/local/x-ui/bin"
read -p "Please enter x-ui bin folder path. Leave blank for default. (Default: '${defaultBinFolder}')" binFolder read -p "Please enter x-ui bin folder path. Leave blank for default. (Default: '${defaultBinFolder}')" binFolder
@@ -1230,10 +1124,10 @@ show_menu() {
${green}17.${plain} Cloudflare SSL Certificate ${green}17.${plain} Cloudflare SSL Certificate
${green}18.${plain} IP Limit Management ${green}18.${plain} IP Limit Management
${green}19.${plain} WARP Management ${green}19.${plain} WARP Management
${green}20.${plain} Firewall Management
———————————————— ————————————————
${green}21.${plain} Enable BBR ${green}20.${plain} Enable BBR
${green}22.${plain} Update Geo Files ${green}21.${plain} Update Geo Files
${green}22.${plain} Active Firewall and open ports
${green}23.${plain} Speedtest by Ookla ${green}23.${plain} Speedtest by Ookla
" "
show_status show_status
@@ -1301,13 +1195,13 @@ show_menu() {
warp_cloudflare warp_cloudflare
;; ;;
20) 20)
firewall_menu enable_bbr
;; ;;
21) 21)
bbr_menu update_geo
;; ;;
22) 22)
update_geo open_ports
;; ;;
23) 23)
run_speedtest run_speedtest

View File

@@ -213,7 +213,6 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
continue continue
} }
isInbound := matchs[1] == "inbound" isInbound := matchs[1] == "inbound"
isOutbound := matchs[1] == "outbound"
tag := matchs[2] tag := matchs[2]
isDown := matchs[3] == "downlink" isDown := matchs[3] == "downlink"
if tag == "api" { if tag == "api" {
@@ -222,9 +221,8 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
traffic, ok := tagTrafficMap[tag] traffic, ok := tagTrafficMap[tag]
if !ok { if !ok {
traffic = &Traffic{ traffic = &Traffic{
IsInbound: isInbound, IsInbound: isInbound,
IsOutbound: isOutbound, Tag: tag,
Tag: tag,
} }
tagTrafficMap[tag] = traffic tagTrafficMap[tag] = traffic
traffics = append(traffics, traffic) traffics = append(traffics, traffic)

View File

@@ -16,8 +16,7 @@ type Config struct {
API json_util.RawMessage `json:"api"` API json_util.RawMessage `json:"api"`
Stats json_util.RawMessage `json:"stats"` Stats json_util.RawMessage `json:"stats"`
Reverse json_util.RawMessage `json:"reverse"` Reverse json_util.RawMessage `json:"reverse"`
FakeDNS json_util.RawMessage `json:"fakedns"` FakeDNS json_util.RawMessage `json:"fakeDns"`
Observatory json_util.RawMessage `json:"observatory"`
} }
func (c *Config) Equals(other *Config) bool { func (c *Config) Equals(other *Config) bool {

View File

@@ -1,7 +1,6 @@
package xray package xray
import ( import (
"regexp"
"strings" "strings"
"x-ui/logger" "x-ui/logger"
) )
@@ -15,29 +14,37 @@ type LogWriter struct {
} }
func (lw *LogWriter) Write(m []byte) (n int, err error) { func (lw *LogWriter) Write(m []byte) (n int, err error) {
regex := regexp.MustCompile(`^(\d{4}/\d{2}/\d{2} \d{2}:\d{2}:\d{2}) \[([^\]]+)\] (.+)$`)
// Convert the data to a string // Convert the data to a string
message := strings.TrimSpace(string(m)) message := strings.TrimSpace(string(m))
messages := strings.Split(message, "\n") messages := strings.Split(message, "\n")
lw.lastLine = messages[len(messages)-1] lw.lastLine = messages[len(messages)-1]
for _, msg := range messages { for _, msg := range messages {
matches := regex.FindStringSubmatch(msg) messageBody := msg
if len(matches) > 3 { // Remove timestamp
level := matches[2] splittedMsg := strings.SplitN(msg, " ", 3)
msgBody := matches[3] if len(splittedMsg) > 2 {
messageBody = strings.TrimSpace(strings.SplitN(msg, " ", 3)[2])
}
// Find level in []
startIndex := strings.Index(messageBody, "[")
endIndex := strings.Index(messageBody, "]")
if startIndex != -1 && endIndex != -1 {
level := strings.TrimSpace(messageBody[startIndex+1 : endIndex])
msgBody := "XRAY: " + strings.TrimSpace(messageBody[endIndex+1:])
// Map the level to the appropriate logger function // Map the level to the appropriate logger function
switch level { switch level {
case "Debug": case "Debug":
logger.Debug("XRAY: " + msgBody) logger.Debug(msgBody)
case "Info": case "Info":
logger.Info("XRAY: " + msgBody) logger.Info(msgBody)
case "Warning": case "Warning":
logger.Warning("XRAY: " + msgBody) logger.Warning(msgBody)
case "Error": case "Error":
logger.Error("XRAY: " + msgBody) logger.Error(msgBody)
default: default:
logger.Debug("XRAY: " + msg) logger.Debug("XRAY: " + msg)
} }

View File

@@ -41,6 +41,10 @@ func GetIPLimitLogPath() string {
return config.GetLogFolder() + "/3xipl.log" return config.GetLogFolder() + "/3xipl.log"
} }
func GetIPLimitPrevLogPath() string {
return config.GetLogFolder() + "/3xipl.prev.log"
}
func GetIPLimitBannedLogPath() string { func GetIPLimitBannedLogPath() string {
return config.GetLogFolder() + "/3xipl-banned.log" return config.GetLogFolder() + "/3xipl-banned.log"
} }

View File

@@ -1,9 +1,8 @@
package xray package xray
type Traffic struct { type Traffic struct {
IsInbound bool IsInbound bool
IsOutbound bool Tag string
Tag string Up int64
Up int64 Down int64
Down int64
} }