Compare commits
98 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
d6cd0611d4 | ||
|
|
aad2cd8739 | ||
|
|
4cc0149317 | ||
|
|
836ee29b78 | ||
|
|
806b57f959 | ||
|
|
e827c1477c | ||
|
|
b827a4680d | ||
|
|
3fd124f76d | ||
|
|
3e8863f6ce | ||
|
|
9b026572cf | ||
|
|
6b34a3ae13 | ||
|
|
8b64180136 | ||
|
|
688ae4da10 | ||
|
|
0785da7223 | ||
|
|
2f1aad3e63 | ||
|
|
754b591e4f | ||
|
|
2b9d2d044c | ||
|
|
511eef54bb | ||
|
|
e705ae8e48 | ||
|
|
c7926d0bc0 | ||
|
|
034bc5e228 | ||
|
|
a39d07a68a | ||
|
|
81c9b4450b | ||
|
|
fc3ea2dd4b | ||
|
|
fe7a5f1813 | ||
|
|
3cd1b59a6c | ||
|
|
7ec6989c99 | ||
|
|
ee4d7a02a9 | ||
|
|
d6fd1c7ff0 | ||
|
|
5fbd5e8518 | ||
|
|
c31882cb92 | ||
|
|
81d47f7512 | ||
|
|
0baa204ce9 | ||
|
|
660e5ad101 | ||
|
|
aebf52efb2 | ||
|
|
c83a1db0c8 | ||
|
|
865d3e08e7 | ||
|
|
91ee6dc7cb | ||
|
|
7708bb9af2 | ||
|
|
03b7a34793 | ||
|
|
f3eb4f055d | ||
|
|
c61575ac9a | ||
|
|
f8796386dc | ||
|
|
ad38481312 | ||
|
|
937285ea3b | ||
|
|
328eeb8233 | ||
|
|
02239c8f2d | ||
|
|
70b3db074a | ||
|
|
d560cd9cc8 | ||
|
|
7526c4d969 | ||
|
|
766ef54b31 | ||
|
|
6b5535e60a | ||
|
|
fe00cfb09b | ||
|
|
2b4d6160c4 | ||
|
|
57029b1a40 | ||
|
|
61489077d7 | ||
|
|
4621933e5b | ||
|
|
fb76b2d500 | ||
|
|
9f6957ef3f | ||
|
|
d6e05d4a1a | ||
|
|
ea67b9760d | ||
|
|
3a503f12c8 | ||
|
|
2b7ad7cb9b | ||
|
|
9f38e19b81 | ||
|
|
73718a5dc5 | ||
|
|
bb9d00a0b3 | ||
|
|
3a1be63a40 | ||
|
|
4daaf0a647 | ||
|
|
f5dacd28e1 | ||
|
|
f65d3a5a98 | ||
|
|
13de2c6ca0 | ||
|
|
6cf29d5145 | ||
|
|
182710b86c | ||
|
|
4a1387ea83 | ||
|
|
c53cee31f5 | ||
|
|
222b9734ca | ||
|
|
c9ba393ce7 | ||
|
|
aa40016ec8 | ||
|
|
dc49304aa5 | ||
|
|
bb7b667467 | ||
|
|
d171850255 | ||
|
|
1207501405 | ||
|
|
2a2bf531ee | ||
|
|
9d724d34e1 | ||
|
|
618a566283 | ||
|
|
6804facabc | ||
|
|
98dd6bb949 | ||
|
|
f0e9aa0b8f | ||
|
|
68a16ef0e2 | ||
|
|
012775833a | ||
|
|
e4567a2b24 | ||
|
|
6c0775b120 | ||
|
|
9fbaede59f | ||
|
|
fd75cca266 | ||
|
|
9f904f8f47 | ||
|
|
78e1194ebb | ||
|
|
e04283c1fb | ||
|
|
a6742f395a |
49
.github/workflows/docker.yml
vendored
49
.github/workflows/docker.yml
vendored
@@ -1,5 +1,4 @@
|
||||
name: Release 3X-UI for Docker
|
||||
|
||||
on:
|
||||
push:
|
||||
tags:
|
||||
@@ -7,44 +6,36 @@ on:
|
||||
workflow_dispatch:
|
||||
|
||||
jobs:
|
||||
build:
|
||||
build_and_push:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
- name: Check out the code
|
||||
uses: actions/checkout@v4
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
submodules: true
|
||||
registry: ghcr.io
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- 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}}
|
||||
images: ghcr.io/${{ github.repository }}
|
||||
|
||||
- name: Set up QEMU
|
||||
uses: docker/setup-qemu-action@v3.0.0
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3.0.0
|
||||
|
||||
- name: Login to GHCR
|
||||
uses: docker/login-action@v3.0.0
|
||||
with:
|
||||
registry: ghcr.io
|
||||
username: ${{ github.repository_owner }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Build and push
|
||||
uses: docker/build-push-action@v5.1.0
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
with:
|
||||
context: .
|
||||
push: true
|
||||
platforms: linux/amd64,linux/arm64/v8,linux/arm/v7,linux/arm/v6,linux/386
|
||||
push: ${{ github.event_name != 'pull_request' }}
|
||||
platforms: linux/amd64, linux/arm64/v8, linux/arm/v7, linux/arm/v6, linux/386
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
13
.github/workflows/release.yml
vendored
13
.github/workflows/release.yml
vendored
@@ -20,12 +20,12 @@ jobs:
|
||||
runs-on: ubuntu-20.04
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4.1.1
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5.0.0
|
||||
uses: actions/setup-go@v5
|
||||
with:
|
||||
go-version: '1.21'
|
||||
go-version: '1.22'
|
||||
|
||||
- name: Install dependencies
|
||||
run: |
|
||||
@@ -77,7 +77,7 @@ jobs:
|
||||
cd x-ui/bin
|
||||
|
||||
# Download dependencies
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v1.8.7/"
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v1.8.8/"
|
||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||
wget ${Xray_URL}Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
@@ -116,12 +116,11 @@ jobs:
|
||||
- name: Package
|
||||
run: tar -zcvf x-ui-linux-${{ matrix.platform }}.tar.gz x-ui
|
||||
|
||||
- name: Upload
|
||||
uses: svenstaro/upload-release-action@2.7.0
|
||||
- name: Upload files to GH release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
prerelease: true
|
||||
overwrite: true
|
||||
|
||||
@@ -27,7 +27,7 @@ case $1 in
|
||||
esac
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.7/Xray-linux-${ARCH}.zip"
|
||||
wget "https://github.com/XTLS/Xray-core/releases/download/v1.8.8/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||
mv xray "xray-linux-${FNAME}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ========================================================
|
||||
# Stage: Builder
|
||||
# ========================================================
|
||||
FROM golang:1.21-alpine AS builder
|
||||
FROM golang:1.22-alpine AS builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
|
||||
@@ -28,12 +28,14 @@ WORKDIR /app
|
||||
RUN apk add --no-cache --update \
|
||||
ca-certificates \
|
||||
tzdata \
|
||||
fail2ban
|
||||
fail2ban \
|
||||
bash
|
||||
|
||||
COPY --from=builder /app/build/ /app/
|
||||
COPY --from=builder /app/DockerEntrypoint.sh /app/
|
||||
COPY --from=builder /app/x-ui.sh /usr/bin/x-ui
|
||||
|
||||
|
||||
# Configure fail2ban
|
||||
RUN rm -f /etc/fail2ban/jail.d/alpine-ssh.conf \
|
||||
&& cp /etc/fail2ban/jail.conf /etc/fail2ban/jail.local \
|
||||
@@ -47,4 +49,5 @@ RUN chmod +x \
|
||||
/usr/bin/x-ui
|
||||
|
||||
VOLUME [ "/etc/x-ui" ]
|
||||
CMD [ "./x-ui" ]
|
||||
ENTRYPOINT [ "/app/DockerEntrypoint.sh" ]
|
||||
|
||||
118
README.md
118
README.md
@@ -1,5 +1,7 @@
|
||||
# 3X-UI
|
||||
|
||||
<p align="center"><a href="#"><img src="./media/3X-UI.png" alt="Image"></a></p>
|
||||
|
||||
**An Advanced Web Panel • Built on Xray Core**
|
||||
|
||||
[](https://github.com/MHSanaei/3x-ui/releases)
|
||||
@@ -12,8 +14,7 @@
|
||||
|
||||
**If this project is helpful to you, you may wish to give it a**:star2:
|
||||
|
||||
<a href="#">
|
||||
<img width="125" alt="image" src="https://github.com/MHSanaei/3x-ui/assets/115543613/7aa895dd-048a-42e7-989b-afd41a74e2e1.jpg"></a>
|
||||
<p align="left"><a href="#"><img width="125" src="https://github.com/MHSanaei/3x-ui/assets/115543613/7aa895dd-048a-42e7-989b-afd41a74e2e1" alt="Image"></a></p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
|
||||
@@ -25,11 +26,39 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
## Install Custom Version
|
||||
|
||||
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.1.2`:
|
||||
To install your desired version, add the version to the end of the installation command. e.g., ver `v2.2.1`:
|
||||
|
||||
```
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.1.2
|
||||
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.1
|
||||
```
|
||||
|
||||
## 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
|
||||
|
||||
<details>
|
||||
@@ -41,7 +70,17 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
|
||||
case "${ARCH}" in
|
||||
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
|
||||
```
|
||||
|
||||
@@ -49,7 +88,16 @@ wget https://github.com/MHSanaei/3x-ui/releases/latest/download/x-ui-linux-${XUI
|
||||
|
||||
```sh
|
||||
ARCH=$(uname -m)
|
||||
[[ "${ARCH}" == "aarch64" || "${ARCH}" == "arm64" ]] && XUI_ARCH="arm64" || XUI_ARCH="amd64"
|
||||
case "${ARCH}" in
|
||||
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/
|
||||
rm -rf x-ui/ /usr/local/x-ui/ /usr/bin/x-ui
|
||||
tar zxvf x-ui-linux-${XUI_ARCH}.tar.gz
|
||||
@@ -136,22 +184,26 @@ remove 3x-ui from docker
|
||||
- AlmaLinux 9+
|
||||
- Rockylinux 9+
|
||||
|
||||
## Compatible Architectures & Devices
|
||||
## Supported Architectures and Devices
|
||||
|
||||
Supports a variety of different architectures and devices. Here are some of the main architectures that we support:
|
||||
<details>
|
||||
<summary>Click for Supported Architectures and devices details</summary>
|
||||
|
||||
- **amd64**: This is the most common architecture for personal computers and servers. It supports most modern operating systems.
|
||||
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:
|
||||
|
||||
- **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)
|
||||
- **amd64**: This prevalent architecture is the standard for personal computers and servers, accommodating most modern operating systems seamlessly.
|
||||
|
||||
- **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,...)
|
||||
- **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.
|
||||
|
||||
- **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,...)
|
||||
- **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.
|
||||
|
||||
- **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,...)
|
||||
- **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**: 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**: 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>
|
||||
|
||||
- **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)
|
||||
|
||||
## Languages
|
||||
|
||||
- English
|
||||
@@ -160,6 +212,8 @@ Supports a variety of different architectures and devices. Here are some of the
|
||||
- Russian
|
||||
- Vietnamese
|
||||
- Spanish
|
||||
- Indonesian
|
||||
- Ukrainian
|
||||
|
||||
|
||||
## Features
|
||||
@@ -201,34 +255,6 @@ Supports a variety of different architectures and devices. Here are some of the
|
||||
|
||||
</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)
|
||||
|
||||
<details>
|
||||
@@ -281,13 +307,13 @@ If you want to use routing to WARP before v2.1.0 follow steps as below:
|
||||
2. Select `IP Limit Management`.
|
||||
3. Choose the appropriate options based on your needs.
|
||||
|
||||
- make sure you have access.log on your Xray Configuration
|
||||
- make sure you have ./access.log on your Xray Configuration after v2.1.3 we have an option for it
|
||||
|
||||
```sh
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"access": "./access.log",
|
||||
"error": "./error.log"
|
||||
"dnsLog": false,
|
||||
"loglevel": "warning"
|
||||
},
|
||||
```
|
||||
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.1.2
|
||||
2.2.1
|
||||
@@ -21,6 +21,7 @@ var db *gorm.DB
|
||||
var initializers = []func() error{
|
||||
initUser,
|
||||
initInbound,
|
||||
initOutbound,
|
||||
initSetting,
|
||||
initInboundClientIps,
|
||||
initClientTraffic,
|
||||
@@ -51,6 +52,10 @@ func initInbound() error {
|
||||
return db.AutoMigrate(&model.Inbound{})
|
||||
}
|
||||
|
||||
func initOutbound() error {
|
||||
return db.AutoMigrate(&model.OutboundTraffics{})
|
||||
}
|
||||
|
||||
func initSetting() error {
|
||||
return db.AutoMigrate(&model.Setting{})
|
||||
}
|
||||
|
||||
@@ -44,6 +44,15 @@ type Inbound struct {
|
||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||
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 {
|
||||
Id int `json:"id" gorm:"primaryKey;autoIncrement"`
|
||||
ClientEmail string `json:"clientEmail" form:"clientEmail" gorm:"unique"`
|
||||
|
||||
51
go.mod
51
go.mod
@@ -1,29 +1,29 @@
|
||||
module x-ui
|
||||
|
||||
go 1.21.4
|
||||
go 1.22.0
|
||||
|
||||
require (
|
||||
github.com/Calidity/gin-sessions v1.3.1
|
||||
github.com/gin-contrib/gzip v0.0.6
|
||||
github.com/gin-gonic/gin v1.9.1
|
||||
github.com/goccy/go-json v0.10.2
|
||||
github.com/mymmrac/telego v0.28.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.3.0
|
||||
github.com/mymmrac/telego v0.29.1
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.1.1
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v3 v3.23.12
|
||||
github.com/valyala/fasthttp v1.51.0
|
||||
github.com/xtls/xray-core v1.8.7
|
||||
github.com/shirou/gopsutil/v3 v3.24.1
|
||||
github.com/valyala/fasthttp v1.52.0
|
||||
github.com/xtls/xray-core v1.8.8
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/text v0.14.0
|
||||
google.golang.org/grpc v1.61.0
|
||||
gorm.io/driver/sqlite v1.5.4
|
||||
gorm.io/gorm v1.25.6
|
||||
google.golang.org/grpc v1.62.0
|
||||
gorm.io/driver/sqlite v1.5.5
|
||||
gorm.io/gorm v1.25.7
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.0.6 // indirect
|
||||
github.com/andybalholm/brotli v1.1.0 // indirect
|
||||
github.com/bytedance/sonic v1.10.2 // indirect
|
||||
github.com/chenzhuoyu/base64x v0.0.0-20230717121745-296ad89f973d // indirect
|
||||
github.com/chenzhuoyu/iasm v0.9.1 // indirect
|
||||
@@ -40,31 +40,31 @@ require (
|
||||
github.com/go-task/slim-sprig v0.0.0-20230315185526-52ccab3ef572 // indirect
|
||||
github.com/golang/protobuf v1.5.3 // indirect
|
||||
github.com/google/btree v1.1.2 // indirect
|
||||
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 // indirect
|
||||
github.com/google/pprof v0.0.0-20240225044709-fd706174c886 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
github.com/gorilla/sessions v1.2.2 // 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/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/klauspost/compress v1.17.4 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.6 // indirect
|
||||
github.com/klauspost/compress v1.17.7 // indirect
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 // indirect
|
||||
github.com/leodido/go-urn v1.2.4 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20231016141302-07b5767bb0ed // indirect
|
||||
github.com/mattn/go-isatty v0.0.19 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.19 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.13.2 // indirect
|
||||
github.com/onsi/ginkgo/v2 v2.15.0 // indirect
|
||||
github.com/pires/go-proxyproto v0.7.0 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20221212215047-62379fc7944b // indirect
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 // indirect
|
||||
github.com/quic-go/quic-go v0.40.1 // indirect
|
||||
github.com/refraction-networking/utls v1.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.41.0 // indirect
|
||||
github.com/refraction-networking/utls v1.6.3 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.11.0 // indirect
|
||||
github.com/sagernet/sing v0.3.0 // indirect
|
||||
github.com/sagernet/sing v0.3.2 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.6 // indirect
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20220411075957-e3b120b3f5fb // indirect
|
||||
@@ -75,6 +75,7 @@ require (
|
||||
github.com/ugorji/go/codec v1.2.11 // indirect
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // 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/netns v0.0.4 // indirect
|
||||
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 // indirect
|
||||
@@ -82,16 +83,16 @@ require (
|
||||
go.uber.org/mock v0.4.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.6.0 // indirect
|
||||
golang.org/x/crypto v0.18.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
|
||||
golang.org/x/mod v0.14.0 // indirect
|
||||
golang.org/x/net v0.20.0 // indirect
|
||||
golang.org/x/sys v0.16.0 // indirect
|
||||
golang.org/x/crypto v0.19.0 // indirect
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 // indirect
|
||||
golang.org/x/mod v0.15.0 // indirect
|
||||
golang.org/x/net v0.21.0 // indirect
|
||||
golang.org/x/sys v0.17.0 // indirect
|
||||
golang.org/x/time v0.5.0 // indirect
|
||||
golang.org/x/tools v0.16.1 // indirect
|
||||
golang.org/x/tools v0.18.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20231211153847-12269c276173 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c // indirect
|
||||
google.golang.org/protobuf v1.32.0 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20231104011432-48a6d7d5bd0b // indirect
|
||||
|
||||
110
go.sum
110
go.sum
@@ -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/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/andybalholm/brotli v1.0.6 h1:Yf9fFpf49Zrxb9NlQaluyE92/+X7UVHlhMNJN2sxfOI=
|
||||
github.com/andybalholm/brotli v1.0.6/go.mod h1:fO7iG3H7G2nSZ7m0zPUDn85XEX2GTukHGRSepvi9Eig=
|
||||
github.com/andybalholm/brotli v1.1.0 h1:eLKJA0d02Lf0mVpIDgYnqXcUn0GqVmEFny3VuID1U3M=
|
||||
github.com/andybalholm/brotli v1.1.0/go.mod h1:sms7XGricyQI9K10gOSf56VKKWS4oLer58Q+mhRPtnY=
|
||||
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/bradfitz/go-smtpd v0.0.0-20170404230938-deb6d6237625/go.mod h1:HYsPBTaaSFSlLx/70C2HPIMNZpVV8+vt/A+FMnYP11g=
|
||||
@@ -88,8 +88,8 @@ github.com/golang/glog v0.0.0-20160126235308-23def4e6c14b/go.mod h1:SBH7ygxi8pfU
|
||||
github.com/golang/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:tluoj9z5200jBnyusfRPU2LqT6J+DAorxEvtC7LHB+E=
|
||||
github.com/golang/mock v1.1.1/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.2.0/go.mod h1:oTYuIxOrZwtPieC+H1uAHpcLFnEyAGVDL/k47Jfbm0A=
|
||||
github.com/golang/mock v1.6.0 h1:ErTB+efbowRARo13NNdxyJji2egdxLGQhRaY+DUumQc=
|
||||
github.com/golang/mock v1.6.0/go.mod h1:p6yTPP+5HYm5mzsMV8JkE6ZKdX+/wYM6Hr+LicevLPs=
|
||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||
github.com/golang/mock v1.7.0-rc.1/go.mod h1:s42URUywIqd+OcERslBJvOjepvNymP31m3q8d/GkuRs=
|
||||
github.com/golang/protobuf v1.2.0/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.3.1/go.mod h1:6lQm79b+lXiMfvg/cZm0SGofjICqVBUtrP5yJMmIC1U=
|
||||
github.com/golang/protobuf v1.5.0/go.mod h1:FsONVRAS9T7sI+LIUmWTfcYkHO4aIWwzhcaSAoJOfIk=
|
||||
@@ -111,8 +111,8 @@ github.com/google/gofuzz v1.2.0 h1:xRy4A+RhZaiKjJ1bPfwQ8sedCA+YS2YcCHW6ec7JMi0=
|
||||
github.com/google/gofuzz v1.2.0/go.mod h1:dBl0BpW6vV/+mYPU4Po3pmUjxk6FQPldtuIdl/M65Eg=
|
||||
github.com/google/martian v2.1.0+incompatible/go.mod h1:9I4somxYTbIHy5NJKHRl3wXiIaQGbYVAs8BPL6v8lEs=
|
||||
github.com/google/pprof v0.0.0-20181206194817-3ea8567a2e57/go.mod h1:zfwlbNMJ+OItoe0UupaVj+oy1omPYYDuagoSzA8v9mc=
|
||||
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42 h1:dHLYa5D8/Ta0aLR2XcPsrkpAgGeFs6thhMcQK0oQ0n8=
|
||||
github.com/google/pprof v0.0.0-20231229205709-960ae82b1e42/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/google/pprof v0.0.0-20240225044709-fd706174c886 h1:JSJUTZTQT1Gzb2ROdAKOY3HwzBYcclS2GgumhMfHqjw=
|
||||
github.com/google/pprof v0.0.0-20240225044709-fd706174c886/go.mod h1:czg5+yv1E0ZGTi6S6vVK1mke0fV+FaUhNGcd6VRS9Ik=
|
||||
github.com/googleapis/gax-go v2.0.0+incompatible/go.mod h1:SFVmujtThgffbyetf+mdk2eWhX2bMyUtNHzFKcPA9HY=
|
||||
github.com/googleapis/gax-go/v2 v2.0.3/go.mod h1:LLvjysVCY1JZeum8Z6l8qUty8fiNwE08qbEPm1M08qg=
|
||||
github.com/gopherjs/gopherjs v0.0.0-20181017120253-0766667cb4d1/go.mod h1:wJfORRmW1u3UXTncJ5qlYoELFm8eSnnEO6hX4iZ3EWY=
|
||||
@@ -124,6 +124,8 @@ 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/websocket v1.5.1 h1:gmztn0JnHVt9JZquRuzLw3g4wouNVzKL15iLr/zn/QY=
|
||||
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/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=
|
||||
@@ -136,11 +138,11 @@ 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/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/klauspost/compress v1.17.4 h1:Ej5ixsIri7BrIjBkRZLTo6ghwrEtHFk7ijlczPW4fZ4=
|
||||
github.com/klauspost/compress v1.17.4/go.mod h1:/dCuZOvVtNoHsyb+cuJD3itjs3NbnF6KH9zAO4BDxPM=
|
||||
github.com/klauspost/compress v1.17.7 h1:ehO88t2UGzQK66LMdE8tibEd1ErmzZjNEqWkjLAKQQg=
|
||||
github.com/klauspost/compress v1.17.7/go.mod h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
|
||||
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/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7 h1:ZWSB3igEs+d0qvnxR/ZBzXVmxkgt8DdzP6m9pfuVLDM=
|
||||
github.com/klauspost/cpuid/v2 v2.2.7/go.mod h1:Lcz8mBdAVJIBVzewtcLocK12l3Y+JytZYpaMropDUws=
|
||||
github.com/knz/go-libedit v1.10.1/go.mod h1:MZTVkCWyz0oBc7JOWP3wNAzd002ZbM/5hgShxwh4x8M=
|
||||
github.com/kr/pretty v0.1.0/go.mod h1:dAy3ld7l9f0ibDNOQOHHMYYIIbhfbHSm3C4ZsoJORNo=
|
||||
github.com/kr/pretty v0.2.1/go.mod h1:ipq/a2n7PKx3OHsz4KJII5eveXtPO4qwEXGdVfWzfnI=
|
||||
@@ -167,24 +169,24 @@ github.com/mattn/go-sqlite3 v1.14.19 h1:fhGleo2h1p8tVChob4I9HpmVFIAkKGpiukdrgQbW
|
||||
github.com/mattn/go-sqlite3 v1.14.19/go.mod h1:2eHXhiwb8IkHr+BDWZGa96P6+rkvnG63S2DGjv9HUNg=
|
||||
github.com/matttproud/golang_protobuf_extensions v1.0.1/go.mod h1:D8He9yQNgCq6Z5Ld7szi9bcBfOoFv/3dc6xSMkL2PC0=
|
||||
github.com/microcosm-cc/bluemonday v1.0.1/go.mod h1:hsXNsILzKxV+sX77C5b8FSuKF00vh2OMYv+xgHpAMF4=
|
||||
github.com/miekg/dns v1.1.57 h1:Jzi7ApEIzwEPLHWRcafCN9LZSBbqQpxjt/wpgvg7wcM=
|
||||
github.com/miekg/dns v1.1.57/go.mod h1:uqRjCRUuEAA6qsOiJvDd+CFo/vW+y5WR6SNmHE55hZk=
|
||||
github.com/miekg/dns v1.1.58 h1:ca2Hdkz+cDg/7eNF6V56jjzuZ4aCAE+DbVkILdQWG/4=
|
||||
github.com/miekg/dns v1.1.58/go.mod h1:Ypv+3b/KadlvW9vJfXOTf300O4UqaHFzFCuHz+rPkBY=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w8PVh93nsPXa1VrQ6jlwL5oN8l14QlcNfg=
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
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/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mymmrac/telego v0.28.0 h1:DNXaYISeZw1J9oB81vCNdskLow8gCRRUJxufqLuH3XE=
|
||||
github.com/mymmrac/telego v0.28.0/go.mod h1:oRperySNzJq8dRTl24+uBF1Uy7tlQGIjid/JQtHDsZg=
|
||||
github.com/mymmrac/telego v0.29.1 h1:nsNnK0mS18OL+unoDjDI6BVfafJBbT8Wtj7rCzEWoM8=
|
||||
github.com/mymmrac/telego v0.29.1/go.mod h1:ZLD1+L2TQRr97NPOCoN1V2w8y9kmFov33OfZ3qT8cF4=
|
||||
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/nicksnyder/go-i18n/v2 v2.3.0 h1:2NPsCsNFCVd7i+Su0xYsBrIhS3bE2XMv5gNTft2O+PQ=
|
||||
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/go.mod h1:XStQ8QcGwLyF4HdfcZB8SFOS/MWCgDuXMSBe6zrvLgM=
|
||||
github.com/onsi/gomega v1.29.0 h1:KIA/t2t5UBzoirT4H9tsML45GEbo3ouUnBHsCfD2tVg=
|
||||
github.com/onsi/gomega v1.29.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.4.0/go.mod h1:nxYSZE9M0bf3Y70gPQjN9ha7XNHX7gMc814+6wVyEI4=
|
||||
github.com/onsi/ginkgo/v2 v2.15.0 h1:79HwNRBAZHOEwrczrgSOPy+eFTTlIGELKy5as+ClttY=
|
||||
github.com/onsi/ginkgo/v2 v2.15.0/go.mod h1:HlxMHtYF57y6Dpf+mc5529KKmSq9h2FpCF+/ZkwUxKM=
|
||||
github.com/onsi/gomega v1.30.0 h1:hvMK7xYz4D3HapigLTeGdId/NcfQx1VHMJc60ew99+8=
|
||||
github.com/onsi/gomega v1.30.0/go.mod h1:9sxs+SwGrKI0+PWe4Fxa9tFQQBG5xSsSbMXOI8PPpoQ=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
|
||||
github.com/openzipkin/zipkin-go v0.1.1/go.mod h1:NtoC/o8u3JlF1lSlyPNswIbeQH9bJTmOf0Erfk+hxe8=
|
||||
@@ -206,12 +208,10 @@ github.com/prometheus/client_golang v0.8.0/go.mod h1:7SWBe2y4D6OKWSNQJUaRYU/AaXP
|
||||
github.com/prometheus/client_model v0.0.0-20180712105110-5c3871d89910/go.mod h1:MbSGuTsp3dbXC40dX6PRTWyKYBIrTGTE9sqQNg2J8bo=
|
||||
github.com/prometheus/common v0.0.0-20180801064454-c7de2306084e/go.mod h1:daVV7qP5qjZbuso7PdcryaAu0sAZbrN9i7WWcTMWvro=
|
||||
github.com/prometheus/procfs v0.0.0-20180725123919-05ee40e3a273/go.mod h1:c3At6R/oaqEKCNdg8wHV1ftS6bRYblBhIjjI8uT2IGk=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1 h1:D33340mCNDAIKBqXuAvexTNMUByrYmFYVfKfDN5nfFs=
|
||||
github.com/quic-go/qtls-go1-20 v0.4.1/go.mod h1:X9Nh97ZL80Z+bX/gUXMbipO6OxdiDi58b/fMC9mAL+k=
|
||||
github.com/quic-go/quic-go v0.40.1 h1:X3AGzUNFs0jVuO3esAGnTfvdgvL4fq655WaOi1snv1Q=
|
||||
github.com/quic-go/quic-go v0.40.1/go.mod h1:PeN7kuVJ4xZbxSv/4OX6S1USOX8MJvydwpTx31vx60c=
|
||||
github.com/refraction-networking/utls v1.6.0 h1:X5vQMqVx7dY7ehxxqkFER/W6DSjy8TMqSItXm8hRDYQ=
|
||||
github.com/refraction-networking/utls v1.6.0/go.mod h1:kHJ6R9DFFA0WsRgBM35iiDku4O7AqPR6y79iuzW7b10=
|
||||
github.com/quic-go/quic-go v0.41.0 h1:aD8MmHfgqTURWNJy48IYFg2OnxwHT3JL7ahGs73lb4k=
|
||||
github.com/quic-go/quic-go v0.41.0/go.mod h1:qCkNjqczPEvgsOnxZ0eCD14lv+B2LHlFAB++CNOh9hA=
|
||||
github.com/refraction-networking/utls v1.6.3 h1:MFOfRN35sSx6K5AZNIoESsBuBxS2LCgRilRIdHb6fDc=
|
||||
github.com/refraction-networking/utls v1.6.3/go.mod h1:yil9+7qSl+gBwJqztoQseO6Pr3h62pQoY1lXiNR/FPs=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3/go.mod h1:HgjTstvQsPGkxUsCd2KWxErBblirPizecHcpD3ffK+s=
|
||||
github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
@@ -221,8 +221,8 @@ github.com/rogpeppe/go-internal v1.8.0/go.mod h1:WmiCO8CzOY8rg0OYDC4/i/2WRWAB6po
|
||||
github.com/rogpeppe/go-internal v1.11.0 h1:cWPaGQEPrBb5/AsnsZesgZZ9yb1OQ+GOISoDNXVBh4M=
|
||||
github.com/rogpeppe/go-internal v1.11.0/go.mod h1:ddIwULY96R17DhadqLgMfk9H9tvdUzkipdSkR5nkCZA=
|
||||
github.com/russross/blackfriday v1.5.2/go.mod h1:JO/DiYxRf+HjHt06OyowR9PTA263kcR/rfWxYHBV53g=
|
||||
github.com/sagernet/sing v0.3.0 h1:PIDVFZHnQAAYRL1UYqNM+0k5s8f/tb1lUW6UDcQiOc8=
|
||||
github.com/sagernet/sing v0.3.0/go.mod h1:9pfuAH6mZfgnz/YjP6xu5sxx882rfyjpcrTdUpd6w3g=
|
||||
github.com/sagernet/sing v0.3.2 h1:CwWcxUBPkMvwgfe2/zUgY5oHG9qOL8Aob/evIFYK9jo=
|
||||
github.com/sagernet/sing v0.3.2/go.mod h1:qHySJ7u8po9DABtMYEkNBcOumx7ZZJf/fbv2sfTkNHE=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.6 h1:xr7ylAS/q1cQYS8oxKKajhuQcchd5VJJ4K4UZrrpp0s=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.6/go.mod h1:j2YZBIpWIuElPFL/5sJAj470bcn/3QQ5lxZUNKLDNAM=
|
||||
github.com/savsgio/gotils v0.0.0-20230208104028-c358bd845dee h1:8Iv5m6xEo1NR1AvpV+7XmhI4r39LGNzwUL4YpMuL5vk=
|
||||
@@ -230,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/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/sergi/go-diff v1.0.0/go.mod h1:0CfEIISq7TuYL3j771MWULgwwjU+GofnZX9QAmXWZgo=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12 h1:z90NtUkp3bMtmICZKpC4+WaknU1eXtp5vtbQ11DgpE4=
|
||||
github.com/shirou/gopsutil/v3 v3.23.12/go.mod h1:1FrWgea594Jp7qmjHUUPlJDTPgcsb9mGnXDxavtikzM=
|
||||
github.com/shirou/gopsutil/v3 v3.24.1 h1:R3t6ondCEvmARp3wxODhXMTLC/klMa87h2PHUw5m7QI=
|
||||
github.com/shirou/gopsutil/v3 v3.24.1/go.mod h1:UU7a2MSBQa+kW1uuDq8DeEBS8kmrnQwsv2b5O513rwU=
|
||||
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/test v0.6.4 h1:kVTaSd7WLz5WZ2IaoM0RSzRsUD+m8wRR+5qvntpn4LU=
|
||||
@@ -288,8 +288,10 @@ 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/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.51.0 h1:8b30A5JlZ6C7AS81RsWjYMQmrZG6feChmgAolCl1SqA=
|
||||
github.com/valyala/fasthttp v1.51.0/go.mod h1:oI2XroL+lI7vdXyYoQk03bXBThfFl2cVdIA3Xl7cH8g=
|
||||
github.com/valyala/fasthttp v1.52.0 h1:wqBQpxH71XW0e2g+Og4dzQM8pk34aFYlA1Ga8db7gU0=
|
||||
github.com/valyala/fasthttp v1.52.0/go.mod h1:hf5C4QnVMkNXMspnsUlfM3WitlgYflyhHYoKol/szxQ=
|
||||
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/toolbox v0.24.0/go.mod h1:OxMCG57V0PXuIP2HNQrtJf2CjqdmbrOx5EkMILuUhzM=
|
||||
github.com/vishvananda/netlink v1.2.1-beta.2.0.20230316163032-ced5aaba43e3 h1:tkMT5pTye+1NlKIXETU78NXw0fyjnaNHmJyyLyzw8+U=
|
||||
@@ -299,8 +301,8 @@ github.com/vishvananda/netns v0.0.4 h1:Oeaw1EM2JMxD51g9uhtC0D7erkIjgmj8+JZc26m1Y
|
||||
github.com/vishvananda/netns v0.0.4/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19 h1:capMfFYRgH9BCLd6A3Er/cH3A9Nz3CU2KwxwOQZIePI=
|
||||
github.com/xtls/reality v0.0.0-20231112171332-de1173cf2b19/go.mod h1:dm4y/1QwzjGaK17ofi0Vs6NpKAHegZky8qk6J2JJZAE=
|
||||
github.com/xtls/xray-core v1.8.7 h1:lb8O1l3/eAg3YAXA6tLm5M6N7BsX8wxW9sJLjU3dHkA=
|
||||
github.com/xtls/xray-core v1.8.7/go.mod h1:9rFpflfQbgFeH1VKJw7yUmEy7myOyDCgNXXl0bmmyOo=
|
||||
github.com/xtls/xray-core v1.8.8 h1:6ApBa5pNkPZ+I7jyJDZod3v5sadizS/BZr0pW7zcs8o=
|
||||
github.com/xtls/xray-core v1.8.8/go.mod h1:Zp33A8cxnhP5Kt6nguQrMgNH4A/tgq7LE8cBedeNje8=
|
||||
github.com/yusufpapurcu/wmi v1.2.3 h1:E1ctvB7uKFMOJw3fdOW32DwGE9I7t++CRUEMKvFoFiw=
|
||||
github.com/yusufpapurcu/wmi v1.2.3/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opencensus.io v0.18.0/go.mod h1:vKdFvxhtzZ9onBp9VKHK8z/sRpBMnKAsufL7wlDrCOA=
|
||||
@@ -319,16 +321,16 @@ 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-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.18.0 h1:PGVlW0xEltQnzFZ55hkuX5+KLyrMYhHld1YHO4AKcdc=
|
||||
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
|
||||
golang.org/x/crypto v0.19.0 h1:ENy+Az/9Y1vSrlrvBSyna3PITt4tiZLf7sgCjZBX7Wo=
|
||||
golang.org/x/crypto v0.19.0/go.mod h1:Iy9bg/ha4yyC70EfRS8jz+B6ybOBKMaSxLj6P6oBDfU=
|
||||
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/go.mod h1:iRJReGqOEeBhDZGkGbynYwcHlctCvnjTYIamk7uXpHI=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225 h1:LfspQV/FYTatPTr/3HzIcmiUFH7PGP+OQ6mgDYo3yuQ=
|
||||
golang.org/x/exp v0.0.0-20240222234643-814bf88cf225/go.mod h1:CxmFvTBINI24O/j8iY7H1xHzx2i4OsyguNBmN/uPtqc=
|
||||
golang.org/x/lint v0.0.0-20180702182130-06c8688daad7/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20181026193005-c67002cb31c3/go.mod h1:UVdnD1Gm6xHRNCYTkRU2/jEulfH38KcIWyp/GAMgvoE=
|
||||
golang.org/x/lint v0.0.0-20190227174305-5b3e6a55c961/go.mod h1:wehouNa3lNwaWXcvxsM5YxQ5yQlVC4a0KAMCusXpPoU=
|
||||
golang.org/x/mod v0.14.0 h1:dGoOF9QVLYng8IHTm7BAyWqCqSheQ5pYWGhzW00YJr0=
|
||||
golang.org/x/mod v0.14.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/mod v0.15.0 h1:SernR4v+D55NyBH2QiEQrlBAnj1ECL6AGrA5+dPaMY8=
|
||||
golang.org/x/mod v0.15.0/go.mod h1:hTbmBsO62+eylJbnUtE2MGJUyE7QWk4xUqPFrRgJ+7c=
|
||||
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180826012351-8a410e7b638d/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
|
||||
@@ -338,8 +340,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-20190313220215-9f648a60d977/go.mod h1:t9HGtf8HONx5eT2rtn7q6eTqICYqUVnKs3thJo3Qplg=
|
||||
golang.org/x/net v0.0.0-20210226172049-e18ecbb05110/go.mod h1:m0MpNAwzfU5UDzcl9v0D8zg8gWTRqZa9RBIspLL5mdg=
|
||||
golang.org/x/net v0.20.0 h1:aCL9BSgETF1k+blQaYUBx9hJ9LOGP3gAVemcZlf1Kpo=
|
||||
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
|
||||
golang.org/x/net v0.21.0 h1:AQyQV4dYCvJ7vGmJyKki9+PBdyvhkSd8EIx/qb0AYv4=
|
||||
golang.org/x/net v0.21.0/go.mod h1:bIjVDfnllIU7BJ2DNgfnXvpSvtn8VRwhlsaeUTyUS44=
|
||||
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-20181203162652-d668ce993890/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
|
||||
@@ -369,9 +371,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.8.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.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/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=
|
||||
@@ -388,8 +390,8 @@ golang.org/x/tools v0.0.0-20180917221912-90fa682c2a6e/go.mod h1:n7NCudcB/nEzxVGm
|
||||
golang.org/x/tools v0.0.0-20181030000716-a0a13e073c7b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190114222345-bf090417da8b/go.mod h1:n7NCudcB/nEzxVGmLbDWY5pfWTLqBcC2KZ6jyYvM4mQ=
|
||||
golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3HoIrodX9oNMXvdceNzlUR8zjMvY=
|
||||
golang.org/x/tools v0.16.1 h1:TLyB3WofjdOEepBHAU20JdNC1Zbg87elYofWYAY5oZA=
|
||||
golang.org/x/tools v0.16.1/go.mod h1:kYVVN6I1mBNoB1OX+noeBjbRk4IUEPa7JJ+TJMEooJ0=
|
||||
golang.org/x/tools v0.18.0 h1:k8NLag8AGHnn+PHbl7g43CtqZAwG60vZkLqgyZgIHgQ=
|
||||
golang.org/x/tools v0.18.0/go.mod h1:GL7B4CwcLLeo59yx/9UWWuNOW1n3VZ4f5axWfML7Lcg=
|
||||
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2/go.mod h1:deeaetjYA+DHMHg+sMSMI58GrEteJUUzzw7en6TJQcI=
|
||||
@@ -407,14 +409,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-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/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1 h1:gphdwh0npgs8elJ4T6J+DQJHPVF7RsuJHCfwztUb4J4=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240108191215-35c7eff3a6b1/go.mod h1:daQN87bsDqDoe316QbbvX60nMoJQa4r6Ds0ZuoAe5yA=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c h1:NUsgEN92SQQqzfA+YtqYNqYmB3DMMYLlIwUZAQFVFbo=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20240221002015-b0ce06bbee7c/go.mod h1:H4O17MA/PE9BsGx3w+a+W2VOLLD1Qf7oJneAoU6WktY=
|
||||
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.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.61.0 h1:TOvOcuXn30kRao+gfcvsebNEa5iZIiLkisYEkf7R7o0=
|
||||
google.golang.org/grpc v1.61.0/go.mod h1:VUbo7IFqmF1QtCAstipjG0GIoq49KvMe9+h1jFLBNJs=
|
||||
google.golang.org/grpc v1.62.0 h1:HQKZ/fa1bXkX1oFOvSjmZEUL8wLSaZTjCcLAlmZRtdk=
|
||||
google.golang.org/grpc v1.62.0/go.mod h1:IWTG0VlJLCh1SkC58F7np9ka9mx/WNkjl4PGJaiq+QE=
|
||||
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.28.0/go.mod h1:HV8QOd/L58Z+nl8r43ehVNZIU/HEI6OcFqwMG9pJV4I=
|
||||
@@ -435,10 +437,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.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.5.4 h1:IqXwXi8M/ZlPzH/947tn5uik3aYQslP9BVveoax0nV0=
|
||||
gorm.io/driver/sqlite v1.5.4/go.mod h1:qxAuCol+2r6PannQDpOP1FP6ag3mKi4esLnB/jHed+4=
|
||||
gorm.io/gorm v1.25.6 h1:V92+vVda1wEISSOMtodHVRcUIOPYa2tgQtyF+DfFx+A=
|
||||
gorm.io/gorm v1.25.6/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
gorm.io/driver/sqlite v1.5.5 h1:7MDMtUZhV065SilG62E0MquljeArQZNfJnjd9i9gx3E=
|
||||
gorm.io/driver/sqlite v1.5.5/go.mod h1:6NgQ7sQWAIFsPrJJl1lSNSu2TABh0ZZ/zm5fosATavE=
|
||||
gorm.io/gorm v1.25.7 h1:VsD6acwRjz2zFxGO50gPO6AkNs7KKnvfzUjHQhZDz/A=
|
||||
gorm.io/gorm v1.25.7/go.mod h1:hbnx/Oo0ChWMn1BIhpy1oYozzpM15i4YPuHDmfYtwg8=
|
||||
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/go.mod h1:10sU+Uh5KKNv1+2x2A0Gvzt8FjD3ASIhorV3YsauXhk=
|
||||
|
||||
@@ -82,16 +82,16 @@ fi
|
||||
install_base() {
|
||||
case "${release}" in
|
||||
centos | almalinux | rocky)
|
||||
yum -y update && yum install -y -q wget curl tar
|
||||
yum -y update && yum install -y -q wget curl tar tzdata
|
||||
;;
|
||||
fedora)
|
||||
dnf -y update && dnf install -y -q wget curl tar
|
||||
dnf -y update && dnf install -y -q wget curl tar tzdata
|
||||
;;
|
||||
arch | manjaro)
|
||||
pacman -Syu && pacman -Syu --noconfirm wget curl tar
|
||||
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
|
||||
;;
|
||||
*)
|
||||
apt-get update && apt install -y -q wget curl tar
|
||||
apt-get update && apt install -y -q wget curl tar tzdata
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
@@ -65,6 +65,16 @@ func Infof(format string, args ...interface{}) {
|
||||
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{}) {
|
||||
logger.Warning(args...)
|
||||
addToBuffer("WARNING", fmt.Sprint(args...))
|
||||
|
||||
BIN
media/3X-UI.png
Normal file
BIN
media/3X-UI.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 198 KiB |
105
sub/default.json
Normal file
105
sub/default.json
Normal file
@@ -0,0 +1,105 @@
|
||||
{
|
||||
"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": {}
|
||||
}
|
||||
44
sub/sub.go
44
sub/sub.go
@@ -47,11 +47,6 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
subPath, err := s.settingService.GetSubPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
subDomain, err := s.settingService.GetSubDomain()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -61,9 +56,44 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
|
||||
}
|
||||
|
||||
g := engine.Group(subPath)
|
||||
LinksPath, err := s.settingService.GetSubPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.sub = NewSUBController(g)
|
||||
JsonPath, err := s.settingService.GetSubJsonPath()
|
||||
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
|
||||
}
|
||||
|
||||
@@ -3,34 +3,56 @@ package sub
|
||||
import (
|
||||
"encoding/base64"
|
||||
"strings"
|
||||
"x-ui/web/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
type SUBController struct {
|
||||
subService SubService
|
||||
settingService service.SettingService
|
||||
subPath string
|
||||
subJsonPath string
|
||||
subEncrypt bool
|
||||
updateInterval string
|
||||
|
||||
subService *SubService
|
||||
subJsonService *SubJsonService
|
||||
}
|
||||
|
||||
func NewSUBController(g *gin.RouterGroup) *SUBController {
|
||||
a := &SUBController{}
|
||||
func NewSUBController(
|
||||
g *gin.RouterGroup,
|
||||
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)
|
||||
return a
|
||||
}
|
||||
|
||||
func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/")
|
||||
gLink := g.Group(a.subPath)
|
||||
gJson := g.Group(a.subJsonPath)
|
||||
|
||||
g.GET("/:subid", a.subs)
|
||||
gLink.GET(":subid", a.subs)
|
||||
|
||||
gJson.GET(":subid", a.subJsons)
|
||||
}
|
||||
|
||||
func (a *SUBController) subs(c *gin.Context) {
|
||||
subEncrypt, _ := a.settingService.GetSubEncrypt()
|
||||
subShowInfo, _ := a.settingService.GetSubShowInfo()
|
||||
subId := c.Param("subid")
|
||||
host := strings.Split(c.Request.Host, ":")[0]
|
||||
subs, headers, err := a.subService.GetSubs(subId, host, subShowInfo)
|
||||
subs, header, err := a.subService.GetSubs(subId, host)
|
||||
if err != nil || len(subs) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
@@ -40,14 +62,31 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||
}
|
||||
|
||||
// Add headers
|
||||
c.Writer.Header().Set("Subscription-Userinfo", headers[0])
|
||||
c.Writer.Header().Set("Profile-Update-Interval", headers[1])
|
||||
c.Writer.Header().Set("Profile-Title", headers[2])
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", subId)
|
||||
|
||||
if subEncrypt {
|
||||
if a.subEncrypt {
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
} else {
|
||||
c.String(200, result)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (a *SUBController) subJsons(c *gin.Context) {
|
||||
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)
|
||||
}
|
||||
}
|
||||
|
||||
355
sub/subJsonService.go
Normal file
355
sub/subJsonService.go
Normal file
@@ -0,0 +1,355 @@
|
||||
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"`
|
||||
}
|
||||
@@ -25,47 +25,42 @@ type SubService struct {
|
||||
settingService service.SettingService
|
||||
}
|
||||
|
||||
func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string, []string, error) {
|
||||
func NewSubService(showInfo bool, remarkModel string) *SubService {
|
||||
return &SubService{
|
||||
showInfo: showInfo,
|
||||
remarkModel: remarkModel,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
|
||||
s.address = host
|
||||
s.showInfo = showInfo
|
||||
var result []string
|
||||
var headers []string
|
||||
var header string
|
||||
var traffic xray.ClientTraffic
|
||||
var clientTraffics []xray.ClientTraffic
|
||||
inbounds, err := s.getInboundsBySubId(subId)
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
s.remarkModel, err = s.settingService.GetRemarkModel()
|
||||
if err != nil {
|
||||
s.remarkModel = "-ieo"
|
||||
return nil, "", err
|
||||
}
|
||||
|
||||
s.datepicker, err = s.settingService.GetDatepicker()
|
||||
if err != nil {
|
||||
s.datepicker = "gregorian"
|
||||
}
|
||||
if err != nil {
|
||||
s.datepicker = "gregorian"
|
||||
}
|
||||
for _, inbound := range inbounds {
|
||||
clients, err := s.inboundService.GetClients(inbound)
|
||||
if err != nil {
|
||||
logger.Error("SubService - GetSub: Unable to get clients from inbound")
|
||||
logger.Error("SubService - GetClients: Unable to get clients from inbound")
|
||||
}
|
||||
if clients == nil {
|
||||
continue
|
||||
}
|
||||
if len(inbound.Listen) > 0 && inbound.Listen[0] == '@' {
|
||||
fallbackMaster, err := s.getFallbackMaster(inbound.Listen)
|
||||
listen, port, streamSettings, err := s.getFallbackMaster(inbound.Listen, inbound.StreamSettings)
|
||||
if err == nil {
|
||||
inbound.Listen = fallbackMaster.Listen
|
||||
inbound.Port = fallbackMaster.Port
|
||||
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)
|
||||
inbound.Listen = listen
|
||||
inbound.Port = port
|
||||
inbound.StreamSettings = streamSettings
|
||||
}
|
||||
}
|
||||
for _, client := range clients {
|
||||
@@ -76,6 +71,8 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Prepare statistics
|
||||
for index, clientTraffic := range clientTraffics {
|
||||
if index == 0 {
|
||||
traffic.Up = clientTraffic.Up
|
||||
@@ -97,11 +94,8 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
|
||||
}
|
||||
}
|
||||
}
|
||||
headers = append(headers, fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000))
|
||||
updateInterval, _ := s.settingService.GetSubUpdates()
|
||||
headers = append(headers, fmt.Sprintf("%d", updateInterval))
|
||||
headers = append(headers, subId)
|
||||
return result, headers, nil
|
||||
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||
return result, header, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||
@@ -130,7 +124,7 @@ func (s *SubService) getClientTraffics(traffics []xray.ClientTraffic, email stri
|
||||
return xray.ClientTraffic{}
|
||||
}
|
||||
|
||||
func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
|
||||
func (s *SubService) getFallbackMaster(dest string, streamSettings string) (string, int, string, error) {
|
||||
db := database.GetDB()
|
||||
var inbound *model.Inbound
|
||||
err := db.Model(model.Inbound{}).
|
||||
@@ -138,9 +132,19 @@ func (s *SubService) getFallbackMaster(dest string) (*model.Inbound, error) {
|
||||
Where("EXISTS (SELECT * FROM json_each(settings, '$.fallbacks') WHERE json_extract(value, '$.dest') = ?)", dest).
|
||||
Find(&inbound).Error
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", 0, "", 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 {
|
||||
@@ -578,6 +582,7 @@ func (s *SubService) genTrojanLink(inbound *model.Inbound, email string) string
|
||||
if sniValue, ok := searchKey(tlsSetting, "serverName"); ok {
|
||||
params["sni"], _ = sniValue.(string)
|
||||
}
|
||||
|
||||
tlsSettings, _ := searchKey(tlsSetting, "settings")
|
||||
if tlsSetting != nil {
|
||||
if fpValue, ok := searchKey(tlsSettings, "fingerprint"); ok {
|
||||
|
||||
@@ -2,7 +2,6 @@ package random
|
||||
|
||||
import (
|
||||
"math/rand"
|
||||
"time"
|
||||
)
|
||||
|
||||
var numSeq [10]rune
|
||||
@@ -13,8 +12,6 @@ var numUpperSeq [36]rune
|
||||
var allSeq [62]rune
|
||||
|
||||
func init() {
|
||||
rand.Seed(time.Now().UnixNano())
|
||||
|
||||
for i := 0; i < 10; i++ {
|
||||
numSeq[i] = rune('0' + i)
|
||||
}
|
||||
@@ -41,3 +38,7 @@ func Seq(n int) string {
|
||||
}
|
||||
return string(runes)
|
||||
}
|
||||
|
||||
func Num(n int) int {
|
||||
return rand.Intn(n)
|
||||
}
|
||||
|
||||
@@ -538,7 +538,7 @@
|
||||
|
||||
var on = function(emitter, type, f) {
|
||||
if (emitter.addEventListener) {
|
||||
emitter.addEventListener(type, f, false);
|
||||
emitter.addEventListener(type, f, { passive: false });
|
||||
} else if (emitter.attachEvent) {
|
||||
emitter.attachEvent("on" + type, f);
|
||||
} else {
|
||||
|
||||
@@ -45,12 +45,12 @@ THE SOFTWARE.
|
||||
.cm-s-xq .CodeMirror-activeline-background { background: #e8f2ff; }
|
||||
.cm-s-xq .CodeMirror-matchingbracket { outline:1px solid grey;color:black !important;background:yellow; }
|
||||
|
||||
.dark .cm-s-xq.CodeMirror { background-color: #222D42; border-color: #2c3950; color: rgb(255 255 255 / 65%); }
|
||||
.dark .cm-s-xq.CodeMirror { background-color: var(--dark-color-surface-200); border-color: var(--dark-color-surface-300); color: rgb(255 255 255 / 65%); }
|
||||
.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 .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-gutters { background: rgb(0 0 0 / 30%); border-right: 1px solid #2c3950; }
|
||||
.dark .cm-s-xq div.CodeMirror-selected { background: var(--dark-color-codemirror-line-selection); }
|
||||
.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: var(--dark-color-codemirror-line-selection); }
|
||||
.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: var(--dark-color-codemirror-line-selection); }
|
||||
.dark .cm-s-xq .CodeMirror-gutters { background: rgb(0 0 0 / 30%); border-right: 1px solid var(--dark-color-surface-300); }
|
||||
.dark .cm-s-xq .CodeMirror-guttermarker { color: #FFBD40; }
|
||||
.dark .cm-s-xq .CodeMirror-guttermarker-subtle { color: rgb(255 255 255 / 70%); }
|
||||
.dark .cm-s-xq .CodeMirror-linenumber { color: rgb(255 255 255 / 50%); }
|
||||
@@ -80,7 +80,7 @@ THE SOFTWARE.
|
||||
|
||||
.Line-Hover{transition: all .2s;}
|
||||
.Line-Hover:hover{ background-color: rgba(0, 102, 85, 0.05) !important; }
|
||||
.dark .Line-Hover:hover{ background-color: rgb(0 0 0 / 20%) !important; }
|
||||
.dark .Line-Hover:hover{ background-color: var(--dark-color-codemirror-line-hover) !important; }
|
||||
|
||||
.CodeMirror-foldmarker { color: #fc8800; text-shadow: #ffd8aa 1px 1px 2px, #ffd8aa -1px -1px 2px, #ffd8aa 1px -1px 2px, #ffd8aa -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; }
|
||||
.dark .CodeMirror-foldmarker { color: #ffffff; text-shadow: #bbb 1px 1px 2px, #bbb -1px -1px 2px, #bbb 1px -1px 2px, #bbb -1px 1px 2px; font-family: arial; line-height: .3; cursor: pointer; }
|
||||
|
||||
@@ -1,3 +1,87 @@
|
||||
:root {
|
||||
--color-primary-100: #008771;
|
||||
--dark-color-background: #0a1222;
|
||||
--dark-color-surface-100: #151f31;
|
||||
--dark-color-surface-200: #222d42;
|
||||
--dark-color-surface-300: #2c3950;
|
||||
--dark-color-surface-400: rgba(65, 85, 119, 0.5); /* line */
|
||||
--dark-color-surface-500: #2c3950; /* popover & switch btn */
|
||||
--dark-color-surface-600: #313f5a; /* dropmenu hover */
|
||||
--dark-color-surface-700: #111929; /* modals */
|
||||
--dark-color-table-hover: rgba(44, 57, 80, 0.2);
|
||||
--dark-color-text-primary: rgba(255, 255, 255, 0.75);
|
||||
--dark-color-stroke: #2c3950;
|
||||
--dark-color-btn-danger: #cd3838;
|
||||
--dark-color-btn-danger-border: transparent;
|
||||
--dark-color-btn-danger-hover: #e94b4b;
|
||||
--dark-color-tag-bg: rgba(255, 255, 255, 0.05);
|
||||
--dark-color-tag-border:rgba(255, 255, 255, 0.15);
|
||||
--dark-color-tag-color:rgba(255, 255, 255, 0.75);
|
||||
--dark-color-tag-green-bg: #112421;
|
||||
--dark-color-tag-green-border: #195141;
|
||||
--dark-color-tag-green-color: #3ad3ba;
|
||||
--dark-color-tag-purple-bg: #201425;
|
||||
--dark-color-tag-purple-border: #5a2969;
|
||||
--dark-color-tag-purple-color: #d988cd;
|
||||
--dark-color-tag-red-bg: #291515;
|
||||
--dark-color-tag-red-border: #5c2626;
|
||||
--dark-color-tag-red-color: #e04141;
|
||||
--dark-color-tag-orange-bg: #312313;
|
||||
--dark-color-tag-orange-border: #593914;
|
||||
--dark-color-tag-orange-color: #ffa031;
|
||||
--dark-color-tag-blue-bg: #111a2c;
|
||||
--dark-color-tag-blue-border: #1348ab;
|
||||
--dark-color-tag-blue-color: #529fff;
|
||||
--dark-color-codemirror-line-hover: rgba(0, 135, 113, 0.2);
|
||||
--dark-color-codemirror-line-selection: rgba(0, 135, 113, 0.3);
|
||||
--dark-color-login-background: var(--dark-color-background);
|
||||
--dark-color-login-wave: var(--dark-color-surface-200);
|
||||
}
|
||||
|
||||
html[data-theme='ultra-dark'] {
|
||||
--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-surface-700: #101113;
|
||||
--dark-color-table-hover: rgba(89, 89, 89, 0.15);
|
||||
--dark-color-text-primary: rgb(255 255 255 / 85%);
|
||||
--dark-color-stroke: #202025;
|
||||
--dark-color-tag-green-bg: #112421;
|
||||
--dark-color-tag-green-border: #1d5f4d;
|
||||
--dark-color-tag-green-color: #59cbac;
|
||||
--dark-color-tag-purple-bg: #241121;
|
||||
--dark-color-tag-purple-border: #5a2969;
|
||||
--dark-color-tag-purple-color: #d686ca;
|
||||
--dark-color-tag-red-bg: #2a1215;
|
||||
--dark-color-tag-red-border: #58181c;
|
||||
--dark-color-tag-red-color: #e84749;
|
||||
--dark-color-tag-orange-bg: #2b1d11;
|
||||
--dark-color-tag-orange-border: #593815;
|
||||
--dark-color-tag-orange-color: #e89a3c;
|
||||
--dark-color-tag-blue-bg: #111a2c;
|
||||
--dark-color-tag-blue-border: #0f367e;
|
||||
--dark-color-tag-blue-color: #3c89e8;
|
||||
--dark-color-codemirror-line-hover: rgba(85, 85, 85, 0.3);
|
||||
--dark-color-codemirror-line-selection: rgba(85, 85, 85, 0.4);
|
||||
--dark-color-login-background: #0a2227;
|
||||
--dark-color-login-wave: #0f2d32;
|
||||
.ant-dropdown-menu-dark {
|
||||
background-color: var(--dark-color-surface-500);
|
||||
}
|
||||
.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:hover:not(.ant-select-dropdown-menu-item-disabled) {
|
||||
background-color: var(--dark-color-surface-300);
|
||||
}
|
||||
.dark .waves-header {
|
||||
background-color: #0a2227;
|
||||
}
|
||||
}
|
||||
|
||||
html,
|
||||
body {
|
||||
height: 100vh;
|
||||
@@ -16,7 +100,7 @@ body {
|
||||
font-feature-settings: "tnum";
|
||||
}
|
||||
html {
|
||||
--antd-wave-shadow-color: #008771;
|
||||
--antd-wave-shadow-color: var(--color-primary-100);
|
||||
line-height: 1.15;
|
||||
text-size-adjust: 100%;
|
||||
-webkit-text-size-adjust: 100%;
|
||||
@@ -26,7 +110,7 @@ html {
|
||||
}
|
||||
|
||||
::selection {
|
||||
color: #008771;
|
||||
color: var(--color-primary-100);
|
||||
background-color: #cfe8e4;
|
||||
}
|
||||
|
||||
@@ -134,7 +218,7 @@ style attribute {
|
||||
padding: 0.5rem;
|
||||
}
|
||||
.ant-modal-body {
|
||||
padding: 10px;
|
||||
padding: 20px;
|
||||
}
|
||||
.ant-form-item-label {
|
||||
line-height: 1.5;
|
||||
@@ -218,7 +302,7 @@ style attribute {
|
||||
.ant-menu-submenu-active,
|
||||
.ant-menu-submenu-title:hover,
|
||||
.ant-menu:not(.ant-menu-inline) .ant-menu-submenu-open {
|
||||
color: #008771;
|
||||
color: var(--color-primary-100);
|
||||
background-color: rgb(232 244 242);
|
||||
border-radius: 0.5rem;
|
||||
}
|
||||
@@ -480,14 +564,14 @@ style attribute {
|
||||
}
|
||||
|
||||
.normal-icon:hover {
|
||||
color: #008771;
|
||||
color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
/* DARK THEME */
|
||||
|
||||
.dark ::selection {
|
||||
color: #fff;
|
||||
background-color: #008771;
|
||||
background-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.dark .normal-icon:hover {
|
||||
@@ -502,13 +586,14 @@ style attribute {
|
||||
.dark .ant-table,
|
||||
.dark .ant-collapse-content,
|
||||
.dark .ant-tabs {
|
||||
background-color: #151f31;
|
||||
color: #ffffffa6;
|
||||
background-color: var(--dark-color-surface-100);
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
|
||||
.dark .ant-card-hoverable:hover,
|
||||
.dark .ant-space-item > .ant-tabs:hover {
|
||||
box-shadow: 0 1px 10px -1px rgb(154 175 238 / 80%);
|
||||
/* box-shadow: 0 1px 10px -1px rgb(154 175 238 / 80%); */
|
||||
box-shadow: 0 2px 8px transparent;
|
||||
}
|
||||
|
||||
.dark > .ant-layout,
|
||||
@@ -518,8 +603,8 @@ style attribute {
|
||||
.dark .ant-table-expanded-row:hover,
|
||||
.dark .ant-table-expanded-row .ant-table-tbody,
|
||||
.dark .ant-calendar {
|
||||
background-color: #101828;
|
||||
color: rgb(255 255 255 /65%);
|
||||
background-color: var(--dark-color-background);
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
|
||||
.dark .ant-table-expanded-row .ant-table-thead > tr:first-child > th {
|
||||
@@ -528,7 +613,7 @@ style attribute {
|
||||
|
||||
.dark .ant-calendar,
|
||||
.dark .ant-card-bordered {
|
||||
border-color: #151f31;
|
||||
border-color: var(--dark-color-background);
|
||||
}
|
||||
|
||||
.dark .ant-table-bordered,
|
||||
@@ -540,7 +625,7 @@ style attribute {
|
||||
.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-thead > tr > th {
|
||||
border-color: #2c3950;
|
||||
border-color: var(--dark-color-surface-400);
|
||||
}
|
||||
|
||||
.dark .ant-table-tbody > tr > td,
|
||||
@@ -553,7 +638,7 @@ style attribute {
|
||||
.dark .ant-popover-title,
|
||||
.dark .ant-calendar-header,
|
||||
.dark .ant-calendar-input-wrap {
|
||||
border-bottom-color: #2c3950;
|
||||
border-bottom-color: var(--dark-color-surface-400);
|
||||
}
|
||||
|
||||
.dark .ant-modal-footer,
|
||||
@@ -561,7 +646,7 @@ style attribute {
|
||||
.dark .ant-calendar-footer,
|
||||
.dark .ant-divider-horizontal.ant-divider-with-text-center:before,
|
||||
.dark .ant-divider-horizontal.ant-divider-with-text-center:after {
|
||||
border-top-color: #2c3950;
|
||||
border-top-color: var(--dark-color-surface-300);
|
||||
}
|
||||
|
||||
.dark .ant-progress-text,
|
||||
@@ -597,7 +682,7 @@ style attribute {
|
||||
.dark .ant-calendar-year-panel-year,
|
||||
.dark .ant-calendar-month-panel-month,
|
||||
.dark .ant-calendar-decade-panel-decade {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
|
||||
.dark .ant-list-item-meta-description {
|
||||
@@ -623,13 +708,12 @@ style attribute {
|
||||
.dark .ant-select-dropdown li,
|
||||
.dark .ant-select-dropdown-menu-item,
|
||||
.dark .ant-divider:not(.ant-divider-with-text-center),
|
||||
.dark .ant-calendar-input,
|
||||
.dark .client-table-header,
|
||||
.dark .ant-select-selection--multiple .ant-select-selection__choice,
|
||||
.dark .ant-calendar-time-picker-inner {
|
||||
background-color: #222d42;
|
||||
border-color: #2c3950;
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
background-color: var(--dark-color-surface-200);
|
||||
border-color: var(--dark-color-surface-300);
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
|
||||
.dark .ant-select-selection:hover,
|
||||
@@ -639,34 +723,34 @@ style attribute {
|
||||
.dark .ant-input:hover,
|
||||
.dark .ant-input:focus {
|
||||
background-color: rgba(0, 135, 113, 0.3);
|
||||
border-color: #008771;
|
||||
border-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.dark .ant-btn:not(.ant-btn-primary):not(.ant-btn-danger) {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
color: var(--dark-color-text-primary);
|
||||
background-color: rgb(10 117 87 / 30%);
|
||||
border: 1px solid #008771;
|
||||
border: 1px solid var(--color-primary-100);
|
||||
}
|
||||
|
||||
.dark .ant-radio-button-wrapper,
|
||||
.dark .ant-radio-button-wrapper:before {
|
||||
color: rgb(255 255 255 / 65%);
|
||||
color: var(--dark-color-text-primary);
|
||||
background-color: rgba(0, 135, 113, 0.3);
|
||||
border-color: #008771;
|
||||
border-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.dark .ant-btn:focus:not(.ant-btn-primary):not(.ant-btn-danger),
|
||||
.dark .ant-btn:hover:not(.ant-btn-primary):not(.ant-btn-danger) {
|
||||
color: #fff;
|
||||
background-color: rgb(10 117 87 / 50%);
|
||||
border-color: #008771;
|
||||
border-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.dark .ant-btn-primary[disabled],
|
||||
.dark .ant-btn-danger[disabled],
|
||||
.dark .ant-calendar-ok-btn-disabled {
|
||||
color: rgb(255 255 255 / 35%);
|
||||
background-color: #2c3950;
|
||||
background-color: var(--dark-color-surface-300);
|
||||
border-color: #42516c;
|
||||
}
|
||||
|
||||
@@ -675,7 +759,7 @@ style attribute {
|
||||
> tr:hover:not(.ant-table-expanded-row):not(.ant-table-row-selected)
|
||||
> td,
|
||||
.dark .client-table-odd-row {
|
||||
background-color: #00877122;
|
||||
background-color: var(--dark-color-table-hover);
|
||||
}
|
||||
|
||||
.dark .ant-table-row-expand-icon {
|
||||
@@ -685,38 +769,37 @@ style attribute {
|
||||
}
|
||||
|
||||
.dark .ant-table-row-expand-icon:hover {
|
||||
color: #008771;
|
||||
color: var(--color-primary-100);
|
||||
background-color: #fff0;
|
||||
border-color: #008771;
|
||||
border-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.dark .ant-switch:not(.ant-switch-checked),
|
||||
.dark .ant-progress-line .ant-progress-inner {
|
||||
background-color: #2c3950;
|
||||
background-color: var(--dark-color-surface-500);
|
||||
}
|
||||
|
||||
.dark .ant-progress-circle-trail {
|
||||
stroke: #2c3950 !important;
|
||||
stroke: var(--dark-color-stroke) !important;
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-dark,
|
||||
.dark .ant-popover-inner {
|
||||
background-color: #222d42;
|
||||
background-color: var(--dark-color-surface-500);
|
||||
}
|
||||
|
||||
.dark > .ant-popover-content > .ant-popover-arrow {
|
||||
border-color: #222d42;
|
||||
border-color: var(--dark-color-surface-500);
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-dark .ant-dropdown-menu-item:hover,
|
||||
.dark .ant-select-dropdown-menu-item-selected,
|
||||
.dark .ant-select-dropdown-menu-item:hover,
|
||||
.dark .ant-calendar-time-picker-select-option-selected {
|
||||
background-color: #313f5a;
|
||||
background-color: var(--dark-color-surface-600);
|
||||
}
|
||||
|
||||
.ant-menu-dark .ant-menu-item:hover {
|
||||
background-color: #2c3950;
|
||||
background-color: var(--dark-color-surface-300);
|
||||
}
|
||||
|
||||
.dark .ant-alert-message {
|
||||
@@ -724,61 +807,61 @@ style attribute {
|
||||
}
|
||||
|
||||
.dark .ant-tag {
|
||||
color: rgba(255, 255, 255, 0.65);
|
||||
background-color: #ffffff0a;
|
||||
border-color: #344461;
|
||||
color: var(--dark-color-tag-color);
|
||||
background-color: var(--dark-color-tag-bg);
|
||||
border-color: var(--dark-color-tag-border);
|
||||
}
|
||||
|
||||
.dark .ant-tag-blue {
|
||||
background-color: #111a2c;
|
||||
border-color: #0f367e;
|
||||
color: #3c89e8;
|
||||
background-color: var(--dark-color-tag-blue-bg);
|
||||
border-color: var(--dark-color-tag-blue-border);
|
||||
color: var(--dark-color-tag-blue-color);
|
||||
}
|
||||
|
||||
.dark .ant-tag-red,
|
||||
.dark .ant-alert-error {
|
||||
background-color: #291515;
|
||||
border-color: #5c2626;
|
||||
color: #e04141;
|
||||
background-color: var(--dark-color-tag-red-bg);
|
||||
border-color: var(--dark-color-tag-red-border);
|
||||
color: var(--dark-color-tag-red-color);
|
||||
}
|
||||
|
||||
.dark .ant-tag-orange,
|
||||
.dark .ant-alert-warning {
|
||||
background-color: #312313;
|
||||
border-color: #593914;
|
||||
color: #ffa031;
|
||||
background-color: var(--dark-color-tag-orange-bg);
|
||||
border-color: var(--dark-color-tag-orange-border);
|
||||
color: var(--dark-color-tag-orange-color);
|
||||
}
|
||||
|
||||
.dark .ant-tag-green {
|
||||
background-color: #112421;
|
||||
border-color: #144840;
|
||||
color: #33bca5;
|
||||
background-color: var(--dark-color-tag-green-bg);
|
||||
border-color: var(--dark-color-tag-green-border);
|
||||
color: var(--dark-color-tag-green-color);
|
||||
}
|
||||
|
||||
.dark .ant-tag-purple {
|
||||
background-color: #2c1e32;
|
||||
border-color: #49394e;
|
||||
color: #cfb9cc;
|
||||
background-color: var(--dark-color-tag-purple-bg);
|
||||
border-color: var(--dark-color-tag-purple-border);
|
||||
color: var(--dark-color-tag-purple-color);
|
||||
}
|
||||
|
||||
.dark .ant-modal-content,
|
||||
.dark .ant-modal-header {
|
||||
background-color: #181f2c;
|
||||
background-color: var(--dark-color-surface-700);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-next-month-btn-day .ant-calendar-date,
|
||||
.dark .ant-calendar-last-month-cell .ant-calendar-date {
|
||||
color: #2c3950;
|
||||
color: var(--dark-color-surface-300);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-selected-day .ant-calendar-date {
|
||||
background-color: #008771 !important;
|
||||
background-color: var(--color-primary-100) !important;
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
.dark .ant-calendar-date:hover,
|
||||
.dark .ant-calendar-time-picker-select li:hover {
|
||||
background-color: #313f5a;
|
||||
background-color: var(--dark-color-surface-600);
|
||||
color: #fff;
|
||||
}
|
||||
|
||||
@@ -792,13 +875,15 @@ style attribute {
|
||||
color: #fff;
|
||||
font-weight: 600;
|
||||
outline: none;
|
||||
background-color: #008771;
|
||||
background-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-time-picker-select {
|
||||
border-right-color: #2c3950;
|
||||
border-right-color: var(--dark-color-surface-300);
|
||||
}
|
||||
|
||||
.has-warning .ant-select-selection,
|
||||
.has-warning .ant-select-selection:hover,
|
||||
.has-warning .ant-input,
|
||||
.has-warning .ant-input:hover {
|
||||
background-color: #ffeee1;
|
||||
@@ -813,6 +898,8 @@ style attribute {
|
||||
border-color: #fec093;
|
||||
}
|
||||
|
||||
.dark .has-warning .ant-select-selection,
|
||||
.dark .has-warning .ant-select-selection:hover,
|
||||
.dark .has-warning .ant-input,
|
||||
.dark .has-warning .ant-input:hover {
|
||||
border-color: #784e1d;
|
||||
@@ -828,7 +915,7 @@ style attribute {
|
||||
}
|
||||
|
||||
.dark .has-success .anticon {
|
||||
color: #61bf39;
|
||||
color: var(--color-primary-100);
|
||||
animation-name: diffZoomIn1 !important;
|
||||
}
|
||||
|
||||
@@ -874,19 +961,19 @@ style attribute {
|
||||
}
|
||||
|
||||
.ant-calendar-today .ant-calendar-date {
|
||||
color: #008771;
|
||||
color: var(--color-primary-100);
|
||||
font-weight: 700;
|
||||
border-color: #008771;
|
||||
border-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-today .ant-calendar-date {
|
||||
color: #fff;
|
||||
font-weight: 700;
|
||||
border-color: #008771;
|
||||
border-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.ant-calendar-selected-day .ant-calendar-date {
|
||||
background: #008771;
|
||||
background: var(--color-primary-100);
|
||||
color: #ffffff;
|
||||
}
|
||||
|
||||
@@ -920,7 +1007,7 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
.ant-select-dropdown.ant-select-dropdown--multiple
|
||||
.ant-select-dropdown-menu-item-selected:hover
|
||||
.ant-select-selected-icon {
|
||||
color: #008771;
|
||||
color: var(--color-primary-100);
|
||||
}
|
||||
.ant-select-selection:hover,
|
||||
.ant-input-number-focused,
|
||||
@@ -929,7 +1016,7 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
}
|
||||
|
||||
.dark .ant-input-number-handler:active {
|
||||
background-color: #008771;
|
||||
background-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.dark .ant-input-number-handler:hover .ant-input-number-handler-down-inner,
|
||||
@@ -957,7 +1044,7 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
}
|
||||
|
||||
.dark .ant-calendar-year-panel-header {
|
||||
border-bottom: 1px solid #222d42;
|
||||
border-bottom: 1px solid var(--dark-color-surface-200);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-year-panel-last-decade-cell .ant-calendar-year-panel-year,
|
||||
@@ -965,10 +1052,11 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
color: rgba(255, 255, 255, 0.35);
|
||||
}
|
||||
|
||||
.ant-dropdown-menu-dark,
|
||||
.dark .ant-calendar-year-panel-year:hover,
|
||||
.dark .ant-calendar-month-panel-month:hover,
|
||||
.dark .ant-calendar-decade-panel-decade:hover {
|
||||
background-color: #222d42;
|
||||
background-color: var(--dark-color-surface-200);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-header a:hover {
|
||||
@@ -976,13 +1064,13 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
}
|
||||
|
||||
.dark .ant-calendar-month-panel-header {
|
||||
background-color: #101828;
|
||||
border-bottom: 1px solid #222d42;
|
||||
background-color: var(--dark-color-background);
|
||||
border-bottom: 1px solid var(--dark-color-surface-200);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-year-panel,
|
||||
.dark .ant-calendar table {
|
||||
background-color: #101828;
|
||||
background-color: var(--dark-color-background);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-year-panel-selected-cell .ant-calendar-year-panel-year,
|
||||
@@ -1000,7 +1088,7 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
.ant-calendar-decade-panel-selected-cell
|
||||
.ant-calendar-decade-panel-decade:hover {
|
||||
color: #fff;
|
||||
background-color: #008771;
|
||||
background-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-last-month-cell .ant-calendar-date,
|
||||
@@ -1014,8 +1102,8 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
|
||||
.dark .ant-calendar-today .ant-calendar-date:hover {
|
||||
color: #fff;
|
||||
border-color: #008771;
|
||||
background-color: #008771;
|
||||
border-color: var(--color-primary-100);
|
||||
background-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.dark
|
||||
@@ -1028,8 +1116,8 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
}
|
||||
|
||||
.dark .ant-calendar-decade-panel-header {
|
||||
border-bottom: 1px solid #222d42;
|
||||
background-color: #101828;
|
||||
border-bottom: 1px solid var(--dark-color-surface-200);
|
||||
background-color: var(--dark-color-background);
|
||||
}
|
||||
|
||||
.dark .ant-checkbox-inner {
|
||||
@@ -1038,24 +1126,30 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
}
|
||||
|
||||
.dark .ant-checkbox-checked .ant-checkbox-inner {
|
||||
background-color: #008771;
|
||||
border-color: #008771;
|
||||
background-color: var(--color-primary-100);
|
||||
border-color: var(--color-primary-100);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-input {
|
||||
background-color: #101828;
|
||||
background-color: var(--dark-color-background);
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
|
||||
.dark .ant-calendar-input::placeholder {
|
||||
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 {
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ant-input-number-handler {
|
||||
border-radius: 0;
|
||||
border-radius: 0;
|
||||
}
|
||||
|
||||
.ant-input-number {
|
||||
@@ -1089,15 +1183,15 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
> td,
|
||||
.ant-table-thead
|
||||
> 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);
|
||||
}
|
||||
|
||||
.dark .ant-dropdown-menu-item: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:hover:not(.ant-select-dropdown-menu-item-disabled) {
|
||||
background-color: #313f5a;
|
||||
background-color: var(--dark-color-surface-600);
|
||||
}
|
||||
|
||||
.ant-select-dropdown,
|
||||
@@ -1110,6 +1204,8 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
}
|
||||
|
||||
.qr-bg {
|
||||
width: 100%;
|
||||
height: 100%;
|
||||
background-color: #fff;
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
@@ -1118,6 +1214,38 @@ li.ant-select-dropdown-menu-item:empty:after {
|
||||
border-radius: 1rem;
|
||||
}
|
||||
|
||||
.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) {
|
||||
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: var(--dark-color-surface-200);
|
||||
border: 1px solid var(--dark-color-surface-300);
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
|
||||
.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);
|
||||
}
|
||||
|
||||
Binary file not shown.
|
Before Width: | Height: | Size: 15 KiB |
@@ -29,6 +29,16 @@ const supportLangs = [
|
||||
value: 'es-ES',
|
||||
icon: '🇪🇸',
|
||||
},
|
||||
{
|
||||
name: 'Indonesian',
|
||||
value: 'id-ID',
|
||||
icon: '🇮🇩',
|
||||
},
|
||||
{
|
||||
name: 'Український',
|
||||
value: 'uk-UA',
|
||||
icon: '🇺🇦',
|
||||
},
|
||||
];
|
||||
|
||||
function getLang() {
|
||||
|
||||
@@ -418,7 +418,7 @@ class Outbound extends CommonClass {
|
||||
}
|
||||
|
||||
canEnableTls() {
|
||||
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan].includes(this.protocol)) return false;
|
||||
if (![Protocols.VMess, Protocols.VLESS, Protocols.Trojan, Protocols.Shadowsocks].includes(this.protocol)) return false;
|
||||
return ["tcp", "ws", "http", "quic", "grpc"].includes(this.stream.network);
|
||||
}
|
||||
|
||||
@@ -861,13 +861,13 @@ Outbound.SocksSettings = class extends CommonClass {
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
servers = json.servers;
|
||||
let servers = json.servers;
|
||||
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
|
||||
return new Outbound.SocksSettings(
|
||||
servers[0].address,
|
||||
servers[0].port,
|
||||
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
|
||||
ObjectUtil.isArrEmpty(servers[0].pass) ? '' : servers[0].users[0].pass,
|
||||
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -891,13 +891,13 @@ Outbound.HttpSettings = class extends CommonClass {
|
||||
}
|
||||
|
||||
static fromJson(json={}) {
|
||||
servers = json.servers;
|
||||
let servers = json.servers;
|
||||
if(ObjectUtil.isArrEmpty(servers)) servers=[{users: [{}]}];
|
||||
return new Outbound.HttpSettings(
|
||||
servers[0].address,
|
||||
servers[0].port,
|
||||
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].user,
|
||||
ObjectUtil.isArrEmpty(servers[0].pass) ? '' : servers[0].users[0].pass,
|
||||
ObjectUtil.isArrEmpty(servers[0].users) ? '' : servers[0].users[0].pass,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -914,8 +914,8 @@ Outbound.HttpSettings = class extends CommonClass {
|
||||
|
||||
Outbound.WireguardSettings = class extends CommonClass {
|
||||
constructor(
|
||||
mtu=1420, secretKey=Wireguard.generateKeypair().privateKey,
|
||||
address=[''], workers=2, domainStrategy='ForceIPv6v4', reserved='',
|
||||
mtu=1420, secretKey='',
|
||||
address=[''], workers=2, domainStrategy='', reserved='',
|
||||
peers=[new Outbound.WireguardSettings.Peer()], kernelMode=false) {
|
||||
super();
|
||||
this.mtu = mtu;
|
||||
@@ -957,7 +957,7 @@ Outbound.WireguardSettings = class extends CommonClass {
|
||||
address: this.address ? this.address.split(",") : [],
|
||||
workers: this.workers?? undefined,
|
||||
domainStrategy: WireguardDomainStrategy.includes(this.domainStrategy) ? this.domainStrategy : undefined,
|
||||
reserved: this.reserved ? this.reserved.split(",") : undefined,
|
||||
reserved: this.reserved ? this.reserved.split(",").map(Number) : undefined,
|
||||
peers: Outbound.WireguardSettings.Peer.toJsonArray(this.peers),
|
||||
kernelMode: this.kernelMode,
|
||||
};
|
||||
@@ -965,7 +965,7 @@ Outbound.WireguardSettings = class extends CommonClass {
|
||||
};
|
||||
|
||||
Outbound.WireguardSettings.Peer = class extends CommonClass {
|
||||
constructor(publicKey=Wireguard.generateKeypair().publicKey, psk='', allowedIPs=['0.0.0.0/0','::/0'], endpoint='', keepAlive=0) {
|
||||
constructor(publicKey='', psk='', allowedIPs=['0.0.0.0/0','::/0'], endpoint='', keepAlive=0) {
|
||||
super();
|
||||
this.publicKey = publicKey;
|
||||
this.psk = psk;
|
||||
|
||||
@@ -28,6 +28,7 @@ class AllSetting {
|
||||
this.subListen = "";
|
||||
this.subPort = "2096";
|
||||
this.subPath = "/sub/";
|
||||
this.subJsonPath = "/json/";
|
||||
this.subDomain = "";
|
||||
this.subCertFile = "";
|
||||
this.subKeyFile = "";
|
||||
@@ -35,6 +36,8 @@ class AllSetting {
|
||||
this.subEncrypt = true;
|
||||
this.subShowInfo = false;
|
||||
this.subURI = '';
|
||||
this.subJsonURI = '';
|
||||
this.subJsonFragment = '';
|
||||
|
||||
this.timeLocation = "Asia/Tehran";
|
||||
|
||||
|
||||
@@ -1146,10 +1146,6 @@ class Inbound extends XrayCommonClass {
|
||||
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() {
|
||||
this.port = RandomUtil.randomIntRange(10000, 60000);
|
||||
this.listen = '';
|
||||
@@ -1534,6 +1530,28 @@ class Inbound extends XrayCommonClass {
|
||||
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) {
|
||||
switch (this.protocol) {
|
||||
case Protocols.VMESS:
|
||||
@@ -1557,7 +1575,7 @@ class Inbound extends XrayCommonClass {
|
||||
const orderChars = remarkModel.slice(1);
|
||||
let orders = {
|
||||
'i': remark,
|
||||
'e': client ? client.email : '',
|
||||
'e': email,
|
||||
'o': '',
|
||||
};
|
||||
if(ObjectUtil.isArrEmpty(this.stream.externalProxy)){
|
||||
@@ -1580,6 +1598,7 @@ class Inbound extends XrayCommonClass {
|
||||
}
|
||||
|
||||
genInboundLinks(remark = '', remarkModel = '-ieo') {
|
||||
let addr = !ObjectUtil.isEmpty(this.listen) && this.listen !== "0.0.0.0" ? this.listen : location.hostname;
|
||||
if(this.clients){
|
||||
let links = [];
|
||||
this.clients.forEach((client) => {
|
||||
@@ -1589,7 +1608,14 @@ class Inbound extends XrayCommonClass {
|
||||
});
|
||||
return links.join('\r\n');
|
||||
} else {
|
||||
if(this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(this.listen, this.port, 'same', remark);
|
||||
if(this.protocol == Protocols.SHADOWSOCKS && !this.isSSMultiUser) return this.genSSLink(addr, 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 '';
|
||||
}
|
||||
}
|
||||
@@ -2269,7 +2295,7 @@ Inbound.WireguardSettings = class extends XrayCommonClass {
|
||||
}
|
||||
|
||||
addPeer() {
|
||||
this.peers.push(new Inbound.WireguardSettings.Peer());
|
||||
this.peers.push(new Inbound.WireguardSettings.Peer(null,null,'',['10.0.0.' + (this.peers.length+2)]));
|
||||
}
|
||||
|
||||
delPeer(index) {
|
||||
@@ -2297,16 +2323,24 @@ Inbound.WireguardSettings = class extends XrayCommonClass {
|
||||
};
|
||||
|
||||
Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
|
||||
constructor(publicKey=Wireguard.generateKeypair().publicKey, psk='', allowedIPs=['0.0.0.0/0','::/0'], keepAlive=0) {
|
||||
constructor(privateKey, publicKey, psk='', allowedIPs=['10.0.0.2/32'], keepAlive=0) {
|
||||
super();
|
||||
this.privateKey = privateKey
|
||||
this.publicKey = publicKey;
|
||||
if (!this.publicKey){
|
||||
[this.publicKey, this.privateKey] = Object.values(Wireguard.generateKeypair())
|
||||
}
|
||||
this.psk = psk;
|
||||
allowedIPs.forEach((a,index) => {
|
||||
if (a.length>0 && !a.includes('/')) allowedIPs[index] += '/32';
|
||||
})
|
||||
this.allowedIPs = allowedIPs;
|
||||
this.keepAlive = keepAlive;
|
||||
}
|
||||
|
||||
static fromJson(json={}){
|
||||
return new Inbound.WireguardSettings.Peer(
|
||||
json.privateKey,
|
||||
json.publicKey,
|
||||
json.preSharedKey,
|
||||
json.allowedIPs,
|
||||
@@ -2315,7 +2349,11 @@ Inbound.WireguardSettings.Peer = class extends XrayCommonClass {
|
||||
}
|
||||
|
||||
toJson() {
|
||||
this.allowedIPs.forEach((a,index) => {
|
||||
if (a.length>0 && !a.includes('/')) this.allowedIPs[index] += '/32';
|
||||
});
|
||||
return {
|
||||
privateKey: this.privateKey,
|
||||
publicKey: this.publicKey,
|
||||
preSharedKey: this.psk.length>0 ? this.psk : undefined,
|
||||
allowedIPs: this.allowedIPs,
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -86,7 +86,7 @@ func (a *InboundController) addInbound(c *gin.Context) {
|
||||
user := session.GetLoginUser(c)
|
||||
inbound.UserId = user.Id
|
||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
||||
inbound.Tag = fmt.Sprintf("inbound-0.0.0.0:%v", inbound.Port)
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
} else {
|
||||
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.UserId = user.Id
|
||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
||||
inbound.Tag = fmt.Sprintf("inbound-0.0.0.0:%v", inbound.Port)
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
} else {
|
||||
inbound.Tag = fmt.Sprintf("inbound-%v:%v", inbound.Listen, inbound.Port)
|
||||
}
|
||||
|
||||
@@ -10,6 +10,7 @@ type XraySettingController struct {
|
||||
XraySettingService service.XraySettingService
|
||||
SettingService service.SettingService
|
||||
InboundService service.InboundService
|
||||
OutboundService service.OutboundService
|
||||
XrayService service.XrayService
|
||||
}
|
||||
|
||||
@@ -27,6 +28,8 @@ func (a *XraySettingController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/getXrayResult", a.getXrayResult)
|
||||
g.GET("/getDefaultJsonConfig", a.getDefaultXrayConfig)
|
||||
g.POST("/warp/:action", a.warp)
|
||||
g.GET("/getOutboundsTraffic", a.getOutboundsTraffic)
|
||||
g.POST("/resetOutboundsTraffic", a.resetOutboundsTraffic)
|
||||
}
|
||||
|
||||
func (a *XraySettingController) getXraySetting(c *gin.Context) {
|
||||
@@ -78,9 +81,27 @@ func (a *XraySettingController) warp(c *gin.Context) {
|
||||
resp, err = a.XraySettingService.RegWarp(skey, pkey)
|
||||
case "license":
|
||||
license := c.PostForm("license")
|
||||
println(license)
|
||||
resp, err = a.XraySettingService.SetWarpLicence(license)
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
@@ -48,6 +48,9 @@ type AllSetting struct {
|
||||
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"`
|
||||
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"`
|
||||
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"`
|
||||
}
|
||||
|
||||
@@ -105,6 +108,13 @@ func (s *AllSetting) CheckValid() error {
|
||||
s.SubPath += "/"
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(s.SubJsonPath, "/") {
|
||||
s.SubJsonPath = "/" + s.SubJsonPath
|
||||
}
|
||||
if !strings.HasSuffix(s.SubJsonPath, "/") {
|
||||
s.SubJsonPath += "/"
|
||||
}
|
||||
|
||||
_, err := time.LoadLocation(s.TimeLocation)
|
||||
if err != nil {
|
||||
return common.NewError("time location not exist:", s.TimeLocation)
|
||||
|
||||
@@ -7,8 +7,6 @@
|
||||
<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/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>
|
||||
[v-cloak] {
|
||||
display: none;
|
||||
@@ -30,4 +28,5 @@
|
||||
</style>
|
||||
<title>{{ .host }}-{{ i18n .title}}</title>
|
||||
</head>
|
||||
<div id="message"></div>
|
||||
{{end}}
|
||||
@@ -1,6 +1,7 @@
|
||||
{{define "promptModal"}}
|
||||
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
|
||||
:closable="true" @ok="promptModal.ok" :mask-closable="false"
|
||||
:confirm-loading="promptModal.confirmLoading"
|
||||
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme">
|
||||
<a-input id="prompt-modal-input" :type="promptModal.type"
|
||||
v-model="promptModal.value"
|
||||
@@ -17,6 +18,7 @@
|
||||
value: '',
|
||||
okText: '{{ i18n "sure"}}',
|
||||
visible: false,
|
||||
confirmLoading: false,
|
||||
keyEnter(e) {
|
||||
if (this.type !== 'textarea') {
|
||||
e.preventDefault();
|
||||
@@ -30,7 +32,6 @@
|
||||
}
|
||||
},
|
||||
ok() {
|
||||
promptModal.close();
|
||||
promptModal.confirm(promptModal.value);
|
||||
},
|
||||
confirm() {},
|
||||
@@ -53,7 +54,10 @@
|
||||
},
|
||||
close() {
|
||||
this.visible = false;
|
||||
}
|
||||
},
|
||||
loading(loading=true) {
|
||||
this.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
const promptModalApp = new Vue({
|
||||
|
||||
@@ -8,13 +8,23 @@
|
||||
{{ i18n "pages.inbounds.clickOnQRcode" }}
|
||||
</a-tag>
|
||||
<template v-if="app.subSettings.enable && qrModal.subId">
|
||||
<a-divider>Subscription</a-divider>
|
||||
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))" id="qrCode-sub" style="width: 100%; height: 100%;"></canvas></div>
|
||||
<a-divider>{{ i18n "pages.settings.subSettings"}}</a-divider>
|
||||
<canvas @click="copyToClipboard('qrCode-sub',genSubLink(qrModal.client.subId))"
|
||||
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>
|
||||
<a-divider>{{ i18n "pages.inbounds.client" }}</a-divider>
|
||||
<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>
|
||||
<div class="qr-bg"><canvas @click="copyToClipboard('qrCode-'+index, row.link)" :id="'qrCode-'+index" style="width: 100%; height: 100%;"></canvas></div>
|
||||
<canvas @click="copyToClipboard('qrCode-'+index, row.link)"
|
||||
:id="'qrCode-'+index"
|
||||
class="qr-bg"></canvas>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
@@ -35,12 +45,21 @@
|
||||
this.client = client;
|
||||
this.subId = '';
|
||||
this.qrcodes = [];
|
||||
this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, client).forEach(l => {
|
||||
this.qrcodes.push({
|
||||
remark: l.remark,
|
||||
link: l.link
|
||||
if (this.inbound.protocol == Protocols.WIREGUARD){
|
||||
this.inbound.genInboundLinks(dbInbound.remark).split('\r\n').forEach((l,index) =>{
|
||||
this.qrcodes.push({
|
||||
remark: "Peer " + (index+1),
|
||||
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;
|
||||
},
|
||||
close: function () {
|
||||
@@ -73,12 +92,16 @@
|
||||
},
|
||||
genSubLink(subID) {
|
||||
return app.subSettings.subURI+subID;
|
||||
},
|
||||
genSubJsonLink(subID) {
|
||||
return app.subSettings.subJsonURI+subID;
|
||||
}
|
||||
},
|
||||
updated() {
|
||||
if (qrModal.client && qrModal.client.subId) {
|
||||
qrModal.subId = qrModal.client.subId;
|
||||
this.setQrCode("qrCode-sub", this.genSubLink(qrModal.subId));
|
||||
this.setQrCode("qrCode-subJson", this.genSubJsonLink(qrModal.subId));
|
||||
}
|
||||
qrModal.qrcodes.forEach((element, index) => {
|
||||
this.setQrCode("qrCode-" + index, element.link);
|
||||
|
||||
@@ -1,15 +1,16 @@
|
||||
{{define "textModal"}}
|
||||
<a-modal id="text-modal" v-model="txtModal.visible" :title="txtModal.title"
|
||||
:closable="true" ok-text='{{ i18n "copy" }}' cancel-text='{{ i18n "close" }}'
|
||||
:class="themeSwitcher.currentTheme"
|
||||
:ok-button-props="{attrs:{id:'txt-modal-ok-btn'}}">
|
||||
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" type="primary" style="margin-bottom: 10px;"
|
||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
|
||||
:download="txtModal.fileName">
|
||||
{{ i18n "download" }} [[ txtModal.fileName ]]
|
||||
</a-button>
|
||||
<a-input type="textarea" v-model="txtModal.content"
|
||||
:autosize="{ minRows: 10, maxRows: 20}"></a-input>
|
||||
:closable="true"
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<template slot="footer">
|
||||
<a-button v-if="!ObjectUtil.isEmpty(txtModal.fileName)" icon="download"
|
||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(txtModal.content)"
|
||||
:download="txtModal.fileName">[[ txtModal.fileName ]]
|
||||
</a-button>
|
||||
<a-button type="primary" id="copy-btn">{{ i18n "copy" }}</a-button>
|
||||
</template>
|
||||
<a-input style="overflow-y: auto;" type="textarea" v-model="txtModal.content"
|
||||
:autosize="{ minRows: 10, maxRows: 20}"></a-input>
|
||||
</a-modal>
|
||||
|
||||
<script>
|
||||
@@ -28,7 +29,7 @@
|
||||
this.visible = true;
|
||||
textModalApp.$nextTick(() => {
|
||||
if (this.clipboard === null) {
|
||||
this.clipboard = new ClipboardJS('#txt-modal-ok-btn', {
|
||||
this.clipboard = new ClipboardJS('#copy-btn', {
|
||||
text: () => this.content,
|
||||
});
|
||||
this.clipboard.on('success', () => {
|
||||
@@ -52,4 +53,4 @@
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -2,9 +2,14 @@
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<style>
|
||||
html * {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
h1 {
|
||||
text-align: center;
|
||||
margin: 20px 0 50px 0;
|
||||
/* margin: 20px 0 50px 0;*/
|
||||
height: 110px;
|
||||
}
|
||||
.ant-btn,
|
||||
.ant-input {
|
||||
@@ -31,7 +36,9 @@
|
||||
}
|
||||
.title {
|
||||
font-size: 32px;
|
||||
font-weight: bold;
|
||||
}
|
||||
.title b {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
#app {
|
||||
overflow: hidden;
|
||||
@@ -42,6 +49,9 @@
|
||||
border-radius: 2rem;
|
||||
padding: 3rem;
|
||||
transition: all 0.3s;
|
||||
user-select:none;
|
||||
-webkit-user-select:none;
|
||||
-moz-user-select: none;
|
||||
}
|
||||
#login:hover {
|
||||
box-shadow: 0 2px 8px rgba(0, 0, 0, 0.09);
|
||||
@@ -61,13 +71,13 @@
|
||||
z-index: 0;
|
||||
}
|
||||
.dark .under {
|
||||
background-color: #0f2d32;
|
||||
background-color: var(--dark-color-login-wave);
|
||||
}
|
||||
.dark #login {
|
||||
background-color: #151f31;
|
||||
background-color: var(--dark-color-surface-100);
|
||||
}
|
||||
.dark h1 {
|
||||
color: rgba(255, 255, 255, 0.85);
|
||||
color: rgba(255, 255, 255);
|
||||
}
|
||||
.ant-form-item {
|
||||
margin-bottom: 16px;
|
||||
@@ -192,7 +202,7 @@
|
||||
z-index: -1;
|
||||
}
|
||||
.dark .waves-header {
|
||||
background-color: #101828;
|
||||
background-color: var(--dark-color-login-background);
|
||||
}
|
||||
.waves-inner-header {
|
||||
height: 50vh;
|
||||
@@ -204,7 +214,7 @@
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 15vh;
|
||||
margin-bottom: -5px; /*Fix for safari gap*/
|
||||
margin-bottom: -8px; /*Fix for safari gap*/
|
||||
min-height: 100px;
|
||||
max-height: 150px;
|
||||
}
|
||||
@@ -212,23 +222,27 @@
|
||||
animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
|
||||
}
|
||||
.dark .parallax > use {
|
||||
fill: rgb(10 117 87 / 20%);
|
||||
fill: var(--dark-color-login-wave);
|
||||
}
|
||||
.parallax > use:nth-child(1) {
|
||||
animation-delay: -2s;
|
||||
animation-duration: 7s;
|
||||
animation-duration: 4s;
|
||||
opacity: 0.2;
|
||||
}
|
||||
.parallax > use:nth-child(2) {
|
||||
animation-delay: -3s;
|
||||
animation-duration: 10s;
|
||||
animation-duration: 7s;
|
||||
opacity: 0.4;
|
||||
}
|
||||
.parallax > use:nth-child(3) {
|
||||
animation-delay: -4s;
|
||||
animation-duration: 13s;
|
||||
animation-duration: 10s;
|
||||
opacity: 0.6;
|
||||
}
|
||||
.parallax > use:nth-child(4) {
|
||||
animation-delay: -5s;
|
||||
animation-duration: 13s;
|
||||
}
|
||||
@keyframes move-forever {
|
||||
0% {
|
||||
transform: translate3d(-90px, 0, 0);
|
||||
@@ -243,90 +257,225 @@
|
||||
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>
|
||||
<body>
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<transition name="list" appear>
|
||||
<a-layout-content class="under" style="min-height: 0;">
|
||||
<div class="waves-header">
|
||||
<div class="waves-inner-header"></div>
|
||||
<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">
|
||||
<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" />
|
||||
</defs>
|
||||
<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="5" fill="rgba(0, 135, 113, 0.08)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="7" fill="rgba(0, 135, 113, 0.08)" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto;">
|
||||
<a-col :xs="22" :sm="20" :md="14" :lg="10" :xl="8" :xxl="6" id="login" style="margin: 3rem 0;">
|
||||
<a-layout-content class="under" style="min-height: 0;">
|
||||
<div class="waves-header">
|
||||
<div class="waves-inner-header"></div>
|
||||
<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">
|
||||
<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" />
|
||||
</defs>
|
||||
<g class="parallax">
|
||||
<use xlink:href="#gentle-wave" x="48" y="0" 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="5" fill="rgba(0, 135, 113, 0.08)" />
|
||||
<use xlink:href="#gentle-wave" x="48" y="7" fill="#c7ebe2" />
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<a-row type="flex" justify="center" align="middle" style="height: 100%; overflow: auto;">
|
||||
<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">
|
||||
<a-col>
|
||||
<h1 class="title">{{ i18n "pages.login.title" }}</h1>
|
||||
</a-col>
|
||||
<a-col style="width: 100%;">
|
||||
<h1 class="title headline zoom">
|
||||
<span class="words-wrapper">
|
||||
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
|
||||
<b>{{ i18n "pages.login.title" }}</b>
|
||||
</span>
|
||||
</h1>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col span="24">
|
||||
<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: '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>
|
||||
<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>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<theme-switch />
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-col>
|
||||
<a-col span="24">
|
||||
<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-icon>
|
||||
</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-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<div style="height: 50px;" 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>
|
||||
<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>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<theme-switch></theme-switch>
|
||||
<a-checkbox v-if="themeSwitcher.isDarkTheme" style="padding-left: 1rem; vertical-align: middle;"
|
||||
:checked="themeSwitcher.isUltra"
|
||||
@click="themeSwitcher.toggleUltra()">
|
||||
Ultra
|
||||
</a-checkbox>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-layout-content>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-layout-content>
|
||||
</transition>
|
||||
</a-layout>
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "component/password" .}}
|
||||
@@ -372,6 +521,42 @@
|
||||
},
|
||||
},
|
||||
});
|
||||
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>
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -220,7 +220,7 @@
|
||||
clientsBulkModal.visible = false;
|
||||
clientsBulkModal.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
loading(loading=true) {
|
||||
clientsBulkModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -72,7 +72,7 @@
|
||||
clientModal.visible = false;
|
||||
clientModal.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
loading(loading=true) {
|
||||
clientModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -1,27 +1,23 @@
|
||||
{{define "menuItems"}}
|
||||
<a-menu-item key="{{ .base_path }}panel/">
|
||||
<a-icon type="dashboard"></a-icon>
|
||||
<span>{{ i18n "menu.dashboard"}}</span>
|
||||
<span><b>{{ i18n "menu.dashboard"}}</b></span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}panel/inbounds">
|
||||
<a-icon type="user"></a-icon>
|
||||
<span>{{ i18n "menu.inbounds"}}</span>
|
||||
<span><b>{{ i18n "menu.inbounds"}}</b></span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}panel/settings">
|
||||
<a-icon type="setting"></a-icon>
|
||||
<span>{{ i18n "menu.settings"}}</span>
|
||||
<span><b>{{ i18n "menu.settings"}}</b></span>
|
||||
</a-menu-item>
|
||||
<a-menu-item key="{{ .base_path }}panel/xray">
|
||||
<a-icon type="tool"></a-icon>
|
||||
<span>{{ i18n "menu.xray"}}</span>
|
||||
<span><b>{{ i18n "menu.xray"}}</b></span>
|
||||
</a-menu-item>
|
||||
<!--<a-menu-item key="{{ .base_path }}panel/clients">-->
|
||||
<!-- <a-icon type="laptop"></a-icon>-->
|
||||
<!-- <span>Client</span>-->
|
||||
<!--</a-menu-item>-->
|
||||
<a-menu-item key="{{ .base_path }}logout">
|
||||
<a-icon type="logout"></a-icon>
|
||||
<span>{{ i18n "menu.logout"}}</span>
|
||||
<span><b>{{ i18n "menu.logout"}}</b></span>
|
||||
</a-menu-item>
|
||||
{{end}}
|
||||
|
||||
@@ -31,7 +27,13 @@
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||
<a-menu-item mode="inline">
|
||||
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
|
||||
<theme-switch />
|
||||
<theme-switch>
|
||||
</theme-switch>
|
||||
<a-checkbox v-if="themeSwitcher.isDarkTheme" style="padding-left: 1rem; vertical-align: middle;"
|
||||
:checked="themeSwitcher.isUltra"
|
||||
@click="themeSwitcher.toggleUltra()">
|
||||
Ultra
|
||||
</a-checkbox>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
@@ -50,7 +52,13 @@
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" selected-keys="">
|
||||
<a-menu-item mode="inline">
|
||||
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>
|
||||
<theme-switch />
|
||||
<theme-switch>
|
||||
</theme-switch>
|
||||
<a-checkbox v-if="themeSwitcher.isDarkTheme" style="padding-left: 1rem; vertical-align: middle;"
|
||||
:checked="themeSwitcher.isUltra"
|
||||
@click="themeSwitcher.toggleUltra()">
|
||||
Ultra
|
||||
</a-checkbox>
|
||||
</a-menu-item>
|
||||
</a-menu>
|
||||
<a-menu :theme="themeSwitcher.currentTheme" mode="inline" :selected-keys="['{{ .request_uri }}']"
|
||||
|
||||
@@ -5,7 +5,7 @@
|
||||
@input="$emit('input', convertToGregorian($event.target.value)); jalaliDatepicker.hide();"
|
||||
:placeholder="placeholder">
|
||||
<template #addonAfter>
|
||||
<a-icon type="calendar" style="font-size: 16px;"/>
|
||||
<a-icon type="calendar" style="font-size: 14px; opacity: 0.5;"/>
|
||||
</template>
|
||||
</a-input>
|
||||
</div>
|
||||
@@ -57,4 +57,4 @@
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
224
web/html/xui/component/sortableTable.html
Normal file
224
web/html/xui/component/sortableTable.html
Normal file
@@ -0,0 +1,224 @@
|
||||
{{define "component/sortableTableTrigger"}}
|
||||
<a-icon type="drag"
|
||||
class="sortable-icon"
|
||||
style="cursor: move;"
|
||||
@mouseup="mouseUpHandler"
|
||||
@mousedown="mouseDownHandler"
|
||||
@click="clickHandler" />
|
||||
{{end}}
|
||||
|
||||
{{define "component/sortableTable"}}
|
||||
<script>
|
||||
const DRAGGABLE_ROW_CLASS = 'draggable-row';
|
||||
|
||||
const findParentRowElement = (el) => {
|
||||
if (!el || !el.tagName) {
|
||||
return null;
|
||||
} else if (el.classList.contains(DRAGGABLE_ROW_CLASS)) {
|
||||
return el;
|
||||
} else if (el.parentNode) {
|
||||
return findParentRowElement(el.parentNode);
|
||||
} else {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
Vue.component('a-table-sortable', {
|
||||
data() {
|
||||
return {
|
||||
sortingElementIndex: null,
|
||||
newElementIndex: null,
|
||||
};
|
||||
},
|
||||
props: ['data-source', 'customRow'],
|
||||
inheritAttrs: false,
|
||||
provide() {
|
||||
const sortable = {}
|
||||
|
||||
Object.defineProperty(sortable, "setSortableIndex", {
|
||||
enumerable: true,
|
||||
get: () => this.setCurrentSortableIndex,
|
||||
});
|
||||
|
||||
Object.defineProperty(sortable, "resetSortableIndex", {
|
||||
enumerable: true,
|
||||
get: () => this.resetSortableIndex,
|
||||
});
|
||||
|
||||
return {
|
||||
sortable,
|
||||
}
|
||||
},
|
||||
render: function (createElement) {
|
||||
return createElement(
|
||||
'a-table',
|
||||
{
|
||||
class: {
|
||||
'ant-table-is-sorting': this.isDragging(),
|
||||
},
|
||||
props: {
|
||||
...this.$attrs,
|
||||
'data-source': this.records,
|
||||
customRow: (record, index) => this.customRowRender(record, index),
|
||||
},
|
||||
on: this.$listeners,
|
||||
nativeOn: {
|
||||
drop: (e) => this.dropHandler(e),
|
||||
},
|
||||
scopedSlots: this.$scopedSlots,
|
||||
},
|
||||
this.$slots.default,
|
||||
)
|
||||
},
|
||||
created() {
|
||||
this.$memoSort = {};
|
||||
},
|
||||
methods: {
|
||||
isDragging() {
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
return currentIndex !== null && currentIndex !== undefined;
|
||||
},
|
||||
resetSortableIndex(e, index) {
|
||||
this.sortingElementIndex = null;
|
||||
this.newElementIndex = null;
|
||||
this.$memoSort = {};
|
||||
},
|
||||
setCurrentSortableIndex(e, index) {
|
||||
this.sortingElementIndex = index;
|
||||
},
|
||||
dragStartHandler(e, index) {
|
||||
if (!this.isDragging()) {
|
||||
e.preventDefault();
|
||||
return;
|
||||
}
|
||||
},
|
||||
dragStopHandler(e, index) {
|
||||
this.resetSortableIndex(e, index);
|
||||
},
|
||||
dragOverHandler(e, index) {
|
||||
if (!this.isDragging()) {
|
||||
return;
|
||||
}
|
||||
|
||||
e.preventDefault();
|
||||
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
if (index === currentIndex) {
|
||||
this.newElementIndex = null;
|
||||
return;
|
||||
}
|
||||
|
||||
const row = findParentRowElement(e.target);
|
||||
if (!row) {
|
||||
return;
|
||||
}
|
||||
|
||||
const rect = row.getBoundingClientRect();
|
||||
const offsetTop = e.pageY - rect.top;
|
||||
|
||||
if (offsetTop < rect.height / 2) {
|
||||
this.newElementIndex = Math.max(index - 1, 0);
|
||||
} else {
|
||||
this.newElementIndex = index;
|
||||
}
|
||||
},
|
||||
dropHandler(e) {
|
||||
if (this.isDragging()) {
|
||||
this.$emit('onsort', this.sortingElementIndex, this.newElementIndex);
|
||||
}
|
||||
},
|
||||
customRowRender(record, index) {
|
||||
const parentMethodResult = this.customRow?.(record, index) || {};
|
||||
const newIndex = this.newElementIndex;
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
|
||||
return {
|
||||
...parentMethodResult,
|
||||
attrs: {
|
||||
...(parentMethodResult?.attrs || {}),
|
||||
draggable: true,
|
||||
},
|
||||
on: {
|
||||
...(parentMethodResult?.on || {}),
|
||||
dragstart: (e) => this.dragStartHandler(e, index),
|
||||
dragend: (e) => this.dragStopHandler(e, index),
|
||||
dragover: (e) => this.dragOverHandler(e, index),
|
||||
},
|
||||
class: {
|
||||
...(parentMethodResult?.class || {}),
|
||||
[DRAGGABLE_ROW_CLASS]: true,
|
||||
['dragging']: this.isDragging()
|
||||
? (newIndex === null ? index === currentIndex : index === newIndex)
|
||||
: false,
|
||||
},
|
||||
};
|
||||
}
|
||||
},
|
||||
computed: {
|
||||
records() {
|
||||
const newIndex = this.newElementIndex;
|
||||
const currentIndex = this.sortingElementIndex;
|
||||
|
||||
if (!this.isDragging() || newIndex === null || currentIndex === newIndex) {
|
||||
return this.dataSource;
|
||||
}
|
||||
|
||||
if (this.$memoSort.newIndex === newIndex) {
|
||||
return this.$memoSort.list;
|
||||
}
|
||||
|
||||
let list = [...this.dataSource];
|
||||
list.splice(newIndex, 0, list.splice(currentIndex, 1)[0]);
|
||||
|
||||
this.$memoSort = {
|
||||
newIndex,
|
||||
list,
|
||||
};
|
||||
|
||||
return list;
|
||||
}
|
||||
}
|
||||
});
|
||||
|
||||
Vue.component('table-sort-trigger', {
|
||||
template: `{{template "component/sortableTableTrigger"}}`,
|
||||
props: ['item-index'],
|
||||
inject: ['sortable'],
|
||||
methods: {
|
||||
mouseDownHandler(e) {
|
||||
if (this.sortable) {
|
||||
this.sortable.setSortableIndex(e, this.itemIndex);
|
||||
}
|
||||
},
|
||||
mouseUpHandler(e) {
|
||||
if (this.sortable) {
|
||||
this.sortable.resetSortableIndex(e, this.itemIndex);
|
||||
}
|
||||
},
|
||||
clickHandler(e) {
|
||||
e.preventDefault();
|
||||
},
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<style>
|
||||
@media only screen and (max-width: 767px) {
|
||||
.sortable-icon {
|
||||
display: none;
|
||||
}
|
||||
}
|
||||
.ant-table-is-sorting .draggable-row td {
|
||||
background-color: white !important;
|
||||
}
|
||||
.dark .ant-table-is-sorting .draggable-row td {
|
||||
background-color: var(--dark-color-surface-100) !important;
|
||||
}
|
||||
.ant-table-is-sorting .dragging {
|
||||
opacity: 0.5;
|
||||
}
|
||||
.ant-table-is-sorting .dragging .ant-table-row-index {
|
||||
opacity: 0;
|
||||
}
|
||||
</style>
|
||||
{{end}}
|
||||
@@ -10,27 +10,48 @@
|
||||
<script>
|
||||
function createThemeSwitcher() {
|
||||
const isDarkTheme = localStorage.getItem('dark-mode') === 'true';
|
||||
const isUltra = localStorage.getItem('isUltraDarkThemeEnabled') === 'true';
|
||||
if (isUltra) {
|
||||
document.documentElement.setAttribute('data-theme', 'ultra-dark');
|
||||
}
|
||||
const theme = isDarkTheme ? 'dark' : 'light';
|
||||
document.querySelector('body').setAttribute('class', theme)
|
||||
document.querySelector('body').setAttribute('class', theme);
|
||||
return {
|
||||
isDarkTheme,
|
||||
isUltra,
|
||||
get currentTheme() {
|
||||
return this.isDarkTheme ? 'dark' : 'light';
|
||||
},
|
||||
toggleTheme() {
|
||||
this.isDarkTheme = !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;
|
||||
},
|
||||
toggleUltra() {
|
||||
this.isUltra = !this.isUltra;
|
||||
if (this.isUltra) {
|
||||
document.documentElement.setAttribute('data-theme', 'ultra-dark');
|
||||
} else {
|
||||
document.documentElement.removeAttribute('data-theme');
|
||||
}
|
||||
localStorage.setItem('isUltraDarkThemeEnabled', this.isUltra.toString());
|
||||
}
|
||||
};
|
||||
}
|
||||
|
||||
const themeSwitcher = createThemeSwitcher();
|
||||
|
||||
Vue.component('theme-switch', {
|
||||
props: [],
|
||||
template: `{{template "component/themeSwitchTemplate"}}`,
|
||||
data: () => ({ themeSwitcher }),
|
||||
data: () => ({
|
||||
themeSwitcher
|
||||
}),
|
||||
mounted() {
|
||||
this.$message.config({
|
||||
getContainer: () => document.getElementById('message')
|
||||
});
|
||||
document.getElementById('message').className = themeSwitcher.currentTheme;
|
||||
}
|
||||
});
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
86
web/html/xui/dns_modal.html
Normal file
86
web/html/xui/dns_modal.html
Normal file
@@ -0,0 +1,86 @@
|
||||
{{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:8} }" :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}}
|
||||
57
web/html/xui/fakedns_modal.html
Normal file
57
web/html/xui/fakedns_modal.html
Normal file
@@ -0,0 +1,57 @@
|
||||
{{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:8} }" :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}}
|
||||
@@ -1,6 +1,6 @@
|
||||
{{define "form/inbound"}}
|
||||
<!-- base -->
|
||||
<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 label='{{ i18n "enable" }}'>
|
||||
<a-switch v-model="dbInbound.enable"></a-switch>
|
||||
</a-form-item>
|
||||
@@ -54,7 +54,7 @@
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
<a-date-picker style="width: 100%;" v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="dbInbound._expiryTime"></a-date-picker>
|
||||
<persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
|
||||
@@ -114,7 +114,7 @@
|
||||
</template>
|
||||
|
||||
<!-- sniffing -->
|
||||
<template v-if="inbound.canSniffing()">
|
||||
<template>
|
||||
{{template "form/sniffing"}}
|
||||
</template>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -134,28 +134,10 @@
|
||||
<a-form-item label='{{ i18n "pages.xray.wireguard.endpoint" }}'>
|
||||
<a-input v-model.trim="peer.endpoint"></a-input>
|
||||
</a-form-item>
|
||||
<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-form-item label='{{ i18n "pages.xray.wireguard.publicKey" }}'>
|
||||
<a-input v-model.trim="peer.publicKey"></a-input>
|
||||
</a-form-item>
|
||||
<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-form-item label='{{ i18n "pages.xray.wireguard.psk" }}'>
|
||||
<a-input v-model.trim="peer.psk"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
@@ -189,7 +171,6 @@
|
||||
<a-form-item label='ID'>
|
||||
<a-input v-model.trim="outbound.settings.id"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
<!-- vless settings -->
|
||||
<template v-if="outbound.canEnableTlsFlow()">
|
||||
<a-form-item label='Flow'>
|
||||
@@ -212,18 +193,23 @@
|
||||
<a-input v-model.trim="outbound.settings.pass"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- shadowsocks -->
|
||||
<template v-if="outbound.protocol === Protocols.Shadowsocks">
|
||||
<!-- trojan/shadowsocks -->
|
||||
<template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
|
||||
<a-form-item label='{{ i18n "password" }}'>
|
||||
<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-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-form-item>
|
||||
<a-form-item label='UDP over TCP'>
|
||||
<a-switch v-model="outbound.settings.uot"></a-switch>
|
||||
</a-form-item>
|
||||
</template>
|
||||
</template>
|
||||
</template>
|
||||
|
||||
<!-- stream settings -->
|
||||
@@ -363,13 +349,15 @@
|
||||
<a-input v-model.trim="outbound.stream.tls.serverName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="outbound.stream.tls.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select v-model="outbound.stream.tls.fingerprint"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<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>
|
||||
</a-form-item>
|
||||
<a-form-item label="ALPN">
|
||||
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
<a-select mode="multiple"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
v-model="outbound.stream.tls.alpn">
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
@@ -381,11 +369,12 @@
|
||||
|
||||
<!-- reality settings -->
|
||||
<template v-if="outbound.stream.isReality">
|
||||
<a-form-item label='{{ i18n "domainName" }}'>
|
||||
<a-form-item label="SNI">
|
||||
<a-input v-model.trim="outbound.stream.reality.serverName"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="outbound.stream.reality.fingerprint" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select v-model="outbound.stream.reality.fingerprint"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "form/dokodemo"}}
|
||||
<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 label='{{ i18n "pages.inbounds.targetAddress"}}'>
|
||||
<a-input v-model.trim="inbound.settings.address"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
{{define "form/http"}}
|
||||
<a-form>
|
||||
<table style="width: 100%; text-align: center; margin-bottom: 10px;">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<table style="width: 100%; text-align: center; margin: 1rem 0;">
|
||||
<tr>
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</td>
|
||||
@@ -18,4 +18,4 @@
|
||||
</a-input>
|
||||
</a-input-group>
|
||||
</a-form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</template>
|
||||
<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 label='{{ i18n "encryption" }}'>
|
||||
<a-select v-model="inbound.settings.method" @change="SSMethodChange" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="(method,method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "form/socks"}}
|
||||
<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 label='{{ i18n "pages.inbounds.enable" }} UDP'>
|
||||
<a-switch v-model="inbound.settings.udp"></a-switch>
|
||||
</a-form-item>
|
||||
@@ -11,7 +11,7 @@
|
||||
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
|
||||
</a-form-item>
|
||||
<template v-if="inbound.settings.auth === 'password'">
|
||||
<table style="width: 100%; text-align: center; margin-bottom: 10px;">
|
||||
<table style="width: 100%; text-align: center; margin: 1rem 0;">
|
||||
<tr>
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</td>
|
||||
|
||||
@@ -19,27 +19,23 @@
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp && !inbound.stream.isReality">
|
||||
<a-form layout="inline">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-row>
|
||||
<a-button type="primary" size="small"
|
||||
@click="inbound.settings.addFallback()">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-button type="primary" size="small" @click="inbound.settings.addFallback()">+</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- trojan fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;">
|
||||
Fallback [[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;" />
|
||||
</a-divider>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
</a-form-item>
|
||||
<a-form-item label='ALPN'>
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
@@ -53,6 +49,6 @@
|
||||
<a-input-number v-model="fallback.xver" :min="0" :max="2"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<a-divider style="margin:0;"></a-divider>
|
||||
<a-divider style="margin:5px 0;"></a-divider>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
@@ -21,27 +21,23 @@
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp && !inbound.stream.isReality">
|
||||
<a-form layout="inline">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-row>
|
||||
<a-button type="primary" size="small"
|
||||
@click="inbound.settings.addFallback()">
|
||||
+
|
||||
</a-button>
|
||||
</a-row>
|
||||
<a-button type="primary" size="small" @click="inbound.settings.addFallback()">+</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<!-- vless fallbacks -->
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
|
||||
:wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;">
|
||||
Fallback [[ index + 1 ]]
|
||||
<a-icon type="delete" @click="() => inbound.settings.delFallback(index)"
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;"/>
|
||||
style="color: rgb(255, 77, 79);cursor: pointer;" />
|
||||
</a-divider>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model="fallback.name"></a-input>
|
||||
</a-form-item>
|
||||
</a-form-item>
|
||||
<a-form-item label='ALPN'>
|
||||
<a-input v-model="fallback.alpn"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "form/wireguard"}}
|
||||
<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>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
@@ -26,7 +26,7 @@
|
||||
<a-form-item label="Peers">
|
||||
<a-button type="primary" size="small" @click="inbound.settings.addPeer()">+</a-button>
|
||||
</a-form-item>
|
||||
<a-form v-for="(peer, index) in inbound.settings.peers" :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form v-for="(peer, index) in inbound.settings.peers" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-divider style="margin:0;">
|
||||
Peer [[ index + 1 ]]
|
||||
<a-icon v-if="inbound.settings.peers.length>1" type="delete" @click="() => inbound.settings.delPeer(index)"
|
||||
@@ -38,10 +38,16 @@
|
||||
<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>
|
||||
{{ i18n "pages.xray.wireguard.secretKey" }}
|
||||
<a-icon @click="[peer.publicKey, peer.privateKey] = Object.values(Wireguard.generateKeypair())"type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</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-form-item>
|
||||
<a-form-item>
|
||||
@@ -51,7 +57,7 @@
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template>
|
||||
{{ i18n "pages.xray.wireguard.psk" }}
|
||||
<a-icon @click="peer.psk = publicKey=Wireguard.generateKeypair().publicKey"type="sync"> </a-icon>
|
||||
<a-icon @click="peer.psk = Wireguard.keyToBase64(Wireguard.generatePresharedKey())"type="sync"> </a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="peer.psk"></a-input>
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
<a-input-number style="width: 15%;" v-model.number="row.port" min="1" max="65531"></a-input-number>
|
||||
</a-tooltip>
|
||||
<a-input style="width: 20%" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'></a-input>
|
||||
<a-button style="width: 10%; margin: 0px" @click="inbound.stream.externalProxy.splice(index, 1)">-</a-button>
|
||||
<a-button style="width: 10%; margin: 0px; border-radius: 0 1rem 1rem 0;" @click="inbound.stream.externalProxy.splice(index, 1)">-</a-button>
|
||||
</a-input-group>
|
||||
</a-form>
|
||||
{{end}}
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "form/streamGRPC"}}
|
||||
<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 label="Service Name">
|
||||
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "form/streamHTTP"}}
|
||||
<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 label='{{ i18n "path" }}'>
|
||||
<a-input v-model.trim="inbound.stream.http.path"></a-input>
|
||||
</a-form-item>
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
{{define "form/streamKCP"}}
|
||||
<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 label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="inbound.stream.kcp.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select v-model="inbound.stream.kcp.type" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">None</a-select-option>
|
||||
<a-select-option value="srtp">SRTP</a-select-option>
|
||||
<a-select-option value="utp">uTP</a-select-option>
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
{{define "form/streamQUIC"}}
|
||||
<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 label='{{ i18n "pages.inbounds.stream.quic.encryption" }}'>
|
||||
<a-select v-model="inbound.stream.quic.security" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">None</a-select-option>
|
||||
@@ -20,7 +20,7 @@
|
||||
<a-input v-model.trim="inbound.stream.quic.key"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "camouflage" }}'>
|
||||
<a-select v-model="inbound.stream.quic.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select v-model="inbound.stream.quic.type" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="none">None</a-select-option>
|
||||
<a-select-option value="srtp">SRTP</a-select-option>
|
||||
<a-select-option value="utp">uTP</a-select-option>
|
||||
|
||||
@@ -1,8 +1,8 @@
|
||||
{{define "form/streamSettings"}}
|
||||
<!-- select stream network -->
|
||||
<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 label='{{ i18n "transmission" }}'>
|
||||
<a-select v-model="inbound.stream.network" @change="streamNetworkChange"
|
||||
<a-select v-model="inbound.stream.network" style="width: 50%" @change="streamNetworkChange"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="tcp">TCP</a-select-option>
|
||||
<a-select-option value="kcp">mKCP</a-select-option>
|
||||
|
||||
@@ -33,7 +33,7 @@
|
||||
</template>
|
||||
</a-form-item>
|
||||
<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 :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
|
||||
@@ -79,4 +79,4 @@
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
{{end}}
|
||||
|
||||
@@ -7,7 +7,7 @@
|
||||
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
|
||||
<a-button size="small" @click="inbound.stream.ws.addHeader()">+</a-button>
|
||||
<a-button size="small" @click="inbound.stream.ws.addHeader('Host', '')">+</a-button>
|
||||
</a-form-item>
|
||||
<a-form-item :wrapper-col="{span:24}">
|
||||
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
|
||||
|
||||
@@ -34,16 +34,16 @@
|
||||
</a-form-item>
|
||||
<a-form-item label="Min/Max Version">
|
||||
<a-input-group compact>
|
||||
<a-select v-model="inbound.stream.tls.minVersion" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select v-model="inbound.stream.tls.minVersion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model="inbound.stream.tls.maxVersion" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select v-model="inbound.stream.tls.maxVersion" style="width: 50%" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item label="uTLS">
|
||||
<a-select v-model="inbound.stream.tls.settings.fingerprint"
|
||||
<a-select v-model="inbound.stream.tls.settings.fingerprint" style="width: 50%"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value=''>None</a-select-option>
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
@@ -73,10 +73,10 @@
|
||||
@click="inbound.stream.tls.removeCert(index)" style="margin-left: 10px">-</a-button>
|
||||
</a-form-item>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input v-model.trim="cert.certFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input v-model.trim="cert.keyFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
@@ -85,10 +85,10 @@
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
@@ -124,10 +124,10 @@
|
||||
@click="inbound.stream.xtls.removeCert(index)" style="margin-left: 10px">-</a-button>
|
||||
</a-form-item>
|
||||
<template v-if="cert.useFile">
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyPath" }}'>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input v-model.trim="cert.certFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyPath" }}'>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input v-model.trim="cert.keyFile"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
@@ -136,10 +136,10 @@
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKeyContent" }}'>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input type="textarea" :rows="3" v-model="cert.cert"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.keyContent" }}'>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input type="textarea" :rows="3" v-model="cert.key"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
@@ -154,7 +154,7 @@
|
||||
<a-input-number v-model.number="inbound.stream.reality.xver" :min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='uTLS'>
|
||||
<a-select v-model="inbound.stream.reality.settings.fingerprint"
|
||||
<a-select v-model="inbound.stream.reality.settings.fingerprint" style="width: 50%"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
@@ -180,10 +180,10 @@
|
||||
<a-form-item label='SpiderX'>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.spiderX"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Private Key'>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.privatekey" }}'>
|
||||
<a-input v-model.trim="inbound.stream.reality.privateKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Public Key'>
|
||||
<a-form-item label='{{ i18n "pages.inbounds.publicKey" }}'>
|
||||
<a-input v-model.trim="inbound.stream.reality.settings.publicKey"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||
</template>
|
||||
<template slot="online" slot-scope="text, client, index">
|
||||
<template v-if="isClientOnline(client.email)">
|
||||
<template v-if="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
@@ -52,7 +52,7 @@
|
||||
<template slot="title">
|
||||
<template v-if="!isClientEnabled(record, client.email)">{{ i18n "depleted" }}</template>
|
||||
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
|
||||
<template v-else-if="isClientOnline(client.email)">{{ i18n "online" }}</template>
|
||||
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
|
||||
</template>
|
||||
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'">
|
||||
</a-badge>
|
||||
|
||||
@@ -166,7 +166,7 @@
|
||||
<template v-if="app.subSettings.enable && infoModal.clientSettings.subId">
|
||||
<a-divider>Subscription URL</a-divider>
|
||||
<a-row>
|
||||
<a-col :sx="24" :md="22"><a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
|
||||
<a-col :sx="24" :md="22">SUB: <a :href="[[ infoModal.subLink ]]" target="_blank">[[ infoModal.subLink ]]</a></a-col>
|
||||
<a-col :sx="24" :md="2" style="text-align: right;">
|
||||
<a-tooltip title='{{ i18n "copy" }}'>
|
||||
<button class="ant-btn ant-btn-primary" id="copy-sub-link" @click="copyToClipboard('copy-sub-link', infoModal.subLink)">
|
||||
@@ -175,14 +175,24 @@
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
</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 v-if="app.tgBotEnable && infoModal.clientSettings.tgId">
|
||||
<a-divider>Telegram ID</a-divider>
|
||||
<a-row>
|
||||
<a-col :sx="24" :md="22"><a :href="[[ infoModal.tgLink ]]" target="_blank">@[[ infoModal.clientSettings.tgId ]]</a></a-col>
|
||||
<a-col :sx="24" :md="22">[[ infoModal.clientSettings.tgId ]]</a-col>
|
||||
<a-col :sx="24" :md="2" style="text-align: right;">
|
||||
<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>
|
||||
</button>
|
||||
</a-tooltip>
|
||||
@@ -283,24 +293,50 @@
|
||||
</tr>
|
||||
<template v-for="(peer, index) in inbound.settings.peers">
|
||||
<tr>
|
||||
<td colspan="2"><a-tag>Peer [[ index + 1 ]]</a-tag></td>
|
||||
<td colspan="2"><a-divider>Peer [[ index + 1 ]]</a-divider></td>
|
||||
</tr>
|
||||
<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>[[ peer.publicKey ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>{{ i18n "pages.xray.wireguard.psk" }}</td>
|
||||
<td>[[ peer.psk ]]</td>
|
||||
</tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<tr>
|
||||
<td>{{ i18n "pages.xray.wireguard.allowedIPs" }}</td>
|
||||
<td>[[ peer.allowedIPs.join(",") ]]</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<tr class="client-table-odd-row">
|
||||
<td>Keep Alive</td>
|
||||
<td>[[ peer.keepAlive ]]</td>
|
||||
</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>
|
||||
</template>
|
||||
</template>
|
||||
@@ -319,7 +355,7 @@
|
||||
index: null,
|
||||
isExpired: false,
|
||||
subLink: '',
|
||||
tgLink: '',
|
||||
subJsonLink: '',
|
||||
show(dbInbound, index) {
|
||||
this.index = index;
|
||||
this.inbound = dbInbound.toInbound();
|
||||
@@ -327,13 +363,15 @@
|
||||
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
|
||||
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.links = this.inbound.genAllLinks(this.dbInbound.remark, app.remarkModel, this.clientSettings);
|
||||
if (this.inbound.protocol == Protocols.WIREGUARD){
|
||||
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.subId) {
|
||||
this.subLink = this.genSubLink(this.clientSettings.subId);
|
||||
}
|
||||
if (this.clientSettings.tgId) {
|
||||
this.tgLink = "https://t.me/" + this.clientSettings.tgId;
|
||||
this.subJsonLink = this.genSubJsonLink(this.clientSettings.subId);
|
||||
}
|
||||
}
|
||||
this.visible = true;
|
||||
@@ -343,6 +381,9 @@
|
||||
},
|
||||
genSubLink(subID) {
|
||||
return app.subSettings.subURI+subID;
|
||||
},
|
||||
genSubJsonLink(subID) {
|
||||
return app.subSettings.subJsonURI+subID;
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -40,7 +40,7 @@
|
||||
inModal.visible = false;
|
||||
inModal.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
loading(loading=true) {
|
||||
inModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
||||
@@ -56,9 +56,13 @@
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-tag v-if="false" color="red" style="margin-bottom: 10px">
|
||||
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
|
||||
</a-tag>
|
||||
<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>
|
||||
<a-card hoverable>
|
||||
@@ -133,6 +137,10 @@
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export" }}
|
||||
</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-icon type="reload"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetAllTraffic" }}
|
||||
@@ -141,7 +149,7 @@
|
||||
<a-icon type="file-done"></a-icon>
|
||||
{{ i18n "pages.inbounds.resetAllClientTraffics" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delDepletedClients">
|
||||
<a-menu-item key="delDepletedClients" style="color: #FF4D4F;">
|
||||
<a-icon type="rest"></a-icon>
|
||||
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||
</a-menu-item>
|
||||
@@ -178,7 +186,7 @@
|
||||
</a-radio-group>
|
||||
</div>
|
||||
<a-back-top></a-back-top>
|
||||
<a-table :columns="isMobile ? mobileColums : columns" :row-key="dbInbound => dbInbound.id"
|
||||
<a-table :columns="isMobile ? mobileColumns : columns" :row-key="dbInbound => dbInbound.id"
|
||||
:data-source="searchedInbounds"
|
||||
:scroll="isMobile ? {} : { x: 1000 }"
|
||||
:pagination=pagination(searchedInbounds)
|
||||
@@ -196,7 +204,7 @@
|
||||
<a-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="qrcode" v-if="dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser">
|
||||
<a-menu-item key="qrcode" v-if="(dbInbound.isSS && !dbInbound.toInbound().isSSMultiUser) || dbInbound.isWireguard">
|
||||
<a-icon type="qrcode"></a-icon>
|
||||
{{ i18n "qrCode" }}
|
||||
</a-menu-item>
|
||||
@@ -217,7 +225,11 @@
|
||||
<a-icon type="export"></a-icon>
|
||||
{{ i18n "pages.inbounds.export"}}
|
||||
</a-menu-item>
|
||||
<a-menu-item key="delDepletedClients">
|
||||
<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="delDepletedClients" style="color: #FF4D4F;">
|
||||
<a-icon type="rest"></a-icon>
|
||||
{{ i18n "pages.inbounds.delDepletedClients" }}
|
||||
</a-menu-item>
|
||||
@@ -496,7 +508,7 @@
|
||||
scopedSlots: { customRender: 'expiryTime' },
|
||||
}];
|
||||
|
||||
const mobileColums = [{
|
||||
const mobileColumns = [{
|
||||
title: "ID",
|
||||
align: 'right',
|
||||
dataIndex: "id",
|
||||
@@ -559,11 +571,13 @@
|
||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||
subSettings: {
|
||||
enable : false,
|
||||
subURI : ''
|
||||
subURI : '',
|
||||
subJsonURI : '',
|
||||
},
|
||||
remarkModel: '-ieo',
|
||||
datepicker: 'gregorian',
|
||||
tgBotEnable: false,
|
||||
showAlert: false,
|
||||
pageSize: 0,
|
||||
isMobile: window.innerWidth <= 768,
|
||||
},
|
||||
@@ -578,6 +592,7 @@
|
||||
this.refreshing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.getOnlineUsers();
|
||||
this.setInbounds(msg.obj);
|
||||
setTimeout(() => {
|
||||
@@ -604,7 +619,8 @@
|
||||
this.tgBotEnable = tgBotEnable;
|
||||
this.subSettings = {
|
||||
enable : subEnable,
|
||||
subURI: subURI
|
||||
subURI: subURI,
|
||||
subJsonURI: subJsonURI
|
||||
};
|
||||
this.pageSize = pageSize;
|
||||
this.remarkModel = remarkModel;
|
||||
@@ -642,8 +658,12 @@
|
||||
clientCount = clients.length;
|
||||
if (dbInbound.enable) {
|
||||
clients.forEach(client => {
|
||||
client.enable ? active.push(client.email) : deactive.push(client.email);
|
||||
if(this.isClientOnline(client.email)) online.push(client.email);
|
||||
if (client.enable) {
|
||||
active.push(client.email);
|
||||
if (this.isClientOnline(client.email)) online.push(client.email);
|
||||
} else {
|
||||
deactive.push(client.email);
|
||||
}
|
||||
});
|
||||
clientStats.forEach(client => {
|
||||
if (!client.enable) {
|
||||
@@ -668,6 +688,7 @@
|
||||
online: online,
|
||||
};
|
||||
},
|
||||
|
||||
searchInbounds(key) {
|
||||
if (ObjectUtil.isEmpty(key)) {
|
||||
this.searchedInbounds = this.dbInbounds.slice();
|
||||
@@ -731,6 +752,9 @@
|
||||
case "export":
|
||||
this.exportAllLinks();
|
||||
break;
|
||||
case "subs":
|
||||
this.exportAllSubs();
|
||||
break;
|
||||
case "resetInbounds":
|
||||
this.resetAllTraffic();
|
||||
break;
|
||||
@@ -762,6 +786,9 @@
|
||||
case "export":
|
||||
this.inboundLinks(dbInbound.id);
|
||||
break;
|
||||
case "subs":
|
||||
this.exportSubs(dbInbound.id);
|
||||
break;
|
||||
case "clipboard":
|
||||
this.copyToClipboard(dbInbound.id);
|
||||
break;
|
||||
@@ -811,7 +838,7 @@
|
||||
protocol: baseInbound.protocol,
|
||||
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
|
||||
streamSettings: baseInbound.stream.toString(),
|
||||
sniffing: baseInbound.canSniffing() ? baseInbound.sniffing.toString() : '{}',
|
||||
sniffing: baseInbound.sniffing.toString(),
|
||||
};
|
||||
await this.submit('/panel/inbound/add', data, inModal);
|
||||
},
|
||||
@@ -821,9 +848,7 @@
|
||||
okText: '{{ i18n "pages.inbounds.create"}}',
|
||||
cancelText: '{{ i18n "close" }}',
|
||||
confirm: async (inbound, dbInbound) => {
|
||||
inModal.loading();
|
||||
await this.addInbound(inbound, dbInbound);
|
||||
inModal.close();
|
||||
await this.addInbound(inbound, dbInbound, inModal);
|
||||
},
|
||||
isEdit: false
|
||||
});
|
||||
@@ -838,9 +863,7 @@
|
||||
inbound: inbound,
|
||||
dbInbound: dbInbound,
|
||||
confirm: async (inbound, dbInbound) => {
|
||||
inModal.loading();
|
||||
await this.updateInbound(inbound, dbInbound);
|
||||
inModal.close();
|
||||
},
|
||||
isEdit: true
|
||||
});
|
||||
@@ -860,7 +883,7 @@
|
||||
settings: inbound.settings.toString(),
|
||||
};
|
||||
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
|
||||
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
|
||||
data.sniffing = inbound.sniffing.toString();
|
||||
|
||||
await this.submit('/panel/inbound/add', data, inModal);
|
||||
},
|
||||
@@ -879,7 +902,7 @@
|
||||
settings: inbound.settings.toString(),
|
||||
};
|
||||
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
|
||||
if (inbound.canSniffing()) data.sniffing = inbound.sniffing.toString();
|
||||
data.sniffing = inbound.sniffing.toString();
|
||||
|
||||
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
|
||||
},
|
||||
@@ -890,9 +913,7 @@
|
||||
okText: '{{ i18n "pages.client.submitAdd"}}',
|
||||
dbInbound: dbInbound,
|
||||
confirm: async (clients, dbInboundId) => {
|
||||
clientModal.loading();
|
||||
await this.addClient(clients, dbInboundId);
|
||||
clientModal.close();
|
||||
await this.addClient(clients, dbInboundId, clientModal);
|
||||
},
|
||||
isEdit: false
|
||||
});
|
||||
@@ -904,9 +925,7 @@
|
||||
okText: '{{ i18n "pages.client.bulk"}}',
|
||||
dbInbound: dbInbound,
|
||||
confirm: async (clients, dbInboundId) => {
|
||||
clientsBulkModal.loading();
|
||||
await this.addClient(clients, dbInboundId);
|
||||
clientsBulkModal.close();
|
||||
await this.addClient(clients, dbInboundId, clientsBulkModal);
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -935,19 +954,19 @@
|
||||
default: return clients.findIndex(item => item.id === client.id && item.email === client.email);
|
||||
}
|
||||
},
|
||||
async addClient(clients, dbInboundId) {
|
||||
async addClient(clients, dbInboundId, modal) {
|
||||
const data = {
|
||||
id: dbInboundId,
|
||||
settings: '{"clients": [' + clients.toString() + ']}',
|
||||
};
|
||||
await this.submit(`/panel/inbound/addClient`, data);
|
||||
await this.submit(`/panel/inbound/addClient`, data, modal);
|
||||
},
|
||||
async updateClient(client, dbInboundId, clientId) {
|
||||
const data = {
|
||||
id: dbInboundId,
|
||||
settings: '{"clients": [' + client.toString() + ']}',
|
||||
};
|
||||
await this.submit(`/panel/inbound/updateClient/${clientId}`, data);
|
||||
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
|
||||
},
|
||||
resetTraffic(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
@@ -967,7 +986,7 @@
|
||||
},
|
||||
delInbound(dbInboundId) {
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}',
|
||||
title: '{{ i18n "pages.inbounds.deleteInbound"}}' + ' #' + dbInboundId,
|
||||
content: '{{ i18n "pages.inbounds.deleteInboundContent"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
@@ -980,7 +999,7 @@
|
||||
clientId = this.getClientId(dbInbound.protocol, client);
|
||||
if (confirmation){
|
||||
this.$confirm({
|
||||
title: '{{ i18n "pages.inbounds.deleteClient"}}',
|
||||
title: '{{ i18n "pages.inbounds.deleteClient"}}' + ' ' + client.email,
|
||||
content: '{{ i18n "pages.inbounds.deleteClientContent"}}',
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
@@ -1050,8 +1069,8 @@
|
||||
await this.updateClient(clients[index], dbInboundId, clientId);
|
||||
this.loading(false);
|
||||
},
|
||||
async submit(url, data) {
|
||||
const msg = await HttpUtil.postWithModal(url, data);
|
||||
async submit(url, data, modal) {
|
||||
const msg = await HttpUtil.postWithModal(url, data, modal);
|
||||
if (msg.success) {
|
||||
await this.getDBInbounds();
|
||||
}
|
||||
@@ -1186,6 +1205,22 @@
|
||||
newDbInbound = this.checkFallback(dbInbound);
|
||||
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() {
|
||||
promptModal.open({
|
||||
title: '{{ i18n "pages.inbounds.importInbound" }}',
|
||||
@@ -1194,10 +1229,26 @@
|
||||
okText: '{{ i18n "pages.inbounds.import" }}',
|
||||
confirm: async (dbInboundText) => {
|
||||
await this.submit('/panel/inbound/import', {data: dbInboundText}, promptModal);
|
||||
promptModal.close();
|
||||
},
|
||||
});
|
||||
},
|
||||
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() {
|
||||
let copyText = [];
|
||||
for (const dbInbound of this.dbInbounds) {
|
||||
@@ -1238,7 +1289,7 @@
|
||||
pagination(obj){
|
||||
if (this.pageSize > 0 && obj.length>this.pageSize) {
|
||||
// Set page options based on object size
|
||||
sizeOptions = []
|
||||
sizeOptions = [];
|
||||
for (i=this.pageSize;i<=obj.length;i=i+this.pageSize) {
|
||||
sizeOptions.push(i.toString());
|
||||
}
|
||||
@@ -1251,8 +1302,8 @@
|
||||
position: 'bottom',
|
||||
pageSize: this.pageSize,
|
||||
pageSizeOptions: sizeOptions
|
||||
}
|
||||
return p
|
||||
};
|
||||
return p;
|
||||
}
|
||||
return false
|
||||
},
|
||||
@@ -1266,6 +1317,9 @@
|
||||
}, 500)
|
||||
},
|
||||
mounted() {
|
||||
if (window.location.protocol !== "https:") {
|
||||
this.showAlert = true;
|
||||
}
|
||||
window.addEventListener('resize', this.onResize);
|
||||
this.onResize();
|
||||
this.loading();
|
||||
@@ -1303,7 +1357,6 @@
|
||||
}
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
{{template "inboundModal"}}
|
||||
@@ -1313,6 +1366,5 @@
|
||||
{{template "inboundInfoModal"}}
|
||||
{{template "clientsModal"}}
|
||||
{{template "clientsBulkModal"}}
|
||||
|
||||
</body>
|
||||
</html>
|
||||
|
||||
@@ -10,13 +10,11 @@
|
||||
margin-inline: 0.3rem;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-col-sm-24 {
|
||||
margin-top: 10px;
|
||||
}
|
||||
|
||||
.ant-card-dark h2 {
|
||||
color: hsla(0, 0%, 100%, .65);
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
</style>
|
||||
|
||||
@@ -26,6 +24,15 @@
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<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>
|
||||
<a-row>
|
||||
<a-card hoverable>
|
||||
@@ -36,15 +43,15 @@
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.cpu.color"
|
||||
:percent="status.cpu.percent"></a-progress>
|
||||
<div>CPU: [[ cpuCoreFormat(status.cpuCores) ]]</div>
|
||||
<div>Speed: [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
|
||||
<div><b>CPU:</b> [[ cpuCoreFormat(status.cpuCores) ]]</div>
|
||||
<div><b>Speed:</b> [[ cpuSpeedFormat(status.cpuSpeedMhz) ]]</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.mem.color"
|
||||
:percent="status.mem.percent"></a-progress>
|
||||
<div>
|
||||
{{ i18n "pages.index.memory"}}: [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
|
||||
<b>{{ i18n "pages.index.memory"}}:</b> [[ sizeFormat(status.mem.current) ]] / [[ sizeFormat(status.mem.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -56,7 +63,7 @@
|
||||
:stroke-color="status.swap.color"
|
||||
:percent="status.swap.percent"></a-progress>
|
||||
<div>
|
||||
Swap: [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
|
||||
<b>Swap:</b> [[ sizeFormat(status.swap.current) ]] / [[ sizeFormat(status.swap.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12" style="text-align: center">
|
||||
@@ -64,7 +71,7 @@
|
||||
:stroke-color="status.disk.color"
|
||||
:percent="status.disk.percent"></a-progress>
|
||||
<div>
|
||||
{{ i18n "pages.index.hard"}}: [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
|
||||
<b>{{ i18n "pages.index.hard"}}:</b> [[ sizeFormat(status.disk.current) ]] / [[ sizeFormat(status.disk.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
</a-row>
|
||||
@@ -75,25 +82,25 @@
|
||||
</transition>
|
||||
<transition name="list" appear>
|
||||
<a-row>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
3X-UI <a 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 href="https://t.me/panel3xui" target="_blank"><a-tag color="green">@panel3xui</a-tag></a>
|
||||
<b>3X-UI:</b>
|
||||
<a rel="noopener" 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://t.me/panel3xui" target="_blank"><a-tag color="green">@Panel3xui</a-tag></a>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
{{ i18n "menu.link" }}:
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</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>
|
||||
<b>{{ i18n "pages.index.operationHours" }}:</b>
|
||||
<a-tag :color="status.xray.color">Xray: [[ formatSecond(status.appStats.uptime) ]]</a-tag>
|
||||
<a-tag color="green">OS: [[ formatSecond(status.uptime) ]]</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
{{ i18n "pages.index.xrayStatus" }}:
|
||||
<a-tag :color="status.xray.color">[[ status.xray.state ]]</a-tag>
|
||||
<b>{{ i18n "pages.index.xrayStatus" }}:</b>
|
||||
<a-tag style="text-transform: capitalize;" :color="status.xray.color">[[ status.xray.state ]]
|
||||
</a-tag>
|
||||
<a-popover v-if="status.xray.state === State.Error"
|
||||
:overlay-class-name="themeSwitcher.currentTheme">
|
||||
<span slot="title" style="font-size: 12pt">An error occurred while running Xray
|
||||
@@ -106,137 +113,143 @@
|
||||
</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="restartXrayService">{{ i18n "pages.index.restartXray" }}</a-tag>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openSelectV2rayVersion">{{ i18n "pages.index.xraySwitch" }}</a-tag>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openSelectV2rayVersion">v[[ status.xray.version ]]</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
{{ i18n "pages.index.operationHours" }}:
|
||||
Xray
|
||||
<a-tag color="green">[[ formatSecond(status.appStats.uptime) ]]</a-tag>
|
||||
OS
|
||||
<a-tag color="green">[[ formatSecond(status.uptime) ]]</a-tag>
|
||||
<b>{{ i18n "menu.link" }}:</b>
|
||||
<a-tag color="purple" style="cursor: pointer;" @click="openLogs()">{{ i18n "pages.index.logs" }}</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-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
{{ i18n "pages.index.systemLoad" }}: [[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
|
||||
<b>{{ i18n "pages.index.systemLoad" }}:</b>
|
||||
<a-tag color="green">
|
||||
<a-tooltip>
|
||||
[[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.systemLoadDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
{{ i18n "usage"}}:
|
||||
RAM [[ sizeFormat(status.appStats.mem) ]] -
|
||||
Threads [[ status.appStats.threads ]]
|
||||
</a-tooltip>
|
||||
<b>{{ i18n "usage"}}:</b>
|
||||
<a-tag color="green">
|
||||
RAM: [[ sizeFormat(status.appStats.mem) ]]
|
||||
</a-tag>
|
||||
<a-tag color="green">
|
||||
Threads: [[ status.appStats.threads ]]
|
||||
</a-tag>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="global"></a-icon>
|
||||
IPv4:
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="global"></a-icon> IPv4
|
||||
<template slot="title">
|
||||
[[ status.publicIP.ipv4 ]]
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-icon type="global"></a-icon>
|
||||
IPv6:
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="global"></a-icon> IPv6
|
||||
<template slot="title">
|
||||
[[ status.publicIP.ipv6 ]]
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="swap"></a-icon>
|
||||
TCP: [[ status.tcpCount ]]
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="swap"></a-icon> TCP: [[ status.tcpCount ]]
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionTcpCountDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-icon type="swap"></a-icon>
|
||||
UDP: [[ status.udpCount ]]
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="swap"></a-icon> UDP: [[ status.udpCount ]]
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.connectionUdpCountDesc" }}
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-up"></a-icon>
|
||||
[[ sizeFormat(status.netIO.up) ]]/s
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="arrow-up"></a-icon>
|
||||
Up: [[ sizeFormat(status.netIO.up) ]]/s
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.upSpeed" }}
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-icon type="arrow-down"></a-icon>
|
||||
[[ sizeFormat(status.netIO.down) ]]/s
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="arrow-down"></a-icon>
|
||||
Down: [[ sizeFormat(status.netIO.down) ]]/s
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.downSpeed" }}
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card hoverable>
|
||||
<a-row>
|
||||
<a-col :span="12">
|
||||
<a-icon type="cloud-upload"></a-icon>
|
||||
[[ sizeFormat(status.netTraffic.sent) ]]
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="cloud-upload"></a-icon>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.totalSent" }}
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</template> Out: [[ sizeFormat(status.netTraffic.sent) ]]
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
<a-col :span="12">
|
||||
<a-icon type="cloud-download"></a-icon>
|
||||
[[ sizeFormat(status.netTraffic.recv) ]]
|
||||
<a-tag>
|
||||
<a-tooltip>
|
||||
<a-icon type="cloud-download"></a-icon>
|
||||
<template slot="title">
|
||||
{{ i18n "pages.index.totalReceive" }}
|
||||
</template>
|
||||
<a-icon type="question-circle"></a-icon>
|
||||
</template> In: [[ sizeFormat(status.netTraffic.recv) ]]
|
||||
</a-tooltip>
|
||||
</a-tag>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-card>
|
||||
@@ -256,51 +269,51 @@
|
||||
></a-alert>
|
||||
<template v-for="version, index in versionModal.versions">
|
||||
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'"
|
||||
style="margin: 10px" @click="switchV2rayVersion(version)">
|
||||
style="margin-right: 10px" @click="switchV2rayVersion(version)">
|
||||
[[ version ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
</a-modal>
|
||||
|
||||
<a-modal id="log-modal" v-model="logModal.visible" title="Logs"
|
||||
:closable="true" @ok="() => logModal.visible = false" @cancel="() => logModal.visible = false"
|
||||
<a-modal id="log-modal" v-model="logModal.visible"
|
||||
:closable="true" @cancel="() => logModal.visible = false"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
width="800px"
|
||||
footer="">
|
||||
width="800px" footer="">
|
||||
<template slot="title">
|
||||
{{ 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-item label="Count">
|
||||
<a-select v-model="logModal.rows"
|
||||
style="width: 80px"
|
||||
@change="openLogs()"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="10">10</a-select-option>
|
||||
<a-select-option value="20">20</a-select-option>
|
||||
<a-select-option value="50">50</a-select-option>
|
||||
<a-select-option value="100">100</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="Log Level">
|
||||
<a-select v-model="logModal.level"
|
||||
style="width: 120px"
|
||||
@change="openLogs()"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="debug">Debug</a-select-option>
|
||||
<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-input-group compact>
|
||||
<a-select v-model="logModal.rows" style="width:70px;"
|
||||
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="10">10</a-select-option>
|
||||
<a-select-option value="20">20</a-select-option>
|
||||
<a-select-option value="50">50</a-select-option>
|
||||
<a-select-option value="100">100</a-select-option>
|
||||
</a-select>
|
||||
<a-select v-model="logModal.level" style="width:100px;"
|
||||
@change="openLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="debug">Debug</a-select-option>
|
||||
<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-input-group>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<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-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-button type="primary" style="margin-bottom: 10px;"
|
||||
<a-form-item style="float: right;">
|
||||
<a-button type="primary" icon="download"
|
||||
:href="'data:application/text;charset=utf-8,' + encodeURIComponent(logModal.logs.join('\n'))" download="x-ui.log">
|
||||
{{ i18n "download" }} x-ui.log
|
||||
</a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
@@ -308,8 +321,8 @@
|
||||
</a-modal>
|
||||
|
||||
<a-modal id="backup-modal" v-model="backupModal.visible" :title="backupModal.title"
|
||||
:closable="true" :class="themeSwitcher.currentTheme"
|
||||
@ok="() => backupModal.hide()" @cancel="() => backupModal.hide()">
|
||||
:closable="true" footer=""
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
|
||||
:message="backupModal.description"
|
||||
show-icon
|
||||
@@ -433,15 +446,14 @@
|
||||
const logModal = {
|
||||
visible: false,
|
||||
logs: [],
|
||||
formattedLogs: '',
|
||||
rows: 20,
|
||||
level: 'info',
|
||||
syslog: false,
|
||||
loading: false,
|
||||
show(logs) {
|
||||
this.visible = true;
|
||||
this.logs = logs;
|
||||
this.formattedLogs = logs.length > 0 ? this.formatLogs(logs) : "No Record...";
|
||||
this.logs = logs;
|
||||
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
|
||||
},
|
||||
formatLogs(logs) {
|
||||
let formattedLogs = '';
|
||||
@@ -519,6 +531,7 @@
|
||||
backupModal,
|
||||
spinning: false,
|
||||
loadingTip: '{{ i18n "loading"}}',
|
||||
showAlert: false,
|
||||
},
|
||||
methods: {
|
||||
loading(spinning, tip = '{{ i18n "loading"}}') {
|
||||
@@ -642,14 +655,14 @@
|
||||
},
|
||||
},
|
||||
async mounted() {
|
||||
let retries = 0;
|
||||
while (retries < 5) {
|
||||
if (window.location.protocol !== "https:") {
|
||||
this.showAlert = true;
|
||||
}
|
||||
while (true) {
|
||||
try {
|
||||
await this.getStatus();
|
||||
retries = 0;
|
||||
} catch (e) {
|
||||
console.error("Error occurred while fetching status:", e);
|
||||
retries++;
|
||||
console.error(e);
|
||||
}
|
||||
await PromiseUtil.sleep(2000);
|
||||
}
|
||||
|
||||
@@ -75,28 +75,37 @@
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="confAlerts.length>0" style="margin: 10px 5px;"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
show-icon
|
||||
closable
|
||||
>
|
||||
<template slot="description">
|
||||
<b>{{ i18n "secAlertConf" }}</b>
|
||||
<ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
|
||||
</template>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<a-space direction="vertical">
|
||||
<a-card hoverable style="margin-bottom: .5rem;">
|
||||
<a-row>
|
||||
<a-col :xs="24" :sm="8" style="padding: 4px;">
|
||||
<a-card hoverable style="margin-bottom: .5rem; overflow-x: hidden;">
|
||||
<a-row style="display: flex; flex-wrap: wrap; align-items: center;">
|
||||
<a-col :xs="24" :sm="10" style="padding: 4px;">
|
||||
<a-space direction="horizontal">
|
||||
<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-space>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="16">
|
||||
<a-col :xs="24" :sm="14">
|
||||
<template>
|
||||
<div>
|
||||
<template>
|
||||
<div>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
|
||||
</a-back-top>
|
||||
<a-alert type="warning" style="float: right; width: fit-content"
|
||||
message='{{ i18n "pages.settings.infoDesc" }}'
|
||||
show-icon
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
|
||||
</a-back-top>
|
||||
<a-alert type="warning" style="float: right; width: fit-content"
|
||||
message='{{ i18n "pages.settings.infoDesc" }}'
|
||||
show-icon
|
||||
>
|
||||
</div>
|
||||
</template>
|
||||
</a-col>
|
||||
@@ -164,7 +173,6 @@
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Language" />
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select
|
||||
@@ -234,7 +242,6 @@
|
||||
<a-button type="primary" :loading="this.changeSecret" @click="updateSecret">{{ i18n "confirm" }}</a-button>
|
||||
</a-form>
|
||||
</a-tab-pane>
|
||||
|
||||
<a-tab-pane key="3" tab='{{ i18n "pages.settings.TGBotSettings"}}'>
|
||||
<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>
|
||||
@@ -250,15 +257,13 @@
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta title="Telegram Bot Language" />
|
||||
</a-col>
|
||||
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select
|
||||
ref="selectBotLang"
|
||||
v-model="allSetting.tgLang"
|
||||
: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">
|
||||
<span role="img" :aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
@@ -285,6 +290,17 @@
|
||||
<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-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-space>
|
||||
</a-spin>
|
||||
@@ -310,11 +326,28 @@
|
||||
saveBtnDisable: true,
|
||||
user: {},
|
||||
lang: getLang(),
|
||||
showAlert: false,
|
||||
remarkModels: {i:'Inbound',e:'Email',o:'Other'},
|
||||
remarkSeparators: [' ','-','_','@',':','~','|',',','.','/'],
|
||||
datepickerList: [{name:'Gregorian (Standard)', value: 'gregorian'}, {name:'Jalalian (شمسی)', value: 'jalalian'}],
|
||||
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() {
|
||||
rm = this.allSetting.remarkModel;
|
||||
return rm.length>1 ? rm.substring(1).split('') : [];
|
||||
@@ -443,14 +476,59 @@
|
||||
}
|
||||
},
|
||||
},
|
||||
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 (window.location.protocol !== "https:") alerts.push('{{ i18n "secAlertSSL" }}');
|
||||
if (this.allSetting.webPort == 54321) alerts.push('{{ i18n "secAlertPanelPort" }}');
|
||||
panelPath = window.location.pathname.split('/').length<4
|
||||
if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "secAlertPanelURI" }}');
|
||||
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 "secAlertSubURI" }}');
|
||||
subJsonPath = this.allSetting.subJsonURI.length >0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
|
||||
if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}');
|
||||
}
|
||||
return alerts
|
||||
}
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
await this.getAllSetting();
|
||||
while (true) {
|
||||
await PromiseUtil.sleep(600);
|
||||
await PromiseUtil.sleep(1000);
|
||||
this.saveBtnDisable = this.oldAllSetting.equals(this.allSetting);
|
||||
}
|
||||
},
|
||||
}
|
||||
});
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
</html>
|
||||
@@ -27,7 +27,7 @@
|
||||
<a-divider style="margin: 0;">{{ i18n "pages.settings.toasts.modifySettings" }}</a-divider>
|
||||
<a-collapse style="margin: 10px 0;">
|
||||
<a-collapse-panel header='WARP/WARP+ License Key'>
|
||||
<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 label="License Key">
|
||||
<a-input v-model="warpPlus"></a-input>
|
||||
<a-button @click="updateLicense(warpPlus)" :disabled="warpPlus.length<26" :loading="warpModal.confirmLoading">{{ i18n "pages.inbounds.update" }}</a-button>
|
||||
@@ -108,7 +108,7 @@
|
||||
this.visible = false;
|
||||
this.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
loading(loading=true) {
|
||||
this.confirmLoading = loading;
|
||||
},
|
||||
async getData(){
|
||||
@@ -140,7 +140,7 @@
|
||||
mtu: 1420,
|
||||
secretKey: warpModal.warpData.private_key,
|
||||
address: Object.values(config.interface.addresses),
|
||||
domainStrategy: 'ForceIPv6v4',
|
||||
domainStrategy: 'ForceIP',
|
||||
peers: [{
|
||||
publicKey: peer.public_key,
|
||||
endpoint: peer.endpoint.host,
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
<!DOCTYPE html>
|
||||
<html lang="en">
|
||||
{{template "head" .}}
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/codemirror.css?{{ .cur_ver }}">
|
||||
<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/lint/lint.css">
|
||||
|
||||
<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/codemirror/codemirror.js"></script>
|
||||
<script src="{{ .base_path }}assets/codemirror/codemirror.js?{{ .cur_ver }}"></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/jsonlint.js"></script>
|
||||
@@ -63,10 +63,19 @@
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
<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-card hoverable style="margin-bottom: .5rem;">
|
||||
<a-row>
|
||||
<a-col :xs="24" :sm="8" style="padding: 4px;">
|
||||
<a-row style="display: flex; flex-wrap: wrap; align-items: center;">
|
||||
<a-col :xs="24" :sm="10" style="padding: 4px;">
|
||||
<a-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateXraySetting">{{ i18n "pages.xray.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartXray">{{ i18n "pages.xray.restart" }}</a-button>
|
||||
@@ -80,7 +89,7 @@
|
||||
</a-popover>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="16">
|
||||
<a-col :xs="24" :sm="14">
|
||||
<template>
|
||||
<div>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
|
||||
@@ -99,7 +108,7 @@
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-tab-pane key="tpl-1" tab='{{ i18n "pages.xray.basicTemplate"}}'>
|
||||
<a-space direction="horizontal" style="padding: 20px 20px">
|
||||
<a-button type="primary" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
|
||||
<a-button type="danger" @click="resetXrayConfigToDefault">{{ i18n "pages.settings.resetDefaultConfig" }}</a-button>
|
||||
</a-space>
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='{{ i18n "pages.xray.generalConfigs"}}'>
|
||||
@@ -114,15 +123,12 @@
|
||||
<a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta
|
||||
title='{{ i18n "pages.xray.FreedomStrategy" }}'
|
||||
description='{{ i18n "pages.xray.FreedomStrategyDesc" }}'/>
|
||||
<a-list-item-meta title='{{ i18n "pages.xray.FreedomStrategy" }}'
|
||||
description='{{ i18n "pages.xray.FreedomStrategyDesc" }}' />
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<template>
|
||||
<a-select
|
||||
v-model="freedomStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
<a-select v-model="freedomStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
style="width: 100%">
|
||||
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
@@ -132,22 +138,67 @@
|
||||
</a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-list-item-meta
|
||||
title='{{ i18n "pages.xray.RoutingStrategy" }}'
|
||||
description='{{ i18n "pages.xray.RoutingStrategyDesc" }}'/>
|
||||
<a-list-item-meta title='{{ i18n "pages.xray.RoutingStrategy" }}'
|
||||
description='{{ i18n "pages.xray.RoutingStrategyDesc" }}' />
|
||||
</a-col>
|
||||
<a-col :lg="24" :xl="12">
|
||||
<a-select v-model="routingStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
|
||||
style="width: 100%">
|
||||
<a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||
</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="routingStrategy"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
style="width: 100%">
|
||||
<a-select-option v-for="s in routingDomainStrategies" :value="s">[[ s ]]</a-select-option>
|
||||
<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>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-list-item>
|
||||
<a-row style="padding: 20px">
|
||||
<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 header='{{ i18n "pages.xray.blockConfigs"}}'>
|
||||
<a-row :xs="24" :sm="24" :lg="12">
|
||||
@@ -227,8 +278,11 @@
|
||||
<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.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>
|
||||
<a-button v-else style="margin: 10px 0;" @click="showWarp">WARP {{ i18n "pages.xray.rules.outbound" }}</a-button>
|
||||
<a-button v-else type="primary" icon="cloud" style="margin: 15px 20px;" @click="showWarp()">WARP</a-button>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-tab-pane>
|
||||
@@ -236,15 +290,19 @@
|
||||
<a-alert type="warning" style="margin-bottom: 10px; width: fit-content"
|
||||
message='{{ i18n "pages.xray.RoutingsDesc"}}' show-icon></a-alert>
|
||||
<a-button type="primary" icon="plus" @click="addRule">{{ i18n "pages.xray.rules.add" }}</a-button>
|
||||
<a-table :columns="isMobile ? rulesMobileColumns : rulesColumns" bordered
|
||||
<a-table-sortable :columns="isMobile ? rulesMobileColumns : rulesColumns" bordered
|
||||
:row-key="r => r.key"
|
||||
:data-source="routingRuleData"
|
||||
:scroll="isMobile ? {} : { x: 1000 }"
|
||||
:pagination="false"
|
||||
:indent-size="0"
|
||||
:style="isMobile ? 'padding: 5px 0' : 'margin-top: 10px;'">
|
||||
:style="isMobile ? 'padding: 5px 0' : 'margin-top: 10px;'"
|
||||
v-on:onSort="replaceRule">
|
||||
<template slot="action" slot-scope="text, rule, index">
|
||||
[[ index+1 ]]
|
||||
<table-sort-trigger :item-index="index"></table-sort-trigger>
|
||||
<span class="ant-table-row-index">
|
||||
[[ index+1 ]]
|
||||
</span>
|
||||
<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">
|
||||
@@ -293,6 +351,14 @@
|
||||
[[ rule.outboundTag ]]
|
||||
</a-popover>
|
||||
</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">
|
||||
<a-popover placement="bottomRight"
|
||||
v-if="(rule.source+rule.sourcePort+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
|
||||
@@ -331,6 +397,10 @@
|
||||
<td>Port</td>
|
||||
<td><a-tag color="green" v-for="r in rule.port.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.balancerTag">
|
||||
<td>Balancer Tag</td>
|
||||
<td><a-tag color="blue">[[ rule.balancerTag ]]</a-tag></td>
|
||||
</tr>
|
||||
</table>
|
||||
</template>
|
||||
<a-button shape="round" size="small" style="font-size: 14px; padding: 0 10px;">
|
||||
@@ -338,11 +408,27 @@
|
||||
</a-button>
|
||||
</a-popover>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-table-sortable>
|
||||
</a-tab-pane>
|
||||
<a-tab-pane key="tpl-3" tab='{{ i18n "pages.xray.Outbounds"}}' style="padding-top: 20px;" force-render="true">
|
||||
<a-button type="primary" icon="plus" @click="addOutbound()" style="margin-bottom: 10px;">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
|
||||
<a-button type="primary" @click="showWarp()" style="margin-bottom: 10px;">WARP</a-button>
|
||||
<a-row>
|
||||
<a-col :xs="12" :sm="12" :lg="12">
|
||||
<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-popconfirm placement="topRight" @confirm="resetOutboundTraffic(-1)"
|
||||
title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
|
||||
:overlay-class-name="themeSwitcher.currentTheme"
|
||||
ok-text='{{ i18n "reset"}}'
|
||||
cancel-text='{{ i18n "cancel"}}'>
|
||||
<a-icon slot="icon" type="question-circle-o" :style="themeSwitcher.isDarkTheme ? 'color: #008771' : 'color: #008771'"></a-icon>
|
||||
<a-icon type="retweet" style="cursor: pointer;"></a-icon>
|
||||
</a-popconfirm>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-table :columns="outboundColumns" bordered
|
||||
:row-key="r => r.key"
|
||||
:data-source="outboundData"
|
||||
@@ -355,10 +441,19 @@
|
||||
<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 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-icon type="edit"></a-icon>
|
||||
{{ i18n "edit" }}
|
||||
</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)">
|
||||
<span style="color: #FF4D4F">
|
||||
<a-icon type="delete"></a-icon> {{ i18n "delete"}}
|
||||
@@ -378,11 +473,14 @@
|
||||
<a-tag style="margin:0;" v-if="outbound.streamSettings.security=='reality'" color="green">reality</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template slot="traffic" slot-scope="text, outbound, index">
|
||||
<a-tag color="green">[[ findOutboundTraffic(outbound) ]]</a-tag>
|
||||
</template>
|
||||
</a-table>
|
||||
</a-tab-pane>
|
||||
<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-table :columns="reverseColumns" bordered
|
||||
<a-table :columns="reverseColumns" bordered v-if="reverseData.length>0"
|
||||
:row-key="r => r.key"
|
||||
:data-source="reverseData"
|
||||
:scroll="isMobile ? {} : { x: 200 }"
|
||||
@@ -408,6 +506,123 @@
|
||||
</template>
|
||||
</a-table>
|
||||
</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-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' : ''">
|
||||
@@ -426,10 +641,14 @@
|
||||
</a-layout>
|
||||
{{template "js" .}}
|
||||
{{template "component/themeSwitcher" .}}
|
||||
{{template "component/sortableTable" .}}
|
||||
{{template "component/setting"}}
|
||||
{{template "ruleModal"}}
|
||||
{{template "outModal"}}
|
||||
{{template "reverseModal"}}
|
||||
{{template "balancerModal"}}
|
||||
{{template "dnsModal"}}
|
||||
{{template "fakednsModal"}}
|
||||
{{template "warpModal"}}
|
||||
<script>
|
||||
const rulesColumns = [
|
||||
@@ -446,9 +665,10 @@
|
||||
{ title: 'Domain', dataIndex: 'domain', align: 'center', width: 20, ellipsis: true },
|
||||
{ title: 'Port', dataIndex: 'port', align: 'center', width: 10, ellipsis: true }]},
|
||||
{ title: '{{ i18n "pages.xray.rules.inbound"}}', children: [
|
||||
{ title: 'Inbound Tag', dataIndex: 'inboundTag', align: 'center', width: 20, ellipsis: true },
|
||||
{ title: 'Inbound Tag', dataIndex: 'inboundTag', align: 'center', width: 15, ellipsis: true },
|
||||
{ title: 'Client Email', dataIndex: 'user', align: 'center', width: 20, ellipsis: true }]},
|
||||
{ title: '{{ i18n "pages.xray.rules.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 20 },
|
||||
{ title: '{{ i18n "pages.xray.rules.outbound"}}', dataIndex: 'outboundTag', align: 'center', width: 15 },
|
||||
{ title: '{{ i18n "pages.xray.rules.balancer"}}', dataIndex: 'balancerTag', align: 'center', width: 15 },
|
||||
];
|
||||
|
||||
const rulesMobileColumns = [
|
||||
@@ -463,6 +683,7 @@
|
||||
{ title: '{{ i18n "pages.xray.outbound.tag"}}', dataIndex: 'tag', align: 'center', width: 50 },
|
||||
{ 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.inbounds.traffic" }}', align: 'center', width: 50, scopedSlots: { customRender: 'traffic' } },
|
||||
];
|
||||
|
||||
const reverseColumns = [
|
||||
@@ -472,6 +693,25 @@
|
||||
{ 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({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
@@ -483,8 +723,11 @@
|
||||
oldXraySetting: '',
|
||||
xraySetting: '',
|
||||
inboundTags: [],
|
||||
outboundsTraffic: [],
|
||||
saveBtnDisable: true,
|
||||
refreshing: false,
|
||||
restartResult: '',
|
||||
showAlert: false,
|
||||
isMobile: window.innerWidth <= 768,
|
||||
advSettings: 'xraySetting',
|
||||
cm: null,
|
||||
@@ -521,6 +764,9 @@
|
||||
protocol: "freedom"
|
||||
},
|
||||
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
|
||||
logLevel: ["none" , "debug" , "info" , "warning", "error"],
|
||||
access: ["none" , "./access.log" ],
|
||||
error: ["none" , "./error.log" ],
|
||||
settingsData: {
|
||||
protocols: {
|
||||
bittorrent: ["bittorrent"],
|
||||
@@ -547,6 +793,9 @@
|
||||
google: ["geosite:google"],
|
||||
spotify: ["geosite:spotify"],
|
||||
netflix: ["geosite:netflix"],
|
||||
meta: ["geosite:meta"],
|
||||
apple: ["geosite:apple"],
|
||||
reddit: ["geosite:reddit"],
|
||||
cn: [
|
||||
"geosite:cn",
|
||||
"regexp:.*\\.cn$"
|
||||
@@ -581,6 +830,12 @@
|
||||
loading(spinning = true) {
|
||||
this.spinning = spinning;
|
||||
},
|
||||
async getOutboundsTraffic() {
|
||||
const msg = await HttpUtil.get("/panel/xray/getOutboundsTraffic");
|
||||
if (msg.success) {
|
||||
this.outboundsTraffic = msg.obj;
|
||||
}
|
||||
},
|
||||
async getXraySetting() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("/panel/xray/");
|
||||
@@ -759,6 +1014,14 @@
|
||||
}
|
||||
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) {
|
||||
serverObj = null;
|
||||
switch(o.protocol){
|
||||
@@ -816,6 +1079,126 @@
|
||||
outbounds.splice(index,1);
|
||||
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(){
|
||||
reverseModal.show({
|
||||
title: '{{ i18n "pages.xray.outbound.addReverse"}}',
|
||||
@@ -898,9 +1281,69 @@
|
||||
newRules = newTemplateSettings.routing.rules.filter(r => r.outboundTag != oldData.tag);
|
||||
}
|
||||
newTemplateSettings.routing.rules = newRules;
|
||||
|
||||
|
||||
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(){
|
||||
ruleModal.show({
|
||||
title: '{{ i18n "pages.xray.rules.add"}}',
|
||||
@@ -947,8 +1390,12 @@
|
||||
}
|
||||
},
|
||||
async mounted() {
|
||||
if (window.location.protocol !== "https:") {
|
||||
this.showAlert = true;
|
||||
}
|
||||
await this.getXraySetting();
|
||||
await this.getXrayResult();
|
||||
await this.getOutboundsTraffic();
|
||||
while (true) {
|
||||
await PromiseUtil.sleep(800);
|
||||
this.saveBtnDisable = this.oldXraySetting === this.xraySetting;
|
||||
@@ -1004,6 +1451,27 @@
|
||||
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: {
|
||||
get: function () { return this.templateSettings ? JSON.stringify(this.templateSettings.routing.rules, null, 2) : null; },
|
||||
set: function (newValue) {
|
||||
@@ -1065,6 +1533,39 @@
|
||||
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: {
|
||||
get: function () {
|
||||
return this.templateRuleGetter({ outboundTag: "blocked", property: "ip" });
|
||||
@@ -1187,14 +1688,14 @@
|
||||
familyProtectSettings: {
|
||||
get: function () {
|
||||
if (!this.templateSettings || !this.templateSettings.dns || !this.templateSettings.dns.servers) return false;
|
||||
return doAllItemsExist(this.templateSettings.dns.servers, this.settingsData.familyProtectDNS.servers);
|
||||
return doAllItemsExist(this.settingsData.familyProtectDNS.servers, this.templateSettings.dns.servers);
|
||||
},
|
||||
set: function (newValue) {
|
||||
newTemplateSettings = this.templateSettings;
|
||||
if (newValue) {
|
||||
newTemplateSettings.dns = this.settingsData.familyProtectDNS;
|
||||
} else {
|
||||
delete newTemplateSettings.dns;
|
||||
newTemplateSettings.dns.servers = newTemplateSettings.dns?.servers?.filter(data => !this.settingsData.familyProtectDNS.servers.includes(data))
|
||||
}
|
||||
this.templateSettings = newTemplateSettings;
|
||||
},
|
||||
@@ -1456,6 +1957,42 @@
|
||||
}
|
||||
},
|
||||
},
|
||||
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: {
|
||||
get: function () {
|
||||
return doAllItemsExist(this.settingsData.domains.spotify, this.warpDomains);
|
||||
@@ -1468,6 +2005,42 @@
|
||||
}
|
||||
},
|
||||
},
|
||||
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>
|
||||
|
||||
113
web/html/xui/xray_balancer_modal.html
Normal file
113
web/html/xui/xray_balancer_modal.html
Normal file
@@ -0,0 +1,113 @@
|
||||
{{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:8} }" :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}}
|
||||
@@ -42,7 +42,7 @@
|
||||
outModal.visible = false;
|
||||
outModal.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
loading(loading=true) {
|
||||
outModal.confirmLoading = loading;
|
||||
},
|
||||
check(){
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a-modal id="reverse-modal" v-model="reverseModal.visible" :title="reverseModal.title" @ok="reverseModal.ok"
|
||||
:confirm-loading="reverseModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:ok-text="reverseModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
<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 label='{{ i18n "pages.xray.outbound.type" }}'>
|
||||
<a-select v-model="reverseModal.reverse.type" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="x,y in reverseTypes" :value="y">[[ x ]]</a-select-option>
|
||||
@@ -120,7 +120,7 @@
|
||||
reverseModal.visible = false;
|
||||
reverseModal.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
loading(loading=true) {
|
||||
reverseModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
@@ -132,8 +132,6 @@
|
||||
reverseModal: reverseModal,
|
||||
reverseTypes: { bridge: '{{ i18n "pages.xray.outbound.bridge" }}', portal:'{{ i18n "pages.xray.outbound.portal" }}'},
|
||||
},
|
||||
methods: {
|
||||
}
|
||||
});
|
||||
|
||||
</script>
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok"
|
||||
:confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
<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 label='Domain Matcher'>
|
||||
<a-select v-model="ruleModal.rule.domainMatcher" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="dm in ['','hybrid','linear']" :value="dm">[[ dm ]]</a-select-option>
|
||||
@@ -20,7 +20,7 @@
|
||||
<a-input v-model.trim="ruleModal.rule.source"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">Source Port
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
@@ -107,6 +107,19 @@
|
||||
<a-select-option v-for="tag in ruleModal.outboundTags" :value="tag">[[ tag ]]</a-select-option>
|
||||
</a-select>
|
||||
</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>
|
||||
</a-form>
|
||||
</a-modal>
|
||||
@@ -133,11 +146,12 @@
|
||||
protocol: [],
|
||||
attrs: [],
|
||||
outboundTag: "",
|
||||
balancerTag: "",
|
||||
},
|
||||
inboundTags: [],
|
||||
outboundTags: [],
|
||||
users: [],
|
||||
balancerTag: [],
|
||||
balancerTags: [],
|
||||
ok() {
|
||||
newRule = ruleModal.getResult();
|
||||
ObjectUtil.execute(ruleModal.confirm, newRule);
|
||||
@@ -160,6 +174,7 @@
|
||||
this.rule.protocol = rule.protocol;
|
||||
this.rule.attrs = rule.attrs ? Object.entries(rule.attrs) : [];
|
||||
this.rule.outboundTag = rule.outboundTag;
|
||||
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
|
||||
} else {
|
||||
this.rule = {
|
||||
domainMatcher: "",
|
||||
@@ -174,24 +189,29 @@
|
||||
protocol: [],
|
||||
attrs: [],
|
||||
outboundTag: "",
|
||||
balancerTag: "",
|
||||
}
|
||||
}
|
||||
this.isEdit = isEdit;
|
||||
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
|
||||
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.bridges) {
|
||||
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.routing && app.templateSettings.routing.balancers) {
|
||||
this.balancerTags = [ "", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
|
||||
}
|
||||
},
|
||||
close() {
|
||||
ruleModal.visible = false;
|
||||
ruleModal.loading(false);
|
||||
},
|
||||
loading(loading) {
|
||||
loading(loading=true) {
|
||||
ruleModal.confirmLoading = loading;
|
||||
},
|
||||
getResult() {
|
||||
@@ -210,7 +230,8 @@
|
||||
rule.inboundTag = value.inboundTag;
|
||||
rule.protocol = value.protocol;
|
||||
rule.attrs = Object.fromEntries(value.attrs);
|
||||
rule.outboundTag = value.outboundTag;
|
||||
rule.outboundTag = value.outboundTag == "" ? undefined : value.outboundTag;
|
||||
rule.balancerTag = value.balancerTag == "" ? undefined : value.balancerTag;
|
||||
|
||||
for (const [key, value] of Object.entries(rule)) {
|
||||
if (
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
package job
|
||||
|
||||
import (
|
||||
"bufio"
|
||||
"encoding/json"
|
||||
"io"
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
@@ -12,6 +14,7 @@ import (
|
||||
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/xray"
|
||||
)
|
||||
@@ -23,7 +26,6 @@ type CheckClientIpJob struct {
|
||||
var job *CheckClientIpJob
|
||||
var ipFiles = []string{
|
||||
xray.GetIPLimitLogPath(),
|
||||
xray.GetIPLimitPrevLogPath(),
|
||||
xray.GetIPLimitBannedLogPath(),
|
||||
xray.GetIPLimitBannedPrevLogPath(),
|
||||
xray.GetAccessPersistentLogPath(),
|
||||
@@ -37,8 +39,10 @@ func NewCheckClientIpJob() *CheckClientIpJob {
|
||||
|
||||
func (j *CheckClientIpJob) Run() {
|
||||
|
||||
// create files required for iplimit if not exists
|
||||
// create files and dirs required for iplimit if not exists
|
||||
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)
|
||||
j.checkError(err)
|
||||
defer file.Close()
|
||||
@@ -49,6 +53,37 @@ func (j *CheckClientIpJob) Run() {
|
||||
j.checkFail2BanInstalled()
|
||||
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 {
|
||||
@@ -92,24 +127,34 @@ func (j *CheckClientIpJob) checkFail2BanInstalled() {
|
||||
|
||||
func (j *CheckClientIpJob) processLogFile() {
|
||||
accessLogPath := xray.GetAccessLogPath()
|
||||
if accessLogPath == "" {
|
||||
logger.Warning("access.log doesn't exist in your config.json")
|
||||
|
||||
if accessLogPath == "none" {
|
||||
logger.Warning("Access log is set to 'none' check your Xray Configs")
|
||||
return
|
||||
}
|
||||
|
||||
data, err := os.ReadFile(accessLogPath)
|
||||
InboundClientIps := make(map[string][]string)
|
||||
j.checkError(err)
|
||||
if accessLogPath == "" {
|
||||
logger.Warning("Access log doesn't exist in your Xray Configs")
|
||||
return
|
||||
}
|
||||
|
||||
lines := strings.Split(string(data), "\n")
|
||||
for _, line := range lines {
|
||||
ipRegx, _ := regexp.Compile(`[0-9]+\.[0-9]+\.[0-9]+\.[0-9]+`)
|
||||
file, err := os.Open(accessLogPath)
|
||||
j.checkError(err)
|
||||
defer file.Close()
|
||||
|
||||
InboundClientIps := make(map[string][]string)
|
||||
|
||||
scanner := bufio.NewScanner(file)
|
||||
for scanner.Scan() {
|
||||
line := scanner.Text()
|
||||
|
||||
ipRegx, _ := regexp.Compile(`(\d+\.\d+\.\d+\.\d+).* accepted`)
|
||||
emailRegx, _ := regexp.Compile(`email:.+`)
|
||||
|
||||
matchesIp := ipRegx.FindString(line)
|
||||
if len(matchesIp) > 0 {
|
||||
ip := string(matchesIp)
|
||||
if ip == "127.0.0.1" || ip == "1.1.1.1" {
|
||||
matches := ipRegx.FindStringSubmatch(line)
|
||||
if len(matches) > 1 {
|
||||
ip := matches[1]
|
||||
if ip == "127.0.0.1" {
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -124,13 +169,14 @@ func (j *CheckClientIpJob) processLogFile() {
|
||||
continue
|
||||
}
|
||||
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
|
||||
|
||||
} else {
|
||||
InboundClientIps[matchesEmail] = append(InboundClientIps[matchesEmail], ip)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
j.checkError(scanner.Err())
|
||||
|
||||
shouldCleanLog := false
|
||||
|
||||
for clientEmail, ips := range InboundClientIps {
|
||||
@@ -141,27 +187,13 @@ func (j *CheckClientIpJob) processLogFile() {
|
||||
} else {
|
||||
shouldCleanLog = j.updateInboundClientIps(inboundClientIps, clientEmail, ips)
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
// added delay before cleaning logs to reduce chance of logging IP that already has been banned
|
||||
time.Sleep(time.Second * 2)
|
||||
|
||||
if shouldCleanLog {
|
||||
// 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)
|
||||
}
|
||||
j.clearAccessLog()
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -15,8 +15,8 @@ func NewClearLogsJob() *ClearLogsJob {
|
||||
// Here Run is an interface method of the Job interface
|
||||
func (j *ClearLogsJob) Run() {
|
||||
logFiles := []string{xray.GetIPLimitLogPath(), xray.GetIPLimitBannedLogPath(), xray.GetAccessPersistentLogPath()}
|
||||
logFilesPrev := []string{xray.GetIPLimitPrevLogPath(), xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()}
|
||||
|
||||
logFilesPrev := []string{xray.GetIPLimitBannedPrevLogPath(), xray.GetAccessPersistentPrevLogPath()}
|
||||
|
||||
// clear old previous logs
|
||||
for i := 0; i < len(logFilesPrev); i++ {
|
||||
if err := os.Truncate(logFilesPrev[i], 0); err != nil {
|
||||
@@ -26,25 +26,26 @@ func (j *ClearLogsJob) Run() {
|
||||
|
||||
// clear log files and copy to previous logs
|
||||
for i := 0; i < len(logFiles); i++ {
|
||||
|
||||
// copy to previous logs
|
||||
logFilePrev, err := os.OpenFile(logFilesPrev[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
|
||||
if err != nil {
|
||||
logger.Warning("clear logs job err:", err)
|
||||
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])
|
||||
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()
|
||||
}
|
||||
|
||||
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)
|
||||
err := os.Truncate(logFiles[i], 0)
|
||||
if err != nil {
|
||||
logger.Warning("clear logs job err:", err)
|
||||
}
|
||||
|
||||
@@ -6,8 +6,9 @@ import (
|
||||
)
|
||||
|
||||
type XrayTrafficJob struct {
|
||||
xrayService service.XrayService
|
||||
inboundService service.InboundService
|
||||
xrayService service.XrayService
|
||||
inboundService service.InboundService
|
||||
outboundService service.OutboundService
|
||||
}
|
||||
|
||||
func NewXrayTrafficJob() *XrayTrafficJob {
|
||||
@@ -24,11 +25,15 @@ func (j *XrayTrafficJob) Run() {
|
||||
logger.Warning("get xray traffic failed:", err)
|
||||
return
|
||||
}
|
||||
err, needRestart := j.inboundService.AddTraffic(traffics, clientTraffics)
|
||||
err, needRestart0 := j.inboundService.AddTraffic(traffics, clientTraffics)
|
||||
if err != nil {
|
||||
logger.Warning("add traffic failed:", err)
|
||||
logger.Warning("add inbound traffic failed:", err)
|
||||
}
|
||||
if needRestart {
|
||||
err, needRestart1 := j.outboundService.AddTraffic(traffics, clientTraffics)
|
||||
if err != nil {
|
||||
logger.Warning("add outbound traffic failed:", err)
|
||||
}
|
||||
if needRestart0 || needRestart1 {
|
||||
j.xrayService.SetToNeedRestart()
|
||||
}
|
||||
|
||||
|
||||
@@ -1,7 +1,9 @@
|
||||
{
|
||||
"log": {
|
||||
"loglevel": "warning",
|
||||
"error": "./error.log"
|
||||
"access": "none",
|
||||
"dnsLog": false,
|
||||
"error": "./error.log",
|
||||
"loglevel": "warning"
|
||||
},
|
||||
"api": {
|
||||
"tag": "api",
|
||||
@@ -43,7 +45,9 @@
|
||||
},
|
||||
"system": {
|
||||
"statsInboundDownlink": true,
|
||||
"statsInboundUplink": true
|
||||
"statsInboundUplink": true,
|
||||
"statsOutboundDownlink": true,
|
||||
"statsOutboundUplink": true
|
||||
}
|
||||
},
|
||||
"routing": {
|
||||
|
||||
@@ -312,7 +312,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
oldInbound.StreamSettings = inbound.StreamSettings
|
||||
oldInbound.Sniffing = inbound.Sniffing
|
||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
||||
oldInbound.Tag = fmt.Sprintf("inbound-0.0.0.0:%v", inbound.Port)
|
||||
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
} else {
|
||||
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
|
||||
}
|
||||
|
||||
func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
||||
func (s *InboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
|
||||
var err error
|
||||
db := database.GetDB()
|
||||
tx := db.Begin()
|
||||
@@ -694,7 +694,7 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
|
||||
tx.Commit()
|
||||
}
|
||||
}()
|
||||
err = s.addInboundTraffic(tx, inboundTraffics)
|
||||
err = s.addInboundTraffic(tx, traffics)
|
||||
if err != nil {
|
||||
return err, false
|
||||
}
|
||||
@@ -1814,7 +1814,7 @@ func (s *InboundService) MigrationRequirements() {
|
||||
json.Unmarshal([]byte(inbounds[inbound_index].Settings), &settings)
|
||||
clients, ok := settings["clients"].([]interface{})
|
||||
if ok {
|
||||
// Fix Clinet configuration problems
|
||||
// Fix Client configuration problems
|
||||
var newClients []interface{}
|
||||
for client_index := range clients {
|
||||
c := clients[client_index].(map[string]interface{})
|
||||
@@ -1900,6 +1900,13 @@ func (s *InboundService) MigrationRequirements() {
|
||||
newStream, _ := json.MarshalIndent(stream, " ", " ")
|
||||
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() {
|
||||
|
||||
101
web/service/outbound.go
Normal file
101
web/service/outbound.go
Normal file
@@ -0,0 +1,101 @@
|
||||
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
|
||||
}
|
||||
@@ -57,6 +57,9 @@ var defaultValueMap = map[string]string{
|
||||
"subEncrypt": "true",
|
||||
"subShowInfo": "true",
|
||||
"subURI": "",
|
||||
"subJsonPath": "/json/",
|
||||
"subJsonURI": "",
|
||||
"subJsonFragment": "",
|
||||
"datepicker": "gregorian",
|
||||
"warp": "",
|
||||
}
|
||||
@@ -387,17 +390,11 @@ func (s *SettingService) GetSubPort() (int, error) {
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubPath() (string, error) {
|
||||
subPath, err := s.getString("subPath")
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
if !strings.HasPrefix(subPath, "/") {
|
||||
subPath = "/" + subPath
|
||||
}
|
||||
if !strings.HasSuffix(subPath, "/") {
|
||||
subPath += "/"
|
||||
}
|
||||
return subPath, nil
|
||||
return s.getString("subPath")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubJsonPath() (string, error) {
|
||||
return s.getString("subJsonPath")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubDomain() (string, error) {
|
||||
@@ -412,8 +409,8 @@ func (s *SettingService) GetSubKeyFile() (string, error) {
|
||||
return s.getString("subKeyFile")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubUpdates() (int, error) {
|
||||
return s.getInt("subUpdates")
|
||||
func (s *SettingService) GetSubUpdates() (string, error) {
|
||||
return s.getString("subUpdates")
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubEncrypt() (bool, error) {
|
||||
@@ -432,6 +429,14 @@ func (s *SettingService) GetSubURI() (string, error) {
|
||||
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) {
|
||||
return s.getString("datepicker")
|
||||
}
|
||||
@@ -484,6 +489,7 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
|
||||
"tgBotEnable": func() (interface{}, error) { return s.GetTgbotenabled() },
|
||||
"subEnable": func() (interface{}, error) { return s.GetSubEnable() },
|
||||
"subURI": func() (interface{}, error) { return s.GetSubURI() },
|
||||
"subJsonURI": func() (interface{}, error) { return s.GetSubJsonURI() },
|
||||
"remarkModel": func() (interface{}, error) { return s.GetRemarkModel() },
|
||||
"datepicker": func() (interface{}, error) { return s.GetDatepicker() },
|
||||
}
|
||||
@@ -498,10 +504,11 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
|
||||
result[key] = value
|
||||
}
|
||||
|
||||
if result["subEnable"].(bool) && result["subURI"].(string) == "" {
|
||||
if result["subEnable"].(bool) && (result["subURI"].(string) == "" || result["subJsonURI"].(string) == "") {
|
||||
subURI := ""
|
||||
subPort, _ := s.GetSubPort()
|
||||
subPath, _ := s.GetSubPath()
|
||||
subJsonPath, _ := s.GetSubJsonPath()
|
||||
subDomain, _ := s.GetSubDomain()
|
||||
subKeyFile, _ := s.GetSubKeyFile()
|
||||
subCertFile, _ := s.GetSubCertFile()
|
||||
@@ -522,12 +529,12 @@ func (s *SettingService) GetDefaultSettings(host string) (interface{}, error) {
|
||||
} else {
|
||||
subURI += fmt.Sprintf("%s:%d", subDomain, subPort)
|
||||
}
|
||||
if subPath[0] == byte('/') {
|
||||
subURI += subPath
|
||||
} else {
|
||||
subURI += "/" + subPath
|
||||
if result["subURI"].(string) == "" {
|
||||
result["subURI"] = subURI + subPath
|
||||
}
|
||||
if result["subJsonURI"].(string) == "" {
|
||||
result["subJsonURI"] = subURI + subJsonPath
|
||||
}
|
||||
result["subURI"] = subURI
|
||||
}
|
||||
|
||||
return result, nil
|
||||
|
||||
@@ -6,9 +6,9 @@ import (
|
||||
"net"
|
||||
"net/url"
|
||||
"os"
|
||||
"slices"
|
||||
"strconv"
|
||||
"strings"
|
||||
"slices"
|
||||
"time"
|
||||
"x-ui/config"
|
||||
"x-ui/database"
|
||||
@@ -115,14 +115,19 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
||||
}
|
||||
|
||||
func (t *Tgbot) NewBot(token string, proxyUrl string) (*telego.Bot, error) {
|
||||
if proxyUrl == "" || !strings.HasPrefix(proxyUrl, "socks5://") {
|
||||
logger.Warning("invalid socks5 url, start with default")
|
||||
if proxyUrl == "" {
|
||||
// No proxy URL provided, use default instance
|
||||
return telego.NewBot(token)
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(proxyUrl, "socks5://") {
|
||||
logger.Warning("Invalid socks5 URL, starting with default")
|
||||
return telego.NewBot(token)
|
||||
}
|
||||
|
||||
_, err := url.Parse(proxyUrl)
|
||||
if err != nil {
|
||||
logger.Warning("cant parse proxy url, use default instance for tgbot:", err)
|
||||
logger.Warning("Can't parse proxy URL, using default instance for tgbot:", err)
|
||||
return telego.NewBot(token)
|
||||
}
|
||||
|
||||
@@ -197,9 +202,13 @@ func (t *Tgbot) OnReceive() {
|
||||
}, th.AnyCallbackQueryWithMessage())
|
||||
|
||||
botHandler.HandleMessage(func(_ *telego.Bot, message telego.Message) {
|
||||
if message.UserShared != nil {
|
||||
if message.UsersShared != nil {
|
||||
if checkAdmin(message.From.ID) {
|
||||
err := t.inboundService.SetClientTelegramUserID(message.UserShared.RequestID, strconv.FormatInt(message.UserShared.UserID, 10))
|
||||
userIDsStr := ""
|
||||
for _, userID := range message.UsersShared.UserIDs {
|
||||
userIDsStr += strconv.FormatInt(userID, 10) + " "
|
||||
}
|
||||
err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userIDsStr)
|
||||
output := ""
|
||||
if err != nil {
|
||||
output += t.I18nBot("tgbot.messages.selectUserFailed")
|
||||
@@ -260,7 +269,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||
msg += t.I18nBot("tgbot.commands.unknown")
|
||||
}
|
||||
|
||||
if msg != ""{
|
||||
if msg != "" {
|
||||
if onlyMessage {
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
return
|
||||
@@ -272,7 +281,7 @@ func (t *Tgbot) answerCommand(message *telego.Message, chatId int64, isAdmin boo
|
||||
|
||||
func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool) {
|
||||
|
||||
chatId := callbackQuery.Message.Chat.ID
|
||||
chatId := callbackQuery.Message.GetChat().ID
|
||||
|
||||
if isAdmin {
|
||||
// get query from hash storage
|
||||
@@ -291,22 +300,22 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
t.searchClient(chatId, email)
|
||||
case "client_refresh":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clientRefreshSuccess", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "client_cancel":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "ips_refresh":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.IpRefreshSuccess", "Email=="+email))
|
||||
t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "ips_cancel":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
|
||||
t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "tgid_refresh":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.TGIdRefreshSuccess", "Email=="+email))
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "tgid_cancel":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+email))
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "reset_traffic":
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
@@ -316,13 +325,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmResetTraffic")).WithCallbackData(t.encodeQuery("reset_traffic_c "+email)),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
case "reset_traffic_c":
|
||||
err := t.inboundService.ResetClientTrafficByEmail(email)
|
||||
if err == nil {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetTrafficSuccess", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
} else {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
}
|
||||
@@ -346,7 +355,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton("40 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 40")),
|
||||
),
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")),
|
||||
tu.InlineKeyboardButton("50 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 50")),
|
||||
tu.InlineKeyboardButton("60 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 60")),
|
||||
tu.InlineKeyboardButton("80 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 80")),
|
||||
),
|
||||
@@ -356,7 +365,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton("200 GB").WithCallbackData(t.encodeQuery("limit_traffic_c "+email+" 200")),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
case "limit_traffic_c":
|
||||
if len(dataArray) == 3 {
|
||||
limitTraffic, err := strconv.Atoi(dataArray[2])
|
||||
@@ -365,13 +374,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if err == nil {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.setTrafficLimitSuccess", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "limit_traffic_in":
|
||||
if len(dataArray) >= 3 {
|
||||
oldInputNumber, err := strconv.Atoi(dataArray[2])
|
||||
@@ -427,12 +436,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("limit_traffic_in "+email+" "+strconv.Itoa(inputNumber)+" -1")),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "reset_exp":
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
@@ -459,7 +468,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")),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
case "reset_exp_c":
|
||||
if len(dataArray) == 3 {
|
||||
days, err := strconv.Atoi(dataArray[2])
|
||||
@@ -494,13 +503,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if err == nil {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.expireResetSuccess", "Email=="+email))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "reset_exp_in":
|
||||
if len(dataArray) >= 3 {
|
||||
oldInputNumber, err := strconv.Atoi(dataArray[2])
|
||||
@@ -556,12 +565,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("reset_exp_in "+email+" "+strconv.Itoa(inputNumber)+" -1")),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "ip_limit":
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
@@ -590,7 +599,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton("10").WithCallbackData(t.encodeQuery("ip_limit_c "+email+" 10")),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
case "ip_limit_c":
|
||||
if len(dataArray) == 3 {
|
||||
count, err := strconv.Atoi(dataArray[2])
|
||||
@@ -599,13 +608,13 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if err == nil {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.resetIpSuccess", "Email=="+email, "Count=="+strconv.Itoa(count)))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
return
|
||||
}
|
||||
}
|
||||
}
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "ip_limit_in":
|
||||
if len(dataArray) >= 3 {
|
||||
oldInputNumber, err := strconv.Atoi(dataArray[2])
|
||||
@@ -661,12 +670,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton("⬅️").WithCallbackData(t.encodeQuery("ip_limit_in "+email+" "+strconv.Itoa(inputNumber)+" -1")),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
return
|
||||
}
|
||||
}
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "clear_ips":
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
@@ -676,12 +685,12 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmClearIps")).WithCallbackData(t.encodeQuery("clear_ips_c "+email)),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
case "clear_ips_c":
|
||||
err := t.inboundService.ClearClientIps(email)
|
||||
if err == nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.clearIpSuccess", "Email=="+email))
|
||||
t.searchClientIps(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClientIps(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
} else {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
}
|
||||
@@ -700,7 +709,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmRemoveTGUser")).WithCallbackData(t.encodeQuery("tgid_remove_c "+email)),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
case "tgid_remove_c":
|
||||
traffic, err := t.inboundService.GetClientTrafficByEmail(email)
|
||||
if err != nil || traffic == nil {
|
||||
@@ -710,7 +719,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
err = t.inboundService.SetClientTelegramUserID(traffic.Id, "")
|
||||
if err == nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.removedTGUserSuccess", "Email=="+email))
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.clientTelegramUserInfo(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
} else {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
}
|
||||
@@ -723,7 +732,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.confirmToggle")).WithCallbackData(t.encodeQuery("toggle_enable_c "+email)),
|
||||
),
|
||||
)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.MessageID, inlineKeyboard)
|
||||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
case "toggle_enable_c":
|
||||
enabled, err := t.inboundService.ToggleClientEnableByEmail(email)
|
||||
if err == nil {
|
||||
@@ -733,7 +742,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
} else {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.disableSuccess", "Email=="+email))
|
||||
}
|
||||
t.searchClient(chatId, email, callbackQuery.Message.MessageID)
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
} else {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
}
|
||||
@@ -769,7 +778,7 @@ func (t *Tgbot) asnwerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
t.onlineClients(chatId)
|
||||
case "onlines_refresh":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
|
||||
t.onlineClients(chatId, callbackQuery.Message.MessageID)
|
||||
t.onlineClients(chatId, callbackQuery.Message.GetMessageID())
|
||||
case "commands":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands"))
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpAdminCommands"))
|
||||
@@ -1022,7 +1031,7 @@ func (t *Tgbot) getInboundUsages() string {
|
||||
}
|
||||
|
||||
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()
|
||||
expiryTime := ""
|
||||
@@ -1210,13 +1219,13 @@ func (t *Tgbot) clientTelegramUserInfo(chatId int64, email string, messageID ...
|
||||
t.editMessageTgBot(chatId, messageID[0], output, inlineKeyboard)
|
||||
} else {
|
||||
t.SendMsgToTgbot(chatId, output, inlineKeyboard)
|
||||
requestUser := telego.KeyboardButtonRequestUser{
|
||||
requestUser := telego.KeyboardButtonRequestUsers{
|
||||
RequestID: int32(traffic.Id),
|
||||
UserIsBot: new(bool),
|
||||
}
|
||||
keyboard := tu.Keyboard(
|
||||
tu.KeyboardRow(
|
||||
tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUser(&requestUser),
|
||||
tu.KeyboardButton(t.I18nBot("tgbot.buttons.selectTGUser")).WithRequestUsers(&requestUser),
|
||||
),
|
||||
tu.KeyboardRow(
|
||||
tu.KeyboardButton(t.I18nBot("tgbot.buttons.closeKeyboard")),
|
||||
@@ -1380,7 +1389,6 @@ func (t *Tgbot) getExhausted(chatId int64) {
|
||||
output += t.I18nBot("tgbot.messages.exhaustedCount", "Type=="+t.I18nBot("tgbot.clients"))
|
||||
output += t.I18nBot("tgbot.messages.disabled", "Disabled=="+strconv.Itoa(len(disabledClients)))
|
||||
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+strconv.Itoa(exhaustedCC))
|
||||
|
||||
|
||||
if exhaustedCC > 0 {
|
||||
output += t.I18nBot("tgbot.messages.depleteSoon", "Deplete=="+t.I18nBot("tgbot.clients"))
|
||||
@@ -1490,7 +1498,6 @@ func (t *Tgbot) onlineClients(chatId int64, messageID ...int) {
|
||||
output := t.I18nBot("tgbot.messages.onlinesCount", "Count=="+fmt.Sprint(onlinesCount))
|
||||
keyboard := tu.InlineKeyboard(tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.refresh")).WithCallbackData(t.encodeQuery("onlines_refresh"))))
|
||||
|
||||
|
||||
if onlinesCount > 0 {
|
||||
var buttons []telego.InlineKeyboardButton
|
||||
@@ -1565,30 +1572,44 @@ func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {
|
||||
|
||||
file, err := os.Open(xray.GetIPLimitBannedPrevLogPath())
|
||||
if err == nil {
|
||||
document := tu.Document(
|
||||
tu.ID(chatId),
|
||||
tu.File(file),
|
||||
)
|
||||
_, err = bot.SendDocument(document)
|
||||
if err != nil {
|
||||
logger.Error("Error in uploading backup: ", err)
|
||||
// Check if the file is non-empty before attempting to upload
|
||||
fileInfo, _ := file.Stat()
|
||||
if fileInfo.Size() > 0 {
|
||||
document := tu.Document(
|
||||
tu.ID(chatId),
|
||||
tu.File(file),
|
||||
)
|
||||
_, 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 {
|
||||
logger.Error("Error in opening db file for backup: ", err)
|
||||
logger.Error("Error in opening IPLimitBannedPrevLog file for backup: ", err)
|
||||
}
|
||||
|
||||
file, err = os.Open(xray.GetIPLimitBannedLogPath())
|
||||
if err == nil {
|
||||
document := tu.Document(
|
||||
tu.ID(chatId),
|
||||
tu.File(file),
|
||||
)
|
||||
_, err = bot.SendDocument(document)
|
||||
if err != nil {
|
||||
logger.Error("Error in uploading config.json: ", err)
|
||||
// Check if the file is non-empty before attempting to upload
|
||||
fileInfo, _ := file.Stat()
|
||||
if fileInfo.Size() > 0 {
|
||||
document := tu.Document(
|
||||
tu.ID(chatId),
|
||||
tu.File(file),
|
||||
)
|
||||
_, 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 {
|
||||
logger.Error("Error in opening config.json file for backup: ", err)
|
||||
logger.Error("Error in opening IPLimitBannedLog file for backup: ", err)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -95,7 +95,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
|
||||
if !clientTraffic.Enable {
|
||||
clients = RemoveIndex(clients, index-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")
|
||||
|
||||
}
|
||||
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"online" = "Online"
|
||||
"domainName" = "Domain Name"
|
||||
"monitor" = "Listen IP"
|
||||
"certificate" = "Certificate"
|
||||
"certificate" = "Digital Certificate"
|
||||
"fail" = " Failed"
|
||||
"success" = " Successful"
|
||||
"getVersion" = "Get Version"
|
||||
@@ -52,6 +52,14 @@
|
||||
"secretToken" = "Secret Token"
|
||||
"remained" = "Remained"
|
||||
"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 settings are vulnerable to attacks. It is recommended to reinforce security protocols to prevent potential breaches."
|
||||
"secAlertSSL" = "Panel lacks secure connection. Please install TLS certificate for data protection."
|
||||
"secAlertPanelPort" = "Panel default port is vulnerable. Please configure a random or specific port."
|
||||
"secAlertPanelURI" = "Panel default URI path is insecure. Please configure a complex URI path."
|
||||
"secAlertSubURI" = "Subscription default URI path is insecure. Please configure a complex URI path."
|
||||
"secAlertSubJsonURI" = "Subscription JSON default URI path is insecure. Please configure a complex URI path."
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Overview"
|
||||
@@ -62,6 +70,7 @@
|
||||
"link" = "Manage"
|
||||
|
||||
[pages.login]
|
||||
"hello" = "Hello"
|
||||
"title" = "Welcome"
|
||||
"loginAgain" = "Your session has expired, please log in again"
|
||||
|
||||
@@ -76,7 +85,7 @@
|
||||
"title" = "Overview"
|
||||
"memory" = "RAM"
|
||||
"hard" = "Disk"
|
||||
"xrayStatus" = "Status"
|
||||
"xrayStatus" = "Xray"
|
||||
"stopXray" = "Stop"
|
||||
"restartXray" = "Restart"
|
||||
"xraySwitch" = "Version"
|
||||
@@ -140,10 +149,8 @@
|
||||
"noRecommendKeepDefault" = "It is recommended to keep the default"
|
||||
"certificatePath" = "File Path"
|
||||
"certificateContent" = "File Content"
|
||||
"publicKeyPath" = "Public Key Path"
|
||||
"publicKeyContent" = "Public Key Content"
|
||||
"keyPath" = "Private Key Path"
|
||||
"keyContent" = "Private Key Content"
|
||||
"publicKey" = "Public Key"
|
||||
"privatekey" = "Private Key"
|
||||
"clickOnQRcode" = "Click on QR Code to Copy"
|
||||
"client" = "Client"
|
||||
"export" = "Export All URLs"
|
||||
@@ -302,6 +309,8 @@
|
||||
"subShowInfoDesc" = "The remaining traffic and date will be displayed in the client apps."
|
||||
"subURI" = "Reverse Proxy URI"
|
||||
"subURIDesc" = "The URI path of the subscription URL for use behind proxies."
|
||||
"fragment" = "Fragmentation"
|
||||
"fragmentDesc" = "Enable fragmentation for TLS hello packet"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Xray Configs"
|
||||
@@ -309,8 +318,10 @@
|
||||
"restart" = "Restart Xray"
|
||||
"basicTemplate" = "Basics"
|
||||
"advancedTemplate" = "Advanced"
|
||||
"generalConfigs" = "General Strategy"
|
||||
"generalConfigsDesc" = "These options will determine general strategy adjustments."
|
||||
"generalConfigs" = "General"
|
||||
"generalConfigsDesc" = "These options will determine general 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"
|
||||
"blockConfigsDesc" = "These options will block traffic based on specific requested protocols and websites."
|
||||
"blockCountryConfigs" = "Block Country"
|
||||
@@ -381,6 +392,12 @@
|
||||
"OpenAIWARPDesc" = "Routes traffic to ChatGPT via WARP."
|
||||
"NetflixWARP" = "Netflix"
|
||||
"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"
|
||||
"SpotifyWARPDesc" = "Routes traffic to Spotify via WARP."
|
||||
"IRWARP" = "Iran domains"
|
||||
@@ -388,10 +405,17 @@
|
||||
"Inbounds" = "Inbounds"
|
||||
"InboundsDesc" = "Accepting the specific clients."
|
||||
"Outbounds" = "Outbounds"
|
||||
"Balancers" = "Balancers"
|
||||
"OutboundsDesc" = "Set the outgoing traffic pathway."
|
||||
"Routings" = "Routing Rules"
|
||||
"RoutingsDesc" = "The priority of each rule is important!"
|
||||
"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]
|
||||
"first" = "First"
|
||||
@@ -402,6 +426,7 @@
|
||||
"dest" = "Destination"
|
||||
"inbound" = "Inbound"
|
||||
"outbound" = "Outbound"
|
||||
"balancer" = "Balancer"
|
||||
"info" = "Info"
|
||||
"add" = "Add Rule"
|
||||
"edit" = "Edit Rule"
|
||||
@@ -422,6 +447,15 @@
|
||||
"portal" = "Portal"
|
||||
"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]
|
||||
"secretKey" = "Secret Key"
|
||||
"publicKey" = "Public Key"
|
||||
@@ -430,6 +464,21 @@
|
||||
"psk" = "PreShared Key"
|
||||
"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]
|
||||
"admin" = "Admin"
|
||||
"secret" = "Secret Token"
|
||||
@@ -452,7 +501,7 @@
|
||||
"wentWrong" = "❌ Something went wrong!"
|
||||
"noIpRecord" = "❗ No IP Record!"
|
||||
"noInbounds" = "❗ No inbound found!"
|
||||
"unlimited" = "♾ Unlimited"
|
||||
"unlimited" = "♾ Unlimited(Reset)"
|
||||
"add" = "Add"
|
||||
"month" = "Month"
|
||||
"months" = "Months"
|
||||
|
||||
@@ -20,7 +20,7 @@
|
||||
"check" = "Verificar"
|
||||
"indefinite" = "Indefinido"
|
||||
"unlimited" = "Ilimitado"
|
||||
"none" = "Ninguno"
|
||||
"none" = "None"
|
||||
"qrCode" = "Código QR"
|
||||
"info" = "Más Información"
|
||||
"edit" = "Editar"
|
||||
@@ -42,7 +42,7 @@
|
||||
"online" = "en línea"
|
||||
"domainName" = "Nombre de dominio"
|
||||
"monitor" = "Listening IP"
|
||||
"certificate" = "Certificado"
|
||||
"certificate" = "Certificado Digital"
|
||||
"fail" = "Falló"
|
||||
"success" = "Éxito"
|
||||
"getVersion" = "Obtener versión"
|
||||
@@ -52,16 +52,25 @@
|
||||
"secretToken" = "Token Secreto"
|
||||
"remained" = "Restante"
|
||||
"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" = "Certae occasus vulnerabiles sunt impetus. Commendatur ad securitatem protocolla roboranda ne interrupta potentiale."
|
||||
"secAlertSSL" = "La panel carece de conexión segura. Por favor, instale un certificado TLS para la protección de datos."
|
||||
"secAlertPanelPort" = "La puerto predeterminado del panel es vulnerable. Por favor, configure un puerto aleatorio o específico."
|
||||
"secAlertPanelURI" = "La ruta URI predeterminada del panel no es segura. Por favor, configure una ruta URI compleja."
|
||||
"secAlertSubURI" = "La ruta URI predeterminada de la suscripción no es segura. Por favor, configure una ruta URI compleja."
|
||||
"secAlertSubJsonURI" = "La ruta URI predeterminada de la suscripción JSON no es segura. Por favor, configure una ruta URI compleja."
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Estado del Sistema"
|
||||
"inbounds" = "Entradas"
|
||||
"settings" = "Configuraciones"
|
||||
"xray" = "Configuración Xray"
|
||||
"xray" = "Ajustes Xray"
|
||||
"logout" = "Cerrar Sesión"
|
||||
"link" = "Gestionar"
|
||||
|
||||
[pages.login]
|
||||
"hello" = "Hola"
|
||||
"title" = "Bienvenido"
|
||||
"loginAgain" = "El límite de tiempo de inicio de sesión ha expirado. Por favor, inicia sesión nuevamente."
|
||||
|
||||
@@ -76,7 +85,7 @@
|
||||
"title" = "Estado del Sistema"
|
||||
"memory" = "Memoria"
|
||||
"hard" = "Disco Duro"
|
||||
"xrayStatus" = "Estado de"
|
||||
"xrayStatus" = "Xray"
|
||||
"stopXray" = "Detener"
|
||||
"restartXray" = "Reiniciar"
|
||||
"xraySwitch" = "Versión"
|
||||
@@ -140,10 +149,8 @@
|
||||
"noRecommendKeepDefault" = "No hay requisitos especiales para mantener la configuración predeterminada"
|
||||
"certificatePath" = "Ruta del Archivo"
|
||||
"certificateContent" = "Contenido del Archivo"
|
||||
"publicKeyPath" = "Ruta de la Clave Pública"
|
||||
"publicKeyContent" = "Contenido de la Clave Pública"
|
||||
"keyPath" = "Ruta de la Clave Privada"
|
||||
"keyContent" = "Contenido de la Clave Privada"
|
||||
"publicKey" = "llave Pública"
|
||||
"privatekey" = "llave Privada"
|
||||
"clickOnQRcode" = "Haz clic en el Código QR para Copiar"
|
||||
"client" = "Cliente"
|
||||
"export" = "Exportar Enlaces"
|
||||
@@ -194,7 +201,7 @@
|
||||
"last" = "Último"
|
||||
"prefix" = "Prefijo"
|
||||
"postfix" = "Sufijo"
|
||||
"delayedStart" = "Iniciar después del primer uso"
|
||||
"delayedStart" = "Inicio Inicial"
|
||||
"expireDays" = "Duratio"
|
||||
"days" = "día(s)"
|
||||
"renew" = "Renovación automática"
|
||||
@@ -302,6 +309,8 @@
|
||||
"subShowInfoDesc" = "Mostrar tráfico restante y fecha después del nombre de configuración."
|
||||
"subURI" = "URI de proxy inverso"
|
||||
"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]
|
||||
"title" = "Xray Configuración"
|
||||
@@ -311,6 +320,8 @@
|
||||
"advancedTemplate" = "Plantilla Avanzada"
|
||||
"generalConfigs" = "Configuraciones 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"
|
||||
"blockConfigsDesc" = "Estas opciones evitarán que los usuarios se conecten a protocolos y sitios web específicos."
|
||||
"blockCountryConfigs" = "Configuraciones de Bloqueo por País"
|
||||
@@ -375,23 +386,36 @@
|
||||
"GoogleIPv4Desc" = "Agregar enrutamiento para que Google se conecte con IPv4."
|
||||
"NetflixIPv4" = "Usar IPv4 para Netflix"
|
||||
"NetflixIPv4Desc" = "Agregar enrutamiento para que Netflix se conecte con IPv4."
|
||||
"GoogleWARP" = "Rutear Google a través de WARP."
|
||||
"GoogleWARPDesc" = "Agregar enrutamiento para Google a través de WARP."
|
||||
"OpenAIWARP" = "Rutear OpenAI (ChatGPT) a través de WARP."
|
||||
"OpenAIWARPDesc" = "Agregar enrutamiento para OpenAI (ChatGPT) a través de WARP."
|
||||
"NetflixWARP" = "Rutear Netflix a través de WARP."
|
||||
"NetflixWARPDesc" = "Agregar enrutamiento para Netflix a través de WARP."
|
||||
"SpotifyWARP" = "Rutear Spotify a través de WARP."
|
||||
"SpotifyWARPDesc" = "Agregar enrutamiento para Spotify a través de WARP."
|
||||
"GoogleWARP" = "Google"
|
||||
"GoogleWARPDesc" = "Enruta el tráfico a Apple a través de WARP."
|
||||
"OpenAIWARP" = "OpenAI (ChatGPT)"
|
||||
"OpenAIWARPDesc" = "Enruta el tráfico a OpenAI (ChatGPT) a través de WARP."
|
||||
"NetflixWARP" = "Netflix"
|
||||
"NetflixWARPDesc" = "Enruta el tráfico a Netflix a través de WARP."
|
||||
"MetaWARP" = "Meta"
|
||||
"MetaWARPDesc" = "Enruta el tráfico a Meta (Instagram, Facebook, WhatsApp, Threads,...) 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."
|
||||
"IRWARPDesc" = "Agregar enrutamiento para dominios de Irán a través de WARP."
|
||||
"Inbounds" = "Entrante"
|
||||
"InboundsDesc" = "Cambia la plantilla de configuración para aceptar clientes específicos."
|
||||
"Outbounds" = "Salidas"
|
||||
"Balancers" = "Equilibradores"
|
||||
"OutboundsDesc" = "Cambia la plantilla de configuración para definir formas de salida para este servidor."
|
||||
"Routings" = "Reglas de enrutamiento"
|
||||
"RoutingsDesc" = "¡La prioridad de cada regla es importante!"
|
||||
"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]
|
||||
"first" = "Primero"
|
||||
@@ -402,6 +426,7 @@
|
||||
"dest" = "Destino"
|
||||
"inbound" = "Entrante"
|
||||
"outbound" = "saliente"
|
||||
"balancer" = "Balancín"
|
||||
"info" = "Información"
|
||||
"add" = "Agregar regla"
|
||||
"edit" = "Editar regla"
|
||||
@@ -422,6 +447,15 @@
|
||||
"portal" = "portal"
|
||||
"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]
|
||||
"secretKey" = "Llave secreta"
|
||||
"publicKey" = "Llave pública"
|
||||
@@ -430,6 +464,21 @@
|
||||
"psk" = "Clave precompartida"
|
||||
"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]
|
||||
"admin" = "Administrador"
|
||||
"secret" = "Token Secreto"
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"online" = "آنلاین"
|
||||
"domainName" = "آدرس دامنه"
|
||||
"monitor" = "آیپی اتصال"
|
||||
"certificate" = "گواهی"
|
||||
"certificate" = "گواهی دیجیتال"
|
||||
"fail" = "ناموفق"
|
||||
"success" = " موفق"
|
||||
"getVersion" = "دریافت نسخه"
|
||||
@@ -52,6 +52,14 @@
|
||||
"secretToken" = "توکن امنیتی"
|
||||
"remained" = "باقیمانده"
|
||||
"security" = "امنیت"
|
||||
"secAlertTitle" = "هشدارامنیتی"
|
||||
"secAlertSsl" = "ایناتصالامن نیست. لطفا تازمانیکه تیالاس برای محافظت از دادهها فعال نشدهاست، از وارد کردن اطلاعات حساس خودداری کنید"
|
||||
"secAlertConf" = "تنظیمات خاصی در برابر حملات آسیب پذیر هستند. توصیه میشود پروتکلهای امنیتی را برای جلوگیری از نفوذ احتمالی تقویت کنید"
|
||||
"secAlertSSL" = "پنل فاقد ارتباط امن است. لطفاً یک گواهینامه تیالاس برای محافظت از دادهها نصب کنید"
|
||||
"secAlertPanelPort" = "استفاده از پورت پیشفرض پنل ناامن است. لطفاً یک پورت تصادفی یا خاص تنظیم کنید"
|
||||
"secAlertPanelURI" = "مسیر پیشفرض لینک پنل ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید"
|
||||
"secAlertSubURI" = "مسیر پیشفرض لینک سابسکریپشن ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید"
|
||||
"secAlertSubJsonURI" = "مسیر پیشفرض لینک سابسکریپشن جیسون ناامن است. لطفاً یک مسیر پیچیده تنظیم کنید"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "نمای کلی"
|
||||
@@ -62,6 +70,7 @@
|
||||
"link" = "مدیریت"
|
||||
|
||||
[pages.login]
|
||||
"hello" = "سلام"
|
||||
"title" = "خوشآمدید"
|
||||
"loginAgain" = "مدت زمان استفاده بهاتمامرسیده، لطفا دوباره وارد شوید"
|
||||
|
||||
@@ -76,7 +85,7 @@
|
||||
"title" = "نمای کلی"
|
||||
"memory" = "RAM"
|
||||
"hard" = "Disk"
|
||||
"xrayStatus" = "وضعیتایکسری"
|
||||
"xrayStatus" = "ایکسری"
|
||||
"stopXray" = "توقف"
|
||||
"restartXray" = "شروعمجدد"
|
||||
"xraySwitch" = "نسخه"
|
||||
@@ -134,16 +143,14 @@
|
||||
"destinationPort" = "پورت مقصد"
|
||||
"targetAddress" = "آدرس مقصد"
|
||||
"monitorDesc" = "بهطور پیشفرض خالیبگذارید"
|
||||
"meansNoLimit" = " = واحد: گیگابایت) نامحدود)"
|
||||
"meansNoLimit" = "0 = واحد: گیگابایت) نامحدود)"
|
||||
"totalFlow" = "ترافیک کل"
|
||||
"leaveBlankToNeverExpire" = "برای منقضینشدن خالیبگذارید"
|
||||
"noRecommendKeepDefault" = "توصیهمیشود بهطور پیشفرض حفظشود"
|
||||
"certificatePath" = "مسیر فایل"
|
||||
"certificateContent" = "محتوای فایل"
|
||||
"publicKeyPath" = "مسیر کلید عمومی"
|
||||
"publicKeyContent" = "محتوای کلید عمومی"
|
||||
"keyPath" = "مسیر کلید خصوصی"
|
||||
"keyContent" = "محتوای کلید خصوصی"
|
||||
"publicKey" = "کلید عمومی"
|
||||
"privatekey" = "کلید خصوصی"
|
||||
"clickOnQRcode" = "برای کپی بر روی کدتصویری کلیک کنید"
|
||||
"client" = "کاربر"
|
||||
"export" = "استخراج لینکها"
|
||||
@@ -302,6 +309,8 @@
|
||||
"subShowInfoDesc" = "ترافیک و زمان باقیمانده را در برنامههای کاربری نمایش میدهد"
|
||||
"subURI" = "پروکسی معکوس URI مسیر"
|
||||
"subURIDesc" = "سابسکریپشن را برای استفاده در پشت پراکسیها تغییر میدهد URI مسیر"
|
||||
"fragment" = "تکهتکه شدن"
|
||||
"fragmentDesc" = "فعال کردن تکه تکه شدن برای بسته نخست تیالاس"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "پیکربندی ایکسری"
|
||||
@@ -311,6 +320,8 @@
|
||||
"advancedTemplate" = "پیشرفته"
|
||||
"generalConfigs" = "استراتژی کلی"
|
||||
"generalConfigsDesc" = "این گزینهها استراتژی کلی ترافیک را تعیین میکنند"
|
||||
"logConfigs" = "گزارش"
|
||||
"logConfigsDesc" = "گزارشها ممکن است بر کارایی سرور شما تأثیر بگذارد. توصیه می شود فقط در صورت نیاز آن را عاقلانه فعال کنید"
|
||||
"blockConfigs" = "سپر محافظ"
|
||||
"blockConfigsDesc" = "این گزینهها ترافیک را بر اساس پروتکلهای درخواستی خاص، و وب سایتها مسدود میکند"
|
||||
"blockCountryConfigs" = "مسدودسازی کشور"
|
||||
@@ -381,6 +392,12 @@
|
||||
"OpenAIWARPDesc" = "ترافیک را از طریق وارپ به چت جیپیتی هدایت میکند"
|
||||
"NetflixWARP" = "نتفلیکس"
|
||||
"NetflixWARPDesc" = "ترافیک را از طریق وارپ به نتفلیکس هدایت میکند"
|
||||
"MetaWARP" = "متا"
|
||||
"MetaWARPDesc" = "ترافیک را از طریق وارپ به متا (اینستاگرام، فیس بوک، واتساپ، تردز و...) هدایت می کند."
|
||||
"AppleWARP" = "اپل"
|
||||
"AppleWARPDesc" = " ترافیک را از طریق وارپ به اپل هدایت میکند"
|
||||
"RedditWARP" = "ردیت"
|
||||
"RedditWARPDesc" = " ترافیک را از طریق وارپ به ردیت هدایت میکند"
|
||||
"SpotifyWARP" = "اسپاتیفای"
|
||||
"SpotifyWARPDesc" = " ترافیک را از طریق وارپ به اسپاتیفای هدایت میکند"
|
||||
"IRWARP" = "دامنههای ایران"
|
||||
@@ -388,10 +405,17 @@
|
||||
"Inbounds" = "ورودیها"
|
||||
"InboundsDesc" = "پذیرش کلاینت خاص"
|
||||
"Outbounds" = "خروجیها"
|
||||
"Balancers" = "بالانسرها"
|
||||
"OutboundsDesc" = "مسیر ترافیک خروجی را تنظیم کنید"
|
||||
"Routings" = "قوانین مسیریابی"
|
||||
"RoutingsDesc" = "اولویت هر قانون مهم است"
|
||||
"completeTemplate" = "کامل"
|
||||
"logLevel" = "سطح گزارش"
|
||||
"logLevelDesc" = "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند."
|
||||
"accessLog" = "مسیر گزارش"
|
||||
"accessLogDesc" = "مسیر فایل برای گزارش دسترسی. مقدار ویژه «هیچ» گزارشهای دسترسی را غیرفعال میکند."
|
||||
"errorLog" = "گزارش خطا"
|
||||
"errorLogDesc" = "مسیر فایل برای ورود به سیستم خطا. مقدار ویژه «هیچ» گزارش های خطا را غیرفعال میکند"
|
||||
|
||||
[pages.xray.rules]
|
||||
"first" = "اولین"
|
||||
@@ -402,6 +426,7 @@
|
||||
"dest" = "مقصد"
|
||||
"inbound" = "ورودی"
|
||||
"outbound" = "خروجی"
|
||||
"balancer" = "بالانسر"
|
||||
"info" = "اطلاعات"
|
||||
"add" = "افزودن قانون"
|
||||
"edit" = "ویرایش قانون"
|
||||
@@ -422,6 +447,15 @@
|
||||
"portal" = "پورتال"
|
||||
"intercon" = "اتصال میانی"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "افزودن بالانسر"
|
||||
"editBalancer" = "ویرایش بالانسر"
|
||||
"balancerStrategy" = "استراتژی"
|
||||
"balancerSelectors" = "انتخابگرها"
|
||||
"tag" = "برچسب"
|
||||
"tagDesc" = "برچسب یگانه"
|
||||
"balancerDesc" = "امکان استفاده همزمان balancerTag و outboundTag باهم وجود ندارد. درصورت استفاده همزمان فقط outboundTag عمل خواهد کرد."
|
||||
|
||||
[pages.xray.wireguard]
|
||||
"secretKey" = "کلید شخصی"
|
||||
"publicKey" = "کلید عمومی"
|
||||
@@ -430,6 +464,21 @@
|
||||
"psk" = "کلید مشترک"
|
||||
"domainStrategy" = "استراتژی حل دامنه"
|
||||
|
||||
[pages.xray.dns]
|
||||
"enable" = "فعال کردن حل دامنه"
|
||||
"enableDesc" = "سرور حل دامنه داخلی را فعال کنید"
|
||||
"strategy" = "استراتژی پرسوجو"
|
||||
"strategyDesc" = "استراتژی کلی برای حل نام دامنه"
|
||||
"add" = "افزودن سرور"
|
||||
"edit" = "ویرایش سرور"
|
||||
"domains" = "دامنهها"
|
||||
|
||||
[pages.xray.fakedns]
|
||||
"add" = "افزودن دیاناس جعلی"
|
||||
"edit" = "ویرایش دیاناس جعلی"
|
||||
"ipPool" = "زیرشبکه استخر آیپی"
|
||||
"poolSize" = "اندازه استخر"
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "مدیر"
|
||||
"secret" = "توکن مخفی"
|
||||
@@ -452,7 +501,7 @@
|
||||
"wentWrong" = "❌ مشکلی رخ داده است!"
|
||||
"noIpRecord" = "❗ رکورد IP یافت نشد!"
|
||||
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
|
||||
"unlimited" = "♾ نامحدود"
|
||||
"unlimited" = "♾ - نامحدود(ریست)"
|
||||
"add" = "اضافه کردن"
|
||||
"month" = "ماه"
|
||||
"months" = "ماهها"
|
||||
|
||||
625
web/translation/translate.id_ID.toml
Normal file
625
web/translation/translate.id_ID.toml
Normal file
@@ -0,0 +1,625 @@
|
||||
"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 Digital"
|
||||
"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" = "Beberapa pengaturan rentan terhadap serangan. Disarankan untuk memperkuat protokol keamanan guna mencegah pelanggaran potensial."
|
||||
"secAlertSSL" = "Panel kekurangan koneksi yang aman. Harap instal sertifikat TLS untuk perlindungan data."
|
||||
"secAlertPanelPort" = "Port default panel rentan. Harap konfigurasi port acak atau tertentu."
|
||||
"secAlertPanelURI" = "Jalur URI default panel tidak aman. Harap konfigurasi jalur URI kompleks."
|
||||
"secAlertSubURI" = "Jalur URI default langganan tidak aman. Harap konfigurasi jalur URI kompleks."
|
||||
"secAlertSubJsonURI" = "Jalur URI default JSON langganan tidak aman. Harap konfigurasikan jalur URI kompleks."
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Ikhtisar"
|
||||
"inbounds" = "Masuk"
|
||||
"settings" = "Pengaturan Panel"
|
||||
"xray" = "Konfigurasi Xray"
|
||||
"logout" = "Keluar"
|
||||
"link" = "Kelola"
|
||||
|
||||
[pages.login]
|
||||
"hello" = "Halo"
|
||||
"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"
|
||||
"publicKey" = "Kunci Publik"
|
||||
"privatekey" = "Kunci Pribadi"
|
||||
"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 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>"
|
||||
@@ -42,7 +42,7 @@
|
||||
"online" = "Онлайн"
|
||||
"domainName" = "Домен"
|
||||
"monitor" = "Порт IP"
|
||||
"certificate" = "Сертификат"
|
||||
"certificate" = "Цифровой сертификат"
|
||||
"fail" = "Неудачно"
|
||||
"success" = "Успешно"
|
||||
"getVersion" = "Узнать версию"
|
||||
@@ -52,6 +52,14 @@
|
||||
"secretToken" = "Секретный токен"
|
||||
"remained" = "остались"
|
||||
"security" = "Безопасность"
|
||||
"secAlertTitle" = "Предупреждение системы безопасности"
|
||||
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, воздержитесь от ввода конфиденциальной информации до тех пор, пока не будет активирован TLS для защиты данных"
|
||||
"secAlertConf" = "Некоторые настройки уязвимы для атак. Рекомендуется усилить протоколы безопасности, чтобы предотвратить потенциальные нарушения."
|
||||
"secAlertSSL" = "В панели отсутствует безопасное соединение. Пожалуйста, установите сертификат TLS для защиты данных."
|
||||
"secAlertPanelPort" = "Порт по умолчанию панели небезопасен. Пожалуйста, настройте случайный или определенный порт."
|
||||
"secAlertPanelURI" = "URI-путь по умолчанию панели небезопасен. Пожалуйста, настройте сложный URI-путь."
|
||||
"secAlertSubURI" = "URI-путь по умолчанию подписки небезопасен. Пожалуйста, настройте сложный URI-путь."
|
||||
"secAlertSubJsonURI" = "URI-путь по умолчанию для JSON подписки небезопасен. Пожалуйста, настройте сложный URI-путь."
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Статус системы"
|
||||
@@ -62,6 +70,7 @@
|
||||
"link" = "менеджмент"
|
||||
|
||||
[pages.login]
|
||||
"hello" = "Привет"
|
||||
"title" = "Добро пожаловать"
|
||||
"loginAgain" = "Время пребывания в сети вышло. Пожалуйста, войдите в систему снова"
|
||||
|
||||
@@ -76,7 +85,7 @@
|
||||
"title" = "Статус системы"
|
||||
"memory" = "Память"
|
||||
"hard" = "Жесткий диск"
|
||||
"xrayStatus" = "Статус"
|
||||
"xrayStatus" = "Xray"
|
||||
"stopXray" = "Остановить"
|
||||
"restartXray" = "Перезапустить"
|
||||
"xraySwitch" = "Версия"
|
||||
@@ -140,10 +149,8 @@
|
||||
"noRecommendKeepDefault" = "Нет требований для сохранения настроек по умолчанию"
|
||||
"certificatePath" = "Путь файла"
|
||||
"certificateContent" = "Содержимое файла"
|
||||
"publicKeyPath" = "Путь к публичному ключу"
|
||||
"publicKeyContent" = "Содержимое публичного ключа"
|
||||
"keyPath" = "Путь к приватному ключу"
|
||||
"keyContent" = "Содержимое приватного ключа"
|
||||
"publicKey" = "Публичный ключ"
|
||||
"privatekey" = "Приватный ключ"
|
||||
"clickOnQRcode" = "Нажмите на QR-код, чтобы скопировать"
|
||||
"client" = "Клиент"
|
||||
"export" = "Экспорт ключей"
|
||||
@@ -194,7 +201,7 @@
|
||||
"last" = "Последний"
|
||||
"prefix" = "Префикс"
|
||||
"postfix" = "Постфикс"
|
||||
"delayedStart" = "Начать с момента первого подключения"
|
||||
"delayedStart" = "Начало использования"
|
||||
"expireDays" = "Длительность"
|
||||
"days" = "дней"
|
||||
"renew" = "Автопродление"
|
||||
@@ -302,6 +309,8 @@
|
||||
"subShowInfoDesc" = "Показывать восстановленный трафик и дату после имени конфигурации"
|
||||
"subURI" = "URI обратного прокси"
|
||||
"subURIDesc" = "Изменить базовый URI URL-адреса подписки для использования за прокси-серверами"
|
||||
"fragment" = "Фрагментация"
|
||||
"fragmentDesc" = "Включить фрагментацию для пакета приветствия TLS"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Настройки Xray"
|
||||
@@ -311,6 +320,8 @@
|
||||
"advancedTemplate" = "Расширенный шаблон"
|
||||
"generalConfigs" = "Основные настройки"
|
||||
"generalConfigsDesc" = "Эти параметры описывают общие настройки"
|
||||
"logConfigs" = "Журнал"
|
||||
"logConfigsDesc" = "Журналы могут повлиять на эффективность вашего сервера. Рекомендуется включать их с умом только в случае ваших нужд!"
|
||||
"blockConfigs" = "Блокировка конфигураций"
|
||||
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
|
||||
"blockCountryConfigs" = "Конфигурации блокировки страны"
|
||||
@@ -375,23 +386,36 @@
|
||||
"GoogleIPv4Desc" = "Добавить маршрутизацию для Google для подключения к IPv4"
|
||||
"NetflixIPv4" = "Использовать IPv4 для Netflix"
|
||||
"NetflixIPv4Desc" = "Добавить маршрутизацию для Netflix для подключения к IPv4"
|
||||
"GoogleWARP" = "Маршрутизация Google через WARP"
|
||||
"GoogleWARPDesc" = "Добавить маршрутизацию для Google через WARP"
|
||||
"OpenAIWARP" = "Маршрутизация OpenAI (ChatGPT) через WARP"
|
||||
"OpenAIWARPDesc" = "Добавить маршрутизацию для OpenAI (ChatGPT) через WARP"
|
||||
"NetflixWARP" = "Маршрутизация Netflix через WARP"
|
||||
"NetflixWARPDesc" = "Добавить маршрутизацию для Netflix через WARP"
|
||||
"SpotifyWARP" = "Маршрутизация Spotify через WARP"
|
||||
"SpotifyWARPDesc" = "Добавить маршрутизацию для Spotify через WARP"
|
||||
"GoogleWARP" = "Google"
|
||||
"GoogleWARPDesc" = "Направляет трафик в Google через WARP."
|
||||
"OpenAIWARP" = "OpenAI (ChatGPT)"
|
||||
"OpenAIWARPDesc" = "Направляет трафик в OpenAI (ChatGPT) через WARP."
|
||||
"NetflixWARP" = "Netflix"
|
||||
"NetflixWARPDesc" = "Направляет трафик в Apple через WARP."
|
||||
"MetaWARP" = "Мета"
|
||||
"MetaWARPDesc" = "Направляет трафик в Meta (Instagram, Facebook, WhatsApp, Threads...) через WARP."
|
||||
"AppleWARP" = "Apple"
|
||||
"AppleWARPDesc" = "Направляет трафик в Apple через WARP."
|
||||
"RedditWARP" = "Reddit"
|
||||
"RedditWARPDesc" = "Направляет трафик в Reddit через WARP."
|
||||
"SpotifyWARP" = "Spotify"
|
||||
"SpotifyWARPDesc" = "Направляет трафик в Spotify через WARP."
|
||||
"IRWARP" = "Маршрутизация доменов Ирана через WARP"
|
||||
"IRWARPDesc" = "Добавить маршрутизацию для доменов Ирана через WARP"
|
||||
"Inbounds" = "Входящие"
|
||||
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных пользователей"
|
||||
"Outbounds" = "Исходящие"
|
||||
"Balancers" = "Балансиры"
|
||||
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие пути для этого сервера"
|
||||
"Routings" = "Правила маршрутизации"
|
||||
"RoutingsDesc" = "Важен приоритет каждого правила!"
|
||||
"completeTemplate" = "Все"
|
||||
"logLevel" = "Уровень журнала"
|
||||
"logLevelDesc" = "Уровень журнала для журналов ошибок, указывающий информацию, которую необходимо записать."
|
||||
"accessLog" = "Журнал доступа"
|
||||
"accessLogDesc" = "Путь к файлу журнала доступа. Специальное значение «none» отключило журналы доступа."
|
||||
"errorLog" = "Журнал ошибок"
|
||||
"errorLogDesc" = "Путь к файлу журнала ошибок. Специальное значение «none» отключает журналы ошибок."
|
||||
|
||||
[pages.xray.rules]
|
||||
"first" = "Первый"
|
||||
@@ -402,6 +426,7 @@
|
||||
"dest" = "Пункт назначения"
|
||||
"inbound" = "Входящий"
|
||||
"outbound" = "Исходящий"
|
||||
"balancer" = "балансир"
|
||||
"info" = "Информация"
|
||||
"add" = "Добавить правило"
|
||||
"edit" = "Редактировать правило"
|
||||
@@ -422,14 +447,38 @@
|
||||
"portal" = "Портал"
|
||||
"intercon" = "Соединение"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "Добавить балансир"
|
||||
"editBalancer" = "Редактировать балансир"
|
||||
"balancerStrategy" = "Стратегия"
|
||||
"balancerSelectors" = "Селекторы"
|
||||
"tag" = "Тег"
|
||||
"tagDesc" = "уникальный тег"
|
||||
"balancerDesc" = "Невозможно одновременно использовать balancerTag и outboundTag. При одновременном использовании будет работать только outboundTag."
|
||||
|
||||
[pages.xray.wireguard]
|
||||
"secretKey" = "Секретный ключ"
|
||||
"publicKey" = "Открытый ключ"
|
||||
"publicKey" = "Публичный ключ"
|
||||
"allowedIPs" = "Разрешенные IP-адреса"
|
||||
"endpoint" = "Конечная точка"
|
||||
"psk" = "Общий ключ"
|
||||
"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]
|
||||
"admin" = "Админ"
|
||||
"secret" = "Секретный токен"
|
||||
|
||||
625
web/translation/translate.uk_UA.toml
Normal file
625
web/translation/translate.uk_UA.toml
Normal file
@@ -0,0 +1,625 @@
|
||||
"username" = "Ім'я користувача"
|
||||
"password" = "Пароль"
|
||||
"login" = "Увійти"
|
||||
"confirm" = "Підтвердити"
|
||||
"cancel" = "Скасувати"
|
||||
"close" = "Закрити"
|
||||
"copy" = "Копіювати"
|
||||
"copied" = "Скопійовано"
|
||||
"download" = "Завантажити"
|
||||
"remark" = "Примітка"
|
||||
"enable" = "Увімкнути"
|
||||
"protocol" = "Протокол"
|
||||
"search" = "Пошук"
|
||||
"filter" = "Фільтр"
|
||||
"loading" = "Завантаження..."
|
||||
"second" = "Секунда"
|
||||
"minute" = "Хвилина"
|
||||
"hour" = "Година"
|
||||
"day" = "День"
|
||||
"check" = "Перевірка"
|
||||
"indefinite" = "Безстроково"
|
||||
"unlimited" = "Безлімітний"
|
||||
"none" = "Немає"
|
||||
"qrCode" = "QR-Код"
|
||||
"info" = "Більше інформації"
|
||||
"edit" = "Редагувати"
|
||||
"delete" = "Видалити"
|
||||
"reset" = "Скидання"
|
||||
"copySuccess" = "Скопійовано успішно"
|
||||
"sure" = "Звичайно"
|
||||
"encryption" = "Шифрування"
|
||||
"transmission" = "Протокол передачи"
|
||||
"host" = "Хост"
|
||||
"path" = "Шлях"
|
||||
"camouflage" = "Маскування"
|
||||
"status" = "Статус"
|
||||
"enabled" = "Увімкнено"
|
||||
"disabled" = "Вимкнено"
|
||||
"depleted" = "Вичерпано"
|
||||
"depletingSoon" = "Вичерпується"
|
||||
"offline" = "Офлайн"
|
||||
"online" = "Онлайн"
|
||||
"domainName" = "Доменне ім`я"
|
||||
"monitor" = "Слухати IP"
|
||||
"certificate" = "Цифровий сертифікат"
|
||||
"fail" = " Помилка"
|
||||
"success" = " Успішно"
|
||||
"getVersion" = "Отримати версію"
|
||||
"install" = "Встановити"
|
||||
"clients" = "Клієнти"
|
||||
"usage" = "Використання"
|
||||
"secretToken" = "Секретний маркер"
|
||||
"remained" = "Залишилося"
|
||||
"security" = "Беспека"
|
||||
"secAlertTitle" = "Попередження системи безпеки"
|
||||
"secAlertSsl" = "Це з'єднання не є безпечним. Будь ласка, уникайте введення конфіденційної інформації, поки TLS не буде активовано для захисту даних."
|
||||
"secAlertConf" = "Деякі налаштування вразливі до атак. Рекомендується посилити протоколи безпеки, щоб запобігти можливим порушенням."
|
||||
"secAlertSSL" = "Панель не має безпечного з'єднання. Будь ласка, встановіть сертифікат TLS для захисту даних."
|
||||
"secAlertPanelPort" = "Стандартний порт панелі вразливий. Будь ласка, сконфігуруйте випадковий або конкретний порт."
|
||||
"secAlertPanelURI" = "Стандартний URI-шлях панелі небезпечний. Будь ласка, сконфігуруйте складний URI-шлях."
|
||||
"secAlertSubURI" = "Стандартний URI-шлях підписки небезпечний. Будь ласка, сконфігуруйте складний URI-шлях."
|
||||
"secAlertSubJsonURI" = "Стандартний URI-шлях JSON підписки небезпечний. Будь ласка, сконфігуруйте складний URI-шлях."
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Огляд"
|
||||
"inbounds" = "Вхідні"
|
||||
"settings" = "Параметри панелі"
|
||||
"xray" = "Конфігурації Xray"
|
||||
"logout" = "Вийти"
|
||||
"link" = "Керувати"
|
||||
|
||||
[pages.login]
|
||||
"hello" = "Привіт"
|
||||
"title" = "Ласкаво просимо"
|
||||
"loginAgain" = "Ваш сеанс закінчився, увійдіть знову"
|
||||
|
||||
[pages.login.toasts]
|
||||
"invalidFormData" = "Формат вхідних даних недійсний."
|
||||
"emptyUsername" = "Потрібне ім'я користувача"
|
||||
"emptyPassword" = "Потрібен пароль"
|
||||
"wrongUsernameOrPassword" = "Невірне ім'я користувача або пароль."
|
||||
"successLogin" = "Вхід"
|
||||
|
||||
[pages.index]
|
||||
"title" = "Огляд"
|
||||
"memory" = "Пам'ять"
|
||||
"hard" = "Диск"
|
||||
"xrayStatus" = "Xray"
|
||||
"stopXray" = "Зупинити"
|
||||
"restartXray" = "Перезапустити"
|
||||
"xraySwitch" = "Версія"
|
||||
"xraySwitchClick" = "Виберіть версію, на яку ви хочете перейти."
|
||||
"xraySwitchClickDesk" = "Вибирайте уважно, оскільки старіші версії можуть бути несумісними з поточними конфігураціями."
|
||||
"operationHours" = "Час роботи"
|
||||
"systemLoad" = "Завантаження системи"
|
||||
"systemLoadDesc" = "Середнє завантаження системи за останні 1, 5 і 15 хвилин"
|
||||
"connectionTcpCountDesc" = "Загальна кількість TCP-з'єднань у системі"
|
||||
"connectionUdpCountDesc" = "Загальна кількість UDP-з'єднань у системі"
|
||||
"connectionCount" = "Статистика з'єднання"
|
||||
"upSpeed" = "Загальна швидкість завантаження в системі"
|
||||
"downSpeed" = "Загальна швидкість завантаження в системі"
|
||||
"totalSent" = "Загальна кількість даних, надісланих через систему з моменту запуску ОС"
|
||||
"totalReceive" = "Загальна кількість даних, отриманих системою з моменту запуску ОС"
|
||||
"xraySwitchVersionDialog" = "Змінити версію Xray"
|
||||
"xraySwitchVersionDialogDesc" = "Ви впевнені, що бажаєте змінити версію Xray на"
|
||||
"dontRefresh" = "Інсталяція триває, будь ласка, не оновлюйте цю сторінку"
|
||||
"logs" = "Журнали"
|
||||
"config" = "Конфігурація"
|
||||
"backup" = "Резервне копіювання та відновлення"
|
||||
"backupTitle" = "Резервне копіювання та відновлення бази даних"
|
||||
"backupDescription" = "Рекомендується зробити резервну копію перед відновленням бази даних."
|
||||
"exportDatabase" = "Резервне копіювання"
|
||||
"importDatabase" = "Відновити"
|
||||
|
||||
[pages.inbounds]
|
||||
"title" = "Вхідні"
|
||||
"totalDownUp" = "Всього надісланих/отриманих"
|
||||
"totalUsage" = "Всього використанно"
|
||||
"inboundCount" = "Загальна кількість вхідних"
|
||||
"operate" = "Меню"
|
||||
"enable" = "Увімкнено"
|
||||
"remark" = "Примітка"
|
||||
"protocol" = "Протокол"
|
||||
"port" = "Порт"
|
||||
"traffic" = "Трафік"
|
||||
"details" = "Деталі"
|
||||
"transportConfig" = "Транспорт"
|
||||
"expireDate" = "Тривалість"
|
||||
"resetTraffic" = "Скинути трафік"
|
||||
"addInbound" = "Додати вхідний"
|
||||
"generalActions" = "Загальні дії"
|
||||
"create" = "Створити"
|
||||
"update" = "Оновити"
|
||||
"modifyInbound" = "Змінити вхідний"
|
||||
"deleteInbound" = "Видалити вхідні"
|
||||
"deleteInboundContent" = "Ви впевнені, що хочете видалити вхідні?"
|
||||
"deleteClient" = "Видалити клієнта"
|
||||
"deleteClientContent" = "Ви впевнені, що хочете видалити клієнт?"
|
||||
"resetTrafficContent" = "Ви впевнені, що хочете скинути трафік?"
|
||||
"copyLink" = "Копіювати URL"
|
||||
"address" = "Адреса"
|
||||
"network" = "Мережа"
|
||||
"destinationPort" = "Порт призначення"
|
||||
"targetAddress" = "Цільова адреса"
|
||||
"monitorDesc" = "Залиште порожнім, щоб слухати всі IP-адреси"
|
||||
"meansNoLimit" = " = Необмежено. (одиниця: ГБ)"
|
||||
"totalFlow" = "Загальна витрата"
|
||||
"leaveBlankToNeverExpire" = "Залиште порожнім, щоб ніколи не закінчувався"
|
||||
"noRecommendKeepDefault" = "Рекомендується зберегти значення за замовчуванням"
|
||||
"certificatePath" = "Шлях до файлу"
|
||||
"certificateContent" = "Вміст файлу"
|
||||
"publicKey" = "Публічний ключ"
|
||||
"privatekey" = "Закритий ключ"
|
||||
"clickOnQRcode" = "Натисніть QR-код, щоб скопіювати"
|
||||
"client" = "Клієнт"
|
||||
"export" = "Експортувати всі URL-адреси"
|
||||
"clone" = "Клон"
|
||||
"cloneInbound" = "Клонувати"
|
||||
"cloneInboundContent" = "Усі налаштування цього вхідного потоку, крім порту, IP-адреси прослуховування та клієнтів, будуть застосовані до клону."
|
||||
"cloneInboundOk" = "Клонувати"
|
||||
"resetAllTraffic" = "Скинути весь вхідний трафік"
|
||||
"resetAllTrafficTitle" = "Скинути весь вхідний трафік"
|
||||
"resetAllTrafficContent" = "Ви впевнені, що бажаєте скинути трафік усіх вхідних?"
|
||||
"resetInboundClientTraffics" = "Скинути трафік клієнтів"
|
||||
"resetInboundClientTrafficTitle" = "Скинути трафік клієнтів"
|
||||
"resetInboundClientTrafficContent" = "Ви впевнені, що бажаєте скинути трафік клієнтів цього вхідного потоку?"
|
||||
"resetAllClientTraffics" = "Скинути весь трафік клієнтів"
|
||||
"resetAllClientTrafficTitle" = "Скинути весь трафік клієнтів"
|
||||
"resetAllClientTrafficContent" = "Ви впевнені, що бажаєте скинути трафік усіх клієнтів?"
|
||||
"delDepletedClients" = "Видалити вичерпані клієнти"
|
||||
"delDepletedClientsTitle" = "Видалити вичерпані клієнти"
|
||||
"delDepletedClientsContent" = "Ви впевнені, що хочете видалити всі вичерпані клієнти?"
|
||||
"email" = "Електронна пошта"
|
||||
"emailDesc" = "Будь ласка, надайте унікальну адресу електронної пошти."
|
||||
"IPLimit" = "Обмеження IP"
|
||||
"IPLimitDesc" = "Вимикає вхідний, якщо кількість перевищує встановлене значення. (0 = вимкнено)"
|
||||
"IPLimitlog" = "Журнал IP"
|
||||
"IPLimitlogDesc" = "Журнал історії IP-адрес. (щоб увімкнути вхідну після вимкнення, очистіть журнал)"
|
||||
"IPLimitlogclear" = "Очистити журнал"
|
||||
"setDefaultCert" = "Установити сертифікат з панелі"
|
||||
"xtlsDesc" = "Xray має бути v1.7.5"
|
||||
"realityDesc" = "Xray має бути v1.8.0+"
|
||||
"telegramDesc" = "Будь ласка, надайте ідентифікатори Telegram або чату без використання '@'. (отримайте його тут @userinfobot) або (використайте команду '/id' у боті)"
|
||||
"subscriptionDesc" = "Щоб знайти URL-адресу вашої підписки, перейдіть до «Деталі». Крім того, ви можете використовувати одне ім'я для кількох клієнтів."
|
||||
"info" = "Інформація"
|
||||
"same" = "Те саме"
|
||||
"inboundData" = "Вхідні дані"
|
||||
"exportInbound" = "Експортувати вхідні"
|
||||
"import" = "Імпорт"
|
||||
"importInbound" = "Імпортувати вхідний"
|
||||
|
||||
[pages.client]
|
||||
"add" = "Додати клієнта"
|
||||
"edit" = "Редагувати клієнта"
|
||||
"submitAdd" = "Додати клієнта"
|
||||
"submitEdit" = "Зберегти зміни"
|
||||
"clientCount" = "Кількість клієнтів"
|
||||
"bulk" = "Додати групу"
|
||||
"method" = "Метод"
|
||||
"first" = "Перший"
|
||||
"last" = "Останній"
|
||||
"prefix" = "Префікс"
|
||||
"postfix" = "Постфікс"
|
||||
"delayedStart" = "Початок використання"
|
||||
"expireDays" = "Тривалість"
|
||||
"days" = "Дні(в)"
|
||||
"renew" = "Автоматичне оновлення"
|
||||
"renewDesc" = "Автоматичне поновлення після закінчення терміну дії. (0 = вимкнено)(одиниця: день)"
|
||||
|
||||
[pages.inbounds.toasts]
|
||||
"obtain" = "Отримати"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Запит"
|
||||
"response" = "Відповідь"
|
||||
"name" = "Ім'я"
|
||||
"value" = "Значення"
|
||||
|
||||
[pages.inbounds.stream.tcp]
|
||||
"version" = "Версія"
|
||||
"method" = "Метод"
|
||||
"path" = "Шлях"
|
||||
"status" = "Статус"
|
||||
"statusDescription" = "Опис стану"
|
||||
"requestHeader" = "Заголовок запиту"
|
||||
"responseHeader" = "Заголовок відповіді"
|
||||
|
||||
[pages.inbounds.stream.quic]
|
||||
"encryption" = "Шифрування"
|
||||
|
||||
[pages.settings]
|
||||
"title" = "Параметри панелі"
|
||||
"save" = "Зберегти"
|
||||
"infoDesc" = "Кожна внесена тут зміна повинна бути збережена. Перезапустіть панель, щоб застосувати зміни."
|
||||
"restartPanel" = "Перезапустити панель"
|
||||
"restartPanelDesc" = "Ви впевнені, що бажаєте перезапустити панель? Якщо ви не можете отримати доступ до панелі після перезапуску, будь ласка, перегляньте інформацію журналу панелі на сервері."
|
||||
"actions" = "Дії"
|
||||
"resetDefaultConfig" = "Відновити значення за замовчуванням"
|
||||
"panelSettings" = "Загальні"
|
||||
"securitySettings" = "Автентифікація"
|
||||
"TGBotSettings" = "Telegram Бот"
|
||||
"panelListeningIP" = "Слухати IP"
|
||||
"panelListeningIPDesc" = "IP-адреса для веб-панелі. (залиште порожнім, щоб слухати всі IP-адреси)"
|
||||
"panelListeningDomain" = "Домен прослуховування"
|
||||
"panelListeningDomainDesc" = "Доменне ім'я для веб-панелі. (залиште порожнім, щоб слухати всі домени та IP-адреси)"
|
||||
"panelPort" = "Порт прослуховування"
|
||||
"panelPortDesc" = "Номер порту для веб-панелі. (має бути невикористаний порт)"
|
||||
"publicKeyPath" = "Шлях відкритого ключа"
|
||||
"publicKeyPathDesc" = "Шлях до файлу відкритого ключа для веб-панелі. (починається з ‘/‘)"
|
||||
"privateKeyPath" = "Шлях приватного ключа"
|
||||
"privateKeyPathDesc" = "Шлях до файлу приватного ключа для веб-панелі. (починається з ‘/‘)"
|
||||
"panelUrlPath" = "Шлях URL"
|
||||
"panelUrlPathDesc" = "Шлях URL для веб-панелі. (починається з ‘/‘ і закінчується ‘/‘)"
|
||||
"pageSize" = "Розмір сторінки"
|
||||
"pageSizeDesc" = "Визначити розмір сторінки для вхідної таблиці. (0 = вимкнено)"
|
||||
"remarkModel" = "Модель зауваження та роздільний символ"
|
||||
"datepicker" = "Тип календаря"
|
||||
"datepickerPlaceholder" = "Виберіть дату"
|
||||
"datepickerDescription" = "Заплановані завдання виконуватимуться на основі цього календаря."
|
||||
"sampleRemark" = "Зразок зауваження"
|
||||
"oldUsername" = "Поточне ім'я користувача"
|
||||
"currentPassword" = "Поточний пароль"
|
||||
"newUsername" = "Нове ім'я користувача"
|
||||
"newPassword" = "Новий пароль"
|
||||
"telegramBotEnable" = "Увімкнути Telegram Bot"
|
||||
"telegramBotEnableDesc" = "Вмикає бота Telegram."
|
||||
"telegramToken" = "Telegram Токен"
|
||||
"telegramTokenDesc" = "Токен бота Telegram, отриманий від '@BotFather'."
|
||||
"telegramProxy" = "SOCKS Проксі"
|
||||
"telegramProxyDesc" = "Вмикає проксі-сервер SOCKS5 для підключення до Telegram. (відкоригуйте параметри відповідно до посібника)"
|
||||
"telegramChatId" = "Ідентифікатор чату адміністратора"
|
||||
"telegramChatIdDesc" = "Ідентифікатори чату адміністратора Telegram. (розділені комами) (отримайте тут @userinfobot) або (використовуйте команду '/id' у боті)"
|
||||
"telegramNotifyTime" = "Час сповіщення"
|
||||
"telegramNotifyTimeDesc" = "Час повідомлення бота Telegram, встановлений для періодичних звітів. (використовуйте формат часу crontab)"
|
||||
"tgNotifyBackup" = "Резервне копіювання бази даних"
|
||||
"tgNotifyBackupDesc" = "Надіслати файл резервної копії бази даних зі звітом."
|
||||
"tgNotifyLogin" = "Сповіщення про вхід"
|
||||
"tgNotifyLoginDesc" = "Отримувати сповіщення про ім'я користувача, IP-адресу та час щоразу, коли хтось намагається увійти у вашу веб-панель."
|
||||
"sessionMaxAge" = "Тривалість сеансу"
|
||||
"sessionMaxAgeDesc" = "Тривалість, протягом якої ви можете залишатися в системі. (одиниця: хвилина)"
|
||||
"expireTimeDiff" = "Повідомлення про дату закінчення"
|
||||
"expireTimeDiffDesc" = "Отримувати сповіщення про термін дії при досягненні цього порогу. (одиниця: день)"
|
||||
"trafficDiff" = "Повідомлення про обмеження трафіку"
|
||||
"trafficDiffDesc" = "Отримувати сповіщення про обмеження трафіку при досягненні цього порогу. (одиниця: ГБ)"
|
||||
"tgNotifyCpu" = "Сповіщення про завантаження ЦП"
|
||||
"tgNotifyCpuDesc" = "Отримувати сповіщення, якщо навантаження ЦП перевищує це порогове значення. (одиниця: %)"
|
||||
"timeZone" = "Часовий пояс"
|
||||
"timeZoneDesc" = "Заплановані завдання виконуватимуться на основі цього часового поясу."
|
||||
"subSettings" = "Підписка"
|
||||
"subEnable" = "Увімкнути службу підписки"
|
||||
"subEnableDesc" = "Вмикає службу підписки."
|
||||
"subListen" = "Слухати IP"
|
||||
"subListenDesc" = "IP-адреса для служби підписки. (залиште порожнім, щоб слухати всі IP-адреси)"
|
||||
"subPort" = "Слухати порт"
|
||||
"subPortDesc" = "Номер порту для служби підписки. (має бути невикористаний порт)"
|
||||
"subCertPath" = "Шлях відкритого ключа"
|
||||
"subCertPathDesc" = "Шлях до файлу відкритого ключа для служби підписки. (починається з ‘/‘)"
|
||||
"subKeyPath" = "Шлях приватного ключа"
|
||||
"subKeyPathDesc" = "Шлях до файлу приватного ключа для служби підписки. (починається з ‘/‘)"
|
||||
"subPath" = "Шлях URI"
|
||||
"subPathDesc" = "Шлях URI для служби підписки. (починається з ‘/‘ і закінчується ‘/‘)"
|
||||
"subDomain" = "Домен прослуховування"
|
||||
"subDomainDesc" = "Ім'я домену для служби підписки. (залиште порожнім, щоб слухати всі домени та IP-адреси)"
|
||||
"subUpdates" = "Інтервали оновлення"
|
||||
"subUpdatesDesc" = "Інтервали оновлення URL-адреси підписки в клієнтських програмах. (одиниця: година)"
|
||||
"subEncrypt" = "Закодувати"
|
||||
"subEncryptDesc" = "Повернений вміст послуги підписки матиме кодування Base64."
|
||||
"subShowInfo" = "Показати інформацію про використання"
|
||||
"subShowInfoDesc" = "Залишок трафіку та дата відображатимуться в клієнтських програмах."
|
||||
"subURI" = "URI зворотного проксі"
|
||||
"subURIDesc" = "URI до URL-адреси підписки для використання за проксі."
|
||||
"fragment" = "Фрагментація"
|
||||
"fragmentDesc" = "Увімкнути фрагментацію для пакету привітання TLS"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Xray конфігурації"
|
||||
"save" = "Зберегти"
|
||||
"restart" = "Перезапустити Xray"
|
||||
"basicTemplate" = "Базовий шаблон"
|
||||
"advancedTemplate" = "Додатково"
|
||||
"generalConfigs" = "Загальні конфігурації"
|
||||
"generalConfigsDesc" = "Ці параметри визначатимуть загальні налаштування."
|
||||
"logConfigs" = "Журнал"
|
||||
"logConfigsDesc" = "Журнали можуть вплинути на ефективність вашого сервера. Рекомендується вмикати його з розумом лише у випадку ваших потреб"
|
||||
"blockConfigs" = "Захисний екран"
|
||||
"blockConfigsDesc" = "Ці параметри блокуватимуть трафік на основі конкретних запитуваних протоколів і веб-сайтів."
|
||||
"blockCountryConfigs" = "Заблокувати країну"
|
||||
"blockCountryConfigsDesc" = "Ці параметри блокуватимуть трафік на основі конкретної запитуваної країни."
|
||||
"directCountryConfigs" = "Пряма країна"
|
||||
"directCountryConfigsDesc" = "Ці параметри безпосередньо перенаправлятимуть трафік на основі конкретної запитуваної країни."
|
||||
"ipv4Configs" = "Маршрутизація IPv4"
|
||||
"ipv4ConfigsDesc" = "Ці параметри спрямовуватимуть трафік на основі певного призначення через IPv4."
|
||||
"warpConfigs" = "WARP маршрутизація"
|
||||
"warpConfigsDesc" = "Ці параметри маршрутизуватимуть трафік на основі певного пункту призначення через WARP."
|
||||
"Template" = "Шаблон розширеної конфігурації Xray"
|
||||
"TemplateDesc" = "Остаточний конфігураційний файл Xray буде створено на основі цього шаблону."
|
||||
"FreedomStrategy" = "Стратегія протоколу свободи"
|
||||
"FreedomStrategyDesc" = "Установити стратегію виведення для мережі в протоколі свободи."
|
||||
"RoutingStrategy" = "Загальна стратегія маршрутизації"
|
||||
"RoutingStrategyDesc" = "Установити загальну стратегію маршрутизації трафіку для вирішення всіх запитів."
|
||||
"Torrent" = "Блокувати протокол BitTorrent"
|
||||
"TorrentDesc" = "Блокує протокол BitTorrent."
|
||||
"PrivateIp" = "Блокувати підключення до приватних IP-адрес"
|
||||
"PrivateIpDesc" = "Блокує встановлення підключень до приватних діапазонів IP."
|
||||
"Ads" = "Блокувати рекламу"
|
||||
"AdsDesc" = "Блокує рекламні веб-сайти."
|
||||
"Family" = "Захист сім'ї"
|
||||
"FamilyDesc" = "Блокує вміст для дорослих і веб-сайти з шкідливими програмами."
|
||||
"Security" = "Щит безпеки"
|
||||
"SecurityDesc" = "Блокує веб-сайти шкідливого програмного забезпечення, фішингу та майнерів."
|
||||
"Speedtest" = "Заблокувати Speedtest"
|
||||
"SpeedtestDesc" = "Блокує підключення до веб-сайтів для тестування швидкості."
|
||||
"IRIp" = "Блокувати підключення до IP-адрес Ірану"
|
||||
"IRIpDesc" = "Блокує встановлення з'єднань з діапазонами IP Ірану."
|
||||
"IRDomain" = "Блокувати підключення до доменів Ірану"
|
||||
"IRDomainDesc" = "Блокує встановлення з'єднань з доменами Ірану."
|
||||
"ChinaIp" = "Блокувати підключення до IP-адрес Китаю"
|
||||
"ChinaIpDesc" = "Блокує встановлення з'єднань із діапазонами IP-адрес Китаю."
|
||||
"ChinaDomain" = "Блокувати підключення до китайських доменів"
|
||||
"ChinaDomainDesc" = "Блокує встановлення підключень до доменів Китаю."
|
||||
"RussiaIp" = "Блокувати підключення до російських IP-адрес"
|
||||
"RussiaIpDesc" = "Блокує встановлення з'єднань з діапазонами IP-адрес Росії."
|
||||
"RussiaDomain" = "Блокувати підключення до російських доменів"
|
||||
"RussiaDomainDesc" = "Блокує встановлення з'єднань з російськими доменами."
|
||||
"VNIp" = "Блокувати підключення до IP-адрес В'єтнаму"
|
||||
"VNIpDesc" = "Блокує встановлення з'єднань із діапазонами IP-адрес В'єтнаму."
|
||||
"VNDomain" = "Блокувати підключення до доменів В'єтнаму"
|
||||
"VNDomainDesc" = "Блокує встановлення з'єднань із доменами В'єтнаму."
|
||||
"DirectIRIp" = "Пряме підключення до IP-адрес Ірану"
|
||||
"DirectIRIpDesc" = "Безпосередньо встановлює з'єднання з діапазонами IP Ірану."
|
||||
"DirectIRDomain" = "Пряме підключення до доменів Ірану"
|
||||
"DirectIRDomainDesc" = "Безпосередньо встановлює підключення до доменів Ірану."
|
||||
"DirectChinaIp" = "Пряме підключення до китайських IP-адрес"
|
||||
"DirectChinaIpDesc" = "Безпосередньо встановлює підключення до IP-діапазонів Китаю."
|
||||
"DirectChinaDomain" = "Пряме підключення до китайських доменів"
|
||||
"DirectChinaDomainDesc" = "Безпосередньо встановлює підключення до доменів Китаю."
|
||||
"DirectRussiaIp" = "Пряме підключення до IP-адрес Росії"
|
||||
"DirectRussiaIpDesc" = "Безпосередньо встановлює з'єднання з діапазонами IP-адрес Росії."
|
||||
"DirectRussiaDomain" = "Пряме підключення до російських доменів"
|
||||
"DirectRussiaDomainDesc" = "Безпосередньо встановлює підключення до російських доменів."
|
||||
"DirectVNIp" = "Пряме підключення до IP-адрес В'єтнаму"
|
||||
"DirectVNIpDesc" = "Безпосередньо встановлює з'єднання з діапазонами IP-адрес В'єтнаму."
|
||||
"DirectVNDomain" = "Пряме підключення до доменів В'єтнаму"
|
||||
"DirectVNDomainDesc" = "Безпосередньо встановлює з'єднання з доменами В'єтнаму."
|
||||
"GoogleIPv4" = "Google"
|
||||
"GoogleIPv4Desc" = "Направляє трафік до Google через IPv4."
|
||||
"NetflixIPv4" = "Netflix"
|
||||
"NetflixIPv4Desc" = "Направляє трафік до Netflix через IPv4."
|
||||
"GoogleWARP" = "Google"
|
||||
"GoogleWARPDesc" = "Додати маршрутизацію для Google через WARP."
|
||||
"OpenAIWARP" = "ChatGPT"
|
||||
"OpenAIWARPDesc" = "Направляє трафік до ChatGPT через WARP."
|
||||
"NetflixWARP" = "Netflix"
|
||||
"NetflixWARPDesc" = "Направляє трафік до Netflix через WARP."
|
||||
"MetaWARP" = "Meta"
|
||||
"MetaWARPDesc" = "Направляє трафік до Meta (Instagram, Facebook, WhatsApp, Threads,...) через WARP."
|
||||
"AppleWARP" = "Apple"
|
||||
"AppleWARPDesc" = "Направляє трафік до Apple через WARP."
|
||||
"RedditWARP" = "Reddit"
|
||||
"RedditWARPDesc" = "Направляє трафік до Reddit через WARP."
|
||||
"SpotifyWARP" = "Spotify"
|
||||
"SpotifyWARPDesc" = "Направляє трафік до Spotify через WARP."
|
||||
"IRWARP" = "Іранські домени"
|
||||
"IRWARPDesc" = "Направляє трафік до доменів Ірану через WARP"
|
||||
"Inbounds" = "Вхідні"
|
||||
"InboundsDesc" = "Прийняття певних клієнтів."
|
||||
"Outbounds" = "Вихід"
|
||||
"Balancers" = "Балансери"
|
||||
"OutboundsDesc" = "Встановити шлях вихідного трафіку."
|
||||
"Routings" = "Правила маршрутизації"
|
||||
"RoutingsDesc" = "Пріоритет кожного правила важливий!"
|
||||
"completeTemplate" = "Усі"
|
||||
"logLevel" = "Рівень журналу"
|
||||
"logLevelDesc" = "Рівень журналу для журналів помилок із зазначенням інформації, яку потрібно записати."
|
||||
"accessLog" = "Журнал доступу"
|
||||
"accessLogDesc" = "Шлях до файлу журналу доступу. Спеціальне значення 'none' вимикає журнали доступу"
|
||||
"errorLog" = "Журнал помилок"
|
||||
"errorLogDesc" = "Шлях до файлу журналу помилок. Спеціальне значення 'none' вимикає журнали помилок"
|
||||
|
||||
[pages.xray.rules]
|
||||
"first" = "Перший"
|
||||
"last" = "Останній"
|
||||
"up" = "Вгору"
|
||||
"down" = "Вниз"
|
||||
"source" = "Джерело"
|
||||
"dest" = "Пункт призначення"
|
||||
"inbound" = "Вхідний"
|
||||
"outbound" = "Вихідний"
|
||||
"balancer" = "Балансувальник"
|
||||
"info" = "Інформація"
|
||||
"add" = "Додати правило"
|
||||
"edit" = "Редагувати правило"
|
||||
"useComma" = "Елементи, розділені комами"
|
||||
|
||||
[pages.xray.outbound]
|
||||
"addOutbound" = "Додати вихідний"
|
||||
"addReverse" = "Додати реверс"
|
||||
"editOutbound" = "Редагувати вихідні"
|
||||
"editReverse" = "Редагувати реверс"
|
||||
"tag" = "Тег"
|
||||
"tagDesc" = "Унікальний тег"
|
||||
"address" = "Адреса"
|
||||
"reverse" = "Зворотний"
|
||||
"domain" = "Домен"
|
||||
"type" = "Тип"
|
||||
"bridge" = "Міст"
|
||||
"portal" = "Портал"
|
||||
"intercon" = "Взаємозв'язок"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "Додати балансир"
|
||||
"editBalancer" = "Редагувати балансир"
|
||||
"balancerStrategy" = "Стратегія"
|
||||
"balancerSelectors" = "Селектори"
|
||||
"tag" = "Тег"
|
||||
"tagDesc" = "Унікальний тег"
|
||||
"balancerDesc" = "Неможливо використовувати balancerTag і outboundTag одночасно. Якщо використовувати одночасно, працюватиме лише outboundTag."
|
||||
|
||||
[pages.xray.wireguard]
|
||||
"secretKey" = "Приватний ключ"
|
||||
"publicKey" = "Публічний ключ"
|
||||
"allowedIPs" = "Дозволені IP-адреси"
|
||||
"endpoint" = "Кінцева точка"
|
||||
"psk" = "Спільний ключ"
|
||||
"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]
|
||||
"admin" = "Адміністратор"
|
||||
"secret" = "Секретний маркер"
|
||||
"loginSecurity" = "Безпечний вхід"
|
||||
"loginSecurityDesc" = "Додає додатковий рівень автентифікації для забезпечення більшої безпеки."
|
||||
"secretToken" = "Секретний маркер"
|
||||
"secretTokenDesc" = "Будь ласка, надійно зберігайте цей маркер у безпечному місці. Цей маркер потрібен для входу, і його неможливо відновити."
|
||||
|
||||
[pages.settings.toasts]
|
||||
"modifySettings" = "Змінити налаштування"
|
||||
"getSettings" = "Отримати налаштування"
|
||||
"modifyUser" = "Змінити адміністратора"
|
||||
"originalUserPassIncorrect" = "Поточне ім'я користувача або пароль недійсні"
|
||||
"userPassMustBeNotEmpty" = "Нове ім'я користувача та пароль порожні"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Спеціальна клавіатура закрита!"
|
||||
"noResult" = "❗ Немає результату!"
|
||||
"noQuery" = "❌ Запит не знайдено! Скористайтеся командою ще раз!"
|
||||
"wentWrong" = "❌ Щось пішло не так!"
|
||||
"noIpRecord" = "❗ Немає IP-запису!"
|
||||
"noInbounds" = "❗ Вхідних не знайдено!"
|
||||
"unlimited" = "♾ Необмежений (скинути)"
|
||||
"add" = "Додати"
|
||||
"month" = "Місяць"
|
||||
"months" = "Місяці"
|
||||
"day" = "День"
|
||||
"days" = "Дні"
|
||||
"hours" = "Годинник"
|
||||
"unknown" = "Невідомо"
|
||||
"inbounds" = "Вхідні"
|
||||
"clients" = "Клієнти"
|
||||
"offline" = "🔴 Офлайн"
|
||||
"online" = "🟢 Онлайн"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ Невідома команда."
|
||||
"pleaseChoose" = "👇 Будь ласка, виберіть:\r\n"
|
||||
"help" = "🤖 Ласкаво просимо до цього бота! Він розроблений, щоб надавати певні дані з веб-панелі та дозволяє вносити зміни за потреби.\r\n\r\n"
|
||||
"start" = "👋 Привіт <i>{{ .Firstname }}</i>.\r\n"
|
||||
"welcome" = "🤖 Ласкаво просимо до <b>{{ .Hostname }}</b> бота керування.\r\n"
|
||||
"status" = "✅ Бот в порядку!"
|
||||
"usage" = "❗ Введіть текст для пошуку!"
|
||||
"getID" = "🆔 Ваш ідентифікатор: <code>{{ .ID }}</code>"
|
||||
"helpAdminCommands" = "Для пошуку електронної пошти клієнта:\r\n<code>/usage [Email]</code>\r\n\r\nДля пошуку вхідних листів (зі статистикою клієнта):\r\n<code>/inbound [Примітка]</code>"
|
||||
"helpClientCommands" = "Для пошуку статистики скористайтеся такою командою:\r\n\r\n<code>/usage [Email]</code>"
|
||||
|
||||
[tgbot.messages]
|
||||
"cpuThreshold" = "🔴 Навантаження ЦП {{ .Percent }}% перевищує порогове значення {{ .Threshold }}%"
|
||||
"selectUserFailed" = "❌ Помилка під час вибору користувача!"
|
||||
"userSaved" = "✅ Користувача Telegram збережено."
|
||||
"loginSuccess" = "✅ Успішно ввійшли в панель\r\n"
|
||||
"loginFailed" = "❗️ Помилка входу в панель.\r\n"
|
||||
"report" = "🕰 Заплановані звіти: {{ .RunTime }}\r\n"
|
||||
"datetime" = "⏰ Дата й час: {{ .DateTime }}\r\n"
|
||||
"hostname" = "💻 Хост: {{ .Hostname }}\r\n"
|
||||
"version" = "🚀 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" = "⏳ Час роботи: {{ .UpTime }} {{ .Unit }}\r\n"
|
||||
"serverLoad" = "📈 Завантаження системи: {{ .Load1 }}, {{ .Load2 }}, {{ .Load3 }}\r\n"
|
||||
"serverMemory" = "📋 RAM: {{ .Current }}/{{ .Total }}\r\n"
|
||||
"tcpCount" = "🔹 TCP: {{ .Count }}\r\n"
|
||||
"udpCount" = "🔸 UDP: {{ .Count }}\r\n"
|
||||
"traffic" = "🚦 Трафік: {{ .Total }} (↑{{ .Upload }},↓{{ .Download }})\r\n"
|
||||
"xrayStatus" = "ℹ️ Статус: {{ .State }}\r\n"
|
||||
"username" = "👤 Ім'я користувача: {{ .Username }}\r\n"
|
||||
"time" = "⏰ Час: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 Inbound: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 Порт: {{ .Port }}\r\n"
|
||||
"expire" = "📅 Дата закінчення: {{ .Time }}\r\n"
|
||||
"expireIn" = "📅 Термін дії: {{ .Time }}\r\n"
|
||||
"active" = "💡 Активний: {{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 Увімкнено: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 Стан підключення: {{ .Status }}\r\n"
|
||||
"email" = "📧 Електронна пошта: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 Download: ↓{{ .Download }}\r\n"
|
||||
"total" = "📊 Всього: ↑↓{{ .UpDown }} / {{ .Total }}\r\n"
|
||||
"TGUser" = "👤 Користувач Telegram: {{ .TelegramID }}\r\n"
|
||||
"exhaustedMsg" = "🚨 Вичерпано {{ .Type }}:\r\n"
|
||||
"exhaustedCount" = "🚨 Вичерпано кількість {{ .Type }} count:\r\n"
|
||||
"onlinesCount" = "🌐 Онлайн-клієнти: {{ .Count }}\r\n"
|
||||
"disabled" = "🛑 Вимкнено: {{ .Disabled }}\r\n"
|
||||
"depleteSoon" = "🔜 Скоро вичерпається: {{ .Deplete }}\r\n\r\n"
|
||||
"backupTime" = "🗄 Час резервного копіювання: {{ .Time }}\r\n"
|
||||
"refreshedOn" = "\r\n📋🔄 Оновлено: {{ .Time }}\r\n\r\n"
|
||||
"yes" = "✅ Так"
|
||||
"no" = "❌ Ні"
|
||||
|
||||
[tgbot.buttons]
|
||||
"closeKeyboard" = "❌ Закрити клавіатуру"
|
||||
"cancel" = "❌ Скасувати"
|
||||
"cancelReset" = "❌ Скасувати скидання"
|
||||
"cancelIpLimit" = "❌ Скасувати обмеження IP"
|
||||
"confirmResetTraffic" = "✅ Підтвердити скидання трафіку?"
|
||||
"confirmClearIps" = "✅ Підтвердити очищення IP-адрес?"
|
||||
"confirmRemoveTGUser" = "✅ Підтвердити видалення користувача Telegram?"
|
||||
"confirmToggle" = "✅ Підтвердити ввімкнути/вимкнути користувача?"
|
||||
"dbBackup" = "Отримати резервну копію БД"
|
||||
"serverUsage" = "Використання сервера"
|
||||
"getInbounds" = "Отримати вхідні"
|
||||
"depleteSoon" = "Скоро вичерпати"
|
||||
"clientUsage" = "Отримати використання"
|
||||
"onlines" = "Онлайн-клієнти"
|
||||
"commands" = "Команди"
|
||||
"refresh" = "🔄 Оновити"
|
||||
"clearIPs" = "❌ Очистити IP-адреси"
|
||||
"removeTGUser" = "❌ Видалити користувача Telegram"
|
||||
"selectTGUser" = "👤 Виберіть користувача Telegram"
|
||||
"selectOneTGUser" = "👤 Виберіть користувача Telegram:"
|
||||
"resetTraffic" = "📈 Скинути трафік"
|
||||
"resetExpire" = "📅 Змінити термін дії"
|
||||
"ipLog" = "🔢 IP журнал"
|
||||
"ipLimit" = "🔢 IP Ліміт"
|
||||
"setTGUser" = "👤 Встановити користувача Telegram"
|
||||
"toggle" = "🔘 Увімкнути / Вимкнути"
|
||||
"custom" = "🔢 Custom"
|
||||
"confirmNumber" = "✅ Підтвердити: {{ .Num }}"
|
||||
"confirmNumberAdd" = "✅ Підтвердити додавання: {{ .Num }}"
|
||||
"limitTraffic" = "🚧 Ліміт трафіку"
|
||||
"getBanLogs" = "Отримати журнали заборон"
|
||||
|
||||
[tgbot.answers]
|
||||
"successfulOperation" = "✅ Операція успішна!"
|
||||
"errorOperation" = "❗ Помилка в роботі."
|
||||
"getInboundsFailed" = "❌ Не вдалося отримати вхідні повідомлення."
|
||||
"canceled" = "❌ {{ .Email }}: Операцію скасовано."
|
||||
"clientRefreshSuccess" = "✅ {{ .Email }}: Клієнт успішно оновлено."
|
||||
"IpRefreshSuccess" = "✅ {{ .Email }}: IP-адреси успішно оновлено."
|
||||
"TGIdRefreshSuccess" = "✅ {{ .Email }}: Користувач Telegram клієнта успішно оновлено."
|
||||
"resetTrafficSuccess" = "✅ {{ .Email }}: Трафік скинуто успішно."
|
||||
"setTrafficLimitSuccess" = "✅ {{ .Email }}: Ліміт трафіку успішно збережено."
|
||||
"expireResetSuccess" = "✅ {{ .Email }}: Успішно скинуто дні закінчення терміну дії."
|
||||
"resetIpSuccess" = "✅ {{ .Email }}: IP обмеження {{ .Count }} успішно збережено."
|
||||
"clearIpSuccess" = "✅ {{ .Email }}: IP успішно очищено."
|
||||
"getIpLog" = "✅ {{ .Email }}: Отримати IP-журнал."
|
||||
"getUserInfo" = "✅ {{ .Email }}: Отримати інформацію про користувача Telegram."
|
||||
"removedTGUserSuccess" = "✅ {{ .Email }}: Користувача Telegram видалено успішно."
|
||||
"enableSuccess" = "✅ {{ .Email }}: Увімкнути успішно."
|
||||
"disableSuccess" = "✅ {{ .Email }}: Успішно вимкнено."
|
||||
"askToAddUserId" = "Вашу конфігурацію не знайдено!\r\nБудь ласка, попросіть свого адміністратора використовувати ваш ідентифікатор Telegram у вашій конфігурації.\r\n\r\nВаш ідентифікатор користувача: <code>{{ .TgUserID }}</code>"
|
||||
@@ -20,7 +20,7 @@
|
||||
"check" = "Kiểm tra"
|
||||
"indefinite" = "Không xác định"
|
||||
"unlimited" = "Không giới hạn"
|
||||
"none" = "Không có"
|
||||
"none" = "None"
|
||||
"qrCode" = "Mã QR"
|
||||
"info" = "Thông tin thêm"
|
||||
"edit" = "Chỉnh sửa"
|
||||
@@ -42,7 +42,7 @@
|
||||
"online" = "Trực tuyến"
|
||||
"domainName" = "Tên miền"
|
||||
"monitor" = "Listening IP"
|
||||
"certificate" = "Chứng chỉ"
|
||||
"certificate" = "Chứng chỉ số"
|
||||
"fail" = "Thất bại"
|
||||
"success" = "Thành công"
|
||||
"getVersion" = "Lấy phiên bản"
|
||||
@@ -52,6 +52,14 @@
|
||||
"secretToken" = "Mã bí mật"
|
||||
"remained" = "Còn lại"
|
||||
"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ài đặt có thể dễ bị tấn công. Đề xuất tăng cường các giao thức bảo mật để ngăn chặn các vi phạm tiềm ẩn."
|
||||
"secAlertSSL" = "Bảng điều khiển thiếu kết nối an toàn. Vui lòng cài đặt chứng chỉ TLS để bảo vệ dữ liệu."
|
||||
"secAlertPanelPort" = "Cổng mặc định của bảng điều khiển có thể dễ bị tấn công. Vui lòng cấu hình một cổng ngẫu nhiên hoặc cụ thể."
|
||||
"secAlertPanelURI" = "Đường dẫn URI mặc định của bảng điều khiển không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp."
|
||||
"secAlertSubURI" = "Đường dẫn URI mặc định của đăng ký không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp."
|
||||
"secAlertSubJsonURI" = "Đường dẫn URI JSON mặc định của đăng ký không an toàn. Vui lòng cấu hình một đường dẫn URI phức tạp."
|
||||
|
||||
[menu]
|
||||
"dashboard" = "Trạng thái hệ thống"
|
||||
@@ -62,6 +70,7 @@
|
||||
"link" = "Quản lý"
|
||||
|
||||
[pages.login]
|
||||
"hello" = "Xin chào"
|
||||
"title" = "Chào mừng"
|
||||
"loginAgain" = "Thời hạn đăng nhập đã hết. Vui lòng đăng nhập lại."
|
||||
|
||||
@@ -76,7 +85,7 @@
|
||||
"title" = "Trạng thái hệ thống"
|
||||
"memory" = "Ram"
|
||||
"hard" = "Dung lượng"
|
||||
"xrayStatus" = "Trạng thái Xray"
|
||||
"xrayStatus" = "Xray"
|
||||
"stopXray" = "Dừng lại"
|
||||
"restartXray" = "Khởi động lại"
|
||||
"xraySwitch" = "Phiên bản"
|
||||
@@ -140,10 +149,8 @@
|
||||
"noRecommendKeepDefault" = "Không yêu cầu đặc biệt để giữ nguyên cài đặt mặc định"
|
||||
"certificatePath" = "Đường dẫn tập"
|
||||
"certificateContent" = "Nội dung tập"
|
||||
"publicKeyPath" = "Đường dẫn khóa công khai"
|
||||
"publicKeyContent" = "Nội dung khóa công khai"
|
||||
"keyPath" = "Đường dẫn khóa riêng tư"
|
||||
"keyContent" = "Nội dung khóa riêng tư"
|
||||
"publicKey" = "Khóa công khai"
|
||||
"privatekey" = "Khóa cá nhân"
|
||||
"clickOnQRcode" = "Nhấn vào Mã QR để sao chép"
|
||||
"client" = "Người dùng"
|
||||
"export" = "Xuất liên kết"
|
||||
@@ -194,7 +201,7 @@
|
||||
"last" = "Cuối cùng"
|
||||
"prefix" = "Tiền tố"
|
||||
"postfix" = "Hậu tố"
|
||||
"delayedStart" = "Bắt đầu sau khi sử dụng lần đầu"
|
||||
"delayedStart" = "Bắt đầu ở Lần Đầu"
|
||||
"expireDays" = "Khoảng thời gian"
|
||||
"days" = "ngày"
|
||||
"renew" = "Tự động gia hạn"
|
||||
@@ -302,6 +309,8 @@
|
||||
"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"
|
||||
"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]
|
||||
"title" = "Cài đặt Xray"
|
||||
@@ -311,6 +320,8 @@
|
||||
"advancedTemplate" = "Mẫu Nâng cao"
|
||||
"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."
|
||||
"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"
|
||||
"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"
|
||||
@@ -375,23 +386,36 @@
|
||||
"GoogleIPv4Desc" = "Thêm định tuyến cho Google để kết nối qua IPv4."
|
||||
"NetflixIPv4" = "Sử dụng IPv4 cho Netflix"
|
||||
"NetflixIPv4Desc" = "Thêm định tuyến cho Netflix để kết nối qua IPv4."
|
||||
"GoogleWARP" = "Định tuyến Google qua WARP."
|
||||
"GoogleWARPDesc" = "Thêm định tuyến cho Google qua WARP."
|
||||
"OpenAIWARP" = "Định tuyến OpenAI (ChatGPT) qua WARP."
|
||||
"OpenAIWARPDesc" = "Thêm định tuyến cho OpenAI (ChatGPT) qua WARP."
|
||||
"NetflixWARP" = "Định tuyến Netflix qua WARP."
|
||||
"NetflixWARPDesc" = "Thêm định tuyến cho Netflix qua WARP."
|
||||
"SpotifyWARP" = "Định tuyến Spotify qua WARP."
|
||||
"SpotifyWARPDesc" = "Thêm định tuyến cho Spotify qua WARP."
|
||||
"GoogleWARP" = "Google"
|
||||
"GoogleWARPDesc" = "Định tuyến lưu lượng truy cập tới Google thông qua WARP."
|
||||
"OpenAIWARP" = "OpenAI (ChatGPT)"
|
||||
"OpenAIWARPDesc" = "Định tuyến lưu lượng truy cập tới OpenAI (ChatGPT) thông qua WARP."
|
||||
"NetflixWARP" = "Netflix"
|
||||
"NetflixWARPDesc" = "Định tuyến lưu lượng truy cập tới Netflix thông qua WARP."
|
||||
"MetaWARP" = "Meta"
|
||||
"MetaWARPDesc" = "Định tuyến lưu lượng truy cập tới Meta (Instagram, Facebook, WhatsApp, Threads,...) thông 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."
|
||||
"IRWARPDesc" = "Thêm định tuyến cho các tên miền của Iran qua WARP."
|
||||
"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ể."
|
||||
"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."
|
||||
"Routings" = "Quy tắc định tuyến"
|
||||
"RoutingsDesc" = "Mức độ ưu tiên của mỗi quy tắc đều quan trọng!"
|
||||
"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]
|
||||
"first" = "Đầu tiên"
|
||||
@@ -402,6 +426,7 @@
|
||||
"dest" = "Đích"
|
||||
"inbound" = "Vào"
|
||||
"outbound" = "Ra"
|
||||
"balancer" = "Cân bằng"
|
||||
"info" = "Thông tin"
|
||||
"add" = "Thêm quy tắc"
|
||||
"edit" = "Chỉnh sửa quy tắc"
|
||||
@@ -422,6 +447,15 @@
|
||||
"portal" = "Cổng thông tin"
|
||||
"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]
|
||||
"secretKey" = "Khoá bí mật"
|
||||
"publicKey" = "Khóa công khai"
|
||||
@@ -430,6 +464,21 @@
|
||||
"psk" = "Khóa chia sẻ"
|
||||
"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]
|
||||
"admin" = "Quản trị viên"
|
||||
"secret" = "Mã thông báo bí mật"
|
||||
|
||||
@@ -42,7 +42,7 @@
|
||||
"online" = "在线"
|
||||
"domainName" = "域名"
|
||||
"monitor" = "监听"
|
||||
"certificate" = "证书"
|
||||
"certificate" = "数字证书"
|
||||
"fail" = "失败"
|
||||
"success" = "成功"
|
||||
"getVersion" = "获取版本"
|
||||
@@ -52,6 +52,14 @@
|
||||
"secretToken" = "安全密钥"
|
||||
"remained" = "剩余"
|
||||
"security" = "安全"
|
||||
"secAlertTitle" = "安全警报"
|
||||
"secAlertSsl" = "此连接不安全;在激活 TLS 进行数据保护之前,请勿输入敏感信息"
|
||||
"secAlertConf" = "某些设置容易受到攻击。建议加强安全协议以防止潜在的违规行为。"
|
||||
"secAlertSSL" = "面板缺乏安全连接。请安装 TLS 证书以保护数据。"
|
||||
"secAlertPanelPort" = "面板默认端口存在漏洞。请配置随机或特定端口。"
|
||||
"secAlertPanelURI" = "面板默认 URI 路径不安全。请配置复杂的 URI 路径。"
|
||||
"secAlertSubURI" = "订阅默认 URI 路径不安全。请配置复杂的 URI 路径。"
|
||||
"secAlertSubJsonURI" = "订阅 JSON 默认 URI 路径不安全。请配置复杂的 URI 路径。"
|
||||
|
||||
[menu]
|
||||
"dashboard" = "系统状态"
|
||||
@@ -62,6 +70,7 @@
|
||||
"link" = "管理"
|
||||
|
||||
[pages.login]
|
||||
"hello" = "你好"
|
||||
"title" = "欢迎"
|
||||
"loginAgain" = "登录时效已过,请重新登录"
|
||||
|
||||
@@ -76,7 +85,7 @@
|
||||
"title" = "系统状态"
|
||||
"memory" = "内存"
|
||||
"hard" = "硬盘"
|
||||
"xrayStatus" = "状态"
|
||||
"xrayStatus" = "Xray"
|
||||
"stopXray" = "停止"
|
||||
"restartXray" = "重启"
|
||||
"xraySwitch" = "版本"
|
||||
@@ -140,10 +149,8 @@
|
||||
"noRecommendKeepDefault" = "没有特殊需求保持默认即可"
|
||||
"certificatePath" = "文件路径"
|
||||
"certificateContent" = "文件内容"
|
||||
"publicKeyPath" = "公钥文件路径"
|
||||
"publicKeyContent" = "公钥内容"
|
||||
"keyPath" = "密钥文件路径"
|
||||
"keyContent" = "密钥内容"
|
||||
"publicKey" = "公钥"
|
||||
"privatekey" = "私钥"
|
||||
"clickOnQRcode" = "点击二维码复制"
|
||||
"client" = "客户"
|
||||
"export" = "导出链接"
|
||||
@@ -302,6 +309,8 @@
|
||||
"subShowInfoDesc" = "在配置名称后显示剩余流量和日期"
|
||||
"subURI" = "反向代理 URI"
|
||||
"subURIDesc" = "更改订阅 URL 的基本 URI 以在代理后面使用"
|
||||
"fragment" = "碎片"
|
||||
"fragmentDesc" = "启用 TLS hello 数据包分段"
|
||||
|
||||
[pages.xray]
|
||||
"title" = "Xray 设置"
|
||||
@@ -311,6 +320,8 @@
|
||||
"advancedTemplate" = "高级模板部件"
|
||||
"generalConfigs" = "通用配置"
|
||||
"generalConfigsDesc" = "这些选项将提供一般调整"
|
||||
"logConfigs"="日志"
|
||||
"logConfigsDesc" = "日志可能会影响您服务器的效率。建议仅在您需要时明智地启用它"
|
||||
"blockConfigs" = "阻塞配置"
|
||||
"blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站"
|
||||
"blockCountryConfigs" = "阻止国家配置"
|
||||
@@ -375,23 +386,36 @@
|
||||
"GoogleIPv4Desc" = "添加谷歌连接IPv4的路由"
|
||||
"NetflixIPv4" = "为 Netflix 使用 IPv4"
|
||||
"NetflixIPv4Desc" = "添加Netflix连接IPv4的路由"
|
||||
"GoogleWARP" = "将谷歌路由到 WARP"
|
||||
"GoogleWARPDesc" = "为谷歌添加路由到WARP"
|
||||
"OpenAIWARP" = "将 OpenAI (ChatGPT) 路由到 WARP"
|
||||
"OpenAIWARPDesc" = "将OpenAI(ChatGPT)路由添加到WARP"
|
||||
"NetflixWARP" = "将 Netflix 路由到 WARP"
|
||||
"NetflixWARPDesc" = "为Netflix添加路由到WARP"
|
||||
"SpotifyWARP" = "将 Spotify 路由到 WARP"
|
||||
"SpotifyWARPDesc" = "为Spotify添加路由到WARP"
|
||||
"GoogleWARP" = "Google"
|
||||
"GoogleWARPDesc" = "通过 WARP 将流量路由到 Google。"
|
||||
"OpenAIWARP" = "OpenAI (ChatGPT)"
|
||||
"OpenAIWARPDesc" = "通过 WARP 将流量路由到 OpenAI (ChatGPT)。"
|
||||
"NetflixWARP" = "Netflix"
|
||||
"NetflixWARPDesc" = "通过 WARP 将流量路由到 Netflix。"
|
||||
"MetaWARP"="Meta"
|
||||
"MetaWARPDesc" = "通过 WARP 将流量路由到 Meta(Instagram、Facebook、WhatsApp、Threads...)"
|
||||
"AppleWARP" = "Apple"
|
||||
"AppleWARPDesc" = "通过 WARP 将流量路由到 Apple。"
|
||||
"RedditWARP" = "Reddit"
|
||||
"RedditWARPDesc" = "通过 WARP 将流量路由到 Reddit。"
|
||||
"SpotifyWARP" = "Spotify"
|
||||
"SpotifyWARPDesc" = "通过 WARP 将流量路由到 Spotify。"
|
||||
"IRWARP" = "将伊朗域名路由到 WARP"
|
||||
"IRWARPDesc" = "将伊朗域的路由添加到 WARP。 重启面板生效"
|
||||
"Inbounds" = "入站"
|
||||
"InboundsDesc" = "更改配置模板接受特殊客户端"
|
||||
"Outbounds" = "出站"
|
||||
"Balancers" = "平衡器"
|
||||
"OutboundsDesc" = "更改配置模板定义此服务器的传出方式"
|
||||
"Routings" = "路由规则"
|
||||
"RoutingsDesc" = "每条规则的优先级都很重要"
|
||||
"completeTemplate" = "全部"
|
||||
"logLevel" = "日志级别"
|
||||
"logLevelDesc" = "错误日志的日志级别,表示需要记录的信息。"
|
||||
"accessLog" = "访问日志"
|
||||
"accessLogDesc" = "访问日志的文件路径。 特殊值“none”禁用访问日志"
|
||||
"errorLog" = "错误日志"
|
||||
"errorLogDesc" = "错误日志的文件路径。 特殊值“none”禁用错误日志"
|
||||
|
||||
[pages.xray.rules]
|
||||
"first" = "第一个"
|
||||
@@ -402,6 +426,7 @@
|
||||
"dest" = "目的地"
|
||||
"inbound" = "入站"
|
||||
"outbound" = "出站"
|
||||
"balancer" = "平衡器"
|
||||
"info" = "信息"
|
||||
"add" = "添加规则"
|
||||
"edit" = "编辑规则"
|
||||
@@ -422,6 +447,15 @@
|
||||
"portal" = "门户"
|
||||
"intercon" = "互连"
|
||||
|
||||
[pages.xray.balancer]
|
||||
"addBalancer" = "添加平衡器"
|
||||
"editBalancer" = "编辑平衡器"
|
||||
"balancerStrategy" = "战略"
|
||||
"balancerSelectors" = "选择器"
|
||||
"tag" = "标签"
|
||||
"tagDesc" = "唯一标记"
|
||||
"balancerDesc" = "不能同时使用balancerTag和outboundTag。 如果同时使用,则只有outboundTag起作用。"
|
||||
|
||||
[pages.xray.wireguard]
|
||||
"secretKey" = "密钥"
|
||||
"publicKey" = "公钥"
|
||||
@@ -430,6 +464,21 @@
|
||||
"psk" = "共享密钥"
|
||||
"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]
|
||||
"admin" = "管理员"
|
||||
"secret" = "密钥"
|
||||
|
||||
22
web/web.go
22
web/web.go
@@ -337,19 +337,17 @@ func (s *Server) Start() (err error) {
|
||||
}
|
||||
if certFile != "" || keyFile != "" {
|
||||
cert, err := tls.LoadX509KeyPair(certFile, keyFile)
|
||||
if err != nil {
|
||||
listener.Close()
|
||||
return err
|
||||
if err == nil {
|
||||
c := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
listener = network.NewAutoHttpsListener(listener)
|
||||
listener = tls.NewListener(listener, c)
|
||||
logger.Info("web server run https on", listener.Addr())
|
||||
} else {
|
||||
logger.Error("error in loading certificates: ", err)
|
||||
logger.Info("web server run http on", listener.Addr())
|
||||
}
|
||||
c := &tls.Config{
|
||||
Certificates: []tls.Certificate{cert},
|
||||
}
|
||||
listener = network.NewAutoHttpsListener(listener)
|
||||
listener = tls.NewListener(listener, c)
|
||||
}
|
||||
|
||||
if certFile != "" || keyFile != "" {
|
||||
logger.Info("web server run https on", listener.Addr())
|
||||
} else {
|
||||
logger.Info("web server run http on", listener.Addr())
|
||||
}
|
||||
|
||||
138
x-ui.sh
138
x-ui.sh
@@ -150,6 +150,12 @@ custom_version() {
|
||||
eval $install_command
|
||||
}
|
||||
|
||||
# Function to handle the deletion of the script file
|
||||
delete_script() {
|
||||
rm "$0" # Remove the script file itself
|
||||
exit 1
|
||||
}
|
||||
|
||||
uninstall() {
|
||||
confirm "Are you sure you want to uninstall the panel? xray will also uninstalled!" "n"
|
||||
if [[ $? != 0 ]]; then
|
||||
@@ -167,12 +173,13 @@ uninstall() {
|
||||
rm /usr/local/x-ui/ -rf
|
||||
|
||||
echo ""
|
||||
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 -e "Uninstalled Successfully.\n"
|
||||
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 ""
|
||||
|
||||
if [[ $# == 0 ]]; then
|
||||
before_show_menu
|
||||
fi
|
||||
# Trap the SIGTERM signal
|
||||
trap delete_script SIGTERM
|
||||
delete_script
|
||||
}
|
||||
|
||||
reset_user() {
|
||||
@@ -338,6 +345,47 @@ show_banlog() {
|
||||
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() {
|
||||
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}"
|
||||
@@ -483,6 +531,33 @@ show_xray_status() {
|
||||
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() {
|
||||
if ! command -v ufw &>/dev/null; then
|
||||
echo "ufw firewall is not installed. Installing now..."
|
||||
@@ -535,6 +610,37 @@ open_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() {
|
||||
local defaultBinFolder="/usr/local/x-ui/bin"
|
||||
read -p "Please enter x-ui bin folder path. Leave blank for default. (Default: '${defaultBinFolder}')" binFolder
|
||||
@@ -841,8 +947,8 @@ run_speedtest() {
|
||||
}
|
||||
|
||||
create_iplimit_jails() {
|
||||
# Use default bantime if not passed => 30 minutes
|
||||
local bantime="${1:-30}"
|
||||
# Use default bantime if not passed => 15 minutes
|
||||
local bantime="${1:-15}"
|
||||
|
||||
# Uncomment 'allowipv6 = auto' in fail2ban.conf
|
||||
sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf
|
||||
@@ -853,8 +959,8 @@ enabled=true
|
||||
filter=3x-ipl
|
||||
action=3x-ipl
|
||||
logpath=${iplimit_log_path}
|
||||
maxretry=4
|
||||
findtime=60
|
||||
maxretry=2
|
||||
findtime=32
|
||||
bantime=${bantime}m
|
||||
EOF
|
||||
|
||||
@@ -867,7 +973,7 @@ EOF
|
||||
|
||||
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
|
||||
[INCLUDES]
|
||||
before = iptables-common.conf
|
||||
before = iptables-allports.conf
|
||||
|
||||
[Definition]
|
||||
actionstart = <iptables> -N f2b-<name>
|
||||
@@ -1124,10 +1230,10 @@ show_menu() {
|
||||
${green}17.${plain} Cloudflare SSL Certificate
|
||||
${green}18.${plain} IP Limit Management
|
||||
${green}19.${plain} WARP Management
|
||||
${green}20.${plain} Firewall Management
|
||||
————————————————
|
||||
${green}20.${plain} Enable BBR
|
||||
${green}21.${plain} Update Geo Files
|
||||
${green}22.${plain} Active Firewall and open ports
|
||||
${green}21.${plain} Enable BBR
|
||||
${green}22.${plain} Update Geo Files
|
||||
${green}23.${plain} Speedtest by Ookla
|
||||
"
|
||||
show_status
|
||||
@@ -1195,13 +1301,13 @@ show_menu() {
|
||||
warp_cloudflare
|
||||
;;
|
||||
20)
|
||||
enable_bbr
|
||||
firewall_menu
|
||||
;;
|
||||
21)
|
||||
update_geo
|
||||
bbr_menu
|
||||
;;
|
||||
22)
|
||||
open_ports
|
||||
update_geo
|
||||
;;
|
||||
23)
|
||||
run_speedtest
|
||||
|
||||
@@ -213,6 +213,7 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
||||
continue
|
||||
}
|
||||
isInbound := matchs[1] == "inbound"
|
||||
isOutbound := matchs[1] == "outbound"
|
||||
tag := matchs[2]
|
||||
isDown := matchs[3] == "downlink"
|
||||
if tag == "api" {
|
||||
@@ -221,8 +222,9 @@ func (x *XrayAPI) GetTraffic(reset bool) ([]*Traffic, []*ClientTraffic, error) {
|
||||
traffic, ok := tagTrafficMap[tag]
|
||||
if !ok {
|
||||
traffic = &Traffic{
|
||||
IsInbound: isInbound,
|
||||
Tag: tag,
|
||||
IsInbound: isInbound,
|
||||
IsOutbound: isOutbound,
|
||||
Tag: tag,
|
||||
}
|
||||
tagTrafficMap[tag] = traffic
|
||||
traffics = append(traffics, traffic)
|
||||
|
||||
@@ -16,7 +16,8 @@ type Config struct {
|
||||
API json_util.RawMessage `json:"api"`
|
||||
Stats json_util.RawMessage `json:"stats"`
|
||||
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 {
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package xray
|
||||
|
||||
import (
|
||||
"regexp"
|
||||
"strings"
|
||||
"x-ui/logger"
|
||||
)
|
||||
@@ -14,37 +15,29 @@ type LogWriter struct {
|
||||
}
|
||||
|
||||
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
|
||||
message := strings.TrimSpace(string(m))
|
||||
messages := strings.Split(message, "\n")
|
||||
lw.lastLine = messages[len(messages)-1]
|
||||
|
||||
for _, msg := range messages {
|
||||
messageBody := msg
|
||||
matches := regex.FindStringSubmatch(msg)
|
||||
|
||||
// Remove timestamp
|
||||
splittedMsg := strings.SplitN(msg, " ", 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:])
|
||||
if len(matches) > 3 {
|
||||
level := matches[2]
|
||||
msgBody := matches[3]
|
||||
|
||||
// Map the level to the appropriate logger function
|
||||
switch level {
|
||||
case "Debug":
|
||||
logger.Debug(msgBody)
|
||||
logger.Debug("XRAY: " + msgBody)
|
||||
case "Info":
|
||||
logger.Info(msgBody)
|
||||
logger.Info("XRAY: " + msgBody)
|
||||
case "Warning":
|
||||
logger.Warning(msgBody)
|
||||
logger.Warning("XRAY: " + msgBody)
|
||||
case "Error":
|
||||
logger.Error(msgBody)
|
||||
logger.Error("XRAY: " + msgBody)
|
||||
default:
|
||||
logger.Debug("XRAY: " + msg)
|
||||
}
|
||||
|
||||
@@ -41,10 +41,6 @@ func GetIPLimitLogPath() string {
|
||||
return config.GetLogFolder() + "/3xipl.log"
|
||||
}
|
||||
|
||||
func GetIPLimitPrevLogPath() string {
|
||||
return config.GetLogFolder() + "/3xipl.prev.log"
|
||||
}
|
||||
|
||||
func GetIPLimitBannedLogPath() string {
|
||||
return config.GetLogFolder() + "/3xipl-banned.log"
|
||||
}
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
package xray
|
||||
|
||||
type Traffic struct {
|
||||
IsInbound bool
|
||||
Tag string
|
||||
Up int64
|
||||
Down int64
|
||||
IsInbound bool
|
||||
IsOutbound bool
|
||||
Tag string
|
||||
Up int64
|
||||
Down int64
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user