mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-12-22 22:32:41 +00:00
Resolve merge conflicts with upstream/main
This commit is contained in:
commit
c76c0bd7cb
46 changed files with 1222 additions and 1146 deletions
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
|
|
@ -85,7 +85,7 @@ jobs:
|
|||
cd x-ui/bin
|
||||
|
||||
# Download dependencies
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.9.11/"
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.12.2/"
|
||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||
wget -q ${Xray_URL}Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
|
|
@ -183,7 +183,7 @@ jobs:
|
|||
cd x-ui\bin
|
||||
|
||||
# Download Xray for Windows
|
||||
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.9.11/"
|
||||
$Xray_URL = "https://github.com/XTLS/Xray-core/releases/download/v25.12.2/"
|
||||
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"
|
||||
|
|
|
|||
|
|
@ -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.9.11/Xray-linux-${ARCH}.zip"
|
||||
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.12.2/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||
mv xray "xray-linux-${FNAME}"
|
||||
|
|
|
|||
|
|
@ -19,7 +19,7 @@
|
|||
**3X-UI** — advanced, open-source web-based control panel designed for managing Xray-core server. It offers a user-friendly interface for configuring and monitoring various VPN and proxy protocols.
|
||||
Test test test
|
||||
> [!IMPORTANT]
|
||||
> This project is only for personal using, please do not use it for illegal purposes, please do not use it in a production environment.
|
||||
> This project is only for personal usage, please do not use it for illegal purposes, and please do not use it in a production environment.
|
||||
|
||||
As an enhanced fork of the original X-UI project, 3X-UI provides improved stability, broader protocol support, and additional features.
|
||||
|
||||
|
|
|
|||
|
|
@ -1 +1 @@
|
|||
2.8.4
|
||||
2.8.5
|
||||
|
|
@ -3,11 +3,13 @@ package database
|
|||
import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/glebarez/sqlite"
|
||||
"github.com/mhsanaei/3x-ui/v2/database/model"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/logger"
|
||||
)
|
||||
|
||||
var db *gorm.DB
|
||||
|
|
@ -38,6 +40,18 @@ func GetDB() *gorm.DB {
|
|||
return db
|
||||
}
|
||||
|
||||
// CloseDB закрывает соединение с БД.
|
||||
func CloseDB() error {
|
||||
if db == nil {
|
||||
return nil
|
||||
}
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
return sqlDB.Close()
|
||||
}
|
||||
|
||||
// IsNotFound — хелпер для проверки "запись не найдена".
|
||||
func IsNotFound(err error) bool {
|
||||
return errors.Is(err, gorm.ErrRecordNotFound)
|
||||
|
|
@ -84,3 +98,29 @@ func SeedAdmin() error {
|
|||
}
|
||||
return db.Create(&admin).Error
|
||||
}
|
||||
|
||||
// ValidateSQLiteDB opens the provided sqlite DB path with a throw-away connection
|
||||
// and runs a PRAGMA integrity_check to ensure the file is structurally sound.
|
||||
// It does not mutate global state or run migrations.
|
||||
func ValidateSQLiteDB(dbPath string) error {
|
||||
if _, err := os.Stat(dbPath); err != nil { // file must exist
|
||||
return err
|
||||
}
|
||||
gdb, err := gorm.Open(sqlite.Open(dbPath), &gorm.Config{Logger: logger.Default.LogMode(logger.Silent)})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
sqlDB, err := gdb.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sqlDB.Close()
|
||||
var res string
|
||||
if err := gdb.Raw("PRAGMA integrity_check;").Scan(&res).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
if res != "ok" {
|
||||
return errors.New("sqlite integrity check failed: " + res)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
|
|
|||
76
go.mod
76
go.mod
|
|
@ -1,33 +1,33 @@
|
|||
module github.com/mhsanaei/3x-ui/v2
|
||||
|
||||
go 1.25.1
|
||||
go 1.25.5
|
||||
|
||||
require (
|
||||
github.com/gin-contrib/gzip v1.2.3
|
||||
github.com/gin-contrib/gzip v1.2.5
|
||||
github.com/gin-contrib/sessions v1.0.4
|
||||
github.com/gin-gonic/gin v1.11.0
|
||||
github.com/go-ldap/ldap/v3 v3.4.11
|
||||
github.com/go-ldap/ldap/v3 v3.4.12
|
||||
github.com/goccy/go-json v0.10.5
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0
|
||||
github.com/google/uuid v1.6.0
|
||||
github.com/gorilla/websocket v1.5.3
|
||||
github.com/joho/godotenv v1.5.1
|
||||
github.com/mymmrac/telego v1.3.0
|
||||
github.com/mymmrac/telego v1.3.1
|
||||
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.8
|
||||
github.com/shirou/gopsutil/v4 v4.25.11
|
||||
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
|
||||
github.com/valyala/fasthttp v1.66.0
|
||||
github.com/valyala/fasthttp v1.68.0
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/xtls/xray-core v1.250911.0
|
||||
github.com/xtls/xray-core v1.251202.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/crypto v0.42.0
|
||||
golang.org/x/sys v0.36.0
|
||||
golang.org/x/text v0.29.0
|
||||
google.golang.org/grpc v1.75.1
|
||||
gorm.io/gorm v1.31.0
|
||||
golang.org/x/crypto v0.45.0
|
||||
golang.org/x/sys v0.38.0
|
||||
golang.org/x/text v0.31.0
|
||||
google.golang.org/grpc v1.77.0
|
||||
gorm.io/gorm v1.31.1
|
||||
)
|
||||
|
||||
require (
|
||||
|
|
@ -41,24 +41,24 @@ require (
|
|||
)
|
||||
|
||||
require (
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 // indirect
|
||||
github.com/Azure/go-ntlmssp v0.1.0 // indirect
|
||||
github.com/andybalholm/brotli v1.2.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/bytedance/sonic v1.14.2 // indirect
|
||||
github.com/bytedance/sonic/loader v0.4.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.9.0 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.10 // indirect
|
||||
github.com/ebitengine/purego v0.9.1 // indirect
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 // indirect
|
||||
github.com/gin-contrib/sse v1.1.0 // indirect
|
||||
github.com/glebarez/sqlite v1.11.0
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 // indirect
|
||||
github.com/go-ole/go-ole v1.3.0 // indirect
|
||||
github.com/go-playground/locales v0.14.1 // indirect
|
||||
github.com/go-playground/universal-translator v0.18.1 // indirect
|
||||
github.com/go-playground/validator/v10 v10.27.0 // indirect
|
||||
github.com/goccy/go-yaml v1.18.0 // indirect
|
||||
github.com/go-playground/validator/v10 v10.28.0 // indirect
|
||||
github.com/goccy/go-yaml v1.19.0 // indirect
|
||||
github.com/google/btree v1.1.3 // indirect
|
||||
github.com/gorilla/context v1.1.2 // indirect
|
||||
github.com/gorilla/securecookie v1.1.2 // indirect
|
||||
|
|
@ -68,48 +68,46 @@ require (
|
|||
github.com/jinzhu/now v1.1.5 // indirect
|
||||
github.com/json-iterator/go v1.1.12 // indirect
|
||||
github.com/juju/ratelimit v1.0.2 // indirect
|
||||
github.com/klauspost/compress v1.18.0 // indirect
|
||||
github.com/klauspost/compress v1.18.2 // indirect
|
||||
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-20250827001030-24949be3fa54 // indirect
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 // indirect
|
||||
github.com/mattn/go-isatty v0.0.20 // 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
|
||||
github.com/pires/go-proxyproto v0.8.1 // indirect
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
|
||||
github.com/quic-go/qpack v0.5.1 // indirect
|
||||
github.com/quic-go/quic-go v0.54.0 // indirect
|
||||
github.com/refraction-networking/utls v1.8.0 // indirect
|
||||
github.com/quic-go/qpack v0.6.0 // indirect
|
||||
github.com/quic-go/quic-go v0.57.1 // indirect
|
||||
github.com/refraction-networking/utls v1.8.1 // 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.10 // indirect
|
||||
github.com/sagernet/sing v0.7.13 // 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
|
||||
github.com/tklauser/go-sysconf v0.3.16 // indirect
|
||||
github.com/tklauser/numcpus v0.11.0 // indirect
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||
github.com/ugorji/go/codec v1.3.0 // indirect
|
||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e // indirect
|
||||
github.com/valyala/bytebufferpool v1.0.0 // indirect
|
||||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c // indirect
|
||||
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237 // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // 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/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.37.0 // indirect
|
||||
golang.org/x/arch v0.23.0 // indirect
|
||||
golang.org/x/mod v0.30.0 // indirect
|
||||
golang.org/x/net v0.47.0 // indirect
|
||||
golang.org/x/sync v0.18.0 // indirect
|
||||
golang.org/x/time v0.14.0 // indirect
|
||||
golang.org/x/tools v0.39.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-20250922171735-9219d122eba9 // indirect
|
||||
google.golang.org/protobuf v1.36.9 // indirect
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 // indirect
|
||||
google.golang.org/protobuf v1.36.10 // indirect
|
||||
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
|
||||
lukechampine.com/blake3 v1.4.1 // indirect
|
||||
)
|
||||
|
|
|
|||
177
go.sum
177
go.sum
|
|
@ -1,22 +1,21 @@
|
|||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+s7s0MwaRv9igoPqLRdzOLzw/8Xvq8=
|
||||
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
|
||||
github.com/Azure/go-ntlmssp v0.1.0 h1:DjFo6YtWzNqNvQdrwEyr/e4nhU3vRiwenz5QX7sFz+A=
|
||||
github.com/Azure/go-ntlmssp v0.1.0/go.mod h1:NYqdhxd/8aAct/s4qSYZEerdPuH1liG2/X9DiVTbhpk=
|
||||
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/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
|
||||
github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
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/bytedance/sonic v1.14.2 h1:k1twIoe97C1DtYUo+fZQy865IuHia4PR5RPiuGPPIIE=
|
||||
github.com/bytedance/sonic v1.14.2/go.mod h1:T80iDELeHiHKSc0C9tubFygiuXoGzrkjKzX2quAx980=
|
||||
github.com/bytedance/sonic/loader v0.4.0 h1:olZ7lEqcxtZygCK9EKYKADnpQoYkRQxaeY2NYzevs+o=
|
||||
github.com/bytedance/sonic/loader v0.4.0/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
github.com/cloudflare/circl v1.6.1/go.mod h1:uddAzsPgqdMAYatqJ0lsjX1oECcQLIlRpzZh3pJrofs=
|
||||
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
|
||||
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
|
||||
github.com/creack/pty v1.1.9/go.mod h1:oKZEueFk5CKHvIhNR5MUki03XCEU+Q6VDXinZuGJ33E=
|
||||
github.com/davecgh/go-spew v1.1.0/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
|
||||
github.com/davecgh/go-spew v1.1.1/go.mod h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
|
||||
|
|
@ -25,14 +24,14 @@ 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/dustin/go-humanize v1.0.1 h1:GzkhY7T5VNhEkwH0PVJgjz+fX1rhBrR7pRT3mDkpeCY=
|
||||
github.com/dustin/go-humanize v1.0.1/go.mod h1:Mu1zIs6XwVuF/gI1OepvI0qD18qycQx+mFykh5fBlto=
|
||||
github.com/ebitengine/purego v0.9.0 h1:mh0zpKBIXDceC63hpvPuGLiJ8ZAa3DfrFTudmfi8A4k=
|
||||
github.com/ebitengine/purego v0.9.0/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
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/ebitengine/purego v0.9.1 h1:a/k2f2HQU3Pi399RPW1MOaZyhKJL9w/xFpKAg4q1s0A=
|
||||
github.com/ebitengine/purego v0.9.1/go.mod h1:iIjxzd6CiRiOG0UyXP+V1+jWqUXVjPKLAI0mRfJZTmQ=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11 h1:AQvxbp830wPhHTqc1u7nzoLT+ZFxGY7emj5DR5DYFik=
|
||||
github.com/gabriel-vasile/mimetype v1.4.11/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=
|
||||
github.com/gin-contrib/gzip v1.2.3/go.mod h1:ad72i4Bzmaypk8M762gNXa2wkxxjbz0icRNnuLJ9a/c=
|
||||
github.com/gin-contrib/gzip v1.2.5 h1:fIZs0S+l17pIu1P5XRJOo/YNqfIuPCrZZ3TWB7pjckI=
|
||||
github.com/gin-contrib/gzip v1.2.5/go.mod h1:aomRgR7ftdZV3uWY0gW/m8rChfxau0n8YVvwlOHONzw=
|
||||
github.com/gin-contrib/sessions v1.0.4 h1:ha6CNdpYiTOK/hTp05miJLbpTSNfOnFg5Jm2kbcqy8U=
|
||||
github.com/gin-contrib/sessions v1.0.4/go.mod h1:ccmkrb2z6iU2osiAHZG3x3J4suJK+OU27oqzlWOqQgs=
|
||||
github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w=
|
||||
|
|
@ -45,8 +44,8 @@ github.com/glebarez/sqlite v1.11.0 h1:wSG0irqzP6VurnMEpFGer5Li19RpIRi2qvQz++w0GM
|
|||
github.com/glebarez/sqlite v1.11.0/go.mod h1:h8/o8j5wiAsqSPoWELDUdJXhjAhsVliSn7bWZjOhrgQ=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
|
||||
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU=
|
||||
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
|
||||
github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
|
||||
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
|
||||
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
|
||||
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
|
||||
|
|
@ -60,12 +59,12 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
|
|||
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
|
||||
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
|
||||
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
|
||||
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4=
|
||||
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo=
|
||||
github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
|
||||
github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
|
||||
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
|
||||
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
|
||||
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
|
||||
github.com/goccy/go-yaml v1.18.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/goccy/go-yaml v1.19.0 h1:EmkZ9RIsX+Uq4DYFowegAuJo8+xdX3T/2dwNPXbxEYE=
|
||||
github.com/goccy/go-yaml v1.19.0/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0 h1:pv4AsKCKKZuqlgs5sUmn4x8UlGa0kEVt/puTpKx9vvo=
|
||||
github.com/golang-jwt/jwt/v5 v5.3.0/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE=
|
||||
github.com/golang/mock v1.7.0-rc.1 h1:YojYx61/OLFsiv6Rw1Z96LpldJIy31o+UHmwAUMJ6/U=
|
||||
|
|
@ -117,8 +116,8 @@ github.com/json-iterator/go v1.1.12 h1:PV8peI4a0ysnczrg+LtxykD8LfKY9ML6u2jnxaEnr
|
|||
github.com/json-iterator/go v1.1.12/go.mod h1:e30LSqwooZae/UwlEbR2852Gd8hjQvJoHmT4TnhNGBo=
|
||||
github.com/juju/ratelimit v1.0.2 h1:sRxmtRiajbvrcLQT7S+JbqU0ntsb9W2yhSdNN8tWfaI=
|
||||
github.com/juju/ratelimit v1.0.2/go.mod h1:qapgC/Gy+xNh9UxzV13HGGl/6UXNN+ct+vwSgWNm/qk=
|
||||
github.com/klauspost/compress v1.18.0 h1:c/Cqfb0r+Yi+JtIEq73FWXVkRonBlf0CRNYc8Zttxdo=
|
||||
github.com/klauspost/compress v1.18.0/go.mod h1:2Pp+KzxcywXVXMr50+X0Q/Lsb43OQHYWRCY2AiWywWQ=
|
||||
github.com/klauspost/compress v1.18.2 h1:iiPHWW0YrcFgpBYhsA6D1+fqHssJscY/Tm/y2Uqnapk=
|
||||
github.com/klauspost/compress v1.18.2/go.mod h1:R0h/fSBs8DE4ENlcrlib3PsXS61voFxhIs2DeRhCvJ4=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0 h1:S4CRMLnYUhGeDFDqkGriYKdfoFlDnMtqTiI/sFzhA9Y=
|
||||
github.com/klauspost/cpuid/v2 v2.3.0/go.mod h1:hqwkgyIinND0mEev00jJYCxPNVRVXFQeu1XKlok6oO0=
|
||||
github.com/kr/pretty v0.3.1 h1:flRD4NNwYAUpkphVc1HcthR4KEIFJ65n8Mw5qdRn3LE=
|
||||
|
|
@ -127,8 +126,8 @@ 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-20250827001030-24949be3fa54 h1:mFWunSatvkQQDhpdyuFAYwyAan3hzCuma+Pz8sqvOfg=
|
||||
github.com/lufia/plan9stats v0.0.0-20250827001030-24949be3fa54/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3 h1:PwQumkgq4/acIiZhtifTV5OUqqiP82UAl0h87xj/l9k=
|
||||
github.com/lufia/plan9stats v0.0.0-20251013123823-9fd1530e3ec3/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/miekg/dns v1.1.68 h1:jsSRkNozw7G/mnmXULynzMNIsgY2dHC8LO6U6Ij2JEA=
|
||||
|
|
@ -138,8 +137,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.3.0 h1:y2bDDCioLgkcs+5luUaPgTNHKel1Qh30iUxFcMUrowg=
|
||||
github.com/mymmrac/telego v1.3.0/go.mod h1:0D2l/IA/gUFn4oqsi1O4/tSnlezw5jNV/ReFRDUEKk8=
|
||||
github.com/mymmrac/telego v1.3.1 h1:dI5D8LKWBw241W02LmJqoSLZXW3tuLokxVoNbIZUYQg=
|
||||
github.com/mymmrac/telego v1.3.1/go.mod h1:3D0h4jJ3OzubY/gI4xDIGx4jkY26fmcPyqx0Lq24+zI=
|
||||
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=
|
||||
|
|
@ -154,12 +153,12 @@ github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZb
|
|||
github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt9k/+g42oCprj/FisM4qX9L3sZB3upGN2ZU=
|
||||
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
|
||||
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
|
||||
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
|
||||
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg=
|
||||
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY=
|
||||
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
|
||||
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8=
|
||||
github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII=
|
||||
github.com/quic-go/quic-go v0.57.1 h1:25KAAR9QR8KZrCZRThWMKVAwGoiHIrNbT72ULHTuI10=
|
||||
github.com/quic-go/quic-go v0.57.1/go.mod h1:ly4QBAjHA2VhdnxhojRsCUOeJwKYg+taDlos92xb1+s=
|
||||
github.com/refraction-networking/utls v1.8.1 h1:yNY1kapmQU8JeM1sSw2H2asfTIwWxIkrMJI0pRUOCAo=
|
||||
github.com/refraction-networking/utls v1.8.1/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20200410134404-eec4a21b6bb0/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec h1:W09IVJc94icq4NjY3clb7Lk8O1qJ8BdBEF8z0ibU0rE=
|
||||
github.com/remyoudompheng/bigfft v0.0.0-20230129092748-24d4a6f8daec/go.mod h1:qqbHyh8v60DhA7CoWK5oRCqLrMHRGoxYCSS9EjAz6Eo=
|
||||
|
|
@ -169,40 +168,42 @@ 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.10 h1:2yPhZFx+EkyHPH8hXNezgyRSHyGY12CboId7CtwLROw=
|
||||
github.com/sagernet/sing v0.7.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing v0.7.13 h1:XNYgd8e3cxMULs/LLJspdn/deHrnPWyrrglNHeCUAYM=
|
||||
github.com/sagernet/sing v0.7.13/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.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
||||
github.com/shirou/gopsutil/v4 v4.25.11 h1:X53gB7muL9Gnwwo2evPSE+SfOrltMoR6V3xJAXZILTY=
|
||||
github.com/shirou/gopsutil/v4 v4.25.11/go.mod h1:EivAfP5x2EhLp2ovdpKSozecVXn1TmuG7SMzs/Wh4PU=
|
||||
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=
|
||||
github.com/stretchr/objx v0.5.2/go.mod h1:FRsXN1f5AsAjCGJKqEizvkpNtU+EGNCLh3NxZ/8L+MA=
|
||||
github.com/stretchr/testify v1.3.0/go.mod h1:M5WIy9Dh21IEIfnGCwXGc5bZfKNJtfHm1UVUgZn+9EI=
|
||||
github.com/stretchr/testify v1.6.1/go.mod h1:6Fq8oRcR53rry900zMqJjRRixrwX3KX962/h/Wwjteg=
|
||||
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.8.4/go.mod h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
|
||||
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=
|
||||
github.com/tklauser/numcpus v0.10.0/go.mod h1:BiTKazU708GQTYF4mB+cmlpT2Is1gLk7XVuEeem8LsQ=
|
||||
github.com/tklauser/go-sysconf v0.3.16 h1:frioLaCQSsF5Cy1jgRBrzr6t502KIIwQ0MArYICU0nA=
|
||||
github.com/tklauser/go-sysconf v0.3.16/go.mod h1:/qNL9xxDhc7tx3HSRsLWNnuzbVfh3e7gh/BmM179nYI=
|
||||
github.com/tklauser/numcpus v0.11.0 h1:nSTwhKH5e1dMNsCdVBukSZrURJRoHbSEQjdEbY+9RXw=
|
||||
github.com/tklauser/numcpus v0.11.0/go.mod h1:z+LwcLq54uWZTX0u/bGobaV34u6V7KNlTZejzM6/3MQ=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1 h1:SU5vSMR7hnwNxj24w34ZyCi/FmDZTkS4MhqMhdFk5YI=
|
||||
github.com/twitchyliquid64/golang-asm v0.15.1/go.mod h1:a1lVb/DtPvCB8fslRZhAngC2+aY1QWCk3Cedj/Gdt08=
|
||||
github.com/ugorji/go/codec v1.3.0 h1:Qd2W2sQawAfG8XSvzwhBeoGq71zXOC/Q1E9y/wUcsUA=
|
||||
github.com/ugorji/go/codec v1.3.0/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/ugorji/go/codec v1.3.1 h1:waO7eEiFDwidsBN6agj1vJQ4AG7lh2yqXyOXqhgQuyY=
|
||||
github.com/ugorji/go/codec v1.3.1/go.mod h1:pRBVtBSKl77K30Bv8R2P+cLSGaTtex6fsA2Wjqmfxj4=
|
||||
github.com/v2fly/ss-bloomring v0.0.0-20210312155135-28617310f63e h1:5QefA066A1tF8gHIiADmOVOV5LS43gt3ONnlEl3xkwI=
|
||||
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.66.0 h1:M87A0Z7EayeyNaV6pfO3tUTUiYO0dZfEJnRGXTVNuyU=
|
||||
github.com/valyala/fasthttp v1.66.0/go.mod h1:Y4eC+zwoocmXSVCB1JmhNbYtS7tZPRI2ztPB72EVObs=
|
||||
github.com/valyala/fasthttp v1.68.0 h1:v12Nx16iepr8r9ySOwqI+5RBJ/DqTxhOy1HrHoDFnok=
|
||||
github.com/valyala/fasthttp v1.68.0/go.mod h1:5EXiRfYQAoiO/khu4oU9VISC/eVY6JqmSpPJoHCKsz4=
|
||||
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=
|
||||
|
|
@ -211,68 +212,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-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/xtls/reality v0.0.0-20251116175510-cd53f7d50237 h1:UXjrmniKlY+ZbIqpN91lejB3pszQQQRVu1vqH/p/aGM=
|
||||
github.com/xtls/reality v0.0.0-20251116175510-cd53f7d50237/go.mod h1:vbHCV/3VWUvy1oKvTxxWJRPEWSeR1sYgQHIh6u/JiZQ=
|
||||
github.com/xtls/xray-core v1.251202.0 h1:VwoBnq9IRTbYWEBhR0CqEw2cNjTlXYH6WxzKbSjx+XE=
|
||||
github.com/xtls/xray-core v1.251202.0/go.mod h1:kclzboEF0g6VBrp9/NXm8C0Aj64SDBt52OfthH1LSr4=
|
||||
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.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.opentelemetry.io/auto/sdk v1.2.1 h1:jXsnJ4Lmnqd11kwkBV2LgLoFMZKizbCi5fNZ/ipaZ64=
|
||||
go.opentelemetry.io/auto/sdk v1.2.1/go.mod h1:KRTj+aOaElaLi+wW1kO/DZRXwkF4C5xPbEe3ZiIhN7Y=
|
||||
go.opentelemetry.io/otel v1.38.0 h1:RkfdswUDRimDg0m2Az18RKOsnI8UDzppJAtj01/Ymk8=
|
||||
go.opentelemetry.io/otel v1.38.0/go.mod h1:zcmtmQ1+YmQM9wrNsTGV/q/uyusom3P8RxwExxkZhjM=
|
||||
go.opentelemetry.io/otel/metric v1.38.0 h1:Kl6lzIYGAh5M159u9NgiRkmoMKjvbsKtYRwgfrA6WpA=
|
||||
go.opentelemetry.io/otel/metric v1.38.0/go.mod h1:kB5n/QoRM8YwmUahxvI3bO34eVtQf2i4utNVLr9gEmI=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0 h1:l48sr5YbNf2hpCUj/FoGhW9yDkl+Ma+LrVl8qaM5b+E=
|
||||
go.opentelemetry.io/otel/sdk v1.38.0/go.mod h1:ghmNdGlVemJI3+ZB5iDEuk4bWA3GkTpW+DOoZMYBVVg=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0 h1:aSH66iL0aZqo//xXzQLYozmWrXxyFkBJ6qT5wthqPoM=
|
||||
go.opentelemetry.io/otel/sdk/metric v1.38.0/go.mod h1:dg9PBnW9XdQ1Hd6ZnRz689CbtrUp0wMMs9iPcgT9EZA=
|
||||
go.opentelemetry.io/otel/trace v1.38.0 h1:Fxk5bKrDZJUH+AMyyIXGcFAPah0oRcT+LuNtJrmcNLE=
|
||||
go.opentelemetry.io/otel/trace v1.38.0/go.mod h1:j1P9ivuFsTceSWe1oY+EeW3sc+Pp42sO++GHkg4wwhs=
|
||||
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.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.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/arch v0.23.0 h1:lKF64A2jF6Zd8L0knGltUnegD62JMFBiCPBmQpToHhg=
|
||||
golang.org/x/arch v0.23.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.45.0 h1:jMBrvKuj23MTlT0bQEOBcAE0mjg8mK9RXFhRH6nyF3Q=
|
||||
golang.org/x/crypto v0.45.0/go.mod h1:XTGrrkGJve7CYK7J8PEww4aY7gM3qMCElcJQ8n8JdX4=
|
||||
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
|
||||
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
|
||||
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
|
||||
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
|
||||
golang.org/x/sync v0.18.0 h1:kr88TuHDroi+UVf+0hZnirlk8o8T+4MrK6mr60WkH/I=
|
||||
golang.org/x/sync v0.18.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.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.37.0 h1:DVSRzp7FwePZW356yEAChSdNcQo6Nsp+fex1SUW09lE=
|
||||
golang.org/x/tools v0.37.0/go.mod h1:MBN5QPQtLMHVdvsbtarmTNukZDdgwdwlO5qGacAzF0w=
|
||||
golang.org/x/sys v0.38.0 h1:3yZWxaJjBmCWXqhN1qh02AkOnCQ1poK6oF+a7xWL6Gc=
|
||||
golang.org/x/sys v0.38.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.31.0 h1:aC8ghyu4JhP8VojJ2lEHBnochRno1sgL6nEi9WGFGMM=
|
||||
golang.org/x/text v0.31.0/go.mod h1:tKRAlv61yKIjGGHX/4tP1LTbc13YSec1pxVEWXzfoeM=
|
||||
golang.org/x/time v0.14.0 h1:MRx4UaLrDotUKUdCIqzPC48t1Y9hANFKIRpNx+Te8PI=
|
||||
golang.org/x/time v0.14.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
|
||||
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
|
||||
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=
|
||||
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-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ=
|
||||
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=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217 h1:gRkg/vSppuSQoDjxyiGfN4Upv/h/DQmIR10ZU8dh4Ww=
|
||||
google.golang.org/genproto/googleapis/rpc v0.0.0-20251202230838-ff82c1b0f217/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
|
||||
google.golang.org/grpc v1.77.0 h1:wVVY6/8cGA6vvffn+wWK5ToddbgdU3d8MNENr4evgXM=
|
||||
google.golang.org/grpc v1.77.0/go.mod h1:z0BY1iVj0q8E1uSQCjL9cppRj+gnZjzDnzV0dHhrNig=
|
||||
google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
|
||||
google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
|
||||
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=
|
||||
|
|
@ -282,8 +283,8 @@ gopkg.in/yaml.v3 v3.0.0-20200313102051-9f266ea9e77c/go.mod h1:K4uyk7z7BCEPqu6E+C
|
|||
gopkg.in/yaml.v3 v3.0.0-20200605160147-a5ece683394c/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
|
||||
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
|
||||
gorm.io/gorm v1.31.0 h1:0VlycGreVhK7RF/Bwt51Fk8v0xLiiiFdbGDPIZQ7mJY=
|
||||
gorm.io/gorm v1.31.0/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg=
|
||||
gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs=
|
||||
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=
|
||||
|
|
|
|||
90
install.sh
90
install.sh
|
|
@ -15,7 +15,7 @@ cur_dir=$(pwd)
|
|||
if [[ -f /etc/os-release ]]; then
|
||||
source /etc/os-release
|
||||
release=$ID
|
||||
elif [[ -f /usr/lib/os-release ]]; then
|
||||
elif [[ -f /usr/lib/os-release ]]; then
|
||||
source /usr/lib/os-release
|
||||
release=$ID
|
||||
else
|
||||
|
|
@ -26,14 +26,14 @@ echo "The OS release is: $release"
|
|||
|
||||
arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64 | x64 | amd64) echo 'amd64' ;;
|
||||
i*86 | x86) echo '386' ;;
|
||||
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
|
||||
armv7* | armv7 | arm) echo 'armv7' ;;
|
||||
armv6* | armv6) echo 'armv6' ;;
|
||||
armv5* | armv5) echo 'armv5' ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
|
||||
x86_64 | x64 | amd64) echo 'amd64' ;;
|
||||
i*86 | x86) echo '386' ;;
|
||||
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
|
||||
armv7* | armv7 | arm) echo 'armv7' ;;
|
||||
armv6* | armv6) echo 'armv6' ;;
|
||||
armv5* | armv5) echo 'armv5' ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) echo -e "${green}Unsupported CPU architecture! ${plain}" && rm -f install.sh && exit 1 ;;
|
||||
esac
|
||||
}
|
||||
|
||||
|
|
@ -41,26 +41,30 @@ echo "Arch: $(arch)"
|
|||
|
||||
install_base() {
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update && apt-get install -y -q wget curl tar tzdata
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update && apt-get install -y -q wget curl tar tzdata
|
||||
;;
|
||||
centos | rhel | almalinux | rocky | ol)
|
||||
yum -y update && yum install -y -q wget curl tar tzdata
|
||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||
dnf -y update && dnf install -y -q wget curl tar tzdata
|
||||
;;
|
||||
fedora | amzn | virtuozzo)
|
||||
dnf -y update && dnf install -y -q wget curl tar tzdata
|
||||
centos)
|
||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||
yum -y update && yum install -y wget curl tar tzdata
|
||||
else
|
||||
dnf -y update && dnf install -y -q wget curl tar tzdata
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu && pacman -Syu --noconfirm wget curl tar tzdata
|
||||
;;
|
||||
opensuse-tumbleweed | opensuse-leap)
|
||||
zypper refresh && zypper -q install -y wget curl tar timezone
|
||||
opensuse-tumbleweed | opensuse-leap)
|
||||
zypper refresh && zypper -q install -y wget curl tar timezone
|
||||
;;
|
||||
alpine)
|
||||
apk update && apk add wget curl tar tzdata
|
||||
alpine)
|
||||
apk update && apk add wget curl tar tzdata
|
||||
;;
|
||||
*)
|
||||
apt-get update && apt-get install -y -q wget curl tar tzdata
|
||||
*)
|
||||
apt-get update && apt-get install -y -q wget curl tar tzdata
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
|
@ -77,11 +81,11 @@ config_after_install() {
|
|||
local existing_port=$(/usr/local/x-ui/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
|
||||
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"
|
||||
"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
|
||||
|
|
@ -90,13 +94,13 @@ config_after_install() {
|
|||
break
|
||||
fi
|
||||
done
|
||||
|
||||
|
||||
if [[ ${#existing_webBasePath} -lt 4 ]]; then
|
||||
if [[ "$existing_hasDefaultCredential" == "true" ]]; then
|
||||
local config_webBasePath=$(gen_random_string 18)
|
||||
local config_username=$(gen_random_string 10)
|
||||
local config_password=$(gen_random_string 10)
|
||||
|
||||
|
||||
read -rp "Would you like to customize the Panel Port settings? (If not, a random port will be applied) [y/n]: " config_confirm
|
||||
if [[ "${config_confirm}" == "y" || "${config_confirm}" == "Y" ]]; then
|
||||
read -rp "Please set up the panel port: " config_port
|
||||
|
|
@ -105,7 +109,7 @@ config_after_install() {
|
|||
local config_port=$(shuf -i 1024-62000 -n 1)
|
||||
echo -e "${yellow}Generated random port: ${config_port}${plain}"
|
||||
fi
|
||||
|
||||
|
||||
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
|
||||
echo -e "This is a fresh installation, generating random login info for security concerns:"
|
||||
echo -e "###############################################"
|
||||
|
|
@ -126,7 +130,7 @@ config_after_install() {
|
|||
if [[ "$existing_hasDefaultCredential" == "true" ]]; then
|
||||
local config_username=$(gen_random_string 10)
|
||||
local config_password=$(gen_random_string 10)
|
||||
|
||||
|
||||
echo -e "${yellow}Default credentials detected. Security update required...${plain}"
|
||||
/usr/local/x-ui/x-ui setting -username "${config_username}" -password "${config_password}"
|
||||
echo -e "Generated new random login credentials:"
|
||||
|
|
@ -138,13 +142,13 @@ config_after_install() {
|
|||
echo -e "${green}Username, Password, and WebBasePath are properly set. Exiting...${plain}"
|
||||
fi
|
||||
fi
|
||||
|
||||
|
||||
/usr/local/x-ui/x-ui migrate
|
||||
}
|
||||
|
||||
install_x-ui() {
|
||||
cd /usr/local/
|
||||
|
||||
|
||||
# Download resources
|
||||
if [ $# == 0 ]; then
|
||||
tag_version=$(curl -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
|
|
@ -166,12 +170,12 @@ install_x-ui() {
|
|||
tag_version=$1
|
||||
tag_version_numeric=${tag_version#v}
|
||||
min_version="2.3.5"
|
||||
|
||||
|
||||
if [[ "$(printf '%s\n' "$min_version" "$tag_version_numeric" | sort -V | head -n1)" != "$min_version" ]]; then
|
||||
echo -e "${red}Please use a newer version (at least v2.3.5). Exiting installation.${plain}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
url="https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz"
|
||||
echo -e "Beginning to install x-ui $1"
|
||||
wget --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz ${url}
|
||||
|
|
@ -185,7 +189,7 @@ install_x-ui() {
|
|||
echo -e "${red}Failed to download x-ui.sh${plain}"
|
||||
exit 1
|
||||
fi
|
||||
|
||||
|
||||
# Stop x-ui service and remove old resources
|
||||
if [[ -e /usr/local/x-ui/ ]]; then
|
||||
if [[ $release == "alpine" ]]; then
|
||||
|
|
@ -195,7 +199,7 @@ install_x-ui() {
|
|||
fi
|
||||
rm /usr/local/x-ui/ -rf
|
||||
fi
|
||||
|
||||
|
||||
# Extract resources and set permissions
|
||||
tar zxvf x-ui-linux-$(arch).tar.gz
|
||||
rm x-ui-linux-$(arch).tar.gz -f
|
||||
|
|
@ -203,19 +207,19 @@ install_x-ui() {
|
|||
cd x-ui
|
||||
chmod +x x-ui
|
||||
chmod +x x-ui.sh
|
||||
|
||||
|
||||
# Check the system's architecture and rename the file accordingly
|
||||
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
|
||||
mv bin/xray-linux-$(arch) bin/xray-linux-arm
|
||||
chmod +x bin/xray-linux-arm
|
||||
fi
|
||||
chmod +x x-ui bin/xray-linux-$(arch)
|
||||
|
||||
|
||||
# Update x-ui cli and se set permission
|
||||
mv -f /usr/bin/x-ui-temp /usr/bin/x-ui
|
||||
chmod +x /usr/bin/x-ui
|
||||
config_after_install
|
||||
|
||||
|
||||
if [[ $release == "alpine" ]]; then
|
||||
wget --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
|
||||
if [[ $? -ne 0 ]]; then
|
||||
|
|
@ -231,7 +235,7 @@ install_x-ui() {
|
|||
systemctl enable x-ui
|
||||
systemctl start x-ui
|
||||
fi
|
||||
|
||||
|
||||
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
|
||||
echo -e ""
|
||||
echo -e "┌───────────────────────────────────────────────────────┐
|
||||
|
|
@ -248,7 +252,7 @@ install_x-ui() {
|
|||
│ ${blue}x-ui log${plain} - Check logs │
|
||||
│ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
|
||||
│ ${blue}x-ui update${plain} - Update │
|
||||
│ ${blue}x-ui legacy${plain} - legacy version │
|
||||
│ ${blue}x-ui legacy${plain} - Legacy version │
|
||||
│ ${blue}x-ui install${plain} - Install │
|
||||
│ ${blue}x-ui uninstall${plain} - Uninstall │
|
||||
└───────────────────────────────────────────────────────┘"
|
||||
|
|
|
|||
132
logger/logger.go
132
logger/logger.go
|
|
@ -1,21 +1,29 @@
|
|||
// Package logger provides logging functionality for the 3x-ui panel with
|
||||
// buffered log storage and multiple log levels.
|
||||
// dual-backend logging (console/syslog and file) and buffered log storage for web UI.
|
||||
package logger
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"time"
|
||||
|
||||
"github.com/mhsanaei/3x-ui/v2/config"
|
||||
"github.com/op/go-logging"
|
||||
)
|
||||
|
||||
var (
|
||||
logger *logging.Logger
|
||||
const (
|
||||
maxLogBufferSize = 10240 // Maximum log entries kept in memory
|
||||
logFileName = "3xui.log" // Log file name
|
||||
timeFormat = "2006/01/02 15:04:05" // Log timestamp format
|
||||
)
|
||||
|
||||
// addToBuffer appends a log entry into the in-memory ring buffer used for
|
||||
// retrieving recent logs via the web UI. It keeps the buffer bounded to avoid
|
||||
// uncontrolled growth.
|
||||
var (
|
||||
logger *logging.Logger
|
||||
logFile *os.File
|
||||
|
||||
// logBuffer maintains recent log entries in memory for web UI retrieval
|
||||
logBuffer []struct {
|
||||
time string
|
||||
level logging.Level
|
||||
|
|
@ -23,37 +31,100 @@ var (
|
|||
}
|
||||
)
|
||||
|
||||
func init() {
|
||||
InitLogger(logging.INFO)
|
||||
}
|
||||
|
||||
// InitLogger initializes the logger with the specified logging level.
|
||||
// InitLogger initializes dual logging backends: console/syslog and file.
|
||||
// Console logging uses the specified level, file logging always uses DEBUG level.
|
||||
func InitLogger(level logging.Level) {
|
||||
newLogger := logging.MustGetLogger("x-ui")
|
||||
var err error
|
||||
var backend logging.Backend
|
||||
var format logging.Formatter
|
||||
ppid := os.Getppid()
|
||||
backends := make([]logging.Backend, 0, 2)
|
||||
|
||||
backend, err = logging.NewSyslogBackend("")
|
||||
if err != nil {
|
||||
println(err)
|
||||
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||
}
|
||||
if ppid > 0 && err != nil {
|
||||
format = logging.MustStringFormatter(`%{time:2006/01/02 15:04:05} %{level} - %{message}`)
|
||||
} else {
|
||||
format = logging.MustStringFormatter(`%{level} - %{message}`)
|
||||
// Console/syslog backend with configurable level
|
||||
if consoleBackend := initDefaultBackend(); consoleBackend != nil {
|
||||
leveledBackend := logging.AddModuleLevel(consoleBackend)
|
||||
leveledBackend.SetLevel(level, "x-ui")
|
||||
backends = append(backends, leveledBackend)
|
||||
}
|
||||
|
||||
backendFormatter := logging.NewBackendFormatter(backend, format)
|
||||
backendLeveled := logging.AddModuleLevel(backendFormatter)
|
||||
backendLeveled.SetLevel(level, "x-ui")
|
||||
newLogger.SetBackend(backendLeveled)
|
||||
// File backend with DEBUG level for comprehensive logging
|
||||
if fileBackend := initFileBackend(); fileBackend != nil {
|
||||
leveledBackend := logging.AddModuleLevel(fileBackend)
|
||||
leveledBackend.SetLevel(logging.DEBUG, "x-ui")
|
||||
backends = append(backends, leveledBackend)
|
||||
}
|
||||
|
||||
multiBackend := logging.MultiLogger(backends...)
|
||||
newLogger.SetBackend(multiBackend)
|
||||
logger = newLogger
|
||||
}
|
||||
|
||||
// initDefaultBackend creates the console/syslog logging backend.
|
||||
// Windows: Uses stderr directly (no syslog support)
|
||||
// Unix-like: Attempts syslog, falls back to stderr
|
||||
func initDefaultBackend() logging.Backend {
|
||||
var backend logging.Backend
|
||||
includeTime := false
|
||||
|
||||
if runtime.GOOS == "windows" {
|
||||
// Windows: Use stderr directly (no syslog support)
|
||||
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||
includeTime = true
|
||||
} else {
|
||||
// Unix-like: Try syslog, fallback to stderr
|
||||
if syslogBackend, err := logging.NewSyslogBackend(""); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "syslog backend disabled: %v\n", err)
|
||||
backend = logging.NewLogBackend(os.Stderr, "", 0)
|
||||
includeTime = os.Getppid() > 0
|
||||
} else {
|
||||
backend = syslogBackend
|
||||
}
|
||||
}
|
||||
|
||||
return logging.NewBackendFormatter(backend, newFormatter(includeTime))
|
||||
}
|
||||
|
||||
// initFileBackend creates the file logging backend.
|
||||
// Creates log directory and truncates log file on startup for fresh logs.
|
||||
func initFileBackend() logging.Backend {
|
||||
logDir := config.GetLogFolder()
|
||||
if err := os.MkdirAll(logDir, 0o750); err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to create log folder %s: %v\n", logDir, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
logPath := filepath.Join(logDir, logFileName)
|
||||
file, err := os.OpenFile(logPath, os.O_CREATE|os.O_WRONLY|os.O_TRUNC, 0o660)
|
||||
if err != nil {
|
||||
fmt.Fprintf(os.Stderr, "failed to open log file %s: %v\n", logPath, err)
|
||||
return nil
|
||||
}
|
||||
|
||||
// Close previous log file if exists
|
||||
if logFile != nil {
|
||||
_ = logFile.Close()
|
||||
}
|
||||
logFile = file
|
||||
|
||||
backend := logging.NewLogBackend(file, "", 0)
|
||||
return logging.NewBackendFormatter(backend, newFormatter(true))
|
||||
}
|
||||
|
||||
// newFormatter creates a log formatter with optional timestamp.
|
||||
func newFormatter(withTime bool) logging.Formatter {
|
||||
format := `%{level} - %{message}`
|
||||
if withTime {
|
||||
format = `%{time:` + timeFormat + `} %{level} - %{message}`
|
||||
}
|
||||
return logging.MustStringFormatter(format)
|
||||
}
|
||||
|
||||
// CloseLogger closes the log file and cleans up resources.
|
||||
// Should be called during application shutdown.
|
||||
func CloseLogger() {
|
||||
if logFile != nil {
|
||||
_ = logFile.Close()
|
||||
logFile = nil
|
||||
}
|
||||
}
|
||||
|
||||
// Debug logs a debug message and adds it to the log buffer.
|
||||
func Debug(args ...any) {
|
||||
logger.Debug(args...)
|
||||
|
|
@ -114,9 +185,10 @@ func Errorf(format string, args ...any) {
|
|||
addToBuffer("ERROR", fmt.Sprintf(format, args...))
|
||||
}
|
||||
|
||||
// addToBuffer adds a log entry to the in-memory ring buffer for web UI retrieval.
|
||||
func addToBuffer(level string, newLog string) {
|
||||
t := time.Now()
|
||||
if len(logBuffer) >= 10240 {
|
||||
if len(logBuffer) >= maxLogBufferSize {
|
||||
logBuffer = logBuffer[1:]
|
||||
}
|
||||
|
||||
|
|
@ -126,7 +198,7 @@ func addToBuffer(level string, newLog string) {
|
|||
level logging.Level
|
||||
log string
|
||||
}{
|
||||
time: t.Format("2006/01/02 15:04:05"),
|
||||
time: t.Format(timeFormat),
|
||||
level: logLevel,
|
||||
log: newLog,
|
||||
})
|
||||
|
|
|
|||
22
main.go
22
main.go
|
|
@ -78,6 +78,10 @@ func runWebServer() {
|
|||
case syscall.SIGHUP:
|
||||
logger.Info("Received SIGHUP signal. Restarting servers...")
|
||||
|
||||
// --- FIX FOR TELEGRAM BOT CONFLICT (409): Stop bot before restart ---
|
||||
service.StopBot()
|
||||
// --
|
||||
|
||||
err := server.Stop()
|
||||
if err != nil {
|
||||
logger.Debug("Error stopping web server:", err)
|
||||
|
|
@ -106,6 +110,10 @@ func runWebServer() {
|
|||
log.Println("Sub server restarted successfully.")
|
||||
|
||||
default:
|
||||
// --- FIX FOR TELEGRAM BOT CONFLICT (409) on full shutdown ---
|
||||
service.StopBot()
|
||||
// ------------------------------------------------------------
|
||||
|
||||
server.Stop()
|
||||
subServer.Stop()
|
||||
log.Println("Shutting down servers.")
|
||||
|
|
@ -321,6 +329,20 @@ func updateCert(publicKey string, privateKey string) {
|
|||
} else {
|
||||
fmt.Println("set certificate private key success")
|
||||
}
|
||||
|
||||
err = settingService.SetSubCertFile(publicKey)
|
||||
if err != nil {
|
||||
fmt.Println("set certificate for subscription public key failed:", err)
|
||||
} else {
|
||||
fmt.Println("set certificate for subscription public key success")
|
||||
}
|
||||
|
||||
err = settingService.SetSubKeyFile(privateKey)
|
||||
if err != nil {
|
||||
fmt.Println("set certificate for subscription private key failed:", err)
|
||||
} else {
|
||||
fmt.Println("set certificate for subscription private key success")
|
||||
}
|
||||
} else {
|
||||
fmt.Println("both public and private key should be entered.")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -103,7 +103,7 @@ func (s *Server) initRouter() (*gin.Engine, error) {
|
|||
if basePath != "/" && !strings.HasSuffix(basePath, "/") {
|
||||
basePath += "/"
|
||||
}
|
||||
logger.Debug("sub: Setting base_path to:", basePath)
|
||||
// logger.Debug("sub: Setting base_path to:", basePath)
|
||||
engine.Use(func(c *gin.Context) {
|
||||
c.Set("base_path", basePath)
|
||||
})
|
||||
|
|
|
|||
376
update.sh
376
update.sh
|
|
@ -9,233 +9,237 @@ plain='\033[0m'
|
|||
# Don't edit this config
|
||||
b_source="${BASH_SOURCE[0]}"
|
||||
while [ -h "$b_source" ]; do
|
||||
b_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
|
||||
b_source="$(readlink "$b_source")"
|
||||
[[ $b_source != /* ]] && b_source="$b_dir/$b_source"
|
||||
b_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
|
||||
b_source="$(readlink "$b_source")"
|
||||
[[ $b_source != /* ]] && b_source="$b_dir/$b_source"
|
||||
done
|
||||
cur_dir="$(cd -P "$(dirname "$b_source")" >/dev/null 2>&1 && pwd || pwd -P)"
|
||||
script_name=$(basename "$0")
|
||||
|
||||
# Check command exist function
|
||||
_command_exists() {
|
||||
type "$1" &>/dev/null
|
||||
type "$1" &>/dev/null
|
||||
}
|
||||
|
||||
# Fail, log and exit script function
|
||||
_fail() {
|
||||
local msg=${1}
|
||||
echo -e "${red}${msg}${plain}"
|
||||
exit 2
|
||||
local msg=${1}
|
||||
echo -e "${red}${msg}${plain}"
|
||||
exit 2
|
||||
}
|
||||
|
||||
# check root
|
||||
[[ $EUID -ne 0 ]] && _fail "FATAL ERROR: Please run this script with root privilege."
|
||||
|
||||
if _command_exists wget; then
|
||||
wget_bin=$(which wget)
|
||||
wget_bin=$(which wget)
|
||||
else
|
||||
_fail "ERROR: Command 'wget' not found."
|
||||
_fail "ERROR: Command 'wget' not found."
|
||||
fi
|
||||
|
||||
if _command_exists curl; then
|
||||
curl_bin=$(which curl)
|
||||
curl_bin=$(which curl)
|
||||
else
|
||||
_fail "ERROR: Command 'curl' not found."
|
||||
_fail "ERROR: Command 'curl' not found."
|
||||
fi
|
||||
|
||||
# Check OS and set release variable
|
||||
if [[ -f /etc/os-release ]]; then
|
||||
source /etc/os-release
|
||||
release=$ID
|
||||
elif [[ -f /usr/lib/os-release ]]; then
|
||||
source /usr/lib/os-release
|
||||
release=$ID
|
||||
source /etc/os-release
|
||||
release=$ID
|
||||
elif [[ -f /usr/lib/os-release ]]; then
|
||||
source /usr/lib/os-release
|
||||
release=$ID
|
||||
else
|
||||
_fail "Failed to check the system OS, please contact the author!"
|
||||
_fail "Failed to check the system OS, please contact the author!"
|
||||
fi
|
||||
echo "The OS release is: $release"
|
||||
|
||||
arch() {
|
||||
case "$(uname -m)" in
|
||||
x86_64 | x64 | amd64) echo 'amd64' ;;
|
||||
i*86 | x86) echo '386' ;;
|
||||
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
|
||||
armv7* | armv7 | arm) echo 'armv7' ;;
|
||||
armv6* | armv6) echo 'armv6' ;;
|
||||
armv5* | armv5) echo 'armv5' ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" >/dev/null 2>&1 && exit 2;;
|
||||
esac
|
||||
case "$(uname -m)" in
|
||||
x86_64 | x64 | amd64) echo 'amd64' ;;
|
||||
i*86 | x86) echo '386' ;;
|
||||
armv8* | armv8 | arm64 | aarch64) echo 'arm64' ;;
|
||||
armv7* | armv7 | arm) echo 'armv7' ;;
|
||||
armv6* | armv6) echo 'armv6' ;;
|
||||
armv5* | armv5) echo 'armv5' ;;
|
||||
s390x) echo 's390x' ;;
|
||||
*) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" >/dev/null 2>&1 && exit 2;;
|
||||
esac
|
||||
}
|
||||
|
||||
echo "Arch: $(arch)"
|
||||
|
||||
install_base() {
|
||||
echo -e "${green}Updating and install dependency packages...${plain}"
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update >/dev/null 2>&1 && apt-get install -y -q wget curl tar tzdata >/dev/null 2>&1
|
||||
;;
|
||||
centos | rhel | almalinux | rocky | ol)
|
||||
yum -y update >/dev/null 2>&1 && yum install -y -q wget curl tar tzdata >/dev/null 2>&1
|
||||
;;
|
||||
fedora | amzn | virtuozzo)
|
||||
dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata >/dev/null 2>&1
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm wget curl tar tzdata >/dev/null 2>&1
|
||||
;;
|
||||
opensuse-tumbleweed | opensuse-leap)
|
||||
zypper refresh >/dev/null 2>&1 && zypper -q install -y wget curl tar timezone >/dev/null 2>&1
|
||||
;;
|
||||
alpine)
|
||||
apk update >/dev/null 2>&1 && apk add wget curl tar tzdata >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
apt-get update >/dev/null 2>&1 && apt install -y -q wget curl tar tzdata >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
echo -e "${green}Updating and install dependency packages...${plain}"
|
||||
case "${release}" in
|
||||
ubuntu | debian | armbian)
|
||||
apt-get update >/dev/null 2>&1 && apt-get install -y -q wget curl tar tzdata >/dev/null 2>&1
|
||||
;;
|
||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||
dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata >/dev/null 2>&1
|
||||
;;
|
||||
centos)
|
||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||
yum -y update >/dev/null 2>&1 && yum install -y -q wget curl tar tzdata >/dev/null 2>&1
|
||||
else
|
||||
dnf -y update >/dev/null 2>&1 && dnf install -y -q wget curl tar tzdata >/dev/null 2>&1
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm wget curl tar tzdata >/dev/null 2>&1
|
||||
;;
|
||||
opensuse-tumbleweed | opensuse-leap)
|
||||
zypper refresh >/dev/null 2>&1 && zypper -q install -y wget curl tar timezone >/dev/null 2>&1
|
||||
;;
|
||||
alpine)
|
||||
apk update >/dev/null 2>&1 && apk add wget curl tar tzdata >/dev/null 2>&1
|
||||
;;
|
||||
*)
|
||||
apt-get update >/dev/null 2>&1 && apt install -y -q wget curl tar tzdata >/dev/null 2>&1
|
||||
;;
|
||||
esac
|
||||
}
|
||||
|
||||
config_after_update() {
|
||||
echo -e "${yellow}x-ui settings:${plain}"
|
||||
/usr/local/x-ui/x-ui setting -show true
|
||||
/usr/local/x-ui/x-ui migrate
|
||||
echo -e "${yellow}x-ui settings:${plain}"
|
||||
/usr/local/x-ui/x-ui setting -show true
|
||||
/usr/local/x-ui/x-ui migrate
|
||||
}
|
||||
|
||||
update_x-ui() {
|
||||
cd /usr/local/
|
||||
|
||||
if [ -f "/usr/local/x-ui/x-ui" ]; then
|
||||
current_xui_version=$(/usr/local/x-ui/x-ui -v)
|
||||
echo -e "${green}Current x-ui version: ${current_xui_version}${plain}"
|
||||
else
|
||||
_fail "ERROR: Current x-ui version: unknown"
|
||||
fi
|
||||
|
||||
echo -e "${green}Downloading new x-ui version...${plain}"
|
||||
|
||||
tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2>/dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
|
||||
tag_version=$(${curl_bin} -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
_fail "ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later"
|
||||
fi
|
||||
fi
|
||||
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
|
||||
${wget_bin} -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
|
||||
${wget_bin} --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e /usr/local/x-ui/ ]]; then
|
||||
echo -e "${green}Stopping x-ui...${plain}"
|
||||
if [[ $release == "alpine" ]]; then
|
||||
if [ -f "/etc/init.d/x-ui" ]; then
|
||||
rc-service x-ui stop >/dev/null 2>&1
|
||||
rc-update del x-ui >/dev/null 2>&1
|
||||
echo -e "${green}Removing old service unit version...${plain}"
|
||||
rm -f /etc/init.d/x-ui >/dev/null 2>&1
|
||||
else
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
_fail "ERROR: x-ui service unit not installed."
|
||||
fi
|
||||
else
|
||||
if [ -f "/etc/systemd/system/x-ui.service" ]; then
|
||||
systemctl stop x-ui >/dev/null 2>&1
|
||||
systemctl disable x-ui >/dev/null 2>&1
|
||||
echo -e "${green}Removing old systemd unit version...${plain}"
|
||||
rm /etc/systemd/system/x-ui.service -f >/dev/null 2>&1
|
||||
systemctl daemon-reload >/dev/null 2>&1
|
||||
else
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
_fail "ERROR: x-ui systemd unit not installed."
|
||||
fi
|
||||
fi
|
||||
echo -e "${green}Removing old x-ui version...${plain}"
|
||||
rm /usr/bin/x-ui -f >/dev/null 2>&1
|
||||
rm /usr/local/x-ui/x-ui.service -f >/dev/null 2>&1
|
||||
rm /usr/local/x-ui/x-ui -f >/dev/null 2>&1
|
||||
rm /usr/local/x-ui/x-ui.sh -f >/dev/null 2>&1
|
||||
echo -e "${green}Removing old xray version...${plain}"
|
||||
rm /usr/local/x-ui/bin/xray-linux-amd64 -f >/dev/null 2>&1
|
||||
echo -e "${green}Removing old README and LICENSE file...${plain}"
|
||||
rm /usr/local/x-ui/bin/README.md -f >/dev/null 2>&1
|
||||
rm /usr/local/x-ui/bin/LICENSE -f >/dev/null 2>&1
|
||||
else
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
_fail "ERROR: x-ui not installed."
|
||||
fi
|
||||
|
||||
echo -e "${green}Installing new x-ui version...${plain}"
|
||||
tar zxvf x-ui-linux-$(arch).tar.gz >/dev/null 2>&1
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
cd x-ui >/dev/null 2>&1
|
||||
chmod +x x-ui >/dev/null 2>&1
|
||||
|
||||
# Check the system's architecture and rename the file accordingly
|
||||
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
|
||||
mv bin/xray-linux-$(arch) bin/xray-linux-arm >/dev/null 2>&1
|
||||
chmod +x bin/xray-linux-arm >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
chmod +x x-ui bin/xray-linux-$(arch) >/dev/null 2>&1
|
||||
|
||||
echo -e "${green}Downloading and installing x-ui.sh script...${plain}"
|
||||
${wget_bin} -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${yellow}Trying to fetch x-ui with IPv4...${plain}"
|
||||
${wget_bin} --inet4-only -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
|
||||
chmod +x /usr/local/x-ui/x-ui.sh >/dev/null 2>&1
|
||||
chmod +x /usr/bin/x-ui >/dev/null 2>&1
|
||||
|
||||
echo -e "${green}Changing owner...${plain}"
|
||||
chown -R root:root /usr/local/x-ui >/dev/null 2>&1
|
||||
|
||||
if [ -f "/usr/local/x-ui/bin/config.json" ]; then
|
||||
echo -e "${green}Changing on config file permissions...${plain}"
|
||||
chmod 640 /usr/local/x-ui/bin/config.json >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [[ $release == "alpine" ]]; then
|
||||
echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}"
|
||||
${wget_bin} -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
${wget_bin} --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
chmod +x /etc/init.d/x-ui >/dev/null 2>&1
|
||||
chown root:root /etc/init.d/x-ui >/dev/null 2>&1
|
||||
rc-update add x-ui >/dev/null 2>&1
|
||||
rc-service x-ui start >/dev/null 2>&1
|
||||
else
|
||||
echo -e "${green}Installing systemd unit...${plain}"
|
||||
cp -f x-ui.service /etc/systemd/system/ >/dev/null 2>&1
|
||||
chown root:root /etc/systemd/system/x-ui.service >/dev/null 2>&1
|
||||
systemctl daemon-reload >/dev/null 2>&1
|
||||
systemctl enable x-ui >/dev/null 2>&1
|
||||
systemctl start x-ui >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
config_after_update
|
||||
|
||||
echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..."
|
||||
echo -e ""
|
||||
echo -e "┌───────────────────────────────────────────────────────┐
|
||||
cd /usr/local/
|
||||
|
||||
if [ -f "/usr/local/x-ui/x-ui" ]; then
|
||||
current_xui_version=$(/usr/local/x-ui/x-ui -v)
|
||||
echo -e "${green}Current x-ui version: ${current_xui_version}${plain}"
|
||||
else
|
||||
_fail "ERROR: Current x-ui version: unknown"
|
||||
fi
|
||||
|
||||
echo -e "${green}Downloading new x-ui version...${plain}"
|
||||
|
||||
tag_version=$(${curl_bin} -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" 2>/dev/null | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
|
||||
tag_version=$(${curl_bin} -4 -Ls "https://api.github.com/repos/MHSanaei/3x-ui/releases/latest" | grep '"tag_name":' | sed -E 's/.*"([^"]+)".*/\1/')
|
||||
if [[ ! -n "$tag_version" ]]; then
|
||||
_fail "ERROR: Failed to fetch x-ui version, it may be due to GitHub API restrictions, please try it later"
|
||||
fi
|
||||
fi
|
||||
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
|
||||
${wget_bin} -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${yellow}Trying to fetch version with IPv4...${plain}"
|
||||
${wget_bin} --inet4-only -N -O /usr/local/x-ui-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download x-ui, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
|
||||
if [[ -e /usr/local/x-ui/ ]]; then
|
||||
echo -e "${green}Stopping x-ui...${plain}"
|
||||
if [[ $release == "alpine" ]]; then
|
||||
if [ -f "/etc/init.d/x-ui" ]; then
|
||||
rc-service x-ui stop >/dev/null 2>&1
|
||||
rc-update del x-ui >/dev/null 2>&1
|
||||
echo -e "${green}Removing old service unit version...${plain}"
|
||||
rm -f /etc/init.d/x-ui >/dev/null 2>&1
|
||||
else
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
_fail "ERROR: x-ui service unit not installed."
|
||||
fi
|
||||
else
|
||||
if [ -f "/etc/systemd/system/x-ui.service" ]; then
|
||||
systemctl stop x-ui >/dev/null 2>&1
|
||||
systemctl disable x-ui >/dev/null 2>&1
|
||||
echo -e "${green}Removing old systemd unit version...${plain}"
|
||||
rm /etc/systemd/system/x-ui.service -f >/dev/null 2>&1
|
||||
systemctl daemon-reload >/dev/null 2>&1
|
||||
else
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
_fail "ERROR: x-ui systemd unit not installed."
|
||||
fi
|
||||
fi
|
||||
echo -e "${green}Removing old x-ui version...${plain}"
|
||||
rm /usr/bin/x-ui -f >/dev/null 2>&1
|
||||
rm /usr/local/x-ui/x-ui.service -f >/dev/null 2>&1
|
||||
rm /usr/local/x-ui/x-ui -f >/dev/null 2>&1
|
||||
rm /usr/local/x-ui/x-ui.sh -f >/dev/null 2>&1
|
||||
echo -e "${green}Removing old xray version...${plain}"
|
||||
rm /usr/local/x-ui/bin/xray-linux-amd64 -f >/dev/null 2>&1
|
||||
echo -e "${green}Removing old README and LICENSE file...${plain}"
|
||||
rm /usr/local/x-ui/bin/README.md -f >/dev/null 2>&1
|
||||
rm /usr/local/x-ui/bin/LICENSE -f >/dev/null 2>&1
|
||||
else
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
_fail "ERROR: x-ui not installed."
|
||||
fi
|
||||
|
||||
echo -e "${green}Installing new x-ui version...${plain}"
|
||||
tar zxvf x-ui-linux-$(arch).tar.gz >/dev/null 2>&1
|
||||
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
|
||||
cd x-ui >/dev/null 2>&1
|
||||
chmod +x x-ui >/dev/null 2>&1
|
||||
|
||||
# Check the system's architecture and rename the file accordingly
|
||||
if [[ $(arch) == "armv5" || $(arch) == "armv6" || $(arch) == "armv7" ]]; then
|
||||
mv bin/xray-linux-$(arch) bin/xray-linux-arm >/dev/null 2>&1
|
||||
chmod +x bin/xray-linux-arm >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
chmod +x x-ui bin/xray-linux-$(arch) >/dev/null 2>&1
|
||||
|
||||
echo -e "${green}Downloading and installing x-ui.sh script...${plain}"
|
||||
${wget_bin} -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
echo -e "${yellow}Trying to fetch x-ui with IPv4...${plain}"
|
||||
${wget_bin} --inet4-only -O /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download x-ui.sh script, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
|
||||
chmod +x /usr/local/x-ui/x-ui.sh >/dev/null 2>&1
|
||||
chmod +x /usr/bin/x-ui >/dev/null 2>&1
|
||||
|
||||
echo -e "${green}Changing owner...${plain}"
|
||||
chown -R root:root /usr/local/x-ui >/dev/null 2>&1
|
||||
|
||||
if [ -f "/usr/local/x-ui/bin/config.json" ]; then
|
||||
echo -e "${green}Changing on config file permissions...${plain}"
|
||||
chmod 640 /usr/local/x-ui/bin/config.json >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
if [[ $release == "alpine" ]]; then
|
||||
echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}"
|
||||
${wget_bin} -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
${wget_bin} --inet4-only -O /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
|
||||
if [[ $? -ne 0 ]]; then
|
||||
_fail "ERROR: Failed to download startup unit x-ui.rc, please be sure that your server can access GitHub"
|
||||
fi
|
||||
fi
|
||||
chmod +x /etc/init.d/x-ui >/dev/null 2>&1
|
||||
chown root:root /etc/init.d/x-ui >/dev/null 2>&1
|
||||
rc-update add x-ui >/dev/null 2>&1
|
||||
rc-service x-ui start >/dev/null 2>&1
|
||||
else
|
||||
echo -e "${green}Installing systemd unit...${plain}"
|
||||
cp -f x-ui.service /etc/systemd/system/ >/dev/null 2>&1
|
||||
chown root:root /etc/systemd/system/x-ui.service >/dev/null 2>&1
|
||||
systemctl daemon-reload >/dev/null 2>&1
|
||||
systemctl enable x-ui >/dev/null 2>&1
|
||||
systemctl start x-ui >/dev/null 2>&1
|
||||
fi
|
||||
|
||||
config_after_update
|
||||
|
||||
echo -e "${green}x-ui ${tag_version}${plain} updating finished, it is running now..."
|
||||
echo -e ""
|
||||
echo -e "┌───────────────────────────────────────────────────────┐
|
||||
│ ${blue}x-ui control menu usages (subcommands):${plain} │
|
||||
│ │
|
||||
│ │
|
||||
│ ${blue}x-ui${plain} - Admin Management Script │
|
||||
│ ${blue}x-ui start${plain} - Start │
|
||||
│ ${blue}x-ui stop${plain} - Stop │
|
||||
|
|
@ -247,7 +251,7 @@ update_x-ui() {
|
|||
│ ${blue}x-ui log${plain} - Check logs │
|
||||
│ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
|
||||
│ ${blue}x-ui update${plain} - Update │
|
||||
│ ${blue}x-ui legacy${plain} - legacy version │
|
||||
│ ${blue}x-ui legacy${plain} - Legacy version │
|
||||
│ ${blue}x-ui install${plain} - Install │
|
||||
│ ${blue}x-ui uninstall${plain} - Uninstall │
|
||||
└───────────────────────────────────────────────────────┘"
|
||||
|
|
|
|||
|
|
@ -23,29 +23,22 @@ type Config struct {
|
|||
|
||||
// FetchVlessFlags returns map[email]enabled
|
||||
func FetchVlessFlags(cfg Config) (map[string]bool, error) {
|
||||
if cfg.Host == "" {
|
||||
return nil, fmt.Errorf("LDAP host is required")
|
||||
}
|
||||
if cfg.BaseDN == "" {
|
||||
return nil, fmt.Errorf("LDAP base DN is required")
|
||||
}
|
||||
|
||||
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
|
||||
var conn *ldap.Conn
|
||||
var err error
|
||||
if cfg.UseTLS {
|
||||
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: true})
|
||||
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
|
||||
} else {
|
||||
conn, err = ldap.Dial("tcp", addr)
|
||||
}
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to connect to LDAP server %s: %w", addr, err)
|
||||
return nil, err
|
||||
}
|
||||
defer conn.Close()
|
||||
|
||||
if cfg.BindDN != "" {
|
||||
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
|
||||
return nil, fmt.Errorf("failed to bind with DN %s: %w", cfg.BindDN, err)
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -70,9 +63,8 @@ func FetchVlessFlags(cfg Config) (map[string]bool, error) {
|
|||
|
||||
res, err := conn.Search(req)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("LDAP search failed: %w", err)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
result := make(map[string]bool, len(res.Entries))
|
||||
for _, e := range res.Entries {
|
||||
user := e.GetAttributeValue(cfg.UserAttr)
|
||||
|
|
@ -97,9 +89,6 @@ func FetchVlessFlags(cfg Config) (map[string]bool, error) {
|
|||
|
||||
// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password.
|
||||
func AuthenticateUser(cfg Config, username, password string) (bool, error) {
|
||||
if cfg.Host == "" {
|
||||
return false, fmt.Errorf("LDAP host is required")
|
||||
}
|
||||
if cfg.BaseDN == "" {
|
||||
return false, fmt.Errorf("LDAP base DN is required")
|
||||
}
|
||||
|
|
@ -114,7 +103,7 @@ func AuthenticateUser(cfg Config, username, password string) (bool, error) {
|
|||
var conn *ldap.Conn
|
||||
var err error
|
||||
if cfg.UseTLS {
|
||||
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: true})
|
||||
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
|
||||
} else {
|
||||
conn, err = ldap.Dial("tcp", addr)
|
||||
}
|
||||
|
|
@ -130,6 +119,9 @@ func AuthenticateUser(cfg Config, username, password string) (bool, error) {
|
|||
}
|
||||
}
|
||||
|
||||
if cfg.UserFilter == "" {
|
||||
cfg.UserFilter = "(objectClass=person)"
|
||||
}
|
||||
if cfg.UserFilter == "" {
|
||||
cfg.UserFilter = "(objectClass=person)"
|
||||
}
|
||||
|
|
@ -148,14 +140,11 @@ func AuthenticateUser(cfg Config, username, password string) (bool, error) {
|
|||
)
|
||||
res, err := conn.Search(req)
|
||||
if err != nil {
|
||||
return false, fmt.Errorf("LDAP search failed for user %s: %w", username, err)
|
||||
return false, err
|
||||
}
|
||||
if len(res.Entries) == 0 {
|
||||
return false, nil
|
||||
}
|
||||
if len(res.Entries) > 1 {
|
||||
return false, fmt.Errorf("multiple entries found for user %s", username)
|
||||
}
|
||||
userDN := res.Entries[0].DN
|
||||
// Try to bind as the user
|
||||
if err := conn.Bind(userDN, password); err != nil {
|
||||
|
|
|
|||
|
|
@ -729,8 +729,8 @@ class RealityStreamSettings extends XrayCommonClass {
|
|||
constructor(
|
||||
show = false,
|
||||
xver = 0,
|
||||
target = 'google.com:443',
|
||||
serverNames = 'google.com,www.google.com',
|
||||
target = '',
|
||||
serverNames = '',
|
||||
privateKey = '',
|
||||
minClientVer = '',
|
||||
maxClientVer = '',
|
||||
|
|
@ -740,6 +740,14 @@ class RealityStreamSettings extends XrayCommonClass {
|
|||
settings = new RealityStreamSettings.Settings()
|
||||
) {
|
||||
super();
|
||||
// If target/serverNames are not provided, use random values
|
||||
if (!target && !serverNames) {
|
||||
const randomTarget = typeof getRandomRealityTarget !== 'undefined'
|
||||
? getRandomRealityTarget()
|
||||
: { target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' };
|
||||
target = randomTarget.target;
|
||||
serverNames = randomTarget.sni;
|
||||
}
|
||||
this.show = show;
|
||||
this.xver = xver;
|
||||
this.target = target;
|
||||
|
|
|
|||
31
web/assets/js/model/reality_targets.js
Normal file
31
web/assets/js/model/reality_targets.js
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
// List of popular services for VLESS Reality Target/SNI randomization
|
||||
const REALITY_TARGETS = [
|
||||
{ target: 'www.icloud.com:443', sni: 'www.icloud.com,icloud.com' },
|
||||
{ target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' },
|
||||
{ target: 'www.tesla.com:443', sni: 'www.tesla.com,tesla.com' },
|
||||
{ target: 'www.sony.com:443', sni: 'www.sony.com,sony.com' },
|
||||
{ target: 'www.nvidia.com:443', sni: 'www.nvidia.com,nvidia.com' },
|
||||
{ target: 'www.amd.com:443', sni: 'www.amd.com,amd.com' },
|
||||
{ target: 'azure.microsoft.com:443', sni: 'azure.microsoft.com,www.azure.com' },
|
||||
{ target: 'aws.amazon.com:443', sni: 'aws.amazon.com,amazon.com' },
|
||||
{ target: 'www.bing.com:443', sni: 'www.bing.com,bing.com' },
|
||||
{ target: 'www.oracle.com:443', sni: 'www.oracle.com,oracle.com' },
|
||||
{ target: 'www.intel.com:443', sni: 'www.intel.com,intel.com' },
|
||||
{ target: 'www.microsoft.com:443', sni: 'www.microsoft.com,microsoft.com' },
|
||||
{ target: 'www.amazon.com:443', sni: 'www.amazon.com,amazon.com' }
|
||||
];
|
||||
|
||||
/**
|
||||
* Returns a random Reality target configuration from the predefined list
|
||||
* @returns {Object} Object with target and sni properties
|
||||
*/
|
||||
function getRandomRealityTarget() {
|
||||
const randomIndex = Math.floor(Math.random() * REALITY_TARGETS.length);
|
||||
const selected = REALITY_TARGETS[randomIndex];
|
||||
// Return a copy to avoid reference issues
|
||||
return {
|
||||
target: selected.target,
|
||||
sni: selected.sni
|
||||
};
|
||||
}
|
||||
|
||||
|
|
@ -1,151 +0,0 @@
|
|||
const oneMinute = 1000 * 60; // MilliseConds in a Minute
|
||||
const oneHour = oneMinute * 60; // The milliseconds of one hour
|
||||
const oneDay = oneHour * 24; // The Number of MilliseConds A Day
|
||||
const oneWeek = oneDay * 7; // The milliseconds per week
|
||||
const oneMonth = oneDay * 30; // The milliseconds of a month
|
||||
|
||||
/**
|
||||
* Decrease according to the number of days
|
||||
*
|
||||
* @param days to reduce the number of days to be reduced
|
||||
*/
|
||||
Date.prototype.minusDays = function (days) {
|
||||
return this.minusMillis(oneDay * days);
|
||||
};
|
||||
|
||||
/**
|
||||
* Increase according to the number of days
|
||||
*
|
||||
* @param days The number of days to be increased
|
||||
*/
|
||||
Date.prototype.plusDays = function (days) {
|
||||
return this.plusMillis(oneDay * days);
|
||||
};
|
||||
|
||||
/**
|
||||
* A few
|
||||
*
|
||||
* @param hours to be reduced
|
||||
*/
|
||||
Date.prototype.minusHours = function (hours) {
|
||||
return this.minusMillis(oneHour * hours);
|
||||
};
|
||||
|
||||
/**
|
||||
* Increase hourly
|
||||
*
|
||||
* @param hours to increase the number of hours
|
||||
*/
|
||||
Date.prototype.plusHours = function (hours) {
|
||||
return this.plusMillis(oneHour * hours);
|
||||
};
|
||||
|
||||
/**
|
||||
* Make reduction in minutes
|
||||
*
|
||||
* @param minutes to reduce the number of minutes
|
||||
*/
|
||||
Date.prototype.minusMinutes = function (minutes) {
|
||||
return this.minusMillis(oneMinute * minutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Add in minutes
|
||||
*
|
||||
* @param minutes to increase the number of minutes
|
||||
*/
|
||||
Date.prototype.plusMinutes = function (minutes) {
|
||||
return this.plusMillis(oneMinute * minutes);
|
||||
};
|
||||
|
||||
/**
|
||||
* Decrease in milliseconds
|
||||
*
|
||||
* @param millis to reduce the milliseconds
|
||||
*/
|
||||
Date.prototype.minusMillis = function(millis) {
|
||||
let time = this.getTime() - millis;
|
||||
let newDate = new Date();
|
||||
newDate.setTime(time);
|
||||
return newDate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Add in milliseconds to increase
|
||||
*
|
||||
* @param millis to increase the milliseconds to increase
|
||||
*/
|
||||
Date.prototype.plusMillis = function(millis) {
|
||||
let time = this.getTime() + millis;
|
||||
let newDate = new Date();
|
||||
newDate.setTime(time);
|
||||
return newDate;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setting time is 00: 00: 00.000 on the day
|
||||
*/
|
||||
Date.prototype.setMinTime = function () {
|
||||
this.setHours(0);
|
||||
this.setMinutes(0);
|
||||
this.setSeconds(0);
|
||||
this.setMilliseconds(0);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Setting time is 23: 59: 59.999 on the same day
|
||||
*/
|
||||
Date.prototype.setMaxTime = function () {
|
||||
this.setHours(23);
|
||||
this.setMinutes(59);
|
||||
this.setSeconds(59);
|
||||
this.setMilliseconds(999);
|
||||
return this;
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatting date
|
||||
*/
|
||||
Date.prototype.formatDate = function () {
|
||||
return this.getFullYear() + "-" + NumberFormatter.addZero(this.getMonth() + 1) + "-" + NumberFormatter.addZero(this.getDate());
|
||||
};
|
||||
|
||||
/**
|
||||
* Format time
|
||||
*/
|
||||
Date.prototype.formatTime = function () {
|
||||
return NumberFormatter.addZero(this.getHours()) + ":" + NumberFormatter.addZero(this.getMinutes()) + ":" + NumberFormatter.addZero(this.getSeconds());
|
||||
};
|
||||
|
||||
/**
|
||||
* Formatting date plus time
|
||||
*
|
||||
* @param split Date and time separation symbols, default is a space
|
||||
*/
|
||||
Date.prototype.formatDateTime = function (split = ' ') {
|
||||
return this.formatDate() + split + this.formatTime();
|
||||
};
|
||||
|
||||
class DateUtil {
|
||||
// String to date object
|
||||
static parseDate(str) {
|
||||
return new Date(str.replace(/-/g, '/'));
|
||||
}
|
||||
|
||||
static formatMillis(millis) {
|
||||
return moment(millis).format('YYYY-M-D HH:mm:ss');
|
||||
}
|
||||
|
||||
static firstDayOfMonth() {
|
||||
const date = new Date();
|
||||
date.setDate(1);
|
||||
date.setMinTime();
|
||||
return date;
|
||||
}
|
||||
|
||||
static convertToJalalian(date) {
|
||||
return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null;
|
||||
}
|
||||
|
||||
}
|
||||
|
|
@ -882,4 +882,35 @@ class FileManager {
|
|||
|
||||
link.remove();
|
||||
}
|
||||
}
|
||||
|
||||
class IntlUtil {
|
||||
static formatDate(date) {
|
||||
const language = LanguageManager.getLanguage()
|
||||
|
||||
let intlOptions = {
|
||||
year: "numeric",
|
||||
month: "numeric",
|
||||
day: "numeric",
|
||||
hour: "numeric",
|
||||
minute: "numeric",
|
||||
second: "numeric"
|
||||
}
|
||||
|
||||
const intl = new Intl.DateTimeFormat(
|
||||
language,
|
||||
intlOptions
|
||||
)
|
||||
|
||||
return intl.format(new Date(date))
|
||||
}
|
||||
static formatRelativeTime(date) {
|
||||
const language = LanguageManager.getLanguage()
|
||||
const now = new Date()
|
||||
|
||||
const diff = Math.round((date - now) / (1000 * 60 * 60 * 24))
|
||||
const formatter = new Intl.RelativeTimeFormat(language, { numeric: 'auto' })
|
||||
|
||||
return formatter.format(diff, 'day');
|
||||
}
|
||||
}
|
||||
|
|
@ -44,7 +44,6 @@
|
|||
<script src="{{ .base_path }}assets/axios/axios.min.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/qs/qs.min.js"></script>
|
||||
<script src="{{ .base_path }}assets/js/axios-init.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/date-util.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/util/index.js?{{ .cur_ver }}"></script>
|
||||
<script>
|
||||
const basePath = '{{ .base_path }}';
|
||||
|
|
|
|||
|
|
@ -111,20 +111,12 @@
|
|||
<template v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
|
||||
<span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
|
||||
</template>
|
||||
<table>
|
||||
<tr class="tr-table-box">
|
||||
<td class="tr-table-rt"> [[ remainedDays(client.expiryTime) ]] </td>
|
||||
<td class="tr-table-rt"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>
|
||||
<td class="infinite-bar tr-table-bar">
|
||||
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
</td>
|
||||
|
|
@ -136,18 +128,10 @@
|
|||
<template v-else>
|
||||
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
|
||||
<span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
|
||||
</template>
|
||||
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
|
||||
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)" :style="{ border: 'none' }" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
|
|
@ -232,20 +216,12 @@
|
|||
</tr>
|
||||
<tr>
|
||||
<template v-if="client.expiryTime !=0 && client.reset >0">
|
||||
<td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ remainedDays(client.expiryTime) ]] </td>
|
||||
<td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </td>
|
||||
<td width="120px" class="infinite-bar">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
|
||||
<span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
|
||||
</template>
|
||||
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
|
||||
</a-popover>
|
||||
|
|
@ -256,18 +232,10 @@
|
|||
<td colspan="3" :style="{ textAlign: 'center' }">
|
||||
<a-popover v-if="client.expiryTime != 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content">
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}
|
||||
</span>
|
||||
<span v-else>
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(client._expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(client._expiryTime)) ]]
|
||||
</template>
|
||||
</span>
|
||||
<span v-if="client.expiryTime < 0">{{ i18n "pages.client.delayedStart" }}</span>
|
||||
<span v-else>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
|
||||
</template>
|
||||
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ remainedDays(client.expiryTime) ]] </a-tag>
|
||||
<a-tag :style="{ minWidth: '50px', border: 'none' }" :color="ColorUtils.userExpiryColor(app.expireDiff, client, themeSwitcher.isDarkTheme)"> [[ IntlUtil.formatRelativeTime(client.expiryTime) ]] </a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else :color="client.enable ? 'purple' : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
|
|
@ -289,12 +257,7 @@
|
|||
</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>
|
||||
[[ IntlUtil.formatDate(client.created_at) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
|
|
@ -302,12 +265,7 @@
|
|||
</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>
|
||||
[[ IntlUtil.formatDate(client.updated_at) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
-
|
||||
|
|
|
|||
|
|
@ -52,9 +52,7 @@
|
|||
<br v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
||||
<span v-if="dbInbound.lastTrafficResetTime && dbInbound.lastTrafficResetTime > 0">
|
||||
<strong>{{ i18n "pages.inbounds.lastReset" }}:</strong>
|
||||
<span v-if="datepicker == 'gregorian'">[[
|
||||
moment(dbInbound.lastTrafficResetTime).format('YYYY-MM-DD HH:mm:ss') ]]</span>
|
||||
<span v-else>[[ DateUtil.convertToJalalian(moment(dbInbound.lastTrafficResetTime)) ]]</span>
|
||||
<span>[[ IntlUtil.formatDate(dbInbound.lastTrafficResetTime) ]]</span>
|
||||
</span>
|
||||
</template>
|
||||
{{ i18n "pages.inbounds.periodicTrafficResetTitle" }}
|
||||
|
|
|
|||
|
|
@ -12,10 +12,26 @@
|
|||
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item label='Target'>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template> Target <a-icon @click="randomizeRealityTarget()"
|
||||
type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.stream.reality.target"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='SNI'>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
<template slot="title">
|
||||
<span>{{ i18n "reset" }}</span>
|
||||
</template> SNI <a-icon @click="randomizeRealityTarget()"
|
||||
type="sync"></a-icon>
|
||||
</a-tooltip>
|
||||
</template>
|
||||
<a-input v-model.trim="inbound.stream.reality.serverNames"></a-input>
|
||||
</a-form-item>
|
||||
<a-form-item label='Max Time Diff (ms)'>
|
||||
|
|
|
|||
|
|
@ -384,15 +384,12 @@
|
|||
</template>
|
||||
<template slot="expiryTime" slot-scope="text, dbInbound">
|
||||
<a-popover v-if="dbInbound.expiryTime > 0" :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content" v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||
</template>
|
||||
<template v-else slot="content">
|
||||
[[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
|
||||
<template slot="content">
|
||||
[[ IntlUtil.formatDate(dbInbound.expiryTime) ]]
|
||||
</template>
|
||||
<a-tag :style="{ minWidth: '50px' }"
|
||||
:color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, dbInbound._expiryTime)">
|
||||
[[ remainedDays(dbInbound._expiryTime) ]]
|
||||
[[ IntlUtil.formatRelativeTime(dbInbound.expiryTime) ]]
|
||||
</a-tag>
|
||||
</a-popover>
|
||||
<a-tag v-else color="purple" class="infinite-tag">
|
||||
|
|
@ -549,12 +546,7 @@
|
|||
<td>
|
||||
<a-tag :style="{ minWidth: '50px', textAlign: 'center' }"
|
||||
v-if="dbInbound.expiryTime > 0" :color="dbInbound.isExpiry? 'red': 'blue'">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(dbInbound.expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(dbInbound.expiryTime)) ]]
|
||||
</template>
|
||||
[[ IntlUtil.formatDate(dbInbound.expiryTime) ]]
|
||||
</a-tag>
|
||||
<a-tag v-else :style="{ textAlign: 'center' }" color="purple" class="infinite-tag">
|
||||
<svg height="10px" width="14px" viewBox="0 0 640 512" fill="currentColor">
|
||||
|
|
@ -602,6 +594,7 @@
|
|||
{{template "page/body_scripts" .}}
|
||||
<script src="{{ .base_path }}assets/qrcode/qrious2.min.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/uri/URI.min.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/reality_targets.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/inbound.js?{{ .cur_ver }}"></script>
|
||||
<script src="{{ .base_path }}assets/js/model/dbinbound.js?{{ .cur_ver }}"></script>
|
||||
{{template "component/aSidebar" .}}
|
||||
|
|
@ -1406,13 +1399,6 @@
|
|||
if (remainedSeconds >= resetSeconds) return 0;
|
||||
return 100 * (1 - (remainedSeconds / resetSeconds));
|
||||
},
|
||||
remainedDays(expTime) {
|
||||
if (expTime == 0) return null;
|
||||
if (expTime < 0) return TimeFormatter.formatSecond(expTime / -1000);
|
||||
now = new Date().getTime();
|
||||
if (expTime < now) return '{{ i18n "depleted" }}';
|
||||
return TimeFormatter.formatSecond((expTime - now) / 1000);
|
||||
},
|
||||
statsExpColor(dbInbound, email) {
|
||||
if (email.length == 0) return '#7a316f';
|
||||
clientStats = dbInbound.clientStats.find(stats => stats.email === email);
|
||||
|
|
@ -1457,10 +1443,7 @@
|
|||
formatLastOnline(email) {
|
||||
const ts = this.getLastOnline(email)
|
||||
if (!ts) return '-'
|
||||
if (this.datepicker === 'gregorian') {
|
||||
return DateUtil.formatMillis(ts)
|
||||
}
|
||||
return DateUtil.convertToJalalian(moment(ts))
|
||||
return IntlUtil.formatDate(ts)
|
||||
},
|
||||
isRemovable(dbInboundId) {
|
||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
|
||||
|
|
|
|||
|
|
@ -846,7 +846,7 @@
|
|||
|
||||
formattedLogs += `
|
||||
<tr ${outboundColor}>
|
||||
<td><b>${new Date(log.DateTime).toLocaleString()}</b></td>
|
||||
<td><b>${IntlUtil.formatDate(log.DateTime)}</b></td>
|
||||
<td>${log.FromAddress}</td>
|
||||
<td>${log.ToAddress}</td>
|
||||
<td>${log.Inbound}</td>
|
||||
|
|
|
|||
|
|
@ -199,12 +199,7 @@
|
|||
<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>
|
||||
<a-tag>[[ IntlUtil.formatDate(infoModal.clientSettings.created_at) ]]</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>-</a-tag>
|
||||
|
|
@ -215,12 +210,7 @@
|
|||
<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>
|
||||
<a-tag>[[ IntlUtil.formatDate(infoModal.clientSettings.updated_at) ]]</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>-</a-tag>
|
||||
|
|
@ -282,12 +272,7 @@
|
|||
<td>
|
||||
<template v-if="infoModal.clientSettings.expiryTime > 0">
|
||||
<a-tag :color="ColorUtils.usageColor(new Date().getTime(), app.expireDiff, infoModal.clientSettings.expiryTime)">
|
||||
<template v-if="app.datepicker === 'gregorian'">
|
||||
[[ DateUtil.formatMillis(infoModal.clientSettings.expiryTime) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
[[ DateUtil.convertToJalalian(moment(infoModal.clientSettings.expiryTime)) ]]
|
||||
</template>
|
||||
[[ IntlUtil.formatDate(infoModal.clientSettings.expiryTime) ]]
|
||||
</a-tag>
|
||||
</template>
|
||||
<a-tag v-else-if="infoModal.clientSettings.expiryTime < 0" color="green">[[ infoModal.clientSettings.expiryTime / -86400000 ]] {{ i18n "pages.client.days" }}
|
||||
|
|
|
|||
|
|
@ -158,6 +158,13 @@
|
|||
this.inbound.stream.reality.mldsa65Seed = '';
|
||||
this.inbound.stream.reality.settings.mldsa65Verify = '';
|
||||
},
|
||||
randomizeRealityTarget() {
|
||||
if (typeof getRandomRealityTarget !== 'undefined') {
|
||||
const randomTarget = getRandomRealityTarget();
|
||||
this.inbound.stream.reality.target = randomTarget.target;
|
||||
this.inbound.stream.reality.serverNames = randomTarget.sni;
|
||||
}
|
||||
},
|
||||
async getNewEchCert() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.post('/panel/api/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });
|
||||
|
|
|
|||
|
|
@ -9,19 +9,20 @@
|
|||
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
|
||||
<transition name="list" appear>
|
||||
<a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }"
|
||||
message='{{ i18n "secAlertTitle" }}'
|
||||
color="red"
|
||||
show-icon closable>
|
||||
message='{{ i18n "secAlertTitle" }}' color="red" show-icon closable>
|
||||
<template slot="description">
|
||||
<b>{{ i18n "secAlertConf" }}</b>
|
||||
<ul><li v-for="a in confAlerts">[[ a ]]</li></ul>
|
||||
<ul>
|
||||
<li v-for="a in confAlerts">[[ a ]]</li>
|
||||
</ul>
|
||||
</template>
|
||||
</a-alert>
|
||||
</transition>
|
||||
<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
|
||||
:style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
|
||||
<a-spin tip='{{ i18n "loading" }}'></a-spin>
|
||||
</a-card>
|
||||
</a-row>
|
||||
|
|
@ -31,17 +32,19 @@
|
|||
<a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
|
||||
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
|
||||
<a-space direction="horizontal">
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button>
|
||||
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n
|
||||
"pages.settings.save" }}</a-button>
|
||||
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n
|
||||
"pages.settings.restartPanel" }}</a-button>
|
||||
</a-space>
|
||||
</a-col>
|
||||
<a-col :xs="24" :sm="14">
|
||||
<template>
|
||||
<div>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top>
|
||||
<a-back-top :target="() => document.getElementById('content-layout')"
|
||||
visibility-height="200"></a-back-top>
|
||||
<a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
|
||||
message='{{ i18n "pages.settings.infoDesc" }}'
|
||||
show-icon>
|
||||
message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
|
||||
</a-alert>
|
||||
</div>
|
||||
</template>
|
||||
|
|
@ -132,7 +135,8 @@
|
|||
fragment: {
|
||||
packets: "tlshello",
|
||||
length: "100-200",
|
||||
interval: "10-20"
|
||||
interval: "10-20",
|
||||
maxSplit: "300-400"
|
||||
}
|
||||
},
|
||||
streamSettings: {
|
||||
|
|
@ -381,11 +385,11 @@
|
|||
},
|
||||
computed: {
|
||||
ldapInboundTagList: {
|
||||
get: function() {
|
||||
get: function () {
|
||||
const csv = this.allSetting.ldapInboundTags || "";
|
||||
return csv.length ? csv.split(',').map(s => s.trim()).filter(Boolean) : [];
|
||||
},
|
||||
set: function(list) {
|
||||
set: function (list) {
|
||||
this.allSetting.ldapInboundTags = Array.isArray(list) ? list.join(',') : '';
|
||||
}
|
||||
},
|
||||
|
|
@ -425,6 +429,16 @@
|
|||
}
|
||||
}
|
||||
},
|
||||
fragmentMaxSplit: {
|
||||
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.maxSplit : ""; },
|
||||
set: function (v) {
|
||||
if (v != "") {
|
||||
newFragment = JSON.parse(this.allSetting.subJsonFragment);
|
||||
newFragment.settings.fragment.maxSplit = v;
|
||||
this.allSetting.subJsonFragment = JSON.stringify(newFragment);
|
||||
}
|
||||
}
|
||||
},
|
||||
noises: {
|
||||
get() {
|
||||
return this.allSetting?.subJsonNoises != "";
|
||||
|
|
|
|||
|
|
@ -5,13 +5,10 @@
|
|||
<template #title>{{ i18n "pages.settings.subPath"}}</template>
|
||||
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
|
||||
<template #control>
|
||||
<a-input
|
||||
type="text"
|
||||
v-model="allSetting.subJsonPath"
|
||||
<a-input type="text" v-model="allSetting.subJsonPath"
|
||||
@input="allSetting.subJsonPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
|
||||
@blur="allSetting.subJsonPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subJsonPath)"
|
||||
placeholder="/json/"
|
||||
></a-input>
|
||||
placeholder="/json/"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
|
|
@ -53,6 +50,12 @@
|
|||
<a-input type="text" v-model="fragmentInterval" placeholder="10-20"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
<a-setting-list-item paddings="small">
|
||||
<template #title>MaxSplit</template>
|
||||
<template #control>
|
||||
<a-input type="text" v-model="fragmentMaxSplit" placeholder="300-400"></a-input>
|
||||
</template>
|
||||
</a-setting-list-item>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
</a-list-item>
|
||||
|
|
@ -74,7 +77,8 @@
|
|||
<a-select :value="noise.type" :style="{ width: '100%' }"
|
||||
:dropdown-class-name="themeSwitcher.currentTheme"
|
||||
@change="(value) => updateNoiseType(index, value)">
|
||||
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']" :key="p">
|
||||
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']"
|
||||
:key="p">
|
||||
<span>[[ p ]]</span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
|
|
|
|||
|
|
@ -4,7 +4,6 @@
|
|||
<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" .}}
|
||||
|
||||
|
|
@ -141,17 +140,7 @@
|
|||
<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>
|
||||
[[ IntlUtil.formatDate(app.lastOnlineMs) ]]
|
||||
</template>
|
||||
<template v-else>
|
||||
<span>-</span>
|
||||
|
|
@ -163,17 +152,7 @@
|
|||
{{ 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>
|
||||
[[ IntlUtil.formatDate(app.expireMs) ]]
|
||||
</template>
|
||||
</a-descriptions-item>
|
||||
</a-descriptions>
|
||||
|
|
|
|||
|
|
@ -27,8 +27,7 @@ type LdapSyncJob struct {
|
|||
func mustGetString(fn func() (string, error)) string {
|
||||
v, err := fn()
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get string setting:", err)
|
||||
return ""
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
|
@ -36,8 +35,7 @@ func mustGetString(fn func() (string, error)) string {
|
|||
func mustGetInt(fn func() (int, error)) int {
|
||||
v, err := fn()
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get int setting:", err)
|
||||
return 0
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
|
@ -45,8 +43,7 @@ func mustGetInt(fn func() (int, error)) int {
|
|||
func mustGetBool(fn func() (bool, error)) bool {
|
||||
v, err := fn()
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get bool setting:", err)
|
||||
return false
|
||||
panic(err)
|
||||
}
|
||||
return v
|
||||
}
|
||||
|
|
@ -60,46 +57,26 @@ func mustGetStringOr(fn func() (string, error), fallback string) string {
|
|||
}
|
||||
|
||||
func NewLdapSyncJob() *LdapSyncJob {
|
||||
return &LdapSyncJob{
|
||||
settingService: service.SettingService{},
|
||||
inboundService: service.InboundService{},
|
||||
xrayService: service.XrayService{},
|
||||
}
|
||||
return new(LdapSyncJob)
|
||||
}
|
||||
|
||||
func (j *LdapSyncJob) Run() {
|
||||
logger.Info("LDAP sync job started")
|
||||
|
||||
enabled, err := j.settingService.GetLdapEnable()
|
||||
if err != nil {
|
||||
logger.Warning("Failed to get LDAP enable setting:", err)
|
||||
return
|
||||
}
|
||||
if !enabled {
|
||||
logger.Debug("LDAP sync is disabled")
|
||||
if err != nil || !enabled {
|
||||
logger.Warning("LDAP disabled or failed to fetch flag")
|
||||
return
|
||||
}
|
||||
|
||||
// --- LDAP fetch ---
|
||||
host := mustGetString(j.settingService.GetLdapHost)
|
||||
if host == "" {
|
||||
logger.Warning("LDAP host is not configured")
|
||||
return
|
||||
}
|
||||
|
||||
baseDN := mustGetString(j.settingService.GetLdapBaseDN)
|
||||
if baseDN == "" {
|
||||
logger.Warning("LDAP base DN is not configured")
|
||||
return
|
||||
}
|
||||
|
||||
cfg := ldaputil.Config{
|
||||
Host: host,
|
||||
Host: mustGetString(j.settingService.GetLdapHost),
|
||||
Port: mustGetInt(j.settingService.GetLdapPort),
|
||||
UseTLS: mustGetBool(j.settingService.GetLdapUseTLS),
|
||||
BindDN: mustGetString(j.settingService.GetLdapBindDN),
|
||||
Password: mustGetString(j.settingService.GetLdapPassword),
|
||||
BaseDN: baseDN,
|
||||
BaseDN: mustGetString(j.settingService.GetLdapBaseDN),
|
||||
UserFilter: mustGetString(j.settingService.GetLdapUserFilter),
|
||||
UserAttr: mustGetString(j.settingService.GetLdapUserAttr),
|
||||
FlagField: mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)),
|
||||
|
|
@ -109,27 +86,16 @@ func (j *LdapSyncJob) Run() {
|
|||
|
||||
flags, err := ldaputil.FetchVlessFlags(cfg)
|
||||
if err != nil {
|
||||
logger.Warningf("LDAP fetch failed: %v", err)
|
||||
logger.Warning("LDAP fetch failed:", err)
|
||||
return
|
||||
}
|
||||
logger.Infof("Fetched %d LDAP flags", len(flags))
|
||||
|
||||
// --- Load all inbounds and all clients once ---
|
||||
inboundTagsStr := mustGetString(j.settingService.GetLdapInboundTags)
|
||||
if inboundTagsStr == "" {
|
||||
logger.Debug("LDAP inbound tags not configured, skipping sync")
|
||||
return
|
||||
}
|
||||
|
||||
inboundTags := splitCsv(inboundTagsStr)
|
||||
if len(inboundTags) == 0 {
|
||||
logger.Debug("No LDAP inbound tags configured, skipping sync")
|
||||
return
|
||||
}
|
||||
|
||||
inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags))
|
||||
inbounds, err := j.inboundService.GetAllInbounds()
|
||||
if err != nil {
|
||||
logger.Warningf("Failed to get inbounds: %v", err)
|
||||
logger.Warning("Failed to get inbounds:", err)
|
||||
return
|
||||
}
|
||||
|
||||
|
|
@ -191,7 +157,6 @@ func (j *LdapSyncJob) Run() {
|
|||
for tag, emails := range clientsToDisable {
|
||||
j.batchSetEnable(inboundMap[tag], emails, false)
|
||||
}
|
||||
|
||||
// --- Auto delete clients not in LDAP ---
|
||||
autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete)
|
||||
if autoDelete {
|
||||
|
|
|
|||
|
|
@ -794,17 +794,17 @@ func (s *ServerService) GetXrayLogs(
|
|||
for i, part := range parts {
|
||||
|
||||
if i == 0 {
|
||||
dateTime, err := time.Parse("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1])
|
||||
dateTime, err := time.ParseInLocation("2006/01/02 15:04:05.999999", parts[0]+" "+parts[1], time.Local)
|
||||
if err != nil {
|
||||
continue
|
||||
}
|
||||
entry.DateTime = dateTime
|
||||
entry.DateTime = dateTime.UTC()
|
||||
}
|
||||
|
||||
if part == "from" {
|
||||
entry.FromAddress = parts[i+1]
|
||||
entry.FromAddress = strings.TrimLeft(parts[i+1], "/")
|
||||
} else if part == "accepted" {
|
||||
entry.ToAddress = parts[i+1]
|
||||
entry.ToAddress = strings.TrimLeft(parts[i+1], "/")
|
||||
} else if strings.HasPrefix(part, "[") {
|
||||
entry.Inbound = part[1:]
|
||||
} else if strings.HasSuffix(part, "]") {
|
||||
|
|
@ -941,13 +941,26 @@ func (s *ServerService) ImportDB(file multipart.File) error {
|
|||
return common.NewErrorf("Error saving db: %v", err)
|
||||
}
|
||||
|
||||
// Check if we can init the db or not
|
||||
if err = database.InitDB(tempPath); err != nil {
|
||||
return common.NewErrorf("Error checking db: %v", err)
|
||||
// Close temp file before opening via sqlite
|
||||
if err = tempFile.Close(); err != nil {
|
||||
return common.NewErrorf("Error closing temporary db file: %v", err)
|
||||
}
|
||||
tempFile = nil
|
||||
|
||||
// Validate integrity (no migrations / side effects)
|
||||
if err = database.ValidateSQLiteDB(tempPath); err != nil {
|
||||
return common.NewErrorf("Invalid or corrupt db file: %v", err)
|
||||
}
|
||||
|
||||
// Stop Xray
|
||||
s.StopXrayService()
|
||||
// Stop Xray (ignore error but log)
|
||||
if errStop := s.StopXrayService(); errStop != nil {
|
||||
logger.Warningf("Failed to stop Xray before DB import: %v", errStop)
|
||||
}
|
||||
|
||||
// Close existing DB to release file locks (especially on Windows)
|
||||
if errClose := database.CloseDB(); errClose != nil {
|
||||
logger.Warningf("Failed to close existing DB before replacement: %v", errClose)
|
||||
}
|
||||
|
||||
// Backup the current database for fallback
|
||||
fallbackPath := fmt.Sprintf("%s.backup", config.GetDBPath())
|
||||
|
|
@ -982,7 +995,7 @@ func (s *ServerService) ImportDB(file multipart.File) error {
|
|||
return common.NewErrorf("Error moving db file: %v", err)
|
||||
}
|
||||
|
||||
// Migrate DB
|
||||
// Open & migrate new DB
|
||||
if err = database.InitDB(config.GetDBPath()); err != nil {
|
||||
if errRename := os.Rename(fallbackPath, config.GetDBPath()); errRename != nil {
|
||||
return common.NewErrorf("Error migrating db and restoring fallback: %v", errRename)
|
||||
|
|
|
|||
|
|
@ -571,10 +571,18 @@ func (s *SettingService) GetSubDomain() (string, error) {
|
|||
return s.getString("subDomain")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetSubCertFile(subCertFile string) error {
|
||||
return s.setString("subCertFile", subCertFile)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubCertFile() (string, error) {
|
||||
return s.getString("subCertFile")
|
||||
}
|
||||
|
||||
func (s *SettingService) SetSubKeyFile(subKeyFile string) error {
|
||||
return s.setString("subKeyFile", subKeyFile)
|
||||
}
|
||||
|
||||
func (s *SettingService) GetSubKeyFile() (string, error) {
|
||||
return s.getString("subKeyFile")
|
||||
}
|
||||
|
|
|
|||
|
|
@ -38,7 +38,15 @@ import (
|
|||
)
|
||||
|
||||
var (
|
||||
bot *telego.Bot
|
||||
bot *telego.Bot
|
||||
|
||||
// botCancel stores the function to cancel the context, stopping Long Polling gracefully.
|
||||
botCancel context.CancelFunc
|
||||
// tgBotMutex protects concurrent access to botCancel variable
|
||||
tgBotMutex sync.Mutex
|
||||
// botWG waits for the OnReceive Long Polling goroutine to finish.
|
||||
botWG sync.WaitGroup
|
||||
|
||||
botHandler *th.BotHandler
|
||||
adminIds []int64
|
||||
isRunning bool
|
||||
|
|
@ -202,7 +210,7 @@ func (t *Tgbot) Start(i18nFS embed.FS) error {
|
|||
// Parse admin IDs from comma-separated string
|
||||
if tgBotID != "" {
|
||||
for _, adminID := range strings.Split(tgBotID, ",") {
|
||||
id, err := strconv.Atoi(adminID)
|
||||
id, err := strconv.ParseInt(adminID, 10, 64)
|
||||
if err != nil {
|
||||
logger.Warning("Failed to parse admin ID from Telegram bot chat ID:", err)
|
||||
return err
|
||||
|
|
@ -306,8 +314,13 @@ func (t *Tgbot) SetHostname() {
|
|||
hostname = host
|
||||
}
|
||||
|
||||
// Stop stops the Telegram bot and cleans up resources.
|
||||
// Stop safely stops the Telegram bot's Long Polling operation.
|
||||
// This method now calls the global StopBot function and cleans up other resources.
|
||||
func (t *Tgbot) Stop() {
|
||||
// Call the global StopBot function to gracefully shut down Long Polling
|
||||
StopBot()
|
||||
|
||||
// Stop the bot handler (in case the goroutine hasn't exited yet)
|
||||
if botHandler != nil {
|
||||
botHandler.Stop()
|
||||
}
|
||||
|
|
@ -316,6 +329,27 @@ func (t *Tgbot) Stop() {
|
|||
adminIds = nil
|
||||
}
|
||||
|
||||
// StopBot safely stops the Telegram bot's Long Polling operation by cancelling its context.
|
||||
// This is the global function called from main.go's signal handler and t.Stop().
|
||||
func StopBot() {
|
||||
tgBotMutex.Lock()
|
||||
defer tgBotMutex.Unlock()
|
||||
|
||||
if botCancel != nil {
|
||||
logger.Info("Sending cancellation signal to Telegram bot...")
|
||||
|
||||
// Calling botCancel() cancels the context passed to UpdatesViaLongPolling,
|
||||
// which stops the Long Polling operation and closes the updates channel,
|
||||
// allowing the th.Start() goroutine to exit cleanly.
|
||||
botCancel()
|
||||
|
||||
botCancel = nil
|
||||
// Giving the goroutine a small delay to exit cleanly.
|
||||
botWG.Wait()
|
||||
logger.Info("Telegram bot successfully stopped.")
|
||||
}
|
||||
}
|
||||
|
||||
// encodeQuery encodes the query string if it's longer than 64 characters.
|
||||
func (t *Tgbot) encodeQuery(query string) string {
|
||||
// NOTE: we only need to hash for more than 64 chars
|
||||
|
|
@ -345,188 +379,207 @@ func (t *Tgbot) OnReceive() {
|
|||
params := telego.GetUpdatesParams{
|
||||
Timeout: 30, // Increased timeout to reduce API calls
|
||||
}
|
||||
// --- GRACEFUL SHUTDOWN FIX: Context creation ---
|
||||
tgBotMutex.Lock()
|
||||
|
||||
updates, _ := bot.UpdatesViaLongPolling(context.Background(), ¶ms)
|
||||
// Create a context with cancellation and store the cancel function.
|
||||
var ctx context.Context
|
||||
|
||||
botHandler, _ = th.NewBotHandler(bot, updates)
|
||||
// Check if botCancel is already set (to prevent race condition overwrite and goroutine leak)
|
||||
if botCancel == nil {
|
||||
ctx, botCancel = context.WithCancel(context.Background())
|
||||
} else {
|
||||
// If botCancel is already set, use a non-cancellable context for this redundant call.
|
||||
// This prevents overwriting the active botCancel and causing a goroutine leak from the previous call.
|
||||
logger.Warning("TgBot OnReceive called concurrently. Using background context for redundant call.")
|
||||
ctx = context.Background() // <<< ИЗМЕНЕНИЕ
|
||||
}
|
||||
|
||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||
delete(userStates, message.Chat.ID)
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
|
||||
return nil
|
||||
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
|
||||
tgBotMutex.Unlock()
|
||||
|
||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||
// Use goroutine with worker pool for concurrent command processing
|
||||
go func() {
|
||||
messageWorkerPool <- struct{}{} // Acquire worker
|
||||
defer func() { <-messageWorkerPool }() // Release worker
|
||||
// Get updates channel using the context.
|
||||
updates, _ := bot.UpdatesViaLongPolling(ctx, ¶ms)
|
||||
botWG.Go(func() {
|
||||
|
||||
botHandler, _ = th.NewBotHandler(bot, updates)
|
||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||
delete(userStates, message.Chat.ID)
|
||||
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
|
||||
}()
|
||||
return nil
|
||||
}, th.AnyCommand())
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.keyboardClosed"), tu.ReplyKeyboardRemove())
|
||||
return nil
|
||||
}, th.TextEqual(t.I18nBot("tgbot.buttons.closeKeyboard")))
|
||||
|
||||
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
|
||||
// Use goroutine with worker pool for concurrent callback processing
|
||||
go func() {
|
||||
messageWorkerPool <- struct{}{} // Acquire worker
|
||||
defer func() { <-messageWorkerPool }() // Release worker
|
||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||
// Use goroutine with worker pool for concurrent command processing
|
||||
go func() {
|
||||
messageWorkerPool <- struct{}{} // Acquire worker
|
||||
defer func() { <-messageWorkerPool }() // Release worker
|
||||
|
||||
delete(userStates, query.Message.GetChat().ID)
|
||||
t.answerCallback(&query, checkAdmin(query.From.ID))
|
||||
}()
|
||||
return nil
|
||||
}, th.AnyCallbackQueryWithMessage())
|
||||
|
||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||
if userState, exists := userStates[message.Chat.ID]; exists {
|
||||
switch userState {
|
||||
case "awaiting_id":
|
||||
if client_Id == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(message.Chat.ID, message_text)
|
||||
return nil
|
||||
}
|
||||
|
||||
client_Id = strings.TrimSpace(message.Text)
|
||||
if t.isSingleWord(client_Id) {
|
||||
userStates[message.Chat.ID] = "awaiting_id"
|
||||
|
||||
cancel_btn_markup := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
||||
),
|
||||
)
|
||||
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
||||
} else {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(message.Chat.ID, message_text)
|
||||
}
|
||||
case "awaiting_password_tr":
|
||||
if client_TrPassword == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
client_TrPassword = strings.TrimSpace(message.Text)
|
||||
if t.isSingleWord(client_TrPassword) {
|
||||
userStates[message.Chat.ID] = "awaiting_password_tr"
|
||||
|
||||
cancel_btn_markup := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
||||
),
|
||||
)
|
||||
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
||||
} else {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(message.Chat.ID, message_text)
|
||||
}
|
||||
case "awaiting_password_sh":
|
||||
if client_ShPassword == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
client_ShPassword = strings.TrimSpace(message.Text)
|
||||
if t.isSingleWord(client_ShPassword) {
|
||||
userStates[message.Chat.ID] = "awaiting_password_sh"
|
||||
|
||||
cancel_btn_markup := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
||||
),
|
||||
)
|
||||
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
||||
} else {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(message.Chat.ID, message_text)
|
||||
}
|
||||
case "awaiting_email":
|
||||
if client_Email == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
client_Email = strings.TrimSpace(message.Text)
|
||||
if t.isSingleWord(client_Email) {
|
||||
userStates[message.Chat.ID] = "awaiting_email"
|
||||
|
||||
cancel_btn_markup := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
||||
),
|
||||
)
|
||||
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
||||
} else {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(message.Chat.ID, message_text)
|
||||
}
|
||||
case "awaiting_comment":
|
||||
if client_Comment == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
client_Comment = strings.TrimSpace(message.Text)
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(message.Chat.ID, message_text)
|
||||
}
|
||||
t.answerCommand(&message, message.Chat.ID, checkAdmin(message.From.ID))
|
||||
}()
|
||||
return nil
|
||||
}, th.AnyCommand())
|
||||
|
||||
} else {
|
||||
if message.UsersShared != nil {
|
||||
if checkAdmin(message.From.ID) {
|
||||
for _, sharedUser := range message.UsersShared.Users {
|
||||
userID := sharedUser.UserID
|
||||
needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID)
|
||||
if needRestart {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
}
|
||||
output := ""
|
||||
if err != nil {
|
||||
output += t.I18nBot("tgbot.messages.selectUserFailed")
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.userSaved")
|
||||
}
|
||||
t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove())
|
||||
botHandler.HandleCallbackQuery(func(ctx *th.Context, query telego.CallbackQuery) error {
|
||||
// Use goroutine with worker pool for concurrent callback processing
|
||||
go func() {
|
||||
messageWorkerPool <- struct{}{} // Acquire worker
|
||||
defer func() { <-messageWorkerPool }() // Release worker
|
||||
|
||||
delete(userStates, query.Message.GetChat().ID)
|
||||
t.answerCallback(&query, checkAdmin(query.From.ID))
|
||||
}()
|
||||
return nil
|
||||
}, th.AnyCallbackQueryWithMessage())
|
||||
|
||||
botHandler.HandleMessage(func(ctx *th.Context, message telego.Message) error {
|
||||
if userState, exists := userStates[message.Chat.ID]; exists {
|
||||
switch userState {
|
||||
case "awaiting_id":
|
||||
if client_Id == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(message.Chat.ID, message_text)
|
||||
return nil
|
||||
}
|
||||
|
||||
client_Id = strings.TrimSpace(message.Text)
|
||||
if t.isSingleWord(client_Id) {
|
||||
userStates[message.Chat.ID] = "awaiting_id"
|
||||
|
||||
cancel_btn_markup := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
||||
),
|
||||
)
|
||||
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
||||
} else {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_id"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(message.Chat.ID, message_text)
|
||||
}
|
||||
case "awaiting_password_tr":
|
||||
if client_TrPassword == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
client_TrPassword = strings.TrimSpace(message.Text)
|
||||
if t.isSingleWord(client_TrPassword) {
|
||||
userStates[message.Chat.ID] = "awaiting_password_tr"
|
||||
|
||||
cancel_btn_markup := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
||||
),
|
||||
)
|
||||
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
||||
} else {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(message.Chat.ID, message_text)
|
||||
}
|
||||
case "awaiting_password_sh":
|
||||
if client_ShPassword == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
client_ShPassword = strings.TrimSpace(message.Text)
|
||||
if t.isSingleWord(client_ShPassword) {
|
||||
userStates[message.Chat.ID] = "awaiting_password_sh"
|
||||
|
||||
cancel_btn_markup := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
||||
),
|
||||
)
|
||||
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
||||
} else {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_password"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(message.Chat.ID, message_text)
|
||||
}
|
||||
case "awaiting_email":
|
||||
if client_Email == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
client_Email = strings.TrimSpace(message.Text)
|
||||
if t.isSingleWord(client_Email) {
|
||||
userStates[message.Chat.ID] = "awaiting_email"
|
||||
|
||||
cancel_btn_markup := tu.InlineKeyboard(
|
||||
tu.InlineKeyboardRow(
|
||||
tu.InlineKeyboardButton(t.I18nBot("tgbot.buttons.use_default")).WithCallbackData("add_client_default_info"),
|
||||
),
|
||||
)
|
||||
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.messages.incorrect_input"), cancel_btn_markup)
|
||||
} else {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_email"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(message.Chat.ID, message_text)
|
||||
}
|
||||
case "awaiting_comment":
|
||||
if client_Comment == strings.TrimSpace(message.Text) {
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.using_default_value"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
return nil
|
||||
}
|
||||
|
||||
client_Comment = strings.TrimSpace(message.Text)
|
||||
t.SendMsgToTgbotDeleteAfter(message.Chat.ID, t.I18nBot("tgbot.messages.received_comment"), 3, tu.ReplyKeyboardRemove())
|
||||
delete(userStates, message.Chat.ID)
|
||||
inbound, _ := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
message_text, _ := t.BuildInboundClientDataMessage(inbound.Remark, inbound.Protocol)
|
||||
t.addClient(message.Chat.ID, message_text)
|
||||
}
|
||||
|
||||
} else {
|
||||
if message.UsersShared != nil {
|
||||
if checkAdmin(message.From.ID) {
|
||||
for _, sharedUser := range message.UsersShared.Users {
|
||||
userID := sharedUser.UserID
|
||||
needRestart, err := t.inboundService.SetClientTelegramUserID(message.UsersShared.RequestID, userID)
|
||||
if needRestart {
|
||||
t.xrayService.SetToNeedRestart()
|
||||
}
|
||||
output := ""
|
||||
if err != nil {
|
||||
output += t.I18nBot("tgbot.messages.selectUserFailed")
|
||||
} else {
|
||||
output += t.I18nBot("tgbot.messages.userSaved")
|
||||
}
|
||||
t.SendMsgToTgbot(message.Chat.ID, output, tu.ReplyKeyboardRemove())
|
||||
}
|
||||
} else {
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.noResult"), tu.ReplyKeyboardRemove())
|
||||
}
|
||||
} else {
|
||||
t.SendMsgToTgbot(message.Chat.ID, t.I18nBot("tgbot.noResult"), tu.ReplyKeyboardRemove())
|
||||
}
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}, th.AnyMessage())
|
||||
return nil
|
||||
}, th.AnyMessage())
|
||||
|
||||
botHandler.Start()
|
||||
botHandler.Start()
|
||||
})
|
||||
}
|
||||
|
||||
// answerCommand processes incoming command messages from Telegram users.
|
||||
|
|
@ -852,8 +905,8 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
t.sendCallbackAnswerTgBot(callbackQuery.ID, t.I18nBot("tgbot.answers.errorOperation"))
|
||||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "add_client_limit_traffic_c":
|
||||
limitTraffic, _ := strconv.Atoi(dataArray[1])
|
||||
client_TotalGB = int64(limitTraffic) * 1024 * 1024 * 1024
|
||||
limitTraffic, _ := strconv.ParseInt(dataArray[1], 10, 64)
|
||||
client_TotalGB = limitTraffic * 1024 * 1024 * 1024
|
||||
messageId := callbackQuery.Message.GetMessageID()
|
||||
inbound, err := t.inboundService.GetInbound(receiver_inbound_ID)
|
||||
if err != nil {
|
||||
|
|
@ -957,7 +1010,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
t.editMessageCallbackTgBot(chatId, callbackQuery.Message.GetMessageID(), inlineKeyboard)
|
||||
case "reset_exp_c":
|
||||
if len(dataArray) == 3 {
|
||||
days, err := strconv.Atoi(dataArray[2])
|
||||
days, err := strconv.ParseInt(dataArray[2], 10, 64)
|
||||
if err == nil {
|
||||
var date int64
|
||||
if days > 0 {
|
||||
|
|
@ -1062,7 +1115,7 @@ func (t *Tgbot) answerCallback(callbackQuery *telego.CallbackQuery, isAdmin bool
|
|||
t.searchClient(chatId, email, callbackQuery.Message.GetMessageID())
|
||||
case "add_client_reset_exp_c":
|
||||
client_ExpiryTime = 0
|
||||
days, _ := strconv.Atoi(dataArray[1])
|
||||
days, _ := strconv.ParseInt(dataArray[1], 10, 64)
|
||||
var date int64
|
||||
if client_ExpiryTime > 0 {
|
||||
if client_ExpiryTime-time.Now().Unix()*1000 < 0 {
|
||||
|
|
@ -2899,10 +2952,12 @@ func (t *Tgbot) clientInfoMsg(
|
|||
}
|
||||
|
||||
status := t.I18nBot("tgbot.offline")
|
||||
isOnline := false
|
||||
if p.IsRunning() {
|
||||
for _, online := range p.GetOnlineClients() {
|
||||
if online == traffic.Email {
|
||||
status = t.I18nBot("tgbot.online")
|
||||
isOnline = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
|
@ -2915,6 +2970,9 @@ func (t *Tgbot) clientInfoMsg(
|
|||
}
|
||||
if printOnline {
|
||||
output += t.I18nBot("tgbot.messages.online", "Status=="+status)
|
||||
if !isOnline && traffic.LastOnline > 0 {
|
||||
output += t.I18nBot("tgbot.messages.lastOnline", "Time=="+time.UnixMilli(traffic.LastOnline).Format("2006-01-02 15:04:05"))
|
||||
}
|
||||
}
|
||||
if printActive {
|
||||
output += t.I18nBot("tgbot.messages.active", "Enable=="+active)
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
"invalidFormData" = "تنسيق البيانات المدخلة مش صحيح."
|
||||
"emptyUsername" = "اسم المستخدم مطلوب"
|
||||
"emptyPassword" = "الباسورد مطلوب"
|
||||
"wrongUsernameOrPassword" = "اسم المستخدم أو كلمة المرور أو كود المصادقة الثنائية غير صحيح."
|
||||
"wrongUsernameOrPassword" = "اسم المستخدم أو كلمة المرور أو كود المصادقة الثنائية غير صحيح."
|
||||
"successLogin" = "لقد تم تسجيل الدخول إلى حسابك بنجاح."
|
||||
|
||||
[pages.index]
|
||||
|
|
@ -565,9 +565,9 @@
|
|||
|
||||
[pages.settings.security]
|
||||
"admin" = "بيانات الأدمن"
|
||||
"twoFactor" = "المصادقة الثنائية"
|
||||
"twoFactorEnable" = "تفعيل المصادقة الثنائية"
|
||||
"twoFactorEnableDesc" = "يضيف طبقة إضافية من المصادقة لتعزيز الأمان."
|
||||
"twoFactor" = "المصادقة الثنائية"
|
||||
"twoFactorEnable" = "تفعيل المصادقة الثنائية"
|
||||
"twoFactorEnableDesc" = "يضيف طبقة إضافية من المصادقة لتعزيز الأمان."
|
||||
"twoFactorModalSetTitle" = "تفعيل المصادقة الثنائية"
|
||||
"twoFactorModalDeleteTitle" = "تعطيل المصادقة الثنائية"
|
||||
"twoFactorModalSteps" = "لإعداد المصادقة الثنائية، قم ببعض الخطوات:"
|
||||
|
|
@ -663,6 +663,7 @@
|
|||
"active" = "💡 مفعل: {{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 مفعل: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 حالة الاتصال: {{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 آخر متصل: {{ .Time }}\r\n"
|
||||
"email" = "📧 الإيميل: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 رفع: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 تنزيل: ↓{{ .Download }}\r\n"
|
||||
|
|
|
|||
|
|
@ -663,6 +663,7 @@
|
|||
"active" = "💡 Active: {{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 Enabled: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 Connection status: {{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 Last online: {{ .Time }}\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 Download: ↓{{ .Download }}\r\n"
|
||||
|
|
@ -688,9 +689,9 @@
|
|||
"inbound_client_data_id" = "🔄 Inbound: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Traffic: {{ .ClientTraffic }}\n📅 Expire Date: {{ .ClientExp }}\n🌐 IP Limit: {{ .IpLimit }}\n💬 Comment: {{ .ClientComment }}\n\nYou can add the client to inbound now!"
|
||||
"inbound_client_data_pass" = "🔄 Inbound: {{ .InboundRemark }}\n\n🔑 Password: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Traffic: {{ .ClientTraffic }}\n📅 Expire Date: {{ .ClientExp }}\n🌐 IP Limit: {{ .IpLimit }}\n💬 Comment: {{ .ClientComment }}\n\nYou can add the client to inbound now!"
|
||||
"cancel" = "❌ Process Canceled! \n\nYou can /start again anytime. 🔄"
|
||||
"error_add_client" = "⚠️ Error:\n\n {{ .error }}"
|
||||
"using_default_value" = "Okay, I'll stick with the default value. 😊"
|
||||
"incorrect_input" ="Your input is not valid.\nThe phrases should be continuous without spaces.\nCorrect example: aaaaaa\nIncorrect example: aaa aaa 🚫"
|
||||
"error_add_client" = "⚠️ Error:\n\n {{ .error }}"
|
||||
"using_default_value" = "Okay, I'll stick with the default value. 😊"
|
||||
"incorrect_input" = "Your input is not valid.\nThe phrases should be continuous without spaces.\nCorrect example: aaaaaa\nIncorrect example: aaa aaa 🚫"
|
||||
"AreYouSure" = "Are you sure? 🤔"
|
||||
"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ✅ Success"
|
||||
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Result: ❌ Failed \n\n🛠️ Error: [ {{ .ErrorMessage }} ]"
|
||||
|
|
|
|||
|
|
@ -663,6 +663,7 @@
|
|||
"active" = "💡 Activo: {{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 Habilitado: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 Estado de conexión: {{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 Última conexión: {{ .Time }}\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Subida: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 Bajada: ↓{{ .Download }}\r\n"
|
||||
|
|
@ -688,9 +689,9 @@
|
|||
"inbound_client_data_id" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Correo: {{ .ClientEmail }}\n📊 Tráfico: {{ .ClientTraffic }}\n📅 Fecha de expiración: {{ .ClientExp }}\n🌐 Límite de IP: {{ .IpLimit }}\n💬 Comentario: {{ .ClientComment }}\n\n¡Ahora puedes agregar al cliente a la entrada!"
|
||||
"inbound_client_data_pass" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 Contraseña: {{ .ClientPass }}\n📧 Correo: {{ .ClientEmail }}\n📊 Tráfico: {{ .ClientTraffic }}\n📅 Fecha de expiración: {{ .ClientExp }}\n🌐 Límite de IP: {{ .IpLimit }}\n💬 Comentario: {{ .ClientComment }}\n\n¡Ahora puedes agregar al cliente a la entrada!"
|
||||
"cancel" = "❌ ¡Proceso cancelado! \n\nPuedes /start de nuevo en cualquier momento. 🔄"
|
||||
"error_add_client" = "⚠️ Error:\n\n {{ .error }}"
|
||||
"using_default_value" = "Está bien, me quedaré con el valor predeterminado. 😊"
|
||||
"incorrect_input" ="Tu entrada no es válida.\nLas frases deben ser continuas sin espacios.\nEjemplo correcto: aaaaaa\nEjemplo incorrecto: aaa aaa 🚫"
|
||||
"error_add_client" = "⚠️ Error:\n\n {{ .error }}"
|
||||
"using_default_value" = "Está bien, me quedaré con el valor predeterminado. 😊"
|
||||
"incorrect_input" = "Tu entrada no es válida.\nLas frases deben ser continuas sin espacios.\nEjemplo correcto: aaaaaa\nEjemplo incorrecto: aaa aaa 🚫"
|
||||
"AreYouSure" = "¿Estás seguro? 🤔"
|
||||
"SuccessResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ✅ Éxito"
|
||||
"FailedResetTraffic" = "📧 Correo: {{ .ClientEmail }}\n🏁 Resultado: ❌ Fallido \n\n🛠️ Error: [ {{ .ErrorMessage }} ]"
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
"invalidFormData" = "اطلاعات بهدرستی وارد نشدهاست"
|
||||
"emptyUsername" = "لطفا یک نامکاربری وارد کنید"
|
||||
"emptyPassword" = "لطفا یک رمزعبور وارد کنید"
|
||||
"wrongUsernameOrPassword" = "نام کاربری، رمز عبور یا کد دو مرحلهای نامعتبر است."
|
||||
"wrongUsernameOrPassword" = "نام کاربری، رمز عبور یا کد دو مرحلهای نامعتبر است."
|
||||
"successLogin" = "شما با موفقیت به حساب کاربری خود وارد شدید."
|
||||
|
||||
[pages.index]
|
||||
|
|
@ -565,9 +565,9 @@
|
|||
|
||||
[pages.settings.security]
|
||||
"admin" = "اعتبارنامههای ادمین"
|
||||
"twoFactor" = "احراز هویت دو مرحلهای"
|
||||
"twoFactorEnable" = "فعالسازی 2FA"
|
||||
"twoFactorEnableDesc" = "یک لایه اضافی امنیتی برای احراز هویت فراهم میکند."
|
||||
"twoFactor" = "احراز هویت دو مرحلهای"
|
||||
"twoFactorEnable" = "فعالسازی 2FA"
|
||||
"twoFactorEnableDesc" = "یک لایه اضافی امنیتی برای احراز هویت فراهم میکند."
|
||||
"twoFactorModalSetTitle" = "فعالسازی احراز هویت دو مرحلهای"
|
||||
"twoFactorModalDeleteTitle" = "غیرفعالسازی احراز هویت دو مرحلهای"
|
||||
"twoFactorModalSteps" = "برای راهاندازی احراز هویت دو مرحلهای، مراحل زیر را انجام دهید:"
|
||||
|
|
@ -663,6 +663,7 @@
|
|||
"active" = "💡 فعال: {{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 وضعیت: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 وضعیت اتصال: {{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 آخرین فعالیت: {{ .Time }}\r\n"
|
||||
"email" = "📧 ایمیل: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 آپلود↑: {{ .Upload }}\r\n"
|
||||
"download" = "🔽 دانلود↓: {{ .Download }}\r\n"
|
||||
|
|
@ -688,9 +689,9 @@
|
|||
"inbound_client_data_id" = "🔄 ورودی: {{ .InboundRemark }}\n\n🔑 شناسه: {{ .ClientId }}\n📧 ایمیل: {{ .ClientEmail }}\n📊 ترافیک: {{ .ClientTraffic }}\n📅 تاریخ انقضا: {{ .ClientExp }}\n🌐 محدودیت IP: {{ .IpLimit }}\n💬 توضیح: {{ .ClientComment }}\n\nاکنون میتونی مشتری را به ورودی اضافه کنی!"
|
||||
"inbound_client_data_pass" = "🔄 ورودی: {{ .InboundRemark }}\n\n🔑 رمز عبور: {{ .ClientPass }}\n📧 ایمیل: {{ .ClientEmail }}\n📊 ترافیک: {{ .ClientTraffic }}\n📅 تاریخ انقضا: {{ .ClientExp }}\n🌐 محدودیت IP: {{ .IpLimit }}\n💬 توضیح: {{ .ClientComment }}\n\nاکنون میتونی مشتری را به ورودی اضافه کنی!"
|
||||
"cancel" = "❌ فرآیند لغو شد! \n\nمیتوانید هر زمان که خواستید /start را دوباره اجرا کنید. 🔄"
|
||||
"error_add_client" = "⚠️ خطا:\n\n {{ .error }}"
|
||||
"using_default_value" = "باشه، از مقدار پیشفرض استفاده میکنم. 😊"
|
||||
"incorrect_input" ="ورودی شما معتبر نیست.\nعبارتها باید بدون فاصله باشند.\nمثال صحیح: aaaaaa\nمثال نادرست: aaa aaa 🚫"
|
||||
"error_add_client" = "⚠️ خطا:\n\n {{ .error }}"
|
||||
"using_default_value" = "باشه، از مقدار پیشفرض استفاده میکنم. 😊"
|
||||
"incorrect_input" = "ورودی شما معتبر نیست.\nعبارتها باید بدون فاصله باشند.\nمثال صحیح: aaaaaa\nمثال نادرست: aaa aaa 🚫"
|
||||
"AreYouSure" = "مطمئنی؟ 🤔"
|
||||
"SuccessResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ✅ موفقیتآمیز"
|
||||
"FailedResetTraffic" = "📧 ایمیل: {{ .ClientEmail }}\n🏁 نتیجه: ❌ ناموفق \n\n🛠️ خطا: [ {{ .ErrorMessage }} ]"
|
||||
|
|
|
|||
|
|
@ -106,12 +106,12 @@
|
|||
"invalidFormData" = "Format data input tidak valid."
|
||||
"emptyUsername" = "Nama Pengguna diperlukan"
|
||||
"emptyPassword" = "Kata Sandi diperlukan"
|
||||
"wrongUsernameOrPassword" = "Username, kata sandi, atau kode dua faktor tidak valid."
|
||||
"wrongUsernameOrPassword" = "Username, kata sandi, atau kode dua faktor tidak valid."
|
||||
"successLogin" = "Anda telah berhasil masuk ke akun Anda."
|
||||
|
||||
[pages.index]
|
||||
"title" = "Ikhtisar"
|
||||
"cpu" = "CPU"
|
||||
"cpu" = "CPU"
|
||||
"logicalProcessors" = "Prosesor logis"
|
||||
"frequency" = "Frekuensi"
|
||||
"swap" = "Swap"
|
||||
|
|
@ -663,6 +663,7 @@
|
|||
"active" = "💡 Aktif: {{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 Diaktifkan: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 Status Koneksi: {{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 Terakhir online: {{ .Time }}\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Unggah: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 Unduh: ↓{{ .Download }}\r\n"
|
||||
|
|
@ -688,9 +689,9 @@
|
|||
"inbound_client_data_id" = "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!"
|
||||
"inbound_client_data_pass" = "🔄 Masuk: {{ .InboundRemark }}\n\n🔑 Kata sandi: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Lalu lintas: {{ .ClientTraffic }}\n📅 Tanggal Kedaluwarsa: {{ .ClientExp }}\n🌐 Batas IP: {{ .IpLimit }}\n💬 Komentar: {{ .ClientComment }}\n\nSekarang kamu bisa menambahkan klien ke inbound!"
|
||||
"cancel" = "❌ Proses Dibatalkan! \n\nAnda dapat /start lagi kapan saja. 🔄"
|
||||
"error_add_client" = "⚠️ Kesalahan:\n\n {{ .error }}"
|
||||
"using_default_value" = "Oke, saya akan tetap menggunakan nilai default. 😊"
|
||||
"incorrect_input" ="Masukan Anda tidak valid.\nFrasa harus berlanjut tanpa spasi.\nContoh benar: aaaaaa\nContoh salah: aaa aaa 🚫"
|
||||
"error_add_client" = "⚠️ Kesalahan:\n\n {{ .error }}"
|
||||
"using_default_value" = "Oke, saya akan tetap menggunakan nilai default. 😊"
|
||||
"incorrect_input" = "Masukan Anda tidak valid.\nFrasa harus berlanjut tanpa spasi.\nContoh benar: aaaaaa\nContoh salah: aaa aaa 🚫"
|
||||
"AreYouSure" = "Apakah kamu yakin? 🤔"
|
||||
"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ✅ Berhasil"
|
||||
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Hasil: ❌ Gagal \n\n🛠️ Kesalahan: [ {{ .ErrorMessage }} ]"
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
"invalidFormData" = "データ形式エラー"
|
||||
"emptyUsername" = "ユーザー名を入力してください"
|
||||
"emptyPassword" = "パスワードを入力してください"
|
||||
"wrongUsernameOrPassword" = "ユーザー名、パスワード、または二段階認証コードが無効です。"
|
||||
"wrongUsernameOrPassword" = "ユーザー名、パスワード、または二段階認証コードが無効です。"
|
||||
"successLogin" = "アカウントに正常にログインしました。"
|
||||
|
||||
[pages.index]
|
||||
|
|
@ -565,9 +565,9 @@
|
|||
|
||||
[pages.settings.security]
|
||||
"admin" = "管理者の資格情報"
|
||||
"twoFactor" = "二段階認証"
|
||||
"twoFactorEnable" = "2FAを有効化"
|
||||
"twoFactorEnableDesc" = "セキュリティを強化するために追加の認証層を追加します。"
|
||||
"twoFactor" = "二段階認証"
|
||||
"twoFactorEnable" = "2FAを有効化"
|
||||
"twoFactorEnableDesc" = "セキュリティを強化するために追加の認証層を追加します。"
|
||||
"twoFactorModalSetTitle" = "二段階認証を有効にする"
|
||||
"twoFactorModalDeleteTitle" = "二段階認証を無効にする"
|
||||
"twoFactorModalSteps" = "二段階認証を設定するには、次の手順を実行してください:"
|
||||
|
|
@ -663,6 +663,7 @@
|
|||
"active" = "💡 有効:{{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 有効化済み:{{ .Enable }}\r\n"
|
||||
"online" = "🌐 接続ステータス:{{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 最終オンライン: {{ .Time }}\r\n"
|
||||
"email" = "📧 メール:{{ .Email }}\r\n"
|
||||
"upload" = "🔼 アップロード↑:{{ .Upload }}\r\n"
|
||||
"download" = "🔽 ダウンロード↓:{{ .Download }}\r\n"
|
||||
|
|
@ -688,9 +689,9 @@
|
|||
"inbound_client_data_id" = "🔄 インバウンド: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 メール: {{ .ClientEmail }}\n📊 トラフィック: {{ .ClientTraffic }}\n📅 有効期限: {{ .ClientExp }}\n🌐 IP制限: {{ .IpLimit }}\n💬 コメント: {{ .ClientComment }}\n\n今すぐこのクライアントをインバウンドに追加できます!"
|
||||
"inbound_client_data_pass" = "🔄 インバウンド: {{ .InboundRemark }}\n\n🔑 パスワード: {{ .ClientPass }}\n📧 メール: {{ .ClientEmail }}\n📊 トラフィック: {{ .ClientTraffic }}\n📅 有効期限: {{ .ClientExp }}\n🌐 IP制限: {{ .IpLimit }}\n💬 コメント: {{ .ClientComment }}\n\n今すぐこのクライアントをインバウンドに追加できます!"
|
||||
"cancel" = "❌ プロセスがキャンセルされました!\n\nいつでも /start で再開できます。 🔄"
|
||||
"error_add_client" = "⚠️ エラー:\n\n {{ .error }}"
|
||||
"using_default_value" = "わかりました、デフォルト値を使用します。 😊"
|
||||
"incorrect_input" ="入力が無効です。\nフレーズはスペースなしで続けて入力してください。\n正しい例: aaaaaa\n間違った例: aaa aaa 🚫"
|
||||
"error_add_client" = "⚠️ エラー:\n\n {{ .error }}"
|
||||
"using_default_value" = "わかりました、デフォルト値を使用します。 😊"
|
||||
"incorrect_input" = "入力が無効です。\nフレーズはスペースなしで続けて入力してください。\n正しい例: aaaaaa\n間違った例: aaa aaa 🚫"
|
||||
"AreYouSure" = "本当にいいですか?🤔"
|
||||
"SuccessResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ✅ 成功"
|
||||
"FailedResetTraffic" = "📧 メール: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠️ エラー: [ {{ .ErrorMessage }} ]"
|
||||
|
|
|
|||
|
|
@ -106,12 +106,12 @@
|
|||
"invalidFormData" = "O formato dos dados de entrada é inválido."
|
||||
"emptyUsername" = "Nome de usuário é obrigatório"
|
||||
"emptyPassword" = "Senha é obrigatória"
|
||||
"wrongUsernameOrPassword" = "Nome de usuário, senha ou código de dois fatores inválido."
|
||||
"wrongUsernameOrPassword" = "Nome de usuário, senha ou código de dois fatores inválido."
|
||||
"successLogin" = "Você entrou na sua conta com sucesso."
|
||||
|
||||
[pages.index]
|
||||
"title" = "Visão Geral"
|
||||
"cpu" = "CPU"
|
||||
"cpu" = "CPU"
|
||||
"logicalProcessors" = "Processadores lógicos"
|
||||
"frequency" = "Frequência"
|
||||
"swap" = "Swap"
|
||||
|
|
@ -565,9 +565,9 @@
|
|||
|
||||
[pages.settings.security]
|
||||
"admin" = "Credenciais de administrador"
|
||||
"twoFactor" = "Autenticação de dois fatores"
|
||||
"twoFactorEnable" = "Ativar 2FA"
|
||||
"twoFactorEnableDesc" = "Adiciona uma camada extra de autenticação para mais segurança."
|
||||
"twoFactor" = "Autenticação de dois fatores"
|
||||
"twoFactorEnable" = "Ativar 2FA"
|
||||
"twoFactorEnableDesc" = "Adiciona uma camada extra de autenticação para mais segurança."
|
||||
"twoFactorModalSetTitle" = "Ativar autenticação de dois fatores"
|
||||
"twoFactorModalDeleteTitle" = "Desativar autenticação de dois fatores"
|
||||
"twoFactorModalSteps" = "Para configurar a autenticação de dois fatores, siga alguns passos:"
|
||||
|
|
@ -663,6 +663,7 @@
|
|||
"active" = "💡 Ativo: {{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 Ativado: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 Status da conexão: {{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 Última vez online: {{ .Time }}\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 Download: ↓{{ .Download }}\r\n"
|
||||
|
|
@ -688,9 +689,9 @@
|
|||
"inbound_client_data_id" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Tráfego: {{ .ClientTraffic }}\n📅 Data de expiração: {{ .ClientExp }}\n🌐 Limite de IP: {{ .IpLimit }}\n💬 Comentário: {{ .ClientComment }}\n\nAgora você pode adicionar o cliente à entrada!"
|
||||
"inbound_client_data_pass" = "🔄 Entrada: {{ .InboundRemark }}\n\n🔑 Senha: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Tráfego: {{ .ClientTraffic }}\n📅 Data de expiração: {{ .ClientExp }}\n🌐 Limite de IP: {{ .IpLimit }}\n💬 Comentário: {{ .ClientComment }}\n\nAgora você pode adicionar o cliente à entrada!"
|
||||
"cancel" = "❌ Processo Cancelado! \n\nVocê pode iniciar novamente a qualquer momento com /start. 🔄"
|
||||
"error_add_client" = "⚠️ Erro:\n\n {{ .error }}"
|
||||
"using_default_value" = "Tudo bem, vou manter o valor padrão. 😊"
|
||||
"incorrect_input" ="Sua entrada não é válida.\nAs frases devem ser contínuas, sem espaços.\nExemplo correto: aaaaaa\nExemplo incorreto: aaa aaa 🚫"
|
||||
"error_add_client" = "⚠️ Erro:\n\n {{ .error }}"
|
||||
"using_default_value" = "Tudo bem, vou manter o valor padrão. 😊"
|
||||
"incorrect_input" = "Sua entrada não é válida.\nAs frases devem ser contínuas, sem espaços.\nExemplo correto: aaaaaa\nExemplo incorreto: aaa aaa 🚫"
|
||||
"AreYouSure" = "Você tem certeza? 🤔"
|
||||
"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ✅ Sucesso"
|
||||
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Resultado: ❌ Falhou \n\n🛠️ Erro: [ {{ .ErrorMessage }} ]"
|
||||
|
|
|
|||
|
|
@ -32,7 +32,7 @@
|
|||
"copySuccess" = "Скопировано"
|
||||
"sure" = "Да"
|
||||
"encryption" = "Шифрование"
|
||||
"useIPv4ForHost" = "Использовать IPv4 для хоста"
|
||||
"useIPv4ForHost" = "Использовать IPv4 для подключения к хосту"
|
||||
"transmission" = "Транспорт"
|
||||
"host" = "Хост"
|
||||
"path" = "Путь"
|
||||
|
|
@ -46,8 +46,8 @@
|
|||
"online" = "Онлайн"
|
||||
"domainName" = "Домен"
|
||||
"monitor" = "Мониторинг IP"
|
||||
"certificate" = "SSL сертификат"
|
||||
"fail" = "Ошибка"
|
||||
"certificate" = "SSL-сертификат"
|
||||
"fail" = "Сбой"
|
||||
"comment" = "Комментарий"
|
||||
"success" = "Успешно"
|
||||
"lastOnline" = "Был(а) в сети"
|
||||
|
|
@ -55,17 +55,17 @@
|
|||
"install" = "Установка"
|
||||
"clients" = "Клиенты"
|
||||
"usage" = "Использование"
|
||||
"twoFactorCode" = "Код"
|
||||
"twoFactorCode" = "Код 2FA"
|
||||
"remained" = "Остаток"
|
||||
"security" = "Безопасность"
|
||||
"secAlertTitle" = "Предупреждение системы безопасности"
|
||||
"secAlertSsl" = "Это соединение не защищено. Пожалуйста, не вводите конфиденциальную информацию, пока не установите SSL сертификат для защиты соединения"
|
||||
"secAlertConf" = "Некоторые настройки уязвимы для атак. Чтобы в будущем не было проблем, нужно усилить защиту."
|
||||
"secAlertSSL" = "Ваше подключение к панели не защищено. Установите SSL сертификат для защиты данных."
|
||||
"secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите случайный или просто другой порт."
|
||||
"secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Сделайте адрес сложным."
|
||||
"secAlertSubURI" = "URI-адрес подписки по умолчанию небезопасен. Пожалуйста, настройте сложный URI-адрес."
|
||||
"secAlertSubJsonURI" = "URI-адрес по умолчанию для JSON подписки небезопасен. Пожалуйста, настройте сложный URI-адрес."
|
||||
"secAlertSsl" = "Соединение не защищено. Не вводите конфиденциальные данные до установки SSL-сертификата."
|
||||
"secAlertConf" = "Некоторые настройки уязвимы. Рекомендуется усилить защиту для предотвращения атак."
|
||||
"secAlertSSL" = "Подключение к панели не защищено. Установите SSL-сертификат для защиты данных."
|
||||
"secAlertPanelPort" = "Порт панели по умолчанию небезопасен. Установите нестандартный или случайный порт."
|
||||
"secAlertPanelURI" = "Адрес панели по умолчанию небезопасен. Настройте уникальный и сложный URI."
|
||||
"secAlertSubURI" = "URI подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес."
|
||||
"secAlertSubJsonURI" = "URI JSON-подписки по умолчанию небезопасен. Настройте уникальный и сложный адрес."
|
||||
"emptyDnsDesc" = "Нет добавленных DNS-серверов."
|
||||
"emptyFakeDnsDesc" = "Нет добавленных Fake DNS-серверов."
|
||||
"emptyBalancersDesc" = "Нет добавленных балансировщиков."
|
||||
|
|
@ -83,15 +83,15 @@
|
|||
"individualLinks" = "Индивидуальные ссылки"
|
||||
"active" = "Активна"
|
||||
"inactive" = "Неактивна"
|
||||
"unlimited" = "Безлимит"
|
||||
"noExpiry" = "Без срока"
|
||||
"unlimited" = "Неограниченно"
|
||||
"noExpiry" = "Бессрочно"
|
||||
|
||||
[menu]
|
||||
"theme" = "Тема"
|
||||
"dark" = "Темная"
|
||||
"ultraDark" = "Очень темная"
|
||||
"dashboard" = "Дашборд"
|
||||
"inbounds" = "Инбаунды"
|
||||
"inbounds" = "Подключения"
|
||||
"settings" = "Настройки"
|
||||
"xray" = "Настройки Xray"
|
||||
"logout" = "Выход"
|
||||
|
|
@ -99,7 +99,7 @@
|
|||
|
||||
[pages.login]
|
||||
"hello" = "Привет!"
|
||||
"title" = "Приветствие!"
|
||||
"title" = "Добро пожаловать!"
|
||||
"loginAgain" = "Сессия истекла. Войдите в систему снова"
|
||||
|
||||
[pages.login.toasts]
|
||||
|
|
@ -107,7 +107,7 @@
|
|||
"emptyUsername" = "Введите имя пользователя"
|
||||
"emptyPassword" = "Введите пароль"
|
||||
"wrongUsernameOrPassword" = "Неверные данные учетной записи."
|
||||
"successLogin" = "Вы успешно вошли в аккаунт"
|
||||
"successLogin" = "Вход выполнен успешно"
|
||||
|
||||
[pages.index]
|
||||
"title" = "Дашборд"
|
||||
|
|
@ -122,7 +122,7 @@
|
|||
"stopXray" = "Остановить"
|
||||
"restartXray" = "Перезапустить"
|
||||
"xraySwitch" = "Выбор версии"
|
||||
"xraySwitchClick" = "Выберите желаемую версию"
|
||||
"xraySwitchClick" = "Выберите нужную версию"
|
||||
"xraySwitchClickDesk" = "Важно: старые версии могут не поддерживать текущие настройки"
|
||||
"xrayStatusUnknown" = "Неизвестно"
|
||||
"xrayStatusRunning" = "Запущен"
|
||||
|
|
@ -134,7 +134,7 @@
|
|||
"systemLoadDesc" = "Средняя загрузка системы за последние 1, 5 и 15 минут"
|
||||
"connectionCount" = "Количество соединений"
|
||||
"ipAddresses" = "IP-адреса сервера"
|
||||
"toggleIpVisibility" = "Переключить видимость IP-адресов сервера"
|
||||
"toggleIpVisibility" = "Скрыть или показать IP-адреса сервера"
|
||||
"overallSpeed" = "Общая скорость передачи трафика"
|
||||
"upload" = "Отправка"
|
||||
"download" = "Загрузка"
|
||||
|
|
@ -168,10 +168,10 @@
|
|||
[pages.inbounds]
|
||||
"allTimeTraffic" = "Общий трафик"
|
||||
"allTimeTrafficUsage" = "Общее использование за все время"
|
||||
"title" = "Инбаунды"
|
||||
"totalDownUp" = "Объем отправленного/полученного трафика"
|
||||
"title" = "Подключения"
|
||||
"totalDownUp" = "Отправлено/получено"
|
||||
"totalUsage" = "Всего трафика"
|
||||
"inboundCount" = "Всего инбаундов"
|
||||
"inboundCount" = "Всего подключений"
|
||||
"operate" = "Меню"
|
||||
"enable" = "Включить"
|
||||
"remark" = "Примечание"
|
||||
|
|
@ -185,13 +185,13 @@
|
|||
"createdAt" = "Создано"
|
||||
"updatedAt" = "Обновлено"
|
||||
"resetTraffic" = "Сброс трафика"
|
||||
"addInbound" = "Создать инбаунд"
|
||||
"addInbound" = "Создать подключение"
|
||||
"generalActions" = "Общие действия"
|
||||
"autoRefresh" = "Автообновление"
|
||||
"autoRefreshInterval" = "Интервал"
|
||||
"modifyInbound" = "Изменить инбаунд"
|
||||
"deleteInbound" = "Удалить инбаунд"
|
||||
"deleteInboundContent" = "Вы уверены, что хотите удалить инбаунд?"
|
||||
"modifyInbound" = "Изменить подключение"
|
||||
"deleteInbound" = "Удалить подключение"
|
||||
"deleteInboundContent" = "Вы уверены, что хотите удалить подключение?"
|
||||
"deleteClient" = "Удалить клиента"
|
||||
"deleteClientContent" = "Вы уверены, что хотите удалить клиента?"
|
||||
"resetTrafficContent" = "Вы уверены, что хотите сбросить трафик?"
|
||||
|
|
@ -214,11 +214,11 @@
|
|||
"export" = "Экспорт ссылок"
|
||||
"clone" = "Клонировать"
|
||||
"cloneInbound" = "Клонировать"
|
||||
"cloneInboundContent" = "Будут клонированы все настройки инбаундов, кроме списка клиентов, порта и IP-адреса прослушивания"
|
||||
"cloneInboundContent" = "Будут клонированы все настройки подключений, кроме списка клиентов, порта и IP-адреса прослушивания"
|
||||
"cloneInboundOk" = "Клонировано"
|
||||
"resetAllTraffic" = "Сброс трафика всех инбаундов"
|
||||
"resetAllTrafficTitle" = "Сброс трафика всех инбаундов"
|
||||
"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех инбаундов?"
|
||||
"resetAllTraffic" = "Сброс трафика всех подключений"
|
||||
"resetAllTrafficTitle" = "Сброс трафика всех подключений"
|
||||
"resetAllTrafficContent" = "Вы уверены, что хотите сбросить трафик всех подключений?"
|
||||
"resetInboundClientTraffics" = "Сброс трафика клиента"
|
||||
"resetInboundClientTrafficTitle" = "Сброс трафика клиентов"
|
||||
"resetInboundClientTrafficContent" = "Вы уверены, что хотите сбросить трафик для этих клиентов?"
|
||||
|
|
@ -231,7 +231,7 @@
|
|||
"email" = "Email"
|
||||
"emailDesc" = "Пожалуйста, укажите уникальный Email"
|
||||
"IPLimit" = "Лимит по количеству IP"
|
||||
"IPLimitDesc" = "Ограничение количества одновременных подключений с разных IP(0 – отключить)"
|
||||
"IPLimitDesc" = "Ограничение числа одновременных подключений с разных IP (0 – отключить)"
|
||||
"IPLimitlog" = "Лог IP-адресов"
|
||||
"IPLimitlogDesc" = "Лог IP-адресов (перед включением лога IP-адресов, вы должны очистить лог)"
|
||||
"IPLimitlogclear" = "Очистить лог"
|
||||
|
|
@ -240,19 +240,19 @@
|
|||
"subscriptionDesc" = "Вы можете найти свою ссылку подписки в разделе 'Подробнее'"
|
||||
"info" = "Информация"
|
||||
"same" = "Тот же"
|
||||
"inboundData" = "Данные инбаундов"
|
||||
"exportInbound" = "Экспорт инбаундов"
|
||||
"inboundData" = "Данные подключений"
|
||||
"exportInbound" = "Экспорт подключений"
|
||||
"import" = "Импортировать"
|
||||
"importInbound" = "Импорт инбаундов"
|
||||
"importInbound" = "Импорт подключений"
|
||||
"periodicTrafficResetTitle" = "Сброс трафика"
|
||||
"periodicTrafficResetDesc" = "Автоматический сброс счетчика трафика через указанные интервалы"
|
||||
"lastReset" = "Последний сброс"
|
||||
|
||||
[pages.client]
|
||||
"add" = "Создать клиента"
|
||||
"add" = "Добавить клиента"
|
||||
"edit" = "Редактировать клиента"
|
||||
"submitAdd" = "Добавить"
|
||||
"submitEdit" = "Сохранить"
|
||||
"submitEdit" = "Сохранить изменения"
|
||||
"clientCount" = "Количество клиентов"
|
||||
"bulk" = "Добавить несколько"
|
||||
"method" = "Метод"
|
||||
|
|
@ -276,13 +276,13 @@
|
|||
"obtain" = "Получить"
|
||||
"updateSuccess" = "Обновление прошло успешно"
|
||||
"logCleanSuccess" = "Лог был очищен"
|
||||
"inboundsUpdateSuccess" = "Инбаунды успешно обновлены"
|
||||
"inboundUpdateSuccess" = "Инбаунд успешно обновлено"
|
||||
"inboundCreateSuccess" = "Инбаунд успешно создано"
|
||||
"inboundDeleteSuccess" = "Инбаунд успешно удалено"
|
||||
"inboundClientAddSuccess" = "Клиент(ы) инбаунда добавлен(ы)"
|
||||
"inboundClientDeleteSuccess" = "Клиент инбаунда удалён"
|
||||
"inboundClientUpdateSuccess" = "Клиент инбаунда обновлён"
|
||||
"inboundsUpdateSuccess" = "Подключения успешно обновлены"
|
||||
"inboundUpdateSuccess" = "Подключение успешно обновлено"
|
||||
"inboundCreateSuccess" = "Подключение успешно создано"
|
||||
"inboundDeleteSuccess" = "Подключение успешно удалено"
|
||||
"inboundClientAddSuccess" = "Клиент(ы) подключения добавлен(ы)"
|
||||
"inboundClientDeleteSuccess" = "Клиент подключения удалён"
|
||||
"inboundClientUpdateSuccess" = "Клиент подключения обновлён"
|
||||
"delDepletedClientsSuccess" = "Все исчерпанные клиенты удалены"
|
||||
"resetAllClientTrafficSuccess" = "Весь трафик клиента сброшен"
|
||||
"resetAllTrafficSuccess" = "Весь трафик сброшен"
|
||||
|
|
@ -310,7 +310,7 @@
|
|||
[pages.settings]
|
||||
"title" = "Настройки"
|
||||
"save" = "Сохранить"
|
||||
"infoDesc" = "Каждое внесённое изменение должно быть сохранено. Пожалуйста, перезапустите панель, чтобы изменения вступили в силу."
|
||||
"infoDesc" = "Сохраните изменения и перезапустите панель для их применения."
|
||||
"restartPanel" = "Перезапуск панели"
|
||||
"restartPanelDesc" = "Вы уверены, что хотите перезапустить панель? Подтвердите, и перезапуск произойдёт через 3 секунды. Если панель будет недоступна, проверьте лог сервера"
|
||||
"restartPanelSuccess" = "Панель успешно перезапущена"
|
||||
|
|
@ -318,11 +318,11 @@
|
|||
"resetDefaultConfig" = "Восстановить настройки по умолчанию"
|
||||
"panelSettings" = "Панель"
|
||||
"securitySettings" = "Учетная запись"
|
||||
"TGBotSettings" = "Telegram"
|
||||
"TGBotSettings" = "Telegram-Бот"
|
||||
"panelListeningIP" = "IP-адрес для управления панелью"
|
||||
"panelListeningIPDesc" = "Оставьте пустым для подключения с любого IP"
|
||||
"panelListeningDomain" = "Домен панели"
|
||||
"panelListeningDomainDesc" = "По умолчанию оставьте пустым, чтобы подключаться с любых доменов и IP-адресов"
|
||||
"panelListeningDomainDesc" = "Оставьте пустым для подключения с любых доменов и IP."
|
||||
"panelPort" = "Порт панели"
|
||||
"panelPortDesc" = "Порт, на котором работает панель"
|
||||
"publicKeyPath" = "Путь к файлу публичного ключа сертификата панели"
|
||||
|
|
@ -332,11 +332,11 @@
|
|||
"panelUrlPath" = "Корневой путь URL адреса панели"
|
||||
"panelUrlPathDesc" = "Должен начинаться с '/' и заканчиваться '/'"
|
||||
"pageSize" = "Размер нумерации страниц"
|
||||
"pageSizeDesc" = "Определить размер страницы для таблицы инбаундов. Установите 0, чтобы отключить"
|
||||
"pageSizeDesc" = "Определить размер страницы для таблицы подключений. Установите 0, чтобы отключить"
|
||||
"remarkModel" = "Модель примечания и символ разделения"
|
||||
"datepicker" = "Выбор даты"
|
||||
"datepicker" = "Тип календаря"
|
||||
"datepickerPlaceholder" = "Выберите дату"
|
||||
"datepickerDescription" = "Запланированные задачи будут выполняться в выбранное время"
|
||||
"datepickerDescription" = "Запланированные задачи будут выполняться в соответствии с этим календарем."
|
||||
"sampleRemark" = "Пример примечания"
|
||||
"oldUsername" = "Текущий логин"
|
||||
"currentPassword" = "Текущий пароль"
|
||||
|
|
@ -346,7 +346,7 @@
|
|||
"telegramBotEnableDesc" = "Доступ к функциям панели через Telegram-бота"
|
||||
"telegramToken" = "Токен Telegram бота"
|
||||
"telegramTokenDesc" = "Необходимо получить токен у менеджера ботов Telegram @botfather"
|
||||
"telegramProxy" = "Прокси Socks5"
|
||||
"telegramProxy" = "Прокси-сервер Socks5"
|
||||
"telegramProxyDesc" = "Если для подключения к Telegram вам нужен прокси Socks5, настройте его параметры согласно руководству."
|
||||
"telegramAPIServer" = "API-сервер Telegram"
|
||||
"telegramAPIServerDesc" = "Используемый API-сервер Telegram. Оставьте пустым, чтобы использовать сервер по умолчанию."
|
||||
|
|
@ -451,11 +451,11 @@
|
|||
"RoutingStrategy" = "Настройка маршрутизации доменов"
|
||||
"RoutingStrategyDesc" = "Установка общей стратегии маршрутизации разрешения DNS"
|
||||
"Torrent" = "Заблокировать BitTorrent"
|
||||
"Inbounds" = "Инбаунды"
|
||||
"Inbounds" = "Входящие подключения"
|
||||
"InboundsDesc" = "Изменение шаблона конфигурации для подключения определенных клиентов"
|
||||
"Outbounds" = "Аутбаунды"
|
||||
"Outbounds" = "Исходящие подключения"
|
||||
"Balancers" = "Балансировщик"
|
||||
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить аутбаунды для этого сервера"
|
||||
"OutboundsDesc" = "Изменение шаблона конфигурации, чтобы определить исходящие подключения для этого сервера"
|
||||
"Routings" = "Маршрутизация"
|
||||
"RoutingsDesc" = "Важен приоритет каждого правила!"
|
||||
"completeTemplate" = "Все"
|
||||
|
|
@ -486,8 +486,8 @@
|
|||
"down" = "Опустить вниз"
|
||||
"source" = "Источник"
|
||||
"dest" = "Пункт назначения"
|
||||
"inbound" = "Инбаунд"
|
||||
"outbound" = "Аутбаунд"
|
||||
"inbound" = "Входящее подключение"
|
||||
"outbound" = "Исходящее подключение"
|
||||
"balancer" = "Балансировщик"
|
||||
"info" = "Информация"
|
||||
"add" = "Создать правило"
|
||||
|
|
@ -495,9 +495,9 @@
|
|||
"useComma" = "Элементы, разделённые запятыми"
|
||||
|
||||
[pages.xray.outbound]
|
||||
"addOutbound" = "Создать аутбаунд"
|
||||
"addOutbound" = "Создать исходящее подключение"
|
||||
"addReverse" = "Создать реверс-прокси"
|
||||
"editOutbound" = "Изменить аутбаунд"
|
||||
"editOutbound" = "Изменить исходящее подключение"
|
||||
"editReverse" = "Редактировать реверс-прокси"
|
||||
"tag" = "Тег"
|
||||
"tagDesc" = "Уникальный тег"
|
||||
|
|
@ -511,7 +511,7 @@
|
|||
"intercon" = "Соединение"
|
||||
"settings" = "Настройки"
|
||||
"accountInfo" = "Информация об учетной записи"
|
||||
"outboundStatus" = "Статус аутбаунда"
|
||||
"outboundStatus" = "Статус исходящего подключения"
|
||||
"sendThrough" = "Отправить через"
|
||||
|
||||
[pages.xray.balancer]
|
||||
|
|
@ -587,8 +587,8 @@
|
|||
"modifyUser" = "Вы успешно изменили учетные данные администратора."
|
||||
"originalUserPassIncorrect" = "Неверное имя пользователя или пароль"
|
||||
"userPassMustBeNotEmpty" = "Новое имя пользователя и новый пароль должны быть заполнены"
|
||||
"getOutboundTrafficError" = "Ошибка получения трафика аутбаунда"
|
||||
"resetOutboundTrafficError" = "Ошибка сброса трафика аутбаунда"
|
||||
"getOutboundTrafficError" = "Ошибка получения трафика исходящего подключения"
|
||||
"resetOutboundTrafficError" = "Ошибка сброса трафика исходящего подключения"
|
||||
|
||||
[tgbot]
|
||||
"keyboardClosed" = "❌ Клавиатура закрыта."
|
||||
|
|
@ -596,7 +596,7 @@
|
|||
"noQuery" = "❌ Запрос не найден. Пожалуйста, повторите команду."
|
||||
"wentWrong" = "❌ Что-то пошло не так..."
|
||||
"noIpRecord" = "❗ Нет записей об IP-адресе."
|
||||
"noInbounds" = "❗ У вас не настроено ни одного инбаунда."
|
||||
"noInbounds" = "❗ У вас не настроено ни одного входящего подключения."
|
||||
"unlimited" = "♾ Безлимит"
|
||||
"add" = "Добавить"
|
||||
"month" = "Месяц"
|
||||
|
|
@ -606,7 +606,7 @@
|
|||
"hours" = "Часов"
|
||||
"minutes" = "Минуты"
|
||||
"unknown" = "Неизвестно"
|
||||
"inbounds" = "Инбаунды"
|
||||
"inbounds" = "Входящие подключения"
|
||||
"clients" = "Клиенты"
|
||||
"offline" = "🔴 Офлайн"
|
||||
"online" = "🟢 Онлайн"
|
||||
|
|
@ -620,7 +620,7 @@
|
|||
"status" = "✅ Бот функционирует нормально."
|
||||
"usage" = "❗ Пожалуйста, укажите email для поиска."
|
||||
"getID" = "🆔 Ваш User ID: <code>{{ .ID }}</code>"
|
||||
"helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\n🔎 Для поиска клиента по email:\r\n<code>/usage [Email]</code>\r\n\r\n📊 Для поиска инбаундов (со статистикой клиентов):\r\n<code>/inbound [имя подключения]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
|
||||
"helpAdminCommands" = "🔃 Для перезапуска Xray Core:\r\n<code>/restart</code>\r\n\r\n🔎 Для поиска клиента по email:\r\n<code>/usage [Email]</code>\r\n\r\n📊 Для поиска входящих подключений (со статистикой клиентов):\r\n<code>/inbound [имя подключения]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
|
||||
"helpClientCommands" = "💲 Для просмотра информации о вашей подписке используйте команду:\r\n<code>/usage [Email]</code>\r\n\r\n🆔 Ваш Telegram User ID:\r\n<code>/id</code>"
|
||||
"restartUsage" = "\r\n\r\n<code>/restart</code>"
|
||||
"restartSuccess" = "✅ Ядро Xray успешно перезапущено."
|
||||
|
|
@ -656,13 +656,14 @@
|
|||
"username" = "👤 Имя пользователя: {{ .Username }}\r\n"
|
||||
"password" = "👤 Пароль: {{ .Password }}\r\n"
|
||||
"time" = "⏰ Время: {{ .Time }}\r\n"
|
||||
"inbound" = "📍 Входящий поток: {{ .Remark }}\r\n"
|
||||
"inbound" = "📍 Входящее подключение: {{ .Remark }}\r\n"
|
||||
"port" = "🔌 Порт: {{ .Port }}\r\n"
|
||||
"expire" = "📅 Дата окончания: {{ .Time }}\r\n"
|
||||
"expireIn" = "📅 Окончание через: {{ .Time }}\r\n"
|
||||
"active" = "💡 Активен: {{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 Активен: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 Статус соединения: {{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 Был(а) в сети: {{ .Time }}\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Исходящий трафик: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 Входящий трафик: ↓{{ .Download }}\r\n"
|
||||
|
|
@ -685,12 +686,12 @@
|
|||
"pass_prompt" = "🔑 Стандартный пароль: {{ .ClientPassword }}\n\nВведите ваш пароль."
|
||||
"email_prompt" = "📧 Стандартный email: {{ .ClientEmail }}\n\nВведите ваш email."
|
||||
"comment_prompt" = "💬 Стандартный комментарий: {{ .ClientComment }}\n\nВведите ваш комментарий."
|
||||
"inbound_client_data_id" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!"
|
||||
"inbound_client_data_pass" = "🔄 Инбаунды: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Дата исчерпания: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в инбаунд!"
|
||||
"inbound_client_data_id" = "🔄 Входящие подключения: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Срок действия: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в входящее подключение!"
|
||||
"inbound_client_data_pass" = "🔄 Входящие подключения: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Трафик: {{ .ClientTraffic }}\n📅 Срок действия: {{ .ClientExp }}\n💬 Комментарий: {{ .ClientComment }}\n\nТеперь вы можете добавить клиента в входящее подключение!"
|
||||
"cancel" = "❌ Процесс отменён! \n\nВы можете снова начать с /start в любое время. 🔄"
|
||||
"error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}"
|
||||
"using_default_value" = "Используется значение по умолчанию👌"
|
||||
"incorrect_input" ="Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫"
|
||||
"error_add_client" = "⚠️ Ошибка:\n\n {{ .error }}"
|
||||
"using_default_value" = "Используется значение по умолчанию👌"
|
||||
"incorrect_input" = "Ваш ввод недействителен.\nФразы должны быть непрерывными без пробелов.\nПравильный пример: aaaaaa\nНеправильный пример: aaa aaa 🚫"
|
||||
"AreYouSure" = "Вы уверены? 🤔"
|
||||
"SuccessResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успешно"
|
||||
"FailedResetTraffic" = "📧 Почта: {{ .ClientEmail }}\n🏁 Результат: ❌ Неудача \n\n🛠️ Ошибка: [ {{ .ErrorMessage }} ]"
|
||||
|
|
@ -707,7 +708,7 @@
|
|||
"confirmToggle" = "✅ Подтвердить вкл/выкл пользователя?"
|
||||
"dbBackup" = "📂 Бэкап БД"
|
||||
"serverUsage" = "💻 Состояние сервера"
|
||||
"getInbounds" = "🔌 Инбаунды"
|
||||
"getInbounds" = "🔌 Входящие подключения"
|
||||
"depleteSoon" = "⚠️ Скоро конец"
|
||||
"clientUsage" = "Статистика клиента"
|
||||
"onlines" = "🟢 Онлайн"
|
||||
|
|
@ -731,7 +732,7 @@
|
|||
"allClients" = "👥 Все клиенты"
|
||||
"addClient" = "➕ Новый клиент"
|
||||
"submitDisable" = "Добавить отключенным ☑️"
|
||||
"submitEnable" = "Добавить включенныи ✅"
|
||||
"submitEnable" = "Добавить включенным ✅"
|
||||
"use_default" = "🏷️ Использовать по умолчанию"
|
||||
"change_id" = "⚙️🔑 ID"
|
||||
"change_password" = "⚙️🔑 Пароль"
|
||||
|
|
@ -743,7 +744,7 @@
|
|||
[tgbot.answers]
|
||||
"successfulOperation" = "✅ Успешно!"
|
||||
"errorOperation" = "❗ Ошибка в операции."
|
||||
"getInboundsFailed" = "❌ Не удалось получить инбаунды."
|
||||
"getInboundsFailed" = "❌ Не удалось получить входящие подключения."
|
||||
"getClientsFailed" = "❌ Не удалось получить клиентов."
|
||||
"canceled" = "❌ {{ .Email }}: Операция отменена."
|
||||
"clientRefreshSuccess" = "✅ {{ .Email }}: Клиент успешно обновлен."
|
||||
|
|
@ -760,5 +761,5 @@
|
|||
"enableSuccess" = "✅ {{ .Email }}: Включено успешно."
|
||||
"disableSuccess" = "✅ {{ .Email }}: Отключено успешно."
|
||||
"askToAddUserId" = "❌ Ваша конфигурация не найдена!\r\n💭 Пожалуйста, попросите администратора использовать ваш Telegram User ID в конфигурации.\r\n\r\n🆔 Ваш User ID: <code>{{ .TgUserID }}</code>"
|
||||
"chooseClient" = "Выберите клиента для инбаунда {{ .Inbound }}"
|
||||
"chooseInbound" = "Выберите инбаунд"
|
||||
"chooseClient" = "Выберите клиента для входящего подключения {{ .Inbound }}"
|
||||
"chooseInbound" = "Выберите входящее подключение"
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
"invalidFormData" = "Girdi verisi formatı geçersiz."
|
||||
"emptyUsername" = "Kullanıcı adı gerekli"
|
||||
"emptyPassword" = "Şifre gerekli"
|
||||
"wrongUsernameOrPassword" = "Geçersiz kullanıcı adı, şifre veya iki adımlı doğrulama kodu."
|
||||
"wrongUsernameOrPassword" = "Geçersiz kullanıcı adı, şifre veya iki adımlı doğrulama kodu."
|
||||
"successLogin" = "Hesabınıza başarıyla giriş yaptınız."
|
||||
|
||||
[pages.index]
|
||||
|
|
@ -565,9 +565,9 @@
|
|||
|
||||
[pages.settings.security]
|
||||
"admin" = "Yönetici kimlik bilgileri"
|
||||
"twoFactor" = "İki adımlı doğrulama"
|
||||
"twoFactorEnable" = "2FA'yı Etkinleştir"
|
||||
"twoFactorEnableDesc" = "Daha fazla güvenlik için ek bir doğrulama katmanı ekler."
|
||||
"twoFactor" = "İki adımlı doğrulama"
|
||||
"twoFactorEnable" = "2FA'yı Etkinleştir"
|
||||
"twoFactorEnableDesc" = "Daha fazla güvenlik için ek bir doğrulama katmanı ekler."
|
||||
"twoFactorModalSetTitle" = "İki adımlı doğrulamayı etkinleştir"
|
||||
"twoFactorModalDeleteTitle" = "İki adımlı doğrulamayı devre dışı bırak"
|
||||
"twoFactorModalSteps" = "İki adımlı doğrulamayı ayarlamak için şu adımları izleyin:"
|
||||
|
|
@ -663,6 +663,7 @@
|
|||
"active" = "💡 Aktif: {{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 Etkin: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 Bağlantı durumu: {{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 Son çevrimiçi: {{ .Time }}\r\n"
|
||||
"email" = "📧 E-posta: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Yükleme: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 İndirme: ↓{{ .Download }}\r\n"
|
||||
|
|
@ -688,9 +689,9 @@
|
|||
"inbound_client_data_id" = "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Kimlik: {{ .ClientId }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!"
|
||||
"inbound_client_data_pass" = "🔄 Giriş: {{ .InboundRemark }}\n\n🔑 Şifre: {{ .ClientPass }}\n📧 E-posta: {{ .ClientEmail }}\n📊 Trafik: {{ .ClientTraffic }}\n📅 Bitiş Tarihi: {{ .ClientExp }}\n🌐 IP Sınırı: {{ .IpLimit }}\n💬 Yorum: {{ .ClientComment }}\n\nArtık bu müşteriyi girişe ekleyebilirsin!"
|
||||
"cancel" = "❌ İşlem iptal edildi! \n\nİstediğiniz zaman /start ile yeniden başlayabilirsiniz. 🔄"
|
||||
"error_add_client" = "⚠️ Hata:\n\n {{ .error }}"
|
||||
"using_default_value" = "Tamam, varsayılan değeri kullanacağım. 😊"
|
||||
"incorrect_input" ="Girdiğiniz değer geçerli değil.\nKelime öbekleri boşluk olmadan devam etmelidir.\nDoğru örnek: aaaaaa\nYanlış örnek: aaa aaa 🚫"
|
||||
"error_add_client" = "⚠️ Hata:\n\n {{ .error }}"
|
||||
"using_default_value" = "Tamam, varsayılan değeri kullanacağım. 😊"
|
||||
"incorrect_input" = "Girdiğiniz değer geçerli değil.\nKelime öbekleri boşluk olmadan devam etmelidir.\nDoğru örnek: aaaaaa\nYanlış örnek: aaa aaa 🚫"
|
||||
"AreYouSure" = "Emin misin? 🤔"
|
||||
"SuccessResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ✅ Başarılı"
|
||||
"FailedResetTraffic" = "📧 E-posta: {{ .ClientEmail }}\n🏁 Sonuç: ❌ Başarısız \n\n🛠️ Hata: [ {{ .ErrorMessage }} ]"
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
"invalidFormData" = "Формат вхідних даних недійсний."
|
||||
"emptyUsername" = "Потрібне ім'я користувача"
|
||||
"emptyPassword" = "Потрібен пароль"
|
||||
"wrongUsernameOrPassword" = "Невірне ім’я користувача, пароль або код двофакторної аутентифікації."
|
||||
"wrongUsernameOrPassword" = "Невірне ім’я користувача, пароль або код двофакторної аутентифікації."
|
||||
"successLogin" = "Ви успішно увійшли до свого облікового запису."
|
||||
|
||||
[pages.index]
|
||||
|
|
@ -565,9 +565,9 @@
|
|||
|
||||
[pages.settings.security]
|
||||
"admin" = "Облікові дані адміністратора"
|
||||
"twoFactor" = "Двофакторна аутентифікація"
|
||||
"twoFactorEnable" = "Увімкнути 2FA"
|
||||
"twoFactorEnableDesc" = "Додає додатковий рівень аутентифікації для підвищення безпеки."
|
||||
"twoFactor" = "Двофакторна аутентифікація"
|
||||
"twoFactorEnable" = "Увімкнути 2FA"
|
||||
"twoFactorEnableDesc" = "Додає додатковий рівень аутентифікації для підвищення безпеки."
|
||||
"twoFactorModalSetTitle" = "Увімкнути двофакторну аутентифікацію"
|
||||
"twoFactorModalDeleteTitle" = "Вимкнути двофакторну аутентифікацію"
|
||||
"twoFactorModalSteps" = "Щоб налаштувати двофакторну аутентифікацію, виконайте кілька кроків:"
|
||||
|
|
@ -663,6 +663,7 @@
|
|||
"active" = "💡 Активний: {{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 Увімкнено: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 Стан підключення: {{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 Був(ла) онлайн: {{ .Time }}\r\n"
|
||||
"email" = "📧 Електронна пошта: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Upload: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 Download: ↓{{ .Download }}\r\n"
|
||||
|
|
@ -688,9 +689,9 @@
|
|||
"inbound_client_data_id" = "🔄 Вхід: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Електронна пошта: {{ .ClientEmail }}\n📊 Трафік: {{ .ClientTraffic }}\n📅 Дата завершення: {{ .ClientExp }}\n🌐 Обмеження IP: {{ .IpLimit }}\n💬 Коментар: {{ .ClientComment }}\n\nТепер ви можете додати клієнта до вхідного з'єднання!"
|
||||
"inbound_client_data_pass" = "🔄 Вхід: {{ .InboundRemark }}\n\n🔑 Пароль: {{ .ClientPass }}\n📧 Електронна пошта: {{ .ClientEmail }}\n📊 Трафік: {{ .ClientTraffic }}\n📅 Дата завершення: {{ .ClientExp }}\n🌐 Обмеження IP: {{ .IpLimit }}\n💬 Коментар: {{ .ClientComment }}\n\nТепер ви можете додати клієнта до вхідного з'єднання!"
|
||||
"cancel" = "❌ Процес скасовано! \n\nВи можете знову розпочати, використовуючи /start у будь-який час. 🔄"
|
||||
"error_add_client" = "⚠️ Помилка:\n\n {{ .error }}"
|
||||
"using_default_value" = "Гаразд, залишу значення за замовчуванням. 😊"
|
||||
"incorrect_input" ="Ваш ввід невірний.\nФрази повинні бути без пробілів.\nПравильний приклад: aaaaaa\nНеправильний приклад: aaa aaa 🚫"
|
||||
"error_add_client" = "⚠️ Помилка:\n\n {{ .error }}"
|
||||
"using_default_value" = "Гаразд, залишу значення за замовчуванням. 😊"
|
||||
"incorrect_input" = "Ваш ввід невірний.\nФрази повинні бути без пробілів.\nПравильний приклад: aaaaaa\nНеправильний приклад: aaa aaa 🚫"
|
||||
"AreYouSure" = "Ви впевнені? 🤔"
|
||||
"SuccessResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ✅ Успішно"
|
||||
"FailedResetTraffic" = "📧 Електронна пошта: {{ .ClientEmail }}\n🏁 Результат: ❌ Невдача \n\n🛠️ Помилка: [ {{ .ErrorMessage }} ]"
|
||||
|
|
|
|||
|
|
@ -663,6 +663,7 @@
|
|||
"active" = "💡 Đang hoạt động: {{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 Đã bật: {{ .Enable }}\r\n"
|
||||
"online" = "🌐 Trạng thái kết nối: {{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 Lần online gần nhất: {{ .Time }}\r\n"
|
||||
"email" = "📧 Email: {{ .Email }}\r\n"
|
||||
"upload" = "🔼 Tải lên: ↑{{ .Upload }}\r\n"
|
||||
"download" = "🔽 Tải xuống: ↓{{ .Download }}\r\n"
|
||||
|
|
@ -688,9 +689,9 @@
|
|||
"inbound_client_data_id" = "🔄 Kết nối vào: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 Email: {{ .ClientEmail }}\n📊 Dung lượng: {{ .ClientTraffic }}\n📅 Ngày hết hạn: {{ .ClientExp }}\n🌐 Giới hạn IP: {{ .IpLimit }}\n💬 Ghi chú: {{ .ClientComment }}\n\nBây giờ bạn có thể thêm khách hàng vào inbound!"
|
||||
"inbound_client_data_pass" = "🔄 Kết nối vào: {{ .InboundRemark }}\n\n🔑 Mật khẩu: {{ .ClientPass }}\n📧 Email: {{ .ClientEmail }}\n📊 Dung lượng: {{ .ClientTraffic }}\n📅 Ngày hết hạn: {{ .ClientExp }}\n🌐 Giới hạn IP: {{ .IpLimit }}\n💬 Ghi chú: {{ .ClientComment }}\n\nBây giờ bạn có thể thêm khách hàng vào inbound!"
|
||||
"cancel" = "❌ Quá trình đã bị hủy! \n\nBạn có thể bắt đầu lại bất cứ lúc nào bằng cách nhập /start. 🔄"
|
||||
"error_add_client" = "⚠️ Lỗi:\n\n {{ .error }}"
|
||||
"using_default_value" = "Được rồi, tôi sẽ sử dụng giá trị mặc định. 😊"
|
||||
"incorrect_input" ="Dữ liệu bạn nhập không hợp lệ.\nCác chuỗi phải liền mạch và không có dấu cách.\nVí dụ đúng: aaaaaa\nVí dụ sai: aaa aaa 🚫"
|
||||
"error_add_client" = "⚠️ Lỗi:\n\n {{ .error }}"
|
||||
"using_default_value" = "Được rồi, tôi sẽ sử dụng giá trị mặc định. 😊"
|
||||
"incorrect_input" = "Dữ liệu bạn nhập không hợp lệ.\nCác chuỗi phải liền mạch và không có dấu cách.\nVí dụ đúng: aaaaaa\nVí dụ sai: aaa aaa 🚫"
|
||||
"AreYouSure" = "Bạn có chắc không? 🤔"
|
||||
"SuccessResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ✅ Thành công"
|
||||
"FailedResetTraffic" = "📧 Email: {{ .ClientEmail }}\n🏁 Kết quả: ❌ Thất bại \n\n🛠️ Lỗi: [ {{ .ErrorMessage }} ]"
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
"invalidFormData" = "数据格式错误"
|
||||
"emptyUsername" = "请输入用户名"
|
||||
"emptyPassword" = "请输入密码"
|
||||
"wrongUsernameOrPassword" = "用户名、密码或双重验证码无效。"
|
||||
"wrongUsernameOrPassword" = "用户名、密码或双重验证码无效。"
|
||||
"successLogin" = "您已成功登录您的账户。"
|
||||
|
||||
[pages.index]
|
||||
|
|
@ -242,7 +242,7 @@
|
|||
"same" = "相同"
|
||||
"inboundData" = "入站数据"
|
||||
"exportInbound" = "导出入站规则"
|
||||
"import"="导入"
|
||||
"import" = "导入"
|
||||
"importInbound" = "导入入站规则"
|
||||
"periodicTrafficResetTitle" = "流量重置"
|
||||
"periodicTrafficResetDesc" = "按指定间隔自动重置流量计数器"
|
||||
|
|
@ -565,9 +565,9 @@
|
|||
|
||||
[pages.settings.security]
|
||||
"admin" = "管理员凭据"
|
||||
"twoFactor" = "双重验证"
|
||||
"twoFactorEnable" = "启用2FA"
|
||||
"twoFactorEnableDesc" = "增加额外的验证层以提高安全性。"
|
||||
"twoFactor" = "双重验证"
|
||||
"twoFactorEnable" = "启用2FA"
|
||||
"twoFactorEnableDesc" = "增加额外的验证层以提高安全性。"
|
||||
"twoFactorModalSetTitle" = "启用双重认证"
|
||||
"twoFactorModalDeleteTitle" = "停用双重认证"
|
||||
"twoFactorModalSteps" = "要设定双重认证,请执行以下步骤:"
|
||||
|
|
@ -663,6 +663,7 @@
|
|||
"active" = "💡 激活:{{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 已启用:{{ .Enable }}\r\n"
|
||||
"online" = "🌐 连接状态:{{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 上次在线: {{ .Time }}\r\n"
|
||||
"email" = "📧 邮箱:{{ .Email }}\r\n"
|
||||
"upload" = "🔼 上传↑:{{ .Upload }}\r\n"
|
||||
"download" = "🔽 下载↓:{{ .Download }}\r\n"
|
||||
|
|
@ -688,9 +689,9 @@
|
|||
"inbound_client_data_id" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 邮箱: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日期: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 备注: {{ .ClientComment }}\n\n你现在可以将客户添加到入站了!"
|
||||
"inbound_client_data_pass" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 密码: {{ .ClientPass }}\n📧 邮箱: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日期: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 备注: {{ .ClientComment }}\n\n你现在可以将客户添加到入站了!"
|
||||
"cancel" = "❌ 进程已取消!\n\n您可以随时使用 /start 重新开始。 🔄"
|
||||
"error_add_client" = "⚠️ 错误:\n\n {{ .error }}"
|
||||
"using_default_value" = "好的,我会使用默认值。 😊"
|
||||
"incorrect_input" ="您的输入无效。\n短语应连续输入,不能有空格。\n正确示例: aaaaaa\n错误示例: aaa aaa 🚫"
|
||||
"error_add_client" = "⚠️ 错误:\n\n {{ .error }}"
|
||||
"using_default_value" = "好的,我会使用默认值。 😊"
|
||||
"incorrect_input" = "您的输入无效。\n短语应连续输入,不能有空格。\n正确示例: aaaaaa\n错误示例: aaa aaa 🚫"
|
||||
"AreYouSure" = "你确定吗?🤔"
|
||||
"SuccessResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ✅ 成功"
|
||||
"FailedResetTraffic" = "📧 邮箱: {{ .ClientEmail }}\n🏁 结果: ❌ 失败 \n\n🛠️ 错误: [ {{ .ErrorMessage }} ]"
|
||||
|
|
|
|||
|
|
@ -106,7 +106,7 @@
|
|||
"invalidFormData" = "資料格式錯誤"
|
||||
"emptyUsername" = "請輸入使用者名稱"
|
||||
"emptyPassword" = "請輸入密碼"
|
||||
"wrongUsernameOrPassword" = "用戶名、密碼或雙重驗證碼無效。"
|
||||
"wrongUsernameOrPassword" = "用戶名、密碼或雙重驗證碼無效。"
|
||||
"successLogin" = "您已成功登入您的帳戶。"
|
||||
|
||||
[pages.index]
|
||||
|
|
@ -242,7 +242,7 @@
|
|||
"same" = "相同"
|
||||
"inboundData" = "入站資料"
|
||||
"exportInbound" = "匯出入站規則"
|
||||
"import"="匯入"
|
||||
"import" = "匯入"
|
||||
"importInbound" = "匯入入站規則"
|
||||
"periodicTrafficResetTitle" = "流量重置"
|
||||
"periodicTrafficResetDesc" = "按指定間隔自動重置流量計數器"
|
||||
|
|
@ -565,9 +565,9 @@
|
|||
|
||||
[pages.settings.security]
|
||||
"admin" = "管理員憑證"
|
||||
"twoFactor" = "雙重驗證"
|
||||
"twoFactorEnable" = "啟用2FA"
|
||||
"twoFactorEnableDesc" = "增加額外的驗證層以提高安全性。"
|
||||
"twoFactor" = "雙重驗證"
|
||||
"twoFactorEnable" = "啟用2FA"
|
||||
"twoFactorEnableDesc" = "增加額外的驗證層以提高安全性。"
|
||||
"twoFactorModalSetTitle" = "啟用雙重認證"
|
||||
"twoFactorModalDeleteTitle" = "停用雙重認證"
|
||||
"twoFactorModalSteps" = "要設定雙重認證,請執行以下步驟:"
|
||||
|
|
@ -663,6 +663,7 @@
|
|||
"active" = "💡 啟用:{{ .Enable }}\r\n"
|
||||
"enabled" = "🚨 已啟用:{{ .Enable }}\r\n"
|
||||
"online" = "🌐 連線狀態:{{ .Status }}\r\n"
|
||||
"lastOnline" = "🔙 上次上線: {{ .Time }}\r\n"
|
||||
"email" = "📧 郵箱:{{ .Email }}\r\n"
|
||||
"upload" = "🔼 上傳↑:{{ .Upload }}\r\n"
|
||||
"download" = "🔽 下載↓:{{ .Download }}\r\n"
|
||||
|
|
@ -688,9 +689,9 @@
|
|||
"inbound_client_data_id" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 ID: {{ .ClientId }}\n📧 電子郵件: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 備註: {{ .ClientComment }}\n\n你現在可以將客戶加入入站了!"
|
||||
"inbound_client_data_pass" = "🔄 入站: {{ .InboundRemark }}\n\n🔑 密碼: {{ .ClientPass }}\n📧 電子郵件: {{ .ClientEmail }}\n📊 流量: {{ .ClientTraffic }}\n📅 到期日: {{ .ClientExp }}\n🌐 IP 限制: {{ .IpLimit }}\n💬 備註: {{ .ClientComment }}\n\n你現在可以將客戶加入入站了!"
|
||||
"cancel" = "❌ 程序已取消!\n\n您可以隨時使用 /start 重新開始。 🔄"
|
||||
"error_add_client" = "⚠️ 錯誤:\n\n {{ .error }}"
|
||||
"using_default_value" = "好的,我會使用預設值。 😊"
|
||||
"incorrect_input" ="您的輸入無效。\n短語應連續輸入,不能有空格。\n正確示例: aaaaaa\n錯誤示例: aaa aaa 🚫"
|
||||
"error_add_client" = "⚠️ 錯誤:\n\n {{ .error }}"
|
||||
"using_default_value" = "好的,我會使用預設值。 😊"
|
||||
"incorrect_input" = "您的輸入無效。\n短語應連續輸入,不能有空格。\n正確示例: aaaaaa\n錯誤示例: aaa aaa 🚫"
|
||||
"AreYouSure" = "你確定嗎?🤔"
|
||||
"SuccessResetTraffic" = "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ✅ 成功"
|
||||
"FailedResetTraffic" = "📧 電子郵件: {{ .ClientEmail }}\n🏁 結果: ❌ 失敗 \n\n🛠️ 錯誤: [ {{ .ErrorMessage }} ]"
|
||||
|
|
|
|||
149
x-ui.sh
149
x-ui.sh
|
|
@ -509,12 +509,16 @@ enable_bbr() {
|
|||
ubuntu | debian | armbian)
|
||||
apt-get update && apt-get install -yqq --no-install-recommends ca-certificates
|
||||
;;
|
||||
centos | rhel | almalinux | rocky | ol)
|
||||
yum -y update && yum -y install ca-certificates
|
||||
;;
|
||||
fedora | amzn | virtuozzo)
|
||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||
dnf -y update && dnf -y install ca-certificates
|
||||
;;
|
||||
centos)
|
||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||
yum -y update && yum -y install ca-certificates
|
||||
else
|
||||
dnf -y update && dnf -y install ca-certificates
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Sy --noconfirm ca-certificates
|
||||
;;
|
||||
|
|
@ -863,10 +867,32 @@ delete_ports() {
|
|||
fi
|
||||
}
|
||||
|
||||
update_all_geofiles() {
|
||||
update_main_geofiles
|
||||
update_ir_geofiles
|
||||
update_ru_geofiles
|
||||
}
|
||||
|
||||
update_main_geofiles() {
|
||||
wget -O geoip.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget -O geosite.dat https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
}
|
||||
|
||||
update_ir_geofiles() {
|
||||
wget -O geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -O geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
}
|
||||
|
||||
update_ru_geofiles() {
|
||||
wget -O geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget -O geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
}
|
||||
|
||||
update_geo() {
|
||||
echo -e "${green}\t1.${plain} Loyalsoldier (geoip.dat, geosite.dat)"
|
||||
echo -e "${green}\t2.${plain} chocolate4u (geoip_IR.dat, geosite_IR.dat)"
|
||||
echo -e "${green}\t3.${plain} runetfreedom (geoip_RU.dat, geosite_RU.dat)"
|
||||
echo -e "${green}\t4.${plain} All"
|
||||
echo -e "${green}\t0.${plain} Back to Main Menu"
|
||||
read -rp "Choose an option: " choice
|
||||
|
||||
|
|
@ -877,41 +903,25 @@ update_geo() {
|
|||
show_menu
|
||||
;;
|
||||
1)
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui stop
|
||||
else
|
||||
systemctl stop x-ui
|
||||
fi
|
||||
rm -f geoip.dat geosite.dat
|
||||
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget -N https://github.com/Loyalsoldier/v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
update_main_geofiles
|
||||
echo -e "${green}Loyalsoldier datasets have been updated successfully!${plain}"
|
||||
restart
|
||||
;;
|
||||
2)
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui stop
|
||||
else
|
||||
systemctl stop x-ui
|
||||
fi
|
||||
rm -f geoip_IR.dat geosite_IR.dat
|
||||
wget -O geoip_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geoip.dat
|
||||
wget -O geosite_IR.dat -N https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
|
||||
update_ir_geofiles
|
||||
echo -e "${green}chocolate4u datasets have been updated successfully!${plain}"
|
||||
restart
|
||||
;;
|
||||
3)
|
||||
if [[ $release == "alpine" ]]; then
|
||||
rc-service x-ui stop
|
||||
else
|
||||
systemctl stop x-ui
|
||||
fi
|
||||
rm -f geoip_RU.dat geosite_RU.dat
|
||||
wget -O geoip_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
|
||||
wget -O geosite_RU.dat -N https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
|
||||
update_ru_geofiles
|
||||
echo -e "${green}runetfreedom datasets have been updated successfully!${plain}"
|
||||
restart
|
||||
;;
|
||||
4)
|
||||
update_all_geofiles
|
||||
echo -e "${green}All geo files have been updated successfully!${plain}"
|
||||
restart
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Invalid option. Please select a valid number.${plain}\n"
|
||||
update_geo
|
||||
|
|
@ -1067,12 +1077,16 @@ ssl_cert_issue() {
|
|||
ubuntu | debian | armbian)
|
||||
apt-get update && apt-get install socat -y
|
||||
;;
|
||||
centos | rhel | almalinux | rocky | ol)
|
||||
yum -y update && yum -y install socat
|
||||
;;
|
||||
fedora | amzn | virtuozzo)
|
||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||
dnf -y update && dnf -y install socat
|
||||
;;
|
||||
centos)
|
||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||
yum -y update && yum -y install socat
|
||||
else
|
||||
dnf -y update && dnf -y install socat
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Sy --noconfirm socat
|
||||
;;
|
||||
|
|
@ -1080,7 +1094,7 @@ ssl_cert_issue() {
|
|||
zypper refresh && zypper -q install -y socat
|
||||
;;
|
||||
alpine)
|
||||
apk add socat
|
||||
apk add socat curl openssl
|
||||
;;
|
||||
*)
|
||||
echo -e "${red}Unsupported operating system. Please check the script and install the necessary packages manually.${plain}\n"
|
||||
|
|
@ -1531,13 +1545,17 @@ install_iplimit() {
|
|||
armbian)
|
||||
apt-get update && apt-get install fail2ban -y
|
||||
;;
|
||||
centos | rhel | almalinux | rocky | ol)
|
||||
yum update -y && yum install epel-release -y
|
||||
yum -y install fail2ban
|
||||
;;
|
||||
fedora | amzn | virtuozzo)
|
||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||
dnf -y update && dnf -y install fail2ban
|
||||
;;
|
||||
centos)
|
||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||
yum update -y && yum install epel-release -y
|
||||
yum -y install fail2ban
|
||||
else
|
||||
dnf -y update && dnf -y install fail2ban
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Syu --noconfirm fail2ban
|
||||
;;
|
||||
|
|
@ -1631,14 +1649,19 @@ remove_iplimit() {
|
|||
apt-get purge -y fail2ban -y
|
||||
apt-get autoremove -y
|
||||
;;
|
||||
centos | rhel | almalinux | rocky | ol)
|
||||
yum remove fail2ban -y
|
||||
yum autoremove -y
|
||||
;;
|
||||
fedora | amzn | virtuozzo)
|
||||
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
|
||||
dnf remove fail2ban -y
|
||||
dnf autoremove -y
|
||||
;;
|
||||
centos)
|
||||
if [[ "${VERSION_ID}" =~ ^7 ]]; then
|
||||
yum remove fail2ban -y
|
||||
yum autoremove -y
|
||||
else
|
||||
dnf remove fail2ban -y
|
||||
dnf autoremove -y
|
||||
fi
|
||||
;;
|
||||
arch | manjaro | parch)
|
||||
pacman -Rns --noconfirm fail2ban
|
||||
;;
|
||||
|
|
@ -1869,24 +1892,25 @@ SSH_port_forwarding() {
|
|||
}
|
||||
|
||||
show_usage() {
|
||||
echo -e "┌───────────────────────────────────────────────────────┐
|
||||
│ ${blue}x-ui control menu usages (subcommands):${plain} │
|
||||
│ │
|
||||
│ ${blue}x-ui${plain} - Admin Management Script │
|
||||
│ ${blue}x-ui start${plain} - Start │
|
||||
│ ${blue}x-ui stop${plain} - Stop │
|
||||
│ ${blue}x-ui restart${plain} - Restart │
|
||||
│ ${blue}x-ui status${plain} - Current Status │
|
||||
│ ${blue}x-ui settings${plain} - Current Settings │
|
||||
│ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │
|
||||
│ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup │
|
||||
│ ${blue}x-ui log${plain} - Check logs │
|
||||
│ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
|
||||
│ ${blue}x-ui update${plain} - Update │
|
||||
│ ${blue}x-ui legacy${plain} - legacy version │
|
||||
│ ${blue}x-ui install${plain} - Install │
|
||||
│ ${blue}x-ui uninstall${plain} - Uninstall │
|
||||
└───────────────────────────────────────────────────────┘"
|
||||
echo -e "┌────────────────────────────────────────────────────────────────┐
|
||||
│ ${blue}x-ui control menu usages (subcommands):${plain} │
|
||||
│ │
|
||||
│ ${blue}x-ui${plain} - Admin Management Script │
|
||||
│ ${blue}x-ui start${plain} - Start │
|
||||
│ ${blue}x-ui stop${plain} - Stop │
|
||||
│ ${blue}x-ui restart${plain} - Restart │
|
||||
│ ${blue}x-ui status${plain} - Current Status │
|
||||
│ ${blue}x-ui settings${plain} - Current Settings │
|
||||
│ ${blue}x-ui enable${plain} - Enable Autostart on OS Startup │
|
||||
│ ${blue}x-ui disable${plain} - Disable Autostart on OS Startup │
|
||||
│ ${blue}x-ui log${plain} - Check logs │
|
||||
│ ${blue}x-ui banlog${plain} - Check Fail2ban ban logs │
|
||||
│ ${blue}x-ui update${plain} - Update │
|
||||
│ ${blue}x-ui update-all-geofiles${plain} - Update all geo files │
|
||||
│ ${blue}x-ui legacy${plain} - Legacy version │
|
||||
│ ${blue}x-ui install${plain} - Install │
|
||||
│ ${blue}x-ui uninstall${plain} - Uninstall │
|
||||
└────────────────────────────────────────────────────────────────┘"
|
||||
}
|
||||
|
||||
show_menu() {
|
||||
|
|
@ -2056,6 +2080,9 @@ if [[ $# > 0 ]]; then
|
|||
"uninstall")
|
||||
check_install 0 && uninstall 0
|
||||
;;
|
||||
"update-all-geofiles")
|
||||
check_install 0 && update_all_geofiles 0 && restart 0
|
||||
;;
|
||||
*) show_usage ;;
|
||||
esac
|
||||
else
|
||||
|
|
|
|||
Loading…
Reference in a new issue