mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2026-03-15 23:46:09 +00:00
Compare commits
61 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
5408a2f82c | ||
|
|
c8d71ea748 | ||
|
|
46de886b53 | ||
|
|
6d41320ed7 | ||
|
|
bf9d2e6aeb | ||
|
|
ed96fa090b | ||
|
|
3ac1d7f546 | ||
|
|
10025ffa66 | ||
|
|
5ee62b25ca | ||
|
|
311d11a3c1 | ||
|
|
40b6d7707a | ||
|
|
cbf316db31 | ||
|
|
33a36ada4b | ||
|
|
82ddd10627 | ||
|
|
2401c99817 | ||
|
|
2f36a4047c | ||
|
|
dc3b0d218a | ||
|
|
610d29765a | ||
|
|
b1ea8005e4 | ||
|
|
3f0bfa2472 | ||
|
|
1e2ff650ad | ||
|
|
c2d6dd923f | ||
|
|
723ec25fb2 | ||
|
|
7dc52e9a53 | ||
|
|
fe9f0d1d0e | ||
|
|
18d74d54ca | ||
|
|
c7ba6ae909 | ||
|
|
3edf79e589 | ||
|
|
5420e643cf | ||
|
|
9fcd0387ca | ||
|
|
7b039d219e | ||
|
|
dbec28b915 | ||
|
|
e5126806d7 | ||
|
|
b008ff4ad2 | ||
|
|
da6b89fdcd | ||
|
|
d7882c25d1 | ||
|
|
ed2a0a0bcf | ||
|
|
4a0914cb1e | ||
|
|
664269d513 | ||
|
|
d0796b26c9 | ||
|
|
2750f46c01 | ||
|
|
023eb513e4 | ||
|
|
0c7b59ed47 | ||
|
|
3087c1b123 | ||
|
|
2198397197 | ||
|
|
d10c312e62 | ||
|
|
24a3411465 | ||
|
|
2198e7a28f | ||
|
|
6b23b416a7 | ||
|
|
16f53ce4c2 | ||
|
|
27445b30e9 | ||
|
|
3d0212c21d | ||
|
|
978755960f | ||
|
|
9b51e9a5c5 | ||
|
|
6879a8fbcb | ||
|
|
7258841491 | ||
|
|
23dd80fbb0 | ||
|
|
6556884c7f | ||
|
|
d5c532c64f | ||
|
|
ad5f774a1e | ||
|
|
aa285914fa |
2
.github/FUNDING.yml
vendored
2
.github/FUNDING.yml
vendored
@@ -1,6 +1,6 @@
|
||||
# These are supported funding model platforms
|
||||
|
||||
github: # Replace with up to 4 GitHub Sponsors-enabled usernames e.g., [user1, user2]
|
||||
github: MHSanaei
|
||||
patreon: # Replace with a single Patreon username
|
||||
open_collective: # Replace with a single Open Collective username
|
||||
ko_fi: # Replace with a single Ko-fi username
|
||||
|
||||
2
.github/workflows/docker.yml
vendored
2
.github/workflows/docker.yml
vendored
@@ -10,7 +10,7 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- uses: actions/checkout@v5
|
||||
with:
|
||||
submodules: true
|
||||
|
||||
|
||||
90
.github/workflows/release.yml
vendored
90
.github/workflows/release.yml
vendored
@@ -7,8 +7,9 @@ on:
|
||||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
paths:
|
||||
- '.github/workflows/release.yml'
|
||||
- '**.js'
|
||||
- '**.css'
|
||||
- '**.html'
|
||||
@@ -35,10 +36,10 @@ jobs:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
@@ -84,7 +85,7 @@ jobs:
|
||||
cd x-ui/bin
|
||||
|
||||
# Download dependencies
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.8.3/"
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.9.11/"
|
||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||
wget -q ${Xray_URL}Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
@@ -135,10 +136,89 @@ jobs:
|
||||
|
||||
- name: Upload files to GH release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
if: |
|
||||
(github.event_name == 'release' && github.event.action == 'published') ||
|
||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||
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
|
||||
overwrite: true
|
||||
prerelease: true
|
||||
|
||||
# =================================
|
||||
# Windows Build
|
||||
# =================================
|
||||
build-windows:
|
||||
name: Build for Windows
|
||||
permissions:
|
||||
contents: write
|
||||
strategy:
|
||||
matrix:
|
||||
platform:
|
||||
- amd64
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
||||
- name: Build 3X-UI for Windows
|
||||
shell: pwsh
|
||||
run: |
|
||||
$env:CGO_ENABLED="1"
|
||||
$env:GOOS="windows"
|
||||
$env:GOARCH="amd64"
|
||||
go build -ldflags "-w -s" -o xui-release.exe -v main.go
|
||||
|
||||
mkdir x-ui
|
||||
Copy-Item xui-release.exe x-ui\
|
||||
mkdir x-ui\bin
|
||||
cd x-ui\bin
|
||||
|
||||
# Download Xray for Windows
|
||||
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/"
|
||||
Invoke-WebRequest -Uri "${Xray_URL}Xray-windows-64.zip" -OutFile "Xray-windows-64.zip"
|
||||
Expand-Archive -Path "Xray-windows-64.zip" -DestinationPath .
|
||||
Remove-Item "Xray-windows-64.zip"
|
||||
Remove-Item geoip.dat, geosite.dat -ErrorAction SilentlyContinue
|
||||
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip.dat"
|
||||
Invoke-WebRequest -Uri "https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite.dat"
|
||||
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat" -OutFile "geoip_IR.dat"
|
||||
Invoke-WebRequest -Uri "https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat" -OutFile "geosite_IR.dat"
|
||||
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat" -OutFile "geoip_RU.dat"
|
||||
Invoke-WebRequest -Uri "https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat" -OutFile "geosite_RU.dat"
|
||||
Rename-Item xray.exe xray-windows-amd64.exe
|
||||
cd ..
|
||||
Copy-Item -Path ..\windows_files\* -Destination . -Recurse
|
||||
cd ..
|
||||
|
||||
- name: Package to Zip
|
||||
shell: pwsh
|
||||
run: |
|
||||
Compress-Archive -Path .\x-ui -DestinationPath "x-ui-windows-amd64.zip"
|
||||
|
||||
- name: Upload files to Artifacts
|
||||
uses: actions/upload-artifact@v4
|
||||
with:
|
||||
name: x-ui-windows-amd64
|
||||
path: ./x-ui-windows-amd64.zip
|
||||
|
||||
- name: Upload files to GH release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
if: |
|
||||
(github.event_name == 'release' && github.event.action == 'published') ||
|
||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-windows-amd64.zip
|
||||
asset_name: x-ui-windows-amd64.zip
|
||||
overwrite: true
|
||||
prerelease: true
|
||||
4
.gitignore
vendored
4
.gitignore
vendored
@@ -29,9 +29,9 @@ main
|
||||
.DS_Store
|
||||
Thumbs.db
|
||||
|
||||
# Ignore Go specific files
|
||||
# Ignore Go build files
|
||||
*.exe
|
||||
*.exe~
|
||||
x-ui.db
|
||||
|
||||
# Ignore Docker specific files
|
||||
docker-compose.override.yml
|
||||
|
||||
@@ -27,7 +27,7 @@ case $1 in
|
||||
esac
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.8.3/Xray-linux-${ARCH}.zip"
|
||||
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||
mv xray "xray-linux-${FNAME}"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
# ========================================================
|
||||
# Stage: Builder
|
||||
# ========================================================
|
||||
FROM golang:1.24-alpine AS builder
|
||||
FROM golang:1.25-alpine AS builder
|
||||
WORKDIR /app
|
||||
ARG TARGETARCH
|
||||
|
||||
|
||||
@@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## النجوم عبر الزمن
|
||||
|
||||
@@ -48,7 +48,7 @@ Para documentación completa, visita la [Wiki del proyecto](https://github.com/M
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## Estrellas a lo Largo del Tiempo
|
||||
|
||||
@@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## ستارهها در طول زمان
|
||||
|
||||
@@ -48,7 +48,7 @@ For full documentation, please visit the [project Wiki](https://github.com/MHSan
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## Stargazers over Time
|
||||
|
||||
@@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## Звезды с течением времени
|
||||
|
||||
@@ -48,7 +48,7 @@ bash <(curl -Ls https://raw.githubusercontent.com/mhsanaei/3x-ui/master/install.
|
||||
</p>
|
||||
|
||||
- USDT (TRC20): `TXncxkvhkDWGts487Pjqq1qT9JmwRUz8CC`
|
||||
- MATIC (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- POL (polygon): `0x41C9548675D044c6Bfb425786C765bc37427256A`
|
||||
- LTC (Litecoin): `ltc1q2ach7x6d2zq0n4l0t4zl7d7xe2s6fs7a3vspwv`
|
||||
|
||||
## 随时间变化的星标数
|
||||
|
||||
@@ -3,7 +3,10 @@ package config
|
||||
import (
|
||||
_ "embed"
|
||||
"fmt"
|
||||
"io"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strings"
|
||||
)
|
||||
|
||||
@@ -54,12 +57,32 @@ func GetBinFolderPath() string {
|
||||
return binFolderPath
|
||||
}
|
||||
|
||||
func getBaseDir() string {
|
||||
exePath, err := os.Executable()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
exeDir := filepath.Dir(exePath)
|
||||
exeDirLower := strings.ToLower(filepath.ToSlash(exeDir))
|
||||
if strings.Contains(exeDirLower, "/appdata/local/temp/") || strings.Contains(exeDirLower, "/go-build") {
|
||||
wd, err := os.Getwd()
|
||||
if err != nil {
|
||||
return "."
|
||||
}
|
||||
return wd
|
||||
}
|
||||
return exeDir
|
||||
}
|
||||
|
||||
func GetDBFolderPath() string {
|
||||
dbFolderPath := os.Getenv("XUI_DB_FOLDER")
|
||||
if dbFolderPath == "" {
|
||||
dbFolderPath = "/etc/x-ui"
|
||||
if dbFolderPath != "" {
|
||||
return dbFolderPath
|
||||
}
|
||||
return dbFolderPath
|
||||
if runtime.GOOS == "windows" {
|
||||
return getBaseDir()
|
||||
}
|
||||
return "/etc/x-ui"
|
||||
}
|
||||
|
||||
func GetDBPath() string {
|
||||
@@ -68,8 +91,54 @@ func GetDBPath() string {
|
||||
|
||||
func GetLogFolder() string {
|
||||
logFolderPath := os.Getenv("XUI_LOG_FOLDER")
|
||||
if logFolderPath == "" {
|
||||
logFolderPath = "/var/log"
|
||||
if logFolderPath != "" {
|
||||
return logFolderPath
|
||||
}
|
||||
return logFolderPath
|
||||
if runtime.GOOS == "windows" {
|
||||
return filepath.Join(".", "log")
|
||||
}
|
||||
return "/var/log"
|
||||
}
|
||||
|
||||
func copyFile(src, dst string) error {
|
||||
in, err := os.Open(src)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer in.Close()
|
||||
|
||||
out, err := os.Create(dst)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
_, err = io.Copy(out, in)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return out.Sync()
|
||||
}
|
||||
|
||||
func init() {
|
||||
if runtime.GOOS != "windows" {
|
||||
return
|
||||
}
|
||||
if os.Getenv("XUI_DB_FOLDER") != "" {
|
||||
return
|
||||
}
|
||||
oldDBFolder := "/etc/x-ui"
|
||||
oldDBPath := fmt.Sprintf("%s/%s.db", oldDBFolder, GetName())
|
||||
newDBFolder := GetDBFolderPath()
|
||||
newDBPath := fmt.Sprintf("%s/%s.db", newDBFolder, GetName())
|
||||
_, err := os.Stat(newDBPath)
|
||||
if err == nil {
|
||||
return // new exists
|
||||
}
|
||||
_, err = os.Stat(oldDBPath)
|
||||
if os.IsNotExist(err) {
|
||||
return // old does not exist
|
||||
}
|
||||
_ = copyFile(oldDBPath, newDBPath) // ignore error
|
||||
}
|
||||
|
||||
@@ -1 +1 @@
|
||||
2.6.6
|
||||
2.8.0
|
||||
@@ -12,11 +12,11 @@ type Protocol string
|
||||
const (
|
||||
VMESS Protocol = "vmess"
|
||||
VLESS Protocol = "vless"
|
||||
DOKODEMO Protocol = "dokodemo-door"
|
||||
Tunnel Protocol = "tunnel"
|
||||
HTTP Protocol = "http"
|
||||
Trojan Protocol = "trojan"
|
||||
Shadowsocks Protocol = "shadowsocks"
|
||||
Socks Protocol = "socks"
|
||||
Mixed Protocol = "mixed"
|
||||
WireGuard Protocol = "wireguard"
|
||||
)
|
||||
|
||||
@@ -32,6 +32,7 @@ type Inbound struct {
|
||||
Up int64 `json:"up" form:"up"`
|
||||
Down int64 `json:"down" form:"down"`
|
||||
Total int64 `json:"total" form:"total"`
|
||||
AllTime int64 `json:"allTime" form:"allTime" gorm:"default:0"`
|
||||
Remark string `json:"remark" form:"remark"`
|
||||
Enable bool `json:"enable" form:"enable"`
|
||||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
@@ -45,7 +46,6 @@ type Inbound struct {
|
||||
StreamSettings string `json:"streamSettings" form:"streamSettings"`
|
||||
Tag string `json:"tag" form:"tag" gorm:"unique"`
|
||||
Sniffing string `json:"sniffing" form:"sniffing"`
|
||||
Allocate string `json:"allocate" form:"allocate"`
|
||||
}
|
||||
|
||||
type OutboundTraffics struct {
|
||||
@@ -80,7 +80,6 @@ func (i *Inbound) GenXrayInboundConfig() *xray.InboundConfig {
|
||||
StreamSettings: json_util.RawMessage(i.StreamSettings),
|
||||
Tag: i.Tag,
|
||||
Sniffing: json_util.RawMessage(i.Sniffing),
|
||||
Allocate: json_util.RawMessage(i.Allocate),
|
||||
}
|
||||
}
|
||||
|
||||
@@ -104,4 +103,13 @@ type Client struct {
|
||||
SubID string `json:"subId" form:"subId"`
|
||||
Comment string `json:"comment" form:"comment"`
|
||||
Reset int `json:"reset" form:"reset"`
|
||||
CreatedAt int64 `json:"created_at,omitempty"`
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type VLESSSettings struct {
|
||||
Clients []Client `json:"clients"`
|
||||
Decryption string `json:"decryption"`
|
||||
Encryption string `json:"encryption"`
|
||||
Fallbacks []any `json:"fallbacks"`
|
||||
}
|
||||
|
||||
52
go.mod
52
go.mod
@@ -1,6 +1,6 @@
|
||||
module x-ui
|
||||
|
||||
go 1.24.5
|
||||
go 1.25.1
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/gzip v1.2.3
|
||||
@@ -9,32 +9,34 @@ require (
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mymmrac/telego v1.2.0
|
||||
github.com/mymmrac/telego v1.3.0
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.7
|
||||
github.com/valyala/fasthttp v1.64.0
|
||||
github.com/shirou/gopsutil/v4 v4.25.8
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/valyala/fasthttp v1.65.0
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/xtls/xray-core v1.250803.0
|
||||
github.com/xtls/xray-core v1.250911.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/text v0.28.0
|
||||
google.golang.org/grpc v1.74.2
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/text v0.29.0
|
||||
google.golang.org/grpc v1.75.1
|
||||
gorm.io/driver/sqlite v1.6.0
|
||||
gorm.io/gorm v1.30.1
|
||||
gorm.io/gorm v1.30.5
|
||||
)
|
||||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 // indirect
|
||||
github.com/ebitengine/purego v0.8.4 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
@@ -54,9 +56,9 @@ require (
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
|
||||
github.com/kr/text v0.2.0 // indirect
|
||||
github.com/leodido/go-urn v1.4.0 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.30 // indirect
|
||||
github.com/mattn/go-sqlite3 v1.14.32 // indirect
|
||||
github.com/miekg/dns v1.1.68 // indirect
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
|
||||
github.com/modern-go/reflect2 v1.0.2 // indirect
|
||||
@@ -67,8 +69,8 @@ require (
|
||||
github.com/refraction-networking/utls v1.8.0 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagernet/sing v0.7.5 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 // indirect
|
||||
github.com/sagernet/sing v0.7.7 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
@@ -79,21 +81,21 @@ require (
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/xtls/reality v0.0.0-20250727231020-de3bb4d08f5a // indirect
|
||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.5.2 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/arch v0.21.0 // indirect
|
||||
golang.org/x/mod v0.28.0 // indirect
|
||||
golang.org/x/net v0.44.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b // indirect
|
||||
google.golang.org/protobuf v1.36.7 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
gopkg.in/yaml.v3 v3.0.1 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
|
||||
126
go.sum
126
go.sum
@@ -2,8 +2,10 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg
|
||||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
@@ -19,8 +21,8 @@ github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33 h1:ucRHb6/lvW/+mT
|
||||
github.com/dgryski/go-metro v0.0.0-20250106013310-edb8663e5e33/go.mod h1:c9O8+fpSOX1DM8cPNSkX/qsBWdkD4yd2dpciOWQjpBw=
|
||||
github.com/ebitengine/purego v0.8.4 h1:CF7LEKg5FFOsASUj0+QwaXf8Ht6TlFxg09+S9wz0omw=
|
||||
github.com/ebitengine/purego v0.8.4/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9 h1:5k+WDwEsD9eTLL8Tz3L0VnmVh9QxGjRmjBvAG7U/oYY=
|
||||
github.com/gabriel-vasile/mimetype v1.4.9/go.mod h1:WnSQhFKJuBlRyLiKohA/2DtIlPFAbguNaG7QCHcyGok=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 h1:zyueNbySn/z8mJZHLt6IPw0KoZsiQNszIpU+bX4+ZK0=
|
||||
github.com/gabriel-vasile/mimetype v1.4.10/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344 h1:Arcl6UOIS/kgO2nW3A65HN+7CMjSDP/gofXL4CZt1V4=
|
||||
github.com/ghodss/yaml v1.0.1-0.20220118164431-d8423dcdf344/go.mod h1:GIjDIg/heH5DOkXY3YJ/wNhfHsQHoXGjl8G8amsYQ1I=
|
||||
github.com/gin-contrib/gzip v1.2.3 h1:dAhT722RuEG330ce2agAs75z7yB+NKvX/ZM1r8w0u2U=
|
||||
@@ -91,12 +93,12 @@ github.com/kr/text v0.2.0 h1:5Nx0Ya0ZqY2ygV366QzturHI13Jq95ApcVaJBhpS+AY=
|
||||
github.com/kr/text v0.2.0/go.mod h1:eLer722TekiGuMkidMxC/pM04lWEeraHUUmBw8l2grE=
|
||||
github.com/leodido/go-urn v1.4.0 h1:WT9HwE9SGECu3lg4d/dIA+jxlljEa1/ffXKmRjqdmIQ=
|
||||
github.com/leodido/go-urn v1.4.0/go.mod h1:bvxc+MVxLKB4z00jd1z+Dvzr47oO32F/QSNjSBOlFxI=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35 h1:PpXWgLPs+Fqr325bN2FD2ISlRRztXibcX6e8f5FR5Dc=
|
||||
github.com/lufia/plan9stats v0.0.0-20250317134145-8bc96cf8fc35/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/mattn/go-isatty v0.0.20 h1:xfD0iDuEKnDkl03q4limB+vH+GxLEtL/jb4xVJSWWEY=
|
||||
github.com/mattn/go-isatty v0.0.20/go.mod h1:W+V8PltTTMOvKvAeJH7IuucS94S2C6jfK/D7dTCTo3Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.30 h1:bVreufq3EAIG1Quvws73du3/QgdeZ3myglJlrzSYYCY=
|
||||
github.com/mattn/go-sqlite3 v1.14.30/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/mattn/go-sqlite3 v1.14.32 h1:JD12Ag3oLy1zQA+BNn74xRgaBbdhbNIDYvQUEuuErjs=
|
||||
github.com/mattn/go-sqlite3 v1.14.32/go.mod h1:Uh1q+B4BYcTPb+yiD3kU8Ct7aC0hY9fxUwlHK0RXw+Y=
|
||||
github.com/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
github.com/miekg/dns v1.1.68/go.mod h1:fujopn7TB3Pu3JM69XaawiU0wqjpL9/8xGop5UrTPps=
|
||||
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
@@ -104,8 +106,8 @@ github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd h1:TRLaZ9cD/w
|
||||
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
|
||||
github.com/modern-go/reflect2 v1.0.2 h1:xBagoLtFs94CBntxluKeaWgTMpvLxC4ur3nMaC9Gz0M=
|
||||
github.com/modern-go/reflect2 v1.0.2/go.mod h1:yWuevngMOJpCy52FWWMvUC8ws7m/LJsjYzDa0/r8luk=
|
||||
github.com/mymmrac/telego v1.2.0 h1:CHmR9eiugpTiF/ttppmK89E6mcu9d4DmNQS0tMpUs6M=
|
||||
github.com/mymmrac/telego v1.2.0/go.mod h1:OiCm4QjqB/ZY2E4VAmkVH8EeLLhM4QuFuO1KOCuvGoM=
|
||||
github.com/mymmrac/telego v1.3.0 h1:y2bDDCioLgkcs+5luUaPgTNHKel1Qh30iUxFcMUrowg=
|
||||
github.com/mymmrac/telego v1.3.0/go.mod h1:0D2l/IA/gUFn4oqsi1O4/tSnlezw5jNV/ReFRDUEKk8=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0 h1:C/m2NNWNiTB6SK4Ao8df5EWm3JETSTIGNXBpMJTxzxQ=
|
||||
github.com/nicksnyder/go-i18n/v2 v2.6.0/go.mod h1:88sRqr0C6OPyJn0/KRNaEz1uWorjxIKP7rUUcvycecE=
|
||||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0CI4q8xvlXPxeZ0gYCVvWbmPlp88=
|
||||
@@ -132,14 +134,16 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
||||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagernet/sing v0.7.5 h1:gNMwZCLPqR+4e0g6dwi0sSsrvOmoMjpZgqxKsuJZatc=
|
||||
github.com/sagernet/sing v0.7.5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
||||
github.com/sagernet/sing v0.7.7 h1:o46FzVZS+wKbBMEkMEdEHoVZxyM9jvfRpKXc7pEgS/c=
|
||||
github.com/sagernet/sing v0.7.7/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0=
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
@@ -148,8 +152,8 @@ github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/
|
||||
github.com/stretchr/testify v1.7.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU=
|
||||
github.com/stretchr/testify v1.8.1/go.mod h1:w2LPCIKwWwSfY2zedu0+kehJoqGctiVI29o6fzry7u4=
|
||||
github.com/stretchr/testify v1.10.0 h1:Xv5erBjTwe/5IxqUQTdXv5kgmIvbHo3QQyRwhJsOfJA=
|
||||
github.com/stretchr/testify v1.10.0/go.mod h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
|
||||
github.com/stretchr/testify v1.11.1 h1:7s2iGBzp5EwR7/aIZr8ao5+dra3wiQyKjjFuvgVKu7U=
|
||||
github.com/stretchr/testify v1.11.1/go.mod h1:wZwfW3scLgRK+23gO65QZefKpKQRnfz6sD981Nm4B6U=
|
||||
github.com/tklauser/go-sysconf v0.3.15 h1:VE89k0criAymJ/Os65CSn1IXaol+1wrsFHEB8Ol49K4=
|
||||
github.com/tklauser/go-sysconf v0.3.15/go.mod h1:Dmjwr6tYFIseJw7a3dRLJfsHAMXZ3nEnL/aZY+0IuI4=
|
||||
github.com/tklauser/numcpus v0.10.0 h1:18njr6LDBk1zuna922MgdjQuJFjrdppsZG60sHGfjso=
|
||||
@@ -162,8 +166,8 @@ github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e/go.mod h1:5t19P9LBIrNamL6AcMQOncg/r10y3Pc01AbHeMhwlpU=
|
||||
github.com/valyala/bytebufferpool v1.0.0 h1:GqA5TC/0021Y/b9FG4Oi9Mr3q7XYx6KllzawFIhcdPw=
|
||||
github.com/valyala/bytebufferpool v1.0.0/go.mod h1:6bBcMArwyJ5K/AmCkWv1jt77kVWyCJ6HpOuEn7z0Csc=
|
||||
github.com/valyala/fasthttp v1.64.0 h1:QBygLLQmiAyiXuRhthf0tuRkqAFcrC42dckN2S+N3og=
|
||||
github.com/valyala/fasthttp v1.64.0/go.mod h1:dGmFxwkWXSK0NbOSJuF7AMVzU+lkHz0wQVvVITv2UQA=
|
||||
github.com/valyala/fasthttp v1.65.0 h1:j/u3uzFEGFfRxw79iYzJN+TteTJwbYkru9uDp3d0Yf8=
|
||||
github.com/valyala/fasthttp v1.65.0/go.mod h1:P/93/YkKPMsKSnATEeELUCkG8a7Y+k99uxNHVbKINr4=
|
||||
github.com/valyala/fastjson v1.6.4 h1:uAUNq9Z6ymTgGhcm0UynUAB6tlbakBrz6CQFax3BXVQ=
|
||||
github.com/valyala/fastjson v1.6.4/go.mod h1:CLCAqky6SMuOcxStkYQvblddUtoRxhYMGLrsQns1aXY=
|
||||
github.com/vishvananda/netlink v1.3.1 h1:3AEMt62VKqz90r0tmNhog0r/PpWKmrEShJU0wJW6bV0=
|
||||
@@ -172,66 +176,68 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
||||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
||||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||
github.com/xtls/reality v0.0.0-20250727231020-de3bb4d08f5a h1:Fs8Pc0JAc/LDOf9Q4DzKrk+Ujf4ILlyvfvDVZcmOZ2o=
|
||||
github.com/xtls/reality v0.0.0-20250727231020-de3bb4d08f5a/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
||||
github.com/xtls/xray-core v1.250803.0 h1:sYdRC243UsujnePINH4IfM4MfHE4lj2p4wZFAfeE2GI=
|
||||
github.com/xtls/xray-core v1.250803.0/go.mod h1:z2vn2o30flYEgpSz1iEhdZP1I46UZ3+gXINZyohH3yE=
|
||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU=
|
||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
||||
github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno=
|
||||
github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
github.com/yusufpapurcu/wmi v1.2.4/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0 h1:cH53jehLUN6UFLY71z+NDOiNJqDdPRaXzTel0sJySYA=
|
||||
go.opentelemetry.io/auto/sdk v1.1.0/go.mod h1:3wSPjt5PWp2RhlCcmmOial7AvC4DQqZb7a7wCow3W8A=
|
||||
go.opentelemetry.io/otel v1.36.0 h1:UumtzIklRBY6cI/lllNZlALOF5nNIzJVb16APdvgTXg=
|
||||
go.opentelemetry.io/otel v1.36.0/go.mod h1:/TcFMXYjyRNh8khOAO9ybYkqaDBb/70aVwkNML4pP8E=
|
||||
go.opentelemetry.io/otel/metric v1.36.0 h1:MoWPKVhQvJ+eeXWHFBOPoBOi20jh6Iq2CcCREuTYufE=
|
||||
go.opentelemetry.io/otel/metric v1.36.0/go.mod h1:zC7Ks+yeyJt4xig9DEw9kuUFe5C3zLbVjV2PzT6qzbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0 h1:b6SYIuLRs88ztox4EyrvRti80uXIFy+Sqzoh9kFULbs=
|
||||
go.opentelemetry.io/otel/sdk v1.36.0/go.mod h1:+lC+mTgD+MUWfjJubi2vvXWcVxyr9rmlshZni72pXeY=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0 h1:r0ntwwGosWGaa0CrSt8cuNuTcccMXERFwHX4dThiPis=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.36.0/go.mod h1:qTNOhFDfKRwX0yXOqJYegL5WRaW376QbB7P4Pb0qva4=
|
||||
go.opentelemetry.io/otel/trace v1.36.0 h1:ahxWNuqZjpdiFAyrIoQ4GIiAIhxAunQR6MUoKrsNd4w=
|
||||
go.opentelemetry.io/otel/trace v1.36.0/go.mod h1:gQ+OnDZzrybY4k4seLzPAWNwVBBVlF2szhehOBB/tGA=
|
||||
go.opentelemetry.io/otel v1.37.0 h1:9zhNfelUvx0KBfu/gb+ZgeAfAgtWrfHJZcAqFC228wQ=
|
||||
go.opentelemetry.io/otel v1.37.0/go.mod h1:ehE/umFRLnuLa/vSccNq9oS1ErUlkkK71gMcN34UG8I=
|
||||
go.opentelemetry.io/otel/metric v1.37.0 h1:mvwbQS5m0tbmqML4NqK+e3aDiO02vsf/WgbsdpcPoZE=
|
||||
go.opentelemetry.io/otel/metric v1.37.0/go.mod h1:04wGrZurHYKOc+RKeye86GwKiTb9FKm1WHtO+4EVr2E=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0 h1:ItB0QUqnjesGRvNcmAcU0LyvkVyGJ2xftD29bWdDvKI=
|
||||
go.opentelemetry.io/otel/sdk v1.37.0/go.mod h1:VredYzxUvuo2q3WRcDnKDjbdvmO0sCzOvVAiY+yUkAg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0 h1:90lI228XrB9jCMuSdA0673aubgRobVZFhbjxHHspCPc=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.37.0/go.mod h1:cNen4ZWfiD37l5NhS+Keb5RXVWZWpRE+9WyVCpbo5ps=
|
||||
go.opentelemetry.io/otel/trace v1.37.0 h1:HLdcFNbRQBE2imdSEgm/kwqmQj1Or1l/7bW6mxVK7z4=
|
||||
go.opentelemetry.io/otel/trace v1.37.0/go.mod h1:TlgrlQ+PtQO5XFerSPUYG0JSgGyryXewPGyayAWSBS0=
|
||||
go.uber.org/atomic v1.11.0 h1:ZvwS0R+56ePWxUNi+Atn9dWONBPp/AUETXlHW0DxSjE=
|
||||
go.uber.org/atomic v1.11.0/go.mod h1:LUxbIzbOniOlMKjJjyPfpl4v+PKK2cNJn91OQbhoJI0=
|
||||
go.uber.org/mock v0.5.2 h1:LbtPTcP8A5k9WPXj54PPPbjcI4Y6lhyOZXn+VS7wNko=
|
||||
go.uber.org/mock v0.5.2/go.mod h1:wLlUxC2vVTPTaE3UD51E0BGOAElKrILxhVSDYQLld5o=
|
||||
go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
||||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
|
||||
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.42.0 h1:chiH31gIWm57EkTXpwnqf8qeuMUi0yekh6mT2AvFlqI=
|
||||
golang.org/x/crypto v0.42.0/go.mod h1:4+rDnOTJhQCx2q7/j6rAN5XDw8kPjeaXEUR2eL94ix8=
|
||||
golang.org/x/mod v0.28.0 h1:gQBtGhjxykdjY9YhZpSlZIsbnaE2+PgjfLWUQTnoZ1U=
|
||||
golang.org/x/mod v0.28.0/go.mod h1:yfB/L0NOf/kmEbXjzCPOx1iK1fRutOydrCMsqRhEBxI=
|
||||
golang.org/x/net v0.44.0 h1:evd8IRDyfNBMBTTY5XRF1vaZlD+EmWx6x8PkhR04H/I=
|
||||
golang.org/x/net v0.44.0/go.mod h1:ECOoLqd5U3Lhyeyo/QDCEVQ4sNgYsqvCZ722XogGieY=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.29.0 h1:1neNs90w9YzJ9BocxfsQNHKuAT4pkghyXc4nhZ6sJvk=
|
||||
golang.org/x/text v0.29.0/go.mod h1:7MhJOA9CD2qZyOKYazxdYMF85OwPdEr9jTtBpO7ydH4=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
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=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+ZbWg+4sHnLp52d5yiIPUxMBSt4X9A=
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b h1:zPKJod4w6F1+nRGDI9ubnXYhU9NSWoFAijkHkUXeTK8=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250804133106-a7a43d27e69b/go.mod h1:qQ0YXyHHx3XkvlzUtpXDkS29lDSafHMZBAZDc03LQ3A=
|
||||
google.golang.org/grpc v1.74.2 h1:WoosgB65DlWVC9FqI82dGsZhWFNBSLjQ84bjROOpMu4=
|
||||
google.golang.org/grpc v1.74.2/go.mod h1:CtQ+BGjaAIXHs/5YS3i473GqwBBa1zGQNevxdeBEXrM=
|
||||
google.golang.org/protobuf v1.36.7 h1:IgrO7UwFQGJdRNXH/sQux4R1Dj1WAKcLElzeeRaXV2A=
|
||||
google.golang.org/protobuf v1.36.7/go.mod h1:jduwjTPXsFjZGTmRluh+L6NjiWu7pchiJ2/5YcXBHnY=
|
||||
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
|
||||
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090 h1:/OQuEa4YWtDt7uQWHd3q3sUMb+QOLQUg1xa8CEsRv5w=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250908214217-97024824d090/go.mod h1:GmFNa4BdJZ2a8G+wCe9Bg3wwThLrJun751XstdJt5Og=
|
||||
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI=
|
||||
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ=
|
||||
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw=
|
||||
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU=
|
||||
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
|
||||
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=
|
||||
@@ -243,8 +249,8 @@ gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ=
|
||||
gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8=
|
||||
gorm.io/gorm v1.30.1 h1:lSHg33jJTBxs2mgJRfRZeLDG+WZaHYCk3Wtfl6Ngzo4=
|
||||
gorm.io/gorm v1.30.1/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gorm.io/gorm v1.30.5 h1:dvEfYwxL+i+xgCNSGGBT1lDjCzfELK8fHZxL3Ee9X0s=
|
||||
gorm.io/gorm v1.30.5/go.mod h1:8Z33v652h4//uMA76KjeDH8mJXPm1QNCYrMeatR0DOE=
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c h1:m/r7OM+Y2Ty1sgBQ7Qb27VgIMBW8ZZhT4gLnUyDIhzI=
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c/go.mod h1:3r5CMtNQMKIvBlrmM9xWUNamjKBYPOWyXOjmg5Kts3g=
|
||||
lukechampine.com/blake3 v1.4.1 h1:I3Smz7gso8w4/TunLKec6K2fn+kyKtDxr/xcQEN84Wg=
|
||||
|
||||
19
install.sh
19
install.sh
@@ -7,7 +7,6 @@ yellow='\033[0;33m'
|
||||
plain='\033[0m'
|
||||
|
||||
cur_dir=$(pwd)
|
||||
show_ip_service_lists=("https://api.ipify.org" "https://4.ident.me")
|
||||
|
||||
# check root
|
||||
[[ $EUID -ne 0 ]] && echo -e "${red}Fatal error: ${plain} Please run this script with root privilege \n " && exit 1
|
||||
@@ -58,7 +57,7 @@ install_base() {
|
||||
zypper refresh && zypper -q install -y wget curl tar timezone
|
||||
;;
|
||||
*)
|
||||
apt-get update && apt install -y -q wget curl tar tzdata
|
||||
apt-get update && apt-get install -y -q wget curl tar tzdata
|
||||
;;
|
||||
esac
|
||||
}
|
||||
@@ -73,10 +72,18 @@ config_after_install() {
|
||||
local existing_hasDefaultCredential=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'hasDefaultCredential: .+' | awk '{print $2}')
|
||||
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
|
||||
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||
|
||||
for ip_service_addr in "${show_ip_service_lists[@]}"; do
|
||||
local server_ip=$(curl -s --max-time 3 ${ip_service_addr} 2>/dev/null)
|
||||
if [ -n "${server_ip}" ]; then
|
||||
local URL_lists=(
|
||||
"https://api4.ipify.org"
|
||||
"https://ipv4.icanhazip.com"
|
||||
"https://v4.api.ipinfo.io/ip"
|
||||
"https://ipv4.myexternalip.com/raw"
|
||||
"https://4.ident.me"
|
||||
"https://check-host.net/ip"
|
||||
)
|
||||
local server_ip=""
|
||||
for ip_address in "${URL_lists[@]}"; do
|
||||
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||
if [[ -n "${server_ip}" ]]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
|
||||
@@ -13,7 +13,7 @@
|
||||
"inbounds": [
|
||||
{
|
||||
"port": 10808,
|
||||
"protocol": "socks",
|
||||
"protocol": "mixed",
|
||||
"settings": {
|
||||
"auth": "noauth",
|
||||
"udp": true,
|
||||
@@ -28,7 +28,7 @@
|
||||
],
|
||||
"enabled": true
|
||||
},
|
||||
"tag": "socks"
|
||||
"tag": "mixed"
|
||||
},
|
||||
{
|
||||
"port": 10809,
|
||||
|
||||
92
sub/sub.go
92
sub/sub.go
@@ -3,14 +3,19 @@ package sub
|
||||
import (
|
||||
"context"
|
||||
"crypto/tls"
|
||||
"html/template"
|
||||
"io"
|
||||
"io/fs"
|
||||
"net"
|
||||
"net/http"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strconv"
|
||||
|
||||
"x-ui/config"
|
||||
"x-ui/logger"
|
||||
"x-ui/util/common"
|
||||
webpkg "x-ui/web"
|
||||
"x-ui/web/locale"
|
||||
"x-ui/web/middleware"
|
||||
"x-ui/web/network"
|
||||
"x-ui/web/service"
|
||||
@@ -18,6 +23,21 @@ import (
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
|
||||
// setEmbeddedTemplates parses and sets embedded templates on the engine
|
||||
func setEmbeddedTemplates(engine *gin.Engine) error {
|
||||
t, err := template.New("").Funcs(engine.FuncMap).ParseFS(
|
||||
webpkg.EmbeddedHTML(),
|
||||
"html/common/page.html",
|
||||
"html/component/aThemeSwitch.html",
|
||||
"html/subscription.html",
|
||||
)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
engine.SetHTMLTemplate(t)
|
||||
return nil
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
@@ -38,13 +58,10 @@ func NewServer() *Server {
|
||||
}
|
||||
|
||||
func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
if config.IsDebug() {
|
||||
gin.SetMode(gin.DebugMode)
|
||||
} else {
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
}
|
||||
// Always run in release mode for the subscription server
|
||||
gin.DefaultWriter = io.Discard
|
||||
gin.DefaultErrorWriter = io.Discard
|
||||
gin.SetMode(gin.ReleaseMode)
|
||||
|
||||
engine := gin.Default()
|
||||
|
||||
@@ -57,6 +74,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
engine.Use(middleware.DomainValidatorMiddleware(subDomain))
|
||||
}
|
||||
|
||||
// Provide base_path in context for templates
|
||||
engine.Use(func(c *gin.Context) {
|
||||
c.Set("base_path", "/")
|
||||
})
|
||||
|
||||
LinksPath, err := s.settingService.GetSubPath()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -112,6 +134,36 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
SubTitle = ""
|
||||
}
|
||||
|
||||
// set per-request localizer from headers/cookies
|
||||
engine.Use(locale.LocalizerMiddleware())
|
||||
|
||||
// register i18n function similar to web server
|
||||
i18nWebFunc := func(key string, params ...string) string {
|
||||
return locale.I18n(locale.Web, key, params...)
|
||||
}
|
||||
engine.SetFuncMap(map[string]any{"i18n": i18nWebFunc})
|
||||
|
||||
// Templates: prefer embedded; fallback to disk if necessary
|
||||
if err := setEmbeddedTemplates(engine); err != nil {
|
||||
logger.Warning("sub: failed to parse embedded templates:", err)
|
||||
if files, derr := s.getHtmlFiles(); derr == nil {
|
||||
engine.LoadHTMLFiles(files...)
|
||||
} else {
|
||||
logger.Error("sub: no templates available (embedded parse and disk load failed)", err, derr)
|
||||
}
|
||||
}
|
||||
|
||||
// Assets: use disk if present, fallback to embedded
|
||||
if _, err := os.Stat("web/assets"); err == nil {
|
||||
engine.StaticFS("/assets", http.FS(os.DirFS("web/assets")))
|
||||
} else {
|
||||
if subFS, err := fs.Sub(webpkg.EmbeddedAssets(), "assets"); err == nil {
|
||||
engine.StaticFS("/assets", http.FS(subFS))
|
||||
} else {
|
||||
logger.Error("sub: failed to mount embedded assets:", err)
|
||||
}
|
||||
}
|
||||
|
||||
g := engine.Group("/")
|
||||
|
||||
s.sub = NewSUBController(
|
||||
@@ -121,6 +173,30 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
return engine, nil
|
||||
}
|
||||
|
||||
// getHtmlFiles loads templates from local folder (used in debug mode)
|
||||
func (s *Server) getHtmlFiles() ([]string, error) {
|
||||
dir, _ := os.Getwd()
|
||||
files := []string{}
|
||||
// common layout
|
||||
common := filepath.Join(dir, "web", "html", "common", "page.html")
|
||||
if _, err := os.Stat(common); err == nil {
|
||||
files = append(files, common)
|
||||
}
|
||||
// components used
|
||||
theme := filepath.Join(dir, "web", "html", "component", "aThemeSwitch.html")
|
||||
if _, err := os.Stat(theme); err == nil {
|
||||
files = append(files, theme)
|
||||
}
|
||||
// page itself
|
||||
page := filepath.Join(dir, "web", "html", "subscription.html")
|
||||
if _, err := os.Stat(page); err == nil {
|
||||
files = append(files, page)
|
||||
} else {
|
||||
return nil, err
|
||||
}
|
||||
return files, nil
|
||||
}
|
||||
|
||||
func (s *Server) Start() (err error) {
|
||||
// This is an anonymous function, no function name
|
||||
defer func() {
|
||||
|
||||
@@ -2,8 +2,8 @@ package sub
|
||||
|
||||
import (
|
||||
"encoding/base64"
|
||||
"net"
|
||||
"strings"
|
||||
"x-ui/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
)
|
||||
@@ -53,27 +53,13 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
||||
gJson := g.Group(a.subJsonPath)
|
||||
|
||||
gLink.GET(":subid", a.subs)
|
||||
|
||||
gJson.GET(":subid", a.subJsons)
|
||||
}
|
||||
|
||||
func (a *SUBController) subs(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
var host string
|
||||
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
|
||||
host = h
|
||||
}
|
||||
if host == "" {
|
||||
host = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if host == "" {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
}
|
||||
subs, header, err := a.subService.GetSubs(subId, host)
|
||||
scheme, host, hostWithPort, hostHeader := a.subService.ResolveRequest(c)
|
||||
subs, header, lastOnline, err := a.subService.GetSubs(subId, host)
|
||||
if err != nil || len(subs) == 0 {
|
||||
c.String(400, "Error!")
|
||||
} else {
|
||||
@@ -82,10 +68,38 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||
result += sub + "\n"
|
||||
}
|
||||
|
||||
// If the request expects HTML (e.g., browser) or explicitly asked (?html=1 or ?view=html), render the info page here
|
||||
accept := c.GetHeader("Accept")
|
||||
if strings.Contains(strings.ToLower(accept), "text/html") || c.Query("html") == "1" || strings.EqualFold(c.Query("view"), "html") {
|
||||
// Build page data in service
|
||||
subURL, subJsonURL := a.subService.BuildURLs(scheme, hostWithPort, a.subPath, a.subJsonPath, subId)
|
||||
page := a.subService.BuildPageData(subId, hostHeader, header, lastOnline, subs, subURL, subJsonURL)
|
||||
c.HTML(200, "subscription.html", gin.H{
|
||||
"title": "subscription.title",
|
||||
"cur_ver": config.GetVersion(),
|
||||
"host": page.Host,
|
||||
"base_path": page.BasePath,
|
||||
"sId": page.SId,
|
||||
"download": page.Download,
|
||||
"upload": page.Upload,
|
||||
"total": page.Total,
|
||||
"used": page.Used,
|
||||
"remained": page.Remained,
|
||||
"expire": page.Expire,
|
||||
"lastOnline": page.LastOnline,
|
||||
"datepicker": page.Datepicker,
|
||||
"downloadByte": page.DownloadByte,
|
||||
"uploadByte": page.UploadByte,
|
||||
"totalByte": page.TotalByte,
|
||||
"subUrl": page.SubUrl,
|
||||
"subJsonUrl": page.SubJsonUrl,
|
||||
"result": page.Result,
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// Add headers
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
|
||||
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
|
||||
|
||||
if a.subEncrypt {
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
@@ -97,41 +111,21 @@ func (a *SUBController) subs(c *gin.Context) {
|
||||
|
||||
func (a *SUBController) subJsons(c *gin.Context) {
|
||||
subId := c.Param("subid")
|
||||
var host string
|
||||
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil {
|
||||
host = h
|
||||
}
|
||||
if host == "" {
|
||||
host = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if host == "" {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
}
|
||||
_, host, _, _ := a.subService.ResolveRequest(c)
|
||||
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", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
|
||||
a.ApplyCommonHeaders(c, header, a.updateInterval, a.subTitle)
|
||||
|
||||
c.String(200, jsonSub)
|
||||
}
|
||||
}
|
||||
|
||||
func getHostFromXFH(s string) (string, error) {
|
||||
if strings.Contains(s, ":") {
|
||||
realHost, _, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return realHost, nil
|
||||
}
|
||||
return s, nil
|
||||
func (a *SUBController) ApplyCommonHeaders(c *gin.Context, header, updateInterval, profileTitle string) {
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(profileTitle)))
|
||||
}
|
||||
|
||||
@@ -184,8 +184,14 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
||||
var newOutbounds []json_util.RawMessage
|
||||
|
||||
switch inbound.Protocol {
|
||||
case "vmess", "vless":
|
||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))
|
||||
case "vmess":
|
||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client, ""))
|
||||
case "vless":
|
||||
var vlessSettings model.VLESSSettings
|
||||
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
|
||||
|
||||
newOutbounds = append(newOutbounds,
|
||||
s.genVnext(inbound, streamSettings, client, vlessSettings.Encryption))
|
||||
case "trojan", "shadowsocks":
|
||||
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
|
||||
}
|
||||
@@ -209,9 +215,10 @@ func (s *SubJsonService) streamData(stream string) map[string]any {
|
||||
var streamSettings map[string]any
|
||||
json.Unmarshal([]byte(stream), &streamSettings)
|
||||
security, _ := streamSettings["security"].(string)
|
||||
if security == "tls" {
|
||||
switch security {
|
||||
case "tls":
|
||||
streamSettings["tlsSettings"] = s.tlsData(streamSettings["tlsSettings"].(map[string]any))
|
||||
} else if security == "reality" {
|
||||
case "reality":
|
||||
streamSettings["realitySettings"] = s.realityData(streamSettings["realitySettings"].(map[string]any))
|
||||
}
|
||||
delete(streamSettings, "sockopt")
|
||||
@@ -283,7 +290,7 @@ func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
|
||||
return rltyData
|
||||
}
|
||||
|
||||
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, encryption string) json_util.RawMessage {
|
||||
outbound := Outbound{}
|
||||
usersData := make([]UserVnext, 1)
|
||||
|
||||
@@ -294,7 +301,7 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
|
||||
}
|
||||
if inbound.Protocol == model.VLESS {
|
||||
usersData[0].Flow = client.Flow
|
||||
usersData[0].Encryption = "none"
|
||||
usersData[0].Encryption = encryption
|
||||
}
|
||||
|
||||
vnextData := make([]VnextSetting, 1)
|
||||
|
||||
@@ -3,10 +3,15 @@ package sub
|
||||
import (
|
||||
"encoding/base64"
|
||||
"fmt"
|
||||
"net"
|
||||
"net/url"
|
||||
"strconv"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"github.com/goccy/go-json"
|
||||
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
@@ -14,8 +19,6 @@ import (
|
||||
"x-ui/util/random"
|
||||
"x-ui/web/service"
|
||||
"x-ui/xray"
|
||||
|
||||
"github.com/goccy/go-json"
|
||||
)
|
||||
|
||||
type SubService struct {
|
||||
@@ -34,19 +37,20 @@ func NewSubService(showInfo bool, remarkModel string) *SubService {
|
||||
}
|
||||
}
|
||||
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, string, error) {
|
||||
func (s *SubService) GetSubs(subId string, host string) ([]string, string, int64, error) {
|
||||
s.address = host
|
||||
var result []string
|
||||
var header string
|
||||
var traffic xray.ClientTraffic
|
||||
var lastOnline int64
|
||||
var clientTraffics []xray.ClientTraffic
|
||||
inbounds, err := s.getInboundsBySubId(subId)
|
||||
if err != nil {
|
||||
return nil, "", err
|
||||
return nil, "", 0, err
|
||||
}
|
||||
|
||||
if len(inbounds) == 0 {
|
||||
return nil, "", common.NewError("No inbounds found with ", subId)
|
||||
return nil, "", 0, common.NewError("No inbounds found with ", subId)
|
||||
}
|
||||
|
||||
s.datepicker, err = s.settingService.GetDatepicker()
|
||||
@@ -73,7 +77,11 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
|
||||
if client.Enable && client.SubID == subId {
|
||||
link := s.getLink(inbound, client.Email)
|
||||
result = append(result, link)
|
||||
clientTraffics = append(clientTraffics, s.getClientTraffics(inbound.ClientStats, client.Email))
|
||||
ct := s.getClientTraffics(inbound.ClientStats, client.Email)
|
||||
clientTraffics = append(clientTraffics, ct)
|
||||
if ct.LastOnline > lastOnline {
|
||||
lastOnline = ct.LastOnline
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -101,7 +109,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, string, error
|
||||
}
|
||||
}
|
||||
header = fmt.Sprintf("upload=%d; download=%d; total=%d; expire=%d", traffic.Up, traffic.Down, traffic.Total, traffic.ExpiryTime/1000)
|
||||
return result, header, nil
|
||||
return result, header, lastOnline, nil
|
||||
}
|
||||
|
||||
func (s *SubService) getInboundsBySubId(subId string) ([]*model.Inbound, error) {
|
||||
@@ -313,6 +321,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
if inbound.Protocol != model.VLESS {
|
||||
return ""
|
||||
}
|
||||
var vlessSettings model.VLESSSettings
|
||||
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
|
||||
|
||||
var stream map[string]any
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
@@ -327,6 +338,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
||||
port := inbound.Port
|
||||
streamNetwork := stream["network"].(string)
|
||||
params := make(map[string]string)
|
||||
if vlessSettings.Encryption != "" {
|
||||
params["encryption"] = vlessSettings.Encryption
|
||||
}
|
||||
params["type"] = streamNetwork
|
||||
|
||||
switch streamNetwork {
|
||||
@@ -995,3 +1009,172 @@ func searchHost(headers any) string {
|
||||
|
||||
return ""
|
||||
}
|
||||
|
||||
// PageData is a view model for subscription.html
|
||||
type PageData struct {
|
||||
Host string
|
||||
BasePath string
|
||||
SId string
|
||||
Download string
|
||||
Upload string
|
||||
Total string
|
||||
Used string
|
||||
Remained string
|
||||
Expire int64
|
||||
LastOnline int64
|
||||
Datepicker string
|
||||
DownloadByte int64
|
||||
UploadByte int64
|
||||
TotalByte int64
|
||||
SubUrl string
|
||||
SubJsonUrl string
|
||||
Result []string
|
||||
}
|
||||
|
||||
// ResolveRequest extracts scheme and host info from request/headers consistently.
|
||||
func (s *SubService) ResolveRequest(c *gin.Context) (scheme string, host string, hostWithPort string, hostHeader string) {
|
||||
// scheme
|
||||
scheme = "http"
|
||||
if c.Request.TLS != nil || strings.EqualFold(c.GetHeader("X-Forwarded-Proto"), "https") {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// base host (no port)
|
||||
if h, err := getHostFromXFH(c.GetHeader("X-Forwarded-Host")); err == nil && h != "" {
|
||||
host = h
|
||||
}
|
||||
if host == "" {
|
||||
host = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if host == "" {
|
||||
var err error
|
||||
host, _, err = net.SplitHostPort(c.Request.Host)
|
||||
if err != nil {
|
||||
host = c.Request.Host
|
||||
}
|
||||
}
|
||||
|
||||
// host:port for URLs
|
||||
hostWithPort = c.GetHeader("X-Forwarded-Host")
|
||||
if hostWithPort == "" {
|
||||
hostWithPort = c.Request.Host
|
||||
}
|
||||
if hostWithPort == "" {
|
||||
hostWithPort = host
|
||||
}
|
||||
|
||||
// header display host
|
||||
hostHeader = c.GetHeader("X-Forwarded-Host")
|
||||
if hostHeader == "" {
|
||||
hostHeader = c.GetHeader("X-Real-IP")
|
||||
}
|
||||
if hostHeader == "" {
|
||||
hostHeader = host
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// BuildURLs constructs absolute subscription and json URLs.
|
||||
func (s *SubService) BuildURLs(scheme, hostWithPort, subPath, subJsonPath, subId string) (subURL, subJsonURL string) {
|
||||
if strings.HasSuffix(subPath, "/") {
|
||||
subURL = scheme + "://" + hostWithPort + subPath + subId
|
||||
} else {
|
||||
subURL = scheme + "://" + hostWithPort + strings.TrimRight(subPath, "/") + "/" + subId
|
||||
}
|
||||
if strings.HasSuffix(subJsonPath, "/") {
|
||||
subJsonURL = scheme + "://" + hostWithPort + subJsonPath + subId
|
||||
} else {
|
||||
subJsonURL = scheme + "://" + hostWithPort + strings.TrimRight(subJsonPath, "/") + "/" + subId
|
||||
}
|
||||
return
|
||||
}
|
||||
|
||||
// BuildPageData parses header and prepares the template view model.
|
||||
func (s *SubService) BuildPageData(subId, hostHeader, header string, lastOnline int64, subs []string, subURL, subJsonURL string) PageData {
|
||||
// Parse header values
|
||||
var uploadByte, downloadByte, totalByte, expire int64
|
||||
parts := strings.Split(header, ";")
|
||||
for _, p := range parts {
|
||||
kv := strings.Split(strings.TrimSpace(p), "=")
|
||||
if len(kv) != 2 {
|
||||
continue
|
||||
}
|
||||
key := strings.ToLower(strings.TrimSpace(kv[0]))
|
||||
val := strings.TrimSpace(kv[1])
|
||||
switch key {
|
||||
case "upload":
|
||||
if v, err := parseInt64(val); err == nil {
|
||||
uploadByte = v
|
||||
}
|
||||
case "download":
|
||||
if v, err := parseInt64(val); err == nil {
|
||||
downloadByte = v
|
||||
}
|
||||
case "total":
|
||||
if v, err := parseInt64(val); err == nil {
|
||||
totalByte = v
|
||||
}
|
||||
case "expire":
|
||||
if v, err := parseInt64(val); err == nil {
|
||||
expire = v
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
download := common.FormatTraffic(downloadByte)
|
||||
upload := common.FormatTraffic(uploadByte)
|
||||
total := "∞"
|
||||
used := common.FormatTraffic(uploadByte + downloadByte)
|
||||
remained := ""
|
||||
if totalByte > 0 {
|
||||
total = common.FormatTraffic(totalByte)
|
||||
left := totalByte - (uploadByte + downloadByte)
|
||||
if left < 0 {
|
||||
left = 0
|
||||
}
|
||||
remained = common.FormatTraffic(left)
|
||||
}
|
||||
|
||||
datepicker := s.datepicker
|
||||
if datepicker == "" {
|
||||
datepicker = "gregorian"
|
||||
}
|
||||
|
||||
return PageData{
|
||||
Host: hostHeader,
|
||||
BasePath: "/",
|
||||
SId: subId,
|
||||
Download: download,
|
||||
Upload: upload,
|
||||
Total: total,
|
||||
Used: used,
|
||||
Remained: remained,
|
||||
Expire: expire,
|
||||
LastOnline: lastOnline,
|
||||
Datepicker: datepicker,
|
||||
DownloadByte: downloadByte,
|
||||
UploadByte: uploadByte,
|
||||
TotalByte: totalByte,
|
||||
SubUrl: subURL,
|
||||
SubJsonUrl: subJsonURL,
|
||||
Result: subs,
|
||||
}
|
||||
}
|
||||
|
||||
func getHostFromXFH(s string) (string, error) {
|
||||
if strings.Contains(s, ":") {
|
||||
realHost, _, err := net.SplitHostPort(s)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
return realHost, nil
|
||||
}
|
||||
return s, nil
|
||||
}
|
||||
|
||||
func parseInt64(s string) (int64, error) {
|
||||
// handle potential quotes
|
||||
s = strings.Trim(s, "\"'")
|
||||
n, err := strconv.ParseInt(s, 10, 64)
|
||||
return n, err
|
||||
}
|
||||
|
||||
File diff suppressed because one or more lines are too long
@@ -1,7 +0,0 @@
|
||||
@import "../lib/style/index.less";
|
||||
@import "../lib/style/components.less";
|
||||
|
||||
@green-6: #008771;
|
||||
@primary-color: @green-6;
|
||||
@border-radius-base: 1rem;
|
||||
@progress-remaining-color: #EDEDED;
|
||||
File diff suppressed because one or more lines are too long
4
web/assets/axios/axios.min.js
vendored
4
web/assets/axios/axios.min.js
vendored
File diff suppressed because one or more lines are too long
2
web/assets/css/custom.min.css
vendored
2
web/assets/css/custom.min.css
vendored
File diff suppressed because one or more lines are too long
@@ -6,6 +6,7 @@ class DBInbound {
|
||||
this.up = 0;
|
||||
this.down = 0;
|
||||
this.total = 0;
|
||||
this.allTime = 0;
|
||||
this.remark = "";
|
||||
this.enable = true;
|
||||
this.expiryTime = 0;
|
||||
@@ -48,8 +49,8 @@ class DBInbound {
|
||||
return this.protocol === Protocols.SHADOWSOCKS;
|
||||
}
|
||||
|
||||
get isSocks() {
|
||||
return this.protocol === Protocols.SOCKS;
|
||||
get isMixed() {
|
||||
return this.protocol === Protocols.MIXED;
|
||||
}
|
||||
|
||||
get isHTTP() {
|
||||
|
||||
@@ -3,18 +3,16 @@ const Protocols = {
|
||||
VLESS: 'vless',
|
||||
TROJAN: 'trojan',
|
||||
SHADOWSOCKS: 'shadowsocks',
|
||||
DOKODEMO: 'dokodemo-door',
|
||||
SOCKS: 'socks',
|
||||
TUNNEL: 'tunnel',
|
||||
MIXED: 'mixed',
|
||||
HTTP: 'http',
|
||||
WIREGUARD: 'wireguard',
|
||||
};
|
||||
|
||||
const SSMethods = {
|
||||
AES_256_GCM: 'aes-256-gcm',
|
||||
AES_128_GCM: 'aes-128-gcm',
|
||||
CHACHA20_POLY1305: 'chacha20-poly1305',
|
||||
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
|
||||
XCHACHA20_POLY1305: 'xchacha20-poly1305',
|
||||
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
|
||||
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
||||
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
||||
@@ -731,7 +729,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
||||
constructor(
|
||||
show = false,
|
||||
xver = 0,
|
||||
dest = 'google.com:443',
|
||||
target = 'google.com:443',
|
||||
serverNames = 'google.com,www.google.com',
|
||||
privateKey = '',
|
||||
minClientVer = '',
|
||||
@@ -744,7 +742,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
||||
super();
|
||||
this.show = show;
|
||||
this.xver = xver;
|
||||
this.dest = dest;
|
||||
this.target = target;
|
||||
this.serverNames = Array.isArray(serverNames) ? serverNames.join(",") : serverNames;
|
||||
this.privateKey = privateKey;
|
||||
this.minClientVer = minClientVer;
|
||||
@@ -769,7 +767,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
||||
return new RealityStreamSettings(
|
||||
json.show,
|
||||
json.xver,
|
||||
json.dest,
|
||||
json.target,
|
||||
json.serverNames,
|
||||
json.privateKey,
|
||||
json.minClientVer,
|
||||
@@ -785,7 +783,7 @@ class RealityStreamSettings extends XrayCommonClass {
|
||||
return {
|
||||
show: this.show,
|
||||
xver: this.xver,
|
||||
dest: this.dest,
|
||||
target: this.target,
|
||||
serverNames: this.serverNames.split(","),
|
||||
privateKey: this.privateKey,
|
||||
minClientVer: this.minClientVer,
|
||||
@@ -1042,27 +1040,6 @@ class Sniffing extends XrayCommonClass {
|
||||
}
|
||||
}
|
||||
|
||||
class Allocate extends XrayCommonClass {
|
||||
constructor(
|
||||
strategy = "always",
|
||||
refresh = 5,
|
||||
concurrency = 3,
|
||||
) {
|
||||
super();
|
||||
this.strategy = strategy;
|
||||
this.refresh = refresh;
|
||||
this.concurrency = concurrency;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Allocate(
|
||||
json.strategy,
|
||||
json.refresh,
|
||||
json.concurrency,
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
class Inbound extends XrayCommonClass {
|
||||
constructor(
|
||||
port = RandomUtil.randomInteger(10000, 60000),
|
||||
@@ -1072,7 +1049,6 @@ class Inbound extends XrayCommonClass {
|
||||
streamSettings = new StreamSettings(),
|
||||
tag = '',
|
||||
sniffing = new Sniffing(),
|
||||
allocate = new Allocate(),
|
||||
clientStats = '',
|
||||
) {
|
||||
super();
|
||||
@@ -1083,7 +1059,6 @@ class Inbound extends XrayCommonClass {
|
||||
this.stream = streamSettings;
|
||||
this.tag = tag;
|
||||
this.sniffing = sniffing;
|
||||
this.allocate = allocate;
|
||||
this.clientStats = clientStats;
|
||||
}
|
||||
getClientStats() {
|
||||
@@ -1248,7 +1223,6 @@ class Inbound extends XrayCommonClass {
|
||||
this.stream = new StreamSettings();
|
||||
this.tag = '';
|
||||
this.sniffing = new Sniffing();
|
||||
this.allocate = new Allocate();
|
||||
}
|
||||
|
||||
genVmessLink(address = '', port = this.port, forceTls, remark = '', clientId, security) {
|
||||
@@ -1325,6 +1299,7 @@ class Inbound extends XrayCommonClass {
|
||||
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
||||
const params = new Map();
|
||||
params.set("type", this.stream.network);
|
||||
params.set("encryption", this.settings.encryption);
|
||||
switch (type) {
|
||||
case "tcp":
|
||||
const tcp = this.stream.tcp;
|
||||
@@ -1703,14 +1678,13 @@ class Inbound extends XrayCommonClass {
|
||||
StreamSettings.fromJson(json.streamSettings),
|
||||
json.tag,
|
||||
Sniffing.fromJson(json.sniffing),
|
||||
Allocate.fromJson(json.allocate),
|
||||
json.clientStats
|
||||
)
|
||||
}
|
||||
|
||||
toJson() {
|
||||
let streamSettings;
|
||||
if (this.canEnableStream()) {
|
||||
if (this.canEnableStream() || this.stream?.sockopt) {
|
||||
streamSettings = this.stream.toJson();
|
||||
}
|
||||
return {
|
||||
@@ -1721,7 +1695,6 @@ class Inbound extends XrayCommonClass {
|
||||
streamSettings: streamSettings,
|
||||
tag: this.tag,
|
||||
sniffing: this.sniffing.toJson(),
|
||||
allocate: this.allocate.toJson(),
|
||||
clientStats: this.clientStats
|
||||
};
|
||||
}
|
||||
@@ -1739,8 +1712,8 @@ Inbound.Settings = class extends XrayCommonClass {
|
||||
case Protocols.VLESS: return new Inbound.VLESSSettings(protocol);
|
||||
case Protocols.TROJAN: return new Inbound.TrojanSettings(protocol);
|
||||
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings(protocol);
|
||||
case Protocols.DOKODEMO: return new Inbound.DokodemoSettings(protocol);
|
||||
case Protocols.SOCKS: return new Inbound.SocksSettings(protocol);
|
||||
case Protocols.TUNNEL: return new Inbound.TunnelSettings(protocol);
|
||||
case Protocols.MIXED: return new Inbound.MixedSettings(protocol);
|
||||
case Protocols.HTTP: return new Inbound.HttpSettings(protocol);
|
||||
case Protocols.WIREGUARD: return new Inbound.WireguardSettings(protocol);
|
||||
default: return null;
|
||||
@@ -1753,8 +1726,8 @@ Inbound.Settings = class extends XrayCommonClass {
|
||||
case Protocols.VLESS: return Inbound.VLESSSettings.fromJson(json);
|
||||
case Protocols.TROJAN: return Inbound.TrojanSettings.fromJson(json);
|
||||
case Protocols.SHADOWSOCKS: return Inbound.ShadowsocksSettings.fromJson(json);
|
||||
case Protocols.DOKODEMO: return Inbound.DokodemoSettings.fromJson(json);
|
||||
case Protocols.SOCKS: return Inbound.SocksSettings.fromJson(json);
|
||||
case Protocols.TUNNEL: return Inbound.TunnelSettings.fromJson(json);
|
||||
case Protocols.MIXED: return Inbound.MixedSettings.fromJson(json);
|
||||
case Protocols.HTTP: return Inbound.HttpSettings.fromJson(json);
|
||||
case Protocols.WIREGUARD: return Inbound.WireguardSettings.fromJson(json);
|
||||
default: return null;
|
||||
@@ -1817,7 +1790,9 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
||||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
) {
|
||||
super();
|
||||
this.id = id;
|
||||
@@ -1831,6 +1806,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
||||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -1846,6 +1823,8 @@ Inbound.VmessSettings.VMESS = class extends XrayCommonClass {
|
||||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
get _expiryTime() {
|
||||
@@ -1879,13 +1858,17 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
constructor(
|
||||
protocol,
|
||||
vlesses = [new Inbound.VLESSSettings.VLESS()],
|
||||
decryption = 'none',
|
||||
fallbacks = []
|
||||
decryption = "none",
|
||||
encryption = "none",
|
||||
fallbacks = [],
|
||||
selectedAuth = undefined,
|
||||
) {
|
||||
super(protocol);
|
||||
this.vlesses = vlesses;
|
||||
this.decryption = decryption;
|
||||
this.encryption = encryption;
|
||||
this.fallbacks = fallbacks;
|
||||
this.selectedAuth = selectedAuth;
|
||||
}
|
||||
|
||||
addFallback() {
|
||||
@@ -1896,22 +1879,43 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
||||
this.fallbacks.splice(index, 1);
|
||||
}
|
||||
|
||||
// decryption should be set to static value
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.VLESSSettings(
|
||||
const obj = new Inbound.VLESSSettings(
|
||||
Protocols.VLESS,
|
||||
json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
||||
json.decryption || 'none',
|
||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks),);
|
||||
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
||||
json.decryption,
|
||||
json.encryption,
|
||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
|
||||
json.selectedAuth
|
||||
);
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
const json = {
|
||||
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
|
||||
decryption: this.decryption,
|
||||
fallbacks: Inbound.VLESSSettings.toJsonArray(this.fallbacks),
|
||||
};
|
||||
|
||||
if (this.decryption) {
|
||||
json.decryption = this.decryption;
|
||||
}
|
||||
|
||||
if (this.encryption) {
|
||||
json.encryption = this.encryption;
|
||||
}
|
||||
|
||||
if (this.fallbacks && this.fallbacks.length > 0) {
|
||||
json.fallbacks = Inbound.VLESSSettings.toJsonArray(this.fallbacks);
|
||||
}
|
||||
if (this.selectedAuth) {
|
||||
json.selectedAuth = this.selectedAuth;
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
@@ -1926,7 +1930,9 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
) {
|
||||
super();
|
||||
this.id = id;
|
||||
@@ -1940,6 +1946,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -1955,6 +1963,8 @@ Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2065,7 +2075,9 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
) {
|
||||
super();
|
||||
this.password = password;
|
||||
@@ -2078,6 +2090,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
@@ -2092,6 +2106,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
subId: this.subId,
|
||||
comment: this.comment,
|
||||
reset: this.reset,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2107,6 +2123,8 @@ Inbound.TrojanSettings.Trojan = class extends XrayCommonClass {
|
||||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2226,7 +2244,9 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
tgId = '',
|
||||
subId = RandomUtil.randomLowerAndNum(16),
|
||||
comment = '',
|
||||
reset = 0
|
||||
reset = 0,
|
||||
created_at = undefined,
|
||||
updated_at = undefined
|
||||
) {
|
||||
super();
|
||||
this.method = method;
|
||||
@@ -2240,6 +2260,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
this.subId = subId;
|
||||
this.comment = comment;
|
||||
this.reset = reset;
|
||||
this.created_at = created_at;
|
||||
this.updated_at = updated_at;
|
||||
}
|
||||
|
||||
toJson() {
|
||||
@@ -2255,6 +2277,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
subId: this.subId,
|
||||
comment: this.comment,
|
||||
reset: this.reset,
|
||||
created_at: this.created_at,
|
||||
updated_at: this.updated_at,
|
||||
};
|
||||
}
|
||||
|
||||
@@ -2271,6 +2295,8 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
json.subId,
|
||||
json.comment,
|
||||
json.reset,
|
||||
json.created_at,
|
||||
json.updated_at,
|
||||
);
|
||||
}
|
||||
|
||||
@@ -2301,7 +2327,7 @@ Inbound.ShadowsocksSettings.Shadowsocks = class extends XrayCommonClass {
|
||||
|
||||
};
|
||||
|
||||
Inbound.DokodemoSettings = class extends Inbound.Settings {
|
||||
Inbound.TunnelSettings = class extends Inbound.Settings {
|
||||
constructor(
|
||||
protocol,
|
||||
address,
|
||||
@@ -2319,8 +2345,8 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.DokodemoSettings(
|
||||
Protocols.DOKODEMO,
|
||||
return new Inbound.TunnelSettings(
|
||||
Protocols.TUNNEL,
|
||||
json.address,
|
||||
json.port,
|
||||
XrayCommonClass.toHeaders(json.portMap),
|
||||
@@ -2340,8 +2366,8 @@ Inbound.DokodemoSettings = class extends Inbound.Settings {
|
||||
}
|
||||
};
|
||||
|
||||
Inbound.SocksSettings = class extends Inbound.Settings {
|
||||
constructor(protocol, auth = 'password', accounts = [new Inbound.SocksSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
|
||||
Inbound.MixedSettings = class extends Inbound.Settings {
|
||||
constructor(protocol, auth = 'password', accounts = [new Inbound.MixedSettings.SocksAccount()], udp = false, ip = '127.0.0.1') {
|
||||
super(protocol);
|
||||
this.auth = auth;
|
||||
this.accounts = accounts;
|
||||
@@ -2361,11 +2387,11 @@ Inbound.SocksSettings = class extends Inbound.Settings {
|
||||
let accounts;
|
||||
if (json.auth === 'password') {
|
||||
accounts = json.accounts.map(
|
||||
account => Inbound.SocksSettings.SocksAccount.fromJson(account)
|
||||
account => Inbound.MixedSettings.SocksAccount.fromJson(account)
|
||||
)
|
||||
}
|
||||
return new Inbound.SocksSettings(
|
||||
Protocols.SOCKS,
|
||||
return new Inbound.MixedSettings(
|
||||
Protocols.MIXED,
|
||||
json.auth,
|
||||
accounts,
|
||||
json.udp,
|
||||
@@ -2382,7 +2408,7 @@ Inbound.SocksSettings = class extends Inbound.Settings {
|
||||
};
|
||||
}
|
||||
};
|
||||
Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
|
||||
Inbound.MixedSettings.SocksAccount = class extends XrayCommonClass {
|
||||
constructor(user = RandomUtil.randomSeq(10), pass = RandomUtil.randomSeq(10)) {
|
||||
super();
|
||||
this.user = user;
|
||||
@@ -2390,7 +2416,7 @@ Inbound.SocksSettings.SocksAccount = class extends XrayCommonClass {
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.SocksSettings.SocksAccount(json.user, json.pass);
|
||||
return new Inbound.MixedSettings.SocksAccount(json.user, json.pass);
|
||||
}
|
||||
};
|
||||
|
||||
|
||||
@@ -813,7 +813,7 @@ class Outbound extends CommonClass {
|
||||
var settings;
|
||||
switch (protocol) {
|
||||
case Protocols.VLESS:
|
||||
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '');
|
||||
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '', url.searchParams.get('encryption') ?? 'none');
|
||||
break;
|
||||
case Protocols.Trojan:
|
||||
settings = new Outbound.TrojanSettings(address, port, userData);
|
||||
@@ -919,12 +919,14 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
|
||||
constructor(
|
||||
packets = '1-3',
|
||||
length = '',
|
||||
interval = ''
|
||||
interval = '',
|
||||
maxSplit = ''
|
||||
) {
|
||||
super();
|
||||
this.packets = packets;
|
||||
this.length = length;
|
||||
this.interval = interval;
|
||||
this.maxSplit = maxSplit;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -932,6 +934,7 @@ Outbound.FreedomSettings.Fragment = class extends CommonClass {
|
||||
json.packets,
|
||||
json.length,
|
||||
json.interval,
|
||||
json.maxSplit
|
||||
);
|
||||
}
|
||||
};
|
||||
@@ -940,12 +943,14 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
|
||||
constructor(
|
||||
type = 'rand',
|
||||
packet = '10-20',
|
||||
delay = '10-16'
|
||||
delay = '10-16',
|
||||
applyTo = 'ip'
|
||||
) {
|
||||
super();
|
||||
this.type = type;
|
||||
this.packet = packet;
|
||||
this.delay = delay;
|
||||
this.applyTo = applyTo;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -953,6 +958,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
|
||||
json.type,
|
||||
json.packet,
|
||||
json.delay,
|
||||
json.applyTo
|
||||
);
|
||||
}
|
||||
|
||||
@@ -961,6 +967,7 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
|
||||
type: this.type,
|
||||
packet: this.packet,
|
||||
delay: this.delay,
|
||||
applyTo: this.applyTo
|
||||
};
|
||||
}
|
||||
};
|
||||
@@ -988,7 +995,7 @@ Outbound.DNSSettings = class extends CommonClass {
|
||||
network = 'udp',
|
||||
address = '',
|
||||
port = 53,
|
||||
nonIPQuery = 'drop',
|
||||
nonIPQuery = 'reject',
|
||||
blockTypes = []
|
||||
) {
|
||||
super();
|
||||
@@ -1039,13 +1046,13 @@ Outbound.VmessSettings = class extends CommonClass {
|
||||
}
|
||||
};
|
||||
Outbound.VLESSSettings = class extends CommonClass {
|
||||
constructor(address, port, id, flow, encryption = 'none') {
|
||||
constructor(address, port, id, flow, encryption) {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.id = id;
|
||||
this.flow = flow;
|
||||
this.encryption = encryption
|
||||
this.encryption = encryption;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
@@ -1064,7 +1071,7 @@ Outbound.VLESSSettings = class extends CommonClass {
|
||||
vnext: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: [{ id: this.id, flow: this.flow, encryption: 'none', }],
|
||||
users: [{ id: this.id, flow: this.flow, encryption: this.encryption }],
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
||||
125
web/assets/js/subscription.js
Normal file
125
web/assets/js/subscription.js
Normal file
@@ -0,0 +1,125 @@
|
||||
(function () {
|
||||
// Vue app for Subscription page
|
||||
const el = document.getElementById('subscription-data');
|
||||
if (!el) return;
|
||||
const textarea = document.getElementById('subscription-links');
|
||||
const rawLinks = (textarea?.value || '').split('\n').filter(Boolean);
|
||||
|
||||
const data = {
|
||||
sId: el.getAttribute('data-sid') || '',
|
||||
subUrl: el.getAttribute('data-sub-url') || '',
|
||||
subJsonUrl: el.getAttribute('data-subjson-url') || '',
|
||||
download: el.getAttribute('data-download') || '',
|
||||
upload: el.getAttribute('data-upload') || '',
|
||||
used: el.getAttribute('data-used') || '',
|
||||
total: el.getAttribute('data-total') || '',
|
||||
remained: el.getAttribute('data-remained') || '',
|
||||
expireMs: (parseInt(el.getAttribute('data-expire') || '0', 10) || 0) * 1000,
|
||||
lastOnlineMs: (parseInt(el.getAttribute('data-lastonline') || '0', 10) || 0),
|
||||
downloadByte: parseInt(el.getAttribute('data-downloadbyte') || '0', 10) || 0,
|
||||
uploadByte: parseInt(el.getAttribute('data-uploadbyte') || '0', 10) || 0,
|
||||
totalByte: parseInt(el.getAttribute('data-totalbyte') || '0', 10) || 0,
|
||||
datepicker: el.getAttribute('data-datepicker') || 'gregorian',
|
||||
};
|
||||
|
||||
// Normalize lastOnline to milliseconds if it looks like seconds
|
||||
if (data.lastOnlineMs && data.lastOnlineMs < 10_000_000_000) {
|
||||
data.lastOnlineMs *= 1000;
|
||||
}
|
||||
|
||||
function renderLink(item) {
|
||||
return (
|
||||
Vue.h('a-list-item', {}, [
|
||||
Vue.h('a-space', { props: { size: 'small' } }, [
|
||||
Vue.h('a-button', { props: { size: 'small' }, on: { click: () => copy(item) } }, [Vue.h('a-icon', { props: { type: 'copy' } })]),
|
||||
Vue.h('span', { class: 'break-all' }, item)
|
||||
])
|
||||
])
|
||||
);
|
||||
}
|
||||
|
||||
function copy(text) {
|
||||
ClipboardManager.copyText(text).then(ok => {
|
||||
const messageType = ok ? 'success' : 'error';
|
||||
Vue.prototype.$message[messageType](ok ? 'Copied' : 'Copy failed');
|
||||
});
|
||||
}
|
||||
|
||||
function open(url) {
|
||||
window.location.href = url;
|
||||
}
|
||||
|
||||
function drawQR(value) {
|
||||
try { new QRious({ element: document.getElementById('qrcode'), value, size: 220 }); } catch (e) { console.warn(e); }
|
||||
}
|
||||
|
||||
// Try to extract a human label (email/ps) from different link types
|
||||
function linkName(link, idx) {
|
||||
try {
|
||||
if (link.startsWith('vmess://')) {
|
||||
const json = JSON.parse(atob(link.replace('vmess://', '')));
|
||||
if (json.ps) return json.ps;
|
||||
if (json.add && json.id) return json.add; // fallback host
|
||||
} else if (link.startsWith('vless://') || link.startsWith('trojan://')) {
|
||||
// vless://<id>@host:port?...#name
|
||||
const hashIdx = link.indexOf('#');
|
||||
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
||||
// email sometimes in query params like sni or remark
|
||||
const qIdx = link.indexOf('?');
|
||||
if (qIdx !== -1) {
|
||||
const qs = new URL('http://x/?' + link.substring(qIdx + 1, hashIdx !== -1 ? hashIdx : undefined)).searchParams;
|
||||
if (qs.get('remark')) return qs.get('remark');
|
||||
if (qs.get('email')) return qs.get('email');
|
||||
}
|
||||
// else take user@host
|
||||
const at = link.indexOf('@');
|
||||
const protSep = link.indexOf('://');
|
||||
if (at !== -1 && protSep !== -1) return link.substring(protSep + 3, at);
|
||||
} else if (link.startsWith('ss://')) {
|
||||
// shadowsocks: label often after #
|
||||
const hashIdx = link.indexOf('#');
|
||||
if (hashIdx !== -1) return decodeURIComponent(link.substring(hashIdx + 1));
|
||||
}
|
||||
} catch (e) { /* ignore and fallback */ }
|
||||
return 'Link ' + (idx + 1);
|
||||
}
|
||||
|
||||
const app = new Vue({
|
||||
delimiters: ['[[', ']]'],
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
app: data,
|
||||
links: rawLinks,
|
||||
lang: '',
|
||||
viewportWidth: (typeof window !== 'undefined' ? window.innerWidth : 1024),
|
||||
},
|
||||
async mounted() {
|
||||
this.lang = LanguageManager.getLanguage();
|
||||
// Discover subJsonUrl if provided via template bootstrap
|
||||
const tpl = document.getElementById('subscription-data');
|
||||
const sj = tpl ? tpl.getAttribute('data-subjson-url') : '';
|
||||
if (sj) this.app.subJsonUrl = sj;
|
||||
drawQR(this.app.subUrl);
|
||||
// Draw second QR if available
|
||||
try { new QRious({ element: document.getElementById('qrcode-subjson'), value: this.app.subJsonUrl || '', size: 220 }); } catch (e) { /* ignore */ }
|
||||
// Track viewport width for responsive behavior
|
||||
this._onResize = () => { this.viewportWidth = window.innerWidth; };
|
||||
window.addEventListener('resize', this._onResize);
|
||||
},
|
||||
beforeDestroy() {
|
||||
if (this._onResize) window.removeEventListener('resize', this._onResize);
|
||||
},
|
||||
computed: {
|
||||
isMobile() { return this.viewportWidth < 576; },
|
||||
isUnlimited() { return !this.app.totalByte; },
|
||||
isActive() {
|
||||
const now = Date.now();
|
||||
const expiryOk = !this.app.expireMs || this.app.expireMs >= now;
|
||||
const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;
|
||||
return expiryOk && trafficOk;
|
||||
},
|
||||
},
|
||||
methods: { renderLink, copy, open, linkName, i18nLabel(key) { return '{{ i18n "' + key + '" }}'; } },
|
||||
});
|
||||
})();
|
||||
@@ -134,7 +134,7 @@ class DateUtil {
|
||||
}
|
||||
|
||||
static formatMillis(millis) {
|
||||
return moment(millis).format('YYYY-M-D H:m:s');
|
||||
return moment(millis).format('YYYY-M-D HH:mm:ss');
|
||||
}
|
||||
|
||||
static firstDayOfMonth() {
|
||||
|
||||
@@ -326,6 +326,14 @@ class ObjectUtil {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
for (const key in b) {
|
||||
if (!b.hasOwnProperty(key)) {
|
||||
continue;
|
||||
}
|
||||
if (!a.hasOwnProperty(key)) {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./vue.common.prod.js')
|
||||
} else {
|
||||
module.exports = require('./vue.common.dev.js')
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
6
web/assets/vue/vue.esm.browser.min.js
vendored
6
web/assets/vue/vue.esm.browser.min.js
vendored
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
11932
web/assets/vue/vue.js
11932
web/assets/vue/vue.js
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -1,5 +0,0 @@
|
||||
if (process.env.NODE_ENV === 'production') {
|
||||
module.exports = require('./vue.runtime.common.prod.js')
|
||||
} else {
|
||||
module.exports = require('./vue.runtime.common.dev.js')
|
||||
}
|
||||
File diff suppressed because one or more lines are too long
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
6
web/assets/vue/vue.runtime.min.js
vendored
6
web/assets/vue/vue.runtime.min.js
vendored
File diff suppressed because one or more lines are too long
@@ -1,76 +0,0 @@
|
||||
import Vue from './vue.runtime.common.js'
|
||||
export default Vue
|
||||
|
||||
// this should be kept in sync with src/v3/index.ts
|
||||
export const {
|
||||
version,
|
||||
|
||||
// refs
|
||||
ref,
|
||||
shallowRef,
|
||||
isRef,
|
||||
toRef,
|
||||
toRefs,
|
||||
unref,
|
||||
proxyRefs,
|
||||
customRef,
|
||||
triggerRef,
|
||||
computed,
|
||||
|
||||
// reactive
|
||||
reactive,
|
||||
isReactive,
|
||||
isReadonly,
|
||||
isShallow,
|
||||
isProxy,
|
||||
shallowReactive,
|
||||
markRaw,
|
||||
toRaw,
|
||||
readonly,
|
||||
shallowReadonly,
|
||||
|
||||
// watch
|
||||
watch,
|
||||
watchEffect,
|
||||
watchPostEffect,
|
||||
watchSyncEffect,
|
||||
|
||||
// effectScope
|
||||
effectScope,
|
||||
onScopeDispose,
|
||||
getCurrentScope,
|
||||
|
||||
// provide / inject
|
||||
provide,
|
||||
inject,
|
||||
|
||||
// lifecycle
|
||||
onBeforeMount,
|
||||
onMounted,
|
||||
onBeforeUpdate,
|
||||
onUpdated,
|
||||
onBeforeUnmount,
|
||||
onUnmounted,
|
||||
onErrorCaptured,
|
||||
onActivated,
|
||||
onDeactivated,
|
||||
onServerPrefetch,
|
||||
onRenderTracked,
|
||||
onRenderTriggered,
|
||||
|
||||
// v2 only
|
||||
set,
|
||||
del,
|
||||
|
||||
// v3 compat
|
||||
h,
|
||||
getCurrentInstance,
|
||||
useSlots,
|
||||
useAttrs,
|
||||
mergeDefaults,
|
||||
nextTick,
|
||||
useCssModule,
|
||||
useCssVars,
|
||||
defineComponent,
|
||||
defineAsyncComponent
|
||||
} = Vue
|
||||
@@ -9,6 +9,7 @@ import (
|
||||
type APIController struct {
|
||||
BaseController
|
||||
inboundController *InboundController
|
||||
serverController *ServerController
|
||||
Tgbot service.Tgbot
|
||||
}
|
||||
|
||||
@@ -19,42 +20,22 @@ func NewAPIController(g *gin.RouterGroup) *APIController {
|
||||
}
|
||||
|
||||
func (a *APIController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/panel/api/inbounds")
|
||||
g.Use(a.checkLogin)
|
||||
// Main API group
|
||||
api := g.Group("/panel/api")
|
||||
api.Use(a.checkLogin)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
// Inbounds API
|
||||
inbounds := api.Group("/inbounds")
|
||||
a.inboundController = NewInboundController(inbounds)
|
||||
|
||||
inboundRoutes := []struct {
|
||||
Method string
|
||||
Path string
|
||||
Handler gin.HandlerFunc
|
||||
}{
|
||||
{"GET", "/createbackup", a.createBackup},
|
||||
{"GET", "/list", a.inboundController.getInbounds},
|
||||
{"GET", "/get/:id", a.inboundController.getInbound},
|
||||
{"GET", "/getClientTraffics/:email", a.inboundController.getClientTraffics},
|
||||
{"GET", "/getClientTrafficsById/:id", a.inboundController.getClientTrafficsById},
|
||||
{"POST", "/add", a.inboundController.addInbound},
|
||||
{"POST", "/del/:id", a.inboundController.delInbound},
|
||||
{"POST", "/update/:id", a.inboundController.updateInbound},
|
||||
{"POST", "/clientIps/:email", a.inboundController.getClientIps},
|
||||
{"POST", "/clearClientIps/:email", a.inboundController.clearClientIps},
|
||||
{"POST", "/addClient", a.inboundController.addInboundClient},
|
||||
{"POST", "/:id/delClient/:clientId", a.inboundController.delInboundClient},
|
||||
{"POST", "/updateClient/:clientId", a.inboundController.updateInboundClient},
|
||||
{"POST", "/:id/resetClientTraffic/:email", a.inboundController.resetClientTraffic},
|
||||
{"POST", "/resetAllTraffics", a.inboundController.resetAllTraffics},
|
||||
{"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
|
||||
{"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
|
||||
{"POST", "/onlines", a.inboundController.onlines},
|
||||
{"POST", "/updateClientTraffic/:email", a.inboundController.updateClientTraffic},
|
||||
}
|
||||
// Server API
|
||||
server := api.Group("/server")
|
||||
a.serverController = NewServerController(server)
|
||||
|
||||
for _, route := range inboundRoutes {
|
||||
g.Handle(route.Method, route.Path, route.Handler)
|
||||
}
|
||||
// Extra routes
|
||||
api.GET("/backuptotgbot", a.BackuptoTgbot)
|
||||
}
|
||||
|
||||
func (a *APIController) createBackup(c *gin.Context) {
|
||||
func (a *APIController) BackuptoTgbot(c *gin.Context) {
|
||||
a.Tgbot.SendBackupToAdmins()
|
||||
}
|
||||
|
||||
@@ -24,9 +24,12 @@ func NewInboundController(g *gin.RouterGroup) *InboundController {
|
||||
}
|
||||
|
||||
func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/inbound")
|
||||
|
||||
g.POST("/list", a.getInbounds)
|
||||
g.GET("/list", a.getInbounds)
|
||||
g.GET("/get/:id", a.getInbound)
|
||||
g.GET("/getClientTraffics/:email", a.getClientTraffics)
|
||||
g.GET("/getClientTrafficsById/:id", a.getClientTrafficsById)
|
||||
|
||||
g.POST("/add", a.addInbound)
|
||||
g.POST("/del/:id", a.delInbound)
|
||||
g.POST("/update/:id", a.updateInbound)
|
||||
@@ -41,6 +44,9 @@ func (a *InboundController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/delDepletedClients/:id", a.delDepletedClients)
|
||||
g.POST("/import", a.importInbound)
|
||||
g.POST("/onlines", a.onlines)
|
||||
g.POST("/lastOnline", a.lastOnline)
|
||||
g.POST("/updateClientTraffic/:email", a.updateClientTraffic)
|
||||
g.POST("/:id/delClientByEmail/:email", a.delInboundClientByEmail)
|
||||
}
|
||||
|
||||
func (a *InboundController) getInbounds(c *gin.Context) {
|
||||
@@ -340,6 +346,11 @@ func (a *InboundController) onlines(c *gin.Context) {
|
||||
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) lastOnline(c *gin.Context) {
|
||||
data, err := a.inboundService.GetClientsLastOnline()
|
||||
jsonObj(c, data, err)
|
||||
}
|
||||
|
||||
func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
@@ -364,3 +375,23 @@ func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
||||
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.inboundClientUpdateSuccess"), nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) delInboundClientByEmail(c *gin.Context) {
|
||||
inboundId, err := strconv.Atoi(c.Param("id"))
|
||||
if err != nil {
|
||||
jsonMsg(c, "Invalid inbound ID", err)
|
||||
return
|
||||
}
|
||||
|
||||
email := c.Param("email")
|
||||
needRestart, err := a.inboundService.DelInboundClientByEmail(inboundId, email)
|
||||
if err != nil {
|
||||
jsonMsg(c, "Failed to delete client by email", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonMsg(c, "Client deleted successfully", nil)
|
||||
if needRestart {
|
||||
a.xrayService.SetToNeedRestart()
|
||||
}
|
||||
}
|
||||
|
||||
@@ -37,11 +37,17 @@ func NewServerController(g *gin.RouterGroup) *ServerController {
|
||||
}
|
||||
|
||||
func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||
g = g.Group("/server")
|
||||
|
||||
g.Use(a.checkLogin)
|
||||
g.POST("/status", a.status)
|
||||
g.POST("/getXrayVersion", a.getXrayVersion)
|
||||
g.GET("/status", a.status)
|
||||
g.GET("/getXrayVersion", a.getXrayVersion)
|
||||
g.GET("/getConfigJson", a.getConfigJson)
|
||||
g.GET("/getDb", a.getDb)
|
||||
g.GET("/getNewUUID", a.getNewUUID)
|
||||
g.GET("/getNewX25519Cert", a.getNewX25519Cert)
|
||||
g.GET("/getNewmldsa65", a.getNewmldsa65)
|
||||
g.GET("/getNewmlkem768", a.getNewmlkem768)
|
||||
g.GET("/getNewVlessEnc", a.getNewVlessEnc)
|
||||
|
||||
g.POST("/stopXrayService", a.stopXrayService)
|
||||
g.POST("/restartXrayService", a.restartXrayService)
|
||||
g.POST("/installXray/:version", a.installXray)
|
||||
@@ -49,11 +55,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
||||
g.POST("/updateGeofile/:fileName", a.updateGeofile)
|
||||
g.POST("/logs/:count", a.getLogs)
|
||||
g.POST("/xraylogs/:count", a.getXrayLogs)
|
||||
g.POST("/getConfigJson", a.getConfigJson)
|
||||
g.GET("/getDb", a.getDb)
|
||||
g.POST("/importDB", a.importDB)
|
||||
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
|
||||
g.POST("/getNewmldsa65", a.getNewmldsa65)
|
||||
g.POST("/getNewEchCert", a.getNewEchCert)
|
||||
}
|
||||
|
||||
@@ -266,3 +268,31 @@ func (a *ServerController) getNewEchCert(c *gin.Context) {
|
||||
}
|
||||
jsonObj(c, cert, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getNewVlessEnc(c *gin.Context) {
|
||||
out, err := a.serverService.GetNewVlessEnc()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewVlessEncError"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, out, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getNewUUID(c *gin.Context) {
|
||||
uuidResp, err := a.serverService.GetNewUUID()
|
||||
if err != nil {
|
||||
jsonMsg(c, "Failed to generate UUID", err)
|
||||
return
|
||||
}
|
||||
|
||||
jsonObj(c, uuidResp, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getNewmlkem768(c *gin.Context) {
|
||||
out, err := a.serverService.GetNewmlkem768()
|
||||
if err != nil {
|
||||
jsonMsg(c, "Failed to generate mlkem768 keys", err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, out, nil)
|
||||
}
|
||||
|
||||
@@ -8,6 +8,7 @@ type XUIController struct {
|
||||
BaseController
|
||||
|
||||
inboundController *InboundController
|
||||
serverController *ServerController
|
||||
settingController *SettingController
|
||||
xraySettingController *XraySettingController
|
||||
}
|
||||
@@ -28,6 +29,7 @@ func (a *XUIController) initRouter(g *gin.RouterGroup) {
|
||||
g.GET("/xray", a.xraySettings)
|
||||
|
||||
a.inboundController = NewInboundController(g)
|
||||
a.serverController = NewServerController(g)
|
||||
a.settingController = NewSettingController(g)
|
||||
a.xraySettingController = NewXraySettingController(g)
|
||||
}
|
||||
|
||||
@@ -2,10 +2,10 @@ package entity
|
||||
|
||||
import (
|
||||
"crypto/tls"
|
||||
"math"
|
||||
"net"
|
||||
"strings"
|
||||
"time"
|
||||
"math"
|
||||
|
||||
"x-ui/util/common"
|
||||
)
|
||||
@@ -39,8 +39,8 @@ type AllSetting struct {
|
||||
TgCpu int `json:"tgCpu" form:"tgCpu"`
|
||||
TgLang string `json:"tgLang" form:"tgLang"`
|
||||
TimeLocation string `json:"timeLocation" form:"timeLocation"`
|
||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
||||
TwoFactorEnable bool `json:"twoFactorEnable" form:"twoFactorEnable"`
|
||||
TwoFactorToken string `json:"twoFactorToken" form:"twoFactorToken"`
|
||||
SubEnable bool `json:"subEnable" form:"subEnable"`
|
||||
SubTitle string `json:"subTitle" form:"subTitle"`
|
||||
SubListen string `json:"subListen" form:"subListen"`
|
||||
|
||||
@@ -33,12 +33,17 @@
|
||||
<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="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content" >
|
||||
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
|
||||
</template>
|
||||
<template v-if="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="client" slot-scope="text, client">
|
||||
<a-space direction="horizontal" :size="2">
|
||||
@@ -98,6 +103,10 @@
|
||||
</table>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<template slot="allTime" slot-scope="text, client">
|
||||
<a-tag>[[ SizeFormatter.sizeFormat(getAllTimeClient(record, client.email)) ]]</a-tag>
|
||||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, client, index">
|
||||
<template v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
@@ -278,4 +287,30 @@
|
||||
</a-badge>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="createdAt" slot-scope="text, client, index">
|
||||
<template v-if="client.created_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client.created_at) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client.created_at)) ]]
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</template>
|
||||
<template slot="updatedAt" slot-scope="text, client, index">
|
||||
<template v-if="client.updated_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client.updated_at) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client.updated_at)) ]]
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
</template>
|
||||
</template>
|
||||
{{end}}
|
||||
|
||||
@@ -1,15 +0,0 @@
|
||||
{{define "form/allocate"}}
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='Strategy'>
|
||||
<a-select v-model="inbound.allocate.strategy" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['always','random']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Refresh'>
|
||||
<a-input-number v-model.number="inbound.allocate.refresh" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
<a-form-item label='Concurrency'>
|
||||
<a-input-number v-model.number="inbound.allocate.concurrency" min="0"></a-input-number>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
{{end}}
|
||||
@@ -83,14 +83,14 @@
|
||||
{{template "form/shadowsocks"}}
|
||||
</template>
|
||||
|
||||
<!-- dokodemo-door -->
|
||||
<template v-if="inbound.protocol === Protocols.DOKODEMO">
|
||||
{{template "form/dokodemo"}}
|
||||
<!-- tunnel -->
|
||||
<template v-if="inbound.protocol === Protocols.TUNNEL">
|
||||
{{template "form/tunnel"}}
|
||||
</template>
|
||||
|
||||
<!-- socks -->
|
||||
<template v-if="inbound.protocol === Protocols.SOCKS">
|
||||
{{template "form/socks"}}
|
||||
<!-- mixed -->
|
||||
<template v-if="inbound.protocol === Protocols.MIXED">
|
||||
{{template "form/mixed"}}
|
||||
</template>
|
||||
|
||||
<!-- http -->
|
||||
@@ -121,13 +121,4 @@
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
|
||||
<!-- allocate -->
|
||||
<!-- Temporarily disabled until we accepts range for port allocation
|
||||
<a-collapse>
|
||||
<a-collapse-panel header='Allocate'>
|
||||
{{template "form/allocate"}}
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
-->
|
||||
|
||||
{{end}}
|
||||
|
||||
@@ -42,6 +42,9 @@
|
||||
<a-form-item label='Interval'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.interval"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Split'>
|
||||
<a-input v-model.trim="outbound.settings.fragment.maxSplit"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
<!-- Switch for Noises -->
|
||||
@@ -75,6 +78,11 @@
|
||||
<a-form-item label='Delay'>
|
||||
<a-input v-model.trim="noise.delay"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Apply To'>
|
||||
<a-select v-model="noise.applyTo" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['ip','ipv4','ipv6']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
</template>
|
||||
@@ -97,7 +105,7 @@
|
||||
</a-form-item>
|
||||
<a-form-item label='non-IP queries'>
|
||||
<a-select v-model="outbound.settings.nonIPQuery" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="s in ['drop','skip']" :value="s">[[ s ]]</a-select-option>
|
||||
<a-select-option v-for="s in ['reject','drop','skip']" :value="s">[[ s ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="outbound.settings.nonIPQuery === 'skip'" label='Block Types' >
|
||||
@@ -218,6 +226,11 @@
|
||||
</template>
|
||||
|
||||
<!-- vless settings -->
|
||||
<template v-if="outbound.protocol === Protocols.VLESS">
|
||||
<a-form-item label='encryption'>
|
||||
<a-input v-model.trim="outbound.settings.encryption"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="outbound.canEnableTlsFlow()">
|
||||
<a-form-item label='Flow'>
|
||||
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
@@ -428,6 +441,9 @@
|
||||
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="ECH Config List">
|
||||
<a-input v-model.trim="outbound.stream.tls.echConfigList"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="Allow Insecure">
|
||||
<a-switch v-model="outbound.stream.tls.allowInsecure"></a-switch>
|
||||
</a-form-item>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{define "form/dokodemo"}}
|
||||
{{define "form/tunnel"}}
|
||||
<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>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
{{define "form/socks"}}
|
||||
{{define "form/mixed"}}
|
||||
<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>
|
||||
@@ -15,7 +15,7 @@
|
||||
<td width="45%">{{ i18n "username" }}</td>
|
||||
<td width="45%">{{ i18n "password" }}</td>
|
||||
<td>
|
||||
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.SocksSettings.SocksAccount())"></a-button>
|
||||
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"></a-button>
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
|
||||
@@ -18,7 +18,29 @@
|
||||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp">
|
||||
<template v-if="!inbound.stream.isTLS || !inbound.stream.isReality">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Authentication">
|
||||
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="X25519, not Post-Quantum">X25519 (not Post-Quantum)</a-select-option>
|
||||
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768 (Post-Quantum)</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label="decryption">
|
||||
<a-input v-model.trim="inbound.settings.decryption"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label="encryption">
|
||||
<a-input v-model="inbound.settings.encryption"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New keys</a-button>
|
||||
<a-button danger @click="clearVlessEnc">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Fallbacks">
|
||||
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
|
||||
|
||||
@@ -12,8 +12,8 @@
|
||||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Dest (Target)'>
|
||||
<a-input v-model.trim="inbound.stream.reality.dest"></a-input>
|
||||
<a-form-item label='Target'>
|
||||
<a-input v-model.trim="inbound.stream.reality.target"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='SNI'>
|
||||
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
|
||||
@@ -48,7 +48,10 @@
|
||||
<a-textarea v-model="inbound.stream.reality.privateKey"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
|
||||
<a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewX25519Cert">Get New Cert</a-button>
|
||||
<a-button danger @click="clearX25519Cert">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
<a-form-item label="mldsa65 Seed">
|
||||
<a-textarea v-model="inbound.stream.reality.mldsa65Seed"></a-textarea>
|
||||
@@ -57,7 +60,10 @@
|
||||
<a-textarea v-model="inbound.stream.reality.settings.mldsa65Verify"></a-textarea>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed</a-button>
|
||||
<a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewmldsa65">Get New Seed</a-button>
|
||||
<a-button danger @click="clearMldsa65">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</template>
|
||||
{{end}}
|
||||
@@ -116,7 +116,10 @@
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label=" ">
|
||||
<a-space>
|
||||
<a-button type="primary" icon="import" @click="getNewEchCert">Get New ECH Cert</a-button>
|
||||
<a-button danger @click="clearEchCert">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</template>
|
||||
|
||||
|
||||
@@ -1,150 +1,8 @@
|
||||
{{ template "page/head_start" .}}
|
||||
<style>
|
||||
.ant-table:not(.ant-table-expanded-row .ant-table) {
|
||||
outline: 1px solid #f0f0f0;
|
||||
outline-offset: -1px;
|
||||
border-radius: 1rem;
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.dark .ant-table:not(.ant-table-expanded-row .ant-table) {
|
||||
outline-color: var(--dark-color-table-ring);
|
||||
}
|
||||
.ant-table .ant-table-content .ant-table-scroll .ant-table-body {
|
||||
overflow-y: hidden;
|
||||
}
|
||||
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
|
||||
margin:-10px 22px !important;
|
||||
}
|
||||
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper .ant-table {
|
||||
border-bottom-left-radius: 1rem;
|
||||
border-bottom-right-radius: 1rem;
|
||||
}
|
||||
.ant-table .ant-table-content .ant-table-tbody tr:last-child tr:last-child td {
|
||||
border-bottom-color: transparent;
|
||||
}
|
||||
.ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:first-child {
|
||||
border-bottom-left-radius: 6px;
|
||||
}
|
||||
.ant-table .ant-table-tbody tr:last-child.ant-table-expanded-row .ant-table-wrapper .ant-table-tbody>tr:last-child>td:last-child {
|
||||
border-bottom-right-radius: 6px;
|
||||
}
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.ant-card-body {
|
||||
padding: .5rem;
|
||||
}
|
||||
.ant-table .ant-table-content .ant-table-tbody tr:last-child .ant-table-wrapper {
|
||||
margin:-10px 2px !important;
|
||||
}
|
||||
}
|
||||
.dark .ant-switch-small:not(.ant-switch-checked) {
|
||||
background-color: var(--dark-color-surface-100) !important;
|
||||
}
|
||||
.ant-custom-popover-title {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 10px;
|
||||
margin: 5px 0;
|
||||
}
|
||||
.ant-col-sm-24 {
|
||||
margin: 0.5rem -2rem 0.5rem 2rem;
|
||||
}
|
||||
tr.hideExpandIcon .ant-table-row-expand-icon {
|
||||
display: none;
|
||||
}
|
||||
.infinite-tag {
|
||||
padding: 0 5px;
|
||||
border-radius: 2rem;
|
||||
min-width: 50px;
|
||||
min-height: 22px;
|
||||
}
|
||||
.infinite-bar .ant-progress-inner .ant-progress-bg {
|
||||
background-color: #F2EAF1;
|
||||
border: #D5BED2 solid 1px;
|
||||
}
|
||||
.dark .infinite-bar .ant-progress-inner .ant-progress-bg {
|
||||
background-color: #7a316f !important;
|
||||
border: #7a316f solid 1px;
|
||||
}
|
||||
.ant-collapse {
|
||||
margin: 5px 0;
|
||||
}
|
||||
.info-large-tag {
|
||||
max-width: 200px;
|
||||
overflow: hidden;
|
||||
}
|
||||
.client-comment {
|
||||
font-size: 12px;
|
||||
opacity: 0.75;
|
||||
cursor: help;
|
||||
}
|
||||
.client-email {
|
||||
font-weight: 500;
|
||||
}
|
||||
.client-popup-item {
|
||||
display: flex;
|
||||
align-items: center;
|
||||
gap: 5px;
|
||||
}
|
||||
.online-animation .ant-badge-status-dot {
|
||||
animation: onlineAnimation 1.2s linear infinite;
|
||||
}
|
||||
@keyframes onlineAnimation {
|
||||
0%,
|
||||
50%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
10% {
|
||||
transform: scale(1.5);
|
||||
opacity: .2;
|
||||
}
|
||||
}
|
||||
.tr-table-box {
|
||||
display: flex;
|
||||
gap: 4px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
.tr-table-rt {
|
||||
flex-basis: 70px;
|
||||
min-width: 70px;
|
||||
text-align: end;
|
||||
}
|
||||
.tr-table-lt {
|
||||
flex-basis: 70px;
|
||||
min-width: 70px;
|
||||
text-align: start;
|
||||
}
|
||||
.tr-table-bar {
|
||||
flex-basis: 160px;
|
||||
min-width: 60px;
|
||||
}
|
||||
.tr-infinity-ch {
|
||||
font-size: 14pt;
|
||||
max-height: 24px;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
.ant-table-expanded-row .ant-table .ant-table-body {
|
||||
overflow-x: hidden;
|
||||
}
|
||||
.ant-table-expanded-row .ant-table-tbody>tr>td {
|
||||
padding: 10px 2px;
|
||||
}
|
||||
.ant-table-expanded-row .ant-table-thead>tr>th {
|
||||
padding: 12px 2px;
|
||||
}
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' inbounds-page'">
|
||||
<a-sidebar></a-sidebar>
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
@@ -167,28 +25,35 @@
|
||||
<a-col>
|
||||
<a-card size="small" :style="{ padding: '16px' }" hoverable>
|
||||
<a-row>
|
||||
<a-col :sm="12" :md="6">
|
||||
<a-col :sm="12" :md="5">
|
||||
<a-custom-statistic title='{{ i18n "pages.inbounds.totalDownUp" }}' :value="`${SizeFormatter.sizeFormat(total.up)} / ${SizeFormatter.sizeFormat(total.down)}`">
|
||||
<template #prefix>
|
||||
<a-icon type="swap"></a-icon>
|
||||
</template>
|
||||
</a-custom-statistic>
|
||||
</a-col>
|
||||
<a-col :sm="12" :md="6">
|
||||
<a-col :sm="12" :md="5">
|
||||
<a-custom-statistic title='{{ i18n "pages.inbounds.totalUsage" }}' :value="SizeFormatter.sizeFormat(total.up + total.down)" :style="{ marginTop: isMobile ? '10px' : 0 }">
|
||||
<template #prefix>
|
||||
<a-icon type="pie-chart"></a-icon>
|
||||
</template>
|
||||
</a-custom-statistic>
|
||||
</a-col>
|
||||
<a-col :sm="12" :md="6">
|
||||
<a-col :sm="12" :md="5">
|
||||
<a-custom-statistic title='{{ i18n "pages.inbounds.allTimeTrafficUsage" }}' :value="SizeFormatter.sizeFormat(total.allTime)" :style="{ marginTop: isMobile ? '10px' : 0 }">
|
||||
<template #prefix>
|
||||
<a-icon type="history"></a-icon>
|
||||
</template>
|
||||
</a-custom-statistic>
|
||||
</a-col>
|
||||
<a-col :sm="12" :md="5">
|
||||
<a-custom-statistic title='{{ i18n "pages.inbounds.inboundCount" }}' :value="dbInbounds.length" :style="{ marginTop: isMobile ? '10px' : 0 }">
|
||||
<template #prefix>
|
||||
<a-icon type="bars"></a-icon>
|
||||
</template>
|
||||
</a-custom-statistic>
|
||||
</a-col>
|
||||
<a-col :sm="12" :md="6">
|
||||
<a-col :sm="12" :md="4">
|
||||
<a-custom-statistic title='{{ i18n "clients" }}' value=" " :style="{ marginTop: isMobile ? '10px' : 0 }">
|
||||
<template #prefix>
|
||||
<a-space direction="horizontal">
|
||||
@@ -484,6 +349,9 @@
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="allTimeInbound" slot-scope="text, dbInbound">
|
||||
<a-tag>[[ SizeFormatter.sizeFormat(dbInbound.allTime || 0) ]]</a-tag>
|
||||
</template>
|
||||
<template slot="enable" slot-scope="text, dbInbound">
|
||||
<a-switch v-model="dbInbound.enable" @change="switchEnable(dbInbound.id,dbInbound.enable)"></a-switch>
|
||||
</template>
|
||||
@@ -696,7 +564,7 @@
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.enable" }}',
|
||||
align: 'center',
|
||||
width: 30,
|
||||
width: 35,
|
||||
scopedSlots: { customRender: 'enable' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.remark" }}',
|
||||
@@ -721,8 +589,13 @@
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.traffic" }}',
|
||||
align: 'center',
|
||||
width: 60,
|
||||
width: 90,
|
||||
scopedSlots: { customRender: 'traffic' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.allTimeTraffic" }}',
|
||||
align: 'center',
|
||||
width: 70,
|
||||
scopedSlots: { customRender: 'allTimeInbound' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.expireDate" }}',
|
||||
align: 'center',
|
||||
@@ -755,10 +628,11 @@
|
||||
|
||||
const innerColumns = [
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
||||
{ title: '{{ i18n "online" }}', width: 30, scopedSlots: { customRender: 'online' } },
|
||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 35, scopedSlots: { customRender: 'enable' } },
|
||||
{ title: '{{ i18n "online" }}', width: 32, scopedSlots: { customRender: 'online' } },
|
||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
|
||||
{ title: '{{ i18n "pages.inbounds.expireDate" }}', width: 80, align: 'center', scopedSlots: { customRender: 'expiryTime' } },
|
||||
];
|
||||
|
||||
@@ -791,6 +665,7 @@
|
||||
defaultKey: '',
|
||||
clientCount: [],
|
||||
onlineClients: [],
|
||||
lastOnlineMap: {},
|
||||
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
||||
refreshing: false,
|
||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||
@@ -813,12 +688,13 @@
|
||||
},
|
||||
async getDBInbounds() {
|
||||
this.refreshing = true;
|
||||
const msg = await HttpUtil.post('/panel/inbound/list');
|
||||
const msg = await HttpUtil.get('/panel/api/inbounds/list');
|
||||
if (!msg.success) {
|
||||
this.refreshing = false;
|
||||
return;
|
||||
}
|
||||
|
||||
await this.getLastOnlineMap();
|
||||
await this.getOnlineUsers();
|
||||
|
||||
this.setInbounds(msg.obj);
|
||||
@@ -827,12 +703,17 @@
|
||||
}, 500);
|
||||
},
|
||||
async getOnlineUsers() {
|
||||
const msg = await HttpUtil.post('/panel/inbound/onlines');
|
||||
const msg = await HttpUtil.post('/panel/api/inbounds/onlines');
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
this.onlineClients = msg.obj != null ? msg.obj : [];
|
||||
},
|
||||
async getLastOnlineMap() {
|
||||
const msg = await HttpUtil.post('/panel/api/inbounds/lastOnline');
|
||||
if (!msg.success || !msg.obj) return;
|
||||
this.lastOnlineMap = msg.obj || {}
|
||||
},
|
||||
async getDefaultSettings() {
|
||||
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
|
||||
if (!msg.success) {
|
||||
@@ -960,11 +841,13 @@
|
||||
const list = this.clientCount[inbound.id][this.filterBy];
|
||||
if (list.length > 0) {
|
||||
const filteredSettings = { "clients": [] };
|
||||
inboundSettings.clients.forEach(client => {
|
||||
if (list.includes(client.email)) {
|
||||
filteredSettings.clients.push(client);
|
||||
}
|
||||
});
|
||||
if (inboundSettings.clients) {
|
||||
inboundSettings.clients.forEach(client => {
|
||||
if (list.includes(client.email)) {
|
||||
filteredSettings.clients.push(client);
|
||||
}
|
||||
});
|
||||
}
|
||||
newInbound.settings = Inbound.Settings.fromJson(inbound.protocol, filteredSettings);
|
||||
this.searchedInbounds.push(newInbound);
|
||||
}
|
||||
@@ -1075,9 +958,8 @@
|
||||
settings: Inbound.Settings.getSettings(baseInbound.protocol).toString(),
|
||||
streamSettings: baseInbound.stream.toString(),
|
||||
sniffing: baseInbound.sniffing.toString(),
|
||||
allocate: baseInbound.allocate.toString(),
|
||||
};
|
||||
await this.submit('/panel/inbound/add', data, inModal);
|
||||
await this.submit('/panel/api/inbounds/add', data, inModal);
|
||||
},
|
||||
openAddInbound() {
|
||||
inModal.show({
|
||||
@@ -1119,11 +1001,14 @@
|
||||
protocol: inbound.protocol,
|
||||
settings: inbound.settings.toString(),
|
||||
};
|
||||
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
|
||||
if (inbound.canEnableStream()){
|
||||
data.streamSettings = inbound.stream.toString();
|
||||
} else if (inbound.stream?.sockopt) {
|
||||
data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
|
||||
}
|
||||
data.sniffing = inbound.sniffing.toString();
|
||||
data.allocate = inbound.allocate.toString();
|
||||
|
||||
await this.submit('/panel/inbound/add', data, inModal);
|
||||
await this.submit('/panel/api/inbounds/add', data, inModal);
|
||||
},
|
||||
async updateInbound(inbound, dbInbound) {
|
||||
const data = {
|
||||
@@ -1139,11 +1024,14 @@
|
||||
protocol: inbound.protocol,
|
||||
settings: inbound.settings.toString(),
|
||||
};
|
||||
if (inbound.canEnableStream()) data.streamSettings = inbound.stream.toString();
|
||||
if (inbound.canEnableStream()){
|
||||
data.streamSettings = inbound.stream.toString();
|
||||
} else if (inbound.stream?.sockopt) {
|
||||
data.streamSettings = JSON.stringify({ sockopt: inbound.stream.sockopt.toJson() }, null, 2);
|
||||
}
|
||||
data.sniffing = inbound.sniffing.toString();
|
||||
data.allocate = inbound.allocate.toString();
|
||||
|
||||
await this.submit(`/panel/inbound/update/${dbInbound.id}`, data, inModal);
|
||||
await this.submit(`/panel/api/inbounds/update/${dbInbound.id}`, data, inModal);
|
||||
},
|
||||
openAddClient(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
@@ -1198,14 +1086,14 @@
|
||||
id: dbInboundId,
|
||||
settings: '{"clients": [' + clients.toString() + ']}',
|
||||
};
|
||||
await this.submit(`/panel/inbound/addClient`, data, modal);
|
||||
await this.submit(`/panel/api/inbounds/addClient`, data, modal);
|
||||
},
|
||||
async updateClient(client, dbInboundId, clientId) {
|
||||
const data = {
|
||||
id: dbInboundId,
|
||||
settings: '{"clients": [' + client.toString() + ']}',
|
||||
};
|
||||
await this.submit(`/panel/inbound/updateClient/${clientId}`, data, clientModal);
|
||||
await this.submit(`/panel/api/inbounds/updateClient/${clientId}`, data, clientModal);
|
||||
},
|
||||
resetTraffic(dbInboundId) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
@@ -1230,7 +1118,7 @@
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/panel/inbound/del/' + dbInboundId),
|
||||
onOk: () => this.submit('/panel/api/inbounds/del/' + dbInboundId),
|
||||
});
|
||||
},
|
||||
delClient(dbInboundId, client,confirmation = true) {
|
||||
@@ -1243,10 +1131,10 @@
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`),
|
||||
onOk: () => this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`),
|
||||
});
|
||||
} else {
|
||||
this.submit(`/panel/inbound/${dbInboundId}/delClient/${clientId}`);
|
||||
this.submit(`/panel/api/inbounds/${dbInboundId}/delClient/${clientId}`);
|
||||
}
|
||||
},
|
||||
getSubGroupClients(dbInbounds, currentClient) {
|
||||
@@ -1325,7 +1213,7 @@
|
||||
switchEnable(dbInboundId,state) {
|
||||
dbInbound = this.dbInbounds.find(row => row.id === dbInboundId);
|
||||
dbInbound.enable = state;
|
||||
this.submit(`/panel/inbound/update/${dbInboundId}`, dbInbound);
|
||||
this.submit(`/panel/api/inbounds/update/${dbInboundId}`, dbInbound);
|
||||
},
|
||||
async switchEnableClient(dbInboundId, client) {
|
||||
this.loading()
|
||||
@@ -1355,10 +1243,10 @@
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email),
|
||||
onOk: () => this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email),
|
||||
})
|
||||
} else {
|
||||
this.submit('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + client.email);
|
||||
this.submit('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + client.email);
|
||||
}
|
||||
},
|
||||
resetAllTraffic() {
|
||||
@@ -1368,7 +1256,7 @@
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/panel/inbound/resetAllTraffics'),
|
||||
onOk: () => this.submit('/panel/api/inbounds/resetAllTraffics'),
|
||||
});
|
||||
},
|
||||
resetAllClientTraffics(dbInboundId) {
|
||||
@@ -1378,7 +1266,7 @@
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "reset"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/panel/inbound/resetAllClientTraffics/' + dbInboundId),
|
||||
onOk: () => this.submit('/panel/api/inbounds/resetAllClientTraffics/' + dbInboundId),
|
||||
})
|
||||
},
|
||||
delDepletedClients(dbInboundId) {
|
||||
@@ -1388,7 +1276,7 @@
|
||||
class: themeSwitcher.currentTheme,
|
||||
okText: '{{ i18n "delete"}}',
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: () => this.submit('/panel/inbound/delDepletedClients/' + dbInboundId),
|
||||
onOk: () => this.submit('/panel/api/inbounds/delDepletedClients/' + dbInboundId),
|
||||
})
|
||||
},
|
||||
isExpiry(dbInbound, index) {
|
||||
@@ -1409,6 +1297,12 @@
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
||||
return clientStats ? clientStats.up + clientStats.down : 0;
|
||||
},
|
||||
getAllTimeClient(dbInbound, email) {
|
||||
if (email.length == 0) return 0;
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
||||
if (!clientStats) return 0;
|
||||
return clientStats.allTime || (clientStats.up + clientStats.down);
|
||||
},
|
||||
getRemStats(dbInbound, email) {
|
||||
if (email.length == 0) return 0;
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
||||
@@ -1466,6 +1360,17 @@
|
||||
isClientOnline(email) {
|
||||
return this.onlineClients.includes(email);
|
||||
},
|
||||
getLastOnline(email) {
|
||||
return this.lastOnlineMap[email] || null
|
||||
},
|
||||
formatLastOnline(email) {
|
||||
const ts = this.getLastOnline(email)
|
||||
if (!ts) return '-'
|
||||
if (this.datepicker === 'gregorian') {
|
||||
return DateUtil.formatMillis(ts)
|
||||
}
|
||||
return DateUtil.convertToJalalian(moment(ts))
|
||||
},
|
||||
isRemovable(dbInboundId) {
|
||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
|
||||
},
|
||||
@@ -1497,7 +1402,7 @@
|
||||
value: '',
|
||||
okText: '{{ i18n "pages.inbounds.import" }}',
|
||||
confirm: async (dbInboundText) => {
|
||||
await this.submit('/panel/inbound/import', {data: dbInboundText}, promptModal);
|
||||
await this.submit('/panel/api/inbounds/import', {data: dbInboundText}, promptModal);
|
||||
},
|
||||
});
|
||||
},
|
||||
@@ -1598,11 +1503,12 @@
|
||||
},
|
||||
computed: {
|
||||
total() {
|
||||
let down = 0, up = 0;
|
||||
let down = 0, up = 0, allTime = 0;
|
||||
let clients = 0, deactive = [], depleted = [], expiring = [];
|
||||
this.dbInbounds.forEach(dbInbound => {
|
||||
down += dbInbound.down;
|
||||
up += dbInbound.up;
|
||||
allTime += (dbInbound.allTime || (dbInbound.up + dbInbound.down));
|
||||
if (this.clientCount[dbInbound.id]) {
|
||||
clients += this.clientCount[dbInbound.id].clients;
|
||||
deactive = deactive.concat(this.clientCount[dbInbound.id].deactive);
|
||||
@@ -1613,6 +1519,7 @@
|
||||
return {
|
||||
down: down,
|
||||
up: up,
|
||||
allTime: allTime,
|
||||
clients: clients,
|
||||
deactive: deactive,
|
||||
depleted: depleted,
|
||||
|
||||
@@ -1,73 +1,4 @@
|
||||
{{ template "page/head_start" .}}
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
.ant-card-dark h2 {
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
.ant-backup-list-item {
|
||||
gap: 10px;
|
||||
}
|
||||
.ant-version-list-item {
|
||||
--padding: 12px;
|
||||
padding: var(--padding) !important;
|
||||
gap: var(--padding);
|
||||
}
|
||||
.dark .ant-version-list-item svg{
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
.dark .ant-backup-list-item svg,
|
||||
.dark .ant-badge-status-text,
|
||||
.dark .ant-card-extra {
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
.dark .ant-card-actions>li {
|
||||
color: rgba(255, 255, 255, 0.55);
|
||||
}
|
||||
.dark .ant-radio-inner {
|
||||
background-color: var(--dark-color-surface-100);
|
||||
border-color: var(--dark-color-surface-600);
|
||||
}
|
||||
.dark .ant-radio-checked .ant-radio-inner {
|
||||
border-color: var(--color-primary-100);
|
||||
}
|
||||
.dark .ant-backup-list,
|
||||
.dark .ant-version-list,
|
||||
.dark .ant-card-actions,
|
||||
.dark .ant-card-actions>li:not(:last-child) {
|
||||
border-color: var(--dark-color-stroke);
|
||||
}
|
||||
.ant-card-actions {
|
||||
background: transparent;
|
||||
}
|
||||
.ip-hidden {
|
||||
-webkit-user-select: none;
|
||||
-moz-user-select: none;
|
||||
user-select: none;
|
||||
filter: blur(10px);
|
||||
}
|
||||
.running-animation .ant-badge-status-dot {
|
||||
animation: runningAnimation 1.2s linear infinite;
|
||||
}
|
||||
.running-animation .ant-badge-status-processing:after {
|
||||
border-color: var(--color-primary-100);
|
||||
}
|
||||
@keyframes runningAnimation {
|
||||
0%,
|
||||
50%,
|
||||
100% {
|
||||
transform: scale(1);
|
||||
opacity: 1;
|
||||
}
|
||||
10% {
|
||||
transform: scale(1.5);
|
||||
opacity: .2;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
@@ -77,7 +8,7 @@
|
||||
<a-layout-content>
|
||||
<a-spin :spinning="loadingStates.spinning" :delay="200" :tip="loadingTip">
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="showAlert && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
||||
<a-alert type="error" v-if="showAlert && loadingStates.fetched" class="mb-10"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
description='{{ i18n "secAlertSsl" }}'
|
||||
@@ -87,7 +18,7 @@
|
||||
<transition name="list" appear>
|
||||
<template>
|
||||
<a-row v-if="!loadingStates.fetched">
|
||||
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
|
||||
<a-card class="card-placeholder text-center">
|
||||
<a-spin tip='{{ i18n "loading" }}'></a-spin>
|
||||
</a-card>
|
||||
</a-row>
|
||||
@@ -97,7 +28,7 @@
|
||||
<a-row :gutter="[0, isMobile ? 16 : 0]">
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-row>
|
||||
<a-col :span="12" :style="{ textAlign: 'center' }">
|
||||
<a-col :span="12" class="text-center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.cpu.color"
|
||||
:percent="status.cpu.percent"></a-progress>
|
||||
@@ -112,7 +43,7 @@
|
||||
</a-tooltip>
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12" :style="{ textAlign: 'center' }">
|
||||
<a-col :span="12" class="text-center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.mem.color"
|
||||
:percent="status.mem.percent"></a-progress>
|
||||
@@ -124,7 +55,7 @@
|
||||
</a-col>
|
||||
<a-col :sm="24" :md="12">
|
||||
<a-row>
|
||||
<a-col :span="12" :style="{ textAlign: 'center' }">
|
||||
<a-col :span="12" class="text-center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.swap.color"
|
||||
:percent="status.swap.percent"></a-progress>
|
||||
@@ -132,7 +63,7 @@
|
||||
<b>{{ i18n "pages.index.swap" }}:</b> [[ SizeFormatter.sizeFormat(status.swap.current) ]] / [[ SizeFormatter.sizeFormat(status.swap.total) ]]
|
||||
</div>
|
||||
</a-col>
|
||||
<a-col :span="12" :style="{ textAlign: 'center' }">
|
||||
<a-col :span="12" class="text-center">
|
||||
<a-progress type="dashboard" status="normal"
|
||||
:stroke-color="status.disk.color"
|
||||
:percent="status.disk.percent"></a-progress>
|
||||
@@ -167,31 +98,31 @@
|
||||
<span>{{ i18n "pages.index.xrayErrorPopoverTitle" }}</span>
|
||||
</a-col>
|
||||
<a-col>
|
||||
<a-icon type="bars" :style="{ cursor: 'pointer', float: 'right' }" @click="openLogs()"></a-icon>
|
||||
<a-icon type="bars" class="cursor-pointer float-right" @click="openLogs()"></a-icon>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</span>
|
||||
<template slot="content">
|
||||
<span :style="{ maxWidth: '400px' }" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
|
||||
<span class="max-w-400" v-for="line in status.xray.errorMsg.split('\n')">[[ line ]]</span>
|
||||
</template>
|
||||
<a-badge :text="status.xray.stateMsg" :color="status.xray.color"/>
|
||||
</a-popover>
|
||||
</template>
|
||||
</template>
|
||||
<template #actions>
|
||||
<a-space v-if="app.ipLimitEnable" direction="horizontal" @click="openXrayLogs()" :style="{ justifyContent: 'center' }">
|
||||
<a-space v-if="app.ipLimitEnable" direction="horizontal" @click="openXrayLogs()" class="jc-center">
|
||||
<a-icon type="bars"></a-icon>
|
||||
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
|
||||
</a-space>
|
||||
<a-space direction="horizontal" @click="stopXrayService" :style="{ justifyContent: 'center' }">
|
||||
<a-space direction="horizontal" @click="stopXrayService" class="jc-center">
|
||||
<a-icon type="poweroff"></a-icon>
|
||||
<span v-if="!isMobile">{{ i18n "pages.index.stopXray" }}</span>
|
||||
</a-space>
|
||||
<a-space direction="horizontal" @click="restartXrayService" :style="{ justifyContent: 'center' }">
|
||||
<a-space direction="horizontal" @click="restartXrayService" class="jc-center">
|
||||
<a-icon type="reload"></a-icon>
|
||||
<span v-if="!isMobile">{{ i18n "pages.index.restartXray" }}</span>
|
||||
</a-space>
|
||||
<a-space direction="horizontal" @click="openSelectV2rayVersion" :style="{ justifyContent: 'center' }">
|
||||
<a-space direction="horizontal" @click="openSelectV2rayVersion" class="jc-center">
|
||||
<a-icon type="tool"></a-icon>
|
||||
<span v-if="!isMobile">
|
||||
[[ status.xray.version != 'Unknown' ? `v${status.xray.version}` : '{{ i18n "pages.index.xraySwitch" }}' ]]
|
||||
@@ -203,15 +134,15 @@
|
||||
<a-col :sm="24" :lg="12">
|
||||
<a-card title='{{ i18n "menu.link" }}' hoverable>
|
||||
<template #actions>
|
||||
<a-space direction="horizontal" @click="openLogs()" :style="{ justifyContent: 'center' }">
|
||||
<a-space direction="horizontal" @click="openLogs()" class="jc-center">
|
||||
<a-icon type="bars"></a-icon>
|
||||
<span v-if="!isMobile">{{ i18n "pages.index.logs" }}</span>
|
||||
</a-space>
|
||||
<a-space direction="horizontal" @click="openConfig" :style="{ justifyContent: 'center' }">
|
||||
<a-space direction="horizontal" @click="openConfig" class="jc-center">
|
||||
<a-icon type="control"></a-icon>
|
||||
<span v-if="!isMobile">{{ i18n "pages.index.config" }}</span>
|
||||
</a-space>
|
||||
<a-space direction="horizontal" @click="openBackup" :style="{ justifyContent: 'center' }">
|
||||
<a-space direction="horizontal" @click="openBackup" class="jc-center">
|
||||
<a-icon type="cloud-server"></a-icon>
|
||||
<span v-if="!isMobile">{{ i18n "pages.index.backup" }}</span>
|
||||
</a-space>
|
||||
@@ -314,7 +245,7 @@
|
||||
<template #title>
|
||||
{{ i18n "pages.index.toggleIpVisibility" }}
|
||||
</template>
|
||||
<a-icon :type="showIp ? 'eye' : 'eye-invisible'" :style="{ fontSize: '1rem' }" @click="showIp = !showIp"></a-icon>
|
||||
<a-icon :type="showIp ? 'eye' : 'eye-invisible'" class="fs-1rem" @click="showIp = !showIp"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-row :class="showIp ? 'ip-visible' : 'ip-hidden'" :gutter="isMobile ? [8,8] : 0">
|
||||
@@ -365,8 +296,8 @@
|
||||
@ok="() => versionModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
|
||||
<a-collapse default-active-key="1">
|
||||
<a-collapse-panel key="1" header='Xray'>
|
||||
<a-alert type="warning" :style="{ marginBottom: '12px', width: '100%' }" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert>
|
||||
<a-list class="ant-version-list" bordered :style="{ width: '100%' }">
|
||||
<a-alert type="warning" class="mb-12 w-100" message='{{ i18n "pages.index.xraySwitchClickDesk" }}' show-icon></a-alert>
|
||||
<a-list class="ant-version-list w-100" bordered>
|
||||
<a-list-item class="ant-version-list-item" v-for="version, index in versionModal.versions">
|
||||
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ version ]]</a-tag>
|
||||
<a-radio :class="themeSwitcher.currentTheme" :checked="version === `v${status.xray.version}`" @click="switchV2rayVersion(version)"></a-radio>
|
||||
@@ -374,15 +305,13 @@
|
||||
</a-list>
|
||||
</a-collapse-panel>
|
||||
<a-collapse-panel key="2" header='Geofiles'>
|
||||
<a-list class="ant-version-list" bordered :style="{ width: '100%' }">
|
||||
<a-list class="ant-version-list w-100" bordered>
|
||||
<a-list-item class="ant-version-list-item" v-for="file, index in ['geosite.dat', 'geoip.dat', 'geosite_IR.dat', 'geoip_IR.dat', 'geosite_RU.dat', 'geoip_RU.dat']">
|
||||
<a-tag :color="index % 2 == 0 ? 'purple' : 'green'">[[ file ]]</a-tag>
|
||||
<a-icon type="reload" @click="updateGeofile(file)" :style="{ marginRight: '8px' }"/>
|
||||
<a-icon type="reload" @click="updateGeofile(file)" class="mr-8"/>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
<div style="margin-top: 5px; display: flex; justify-content: flex-end;">
|
||||
<a-button @click="updateGeofile('')">{{ i18n "pages.index.geofilesUpdateAll" }}</a-button>
|
||||
</div>
|
||||
<div class="mt-5 d-flex justify-end"><a-button @click="updateGeofile('')">{{ i18n "pages.index.geofilesUpdateAll" }}</a-button></div>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-modal>
|
||||
@@ -394,15 +323,15 @@
|
||||
{{ i18n "pages.index.logs" }}
|
||||
<a-icon :spin="logModal.loading"
|
||||
type="sync"
|
||||
:style="{ verticalAlign: 'middle', marginLeft: '10px' }"
|
||||
class="va-middle ml-10"
|
||||
:disabled="logModal.loading"
|
||||
@click="openLogs()">
|
||||
</a-icon>
|
||||
</template>
|
||||
<a-form layout="inline">
|
||||
<a-form-item :style="{ marginRight: '0.5rem' }">
|
||||
<a-form-item class="mr-05">
|
||||
<a-input-group compact>
|
||||
<a-select size="small" v-model="logModal.rows" :style="{ width: '70px' }"
|
||||
<a-select size="small" v-model="logModal.rows" class="w-70"
|
||||
@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>
|
||||
@@ -410,7 +339,7 @@
|
||||
<a-select-option value="100">100</a-select-option>
|
||||
<a-select-option value="500">500</a-select-option>
|
||||
</a-select>
|
||||
<a-select size="small" v-model="logModal.level" :style="{ width: '95px' }"
|
||||
<a-select size="small" v-model="logModal.level" class="w-95"
|
||||
@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>
|
||||
@@ -423,11 +352,11 @@
|
||||
<a-form-item>
|
||||
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
|
||||
</a-form-item>
|
||||
<a-form-item :style="{ float: 'right' }">
|
||||
<a-form-item style="float: right;">
|
||||
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="logModal.formattedLogs"></div>
|
||||
<div class="ant-input log-container" v-html="logModal.formattedLogs"></div>
|
||||
</a-modal>
|
||||
<a-modal id="xraylog-modal"
|
||||
v-model="xraylogModal.visible"
|
||||
@@ -439,15 +368,15 @@
|
||||
{{ i18n "pages.index.logs" }}
|
||||
<a-icon :spin="xraylogModal.loading"
|
||||
type="sync"
|
||||
:style="{ verticalAlign: 'middle', marginLeft: '10px' }"
|
||||
class="va-middle ml-10"
|
||||
:disabled="xraylogModal.loading"
|
||||
@click="openXrayLogs()">
|
||||
</a-icon>
|
||||
</template>
|
||||
<a-form layout="inline">
|
||||
<a-form-item :style="{ marginRight: '0.5rem' }">
|
||||
<a-form-item class="mr-05">
|
||||
<a-input-group compact>
|
||||
<a-select size="small" v-model="xraylogModal.rows" :style="{ width: '70px' }"
|
||||
<a-select size="small" v-model="xraylogModal.rows" class="w-70"
|
||||
@change="openXrayLogs()" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option value="10">10</a-select-option>
|
||||
<a-select-option value="20">20</a-select-option>
|
||||
@@ -465,11 +394,11 @@
|
||||
<a-checkbox v-model="xraylogModal.showBlocked" @change="openXrayLogs()">Blocked</a-checkbox>
|
||||
<a-checkbox v-model="xraylogModal.showProxy" @change="openXrayLogs()">Proxy</a-checkbox>
|
||||
</a-form-item>
|
||||
<a-form-item :style="{ float: 'right' }">
|
||||
<a-form-item style="float: right;">
|
||||
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(xraylogModal.logs?.join('\n'), 'x-ui.log')"></a-button>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
<div class="ant-input" :style="{ height: 'auto', maxHeight: '500px', overflow: 'auto', marginTop: '0.5rem' }" v-html="xraylogModal.formattedLogs"></div>
|
||||
<div class="ant-input log-container" v-html="xraylogModal.formattedLogs"></div>
|
||||
</a-modal>
|
||||
<a-modal id="backup-modal"
|
||||
v-model="backupModal.visible"
|
||||
@@ -477,7 +406,7 @@
|
||||
:closable="true"
|
||||
footer=""
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-list class="ant-backup-list" bordered :style="{ width: '100%' }">
|
||||
<a-list class="ant-backup-list w-100" bordered>
|
||||
<a-list-item class="ant-backup-list-item">
|
||||
<a-list-item-meta>
|
||||
<template #title>{{ i18n "pages.index.exportDatabase" }}</template>
|
||||
@@ -746,7 +675,7 @@ ${dateTime}
|
||||
},
|
||||
async getStatus() {
|
||||
try {
|
||||
const msg = await HttpUtil.post('/server/status');
|
||||
const msg = await HttpUtil.get('/panel/api/server/status');
|
||||
if (msg.success) {
|
||||
if (!this.loadingStates.fetched) {
|
||||
this.loadingStates.fetched = true;
|
||||
@@ -763,7 +692,7 @@ ${dateTime}
|
||||
},
|
||||
async openSelectV2rayVersion() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('server/getXrayVersion');
|
||||
const msg = await HttpUtil.get('/panel/api/server/getXrayVersion');
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
@@ -780,7 +709,7 @@ ${dateTime}
|
||||
onOk: async () => {
|
||||
versionModal.hide();
|
||||
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
||||
await HttpUtil.post(`/server/installXray/${version}`);
|
||||
await HttpUtil.post(`/panel/api/server/installXray/${version}`);
|
||||
this.loading(false);
|
||||
},
|
||||
});
|
||||
@@ -798,9 +727,9 @@ ${dateTime}
|
||||
onOk: async () => {
|
||||
versionModal.hide();
|
||||
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
|
||||
const url = isSingleFile
|
||||
? `/server/updateGeofile/${fileName}`
|
||||
: `/server/updateGeofile`;
|
||||
const url = isSingleFile
|
||||
? `/panel/api/server/updateGeofile/${fileName}`
|
||||
: `/panel/api/server/updateGeofile`;
|
||||
await HttpUtil.post(url);
|
||||
this.loading(false);
|
||||
},
|
||||
@@ -808,7 +737,7 @@ ${dateTime}
|
||||
},
|
||||
async stopXrayService() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('server/stopXrayService');
|
||||
const msg = await HttpUtil.post('/panel/api/server/stopXrayService');
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
@@ -816,7 +745,7 @@ ${dateTime}
|
||||
},
|
||||
async restartXrayService() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('server/restartXrayService');
|
||||
const msg = await HttpUtil.post('/panel/api/server/restartXrayService');
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
@@ -824,7 +753,7 @@ ${dateTime}
|
||||
},
|
||||
async openLogs(){
|
||||
logModal.loading = true;
|
||||
const msg = await HttpUtil.post('server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
|
||||
const msg = await HttpUtil.post('/panel/api/server/logs/'+logModal.rows,{level: logModal.level, syslog: logModal.syslog});
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
@@ -834,7 +763,7 @@ ${dateTime}
|
||||
},
|
||||
async openXrayLogs(){
|
||||
xraylogModal.loading = true;
|
||||
const msg = await HttpUtil.post('server/xraylogs/'+xraylogModal.rows,{filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy});
|
||||
const msg = await HttpUtil.post('/panel/api/server/xraylogs/'+xraylogModal.rows,{filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy});
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
@@ -844,7 +773,7 @@ ${dateTime}
|
||||
},
|
||||
async openConfig() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post('server/getConfigJson');
|
||||
const msg = await HttpUtil.get('/panel/api/server/getConfigJson');
|
||||
this.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
@@ -855,7 +784,7 @@ ${dateTime}
|
||||
backupModal.show();
|
||||
},
|
||||
exportDatabase() {
|
||||
window.location = basePath + 'server/getDb';
|
||||
window.location = basePath + 'panel/api/server/getDb';
|
||||
},
|
||||
importDatabase() {
|
||||
const fileInput = document.createElement('input');
|
||||
@@ -868,7 +797,7 @@ ${dateTime}
|
||||
formData.append('db', dbFile);
|
||||
backupModal.hide();
|
||||
this.loading(true);
|
||||
const uploadMsg = await HttpUtil.post('server/importDB', formData, {
|
||||
const uploadMsg = await HttpUtil.post('/panel/api/server/importDB', formData, {
|
||||
headers: {
|
||||
'Content-Type': 'multipart/form-data',
|
||||
}
|
||||
|
||||
@@ -1,456 +1,10 @@
|
||||
{{ template "page/head_start" .}}
|
||||
<style>
|
||||
html * {
|
||||
-webkit-font-smoothing: antialiased;
|
||||
-moz-osx-font-smoothing: grayscale;
|
||||
}
|
||||
|
||||
h1 {
|
||||
text-align: center;
|
||||
/*margin: 20px 0 50px 0;*/
|
||||
height: 110px;
|
||||
}
|
||||
|
||||
.ant-form-item-children .ant-btn,
|
||||
.ant-input {
|
||||
height: 50px;
|
||||
border-radius: 30px;
|
||||
}
|
||||
|
||||
.ant-input-group-addon {
|
||||
border-radius: 0 30px 30px 0;
|
||||
width: 50px;
|
||||
font-size: 18px;
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper .ant-input-prefix {
|
||||
left: 23px;
|
||||
}
|
||||
|
||||
.ant-input-affix-wrapper .ant-input:not(:first-child) {
|
||||
padding-left: 50px;
|
||||
}
|
||||
|
||||
.centered {
|
||||
display: flex;
|
||||
text-align: center;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.title {
|
||||
font-size: 2rem;
|
||||
margin-block-end: 2rem;
|
||||
}
|
||||
|
||||
.title b {
|
||||
font-weight: bold !important;
|
||||
}
|
||||
|
||||
#app {
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#login {
|
||||
animation: charge 0.5s both;
|
||||
background-color: #fff;
|
||||
border-radius: 2rem;
|
||||
padding: 4rem 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);
|
||||
}
|
||||
|
||||
@keyframes charge {
|
||||
from {
|
||||
transform: translateY(5rem);
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
to {
|
||||
transform: translateY(0);
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
|
||||
.under {
|
||||
background-color: #c7ebe2;
|
||||
z-index: 0;
|
||||
}
|
||||
|
||||
.dark .under {
|
||||
background-color: var(--dark-color-login-wave);
|
||||
}
|
||||
|
||||
.dark #login {
|
||||
background-color: var(--dark-color-surface-100);
|
||||
}
|
||||
|
||||
.dark h1 {
|
||||
color: rgba(255, 255, 255);
|
||||
}
|
||||
|
||||
.ant-btn-primary-login {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.ant-btn-primary-login:focus,
|
||||
.ant-btn-primary-login:hover {
|
||||
color: #fff;
|
||||
background-color: #006655;
|
||||
border-color: #006655;
|
||||
background-image: linear-gradient(270deg,
|
||||
rgba(123, 199, 77, 0) 30%,
|
||||
#009980,
|
||||
rgba(123, 199, 77, 0) 100%);
|
||||
background-repeat: no-repeat;
|
||||
animation: ma-bg-move ease-in-out 5s infinite;
|
||||
background-position-x: -500px;
|
||||
width: 95%;
|
||||
animation-delay: -0.5s;
|
||||
box-shadow: 0 2px 0 rgba(0, 0, 0, 0.045);
|
||||
}
|
||||
|
||||
.ant-btn-primary-login.active,
|
||||
.ant-btn-primary-login:active {
|
||||
color: #fff;
|
||||
background-color: #006655;
|
||||
border-color: #006655;
|
||||
}
|
||||
|
||||
@keyframes ma-bg-move {
|
||||
0% {
|
||||
background-position: -500px 0;
|
||||
}
|
||||
|
||||
50% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
|
||||
100% {
|
||||
background-position: 1000px 0;
|
||||
}
|
||||
}
|
||||
|
||||
.wave-btn-bg {
|
||||
position: relative;
|
||||
border-radius: 25px;
|
||||
width: 100%;
|
||||
transition: all 0.3s cubic-bezier(.645, .045, .355, 1);
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg {
|
||||
color: #fff;
|
||||
position: relative;
|
||||
background-color: #0a7557;
|
||||
border: 2px double transparent;
|
||||
background-origin: border-box;
|
||||
background-clip: padding-box, border-box;
|
||||
background-size: 300%;
|
||||
width: 100%;
|
||||
z-index: 1;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg:hover {
|
||||
animation: wave-btn-tara 4s ease infinite;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg-cl {
|
||||
background-image: linear-gradient(rgba(13, 14, 33, 0), rgba(13, 14, 33, 0)),
|
||||
radial-gradient(circle at left top, #006655, #009980, #006655) !important;
|
||||
border-radius: 3em;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg-cl:hover {
|
||||
width: 95%;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg-cl:before {
|
||||
position: absolute;
|
||||
content: "";
|
||||
top: -5px;
|
||||
left: -5px;
|
||||
bottom: -5px;
|
||||
right: -5px;
|
||||
z-index: -1;
|
||||
background: inherit;
|
||||
background-size: inherit;
|
||||
border-radius: 4em;
|
||||
opacity: 0;
|
||||
transition: 0.5s;
|
||||
}
|
||||
|
||||
.dark .wave-btn-bg-cl:hover::before {
|
||||
opacity: 1;
|
||||
filter: blur(20px);
|
||||
animation: wave-btn-tara 8s linear infinite;
|
||||
}
|
||||
|
||||
@keyframes wave-btn-tara {
|
||||
to {
|
||||
background-position: 300%;
|
||||
}
|
||||
}
|
||||
|
||||
.dark .ant-btn-primary-login {
|
||||
font-size: 14px;
|
||||
color: #fff;
|
||||
text-align: center;
|
||||
background-image: linear-gradient(rgba(13, 14, 33, 0.45),
|
||||
rgba(13, 14, 33, 0.35));
|
||||
border-radius: 2rem;
|
||||
border: none;
|
||||
outline: none;
|
||||
background-color: transparent;
|
||||
height: 46px;
|
||||
position: relative;
|
||||
white-space: nowrap;
|
||||
cursor: pointer;
|
||||
touch-action: manipulation;
|
||||
padding: 0 15px;
|
||||
width: 100%;
|
||||
animation: none;
|
||||
background-position-x: 0;
|
||||
box-shadow: none;
|
||||
}
|
||||
|
||||
.waves-header {
|
||||
position: fixed;
|
||||
width: 100%;
|
||||
text-align: center;
|
||||
background-color: #dbf5ed;
|
||||
color: white;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
.dark .waves-header {
|
||||
background-color: var(--dark-color-login-background);
|
||||
}
|
||||
|
||||
.waves-inner-header {
|
||||
height: 50vh;
|
||||
width: 100%;
|
||||
margin: 0;
|
||||
padding: 0;
|
||||
}
|
||||
|
||||
.waves {
|
||||
position: relative;
|
||||
width: 100%;
|
||||
height: 15vh;
|
||||
margin-bottom: -8px;
|
||||
/*Fix for safari gap*/
|
||||
min-height: 100px;
|
||||
max-height: 150px;
|
||||
}
|
||||
|
||||
.parallax>use {
|
||||
animation: move-forever 25s cubic-bezier(0.55, 0.5, 0.45, 0.5) infinite;
|
||||
}
|
||||
|
||||
.dark .parallax>use {
|
||||
fill: var(--dark-color-login-wave);
|
||||
}
|
||||
|
||||
.parallax>use:nth-child(1) {
|
||||
animation-delay: -2s;
|
||||
animation-duration: 4s;
|
||||
opacity: 0.2;
|
||||
}
|
||||
|
||||
.parallax>use:nth-child(2) {
|
||||
animation-delay: -3s;
|
||||
animation-duration: 7s;
|
||||
opacity: 0.4;
|
||||
}
|
||||
|
||||
.parallax>use:nth-child(3) {
|
||||
animation-delay: -4s;
|
||||
animation-duration: 10s;
|
||||
opacity: 0.6;
|
||||
}
|
||||
|
||||
.parallax>use:nth-child(4) {
|
||||
animation-delay: -5s;
|
||||
animation-duration: 13s;
|
||||
}
|
||||
|
||||
@keyframes move-forever {
|
||||
0% {
|
||||
transform: translate3d(-90px, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
transform: translate3d(85px, 0, 0);
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.waves {
|
||||
height: 40px;
|
||||
min-height: 40px;
|
||||
}
|
||||
}
|
||||
|
||||
.words-wrapper {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
position: relative;
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
.words-wrapper b {
|
||||
width: 100%;
|
||||
display: inline-block;
|
||||
position: absolute;
|
||||
left: 0;
|
||||
top: 0;
|
||||
}
|
||||
|
||||
.words-wrapper b.is-visible {
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.headline.zoom .words-wrapper {
|
||||
-webkit-perspective: 300px;
|
||||
-moz-perspective: 300px;
|
||||
perspective: 300px;
|
||||
}
|
||||
|
||||
.headline {
|
||||
display: flex;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
.headline.zoom b {
|
||||
opacity: 0;
|
||||
}
|
||||
|
||||
.headline.zoom b.is-visible {
|
||||
opacity: 1;
|
||||
-webkit-animation: zoom-in 0.8s;
|
||||
-moz-animation: zoom-in 0.8s;
|
||||
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-in 0.8s;
|
||||
}
|
||||
|
||||
.headline.zoom b.is-hidden {
|
||||
-webkit-animation: zoom-out 0.8s;
|
||||
-moz-animation: zoom-out 0.8s;
|
||||
animation: cubic-bezier(0.215, 0.610, 0.355, 1.000) zoom-out 0.4s;
|
||||
}
|
||||
|
||||
@-webkit-keyframes zoom-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateZ(100px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes zoom-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-moz-transform: translateZ(100px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-moz-transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoom-in {
|
||||
0% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateZ(100px);
|
||||
-moz-transform: translateZ(100px);
|
||||
-ms-transform: translateZ(100px);
|
||||
-o-transform: translateZ(100px);
|
||||
transform: translateZ(100px);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateZ(0);
|
||||
-moz-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
-o-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
}
|
||||
|
||||
@-webkit-keyframes zoom-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateZ(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateZ(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
@-moz-keyframes zoom-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
-moz-transform: translateZ(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-moz-transform: translateZ(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
@keyframes zoom-out {
|
||||
0% {
|
||||
opacity: 1;
|
||||
-webkit-transform: translateZ(0);
|
||||
-moz-transform: translateZ(0);
|
||||
-ms-transform: translateZ(0);
|
||||
-o-transform: translateZ(0);
|
||||
transform: translateZ(0);
|
||||
}
|
||||
|
||||
100% {
|
||||
opacity: 0;
|
||||
-webkit-transform: translateZ(-100px);
|
||||
-moz-transform: translateZ(-100px);
|
||||
-ms-transform: translateZ(-100px);
|
||||
-o-transform: translateZ(-100px);
|
||||
transform: translateZ(-100px);
|
||||
}
|
||||
}
|
||||
|
||||
.setting-section {
|
||||
position: absolute;
|
||||
top: 0;
|
||||
right: 0;
|
||||
padding: 22px;
|
||||
}
|
||||
|
||||
.ant-space-item .ant-switch {
|
||||
margin: 2px 0 4px;
|
||||
}
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' login-app'">
|
||||
<transition name="list" appear>
|
||||
<a-layout-content class="under" :style="{ minHeight: '0' }">
|
||||
<a-layout-content class="under min-h-100vh">
|
||||
<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"
|
||||
@@ -466,71 +20,80 @@
|
||||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<a-row type="flex" justify="center" align="middle" :style="{ height: '100%', overflow: 'auto', overflowX: 'hidden' }">
|
||||
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" :style="{ margin: '3rem 0' }">
|
||||
<div class="setting-section">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}' placement="bottomRight" trigger="click">
|
||||
<template slot="content">
|
||||
<a-space direction="vertical" :size="10">
|
||||
<a-theme-switch-login></a-theme-switch-login>
|
||||
<span>{{ i18n "pages.settings.language" }}</span>
|
||||
<a-select ref="selectLang" :style="{ width: '100%' }" v-model="lang" @change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-button shape="circle" icon="setting"></a-button>
|
||||
</a-popover>
|
||||
</div>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col :style="{ width: '100%' }">
|
||||
<h2 class="title headline zoom">
|
||||
<span class="words-wrapper">
|
||||
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
|
||||
<b>{{ i18n "pages.login.title" }}</b>
|
||||
</span>
|
||||
</h2>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col span="24">
|
||||
<a-form>
|
||||
<a-space direction="vertical" size="middle">
|
||||
<a-form-item>
|
||||
<a-input autocomplete="username" name="username" v-model.trim="user.username"
|
||||
placeholder='{{ i18n "username" }}' @keydown.enter.native="login" autofocus>
|
||||
<a-icon slot="prefix" type="user" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
|
||||
<a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="twoFactorEnable">
|
||||
<a-input autocomplete="totp" name="twoFactorCode" v-model.trim="user.twoFactorCode"
|
||||
placeholder='{{ i18n "twoFactorCode" }}' @keydown.enter.native="login">
|
||||
<a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<div :style="{ height: '50px', marginTop: '1rem', ...loading ? { width: '52px' } : { display: 'inline-block' } }" class="wave-btn-bg wave-btn-bg-cl">
|
||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login"
|
||||
:icon="loading ? 'poweroff' : undefined">
|
||||
[[ loading ? '' : '{{ i18n "login" }}' ]]
|
||||
</a-button>
|
||||
</div>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row type="flex" justify="center" align="middle" class="h-100 overflow-hidden-auto">
|
||||
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" class="my-3rem">
|
||||
<template v-if="!loadingStates.fetched">
|
||||
<div class="text-center">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="setting-section">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}'
|
||||
placement="bottomRight" trigger="click">
|
||||
<template slot="content">
|
||||
<a-space direction="vertical" :size="10">
|
||||
<a-theme-switch-login></a-theme-switch-login>
|
||||
<span>{{ i18n "pages.settings.language" }}</span>
|
||||
<a-select ref="selectLang" class="w-100" v-model="lang"
|
||||
@change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-button shape="circle" icon="setting"></a-button>
|
||||
</a-popover>
|
||||
</div>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col :style="{ width: '100%' }">
|
||||
<h2 class="title headline zoom">
|
||||
<span class="words-wrapper">
|
||||
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
|
||||
<b>{{ i18n "pages.login.title" }}</b>
|
||||
</span>
|
||||
</h2>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col span="24">
|
||||
<a-form @submit.prevent="login">
|
||||
<a-space direction="vertical" size="middle">
|
||||
<a-form-item>
|
||||
<a-input autocomplete="username" name="username" v-model.trim="user.username"
|
||||
placeholder='{{ i18n "username" }}' autofocus required>
|
||||
<a-icon slot="prefix" type="user" class="fs-1rem"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' required>
|
||||
<a-icon slot="prefix" type="lock" class="fs-1rem"></a-icon>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="twoFactorEnable">
|
||||
<a-input autocomplete="one-time-code" name="twoFactorCode" v-model.trim="user.twoFactorCode"
|
||||
placeholder='{{ i18n "twoFactorCode" }}' required>
|
||||
<a-icon slot="prefix" type="key" class="fs-1rem"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<div class="wave-btn-bg wave-btn-bg-cl h-50px mt-1rem" :style="loadingStates.spinning ? 'width: 52px' : 'display: inline-block'">
|
||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
||||
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
||||
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
||||
</a-button>
|
||||
</div>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-layout-content>
|
||||
@@ -544,7 +107,10 @@
|
||||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
loading: false,
|
||||
loadingStates: {
|
||||
fetched: false,
|
||||
spinning: false
|
||||
},
|
||||
user: {
|
||||
username: "",
|
||||
password: "",
|
||||
@@ -559,19 +125,23 @@
|
||||
},
|
||||
methods: {
|
||||
async login() {
|
||||
this.loading = true;
|
||||
this.loadingStates.spinning = true;
|
||||
|
||||
const msg = await HttpUtil.post('/login', this.user);
|
||||
this.loading = false;
|
||||
|
||||
if (msg.success) {
|
||||
location.href = basePath + 'panel/';
|
||||
}
|
||||
|
||||
this.loadingStates.spinning = false;
|
||||
},
|
||||
async getTwoFactorEnable() {
|
||||
this.loading = true;
|
||||
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
||||
this.loading = false;
|
||||
|
||||
if (msg.success) {
|
||||
this.twoFactorEnable = msg.obj;
|
||||
this.loadingStates.fetched = true;
|
||||
|
||||
return msg.obj;
|
||||
}
|
||||
},
|
||||
|
||||
@@ -121,7 +121,7 @@
|
||||
},
|
||||
methods: {
|
||||
async getDBClientIps(email) {
|
||||
const msg = await HttpUtil.post(`/panel/inbound/clientIps/${email}`);
|
||||
const msg = await HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`);
|
||||
if (!msg.success) {
|
||||
document.getElementById("clientIPs").value = msg.obj;
|
||||
return;
|
||||
@@ -139,7 +139,7 @@
|
||||
},
|
||||
async clearDBClientIps(email) {
|
||||
try {
|
||||
const msg = await HttpUtil.post(`/panel/inbound/clearClientIps/${email}`);
|
||||
const msg = await HttpUtil.post(`/panel/api/inbounds/clearClientIps/${email}`);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
@@ -156,7 +156,7 @@
|
||||
cancelText: '{{ i18n "cancel"}}',
|
||||
onOk: async () => {
|
||||
iconElement.disabled = true;
|
||||
const msg = await HttpUtil.postWithModal('/panel/inbound/' + dbInboundId + '/resetClientTraffic/' + email);
|
||||
const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + email);
|
||||
if (msg.success) {
|
||||
this.clientModal.clientStats.up = 0;
|
||||
this.clientModal.clientStats.down = 0;
|
||||
|
||||
@@ -101,6 +101,12 @@
|
||||
{{ i18n "security" }}
|
||||
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
|
||||
<br />
|
||||
<td>Authentication</td>
|
||||
<a-tag :color="inbound.settings.selectedAuth ? 'green' : 'red'">[[ inbound.settings.selectedAuth ? inbound.settings.selectedAuth : '' ]]</a-tag>
|
||||
<br />
|
||||
{{ i18n "encryption" }}
|
||||
<a-tag :color="inbound.settings.encryption ? 'green' : 'red'">[[ inbound.settings.encryption ? inbound.settings.encryption : '' ]]</a-tag>
|
||||
<br />
|
||||
<template v-if="inbound.stream.security != 'none'">
|
||||
{{ i18n "domainName" }}
|
||||
<a-tag v-if="inbound.serverName" :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
@@ -185,6 +191,44 @@
|
||||
<a-tag>↑ [[ SizeFormatter.sizeFormat(infoModal.clientStats.up) ]] / [[ SizeFormatter.sizeFormat(infoModal.clientStats.down) ]] ↓</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.createdAt" }}</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings && infoModal.clientSettings.created_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.created_at) ]]</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.created_at)) ]]</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>-</a-tag>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "pages.inbounds.updatedAt" }}</td>
|
||||
<td>
|
||||
<template v-if="infoModal.clientSettings && infoModal.clientSettings.updated_at">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
<a-tag>[[ DateUtil.formatMillis(infoModal.clientSettings.updated_at) ]]</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.updated_at)) ]]</a-tag>
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>-</a-tag>
|
||||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "lastOnline" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ app.formatLastOnline(infoModal.clientSettings && infoModal.clientSettings.email ? infoModal.clientSettings.email : '') ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.comment">
|
||||
<td>{{ i18n "comment" }}</td>
|
||||
<td>
|
||||
@@ -310,7 +354,7 @@
|
||||
<code>[[ link.link ]]</code>
|
||||
</tr-info-row>
|
||||
</template>
|
||||
<table v-if="inbound.protocol == Protocols.DOKODEMO" class="tr-info-table">
|
||||
<table v-if="inbound.protocol == Protocols.TUNNEL" class="tr-info-table">
|
||||
<tr>
|
||||
<th>{{ i18n "pages.inbounds.targetAddress" }}</th>
|
||||
<th>{{ i18n "pages.inbounds.destinationPort" }}</th>
|
||||
@@ -332,7 +376,7 @@
|
||||
</td>
|
||||
</tr>
|
||||
</table>
|
||||
<table v-if="dbInbound.isSocks" class="tr-info-table">
|
||||
<table v-if="dbInbound.isMixed" class="tr-info-table">
|
||||
<tr>
|
||||
<th>{{ i18n "password" }} Auth</th>
|
||||
<th>{{ i18n "pages.inbounds.enable" }} udp</th>
|
||||
@@ -448,7 +492,7 @@
|
||||
</a-modal>
|
||||
<script>
|
||||
function refreshIPs(email) {
|
||||
return HttpUtil.post(`/panel/inbound/clientIps/${email}`).then((msg) => {
|
||||
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
|
||||
if (msg.success) {
|
||||
try {
|
||||
return JSON.parse(msg.obj).join(', ');
|
||||
@@ -569,7 +613,7 @@
|
||||
});
|
||||
},
|
||||
clearClientIps() {
|
||||
HttpUtil.post(`/panel/inbound/clearClientIps/${this.infoModal.clientStats.email}`)
|
||||
HttpUtil.post(`/panel/api/inbounds/clearClientIps/${this.infoModal.clientStats.email}`)
|
||||
.then((msg) => {
|
||||
if (!msg.success) {
|
||||
return;
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
{{define "modals/inboundModal"}}
|
||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title"
|
||||
:dialog-style="{ top: '20px' }" @ok="inModal.ok"
|
||||
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" :dialog-style="{ top: '20px' }"
|
||||
@ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
{{template "form/inbound"}}
|
||||
</a-modal>
|
||||
<script>
|
||||
@@ -20,7 +18,7 @@
|
||||
ok() {
|
||||
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
|
||||
},
|
||||
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => {}, isEdit = false }) {
|
||||
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => { }, isEdit = false }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
if (inbound) {
|
||||
@@ -41,7 +39,7 @@
|
||||
inModal.visible = false;
|
||||
inModal.loading(false);
|
||||
},
|
||||
loading(loading=true) {
|
||||
loading(loading = true) {
|
||||
inModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
@@ -105,9 +103,9 @@
|
||||
},
|
||||
SSMethodChange() {
|
||||
this.inModal.inbound.settings.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
|
||||
|
||||
|
||||
if (this.inModal.inbound.isSSMultiUser) {
|
||||
if (this.inModal.inbound.settings.shadowsockses.length ==0){
|
||||
if (this.inModal.inbound.settings.shadowsockses.length == 0) {
|
||||
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
|
||||
}
|
||||
if (!this.inModal.inbound.isSS2022) {
|
||||
@@ -123,7 +121,7 @@
|
||||
client.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
|
||||
})
|
||||
} else {
|
||||
if (this.inModal.inbound.settings.shadowsockses.length > 0){
|
||||
if (this.inModal.inbound.settings.shadowsockses.length > 0) {
|
||||
this.inModal.inbound.settings.shadowsockses = [];
|
||||
}
|
||||
}
|
||||
@@ -134,7 +132,7 @@
|
||||
},
|
||||
async getNewX25519Cert() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.post('/server/getNewX25519Cert');
|
||||
const msg = await HttpUtil.get('/panel/api/server/getNewX25519Cert');
|
||||
inModal.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
@@ -142,9 +140,13 @@
|
||||
inModal.inbound.stream.reality.privateKey = msg.obj.privateKey;
|
||||
inModal.inbound.stream.reality.settings.publicKey = msg.obj.publicKey;
|
||||
},
|
||||
clearX25519Cert() {
|
||||
this.inbound.stream.reality.privateKey = '';
|
||||
this.inbound.stream.reality.settings.publicKey = '';
|
||||
},
|
||||
async getNewmldsa65() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.post('/server/getNewmldsa65');
|
||||
const msg = await HttpUtil.get('/panel/api/server/getNewmldsa65');
|
||||
inModal.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
@@ -152,9 +154,13 @@
|
||||
inModal.inbound.stream.reality.mldsa65Seed = msg.obj.seed;
|
||||
inModal.inbound.stream.reality.settings.mldsa65Verify = msg.obj.verify;
|
||||
},
|
||||
clearMldsa65() {
|
||||
this.inbound.stream.reality.mldsa65Seed = '';
|
||||
this.inbound.stream.reality.settings.mldsa65Verify = '';
|
||||
},
|
||||
async getNewEchCert() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.post('/server/getNewEchCert', {sni: inModal.inbound.stream.tls.sni});
|
||||
const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });
|
||||
inModal.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
@@ -162,8 +168,39 @@
|
||||
inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
|
||||
inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList;
|
||||
},
|
||||
clearEchCert() {
|
||||
this.inbound.stream.tls.echServerKeys = '';
|
||||
this.inbound.stream.tls.settings.echConfigList = '';
|
||||
},
|
||||
async getNewVlessEnc() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.get('/panel/api/server/getNewVlessEnc');
|
||||
inModal.loading(false);
|
||||
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auths = msg.obj.auths || [];
|
||||
const selected = inModal.inbound.settings.selectedAuth;
|
||||
const block = auths.find(a => a.label === selected);
|
||||
|
||||
if (!block) {
|
||||
console.error("No auth block for", selected);
|
||||
return;
|
||||
}
|
||||
|
||||
inModal.inbound.settings.decryption = block.decryption;
|
||||
inModal.inbound.settings.encryption = block.encryption;
|
||||
},
|
||||
clearVlessEnc() {
|
||||
this.inbound.settings.decryption = 'none';
|
||||
this.inbound.settings.encryption = 'none';
|
||||
this.inbound.settings.selectedAuth = undefined;
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
||||
@@ -151,7 +151,7 @@
|
||||
methods: {
|
||||
async getStatus() {
|
||||
try {
|
||||
const msg = await HttpUtil.post('/server/status');
|
||||
const msg = await HttpUtil.get('/panel/api/server/status');
|
||||
if (msg.success) {
|
||||
this.serverStatus = msg.obj;
|
||||
}
|
||||
|
||||
@@ -1,11 +1,6 @@
|
||||
{{define "modals/ruleModal"}}
|
||||
<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: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>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
@@ -14,7 +9,7 @@
|
||||
</template> Source IPs <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.source"></a-input>
|
||||
<a-input v-model.trim="ruleModal.rule.sourceIP" placeholder="e.g. 0.0.0.0/8, fc00::/7, geoip:ir"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
@@ -24,7 +19,17 @@
|
||||
</template> Source Port <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.sourcePort"></a-input>
|
||||
<a-input v-model.trim="ruleModal.rule.sourcePort" placeholder="e.g. 53,443,1000-2000"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "pages.xray.rules.useComma" }}</span>
|
||||
</template> VLESS Route <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.vlessRoute" placeholder="e.g. 53,443,1000-2000"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Network'>
|
||||
<a-select v-model="ruleModal.rule.network" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
@@ -57,7 +62,7 @@
|
||||
</template> IP <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.ip"></a-input>
|
||||
<a-input v-model.trim="ruleModal.rule.ip" placeholder="e.g. 0.0.0.0/8, fc00::/7, geoip:ir"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
@@ -67,7 +72,7 @@
|
||||
</template> Domain <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.domain"></a-input>
|
||||
<a-input v-model.trim="ruleModal.rule.domain" placeholder="e.g. google.com, geosite:cn"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
@@ -77,7 +82,7 @@
|
||||
</template> User <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.user"></a-input>
|
||||
<a-input v-model.trim="ruleModal.rule.user" placeholder="e.g. email address"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
@@ -87,7 +92,7 @@
|
||||
</template> Port <a-icon type="question-circle"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="ruleModal.rule.port"></a-input>
|
||||
<a-input v-model.trim="ruleModal.rule.port" placeholder="e.g. 53,443,1000-2000"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Inbound Tags'>
|
||||
<a-select v-model="ruleModal.rule.inboundTag" mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
@@ -123,13 +128,13 @@
|
||||
confirm: null,
|
||||
rule: {
|
||||
type: "field",
|
||||
domainMatcher: "",
|
||||
domain: "",
|
||||
ip: "",
|
||||
port: "",
|
||||
sourcePort: "",
|
||||
vlessRoute: "",
|
||||
network: "",
|
||||
source: "",
|
||||
sourceIP: "",
|
||||
user: "",
|
||||
inboundTag: [],
|
||||
protocol: [],
|
||||
@@ -157,13 +162,13 @@
|
||||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
if (isEdit) {
|
||||
this.rule.domainMatcher = rule.domainMatcher;
|
||||
this.rule.domain = rule.domain ? rule.domain.join(',') : [];
|
||||
this.rule.ip = rule.ip ? rule.ip.join(',') : [];
|
||||
this.rule.port = rule.port;
|
||||
this.rule.sourcePort = rule.sourcePort;
|
||||
this.rule.vlessRoute = rule.vlessRoute;
|
||||
this.rule.network = rule.network;
|
||||
this.rule.source = rule.source ? rule.source.join(',') : [];
|
||||
this.rule.sourceIP = rule.sourceIP ? rule.sourceIP.join(',') : [];
|
||||
this.rule.user = rule.user ? rule.user.join(',') : [];
|
||||
this.rule.inboundTag = rule.inboundTag;
|
||||
this.rule.protocol = rule.protocol;
|
||||
@@ -172,13 +177,13 @@
|
||||
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
|
||||
} else {
|
||||
this.rule = {
|
||||
domainMatcher: "",
|
||||
domain: "",
|
||||
ip: "",
|
||||
port: "",
|
||||
sourcePort: "",
|
||||
vlessRoute: "",
|
||||
network: "",
|
||||
source: "",
|
||||
sourceIP: "",
|
||||
user: "",
|
||||
inboundTag: [],
|
||||
protocol: [],
|
||||
@@ -214,13 +219,13 @@
|
||||
rule = {};
|
||||
newRule = {};
|
||||
rule.type = "field";
|
||||
rule.domainMatcher = value.domainMatcher;
|
||||
rule.domain = value.domain.length > 0 ? value.domain.split(',') : [];
|
||||
rule.ip = value.ip.length > 0 ? value.ip.split(',') : [];
|
||||
rule.port = value.port;
|
||||
rule.sourcePort = value.sourcePort;
|
||||
rule.vlessRoute = value.vlessRoute;
|
||||
rule.network = value.network;
|
||||
rule.source = value.source.length > 0 ? value.source.split(',') : [];
|
||||
rule.sourceIP = value.sourceIP.length > 0 ? value.sourceIP.split(',') : [];
|
||||
rule.user = value.user.length > 0 ? value.user.split(',') : [];
|
||||
rule.inboundTag = value.inboundTag;
|
||||
rule.protocol = value.protocol;
|
||||
|
||||
@@ -1,67 +1,8 @@
|
||||
{{ template "page/head_start" .}}
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
@media (max-width: 768px) {
|
||||
.ant-tabs-nav .ant-tabs-tab {
|
||||
margin: 0;
|
||||
padding: 12px .5rem;
|
||||
}
|
||||
}
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
}
|
||||
.ant-list-item {
|
||||
display: block;
|
||||
}
|
||||
.alert-msg {
|
||||
color: rgb(194, 117, 18);
|
||||
font-weight: normal;
|
||||
font-size: 16px;
|
||||
padding: .5rem 1rem;
|
||||
text-align: center;
|
||||
background: rgb(255 145 0 / 15%);
|
||||
margin: 1.5rem 2.5rem 0rem;
|
||||
border-radius: .5rem;
|
||||
transition: all 0.5s;
|
||||
animation: signal 3s cubic-bezier(0.18, 0.89, 0.32, 1.28) infinite;
|
||||
}
|
||||
.alert-msg:hover {
|
||||
cursor: default;
|
||||
transition-duration: .3s;
|
||||
animation: signal 0.9s ease infinite;
|
||||
}
|
||||
@keyframes signal {
|
||||
0% {
|
||||
box-shadow: 0 0 0 0 rgba(194, 118, 18, 0.5);
|
||||
}
|
||||
|
||||
50% {
|
||||
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
|
||||
}
|
||||
|
||||
100% {
|
||||
box-shadow: 0 0 0 6px rgba(0, 0, 0, 0);
|
||||
}
|
||||
}
|
||||
.alert-msg>i {
|
||||
color: inherit;
|
||||
font-size: 24px;
|
||||
}
|
||||
.dark .ant-input-password-icon {
|
||||
color: var(--dark-color-text-primary);
|
||||
}
|
||||
.ant-collapse-content-box .ant-alert {
|
||||
margin-block-end: 12px;
|
||||
}
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' settings-page'">
|
||||
<a-sidebar></a-sidebar>
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
@@ -207,7 +148,7 @@
|
||||
settings: {
|
||||
domainStrategy: "AsIs",
|
||||
noises: [
|
||||
{ type: "rand", packet: "10-20", delay: "10-16" },
|
||||
{ type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" },
|
||||
],
|
||||
},
|
||||
},
|
||||
@@ -397,7 +338,7 @@
|
||||
}
|
||||
},
|
||||
addNoise() {
|
||||
const newNoise = { type: "rand", packet: "10-20", delay: "10-16" };
|
||||
const newNoise = { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" };
|
||||
this.noisesArray = [...this.noisesArray, newNoise];
|
||||
},
|
||||
removeNoise(index) {
|
||||
@@ -420,6 +361,11 @@
|
||||
updatedNoises[index] = { ...updatedNoises[index], delay: value };
|
||||
this.noisesArray = updatedNoises;
|
||||
},
|
||||
updateNoiseApplyTo(index, value) {
|
||||
const updatedNoises = [...this.noisesArray];
|
||||
updatedNoises[index] = { ...updatedNoises[index], applyTo: value };
|
||||
this.noisesArray = updatedNoises;
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
fragment: {
|
||||
|
||||
@@ -90,6 +90,18 @@
|
||||
placeholder="10-20"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>ApplyTo</template>
|
||||
<template #control>
|
||||
<a-select :value="noise.applyTo" :style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
@change="(value) => updateNoiseApplyTo(index, value)">
|
||||
<a-select-option :value="p" :label="p" v-for="p in ['ip', 'ipv4', 'ipv6']" :key="p">
|
||||
<span>[[ p ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-space direction="horizontal" :style="{ padding: '10px 20px' }">
|
||||
<a-button v-if="noisesArray.length > 1" type="danger"
|
||||
@click="removeNoise(index)">Remove</a-button>
|
||||
|
||||
@@ -67,18 +67,22 @@
|
||||
</template>
|
||||
<template slot="info" slot-scope="text, rule, index">
|
||||
<a-popover placement="bottomRight"
|
||||
v-if="(rule.source+rule.sourcePort+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
|
||||
v-if="(rule.sourceIP+rule.sourcePort+rule.vlessRoute+rule.network+rule.protocol+rule.attrs+rule.ip+rule.domain+rule.port).length>0"
|
||||
:overlay-class-name="themeSwitcher.currentTheme" trigger="click">
|
||||
<template slot="content">
|
||||
<table cellpadding="2" :style="{ maxWidth: '300px' }">
|
||||
<tr v-if="rule.source">
|
||||
<td>Source</td>
|
||||
<td><a-tag color="blue" v-for="r in rule.source.split(',')">[[ r ]]</a-tag></td>
|
||||
<tr v-if="rule.sourceIP">
|
||||
<td>Source IP</td>
|
||||
<td><a-tag color="blue" v-for="r in rule.sourceIP.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.sourcePort">
|
||||
<td>Source Port</td>
|
||||
<td><a-tag color="green" v-for="r in rule.sourcePort.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.vlessRoute">
|
||||
<td>VLESS Route</td>
|
||||
<td><a-tag color="geekblue" v-for="r in rule.vlessRoute.split(',')">[[ r ]]</a-tag></td>
|
||||
</tr>
|
||||
<tr v-if="rule.network">
|
||||
<td>Network</td>
|
||||
<td><a-tag color="blue" v-for="r in rule.network.split(',')">[[ r ]]</a-tag></td>
|
||||
|
||||
275
web/html/subscription.html
Normal file
275
web/html/subscription.html
Normal file
@@ -0,0 +1,275 @@
|
||||
{{ template "page/head_start" .}}
|
||||
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/moment/moment-jalali.min.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-layout-content class="p-2">
|
||||
<a-row type="flex" justify="center" class="mt-2">
|
||||
<a-col :xs="24" :sm="22" :md="18" :lg="14" :xl="12">
|
||||
<a-card hoverable class="subscription-card">
|
||||
<template #title>
|
||||
<a-space>
|
||||
<span>{{ i18n "subscription.title" }}</span>
|
||||
<a-tag>{{ .sId }}</a-tag>
|
||||
</a-space>
|
||||
</template>
|
||||
<template #extra>
|
||||
<a-popover
|
||||
:overlay-class-name="themeSwitcher.currentTheme"
|
||||
title='{{ i18n "menu.settings" }}'
|
||||
placement="bottomRight" trigger="click">
|
||||
<template #content>
|
||||
<a-space direction="vertical" :size="10">
|
||||
<a-theme-switch-login></a-theme-switch-login>
|
||||
<span>{{ i18n "pages.settings.language"
|
||||
}}</span>
|
||||
<a-select ref="selectLang" class="w-100"
|
||||
v-model="lang"
|
||||
@change="LanguageManager.setLanguage(lang)"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value"
|
||||
label="English"
|
||||
v-for="l in LanguageManager.supportedLanguages"
|
||||
:key="l.value">
|
||||
<span role="img"
|
||||
:aria-label="l.name"
|
||||
v-text="l.icon"></span>
|
||||
<span
|
||||
v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-button shape="circle" icon="setting"></a-button>
|
||||
</a-popover>
|
||||
</template>
|
||||
|
||||
<a-form layout="vertical">
|
||||
<a-form-item>
|
||||
<a-space direction="vertical" align="center">
|
||||
<a-row type="flex" :gutter="[8,8]"
|
||||
justify="center" style="width:100%">
|
||||
<a-col :xs="24" :sm="12"
|
||||
style="text-align:center;">
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="purple"
|
||||
class="qr-tag">
|
||||
<span>{{ i18n
|
||||
"pages.settings.subSettings"}}</span>
|
||||
</a-tag>
|
||||
<tr-qr-bg class="qr-bg-sub">
|
||||
<tr-qr-bg-inner
|
||||
class="qr-bg-sub-inner">
|
||||
<canvas id="qrcode"
|
||||
class="qr-cv"
|
||||
title='{{ i18n "copy" }}'
|
||||
@click="copy(app.subUrl)"></canvas>
|
||||
</tr-qr-bg-inner>
|
||||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12"
|
||||
style="text-align:center;">
|
||||
<tr-qr-box class="qr-box">
|
||||
<a-tag color="purple"
|
||||
class="qr-tag">
|
||||
<span>{{ i18n
|
||||
"pages.settings.subSettings"}}
|
||||
Json</span>
|
||||
</a-tag>
|
||||
<tr-qr-bg class="qr-bg-sub">
|
||||
<tr-qr-bg-inner
|
||||
class="qr-bg-sub-inner">
|
||||
<canvas id="qrcode-subjson"
|
||||
class="qr-cv"
|
||||
title='{{ i18n "copy" }}'
|
||||
@click="copy(app.subJsonUrl)"></canvas>
|
||||
</tr-qr-bg-inner>
|
||||
</tr-qr-bg>
|
||||
</tr-qr-box>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
|
||||
<a-form-item>
|
||||
<a-descriptions bordered :column="1" size="small">
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.subId" }}'>[[
|
||||
app.sId
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.status" }}'>
|
||||
<template v-if="isUnlimited">
|
||||
<a-tag color="purple">{{ i18n
|
||||
"subscription.unlimited" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag
|
||||
:color="isActive ? 'green' : 'red'">[[
|
||||
isActive ? '{{ i18n
|
||||
"subscription.active" }}' : '{{ i18n
|
||||
"subscription.inactive" }}'
|
||||
]]</a-tag>
|
||||
</template>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.downloaded" }}'>[[
|
||||
app.download
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.uploaded" }}'>[[
|
||||
app.upload
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "usage" }}'>[[ app.used
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.totalQuota" }}'>[[
|
||||
app.total
|
||||
]]</a-descriptions-item>
|
||||
<a-descriptions-item v-if="app.totalByte > 0"
|
||||
label='{{ i18n "remained" }}'>[[
|
||||
app.remained ]]</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "lastOnline" }}'>
|
||||
<template v-if="app.lastOnlineMs > 0">
|
||||
<template
|
||||
v-if="app.datepicker === 'gregorian'">
|
||||
[[
|
||||
DateUtil.formatMillis(app.lastOnlineMs)
|
||||
]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[
|
||||
DateUtil.convertToJalalian(moment(app.lastOnlineMs))
|
||||
]]
|
||||
</template>
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>-</span>
|
||||
</template>
|
||||
</a-descriptions-item>
|
||||
<a-descriptions-item
|
||||
label='{{ i18n "subscription.expiry" }}'>
|
||||
<template v-if="app.expireMs === 0">
|
||||
{{ i18n "subscription.noExpiry" }}
|
||||
</template>
|
||||
<template v-else>
|
||||
<template
|
||||
v-if="app.datepicker === 'gregorian'">
|
||||
[[
|
||||
DateUtil.formatMillis(app.expireMs)
|
||||
]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[
|
||||
DateUtil.convertToJalalian(moment(app.expireMs))
|
||||
]]
|
||||
</template>
|
||||
</template>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
|
||||
<br />
|
||||
<a-list bordered>
|
||||
<a-list-item v-for="(link, idx) in links" :key="link">
|
||||
<div style="width:100%; text-align:center;">
|
||||
<a-button type="primary" :block="isMobile"
|
||||
@click="copy(link)">[[ linkName(link, idx)
|
||||
]]</a-button>
|
||||
</div>
|
||||
</a-list-item>
|
||||
</a-list>
|
||||
<br />
|
||||
|
||||
<a-form layout="vertical">
|
||||
<a-form-item>
|
||||
<a-row type="flex" justify="center" :gutter="[8,8]"
|
||||
style="width:100%">
|
||||
<a-col :xs="24" :sm="12"
|
||||
style="text-align:center;">
|
||||
<!-- Android dropdown -->
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button :block="isMobile"
|
||||
:style="{ marginTop: isMobile ? '6px' : 0 }"
|
||||
size="large" type="primary">
|
||||
Android <a-icon type="down" />
|
||||
</a-button>
|
||||
<a-menu slot="overlay"
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="android-v2box"
|
||||
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
|
||||
<a-menu-item key="android-v2rayng"
|
||||
@click="open('v2rayng://install-config?url=' + encodeURIComponent(app.subUrl))">V2RayNG</a-menu-item>
|
||||
<a-menu-item key="android-singbox"
|
||||
@click="copy(app.subUrl)">Sing-box</a-menu-item>
|
||||
<a-menu-item key="android-v2raytun"
|
||||
@click="copy(app.subUrl)">V2RayTun</a-menu-item>
|
||||
<a-menu-item key="android-npvtunnel"
|
||||
@click="copy(app.subUrl)">NPV
|
||||
Tunnel</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="12"
|
||||
style="text-align:center;">
|
||||
<!-- iOS dropdown -->
|
||||
<a-dropdown :trigger="['click']">
|
||||
<a-button :block="isMobile"
|
||||
:style="{ marginTop: isMobile ? '6px' : 0 }"
|
||||
size="large" type="primary">
|
||||
iOS <a-icon type="down" />
|
||||
</a-button>
|
||||
<a-menu slot="overlay"
|
||||
:class="themeSwitcher.currentTheme">
|
||||
<a-menu-item key="ios-shadowrocket"
|
||||
@click="open('shadowrocket://add/subscription?url=' + encodeURIComponent(app.subUrl) + '&remark=' + encodeURIComponent(app.sId))">Shadowrocket</a-menu-item>
|
||||
<a-menu-item key="ios-v2box"
|
||||
@click="open('v2box://install-sub?url=' + encodeURIComponent(app.subUrl) + '&name=' + encodeURIComponent(app.sId))">V2Box</a-menu-item>
|
||||
<a-menu-item key="ios-streisand"
|
||||
@click="open('streisand://import/' + encodeURIComponent(app.subUrl))">Streisand</a-menu-item>
|
||||
<a-menu-item key="ios-v2raytun"
|
||||
@click="copy(app.subUrl)">V2RayTun</a-menu-item>
|
||||
<a-menu-item key="ios-npvtunnel"
|
||||
@click="copy(app.subUrl)">NPV
|
||||
Tunnel</a-menu-item>
|
||||
</a-menu>
|
||||
</a-dropdown>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</a-card>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-layout-content>
|
||||
</a-layout>
|
||||
|
||||
<!-- Bootstrap data for external JS -->
|
||||
<template id="subscription-data" data-sid="{{ .sId }}"
|
||||
data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}"
|
||||
data-download="{{ .download }}"
|
||||
data-upload="{{ .upload }}" data-used="{{ .used }}"
|
||||
data-total="{{ .total }}" data-remained="{{ .remained }}"
|
||||
data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}"
|
||||
data-downloadbyte="{{ .downloadByte }}"
|
||||
data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}"
|
||||
data-datepicker="{{ .datepicker }}"></template>
|
||||
<textarea id="subscription-links"
|
||||
style="display:none">{{ range .result }}{{ . }}
|
||||
{{ end }}</textarea>
|
||||
|
||||
{{template "component/aThemeSwitch" .}}
|
||||
<script src="{{ .base_path }}assets/js/subscription.js?{{ .cur_ver }}"></script>
|
||||
|
||||
{{ template "page/body_end" .}}
|
||||
@@ -3,45 +3,10 @@
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/fold/foldgutter.css">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/xq.min.css?{{ .cur_ver }}">
|
||||
<link rel="stylesheet" href="{{ .base_path }}assets/codemirror/lint/lint.css">
|
||||
<style>
|
||||
@media (min-width: 769px) {
|
||||
.ant-layout-content {
|
||||
margin: 24px 16px;
|
||||
}
|
||||
}
|
||||
|
||||
@media (max-width: 768px) {
|
||||
.ant-tabs-nav .ant-tabs-tab {
|
||||
margin: 0;
|
||||
padding: 12px .5rem;
|
||||
}
|
||||
|
||||
.ant-table-thead>tr>th,
|
||||
.ant-table-tbody>tr>td {
|
||||
padding: 10px 0px;
|
||||
}
|
||||
}
|
||||
|
||||
.ant-tabs-bar {
|
||||
margin: 0;
|
||||
}
|
||||
|
||||
.ant-list-item {
|
||||
display: block;
|
||||
}
|
||||
|
||||
.ant-list-item>li {
|
||||
padding: 10px 20px !important;
|
||||
}
|
||||
|
||||
.ant-collapse-content-box .ant-alert {
|
||||
margin-block-end: 12px;
|
||||
}
|
||||
</style>
|
||||
{{ template "page/head_end" .}}
|
||||
|
||||
{{ template "page/body_start" .}}
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme">
|
||||
<a-layout id="app" v-cloak :class="themeSwitcher.currentTheme + ' xray-page'">
|
||||
<a-sidebar></a-sidebar>
|
||||
<a-layout id="content-layout">
|
||||
<a-layout-content>
|
||||
@@ -181,8 +146,9 @@
|
||||
{ title: "#", align: 'center', width: 15, scopedSlots: { customRender: 'action' } },
|
||||
{
|
||||
title: '{{ i18n "pages.xray.rules.source"}}', children: [
|
||||
{ title: 'IP', dataIndex: "source", align: 'center', width: 20, ellipsis: true },
|
||||
{ title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true }]
|
||||
{ title: 'IP', dataIndex: "sourceIP", align: 'center', width: 20, ellipsis: true },
|
||||
{ title: '{{ i18n "pages.inbounds.port" }}', dataIndex: 'sourcePort', align: 'center', width: 10, ellipsis: true },
|
||||
{ title: 'VLESS Route', dataIndex: 'vlessRoute', align: 'center', width: 15, ellipsis: true }]
|
||||
},
|
||||
{
|
||||
title: '{{ i18n "pages.inbounds.network"}}', children: [
|
||||
@@ -351,7 +317,7 @@
|
||||
{ label: '🇨🇳 China', value: 'geosite:cn' },
|
||||
{ label: '🇨🇳 .cn', value: 'regexp:.*\\.cn$' },
|
||||
{ label: '🇷🇺 Russia', value: 'ext:geosite_RU.dat:ru-available-only-inside' },
|
||||
{ label: '🇷🇺 .ru', value: 'regexp:.*\\.ru' },
|
||||
{ label: '🇷🇺 .ru', value: 'regexp:.*\\.ru$' },
|
||||
{ label: '🇷🇺 .su', value: 'regexp:.*\\.su$' },
|
||||
{ label: '🇷🇺 .рф', value: 'regexp:.*\\.xn--p1ai$' },
|
||||
{ label: '🇻🇳 .vn', value: 'regexp:.*\\.vn$' },
|
||||
@@ -420,7 +386,7 @@
|
||||
},
|
||||
async restartXray() {
|
||||
this.loading(true);
|
||||
const msg = await HttpUtil.post("server/restartXrayService");
|
||||
const msg = await HttpUtil.post("/panel/api/server/restartXrayService");
|
||||
this.loading(false);
|
||||
if (msg.success) {
|
||||
await PromiseUtil.sleep(500);
|
||||
@@ -572,7 +538,7 @@
|
||||
serverObj = o.settings.vnext;
|
||||
break;
|
||||
case Protocols.HTTP:
|
||||
case Protocols.Socks:
|
||||
case Protocols.Mixed:
|
||||
case Protocols.Shadowsocks:
|
||||
case Protocols.Trojan:
|
||||
serverObj = o.settings.servers;
|
||||
|
||||
@@ -8,10 +8,10 @@ import (
|
||||
"os"
|
||||
"os/exec"
|
||||
"regexp"
|
||||
"runtime"
|
||||
"sort"
|
||||
"time"
|
||||
|
||||
"slices"
|
||||
"x-ui/database"
|
||||
"x-ui/database/model"
|
||||
"x-ui/logger"
|
||||
@@ -40,12 +40,20 @@ func (j *CheckClientIpJob) Run() {
|
||||
f2bInstalled := j.checkFail2BanInstalled()
|
||||
isAccessLogAvailable := j.checkAccessLogAvailable(iplimitActive)
|
||||
|
||||
if iplimitActive {
|
||||
if f2bInstalled && isAccessLogAvailable {
|
||||
shouldClearAccessLog = j.processLogFile()
|
||||
if isAccessLogAvailable {
|
||||
if runtime.GOOS == "windows" {
|
||||
if iplimitActive {
|
||||
shouldClearAccessLog = j.processLogFile()
|
||||
}
|
||||
} else {
|
||||
if !f2bInstalled {
|
||||
logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.")
|
||||
if iplimitActive {
|
||||
if f2bInstalled {
|
||||
shouldClearAccessLog = j.processLogFile()
|
||||
} else {
|
||||
if !f2bInstalled {
|
||||
logger.Warning("[LimitIP] Fail2Ban is not installed, Please install Fail2Ban from the x-ui bash menu.")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -58,21 +66,21 @@ func (j *CheckClientIpJob) Run() {
|
||||
func (j *CheckClientIpJob) clearAccessLog() {
|
||||
logAccessP, err := os.OpenFile(xray.GetAccessPersistentLogPath(), os.O_CREATE|os.O_APPEND|os.O_WRONLY, 0o644)
|
||||
j.checkError(err)
|
||||
defer logAccessP.Close()
|
||||
|
||||
accessLogPath, err := xray.GetAccessLogPath()
|
||||
j.checkError(err)
|
||||
|
||||
file, err := os.Open(accessLogPath)
|
||||
j.checkError(err)
|
||||
defer file.Close()
|
||||
|
||||
_, err = io.Copy(logAccessP, file)
|
||||
j.checkError(err)
|
||||
|
||||
logAccessP.Close()
|
||||
file.Close()
|
||||
|
||||
err = os.Truncate(accessLogPath, 0)
|
||||
j.checkError(err)
|
||||
|
||||
j.lastClear = time.Now().Unix()
|
||||
}
|
||||
|
||||
@@ -193,10 +201,6 @@ func (j *CheckClientIpJob) checkError(e error) {
|
||||
}
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) contains(s []string, str string) bool {
|
||||
return slices.Contains(s, str)
|
||||
}
|
||||
|
||||
func (j *CheckClientIpJob) getInboundClientIps(clientEmail string) (*model.InboundClientIps, error) {
|
||||
db := database.GetDB()
|
||||
InboundClientIps := &model.InboundClientIps{}
|
||||
|
||||
@@ -3,6 +3,7 @@ package locale
|
||||
import (
|
||||
"embed"
|
||||
"io/fs"
|
||||
"os"
|
||||
"strings"
|
||||
|
||||
"x-ui/logger"
|
||||
@@ -78,6 +79,11 @@ func I18n(i18nType I18nType, key string, params ...string) string {
|
||||
|
||||
templateData := createTemplateData(params)
|
||||
|
||||
if localizer == nil {
|
||||
// Fallback to key if localizer not ready; prevents nil panic on pages like sub
|
||||
return key
|
||||
}
|
||||
|
||||
msg, err := localizer.Localize(&i18n.LocalizeConfig{
|
||||
MessageID: key,
|
||||
TemplateData: templateData,
|
||||
@@ -102,6 +108,15 @@ func initTGBotLocalizer(settingService SettingService) error {
|
||||
|
||||
func LocalizerMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// Ensure bundle is initialized so creating a Localizer won't panic
|
||||
if i18nBundle == nil {
|
||||
i18nBundle = i18n.NewBundle(language.MustParse("en-US"))
|
||||
i18nBundle.RegisterUnmarshalFunc("toml", toml.Unmarshal)
|
||||
// Try lazy-load from disk when running sub server without InitLocalizer
|
||||
if err := loadTranslationsFromDisk(i18nBundle); err != nil {
|
||||
logger.Warning("i18n lazy load failed:", err)
|
||||
}
|
||||
}
|
||||
var lang string
|
||||
|
||||
if cookie, err := c.Request.Cookie("lang"); err == nil {
|
||||
@@ -118,6 +133,25 @@ func LocalizerMiddleware() gin.HandlerFunc {
|
||||
}
|
||||
}
|
||||
|
||||
// loadTranslationsFromDisk attempts to load translation files from "web/translation" using the local filesystem.
|
||||
func loadTranslationsFromDisk(bundle *i18n.Bundle) error {
|
||||
root := os.DirFS("web")
|
||||
return fs.WalkDir(root, "translation", func(path string, d fs.DirEntry, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if d.IsDir() {
|
||||
return nil
|
||||
}
|
||||
data, err := fs.ReadFile(root, path)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
_, err = bundle.ParseMessageFileBytes(data, path)
|
||||
return err
|
||||
})
|
||||
}
|
||||
|
||||
func parseTranslationFiles(i18nFS embed.FS, i18nBundle *i18n.Bundle) error {
|
||||
err := fs.WalkDir(i18nFS, "translation",
|
||||
func(path string, d fs.DirEntry, err error) error {
|
||||
|
||||
@@ -19,7 +19,7 @@
|
||||
"tag": "api",
|
||||
"listen": "127.0.0.1",
|
||||
"port": 62789,
|
||||
"protocol": "dokodemo-door",
|
||||
"protocol": "tunnel",
|
||||
"settings": {
|
||||
"address": "127.0.0.1"
|
||||
}
|
||||
|
||||
@@ -175,17 +175,42 @@ func (s *InboundService) AddInbound(inbound *model.Inbound) (*model.Inbound, boo
|
||||
return inbound, false, err
|
||||
}
|
||||
|
||||
// Ensure created_at and updated_at on clients in settings
|
||||
if len(clients) > 0 {
|
||||
var settings map[string]any
|
||||
if err2 := json.Unmarshal([]byte(inbound.Settings), &settings); err2 == nil && settings != nil {
|
||||
now := time.Now().Unix() * 1000
|
||||
updatedClients := make([]model.Client, 0, len(clients))
|
||||
for _, c := range clients {
|
||||
if c.CreatedAt == 0 {
|
||||
c.CreatedAt = now
|
||||
}
|
||||
c.UpdatedAt = now
|
||||
updatedClients = append(updatedClients, c)
|
||||
}
|
||||
settings["clients"] = updatedClients
|
||||
if bs, err3 := json.MarshalIndent(settings, "", " "); err3 == nil {
|
||||
inbound.Settings = string(bs)
|
||||
} else {
|
||||
logger.Debug("Unable to marshal inbound settings with timestamps:", err3)
|
||||
}
|
||||
} else if err2 != nil {
|
||||
logger.Debug("Unable to parse inbound settings for timestamps:", err2)
|
||||
}
|
||||
}
|
||||
|
||||
// Secure client ID
|
||||
for _, client := range clients {
|
||||
if inbound.Protocol == "trojan" {
|
||||
switch inbound.Protocol {
|
||||
case "trojan":
|
||||
if client.Password == "" {
|
||||
return inbound, false, common.NewError("empty client ID")
|
||||
}
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
if client.Email == "" {
|
||||
return inbound, false, common.NewError("empty client ID")
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
if client.ID == "" {
|
||||
return inbound, false, common.NewError("empty client ID")
|
||||
}
|
||||
@@ -319,6 +344,65 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
return inbound, false, err
|
||||
}
|
||||
|
||||
// Ensure created_at and updated_at exist in inbound.Settings clients
|
||||
{
|
||||
var oldSettings map[string]any
|
||||
_ = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
|
||||
emailToCreated := map[string]int64{}
|
||||
emailToUpdated := map[string]int64{}
|
||||
if oldSettings != nil {
|
||||
if oc, ok := oldSettings["clients"].([]any); ok {
|
||||
for _, it := range oc {
|
||||
if m, ok2 := it.(map[string]any); ok2 {
|
||||
if email, ok3 := m["email"].(string); ok3 {
|
||||
switch v := m["created_at"].(type) {
|
||||
case float64:
|
||||
emailToCreated[email] = int64(v)
|
||||
case int64:
|
||||
emailToCreated[email] = v
|
||||
}
|
||||
switch v := m["updated_at"].(type) {
|
||||
case float64:
|
||||
emailToUpdated[email] = int64(v)
|
||||
case int64:
|
||||
emailToUpdated[email] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
var newSettings map[string]any
|
||||
if err2 := json.Unmarshal([]byte(inbound.Settings), &newSettings); err2 == nil && newSettings != nil {
|
||||
now := time.Now().Unix() * 1000
|
||||
if nSlice, ok := newSettings["clients"].([]any); ok {
|
||||
for i := range nSlice {
|
||||
if m, ok2 := nSlice[i].(map[string]any); ok2 {
|
||||
email, _ := m["email"].(string)
|
||||
if _, ok3 := m["created_at"]; !ok3 {
|
||||
if v, ok4 := emailToCreated[email]; ok4 && v > 0 {
|
||||
m["created_at"] = v
|
||||
} else {
|
||||
m["created_at"] = now
|
||||
}
|
||||
}
|
||||
// Preserve client's updated_at if present; do not bump on parent inbound update
|
||||
if _, hasUpdated := m["updated_at"]; !hasUpdated {
|
||||
if v, ok4 := emailToUpdated[email]; ok4 && v > 0 {
|
||||
m["updated_at"] = v
|
||||
}
|
||||
}
|
||||
nSlice[i] = m
|
||||
}
|
||||
}
|
||||
newSettings["clients"] = nSlice
|
||||
if bs, err3 := json.MarshalIndent(newSettings, "", " "); err3 == nil {
|
||||
inbound.Settings = string(bs)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
oldInbound.Up = inbound.Up
|
||||
oldInbound.Down = inbound.Down
|
||||
oldInbound.Total = inbound.Total
|
||||
@@ -331,7 +415,6 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
||||
oldInbound.Settings = inbound.Settings
|
||||
oldInbound.StreamSettings = inbound.StreamSettings
|
||||
oldInbound.Sniffing = inbound.Sniffing
|
||||
oldInbound.Allocate = inbound.Allocate
|
||||
if inbound.Listen == "" || inbound.Listen == "0.0.0.0" || inbound.Listen == "::" || inbound.Listen == "::0" {
|
||||
oldInbound.Tag = fmt.Sprintf("inbound-%v", inbound.Port)
|
||||
} else {
|
||||
@@ -421,6 +504,17 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
||||
}
|
||||
|
||||
interfaceClients := settings["clients"].([]any)
|
||||
// Add timestamps for new clients being appended
|
||||
nowTs := time.Now().Unix() * 1000
|
||||
for i := range interfaceClients {
|
||||
if cm, ok := interfaceClients[i].(map[string]any); ok {
|
||||
if _, ok2 := cm["created_at"]; !ok2 {
|
||||
cm["created_at"] = nowTs
|
||||
}
|
||||
cm["updated_at"] = nowTs
|
||||
interfaceClients[i] = cm
|
||||
}
|
||||
}
|
||||
existEmail, err := s.checkEmailsExistForClients(clients)
|
||||
if err != nil {
|
||||
return false, err
|
||||
@@ -436,15 +530,16 @@ func (s *InboundService) AddInboundClient(data *model.Inbound) (bool, error) {
|
||||
|
||||
// Secure client ID
|
||||
for _, client := range clients {
|
||||
if oldInbound.Protocol == "trojan" {
|
||||
switch oldInbound.Protocol {
|
||||
case "trojan":
|
||||
if client.Password == "" {
|
||||
return false, common.NewError("empty client ID")
|
||||
}
|
||||
} else if oldInbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
if client.Email == "" {
|
||||
return false, common.NewError("empty client ID")
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
if client.ID == "" {
|
||||
return false, common.NewError("empty client ID")
|
||||
}
|
||||
@@ -631,13 +726,14 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||
clientIndex := -1
|
||||
for index, oldClient := range oldClients {
|
||||
oldClientId := ""
|
||||
if oldInbound.Protocol == "trojan" {
|
||||
switch oldInbound.Protocol {
|
||||
case "trojan":
|
||||
oldClientId = oldClient.Password
|
||||
newClientId = clients[0].Password
|
||||
} else if oldInbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
oldClientId = oldClient.Email
|
||||
newClientId = clients[0].Email
|
||||
} else {
|
||||
default:
|
||||
oldClientId = oldClient.ID
|
||||
newClientId = clients[0].ID
|
||||
}
|
||||
@@ -669,6 +765,25 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
|
||||
return false, err
|
||||
}
|
||||
settingsClients := oldSettings["clients"].([]any)
|
||||
// Preserve created_at and set updated_at for the replacing client
|
||||
var preservedCreated any
|
||||
if clientIndex >= 0 && clientIndex < len(settingsClients) {
|
||||
if oldMap, ok := settingsClients[clientIndex].(map[string]any); ok {
|
||||
if v, ok2 := oldMap["created_at"]; ok2 {
|
||||
preservedCreated = v
|
||||
}
|
||||
}
|
||||
}
|
||||
if len(interfaceClients) > 0 {
|
||||
if newMap, ok := interfaceClients[0].(map[string]any); ok {
|
||||
if preservedCreated == nil {
|
||||
preservedCreated = time.Now().Unix() * 1000
|
||||
}
|
||||
newMap["created_at"] = preservedCreated
|
||||
newMap["updated_at"] = time.Now().Unix() * 1000
|
||||
interfaceClients[0] = newMap
|
||||
}
|
||||
}
|
||||
settingsClients[clientIndex] = interfaceClients[0]
|
||||
oldSettings["clients"] = settingsClients
|
||||
|
||||
@@ -811,8 +926,9 @@ func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic
|
||||
if traffic.IsInbound {
|
||||
err = tx.Model(&model.Inbound{}).Where("tag = ?", traffic.Tag).
|
||||
Updates(map[string]any{
|
||||
"up": gorm.Expr("up + ?", traffic.Up),
|
||||
"down": gorm.Expr("down + ?", traffic.Down),
|
||||
"up": gorm.Expr("up + ?", traffic.Up),
|
||||
"down": gorm.Expr("down + ?", traffic.Down),
|
||||
"all_time": gorm.Expr("COALESCE(all_time, 0) + ?", traffic.Up+traffic.Down),
|
||||
}).Error
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -858,10 +974,12 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
|
||||
if dbClientTraffics[dbTraffic_index].Email == traffics[traffic_index].Email {
|
||||
dbClientTraffics[dbTraffic_index].Up += traffics[traffic_index].Up
|
||||
dbClientTraffics[dbTraffic_index].Down += traffics[traffic_index].Down
|
||||
dbClientTraffics[dbTraffic_index].AllTime += (traffics[traffic_index].Up + traffics[traffic_index].Down)
|
||||
|
||||
// Add user in onlineUsers array on traffic
|
||||
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
|
||||
onlineClients = append(onlineClients, traffics[traffic_index].Email)
|
||||
dbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli()
|
||||
}
|
||||
break
|
||||
}
|
||||
@@ -906,10 +1024,16 @@ func (s *InboundService) adjustTraffics(tx *gorm.DB, dbClientTraffics []*xray.Cl
|
||||
oldExpiryTime := c["expiryTime"].(float64)
|
||||
newExpiryTime := (time.Now().Unix() * 1000) - int64(oldExpiryTime)
|
||||
c["expiryTime"] = newExpiryTime
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
dbClientTraffics[traffic_index].ExpiryTime = newExpiryTime
|
||||
break
|
||||
}
|
||||
}
|
||||
// Backfill created_at and updated_at
|
||||
if _, ok := c["created_at"]; !ok {
|
||||
c["created_at"] = time.Now().Unix() * 1000
|
||||
}
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
settings["clients"] = newClients
|
||||
@@ -1244,11 +1368,12 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo
|
||||
|
||||
for _, oldClient := range oldClients {
|
||||
if oldClient.Email == clientEmail {
|
||||
if inbound.Protocol == "trojan" {
|
||||
switch inbound.Protocol {
|
||||
case "trojan":
|
||||
clientId = oldClient.Password
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
clientId = oldClient.Email
|
||||
} else {
|
||||
default:
|
||||
clientId = oldClient.ID
|
||||
}
|
||||
break
|
||||
@@ -1270,6 +1395,7 @@ func (s *InboundService) SetClientTelegramUserID(trafficId int, tgId int64) (boo
|
||||
c := clients[client_index].(map[string]any)
|
||||
if c["email"] == clientEmail {
|
||||
c["tgId"] = tgId
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
}
|
||||
@@ -1328,11 +1454,12 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
|
||||
|
||||
for _, oldClient := range oldClients {
|
||||
if oldClient.Email == clientEmail {
|
||||
if inbound.Protocol == "trojan" {
|
||||
switch inbound.Protocol {
|
||||
case "trojan":
|
||||
clientId = oldClient.Password
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
clientId = oldClient.Email
|
||||
} else {
|
||||
default:
|
||||
clientId = oldClient.ID
|
||||
}
|
||||
clientOldEnabled = oldClient.Enable
|
||||
@@ -1355,6 +1482,7 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
|
||||
c := clients[client_index].(map[string]any)
|
||||
if c["email"] == clientEmail {
|
||||
c["enable"] = !clientOldEnabled
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
}
|
||||
@@ -1391,11 +1519,12 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
|
||||
|
||||
for _, oldClient := range oldClients {
|
||||
if oldClient.Email == clientEmail {
|
||||
if inbound.Protocol == "trojan" {
|
||||
switch inbound.Protocol {
|
||||
case "trojan":
|
||||
clientId = oldClient.Password
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
clientId = oldClient.Email
|
||||
} else {
|
||||
default:
|
||||
clientId = oldClient.ID
|
||||
}
|
||||
break
|
||||
@@ -1417,6 +1546,7 @@ func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int
|
||||
c := clients[client_index].(map[string]any)
|
||||
if c["email"] == clientEmail {
|
||||
c["limitIp"] = count
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
}
|
||||
@@ -1448,11 +1578,12 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
|
||||
|
||||
for _, oldClient := range oldClients {
|
||||
if oldClient.Email == clientEmail {
|
||||
if inbound.Protocol == "trojan" {
|
||||
switch inbound.Protocol {
|
||||
case "trojan":
|
||||
clientId = oldClient.Password
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
clientId = oldClient.Email
|
||||
} else {
|
||||
default:
|
||||
clientId = oldClient.ID
|
||||
}
|
||||
break
|
||||
@@ -1474,6 +1605,7 @@ func (s *InboundService) ResetClientExpiryTimeByEmail(clientEmail string, expiry
|
||||
c := clients[client_index].(map[string]any)
|
||||
if c["email"] == clientEmail {
|
||||
c["expiryTime"] = expiry_time
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
}
|
||||
@@ -1508,11 +1640,12 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
|
||||
|
||||
for _, oldClient := range oldClients {
|
||||
if oldClient.Email == clientEmail {
|
||||
if inbound.Protocol == "trojan" {
|
||||
switch inbound.Protocol {
|
||||
case "trojan":
|
||||
clientId = oldClient.Password
|
||||
} else if inbound.Protocol == "shadowsocks" {
|
||||
case "shadowsocks":
|
||||
clientId = oldClient.Email
|
||||
} else {
|
||||
default:
|
||||
clientId = oldClient.ID
|
||||
}
|
||||
break
|
||||
@@ -1534,6 +1667,7 @@ func (s *InboundService) ResetClientTrafficLimitByEmail(clientEmail string, tota
|
||||
c := clients[client_index].(map[string]any)
|
||||
if c["email"] == clientEmail {
|
||||
c["totalGB"] = totalGB * 1024 * 1024 * 1024
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
}
|
||||
@@ -1916,6 +2050,25 @@ func (s *InboundService) MigrationRequirements() {
|
||||
}
|
||||
}()
|
||||
|
||||
// Calculate and backfill all_time from up+down for inbounds and clients
|
||||
err = tx.Exec(`
|
||||
UPDATE inbounds
|
||||
SET all_time = IFNULL(up, 0) + IFNULL(down, 0)
|
||||
WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0
|
||||
`).Error
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
err = tx.Exec(`
|
||||
UPDATE client_traffics
|
||||
SET all_time = IFNULL(up, 0) + IFNULL(down, 0)
|
||||
WHERE IFNULL(all_time, 0) = 0 AND (IFNULL(up, 0) + IFNULL(down, 0)) > 0
|
||||
`).Error
|
||||
|
||||
if err != nil {
|
||||
return
|
||||
}
|
||||
|
||||
// Fix inbounds based problems
|
||||
var inbounds []*model.Inbound
|
||||
err = tx.Model(model.Inbound{}).Where("protocol IN (?)", []string{"vmess", "vless", "trojan"}).Find(&inbounds).Error
|
||||
@@ -1954,6 +2107,11 @@ func (s *InboundService) MigrationRequirements() {
|
||||
c["flow"] = ""
|
||||
}
|
||||
}
|
||||
// Backfill created_at and updated_at
|
||||
if _, ok := c["created_at"]; !ok {
|
||||
c["created_at"] = time.Now().Unix() * 1000
|
||||
}
|
||||
c["updated_at"] = time.Now().Unix() * 1000
|
||||
newClients = append(newClients, any(c))
|
||||
}
|
||||
settings["clients"] = newClients
|
||||
@@ -2042,6 +2200,20 @@ func (s *InboundService) GetOnlineClients() []string {
|
||||
return p.GetOnlineClients()
|
||||
}
|
||||
|
||||
func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {
|
||||
db := database.GetDB()
|
||||
var rows []xray.ClientTraffic
|
||||
err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]int64, len(rows))
|
||||
for _, r := range rows {
|
||||
result[r.Email] = r.LastOnline
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
|
||||
db := database.GetDB()
|
||||
|
||||
@@ -2075,3 +2247,95 @@ func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, [
|
||||
|
||||
return validEmails, extraEmails, nil
|
||||
}
|
||||
func (s *InboundService) DelInboundClientByEmail(inboundId int, email string) (bool, error) {
|
||||
oldInbound, err := s.GetInbound(inboundId)
|
||||
if err != nil {
|
||||
logger.Error("Load Old Data Error")
|
||||
return false, err
|
||||
}
|
||||
|
||||
var settings map[string]any
|
||||
if err := json.Unmarshal([]byte(oldInbound.Settings), &settings); err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
interfaceClients, ok := settings["clients"].([]any)
|
||||
if !ok {
|
||||
return false, common.NewError("invalid clients format in inbound settings")
|
||||
}
|
||||
|
||||
var newClients []any
|
||||
needApiDel := false
|
||||
found := false
|
||||
|
||||
for _, client := range interfaceClients {
|
||||
c, ok := client.(map[string]any)
|
||||
if !ok {
|
||||
continue
|
||||
}
|
||||
if cEmail, ok := c["email"].(string); ok && cEmail == email {
|
||||
// matched client, drop it
|
||||
found = true
|
||||
needApiDel, _ = c["enable"].(bool)
|
||||
} else {
|
||||
newClients = append(newClients, client)
|
||||
}
|
||||
}
|
||||
|
||||
if !found {
|
||||
return false, common.NewError(fmt.Sprintf("client with email %s not found", email))
|
||||
}
|
||||
if len(newClients) == 0 {
|
||||
return false, common.NewError("no client remained in Inbound")
|
||||
}
|
||||
|
||||
settings["clients"] = newClients
|
||||
newSettings, err := json.MarshalIndent(settings, "", " ")
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
|
||||
oldInbound.Settings = string(newSettings)
|
||||
|
||||
db := database.GetDB()
|
||||
|
||||
// remove IP bindings
|
||||
if err := s.DelClientIPs(db, email); err != nil {
|
||||
logger.Error("Error in delete client IPs")
|
||||
return false, err
|
||||
}
|
||||
|
||||
needRestart := false
|
||||
|
||||
// remove stats too
|
||||
if len(email) > 0 {
|
||||
traffic, err := s.GetClientTrafficByEmail(email)
|
||||
if err != nil {
|
||||
return false, err
|
||||
}
|
||||
if traffic != nil {
|
||||
if err := s.DelClientStat(db, email); err != nil {
|
||||
logger.Error("Delete stats Data Error")
|
||||
return false, err
|
||||
}
|
||||
}
|
||||
|
||||
if needApiDel {
|
||||
s.xrayApi.Init(p.GetAPIPort())
|
||||
if err1 := s.xrayApi.RemoveUser(oldInbound.Tag, email); err1 == nil {
|
||||
logger.Debug("Client deleted by api:", email)
|
||||
needRestart = false
|
||||
} else {
|
||||
if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", email)) {
|
||||
logger.Debug("User is already deleted. Nothing to do more...")
|
||||
} else {
|
||||
logger.Debug("Error in deleting client by api:", err1)
|
||||
needRestart = true
|
||||
}
|
||||
}
|
||||
s.xrayApi.Close()
|
||||
}
|
||||
}
|
||||
|
||||
return needRestart, db.Save(oldInbound).Error
|
||||
}
|
||||
|
||||
@@ -12,6 +12,7 @@ import (
|
||||
"net/http"
|
||||
"os"
|
||||
"os/exec"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"strconv"
|
||||
"strings"
|
||||
@@ -24,6 +25,7 @@ import (
|
||||
"x-ui/util/sys"
|
||||
"x-ui/xray"
|
||||
|
||||
"github.com/google/uuid"
|
||||
"github.com/shirou/gopsutil/v4/cpu"
|
||||
"github.com/shirou/gopsutil/v4/disk"
|
||||
"github.com/shirou/gopsutil/v4/host"
|
||||
@@ -235,8 +237,21 @@ func (s *ServerService) GetStatus(lastStatus *Status) *Status {
|
||||
}
|
||||
|
||||
// IP fetching with caching
|
||||
showIp4ServiceLists := []string{"https://api.ipify.org", "https://4.ident.me"}
|
||||
showIp6ServiceLists := []string{"https://api6.ipify.org", "https://6.ident.me"}
|
||||
showIp4ServiceLists := []string{
|
||||
"https://api4.ipify.org",
|
||||
"https://ipv4.icanhazip.com",
|
||||
"https://v4.api.ipinfo.io/ip",
|
||||
"https://ipv4.myexternalip.com/raw",
|
||||
"https://4.ident.me",
|
||||
"https://check-host.net/ip",
|
||||
}
|
||||
showIp6ServiceLists := []string{
|
||||
"https://api6.ipify.org",
|
||||
"https://ipv6.icanhazip.com",
|
||||
"https://v6.api.ipinfo.io/ip",
|
||||
"https://ipv6.myexternalip.com/raw",
|
||||
"https://6.ident.me",
|
||||
}
|
||||
|
||||
if s.cachedIPv4 == "" {
|
||||
for _, ip4Service := range showIp4ServiceLists {
|
||||
@@ -330,7 +345,7 @@ func (s *ServerService) GetXrayVersions() ([]string, error) {
|
||||
continue
|
||||
}
|
||||
|
||||
if major > 25 || (major == 25 && minor > 8) || (major == 25 && minor == 8 && patch >= 3) {
|
||||
if major > 25 || (major == 25 && minor > 9) || (major == 25 && minor == 9 && patch >= 11) {
|
||||
versions = append(versions, release.TagName)
|
||||
}
|
||||
}
|
||||
@@ -362,6 +377,8 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
|
||||
switch osName {
|
||||
case "darwin":
|
||||
osName = "macos"
|
||||
case "windows":
|
||||
osName = "windows"
|
||||
}
|
||||
|
||||
switch arch {
|
||||
@@ -405,19 +422,23 @@ func (s *ServerService) downloadXRay(version string) (string, error) {
|
||||
}
|
||||
|
||||
func (s *ServerService) UpdateXray(version string) error {
|
||||
// 1. Stop xray before doing anything
|
||||
if err := s.StopXrayService(); err != nil {
|
||||
logger.Warning("failed to stop xray before update:", err)
|
||||
}
|
||||
|
||||
// 2. Download the zip
|
||||
zipFileName, err := s.downloadXRay(version)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer os.Remove(zipFileName)
|
||||
|
||||
zipFile, err := os.Open(zipFileName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer func() {
|
||||
zipFile.Close()
|
||||
os.Remove(zipFileName)
|
||||
}()
|
||||
defer zipFile.Close()
|
||||
|
||||
stat, err := zipFile.Stat()
|
||||
if err != nil {
|
||||
@@ -428,19 +449,14 @@ func (s *ServerService) UpdateXray(version string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
s.xrayService.StopXray()
|
||||
defer func() {
|
||||
err := s.xrayService.RestartXray(true)
|
||||
if err != nil {
|
||||
logger.Error("start xray failed:", err)
|
||||
}
|
||||
}()
|
||||
|
||||
// 3. Helper to extract files
|
||||
copyZipFile := func(zipName string, fileName string) error {
|
||||
zipFile, err := reader.Open(zipName)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer zipFile.Close()
|
||||
os.MkdirAll(filepath.Dir(fileName), 0755)
|
||||
os.Remove(fileName)
|
||||
file, err := os.OpenFile(fileName, os.O_CREATE|os.O_RDWR|os.O_TRUNC, fs.ModePerm)
|
||||
if err != nil {
|
||||
@@ -451,11 +467,23 @@ func (s *ServerService) UpdateXray(version string) error {
|
||||
return err
|
||||
}
|
||||
|
||||
err = copyZipFile("xray", xray.GetBinaryPath())
|
||||
// 4. Extract correct binary
|
||||
if runtime.GOOS == "windows" {
|
||||
targetBinary := filepath.Join("bin", "xray-windows-amd64.exe")
|
||||
err = copyZipFile("xray.exe", targetBinary)
|
||||
} else {
|
||||
err = copyZipFile("xray", xray.GetBinaryPath())
|
||||
}
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 5. Restart xray
|
||||
if err := s.xrayService.RestartXray(true); err != nil {
|
||||
logger.Error("start xray failed:", err)
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@@ -858,3 +886,80 @@ func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
|
||||
"echConfigList": configList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ServerService) GetNewVlessEnc() (any, error) {
|
||||
cmd := exec.Command(xray.GetBinaryPath(), "vlessenc")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(out.String(), "\n")
|
||||
var auths []map[string]string
|
||||
var current map[string]string
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Authentication:") {
|
||||
if current != nil {
|
||||
auths = append(auths, current)
|
||||
}
|
||||
current = map[string]string{
|
||||
"label": strings.TrimSpace(strings.TrimPrefix(line, "Authentication:")),
|
||||
}
|
||||
} else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 && current != nil {
|
||||
key := strings.Trim(parts[0], `" `)
|
||||
val := strings.Trim(parts[1], `" `)
|
||||
current[key] = val
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if current != nil {
|
||||
auths = append(auths, current)
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"auths": auths,
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ServerService) GetNewUUID() (map[string]string, error) {
|
||||
newUUID, err := uuid.NewRandom()
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate UUID: %w", err)
|
||||
}
|
||||
|
||||
return map[string]string{
|
||||
"uuid": newUUID.String(),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ServerService) GetNewmlkem768() (any, error) {
|
||||
// Run the command
|
||||
cmd := exec.Command(xray.GetBinaryPath(), "mlkem768")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
err := cmd.Run()
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(out.String(), "\n")
|
||||
|
||||
SeedLine := strings.Split(lines[0], ":")
|
||||
ClientLine := strings.Split(lines[1], ":")
|
||||
|
||||
seed := strings.TrimSpace(SeedLine[1])
|
||||
client := strings.TrimSpace(ClientLine[1])
|
||||
|
||||
keyPair := map[string]any{
|
||||
"seed": seed,
|
||||
"client": client,
|
||||
}
|
||||
|
||||
return keyPair, nil
|
||||
}
|
||||
|
||||
@@ -7,8 +7,10 @@ import (
|
||||
"encoding/base64"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io"
|
||||
"math/big"
|
||||
"net"
|
||||
"net/http"
|
||||
"net/url"
|
||||
"os"
|
||||
"regexp"
|
||||
@@ -29,6 +31,7 @@ import (
|
||||
"github.com/mymmrac/telego"
|
||||
th "github.com/mymmrac/telego/telegohandler"
|
||||
tu "github.com/mymmrac/telego/telegoutil"
|
||||
"github.com/skip2/go-qrcode"
|
||||
"github.com/valyala/fasthttp"
|
||||
"github.com/valyala/fasthttp/fasthttpproxy"
|
||||
)
|
||||
@@ -40,7 +43,6 @@ var (
|
||||
isRunning bool
|
||||
hostname string
|
||||
hashStorage *global.HashStorage
|
||||
handler *th.Handler
|
||||
|
||||
// clients data to adding new client
|
||||
receiver_inbound_ID int
|
||||
@@ -641,13 +643,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if len(dataArray) == 4 {
|
||||
num, err := strconv.Atoi(dataArray[3])
|
||||
if err == nil {
|
||||
if num == -2 {
|
||||
switch num {
|
||||
case -2:
|
||||
inputNumber = 0
|
||||
} else if num == -1 {
|
||||
case -1:
|
||||
if inputNumber > 0 {
|
||||
inputNumber = (inputNumber / 10)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
inputNumber = (inputNumber * 10) + num
|
||||
}
|
||||
}
|
||||
@@ -704,6 +707,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
return
|
||||
}
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
if err != nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
|
||||
@@ -715,13 +722,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if len(dataArray) == 3 {
|
||||
num, err := strconv.Atoi(dataArray[2])
|
||||
if err == nil {
|
||||
if num == -2 {
|
||||
switch num {
|
||||
case -2:
|
||||
inputNumber = 0
|
||||
} else if num == -1 {
|
||||
case -1:
|
||||
if inputNumber > 0 {
|
||||
inputNumber = (inputNumber / 10)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
inputNumber = (inputNumber * 10) + num
|
||||
}
|
||||
}
|
||||
@@ -844,13 +852,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if len(dataArray) == 4 {
|
||||
num, err := strconv.Atoi(dataArray[3])
|
||||
if err == nil {
|
||||
if num == -2 {
|
||||
switch num {
|
||||
case -2:
|
||||
inputNumber = 0
|
||||
} else if num == -1 {
|
||||
case -1:
|
||||
if inputNumber > 0 {
|
||||
inputNumber = (inputNumber / 10)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
inputNumber = (inputNumber * 10) + num
|
||||
}
|
||||
}
|
||||
@@ -919,6 +928,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
return
|
||||
}
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
if err != nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
|
||||
@@ -930,13 +943,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if len(dataArray) == 3 {
|
||||
num, err := strconv.Atoi(dataArray[2])
|
||||
if err == nil {
|
||||
if num == -2 {
|
||||
switch num {
|
||||
case -2:
|
||||
inputNumber = 0
|
||||
} else if num == -1 {
|
||||
case -1:
|
||||
if inputNumber > 0 {
|
||||
inputNumber = (inputNumber / 10)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
inputNumber = (inputNumber * 10) + num
|
||||
}
|
||||
}
|
||||
@@ -1035,13 +1049,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if len(dataArray) == 4 {
|
||||
num, err := strconv.Atoi(dataArray[3])
|
||||
if err == nil {
|
||||
if num == -2 {
|
||||
switch num {
|
||||
case -2:
|
||||
inputNumber = 0
|
||||
} else if num == -1 {
|
||||
case -1:
|
||||
if inputNumber > 0 {
|
||||
inputNumber = (inputNumber / 10)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
inputNumber = (inputNumber * 10) + num
|
||||
}
|
||||
}
|
||||
@@ -1101,6 +1116,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
return
|
||||
}
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
if err != nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t.addClient(callbackQuery.Message.GetChat().ID, message_text, messageId)
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.successfulOperation"))
|
||||
@@ -1112,13 +1131,14 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
if len(dataArray) == 3 {
|
||||
num, err := strconv.Atoi(dataArray[2])
|
||||
if err == nil {
|
||||
if num == -2 {
|
||||
switch num {
|
||||
case -2:
|
||||
inputNumber = 0
|
||||
} else if num == -1 {
|
||||
case -1:
|
||||
if inputNumber > 0 {
|
||||
inputNumber = (inputNumber / 10)
|
||||
}
|
||||
} else {
|
||||
default:
|
||||
inputNumber = (inputNumber * 10) + num
|
||||
}
|
||||
}
|
||||
@@ -1288,6 +1308,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
}
|
||||
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
if err != nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
t.addClient(callbackQuery.Message.GetChat().ID, message_text)
|
||||
}
|
||||
@@ -1334,6 +1358,73 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
case "client_commands":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.commands"))
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.helpClientCommands"))
|
||||
case "client_sub_links":
|
||||
// show user's own clients to choose one for sub links
|
||||
tgUserID := callbackQuery.From.ID
|
||||
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
|
||||
if err != nil {
|
||||
// fallback to message
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||
return
|
||||
}
|
||||
if len(traffics) == 0 {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10)))
|
||||
return
|
||||
}
|
||||
var buttons []telego.InlineKeyboardButton
|
||||
for _, tr := range traffics {
|
||||
buttons = append(buttons, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery("client_sub_links "+tr.Email)))
|
||||
}
|
||||
cols := 1
|
||||
if len(buttons) >= 6 {
|
||||
cols = 2
|
||||
}
|
||||
keyboard := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols, buttons...))
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), keyboard)
|
||||
case "client_individual_links":
|
||||
// show user's clients to choose for individual links
|
||||
tgUserID := callbackQuery.From.ID
|
||||
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
|
||||
if err != nil {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||
return
|
||||
}
|
||||
if len(traffics) == 0 {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10)))
|
||||
return
|
||||
}
|
||||
var buttons2 []telego.InlineKeyboardButton
|
||||
for _, tr := range traffics {
|
||||
buttons2 = append(buttons2, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery("client_individual_links "+tr.Email)))
|
||||
}
|
||||
cols2 := 1
|
||||
if len(buttons2) >= 6 {
|
||||
cols2 = 2
|
||||
}
|
||||
keyboard2 := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols2, buttons2...))
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), keyboard2)
|
||||
case "client_qr_links":
|
||||
// show user's clients to choose for QR codes
|
||||
tgUserID := callbackQuery.From.ID
|
||||
traffics, err := t.inboundService.GetClientTrafficTgBot(tgUserID)
|
||||
if err != nil {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOccurred")+"\r\n"+err.Error())
|
||||
return
|
||||
}
|
||||
if len(traffics) == 0 {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.askToAddUserId", "TgUserID=="+strconv.FormatInt(tgUserID, 10)))
|
||||
return
|
||||
}
|
||||
var buttons3 []telego.InlineKeyboardButton
|
||||
for _, tr := range traffics {
|
||||
buttons3 = append(buttons3, tu.InlineKeyboardButton(tr.Email).WithCallbackData(t.encodeQuery("client_qr_links "+tr.Email)))
|
||||
}
|
||||
cols3 := 1
|
||||
if len(buttons3) >= 6 {
|
||||
cols3 = 2
|
||||
}
|
||||
keyboard3 := tu.InlineKeyboardGrid(tu.InlineKeyboardCols(cols3, buttons3...))
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.commands.pleaseChoose"), keyboard3)
|
||||
case "onlines":
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.buttons.onlines"))
|
||||
t.onlineClients(chatId)
|
||||
@@ -1418,6 +1509,23 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
)
|
||||
prompt_message := t.I18nBot("tgbot.messages.comment_prompt", "ClientComment=="+client_Comment)
|
||||
t.SendMsgToTgbot(chatId, prompt_message, cancel_btn_markup)
|
||||
default:
|
||||
// dynamic callbacks
|
||||
if strings.HasPrefix(callbackQuery.Data, "client_sub_links ") {
|
||||
email := strings.TrimPrefix(callbackQuery.Data, "client_sub_links ")
|
||||
t.sendClientSubLinks(chatId, email)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(callbackQuery.Data, "client_individual_links ") {
|
||||
email := strings.TrimPrefix(callbackQuery.Data, "client_individual_links ")
|
||||
t.sendClientIndividualLinks(chatId, email)
|
||||
return
|
||||
}
|
||||
if strings.HasPrefix(callbackQuery.Data, "client_qr_links ") {
|
||||
email := strings.TrimPrefix(callbackQuery.Data, "client_qr_links ")
|
||||
t.sendClientQRLinks(chatId, email)
|
||||
return
|
||||
}
|
||||
case "add_client_ch_default_traffic":
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
@@ -1524,6 +1632,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
return
|
||||
}
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
if err != nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
t.addClient(chatId, message_text, messageId)
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
|
||||
case "add_client_default_ip_limit":
|
||||
@@ -1534,6 +1646,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
return
|
||||
}
|
||||
message_text, err := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
if err != nil {
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, err.Error())
|
||||
return
|
||||
}
|
||||
t.addClient(chatId, message_text, messageId)
|
||||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.canceled", "Email=="+client_Email))
|
||||
case "add_client_submit_disable":
|
||||
@@ -1598,6 +1714,10 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
||||
return
|
||||
}
|
||||
valid_emails, extra_emails, err := t.inboundService.FilterAndSortClientEmails(emails)
|
||||
if err != nil {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation"), tu.ReplyKeyboardRemove())
|
||||
return
|
||||
}
|
||||
|
||||
for _, valid_emails := range valid_emails {
|
||||
traffic, err := t.inboundService.GetClientTrafficByEmail(valid_emails)
|
||||
@@ -1760,6 +1880,10 @@ func (t *Tgbot) SubmitAddClient() (bool, error) {
|
||||
}
|
||||
|
||||
jsonString, err := t.BuildJSONForProtocol(inbound.Protocol)
|
||||
if err != nil {
|
||||
logger.Warning("BuildJSONForProtocol run failed:", err)
|
||||
return false, errors.New("failed to build JSON for protocol")
|
||||
}
|
||||
|
||||
newInbound := &model.Inbound{
|
||||
Id: receiver_inbound_ID,
|
||||
@@ -1810,6 +1934,13 @@ func (t *Tgbot) SendAnswer(chatId int64, msg string, isAdmin bool) {
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.clientUsage")).WithCallbackData(t.encodeQuery("client_traffic")),
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.commands")).WithCallbackData(t.encodeQuery("client_commands")),
|
||||
),
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("pages.settings.subSettings")).WithCallbackData(t.encodeQuery("client_sub_links")),
|
||||
tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links")),
|
||||
),
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("qrCode")).WithCallbackData(t.encodeQuery("client_qr_links")),
|
||||
),
|
||||
)
|
||||
|
||||
var ReplyMarkup telego.ReplyMarkup
|
||||
@@ -1871,6 +2002,255 @@ func (t *Tgbot) SendMsgToTgbot(chatId int64, msg string, replyMarkup ...telego.R
|
||||
}
|
||||
}
|
||||
|
||||
// buildSubscriptionURLs builds the HTML sub page URL and JSON subscription URL for a client email
|
||||
func (t *Tgbot) buildSubscriptionURLs(email string) (string, string, error) {
|
||||
// Resolve subId from client email
|
||||
traffic, client, err := t.inboundService.GetClientByEmail(email)
|
||||
_ = traffic
|
||||
if err != nil || client == nil {
|
||||
return "", "", errors.New("client not found")
|
||||
}
|
||||
|
||||
// Gather settings to construct absolute URLs
|
||||
subDomain, _ := t.settingService.GetSubDomain()
|
||||
subPort, _ := t.settingService.GetSubPort()
|
||||
subPath, _ := t.settingService.GetSubPath()
|
||||
subJsonPath, _ := t.settingService.GetSubJsonPath()
|
||||
subKeyFile, _ := t.settingService.GetSubKeyFile()
|
||||
subCertFile, _ := t.settingService.GetSubCertFile()
|
||||
|
||||
tls := (subKeyFile != "" && subCertFile != "")
|
||||
scheme := "http"
|
||||
if tls {
|
||||
scheme = "https"
|
||||
}
|
||||
|
||||
// Fallbacks
|
||||
if subDomain == "" {
|
||||
// try panel domain, otherwise OS hostname
|
||||
if d, err := t.settingService.GetWebDomain(); err == nil && d != "" {
|
||||
subDomain = d
|
||||
} else if hostname != "" {
|
||||
subDomain = hostname
|
||||
} else {
|
||||
subDomain = "localhost"
|
||||
}
|
||||
}
|
||||
|
||||
host := subDomain
|
||||
if (subPort == 443 && tls) || (subPort == 80 && !tls) {
|
||||
// standard ports: no port in host
|
||||
} else {
|
||||
host = fmt.Sprintf("%s:%d", subDomain, subPort)
|
||||
}
|
||||
|
||||
// Ensure paths
|
||||
if !strings.HasPrefix(subPath, "/") {
|
||||
subPath = "/" + subPath
|
||||
}
|
||||
if !strings.HasSuffix(subPath, "/") {
|
||||
subPath = subPath + "/"
|
||||
}
|
||||
if !strings.HasPrefix(subJsonPath, "/") {
|
||||
subJsonPath = "/" + subJsonPath
|
||||
}
|
||||
if !strings.HasSuffix(subJsonPath, "/") {
|
||||
subJsonPath = subJsonPath + "/"
|
||||
}
|
||||
|
||||
subURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subPath, client.SubID)
|
||||
subJsonURL := fmt.Sprintf("%s://%s%s%s", scheme, host, subJsonPath, client.SubID)
|
||||
return subURL, subJsonURL, nil
|
||||
}
|
||||
|
||||
func (t *Tgbot) sendClientSubLinks(chatId int64, email string) {
|
||||
subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
|
||||
if err != nil {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||
return
|
||||
}
|
||||
msg := "Subscription URL:\r\n<code>" + subURL + "</code>\r\n\r\n" +
|
||||
"JSON URL:\r\n<code>" + subJsonURL + "</code>"
|
||||
inlineKeyboard := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("subscription.individualLinks")).WithCallbackData(t.encodeQuery("client_individual_links " + email)),
|
||||
),
|
||||
)
|
||||
t.SendMsgToTgbot(chatId, msg, inlineKeyboard)
|
||||
}
|
||||
|
||||
// sendClientIndividualLinks fetches the subscription content (individual links) and sends it to the user
|
||||
func (t *Tgbot) sendClientIndividualLinks(chatId int64, email string) {
|
||||
// Build the HTML sub page URL; we'll call it with header Accept to get raw content
|
||||
subURL, _, err := t.buildSubscriptionURLs(email)
|
||||
if err != nil {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Try to fetch raw subscription links. Prefer plain text response.
|
||||
req, err := http.NewRequest("GET", subURL, nil)
|
||||
if err != nil {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||
return
|
||||
}
|
||||
// Force plain text to avoid HTML page; controller respects Accept header
|
||||
req.Header.Set("Accept", "text/plain, */*;q=0.1")
|
||||
|
||||
// Use default client with reasonable timeout via context
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
req = req.WithContext(ctx)
|
||||
|
||||
resp, err := http.DefaultClient.Do(req)
|
||||
if err != nil {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||
return
|
||||
}
|
||||
defer resp.Body.Close()
|
||||
|
||||
bodyBytes, err := io.ReadAll(resp.Body)
|
||||
if err != nil {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// If service is configured to encode (Base64), decode it
|
||||
encoded, _ := t.settingService.GetSubEncrypt()
|
||||
var content string
|
||||
if encoded {
|
||||
decoded, err := base64.StdEncoding.DecodeString(string(bodyBytes))
|
||||
if err != nil {
|
||||
// fallback to raw text
|
||||
content = string(bodyBytes)
|
||||
} else {
|
||||
content = string(decoded)
|
||||
}
|
||||
} else {
|
||||
content = string(bodyBytes)
|
||||
}
|
||||
|
||||
// Normalize line endings and trim
|
||||
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
|
||||
var cleaned []string
|
||||
for _, l := range lines {
|
||||
l = strings.TrimSpace(l)
|
||||
if l != "" {
|
||||
cleaned = append(cleaned, l)
|
||||
}
|
||||
}
|
||||
if len(cleaned) == 0 {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.noResult"))
|
||||
return
|
||||
}
|
||||
|
||||
// Send in chunks to respect message length; use monospace formatting
|
||||
const maxPerMessage = 50
|
||||
for i := 0; i < len(cleaned); i += maxPerMessage {
|
||||
j := i + maxPerMessage
|
||||
if j > len(cleaned) {
|
||||
j = len(cleaned)
|
||||
}
|
||||
chunk := cleaned[i:j]
|
||||
msg := t.I18nBot("subscription.individualLinks") + ":\r\n"
|
||||
for _, link := range chunk {
|
||||
// wrap each link in <code>
|
||||
msg += "<code>" + link + "</code>\r\n"
|
||||
}
|
||||
t.SendMsgToTgbot(chatId, msg)
|
||||
}
|
||||
}
|
||||
|
||||
// sendClientQRLinks generates QR images for subscription URL, JSON URL, and a few individual links, then sends them
|
||||
func (t *Tgbot) sendClientQRLinks(chatId int64, email string) {
|
||||
subURL, subJsonURL, err := t.buildSubscriptionURLs(email)
|
||||
if err != nil {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// Helper to create QR PNG bytes from content
|
||||
createQR := func(content string, size int) ([]byte, error) {
|
||||
if size <= 0 {
|
||||
size = 256
|
||||
}
|
||||
return qrcode.Encode(content, qrcode.Medium, size)
|
||||
}
|
||||
|
||||
// Inform user
|
||||
t.SendMsgToTgbot(chatId, "QRCode"+":")
|
||||
|
||||
// Send sub URL QR (filename: sub.png)
|
||||
if png, err := createQR(subURL, 320); err == nil {
|
||||
document := tu.Document(
|
||||
tu.ID(chatId),
|
||||
tu.FileFromBytes(png, "sub.png"),
|
||||
)
|
||||
_, _ = bot.SendDocument(context.Background(), document)
|
||||
} else {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||
}
|
||||
|
||||
// Send JSON URL QR (filename: subjson.png)
|
||||
if png, err := createQR(subJsonURL, 320); err == nil {
|
||||
document := tu.Document(
|
||||
tu.ID(chatId),
|
||||
tu.FileFromBytes(png, "subjson.png"),
|
||||
)
|
||||
_, _ = bot.SendDocument(context.Background(), document)
|
||||
} else {
|
||||
t.SendMsgToTgbot(chatId, t.I18nBot("tgbot.answers.errorOperation")+"\r\n"+err.Error())
|
||||
}
|
||||
|
||||
// Also generate a few individual links' QRs (first up to 5)
|
||||
subPageURL := subURL
|
||||
req, err := http.NewRequest("GET", subPageURL, nil)
|
||||
if err == nil {
|
||||
req.Header.Set("Accept", "text/plain, */*;q=0.1")
|
||||
ctx, cancel := context.WithTimeout(context.Background(), 10*time.Second)
|
||||
defer cancel()
|
||||
req = req.WithContext(ctx)
|
||||
if resp, err := http.DefaultClient.Do(req); err == nil {
|
||||
body, _ := io.ReadAll(resp.Body)
|
||||
_ = resp.Body.Close()
|
||||
encoded, _ := t.settingService.GetSubEncrypt()
|
||||
var content string
|
||||
if encoded {
|
||||
if dec, err := base64.StdEncoding.DecodeString(string(body)); err == nil {
|
||||
content = string(dec)
|
||||
} else {
|
||||
content = string(body)
|
||||
}
|
||||
} else {
|
||||
content = string(body)
|
||||
}
|
||||
lines := strings.Split(strings.ReplaceAll(content, "\r\n", "\n"), "\n")
|
||||
var cleaned []string
|
||||
for _, l := range lines {
|
||||
l = strings.TrimSpace(l)
|
||||
if l != "" {
|
||||
cleaned = append(cleaned, l)
|
||||
}
|
||||
}
|
||||
if len(cleaned) > 0 {
|
||||
max := min(len(cleaned), 5)
|
||||
for i := range max {
|
||||
if png, err := createQR(cleaned[i], 320); err == nil {
|
||||
// Use the email as filename for individual link QR
|
||||
filename := email + ".png"
|
||||
document := tu.Document(
|
||||
tu.ID(chatId),
|
||||
tu.FileFromBytes(png, filename),
|
||||
)
|
||||
_, _ = bot.SendDocument(context.Background(), document)
|
||||
time.Sleep(200 * time.Millisecond)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func (t *Tgbot) SendMsgToTgbotAdmins(msg string, replyMarkup ...telego.ReplyMarkup) {
|
||||
if len(replyMarkup) > 0 {
|
||||
for _, adminId := range adminIds {
|
||||
@@ -2008,10 +2388,11 @@ func (t *Tgbot) UserLoginNotify(username string, password string, ip string, tim
|
||||
}
|
||||
|
||||
msg := ""
|
||||
if status == LoginSuccess {
|
||||
switch status {
|
||||
case LoginSuccess:
|
||||
msg += t.I18nBot("tgbot.messages.loginSuccess")
|
||||
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
|
||||
} else if status == LoginFail {
|
||||
case LoginFail:
|
||||
msg += t.I18nBot("tgbot.messages.loginFailed")
|
||||
msg += t.I18nBot("tgbot.messages.hostname", "Hostname=="+hostname)
|
||||
msg += t.I18nBot("tgbot.messages.password", "Password=="+password)
|
||||
@@ -2091,8 +2472,8 @@ func (t *Tgbot) getInboundsAddClient() (*telego.InlineKeyboardMarkup, error) {
|
||||
}
|
||||
|
||||
excludedProtocols := map[model.Protocol]bool{
|
||||
model.DOKODEMO: true,
|
||||
model.Socks: true,
|
||||
model.Tunnel: true,
|
||||
model.Mixed: true,
|
||||
model.WireGuard: true,
|
||||
model.HTTP: true,
|
||||
}
|
||||
@@ -2171,6 +2552,22 @@ func (t *Tgbot) clientInfoMsg(
|
||||
expiryTime = t.I18nBot("tgbot.unlimited")
|
||||
} else if diff > 172800 || !traffic.Enable {
|
||||
expiryTime = time.Unix((traffic.ExpiryTime / 1000), 0).Format("2006-01-02 15:04:05")
|
||||
if diff > 0 {
|
||||
days := diff / 86400
|
||||
hours := (diff % 86400) / 3600
|
||||
minutes := (diff % 3600) / 60
|
||||
remainingTime := ""
|
||||
if days > 0 {
|
||||
remainingTime += fmt.Sprintf("%d %s ", days, t.I18nBot("tgbot.days"))
|
||||
}
|
||||
if hours > 0 {
|
||||
remainingTime += fmt.Sprintf("%d %s ", hours, t.I18nBot("tgbot.hours"))
|
||||
}
|
||||
if minutes > 0 {
|
||||
remainingTime += fmt.Sprintf("%d %s", minutes, t.I18nBot("tgbot.minutes"))
|
||||
}
|
||||
expiryTime += fmt.Sprintf(" (%s)", remainingTime)
|
||||
}
|
||||
} else if traffic.ExpiryTime < 0 {
|
||||
expiryTime = fmt.Sprintf("%d %s", traffic.ExpiryTime/-86400000, t.I18nBot("tgbot.days"))
|
||||
flag = true
|
||||
|
||||
@@ -2,6 +2,7 @@ package session
|
||||
|
||||
import (
|
||||
"encoding/gob"
|
||||
"net/http"
|
||||
|
||||
"x-ui/database/model"
|
||||
|
||||
@@ -32,6 +33,7 @@ func SetMaxAge(c *gin.Context, maxAge int) {
|
||||
Path: defaultPath,
|
||||
MaxAge: maxAge,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -61,5 +63,6 @@ func ClearSession(c *gin.Context) {
|
||||
Path: defaultPath,
|
||||
MaxAge: -1,
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "فشل"
|
||||
"comment" = "تعليق"
|
||||
"success" = "تم بنجاح"
|
||||
"lastOnline" = "آخر متصل"
|
||||
"getVersion" = "جيب النسخة"
|
||||
"install" = "تثبيت"
|
||||
"clients" = "عملاء"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "مفيش بروكسي عكسي مضاف."
|
||||
"somethingWentWrong" = "حدث خطأ ما"
|
||||
|
||||
[subscription]
|
||||
"title" = "معلومات الاشتراك"
|
||||
"subId" = "معرّف الاشتراك"
|
||||
"status" = "الحالة"
|
||||
"downloaded" = "التنزيل"
|
||||
"uploaded" = "الرفع"
|
||||
"expiry" = "تاريخ الانتهاء"
|
||||
"totalQuota" = "الحصة الإجمالية"
|
||||
"individualLinks" = "روابط فردية"
|
||||
"active" = "نشط"
|
||||
"inactive" = "غير نشط"
|
||||
"unlimited" = "غير محدود"
|
||||
"noExpiry" = "بدون انتهاء"
|
||||
|
||||
[menu]
|
||||
"theme" = "الثيم"
|
||||
"dark" = "داكن"
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "حدث خطأ أثناء استرجاع ملف الإعدادات"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "إجمالي حركة المرور"
|
||||
"allTimeTrafficUsage" = "إجمالي الاستخدام طوال الوقت"
|
||||
"title" = "الإدخالات"
|
||||
"totalDownUp" = "إجمالي المرسل/المستقبل"
|
||||
"totalUsage" = "إجمالي الاستخدام"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "تفاصيل"
|
||||
"transportConfig" = "نقل"
|
||||
"expireDate" = "المدة"
|
||||
"createdAt" = "تاريخ الإنشاء"
|
||||
"updatedAt" = "تاريخ التحديث"
|
||||
"resetTraffic" = "إعادة ضبط الترافيك"
|
||||
"addInbound" = "أضف إدخال"
|
||||
"generalActions" = "إجراءات عامة"
|
||||
@@ -262,6 +281,7 @@
|
||||
"trafficGetError" = "خطأ في الحصول على حركات المرور"
|
||||
"getNewX25519CertError" = "حدث خطأ أثناء الحصول على شهادة X25519."
|
||||
"getNewmldsa65Error" = "حدث خطاء في الحصول على mldsa65."
|
||||
"getNewVlessEncError" = "حدث خطأ أثناء الحصول على VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "طلب"
|
||||
@@ -561,24 +581,25 @@
|
||||
"resetOutboundTrafficError" = "خطأ في إعادة تعيين حركات المرور الصادرة"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ الكيبورد المخصص اتقفلت!"
|
||||
"noResult" = "❗ مفيش نتيجة!"
|
||||
"noQuery" = "❌ مش لاقي السؤال! استخدم الأمر تاني!"
|
||||
"wentWrong" = "❌ حصل خطأ!"
|
||||
"noIpRecord" = "❗ مفيش سجل IP!"
|
||||
"noInbounds" = "❗ مفيش إدخال متواجد!"
|
||||
"unlimited" = "♾ غير محدود (إعادة ضبط)"
|
||||
"add" = "أضف"
|
||||
"keyboardClosed" = "❌ لوحة المفاتيح مغلقة!"
|
||||
"noResult" = "❗ لا يوجد نتائج!"
|
||||
"noQuery" = "❌ لم يتم العثور على الاستعلام! يرجى استخدام الأمر مرة أخرى!"
|
||||
"wentWrong" = "❌ حدث خطأ ما!"
|
||||
"noIpRecord" = "❗ لا يوجد سجل IP!"
|
||||
"noInbounds" = "❗ لم يتم العثور على أي وارد!"
|
||||
"unlimited" = "♾ غير محدود (إعادة تعيين)"
|
||||
"add" = "إضافة"
|
||||
"month" = "شهر"
|
||||
"months" = "شهور"
|
||||
"months" = "أشهر"
|
||||
"day" = "يوم"
|
||||
"days" = "أيام"
|
||||
"hours" = "ساعات"
|
||||
"unknown" = "مش معروف"
|
||||
"inbounds" = "الإدخالات"
|
||||
"minutes" = "دقائق"
|
||||
"unknown" = "غير معروف"
|
||||
"inbounds" = "الواردات"
|
||||
"clients" = "العملاء"
|
||||
"offline" = "🔴 أوفلاين"
|
||||
"online" = "🟢 أونلاين"
|
||||
"offline" = "🔴 غير متصل"
|
||||
"online" = "🟢 متصل"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ أمر مش معروف."
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "Failed"
|
||||
"comment" = "Comment"
|
||||
"success" = "Successfully"
|
||||
"lastOnline" = "Last Online"
|
||||
"getVersion" = "Get Version"
|
||||
"install" = "Install"
|
||||
"clients" = "Clients"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "No added reverse proxies."
|
||||
"somethingWentWrong" = "Something went wrong"
|
||||
|
||||
[subscription]
|
||||
"title" = "Subscription info"
|
||||
"subId" = "Subscription ID"
|
||||
"status" = "Status"
|
||||
"downloaded" = "Downloaded"
|
||||
"uploaded" = "Uploaded"
|
||||
"expiry" = "Expiry"
|
||||
"totalQuota" = "Total quota"
|
||||
"individualLinks" = "Individual links"
|
||||
"active" = "Active"
|
||||
"inactive" = "Inactive"
|
||||
"unlimited" = "Unlimited"
|
||||
"noExpiry" = "No expiry"
|
||||
|
||||
[menu]
|
||||
"theme" = "Theme"
|
||||
"dark" = "Dark"
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "An error occurred while retrieving the config file."
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "All-time Traffic"
|
||||
"allTimeTrafficUsage" = "All Time Total Usage"
|
||||
"title" = "Inbounds"
|
||||
"totalDownUp" = "Total Sent/Received"
|
||||
"totalUsage" = "Total Usage"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "Details"
|
||||
"transportConfig" = "Transport"
|
||||
"expireDate" = "Duration"
|
||||
"createdAt" = "Created"
|
||||
"updatedAt" = "Updated"
|
||||
"resetTraffic" = "Reset Traffic"
|
||||
"addInbound" = "Add Inbound"
|
||||
"generalActions" = "General Actions"
|
||||
@@ -262,6 +281,7 @@
|
||||
"trafficGetError" = "Error getting traffics."
|
||||
"getNewX25519CertError" = "Error while obtaining the X25519 certificate."
|
||||
"getNewmldsa65Error" = "Error while obtaining mldsa65."
|
||||
"getNewVlessEncError" = "Error while obtaining VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Request"
|
||||
@@ -574,6 +594,7 @@
|
||||
"day" = "Day"
|
||||
"days" = "Days"
|
||||
"hours" = "Hours"
|
||||
"minutes" = "Minutes"
|
||||
"unknown" = "Unknown"
|
||||
"inbounds" = "Inbounds"
|
||||
"clients" = "Clients"
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "Falló"
|
||||
"comment" = "Comentario"
|
||||
"success" = "Éxito"
|
||||
"lastOnline" = "Última conexión"
|
||||
"getVersion" = "Obtener versión"
|
||||
"install" = "Instalar"
|
||||
"clients" = "Clientes"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "No hay proxies inversos añadidos."
|
||||
"somethingWentWrong" = "Algo salió mal"
|
||||
|
||||
[subscription]
|
||||
"title" = "Información de suscripción"
|
||||
"subId" = "ID de suscripción"
|
||||
"status" = "Estado"
|
||||
"downloaded" = "Descargado"
|
||||
"uploaded" = "Subido"
|
||||
"expiry" = "Caducidad"
|
||||
"totalQuota" = "Cuota total"
|
||||
"individualLinks" = "Enlaces individuales"
|
||||
"active" = "Activo"
|
||||
"inactive" = "Inactivo"
|
||||
"unlimited" = "Ilimitado"
|
||||
"noExpiry" = "Sin caducidad"
|
||||
|
||||
[menu]
|
||||
"theme" = "Tema"
|
||||
"dark" = "Oscuro"
|
||||
@@ -91,7 +106,7 @@
|
||||
"invalidFormData" = "El formato de los datos de entrada es inválido."
|
||||
"emptyUsername" = "Por favor ingresa el nombre de usuario."
|
||||
"emptyPassword" = "Por favor ingresa la contraseña."
|
||||
"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."
|
||||
"wrongUsernameOrPassword" = "Nombre de usuario, contraseña o código de dos factores incorrecto."
|
||||
"successLogin" = "Has iniciado sesión en tu cuenta correctamente."
|
||||
|
||||
[pages.index]
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "Ocurrió un error al obtener el archivo de configuración"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "Tráfico Total"
|
||||
"allTimeTrafficUsage" = "Uso total de todos los tiempos"
|
||||
"title" = "Entradas"
|
||||
"totalDownUp" = "Subidas/Descargas Totales"
|
||||
"totalUsage" = "Uso Total"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "Detalles"
|
||||
"transportConfig" = "Transporte"
|
||||
"expireDate" = "Fecha de Expiración"
|
||||
"createdAt" = "Creado"
|
||||
"updatedAt" = "Actualizado"
|
||||
"resetTraffic" = "Restablecer Tráfico"
|
||||
"addInbound" = "Agregar Entrada"
|
||||
"generalActions" = "Acciones Generales"
|
||||
@@ -262,6 +281,7 @@
|
||||
"trafficGetError" = "Error al obtener los tráficos"
|
||||
"getNewX25519CertError" = "Error al obtener el certificado X25519."
|
||||
"getNewmldsa65Error" = "Error al obtener el certificado mldsa65."
|
||||
"getNewVlessEncError" = "Error al obtener el certificado VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Pedido"
|
||||
@@ -535,9 +555,9 @@
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "Credenciales de administrador"
|
||||
"twoFactor" = "Autenticación de dos factores"
|
||||
"twoFactorEnable" = "Habilitar 2FA"
|
||||
"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."
|
||||
"twoFactor" = "Autenticación de dos factores"
|
||||
"twoFactorEnable" = "Habilitar 2FA"
|
||||
"twoFactorEnableDesc" = "Añade una capa adicional de autenticación para mayor seguridad."
|
||||
"twoFactorModalSetTitle" = "Activar autenticación de dos factores"
|
||||
"twoFactorModalDeleteTitle" = "Desactivar autenticación de dos factores"
|
||||
"twoFactorModalSteps" = "Para configurar la autenticación de dos factores, sigue estos pasos:"
|
||||
@@ -561,23 +581,24 @@
|
||||
"resetOutboundTrafficError" = "Error al reiniciar el tráfico saliente"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ ¡Teclado personalizado cerrado!"
|
||||
"noResult" = "❗ ¡Sin resultados!"
|
||||
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor utiliza el comando nuevamente!"
|
||||
"keyboardClosed" = "❌ Teclado cerrado!"
|
||||
"noResult" = "❗ ¡No hay resultados!"
|
||||
"noQuery" = "❌ ¡Consulta no encontrada! ¡Por favor, use el comando de nuevo!"
|
||||
"wentWrong" = "❌ ¡Algo salió mal!"
|
||||
"noIpRecord" = "❗ ¡Sin Registro de IP!"
|
||||
"noIpRecord" = "❗ ¡No hay registro de IP!"
|
||||
"noInbounds" = "❗ ¡No se encontraron entradas!"
|
||||
"unlimited" = "♾ Ilimitado"
|
||||
"add" = "Agregar"
|
||||
"unlimited" = "♾ Ilimitado (Restablecer)"
|
||||
"add" = "Añadir"
|
||||
"month" = "Mes"
|
||||
"months" = "Meses"
|
||||
"day" = "Día"
|
||||
"days" = "Días"
|
||||
"hours" = "Horas"
|
||||
"minutes" = "Minutos"
|
||||
"unknown" = "Desconocido"
|
||||
"inbounds" = "Entradas"
|
||||
"clients" = "Clientes"
|
||||
"offline" = "🔴 Sin conexión"
|
||||
"offline" = "🔴 Desconectado"
|
||||
"online" = "🟢 En línea"
|
||||
|
||||
[tgbot.commands]
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "ناموفق"
|
||||
"comment" = "توضیحات"
|
||||
"success" = "موفق"
|
||||
"lastOnline" = "آخرین فعالیت"
|
||||
"getVersion" = "دریافت نسخه"
|
||||
"install" = "نصب"
|
||||
"clients" = "کاربران"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "هیچ پروکسی معکوس اضافه نشده است."
|
||||
"somethingWentWrong" = "مشکلی پیش آمد"
|
||||
|
||||
[subscription]
|
||||
"title" = "اطلاعات سابسکریپشن"
|
||||
"subId" = "شناسه اشتراک"
|
||||
"status" = "وضعیت"
|
||||
"downloaded" = "دانلود"
|
||||
"uploaded" = "آپلود"
|
||||
"expiry" = "تاریخ پایان"
|
||||
"totalQuota" = "حجم کلی"
|
||||
"individualLinks" = "لینکهای تکی"
|
||||
"active" = "فعال"
|
||||
"inactive" = "غیرفعال"
|
||||
"unlimited" = "نامحدود"
|
||||
"noExpiry" = "بدون انقضا"
|
||||
|
||||
[menu]
|
||||
"theme" = "تم"
|
||||
"dark" = "تیره"
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "خطا در دریافت فایل پیکربندی"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "کل ترافیک"
|
||||
"allTimeTrafficUsage" = "کل استفاده در تمام مدت"
|
||||
"title" = "کاربران"
|
||||
"totalDownUp" = "دریافت/ارسال کل"
|
||||
"totalUsage" = "مصرف کل"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "توضیحات"
|
||||
"transportConfig" = "نحوه اتصال"
|
||||
"expireDate" = "مدت زمان"
|
||||
"createdAt" = "ایجاد"
|
||||
"updatedAt" = "بهروزرسانی"
|
||||
"resetTraffic" = "ریست ترافیک"
|
||||
"addInbound" = "افزودن ورودی"
|
||||
"generalActions" = "عملیات کلی"
|
||||
@@ -262,6 +281,7 @@
|
||||
"trafficGetError" = "خطا در دریافت ترافیکها"
|
||||
"getNewX25519CertError" = "خطا در دریافت گواهی X25519."
|
||||
"getNewmldsa65Error" = "خطا در دریافت گواهی mldsa65."
|
||||
"getNewVlessEncError" = "خطا در دریافت گواهی VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "درخواست"
|
||||
@@ -561,22 +581,23 @@
|
||||
"resetOutboundTrafficError" = "خطا در بازنشانی ترافیک خروجی"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ کیبورد سفارشی بسته شد!"
|
||||
"noResult" = "❗ نتیجهای یافت نشد!"
|
||||
"noQuery" = "❌ کوئری یافت نشد! لطفاً دستور را مجدداً استفاده کنید!"
|
||||
"wentWrong" = "❌ مشکلی رخ داده است!"
|
||||
"noIpRecord" = "❗ رکورد IP یافت نشد!"
|
||||
"keyboardClosed" = "❌ صفحه کلید بسته شد!"
|
||||
"noResult" = "❗ نتیجه ای یافت نشد!"
|
||||
"noQuery" = "❌ درخواست یافت نشد! لطفا دوباره تلاش کنید!"
|
||||
"wentWrong" = "❌ مشکلی پیش آمد!"
|
||||
"noIpRecord" = "❗ رکورد آی پی وجود ندارد!"
|
||||
"noInbounds" = "❗ هیچ ورودی یافت نشد!"
|
||||
"unlimited" = "♾ - نامحدود(ریست)"
|
||||
"add" = "اضافه کردن"
|
||||
"unlimited" = "♾ نامحدود(ریست)"
|
||||
"add" = "افزودن"
|
||||
"month" = "ماه"
|
||||
"months" = "ماه"
|
||||
"months" = "ماه"
|
||||
"day" = "روز"
|
||||
"days" = "روز"
|
||||
"hours" = "ساعت"
|
||||
"hours" = "ساعت"
|
||||
"minutes" = "دقیقه"
|
||||
"unknown" = "نامشخص"
|
||||
"inbounds" = "ورودیها"
|
||||
"clients" = "کلاینتها"
|
||||
"inbounds" = "ورودی ها"
|
||||
"clients" = "کاربران"
|
||||
"offline" = "🔴 آفلاین"
|
||||
"online" = "🟢 آنلاین"
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "Gagal"
|
||||
"comment" = "Komentar"
|
||||
"success" = "Berhasil"
|
||||
"lastOnline" = "Terakhir online"
|
||||
"getVersion" = "Dapatkan Versi"
|
||||
"install" = "Instal"
|
||||
"clients" = "Klien"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "Tidak ada proxy terbalik yang ditambahkan."
|
||||
"somethingWentWrong" = "Terjadi kesalahan"
|
||||
|
||||
[subscription]
|
||||
"title" = "Info langganan"
|
||||
"subId" = "ID langganan"
|
||||
"status" = "Status"
|
||||
"downloaded" = "Diunduh"
|
||||
"uploaded" = "Diunggah"
|
||||
"expiry" = "Kedaluwarsa"
|
||||
"totalQuota" = "Kuota total"
|
||||
"individualLinks" = "Tautan individual"
|
||||
"active" = "Aktif"
|
||||
"inactive" = "Nonaktif"
|
||||
"unlimited" = "Tanpa batas"
|
||||
"noExpiry" = "Tanpa kedaluwarsa"
|
||||
|
||||
[menu]
|
||||
"theme" = "Tema"
|
||||
"dark" = "Gelap"
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "Terjadi kesalahan saat mengambil file konfigurasi"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "Total Lalu Lintas"
|
||||
"allTimeTrafficUsage" = "Total Penggunaan Sepanjang Waktu"
|
||||
"title" = "Masuk"
|
||||
"totalDownUp" = "Total Terkirim/Diterima"
|
||||
"totalUsage" = "Penggunaan Total"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "Rincian"
|
||||
"transportConfig" = "Transport"
|
||||
"expireDate" = "Durasi"
|
||||
"createdAt" = "Dibuat"
|
||||
"updatedAt" = "Diperbarui"
|
||||
"resetTraffic" = "Reset Traffic"
|
||||
"addInbound" = "Tambahkan Masuk"
|
||||
"generalActions" = "Tindakan Umum"
|
||||
@@ -262,6 +281,7 @@
|
||||
"trafficGetError" = "Gagal mendapatkan data lalu lintas"
|
||||
"getNewX25519CertError" = "Terjadi kesalahan saat mendapatkan sertifikat X25519."
|
||||
"getNewmldsa65Error" = "Terjadi kesalahan saat mendapatkan sertifikat mldsa65."
|
||||
"getNewVlessEncError" = "Terjadi kesalahan saat mendapatkan sertifikat VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Permintaan"
|
||||
@@ -561,21 +581,22 @@
|
||||
"resetOutboundTrafficError" = "Gagal mereset lalu lintas keluar"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Papan ketik kustom ditutup!"
|
||||
"keyboardClosed" = "❌ Keyboard ditutup!"
|
||||
"noResult" = "❗ Tidak ada hasil!"
|
||||
"noQuery" = "❌ Permintaan tidak ditemukan! Harap gunakan perintah lagi!"
|
||||
"wentWrong" = "❌ Ada yang salah!"
|
||||
"noQuery" = "❌ Kueri tidak ditemukan! Silakan gunakan perintah lagi!"
|
||||
"wentWrong" = "❌ Terjadi kesalahan!"
|
||||
"noIpRecord" = "❗ Tidak ada Catatan IP!"
|
||||
"noInbounds" = "❗ Tidak ada masuk ditemukan!"
|
||||
"unlimited" = "♾ Tak terbatas"
|
||||
"noInbounds" = "❗ Tidak ada inbound yang ditemukan!"
|
||||
"unlimited" = "♾ Tidak terbatas (Reset)"
|
||||
"add" = "Tambah"
|
||||
"month" = "Bulan"
|
||||
"months" = "Bulan"
|
||||
"day" = "Hari"
|
||||
"days" = "Hari"
|
||||
"hours" = "Jam"
|
||||
"minutes" = "Menit"
|
||||
"unknown" = "Tidak diketahui"
|
||||
"inbounds" = "Masuk"
|
||||
"inbounds" = "Inbound"
|
||||
"clients" = "Klien"
|
||||
"offline" = "🔴 Offline"
|
||||
"online" = "🟢 Online"
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "失敗"
|
||||
"comment" = "コメント"
|
||||
"success" = "成功"
|
||||
"lastOnline" = "最終オンライン"
|
||||
"getVersion" = "バージョン取得"
|
||||
"install" = "インストール"
|
||||
"clients" = "クライアント"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "追加されたリバースプロキシはありません。"
|
||||
"somethingWentWrong" = "エラーが発生しました"
|
||||
|
||||
[subscription]
|
||||
"title" = "サブスクリプション情報"
|
||||
"subId" = "サブスクリプションID"
|
||||
"status" = "ステータス"
|
||||
"downloaded" = "ダウンロード"
|
||||
"uploaded" = "アップロード"
|
||||
"expiry" = "有効期限"
|
||||
"totalQuota" = "合計クォータ"
|
||||
"individualLinks" = "個別リンク"
|
||||
"active" = "有効"
|
||||
"inactive" = "無効"
|
||||
"unlimited" = "無制限"
|
||||
"noExpiry" = "期限なし"
|
||||
|
||||
[menu]
|
||||
"theme" = "テーマ"
|
||||
"dark" = "ダーク"
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "設定ファイルの取得中にエラーが発生しました"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "総トラフィック"
|
||||
"allTimeTrafficUsage" = "これまでの総使用量"
|
||||
"title" = "インバウンド一覧"
|
||||
"totalDownUp" = "総アップロード / ダウンロード"
|
||||
"totalUsage" = "総使用量"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "詳細情報"
|
||||
"transportConfig" = "トランスポート設定"
|
||||
"expireDate" = "有効期限"
|
||||
"createdAt" = "作成"
|
||||
"updatedAt" = "更新"
|
||||
"resetTraffic" = "トラフィックリセット"
|
||||
"addInbound" = "インバウンド追加"
|
||||
"generalActions" = "一般操作"
|
||||
@@ -262,6 +281,7 @@
|
||||
"trafficGetError" = "トラフィックの取得中にエラーが発生しました"
|
||||
"getNewX25519CertError" = "X25519証明書の取得中にエラーが発生しました。"
|
||||
"getNewmldsa65Error" = "mldsa65証明書の取得中にエラーが発生しました。"
|
||||
"getNewVlessEncError" = "VlessEnc証明書の取得中にエラーが発生しました。"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "リクエスト"
|
||||
@@ -561,21 +581,22 @@
|
||||
"resetOutboundTrafficError" = "送信トラフィックのリセットエラー"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ カスタムキーボードが閉じられました!"
|
||||
"keyboardClosed" = "❌ キーボードを閉じました!"
|
||||
"noResult" = "❗ 結果がありません!"
|
||||
"noQuery" = "❌ クエリが見つかりませんでした!もう一度コマンドを使用してください!"
|
||||
"wentWrong" = "❌ 問題が発生しました!"
|
||||
"noIpRecord" = "❗ IP記録がありません!"
|
||||
"noInbounds" = "❗ インバウンド接続が見つかりません!"
|
||||
"unlimited" = "♾ 無制限"
|
||||
"noQuery" = "❌ クエリが見つかりません!コマンドを再利用してください!"
|
||||
"wentWrong" = "❌ 何かがうまくいかなかった!"
|
||||
"noIpRecord" = "❗ IPレコードがありません!"
|
||||
"noInbounds" = "❗ インバウンドが見つかりません!"
|
||||
"unlimited" = "♾ 無制限(リセット)"
|
||||
"add" = "追加"
|
||||
"month" = "月"
|
||||
"months" = "月"
|
||||
"months" = "ヶ月"
|
||||
"day" = "日"
|
||||
"days" = "日"
|
||||
"days" = "日間"
|
||||
"hours" = "時間"
|
||||
"minutes" = "分"
|
||||
"unknown" = "不明"
|
||||
"inbounds" = "インバウンド接続"
|
||||
"inbounds" = "インバウンド"
|
||||
"clients" = "クライアント"
|
||||
"offline" = "🔴 オフライン"
|
||||
"online" = "🟢 オンライン"
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "Falhou"
|
||||
"comment" = "Comentário"
|
||||
"success" = "Com Sucesso"
|
||||
"lastOnline" = "Última vez online"
|
||||
"getVersion" = "Obter Versão"
|
||||
"install" = "Instalar"
|
||||
"clients" = "Clientes"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "Nenhum proxy reverso adicionado."
|
||||
"somethingWentWrong" = "Algo deu errado"
|
||||
|
||||
[subscription]
|
||||
"title" = "Informações da assinatura"
|
||||
"subId" = "ID da assinatura"
|
||||
"status" = "Status"
|
||||
"downloaded" = "Baixado"
|
||||
"uploaded" = "Enviado"
|
||||
"expiry" = "Validade"
|
||||
"totalQuota" = "Cota total"
|
||||
"individualLinks" = "Links individuais"
|
||||
"active" = "Ativo"
|
||||
"inactive" = "Inativo"
|
||||
"unlimited" = "Ilimitado"
|
||||
"noExpiry" = "Sem validade"
|
||||
|
||||
[menu]
|
||||
"theme" = "Tema"
|
||||
"dark" = "Escuro"
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "Ocorreu um erro ao recuperar o arquivo de configuração"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "Tráfego Total"
|
||||
"allTimeTrafficUsage" = "Uso total de todos os tempos"
|
||||
"title" = "Inbounds"
|
||||
"totalDownUp" = "Total Enviado/Recebido"
|
||||
"totalUsage" = "Uso Total"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "Detalhes"
|
||||
"transportConfig" = "Transporte"
|
||||
"expireDate" = "Duração"
|
||||
"createdAt" = "Criado"
|
||||
"updatedAt" = "Atualizado"
|
||||
"resetTraffic" = "Redefinir Tráfego"
|
||||
"addInbound" = "Adicionar Inbound"
|
||||
"generalActions" = "Ações Gerais"
|
||||
@@ -262,6 +281,7 @@
|
||||
"trafficGetError" = "Erro ao obter tráfegos"
|
||||
"getNewX25519CertError" = "Erro ao obter o certificado X25519."
|
||||
"getNewmldsa65Error" = "Erro ao obter o certificado mldsa65."
|
||||
"getNewVlessEncError" = "Erro ao obter o certificado VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Requisição"
|
||||
@@ -561,21 +581,22 @@
|
||||
"resetOutboundTrafficError" = "Erro ao redefinir tráfego de saída"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Teclado personalizado fechado!"
|
||||
"keyboardClosed" = "❌ Teclado fechado!"
|
||||
"noResult" = "❗ Nenhum resultado!"
|
||||
"noQuery" = "❌ Consulta não encontrada! Por favor, use o comando novamente!"
|
||||
"wentWrong" = "❌ Algo deu errado!"
|
||||
"noIpRecord" = "❗ Nenhum registro de IP!"
|
||||
"noInbounds" = "❗ Nenhuma entrada encontrada!"
|
||||
"unlimited" = "♾ Ilimitado (Reiniciar)"
|
||||
"noInbounds" = "❗ Nenhum inbound encontrado!"
|
||||
"unlimited" = "♾ Ilimitado (Reset)"
|
||||
"add" = "Adicionar"
|
||||
"month" = "Mês"
|
||||
"months" = "Meses"
|
||||
"day" = "Dia"
|
||||
"days" = "Dias"
|
||||
"hours" = "Horas"
|
||||
"minutes" = "Minutos"
|
||||
"unknown" = "Desconhecido"
|
||||
"inbounds" = "Entradas"
|
||||
"inbounds" = "Inbounds"
|
||||
"clients" = "Clientes"
|
||||
"offline" = "🔴 Offline"
|
||||
"online" = "🟢 Online"
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "Ошибка"
|
||||
"comment" = "Комментарий"
|
||||
"success" = "Успешно"
|
||||
"lastOnline" = "Был(а) в сети"
|
||||
"getVersion" = "Узнать версию"
|
||||
"install" = "Установка"
|
||||
"clients" = "Клиенты"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "Нет добавленных реверс-прокси."
|
||||
"somethingWentWrong" = "Что-то пошло не так"
|
||||
|
||||
[subscription]
|
||||
"title" = "Информация о подписке"
|
||||
"subId" = "ID подписки"
|
||||
"status" = "Статус"
|
||||
"downloaded" = "Загружено"
|
||||
"uploaded" = "Отправлено"
|
||||
"expiry" = "Срок действия"
|
||||
"totalQuota" = "Общий лимит"
|
||||
"individualLinks" = "Индивидуальные ссылки"
|
||||
"active" = "Активна"
|
||||
"inactive" = "Неактивна"
|
||||
"unlimited" = "Безлимит"
|
||||
"noExpiry" = "Без срока"
|
||||
|
||||
[menu]
|
||||
"theme" = "Тема"
|
||||
"dark" = "Темная"
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "Произошла ошибка при получении конфигурационного файла"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "Общий трафик"
|
||||
"allTimeTrafficUsage" = "Общее использование за все время"
|
||||
"title" = "Инбаунды"
|
||||
"totalDownUp" = "Объем отправленного/полученного трафика"
|
||||
"totalUsage" = "Всего трафика"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "Подробнее"
|
||||
"transportConfig" = "Транспорт"
|
||||
"expireDate" = "Дата окончания"
|
||||
"createdAt" = "Создано"
|
||||
"updatedAt" = "Обновлено"
|
||||
"resetTraffic" = "Сброс трафика"
|
||||
"addInbound" = "Создать инбаунд"
|
||||
"generalActions" = "Общие действия"
|
||||
@@ -262,6 +281,7 @@
|
||||
"trafficGetError" = "Ошибка получения данных о трафике"
|
||||
"getNewX25519CertError" = "Ошибка при получении сертификата X25519."
|
||||
"getNewmldsa65Error" = "Ошибка при получении сертификата mldsa65."
|
||||
"getNewVlessEncError" = "Ошибка при получении сертификата VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Запрос"
|
||||
@@ -574,6 +594,7 @@
|
||||
"day" = "День"
|
||||
"days" = "Дней"
|
||||
"hours" = "Часов"
|
||||
"minutes" = "Минуты"
|
||||
"unknown" = "Неизвестно"
|
||||
"inbounds" = "Инбаунды"
|
||||
"clients" = "Клиенты"
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "Başarısız"
|
||||
"comment" = "Yorum"
|
||||
"success" = "Başarılı"
|
||||
"lastOnline" = "Son çevrimiçi"
|
||||
"getVersion" = "Sürümü Al"
|
||||
"install" = "Yükle"
|
||||
"clients" = "Müşteriler"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "Eklenmiş ters proxy yok."
|
||||
"somethingWentWrong" = "Bir şeyler yanlış gitti"
|
||||
|
||||
[subscription]
|
||||
"title" = "Abonelik Bilgisi"
|
||||
"subId" = "Abonelik Kimliği"
|
||||
"status" = "Durum"
|
||||
"downloaded" = "İndirilen"
|
||||
"uploaded" = "Yüklenen"
|
||||
"expiry" = "Son Kullanma"
|
||||
"totalQuota" = "Toplam Kota"
|
||||
"individualLinks" = "Bireysel Bağlantılar"
|
||||
"active" = "Aktif"
|
||||
"inactive" = "Pasif"
|
||||
"unlimited" = "Sınırsız"
|
||||
"noExpiry" = "Süresiz"
|
||||
|
||||
[menu]
|
||||
"theme" = "Tema"
|
||||
"dark" = "Koyu"
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "Yapılandırma dosyası alınırken bir hata oluştu"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "Toplam Trafik"
|
||||
"allTimeTrafficUsage" = "Tüm Zamanların Toplam Kullanımı"
|
||||
"title" = "Gelenler"
|
||||
"totalDownUp" = "Toplam Gönderilen/Alınan"
|
||||
"totalUsage" = "Toplam Kullanım"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "Detaylar"
|
||||
"transportConfig" = "Taşıma"
|
||||
"expireDate" = "Süre"
|
||||
"createdAt" = "Oluşturuldu"
|
||||
"updatedAt" = "Güncellendi"
|
||||
"resetTraffic" = "Trafiği Sıfırla"
|
||||
"addInbound" = "Gelen Ekle"
|
||||
"generalActions" = "Genel Eylemler"
|
||||
@@ -262,6 +281,7 @@
|
||||
"trafficGetError" = "Trafik bilgisi alınırken hata oluştu"
|
||||
"getNewX25519CertError" = "X25519 sertifikası alınırken hata oluştu."
|
||||
"getNewmldsa65Error" = "mldsa65 sertifikası alınırken hata oluştu."
|
||||
"getNewVlessEncError" = "VlessEnc sertifikası alınırken hata oluştu."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "İstek"
|
||||
@@ -561,22 +581,23 @@
|
||||
"resetOutboundTrafficError" = "Giden trafik sıfırlanırken hata"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Özel klavye kapalı!"
|
||||
"keyboardClosed" = "❌ Klavye kapatıldı!"
|
||||
"noResult" = "❗ Sonuç yok!"
|
||||
"noQuery" = "❌ Sorgu bulunamadı! Lütfen komutu tekrar kullanın!"
|
||||
"wentWrong" = "❌ Bir şeyler yanlış gitti!"
|
||||
"noIpRecord" = "❗ IP Kaydı yok!"
|
||||
"noInbounds" = "❗ Gelen bulunamadı!"
|
||||
"unlimited" = "♾ Sınırsız(Sıfırla)"
|
||||
"noIpRecord" = "❗ IP Kaydı Yok!"
|
||||
"noInbounds" = "❗ Gelen bağlantı bulunamadı!"
|
||||
"unlimited" = "♾ Sınırsız (Sıfırla)"
|
||||
"add" = "Ekle"
|
||||
"month" = "Ay"
|
||||
"months" = "Aylar"
|
||||
"day" = "Gün"
|
||||
"days" = "Günler"
|
||||
"hours" = "Saatler"
|
||||
"unknown" = "Bilinmiyor"
|
||||
"minutes" = "Dakika"
|
||||
"unknown" = "Bilinmeyen"
|
||||
"inbounds" = "Gelenler"
|
||||
"clients" = "Müşteriler"
|
||||
"clients" = "İstemciler"
|
||||
"offline" = "🔴 Çevrimdışı"
|
||||
"online" = "🟢 Çevrimiçi"
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "Помилка"
|
||||
"comment" = "Коментар"
|
||||
"success" = "Успішно"
|
||||
"lastOnline" = "Був(ла) онлайн"
|
||||
"getVersion" = "Отримати версію"
|
||||
"install" = "Встановити"
|
||||
"clients" = "Клієнти"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "Немає доданих зворотних проксі."
|
||||
"somethingWentWrong" = "Щось пішло не так"
|
||||
|
||||
[subscription]
|
||||
"title" = "Інформація про підписку"
|
||||
"subId" = "ID підписки"
|
||||
"status" = "Статус"
|
||||
"downloaded" = "Завантажено"
|
||||
"uploaded" = "Відвантажено"
|
||||
"expiry" = "Термін дії"
|
||||
"totalQuota" = "Загальна квота"
|
||||
"individualLinks" = "Окремі посилання"
|
||||
"active" = "Активна"
|
||||
"inactive" = "Неактивна"
|
||||
"unlimited" = "Безліміт"
|
||||
"noExpiry" = "Без строку"
|
||||
|
||||
[menu]
|
||||
"theme" = "Тема"
|
||||
"dark" = "Темна"
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "Виникла помилка під час отримання файлу конфігурації"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "Загальний трафік"
|
||||
"allTimeTrafficUsage" = "Загальне використання за весь час"
|
||||
"title" = "Вхідні"
|
||||
"totalDownUp" = "Всього надісланих/отриманих"
|
||||
"totalUsage" = "Всього використанно"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "Деталі"
|
||||
"transportConfig" = "Транспорт"
|
||||
"expireDate" = "Тривалість"
|
||||
"createdAt" = "Створено"
|
||||
"updatedAt" = "Оновлено"
|
||||
"resetTraffic" = "Скинути трафік"
|
||||
"addInbound" = "Додати вхідний"
|
||||
"generalActions" = "Загальні дії"
|
||||
@@ -262,6 +281,7 @@
|
||||
"trafficGetError" = "Помилка отримання даних про трафік"
|
||||
"getNewX25519CertError" = "Помилка при отриманні сертифіката X25519."
|
||||
"getNewmldsa65Error" = "Помилка при отриманні сертифіката mldsa65."
|
||||
"getNewVlessEncError" = "Помилка при отриманні сертифіката VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Запит"
|
||||
@@ -561,19 +581,20 @@
|
||||
"resetOutboundTrafficError" = "Помилка скидання вихідного трафіку"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Спеціальна клавіатура закрита!"
|
||||
"keyboardClosed" = "❌ Клавіатуру закрито!"
|
||||
"noResult" = "❗ Немає результату!"
|
||||
"noQuery" = "❌ Запит не знайдено! Скористайтеся командою ще раз!"
|
||||
"noQuery" = "❌ Запит не знайдено! Будь ласка, використовуйте команду ще раз!"
|
||||
"wentWrong" = "❌ Щось пішло не так!"
|
||||
"noIpRecord" = "❗ Немає IP-запису!"
|
||||
"noInbounds" = "❗ Вхідних не знайдено!"
|
||||
"unlimited" = "♾ Необмежений (скинути)"
|
||||
"noIpRecord" = "❗ Немає запису IP!"
|
||||
"noInbounds" = "❗ Вхідні не знайдені!"
|
||||
"unlimited" = "♾ Необмежено (Скинути)"
|
||||
"add" = "Додати"
|
||||
"month" = "Місяць"
|
||||
"months" = "Місяці"
|
||||
"day" = "День"
|
||||
"days" = "Дні"
|
||||
"hours" = "Годинник"
|
||||
"hours" = "Години"
|
||||
"minutes" = "Хвилини"
|
||||
"unknown" = "Невідомо"
|
||||
"inbounds" = "Вхідні"
|
||||
"clients" = "Клієнти"
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "Thất bại"
|
||||
"comment" = "Bình luận"
|
||||
"success" = "Thành công"
|
||||
"lastOnline" = "Lần online gần nhất"
|
||||
"getVersion" = "Lấy phiên bản"
|
||||
"install" = "Cài đặt"
|
||||
"clients" = "Các khách hàng"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "Không có proxy ngược nào được thêm."
|
||||
"somethingWentWrong" = "Đã xảy ra lỗi"
|
||||
|
||||
[subscription]
|
||||
"title" = "Thông tin đăng ký"
|
||||
"subId" = "ID đăng ký"
|
||||
"status" = "Trạng thái"
|
||||
"downloaded" = "Đã tải xuống"
|
||||
"uploaded" = "Đã tải lên"
|
||||
"expiry" = "Hết hạn"
|
||||
"totalQuota" = "Tổng hạn mức"
|
||||
"individualLinks" = "Liên kết riêng lẻ"
|
||||
"active" = "Hoạt động"
|
||||
"inactive" = "Không hoạt động"
|
||||
"unlimited" = "Không giới hạn"
|
||||
"noExpiry" = "Không hết hạn"
|
||||
|
||||
[menu]
|
||||
"theme" = "Chủ đề"
|
||||
"dark" = "Tối"
|
||||
@@ -91,7 +106,7 @@
|
||||
"invalidFormData" = "Dạng dữ liệu nhập không hợp lệ."
|
||||
"emptyUsername" = "Vui lòng nhập tên người dùng."
|
||||
"emptyPassword" = "Vui lòng nhập mật khẩu."
|
||||
"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."
|
||||
"wrongUsernameOrPassword" = "Tên người dùng, mật khẩu hoặc mã xác thực hai yếu tố không hợp lệ."
|
||||
"successLogin" = "Bạn đã đăng nhập vào tài khoản thành công."
|
||||
|
||||
[pages.index]
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "Lỗi xảy ra khi truy xuất tệp cấu hình"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "Tổng Lưu Lượng"
|
||||
"allTimeTrafficUsage" = "Tổng mức sử dụng mọi lúc"
|
||||
"title" = "Điểm vào (Inbounds)"
|
||||
"totalDownUp" = "Tổng tải lên/tải xuống"
|
||||
"totalUsage" = "Tổng sử dụng"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "Chi tiết"
|
||||
"transportConfig" = "Giao vận"
|
||||
"expireDate" = "Ngày hết hạn"
|
||||
"createdAt" = "Tạo lúc"
|
||||
"updatedAt" = "Cập nhật"
|
||||
"resetTraffic" = "Đặt lại lưu lượng"
|
||||
"addInbound" = "Thêm điểm vào"
|
||||
"generalActions" = "Hành động chung"
|
||||
@@ -261,7 +280,8 @@
|
||||
"resetInboundClientTrafficSuccess" = "Đã đặt lại lưu lượng"
|
||||
"trafficGetError" = "Lỗi khi lấy thông tin lưu lượng"
|
||||
"getNewX25519CertError" = "Lỗi khi lấy chứng chỉ X25519."
|
||||
"getNewmldsa65Error" = "Lỗi khi lấy chúng tôi mldsa65."
|
||||
"getNewmldsa65Error" = "Lỗi khi lấy chứng chỉ mldsa65."
|
||||
"getNewVlessEncError" = "Lỗi khi lấy chứng chỉ VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Lời yêu cầu"
|
||||
@@ -535,9 +555,9 @@
|
||||
|
||||
[pages.settings.security]
|
||||
"admin" = "Thông tin đăng nhập quản trị viên"
|
||||
"twoFactor" = "Xác thực hai yếu tố"
|
||||
"twoFactorEnable" = "Bật 2FA"
|
||||
"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."
|
||||
"twoFactor" = "Xác thực hai yếu tố"
|
||||
"twoFactorEnable" = "Bật 2FA"
|
||||
"twoFactorEnableDesc" = "Thêm một lớp bảo mật bổ sung để tăng cường an toàn."
|
||||
"twoFactorModalSetTitle" = "Bật xác thực hai yếu tố"
|
||||
"twoFactorModalDeleteTitle" = "Tắt xác thực hai yếu tố"
|
||||
"twoFactorModalSteps" = "Để thiết lập xác thực hai yếu tố, hãy thực hiện các bước sau:"
|
||||
@@ -561,22 +581,23 @@
|
||||
"resetOutboundTrafficError" = "Lỗi khi đặt lại lưu lượng truy cập đi"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Bàn phím tùy chỉnh đã đóng!"
|
||||
"keyboardClosed" = "❌ Bàn phím đã đóng!"
|
||||
"noResult" = "❗ Không có kết quả!"
|
||||
"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lệnh lại!"
|
||||
"noQuery" = "❌ Không tìm thấy truy vấn! Vui lòng sử dụng lại lệnh!"
|
||||
"wentWrong" = "❌ Đã xảy ra lỗi!"
|
||||
"noIpRecord" = "❗ Không có bản ghi IP!"
|
||||
"noInbounds" = "❗ Không tìm thấy inbound!"
|
||||
"unlimited" = "♾ Không giới hạn"
|
||||
"unlimited" = "♾ Không giới hạn (Đặt lại)"
|
||||
"add" = "Thêm"
|
||||
"month" = "Tháng"
|
||||
"months" = "Tháng"
|
||||
"day" = "Ngày"
|
||||
"days" = "Ngày"
|
||||
"hours" = "Giờ"
|
||||
"unknown" = "Không rõ"
|
||||
"inbounds" = "Vào"
|
||||
"clients" = "Các người dùng"
|
||||
"minutes" = "Phút"
|
||||
"unknown" = "Không xác định"
|
||||
"inbounds" = "Inbound"
|
||||
"clients" = "Client"
|
||||
"offline" = "🔴 Ngoại tuyến"
|
||||
"online" = "🟢 Trực tuyến"
|
||||
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "失败"
|
||||
"comment" = "评论"
|
||||
"success" = "成功"
|
||||
"lastOnline" = "上次在线"
|
||||
"getVersion" = "获取版本"
|
||||
"install" = "安装"
|
||||
"clients" = "客户端"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "未添加反向代理。"
|
||||
"somethingWentWrong" = "出了点问题"
|
||||
|
||||
[subscription]
|
||||
"title" = "订阅信息"
|
||||
"subId" = "订阅 ID"
|
||||
"status" = "状态"
|
||||
"downloaded" = "已下载"
|
||||
"uploaded" = "已上传"
|
||||
"expiry" = "到期"
|
||||
"totalQuota" = "总配额"
|
||||
"individualLinks" = "单独链接"
|
||||
"active" = "启用"
|
||||
"inactive" = "停用"
|
||||
"unlimited" = "无限制"
|
||||
"noExpiry" = "无到期"
|
||||
|
||||
[menu]
|
||||
"theme" = "主题"
|
||||
"dark" = "暗色"
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "检索配置文件时出错"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "累计总流量"
|
||||
"allTimeTrafficUsage" = "所有时间总使用量"
|
||||
"title" = "入站列表"
|
||||
"totalDownUp" = "总上传 / 下载"
|
||||
"totalUsage" = "总用量"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "详细信息"
|
||||
"transportConfig" = "传输配置"
|
||||
"expireDate" = "到期时间"
|
||||
"createdAt" = "创建时间"
|
||||
"updatedAt" = "更新时间"
|
||||
"resetTraffic" = "重置流量"
|
||||
"addInbound" = "添加入站"
|
||||
"generalActions" = "通用操作"
|
||||
@@ -262,6 +281,7 @@
|
||||
"trafficGetError" = "获取流量数据时出错"
|
||||
"getNewX25519CertError" = "获取X25519证书时出错。"
|
||||
"getNewmldsa65Error" = "获取mldsa65证书时出错。"
|
||||
"getNewVlessEncError" = "获取VlessEnc证书时出错。"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "请求"
|
||||
@@ -563,19 +583,20 @@
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ 自定义键盘已关闭!"
|
||||
"noResult" = "❗ 没有结果!"
|
||||
"noQuery" = "❌ 未找到查询!请重新使用命令!"
|
||||
"noQuery" = "❌ 未找到查询!请再次使用该命令!"
|
||||
"wentWrong" = "❌ 出了点问题!"
|
||||
"noIpRecord" = "❗ 没有 IP 记录!"
|
||||
"noInbounds" = "❗ 没有找到入站连接!"
|
||||
"unlimited" = "♾ 无限制"
|
||||
"noIpRecord" = "❗ 没有IP记录!"
|
||||
"noInbounds" = "❗ 未找到入站!"
|
||||
"unlimited" = "♾ 无限(重置)"
|
||||
"add" = "添加"
|
||||
"month" = "月"
|
||||
"months" = "月"
|
||||
"day" = "天"
|
||||
"days" = "天"
|
||||
"hours" = "小时"
|
||||
"minutes" = "分钟"
|
||||
"unknown" = "未知"
|
||||
"inbounds" = "入站连接"
|
||||
"inbounds" = "入站"
|
||||
"clients" = "客户端"
|
||||
"offline" = "🔴 离线"
|
||||
"online" = "🟢 在线"
|
||||
|
||||
@@ -50,6 +50,7 @@
|
||||
"fail" = "失敗"
|
||||
"comment" = "評論"
|
||||
"success" = "成功"
|
||||
"lastOnline" = "上次上線"
|
||||
"getVersion" = "獲取版本"
|
||||
"install" = "安裝"
|
||||
"clients" = "客戶端"
|
||||
@@ -71,6 +72,20 @@
|
||||
"emptyReverseDesc" = "未添加反向代理。"
|
||||
"somethingWentWrong" = "發生錯誤"
|
||||
|
||||
[subscription]
|
||||
"title" = "訂閱資訊"
|
||||
"subId" = "訂閱 ID"
|
||||
"status" = "狀態"
|
||||
"downloaded" = "已下載"
|
||||
"uploaded" = "已上傳"
|
||||
"expiry" = "到期"
|
||||
"totalQuota" = "總配額"
|
||||
"individualLinks" = "個別連結"
|
||||
"active" = "啟用"
|
||||
"inactive" = "停用"
|
||||
"unlimited" = "無限制"
|
||||
"noExpiry" = "無到期"
|
||||
|
||||
[menu]
|
||||
"theme" = "主題"
|
||||
"dark" = "深色"
|
||||
@@ -151,6 +166,8 @@
|
||||
"getConfigError" = "檢索設定檔時發生錯誤"
|
||||
|
||||
[pages.inbounds]
|
||||
"allTimeTraffic" = "累計總流量"
|
||||
"allTimeTrafficUsage" = "所有时间总使用量"
|
||||
"title" = "入站列表"
|
||||
"totalDownUp" = "總上傳 / 下載"
|
||||
"totalUsage" = "總用量"
|
||||
@@ -165,6 +182,8 @@
|
||||
"details" = "詳細資訊"
|
||||
"transportConfig" = "傳輸配置"
|
||||
"expireDate" = "到期時間"
|
||||
"createdAt" = "建立時間"
|
||||
"updatedAt" = "更新時間"
|
||||
"resetTraffic" = "重置流量"
|
||||
"addInbound" = "新增入站"
|
||||
"generalActions" = "通用操作"
|
||||
@@ -262,6 +281,7 @@
|
||||
"trafficGetError" = "取得流量資料時發生錯誤"
|
||||
"getNewX25519CertError" = "取得X25519憑證時發生錯誤。"
|
||||
"getNewmldsa65Error" = "取得mldsa65憑證時發生錯誤。"
|
||||
"getNewVlessEncError" = "取得VlessEnc憑證時發生錯誤。"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "請求"
|
||||
@@ -563,22 +583,23 @@
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ 自定義鍵盤已關閉!"
|
||||
"noResult" = "❗ 沒有結果!"
|
||||
"noQuery" = "❌ 未找到查詢!請重新使用命令!"
|
||||
"noQuery" = "❌ 未找到查詢!請再次使用該命令!"
|
||||
"wentWrong" = "❌ 出了點問題!"
|
||||
"noIpRecord" = "❗ 沒有 IP 記錄!"
|
||||
"noInbounds" = "❗ 沒有找到入站連線!"
|
||||
"unlimited" = "♾ 無限制"
|
||||
"add" = "新增"
|
||||
"noIpRecord" = "❗ 沒有IP記錄!"
|
||||
"noInbounds" = "❗ 未找到入站!"
|
||||
"unlimited" = "♾ 無限(重置)"
|
||||
"add" = "添加"
|
||||
"month" = "月"
|
||||
"months" = "月"
|
||||
"day" = "天"
|
||||
"days" = "天"
|
||||
"hours" = "小時"
|
||||
"minutes" = "分鐘"
|
||||
"unknown" = "未知"
|
||||
"inbounds" = "入站連線"
|
||||
"inbounds" = "入站"
|
||||
"clients" = "客戶端"
|
||||
"offline" = "🔴 離線"
|
||||
"online" = "🟢 線上"
|
||||
"online" = "🟢 在線"
|
||||
|
||||
[tgbot.commands]
|
||||
"unknown" = "❗ 未知命令"
|
||||
|
||||
31
web/web.go
31
web/web.go
@@ -31,7 +31,7 @@ import (
|
||||
"github.com/robfig/cron/v3"
|
||||
)
|
||||
|
||||
//go:embed assets/*
|
||||
//go:embed assets
|
||||
var assetsFS embed.FS
|
||||
|
||||
//go:embed html/*
|
||||
@@ -78,6 +78,15 @@ func (f *wrapAssetsFileInfo) ModTime() time.Time {
|
||||
return startTime
|
||||
}
|
||||
|
||||
// Expose embedded resources for reuse by other servers (e.g., sub server)
|
||||
func EmbeddedHTML() embed.FS {
|
||||
return htmlFS
|
||||
}
|
||||
|
||||
func EmbeddedAssets() embed.FS {
|
||||
return assetsFS
|
||||
}
|
||||
|
||||
type Server struct {
|
||||
httpServer *http.Server
|
||||
listener net.Listener
|
||||
@@ -176,10 +185,19 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/API/"})))
|
||||
engine.Use(gzip.Gzip(gzip.DefaultCompression, gzip.WithExcludedPaths([]string{basePath + "panel/api/"})))
|
||||
assetsBasePath := basePath + "assets/"
|
||||
|
||||
store := cookie.NewStore(secret)
|
||||
// Configure default session cookie options, including expiration (MaxAge)
|
||||
if sessionMaxAge, err := s.settingService.GetSessionMaxAge(); err == nil {
|
||||
store.Options(sessions.Options{
|
||||
Path: "/",
|
||||
MaxAge: sessionMaxAge * 60, // minutes -> seconds
|
||||
HttpOnly: true,
|
||||
SameSite: http.SameSiteLaxMode,
|
||||
})
|
||||
}
|
||||
engine.Use(sessions.Sessions("3x-ui", store))
|
||||
engine.Use(func(c *gin.Context) {
|
||||
c.Set("base_path", basePath)
|
||||
@@ -201,7 +219,11 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
i18nWebFunc := func(key string, params ...string) string {
|
||||
return locale.I18n(locale.Web, key, params...)
|
||||
}
|
||||
engine.FuncMap["i18n"] = i18nWebFunc
|
||||
// Register template functions before loading templates
|
||||
funcMap := template.FuncMap{
|
||||
"i18n": i18nWebFunc,
|
||||
}
|
||||
engine.SetFuncMap(funcMap)
|
||||
engine.Use(locale.LocalizerMiddleware())
|
||||
|
||||
// set static files and template
|
||||
@@ -211,11 +233,12 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// Use the registered func map with the loaded templates
|
||||
engine.LoadHTMLFiles(files...)
|
||||
engine.StaticFS(basePath+"assets", http.FS(os.DirFS("web/assets")))
|
||||
} else {
|
||||
// for production
|
||||
template, err := s.getHtmlTemplate(engine.FuncMap)
|
||||
template, err := s.getHtmlTemplate(funcMap)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
BIN
windows_files/SSL/Win64OpenSSL_Light-3_5_2.exe
Normal file
BIN
windows_files/SSL/Win64OpenSSL_Light-3_5_2.exe
Normal file
Binary file not shown.
13
windows_files/readme.txt
Normal file
13
windows_files/readme.txt
Normal file
@@ -0,0 +1,13 @@
|
||||
you can't install fail2ban on windows
|
||||
we don't have bash menu for windows
|
||||
if you forgot your password you need to check your database with https://sqlitebrowser.org/
|
||||
the app need to be open all the time
|
||||
|
||||
default setting:
|
||||
http://localhost:2053/
|
||||
user: admin
|
||||
pass: admin
|
||||
port: 2053
|
||||
|
||||
|
||||
openssl req -x509 -nodes -days 365 -newkey rsa:2048 -keyout localhost.key -out localhost.crt
|
||||
@@ -12,4 +12,4 @@ Restart=on-failure
|
||||
RestartSec=5s
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
WantedBy=multi-user.target
|
||||
|
||||
249
x-ui.sh
249
x-ui.sh
@@ -398,37 +398,6 @@ show_log() {
|
||||
esac
|
||||
}
|
||||
|
||||
show_banlog() {
|
||||
local system_log="/var/log/fail2ban.log"
|
||||
|
||||
echo -e "${green}Checking ban logs...${plain}\n"
|
||||
|
||||
if ! systemctl is-active --quiet fail2ban; then
|
||||
echo -e "${red}Fail2ban service is not running!${plain}\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -f "$system_log" ]]; then
|
||||
echo -e "${green}Recent system ban activities from fail2ban.log:${plain}"
|
||||
grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [[ -f "${iplimit_banned_log_path}" ]]; then
|
||||
echo -e "${green}3X-IPL ban log entries:${plain}"
|
||||
if [[ -s "${iplimit_banned_log_path}" ]]; then
|
||||
grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}"
|
||||
else
|
||||
echo -e "${yellow}Ban log file is empty${plain}"
|
||||
fi
|
||||
else
|
||||
echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}"
|
||||
fi
|
||||
|
||||
echo -e "\n${green}Current jail status:${plain}"
|
||||
fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}"
|
||||
}
|
||||
|
||||
bbr_menu() {
|
||||
echo -e "${green}\t1.${plain} Enable BBR"
|
||||
echo -e "${green}\t2.${plain} Disable BBR"
|
||||
@@ -1005,7 +974,7 @@ ssl_cert_issue() {
|
||||
# install socat second
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
apt update && apt install socat -y
|
||||
apt-get update && apt-get install socat -y
|
||||
;;
|
||||
centos | rhel | almalinux | rocky | ol)
|
||||
yum -y update && yum -y install socat
|
||||
@@ -1330,81 +1299,7 @@ run_speedtest() {
|
||||
speedtest
|
||||
}
|
||||
|
||||
create_iplimit_jails() {
|
||||
# Use default bantime if not passed => 30 minutes
|
||||
local bantime="${1:-30}"
|
||||
|
||||
# Uncomment 'allowipv6 = auto' in fail2ban.conf
|
||||
sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf
|
||||
|
||||
# On Debian 12+ fail2ban's default backend should be changed to systemd
|
||||
if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then
|
||||
sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf
|
||||
fi
|
||||
|
||||
cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
|
||||
[3x-ipl]
|
||||
enabled=true
|
||||
backend=auto
|
||||
filter=3x-ipl
|
||||
action=3x-ipl
|
||||
logpath=${iplimit_log_path}
|
||||
maxretry=2
|
||||
findtime=32
|
||||
bantime=${bantime}m
|
||||
EOF
|
||||
|
||||
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
|
||||
[Definition]
|
||||
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
|
||||
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
|
||||
ignoreregex =
|
||||
EOF
|
||||
|
||||
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
|
||||
[INCLUDES]
|
||||
before = iptables-allports.conf
|
||||
|
||||
[Definition]
|
||||
actionstart = <iptables> -N f2b-<name>
|
||||
<iptables> -A f2b-<name> -j <returntype>
|
||||
<iptables> -I <chain> -p <protocol> -j f2b-<name>
|
||||
|
||||
actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
|
||||
<actionflush>
|
||||
<iptables> -X f2b-<name>
|
||||
|
||||
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
|
||||
|
||||
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
|
||||
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds." >> ${iplimit_banned_log_path}
|
||||
|
||||
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
|
||||
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = <F-USER> [IP] = <ip> unbanned." >> ${iplimit_banned_log_path}
|
||||
|
||||
[Init]
|
||||
name = default
|
||||
protocol = tcp
|
||||
chain = INPUT
|
||||
EOF
|
||||
|
||||
echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}"
|
||||
}
|
||||
|
||||
iplimit_remove_conflicts() {
|
||||
local jail_files=(
|
||||
/etc/fail2ban/jail.conf
|
||||
/etc/fail2ban/jail.local
|
||||
)
|
||||
|
||||
for file in "${jail_files[@]}"; do
|
||||
# Check for [3x-ipl] config in jail file then remove it
|
||||
if test -f "${file}" && grep -qw '3x-ipl' ${file}; then
|
||||
sed -i "/\[3x-ipl\]/,/^$/d" ${file}
|
||||
echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
ip_validation() {
|
||||
ipv6_regex="^(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))$"
|
||||
@@ -1514,14 +1409,22 @@ install_iplimit() {
|
||||
# Check the OS and install necessary packages
|
||||
case "${release}" in
|
||||
ubuntu)
|
||||
apt-get update
|
||||
if [[ "${os_version}" -ge 24 ]]; then
|
||||
apt update && apt install python3-pip -y
|
||||
apt-get install python3-pip -y
|
||||
python3 -m pip install pyasynchat --break-system-packages
|
||||
fi
|
||||
apt update && apt install fail2ban -y
|
||||
apt-get install fail2ban -y
|
||||
;;
|
||||
debian | armbian)
|
||||
apt update && apt install fail2ban -y
|
||||
debian)
|
||||
apt-get update
|
||||
if [ "$os_version" -ge 12 ]; then
|
||||
apt-get install -y python3-systemd
|
||||
fi
|
||||
apt-get install -y fail2ban
|
||||
;;
|
||||
armbian)
|
||||
apt-get update && apt-get install fail2ban -y
|
||||
;;
|
||||
centos | rhel | almalinux | rocky | ol)
|
||||
yum update -y && yum install epel-release -y
|
||||
@@ -1632,11 +1535,129 @@ remove_iplimit() {
|
||||
esac
|
||||
}
|
||||
|
||||
SSH_port_forwarding() {
|
||||
local server_ip=$(curl -s --max-time 3 https://api.ipify.org)
|
||||
if [ -z "$server_ip" ]; then
|
||||
server_ip=$(curl -s --max-time 3 https://4.ident.me)
|
||||
show_banlog() {
|
||||
local system_log="/var/log/fail2ban.log"
|
||||
|
||||
echo -e "${green}Checking ban logs...${plain}\n"
|
||||
|
||||
if ! systemctl is-active --quiet fail2ban; then
|
||||
echo -e "${red}Fail2ban service is not running!${plain}\n"
|
||||
return 1
|
||||
fi
|
||||
|
||||
if [[ -f "$system_log" ]]; then
|
||||
echo -e "${green}Recent system ban activities from fail2ban.log:${plain}"
|
||||
grep "3x-ipl" "$system_log" | grep -E "Ban|Unban" | tail -n 10 || echo -e "${yellow}No recent system ban activities found${plain}"
|
||||
echo ""
|
||||
fi
|
||||
|
||||
if [[ -f "${iplimit_banned_log_path}" ]]; then
|
||||
echo -e "${green}3X-IPL ban log entries:${plain}"
|
||||
if [[ -s "${iplimit_banned_log_path}" ]]; then
|
||||
grep -v "INIT" "${iplimit_banned_log_path}" | tail -n 10 || echo -e "${yellow}No ban entries found${plain}"
|
||||
else
|
||||
echo -e "${yellow}Ban log file is empty${plain}"
|
||||
fi
|
||||
else
|
||||
echo -e "${red}Ban log file not found at: ${iplimit_banned_log_path}${plain}"
|
||||
fi
|
||||
|
||||
echo -e "\n${green}Current jail status:${plain}"
|
||||
fail2ban-client status 3x-ipl || echo -e "${yellow}Unable to get jail status${plain}"
|
||||
}
|
||||
|
||||
create_iplimit_jails() {
|
||||
# Use default bantime if not passed => 30 minutes
|
||||
local bantime="${1:-30}"
|
||||
|
||||
# Uncomment 'allowipv6 = auto' in fail2ban.conf
|
||||
sed -i 's/#allowipv6 = auto/allowipv6 = auto/g' /etc/fail2ban/fail2ban.conf
|
||||
|
||||
# On Debian 12+ fail2ban's default backend should be changed to systemd
|
||||
if [[ "${release}" == "debian" && ${os_version} -ge 12 ]]; then
|
||||
sed -i '0,/action =/s/backend = auto/backend = systemd/' /etc/fail2ban/jail.conf
|
||||
fi
|
||||
|
||||
cat << EOF > /etc/fail2ban/jail.d/3x-ipl.conf
|
||||
[3x-ipl]
|
||||
enabled=true
|
||||
backend=auto
|
||||
filter=3x-ipl
|
||||
action=3x-ipl
|
||||
logpath=${iplimit_log_path}
|
||||
maxretry=2
|
||||
findtime=32
|
||||
bantime=${bantime}m
|
||||
EOF
|
||||
|
||||
cat << EOF > /etc/fail2ban/filter.d/3x-ipl.conf
|
||||
[Definition]
|
||||
datepattern = ^%%Y/%%m/%%d %%H:%%M:%%S
|
||||
failregex = \[LIMIT_IP\]\s*Email\s*=\s*<F-USER>.+</F-USER>\s*\|\|\s*SRC\s*=\s*<ADDR>
|
||||
ignoreregex =
|
||||
EOF
|
||||
|
||||
cat << EOF > /etc/fail2ban/action.d/3x-ipl.conf
|
||||
[INCLUDES]
|
||||
before = iptables-allports.conf
|
||||
|
||||
[Definition]
|
||||
actionstart = <iptables> -N f2b-<name>
|
||||
<iptables> -A f2b-<name> -j <returntype>
|
||||
<iptables> -I <chain> -p <protocol> -j f2b-<name>
|
||||
|
||||
actionstop = <iptables> -D <chain> -p <protocol> -j f2b-<name>
|
||||
<actionflush>
|
||||
<iptables> -X f2b-<name>
|
||||
|
||||
actioncheck = <iptables> -n -L <chain> | grep -q 'f2b-<name>[ \t]'
|
||||
|
||||
actionban = <iptables> -I f2b-<name> 1 -s <ip> -j <blocktype>
|
||||
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") BAN [Email] = <F-USER> [IP] = <ip> banned for <bantime> seconds." >> ${iplimit_banned_log_path}
|
||||
|
||||
actionunban = <iptables> -D f2b-<name> -s <ip> -j <blocktype>
|
||||
echo "\$(date +"%%Y/%%m/%%d %%H:%%M:%%S") UNBAN [Email] = <F-USER> [IP] = <ip> unbanned." >> ${iplimit_banned_log_path}
|
||||
|
||||
[Init]
|
||||
name = default
|
||||
protocol = tcp
|
||||
chain = INPUT
|
||||
EOF
|
||||
|
||||
echo -e "${green}Ip Limit jail files created with a bantime of ${bantime} minutes.${plain}"
|
||||
}
|
||||
|
||||
iplimit_remove_conflicts() {
|
||||
local jail_files=(
|
||||
/etc/fail2ban/jail.conf
|
||||
/etc/fail2ban/jail.local
|
||||
)
|
||||
|
||||
for file in "${jail_files[@]}"; do
|
||||
# Check for [3x-ipl] config in jail file then remove it
|
||||
if test -f "${file}" && grep -qw '3x-ipl' ${file}; then
|
||||
sed -i "/\[3x-ipl\]/,/^$/d" ${file}
|
||||
echo -e "${yellow}Removing conflicts of [3x-ipl] in jail (${file})!${plain}\n"
|
||||
fi
|
||||
done
|
||||
}
|
||||
|
||||
SSH_port_forwarding() {
|
||||
local URL_lists=(
|
||||
"https://api4.ipify.org"
|
||||
"https://ipv4.icanhazip.com"
|
||||
"https://v4.api.ipinfo.io/ip"
|
||||
"https://ipv4.myexternalip.com/raw"
|
||||
"https://4.ident.me"
|
||||
"https://check-host.net/ip"
|
||||
)
|
||||
local server_ip=""
|
||||
for ip_address in "${URL_lists[@]}"; do
|
||||
server_ip=$(curl -s --max-time 3 "${ip_address}" 2>/dev/null | tr -d '[:space:]')
|
||||
if [[ -n "${server_ip}" ]]; then
|
||||
break
|
||||
fi
|
||||
done
|
||||
local existing_webBasePath=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}')
|
||||
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||
local existing_listenIP=$(/usr/local/x-ui/x-ui setting -getListen true | grep -Eo 'listenIP: .+' | awk '{print $2}')
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user