Merge branch 'MHSanaei:main' into main

This commit is contained in:
emirjorge 2024-03-05 02:56:28 -05:00 committed by GitHub
commit 4136d37faf
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
87 changed files with 3570 additions and 941 deletions

View file

@ -11,15 +11,15 @@ jobs:
steps:
- name: Check out the code
uses: actions/checkout@v4.1.1
uses: actions/checkout@v4
- name: Set up QEMU
uses: docker/setup-qemu-action@v3.0.0
uses: docker/setup-qemu-action@v3
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3.0.0
uses: docker/setup-buildx-action@v3
- name: Login to GHCR
uses: docker/login-action@v3.0.0
uses: docker/login-action@v3
with:
registry: ghcr.io
username: ${{ github.actor }}
@ -27,12 +27,12 @@ jobs:
- name: Docker meta
id: meta
uses: docker/metadata-action@v5.5.1
uses: docker/metadata-action@v5
with:
images: ghcr.io/${{ github.repository }}
- name: Build and push Docker image
uses: docker/build-push-action@v5.1.0
uses: docker/build-push-action@v5
with:
context: .
push: ${{ github.event_name != 'pull_request' }}

View file

@ -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

View file

@ -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}"

View file

@ -1,7 +1,7 @@
# ========================================================
# Stage: Builder
# ========================================================
FROM golang:1.21-alpine AS builder
FROM golang:1.22-alpine AS builder
WORKDIR /app
ARG TARGETARCH

View file

@ -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://img.shields.io/github/v/release/mhsanaei/3x-ui.svg)](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,10 +26,10 @@ 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.3`:
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.3
bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.sh) v2.2.1
```
## SSL Certificate
@ -69,7 +70,17 @@ certbot renew --dry-run
```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
```
@ -77,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
@ -164,21 +184,25 @@ 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.
- **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)
- **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>
## Languages
@ -188,6 +212,8 @@ Supports a variety of different architectures and devices. Here are some of the
- Russian
- Vietnamese
- Spanish
- Indonesian
- Ukrainian
## Features

View file

@ -1 +1 @@
2.1.3
2.2.1

29
go.mod
View file

@ -1,6 +1,6 @@
module x-ui
go 1.21.4
go 1.22.0
require (
github.com/Calidity/gin-sessions v1.3.1
@ -14,10 +14,10 @@ require (
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v3 v3.24.1
github.com/valyala/fasthttp v1.52.0
github.com/xtls/xray-core v1.8.7
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.1
google.golang.org/grpc v1.62.0
gorm.io/driver/sqlite v1.5.5
gorm.io/gorm v1.25.7
)
@ -40,7 +40,7 @@ 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
@ -49,23 +49,22 @@ require (
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.6 // 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
@ -85,15 +84,15 @@ require (
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.6.0 // indirect
golang.org/x/crypto v0.19.0 // indirect
golang.org/x/exp v0.0.0-20240103183307-be819d1f06fc // indirect
golang.org/x/mod v0.14.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-20240102182953-50ed04b92917 // 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

66
go.sum
View file

@ -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=
@ -138,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.6 h1:60eq2E/jlfwQXtvZEeBUYADs+BwKBWURIY+Gj2eRGjI=
github.com/klauspost/compress v1.17.6/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=
@ -169,8 +169,8 @@ 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=
@ -183,10 +183,10 @@ github.com/neelance/astrewrite v0.0.0-20160511093645-99348263ae86/go.mod h1:kHJE
github.com/neelance/sourcemap v0.0.0-20151028013722-8c68805598ab/go.mod h1:Qr6/a/Q4r9LP1IltGz7tA7iOK1WonHEYhu1HRBA7ZiM=
github.com/nicksnyder/go-i18n/v2 v2.4.0 h1:3IcvPOAvnCKwNm0TB0dLDTuawWEj+ax/RERNC+diLMM=
github.com/nicksnyder/go-i18n/v2 v2.4.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/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=
@ -208,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=
@ -223,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=
@ -303,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=
@ -326,13 +324,13 @@ golang.org/x/crypto v0.0.0-20210711020723-a769d52b0f97/go.mod h1:GvvjBRRGRdwPK5y
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=
@ -392,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=
@ -411,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-20240102182953-50ed04b92917 h1:6G8oQ016D88m1xAKljMlBOOGWDZkes4kMhgGFlf8WcQ=
google.golang.org/genproto/googleapis/rpc v0.0.0-20240102182953-50ed04b92917/go.mod h1:xtjpI3tXFPP051KaWnhvxkiubL/6dJ18vLVf7q2pTOU=
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.1 h1:kLAiWrZs7YeDM6MumDe7m3y4aM6wacLzM1Y/wiLP9XY=
google.golang.org/grpc v1.61.1/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=

View file

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

BIN
media/3X-UI.png Normal file

Binary file not shown.

After

Width:  |  Height:  |  Size: 198 KiB

105
sub/default.json Normal file
View 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": {}
}

View file

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

View file

@ -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
View 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"`
}

View file

@ -25,21 +25,24 @@ 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"
@ -47,25 +50,17 @@ func (s *SubService) GetSubs(subId string, host string, showInfo bool) ([]string
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 {

View file

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

View file

@ -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 {

View file

@ -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; }

View file

@ -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,36 @@ 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 +806,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 +874,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 +897,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 +914,7 @@ style attribute {
}
.dark .has-success .anticon {
color: #61bf39;
color: var(--color-primary-100);
animation-name: diffZoomIn1 !important;
}
@ -874,19 +960,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 +1006,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 +1015,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 +1043,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 +1051,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 +1063,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 +1087,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 +1101,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 +1115,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,12 +1125,13 @@ 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 {
@ -1099,11 +1187,10 @@ li.ant-select-dropdown-menu-item:empty:after {
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,
@ -1116,6 +1203,8 @@ li.ant-select-dropdown-menu-item:empty:after {
}
.qr-bg {
width: 100%;
height: 100%;
background-color: #fff;
display: flex;
justify-content: center;
@ -1124,7 +1213,7 @@ 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;
}
@ -1135,3 +1224,36 @@ li.ant-select-dropdown-menu-item:empty:after {
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);
}
.ant-empty-small {
margin: 4px 0;
background-color: transparent !important;
}
.ant-empty-small .ant-empty-image {
height: 20px;
}

Binary file not shown.

Before

Width:  |  Height:  |  Size: 15 KiB

View file

@ -34,6 +34,11 @@ const supportLangs = [
value: 'id-ID',
icon: '🇮🇩',
},
{
name: 'Український',
value: 'uk-UA',
icon: '🇺🇦',
},
];
function getLang() {

View file

@ -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,
};

View file

@ -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";

View file

@ -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,

View file

@ -1,39 +0,0 @@
var self = this;
var filesToCache = [
'/'
];
self.addEventListener('install', function (e) {
e.waitUntil(
caches.open('3xPanel').then(function (cache) {
return cache.addAll(filesToCache);
})
);
});
self.addEventListener('activate', function (event) {
event.waitUntil(
caches.keys().then(function (cacheNames) {
return Promise.all(
cacheNames.filter(function (cacheName) {
}).map(function (cacheName) {
return caches.delete(cacheName);
})
);
})
);
});
self.addEventListener('fetch', function (event) {
event.respondWith(
caches.open('mysite-dynamic').then(function (cache) {
return cache.match(event.request).then(function (response) {
return response || fetch(event.request).then(function (response) {
cache.put(event.request, response.clone());
return response;
});
});
})
);
});
console.clear();

View file

@ -1,41 +0,0 @@
{
"name": "3x-UI Panel",
"short_name": "3xPanel",
"description": "3x-ui panel converted to PWA",
"start_url": "/",
"background_color": "#F4F4F4",
"display": "fullscreen",
"theme_color": "#293343",
"icons": [
{
"src": "./assets/icons/16.png",
"sizes": "16x16",
"type": "image/png"
},
{
"src": "./assets/icons/24.png",
"sizes": "24x24",
"type": "image/png"
},
{
"src": "./assets/icons/32.png",
"sizes": "32x32",
"type": "image/png"
},
{
"src": "./assets/icons/64.png",
"sizes": "64x64",
"type": "image/png"
},
{
"src": "./assets/icons/192.png",
"sizes": "192x192",
"type": "image/png"
},
{
"src": "./assets/icons/512.png",
"sizes": "512x512",
"type": "image/png"
}
]
}

View file

@ -33,7 +33,8 @@ jdp-container :before {
}
jdp-container .jdp-icon-minus,
jdp-container .jdp-icon-plus {
border: 1px solid rgb(232 244 242);
outline: 1px solid rgb(232 244 242);
outline-offset: -1px;
border-radius: 6px;
cursor: pointer;
display: flex;
@ -205,15 +206,15 @@ jdp-container .jdp-day-name {
}
jdp-container .jdp-day-name.today,
jdp-container .jdp-day.today {
border-color: #008771;
color: #008771;
border-color: var(--color-primary-100);
color: var(--color-primary-100);
font-weight: 700;
}
.dark jdp-container .jdp-day-name.selected,
.dark jdp-container .jdp-day.selected,
jdp-container .jdp-day-name.selected,
jdp-container .jdp-day.selected {
background-color: #008771 !important;
background-color: var(--color-primary-100) !important;
color: #fff !important;
opacity: 1 !important;
}
@ -266,7 +267,7 @@ jdp-container .jdp-btn-empty,
jdp-container .jdp-btn-today {
background: #00877000;
border-radius: 5px;
color: #008771;
color: var(--color-primary-100);
cursor: pointer;
display: inline-block;
font-size: 90%;
@ -368,26 +369,26 @@ jdp-container .jdp-time-container.jdp-only-time .jdp-time:after {
}
.dark jdp-container .jdp-days {
border-color: #313f5a;
border-color: var(--dark-color-surface-400);
}
.dark jdp-overlay {
background-color: #181f2c;
}
.dark jdp-container {
background: #101828;
background: var(--dark-color-background);
border-color: #2c3950;
box-shadow: 0 2px 8px rgba(0,0,0,.15);
color: #fff;
}
.dark jdp-container .jdp-icon-minus,
.dark jdp-container .jdp-icon-plus {
border-color: #313f5a;
outline-color: var(--dark-color-surface-600);
}
.dark jdp-container .jdp-icon-minus:hover,
.dark jdp-container .jdp-icon-plus:hover {
background-color: #313f5a;
background-color: var(--dark-color-surface-600);
}
.dark jdp-container .jdp-months,
@ -404,27 +405,27 @@ jdp-container .jdp-time-container.jdp-only-time .jdp-time:after {
.dark jdp-container .jdp-year,
.dark jdp-container .jdp-year input,
.dark jdp-container .jdp-year select {
background: #101828;
color: rgb(255 255 255 / 65%);
background: var(--dark-color-background);
color: var(--dark-color-text-primary);
}
.dark jdp-container .jdp-day,
.dark jdp-container .jdp-day-name {
border: 1px solid transparent;
color: rgba(255, 255, 255, 0.65);
color: var(--dark-color-text-primary);
}
.dark jdp-container .jdp-day-name.today,
.dark jdp-container .jdp-day.today {
border-color: #008771;
border-color: var(--color-primary-100);
}
.dark jdp-container .jdp-day.disabled-day {
opacity: 0.15;
}
.dark jdp-container .jdp-day:not(.disabled-day):hover {
background-color: #313f5a;
background-color: var(--dark-color-surface-600);
color: #fff;
}
.dark jdp-container .jdp-footer {
border-color: #313f5a;
border-color: var(--dark-color-surface-400);
}
.dark jdp-container .jdp-btn-close,
.dark jdp-container .jdp-btn-empty,
@ -445,10 +446,10 @@ jdp-container .jdp-time-container.jdp-only-time .jdp-time:after {
}
.dark jdp-container .jdp-time-container .jdp-time select:hover {
background-color: #313f5a;
background-color: var(--dark-color-surface-600);
color: #fff;
}
.dark jdp-container .jdp-time-container .jdp-time select {
border: 1px solid rgb(49 63 90);
border: 1px solid var(--dark-color-surface-600);
}

View file

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

View file

@ -81,7 +81,6 @@ 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)
}

View file

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

View file

@ -7,18 +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">
<link rel="manifest" href="{{ .base_path }}assets/manifest.json">
<script>
if ('serviceWorker' in navigator) {
navigator.serviceWorker.register('{{ .base_path }}assets/js/sw.js')
.then(function () {
console.log('Service Worker Registered');
});
}
</script>
<style>
[v-cloak] {
display: none;
@ -40,4 +28,5 @@
</style>
<title>{{ .host }}-{{ i18n .title}}</title>
</head>
<div id="message"></div>
{{end}}

View file

@ -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({

View file

@ -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 = [];
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);

View file

@ -1,14 +1,15 @@
{{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;"
: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">
{{ i18n "download" }} [[ txtModal.fileName ]]
:download="txtModal.fileName">[[ txtModal.fileName ]]
</a-button>
<a-input type="textarea" v-model="txtModal.content"
<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>
@ -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', () => {

View file

@ -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: 600;
}
.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;
@ -212,7 +222,7 @@
animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
}
.dark .parallax > use {
fill: #0f2d32;
fill: var(--dark-color-login-wave);
}
.parallax > use:nth-child(1) {
animation-delay: -2s;
@ -247,6 +257,123 @@
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">
@ -254,7 +381,8 @@
<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">
<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>
@ -269,8 +397,13 @@
<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 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">
@ -279,24 +412,28 @@
<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 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">
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">
placeholder='{{ i18n "secretToken" }}'
@keydown.enter.native="login">
</password-input>
</a-input>
</a-form-item>
<a-form-item>
<a-row justify="center" class="centered">
<div class="wave-btn-bg wave-btn-bg-cl" :style="loading ? { width: '52px' } : { display: 'inline-block' }">
<a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login" :icon="loading ? 'poweroff' : undefined">
<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>
@ -305,7 +442,9 @@
<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 ref="selectLang" v-model="lang"
@change="setLang(lang)" style="width: 150px;"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l.value" label="English" v-for="l in supportLangs">
<span role="img" aria-label="l.name" v-text="l.icon"></span>
&nbsp;&nbsp;<span v-text="l.name"></span>
@ -320,7 +459,12 @@
<a-icon type="bulb" :theme="themeSwitcher.isDarkTheme ? 'filled' : 'outlined'"></a-icon>&nbsp;
</a-col>
<a-col>
<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-col>
</a-row>
</a-form-item>
@ -377,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>

View file

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

View file

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

View file

@ -15,10 +15,6 @@
<a-icon type="tool"></a-icon>
<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><b>{{ i18n "menu.logout"}}</b></span>
@ -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 }}']"

View file

@ -0,0 +1,237 @@
{{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;
}
const hideDragImage = this.$el.cloneNode(true);
hideDragImage.id = "hideDragImage-hide";
hideDragImage.style.opacity = 0;
document.body.appendChild(hideDragImage);
e.dataTransfer.setDragImage(hideDragImage, 0, 0);
},
dragStopHandler(e, index) {
const hideDragImage = document.getElementById('hideDragImage-hide');
if (hideDragImage) hideDragImage.remove();
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: #ffffff !important;
}
.dark .ant-table-is-sorting .draggable-row td {
background-color: var(--dark-color-surface-100) !important;
}
.ant-table-is-sorting .dragging td {
background-color: rgb(232 244 242) !important;
color: rgba(0, 0, 0, 0.3);
}
.dark .ant-table-is-sorting .dragging td {
background-color: var(--dark-color-table-hover) !important;
color: rgba(255, 255, 255, 0.3);
}
.ant-table-is-sorting .dragging {
opacity: 1;
box-shadow: 1px -2px 2px #008771;
transition: all 0.2s;
}
.ant-table-is-sorting .dragging .ant-table-row-index {
opacity: 0.3;
}
</style>
{{end}}

View file

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

View 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}}

View 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}}

View file

@ -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>
@ -114,7 +114,7 @@
</template>
<!-- sniffing -->
<template v-if="inbound.canSniffing()">
<template>
{{template "form/sniffing"}}
</template>
{{end}}

View file

@ -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,7 +193,6 @@
<a-input v-model.trim="outbound.settings.pass"></a-input>
</a-form-item>
</template>
<!-- trojan/shadowsocks -->
<template v-if="[Protocols.Trojan, Protocols.Shadowsocks].includes(outbound.protocol)">
<a-form-item label='{{ i18n "password" }}'>
@ -369,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>
@ -391,7 +373,8 @@
<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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -19,19 +19,15 @@
</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)"
@ -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}}

View file

@ -21,19 +21,15 @@
</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)"

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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>

View file

@ -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=" ">

View file

@ -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>

View file

@ -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) : [];
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;
}
};

View file

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

View file

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

View file

@ -10,21 +10,11 @@
margin-inline: 0.3rem;
}
}
.ant-col-sm-24 {
margin-top: 10px;
}
.ant-card-dark h2 {
color: hsla(0, 0%, 100%, .65);
}
.ant-tag-df {
color: rgb(0 0 0 / 80%);
}
.dark .ant-tag-df {
color: rgb(255 255 255 / 80%);
color: var(--dark-color-text-primary);
}
</style>
@ -34,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>
@ -93,8 +92,8 @@
<a-col :sm="24" :lg="12">
<a-card hoverable>
<b>{{ i18n "pages.index.operationHours" }}:</b>
<a-tag class="ant-tag-df">Xray [[ formatSecond(status.appStats.uptime) ]]</a-tag>
<a-tag class="ant-tag-df">OS [[ formatSecond(status.uptime) ]]</a-tag>
<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" :lg="12">
@ -128,7 +127,7 @@
<a-col :sm="24" :lg="12">
<a-card hoverable>
<b>{{ i18n "pages.index.systemLoad" }}:</b>
<a-tag class="ant-tag-df">
<a-tag color="green">
<a-tooltip>
[[ status.loads[0] ]] | [[ status.loads[1] ]] | [[ status.loads[2] ]]
<template slot="title">
@ -141,11 +140,11 @@
<a-col :sm="24" :lg="12">
<a-card hoverable>
<b>{{ i18n "usage"}}:</b>
<a-tag class="ant-tag-df">
RAM [[ sizeFormat(status.appStats.mem) ]]
<a-tag color="green">
RAM: [[ sizeFormat(status.appStats.mem) ]]
</a-tag>
<a-tag class="ant-tag-df">
Threads [[ status.appStats.threads ]]
<a-tag color="green">
Threads: [[ status.appStats.threads ]]
</a-tag>
</a-card>
</a-col>
@ -153,7 +152,7 @@
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-tag class="ant-tag-df">
<a-tag>
<a-tooltip>
<a-icon type="global"></a-icon> IPv4
<template slot="title">
@ -163,7 +162,7 @@
</a-tag>
</a-col>
<a-col :span="12">
<a-tag class="ant-tag-df">
<a-tag>
<a-tooltip>
<a-icon type="global"></a-icon> IPv6
<template slot="title">
@ -179,7 +178,7 @@
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-tag class="ant-tag-df">
<a-tag>
<a-tooltip>
<a-icon type="swap"></a-icon> TCP: [[ status.tcpCount ]]
<template slot="title">
@ -189,7 +188,7 @@
</a-tag>
</a-col>
<a-col :span="12">
<a-tag class="ant-tag-df">
<a-tag>
<a-tooltip>
<a-icon type="swap"></a-icon> UDP: [[ status.udpCount ]]
<template slot="title">
@ -205,7 +204,7 @@
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-tag class="ant-tag-df">
<a-tag>
<a-tooltip>
<a-icon type="arrow-up"></a-icon>
Up: [[ sizeFormat(status.netIO.up) ]]/s
@ -216,7 +215,7 @@
</a-tag>
</a-col>
<a-col :span="12">
<a-tag class="ant-tag-df">
<a-tag>
<a-tooltip>
<a-icon type="arrow-down"></a-icon>
Down: [[ sizeFormat(status.netIO.down) ]]/s
@ -233,7 +232,7 @@
<a-card hoverable>
<a-row>
<a-col :span="12">
<a-tag class="ant-tag-df">
<a-tag>
<a-tooltip>
<a-icon type="cloud-upload"></a-icon>
<template slot="title">
@ -243,7 +242,7 @@
</a-tag>
</a-col>
<a-col :span="12">
<a-tag class="ant-tag-df">
<a-tag>
<a-tooltip>
<a-icon type="cloud-download"></a-icon>
<template slot="title">
@ -276,45 +275,45 @@
</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-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-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 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-form-item>
<a-form-item label="SysLog">
<a-checkbox v-model="logModal.syslog" @change="openLogs()"></a-checkbox>
</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>
@ -322,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
@ -447,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 = this.logs.length > 0 ? this.formatLogs(this.logs) : "No Record...";
this.logs = logs;
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
},
formatLogs(logs) {
let formattedLogs = '';
@ -533,6 +531,7 @@
backupModal,
spinning: false,
loadingTip: '{{ i18n "loading"}}',
showAlert: false,
},
methods: {
loading(spinning, tip = '{{ i18n "loading"}}') {
@ -656,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);
}

View file

@ -75,6 +75,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="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; overflow-x: hidden;">
<a-row style="display: flex; flex-wrap: wrap; align-items: center;">
@ -85,8 +98,6 @@
</a-space>
</a-col>
<a-col :xs="24" :sm="14">
<template>
<div>
<template>
<div>
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200">
@ -97,8 +108,6 @@
>
</div>
</template>
</div>
</template>
</a-col>
</a-row>
</a-card>
@ -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>
&nbsp;&nbsp;<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,13 +476,58 @@
}
},
},
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>

View file

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

View file

@ -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" }}'
<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,33 +138,35 @@
</a-list-item>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.RoutingStrategy" }}'
<a-list-item-meta title='{{ i18n "pages.xray.RoutingStrategy" }}'
description='{{ i18n "pages.xray.RoutingStrategyDesc" }}' />
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
v-model="routingStrategy"
:dropdown-class-name="themeSwitcher.currentTheme"
<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>
</template>
</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" }}'
<a-list-item-meta title='{{ i18n "pages.xray.logLevel" }}'
description='{{ i18n "pages.xray.logLevelDesc" }}' />
</a-col>
<a-col :lg="24" :xl="12">
<template>
<a-select
v-model="setLogLevel"
:dropdown-class-name="themeSwitcher.currentTheme"
style="width: 100%">
<a-select 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>
@ -166,21 +174,30 @@
</a-row>
<a-row style="padding: 20px">
<a-col :lg="24" :xl="12">
<a-list-item-meta
title='{{ i18n "pages.xray.accessLog" }}'
<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="setAccessLog"
:dropdown-class-name="themeSwitcher.currentTheme"
style="width: 100%">
<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"}}'>
@ -261,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>
@ -270,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">
<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">
@ -373,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;">
@ -380,18 +408,25 @@
</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-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" @click="showWarp()" style="margin-bottom: 10px;">WARP</a-button>
<a-button type="primary" icon="cloud" @click="showWarp()" style="margin-bottom: 10px;">WARP</a-button>
</a-col>
<a-col :xs="12" :sm="12" :lg="12" style="text-align: right;">
<a-icon type="sync" :spin="refreshing" @click="refreshOutboundTraffic()" style="margin: 0 5px;"></a-icon>
<a-icon type="retweet" @click="resetOutboundTraffic(-1)"></a-icon>
<a-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
@ -406,6 +441,10 @@
<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" }}
@ -441,7 +480,7 @@
</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 }"
@ -467,9 +506,11 @@
</template>
</a-table>
</a-tab-pane>
<a-tab-pane key="tpl-balancers" tab='{{ i18n "pages.xray.Balancers"}}' style="padding-top: 20px;" force-render="true">
<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
<a-table :columns="balancerColumns" bordered v-if="balancersData.length>0"
:row-key="r => r.key"
:data-source="balancersData"
:scroll="isMobile ? {} : { x: 200 }"
@ -502,6 +543,86 @@
</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' : ''">
@ -520,11 +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 = [
@ -569,6 +693,18 @@
{ 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 },
@ -591,6 +727,7 @@
saveBtnDisable: true,
refreshing: false,
restartResult: '',
showAlert: false,
isMobile: window.innerWidth <= 768,
advSettings: 'xraySetting',
cm: null,
@ -629,6 +766,7 @@
routingDomainStrategies: ["AsIs", "IPIfNonMatch", "IPOnDemand"],
logLevel: ["none" , "debug" , "info" , "warning", "error"],
access: ["none" , "./access.log" ],
error: ["none" , "./error.log" ],
settingsData: {
protocols: {
bittorrent: ["bittorrent"],
@ -655,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$"
@ -938,6 +1079,11 @@
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;
@ -1138,6 +1284,66 @@
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"}}',
@ -1184,6 +1390,9 @@
}
},
async mounted() {
if (window.location.protocol !== "https:") {
this.showAlert = true;
}
await this.getXraySetting();
await this.getXrayResult();
await this.getOutboundsTraffic();
@ -1335,9 +1544,9 @@
this.templateSettings = newTemplateSettings;
}
},
setAccessLog: {
accessLog: {
get: function () {
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.access) return "none";
if (!this.templateSettings || !this.templateSettings.log || !this.templateSettings.log.access) return "";
return this.templateSettings.log.access;
},
set: function (newValue) {
@ -1346,6 +1555,17 @@
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" });
@ -1468,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;
},
@ -1737,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);
@ -1749,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>

View file

@ -11,7 +11,7 @@
:ok-text="balancerModal.okText"
cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:14} }">
<a-form :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()"
@ -23,7 +23,8 @@
<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-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>
@ -74,17 +75,18 @@
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.check();
this.checkSelector();
},
close() {
balancerModal.visible = false;
balancerModal.loading(false);
this.visible = false;
this.loading(false);
},
loading(loading) {
balancerModal.confirmLoading = loading;
loading(loading=true) {
this.confirmLoading = loading;
},
check() {
if (balancerModal.balancer.tag == '' || balancerModal.balancerTags.includes(balancerModal.balancer.tag)) {
if (this.balancer.tag == '' || this.balancerTags.includes(this.balancer.tag)) {
this.duplicateTag = true;
this.isValid = false;
} else {
@ -93,7 +95,7 @@
}
},
checkSelector() {
balancerModal.emptySelector = balancerModal.balancer.selector.length == 0;
this.emptySelector = this.balancer.selector.length == 0;
}
};

View file

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

View file

@ -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>

View file

@ -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>
@ -174,7 +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 : ""
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
} else {
this.rule = {
domainMatcher: "",
@ -195,7 +195,7 @@
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));
@ -204,14 +204,14 @@
}
if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
this.balancerTags = app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)
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() {
@ -230,8 +230,8 @@
rule.inboundTag = value.inboundTag;
rule.protocol = value.protocol;
rule.attrs = Object.fromEntries(value.attrs);
rule.outboundTag = value.outboundTag;
rule.balancerTag = value.balancerTag;
rule.outboundTag = value.outboundTag == "" ? undefined : value.outboundTag;
rule.balancerTag = value.balancerTag == "" ? undefined : value.balancerTag;
for (const [key, value] of Object.entries(rule)) {
if (

View file

@ -10,8 +10,10 @@ import (
"regexp"
"sort"
"strings"
"sync"
"time"
"x-ui/config"
"x-ui/database"
"x-ui/database/model"
"x-ui/logger"
@ -37,28 +39,43 @@ func NewCheckClientIpJob() *CheckClientIpJob {
}
func (j *CheckClientIpJob) Run() {
var wg sync.WaitGroup
// create files required for iplimit if not exists
for i := 0; i < len(ipFiles); i++ {
file, err := os.OpenFile(ipFiles[i], os.O_CREATE|os.O_APPEND|os.O_RDWR, 0644)
j.checkError(err)
defer file.Close()
if j.checkFail2BanInstalled() {
j.openLogFiles(ipFiles)
}
// check for limit ip
if j.hasLimitIp() {
j.checkFail2BanInstalled()
if j.checkFail2BanInstalled() && xray.GetAccessLogPath() == "./access.log" {
j.processLogFile()
} else {
if !j.checkFail2BanInstalled() {
logger.Warning("fail2ban is not installed. IP limiting may not work properly.")
}
switch xray.GetAccessLogPath() {
case "none":
logger.Warning("Access log is set to 'none', check your Xray Configs")
case "":
logger.Warning("Access log doesn't exist in your Xray Configs")
}
}
}
if !j.hasLimitIp() && xray.GetAccessLogPath() == "./access.log" {
go j.clearLogTime()
if !j.checkFail2BanInstalled() && xray.GetAccessLogPath() == "./access.log" {
wg.Add(1)
go func() {
defer wg.Done()
j.clearLogTime()
}()
wg.Wait()
}
}
func (j *CheckClientIpJob) clearLogTime() {
for {
time.Sleep(time.Hour)
ticker := time.NewTicker(time.Hour)
defer ticker.Stop()
for range ticker.C {
j.clearAccessLog()
}
}
@ -72,15 +89,18 @@ func (j *CheckClientIpJob) clearAccessLog() {
// 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)
// close the file after copying content
file.Close()
// clean access log
err = os.Truncate(accessLogPath, 0)
j.checkError(err)
}
func (j *CheckClientIpJob) hasLimitIp() bool {
@ -112,29 +132,26 @@ func (j *CheckClientIpJob) hasLimitIp() bool {
return false
}
func (j *CheckClientIpJob) checkFail2BanInstalled() {
func (j *CheckClientIpJob) checkFail2BanInstalled() bool {
cmd := "fail2ban-client"
args := []string{"-h"}
err := exec.Command(cmd, args...).Run()
if err != nil {
logger.Error("fail2ban is not installed. IP limiting may not work properly.")
return err == nil
}
func (j *CheckClientIpJob) openLogFiles(ipFiles []string) {
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()
}
}
func (j *CheckClientIpJob) processLogFile() {
accessLogPath := xray.GetAccessLogPath()
if accessLogPath == "none" {
logger.Error("Access log is set to 'none' check your Xray Configs")
return
}
if accessLogPath == "" {
logger.Error("Access log doesn't exist in your Xray Configs")
return
}
file, err := os.Open(accessLogPath)
j.checkError(err)
defer file.Close()

View file

@ -2,6 +2,7 @@
"log": {
"access": "none",
"dnsLog": false,
"error": "./error.log",
"loglevel": "warning"
},
"api": {

View file

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

View file

@ -10,7 +10,6 @@ import (
)
type OutboundService struct {
xrayApi xray.XrayAPI
}
func (s *OutboundService) AddTraffic(traffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {

View file

@ -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
return s.getString("subPath")
}
if !strings.HasPrefix(subPath, "/") {
subPath = "/" + subPath
}
if !strings.HasSuffix(subPath, "/") {
subPath += "/"
}
return subPath, nil
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

View file

@ -1560,8 +1560,6 @@ func (t *Tgbot) sendBackup(chatId int64) {
} else {
logger.Error("Error in opening config.json file for backup: ", err)
}
t.sendBanLogs(chatId, false)
}
func (t *Tgbot) sendBanLogs(chatId int64, dt bool) {

View file

@ -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"
@ -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"
@ -311,12 +320,14 @@
"advancedTemplate" = "Advanced"
"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"
"blockCountryConfigsDesc" = "These options will block traffic based on the specific requested country."
"directCountryConfigs" = "Direct Country"
"directCountryConfigsDesc" = "These options will directly forward traffic based on the specific requested country."
"directCountryConfigsDesc" = "A direct connection ensures that specific traffic is not routed through another server."
"ipv4Configs" = "IPv4 Routing"
"ipv4ConfigsDesc" = "These options will route traffic based on a specific destination via IPv4."
"warpConfigs" = "WARP Routing"
@ -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"
@ -397,6 +414,8 @@
"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"
@ -445,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"

View file

@ -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,6 +52,14 @@
"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"
@ -62,6 +70,7 @@
"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."
@ -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,12 +320,14 @@
"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"
"blockCountryConfigsDesc" = "Estas opciones evitarán que los usuarios se conecten a dominios de países específicos."
"directCountryConfigs" = "Configuraciones de Conexión Directa por País"
"directCountryConfigsDesc" = "Estas opciones conectarán a los usuarios directamente a dominios de países específicos."
"directCountryConfigsDesc" = "Una conexión directa asegura que el tráfico específico no se enrutará a través de otro servidor."
"ipv4Configs" = "Configuraciones IPv4"
"ipv4ConfigsDesc" = "Estas opciones solo enrutarán a los dominios objetivo a través de IPv4."
"warpConfigs" = "Configuraciones de WARP"
@ -375,14 +386,20 @@
"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"
@ -397,6 +414,8 @@
"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"
@ -445,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"

View file

@ -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" = "مدت زمان استفاده به‌اتمام‌رسیده، لطفا دوباره وارد شوید"
@ -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,12 +320,14 @@
"advancedTemplate" = "پیشرفته"
"generalConfigs" = "استراتژی‌ کلی"
"generalConfigsDesc" = "این گزینه‌ها استراتژی کلی ترافیک را تعیین می‌کنند"
"logConfigs" = "گزارش"
"logConfigsDesc" = "گزارش‌ها ممکن است بر کارایی سرور شما تأثیر بگذارد. توصیه می شود فقط در صورت نیاز آن را عاقلانه فعال کنید"
"blockConfigs" = "سپر محافظ"
"blockConfigsDesc" = "این گزینه‌ها ترافیک را بر اساس پروتکل‌های درخواستی خاص، و وب سایت‌ها مسدود می‌کند"
"blockCountryConfigs" = "مسدودسازی کشور"
"blockCountryConfigsDesc" = "این گزینه‌ها ترافیک را بر اساس کشور درخواستی خاص مسدود می‌کند"
"directCountryConfigs" = "اتصال مستقیم کشور"
"directCountryConfigsDesc" = "این گزینه‌ها ترافیک را بر اساس کشور درخواستی خاص بصورت مستقیم ارسال می‌کند"
"directCountryConfigsDesc" = "اتصال مستقیم اطمینان حاصل می‌کند که ترافیک خاص از طریق یک سرور دیگر هدایت نمی‌شود."
"ipv4Configs" = "IPv4 مسیریابی"
"ipv4ConfigsDesc" = "این گزینه‌ها ترافیک‌ را از طریق آیپینسخه4 به مقصد هدایت می‌کند"
"warpConfigs" = "WARP مسیریابی"
@ -381,6 +392,12 @@
"OpenAIWARPDesc" = "ترافیک را از طریق وارپ به چت جی‌پی‌تی هدایت می‌کند"
"NetflixWARP" = "نتفلیکس"
"NetflixWARPDesc" = "ترافیک را از طریق وارپ به نتفلیکس هدایت می‌کند"
"MetaWARP" = "متا"
"MetaWARPDesc" = "ترافیک را از طریق وارپ به متا (اینستاگرام، فیس بوک، واتساپ، تردز و...) هدایت می کند."
"AppleWARP" = "اپل"
"AppleWARPDesc" = " ترافیک را از طریق وارپ به اپل هدایت می‌کند"
"RedditWARP" = "ردیت"
"RedditWARPDesc" = " ترافیک را از طریق وارپ به ردیت هدایت می‌کند"
"SpotifyWARP" = "اسپاتیفای"
"SpotifyWARPDesc" = " ترافیک را از طریق وارپ به اسپاتیفای هدایت می‌کند"
"IRWARP" = "دامنه‌های ایران"
@ -397,6 +414,8 @@
"logLevelDesc" = "سطح گزارش برای گزارش های خطا، نشان دهنده اطلاعاتی است که باید ثبت شوند."
"accessLog" = "مسیر گزارش"
"accessLogDesc" = "مسیر فایل برای گزارش دسترسی. مقدار ویژه «هیچ» گزارش‌های دسترسی را غیرفعال میکند."
"errorLog" = "گزارش خطا"
"errorLogDesc" = "مسیر فایل برای ورود به سیستم خطا. مقدار ویژه «هیچ» گزارش های خطا را غیرفعال میکند"
[pages.xray.rules]
"first" = "اولین"
@ -445,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" = "توکن مخفی"

View file

@ -42,7 +42,7 @@
"online" = "Online"
"domainName" = "Nama Domain"
"monitor" = "IP Pemantauan"
"certificate" = "Sertifikat"
"certificate" = "Sertifikat Digital"
"fail" = "Gagal"
"success" = "Berhasil"
"getVersion" = "Dapatkan Versi"
@ -52,6 +52,14 @@
"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"
@ -62,6 +70,7 @@
"link" = "Kelola"
[pages.login]
"hello" = "Halo"
"title" = "Selamat Datang"
"loginAgain" = "Sesi Anda telah berakhir, harap masuk kembali"
@ -140,10 +149,8 @@
"noRecommendKeepDefault" = "Disarankan untuk tetap menggunakan pengaturan default"
"certificatePath" = "Path Berkas"
"certificateContent" = "Konten Berkas"
"publicKeyPath" = "Path Kunci Publik"
"publicKeyContent" = "Konten Kunci Publik"
"keyPath" = "Path Kunci Privat"
"keyContent" = "Konten Kunci Privat"
"publicKey" = "Kunci Publik"
"privatekey" = "Kunci Pribadi"
"clickOnQRcode" = "Klik pada Kode QR untuk Menyalin"
"client" = "Klien"
"export" = "Ekspor Semua URL"
@ -194,7 +201,7 @@
"last" = "Terakhir"
"prefix" = "Awalan"
"postfix" = "Akhiran"
"delayedStart" = "Mulai saat Penggunaan Awal"
"delayedStart" = "Mulai Awal"
"expireDays" = "Durasi"
"days" = "Hari"
"renew" = "Perpanjang Otomatis"
@ -302,6 +309,8 @@
"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"
@ -311,12 +320,14 @@
"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."
"directCountryConfigsDesc" = "Koneksi langsung memastikan bahwa lalu lintas tertentu tidak diarahkan melalui server lain."
"ipv4Configs" = "Pengalihan IPv4"
"ipv4ConfigsDesc" = "Opsi ini akan mengalihkan lalu lintas berdasarkan tujuan tertentu melalui IPv4."
"warpConfigs" = "Pengalihan WARP"
@ -381,6 +392,12 @@
"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"
@ -397,6 +414,8 @@
"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"
@ -445,6 +464,21 @@
"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"

View file

@ -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" = "Время пребывания в сети вышло. Пожалуйста, войдите в систему снова"
@ -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,12 +320,14 @@
"advancedTemplate" = "Расширенный шаблон"
"generalConfigs" = "Основные настройки"
"generalConfigsDesc" = "Эти параметры описывают общие настройки"
"logConfigs" = "Журнал"
"logConfigsDesc" = "Журналы могут повлиять на эффективность вашего сервера. Рекомендуется включать их с умом только в случае ваших нужд!"
"blockConfigs" = "Блокировка конфигураций"
"blockConfigsDesc" = "Эти параметры не позволят пользователям подключаться к определенным протоколам и веб-сайтам"
"blockCountryConfigs" = "Конфигурации блокировки страны"
"blockCountryConfigsDesc" = "Эти параметры не позволят пользователям подключаться к доменам определенной страны"
"directCountryConfigs" = "Настройки прямого подключения для страны"
"directCountryConfigsDesc" = "Эти параметры позволят пользователям подключаться напрямую к доменам определенной страны"
"directCountryConfigsDesc" = "Прямое подключение обеспечивает, что конкретный трафик не направляется через другой сервер."
"ipv4Configs" = "Настройки IPv4"
"ipv4ConfigsDesc" = "Эти параметры позволят пользователям маршрутизироваться к целевым доменам только через IPv4"
"warpConfigs" = "Настройки WARP"
@ -375,14 +386,20 @@
"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" = "Входящие"
@ -397,6 +414,8 @@
"logLevelDesc" = "Уровень журнала для журналов ошибок, указывающий информацию, которую необходимо записать."
"accessLog" = "Журнал доступа"
"accessLogDesc" = "Путь к файлу журнала доступа. Специальное значение «none» отключило журналы доступа."
"errorLog" = "Журнал ошибок"
"errorLogDesc" = "Путь к файлу журнала ошибок. Специальное значение «none» отключает журналы ошибок."
[pages.xray.rules]
"first" = "Первый"
@ -439,12 +458,27 @@
[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" = "Секретный токен"

View 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>"

View file

@ -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."
@ -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,12 +320,14 @@
"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"
"blockCountryConfigsDesc" = "Những tùy chọn này sẽ ngăn người dùng kết nối đến các tên miền quốc gia cụ thể."
"directCountryConfigs" = "Cấu hình Kết nối Trực tiếp Quốc gia"
"directCountryConfigsDesc" = "Những tùy chọn này sẽ kết nối người dùng trực tiếp đến các tên miền quốc gia cụ thể."
"directCountryConfigsDesc" = "Một kết nối trực tiếp đảm bảo rằng lưu lượng cụ thể không được định tuyến qua một máy chủ khác."
"ipv4Configs" = "Cấu hình IPv4"
"ipv4ConfigsDesc" = "Những tùy chọn này sẽ chỉ định kết nối đến các tên miền mục tiêu qua IPv4."
"warpConfigs" = "Cấu hình WARP"
@ -375,14 +386,20 @@
"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"
@ -397,6 +414,8 @@
"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"
@ -445,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"

View file

@ -42,16 +42,24 @@
"online" = "在线"
"domainName" = "域名"
"monitor" = "监听"
"certificate" = "证书"
"certificate" = "数字证书"
"fail" = "失败"
"success" = "成功"
"getVersion" = "获取版本"
"install" = "安装"
"clients" = "客户端"
"usage" = "用法"
"usage" = "使用情况"
"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" = "登录时效已过,请重新登录"
@ -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,12 +320,14 @@
"advancedTemplate" = "高级模板部件"
"generalConfigs" = "通用配置"
"generalConfigsDesc" = "这些选项将提供一般调整"
"logConfigs"="日志"
"logConfigsDesc" = "日志可能会影响您服务器的效率。建议仅在您需要时明智地启用它"
"blockConfigs" = "阻塞配置"
"blockConfigsDesc" = "这些选项将阻止用户连接到特定协议和网站"
"blockCountryConfigs" = "阻止国家配置"
"blockCountryConfigsDesc" = "这些选项将阻止用户连接到特定国家/地区的域。"
"directCountryConfigs" = "直接国家配置"
"directCountryConfigsDesc" = "这些选项会将用户直接连接到特定国家/地区的域。"
"directCountryConfigsDesc" = "直接连接确保特定流量不通过另一台服务器路由。"
"ipv4Configs" = "IPv4 配置"
"ipv4ConfigsDesc" = "此选项将仅通过 IPv4 路由到目标域"
"warpConfigs" = "WARP 配置"
@ -375,14 +386,20 @@
"GoogleIPv4Desc" = "添加谷歌连接IPv4的路由"
"NetflixIPv4" = "为 Netflix 使用 IPv4"
"NetflixIPv4Desc" = "添加Netflix连接IPv4的路由"
"GoogleWARP" = "将谷歌路由到 WARP"
"GoogleWARPDesc" = "为谷歌添加路由到WARP"
"OpenAIWARP" = "将 OpenAI (ChatGPT) 路由到 WARP"
"OpenAIWARPDesc" = "将OpenAIChatGPT路由添加到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 将流量路由到 MetaInstagram、Facebook、WhatsApp、Threads..."
"AppleWARP" = "Apple"
"AppleWARPDesc" = "通过 WARP 将流量路由到 Apple。"
"RedditWARP" = "Reddit"
"RedditWARPDesc" = "通过 WARP 将流量路由到 Reddit。"
"SpotifyWARP" = "Spotify"
"SpotifyWARPDesc" = "通过 WARP 将流量路由到 Spotify。"
"IRWARP" = "将伊朗域名路由到 WARP"
"IRWARPDesc" = "将伊朗域的路由添加到 WARP。 重启面板生效"
"Inbounds" = "入站"
@ -397,6 +414,8 @@
"logLevelDesc" = "错误日志的日志级别,表示需要记录的信息。"
"accessLog" = "访问日志"
"accessLogDesc" = "访问日志的文件路径。 特殊值“none”禁用访问日志"
"errorLog" = "错误日志"
"errorLogDesc" = "错误日志的文件路径。 特殊值“none”禁用错误日志"
[pages.xray.rules]
"first" = "第一个"
@ -445,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" = "密钥"

View file

@ -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)
}
if certFile != "" || keyFile != "" {
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())
}
} else {
logger.Info("web server run http on", listener.Addr())
}

53
x-ui.sh
View file

@ -345,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}"
@ -906,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
@ -918,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
@ -932,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>
@ -1263,7 +1304,7 @@ show_menu() {
firewall_menu
;;
21)
enable_bbr
bbr_menu
;;
22)
update_geo

View file

@ -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 {

View file

@ -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 && startIndex < endIndex {
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)
}