Merge upstream MHSanaei/main into PR branch (resolve conflicts in favor of PR redesign)

This commit is contained in:
lolka1333 2026-05-05 08:30:55 +02:00
commit 691e7b5e75
98 changed files with 4621 additions and 4723 deletions

View file

@ -37,4 +37,4 @@ curl -sfLRo geoip_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/release
curl -sfLRo geosite_IR.dat https://github.com/chocolate4u/Iran-v2ray-rules/releases/latest/download/geosite.dat
curl -sfLRo geoip_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geoip.dat
curl -sfLRo geosite_RU.dat https://github.com/runetfreedom/russia-v2ray-rules-dat/releases/latest/download/geosite.dat
cd ../../
cd ../../

20
go.mod
View file

@ -15,9 +15,9 @@ require (
github.com/mymmrac/telego v1.8.0
github.com/nicksnyder/go-i18n/v2 v2.6.1
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.3.0
github.com/pelletier/go-toml/v2 v2.3.1
github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.26.3
github.com/shirou/gopsutil/v4 v4.26.4
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/valyala/fasthttp v1.70.0
github.com/xlzd/gotp v0.1.0
@ -26,7 +26,7 @@ require (
golang.org/x/crypto v0.50.0
golang.org/x/sys v0.43.0
golang.org/x/text v0.36.0
google.golang.org/grpc v1.80.0
google.golang.org/grpc v1.81.0
gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.1
)
@ -36,10 +36,10 @@ require (
github.com/andybalholm/brotli v1.2.1 // indirect
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 // indirect
github.com/bytedance/gopkg v0.1.4 // indirect
github.com/bytedance/sonic v1.15.0 // indirect
github.com/bytedance/sonic v1.15.1 // indirect
github.com/bytedance/sonic/loader v0.5.1 // indirect
github.com/cloudflare/circl v1.6.3 // indirect
github.com/cloudwego/base64x v0.1.6 // indirect
github.com/cloudwego/base64x v0.1.7 // indirect
github.com/ebitengine/purego v0.10.0 // indirect
github.com/gabriel-vasile/mimetype v1.4.13 // indirect
github.com/gin-contrib/sse v1.1.1 // indirect
@ -57,12 +57,12 @@ 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.5 // indirect
github.com/klauspost/compress v1.18.6 // indirect
github.com/klauspost/cpuid/v2 v2.3.0 // indirect
github.com/leodido/go-urn v1.4.0 // indirect
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e // indirect
github.com/mattn/go-isatty v0.0.21 // indirect
github.com/mattn/go-sqlite3 v1.14.42 // indirect
github.com/mattn/go-isatty v0.0.22 // indirect
github.com/mattn/go-sqlite3 v1.14.44 // indirect
github.com/miekg/dns v1.1.72 // indirect
github.com/modern-go/concurrent v0.0.0-20180306012644-bacd9c7ef1dd // indirect
github.com/modern-go/reflect2 v1.0.2 // indirect
@ -84,7 +84,7 @@ require (
github.com/vishvananda/netns v0.0.5 // indirect
github.com/xtls/reality v0.0.0-20260322125925-9234c772ba8f // indirect
github.com/yusufpapurcu/wmi v1.2.4 // indirect
go.mongodb.org/mongo-driver/v2 v2.5.1 // indirect
go.mongodb.org/mongo-driver/v2 v2.6.0 // indirect
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.26.0 // indirect
golang.org/x/exp v0.0.0-20260410095643-746e56fc9e2f // indirect
@ -95,7 +95,7 @@ require (
golang.org/x/tools v0.44.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-20260420184626-e10c466a9529 // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 // indirect
google.golang.org/protobuf v1.36.11 // indirect
gvisor.dev/gvisor v0.0.0-20260122175437-89a5d21be8f0 // indirect
lukechampine.com/blake3 v1.4.1 // indirect

60
go.sum
View file

@ -10,16 +10,16 @@ github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22 h1:00ziBGnLWQEc
github.com/apernet/quic-go v0.59.1-0.20260217092621-db4786c77a22/go.mod h1:Npbg8qBtAZlsAB3FWmqwlVh5jtVG6a4DlYsOylUpvzA=
github.com/bytedance/gopkg v0.1.4 h1:oZnQwnX82KAIWb7033bEwtxvTqXcYMxDBaQxo5JJHWM=
github.com/bytedance/gopkg v0.1.4/go.mod h1:v1zWfPm21Fb+OsyXN2VAHdL6TBb2L88anLQgdyje6R4=
github.com/bytedance/sonic v1.15.0 h1:/PXeWFaR5ElNcVE84U0dOHjiMHQOwNIx3K4ymzh/uSE=
github.com/bytedance/sonic v1.15.0/go.mod h1:tFkWrPz0/CUCLEF4ri4UkHekCIcdnkqXw9VduqpJh0k=
github.com/bytedance/sonic v1.15.1 h1:nJD5PmM0vY7J8CT6MxoqbVAAMhkSmV2HgRAUrrpLoOw=
github.com/bytedance/sonic v1.15.1/go.mod h1:mT2NbXunuaEbnZ+mRIX/vYqKISmgEuHFDI4UzmKx2SA=
github.com/bytedance/sonic/loader v0.5.1 h1:Ygpfa9zwRCCKSlrp5bBP/b/Xzc3VxsAW+5NIYXrOOpI=
github.com/bytedance/sonic/loader v0.5.1/go.mod h1:AR4NYCk5DdzZizZ5djGqQ92eEhCCcdf5x77udYiSJRo=
github.com/cespare/xxhash/v2 v2.3.0 h1:UL815xU9SqsFlibzuggzjXhog7bL6oX9BbNZnL2UFvs=
github.com/cespare/xxhash/v2 v2.3.0/go.mod h1:VGX0DQ3Q6kWi7AoAeZDth3/j3BFtOZR5XLFGgcrjCOs=
github.com/cloudflare/circl v1.6.3 h1:9GPOhQGF9MCYUeXyMYlqTR6a5gTrgR/fBLXvUgtVcg8=
github.com/cloudflare/circl v1.6.3/go.mod h1:2eXP6Qfat4O/Yhh8BznvKnJ+uzEoTQ6jVKJRn81BiS4=
github.com/cloudwego/base64x v0.1.6 h1:t11wG9AECkCDk5fMSoxmufanudBtJ+/HemLstXDLI2M=
github.com/cloudwego/base64x v0.1.6/go.mod h1:OFcloc187FXDaYHvrNIjxSe8ncn0OOM8gEHfghB2IPU=
github.com/cloudwego/base64x v0.1.7 h1:NppS+Fgzg5ovhn4NkUXaDT3x9jldgH5ToMCqzBSi2zI=
github.com/cloudwego/base64x v0.1.7/go.mod h1:Cu1PV9zfrSf7ET2tIbWbbEy7jO7HHJ13q4X2SQ8aWYg=
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=
@ -107,8 +107,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.5 h1:/h1gH5Ce+VWNLSWqPzOVn6XBO+vJbCNGvjoaGBFW2IE=
github.com/klauspost/compress v1.18.5/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
github.com/klauspost/compress v1.18.6 h1:2jupLlAwFm95+YDR+NwD2MEfFO9d4z4Prjl1XXDjuao=
github.com/klauspost/compress v1.18.6/go.mod h1:cwPg85FWrGar70rWktvGQj8/hthj3wpl0PGDogxkrSQ=
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=
@ -119,10 +119,10 @@ 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-20260330125221-c963978e514e h1:Q6MvJtQK/iRcRtzAscm/zF23XxJlbECiGPyRicsX+Ak=
github.com/lufia/plan9stats v0.0.0-20260330125221-c963978e514e/go.mod h1:autxFIvghDt3jPTLoqZ9OZ7s9qTGNAWmYCjVFWPX/zg=
github.com/mattn/go-isatty v0.0.21 h1:xYae+lCNBP7QuW4PUnNG61ffM4hVIfm+zUzDuSzYLGs=
github.com/mattn/go-isatty v0.0.21/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-sqlite3 v1.14.42 h1:MigqEP4ZmHw3aIdIT7T+9TLa90Z6smwcthx+Azv4Cgo=
github.com/mattn/go-sqlite3 v1.14.42/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/mattn/go-isatty v0.0.22 h1:j8l17JJ9i6VGPUFUYoTUKPSgKe/83EYU2zBC7YNKMw4=
github.com/mattn/go-isatty v0.0.22/go.mod h1:ZXfXG4SQHsB/w3ZeOYbR0PrPwLy+n6xiMrJlRFqopa4=
github.com/mattn/go-sqlite3 v1.14.44 h1:3VSe+xafpbzsLbdr2AWlAZk9yRHiBhTBakioXaCKTF8=
github.com/mattn/go-sqlite3 v1.14.44/go.mod h1:pjEuOr8IwzLJP2MfGeTb0A35jauH+C2kbHKBr7yXKVQ=
github.com/miekg/dns v1.1.72 h1:vhmr+TF2A3tuoGNkLDFK9zi36F2LS+hKTRW0Uf8kbzI=
github.com/miekg/dns v1.1.72/go.mod h1:+EuEPhdHOsfk6Wk5TT2CzssZdqkmFhf8r+aVyDEToIs=
github.com/modern-go/concurrent v0.0.0-20180228061459-e0a39a4cb421/go.mod h1:6dJC0mAP4ikYIbvyc7fijjWJddQyLn8Ig3JB5CqoB9Q=
@ -138,8 +138,8 @@ github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 h1:lDH9UUVJtmYCjyT0C
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7/go.mod h1:HzydrMdWErDVzsI23lYNej1Htcns9BCg93Dk0bBINWk=
github.com/pelletier/go-toml v1.9.5 h1:4yBQzkHv+7BHq2PQUZF3Mx0IYxG7LsP222s7Agd3ve8=
github.com/pelletier/go-toml v1.9.5/go.mod h1:u1nR/EPcESfeI/szUZKdtJ0xRNbUoANCkoOuaOx1Y+c=
github.com/pelletier/go-toml/v2 v2.3.0 h1:k59bC/lIZREW0/iVaQR8nDHxVq8OVlIzYCOJf421CaM=
github.com/pelletier/go-toml/v2 v2.3.0/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pelletier/go-toml/v2 v2.3.1 h1:MYEvvGnQjeNkRF1qUuGolNtNExTDwct51yp7olPtrEc=
github.com/pelletier/go-toml/v2 v2.3.1/go.mod h1:2gIqNv+qfxSVS7cM2xJQKtLSTLUE9V8t9Stt+h56mCY=
github.com/pires/go-proxyproto v0.12.0 h1:TTCxD66dU898tahivkqc3hoceZp7P44FnorWyo9d5vM=
github.com/pires/go-proxyproto v0.12.0/go.mod h1:qUvfqUMEoX7T8g0q7TQLDnhMjdTrxnG0hvpMn+7ePNI=
github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
@ -160,8 +160,8 @@ github.com/sagernet/sing v0.8.9 h1:iX8FyMrWNl/divVgTe7cLT9n36v6bfzfnCYlcM1cLaU=
github.com/sagernet/sing v0.8.9/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/shirou/gopsutil/v4 v4.26.3 h1:2ESdQt90yU3oXF/CdOlRCJxrP+Am1aBYubTMTfxJ1qc=
github.com/shirou/gopsutil/v4 v4.26.3/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
github.com/shirou/gopsutil/v4 v4.26.4 h1:B4SXVbcwTyrocPHEmWBC4uCYr4Xcu3MK1TXqbprAOWY=
github.com/shirou/gopsutil/v4 v4.26.4/go.mod h1:LZ6ewCSkBqUpvSOf+LsTGnRinC6iaNUNMGBtDkJBaLQ=
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=
@ -203,20 +203,20 @@ github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZ
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.mongodb.org/mongo-driver/v2 v2.5.1 h1:j2U/Qp+wvueSpqitLCSZPT/+ZpVc1xzuwdHWwl7d8ro=
go.mongodb.org/mongo-driver/v2 v2.5.1/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
go.mongodb.org/mongo-driver/v2 v2.6.0 h1:b9sJOYrkmt4l8bY43ZenFBcPlhYIjaOfYHLtbB/5qi8=
go.mongodb.org/mongo-driver/v2 v2.6.0/go.mod h1:yOI9kBsufol30iFsl1slpdq1I0eHPzybRWdyYUs8K/0=
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.39.0 h1:8yPrr/S0ND9QEfTfdP9V+SiwT4E0G7Y5MO7p85nis48=
go.opentelemetry.io/otel v1.39.0/go.mod h1:kLlFTywNWrFyEdH0oj2xK0bFYZtHRYUdv1NklR/tgc8=
go.opentelemetry.io/otel/metric v1.39.0 h1:d1UzonvEZriVfpNKEVmHXbdf909uGTOQjA0HF0Ls5Q0=
go.opentelemetry.io/otel/metric v1.39.0/go.mod h1:jrZSWL33sD7bBxg1xjrqyDjnuzTUB0x1nBERXd7Ftcs=
go.opentelemetry.io/otel/sdk v1.39.0 h1:nMLYcjVsvdui1B/4FRkwjzoRVsMK8uL/cj0OyhKzt18=
go.opentelemetry.io/otel/sdk v1.39.0/go.mod h1:vDojkC4/jsTJsE+kh+LXYQlbL8CgrEcwmt1ENZszdJE=
go.opentelemetry.io/otel/sdk/metric v1.39.0 h1:cXMVVFVgsIf2YL6QkRF4Urbr/aMInf+2WKg+sEJTtB8=
go.opentelemetry.io/otel/sdk/metric v1.39.0/go.mod h1:xq9HEVH7qeX69/JnwEfp6fVq5wosJsY1mt4lLfYdVew=
go.opentelemetry.io/otel/trace v1.39.0 h1:2d2vfpEDmCJ5zVYz7ijaJdOF59xLomrvj7bjt6/qCJI=
go.opentelemetry.io/otel/trace v1.39.0/go.mod h1:88w4/PnZSazkGzz/w84VHpQafiU4EtqqlVdxWy+rNOA=
go.opentelemetry.io/otel v1.43.0 h1:mYIM03dnh5zfN7HautFE4ieIig9amkNANT+xcVxAj9I=
go.opentelemetry.io/otel v1.43.0/go.mod h1:JuG+u74mvjvcm8vj8pI5XiHy1zDeoCS2LB1spIq7Ay0=
go.opentelemetry.io/otel/metric v1.43.0 h1:d7638QeInOnuwOONPp4JAOGfbCEpYb+K6DVWvdxGzgM=
go.opentelemetry.io/otel/metric v1.43.0/go.mod h1:RDnPtIxvqlgO8GRW18W6Z/4P462ldprJtfxHxyKd2PY=
go.opentelemetry.io/otel/sdk v1.43.0 h1:pi5mE86i5rTeLXqoF/hhiBtUNcrAGHLKQdhg4h4V9Dg=
go.opentelemetry.io/otel/sdk v1.43.0/go.mod h1:P+IkVU3iWukmiit/Yf9AWvpyRDlUeBaRg6Y+C58QHzg=
go.opentelemetry.io/otel/sdk/metric v1.43.0 h1:S88dyqXjJkuBNLeMcVPRFXpRw2fuwdvfCGLEo89fDkw=
go.opentelemetry.io/otel/sdk/metric v1.43.0/go.mod h1:C/RJtwSEJ5hzTiUz5pXF1kILHStzb9zFlIEe85bhj6A=
go.opentelemetry.io/otel/trace v1.43.0 h1:BkNrHpup+4k4w+ZZ86CZoHHEkohws8AY+WTX09nk+3A=
go.opentelemetry.io/otel/trace v1.43.0/go.mod h1:/QJhyVBUUswCphDVxq+8mld+AvhXZLhe+8WVFxiFff0=
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=
@ -256,10 +256,10 @@ golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb h1:whnFRlWMcXI9d+Z
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gonum.org/v1/gonum v0.17.0 h1:VbpOemQlsSMrYmn7T2OUvQ4dqxQXU+ouZFQsZOx50z4=
gonum.org/v1/gonum v0.17.0/go.mod h1:El3tOrEuMpv2UdMrbNlKEh9vd86bmQ6vqIcDwxEOc1E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529 h1:XF8+t6QQiS0o9ArVan/HW8Q7cycNPGsJf6GA2nXxYAg=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260420184626-e10c466a9529/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.80.0 h1:Xr6m2WmWZLETvUNvIUmeD5OAagMw3FiKmMlTdViWsHM=
google.golang.org/grpc v1.80.0/go.mod h1:ho/dLnxwi3EDJA4Zghp7k2Ec1+c2jqup0bFkw07bwF4=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4 h1:tEkOQcXgF6dH1G+MVKZrfpYvozGrzb91k6ha7jireSM=
google.golang.org/genproto/googleapis/rpc v0.0.0-20260427160629-7cedc36a6bc4/go.mod h1:4Hqkh8ycfw05ld/3BWL7rJOSfebL2Q+DVDeRgYgxUU8=
google.golang.org/grpc v1.81.0 h1:W3G9N3KQf3BU+YuCtGKJk0CmxQNbAISICD/9AORxLIw=
google.golang.org/grpc v1.81.0/go.mod h1:xGH9GfzOyMTGIOXBJmXt+BX/V0kcdQbdcuwQ/zNw42I=
google.golang.org/protobuf v1.36.11 h1:fV6ZwhNocDyBLK0dj+fg8ektcVegBBuEolpbTQyBNVE=
google.golang.org/protobuf v1.36.11/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=

View file

@ -18,7 +18,7 @@ xui_service="${XUI_SERVICE:=/etc/systemd/system}"
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
@ -59,16 +59,16 @@ is_domain() {
# Port helpers
is_port_in_use() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
if command -v ss > /dev/null 2>&1; then
ss -ltn 2> /dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
return
fi
if command -v netstat >/dev/null 2>&1; then
netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
if command -v netstat > /dev/null 2>&1; then
netstat -lnt 2> /dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
return
fi
if command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
if command -v lsof > /dev/null 2>&1; then
lsof -nP -iTCP:${port} -sTCP:LISTEN > /dev/null 2>&1 && return 0
fi
return 1
}
@ -77,35 +77,35 @@ install_base() {
case "${release}" in
ubuntu | debian | armbian)
apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl
;;
;;
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
dnf -y update && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl
;;
;;
centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum -y update && yum install -y cronie curl tar tzdata socat ca-certificates openssl
else
dnf -y update && dnf install -y -q cronie curl tar tzdata socat ca-certificates openssl
fi
;;
;;
arch | manjaro | parch)
pacman -Syu && pacman -Syu --noconfirm cronie curl tar tzdata socat ca-certificates openssl
;;
;;
opensuse-tumbleweed | opensuse-leap)
zypper refresh && zypper -q install -y cron curl tar timezone socat ca-certificates openssl
;;
;;
alpine)
apk update && apk add dcron curl tar tzdata socat ca-certificates openssl
;;
;;
*)
apt-get update && apt-get install -y -q cron curl tar tzdata socat ca-certificates openssl
;;
;;
esac
}
gen_random_string() {
local length="$1"
openssl rand -base64 $(( length * 2 )) \
openssl rand -base64 $((length * 2)) \
| tr -dc 'a-zA-Z0-9' \
| head -c "$length"
}
@ -113,7 +113,7 @@ gen_random_string() {
install_acme() {
echo -e "${green}Installing acme.sh for SSL certificate management...${plain}"
cd ~ || return 1
curl -s https://get.acme.sh | sh >/dev/null 2>&1
curl -s https://get.acme.sh | sh > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${red}Failed to install acme.sh${plain}"
return 1
@ -128,60 +128,60 @@ setup_ssl_certificate() {
local server_ip="$2"
local existing_port="$3"
local existing_webBasePath="$4"
echo -e "${green}Setting up SSL certificate...${plain}"
# Check if acme.sh is installed
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
install_acme
if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}"
return 1
fi
fi
# Create certificate directory
local certPath="/root/cert/${domain}"
mkdir -p "$certPath"
# Issue certificate
echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to issue certificate for ${domain}${plain}"
echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}"
rm -rf ~/.acme.sh/${domain} 2>/dev/null
rm -rf "$certPath" 2>/dev/null
rm -rf ~/.acme.sh/${domain} 2> /dev/null
rm -rf "$certPath" 2> /dev/null
return 1
fi
# Install certificate
~/.acme.sh/acme.sh --installcert -d ${domain} \
--key-file /root/cert/${domain}/privkey.pem \
--fullchain-file /root/cert/${domain}/fullchain.pem \
--reloadcmd "systemctl restart x-ui" >/dev/null 2>&1
--reloadcmd "systemctl restart x-ui" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to install certificate${plain}"
return 1
fi
# Enable auto-renew
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
# Secure permissions: private key readable only by owner
chmod 600 $certPath/privkey.pem 2>/dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null
chmod 600 $certPath/privkey.pem 2> /dev/null
chmod 644 $certPath/fullchain.pem 2> /dev/null
# Set certificate for panel
local webCertFile="/root/cert/${domain}/fullchain.pem"
local webKeyFile="/root/cert/${domain}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" > /dev/null 2>&1
echo -e "${green}SSL certificate installed and configured successfully!${plain}"
return 0
else
@ -194,14 +194,14 @@ setup_ssl_certificate() {
# Requires acme.sh and port 80 open for HTTP-01 challenge
setup_ip_certificate() {
local ipv4="$1"
local ipv6="$2" # optional
local ipv6="$2" # optional
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
# Check for acme.sh
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
install_acme
if [ $? -ne 0 ]; then
echo -e "${red}Failed to install acme.sh${plain}"
@ -273,8 +273,8 @@ setup_ip_certificate() {
# Issue certificate with shortlived profile
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
~/.acme.sh/acme.sh --issue \
${domain_args} \
--standalone \
@ -288,9 +288,9 @@ setup_ip_certificate() {
echo -e "${red}Failed to issue IP certificate${plain}"
echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
rm -rf ${certDir} 2>/dev/null
rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
rm -rf ${certDir} 2> /dev/null
return 1
fi
@ -308,25 +308,25 @@ setup_ip_certificate() {
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
echo -e "${red}Certificate files not found after installation${plain}"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
rm -rf ${certDir} 2>/dev/null
rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
rm -rf ${certDir} 2> /dev/null
return 1
fi
echo -e "${green}Certificate files installed successfully${plain}"
# Enable auto-upgrade for acme.sh (ensures cron job runs)
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
# Secure permissions: private key readable only by owner
chmod 600 ${certDir}/privkey.pem 2>/dev/null
chmod 644 ${certDir}/fullchain.pem 2>/dev/null
chmod 600 ${certDir}/privkey.pem 2> /dev/null
chmod 644 ${certDir}/fullchain.pem 2> /dev/null
# Configure panel to use the certificate
echo -e "${green}Setting certificate paths for the panel...${plain}"
${xui_folder}/x-ui cert -webCert "${certDir}/fullchain.pem" -webCertKey "${certDir}/privkey.pem"
if [ $? -ne 0 ]; then
echo -e "${yellow}Warning: Could not set certificate paths automatically${plain}"
echo -e "${yellow}Certificate files are at:${plain}"
@ -346,9 +346,9 @@ setup_ip_certificate() {
ssl_cert_issue() {
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
# check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
echo "acme.sh could not be found. Installing now..."
cd ~ || return 1
curl -s https://get.acme.sh | sh
@ -364,18 +364,18 @@ ssl_cert_issue() {
local domain=""
while true; do
read -rp "Please enter your domain name: " domain
domain="${domain// /}" # Trim whitespace
domain="${domain// /}" # Trim whitespace
if [[ -z "$domain" ]]; then
echo -e "${red}Domain name cannot be empty. Please try again.${plain}"
continue
fi
if ! is_domain "$domain"; then
echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}"
continue
fi
break
done
echo -e "${green}Your domain is: ${domain}, checking it...${plain}"
@ -383,9 +383,9 @@ ssl_cert_issue() {
# detect existing certificate and reuse it if present
local cert_exists=0
if ~/.acme.sh/acme.sh --list 2>/dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then
if ~/.acme.sh/acme.sh --list 2> /dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then
cert_exists=1
local certInfo=$(~/.acme.sh/acme.sh --list 2>/dev/null | grep -F "${domain}")
local certInfo=$(~/.acme.sh/acme.sh --list 2> /dev/null | grep -F "${domain}")
echo -e "${yellow}Existing certificate found for ${domain}, will reuse it.${plain}"
[[ -n "${certInfo}" ]] && echo "$certInfo"
else
@ -412,7 +412,7 @@ ssl_cert_issue() {
# Stop panel temporarily
echo -e "${yellow}Stopping panel temporarily...${plain}"
systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null
systemctl stop x-ui 2> /dev/null || rc-service x-ui stop 2> /dev/null
if [[ ${cert_exists} -eq 0 ]]; then
# issue the certificate
@ -421,7 +421,7 @@ ssl_cert_issue() {
if [ $? -ne 0 ]; then
echo -e "${red}Issuing certificate failed, please check logs.${plain}"
rm -rf ~/.acme.sh/${domain}
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
return 1
else
echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}"
@ -441,18 +441,18 @@ ssl_cert_issue() {
echo -e "${green}\t0.${plain} Keep default reloadcmd"
read -rp "Choose an option: " choice
case "$choice" in
1)
echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
;;
2)
echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
read -rp "Please enter your custom reloadcmd: " reloadCmd
echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
;;
*)
echo -e "${green}Keeping default reloadcmd${plain}"
;;
1)
echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
;;
2)
echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
read -rp "Please enter your custom reloadcmd: " reloadCmd
echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
;;
*)
echo -e "${green}Keeping default reloadcmd${plain}"
;;
esac
fi
@ -469,14 +469,14 @@ ssl_cert_issue() {
installWroteFiles=1
fi
if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && ( ${installRc} -eq 0 || ${installWroteFiles} -eq 1 ) ]]; then
if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && (${installRc} -eq 0 || ${installWroteFiles} -eq 1) ]]; then
echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}"
else
echo -e "${red}Installing certificate failed, exiting.${plain}"
if [[ ${cert_exists} -eq 0 ]]; then
rm -rf ~/.acme.sh/${domain}
fi
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
return 1
fi
@ -486,18 +486,18 @@ ssl_cert_issue() {
echo -e "${yellow}Auto renew setup had issues, certificate details:${plain}"
ls -lah /root/cert/${domain}/
# Secure permissions: private key readable only by owner
chmod 600 $certPath/privkey.pem 2>/dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null
chmod 600 $certPath/privkey.pem 2> /dev/null
chmod 644 $certPath/fullchain.pem 2> /dev/null
else
echo -e "${green}Auto renew succeeded, certificate details:${plain}"
ls -lah /root/cert/${domain}/
# Secure permissions: private key readable only by owner
chmod 600 $certPath/privkey.pem 2>/dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null
chmod 600 $certPath/privkey.pem 2> /dev/null
chmod 644 $certPath/fullchain.pem 2> /dev/null
fi
# start panel
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
# Prompt user to set panel paths after successful certificate installation
read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel
@ -513,14 +513,14 @@ ssl_cert_issue() {
echo ""
echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}"
echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}"
systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null
systemctl restart x-ui 2> /dev/null || rc-service x-ui restart 2> /dev/null
else
echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}"
fi
else
echo -e "${yellow}Skipping panel path setting.${plain}"
fi
return 0
}
@ -528,7 +528,7 @@ ssl_cert_issue() {
# Sets global `SSL_HOST` to the chosen domain/IP for Access URL usage
prompt_and_setup_ssl() {
local panel_port="$1"
local web_base_path="$2" # expected without leading slash
local web_base_path="$2" # expected without leading slash
local server_ip="$3"
local ssl_choice=""
@ -539,124 +539,124 @@ prompt_and_setup_ssl() {
echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths."
read -rp "Choose an option (default 2 for IP): " ssl_choice
ssl_choice="${ssl_choice// /}" # Trim whitespace
ssl_choice="${ssl_choice// /}" # Trim whitespace
# Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)
if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
ssl_choice="2"
fi
case "$ssl_choice" in
1)
# User chose Let's Encrypt domain option
echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
if ssl_cert_issue; then
local cert_domain="${SSL_ISSUED_DOMAIN}"
if [[ -z "${cert_domain}" ]]; then
cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')
fi
1)
# User chose Let's Encrypt domain option
echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
if ssl_cert_issue; then
local cert_domain="${SSL_ISSUED_DOMAIN}"
if [[ -z "${cert_domain}" ]]; then
cert_domain=$(~/.acme.sh/acme.sh --list 2> /dev/null | tail -1 | awk '{print $1}')
fi
if [[ -n "${cert_domain}" ]]; then
SSL_HOST="${cert_domain}"
echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
if [[ -n "${cert_domain}" ]]; then
SSL_HOST="${cert_domain}"
echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
else
echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
SSL_HOST="${server_ip}"
fi
else
echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
echo -e "${red}SSL certificate setup failed for domain mode.${plain}"
SSL_HOST="${server_ip}"
fi
else
echo -e "${red}SSL certificate setup failed for domain mode.${plain}"
SSL_HOST="${server_ip}"
fi
;;
2)
# User chose Let's Encrypt IP certificate option
echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
# Ask for optional IPv6
local ipv6_addr=""
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
# Stop panel if running (port 80 needed)
if [[ $release == "alpine" ]]; then
rc-service x-ui stop >/dev/null 2>&1
else
systemctl stop x-ui >/dev/null 2>&1
fi
setup_ip_certificate "${server_ip}" "${ipv6_addr}"
if [ $? -eq 0 ]; then
SSL_HOST="${server_ip}"
echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
else
echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
SSL_HOST="${server_ip}"
fi
;;
3)
# User chose Custom Paths (User Provided) option
echo -e "${green}Using custom existing certificate...${plain}"
local custom_cert=""
local custom_key=""
local custom_domain=""
;;
2)
# User chose Let's Encrypt IP certificate option
echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
# 3.1 Request Domain to compose Panel URL later
read -rp "Please enter domain name certificate issued for: " custom_domain
custom_domain="${custom_domain// /}" # Remove spaces
# Ask for optional IPv6
local ipv6_addr=""
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
# 3.2 Loop for Certificate Path
while true; do
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
# Strip quotes if present
custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
break
elif [[ ! -f "$custom_cert" ]]; then
echo -e "${red}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_cert" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
# Stop panel if running (port 80 needed)
if [[ $release == "alpine" ]]; then
rc-service x-ui stop > /dev/null 2>&1
else
echo -e "${red}Error: File is empty!${plain}"
systemctl stop x-ui > /dev/null 2>&1
fi
done
# 3.3 Loop for Private Key Path
while true; do
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
# Strip quotes if present
custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
break
elif [[ ! -f "$custom_key" ]]; then
echo -e "${red}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_key" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
setup_ip_certificate "${server_ip}" "${ipv6_addr}"
if [ $? -eq 0 ]; then
SSL_HOST="${server_ip}"
echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
SSL_HOST="${server_ip}"
fi
done
;;
3)
# User chose Custom Paths (User Provided) option
echo -e "${green}Using custom existing certificate...${plain}"
local custom_cert=""
local custom_key=""
local custom_domain=""
# 3.4 Apply Settings via x-ui binary
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1
# Set SSL_HOST for composing Panel URL
if [[ -n "$custom_domain" ]]; then
SSL_HOST="$custom_domain"
else
# 3.1 Request Domain to compose Panel URL later
read -rp "Please enter domain name certificate issued for: " custom_domain
custom_domain="${custom_domain// /}" # Remove spaces
# 3.2 Loop for Certificate Path
while true; do
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
# Strip quotes if present
custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
break
elif [[ ! -f "$custom_cert" ]]; then
echo -e "${red}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_cert" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
fi
done
# 3.3 Loop for Private Key Path
while true; do
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
# Strip quotes if present
custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
break
elif [[ ! -f "$custom_key" ]]; then
echo -e "${red}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_key" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
fi
done
# 3.4 Apply Settings via x-ui binary
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" > /dev/null 2>&1
# Set SSL_HOST for composing Panel URL
if [[ -n "$custom_domain" ]]; then
SSL_HOST="$custom_domain"
else
SSL_HOST="${server_ip}"
fi
echo -e "${green}✓ Custom certificate paths applied.${plain}"
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1
;;
*)
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
SSL_HOST="${server_ip}"
fi
echo -e "${green}✓ Custom certificate paths applied.${plain}"
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
;;
*)
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
SSL_HOST="${server_ip}"
;;
;;
esac
}
@ -676,7 +676,7 @@ config_after_install() {
)
local server_ip=""
for ip_address in "${URL_lists[@]}"; do
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2> /dev/null)
local http_code=$(echo "$response" | tail -n1)
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
@ -684,13 +684,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
@ -699,9 +699,9 @@ config_after_install() {
local config_port=$(shuf -i 1024-62000 -n 1)
echo -e "${yellow}Generated random port: ${config_port}${plain}"
fi
${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}" -port "${config_port}" -webBasePath "${config_webBasePath}"
echo ""
echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${green} SSL Certificate Setup (MANDATORY) ${plain}"
@ -711,7 +711,7 @@ config_after_install() {
echo ""
prompt_and_setup_ssl "${config_port}" "${config_webBasePath}" "${server_ip}"
# Display final credentials and access information
echo ""
echo -e "${green}═══════════════════════════════════════════${plain}"
@ -750,7 +750,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}"
${xui_folder}/x-ui setting -username "${config_username}" -password "${config_password}"
echo -e "Generated new random login credentials:"
@ -778,13 +778,13 @@ config_after_install() {
echo -e "${green}SSL certificate already configured. No action needed.${plain}"
fi
fi
${xui_folder}/x-ui migrate
}
install_x-ui() {
cd ${xui_folder%/x-ui}/
# 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/')
@ -806,12 +806,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"
curl -4fLRo ${xui_folder}-linux-$(arch).tar.gz ${url}
@ -825,7 +825,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 ${xui_folder}/ ]]; then
if [[ $release == "alpine" ]]; then
@ -835,22 +835,22 @@ install_x-ui() {
fi
rm ${xui_folder}/ -rf
fi
# Extract resources and set permissions
tar zxvf x-ui-linux-$(arch).tar.gz
rm x-ui-linux-$(arch).tar.gz -f
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
@ -870,7 +870,7 @@ install_x-ui() {
echo -e "${green}Created /etc/.gitignore and added x-ui.db for etckeeper${plain}"
fi
fi
if [[ $release == "alpine" ]]; then
curl -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc
if [[ $? -ne 0 ]]; then
@ -883,73 +883,73 @@ install_x-ui() {
else
# Install systemd service file
service_installed=false
if [ -f "x-ui.service" ]; then
echo -e "${green}Found x-ui.service in extracted files, installing...${plain}"
cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1
cp -f x-ui.service ${xui_service}/ > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
service_installed=true
fi
fi
if [ "$service_installed" = false ]; then
case "${release}" in
ubuntu | debian | armbian)
if [ -f "x-ui.service.debian" ]; then
echo -e "${green}Found x-ui.service.debian in extracted files, installing...${plain}"
cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1
cp -f x-ui.service.debian ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
service_installed=true
fi
fi
;;
;;
arch | manjaro | parch)
if [ -f "x-ui.service.arch" ]; then
echo -e "${green}Found x-ui.service.arch in extracted files, installing...${plain}"
cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1
cp -f x-ui.service.arch ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
service_installed=true
fi
fi
;;
;;
*)
if [ -f "x-ui.service.rhel" ]; then
echo -e "${green}Found x-ui.service.rhel in extracted files, installing...${plain}"
cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1
cp -f x-ui.service.rhel ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
service_installed=true
fi
fi
;;
;;
esac
fi
# If service file not found in tar.gz, download from GitHub
if [ "$service_installed" = false ]; then
echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}"
case "${release}" in
ubuntu | debian | armbian)
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
;;
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian > /dev/null 2>&1
;;
arch | manjaro | parch)
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1
;;
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch > /dev/null 2>&1
;;
*)
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
;;
curl -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel > /dev/null 2>&1
;;
esac
if [[ $? -ne 0 ]]; then
echo -e "${red}Failed to install x-ui.service from GitHub${plain}"
exit 1
fi
service_installed=true
fi
if [ "$service_installed" = true ]; then
echo -e "${green}Setting up systemd unit...${plain}"
chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1
chmod 644 ${xui_service}/x-ui.service >/dev/null 2>&1
chown root:root ${xui_service}/x-ui.service > /dev/null 2>&1
chmod 644 ${xui_service}/x-ui.service > /dev/null 2>&1
systemctl daemon-reload
systemctl enable x-ui
systemctl start x-ui
@ -958,7 +958,7 @@ install_x-ui() {
exit 1
fi
fi
echo -e "${green}x-ui ${tag_version}${plain} installation finished, it is running now..."
echo -e ""
echo -e "┌───────────────────────────────────────────────────────┐

View file

@ -55,7 +55,7 @@ func (s *SubClashService) GetClash(subId string, host string) (string, string, e
}
}
for _, client := range clients {
if client.Enable && client.SubID == subId {
if client.SubID == subId {
clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email))
proxies = append(proxies, s.getProxies(inbound, client, host)...)
}

View file

@ -138,6 +138,7 @@ func (a *SUBController) subs(c *gin.Context) {
"host": page.Host,
"base_path": page.BasePath,
"sId": page.SId,
"enabled": page.Enabled,
"download": page.Download,
"upload": page.Upload,
"total": page.Total,

View file

@ -116,7 +116,7 @@ func (s *SubJsonService) GetJson(subId string, host string) (string, string, err
}
for _, client := range clients {
if client.Enable && client.SubID == subId {
if client.SubID == subId {
clientTraffics = append(clientTraffics, s.SubService.getClientTraffics(inbound.ClientStats, client.Email))
newConfigs := s.getConfig(inbound, client, host)
configArray = append(configArray, newConfigs...)

View file

@ -46,6 +46,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
var result []string
var traffic xray.ClientTraffic
var lastOnline int64
var hasEnabledClient bool
var clientTraffics []xray.ClientTraffic
inbounds, err := s.getInboundsBySubId(subId)
if err != nil {
@ -77,7 +78,10 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
}
}
for _, client := range clients {
if client.Enable && client.SubID == subId {
if client.SubID == subId {
if client.Enable {
hasEnabledClient = true
}
link := s.getLink(inbound, client.Email)
result = append(result, link)
ct := s.getClientTraffics(inbound.ClientStats, client.Email)
@ -111,6 +115,7 @@ func (s *SubService) GetSubs(subId string, host string) ([]string, int64, xray.C
}
}
}
traffic.Enable = hasEnabledClient
return result, lastOnline, traffic, nil
}
@ -1304,6 +1309,7 @@ type PageData struct {
Host string
BasePath string
SId string
Enabled bool
Download string
Upload string
Total string
@ -1453,6 +1459,7 @@ func (s *SubService) BuildPageData(subId string, hostHeader string, traffic xray
Host: hostHeader,
BasePath: basePath,
SId: subId,
Enabled: traffic.Enable,
Download: download,
Upload: upload,
Total: total,

564
update.sh
View file

@ -12,16 +12,16 @@ xui_service="${XUI_SERVICE:=/etc/systemd/system}"
# 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_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)"
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
@ -44,7 +44,7 @@ fi
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
@ -61,7 +61,7 @@ arch() {
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;;
*) echo -e "${red}Unsupported CPU architecture!${plain}" && rm -f "${cur_dir}/${script_name}" > /dev/null 2>&1 && exit 2 ;;
esac
}
@ -84,23 +84,23 @@ is_domain() {
# Port helpers
is_port_in_use() {
local port="$1"
if command -v ss >/dev/null 2>&1; then
ss -ltn 2>/dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
if command -v ss > /dev/null 2>&1; then
ss -ltn 2> /dev/null | awk -v p=":${port}$" '$4 ~ p {exit 0} END {exit 1}'
return
fi
if command -v netstat >/dev/null 2>&1; then
netstat -lnt 2>/dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
if command -v netstat > /dev/null 2>&1; then
netstat -lnt 2> /dev/null | awk -v p=":${port} " '$4 ~ p {exit 0} END {exit 1}'
return
fi
if command -v lsof >/dev/null 2>&1; then
lsof -nP -iTCP:${port} -sTCP:LISTEN >/dev/null 2>&1 && return 0
if command -v lsof > /dev/null 2>&1; then
lsof -nP -iTCP:${port} -sTCP:LISTEN > /dev/null 2>&1 && return 0
fi
return 1
}
gen_random_string() {
local length="$1"
openssl rand -base64 $(( length * 2 )) \
openssl rand -base64 $((length * 2)) \
| tr -dc 'a-zA-Z0-9' \
| head -c "$length"
}
@ -109,37 +109,37 @@ 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 cron curl tar tzdata socat openssl >/dev/null 2>&1
;;
apt-get update > /dev/null 2>&1 && apt-get install -y -q cron curl tar tzdata socat openssl > /dev/null 2>&1
;;
fedora | amzn | virtuozzo | rhel | almalinux | rocky | ol)
dnf -y update >/dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl >/dev/null 2>&1
;;
dnf -y update > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
;;
centos)
if [[ "${VERSION_ID}" =~ ^7 ]]; then
yum -y update >/dev/null 2>&1 && yum install -y -q cronie curl tar tzdata socat openssl >/dev/null 2>&1
yum -y update > /dev/null 2>&1 && yum install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
else
dnf -y update >/dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl >/dev/null 2>&1
dnf -y update > /dev/null 2>&1 && dnf install -y -q cronie curl tar tzdata socat openssl > /dev/null 2>&1
fi
;;
;;
arch | manjaro | parch)
pacman -Syu >/dev/null 2>&1 && pacman -Syu --noconfirm cronie curl tar tzdata socat openssl >/dev/null 2>&1
;;
pacman -Syu > /dev/null 2>&1 && pacman -Syu --noconfirm cronie curl tar tzdata socat openssl > /dev/null 2>&1
;;
opensuse-tumbleweed | opensuse-leap)
zypper refresh >/dev/null 2>&1 && zypper -q install -y cron curl tar timezone socat openssl >/dev/null 2>&1
;;
zypper refresh > /dev/null 2>&1 && zypper -q install -y cron curl tar timezone socat openssl > /dev/null 2>&1
;;
alpine)
apk update >/dev/null 2>&1 && apk add dcron curl tar tzdata socat openssl>/dev/null 2>&1
;;
apk update > /dev/null 2>&1 && apk add dcron curl tar tzdata socat openssl > /dev/null 2>&1
;;
*)
apt-get update >/dev/null 2>&1 && apt install -y -q cron curl tar tzdata socat openssl >/dev/null 2>&1
;;
apt-get update > /dev/null 2>&1 && apt install -y -q cron curl tar tzdata socat openssl > /dev/null 2>&1
;;
esac
}
install_acme() {
echo -e "${green}Installing acme.sh for SSL certificate management...${plain}"
cd ~ || return 1
curl -s https://get.acme.sh | sh >/dev/null 2>&1
curl -s https://get.acme.sh | sh > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${red}Failed to install acme.sh${plain}"
return 1
@ -154,59 +154,59 @@ setup_ssl_certificate() {
local server_ip="$2"
local existing_port="$3"
local existing_webBasePath="$4"
echo -e "${green}Setting up SSL certificate...${plain}"
# Check if acme.sh is installed
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
install_acme
if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to install acme.sh, skipping SSL setup${plain}"
return 1
fi
fi
# Create certificate directory
local certPath="/root/cert/${domain}"
mkdir -p "$certPath"
# Issue certificate
echo -e "${green}Issuing SSL certificate for ${domain}...${plain}"
echo -e "${yellow}Note: Port 80 must be open and accessible from the internet${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
~/.acme.sh/acme.sh --issue -d ${domain} --listen-v6 --standalone --httpport 80 --force
if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to issue certificate for ${domain}${plain}"
echo -e "${yellow}Please ensure port 80 is open and try again later with: x-ui${plain}"
rm -rf ~/.acme.sh/${domain} 2>/dev/null
rm -rf "$certPath" 2>/dev/null
rm -rf ~/.acme.sh/${domain} 2> /dev/null
rm -rf "$certPath" 2> /dev/null
return 1
fi
# Install certificate
~/.acme.sh/acme.sh --installcert -d ${domain} \
--key-file /root/cert/${domain}/privkey.pem \
--fullchain-file /root/cert/${domain}/fullchain.pem \
--reloadcmd "systemctl restart x-ui" >/dev/null 2>&1
--reloadcmd "systemctl restart x-ui" > /dev/null 2>&1
if [ $? -ne 0 ]; then
echo -e "${yellow}Failed to install certificate${plain}"
return 1
fi
# Enable auto-renew
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
chmod 600 $certPath/privkey.pem 2>/dev/null
chmod 644 $certPath/fullchain.pem 2>/dev/null
~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
chmod 600 $certPath/privkey.pem 2> /dev/null
chmod 644 $certPath/fullchain.pem 2> /dev/null
# Set certificate for panel
local webCertFile="/root/cert/${domain}/fullchain.pem"
local webKeyFile="/root/cert/${domain}/privkey.pem"
if [[ -f "$webCertFile" && -f "$webKeyFile" ]]; then
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" >/dev/null 2>&1
${xui_folder}/x-ui cert -webCert "$webCertFile" -webCertKey "$webKeyFile" > /dev/null 2>&1
echo -e "${green}SSL certificate installed and configured successfully!${plain}"
return 0
else
@ -219,14 +219,14 @@ setup_ssl_certificate() {
# Requires acme.sh and port 80 open for HTTP-01 challenge
setup_ip_certificate() {
local ipv4="$1"
local ipv6="$2" # optional
local ipv6="$2" # optional
echo -e "${green}Setting up Let's Encrypt IP certificate (shortlived profile)...${plain}"
echo -e "${yellow}Note: IP certificates are valid for ~6 days and will auto-renew.${plain}"
echo -e "${yellow}Default listener is port 80. If you choose another port, ensure external port 80 forwards to it.${plain}"
# Check for acme.sh
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
install_acme
if [ $? -ne 0 ]; then
echo -e "${red}Failed to install acme.sh${plain}"
@ -298,8 +298,8 @@ setup_ip_certificate() {
# Issue certificate with shortlived profile
echo -e "${green}Issuing IP certificate for ${ipv4}...${plain}"
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force >/dev/null 2>&1
~/.acme.sh/acme.sh --set-default-ca --server letsencrypt --force > /dev/null 2>&1
~/.acme.sh/acme.sh --issue \
${domain_args} \
--standalone \
@ -313,9 +313,9 @@ setup_ip_certificate() {
echo -e "${red}Failed to issue IP certificate${plain}"
echo -e "${yellow}Please ensure port ${WebPort} is reachable (or forwarded from external port 80)${plain}"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
rm -rf ${certDir} 2>/dev/null
rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
rm -rf ${certDir} 2> /dev/null
return 1
fi
@ -333,19 +333,19 @@ setup_ip_certificate() {
if [[ ! -f "${certDir}/fullchain.pem" || ! -f "${certDir}/privkey.pem" ]]; then
echo -e "${red}Certificate files not found after installation${plain}"
# Cleanup acme.sh data for both IPv4 and IPv6 if specified
rm -rf ~/.acme.sh/${ipv4} 2>/dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2>/dev/null
rm -rf ${certDir} 2>/dev/null
rm -rf ~/.acme.sh/${ipv4} 2> /dev/null
[[ -n "$ipv6" ]] && rm -rf ~/.acme.sh/${ipv6} 2> /dev/null
rm -rf ${certDir} 2> /dev/null
return 1
fi
echo -e "${green}Certificate files installed successfully${plain}"
# Enable auto-upgrade for acme.sh (ensures cron job runs)
~/.acme.sh/acme.sh --upgrade --auto-upgrade >/dev/null 2>&1
~/.acme.sh/acme.sh --upgrade --auto-upgrade > /dev/null 2>&1
chmod 600 ${certDir}/privkey.pem 2>/dev/null
chmod 644 ${certDir}/fullchain.pem 2>/dev/null
chmod 600 ${certDir}/privkey.pem 2> /dev/null
chmod 644 ${certDir}/fullchain.pem 2> /dev/null
# Configure panel to use the certificate
echo -e "${green}Setting certificate paths for the panel...${plain}"
@ -369,9 +369,9 @@ setup_ip_certificate() {
ssl_cert_issue() {
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep 'webBasePath:' | awk -F': ' '{print $2}' | tr -d '[:space:]' | sed 's#^/##')
local existing_port=$(${xui_folder}/x-ui setting -show true | grep 'port:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
# check for acme.sh first
if ! command -v ~/.acme.sh/acme.sh &>/dev/null; then
if ! command -v ~/.acme.sh/acme.sh &> /dev/null; then
echo "acme.sh could not be found. Installing now..."
cd ~ || return 1
curl -s https://get.acme.sh | sh
@ -387,18 +387,18 @@ ssl_cert_issue() {
local domain=""
while true; do
read -rp "Please enter your domain name: " domain
domain="${domain// /}" # Trim whitespace
domain="${domain// /}" # Trim whitespace
if [[ -z "$domain" ]]; then
echo -e "${red}Domain name cannot be empty. Please try again.${plain}"
continue
fi
if ! is_domain "$domain"; then
echo -e "${red}Invalid domain format: ${domain}. Please enter a valid domain name.${plain}"
continue
fi
break
done
echo -e "${green}Your domain is: ${domain}, checking it...${plain}"
@ -406,9 +406,9 @@ ssl_cert_issue() {
# detect existing certificate and reuse it if present
local cert_exists=0
if ~/.acme.sh/acme.sh --list 2>/dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then
if ~/.acme.sh/acme.sh --list 2> /dev/null | awk '{print $1}' | grep -Fxq "${domain}"; then
cert_exists=1
local certInfo=$(~/.acme.sh/acme.sh --list 2>/dev/null | grep -F "${domain}")
local certInfo=$(~/.acme.sh/acme.sh --list 2> /dev/null | grep -F "${domain}")
echo -e "${yellow}Existing certificate found for ${domain}, will reuse it.${plain}"
[[ -n "${certInfo}" ]] && echo "$certInfo"
else
@ -435,7 +435,7 @@ ssl_cert_issue() {
# Stop panel temporarily
echo -e "${yellow}Stopping panel temporarily...${plain}"
systemctl stop x-ui 2>/dev/null || rc-service x-ui stop 2>/dev/null
systemctl stop x-ui 2> /dev/null || rc-service x-ui stop 2> /dev/null
if [[ ${cert_exists} -eq 0 ]]; then
# issue the certificate
@ -444,7 +444,7 @@ ssl_cert_issue() {
if [ $? -ne 0 ]; then
echo -e "${red}Issuing certificate failed, please check logs.${plain}"
rm -rf ~/.acme.sh/${domain}
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
return 1
else
echo -e "${green}Issuing certificate succeeded, installing certificates...${plain}"
@ -464,18 +464,18 @@ ssl_cert_issue() {
echo -e "${green}\t0.${plain} Keep default reloadcmd"
read -rp "Choose an option: " choice
case "$choice" in
1)
echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
;;
2)
echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
read -rp "Please enter your custom reloadcmd: " reloadCmd
echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
;;
*)
echo -e "${green}Keeping default reloadcmd${plain}"
;;
1)
echo -e "${green}Reloadcmd is: systemctl reload nginx ; systemctl restart x-ui${plain}"
reloadCmd="systemctl reload nginx ; systemctl restart x-ui"
;;
2)
echo -e "${yellow}It's recommended to put x-ui restart at the end${plain}"
read -rp "Please enter your custom reloadcmd: " reloadCmd
echo -e "${green}Reloadcmd is: ${reloadCmd}${plain}"
;;
*)
echo -e "${green}Keeping default reloadcmd${plain}"
;;
esac
fi
@ -492,14 +492,14 @@ ssl_cert_issue() {
installWroteFiles=1
fi
if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && ( ${installRc} -eq 0 || ${installWroteFiles} -eq 1 ) ]]; then
if [[ -f "/root/cert/${domain}/privkey.pem" && -f "/root/cert/${domain}/fullchain.pem" && (${installRc} -eq 0 || ${installWroteFiles} -eq 1) ]]; then
echo -e "${green}Installing certificate succeeded, enabling auto renew...${plain}"
else
echo -e "${red}Installing certificate failed, exiting.${plain}"
if [[ ${cert_exists} -eq 0 ]]; then
rm -rf ~/.acme.sh/${domain}
fi
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
return 1
fi
@ -518,7 +518,7 @@ ssl_cert_issue() {
fi
# Restart panel
systemctl start x-ui 2>/dev/null || rc-service x-ui start 2>/dev/null
systemctl start x-ui 2> /dev/null || rc-service x-ui start 2> /dev/null
# Prompt user to set panel paths after successful certificate installation
read -rp "Would you like to set this certificate for the panel? (y/n): " setPanel
@ -534,21 +534,21 @@ ssl_cert_issue() {
echo ""
echo -e "${green}Access URL: https://${domain}:${existing_port}/${existing_webBasePath}${plain}"
echo -e "${yellow}Panel will restart to apply SSL certificate...${plain}"
systemctl restart x-ui 2>/dev/null || rc-service x-ui restart 2>/dev/null
systemctl restart x-ui 2> /dev/null || rc-service x-ui restart 2> /dev/null
else
echo -e "${red}Error: Certificate or private key file not found for domain: $domain.${plain}"
fi
else
echo -e "${yellow}Skipping panel path setting.${plain}"
fi
return 0
}
# Unified interactive SSL setup (domain or IP)
# Sets global `SSL_HOST` to the chosen domain/IP
prompt_and_setup_ssl() {
local panel_port="$1"
local web_base_path="$2" # expected without leading slash
local web_base_path="$2" # expected without leading slash
local server_ip="$3"
local ssl_choice=""
@ -559,132 +559,132 @@ prompt_and_setup_ssl() {
echo -e "${green}3.${plain} Custom SSL Certificate (Path to existing files)"
echo -e "${blue}Note:${plain} Options 1 & 2 require port 80 open. Option 3 requires manual paths."
read -rp "Choose an option (default 2 for IP): " ssl_choice
ssl_choice="${ssl_choice// /}" # Trim whitespace
ssl_choice="${ssl_choice// /}" # Trim whitespace
# Default to 2 (IP cert) if input is empty or invalid (not 1 or 3)
if [[ "$ssl_choice" != "1" && "$ssl_choice" != "3" ]]; then
ssl_choice="2"
fi
case "$ssl_choice" in
1)
# User chose Let's Encrypt domain option
echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
if ssl_cert_issue; then
local cert_domain="${SSL_ISSUED_DOMAIN}"
if [[ -z "${cert_domain}" ]]; then
cert_domain=$(~/.acme.sh/acme.sh --list 2>/dev/null | tail -1 | awk '{print $1}')
fi
1)
# User chose Let's Encrypt domain option
echo -e "${green}Using Let's Encrypt for domain certificate...${plain}"
if ssl_cert_issue; then
local cert_domain="${SSL_ISSUED_DOMAIN}"
if [[ -z "${cert_domain}" ]]; then
cert_domain=$(~/.acme.sh/acme.sh --list 2> /dev/null | tail -1 | awk '{print $1}')
fi
if [[ -n "${cert_domain}" ]]; then
SSL_HOST="${cert_domain}"
echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
if [[ -n "${cert_domain}" ]]; then
SSL_HOST="${cert_domain}"
echo -e "${green}✓ SSL certificate configured successfully with domain: ${cert_domain}${plain}"
else
echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
SSL_HOST="${server_ip}"
fi
else
echo -e "${yellow}SSL setup may have completed, but domain extraction failed${plain}"
echo -e "${red}SSL certificate setup failed for domain mode.${plain}"
SSL_HOST="${server_ip}"
fi
else
echo -e "${red}SSL certificate setup failed for domain mode.${plain}"
SSL_HOST="${server_ip}"
fi
;;
2)
# User chose Let's Encrypt IP certificate option
echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
# Ask for optional IPv6
local ipv6_addr=""
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
# Stop panel if running (port 80 needed)
if [[ $release == "alpine" ]]; then
rc-service x-ui stop >/dev/null 2>&1
else
systemctl stop x-ui >/dev/null 2>&1
fi
setup_ip_certificate "${server_ip}" "${ipv6_addr}"
if [ $? -eq 0 ]; then
SSL_HOST="${server_ip}"
echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
else
echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
SSL_HOST="${server_ip}"
fi
# Restart panel after SSL is configured (restart applies new cert settings)
if [[ $release == "alpine" ]]; then
rc-service x-ui restart >/dev/null 2>&1
else
systemctl restart x-ui >/dev/null 2>&1
fi
;;
2)
# User chose Let's Encrypt IP certificate option
echo -e "${green}Using Let's Encrypt for IP certificate (shortlived profile)...${plain}"
;;
3)
# User chose Custom Paths (User Provided) option
echo -e "${green}Using custom existing certificate...${plain}"
local custom_cert=""
local custom_key=""
local custom_domain=""
# Ask for optional IPv6
local ipv6_addr=""
read -rp "Do you have an IPv6 address to include? (leave empty to skip): " ipv6_addr
ipv6_addr="${ipv6_addr// /}" # Trim whitespace
# 3.1 Request Domain to compose Panel URL later
read -rp "Please enter domain name certificate issued for: " custom_domain
custom_domain="${custom_domain// /}" # Remove spaces
# 3.2 Loop for Certificate Path
while true; do
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
# Strip quotes if present
custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
break
elif [[ ! -f "$custom_cert" ]]; then
echo -e "${red}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_cert" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
# Stop panel if running (port 80 needed)
if [[ $release == "alpine" ]]; then
rc-service x-ui stop > /dev/null 2>&1
else
echo -e "${red}Error: File is empty!${plain}"
systemctl stop x-ui > /dev/null 2>&1
fi
done
# 3.3 Loop for Private Key Path
while true; do
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
# Strip quotes if present
custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
break
elif [[ ! -f "$custom_key" ]]; then
echo -e "${red}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_key" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
setup_ip_certificate "${server_ip}" "${ipv6_addr}"
if [ $? -eq 0 ]; then
SSL_HOST="${server_ip}"
echo -e "${green}✓ Let's Encrypt IP certificate configured successfully${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
echo -e "${red}✗ IP certificate setup failed. Please check port 80 is open.${plain}"
SSL_HOST="${server_ip}"
fi
done
# 3.4 Apply Settings via x-ui binary
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" >/dev/null 2>&1
# Restart panel after SSL is configured (restart applies new cert settings)
if [[ $release == "alpine" ]]; then
rc-service x-ui restart > /dev/null 2>&1
else
systemctl restart x-ui > /dev/null 2>&1
fi
# Set SSL_HOST for composing Panel URL
if [[ -n "$custom_domain" ]]; then
SSL_HOST="$custom_domain"
else
;;
3)
# User chose Custom Paths (User Provided) option
echo -e "${green}Using custom existing certificate...${plain}"
local custom_cert=""
local custom_key=""
local custom_domain=""
# 3.1 Request Domain to compose Panel URL later
read -rp "Please enter domain name certificate issued for: " custom_domain
custom_domain="${custom_domain// /}" # Remove spaces
# 3.2 Loop for Certificate Path
while true; do
read -rp "Input certificate path (keywords: .crt / fullchain): " custom_cert
# Strip quotes if present
custom_cert=$(echo "$custom_cert" | tr -d '"' | tr -d "'")
if [[ -f "$custom_cert" && -r "$custom_cert" && -s "$custom_cert" ]]; then
break
elif [[ ! -f "$custom_cert" ]]; then
echo -e "${red}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_cert" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
fi
done
# 3.3 Loop for Private Key Path
while true; do
read -rp "Input private key path (keywords: .key / privatekey): " custom_key
# Strip quotes if present
custom_key=$(echo "$custom_key" | tr -d '"' | tr -d "'")
if [[ -f "$custom_key" && -r "$custom_key" && -s "$custom_key" ]]; then
break
elif [[ ! -f "$custom_key" ]]; then
echo -e "${red}Error: File does not exist! Try again.${plain}"
elif [[ ! -r "$custom_key" ]]; then
echo -e "${red}Error: File exists but is not readable (check permissions)!${plain}"
else
echo -e "${red}Error: File is empty!${plain}"
fi
done
# 3.4 Apply Settings via x-ui binary
${xui_folder}/x-ui cert -webCert "$custom_cert" -webCertKey "$custom_key" > /dev/null 2>&1
# Set SSL_HOST for composing Panel URL
if [[ -n "$custom_domain" ]]; then
SSL_HOST="$custom_domain"
else
SSL_HOST="${server_ip}"
fi
echo -e "${green}✓ Custom certificate paths applied.${plain}"
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
systemctl restart x-ui > /dev/null 2>&1 || rc-service x-ui restart > /dev/null 2>&1
;;
*)
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
SSL_HOST="${server_ip}"
fi
echo -e "${green}✓ Custom certificate paths applied.${plain}"
echo -e "${yellow}Note: You are responsible for renewing these files externally.${plain}"
systemctl restart x-ui >/dev/null 2>&1 || rc-service x-ui restart >/dev/null 2>&1
;;
*)
echo -e "${red}Invalid option. Skipping SSL setup.${plain}"
SSL_HOST="${server_ip}"
;;
;;
esac
}
@ -692,12 +692,12 @@ config_after_update() {
echo -e "${yellow}x-ui settings:${plain}"
${xui_folder}/x-ui setting -show true
${xui_folder}/x-ui migrate
# Properly detect empty cert by checking if cert: line exists and has content after it
local existing_cert=$(${xui_folder}/x-ui setting -getCert true 2>/dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
local existing_cert=$(${xui_folder}/x-ui setting -getCert true 2> /dev/null | grep 'cert:' | awk -F': ' '{print $2}' | tr -d '[:space:]')
local existing_port=$(${xui_folder}/x-ui setting -show true | grep -Eo 'port: .+' | awk '{print $2}')
local existing_webBasePath=$(${xui_folder}/x-ui setting -show true | grep -Eo 'webBasePath: .+' | awk '{print $2}' | sed 's#^/##')
# Get server IP
local URL_lists=(
"https://api4.ipify.org"
@ -709,7 +709,7 @@ config_after_update() {
)
local server_ip=""
for ip_address in "${URL_lists[@]}"; do
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2>/dev/null)
local response=$(curl -s -w "\n%{http_code}" --max-time 3 "${ip_address}" 2> /dev/null)
local http_code=$(echo "$response" | tail -n1)
local ip_result=$(echo "$response" | head -n-1 | tr -d '[:space:]')
if [[ "${http_code}" == "200" && -n "${ip_result}" ]]; then
@ -717,7 +717,7 @@ config_after_update() {
break
fi
done
# Handle missing/short webBasePath
if [[ ${#existing_webBasePath} -lt 4 ]]; then
echo -e "${yellow}WebBasePath is missing or too short. Generating a new one...${plain}"
@ -726,7 +726,7 @@ config_after_update() {
existing_webBasePath="${config_webBasePath}"
echo -e "${green}New WebBasePath: ${config_webBasePath}${plain}"
fi
# Check and prompt for SSL if missing
if [[ -z "$existing_cert" ]]; then
echo ""
@ -736,16 +736,16 @@ config_after_update() {
echo -e "${yellow}For security, SSL certificate is MANDATORY for all panels.${plain}"
echo -e "${yellow}Let's Encrypt now supports both domains and IP addresses!${plain}"
echo ""
if [[ -z "${server_ip}" ]]; then
echo -e "${red}Failed to detect server IP${plain}"
echo -e "${yellow}Please configure SSL manually using: x-ui${plain}"
return
fi
# Prompt and setup SSL (domain or IP)
prompt_and_setup_ssl "${existing_port}" "${existing_webBasePath}" "${server_ip}"
echo ""
echo -e "${green}═══════════════════════════════════════════${plain}"
echo -e "${green} Panel Access Information ${plain}"
@ -768,17 +768,17 @@ config_after_update() {
update_x-ui() {
cd ${xui_folder%/x-ui}/
if [ -f "${xui_folder}/x-ui" ]; then
current_xui_version=$(${xui_folder}/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/')
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/')
@ -787,110 +787,110 @@ update_x-ui() {
fi
fi
echo -e "Got x-ui latest version: ${tag_version}, beginning the installation..."
${curl_bin} -fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
${curl_bin} -fLRo ${xui_folder}-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}"
${curl_bin} -4fLRo ${xui_folder}-linux-$(arch).tar.gz https://github.com/MHSanaei/3x-ui/releases/download/${tag_version}/x-ui-linux-$(arch).tar.gz 2>/dev/null
${curl_bin} -4fLRo ${xui_folder}-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 ${xui_folder}/ ]]; 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
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
rm -f /etc/init.d/x-ui > /dev/null 2>&1
else
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
rm x-ui-linux-$(arch).tar.gz -f > /dev/null 2>&1
_fail "ERROR: x-ui service unit not installed."
fi
else
if [ -f "${xui_service}/x-ui.service" ]; then
systemctl stop x-ui >/dev/null 2>&1
systemctl disable x-ui >/dev/null 2>&1
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 ${xui_service}/x-ui.service -f >/dev/null 2>&1
systemctl daemon-reload >/dev/null 2>&1
rm ${xui_service}/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
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 ${xui_folder} -f >/dev/null 2>&1
rm ${xui_folder}/x-ui.service -f >/dev/null 2>&1
rm ${xui_folder}/x-ui.service.debian -f >/dev/null 2>&1
rm ${xui_folder}/x-ui.service.arch -f >/dev/null 2>&1
rm ${xui_folder}/x-ui.service.rhel -f >/dev/null 2>&1
rm ${xui_folder}/x-ui -f >/dev/null 2>&1
rm ${xui_folder}/x-ui.sh -f >/dev/null 2>&1
rm ${xui_folder} -f > /dev/null 2>&1
rm ${xui_folder}/x-ui.service -f > /dev/null 2>&1
rm ${xui_folder}/x-ui.service.debian -f > /dev/null 2>&1
rm ${xui_folder}/x-ui.service.arch -f > /dev/null 2>&1
rm ${xui_folder}/x-ui.service.rhel -f > /dev/null 2>&1
rm ${xui_folder}/x-ui -f > /dev/null 2>&1
rm ${xui_folder}/x-ui.sh -f > /dev/null 2>&1
echo -e "${green}Removing old xray version...${plain}"
rm ${xui_folder}/bin/xray-linux-amd64 -f >/dev/null 2>&1
rm ${xui_folder}/bin/xray-linux-amd64 -f > /dev/null 2>&1
echo -e "${green}Removing old README and LICENSE file...${plain}"
rm ${xui_folder}/bin/README.md -f >/dev/null 2>&1
rm ${xui_folder}/bin/LICENSE -f >/dev/null 2>&1
rm ${xui_folder}/bin/README.md -f > /dev/null 2>&1
rm ${xui_folder}/bin/LICENSE -f > /dev/null 2>&1
else
rm x-ui-linux-$(arch).tar.gz -f >/dev/null 2>&1
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
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
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
chmod +x x-ui bin/xray-linux-$(arch) > /dev/null 2>&1
echo -e "${green}Downloading and installing x-ui.sh script...${plain}"
${curl_bin} -fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
${curl_bin} -fLRo /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}"
${curl_bin} -4fLRo /usr/bin/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.sh >/dev/null 2>&1
${curl_bin} -4fLRo /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 ${xui_folder}/x-ui.sh >/dev/null 2>&1
chmod +x /usr/bin/x-ui >/dev/null 2>&1
mkdir -p /var/log/x-ui >/dev/null 2>&1
chmod +x ${xui_folder}/x-ui.sh > /dev/null 2>&1
chmod +x /usr/bin/x-ui > /dev/null 2>&1
mkdir -p /var/log/x-ui > /dev/null 2>&1
echo -e "${green}Changing owner...${plain}"
chown -R root:root ${xui_folder} >/dev/null 2>&1
chown -R root:root ${xui_folder} > /dev/null 2>&1
if [ -f "${xui_folder}/bin/config.json" ]; then
echo -e "${green}Changing on config file permissions...${plain}"
chmod 640 ${xui_folder}/bin/config.json >/dev/null 2>&1
chmod 640 ${xui_folder}/bin/config.json > /dev/null 2>&1
fi
if [[ $release == "alpine" ]]; then
echo -e "${green}Downloading and installing startup unit x-ui.rc...${plain}"
${curl_bin} -fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
${curl_bin} -fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc > /dev/null 2>&1
if [[ $? -ne 0 ]]; then
${curl_bin} -4fLRo /etc/init.d/x-ui https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.rc >/dev/null 2>&1
${curl_bin} -4fLRo /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
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
if [ -f "x-ui.service" ]; then
echo -e "${green}Installing systemd unit...${plain}"
cp -f x-ui.service ${xui_service}/ >/dev/null 2>&1
cp -f x-ui.service ${xui_service}/ > /dev/null 2>&1
if [[ $? -ne 0 ]]; then
echo -e "${red}Failed to copy x-ui.service${plain}"
exit 1
@ -901,62 +901,62 @@ update_x-ui() {
ubuntu | debian | armbian)
if [ -f "x-ui.service.debian" ]; then
echo -e "${green}Installing debian-like systemd unit...${plain}"
cp -f x-ui.service.debian ${xui_service}/x-ui.service >/dev/null 2>&1
cp -f x-ui.service.debian ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
service_installed=true
fi
fi
;;
;;
arch | manjaro | parch)
if [ -f "x-ui.service.arch" ]; then
echo -e "${green}Installing arch-like systemd unit...${plain}"
cp -f x-ui.service.arch ${xui_service}/x-ui.service >/dev/null 2>&1
cp -f x-ui.service.arch ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
service_installed=true
fi
fi
;;
;;
*)
if [ -f "x-ui.service.rhel" ]; then
echo -e "${green}Installing rhel-like systemd unit...${plain}"
cp -f x-ui.service.rhel ${xui_service}/x-ui.service >/dev/null 2>&1
cp -f x-ui.service.rhel ${xui_service}/x-ui.service > /dev/null 2>&1
if [[ $? -eq 0 ]]; then
service_installed=true
fi
fi
;;
;;
esac
# If service file not found in tar.gz, download from GitHub
if [ "$service_installed" = false ]; then
echo -e "${yellow}Service files not found in tar.gz, downloading from GitHub...${plain}"
case "${release}" in
ubuntu | debian | armbian)
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian >/dev/null 2>&1
;;
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.debian > /dev/null 2>&1
;;
arch | manjaro | parch)
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch >/dev/null 2>&1
;;
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.arch > /dev/null 2>&1
;;
*)
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel >/dev/null 2>&1
;;
${curl_bin} -4fLRo ${xui_service}/x-ui.service https://raw.githubusercontent.com/MHSanaei/3x-ui/main/x-ui.service.rhel > /dev/null 2>&1
;;
esac
if [[ $? -ne 0 ]]; then
echo -e "${red}Failed to install x-ui.service from GitHub${plain}"
exit 1
fi
fi
fi
chown root:root ${xui_service}/x-ui.service >/dev/null 2>&1
chmod 644 ${xui_service}/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
chown root:root ${xui_service}/x-ui.service > /dev/null 2>&1
chmod 644 ${xui_service}/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 "┌───────────────────────────────────────────────────────┐

View file

@ -697,7 +697,6 @@ class TlsStreamSettings extends XrayCommonClass {
certificates = [new TlsStreamSettings.Cert()],
alpn = [ALPN_OPTION.H2, ALPN_OPTION.HTTP1],
echServerKeys = '',
echForceQuery = 'none',
settings = new TlsStreamSettings.Settings()
) {
super();
@ -711,7 +710,6 @@ class TlsStreamSettings extends XrayCommonClass {
this.certs = certificates;
this.alpn = alpn;
this.echServerKeys = echServerKeys;
this.echForceQuery = echForceQuery;
this.settings = settings;
}
@ -744,7 +742,6 @@ class TlsStreamSettings extends XrayCommonClass {
certs,
json.alpn,
json.echServerKeys,
json.echForceQuery,
settings,
);
}
@ -761,7 +758,6 @@ class TlsStreamSettings extends XrayCommonClass {
certificates: TlsStreamSettings.toJsonArray(this.certs),
alpn: this.alpn,
echServerKeys: this.echServerKeys,
echForceQuery: this.echForceQuery,
settings: this.settings,
};
}
@ -875,7 +871,7 @@ class RealityStreamSettings extends XrayCommonClass {
if (!target && !serverNames) {
const randomTarget = typeof getRandomRealityTarget !== 'undefined'
? getRandomRealityTarget()
: { target: 'www.apple.com:443', sni: 'www.apple.com,apple.com' };
: { target: 'www.amazon.com:443', sni: 'www.amazon.com,amazon.com' };
target = randomTarget.target;
serverNames = randomTarget.sni;
}
@ -1211,15 +1207,15 @@ class QuicParams extends XrayCommonClass {
constructor(
congestion = 'bbr',
debug = false,
brutalUp = '',
brutalDown = '',
brutalUp = 65537,
brutalDown = 65537,
udpHop = undefined,
initStreamReceiveWindow = 8388608,
maxStreamReceiveWindow = 8388608,
initConnectionReceiveWindow = 20971520,
maxConnectionReceiveWindow = 20971520,
maxIdleTimeout = 30,
keepAlivePeriod = 0,
keepAlivePeriod = 5,
disablePathMTUDiscovery = false,
maxIncomingStreams = 1024,
) {
@ -1269,8 +1265,10 @@ class QuicParams extends XrayCommonClass {
toJson() {
const result = { congestion: this.congestion };
if (this.debug) result.debug = this.debug;
if (this.brutalUp) result.brutalUp = this.brutalUp;
if (this.brutalDown) result.brutalDown = this.brutalDown;
if (['brutal', 'force-brutal'].includes(this.congestion)) {
if (this.brutalUp) result.brutalUp = this.brutalUp;
if (this.brutalDown) result.brutalDown = this.brutalDown;
}
if (this.udpHop) result.udpHop = { ports: this.udpHop.ports, interval: this.udpHop.interval };
if (this.initStreamReceiveWindow > 0) result.initStreamReceiveWindow = this.initStreamReceiveWindow;
if (this.maxStreamReceiveWindow > 0) result.maxStreamReceiveWindow = this.maxStreamReceiveWindow;

View file

@ -6,10 +6,10 @@ const Protocols = {
VLESS: "vless",
Trojan: "trojan",
Shadowsocks: "shadowsocks",
Wireguard: "wireguard",
Hysteria: "hysteria",
Socks: "socks",
HTTP: "http",
Wireguard: "wireguard",
Hysteria: "hysteria"
};
const SSMethods = {
@ -500,7 +500,7 @@ class HysteriaStreamSettings extends CommonClass {
initConnectionReceiveWindow = 20971520,
maxConnectionReceiveWindow = 20971520,
maxIdleTimeout = 30,
keepAlivePeriod = 0,
keepAlivePeriod = 2,
disablePathMTUDiscovery = false
) {
super();
@ -789,9 +789,17 @@ class QuicParams extends CommonClass {
constructor(
congestion = 'bbr',
debug = false,
brutalUp = '',
brutalDown = '',
brutalUp = 65537,
brutalDown = 65537,
udpHop = undefined,
initStreamReceiveWindow = 8388608,
maxStreamReceiveWindow = 8388608,
initConnectionReceiveWindow = 20971520,
maxConnectionReceiveWindow = 20971520,
maxIdleTimeout = 30,
keepAlivePeriod = 5,
disablePathMTUDiscovery = false,
maxIncomingStreams = 1024,
) {
super();
this.congestion = congestion;
@ -799,6 +807,14 @@ class QuicParams extends CommonClass {
this.brutalUp = brutalUp;
this.brutalDown = brutalDown;
this.udpHop = udpHop;
this.initStreamReceiveWindow = initStreamReceiveWindow;
this.maxStreamReceiveWindow = maxStreamReceiveWindow;
this.initConnectionReceiveWindow = initConnectionReceiveWindow;
this.maxConnectionReceiveWindow = maxConnectionReceiveWindow;
this.maxIdleTimeout = maxIdleTimeout;
this.keepAlivePeriod = keepAlivePeriod;
this.disablePathMTUDiscovery = disablePathMTUDiscovery;
this.maxIncomingStreams = maxIncomingStreams;
}
get hasUdpHop() {
@ -817,15 +833,33 @@ class QuicParams extends CommonClass {
json.brutalUp,
json.brutalDown,
json.udpHop ? { ports: json.udpHop.ports, interval: json.udpHop.interval } : undefined,
json.initStreamReceiveWindow,
json.maxStreamReceiveWindow,
json.initConnectionReceiveWindow,
json.maxConnectionReceiveWindow,
json.maxIdleTimeout,
json.keepAlivePeriod,
json.disablePathMTUDiscovery,
json.maxIncomingStreams,
);
}
toJson() {
const result = { congestion: this.congestion };
if (this.debug) result.debug = this.debug;
if (this.brutalUp) result.brutalUp = this.brutalUp;
if (this.brutalDown) result.brutalDown = this.brutalDown;
if (['brutal', 'force-brutal'].includes(this.congestion)) {
if (this.brutalUp) result.brutalUp = this.brutalUp;
if (this.brutalDown) result.brutalDown = this.brutalDown;
}
if (this.udpHop) result.udpHop = { ports: this.udpHop.ports, interval: this.udpHop.interval };
if (this.initStreamReceiveWindow > 0) result.initStreamReceiveWindow = this.initStreamReceiveWindow;
if (this.maxStreamReceiveWindow > 0) result.maxStreamReceiveWindow = this.maxStreamReceiveWindow;
if (this.initConnectionReceiveWindow > 0) result.initConnectionReceiveWindow = this.initConnectionReceiveWindow;
if (this.maxConnectionReceiveWindow > 0) result.maxConnectionReceiveWindow = this.maxConnectionReceiveWindow;
if (this.maxIdleTimeout !== 30 && this.maxIdleTimeout > 0) result.maxIdleTimeout = this.maxIdleTimeout;
if (this.keepAlivePeriod > 0) result.keepAlivePeriod = this.keepAlivePeriod;
if (this.disablePathMTUDiscovery) result.disablePathMTUDiscovery = this.disablePathMTUDiscovery;
if (this.maxIncomingStreams > 0) result.maxIncomingStreams = this.maxIncomingStreams;
return result;
}
}
@ -1425,14 +1459,16 @@ Outbound.FreedomSettings = class extends CommonClass {
redirect = '',
fragment = {},
noises = [],
ipsBlocked = [],
finalRules = [],
) {
super();
this.domainStrategy = domainStrategy;
this.redirect = redirect;
this.fragment = fragment || {};
this.noises = Array.isArray(noises) ? noises : [];
this.ipsBlocked = Array.isArray(ipsBlocked) ? ipsBlocked : [];
this.finalRules = Array.isArray(finalRules)
? finalRules.map(rule => rule instanceof Outbound.FreedomSettings.FinalRule ? rule : Outbound.FreedomSettings.FinalRule.fromJson(rule))
: [];
}
addNoise() {
@ -1443,13 +1479,30 @@ Outbound.FreedomSettings = class extends CommonClass {
this.noises.splice(index, 1);
}
addFinalRule(action = 'block') {
this.finalRules.push(new Outbound.FreedomSettings.FinalRule(action));
}
delFinalRule(index) {
this.finalRules.splice(index, 1);
}
static fromJson(json = {}) {
const finalRules = Array.isArray(json.finalRules)
? json.finalRules.map(rule => Outbound.FreedomSettings.FinalRule.fromJson(rule))
: [];
// Backward compatibility: map legacy ipsBlocked entries to blocking finalRules.
if (finalRules.length === 0 && Array.isArray(json.ipsBlocked) && json.ipsBlocked.length > 0) {
finalRules.push(new Outbound.FreedomSettings.FinalRule('block', '', '', json.ipsBlocked, ''));
}
return new Outbound.FreedomSettings(
json.domainStrategy,
json.redirect,
json.fragment ? Outbound.FreedomSettings.Fragment.fromJson(json.fragment) : {},
json.noises ? json.noises.map(noise => Outbound.FreedomSettings.Noise.fromJson(noise)) : [],
json.ipsBlocked || [],
finalRules,
);
}
@ -1459,7 +1512,7 @@ Outbound.FreedomSettings = class extends CommonClass {
redirect: ObjectUtil.isEmpty(this.redirect) ? undefined : this.redirect,
fragment: Object.keys(this.fragment).length === 0 ? undefined : this.fragment,
noises: this.noises.length === 0 ? undefined : Outbound.FreedomSettings.Noise.toJsonArray(this.noises),
ipsBlocked: this.ipsBlocked.length === 0 ? undefined : this.ipsBlocked,
finalRules: this.finalRules.length === 0 ? undefined : Outbound.FreedomSettings.FinalRule.toJsonArray(this.finalRules),
};
}
};
@ -1521,6 +1574,37 @@ Outbound.FreedomSettings.Noise = class extends CommonClass {
}
};
Outbound.FreedomSettings.FinalRule = class extends CommonClass {
constructor(action = 'block', network = '', port = '', ip = [], blockDelay = '') {
super();
this.action = action;
this.network = network;
this.port = port;
this.ip = Array.isArray(ip) ? ip : [];
this.blockDelay = blockDelay;
}
static fromJson(json = {}) {
return new Outbound.FreedomSettings.FinalRule(
json.action,
Array.isArray(json.network) ? json.network.join(',') : json.network,
json.port,
json.ip || [],
json.blockDelay,
);
}
toJson() {
return {
action: ['allow', 'block'].includes(this.action) ? this.action : 'block',
network: ObjectUtil.isEmpty(this.network) ? undefined : this.network,
port: ObjectUtil.isEmpty(this.port) ? undefined : this.port,
ip: this.ip.length === 0 ? undefined : this.ip,
blockDelay: this.action === 'block' && !ObjectUtil.isEmpty(this.blockDelay) ? this.blockDelay : undefined,
};
}
};
Outbound.BlackholeSettings = class extends CommonClass {
constructor(type) {
super();

View file

@ -1,7 +1,5 @@
// List of popular services for VLESS Reality Target/SNI randomization
const REALITY_TARGETS = [
{ target: 'www.apple.com:443', sni: 'www.apple.com' },
{ target: 'www.icloud.com:443', sni: 'www.icloud.com' },
{ target: 'www.amazon.com:443', sni: 'www.amazon.com' },
{ target: 'aws.amazon.com:443', sni: 'aws.amazon.com' },
{ target: 'www.oracle.com:443', sni: 'www.oracle.com' },

View file

@ -43,6 +43,7 @@ class AllSetting {
this.subDomain = "";
this.externalTrafficInformEnable = false;
this.externalTrafficInformURI = "";
this.restartXrayOnClientDisable = true;
this.subCertFile = "";
this.subKeyFile = "";
this.subUpdates = 12;

View file

@ -7,6 +7,7 @@
const data = {
sId: el.getAttribute('data-sid') || '',
enabled: (el.getAttribute('data-enabled') || '').toLowerCase() === 'true',
subUrl: el.getAttribute('data-sub-url') || '',
subJsonUrl: el.getAttribute('data-subjson-url') || '',
subClashUrl: el.getAttribute('data-subclash-url') || '',
@ -128,9 +129,10 @@
},
isActive() {
const now = Date.now();
const enabledOk = this.app.enabled;
const expiryOk = !this.app.expireMs || this.app.expireMs >= now;
const trafficOk = !this.app.totalByte || (this.app.uploadByte + this.app.downloadByte) <= this.app.totalByte;
return expiryOk && trafficOk;
return enabledOk && expiryOk && trafficOk;
},
shadowrocketUrl() {
const rawUrl = this.app.subUrl + '?flag=shadowrocket';

View file

@ -1,8 +1,10 @@
package controller
import (
"fmt"
"net"
"net/http"
"net/netip"
"strings"
"github.com/mhsanaei/3x-ui/v2/config"
@ -14,18 +16,58 @@ import (
// getRemoteIp extracts the real IP address from the request headers or remote address.
func getRemoteIp(c *gin.Context) string {
value := c.GetHeader("X-Real-IP")
if value != "" {
return value
if ip, ok := extractTrustedIP(c.GetHeader("X-Real-IP")); ok {
return ip
}
value = c.GetHeader("X-Forwarded-For")
if value != "" {
ips := strings.Split(value, ",")
return ips[0]
if xff := c.GetHeader("X-Forwarded-For"); xff != "" {
for _, part := range strings.Split(xff, ",") {
if ip, ok := extractTrustedIP(part); ok {
return ip
}
}
}
addr := c.Request.RemoteAddr
ip, _, _ := net.SplitHostPort(addr)
return ip
if ip, ok := extractTrustedIP(c.Request.RemoteAddr); ok {
return ip
}
return "unknown"
}
func extractTrustedIP(value string) (string, bool) {
candidate := strings.TrimSpace(value)
if candidate == "" {
return "", false
}
if ip, ok := parseIPCandidate(candidate); ok {
return ip.String(), true
}
if host, _, err := net.SplitHostPort(candidate); err == nil {
if ip, ok := parseIPCandidate(host); ok {
return ip.String(), true
}
}
if strings.Count(candidate, ":") == 1 {
if host, _, err := net.SplitHostPort(fmt.Sprintf("[%s]", candidate)); err == nil {
if ip, ok := parseIPCandidate(host); ok {
return ip.String(), true
}
}
}
return "", false
}
func parseIPCandidate(value string) (netip.Addr, bool) {
ip, err := netip.ParseAddr(strings.TrimSpace(value))
if err != nil {
return netip.Addr{}, false
}
return ip.Unmap(), true
}
// jsonMsg sends a JSON response with a message and error status.

View file

@ -71,6 +71,7 @@ type AllSetting struct {
SubUpdates int `json:"subUpdates" form:"subUpdates"` // Subscription update interval in minutes
ExternalTrafficInformEnable bool `json:"externalTrafficInformEnable" form:"externalTrafficInformEnable"` // Enable external traffic reporting
ExternalTrafficInformURI string `json:"externalTrafficInformURI" form:"externalTrafficInformURI"` // URI for external traffic reporting
RestartXrayOnClientDisable bool `json:"restartXrayOnClientDisable" form:"restartXrayOnClientDisable"` // Restart Xray when clients are auto-disabled by expiry/traffic limit
SubEncrypt bool `json:"subEncrypt" form:"subEncrypt"` // Encrypt subscription responses
SubShowInfo bool `json:"subShowInfo" form:"subShowInfo"` // Show client information in subscriptions
SubURI string `json:"subURI" form:"subURI"` // Subscription server URI

View file

@ -1,6 +1,7 @@
{{ define "page/head_start" }}
<!DOCTYPE html>
<html>
<head>
<meta charset="UTF-8">
<meta name="renderer" content="webkit">
@ -12,6 +13,7 @@
[v-cloak] {
display: none;
}
/* vazirmatn-regular - arabic_latin_latin-ext */
@font-face {
font-display: swap;
@ -21,10 +23,11 @@
src: url('{{ .base_path }}assets/Vazirmatn-UI-NL-Regular.woff2') format('woff2');
unicode-range: U+0600-06FF, U+200C-200E, U+2010-2011, U+204F, U+2E41, U+FB50-FDFF, U+FE80-FEFC, U+0030-0039;
}
body {
font-family: system-ui, -apple-system, BlinkMacSystemFont, 'Vazirmatn', 'Segoe UI', Roboto, Oxygen, Ubuntu, Cantarell, 'Open Sans', 'Helvetica Neue', sans-serif;
}
/* mobile touch scrolling for tabs */
@media (max-width: 576px) {
.ant-tabs-nav-container {
@ -34,59 +37,69 @@
overscroll-behavior-x: contain;
white-space: nowrap;
max-width: 100%;
padding: 0 !important; /* Remove padding for arrows */
padding: 0 !important;
/* Remove padding for arrows */
}
.ant-tabs-nav-wrap {
overflow: visible !important;
padding: 0 !important;
}
.ant-tabs-nav-scroll {
overflow: visible !important;
box-shadow: none !important;
}
.ant-tabs-nav {
display: flex !important;
transform: none !important; /* Disable JS transform */
width: auto !important;
margin: 0 !important;
display: flex !important;
transform: none !important;
/* Disable JS transform */
width: auto !important;
margin: 0 !important;
}
.ant-tabs-tab-prev,
.ant-tabs-tab-next {
display: none !important; /* Hide arrows */
display: none !important;
/* Hide arrows */
}
.ant-tabs-nav-container::-webkit-scrollbar {
display: none;
}
}
</style>
<title>{{ .host }} {{ i18n .title}}</title>
{{ end }}
{{ end }}
{{ define "page/head_end" }}
{{ define "page/head_end" }}
</head>
{{ end }}
{{ define "page/body_start" }}
<body>
<div id="message"></div>
{{ end }}
{{ end }}
{{ define "page/body_scripts" }}
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
<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/index.js?{{ .cur_ver }}"></script>
<script>
const basePath = '{{ .base_path }}';
axios.defaults.baseURL = basePath;
</script>
<script src="{{ .base_path }}assets/js/websocket.js?{{ .cur_ver }}"></script>
{{ end }}
{{ define "page/body_end" }}
{{ define "page/body_scripts" }}
<script src="{{ .base_path }}assets/vue/vue.min.js?{{ .cur_ver }}"></script>
<script src="{{ .base_path }}assets/moment/moment.min.js"></script>
<script src="{{ .base_path }}assets/ant-design-vue/antd.min.js"></script>
<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/index.js?{{ .cur_ver }}"></script>
<script>
const basePath = '{{ .base_path }}';
axios.defaults.baseURL = basePath;
</script>
<script src="{{ .base_path }}assets/js/websocket.js?{{ .cur_ver }}"></script>
{{ end }}
{{ define "page/body_end" }}
</body>
</html>
{{ end }}

View file

@ -2,30 +2,39 @@
<template slot="actions" slot-scope="text, client, index">
<a-tooltip>
<template slot="title">{{ i18n "qrCode" }}</template>
<a-icon :style="{ fontSize: '22px', marginInlineStart: '14px' }" class="normal-icon" type="qrcode" v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
<a-icon :style="{ fontSize: '22px', marginInlineStart: '14px' }" class="normal-icon" type="qrcode"
v-if="record.hasLink()" @click="showQrcode(record.id,client);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.client.edit" }}</template>
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="edit" @click="openEditClient(record.id,client);"></a-icon>
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="edit"
@click="openEditClient(record.id,client);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "info" }}</template>
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="info-circle" @click="showInfo(record.id,client);"></a-icon>
<a-icon :style="{ fontSize: '22px' }" class="normal-icon" type="info-circle"
@click="showInfo(record.id,client);"></a-icon>
</a-tooltip>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)" title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
<a-popconfirm @confirm="resetClientTraffic(client,record.id,false)"
title='{{ i18n "pages.inbounds.resetTrafficContent"}}' :overlay-class-name="themeSwitcher.currentTheme"
ok-text='{{ i18n "reset"}}' cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" :style="{ color: 'var(--color-primary-100)'}"></a-icon>
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="normal-icon" type="retweet" v-if="client.email.length > 0"></a-icon>
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="normal-icon" type="retweet"
v-if="client.email.length > 0"></a-icon>
</a-popconfirm>
</a-tooltip>
<a-tooltip>
<template slot="title">
<span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span>
</template>
<a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}' :overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger" cancel-text='{{ i18n "cancel"}}'>
<a-popconfirm @confirm="delClient(record.id,client,false)" title='{{ i18n "pages.inbounds.deleteClientContent"}}'
:overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "delete"}}' ok-type="danger"
cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o" :style="{ color: '#e04141' }"></a-icon>
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="delete-icon" type="delete" v-if="isRemovable(record.id)"></a-icon>
<a-icon :style="{ fontSize: '22px', cursor: 'pointer' }" class="delete-icon" type="delete"
v-if="isRemovable(record.id)"></a-icon>
</a-popconfirm>
</a-tooltip>
</template>
@ -34,7 +43,7 @@
</template>
<template slot="online" slot-scope="text, client, index">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
<template slot="content" >
<template slot="content">
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
</template>
<template v-if="client.enable && isClientOnline(client.email)">
@ -53,7 +62,8 @@
<template v-else-if="!client.enable">{{ i18n "disabled" }}</template>
<template v-else-if="client.enable && isClientOnline(client.email)">{{ i18n "online" }}</template>
</template>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''" :color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
<a-badge :class="isClientOnline(client.email)? 'online-animation' : ''"
:color="client.enable ? statsExpColor(record, client.email) : themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc'"></a-badge>
</a-tooltip>
<a-space direction="vertical" :size="2">
<span class="client-email">[[ client.email ]]</span>
@ -127,11 +137,16 @@
<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)"> [[ IntlUtil.formatRelativeTime(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">
<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">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
<path
d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
fill="currentColor"></path>
</svg>
</a-tag>
</template>
@ -161,7 +176,8 @@
<span :style="{ color: '#FF4D4F' }"> {{ i18n "delete"}}</span>
</a-menu-item>
<a-menu-item>
<a-switch v-model="client.enable" size="small" @change="switchEnableClient(record.id, client, $event)"></a-switch>
<a-switch v-model="client.enable" size="small"
@change="switchEnableClient(record.id, client, $event)"></a-switch>
{{ i18n "enable"}}
</a-menu-item>
</a-menu>
@ -175,9 +191,11 @@
<td colspan="3" :style="{ textAlign: 'center' }">{{ i18n "pages.inbounds.traffic" }}</td>
</tr>
<tr>
<td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ SizeFormatter.sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] </td>
<td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[
SizeFormatter.sizeFormat(getUpStats(record, client.email) + getDownStats(record, client.email)) ]] </td>
<td width="120px" v-if="!client.enable">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false" :percent="statsProgress(record, client.email)" />
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? 'rgb(72 84 105)' : '#bcbcbc'" :show-info="false"
:percent="statsProgress(record, client.email)" />
</td>
<td width="120px" v-else-if="client.totalGB > 0">
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
@ -193,11 +211,14 @@
</tr>
</table>
</template>
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="statsProgress(record, client.email)" />
<a-progress :stroke-color="clientStatsColor(record, client.email)" :show-info="false"
:status="isClientDepleted(record, client.email)? 'exception' : ''"
:percent="statsProgress(record, client.email)" />
</a-popover>
</td>
<td width="120px" v-else class="infinite-bar">
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'" :show-info="false" :percent="100"></a-progress>
<a-progress :stroke-color="themeSwitcher.isDarkTheme ? '#2c1e32':'#F2EAF1'" :show-info="false"
:percent="100"></a-progress>
</td>
<td width="80px">
<template v-if="client.totalGB > 0">[[ client._totalGB + "GB" ]]</template>
@ -212,14 +233,16 @@
</tr>
<tr>
<template v-if="client.expiryTime !=0 && client.reset >0">
<td width="80px" :style="{ margin: '0', textAlign: 'right', fontSize: '1em' }"> [[ IntlUtil.formatRelativeTime(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>[[ IntlUtil.formatDate(client.expiryTime) ]]</span>
</template>
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''" :percent="expireProgress(client.expiryTime, client.reset)" />
<a-progress :show-info="false" :status="isClientDepleted(record, client.email)? 'exception' : ''"
:percent="expireProgress(client.expiryTime, client.reset)" />
</a-popover>
</td>
<td width="60px">[[ client.reset + "d" ]]</td>
@ -231,11 +254,16 @@
<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)"> [[ IntlUtil.formatRelativeTime(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">
<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">
<path d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z" fill="currentColor"></path>
<path
d="M484.4 96C407 96 349.2 164.1 320 208.5C290.8 164.1 233 96 155.6 96C69.75 96 0 167.8 0 256s69.75 160 155.6 160C233.1 416 290.8 347.9 320 303.5C349.2 347.9 407 416 484.4 416C570.3 416 640 344.2 640 256S570.3 96 484.4 96zM155.6 368C96.25 368 48 317.8 48 256s48.25-112 107.6-112c67.75 0 120.5 82.25 137.1 112C276 285.8 223.4 368 155.6 368zM484.4 368c-67.75 0-120.5-82.25-137.1-112C364 226.2 416.6 144 484.4 144C543.8 144 592 194.2 592 256S543.8 368 484.4 368z"
fill="currentColor"></path>
</svg>
</a-tag>
</template>
@ -244,7 +272,8 @@
</table>
</template>
<a-badge>
<a-icon v-if="!client.enable" slot="count" type="pause-circle" theme="filled" :style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
<a-icon v-if="!client.enable" slot="count" type="pause-circle" theme="filled"
:style="{ color: themeSwitcher.isDarkTheme ? '#2c3950' : '#bcbcbc' }"></a-icon>
<a-button shape="round" size="small" :style="{ fontSize: '14px', padding: '0 10px' }">
<a-icon type="solution"></a-icon>
</a-button>
@ -267,4 +296,4 @@
-
</template>
</template>
{{end}}
{{end}}

View file

@ -1,13 +1,13 @@
{{define "component/customStatistic"}}
<template>
<a-statistic :title="title" :value="value">
<template #prefix>
<slot name="prefix"></slot>
</template>
<template #suffix>
<slot name="suffix"></slot>
</template>
</a-statistic>
<a-statistic :title="title" :value="value">
<template #prefix>
<slot name="prefix"></slot>
</template>
<template #suffix>
<slot name="suffix"></slot>
</template>
</a-statistic>
</template>
{{end}}
@ -16,9 +16,11 @@
.dark .ant-statistic-content {
color: var(--dark-color-text-primary)
}
.dark .ant-statistic-title {
color: rgba(255, 255, 255, 0.55)
}
.ant-statistic-content {
font-size: 16px;
}
@ -36,7 +38,7 @@
required: false
}
},
template: `{{template "component/customStatistic"}}`,
template: `{{template "component/customStatistic" .}}`,
});
</script>
{{end}}

View file

@ -34,7 +34,7 @@
required: false,
},
},
template: `{{template "component/persianDatepickerTemplate"}}`,
template: `{{template "component/persianDatepickerTemplate" .}}`,
data() {
return {
date: '',
@ -42,7 +42,7 @@
};
},
watch: {
value: function (date) {
value: function(date) {
this.date = this.convertToJalalian(date)
}
},
@ -52,7 +52,8 @@
},
methods: {
convertToGregorian(date) {
return date ? moment(moment(date, 'jYYYY/jMM/jDD HH:mm:ss').format('YYYY-MM-DD HH:mm:ss')) : null
return date ? moment(moment(date, 'jYYYY/jMM/jDD HH:mm:ss').format('YYYY-MM-DD HH:mm:ss')) :
null
},
convertToJalalian(date) {
return date && moment.isMoment(date) ? date.format('jYYYY/jMM/jDD HH:mm:ss') : null

View file

@ -26,7 +26,7 @@
type: String,
required: false,
defaultValue: "default",
validator: function (value) {
validator: function(value) {
return ['small', 'default'].includes(value)
}
}
@ -46,4 +46,4 @@
}
})
</script>
{{end}}
{{end}}

View file

@ -43,8 +43,7 @@
Vue.component('a-sidebar', {
data() {
return {
tabs: [
{
tabs: [{
key: '{{ .base_path }}panel/',
icon: 'dashboard',
title: '{{ i18n "menu.dashboard"}}'
@ -79,8 +78,8 @@
},
methods: {
openLink(key) {
return key.startsWith('http') ?
window.open(key) :
return key.startsWith('http') ?
window.open(key) :
location.href = key
},
closeDrawer() {
@ -97,7 +96,7 @@
}
}
},
template: `{{template "component/sidebar/content"}}`,
template: `{{template "component/sidebar/content" .}}`,
});
</script>
{{end}}

View file

@ -213,7 +213,7 @@
});
Vue.component('a-table-sort-trigger', {
template: `{{template "component/sortableTableTrigger"}}`,
template: `{{template "component/sortableTableTrigger" .}}`,
props: {
'item-index': { type: undefined, required: false },
},

View file

@ -24,9 +24,11 @@
{{define "component/themeSwitchTemplateLogin"}}
<template>
<a-space @mousedown="themeSwitcher.animationsOff()" id="change-theme" direction="vertical" :size="10" :style="{ width: '100%' }">
<a-space @mousedown="themeSwitcher.animationsOff()" id="change-theme" direction="vertical" :size="10"
:style="{ width: '100%' }">
<a-space direction="horizontal" size="small">
<a-switch size="small" :default-checked="themeSwitcher.isDarkTheme" @change="themeSwitcher.toggleTheme()"></a-switch>
<a-switch size="small" :default-checked="themeSwitcher.isDarkTheme"
@change="themeSwitcher.toggleTheme()"></a-switch>
<span>{{ i18n "menu.dark" }}</span>
</a-space>
<a-space v-if="themeSwitcher.isDarkTheme" direction="horizontal" size="small">
@ -93,7 +95,7 @@
}
const themeSwitcher = createThemeSwitcher();
Vue.component('a-theme-switch', {
template: `{{template "component/themeSwitchTemplate"}}`,
template: `{{template "component/themeSwitchTemplate" .}}`,
data: () => ({
themeSwitcher
}),
@ -105,7 +107,7 @@
}
});
Vue.component('a-theme-switch-login', {
template: `{{template "component/themeSwitchTemplateLogin"}}`,
template: `{{template "component/themeSwitchTemplateLogin" .}}`,
data: () => ({
themeSwitcher
}),

View file

@ -1,11 +1,5 @@
{{define "form/client"}}
<a-form
layout="horizontal"
v-if="client"
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form layout="horizontal" v-if="client" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.enable" }}'>
<a-switch v-model="client.enable"></a-switch>
</a-form-item>
@ -16,33 +10,22 @@
<span>{{ i18n "pages.inbounds.emailDesc" }}</span>
</template>
{{ i18n "pages.inbounds.email" }}
<a-icon
type="sync"
@click="client.email = RandomUtil.randomLowerAndNum(9)"
></a-icon>
<a-icon type="sync" @click="client.email = RandomUtil.randomLowerAndNum(9)"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="client.email"></a-input>
</a-form-item>
<a-form-item
v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS"
>
<a-form-item v-if="inbound.protocol === Protocols.TROJAN || inbound.protocol === Protocols.SHADOWSOCKS">
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
{{ i18n "password" }}
<a-icon
v-if="inbound.protocol === Protocols.SHADOWSOCKS"
@click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)"
type="sync"
></a-icon>
<a-icon
v-if="inbound.protocol === Protocols.TROJAN"
@click="client.password = RandomUtil.randomSeq(10)"
type="sync"
>
<a-icon v-if="inbound.protocol === Protocols.SHADOWSOCKS"
@click="client.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)" type="sync"></a-icon>
<a-icon v-if="inbound.protocol === Protocols.TROJAN" @click="client.password = RandomUtil.randomSeq(10)"
type="sync">
</a-icon>
</a-tooltip>
</template>
@ -55,42 +38,26 @@
<span>{{ i18n "reset" }}</span>
</template>
Auth Password
<a-icon
@click="client.auth = RandomUtil.randomSeq(10)"
type="sync"
></a-icon>
<a-icon @click="client.auth = RandomUtil.randomSeq(10)" type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="client.auth"></a-input>
</a-form-item>
<a-form-item
v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS"
>
<a-form-item v-if="inbound.protocol === Protocols.VMESS || inbound.protocol === Protocols.VLESS">
<template slot="label">
<a-tooltip>
<template slot="title">
<span>{{ i18n "reset" }}</span>
</template>
ID
<a-icon
@click="client.id = RandomUtil.randomUUID()"
type="sync"
></a-icon>
<a-icon @click="client.id = RandomUtil.randomUUID()" type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="client.id"></a-input>
</a-form-item>
<a-form-item
v-if="inbound.protocol === Protocols.VMESS"
label='{{ i18n "security" }}'
>
<a-select
v-model="client.security"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option v-for="key in USERS_SECURITY" :value="key"
>[[ key ]]</a-select-option
>
<a-form-item v-if="inbound.protocol === Protocols.VMESS" label='{{ i18n "security" }}'>
<a-select v-model="client.security" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="client.email && app.subSettings?.enable">
@ -100,10 +67,7 @@
<span>{{ i18n "pages.inbounds.subscriptionDesc" }}</span>
</template>
Subscription
<a-icon
@click="client.subId = RandomUtil.randomLowerAndNum(16)"
type="sync"
></a-icon>
<a-icon @click="client.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="client.subId"></a-input>
@ -118,11 +82,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number
:style="{ width: '50%' }"
v-model.number="client.tgId"
min="0"
></a-input-number>
<a-input-number :style="{ width: '50%' }" v-model.number="client.tgId" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="client.email" label='{{ i18n "comment" }}'>
<a-input v-model.trim="client.comment"></a-input>
@ -139,9 +99,7 @@
</template>
<a-input-number v-model.number="client.limitIp" min="0"></a-input-number>
</a-form-item>
<a-form-item
v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit"
>
<a-form-item v-if="app.ipLimitEnable && client.limitIp > 0 && client.email && isEdit">
<template slot="label">
<a-tooltip>
<template slot="title">
@ -160,25 +118,15 @@
</span>
</a-tooltip>
<a-form layout="block">
<a-textarea
id="clientIPs"
readonly
@click="getDBClientIps(client.email)"
placeholder="Click To Get IPs"
:auto-size="{ minRows: 5, maxRows: 10 }"
>
<a-textarea id="clientIPs" readonly @click="getDBClientIps(client.email)" placeholder="Click To Get IPs"
:auto-size="{ minRows: 5, maxRows: 10 }">
</a-textarea>
</a-form>
</a-form-item>
<a-form-item v-if="inbound.canEnableTlsFlow()" label="Flow">
<a-select
v-model="client.flow"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select v-model="client.flow" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value selected>{{ i18n "none" }}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key"
>[[ key ]]</a-select-option
>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item>
@ -201,45 +149,28 @@
</a-tag>
<a-tooltip>
<template slot="title">{{ i18n "pages.inbounds.resetTraffic" }}</template>
<a-icon
type="retweet"
@click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"
></a-icon>
<a-icon type="retweet" @click="resetClientTraffic(client.email,clientStats.inboundId,$event.target)"
v-if="client.email.length > 0"></a-icon>
</a-tooltip>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="delayedStart" @click="client._expiryTime=0"></a-switch>
</a-form-item>
<a-form-item v-if="delayedStart" label='{{ i18n "pages.client.expireDays" }}'>
<a-input-number
v-model.number="delayedExpireDays"
:min="0"
></a-input-number>
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<template slot="label">
<a-tooltip>
<template slot="title"
>{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</template
>
<template slot="title">{{ i18n "pages.inbounds.leaveBlankToNeverExpire" }}</template>
{{ i18n "pages.inbounds.expireDate" }}
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-date-picker
v-if="datepicker == 'gregorian'"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="client._expiryTime"
></a-date-picker>
<a-persian-datepicker
v-else
placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="client._expiryTime"
v-model="client._expiryTime"
></a-persian-datepicker>
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }" format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme" v-model="client._expiryTime"></a-date-picker>
<a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="client._expiryTime" v-model="client._expiryTime"></a-persian-datepicker>
<a-tag color="red" v-if="isEdit && isExpiry">Expired</a-tag>
</a-form-item>
<a-form-item v-if="client.expiryTime != 0">
@ -253,4 +184,4 @@
<a-input-number v-model.number="client.reset" :min="0"></a-input-number>
</a-form-item>
</a-form>
{{end}}
{{end}}

View file

@ -102,69 +102,69 @@
<!-- vmess settings -->
<template v-if="inbound.protocol === Protocols.VMESS">
{{template "form/vmess"}}
{{template "form/vmess" .}}
</template>
<!-- vless settings -->
<template v-if="inbound.protocol === Protocols.VLESS">
{{template "form/vless"}}
{{template "form/vless" .}}
</template>
<!-- trojan settings -->
<template v-if="inbound.protocol === Protocols.TROJAN">
{{template "form/trojan"}}
{{template "form/trojan" .}}
</template>
<!-- shadowsocks -->
<template v-if="inbound.protocol === Protocols.SHADOWSOCKS">
{{template "form/shadowsocks"}}
{{template "form/shadowsocks" .}}
</template>
<!-- tunnel -->
<template v-if="inbound.protocol === Protocols.TUNNEL">
{{template "form/tunnel"}}
{{template "form/tunnel" .}}
</template>
<!-- mixed -->
<template v-if="inbound.protocol === Protocols.MIXED">
{{template "form/mixed"}}
{{template "form/mixed" .}}
</template>
<!-- http -->
<template v-if="inbound.protocol === Protocols.HTTP">
{{template "form/http"}}
{{template "form/http" .}}
</template>
<!-- wireguard -->
<template v-if="inbound.protocol === Protocols.WIREGUARD">
{{template "form/wireguard"}}
{{template "form/wireguard" .}}
</template>
<!-- tun -->
<template v-if="inbound.protocol === Protocols.TUN">
{{template "form/tun"}}
{{template "form/tun" .}}
</template>
<!-- hysteria -->
<template v-if="inbound.protocol === Protocols.HYSTERIA">
{{template "form/hysteria"}}
{{template "form/hysteria" .}}
</template>
<!-- stream settings -->
<template v-if="inbound.canEnableStream()">
{{template "form/streamSettings"}}
{{template "form/externalProxy" }}
{{template "form/streamSettings" .}}
{{template "form/externalProxy" .}}
</template>
<!-- tls settings -->
<template v-if="inbound.canEnableTls()">
{{template "form/tlsSettings"}}
{{template "form/tlsSettings" .}}
</template>
<!-- sniffing -->
<a-collapse>
<a-collapse-panel header='Sniffing'>
{{template "form/sniffing"}}
{{template "form/sniffing" .}}
</a-collapse-panel>
</a-collapse>

File diff suppressed because it is too large Load diff

View file

@ -25,13 +25,13 @@
<a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="udp">UDP</a-select-option>
</a-select>
</a-form-item>
</a-form-item>
<a-form-item label='Follow Redirect'>
<a-switch v-model="inbound.settings.followRedirect"></a-switch>
</a-form-item>
</a-form>
<!-- sockopt -->
<template>
{{template "form/streamSockopt"}}
{{template "form/streamSockopt" .}}
</template>
{{end}}
{{end}}

View file

@ -5,7 +5,8 @@
<td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td>
<td>
<a-button icon="plus" size="small" @click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())"></a-button>
<a-button icon="plus" size="small"
@click="inbound.settings.addAccount(new Inbound.HttpSettings.HttpAccount())"></a-button>
</td>
</tr>
</table>
@ -23,4 +24,4 @@
<a-switch v-model="inbound.settings.allowTransparent" />
</a-form-item>
</a-form>
{{end}}
{{end}}

View file

@ -1,9 +1,7 @@
{{define "form/hysteria"}}
<a-collapse activeKey="0"
v-for="(client, index) in inbound.settings.hysterias.slice(0,1)"
v-if="!isEdit">
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.hysterias.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}}
{{template "form/client" .}}
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
@ -22,11 +20,9 @@
</table>
</a-collapse-panel>
</a-collapse>
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item :label="'{{ i18n "pages.inbounds.stream.tcp.version" }}'">
<a-input-number v-model.number="inbound.settings.version" :min="2"
:max="2" disabled></a-input-number>
<a-input-number v-model.number="inbound.settings.version" :min="2" :max="2" disabled></a-input-number>
</a-form-item>
</a-form>
{{end}}

View file

@ -1,12 +1,8 @@
{{define "form/shadowsocks"}}
<template v-if="inbound.isSSMultiUser">
<a-collapse
activeKey="0"
v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)"
v-if="!isEdit"
>
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.shadowsockses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}}
{{template "form/client" .}}
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
@ -16,10 +12,8 @@
<th>{{ i18n "pages.inbounds.email" }}</th>
<th>Password</th>
</tr>
<tr
v-for="(client, index) in inbound.settings.shadowsockses"
:class="index % 2 == 1 ? ' client-table-odd-row' : ''"
>
<tr v-for="(client, index) in inbound.settings.shadowsockses"
:class="index % 2 == 1 ? ' client-table-odd-row' : ''">
<td>[[ client.email ]]</td>
<td>[[ client.password ]]</td>
</tr>
@ -27,20 +21,11 @@
</a-collapse-panel>
</a-collapse>
</template>
<a-form
:colon=" false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon=" false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "encryption" }}'>
<a-select
v-model="inbound.settings.method"
@change="SSMethodChange"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option v-for="(method,method_name) in SSMethods" :value="method"
>[[ method_name ]]</a-select-option
>
<a-select v-model="inbound.settings.method" @change="SSMethodChange"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="(method,method_name) in SSMethods" :value="method">[[ method_name ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item v-if="inbound.isSS2022">
@ -50,20 +35,15 @@
<span>{{ i18n "reset" }}</span>
</template>
Password
<a-icon
@click="inbound.settings.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)"
type="sync"
></a-icon>
<a-icon @click="inbound.settings.password = RandomUtil.randomShadowsocksPassword(inbound.settings.method)"
type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="inbound.settings.password"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.network" }}'>
<a-select
v-model="inbound.settings.network"
:style="{ width: '100px' }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select v-model="inbound.settings.network" :style="{ width: '100px' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp,udp">TCP,UDP</a-select-option>
<a-select-option value="tcp">TCP</a-select-option>
<a-select-option value="udp">UDP</a-select-option>
@ -73,4 +53,4 @@
<a-switch v-model="inbound.settings.ivCheck"></a-switch>
</a-form-item>
</a-form>
{{end}}
{{end}}

View file

@ -1,9 +1,5 @@
{{define "form/mixed"}}
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.inbounds.enable" }} UDP'>
<a-switch v-model="inbound.settings.udp"></a-switch>
</a-form-item>
@ -11,10 +7,8 @@
<a-input v-model.trim="inbound.settings.ip"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "password" }}'>
<a-switch
:checked="inbound.settings.auth === 'password'"
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"
></a-switch>
<a-switch :checked="inbound.settings.auth === 'password'"
@change="checked => inbound.settings.auth = checked ? 'password' : 'noauth'"></a-switch>
</a-form-item>
<template v-if="inbound.settings.auth === 'password'">
<table :style="{ width: '100%', textAlign: 'center', margin: '1rem 0' }">
@ -22,42 +16,21 @@
<td width="45%">{{ i18n "username" }}</td>
<td width="45%">{{ i18n "password" }}</td>
<td>
<a-button
icon="plus"
size="small"
@click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"
></a-button>
<a-button icon="plus" size="small"
@click="inbound.settings.addAccount(new Inbound.MixedSettings.SocksAccount())"></a-button>
</td>
</tr>
</table>
<a-input-group
compact
v-for="(account, index) in inbound.settings.accounts"
:style="{ marginBottom: '10px' }"
>
<a-input
:style="{ width: '50%' }"
v-model.trim="account.user"
placeholder='{{ i18n "username" }}'
>
<template slot="addonBefore" :style="{ margin: '0' }"
>[[ index+1 ]]</template
>
<a-input-group compact v-for="(account, index) in inbound.settings.accounts" :style="{ marginBottom: '10px' }">
<a-input :style="{ width: '50%' }" v-model.trim="account.user" placeholder='{{ i18n "username" }}'>
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
</a-input>
<a-input
:style="{ width: '50%' }"
v-model.trim="account.pass"
placeholder='{{ i18n "password" }}'
>
<a-input :style="{ width: '50%' }" v-model.trim="account.pass" placeholder='{{ i18n "password" }}'>
<template slot="addonAfter">
<a-button
icon="minus"
size="small"
@click="inbound.settings.delAccount(index)"
></a-button>
<a-button icon="minus" size="small" @click="inbound.settings.delAccount(index)"></a-button>
</template>
</a-input>
</a-input-group>
</template>
</a-form>
{{end}}
{{end}}

View file

@ -1,7 +1,7 @@
{{define "form/trojan"}}
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.trojans.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}}
{{template "form/client" .}}
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
@ -19,35 +19,35 @@
</a-collapse-panel>
</a-collapse>
<template v-if=" inbound.isTcp">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
</a-form-item>
</a-form>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
</a-form-item>
</a-form>
<!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
@click="() => inbound.settings.delFallback(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='SNI'>
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label='ALPN'>
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label='Path'>
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label='Dest'>
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label='xVer'>
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</a-form>
<a-divider style="margin:5px 0;"></a-divider>
</template>
{{end}}
<!-- trojan fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
@click="() => inbound.settings.delFallback(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='SNI'>
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label='ALPN'>
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label='Path'>
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label='Dest'>
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label='xVer'>
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</a-form>
<a-divider style="margin:5px 0;"></a-divider>
</template>
{{end}}

View file

@ -1,9 +1,5 @@
{{define "form/tun"}}
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item>
<template slot="label">
<a-tooltip>
@ -26,38 +22,18 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number
v-model.number="inbound.settings.mtu[0]"
:min="1"
:max="9000"
placeholder="1500"
></a-input-number>
<a-input-number v-model.number="inbound.settings.mtu[0]" :min="1" :max="9000" placeholder="1500"></a-input-number>
</a-form-item>
<a-form-item label="MTU IPv6">
<a-input-number
v-model.number="inbound.settings.mtu[1]"
:min="1"
:max="9000"
placeholder="1280"
></a-input-number>
<a-input-number v-model.number="inbound.settings.mtu[1]" :min="1" :max="9000" placeholder="1280"></a-input-number>
</a-form-item>
<a-form-item label="Gateway">
<a-select
mode="tags"
v-model="inbound.settings.gateway"
:style="{ width: '100%' }"
:token-separators="[',']"
placeholder="IPv4/IPv6 gateway"
></a-select>
<a-select mode="tags" v-model="inbound.settings.gateway" :style="{ width: '100%' }" :token-separators="[',']"
placeholder="IPv4/IPv6 gateway"></a-select>
</a-form-item>
<a-form-item label="DNS">
<a-select
mode="tags"
v-model="inbound.settings.dns"
:style="{ width: '100%' }"
:token-separators="[',']"
placeholder="DNS servers"
></a-select>
<a-select mode="tags" v-model="inbound.settings.dns" :style="{ width: '100%' }" :token-separators="[',']"
placeholder="DNS servers"></a-select>
</a-form-item>
<a-form-item>
<template slot="label">
@ -69,26 +45,14 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number
v-model.number="inbound.settings.userLevel"
:min="0"
placeholder="0"
></a-input-number>
<a-input-number v-model.number="inbound.settings.userLevel" :min="0" placeholder="0"></a-input-number>
</a-form-item>
<a-form-item label="Auto Routing Table">
<a-select
mode="tags"
v-model="inbound.settings.autoSystemRoutingTable"
:style="{ width: '100%' }"
:token-separators="[',']"
placeholder="e.g. vpn, proxy"
></a-select>
<a-select mode="tags" v-model="inbound.settings.autoSystemRoutingTable" :style="{ width: '100%' }"
:token-separators="[',']" placeholder="e.g. vpn, proxy"></a-select>
</a-form-item>
<a-form-item label="Auto Outbounds">
<a-input
v-model.trim="inbound.settings.autoOutboundsInterface"
placeholder="auto"
></a-input>
<a-input v-model.trim="inbound.settings.autoOutboundsInterface" placeholder="auto"></a-input>
</a-form-item>
</a-form>
{{end}}
{{end}}

View file

@ -1,7 +1,7 @@
{{define "form/vless"}}
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vlesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}}
{{template "form/client" .}}
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
@ -12,8 +12,7 @@
<th>{{ i18n "pages.inbounds.email" }}</th>
<th>ID</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vlesses"
:class="index % 2 == 1 ? ' client-table-odd-row' : ''">
<tr v-for="(client, index) in inbound.settings.vlesses" :class="index % 2 == 1 ? ' client-table-odd-row' : ''">
<td>[[ client.email ]]</td>
<td>[[ client.id ]]</td>
</tr>
@ -21,104 +20,104 @@
</a-collapse-panel>
</a-collapse>
<template v-if=" !inbound.stream.isTLS || !inbound.stream.isReality">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Authentication">
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="undefined">None</a-select-option>
<a-select-option value="X25519, not Post-Quantum">X25519 (not
Post-Quantum)</a-select-option>
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768
(Post-Quantum)</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="decryption">
<a-input v-model.trim="inbound.settings.decryption"></a-input>
</a-form-item>
<a-form-item label="encryption">
<a-input v-model="inbound.settings.encryption"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New
keys</a-button>
<a-button danger @click="clearVlessEnc">Clear</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
</a-form-item>
</a-form>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Authentication">
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="undefined">None</a-select-option>
<a-select-option value="X25519, not Post-Quantum">X25519 (not
Post-Quantum)</a-select-option>
<a-select-option value="ML-KEM-768, Post-Quantum">ML-KEM-768
(Post-Quantum)</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="decryption">
<a-input v-model.trim="inbound.settings.decryption"></a-input>
</a-form-item>
<a-form-item label="encryption">
<a-input v-model="inbound.settings.encryption"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewVlessEnc">Get New
keys</a-button>
<a-button danger @click="clearVlessEnc">Clear</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
<template v-if="inbound.isTcp && !inbound.settings.selectedAuth">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Fallbacks">
<a-button icon="plus" type="primary" size="small" @click="inbound.settings.addFallback()"></a-button>
</a-form-item>
</a-form>
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
@click="() => inbound.settings.delFallback(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='SNI'>
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label='ALPN'>
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label='Path'>
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label='Dest'>
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label='xVer'>
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
<template v-if="inbound.canEnableVisionSeed()">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Vision Seed">
<a-row :gutter="8">
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
@change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }"
placeholder="900" addon-before="[0]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
@change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }"
placeholder="500" addon-before="[1]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
@change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }"
placeholder="900" addon-before="[2]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
@change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }"
placeholder="256" addon-before="[3]"></a-input-number>
</a-col>
</a-row>
<a-space :size="8" :style="{ marginTop: '8px' }">
<a-button type="primary" @click="setRandomTestseed">
Rand
</a-button>
<a-button @click="resetTestseed">
Reset
</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
{{end}}
<!-- vless fallbacks -->
<a-form v-for="(fallback, index) in inbound.settings.fallbacks" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }"> Fallback [[ index + 1 ]] <a-icon type="delete"
@click="() => inbound.settings.delFallback(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='SNI'>
<a-input v-model="fallback.name"></a-input>
</a-form-item>
<a-form-item label='ALPN'>
<a-input v-model="fallback.alpn"></a-input>
</a-form-item>
<a-form-item label='Path'>
<a-input v-model="fallback.path"></a-input>
</a-form-item>
<a-form-item label='Dest'>
<a-input v-model="fallback.dest"></a-input>
</a-form-item>
<a-form-item label='xVer'>
<a-input-number v-model.number="fallback.xver" :min="0" :max="2"></a-input-number>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
<template v-if="inbound.canEnableVisionSeed()">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Vision Seed">
<a-row :gutter="8">
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[0] !== undefined) ? inbound.settings.testseed[0] : 900"
@change="(val) => updateTestseed(0, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900"
addon-before="[0]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[1] !== undefined) ? inbound.settings.testseed[1] : 500"
@change="(val) => updateTestseed(1, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="500"
addon-before="[1]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[2] !== undefined) ? inbound.settings.testseed[2] : 900"
@change="(val) => updateTestseed(2, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="900"
addon-before="[2]"></a-input-number>
</a-col>
<a-col :span="6">
<a-input-number
:value="(inbound.settings.testseed && inbound.settings.testseed[3] !== undefined) ? inbound.settings.testseed[3] : 256"
@change="(val) => updateTestseed(3, val)" :min="0" :max="9999" :style="{ width: '100%' }" placeholder="256"
addon-before="[3]"></a-input-number>
</a-col>
</a-row>
<a-space :size="8" :style="{ marginTop: '8px' }">
<a-button type="primary" @click="setRandomTestseed">
Rand
</a-button>
<a-button @click="resetTestseed">
Reset
</a-button>
</a-space>
</a-form-item>
</a-form>
<a-divider :style="{ margin: '5px 0' }"></a-divider>
</template>
{{end}}

View file

@ -1,7 +1,7 @@
{{define "form/vmess"}}
<a-collapse activeKey="0" v-for="(client, index) in inbound.settings.vmesses.slice(0,1)" v-if="!isEdit">
<a-collapse-panel header='{{ i18n "pages.inbounds.client" }}'>
{{template "form/client"}}
{{template "form/client" .}}
</a-collapse-panel>
</a-collapse>
<a-collapse v-else>
@ -12,8 +12,8 @@
<th>ID</th>
<th>{{ i18n "security" }}</th>
</tr>
<tr v-for="(client, index) in inbound.settings.vmesses"
:class="index % 2 == 1 ? ' client-table-odd-row' : ''">
<tr v-for="(client, index) in inbound.settings.vmesses"
:class="index % 2 == 1 ? ' client-table-odd-row' : ''">
<td>[[ client.email ]]</td>
<td>[[ client.id ]]</td>
<td>[[ client.security ]]</td>

View file

@ -1,9 +1,5 @@
{{define "form/sniffing"}}
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item>
<span slot="label">
{{ i18n "enabled" }}
@ -19,9 +15,7 @@
<template v-if="inbound.sniffing.enabled">
<a-form-item :wrapper-col="{span:24}">
<a-checkbox-group v-model="inbound.sniffing.destOverride">
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key"
>[[ value ]]</a-checkbox
>
<a-checkbox v-for="key,value in SNIFFING_OPTION" :value="key">[[ value ]]</a-checkbox>
</a-checkbox-group>
</a-form-item>
<a-form-item label="Metadata Only">
@ -31,23 +25,13 @@
<a-switch v-model="inbound.sniffing.routeOnly"></a-switch>
</a-form-item>
<a-form-item label="IPs Excluded">
<a-select
mode="tags"
v-model="inbound.sniffing.ipsExcluded"
:style="{ width: '100%' }"
:token-separators="[',']"
placeholder="IP/CIDR/geoip:*/ext:*"
></a-select>
<a-select mode="tags" v-model="inbound.sniffing.ipsExcluded" :style="{ width: '100%' }" :token-separators="[',']"
placeholder="IP/CIDR/geoip:*/ext:*"></a-select>
</a-form-item>
<a-form-item label="Domains Excluded">
<a-select
mode="tags"
v-model="inbound.sniffing.domainsExcluded"
:style="{ width: '100%' }"
:token-separators="[',']"
placeholder="domain:*/ext:*"
></a-select>
<a-select mode="tags" v-model="inbound.sniffing.domainsExcluded" :style="{ width: '100%' }"
:token-separators="[',']" placeholder="domain:*/ext:*"></a-select>
</a-form-item>
</template>
</a-form>
{{end}}
{{end}}

View file

@ -1,67 +1,31 @@
{{define "form/externalProxy"}}
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '5px 0 0' }"></a-divider>
<a-form-item label="External Proxy">
<a-switch v-model="externalProxy"></a-switch>
<a-button
icon="plus"
v-if="externalProxy"
type="primary"
:style="{ marginLeft: '10px' }"
size="small"
@click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"
></a-button>
<a-button icon="plus" v-if="externalProxy" type="primary" :style="{ marginLeft: '10px' }" size="small"
@click="inbound.stream.externalProxy.push({forceTls: 'same', dest: '', port: 443, remark: ''})"></a-button>
</a-form-item>
<a-input-group
:style="{ margin: '8px 0' }"
compact
v-for="(row, index) in inbound.stream.externalProxy"
>
<a-input-group :style="{ margin: '8px 0' }" compact v-for="(row, index) in inbound.stream.externalProxy">
<template>
<a-tooltip title="Force TLS">
<a-select
v-model="row.forceTls"
:style="{ width: '20%', margin: '0px' }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="same"
>{{ i18n "pages.inbounds.same" }}</a-select-option
>
<a-select v-model="row.forceTls" :style="{ width: '20%', margin: '0px' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="same">{{ i18n "pages.inbounds.same" }}</a-select-option>
<a-select-option value="none">{{ i18n "none" }}</a-select-option>
<a-select-option value="tls">TLS</a-select-option>
</a-select>
</a-tooltip>
</template>
<a-input
:style="{ width: '30%' }"
v-model.trim="row.dest"
placeholder='{{ i18n "host" }}'
></a-input>
<a-input :style="{ width: '30%' }" v-model.trim="row.dest" placeholder='{{ i18n "host" }}'></a-input>
<a-tooltip title='{{ i18n "pages.inbounds.port" }}'>
<a-input-number
:style="{ width: '15%' }"
v-model.number="row.port"
min="1"
max="65535"
></a-input-number>
<a-input-number :style="{ width: '15%' }" v-model.number="row.port" min="1" max="65535"></a-input-number>
</a-tooltip>
<a-input
:style="{ width: '30%', top: '0' }"
v-model.trim="row.remark"
placeholder='{{ i18n "remark" }}'
>
<a-input :style="{ width: '30%', top: '0' }" v-model.trim="row.remark" placeholder='{{ i18n "remark" }}'>
<template slot="addonAfter">
<a-button
icon="minus"
size="small"
@click="inbound.stream.externalProxy.splice(index, 1)"
></a-button>
<a-button icon="minus" size="small" @click="inbound.stream.externalProxy.splice(index, 1)"></a-button>
</template>
</a-input>
</a-input-group>
</a-form>
{{end}}
{{end}}

View file

@ -1,446 +1,339 @@
{{define "form/streamFinalMask"}}
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
v-if="inbound.protocol == Protocols.HYSTERIA || ['kcp', 'xhttp', 'raw', 'tcp', 'httpupgrade', 'ws', 'grpc'].includes(inbound.stream.network)"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"
v-if="inbound.protocol == Protocols.HYSTERIA || ['kcp', 'xhttp', 'raw', 'tcp', 'httpupgrade', 'ws', 'grpc'].includes(inbound.stream.network)">
<a-divider :style="{ margin: '5px 0 0' }"></a-divider>
<!-- TCP Masks for raw/tcp/httpupgrade/ws/grpc/xhttp -->
<template v-if="['raw', 'tcp', 'httpupgrade', 'ws', 'grpc', 'xhttp'].includes(inbound.stream.network)">
<a-form-item label="TCP Masks">
<a-button
icon="plus"
type="primary"
size="small"
@click="inbound.stream.addTcpMask('fragment')"
></a-button>
</a-form-item>
<template v-if="inbound.stream.finalmask.tcp && inbound.stream.finalmask.tcp.length > 0">
<a-form
v-for="(mask, index) in inbound.stream.finalmask.tcp"
:key="index"
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-divider :style="{ margin: '0' }">
TCP Mask [[ index + 1 ]]
<a-icon
type="delete"
@click="() => inbound.stream.delTcpMask(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select
v-model="mask.type"
@change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="fragment">Fragment</a-select-option>
<a-select-option value="header-custom">Header Custom</a-select-option>
<a-select-option value="sudoku">Sudoku</a-select-option>
</a-select>
</a-form-item>
<!-- Fragment settings -->
<template v-if="mask.type === 'fragment'">
<a-form-item label="Packets">
<a-select
v-model="mask.settings.packets"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="tlshello">tlshello</a-select-option>
<a-select-option value="1-3">1-3</a-select-option>
<a-select-option value="1-5">1-5</a-select-option>
<a-form-item label="TCP Masks">
<a-button icon="plus" type="primary" size="small" @click="inbound.stream.addTcpMask('fragment')"></a-button>
</a-form-item>
<template v-if="inbound.stream.finalmask.tcp && inbound.stream.finalmask.tcp.length > 0">
<a-form v-for="(mask, index) in inbound.stream.finalmask.tcp" :key="index" :colon="false"
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }">
TCP Mask [[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.stream.delTcpMask(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="mask.type" @change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="fragment">Fragment</a-select-option>
<a-select-option value="header-custom">Header Custom</a-select-option>
<a-select-option value="sudoku">Sudoku</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Length">
<a-input v-model.trim="mask.settings.length" placeholder="e.g. 100-200" />
</a-form-item>
<a-form-item label="Delay">
<a-input v-model.trim="mask.settings.delay" placeholder="e.g. 10-20" />
</a-form-item>
<a-form-item label="Max Split">
<a-input v-model.trim="mask.settings.maxSplit" placeholder="e.g. 3-6" />
</a-form-item>
</template>
<!-- Sudoku settings (TCP) -->
<template v-if="mask.type === 'sudoku'">
<a-form-item label="Password">
<a-input v-model.trim="mask.settings.password" placeholder="Obfuscation password" />
</a-form-item>
<a-form-item label="ASCII">
<a-input v-model.trim="mask.settings.ascii" placeholder="ASCII" />
</a-form-item>
<a-form-item label="Custom Table">
<a-input v-model.trim="mask.settings.customTable" placeholder="Custom Table" />
</a-form-item>
<a-form-item label="Custom Tables">
<a-input v-model.trim="mask.settings.customTables" placeholder="Custom Tables" />
</a-form-item>
<a-form-item label="Padding Min">
<a-input-number v-model.number="mask.settings.paddingMin" :min="0" />
</a-form-item>
<a-form-item label="Padding Max">
<a-input-number v-model.number="mask.settings.paddingMax" :min="0" />
</a-form-item>
</template>
<!-- Fragment settings -->
<template v-if="mask.type === 'fragment'">
<a-form-item label="Packets">
<a-select v-model="mask.settings.packets" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tlshello">tlshello</a-select-option>
<a-select-option value="1-3">1-3</a-select-option>
<a-select-option value="1-5">1-5</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Length">
<a-input v-model.trim="mask.settings.length" placeholder="e.g. 100-200" />
</a-form-item>
<a-form-item label="Delay">
<a-input v-model.trim="mask.settings.delay" placeholder="e.g. 10-20" />
</a-form-item>
<a-form-item label="Max Split">
<a-input v-model.trim="mask.settings.maxSplit" placeholder="e.g. 3-6" />
</a-form-item>
</template>
<!-- Header Custom (TCP) clients/servers/errors are 2D arrays of groups -->
<template v-if="mask.type === 'header-custom'">
<!-- Clients -->
<a-form-item label="Clients">
<a-button
icon="plus"
type="primary"
size="small"
@click="mask.settings.clients.push([{delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: []}])"
></a-button>
</a-form-item>
<template v-for="(group, gi) in mask.settings.clients" :key="'cg'+gi">
<a-divider :style="{ margin: '0' }">
Clients Group [[ gi + 1 ]]
<a-icon
type="delete"
@click="mask.settings.clients.splice(gi, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '8px' }"
></a-icon>
</a-divider>
<template v-for="(item, ii) in group" :key="'ci'+ii">
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="item.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') item.packet = RandomUtil.randomBase64(); else if(t === 'array') { item.rand = 0; item.packet = []; } else { item.packet = ''; } }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Delay (ms)">
<a-input-number v-model.number="item.delay" :min="0" />
</a-form-item>
<template v-if="item.type === 'array'">
<a-form-item label="Rand">
<a-input-number v-model.number="item.rand" :min="0" />
<!-- Sudoku settings (TCP) -->
<template v-if="mask.type === 'sudoku'">
<a-form-item label="Password">
<a-input v-model.trim="mask.settings.password" placeholder="Obfuscation password" />
</a-form-item>
<a-form-item label="ASCII">
<a-input v-model.trim="mask.settings.ascii" placeholder="ASCII" />
</a-form-item>
<a-form-item label="Custom Table">
<a-input v-model.trim="mask.settings.customTable" placeholder="Custom Table" />
</a-form-item>
<a-form-item label="Custom Tables">
<a-input v-model.trim="mask.settings.customTables" placeholder="Custom Tables" />
</a-form-item>
<a-form-item label="Padding Min">
<a-input-number v-model.number="mask.settings.paddingMin" :min="0" />
</a-form-item>
<a-form-item label="Padding Max">
<a-input-number v-model.number="mask.settings.paddingMax" :min="0" />
</a-form-item>
</template>
<!-- Header Custom (TCP) clients/servers/errors are 2D arrays of groups -->
<template v-if="mask.type === 'header-custom'">
<!-- Clients -->
<a-form-item label="Clients">
<a-button icon="plus" type="primary" size="small"
@click="mask.settings.clients.push([{delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: []}])"></a-button>
</a-form-item>
<template v-for="(group, gi) in mask.settings.clients" :key="'cg'+gi">
<a-divider :style="{ margin: '0' }">
Clients Group [[ gi + 1 ]]
<a-icon type="delete" @click="mask.settings.clients.splice(gi, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '8px' }"></a-icon>
</a-divider>
<template v-for="(item, ii) in group" :key="'ci'+ii">
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="item.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') item.packet = RandomUtil.randomBase64(); else if(t === 'array') { item.rand = 0; item.packet = []; } else { item.packet = ''; } }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="item.randRange" placeholder="0-255" />
<a-form-item label="Delay (ms)">
<a-input-number v-model.number="item.delay" :min="0" />
</a-form-item>
<template v-if="item.type === 'array'">
<a-form-item label="Rand">
<a-input-number v-model.number="item.rand" :min="0" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="item.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="item.type === 'base64'">
<a-input v-model.trim="item.packet" placeholder="binary data"
:style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="item.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="item.packet" placeholder="binary data" />
</a-form-item>
</template>
</template>
<!-- Servers -->
<a-form-item label="Servers">
<a-button icon="plus" type="primary" size="small"
@click="mask.settings.servers.push([{delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: []}])"></a-button>
</a-form-item>
<template v-for="(group, gi) in mask.settings.servers" :key="'sg'+gi">
<a-divider :style="{ margin: '0' }">
Servers Group [[ gi + 1 ]]
<a-icon type="delete" @click="mask.settings.servers.splice(gi, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '8px' }"></a-icon>
</a-divider>
<template v-for="(item, ii) in group" :key="'si'+ii">
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="item.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') item.packet = RandomUtil.randomBase64(); else if(t === 'array') { item.rand = 0; item.packet = []; } else { item.packet = ''; } }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Delay (ms)">
<a-input-number v-model.number="item.delay" :min="0" />
</a-form-item>
<template v-if="item.type === 'array'">
<a-form-item label="Rand">
<a-input-number v-model.number="item.rand" :min="0" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="item.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="item.type === 'base64'">
<a-input v-model.trim="item.packet" placeholder="binary data"
:style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="item.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="item.packet" placeholder="binary data" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="item.type === 'base64'">
<a-input v-model.trim="item.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="item.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="item.packet" placeholder="binary data" />
</a-form-item>
</template>
</template>
<!-- Servers -->
<a-form-item label="Servers">
<a-button
icon="plus"
type="primary"
size="small"
@click="mask.settings.servers.push([{delay: 0, rand: 0, randRange: '0-255', type: 'array', packet: []}])"
></a-button>
</a-form-item>
<template v-for="(group, gi) in mask.settings.servers" :key="'sg'+gi">
<a-divider :style="{ margin: '0' }">
Servers Group [[ gi + 1 ]]
<a-icon
type="delete"
@click="mask.settings.servers.splice(gi, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer', marginLeft: '8px' }"
></a-icon>
</a-divider>
<template v-for="(item, ii) in group" :key="'si'+ii">
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="item.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') item.packet = RandomUtil.randomBase64(); else if(t === 'array') { item.rand = 0; item.packet = []; } else { item.packet = ''; } }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Delay (ms)">
<a-input-number v-model.number="item.delay" :min="0" />
</a-form-item>
<template v-if="item.type === 'array'">
<a-form-item label="Rand">
<a-input-number v-model.number="item.rand" :min="0" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="item.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="item.type === 'base64'">
<a-input v-model.trim="item.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="item.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="item.packet" placeholder="binary data" />
</a-form-item>
</template>
</template>
</template>
</a-form>
</template>
</a-form>
</template>
</template>
<template v-if="inbound.protocol == Protocols.HYSTERIA || inbound.stream.network == 'kcp'">
<a-form-item label="UDP Masks">
<a-button
icon="plus"
type="primary"
size="small"
@click="inbound.stream.addUdpMask(inbound.protocol === Protocols.HYSTERIA ? 'salamander' : 'mkcp-aes128gcm')"
></a-button>
</a-form-item>
<template
v-if="inbound.stream.finalmask.udp && inbound.stream.finalmask.udp.length > 0"
>
<a-form
v-for="(mask, index) in inbound.stream.finalmask.udp"
:key="index"
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-divider :style="{ margin: '0' }">
UDP Mask [[ index + 1 ]]
<a-icon
type="delete"
@click="() => inbound.stream.delUdpMask(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select
v-model="mask.type"
@change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(inbound.stream.network === 'kcp') { inbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<template v-if="inbound.protocol === Protocols.HYSTERIA">
<a-select-option value="salamander"
>Salamander (Hysteria2)</a-select-option
>
</template>
<template v-else>
<a-select-option value="mkcp-aes128gcm"
>mKCP AES-128-GCM</a-select-option
>
<a-select-option value="header-dns">Header DNS</a-select-option>
<a-select-option value="header-dtls"
>Header DTLS 1.2</a-select-option
>
<a-select-option value="header-srtp">Header SRTP</a-select-option>
<a-select-option value="header-utp">Header uTP</a-select-option>
<a-select-option value="header-wechat"
>Header WeChat Video</a-select-option
>
<a-select-option value="header-wireguard"
>Header WireGuard</a-select-option
>
<a-select-option value="mkcp-original"
>mKCP Original</a-select-option
>
<a-select-option value="xdns">xDNS</a-select-option>
<a-select-option value="xicmp">xICMP</a-select-option>
<a-select-option value="header-custom">Header Custom</a-select-option>
<a-select-option value="noise">Noise</a-select-option>
</template>
</a-select>
</a-form-item>
<a-form-item
label="Password"
v-if="['mkcp-aes128gcm', 'salamander'].includes(mask.type)"
>
<a-input
v-model.trim="mask.settings.password"
placeholder="Obfuscation password"
></a-input>
</a-form-item>
<a-form-item label="Domain" v-if="mask.type === 'header-dns'">
<a-input
v-model.trim="mask.settings.domain"
placeholder="e.g., www.example.com"
></a-input>
</a-form-item>
<template v-if="mask.type === 'xdns'">
<a-form-item label="Domains">
<a-select
mode="tags"
v-model="mask.settings.domains"
:style="{ width: '100%' }"
:token-separators="[',']"
placeholder="e.g., www.example.com"
></a-select>
<a-form-item label="UDP Masks">
<a-button icon="plus" type="primary" size="small"
@click="inbound.stream.addUdpMask(inbound.protocol === Protocols.HYSTERIA ? 'salamander' : 'mkcp-aes128gcm')"></a-button>
</a-form-item>
<template v-if="inbound.stream.finalmask.udp && inbound.stream.finalmask.udp.length > 0">
<a-form v-for="(mask, index) in inbound.stream.finalmask.udp" :key="index" :colon="false"
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-divider :style="{ margin: '0' }">
UDP Mask [[ index + 1 ]]
<a-icon type="delete" @click="() => inbound.stream.delUdpMask(index)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="mask.type"
@change="(type) => { mask.settings = mask._getDefaultSettings(type, {}); if(inbound.stream.network === 'kcp') { inbound.stream.kcp.mtu = type === 'xdns' ? 900 : 1350; } }"
:dropdown-class-name="themeSwitcher.currentTheme">
<template v-if="inbound.protocol === Protocols.HYSTERIA">
<a-select-option value="salamander">Salamander (Hysteria2)</a-select-option>
</template>
<template v-else>
<a-select-option value="mkcp-aes128gcm">mKCP AES-128-GCM</a-select-option>
<a-select-option value="header-dns">Header DNS</a-select-option>
<a-select-option value="header-dtls">Header DTLS 1.2</a-select-option>
<a-select-option value="header-srtp">Header SRTP</a-select-option>
<a-select-option value="header-utp">Header uTP</a-select-option>
<a-select-option value="header-wechat">Header WeChat Video</a-select-option>
<a-select-option value="header-wireguard">Header WireGuard</a-select-option>
<a-select-option value="mkcp-original">mKCP Original</a-select-option>
<a-select-option value="xdns">xDNS</a-select-option>
<a-select-option value="xicmp">xICMP</a-select-option>
<a-select-option value="header-custom">Header Custom</a-select-option>
<a-select-option value="noise">Noise</a-select-option>
</template>
</a-select>
</a-form-item>
</template>
<template v-if="mask.type === 'noise'">
<a-form-item label="Reset">
<a-input-number v-model.number="mask.settings.reset" :min="0" />
<a-form-item label="Password" v-if="['mkcp-aes128gcm', 'salamander'].includes(mask.type)">
<a-input v-model.trim="mask.settings.password" placeholder="Obfuscation password"></a-input>
</a-form-item>
<a-form-item label="Noise">
<a-button
icon="plus"
type="primary"
size="small"
@click="mask.settings.noise.push({rand: '1-8192', randRange: '0-255', type: 'array', packet: [], delay: '10-20'})"
></a-button>
<a-form-item label="Domain" v-if="mask.type === 'header-dns'">
<a-input v-model.trim="mask.settings.domain" placeholder="e.g., www.example.com"></a-input>
</a-form-item>
<template v-for="(n, index) in mask.settings.noise" :key="index">
<a-divider :style="{ margin: '0' }">
Noise [[ index + 1 ]]
<a-icon
type="delete"
@click="() => mask.settings.noise.splice(index, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select
v-model="n.type"
:dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') n.packet = RandomUtil.randomBase64(); else if(t === 'array') n.packet = []; else n.packet = ''; }"
>
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item>
<template v-if="n.type === 'array'">
<a-form-item label="Rand">
<a-input v-model.trim="n.rand" placeholder="0 or 1-8192" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="n.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="n.type === 'base64'">
<a-input v-model.trim="n.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="n.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="n.packet" placeholder="binary data" />
</a-form-item>
<a-form-item label="Delay">
<a-input v-model.trim="n.delay" placeholder="10-20" />
<template v-if="mask.type === 'xdns'">
<a-form-item label="Domains">
<a-select mode="tags" v-model="mask.settings.domains" :style="{ width: '100%' }" :token-separators="[',']"
placeholder="e.g., www.example.com"></a-select>
</a-form-item>
</template>
</template>
<template v-if="mask.type === 'header-custom'">
<a-form-item label="Client">
<a-button
icon="plus"
type="primary"
size="small"
@click="mask.settings.client.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"
></a-button>
</a-form-item>
<template v-for="(c, index) in mask.settings.client" :key="index">
<a-divider :style="{ margin: '0' }">
Client [[ index + 1 ]]
<a-icon
type="delete"
@click="mask.settings.client.splice(index, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select
v-model="c.type"
:dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') c.packet = RandomUtil.randomBase64(); else if(t === 'array') c.packet = []; else c.packet = ''; }"
>
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
<template v-if="mask.type === 'noise'">
<a-form-item label="Reset">
<a-input-number v-model.number="mask.settings.reset" :min="0" />
</a-form-item>
<template v-if="c.type === 'array'">
<a-form-item label="Rand">
<a-input-number v-model.number="c.rand" />
<a-form-item label="Noise">
<a-button icon="plus" type="primary" size="small"
@click="mask.settings.noise.push({rand: '1-8192', randRange: '0-255', type: 'array', packet: [], delay: '10-20'})"></a-button>
</a-form-item>
<template v-for="(n, index) in mask.settings.noise" :key="index">
<a-divider :style="{ margin: '0' }">
Noise [[ index + 1 ]]
<a-icon type="delete" @click="() => mask.settings.noise.splice(index, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="n.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') n.packet = RandomUtil.randomBase64(); else if(t === 'array') n.packet = []; else n.packet = ''; }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="c.randRange" placeholder="0-255" />
<template v-if="n.type === 'array'">
<a-form-item label="Rand">
<a-input v-model.trim="n.rand" placeholder="0 or 1-8192" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="n.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="n.type === 'base64'">
<a-input v-model.trim="n.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="n.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="n.packet" placeholder="binary data" />
</a-form-item>
<a-form-item label="Delay">
<a-input v-model.trim="n.delay" placeholder="10-20" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="c.type === 'base64'">
<a-input v-model.trim="c.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="c.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="c.packet" placeholder="binary data" />
</a-form-item>
</template>
<a-divider :style="{ margin: '0' }"></a-divider>
<a-form-item label="Server">
<a-button
icon="plus"
type="primary"
size="small"
@click="mask.settings.server.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"
></a-button>
</a-form-item>
<template v-for="(s, index) in mask.settings.server" :key="index">
<a-divider :style="{ margin: '0' }">
Server [[ index + 1 ]]
<a-icon
type="delete"
@click="mask.settings.server.splice(index, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"
></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select
v-model="s.type"
:dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') s.packet = RandomUtil.randomBase64(); else if(t === 'array') s.packet = []; else s.packet = ''; }"
>
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
<template v-if="mask.type === 'header-custom'">
<a-form-item label="Client">
<a-button icon="plus" type="primary" size="small"
@click="mask.settings.client.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"></a-button>
</a-form-item>
<template v-if="s.type === 'array'">
<a-form-item label="Rand">
<a-input-number v-model.number="s.rand" />
<template v-for="(c, index) in mask.settings.client" :key="index">
<a-divider :style="{ margin: '0' }">
Client [[ index + 1 ]]
<a-icon type="delete" @click="mask.settings.client.splice(index, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="c.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') c.packet = RandomUtil.randomBase64(); else if(t === 'array') c.packet = []; else c.packet = ''; }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="s.randRange" placeholder="0-255" />
<template v-if="c.type === 'array'">
<a-form-item label="Rand">
<a-input-number v-model.number="c.rand" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="c.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="c.type === 'base64'">
<a-input v-model.trim="c.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="c.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="c.packet" placeholder="binary data" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="s.type === 'base64'">
<a-input v-model.trim="s.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="s.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="s.packet" placeholder="binary data" />
<a-divider :style="{ margin: '0' }"></a-divider>
<a-form-item label="Server">
<a-button icon="plus" type="primary" size="small"
@click="mask.settings.server.push({rand: 0, randRange: '0-255', type: 'array', packet: []})"></a-button>
</a-form-item>
<template v-for="(s, index) in mask.settings.server" :key="index">
<a-divider :style="{ margin: '0' }">
Server [[ index + 1 ]]
<a-icon type="delete" @click="mask.settings.server.splice(index, 1)"
:style="{ color: 'rgb(255, 77, 79)', cursor: 'pointer' }"></a-icon>
</a-divider>
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="s.type" :dropdown-class-name="themeSwitcher.currentTheme"
@change="t => { if(t === 'base64') s.packet = RandomUtil.randomBase64(); else if(t === 'array') s.packet = []; else s.packet = ''; }">
<a-select-option value="array">Array</a-select-option>
<a-select-option value="str">String</a-select-option>
<a-select-option value="hex">Hex</a-select-option>
<a-select-option value="base64">Base64</a-select-option>
</a-select>
</a-form-item>
<template v-if="s.type === 'array'">
<a-form-item label="Rand">
<a-input-number v-model.number="s.rand" />
</a-form-item>
<a-form-item label="Rand Range">
<a-input v-model.trim="s.randRange" placeholder="0-255" />
</a-form-item>
</template>
<a-form-item v-else label="Packet">
<a-input-group compact v-if="s.type === 'base64'">
<a-input v-model.trim="s.packet" placeholder="binary data" :style="{ width: 'calc(100% - 32px)' }" />
<a-button icon="reload" @click="s.packet = RandomUtil.randomBase64()" />
</a-input-group>
<a-input v-else v-model.trim="s.packet" placeholder="binary data" />
</a-form-item>
</template>
</template>
<template v-if="mask.type === 'xicmp'">
<a-form-item label="IP">
<a-input v-model.trim="mask.settings.ip" placeholder="0.0.0.0" />
</a-form-item>
<a-form-item label="ID">
<a-input-number v-model.number="mask.settings.id" :min="0" />
</a-form-item>
</template>
</template>
<template v-if="mask.type === 'xicmp'">
<a-form-item label="IP">
<a-input v-model.trim="mask.settings.ip" placeholder="0.0.0.0" />
</a-form-item>
<a-form-item label="ID">
<a-input-number v-model.number="mask.settings.id" :min="0" />
</a-form-item>
</template>
</a-form>
</template>
</a-form>
</template>
</template>
<!-- quicParams only for xhttp H3 and hysteria -->
@ -450,10 +343,8 @@
</a-form-item>
<template v-if="inbound.stream.finalmask.enableQuicParams">
<a-form-item label="Congestion">
<a-select
v-model="inbound.stream.finalmask.quicParams.congestion"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select v-model="inbound.stream.finalmask.quicParams.congestion"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="reno">Reno</a-select-option>
<a-select-option value="bbr">BBR</a-select-option>
<a-select-option value="brutal">Brutal</a-select-option>
@ -465,10 +356,10 @@
</a-form-item>
<template v-if="['brutal','force-brutal'].includes(inbound.stream.finalmask.quicParams.congestion)">
<a-form-item label="Brutal Up">
<a-input v-model.trim="inbound.stream.finalmask.quicParams.brutalUp" placeholder="e.g. 60 mbps" />
<a-input v-model.trim="inbound.stream.finalmask.quicParams.brutalUp" :min="65537" placeholder="65537" />
</a-form-item>
<a-form-item label="Brutal Down">
<a-input v-model.trim="inbound.stream.finalmask.quicParams.brutalDown" placeholder="e.g. 60 mbps" />
<a-input v-model.trim="inbound.stream.finalmask.quicParams.brutalDown" :min="65537" placeholder="65537" />
</a-form-item>
</template>
<a-form-item label="UDP Hop">
@ -486,27 +377,32 @@
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxIdleTimeout" :min="4" :max="120" />
</a-form-item>
<a-form-item label="Keep Alive Period (s)">
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.keepAlivePeriod" :min="0" :max="60" />
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.keepAlivePeriod" :min="2" :max="60" />
</a-form-item>
<a-form-item label="Disable Path MTU Dis">
<a-switch v-model="inbound.stream.finalmask.quicParams.disablePathMTUDiscovery"></a-switch>
</a-form-item>
<a-form-item label="Max Incoming Streams">
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxIncomingStreams" :min="0" placeholder="0 = default" />
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxIncomingStreams" :min="8"
placeholder="1024 = default" />
</a-form-item>
<a-form-item label="Init Stream Window">
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.initStreamReceiveWindow" :min="0" placeholder="0 = default" />
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.initStreamReceiveWindow" :min="16384"
placeholder="8388608 = default" />
</a-form-item>
<a-form-item label="Max Stream Window">
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxStreamReceiveWindow" :min="0" placeholder="0 = default" />
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxStreamReceiveWindow" :min="16384"
placeholder="8388608 = default" />
</a-form-item>
<a-form-item label="Init Conn Window">
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.initConnectionReceiveWindow" :min="0" placeholder="0 = default" />
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.initConnectionReceiveWindow" :min="16384"
placeholder="20971520 = default" />
</a-form-item>
<a-form-item label="Max Conn Window">
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxConnectionReceiveWindow" :min="0" placeholder="0 = default" />
<a-input-number v-model.number="inbound.stream.finalmask.quicParams.maxConnectionReceiveWindow" :min="16384"
placeholder="20971520 = default" />
</a-form-item>
</template>
</template>
</a-form>
{{end}}
{{end}}

View file

@ -1,9 +1,5 @@
{{define "form/streamGRPC"}}
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Service Name">
<a-input v-model.trim="inbound.stream.grpc.serviceName"></a-input>
</a-form-item>
@ -14,4 +10,4 @@
<a-switch v-model="inbound.stream.grpc.multiMode"></a-switch>
</a-form-item>
</a-form>
{{end}}
{{end}}

View file

@ -1,13 +1,7 @@
{{define "form/streamHTTPUpgrade"}}
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Proxy Protocol">
<a-switch
v-model="inbound.stream.httpupgrade.acceptProxyProtocol"
></a-switch>
<a-switch v-model="inbound.stream.httpupgrade.acceptProxyProtocol"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "host" }}'>
<a-input v-model.trim="inbound.stream.httpupgrade.host"></a-input>
@ -16,39 +10,20 @@
<a-input v-model.trim="inbound.stream.httpupgrade.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button
icon="plus"
size="small"
@click="inbound.stream.httpupgrade.addHeader('', '')"
></a-button>
<a-button icon="plus" size="small" @click="inbound.stream.httpupgrade.addHeader('', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group
compact
v-for="(header, index) in inbound.stream.httpupgrade.headers"
>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
>
<template slot="addonBefore" :style="{ margin: '0' }"
>[[ index+1 ]]</template
>
<a-input-group compact v-for="(header, index) in inbound.stream.httpupgrade.headers">
<a-input :style="{ width: '50%' }" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
</a-input>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
>
<a-button
icon="minus"
slot="addonAfter"
size="small"
@click="inbound.stream.httpupgrade.removeHeader(index)"
></a-button>
<a-input :style="{ width: '50%' }" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button icon="minus" slot="addonAfter" size="small"
@click="inbound.stream.httpupgrade.removeHeader(index)"></a-button>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
{{end}}
{{end}}

View file

@ -1,14 +1,7 @@
{{define "form/streamHysteria"}}
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="UDP Idle Timeout">
<a-input-number
v-model.number="inbound.stream.hysteria.udpIdleTimeout"
:min="0"
></a-input-number>
<a-input-number v-model.number="inbound.stream.hysteria.udpIdleTimeout" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="Masquerade">
<a-switch v-model="inbound.stream.hysteria.masqueradeSwitch"></a-switch>
@ -16,85 +9,50 @@
<template v-if="inbound.stream.hysteria.masqueradeSwitch">
<a-divider :style="{ margin: '5px 0 0' }">Masquerade</a-divider>
<a-form-item label="Type">
<a-select
v-model="inbound.stream.hysteria.masquerade.type"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select v-model="inbound.stream.hysteria.masquerade.type" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="file">File</a-select-option>
<a-select-option value="proxy">Proxy</a-select-option>
<a-select-option value="string">String</a-select-option>
</a-select>
</a-form-item>
<a-form-item
label="Dir"
v-if="inbound.stream.hysteria.masquerade.type === 'file'"
>
<a-form-item label="Dir" v-if="inbound.stream.hysteria.masquerade.type === 'file'">
<a-input v-model.trim="inbound.stream.hysteria.masquerade.dir"></a-input>
</a-form-item>
<template v-if="inbound.stream.hysteria.masquerade.type === 'proxy'">
<a-form-item label="URL">
<a-input
v-model.trim="inbound.stream.hysteria.masquerade.url"
></a-input>
<a-input v-model.trim="inbound.stream.hysteria.masquerade.url"></a-input>
</a-form-item>
<a-form-item label="Rewrite Host">
<a-switch
v-model="inbound.stream.hysteria.masquerade.rewriteHost"
></a-switch>
<a-switch v-model="inbound.stream.hysteria.masquerade.rewriteHost"></a-switch>
</a-form-item>
<a-form-item label="Insecure">
<a-switch
v-model="inbound.stream.hysteria.masquerade.insecure"
></a-switch>
<a-switch v-model="inbound.stream.hysteria.masquerade.insecure"></a-switch>
</a-form-item>
</template>
<template v-if="inbound.stream.hysteria.masquerade.type === 'string'">
<a-form-item label="Content">
<a-input
v-model.trim="inbound.stream.hysteria.masquerade.content"
></a-input>
<a-input v-model.trim="inbound.stream.hysteria.masquerade.content"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button
size="small"
@click="inbound.stream.hysteria.masquerade.addHeader('', '')"
>+</a-button
>
<a-button size="small" @click="inbound.stream.hysteria.masquerade.addHeader('', '')">+</a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group
compact
v-for="(header, index) in inbound.stream.hysteria.masquerade.headers"
>
<a-input
style="width: 50%"
v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
>
<template slot="addonBefore" style="margin: 0"
>[[ index+1 ]]</template
>
<a-input-group compact v-for="(header, index) in inbound.stream.hysteria.masquerade.headers">
<a-input style="width: 50%" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
<template slot="addonBefore" style="margin: 0">[[ index+1 ]]</template>
</a-input>
<a-input
style="width: 50%"
v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
>
<a-button
slot="addonAfter"
size="small"
@click="inbound.stream.hysteria.masquerade.removeHeader(index)"
>-</a-button
>
<a-input style="width: 50%" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button slot="addonAfter" size="small"
@click="inbound.stream.hysteria.masquerade.removeHeader(index)">-</a-button>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item label="Status Code">
<a-input-number
v-model.number="inbound.stream.hysteria.masquerade.statusCode"
></a-input-number>
<a-input-number v-model.number="inbound.stream.hysteria.masquerade.statusCode"></a-input-number>
</a-form-item>
</template>
</template>
</a-form>
{{end}}
{{end}}

View file

@ -1,46 +1,22 @@
{{define "form/streamKCP"}}
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="MTU">
<a-input-number
v-model.number="inbound.stream.kcp.mtu"
:min="576"
:max="1460"
></a-input-number>
<a-input-number v-model.number="inbound.stream.kcp.mtu" :min="576" :max="1460"></a-input-number>
</a-form-item>
<a-form-item label="TTI (ms)">
<a-input-number
v-model.number="inbound.stream.kcp.tti"
:min="10"
:max="100"
></a-input-number>
<a-input-number v-model.number="inbound.stream.kcp.tti" :min="10" :max="100"></a-input-number>
</a-form-item>
<a-form-item label="Uplink (MB/s)">
<a-input-number
v-model.number="inbound.stream.kcp.upCap"
:min="0"
></a-input-number>
<a-input-number v-model.number="inbound.stream.kcp.upCap" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="Downlink (MB/s)">
<a-input-number
v-model.number="inbound.stream.kcp.downCap"
:min="0"
></a-input-number>
<a-input-number v-model.number="inbound.stream.kcp.downCap" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="CWND Multiplier">
<a-input-number
v-model.number="inbound.stream.kcp.cwndMultiplier"
:min="1"
></a-input-number>
<a-input-number v-model.number="inbound.stream.kcp.cwndMultiplier" :min="1"></a-input-number>
</a-form-item>
<a-form-item label="Max Sending Window">
<a-input-number
v-model.number="inbound.stream.kcp.maxSendingWindow"
:min="0"
></a-input-number>
<a-input-number v-model.number="inbound.stream.kcp.maxSendingWindow" :min="0"></a-input-number>
</a-form-item>
</a-form>
{{end}}
{{end}}

View file

@ -1,18 +1,10 @@
{{define "form/streamSettings"}}
<!-- select stream network -->
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
v-if="inbound.protocol != Protocols.HYSTERIA"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }"
v-if="inbound.protocol != Protocols.HYSTERIA">
<a-form-item label='{{ i18n "transmission" }}'>
<a-select
v-model="inbound.stream.network"
:style="{ width: '75%' }"
@change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select v-model="inbound.stream.network" :style="{ width: '75%' }" @change="streamNetworkChange"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="tcp">TCP (RAW)</a-select-option>
<a-select-option value="kcp">mKCP</a-select-option>
<a-select-option value="ws">WebSocket</a-select-option>
@ -25,42 +17,42 @@
<!-- tcp -->
<template v-if="inbound.stream.network === 'tcp'">
{{template "form/streamTCP"}}
{{template "form/streamTCP" .}}
</template>
<!-- kcp -->
<template v-if="inbound.stream.network === 'kcp'">
{{template "form/streamKCP"}}
{{template "form/streamKCP" .}}
</template>
<!-- ws -->
<template v-if="inbound.stream.network === 'ws'">
{{template "form/streamWS"}}
{{template "form/streamWS" .}}
</template>
<!-- grpc -->
<template v-if="inbound.stream.network === 'grpc'">
{{template "form/streamGRPC"}}
{{template "form/streamGRPC" .}}
</template>
<!-- hysteria -->
<template v-if="inbound.stream.network === 'hysteria'">
{{template "form/streamHysteria"}}
{{template "form/streamHysteria" .}}
</template>
<!-- httpupgrade -->
<template v-if="inbound.stream.network === 'httpupgrade'">
{{template "form/streamHTTPUpgrade"}}
{{template "form/streamHTTPUpgrade" .}}
</template>
<!-- xhttp -->
<template v-if="inbound.stream.network === 'xhttp'">
{{template "form/streamXHTTP"}}
{{template "form/streamXHTTP" .}}
</template>
<!-- sockopt -->
<template> {{template "form/streamSockopt"}} </template>
<template> {{template "form/streamSockopt" .}} </template>
<!-- finalmask -->
<template> {{template "form/streamFinalMask"}} </template>
{{end}}
<template> {{template "form/streamFinalMask" .}} </template>
{{end}}

View file

@ -1,49 +1,27 @@
{{define "form/streamSockopt"}}
<a-divider :style="{ margin: '5px 0 0' }"></a-divider>
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Sockopt">
<a-switch v-model="inbound.stream.sockoptSwitch"></a-switch>
</a-form-item>
<template v-if="inbound.stream.sockoptSwitch">
<a-form-item label="Route Mark">
<a-input-number
v-model.number="inbound.stream.sockopt.mark"
:min="0"
></a-input-number>
<a-input-number v-model.number="inbound.stream.sockopt.mark" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Keep Alive Interval">
<a-input-number
v-model.number="inbound.stream.sockopt.tcpKeepAliveInterval"
:min="0"
></a-input-number>
<a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveInterval" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Keep Alive Idle">
<a-input-number
v-model.number="inbound.stream.sockopt.tcpKeepAliveIdle"
:min="0"
></a-input-number>
<a-input-number v-model.number="inbound.stream.sockopt.tcpKeepAliveIdle" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Max Seg">
<a-input-number
v-model.number="inbound.stream.sockopt.tcpMaxSeg"
:min="0"
></a-input-number>
<a-input-number v-model.number="inbound.stream.sockopt.tcpMaxSeg" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP User Timeout">
<a-input-number
v-model.number="inbound.stream.sockopt.tcpUserTimeout"
:min="0"
></a-input-number>
<a-input-number v-model.number="inbound.stream.sockopt.tcpUserTimeout" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="TCP Window Clamp">
<a-input-number
v-model.number="inbound.stream.sockopt.tcpWindowClamp"
:min="0"
></a-input-number>
<a-input-number v-model.number="inbound.stream.sockopt.tcpWindowClamp" :min="0"></a-input-number>
</a-form-item>
<a-form-item label="Proxy Protocol">
<a-switch v-model="inbound.stream.sockopt.acceptProxyProtocol"></a-switch>
@ -61,33 +39,20 @@
<a-switch v-model.trim="inbound.stream.sockopt.V6Only"></a-switch>
</a-form-item>
<a-form-item label="Domain Strategy">
<a-select
v-model="inbound.stream.sockopt.domainStrategy"
:style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option v-for="key in DOMAIN_STRATEGY_OPTION" :value="key"
>[[ key ]]</a-select-option
>
<a-select v-model="inbound.stream.sockopt.domainStrategy" :style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in DOMAIN_STRATEGY_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="TCP Congestion">
<a-select
v-model="inbound.stream.sockopt.tcpcongestion"
:style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option v-for="key in TCP_CONGESTION_OPTION" :value="key"
>[[ key ]]</a-select-option
>
<a-select v-model="inbound.stream.sockopt.tcpcongestion" :style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TCP_CONGESTION_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label="TProxy">
<a-select
v-model="inbound.stream.sockopt.tproxy"
:style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select v-model="inbound.stream.sockopt.tproxy" :style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="off">Off</a-select-option>
<a-select-option value="redirect">Redirect</a-select-option>
<a-select-option value="tproxy">TProxy</a-select-option>
@ -100,15 +65,9 @@
<a-input v-model="inbound.stream.sockopt.interfaceName"></a-input>
</a-form-item>
<a-form-item label="Trusted X-Forwarded-For">
<a-select
mode="tags"
v-model="inbound.stream.sockopt.trustedXForwardedFor"
:style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option value="CF-Connecting-IP"
>CF-Connecting-IP</a-select-option
>
<a-select mode="tags" v-model="inbound.stream.sockopt.trustedXForwardedFor" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="CF-Connecting-IP">CF-Connecting-IP</a-select-option>
<a-select-option value="X-Real-IP">X-Real-IP</a-select-option>
<a-select-option value="True-Client-IP">True-Client-IP</a-select-option>
<a-select-option value="X-Client-IP">X-Client-IP</a-select-option>
@ -116,4 +75,4 @@
</a-form-item>
</template>
</a-form>
{{end}}
{{end}}

View file

@ -1,31 +1,19 @@
{{define "form/streamTCP"}}
<!-- tcp type -->
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Proxy Protocol" v-if="inbound.canEnableTls()">
<a-switch v-model="inbound.stream.tcp.acceptProxyProtocol"></a-switch>
</a-form-item>
<a-form-item label='HTTP {{ i18n "camouflage" }}'>
<a-switch
:checked="inbound.stream.tcp.type === 'http'"
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"
></a-switch>
<a-switch :checked="inbound.stream.tcp.type === 'http'"
@change="checked => inbound.stream.tcp.type = checked ? 'http' : 'none'"></a-switch>
</a-form-item>
</a-form>
<a-form
v-if="inbound.stream.tcp.type === 'http'"
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form v-if="inbound.stream.tcp.type === 'http'" :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<!-- tcp request -->
<a-divider :style="{ margin: '0' }"
>{{ i18n "pages.inbounds.stream.general.request" }}</a-divider
>
<a-divider :style="{ margin: '0' }">{{ i18n "pages.inbounds.stream.general.request" }}</a-divider>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
<a-input v-model.trim="inbound.stream.tcp.request.version"></a-input>
</a-form-item>
@ -33,66 +21,35 @@
<a-input v-model.trim="inbound.stream.tcp.request.method"></a-input>
</a-form-item>
<a-form-item>
<template slot="label"
>{{ i18n "pages.inbounds.stream.tcp.path" }}
<a-button
icon="plus"
size="small"
@click="inbound.stream.tcp.request.addPath('/')"
></a-button>
<template slot="label">{{ i18n "pages.inbounds.stream.tcp.path" }}
<a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addPath('/')"></a-button>
</template>
<template v-for="(path, index) in inbound.stream.tcp.request.path">
<a-input v-model.trim="inbound.stream.tcp.request.path[index]">
<a-button
icon="minus"
size="small"
slot="addonAfter"
@click="inbound.stream.tcp.request.removePath(index)"
v-if="inbound.stream.tcp.request.path.length>1"
></a-button>
<a-button icon="minus" size="small" slot="addonAfter" @click="inbound.stream.tcp.request.removePath(index)"
v-if="inbound.stream.tcp.request.path.length>1"></a-button>
</a-input>
</template>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button
icon="plus"
size="small"
@click="inbound.stream.tcp.request.addHeader('Host', '')"
></a-button>
<a-button icon="plus" size="small" @click="inbound.stream.tcp.request.addHeader('Host', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group
compact
v-for="(header, index) in inbound.stream.tcp.request.headers"
>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'
>
<template slot="addonBefore" :style="{ margin: '0' }"
>[[ index+1 ]]</template
>
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.request.headers">
<a-input :style="{ width: '50%' }" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
</a-input>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
>
<a-button
icon="minus"
slot="addonAfter"
size="small"
@click="inbound.stream.tcp.request.removeHeader(index)"
></a-button>
<a-input :style="{ width: '50%' }" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button icon="minus" slot="addonAfter" size="small"
@click="inbound.stream.tcp.request.removeHeader(index)"></a-button>
</a-input>
</a-input-group>
</a-form-item>
<!-- tcp response -->
<a-divider :style="{ margin: '0' }"
>{{ i18n "pages.inbounds.stream.general.response" }}</a-divider
>
<a-divider :style="{ margin: '0' }">{{ i18n "pages.inbounds.stream.general.response" }}</a-divider>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.version" }}'>
<a-input v-model.trim="inbound.stream.tcp.response.version"></a-input>
</a-form-item>
@ -103,40 +60,22 @@
<a-input v-model.trim="inbound.stream.tcp.response.reason"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.responseHeader" }}'>
<a-button
icon="plus"
size="small"
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"
></a-button>
<a-button icon="plus" size="small"
@click="inbound.stream.tcp.response.addHeader('Content-Type', 'application/octet-stream')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group
compact
v-for="(header, index) in inbound.stream.tcp.response.headers"
>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'
>
<template slot="addonBefore" :style="{ margin: '0' }"
>[[ index+1 ]]</template
>
<a-input-group compact v-for="(header, index) in inbound.stream.tcp.response.headers">
<a-input :style="{ width: '50%' }" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
</a-input>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
>
<a-input :style="{ width: '50%' }" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<template slot="addonAfter">
<a-button
icon="minus"
size="small"
@click="inbound.stream.tcp.response.removeHeader(index)"
></a-button>
<a-button icon="minus" size="small" @click="inbound.stream.tcp.response.removeHeader(index)"></a-button>
</template>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
{{end}}
{{end}}

View file

@ -1,9 +1,5 @@
{{define "form/streamWS"}}
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label="Proxy Protocol">
<a-switch v-model="inbound.stream.ws.acceptProxyProtocol"></a-switch>
</a-form-item>
@ -14,42 +10,22 @@
<a-input v-model.trim="inbound.stream.ws.path"></a-input>
</a-form-item>
<a-form-item label="Heartbeat Period">
<a-input-number
v-model.number="inbound.stream.ws.heartbeatPeriod"
:min="0"
></a-input-number>
<a-input-number v-model.number="inbound.stream.ws.heartbeatPeriod" :min="0"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button
icon="plus"
size="small"
@click="inbound.stream.ws.addHeader('', '')"
></a-button>
<a-button icon="plus" size="small" @click="inbound.stream.ws.addHeader('', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group compact v-for="(header, index) in inbound.stream.ws.headers">
<a-input
:style="{ width: '50%' }"
v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
>
<template slot="addonBefore" :style="{ margin: '0' }"
>[[ index+1 ]]</template
>
<a-input :style="{ width: '50%' }" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
</a-input>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
>
<a-button
icon="minus"
slot="addonAfter"
size="small"
@click="inbound.stream.ws.removeHeader(index)"
></a-button>
<a-input :style="{ width: '50%' }" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button icon="minus" slot="addonAfter" size="small" @click="inbound.stream.ws.removeHeader(index)"></a-button>
</a-input>
</a-input-group>
</a-form-item>
</a-form>
{{end}}
{{end}}

View file

@ -1,9 +1,5 @@
{{define "form/streamXHTTP"}}
<a-form
:colon="false"
:label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }"
>
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "host" }}'>
<a-input v-model.trim="inbound.stream.xhttp.host"></a-input>
</a-form-item>
@ -11,69 +7,34 @@
<a-input v-model.trim="inbound.stream.xhttp.path"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.inbounds.stream.tcp.requestHeader" }}'>
<a-button
icon="plus"
size="small"
@click="inbound.stream.xhttp.addHeader('', '')"
></a-button>
<a-button icon="plus" size="small" @click="inbound.stream.xhttp.addHeader('', '')"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span:24}">
<a-input-group
compact
v-for="(header, index) in inbound.stream.xhttp.headers"
>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'
>
<template slot="addonBefore" :style="{ margin: '0' }"
>[[ index+1 ]]</template
>
<a-input-group compact v-for="(header, index) in inbound.stream.xhttp.headers">
<a-input :style="{ width: '50%' }" v-model.trim="header.name"
placeholder='{{ i18n "pages.inbounds.stream.general.name"}}'>
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
</a-input>
<a-input
:style="{ width: '50%' }"
v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'
>
<a-button
icon="minus"
slot="addonAfter"
size="small"
@click="inbound.stream.xhttp.removeHeader(index)"
></a-button>
<a-input :style="{ width: '50%' }" v-model.trim="header.value"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button icon="minus" slot="addonAfter" size="small"
@click="inbound.stream.xhttp.removeHeader(index)"></a-button>
</a-input>
</a-input-group>
</a-form-item>
<a-form-item label="Mode">
<a-select
v-model="inbound.stream.xhttp.mode"
:style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select-option v-for="key in MODE_OPTION" :value="key"
>[[ key ]]</a-select-option
>
<a-select v-model="inbound.stream.xhttp.mode" :style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in MODE_OPTION" :value="key">[[ key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item
label="Max Buffered Upload"
v-if="inbound.stream.xhttp.mode === 'packet-up'"
>
<a-input-number
v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"
></a-input-number>
<a-form-item label="Max Buffered Upload" v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-input-number v-model.number="inbound.stream.xhttp.scMaxBufferedPosts"></a-input-number>
</a-form-item>
<a-form-item
label="Max Upload Size (Byte)"
v-if="inbound.stream.xhttp.mode === 'packet-up'"
>
<a-form-item label="Max Upload Size (Byte)" v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-input v-model.trim="inbound.stream.xhttp.scMaxEachPostBytes"></a-input>
</a-form-item>
<a-form-item
label="Stream-Up Server"
v-if="inbound.stream.xhttp.mode === 'stream-up'"
>
<a-form-item label="Stream-Up Server" v-if="inbound.stream.xhttp.mode === 'stream-up'">
<a-input v-model.trim="inbound.stream.xhttp.scStreamUpServerSecs"></a-input>
</a-form-item>
<a-form-item label="Padding Bytes">
@ -84,22 +45,13 @@
</a-form-item>
<template v-if="inbound.stream.xhttp.xPaddingObfsMode">
<a-form-item label="Padding Key">
<a-input
v-model.trim="inbound.stream.xhttp.xPaddingKey"
placeholder="x_padding"
></a-input>
<a-input v-model.trim="inbound.stream.xhttp.xPaddingKey" placeholder="x_padding"></a-input>
</a-form-item>
<a-form-item label="Padding Header">
<a-input
v-model.trim="inbound.stream.xhttp.xPaddingHeader"
placeholder="X-Padding"
></a-input>
<a-input v-model.trim="inbound.stream.xhttp.xPaddingHeader" placeholder="X-Padding"></a-input>
</a-form-item>
<a-form-item label="Padding Placement">
<a-select
v-model="inbound.stream.xhttp.xPaddingPlacement"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select v-model="inbound.stream.xhttp.xPaddingPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Default (queryInHeader)</a-select-option>
<a-select-option value="queryInHeader">queryInHeader</a-select-option>
<a-select-option value="header">header</a-select-option>
@ -108,10 +60,7 @@
</a-select>
</a-form-item>
<a-form-item label="Padding Method">
<a-select
v-model="inbound.stream.xhttp.xPaddingMethod"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select v-model="inbound.stream.xhttp.xPaddingMethod" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Default (repeat-x)</a-select-option>
<a-select-option value="repeat-x">repeat-x</a-select-option>
<a-select-option value="tokenish">tokenish</a-select-option>
@ -119,10 +68,7 @@
</a-form-item>
</template>
<a-form-item label="Uplink HTTP Method">
<a-select
v-model="inbound.stream.xhttp.uplinkHTTPMethod"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select v-model="inbound.stream.xhttp.uplinkHTTPMethod" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Default (POST)</a-select-option>
<a-select-option value="POST">POST</a-select-option>
<a-select-option value="PUT">PUT</a-select-option>
@ -130,10 +76,7 @@
</a-select>
</a-form-item>
<a-form-item label="Session Placement">
<a-select
v-model="inbound.stream.xhttp.sessionPlacement"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select v-model="inbound.stream.xhttp.sessionPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Default (path)</a-select-option>
<a-select-option value="path">path</a-select-option>
<a-select-option value="header">header</a-select-option>
@ -141,20 +84,12 @@
<a-select-option value="query">query</a-select-option>
</a-select>
</a-form-item>
<a-form-item
label="Session Key"
v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'"
>
<a-input
v-model.trim="inbound.stream.xhttp.sessionKey"
placeholder="x_session"
></a-input>
<a-form-item label="Session Key"
v-if="inbound.stream.xhttp.sessionPlacement && inbound.stream.xhttp.sessionPlacement !== 'path'">
<a-input v-model.trim="inbound.stream.xhttp.sessionKey" placeholder="x_session"></a-input>
</a-form-item>
<a-form-item label="Sequence Placement">
<a-select
v-model="inbound.stream.xhttp.seqPlacement"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-select v-model="inbound.stream.xhttp.seqPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Default (path)</a-select-option>
<a-select-option value="path">path</a-select-option>
<a-select-option value="header">header</a-select-option>
@ -162,23 +97,12 @@
<a-select-option value="query">query</a-select-option>
</a-select>
</a-form-item>
<a-form-item
label="Sequence Key"
v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'"
>
<a-input
v-model.trim="inbound.stream.xhttp.seqKey"
placeholder="x_seq"
></a-input>
<a-form-item label="Sequence Key"
v-if="inbound.stream.xhttp.seqPlacement && inbound.stream.xhttp.seqPlacement !== 'path'">
<a-input v-model.trim="inbound.stream.xhttp.seqKey" placeholder="x_seq"></a-input>
</a-form-item>
<a-form-item
label="Uplink Data Placement"
v-if="inbound.stream.xhttp.mode === 'packet-up'"
>
<a-select
v-model="inbound.stream.xhttp.uplinkDataPlacement"
:dropdown-class-name="themeSwitcher.currentTheme"
>
<a-form-item label="Uplink Data Placement" v-if="inbound.stream.xhttp.mode === 'packet-up'">
<a-select v-model="inbound.stream.xhttp.uplinkDataPlacement" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Default (body)</a-select-option>
<a-select-option value="body">body</a-select-option>
<a-select-option value="header">header</a-select-option>
@ -186,27 +110,17 @@
<a-select-option value="query">query</a-select-option>
</a-select>
</a-form-item>
<a-form-item
label="Uplink Data Key"
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'"
>
<a-input
v-model.trim="inbound.stream.xhttp.uplinkDataKey"
placeholder="x_data"
></a-input>
<a-form-item label="Uplink Data Key"
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
<a-input v-model.trim="inbound.stream.xhttp.uplinkDataKey" placeholder="x_data"></a-input>
</a-form-item>
<a-form-item
label="Uplink Chunk Size"
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'"
>
<a-input-number
v-model.number="inbound.stream.xhttp.uplinkChunkSize"
:min="0"
placeholder="0 (unlimited)"
></a-input-number>
<a-form-item label="Uplink Chunk Size"
v-if="inbound.stream.xhttp.mode === 'packet-up' && inbound.stream.xhttp.uplinkDataPlacement && inbound.stream.xhttp.uplinkDataPlacement !== 'body'">
<a-input-number v-model.number="inbound.stream.xhttp.uplinkChunkSize" :min="0"
placeholder="0 (unlimited)"></a-input-number>
</a-form-item>
<a-form-item label="No SSE Header">
<a-switch v-model="inbound.stream.xhttp.noSSEHeader"></a-switch>
</a-form-item>
</a-form>
{{end}}
{{end}}

View file

@ -1,14 +1,12 @@
{{define "form/tlsSettings"}}
<!-- tls enable -->
<a-form v-if="inbound.canEnableTls()" :colon="false"
:label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form v-if="inbound.canEnableTls()" :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<template v-if="inbound.protocol !== Protocols.HYSTERIA">
<a-divider :style="{ margin: '3px 0' }"></a-divider>
<a-form-item label='{{ i18n "security" }}'>
<a-radio-group v-model="inbound.stream.security" button-style="solid">
<a-radio-button value="none">{{ i18n "none" }}</a-radio-button>
<a-radio-button v-if="inbound.canEnableReality()"
value="reality">Reality</a-radio-button>
<a-radio-button v-if="inbound.canEnableReality()" value="reality">Reality</a-radio-button>
<a-radio-button value="tls">TLS</a-radio-button>
</a-radio-group>
</a-form-item>
@ -21,8 +19,7 @@
<a-input v-model.trim="inbound.stream.tls.sni"></a-input>
</a-form-item>
<a-form-item label="Cipher Suites">
<a-select v-model="inbound.stream.tls.cipherSuites"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="inbound.stream.tls.cipherSuites" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>Auto</a-select-option>
<a-select-option v-for="key,value in TLS_CIPHER_OPTION" :value="key">[[
value ]]</a-select-option>
@ -30,14 +27,12 @@
</a-form-item>
<a-form-item label="Min/Max Version">
<a-input-group compact>
<a-select v-model="inbound.stream.tls.minVersion"
:style="{ width: '50%' }"
<a-select v-model="inbound.stream.tls.minVersion" :style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key
]]</a-select-option>
</a-select>
<a-select v-model="inbound.stream.tls.maxVersion"
:style="{ width: '50%' }"
<a-select v-model="inbound.stream.tls.maxVersion" :style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in TLS_VERSION_OPTION" :value="key">[[ key
]]</a-select-option>
@ -45,8 +40,7 @@
</a-input-group>
</a-form-item>
<a-form-item label="uTLS">
<a-select v-model="inbound.stream.tls.settings.fingerprint"
:style="{ width: '100%' }"
<a-select v-model="inbound.stream.tls.settings.fingerprint" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value>None</a-select-option>
<a-select-option v-for="key in UTLS_FINGERPRINT" :value="key">[[ key
@ -54,9 +48,7 @@
</a-select>
</a-form-item>
<a-form-item label="ALPN">
<a-select mode="multiple"
:dropdown-class-name="themeSwitcher.currentTheme"
v-model="inbound.stream.tls.alpn">
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" v-model="inbound.stream.tls.alpn">
<a-select-option v-for="alpn in ALPN_OPTION" :value="alpn">[[ alpn
]]</a-select-option>
</a-select>
@ -75,8 +67,7 @@
<a-form-item label='{{ i18n "certificate" }}'>
<a-radio-group v-model="cert.useFile" button-style="solid"
:style="{ display: 'inline-flex', whiteSpace: 'nowrap', maxWidth: '100%' }">
<a-radio-button :value="true"
:style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
<a-radio-button :value="true" :style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
i18n "pages.inbounds.certificatePath" }}</a-radio-button>
<a-radio-button :value="false"
:style="{ overflow: 'hidden', textOverflow: 'ellipsis', whiteSpace: 'nowrap' }">{{
@ -87,8 +78,7 @@
<a-space>
<a-button icon="plus" v-if="index === 0" type="primary" size="small"
@click="inbound.stream.tls.addCert()"></a-button>
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1"
type="primary" size="small"
<a-button icon="minus" v-if="inbound.stream.tls.certs.length>1" type="primary" size="small"
@click="inbound.stream.tls.removeCert(index)"></a-button>
</a-space>
</a-form-item>
@ -100,8 +90,7 @@
<a-input v-model.trim="cert.keyFile"></a-input>
</a-form-item>
<a-form-item label=" ">
<a-button type="primary" icon="import"
@click="setDefaultCertData(index)">
<a-button type="primary" icon="import" @click="setDefaultCertData(index)">
{{ i18n "pages.inbounds.setDefaultCert" }}</a-button>
</a-form-item>
</template>
@ -117,8 +106,7 @@
<a-switch v-model="cert.oneTimeLoading"></a-switch>
</a-form-item>
<a-form-item label='Usage Option'>
<a-select v-model="cert.usage" :style="{ width: '50%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="cert.usage" :style="{ width: '50%' }" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USAGE_OPTION" :value="key">[[ key
]]</a-select-option>
</a-select>
@ -133,13 +121,6 @@
<a-form-item label='ECH config'>
<a-input v-model="inbound.stream.tls.settings.echConfigList"></a-input>
</a-form-item>
<a-form-item label='ECH force query'>
<a-select v-model="inbound.stream.tls.echForceQuery"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in ['none', 'half', 'full']" :value="key">[[
key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label=" ">
<a-space>
<a-button type="primary" icon="import" @click="getNewEchCert">Get New
@ -151,7 +132,7 @@
<!-- reality settings -->
<template v-if="inbound.stream.isReality">
{{template "form/realitySettings"}}
{{template "form/realitySettings" .}}
</template>
</a-form>
{{end}}

File diff suppressed because it is too large Load diff

View file

@ -10,6 +10,7 @@
padding: 2px 6px;
border-radius: 3px;
}
html[data-theme="ultra-dark"] body.dark .custom-geo-section code.custom-geo-ext-code {
color: var(--dark-color-text-primary, rgba(255, 255, 255, 0.88));
background: var(--dark-color-surface-700, #111929);
@ -119,7 +120,8 @@
</a-row>
</span>
<template slot="content">
<span class="max-w-400" v-for="line in (status.xray.errorMsg || '').split('\n')">[[ line ]]</span>
<span class="max-w-400" v-for="line in (status.xray.errorMsg || '').split('\n')">[[ line
]]</span>
</template>
<a-badge :text="status.xray.stateMsg" :color="status.xray.color"
:class="status.xray.color === 'red' ? 'xray-error-animation' : ''" />
@ -170,10 +172,11 @@
<a-col :sm="24" :lg="12">
<a-card title='3X-UI' hoverable>
<template v-if="panelUpdateModal.info.updateAvailable" #extra>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme" :title='`{{ i18n "pages.index.updatePanel" }}: ${panelUpdateModal.info.latestVersion}`'>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme"
:title='`{{ i18n "pages.index.updatePanel" }}: ${panelUpdateModal.info.latestVersion}`'>
<a-tag color="orange" style="cursor:pointer;margin:0" @click="openPanelUpdate">
<a-icon type="cloud-download"></a-icon>[[ panelUpdateModal.info.latestVersion ]]
<span v-if="!isMobile">{{ i18n "pages.index.updatePanel" }}</span>
<span v-if="!isMobile">{{ i18n "pages.index.updatePanel" }}</span>
</a-tag>
</a-tooltip>
</template>
@ -327,8 +330,7 @@
</a-layout>
<a-modal id="panel-update-modal" v-model="panelUpdateModal.visible" title='{{ i18n "pages.index.updatePanel" }}'
:closable="true" @ok="() => panelUpdateModal.visible = false" :class="themeSwitcher.currentTheme" footer="">
<a-alert type="warning" class="mb-12 w-100" message='{{ i18n "pages.index.panelUpdateDesc" }}'
show-icon></a-alert>
<a-alert type="warning" class="mb-12 w-100" message='{{ i18n "pages.index.panelUpdateDesc" }}' show-icon></a-alert>
<a-list class="ant-version-list w-100" bordered>
<a-list-item class="ant-version-list-item">
<span>{{ i18n "pages.index.currentPanelVersion" }}</span>
@ -379,57 +381,62 @@
</a-collapse-panel>
<a-collapse-panel key="3" header='{{ i18n "pages.index.customGeoTitle" }}'>
<div class="custom-geo-section">
<a-alert type="info" show-icon class="mb-10"
message='{{ i18n "pages.index.customGeoRoutingHint" }}'></a-alert>
<div class="mb-10">
<a-button type="primary" icon="plus" @click="openCustomGeoModal(null)" :loading="customGeoLoading">
{{ i18n "pages.index.customGeoAdd" }}
</a-button>
<a-button class="ml-8" icon="reload" @click="updateAllCustomGeo" :loading="customGeoUpdatingAll">{{ i18n
<a-alert type="info" show-icon class="mb-10"
message='{{ i18n "pages.index.customGeoRoutingHint" }}'></a-alert>
<div class="mb-10">
<a-button type="primary" icon="plus" @click="openCustomGeoModal(null)" :loading="customGeoLoading">
{{ i18n "pages.index.customGeoAdd" }}
</a-button>
<a-button class="ml-8" icon="reload" @click="updateAllCustomGeo" :loading="customGeoUpdatingAll">{{ i18n
"pages.index.geofilesUpdateAll" }}</a-button>
</div>
<a-table :columns="customGeoColumns" :data-source="customGeoList" :pagination="false" :row-key="r => r.id"
:loading="customGeoLoading" size="small" :scroll="{ x: 520 }">
<template slot="extDat" slot-scope="text, record">
<code class="custom-geo-ext-code">[[ customGeoExtDisplay(record) ]]</code>
</template>
<template slot="lastUpdatedAt" slot-scope="text, record">
<span v-if="record.lastUpdatedAt">[[ customGeoFormatTime(record.lastUpdatedAt) ]]</span>
<span v-else></span>
</template>
<template slot="action" slot-scope="text, record">
<a-space size="small">
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template slot="title">{{ i18n "pages.index.customGeoEdit" }}</template>
<a-button type="link" size="small" icon="edit" @click="openCustomGeoModal(record)"></a-button>
</a-tooltip>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template slot="title">{{ i18n "pages.index.customGeoDownload" }}</template>
<a-button type="link" size="small" icon="reload" @click="downloadCustomGeo(record.id)" :loading="customGeoActionId === record.id"></a-button>
</a-tooltip>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template slot="title">{{ i18n "pages.index.customGeoDelete" }}</template>
<a-button type="link" size="small" icon="delete" @click="confirmDeleteCustomGeo(record)"></a-button>
</a-tooltip>
</a-space>
</template>
</a-table>
</div>
<a-table :columns="customGeoColumns" :data-source="customGeoList" :pagination="false" :row-key="r => r.id"
:loading="customGeoLoading" size="small" :scroll="{ x: 520 }">
<template slot="extDat" slot-scope="text, record">
<code class="custom-geo-ext-code">[[ customGeoExtDisplay(record) ]]</code>
</template>
<template slot="lastUpdatedAt" slot-scope="text, record">
<span v-if="record.lastUpdatedAt">[[ customGeoFormatTime(record.lastUpdatedAt) ]]</span>
<span v-else></span>
</template>
<template slot="action" slot-scope="text, record">
<a-space size="small">
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template slot="title">{{ i18n "pages.index.customGeoEdit" }}</template>
<a-button type="link" size="small" icon="edit" @click="openCustomGeoModal(record)"></a-button>
</a-tooltip>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template slot="title">{{ i18n "pages.index.customGeoDownload" }}</template>
<a-button type="link" size="small" icon="reload" @click="downloadCustomGeo(record.id)"
:loading="customGeoActionId === record.id"></a-button>
</a-tooltip>
<a-tooltip :overlay-class-name="themeSwitcher.currentTheme">
<template slot="title">{{ i18n "pages.index.customGeoDelete" }}</template>
<a-button type="link" size="small" icon="delete" @click="confirmDeleteCustomGeo(record)"></a-button>
</a-tooltip>
</a-space>
</template>
</a-table>
</div>
</a-collapse-panel>
</a-collapse>
</a-modal>
<a-modal v-model="customGeoModal.visible" :title="customGeoModal.editId ? '{{ i18n "pages.index.customGeoModalEdit" }}' : '{{ i18n "pages.index.customGeoModalAdd" }}'"
:confirm-loading="customGeoModal.saving" @ok="submitCustomGeo" :ok-text="'{{ i18n "pages.index.customGeoModalSave" }}'" :cancel-text="'{{ i18n "close" }}'"
<a-modal v-model="customGeoModal.visible"
:title="customGeoModal.editId ? '{{ i18n "pages.index.customGeoModalEdit" }}' : '{{ i18n "pages.index.customGeoModalAdd" }}'"
:confirm-loading="customGeoModal.saving" @ok="submitCustomGeo"
:ok-text="'{{ i18n "pages.index.customGeoModalSave" }}'" :cancel-text="'{{ i18n "close" }}'"
:class="themeSwitcher.currentTheme">
<a-form layout="vertical">
<a-form-item label='{{ i18n "pages.index.customGeoType" }}'>
<a-select v-model="customGeoModal.form.type" :disabled="!!customGeoModal.editId" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select v-model="customGeoModal.form.type" :disabled="!!customGeoModal.editId"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value="geosite">geosite</a-select-option>
<a-select-option value="geoip">geoip</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "pages.index.customGeoAlias" }}'>
<a-input v-model.trim="customGeoModal.form.alias" :disabled="!!customGeoModal.editId" placeholder='{{ i18n "pages.index.customGeoAliasPlaceholder" }}'></a-input>
<a-input v-model.trim="customGeoModal.form.alias" :disabled="!!customGeoModal.editId"
placeholder='{{ i18n "pages.index.customGeoAliasPlaceholder" }}'></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.index.customGeoUrl" }}'>
<a-input v-model.trim="customGeoModal.form.url" placeholder="https://"></a-input>
@ -469,7 +476,8 @@
<a-checkbox v-model="logModal.syslog" @change="openLogs()">SysLog</a-checkbox>
</a-form-item>
<a-form-item style="float: right;">
<a-button type="primary" icon="download" @click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
<a-button type="primary" icon="download"
@click="FileManager.downloadTextFile(logModal.logs?.join('\n'), 'x-ui.log')"></a-button>
</a-form-item>
</a-form>
<div class="ant-input log-container" v-html="logModal.formattedLogs"></div>
@ -547,7 +555,8 @@
<sparkline :data="cpuHistoryLong" :labels="cpuHistoryLabels" :vb-width="840" :height="220"
:stroke="status.cpu.color" :stroke-width="2.2" :show-grid="true" :show-axes="true" :tick-count-x="5"
:max-points="cpuHistoryLong.length" :fill-opacity="0.18" :marker-radius="3.2" :show-tooltip="true" />
<div style="margin-top:4px;font-size:11px;opacity:0.65">Timeframe: [[ cpuHistoryModal.bucket ]] sec per point (total [[ cpuHistoryLong.length ]] points)</div>
<div style="margin-top:4px;font-size:11px;opacity:0.65">Timeframe: [[ cpuHistoryModal.bucket ]] sec per point
(total [[ cpuHistoryLong.length ]] points)</div>
</div>
</a-modal>
</a-layout>
@ -555,33 +564,93 @@
{{template "component/aSidebar" .}}
{{template "component/aThemeSwitch" .}}
{{template "component/aCustomStatistic" .}}
{{template "modals/textModal"}}
{{template "modals/textModal" .}}
<script>
// Tiny Sparkline component using an inline SVG polyline
Vue.component('sparkline', {
props: {
data: { type: Array, required: true },
data: {
type: Array,
required: true
},
// viewBox width for drawing space; SVG width will be 100% of container
vbWidth: { type: Number, default: 320 },
height: { type: Number, default: 80 },
stroke: { type: String, default: '#008771' },
strokeWidth: { type: Number, default: 2 },
maxPoints: { type: Number, default: 120 },
showGrid: { type: Boolean, default: true },
gridColor: { type: String, default: 'rgba(0,0,0,0.1)' },
fillOpacity: { type: Number, default: 0.15 },
showMarker: { type: Boolean, default: true },
markerRadius: { type: Number, default: 2.8 },
vbWidth: {
type: Number,
default: 320
},
height: {
type: Number,
default: 80
},
stroke: {
type: String,
default: '#008771'
},
strokeWidth: {
type: Number,
default: 2
},
maxPoints: {
type: Number,
default: 120
},
showGrid: {
type: Boolean,
default: true
},
gridColor: {
type: String,
default: 'rgba(0,0,0,0.1)'
},
fillOpacity: {
type: Number,
default: 0.15
},
showMarker: {
type: Boolean,
default: true
},
markerRadius: {
type: Number,
default: 2.8
},
// New opts for axes/labels/tooltip
labels: { type: Array, default: () => [] }, // same length as data for x labels (e.g., timestamps)
showAxes: { type: Boolean, default: false },
yTickStep: { type: Number, default: 25 }, // percent ticks
tickCountX: { type: Number, default: 4 },
paddingLeft: { type: Number, default: 32 },
paddingRight: { type: Number, default: 6 },
paddingTop: { type: Number, default: 6 },
paddingBottom: { type: Number, default: 20 },
showTooltip: { type: Boolean, default: false },
labels: {
type: Array,
default: () => []
}, // same length as data for x labels (e.g., timestamps)
showAxes: {
type: Boolean,
default: false
},
yTickStep: {
type: Number,
default: 25
}, // percent ticks
tickCountX: {
type: Number,
default: 4
},
paddingLeft: {
type: Number,
default: 32
},
paddingRight: {
type: Number,
default: 6
},
paddingTop: {
type: Number,
default: 6
},
paddingBottom: {
type: Number,
default: 20
},
showTooltip: {
type: Boolean,
default: false
},
},
data() {
return {
@ -644,7 +713,12 @@
// draw at 25%, 50%, 75%
return [0, 0.25, 0.5, 0.75, 1]
.map(r => Math.round(this.paddingTop + h * r))
.map(y => ({ x1: this.paddingLeft, y1: y, x2: this.paddingLeft + w, y2: y }))
.map(y => ({
x1: this.paddingLeft,
y1: y,
x2: this.paddingLeft + w,
y2: y
}))
},
lastPoint() {
if (this.pointsArr.length === 0) return null
@ -656,7 +730,10 @@
const ticks = []
for (let p = 0; p <= 100; p += step) {
const y = Math.round(this.paddingTop + (this.drawHeight - (p / 100) * this.drawHeight))
ticks.push({ y, label: `${p}%` })
ticks.push({
y,
label: `${p}%`
})
}
return ticks
},
@ -677,7 +754,10 @@
positions.forEach(idx => {
const label = labels[idx] != null ? String(labels[idx]) : String(idx)
const x = Math.round(this.paddingLeft + idx * dx)
ticks.push({ x, label })
ticks.push({
x,
label
})
})
return ticks
},
@ -778,17 +858,36 @@
this.disk = new CurTotal(0, 0);
this.loads = [0, 0, 0];
this.mem = new CurTotal(0, 0);
this.netIO = { up: 0, down: 0 };
this.netTraffic = { sent: 0, recv: 0 };
this.publicIP = { ipv4: 0, ipv6: 0 };
this.netIO = {
up: 0,
down: 0
};
this.netTraffic = {
sent: 0,
recv: 0
};
this.publicIP = {
ipv4: 0,
ipv6: 0
};
this.swap = new CurTotal(0, 0);
this.tcpCount = 0;
this.udpCount = 0;
this.uptime = 0;
this.appUptime = 0;
this.appStats = { threads: 0, mem: 0, uptime: 0 };
this.appStats = {
threads: 0,
mem: 0,
uptime: 0
};
this.xray = { state: 'stop', stateMsg: "", errorMsg: "", version: "", color: "" };
this.xray = {
state: 'stop',
stateMsg: "",
errorMsg: "",
version: "",
color: ""
};
if (data == null) {
return;
@ -864,6 +963,18 @@
},
};
const escapeHtml = (value) => {
if (value === null || value === undefined) {
return '';
}
return String(value)
.replace(/&/g, '&amp;')
.replace(/</g, '&lt;')
.replace(/>/g, '&gt;')
.replace(/"/g, '&quot;')
.replace(/'/g, '&#39;');
};
const logModal = {
visible: false,
logs: [],
@ -887,24 +998,28 @@
if (index > 0) formattedLogs += '<br>';
if (parts.length === 3) {
const d = parts[0];
const t = parts[1];
const level = parts[2];
const levelIndex = levels.indexOf(level, levels) || 5;
const d = escapeHtml(parts[0]);
const t = escapeHtml(parts[1]);
const levelRaw = parts[2];
const level = escapeHtml(levelRaw);
const idx = levels.indexOf(levelRaw);
const levelIndex = idx >= 0 ? idx : 5;
//formattedLogs += `<span style="color: gray;">${index + 1}.</span>`;
formattedLogs += `<span style="color: ${levelColors[0]};">${d} ${t}</span> `;
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${level}</span>`;
} else {
const levelIndex = levels.indexOf(data, levels) || 5;
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${data}</span>`;
const idx = levels.indexOf(data);
const levelIndex = idx >= 0 ? idx : 5;
formattedLogs += `<span style="color: ${levelColors[levelIndex]}">${escapeHtml(data)}</span>`;
}
if (message) {
if (message.startsWith("XRAY:"))
message = "<b>XRAY: </b>" + message.substring(5);
else
message = "<b>X-UI: </b>" + message;
if (message.startsWith("XRAY:")) {
message = "<b>XRAY: </b>" + escapeHtml(message.substring(5));
} else {
message = "<b>X-UI: </b>" + escapeHtml(message);
}
}
formattedLogs += message ? ' - ' + message : '';
@ -918,20 +1033,20 @@
};
const xraylogModal = {
visible: false,
logs: [],
rows: 20,
showDirect: true,
showBlocked: true,
showProxy: true,
loading: false,
show(logs) {
this.visible = true;
this.logs = logs;
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
},
formatLogs(logs) {
let formattedLogs = `
visible: false,
logs: [],
rows: 20,
showDirect: true,
showBlocked: true,
showProxy: true,
loading: false,
show(logs) {
this.visible = true;
this.logs = logs;
this.formattedLogs = this.logs?.length > 0 ? this.formatLogs(this.logs) : "No Record...";
},
formatLogs(logs) {
let formattedLogs = `
<style>
table {
border-collapse: collapse;
@ -954,38 +1069,37 @@
</tr>
`;
logs.reverse().forEach((log, index) => {
let outboundColor = '';
if (log.Event === 1) {
outboundColor = ' style="color: #e04141;"'; //red for blocked
}
else if (log.Event === 2) {
outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
}
logs.reverse().forEach((log, index) => {
let outboundColor = '';
if (log.Event === 1) {
outboundColor = ' style="color: #e04141;"'; //red for blocked
} else if (log.Event === 2) {
outboundColor = ' style="color: #3c89e8;"'; //blue for proxies
}
let text = ``;
if (log.Email !== "") {
text = `<td>${log.Email}</td>`;
}
let text = ``;
if (log.Email !== "") {
text = `<td>${escapeHtml(log.Email)}</td>`;
}
formattedLogs += `
formattedLogs += `
<tr ${outboundColor}>
<td><b>${IntlUtil.formatDate(log.DateTime)}</b></td>
<td>${log.FromAddress}</td>
<td>${log.ToAddress}</td>
<td>${log.Inbound}</td>
<td>${log.Outbound}</td>
<td><b>${escapeHtml(IntlUtil.formatDate(log.DateTime))}</b></td>
<td>${escapeHtml(log.FromAddress)}</td>
<td>${escapeHtml(log.ToAddress)}</td>
<td>${escapeHtml(log.Inbound)}</td>
<td>${escapeHtml(log.Outbound)}</td>
${text}
</tr>
`;
});
});
return formattedLogs += "</table>";
},
hide() {
this.visible = false;
},
};
return formattedLogs += "</table>";
},
hide() {
this.visible = false;
},
};
const backupModal = {
visible: false,
show() {
@ -996,10 +1110,31 @@
},
};
const customGeoColumns = [
{ title: '{{ i18n "pages.index.customGeoExtColumn" }}', key: 'extDat', scopedSlots: { customRender: 'extDat' }, ellipsis: true },
{ title: '{{ i18n "pages.index.customGeoLastUpdated" }}', key: 'lastUpdatedAt', scopedSlots: { customRender: 'lastUpdatedAt' }, width: 160 },
{ title: '{{ i18n "pages.index.customGeoActions" }}', key: 'action', scopedSlots: { customRender: 'action' }, width: 120, fixed: 'right' },
const customGeoColumns = [{
title: '{{ i18n "pages.index.customGeoExtColumn" }}',
key: 'extDat',
scopedSlots: {
customRender: 'extDat'
},
ellipsis: true
},
{
title: '{{ i18n "pages.index.customGeoLastUpdated" }}',
key: 'lastUpdatedAt',
scopedSlots: {
customRender: 'lastUpdatedAt'
},
width: 160
},
{
title: '{{ i18n "pages.index.customGeoActions" }}',
key: 'action',
scopedSlots: {
customRender: 'action'
},
width: 120,
fixed: 'right'
},
];
const app = new Vue({
@ -1016,7 +1151,10 @@
cpuHistory: [], // small live widget history
cpuHistoryLong: [], // aggregated points from backend
cpuHistoryLabels: [],
cpuHistoryModal: { visible: false, bucket: 2 },
cpuHistoryModal: {
visible: false,
bucket: 2
},
versionModal,
panelUpdateModal,
logModal,
@ -1092,16 +1230,16 @@
const labels = []
for (const p of msg.obj) {
const d = new Date(p.t * 1000)
const hh = String(d.getHours()).padStart(2,'0')
const mm = String(d.getMinutes()).padStart(2,'0')
const ss = String(d.getSeconds()).padStart(2,'0')
labels.push(bucket>=60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`)
const hh = String(d.getHours()).padStart(2, '0')
const mm = String(d.getMinutes()).padStart(2, '0')
const ss = String(d.getSeconds()).padStart(2, '0')
labels.push(bucket >= 60 ? `${hh}:${mm}` : `${hh}:${mm}:${ss}`)
vals.push(Math.max(0, Math.min(100, p.cpu)))
}
this.cpuHistoryLabels = labels
this.cpuHistoryLong = vals
}
} catch(e) {
} catch (e) {
console.error('Failed to fetch bucketed cpu history', e)
}
},
@ -1129,9 +1267,9 @@
return typeof moment !== 'undefined' ? moment(ts * 1000).format('YYYY-MM-DD HH:mm') : String(ts);
},
customGeoExtDisplay(record) {
const fn = record.type === 'geoip'
? `geoip_${record.alias}.dat`
: `geosite_${record.alias}.dat`;
const fn = record.type === 'geoip' ?
`geoip_${record.alias}.dat` :
`geosite_${record.alias}.dat`;
return `ext:${fn}:tag`;
},
async loadCustomGeo() {
@ -1285,18 +1423,18 @@
const isSingleFile = !!fileName;
this.$confirm({
title: '{{ i18n "pages.index.geofileUpdateDialog" }}',
content: isSingleFile
? '{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName)
: '{{ i18n "pages.index.geofilesUpdateDialogDesc" }}',
content: isSingleFile ?
'{{ i18n "pages.index.geofileUpdateDialogDesc" }}'.replace("#filename#", fileName) :
'{{ i18n "pages.index.geofilesUpdateDialogDesc" }}',
okText: '{{ i18n "confirm"}}',
class: themeSwitcher.currentTheme,
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
versionModal.hide();
this.loading(true, '{{ i18n "pages.index.dontRefresh"}}');
const url = isSingleFile
? `/panel/api/server/updateGeofile/${fileName}`
: `/panel/api/server/updateGeofile`;
const url = isSingleFile ?
`/panel/api/server/updateGeofile/${fileName}` :
`/panel/api/server/updateGeofile`;
await HttpUtil.post(url);
this.loading(false);
},
@ -1320,7 +1458,10 @@
},
async openLogs() {
logModal.loading = true;
const msg = await HttpUtil.post('/panel/api/server/logs/' + logModal.rows, { level: logModal.level, syslog: logModal.syslog });
const msg = await HttpUtil.post('/panel/api/server/logs/' + logModal.rows, {
level: logModal.level,
syslog: logModal.syslog
});
if (!msg.success) {
return;
}
@ -1330,7 +1471,12 @@
},
async openXrayLogs() {
xraylogModal.loading = true;
const msg = await HttpUtil.post('/panel/api/server/xraylogs/' + xraylogModal.rows, { filter: xraylogModal.filter, showDirect: xraylogModal.showDirect, showBlocked: xraylogModal.showBlocked, showProxy: xraylogModal.showProxy });
const msg = await HttpUtil.post('/panel/api/server/xraylogs/' + xraylogModal.rows, {
filter: xraylogModal.filter,
showDirect: xraylogModal.showDirect,
showBlocked: xraylogModal.showBlocked,
showProxy: xraylogModal.showProxy
});
if (!msg.success) {
return;
}
@ -1347,10 +1493,15 @@
try {
const dt = l.DateTime ? new Date(l.DateTime) : null;
const dateStr = dt && !isNaN(dt.getTime()) ? dt.toISOString() : '';
const eventMap = { 0: 'DIRECT', 1: 'BLOCKED', 2: 'PROXY' };
const eventMap = {
0: 'DIRECT',
1: 'BLOCKED',
2: 'PROXY'
};
const eventText = eventMap[l.Event] || String(l.Event ?? '');
const emailPart = l.Email ? ` Email=${l.Email}` : '';
return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`.trim();
return `${dateStr} FROM=${l.FromAddress || ''} TO=${l.ToAddress || ''} INBOUND=${l.Inbound || ''} OUTBOUND=${l.Outbound || ''}${emailPart} EVENT=${eventText}`
.trim();
} catch (e) {
return JSON.stringify(l);
}
@ -1442,7 +1593,7 @@
// Setup WebSocket for real-time updates
if (window.wsClient) {
window.wsClient.connect();
// Listen for status updates
window.wsClient.on('status', (payload) => {
this.setStatus(payload);
@ -1491,4 +1642,4 @@
},
});
</script>
{{ template "page/body_end" .}}
{{ template "page/body_end" .}}

View file

@ -108,8 +108,15 @@
el: '#app',
data: {
themeSwitcher,
loadingStates: { fetched: false, spinning: false },
user: { username: "", password: "", twoFactorCode: "" },
loadingStates: {
fetched: false,
spinning: false
},
user: {
username: "",
password: "",
twoFactorCode: ""
},
twoFactorEnable: false,
lang: "",
animationStarted: false
@ -143,7 +150,11 @@
},
initHeadline() {
const animationDelay = 2000;
const headlines = this.$el.querySelectorAll('.headline');
const rootEl = this.$el instanceof Element ? this.$el : document.getElementById('app');
if (!rootEl || typeof rootEl.querySelectorAll !== 'function') {
return;
}
const headlines = rootEl.querySelectorAll('.headline');
headlines.forEach((headline) => {
const first = headline.querySelector('.is-visible');
if (!first) return;
@ -233,13 +244,18 @@
}
}
});
pm_wait_for_forms.observe(pm_host, { childList: true, subtree: true });
pm_wait_for_forms.observe(pm_host, {
childList: true,
subtree: true
});
}
if (document.readyState === 'loading') {
document.addEventListener('DOMContentLoaded', pm_init, { once: true });
document.addEventListener('DOMContentLoaded', pm_init, {
once: true
});
} else {
pm_init();
}
</script>
{{ template "page/body_end" .}}
{{ template "page/body_end" .}}

View file

@ -1,58 +1,41 @@
{{define "modals/clientsBulkModal"}}
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible"
:title="clientsBulkModal.title"
@ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading"
:closable="true" :mask-closable="false"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }"
:wrapper-col="{ md: {span:14} }">
<a-modal id="client-bulk-modal" v-model="clientsBulkModal.visible" :title="clientsBulkModal.title"
@ok="clientsBulkModal.ok" :confirm-loading="clientsBulkModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-text="clientsBulkModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.client.method" }}'>
<a-select v-model="clientsBulkModal.emailMethod" buttonStyle="solid"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="0">Random</a-select-option>
<a-select-option :value="1">Random+Prefix</a-select-option>
<a-select-option :value="2">Random+Prefix+Num</a-select-option>
<a-select-option
:value="3">Random+Prefix+Num+Postfix</a-select-option>
<a-select-option :value="3">Random+Prefix+Num+Postfix</a-select-option>
<a-select-option :value="4">Prefix+Num+Postfix</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.first" }}'
v-if="clientsBulkModal.emailMethod>1">
<a-input-number v-model.number="clientsBulkModal.firstNum"
:min="1"></a-input-number>
<a-form-item label='{{ i18n "pages.client.first" }}' v-if="clientsBulkModal.emailMethod>1">
<a-input-number v-model.number="clientsBulkModal.firstNum" :min="1"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.last" }}'
v-if="clientsBulkModal.emailMethod>1">
<a-input-number v-model.number="clientsBulkModal.lastNum"
:min="clientsBulkModal.firstNum"></a-input-number>
<a-form-item label='{{ i18n "pages.client.last" }}' v-if="clientsBulkModal.emailMethod>1">
<a-input-number v-model.number="clientsBulkModal.lastNum" :min="clientsBulkModal.firstNum"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.prefix" }}'
v-if="clientsBulkModal.emailMethod>0">
<a-form-item label='{{ i18n "pages.client.prefix" }}' v-if="clientsBulkModal.emailMethod>0">
<a-input v-model.trim="clientsBulkModal.emailPrefix"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.postfix" }}'
v-if="clientsBulkModal.emailMethod>2">
<a-form-item label='{{ i18n "pages.client.postfix" }}' v-if="clientsBulkModal.emailMethod>2">
<a-input v-model.trim="clientsBulkModal.emailPostfix"></a-input>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.clientCount" }}'
v-if="clientsBulkModal.emailMethod < 2">
<a-input-number v-model.number="clientsBulkModal.quantity" :min="1"
:max="500"></a-input-number>
<a-form-item label='{{ i18n "pages.client.clientCount" }}' v-if="clientsBulkModal.emailMethod < 2">
<a-input-number v-model.number="clientsBulkModal.quantity" :min="1" :max="500"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "security" }}'
v-if="inbound.protocol === Protocols.VMESS">
<a-select v-model="clientsBulkModal.security"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-form-item label='{{ i18n "security" }}' v-if="inbound.protocol === Protocols.VMESS">
<a-select v-model="clientsBulkModal.security" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="key in USERS_SECURITY" :value="key">[[
key ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='Flow'
v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
<a-select v-model="clientsBulkModal.flow"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-form-item label='Flow' v-if="clientsBulkModal.inbound.canEnableTlsFlow()">
<a-select v-model="clientsBulkModal.flow" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option value selected>{{ i18n "none"
}}</a-select-option>
<a-select-option v-for="key in TLS_FLOW_CONTROL" :value="key">[[
@ -67,9 +50,7 @@
}}</span>
</template>
Subscription
<a-icon
@click="clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)"
type="sync"></a-icon>
<a-icon @click="clientsBulkModal.subId = RandomUtil.randomLowerAndNum(16)" type="sync"></a-icon>
</a-tooltip>
</template>
<a-input v-model.trim="clientsBulkModal.subId"></a-input>
@ -84,8 +65,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number :style="{ width: '50%' }"
v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
<a-input-number :style="{ width: '50%' }" v-model.number="clientsBulkModal.tgId" min="0"></a-input-number>
</a-form-item>
<a-form-item v-if="app.ipLimitEnable">
<template slot="label">
@ -97,8 +77,7 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="clientsBulkModal.limitIp"
min="0"></a-input-number>
<a-input-number v-model.number="clientsBulkModal.limitIp" min="0"></a-input-number>
</a-form-item>
<a-form-item>
<template slot="label">
@ -110,17 +89,13 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="clientsBulkModal.totalGB"
:min="0"></a-input-number>
<a-input-number v-model.number="clientsBulkModal.totalGB" :min="0"></a-input-number>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.delayedStart" }}'>
<a-switch v-model="clientsBulkModal.delayedStart"
@click="clientsBulkModal.expiryTime=0"></a-switch>
<a-switch v-model="clientsBulkModal.delayedStart" @click="clientsBulkModal.expiryTime=0"></a-switch>
</a-form-item>
<a-form-item label='{{ i18n "pages.client.expireDays" }}'
v-if="clientsBulkModal.delayedStart">
<a-input-number v-model.number="delayedExpireDays"
:min="0"></a-input-number>
<a-form-item label='{{ i18n "pages.client.expireDays" }}' v-if="clientsBulkModal.delayedStart">
<a-input-number v-model.number="delayedExpireDays" :min="0"></a-input-number>
</a-form-item>
<a-form-item v-else>
<template slot="label">
@ -133,15 +108,11 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-date-picker v-if="datepicker == 'gregorian'"
:show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss"
:dropdown-class-name="themeSwitcher.currentTheme"
<a-date-picker v-if="datepicker == 'gregorian'" :show-time="{ format: 'HH:mm:ss' }"
format="YYYY-MM-DD HH:mm:ss" :dropdown-class-name="themeSwitcher.currentTheme"
v-model="clientsBulkModal.expiryTime"></a-date-picker>
<a-persian-datepicker v-else
placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="clientsBulkModal.expiryTime"
v-model="clientsBulkModal.expiryTime">
<a-persian-datepicker v-else placeholder='{{ i18n "pages.settings.datepickerPlaceholder" }}'
value="clientsBulkModal.expiryTime" v-model="clientsBulkModal.expiryTime">
</a-persian-datepicker>
</a-form-item>
<a-form-item v-if="clientsBulkModal.expiryTime != 0">
@ -154,13 +125,11 @@
<a-icon type="question-circle"></a-icon>
</a-tooltip>
</template>
<a-input-number v-model.number="clientsBulkModal.reset"
:min="0"></a-input-number>
<a-input-number v-model.number="clientsBulkModal.reset" :min="0"></a-input-number>
</a-form-item>
</a-form>
</a-modal>
<script>
const clientsBulkModal = {
visible: false,
confirmLoading: false,
@ -219,7 +188,7 @@
title = '',
okText = '{{ i18n "sure" }}',
dbInbound = null,
confirm = (inbound, dbInbound) => { }
confirm = (inbound, dbInbound) => {}
}) {
this.visible = true;
this.title = title;
@ -245,12 +214,19 @@
},
newClient(protocol) {
switch (protocol) {
case Protocols.VMESS: return new Inbound.VmessSettings.VMESS();
case Protocols.VLESS: return new Inbound.VLESSSettings.VLESS();
case Protocols.TROJAN: return new Inbound.TrojanSettings.Trojan();
case Protocols.SHADOWSOCKS: return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings.shadowsockses[0].method);
case Protocols.HYSTERIA: return new Inbound.HysteriaSettings.Hysteria();
default: return null;
case Protocols.VMESS:
return new Inbound.VmessSettings.VMESS();
case Protocols.VLESS:
return new Inbound.VLESSSettings.VLESS();
case Protocols.TROJAN:
return new Inbound.TrojanSettings.Trojan();
case Protocols.SHADOWSOCKS:
return new Inbound.ShadowsocksSettings.Shadowsocks(clientsBulkModal.inbound.settings
.shadowsockses[0].method);
case Protocols.HYSTERIA:
return new Inbound.HysteriaSettings.Hysteria();
default:
return null;
}
},
close() {
@ -271,7 +247,8 @@
return this.clientsBulkModal.inbound;
},
get delayedExpireDays() {
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 : 0;
return this.clientsBulkModal.expiryTime < 0 ? this.clientsBulkModal.expiryTime / -86400000 :
0;
},
get datepicker() {
return app.datepicker;
@ -281,6 +258,5 @@
},
},
});
</script>
{{end}}
{{end}}

View file

@ -1,19 +1,15 @@
{{define "modals/clientsModal"}}
<a-modal id="client-modal" v-model="clientModal.visible"
:title="clientModal.title" @ok="clientModal.ok"
:confirm-loading="clientModal.confirmLoading" :closable="true"
:mask-closable="false"
:class="themeSwitcher.currentTheme"
:ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
<a-modal id="client-modal" v-model="clientModal.visible" :title="clientModal.title" @ok="clientModal.ok"
:confirm-loading="clientModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.currentTheme" :ok-text="clientModal.okText" cancel-text='{{ i18n "close" }}'>
<template v-if="isEdit">
<a-tag v-if="isExpiry || isTrafficExhausted" color="red"
:style="{ marginBottom: '10px', display: 'block', textAlign: 'center' }">Account
is (Expired|Traffic Ended) And Disabled</a-tag>
</template>
{{template "form/client"}}
{{template "form/client" .}}
</a-modal>
<script>
const clientModal = {
visible: false,
confirmLoading: false,
@ -30,12 +26,20 @@
delayedStart: false,
ok() {
if (clientModal.isEdit) {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal.oldClientId);
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id, clientModal
.oldClientId);
} else {
ObjectUtil.execute(clientModal.confirm, clientModalApp.client, clientModal.dbInbound.id);
}
},
show({ title = '', okText = '{{ i18n "sure" }}', index = null, dbInbound = null, confirm = () => { }, isEdit = false }) {
show({
title = '',
okText = '{{ i18n "sure" }}',
index = null,
dbInbound = null,
confirm = () => {},
isEdit = false
}) {
this.visible = true;
this.title = title;
this.okText = okText;
@ -55,30 +59,41 @@
}
this.clientStats = this.dbInbound.clientStats.find(row => row.email === this.clients[this.index].email);
this.confirm = confirm;
},
},
getClientId(protocol, client) {
switch (protocol) {
case Protocols.TROJAN: return client.password;
case Protocols.SHADOWSOCKS: return client.email;
case Protocols.HYSTERIA: return client.auth;
default: return client.id;
case Protocols.TROJAN:
return client.password;
case Protocols.SHADOWSOCKS:
return client.email;
case Protocols.HYSTERIA:
return client.auth;
default:
return client.id;
}
},
addClient(inbound, clients) {
switch (inbound.protocol) {
case Protocols.VMESS: return clients.push(new Inbound.VmessSettings.VMESS());
case Protocols.VLESS: return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN: return clients.push(new Inbound.TrojanSettings.Trojan());
case Protocols.SHADOWSOCKS: return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil.randomShadowsocksPassword(inbound.settings.method)));
case Protocols.HYSTERIA: return clients.push(new Inbound.HysteriaSettings.Hysteria());
default: return null;
case Protocols.VMESS:
return clients.push(new Inbound.VmessSettings.VMESS());
case Protocols.VLESS:
return clients.push(new Inbound.VLESSSettings.VLESS());
case Protocols.TROJAN:
return clients.push(new Inbound.TrojanSettings.Trojan());
case Protocols.SHADOWSOCKS:
return clients.push(new Inbound.ShadowsocksSettings.Shadowsocks(clients[0].method, RandomUtil
.randomShadowsocksPassword(inbound.settings.method)));
case Protocols.HYSTERIA:
return clients.push(new Inbound.HysteriaSettings.Hysteria());
default:
return null;
}
},
close() {
clientModal.visible = false;
clientModal.loading(false);
},
loading(loading=true) {
loading(loading = true) {
clientModal.confirmLoading = loading;
},
};
@ -110,7 +125,8 @@
return true
},
get isExpiry() {
return this.clientModal.isEdit && this.client.expiryTime >0 ? (this.client.expiryTime < new Date().getTime()) : false;
return this.clientModal.isEdit && this.client.expiryTime > 0 ? (this.client.expiryTime <
new Date().getTime()) : false;
},
get delayedStart() {
return this.clientModal.delayedStart;
@ -150,8 +166,7 @@
return;
}
document.getElementById("clientIPs").value = "";
} catch (error) {
}
} catch (error) {}
},
resetClientTraffic(email, dbInboundId, iconElement) {
this.$confirm({
@ -162,7 +177,8 @@
cancelText: '{{ i18n "cancel"}}',
onOk: async () => {
iconElement.disabled = true;
const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' + dbInboundId + '/resetClientTraffic/' + email);
const msg = await HttpUtil.postWithModal('/panel/api/inbounds/' +
dbInboundId + '/resetClientTraffic/' + email);
if (msg.success) {
this.clientModal.clientStats.up = 0;
this.clientModal.clientStats.down = 0;
@ -173,6 +189,5 @@
},
},
});
</script>
{{end}}
{{end}}

View file

@ -5,10 +5,12 @@
<a-list-item v-for="dns in dnsPresetsDatabase" :style="{ padding: '12px 16px' }">
<div class="ant-dns-presets-line">
<a-space direction="horizontal" size="small" align="center">
<a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}' : 'DNS' ]]</a-tag>
<a-tag :color="dns.family ? 'purple' : 'green'">[[ dns.family ? '{{ i18n "pages.xray.dns.dnsPresetFamily" }}'
: 'DNS' ]]</a-tag>
<span class="ant-dns-presets-list-name">[[ dns.name ]]</span>
</a-space>
<a-button class="ant-dns-presets-install" type="primary" @click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
<a-button class="ant-dns-presets-install" type="primary"
@click="dnsPresetsModal.install(dns.data)">{{ i18n "install" }}</a-button>
</div>
</a-list-item>
</a-list>
@ -36,8 +38,7 @@
</style>
<script>
const dnsPresetsDatabase = [
{
const dnsPresetsDatabase = [{
name: 'Google DNS',
family: false,
data: [
@ -96,7 +97,11 @@
install(selectedPreset) {
return ObjectUtil.execute(dnsPresetsModal.selected, selectedPreset);
},
show({ title = '', selected = (selectedPreset) => { }, isEdit = false }) {
show({
title = '',
selected = (selectedPreset) => {},
isEdit = false
}) {
this.title = title;
this.selected = selected;
this.visible = true;

View file

@ -521,7 +521,8 @@
@click="copy(infoModal.wireguardLinks[index])"></a-button>
</a-tooltip>
</tr-info-title>
<code :style="{ display: 'block', whiteSpace: 'normal', wordBreak: 'break-all' }">[[ infoModal.wireguardLinks[index] ]]</code>
<code :style="{ display: 'block', whiteSpace: 'normal', wordBreak: 'break-all' }">[[
infoModal.wireguardLinks[index] ]]</code>
</tr-info-row>
</td>
</tr>
@ -534,7 +535,10 @@
function refreshIPs(email) {
return HttpUtil.post(`/panel/api/inbounds/clientIps/${email}`).then((msg) => {
if (!msg.success) {
return { text: 'No IP Record', array: [] };
return {
text: 'No IP Record',
array: []
};
}
const formatIpRecord = (record) => {
@ -574,7 +578,10 @@
try {
ips = JSON.parse(ips);
} catch (e) {
return { text: String(ips), array: [String(ips)] };
return {
text: String(ips),
array: [String(ips)]
};
}
}
@ -586,20 +593,32 @@
// New format or object array
if (Array.isArray(ips) && ips.length > 0 && typeof ips[0] === 'object') {
const result = ips.map((item) => formatIpRecord(item)).filter(Boolean);
return { text: result.join(' | '), array: result };
return {
text: result.join(' | '),
array: result
};
}
// Old format - simple array of IPs
if (Array.isArray(ips) && ips.length > 0) {
const result = ips.map((ip) => String(ip));
return { text: result.join(', '), array: result };
return {
text: result.join(', '),
array: result
};
}
// Fallback for any other format
return { text: String(ips), array: [String(ips)] };
return {
text: String(ips),
array: [String(ips)]
};
} catch (e) {
return { text: 'Error loading IPs', array: [] };
return {
text: 'Error loading IPs',
array: []
};
}
});
}
@ -626,7 +645,8 @@
this.dbInbound = new DBInbound(dbInbound);
this.clientSettings = this.inbound.clients ? this.inbound.clients[index] : null;
this.isExpired = this.inbound.clients ? this.inbound.isExpiry(index) : this.dbInbound.isExpiry;
this.clientStats = this.inbound.clients ? (this.dbInbound.clientStats.find(row => row.email === this.clientSettings.email) || null) : null;
this.clientStats = this.inbound.clients ? (this.dbInbound.clientStats.find(row => row.email === this
.clientSettings.email) || null) : null;
if (
[
@ -752,7 +772,8 @@
return ColorUtils.usageColor(stats.up + stats.down, app.trafficDiff, stats.total);
},
getRemStats() {
remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats.down;
remained = this.infoModal.clientStats.total - this.infoModal.clientStats.up - this.infoModal.clientStats
.down;
return remained > 0 ? SizeFormatter.sizeFormat(remained) : '-';
},
refreshIPs() {
@ -775,7 +796,7 @@
this.infoModal.clientIps = 'No IP Record';
this.infoModal.clientIpsArray = [];
})
.catch(() => { });
.catch(() => {});
},
},
});

View file

@ -2,7 +2,7 @@
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" :dialog-style="{ top: '20px' }"
@ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
:class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
{{template "form/inbound"}}
{{template "form/inbound" .}}
</a-modal>
<script>
// Make inModal globally available to ensure it works with any base path
@ -23,7 +23,7 @@
okText = '{{ i18n "sure" }}',
inbound = null,
dbInbound = null,
confirm = (inbound, dbInbound) => { },
confirm = (inbound, dbInbound) => {},
isEdit = false,
}) {
this.title = title;
@ -127,17 +127,17 @@
get client() {
return inModal.inbound &&
inModal.inbound.clients &&
inModal.inbound.clients.length > 0
? inModal.inbound.clients[0]
: null;
inModal.inbound.clients.length > 0 ?
inModal.inbound.clients[0] :
null;
},
get datepicker() {
return app.datepicker;
},
get delayedExpireDays() {
return this.client && this.client.expiryTime < 0
? this.client.expiryTime / -86400000
: 0;
return this.client && this.client.expiryTime < 0 ?
this.client.expiryTime / -86400000 :
0;
},
set delayedExpireDays(days) {
this.client.expiryTime = -86400000 * days;
@ -147,14 +147,12 @@
},
set externalProxy(value) {
if (value) {
inModal.inbound.stream.externalProxy = [
{
forceTls: "same",
dest: window.location.hostname,
port: inModal.inbound.port,
remark: "",
},
];
inModal.inbound.stream.externalProxy = [{
forceTls: "same",
dest: window.location.hostname,
port: inModal.inbound.port,
remark: "",
}, ];
} else {
inModal.inbound.stream.externalProxy = [];
}
@ -182,8 +180,8 @@
) {
const hasVisionFlow = inModal.inbound.settings.vlesses.some(
(c) =>
c.flow === "xtls-rprx-vision" ||
c.flow === "xtls-rprx-vision-udp443",
c.flow === "xtls-rprx-vision" ||
c.flow === "xtls-rprx-vision-udp443",
);
if (
hasVisionFlow &&

View file

@ -1,25 +1,31 @@
{{define "modals/nordModal"}}
<a-modal id="nord-modal" v-model="nordModal.visible" title="NordVPN NordLynx"
:confirm-loading="nordModal.confirmLoading" :closable="true" :mask-closable="true"
:footer="null" :class="themeSwitcher.currentTheme">
:confirm-loading="nordModal.confirmLoading" :closable="true" :mask-closable="true" :footer="null"
:class="themeSwitcher.currentTheme">
<template v-if="nordModal.nordData == null">
<a-tabs default-active-key="token" :class="themeSwitcher.currentTheme">
<a-tab-pane key="token" tab='{{ i18n "pages.xray.outbound.accessToken" }}'>
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }"
:style="{ marginTop: '20px' }">
<a-form-item label='{{ i18n "pages.xray.outbound.accessToken" }}'>
<a-input v-model="nordModal.token" placeholder='{{ i18n "pages.xray.outbound.accessToken" }}'></a-input>
<a-input v-model="nordModal.token"
placeholder='{{ i18n "pages.xray.outbound.accessToken" }}'></a-input>
<div :style="{ marginTop: '10px' }">
<a-button type="primary" icon="login" @click="login()" :loading="nordModal.confirmLoading">{{ i18n "login" }}</a-button>
<a-button type="primary" icon="login" @click="login()"
:loading="nordModal.confirmLoading">{{ i18n "login" }}</a-button>
</div>
</a-form-item>
</a-form>
</a-tab-pane>
<a-tab-pane key="key" tab='{{ i18n "pages.xray.outbound.privateKey" }}'>
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '20px' }">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }"
:style="{ marginTop: '20px' }">
<a-form-item label='{{ i18n "pages.xray.outbound.privateKey" }}'>
<a-input v-model="nordModal.manualKey" placeholder='{{ i18n "pages.xray.outbound.privateKey" }}'></a-input>
<a-input v-model="nordModal.manualKey"
placeholder='{{ i18n "pages.xray.outbound.privateKey" }}'></a-input>
<div :style="{ marginTop: '10px' }">
<a-button type="primary" icon="save" @click="saveKey()" :loading="nordModal.confirmLoading">{{ i18n "save" }}</a-button>
<a-button type="primary" icon="save" @click="saveKey()"
:loading="nordModal.confirmLoading">{{ i18n "save" }}</a-button>
</div>
</a-form-item>
</a-form>
@ -39,7 +45,8 @@
</table>
<a-button @click="logout" :loading="nordModal.confirmLoading" type="danger">{{ i18n "logout" }}</a-button>
<a-divider :style="{ margin: '0' }">{{ i18n "pages.xray.outbound.settings" }}</a-divider>
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }" :style="{ marginTop: '10px' }">
<a-form :colon="false" :label-col="{ md: {span:6} }" :wrapper-col="{ md: {span:18} }"
:style="{ marginTop: '10px' }">
<a-form-item label='{{ i18n "pages.xray.outbound.country" }}'>
<a-select v-model="nordModal.countryId" @change="fetchServers" show-search option-filter-prop="label">
<a-select-option v-for="c in nordModal.countries" :key="c.id" :value="c.id" :label="c.name">
@ -69,11 +76,13 @@
<a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<template v-if="nordOutboundIndex>=0">
<a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag>
<a-button @click="resetOutbound" :loading="nordModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button>
<a-button @click="resetOutbound" :loading="nordModal.confirmLoading"
type="danger">{{ i18n "reset" }}</a-button>
</template>
<template v-else>
<a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag>
<a-button @click="addOutbound" :disabled="!nordModal.serverId" :loading="nordModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
<a-button @click="addOutbound" :disabled="!nordModal.serverId" :loading="nordModal.confirmLoading"
type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
</template>
</a-form>
</template>
@ -115,7 +124,9 @@
},
async login() {
this.loading(true);
const msg = await HttpUtil.post('/panel/xray/nord/reg', { token: this.token });
const msg = await HttpUtil.post('/panel/xray/nord/reg', {
token: this.token
});
if (msg.success) {
this.nordData = JSON.parse(msg.obj);
await this.fetchCountries();
@ -124,7 +135,9 @@
},
async saveKey() {
this.loading(true);
const msg = await HttpUtil.post('/panel/xray/nord/setKey', { key: this.manualKey });
const msg = await HttpUtil.post('/panel/xray/nord/setKey', {
key: this.manualKey
});
if (msg.success) {
this.nordData = JSON.parse(msg.obj);
await this.fetchCountries();
@ -160,7 +173,9 @@
this.cities = [];
this.serverId = null;
this.cityId = null;
const msg = await HttpUtil.post('/panel/xray/nord/servers', { countryId: this.countryId });
const msg = await HttpUtil.post('/panel/xray/nord/servers', {
countryId: this.countryId
});
if (msg.success) {
const data = JSON.parse(msg.obj);
const locations = data.locations || [];
@ -173,7 +188,7 @@
}
});
this.cities = Array.from(citiesMap.values()).sort((a, b) => a.name.localeCompare(b.name));
this.servers = (data.servers || []).map(s => {
const firstLocId = (s.location_ids || [])[0];
const city = locToCity[firstLocId];
@ -195,7 +210,7 @@
addOutbound() {
const server = this.servers.find(s => s.id === this.serverId);
if (!server) return;
const tech = server.technologies.find(t => t.id === 35);
const publicKey = tech.metadata.find(m => m.name === 'public_key').value;
@ -221,7 +236,7 @@
resetOutbound(index) {
const server = this.servers.find(s => s.id === this.serverId);
if (!server || index === -1) return;
const tech = server.technologies.find(t => t.id === 35);
const publicKey = tech.metadata.find(m => m.name === 'public_key').value;
@ -242,7 +257,7 @@
}
};
app.templateSettings.outbounds[index] = outbound;
// Sync routing rules
app.templateSettings.routing.rules.forEach(r => {
if (r.outboundTag === oldTag) {
@ -262,7 +277,8 @@
},
delRouting() {
if (app.templateSettings && app.templateSettings.routing) {
app.templateSettings.routing.rules = app.templateSettings.routing.rules.filter(r => !r.outboundTag.startsWith("nord-"));
app.templateSettings.routing.rules = app.templateSettings.routing.rules.filter(r => !r.outboundTag
.startsWith("nord-"));
}
}
};
@ -276,10 +292,14 @@
methods: {
login: () => nordModal.login(),
saveKey: () => nordModal.saveKey(),
logout() { nordModal.logout(this.nordOutboundIndex) },
logout() {
nordModal.logout(this.nordOutboundIndex)
},
fetchServers: () => nordModal.fetchServers(),
addOutbound: () => nordModal.addOutbound(),
resetOutbound() { nordModal.resetOutbound(this.nordOutboundIndex) },
resetOutbound() {
nordModal.resetOutbound(this.nordOutboundIndex)
},
onCityChange() {
if (this.filteredServers.length > 0) {
this.nordModal.serverId = this.filteredServers[0].id;
@ -290,8 +310,9 @@
},
computed: {
nordOutboundIndex: {
get: function () {
return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag.startsWith("nord-")) : -1;
get: function() {
return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag
.startsWith("nord-")) : -1;
}
},
filteredServers: function() {
@ -303,4 +324,4 @@
}
});
</script>
{{end}}
{{end}}

View file

@ -1,17 +1,13 @@
{{define "modals/promptModal"}}
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title"
:closable="true" @ok="promptModal.ok" :mask-closable="false"
:confirm-loading="promptModal.confirmLoading"
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme">
<a-input id="prompt-modal-input" :type="promptModal.type"
v-model="promptModal.value"
:autosize="{minRows: 10, maxRows: 20}"
@keydown.enter.native="promptModal.keyEnter"
@keydown.ctrl.83="promptModal.ctrlS"></a-input>
<a-modal id="prompt-modal" v-model="promptModal.visible" :title="promptModal.title" :closable="true"
@ok="promptModal.ok" :mask-closable="false" :confirm-loading="promptModal.confirmLoading"
:ok-text="promptModal.okText" cancel-text='{{ i18n "cancel" }}' :class="themeSwitcher.currentTheme">
<a-input id="prompt-modal-input" :type="promptModal.type" v-model="promptModal.value"
:autosize="{minRows: 10, maxRows: 20}" @keydown.enter.native="promptModal.keyEnter"
@keydown.ctrl.83="promptModal.ctrlS"></a-input>
</a-modal>
<script>
const promptModal = {
title: '',
type: '',
@ -55,7 +51,7 @@
close() {
this.visible = false;
},
loading(loading=true) {
loading(loading = true) {
this.confirmLoading = loading;
},
};
@ -66,6 +62,5 @@
promptModal: promptModal,
},
});
</script>
{{end}}

View file

@ -57,39 +57,44 @@
border-radius: 1rem;
overflow-x: hidden;
}
/* QR code transition effects */
.qr-cv {
transition: all 0.3s ease-in-out;
}
.qr-transition-enter-active, .qr-transition-leave-active {
.qr-transition-enter-active,
.qr-transition-leave-active {
transition: opacity 0.3s, transform 0.3s;
}
.qr-transition-enter, .qr-transition-leave-to {
.qr-transition-enter,
.qr-transition-leave-to {
opacity: 0;
transform: scale(0.9);
}
.qr-transition-enter-to, .qr-transition-leave {
.qr-transition-enter-to,
.qr-transition-leave {
opacity: 1;
transform: scale(1);
}
.qr-flash {
animation: qr-flash-animation 0.6s;
}
@keyframes qr-flash-animation {
0% {
opacity: 1;
transform: scale(1);
}
50% {
opacity: 0.5;
transform: scale(0.95);
}
100% {
opacity: 1;
transform: scale(1);
@ -105,7 +110,7 @@
qrcodes: [],
visible: false,
subId: '',
show: function (title = '', dbInbound, client) {
show: function(title = '', dbInbound, client) {
this.title = title;
this.dbInbound = dbInbound;
this.inbound = dbInbound.toInbound();
@ -135,7 +140,7 @@
}
this.visible = true;
},
close: function () {
close: function() {
this.visible = false;
},
};
@ -159,7 +164,7 @@
console.error("Failed to get status:", e);
}
},
toggleIPv4(index) {
const row = qrModal.qrcodes[index];
row.useIPv4 = !row.useIPv4;
@ -170,13 +175,13 @@
if (!this.serverStatus || !this.serverStatus.publicIP) {
return;
}
if (row.useIPv4 && this.serverStatus.publicIP.ipv4) {
// Replace the hostname or IP in the link with the IPv4 address
const originalLink = row.originalLink;
const url = new URL(originalLink);
const ipv4 = this.serverStatus.publicIP.ipv4;
if (qrModal.inbound.protocol == Protocols.WIREGUARD) {
// Special handling for WireGuard config
const endpointRegex = /Endpoint = ([^:]+):(\d+)/;
@ -196,19 +201,19 @@
// Restore original link
row.link = row.originalLink;
}
// Update QR code with transition effect
const canvasElement = document.querySelector('#qrCode-' + index);
if (canvasElement) {
// Add flash animation class
canvasElement.classList.add('qr-flash');
// Remove the class after animation completes
setTimeout(() => {
canvasElement.classList.remove('qr-flash');
}, 600);
}
this.setQrCode("qrCode-" + index, row.link);
},
copy(content) {

View file

@ -21,13 +21,13 @@
fileName: '',
qrcode: null,
visible: false,
show: function (title = '', content = '', fileName = '') {
show: function(title = '', content = '', fileName = '') {
this.title = title;
this.content = content;
this.fileName = fileName;
this.visible = true;
},
copy: function (content = '') {
copy: function(content = '') {
ClipboardManager
.copyText(content)
.then(() => {
@ -35,7 +35,7 @@
this.close();
})
},
close: function () {
close: function() {
this.visible = false;
},
};

View file

@ -55,12 +55,12 @@
twoFactorModal.close()
},
show: function ({
show: function({
title = '',
description = '',
token = '',
type = 'set',
confirm = (success) => { }
confirm = (success) => {}
}) {
this.title = title;
this.description = description;
@ -78,7 +78,7 @@
secret: twoFactorModal.token,
});
},
close: function () {
close: function() {
twoFactorModal.enteredCode = "";
twoFactorModal.visible = false;
},
@ -91,34 +91,34 @@
twoFactorModal: twoFactorModal,
},
updated() {
if (
this.twoFactorModal.visible &&
this.twoFactorModal.type === 'set' &&
document.getElementById('twofactor-qrcode')
) {
this.setQrCode('twofactor-qrcode', this.twoFactorModal.totpObject.toString());
}
if (
this.twoFactorModal.visible &&
this.twoFactorModal.type === 'set' &&
document.getElementById('twofactor-qrcode')
) {
this.setQrCode('twofactor-qrcode', this.twoFactorModal.totpObject.toString());
}
},
methods: {
setQrCode(elementId, content) {
new QRious({
element: document.getElementById(elementId),
size: 200,
value: content,
background: 'white',
backgroundAlpha: 0,
foreground: 'black',
padding: 2,
level: 'L'
});
},
copy(content) {
ClipboardManager
.copyText(content)
.then(() => {
app.$message.success('{{ i18n "copied" }}')
})
},
setQrCode(elementId, content) {
new QRious({
element: document.getElementById(elementId),
size: 200,
value: content,
background: 'white',
backgroundAlpha: 0,
foreground: 'black',
padding: 2,
level: 'L'
});
},
copy(content) {
ClipboardManager
.copyText(content)
.then(() => {
app.$message.success('{{ i18n "copied" }}')
})
},
}
});
</script>

View file

@ -1,7 +1,6 @@
{{define "modals/warpModal"}}
<a-modal id="warp-modal" v-model="warpModal.visible" title="Cloudflare WARP"
:confirm-loading="warpModal.confirmLoading" :closable="true" :mask-closable="true"
:footer="null" :class="themeSwitcher.currentTheme">
<a-modal id="warp-modal" v-model="warpModal.visible" title="Cloudflare WARP" :confirm-loading="warpModal.confirmLoading"
:closable="true" :mask-closable="true" :footer="null" :class="themeSwitcher.currentTheme">
<template v-if="ObjectUtil.isEmpty(warpModal.warpData)">
<a-button icon="api" @click="register" :loading="warpModal.confirmLoading">{{ i18n "create" }}</a-button>
</template>
@ -81,11 +80,13 @@
<a-form :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<template v-if="warpOutboundIndex>=0">
<a-tag color="green" :style="{ lineHeight: '31px' }">{{ i18n "enabled" }}</a-tag>
<a-button @click="resetOutbound" :loading="warpModal.confirmLoading" type="danger">{{ i18n "reset" }}</a-button>
<a-button @click="resetOutbound" :loading="warpModal.confirmLoading"
type="danger">{{ i18n "reset" }}</a-button>
</template>
<template v-else>
<a-tag color="orange" :style="{ lineHeight: '31px' }">{{ i18n "disabled" }}</a-tag>
<a-button @click="addOutbound" :loading="warpModal.confirmLoading" type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
<a-button @click="addOutbound" :loading="warpModal.confirmLoading"
type="primary">{{ i18n "pages.xray.outbound.addOutbound" }}</a-button>
</template>
</a-form-item>
</a-form>
@ -93,7 +94,6 @@
</template>
</a-modal>
<script>
const warpModal = {
visible: false,
confirmLoading: false,
@ -188,7 +188,9 @@
},
async updateLicense(l) {
warpModal.loading(true);
const msg = await HttpUtil.post('/panel/xray/warp/license', { license: l });
const msg = await HttpUtil.post('/panel/xray/warp/license', {
license: l
});
if (msg.success) {
warpModal.warpData = JSON.parse(msg.obj);
warpModal.warpConfig = null;
@ -235,12 +237,12 @@
},
computed: {
warpOutboundIndex: {
get: function () {
return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag == 'warp') : -1;
get: function() {
return app.templateSettings ? app.templateSettings.outbounds.findIndex((o) => o.tag ==
'warp') : -1;
}
}
}
});
</script>
{{end}}

View file

@ -1,15 +1,7 @@
{{define "modals/balancerModal"}}
<a-modal
id="balancer-modal"
v-model="balancerModal.visible"
:title="balancerModal.title"
@ok="balancerModal.ok"
:confirm-loading="balancerModal.confirmLoading"
:ok-button-props="{ props: { disabled: !balancerModal.isValid } }"
:closable="true"
:mask-closable="false"
:ok-text="balancerModal.okText"
cancel-text='{{ i18n "close" }}'
<a-modal id="balancer-modal" v-model="balancerModal.visible" :title="balancerModal.title" @ok="balancerModal.ok"
:confirm-loading="balancerModal.confirmLoading" :ok-button-props="{ props: { disabled: !balancerModal.isValid } }"
:closable="true" :mask-closable="false" :ok-text="balancerModal.okText" cancel-text='{{ i18n "close" }}'
:class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.balancer.tag" }}' has-feedback
@ -35,7 +27,8 @@
<a-form-item label="Fallback">
<a-select v-model="balancerModal.balancer.fallbackTag" clearable
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="tag in [ '', ...balancerModal.outboundTags]" :value="tag">[[ tag ]]</a-select-option>
<a-select-option v-for="tag in [ '', ...balancerModal.outboundTags]" :value="tag">[[ tag
]]</a-select-option>
</a-select>
</a-form-item>
</table>
@ -58,7 +51,7 @@
fallbackTag: ''
},
outboundTags: [],
balancerTags:[],
balancerTags: [],
ok() {
if (balancerModal.balancer.selector.length == 0) {
balancerModal.emptySelector = true;
@ -67,7 +60,14 @@
balancerModal.emptySelector = false;
ObjectUtil.execute(balancerModal.confirm, balancerModal.balancer);
},
show({ title = '', okText = '{{ i18n "sure" }}', balancerTags = [], balancer, confirm = (balancer) => { }, isEdit = false }) {
show({
title = '',
okText = '{{ i18n "sure" }}',
balancerTags = [],
balancer,
confirm = (balancer) => {},
isEdit = false
}) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
@ -83,7 +83,8 @@
};
}
this.balancerTags = balancerTags.filter((tag) => tag != balancer.tag);
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj =>
obj.tag);
this.isEdit = isEdit;
this.check();
this.checkSelector();
@ -92,7 +93,7 @@
this.visible = false;
this.loading(false);
},
loading(loading=true) {
loading(loading = true) {
this.confirmLoading = loading;
},
check() {
@ -115,9 +116,7 @@
data: {
balancerModal: balancerModal
},
methods: {
}
methods: {}
});
</script>
{{end}}

View file

@ -75,15 +75,19 @@
okText: '{{ i18n "confirm" }}',
isEdit: false,
confirm: null,
dnsServer: { ...defaultDnsObject },
dnsServer: {
...defaultDnsObject
},
ok() {
ObjectUtil.execute(dnsModal.confirm, { ...dnsModal.dnsServer });
ObjectUtil.execute(dnsModal.confirm, {
...dnsModal.dnsServer
});
},
show({
title = '',
okText = '{{ i18n "confirm" }}',
dnsServer,
confirm = (dnsServer) => { },
confirm = (dnsServer) => {},
isEdit = false
}) {
this.title = title;
@ -95,7 +99,9 @@
if (isEdit) {
switch (typeof dnsServer) {
case 'string':
const dnsObj = { ...defaultDnsObject };
const dnsObj = {
...defaultDnsObject
};
dnsObj.address = dnsServer;
@ -106,7 +112,9 @@
break;
}
} else {
this.dnsServer = { ...defaultDnsObject };
this.dnsServer = {
...defaultDnsObject
};
this.dnsServer.domains = [];
this.dnsServer.expectIPs = [];

View file

@ -23,11 +23,19 @@
okText: '{{ i18n "confirm" }}',
isEdit: false,
confirm: null,
fakeDns: { ...fakednsDefaultData },
fakeDns: {
...fakednsDefaultData
},
ok() {
ObjectUtil.execute(fakednsModal.confirm, fakednsModal.fakeDns);
},
show({ title = '', okText = '{{ i18n "confirm" }}', fakeDns, confirm = (fakeDns) => { }, isEdit = false }) {
show({
title = '',
okText = '{{ i18n "confirm" }}',
fakeDns,
confirm = (fakeDns) => {},
isEdit = false
}) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
@ -35,7 +43,9 @@
if (isEdit) {
this.fakeDns = fakeDns;
} else {
this.fakeDns = { ...fakednsDefaultData }
this.fakeDns = {
...fakednsDefaultData
}
}
this.isEdit = isEdit;
},
@ -51,6 +61,5 @@
fakednsModal: fakednsModal,
}
});
</script>
{{end}}

View file

@ -1,12 +1,11 @@
{{define "modals/outModal"}}
<a-modal id="out-modal" v-model="outModal.visible" :title="outModal.title" @ok="outModal.ok"
:confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-button-props="{ props: { disabled: !outModal.isValid } }" :style="{ overflow: 'hidden' }"
:ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
{{template "form/outbound"}}
:confirm-loading="outModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-button-props="{ props: { disabled: !outModal.isValid } }" :style="{ overflow: 'hidden' }"
:ok-text="outModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
{{template "form/outbound" .}}
</a-modal>
<script>
const outModal = {
title: '',
visible: false,
@ -25,7 +24,14 @@
ok() {
ObjectUtil.execute(outModal.confirm, outModal.outbound.toJson());
},
show({ title='', okText='{{ i18n "sure" }}', outbound, confirm=(outbound)=>{}, isEdit=false, tags=[] }) {
show({
title = '',
okText = '{{ i18n "sure" }}',
outbound,
confirm = (outbound) => {},
isEdit = false,
tags = []
}) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
@ -42,11 +48,11 @@
outModal.visible = false;
outModal.loading(false);
},
loading(loading=true) {
loading(loading = true) {
outModal.confirmLoading = loading;
},
check(){
if(outModal.outbound.tag == '' || outModal.tags.includes(outModal.outbound.tag)){
check() {
if (outModal.outbound.tag == '' || outModal.tags.includes(outModal.outbound.tag)) {
this.duplicateTag = true;
this.isValid = false;
} else {
@ -56,25 +62,25 @@
},
toggleJson(jsonTab) {
textAreaObj = document.getElementById('outboundJson');
if(jsonTab){
if(this.cm != null) {
this.cm.toTextArea();
this.cm=null;
if (jsonTab) {
if (this.cm != null) {
this.cm.toTextArea();
this.cm = null;
}
textAreaObj.value = JSON.stringify(this.outbound.toJson(), null, 2);
this.cm = CodeMirror.fromTextArea(textAreaObj, app.cmOptions);
this.cm.on('change',editor => {
this.cm.on('change', editor => {
value = editor.getValue();
if(this.isJsonString(value)){
if (this.isJsonString(value)) {
this.outbound = Outbound.fromJson(JSON.parse(value));
this.check();
}
});
this.activeKey = '2';
} else {
if(this.cm != null) {
this.cm.toTextArea();
this.cm=null;
if (this.cm != null) {
this.cm.toTextArea();
this.cm = null;
}
this.activeKey = '1';
}
@ -100,20 +106,21 @@
},
methods: {
streamNetworkChange() {
if (this.outModal.outbound.protocol == Protocols.VLESS && !outModal.outbound.canEnableTlsFlow()) {
if (this.outModal.outbound.protocol == Protocols.VLESS && !outModal.outbound
.canEnableTlsFlow()) {
delete this.outModal.outbound.settings.flow;
}
},
canEnableTls() {
return this.outModal.outbound.canEnableTls();
},
convertLink(){
convertLink() {
newOutbound = Outbound.fromLink(outModal.link);
if(newOutbound){
if (newOutbound) {
this.outModal.outbound = newOutbound;
this.outModal.toggleJson(true);
this.outModal.check();
this.$message.success('Link imported successfully...');
this.$message.success('Link imported successfully...');
outModal.link = '';
} else {
this.$message.error('Wrong Link!');
@ -122,6 +129,5 @@
},
},
});
</script>
{{end}}
{{end}}

View file

@ -1,7 +1,7 @@
{{define "modals/reverseModal"}}
<a-modal id="reverse-modal" v-model="reverseModal.visible" :title="reverseModal.title" @ok="reverseModal.ok"
:confirm-loading="reverseModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-text="reverseModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
:confirm-loading="reverseModal.confirmLoading" :closable="true" :mask-closable="false"
:ok-text="reverseModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item label='{{ i18n "pages.xray.outbound.type" }}'>
<a-select v-model="reverseModal.reverse.type" :dropdown-class-name="themeSwitcher.currentTheme">
@ -15,26 +15,24 @@
<a-input v-model.trim="reverseModal.reverse.domain"></a-input>
</a-form-item>
<template v-if="reverseModal.reverse.type=='bridge'">
<a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'>
<a-select v-model="reverseModal.rules[0].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.rules.outbound" }}'>
<a-select v-model="reverseModal.rules[1].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'>
<a-select v-model="reverseModal.rules[0].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.rules.outbound" }}'>
<a-select v-model="reverseModal.rules[1].outboundTag" :dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option v-for="x in reverseModal.outboundTags" :value="x">[[ x ]]</a-select-option>
</a-select>
</a-form-item>
</template>
<template v-else>
<a-form-item label='{{ i18n "pages.xray.outbound.intercon" }}'>
<a-checkbox-group
v-model="reverseModal.rules[0].inboundTag"
<a-checkbox-group v-model="reverseModal.rules[0].inboundTag"
:options="reverseModal.inboundTags"></a-checkbox-group>
</a-form-item>
<a-form-item label='{{ i18n "pages.xray.rules.inbound" }}'>
<a-checkbox-group
v-model="reverseModal.rules[1].inboundTag"
<a-checkbox-group v-model="reverseModal.rules[1].inboundTag"
:options="reverseModal.inboundTags"></a-checkbox-group>
</a-form-item>
</template>
@ -53,9 +51,14 @@
type: "",
domain: ""
},
rules: [
{ outboundTag: '', inboundTag: []},
{ outboundTag: '', inboundTag: []}
rules: [{
outboundTag: '',
inboundTag: []
},
{
outboundTag: '',
inboundTag: []
}
],
inboundTags: [],
outboundTags: [],
@ -64,7 +67,7 @@
reverseModal.rules[0].type = 'field';
reverseModal.rules[1].type = 'field';
if(reverseModal.reverse.type == 'bridge'){
if (reverseModal.reverse.type == 'bridge') {
reverseModal.rules[0].inboundTag = [reverseModal.reverse.tag];
reverseModal.rules[1].inboundTag = [reverseModal.reverse.tag];
} else {
@ -73,22 +76,36 @@
}
ObjectUtil.execute(reverseModal.confirm, reverseModal.reverse, reverseModal.rules);
},
show({ title='', okText='{{ i18n "sure" }}', reverse, rules, confirm=(reverse, rules)=>{}, isEdit=false }) {
show({
title = '',
okText = '{{ i18n "sure" }}',
reverse,
rules,
confirm = (reverse, rules) => {},
isEdit = false
}) {
this.title = title;
this.okText = okText;
this.confirm = confirm;
this.visible = true;
if(isEdit) {
if (isEdit) {
this.reverse = {
tag: reverse.tag,
type: reverse.type,
domain: reverse.domain,
};
reverse;
reverse;
rules0 = rules.filter(r => r.domain != null);
if(rules0.length == 0) rules0 = [{ outboundTag: '', domain: ["full:" + this.reverse.domain], inboundTag: []}];
if (rules0.length == 0) rules0 = [{
outboundTag: '',
domain: ["full:" + this.reverse.domain],
inboundTag: []
}];
rules1 = rules.filter(r => r.domain == null);
if(rules1.length == 0) rules1 = [{ outboundTag: '', inboundTag: []}];
if (rules1.length == 0) rules1 = [{
outboundTag: '',
inboundTag: []
}];
this.rules = [];
this.rules.push({
domain: rules0[0].domain,
@ -105,22 +122,29 @@
type: "bridge",
domain: "reverse.xui"
}
this.rules = [
{ outboundTag: '', inboundTag: []},
{ outboundTag: '', inboundTag: []}
this.rules = [{
outboundTag: '',
inboundTag: []
},
{
outboundTag: '',
inboundTag: []
}
]
}
this.isEdit = isEdit;
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj =>
obj.tag);
this.inboundTags.push(...app.inboundTags);
if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag);
this.outboundTags = app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj =>
obj.tag);
},
close() {
reverseModal.visible = false;
reverseModal.loading(false);
},
loading(loading=true) {
loading(loading = true) {
reverseModal.confirmLoading = loading;
},
};
@ -130,9 +154,11 @@
el: '#reverse-modal',
data: {
reverseModal: reverseModal,
reverseTypes: { bridge: '{{ i18n "pages.xray.outbound.bridge" }}', portal:'{{ i18n "pages.xray.outbound.portal" }}'},
reverseTypes: {
bridge: '{{ i18n "pages.xray.outbound.bridge" }}',
portal: '{{ i18n "pages.xray.outbound.portal" }}'
},
},
});
</script>
{{end}}
{{end}}

View file

@ -1,5 +1,7 @@
{{define "modals/ruleModal"}}
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok"
:confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText"
cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
<a-form-item>
<template slot="label">
@ -42,15 +44,19 @@
</a-select>
</a-form-item>
<a-form-item label='Attributes'>
<a-button icon="plus" size="small" :style="{ marginLeft: '10px' }" @click="ruleModal.rule.attrs.push(['', ''])"></a-button>
<a-button icon="plus" size="small" :style="{ marginLeft: '10px' }"
@click="ruleModal.rule.attrs.push(['', ''])"></a-button>
</a-form-item>
<a-form-item :wrapper-col="{span: 24}">
<a-input-group compact v-for="(attr,index) in ruleModal.rule.attrs">
<a-input :style="{ width: '50%' }" v-model="attr[0]" placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<a-input :style="{ width: '50%' }" v-model="attr[0]"
placeholder='{{ i18n "pages.inbounds.stream.general.name" }}'>
<template slot="addonBefore" :style="{ margin: '0' }">[[ index+1 ]]</template>
</a-input>
<a-input :style="{ width: '50%' }" v-model="attr[1]" placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button icon="minus" slot="addonAfter" size="small" @click="ruleModal.rule.attrs.splice(index,1)"></a-button>
<a-input :style="{ width: '50%' }" v-model="attr[1]"
placeholder='{{ i18n "pages.inbounds.stream.general.value" }}'>
<a-button icon="minus" slot="addonAfter" size="small"
@click="ruleModal.rule.attrs.splice(index,1)"></a-button>
</a-input>
</a-input-group>
</a-form-item>
@ -196,16 +202,20 @@
this.inboundTags = app.templateSettings.inbounds.filter((i) => !ObjectUtil.isEmpty(i.tag)).map(obj => obj.tag);
this.inboundTags.push(...app.inboundTags);
if (app.enableDNS && !ObjectUtil.isEmpty(app.dnsTag)) this.inboundTags.push(app.dnsTag)
this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
this.outboundTags = ["", ...app.templateSettings.outbounds.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj =>
obj.tag)];
if (app.templateSettings.reverse) {
if (app.templateSettings.reverse.bridges) {
this.inboundTags.push(...app.templateSettings.reverse.bridges.map(b => b.tag));
}
if (app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(b => b.tag));
if (app.templateSettings.reverse.portals) this.outboundTags.push(...app.templateSettings.reverse.portals.map(
b => b.tag));
}
this.balancerTags = [""];
if (app.templateSettings.routing && app.templateSettings.routing.balancers) {
this.balancerTags = ["", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag)).map(obj => obj.tag)];
this.balancerTags = ["", ...app.templateSettings.routing.balancers.filter((o) => !ObjectUtil.isEmpty(o.tag))
.map(obj => obj.tag)
];
}
},
close() {
@ -234,7 +244,8 @@
rule.outboundTag = value.outboundTag == "" ? undefined : value.outboundTag;
rule.balancerTag = value.balancerTag == "" ? undefined : value.balancerTag;
for (const [key, value] of Object.entries(rule)) {
if (value !== null && value !== undefined && !(Array.isArray(value) && value.length === 0) && !(typeof value === 'object' && Object.keys(value).length === 0) && value !== '') {
if (value !== null && value !== undefined && !(Array.isArray(value) && value.length === 0) && !(
typeof value === 'object' && Object.keys(value).length === 0) && value !== '') {
newRule[key] = value;
}
}
@ -249,4 +260,4 @@
}
});
</script>
{{end}}
{{end}}

View file

@ -79,7 +79,8 @@
</template>
{{ template "settings/panel/subscription/general" . }}
</a-tab-pane>
<a-tab-pane key="5" v-if="allSetting.subJsonEnable || allSetting.subClashEnable" :style="{ paddingTop: '20px' }">
<a-tab-pane key="5" v-if="allSetting.subJsonEnable || allSetting.subClashEnable"
:style="{ paddingTop: '20px' }">
<template #tab>
<a-icon type="code"></a-icon>
<span>{{ i18n "pages.settings.subSettings" }} (Formats)</span>
@ -102,7 +103,7 @@
{{template "component/aSidebar" .}}
{{template "component/aThemeSwitch" .}}
{{template "component/aSettingListItem" .}}
{{template "modals/twoFactorModal"}}
{{template "modals/twoFactorModal" .}}
<script>
const app = new Vue({
delimiters: ['[[', ']]'],
@ -124,9 +125,19 @@
user: {},
lang: LanguageManager.getLanguage(),
inboundOptions: [],
remarkModels: { i: 'Inbound', e: 'Email', o: 'Other' },
remarkModels: {
i: 'Inbound',
e: 'Email',
o: 'Other'
},
remarkSeparators: [' ', '-', '_', '@', ':', '~', '|', ',', '.', '/'],
datepickerList: [{ name: 'Gregorian (Standard)', value: 'gregorian' }, { name: 'Jalalian (شمسی)', value: 'jalalian' }],
datepickerList: [{
name: 'Gregorian (Standard)',
value: 'gregorian'
}, {
name: 'Jalalian (شمسی)',
value: 'jalalian'
}],
remarkSample: '',
defaultFragment: {
packets: "tlshello",
@ -134,17 +145,19 @@
interval: "10-20",
maxSplit: "300-400"
},
defaultNoises: [
{ type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" }
],
defaultNoises: [{
type: "rand",
packet: "10-20",
delay: "10-16",
applyTo: "ip"
}],
defaultMux: {
enabled: true,
concurrency: 8,
xudpConcurrency: 16,
xudpProxyUDP443: "reject"
},
defaultRules: [
{
defaultRules: [{
type: "field",
outboundTag: "direct",
domain: [
@ -160,26 +173,75 @@
]
},
],
directIPsOptions: [
{ label: 'Private IP', value: 'geoip:private' },
{ label: '🇮🇷 Iran', value: 'geoip:ir' },
{ label: '🇨🇳 China', value: 'geoip:cn' },
{ label: '🇷🇺 Russia', value: 'geoip:ru' },
{ label: '🇻🇳 Vietnam', value: 'geoip:vn' },
{ label: '🇪🇸 Spain', value: 'geoip:es' },
{ label: '🇮🇩 Indonesia', value: 'geoip:id' },
{ label: '🇺🇦 Ukraine', value: 'geoip:ua' },
{ label: '🇹🇷 Türkiye', value: 'geoip:tr' },
{ label: '🇧🇷 Brazil', value: 'geoip:br' },
directIPsOptions: [{
label: 'Private IP',
value: 'geoip:private'
},
{
label: '🇮🇷 Iran',
value: 'geoip:ir'
},
{
label: '🇨🇳 China',
value: 'geoip:cn'
},
{
label: '🇷🇺 Russia',
value: 'geoip:ru'
},
{
label: '🇻🇳 Vietnam',
value: 'geoip:vn'
},
{
label: '🇪🇸 Spain',
value: 'geoip:es'
},
{
label: '🇮🇩 Indonesia',
value: 'geoip:id'
},
{
label: '🇺🇦 Ukraine',
value: 'geoip:ua'
},
{
label: '🇹🇷 Türkiye',
value: 'geoip:tr'
},
{
label: '🇧🇷 Brazil',
value: 'geoip:br'
},
],
diretDomainsOptions: [
{ label: 'Private DNS', value: 'geosite:private' },
{ label: '🇮🇷 Iran', value: 'geosite:category-ir' },
{ label: '🇨🇳 China', value: 'geosite:cn' },
{ label: '🇷🇺 Russia', value: 'geosite:category-ru' },
{ label: 'Apple', value: 'geosite:apple' },
{ label: 'Meta', value: 'geosite:meta' },
{ label: 'Google', value: 'geosite:google' },
diretDomainsOptions: [{
label: 'Private DNS',
value: 'geosite:private'
},
{
label: '🇮🇷 Iran',
value: 'geosite:category-ir'
},
{
label: '🇨🇳 China',
value: 'geosite:cn'
},
{
label: '🇷🇺 Russia',
value: 'geosite:category-ru'
},
{
label: 'Apple',
value: 'geosite:apple'
},
{
label: 'Meta',
value: 'geosite:meta'
},
{
label: 'Google',
value: 'geosite:google'
},
],
get remarkModel() {
rm = this.allSetting.remarkModel;
@ -317,7 +379,13 @@
this.loading(true);
await PromiseUtil.sleep(5000);
const { webDomain, webPort, webBasePath, webCertFile, webKeyFile } = this.allSetting;
const {
webDomain,
webPort,
webBasePath,
webCertFile,
webKeyFile
} = this.allSetting;
const newProtocol = (webCertFile || webKeyFile) ? "https:" : "http:";
let base = webBasePath ? webBasePath.replace(/^\//, "") : "";
@ -358,7 +426,8 @@
type: 'set',
confirm: (success) => {
if (success) {
Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalSetSuccess" }}')
Vue.prototype.$message['success'](
'{{ i18n "pages.settings.security.twoFactorModalSetSuccess" }}')
this.allSetting.twoFactorToken = newTwoFactorToken
}
@ -374,7 +443,8 @@
type: 'confirm',
confirm: (success) => {
if (success) {
Vue.prototype.$message['success']('{{ i18n "pages.settings.security.twoFactorModalDeleteSuccess" }}')
Vue.prototype.$message['success'](
'{{ i18n "pages.settings.security.twoFactorModalDeleteSuccess" }}')
this.allSetting.twoFactorEnable = false
this.allSetting.twoFactorToken = ""
@ -384,7 +454,12 @@
}
},
addNoise() {
const newNoise = { type: "rand", packet: "10-20", delay: "10-16", applyTo: "ip" };
const newNoise = {
type: "rand",
packet: "10-20",
delay: "10-16",
applyTo: "ip"
};
this.noisesArray = [...this.noisesArray, newNoise];
},
removeNoise(index) {
@ -394,44 +469,60 @@
},
updateNoiseType(index, value) {
const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], type: value };
updatedNoises[index] = {
...updatedNoises[index],
type: value
};
this.noisesArray = updatedNoises;
},
updateNoisePacket(index, value) {
const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], packet: value };
updatedNoises[index] = {
...updatedNoises[index],
packet: value
};
this.noisesArray = updatedNoises;
},
updateNoiseDelay(index, value) {
const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], delay: value };
updatedNoises[index] = {
...updatedNoises[index],
delay: value
};
this.noisesArray = updatedNoises;
},
updateNoiseApplyTo(index, value) {
const updatedNoises = [...this.noisesArray];
updatedNoises[index] = { ...updatedNoises[index], applyTo: value };
updatedNoises[index] = {
...updatedNoises[index],
applyTo: value
};
this.noisesArray = updatedNoises;
},
},
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(',') : '';
}
},
fragment: {
get: function () { return this.allSetting?.subJsonFragment != ""; },
set: function (v) {
get: function() {
return this.allSetting?.subJsonFragment != "";
},
set: function(v) {
this.allSetting.subJsonFragment = v ? JSON.stringify(this.defaultFragment) : "";
}
},
fragmentPackets: {
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).packets : ""; },
set: function (v) {
get: function() {
return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).packets : "";
},
set: function(v) {
if (v != "") {
newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.packets = v;
@ -440,8 +531,10 @@
}
},
fragmentLength: {
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).length : ""; },
set: function (v) {
get: function() {
return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).length : "";
},
set: function(v) {
if (v != "") {
newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.length = v;
@ -450,8 +543,10 @@
}
},
fragmentInterval: {
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).interval : ""; },
set: function (v) {
get: function() {
return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).interval : "";
},
set: function(v) {
if (v != "") {
newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.interval = v;
@ -460,8 +555,10 @@
}
},
fragmentMaxSplit: {
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).maxSplit : ""; },
set: function (v) {
get: function() {
return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).maxSplit : "";
},
set: function(v) {
if (v != "") {
newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.maxSplit = v;
@ -492,50 +589,60 @@
}
},
enableMux: {
get: function () { return this.allSetting?.subJsonMux != ""; },
set: function (v) {
get: function() {
return this.allSetting?.subJsonMux != "";
},
set: function(v) {
this.allSetting.subJsonMux = v ? JSON.stringify(this.defaultMux) : "";
}
},
muxConcurrency: {
get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).concurrency : -1; },
set: function (v) {
get: function() {
return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).concurrency : -1;
},
set: function(v) {
newMux = JSON.parse(this.allSetting.subJsonMux);
newMux.concurrency = v;
this.allSetting.subJsonMux = JSON.stringify(newMux);
}
},
muxXudpConcurrency: {
get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpConcurrency : -1; },
set: function (v) {
get: function() {
return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpConcurrency : -1;
},
set: function(v) {
newMux = JSON.parse(this.allSetting.subJsonMux);
newMux.xudpConcurrency = v;
this.allSetting.subJsonMux = JSON.stringify(newMux);
}
},
muxXudpProxyUDP443: {
get: function () { return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpProxyUDP443 : "reject"; },
set: function (v) {
get: function() {
return this.enableMux ? JSON.parse(this.allSetting.subJsonMux).xudpProxyUDP443 : "reject";
},
set: function(v) {
newMux = JSON.parse(this.allSetting.subJsonMux);
newMux.xudpProxyUDP443 = v;
this.allSetting.subJsonMux = JSON.stringify(newMux);
}
},
enableDirect: {
get: function () { return this.allSetting?.subJsonRules != ""; },
set: function (v) {
get: function() {
return this.allSetting?.subJsonRules != "";
},
set: function(v) {
this.allSetting.subJsonRules = v ? JSON.stringify(this.defaultRules) : "";
}
},
directIPs: {
get: function () {
get: function() {
if (!this.enableDirect) return [];
const rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return [];
const ipRule = rules.find(r => r.ip);
return ipRule?.ip ?? [];
},
set: function (v) {
set: function(v) {
let rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return;
@ -554,14 +661,14 @@
}
},
directDomains: {
get: function () {
get: function() {
if (!this.enableDirect) return [];
const rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return [];
const domainRule = rules.find(r => r.domain);
return domainRule?.domain ?? [];
},
set: function (v) {
set: function(v) {
let rules = JSON.parse(this.allSetting.subJsonRules);
if (!Array.isArray(rules)) return;
if (v.length == 0) {
@ -576,7 +683,7 @@
}
},
confAlerts: {
get: function () {
get: function() {
if (!this.allSetting) return [];
var alerts = []
if (window.location.protocol !== "https:") alerts.push('{{ i18n "secAlertSSL" }}');
@ -584,11 +691,13 @@
panelPath = window.location.pathname.split('/').length < 4
if (panelPath && this.allSetting.webBasePath == '/') alerts.push('{{ i18n "secAlertPanelURI" }}');
if (this.allSetting.subEnable) {
subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this.allSetting.subPath;
subPath = this.allSetting.subURI.length > 0 ? new URL(this.allSetting.subURI).pathname : this
.allSetting.subPath;
if (subPath == '/sub/') alerts.push('{{ i18n "secAlertSubURI" }}');
}
if (this.allSetting.subJsonEnable) {
subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname : this.allSetting.subJsonPath;
subJsonPath = this.allSetting.subJsonURI.length > 0 ? new URL(this.allSetting.subJsonURI).pathname :
this.allSetting.subJsonPath;
if (subJsonPath == '/json/') alerts.push('{{ i18n "secAlertSubJsonURI" }}');
}
return alerts

View file

@ -124,6 +124,13 @@
v-model="allSetting.externalTrafficInformURI"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.settings.restartXrayOnClientDisable"}}</template>
<template #description>{{ i18n "pages.settings.restartXrayOnClientDisableDesc"}}</template>
<template #control>
<a-switch v-model="allSetting.restartXrayOnClientDisable"></a-switch>
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel key="5" header='{{ i18n "pages.settings.dateAndTime" }}'>
<a-setting-list-item paddings="small">
@ -162,7 +169,8 @@
<a-setting-list-item paddings="small">
<template #title>LDAP Port</template>
<template #control>
<a-input-number :min="1" :max="65535" v-model="allSetting.ldapPort" :style="{ width: '100%' }"></a-input-number>
<a-input-number :min="1" :max="65535" v-model="allSetting.ldapPort"
:style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
@ -239,10 +247,13 @@
<template #title>Inbound tags</template>
<template #description>Select inbounds to manage (auto create/delete)</template>
<template #control>
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }" v-model="ldapInboundTagList">
<a-select-option v-for="opt in inboundOptions" :key="opt.value" :value="opt.value">[[ opt.label ]]</a-select-option>
<a-select mode="multiple" :dropdown-class-name="themeSwitcher.currentTheme" :style="{ width: '100%' }"
v-model="ldapInboundTagList">
<a-select-option v-for="opt in inboundOptions" :key="opt.value" :value="opt.value">[[ opt.label
]]</a-select-option>
</a-select>
<div v-if="inboundOptions.length==0" style="margin-top:6px;color:#999">No inbounds found. Please create one in Inbounds.</div>
<div v-if="inboundOptions.length==0" style="margin-top:6px;color:#999">No inbounds found. Please create
one in Inbounds.</div>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
@ -260,19 +271,22 @@
<a-setting-list-item paddings="small">
<template #title>Default total (GB)</template>
<template #control>
<a-input-number :min="0" v-model="allSetting.ldapDefaultTotalGB" :style="{ width: '100%' }"></a-input-number>
<a-input-number :min="0" v-model="allSetting.ldapDefaultTotalGB"
:style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Default expiry (days)</template>
<template #control>
<a-input-number :min="0" v-model="allSetting.ldapDefaultExpiryDays" :style="{ width: '100%' }"></a-input-number>
<a-input-number :min="0" v-model="allSetting.ldapDefaultExpiryDays"
:style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>Default Limit IP</template>
<template #control>
<a-input-number :min="0" v-model="allSetting.ldapDefaultLimitIP" :style="{ width: '100%' }"></a-input-number>
<a-input-number :min="0" v-model="allSetting.ldapDefaultLimitIP"
:style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
</a-collapse-panel>

View file

@ -46,8 +46,7 @@
<template #description>{{ i18n
"pages.settings.subPortDesc"}}</template>
<template #control>
<a-input-number v-model="allSetting.subPort" :min="1"
:min="65535"
<a-input-number v-model="allSetting.subPort" :min="1" :min="65535"
:style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
@ -67,8 +66,7 @@
<template #description>{{ i18n
"pages.settings.subURIDesc"}}</template>
<template #control>
<a-input type="text"
placeholder="(http|https)://domain[:port]/path/"
<a-input type="text" placeholder="(http|https)://domain[:port]/path/"
v-model="allSetting.subURI"></a-input>
</template>
</a-setting-list-item>
@ -104,8 +102,7 @@
<template #description>{{ i18n
"pages.settings.subSupportUrlDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subSupportUrl"
placeholder="https://example.com"></a-input>
<a-input type="text" v-model="allSetting.subSupportUrl" placeholder="https://example.com"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
@ -113,8 +110,7 @@
<template #description>{{ i18n
"pages.settings.subProfileUrlDesc"}}</template>
<template #control>
<a-input type="text" v-model="allSetting.subProfileUrl"
placeholder="https://example.com"></a-input>
<a-input type="text" v-model="allSetting.subProfileUrl" placeholder="https://example.com"></a-input>
</template>
</a-setting-list-item>
<a-setting-list-item paddings="small">
@ -141,8 +137,7 @@
<template #description>{{ i18n
"pages.settings.subRoutingRulesDesc"}}</template>
<template #control>
<a-textarea v-model="allSetting.subRoutingRules"
placeholder="happ://routing/add/..."></a-textarea>
<a-textarea v-model="allSetting.subRoutingRules" placeholder="happ://routing/add/..."></a-textarea>
</template>
</a-setting-list-item>
</a-collapse-panel>
@ -170,8 +165,7 @@
<template #description>{{ i18n
"pages.settings.subUpdatesDesc"}}</template>
<template #control>
<a-input-number :min="1" v-model="allSetting.subUpdates"
:style="{ width: '100%' }"></a-input-number>
<a-input-number :min="1" v-model="allSetting.subUpdates" :style="{ width: '100%' }"></a-input-number>
</template>
</a-setting-list-item>
</a-collapse-panel>

View file

@ -100,7 +100,8 @@
<a-form-item>
<a-space direction="vertical" align="center">
<a-row type="flex" :gutter="[8,8]" justify="center" style="width:100%">
<a-col :xs="24" :sm="app.subJsonUrl || app.subClashUrl ? 12 : 24" style="text-align:center;">
<a-col :xs="24" :sm="app.subJsonUrl || app.subClashUrl ? 12 : 24"
style="text-align:center;">
<tr-qr-box class="qr-box">
<a-tag color="purple" class="qr-tag">
<span>{{ i18n
@ -152,7 +153,10 @@
app.sId
]]</a-descriptions-item>
<a-descriptions-item label='{{ i18n "subscription.status" }}'>
<template v-if="isUnlimited">
<template v-if="!app.enabled">
<a-tag color="red">{{ i18n "subscription.inactive" }}</a-tag>
</template>
<template v-else-if="isUnlimited">
<a-tag color="purple">{{ i18n
"subscription.unlimited" }}</a-tag>
</template>
@ -270,11 +274,11 @@
</a-layout>
<!-- Bootstrap data for external JS -->
<template id="subscription-data" data-sid="{{ .sId }}" data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}" data-subclash-url="{{ .subClashUrl }}"
data-download="{{ .download }}" data-upload="{{ .upload }}" data-used="{{ .used }}" data-total="{{ .total }}"
data-remained="{{ .remained }}" data-expire="{{ .expire }}" data-lastonline="{{ .lastOnline }}"
data-downloadbyte="{{ .downloadByte }}" data-uploadbyte="{{ .uploadByte }}" data-totalbyte="{{ .totalByte }}"
data-datepicker="{{ .datepicker }}"></template>
<template id="subscription-data" data-sid="{{ .sId }}" data-sub-url="{{ .subUrl }}" data-subjson-url="{{ .subJsonUrl }}"
data-subclash-url="{{ .subClashUrl }}" data-download="{{ .download }}" data-upload="{{ .upload }}"
data-used="{{ .used }}" data-total="{{ .total }}" data-remained="{{ .remained }}" data-expire="{{ .expire }}"
data-lastonline="{{ .lastOnline }}" data-downloadbyte="{{ .downloadByte }}" data-uploadbyte="{{ .uploadByte }}"
data-totalbyte="{{ .totalByte }}" data-datepicker="{{ .datepicker }}" data-enabled="{{ .enabled }}"></template>
<textarea id="subscription-links" style="display:none">{{ range .result }}{{ . }}
{{ end }}</textarea>

View file

@ -5,7 +5,8 @@
<span>{{ i18n "pages.xray.balancer.addBalancer"}}</span>
</a-button>
<a-table :columns="balancerColumns" bordered :row-key="r => r.key" :data-source="balancersData"
:scroll="isMobile ? {} : { x: 200 }" :pagination="false" :indent-size="0" :locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'>
:scroll="isMobile ? {} : { x: 200 }" :pagination="false" :indent-size="0"
:locale='{ filterConfirm: `{{ i18n "confirm" }}`, filterReset: `{{ i18n "reset" }}` }'>
<template slot="action" slot-scope="text, balancer, index">
<span>[[ index+1 ]]</span>
<a-dropdown :trigger="['click']">
@ -18,7 +19,7 @@
</a-menu-item>
<a-menu-item @click="deleteBalancer(index)">
<span :style="{ color: '#FF4D4F' }">
<a-icon type="delete"></a-icon>
<a-icon type="delete"></a-icon>
<span>{{ i18n "delete"}}</span>
</span>
</a-menu-item>
@ -32,7 +33,8 @@
<a-tag :style="{ margin: '0' }" v-if="balancer.strategy=='leastPing'" color="green">Least Ping</a-tag>
</template>
<template slot="selector" slot-scope="text, balancer, index">
<a-tag class="info-large-tag" :style="{ margin: '1' }" v-for="sel in balancer.selector">[[ sel ]]</a-tag>
<a-tag class="info-large-tag" :style="{ margin: '1' }" v-for="sel in balancer.selector">[[ sel
]]</a-tag>
</template>
</a-table>
<a-radio-group v-if="observatoryEnable || burstObservatoryEnable" v-model="obsSettings" @change="changeObsCode"

View file

@ -4,8 +4,7 @@
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled"
:style="{ color: '#FFA031' }"></a-icon>
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.generalConfigsDesc" }}</span>
</template>
</a-alert>
@ -15,11 +14,9 @@
<template #description>{{ i18n "pages.xray.FreedomStrategyDesc"
}}</template>
<template #control>
<a-select v-model="freedomStrategy"
:dropdown-class-name="themeSwitcher.currentTheme"
<a-select v-model="freedomStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }">
<a-select-option v-for="s in OutboundDomainStrategies"
:value="s">
<a-select-option v-for="s in OutboundDomainStrategies" :value="s">
<span>[[ s ]]</span>
</a-select-option>
</a-select>
@ -30,11 +27,9 @@
<template #description>{{ i18n "pages.xray.RoutingStrategyDesc"
}}</template>
<template #control>
<a-select v-model="routingStrategy"
:dropdown-class-name="themeSwitcher.currentTheme"
<a-select v-model="routingStrategy" :dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }">
<a-select-option v-for="s in routingDomainStrategies"
:value="s">
<a-select-option v-for="s in routingDomainStrategies" :value="s">
<span>[[ s ]]</span>
</a-select-option>
</a-select>
@ -45,8 +40,7 @@
<template #description>{{ i18n "pages.xray.outboundTestUrlDesc"
}}</template>
<template #control>
<a-input v-model="outboundTestUrl"
:placeholder="'https://www.google.com/generate_204'"
<a-input v-model="outboundTestUrl" :placeholder="'https://www.google.com/generate_204'"
:style="{ width: '100%' }"></a-input>
</template>
</a-setting-list-item>
@ -93,8 +87,7 @@
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled"
:style="{ color: '#FFA031' }"></a-icon>
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.logConfigsDesc" }}</span>
</template>
</a-alert>
@ -104,8 +97,7 @@
<template #description>{{ i18n "pages.xray.logLevelDesc"
}}</template>
<template #control>
<a-select v-model="logLevel"
:dropdown-class-name="themeSwitcher.currentTheme"
<a-select v-model="logLevel" :dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }">
<a-select-option v-for="s in log.loglevel" :value="s">
<span>[[ s ]]</span>
@ -118,8 +110,7 @@
<template #description>{{ i18n "pages.xray.accessLogDesc"
}}</template>
<template #control>
<a-select v-model="accessLog"
:dropdown-class-name="themeSwitcher.currentTheme"
<a-select v-model="accessLog" :dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }">
<a-select-option value>
<span>Empty</span>
@ -135,8 +126,7 @@
<template #description>{{ i18n "pages.xray.errorLogDesc"
}}</template>
<template #control>
<a-select v-model="errorLog"
:dropdown-class-name="themeSwitcher.currentTheme"
<a-select v-model="errorLog" :dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }">
<a-select-option value>
<span>Empty</span>
@ -152,8 +142,7 @@
<template #description>{{ i18n "pages.xray.maskAddressDesc"
}}</template>
<template #control>
<a-select v-model="maskAddressLog"
:dropdown-class-name="themeSwitcher.currentTheme"
<a-select v-model="maskAddressLog" :dropdown-class-name="themeSwitcher.currentTheme"
:style="{ width: '100%' }">
<a-select-option value>
<span>Empty</span>
@ -176,8 +165,7 @@
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled"
:style="{ color: '#FFA031' }"></a-icon>
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.blockConfigsDesc" }}</span>
</template>
</a-alert>
@ -191,8 +179,7 @@
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning" :style="{ textAlign: 'center' }">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled"
:style="{ color: '#FFA031' }"></a-icon>
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.blockConnectionsConfigsDesc"
}}</span>
</template>
@ -201,11 +188,9 @@
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.blockips" }}</template>
<template #control>
<a-select mode="tags" v-model="blockedIPs"
:style="{ width: '100%' }"
<a-select mode="tags" v-model="blockedIPs" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.IPsOptions">
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.IPsOptions">
<span>[[ p.label ]]</span>
</a-select-option>
</a-select>
@ -214,22 +199,18 @@
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.blockdomains" }}</template>
<template #control>
<a-select mode="tags" v-model="blockedDomains"
:style="{ width: '100%' }"
<a-select mode="tags" v-model="blockedDomains" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.BlockDomainsOptions">
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.BlockDomainsOptions">
<span>[[ p.label ]]</span>
</a-select-option>
</a-select>
</template>
</a-setting-list-item>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning"
:style="{ textAlign: 'center', marginTop: '20px' }">
<a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled"
:style="{ color: '#FFA031' }"></a-icon>
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.directConnectionsConfigsDesc"
}}</span>
</template>
@ -238,11 +219,9 @@
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.directips" }}</template>
<template #control>
<a-select mode="tags" :style="{ width: '100%' }"
v-model="directIPs"
<a-select mode="tags" :style="{ width: '100%' }" v-model="directIPs"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.IPsOptions">
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.IPsOptions">
<span>[[ p.label ]]</span>
</a-select-option>
</a-select>
@ -251,22 +230,18 @@
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.directdomains" }}</template>
<template #control>
<a-select mode="tags" :style="{ width: '100%' }"
v-model="directDomains"
<a-select mode="tags" :style="{ width: '100%' }" v-model="directDomains"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.DomainsOptions">
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.DomainsOptions">
<span>[[ p.label ]]</span>
</a-select-option>
</a-select>
</template>
</a-setting-list-item>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning"
:style="{ textAlign: 'center', marginTop: '20px' }">
<a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled"
:style="{ color: '#FFA031' }"></a-icon>
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
<span>{{ i18n "pages.xray.ipv4RoutingDesc" }}</span>
</template>
</a-alert>
@ -274,22 +249,18 @@
<a-setting-list-item paddings="small">
<template #title>{{ i18n "pages.xray.ipv4Routing" }}</template>
<template #control>
<a-select mode="tags" :style="{ width: '100%' }"
v-model="ipv4Domains"
<a-select mode="tags" :style="{ width: '100%' }" v-model="ipv4Domains"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.ServicesOptions">
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
<span>[[ p.label ]]</span>
</a-select-option>
</a-select>
</template>
</a-setting-list-item>
<a-row :xs="24" :sm="24" :lg="12">
<a-alert type="warning"
:style="{ textAlign: 'center', marginTop: '20px' }">
<a-alert type="warning" :style="{ textAlign: 'center', marginTop: '20px' }">
<template slot="message">
<a-icon type="exclamation-circle" theme="filled"
:style="{ color: '#FFA031' }"></a-icon>
<a-icon type="exclamation-circle" theme="filled" :style="{ color: '#FFA031' }"></a-icon>
{{ i18n "pages.xray.warpRoutingDesc" }}
</template>
</a-alert>
@ -298,18 +269,15 @@
<template #title>{{ i18n "pages.xray.warpRouting" }}</template>
<template #control>
<template v-if="WarpExist">
<a-select mode="tags" :style="{ width: '100%' }"
v-model="warpDomains"
<a-select mode="tags" :style="{ width: '100%' }" v-model="warpDomains"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.ServicesOptions">
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
<span>[[ p.label ]]</span>
</a-select-option>
</a-select>
</template>
<template v-else>
<a-button type="primary" icon="cloud"
@click="showWarp()">WARP</a-button>
<a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
</template>
</template>
</a-setting-list-item>
@ -317,11 +285,9 @@
<template #title>{{ i18n "pages.xray.nordRouting" }}</template>
<template #control>
<template v-if="NordExist">
<a-select mode="tags" :style="{ width: '100%' }"
v-model="nordDomains"
<a-select mode="tags" :style="{ width: '100%' }" v-model="nordDomains"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="p.value" :label="p.label"
v-for="p in settingsData.ServicesOptions">
<a-select-option :value="p.value" :label="p.label" v-for="p in settingsData.ServicesOptions">
<span>[[ p.label ]]</span>
</a-select-option>
</a-select>
@ -333,8 +299,7 @@
</template>
</a-setting-list-item>
</a-collapse-panel>
<a-collapse-panel key="6"
header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
<a-collapse-panel key="6" header='{{ i18n "pages.settings.resetDefaultConfig"}}'>
<a-space direction="horizontal" :style="{ padding: '0 20px' }">
<a-button type="danger" @click="resetXrayConfigToDefault">
<span>{{ i18n "pages.settings.resetDefaultConfig" }}</span>

View file

@ -29,7 +29,8 @@
<template #control>
<a-select v-model="dnsStrategy" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme">
<a-select-option :value="l" :label="l" v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']">
<a-select-option :value="l" :label="l"
v-for="l in ['UseSystem', 'UseIP', 'UseIPv4', 'UseIPv6']">
<span>[[ l ]]</span>
</a-select-option>
</a-select>

View file

@ -7,21 +7,16 @@
<span v-if="!isMobile">{{ i18n
"pages.xray.outbound.addOutbound" }}</span>
</a-button>
<a-button type="primary" icon="cloud"
@click="showWarp()">WARP</a-button>
<a-button type="primary" icon="api"
@click="showNord()">NordVPN</a-button>
<a-button type="primary" icon="cloud" @click="showWarp()">WARP</a-button>
<a-button type="primary" icon="api" @click="showNord()">NordVPN</a-button>
</a-space>
</a-col>
<a-col :xs="24" :sm="10" :lg="10" class="outbounds-toolbar-right">
<a-button-group>
<a-button icon="sync" @click="refreshOutboundTraffic()"
:loading="refreshing"></a-button>
<a-popconfirm placement="topRight"
@confirm="resetOutboundTraffic(-1)"
<a-button icon="sync" @click="refreshOutboundTraffic()" :loading="refreshing"></a-button>
<a-popconfirm placement="topRight" @confirm="resetOutboundTraffic(-1)"
title='{{ i18n "pages.inbounds.resetTrafficContent"}}'
:overlay-class-name="themeSwitcher.currentTheme"
ok-text='{{ i18n "reset"}}'
:overlay-class-name="themeSwitcher.currentTheme" ok-text='{{ i18n "reset"}}'
cancel-text='{{ i18n "cancel"}}'>
<a-icon slot="icon" type="question-circle-o"
:style="{ color: '#008771' }"></a-icon>

File diff suppressed because it is too large Load diff

View file

@ -199,6 +199,9 @@ func (j *CheckClientIpJob) processLogFile() bool {
inboundClientIps[email][ip] = timestamp
}
}
if err := scanner.Err(); err != nil {
j.checkError(err)
}
shouldCleanLog := false
for email, ipTimestamps := range inboundClientIps {

View file

@ -35,7 +35,7 @@ func (j *XrayTrafficJob) Run() {
if err != nil {
return
}
err, needRestart0 := j.inboundService.AddTraffic(traffics, clientTraffics)
err, needRestart0, clientsDisabled := j.inboundService.AddTraffic(traffics, clientTraffics)
if err != nil {
logger.Warning("add inbound traffic failed:", err)
}
@ -43,6 +43,18 @@ func (j *XrayTrafficJob) Run() {
if err != nil {
logger.Warning("add outbound traffic failed:", err)
}
if clientsDisabled {
restartOnDisable, settingErr := j.settingService.GetRestartXrayOnClientDisable()
if settingErr != nil {
logger.Warning("get RestartXrayOnClientDisable failed:", settingErr)
}
if restartOnDisable {
if err := j.xrayService.RestartXray(true); err != nil {
logger.Warning("restart xray after disabling clients failed:", err)
j.xrayService.SetToNeedRestart()
}
}
}
if ExternalTrafficInformEnable, err := j.settingService.GetExternalTrafficInformEnable(); ExternalTrafficInformEnable {
j.informTrafficToExternalAPI(traffics, clientTraffics)
} else if err != nil {

View file

@ -1068,10 +1068,12 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
interfaceClients := settings["clients"].([]any)
var newClients []any
needApiDel := false
clientFound := false
for _, client := range interfaceClients {
c := client.(map[string]any)
c_id := c[client_key].(string)
if c_id == clientId {
clientFound = true
email, _ = c["email"].(string)
needApiDel, _ = c["enable"].(bool)
} else {
@ -1079,6 +1081,10 @@ func (s *InboundService) DelInboundClient(inboundId int, clientId string) (bool,
}
}
if !clientFound {
return false, common.NewError("Client Not Found In Inbound For ID:", clientId)
}
if len(newClients) == 0 {
return false, common.NewError("no client remained in Inbound")
}
@ -1311,7 +1317,7 @@ func (s *InboundService) UpdateInboundClient(data *model.Inbound, clientId strin
return needRestart, tx.Save(oldInbound).Error
}
func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool) {
func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraffics []*xray.ClientTraffic) (error, bool, bool) {
var err error
db := database.GetDB()
tx := db.Begin()
@ -1325,11 +1331,11 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
}()
err = s.addInboundTraffic(tx, inboundTraffics)
if err != nil {
return err, false
return err, false, false
}
err = s.addClientTraffic(tx, clientTraffics)
if err != nil {
return err, false
return err, false, false
}
needRestart0, count, err := s.autoRenewClients(tx)
@ -1339,11 +1345,13 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
logger.Debugf("%v clients renewed", count)
}
disabledClientsCount := int64(0)
needRestart1, count, err := s.disableInvalidClients(tx)
if err != nil {
logger.Warning("Error in disabling invalid clients:", err)
} else if count > 0 {
logger.Debugf("%v clients disabled", count)
disabledClientsCount = count
}
needRestart2, count, err := s.disableInvalidInbounds(tx)
@ -1352,7 +1360,7 @@ func (s *InboundService) AddTraffic(inboundTraffics []*xray.Traffic, clientTraff
} else if count > 0 {
logger.Debugf("%v inbounds disabled", count)
}
return nil, (needRestart0 || needRestart1 || needRestart2)
return nil, (needRestart0 || needRestart1 || needRestart2), disabledClientsCount > 0
}
func (s *InboundService) addInboundTraffic(tx *gorm.DB, traffics []*xray.Traffic) error {
@ -1641,46 +1649,105 @@ func (s *InboundService) disableInvalidClients(tx *gorm.DB) (bool, int64, error)
now := time.Now().Unix() * 1000
needRestart := false
if p != nil {
var results []struct {
Tag string
Email string
}
var clientsToDisable []struct {
InboundId int
Tag string
Email string
}
err := tx.Table("inbounds").
Select("inbounds.tag, client_traffics.email").
Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id").
Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true).
Scan(&results).Error
if err != nil {
return false, 0, err
}
err := tx.Table("inbounds").
Select("inbounds.id as inbound_id, inbounds.tag, client_traffics.email").
Joins("JOIN client_traffics ON inbounds.id = client_traffics.inbound_id").
Where("((client_traffics.total > 0 AND client_traffics.up + client_traffics.down >= client_traffics.total) OR (client_traffics.expiry_time > 0 AND client_traffics.expiry_time <= ?)) AND client_traffics.enable = ?", now, true).
Scan(&clientsToDisable).Error
if err != nil {
return false, 0, err
}
if p != nil {
s.xrayApi.Init(p.GetAPIPort())
for _, result := range results {
err1 := s.xrayApi.RemoveUser(result.Tag, result.Email)
for _, client := range clientsToDisable {
err1 := s.xrayApi.RemoveUser(client.Tag, client.Email)
if err1 == nil {
logger.Debug("Client disabled by api:", result.Email)
logger.Debug("Client disabled by api:", client.Email)
} else {
if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) {
if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", client.Email)) {
logger.Debug("User is already disabled. Nothing to do more...")
} else {
if strings.Contains(err1.Error(), fmt.Sprintf("User %s not found.", result.Email)) {
logger.Debug("User is already disabled. Nothing to do more...")
} else {
logger.Debug("Error in disabling client by api:", err1)
needRestart = true
}
logger.Debug("Error in disabling client by api:", err1)
needRestart = true
}
}
}
s.xrayApi.Close()
}
result := tx.Model(xray.ClientTraffic{}).
Where("((total > 0 and up + down >= total) or (expiry_time > 0 and expiry_time <= ?)) and enable = ?", now, true).
Update("enable", false)
err := result.Error
err = result.Error
count := result.RowsAffected
return needRestart, count, err
if err != nil {
return needRestart, count, err
}
// Also set enable=false in inbounds.settings JSON so clients are visibly disabled
if len(clientsToDisable) > 0 {
inboundEmailMap := make(map[int]map[string]struct{})
for _, c := range clientsToDisable {
if inboundEmailMap[c.InboundId] == nil {
inboundEmailMap[c.InboundId] = make(map[string]struct{})
}
inboundEmailMap[c.InboundId][c.Email] = struct{}{}
}
inboundIds := make([]int, 0, len(inboundEmailMap))
for id := range inboundEmailMap {
inboundIds = append(inboundIds, id)
}
var inbounds []*model.Inbound
if err = tx.Model(model.Inbound{}).Where("id IN ?", inboundIds).Find(&inbounds).Error; err != nil {
logger.Warning("disableInvalidClients fetch inbounds:", err)
return needRestart, count, nil
}
for _, inbound := range inbounds {
settings := map[string]any{}
if jsonErr := json.Unmarshal([]byte(inbound.Settings), &settings); jsonErr != nil {
continue
}
clients, ok := settings["clients"].([]any)
if !ok {
continue
}
emailSet := inboundEmailMap[inbound.Id]
changed := false
for i := range clients {
c, ok := clients[i].(map[string]any)
if !ok {
continue
}
email, _ := c["email"].(string)
if _, shouldDisable := emailSet[email]; shouldDisable {
c["enable"] = false
c["updated_at"] = time.Now().Unix() * 1000
clients[i] = c
changed = true
}
}
if changed {
settings["clients"] = clients
modifiedSettings, jsonErr := json.MarshalIndent(settings, "", " ")
if jsonErr != nil {
continue
}
inbound.Settings = string(modifiedSettings)
}
}
if err = tx.Save(inbounds).Error; err != nil {
logger.Warning("disableInvalidClients update inbound settings:", err)
}
}
return needRestart, count, nil
}
func (s *InboundService) GetInboundTags() (string, error) {

View file

@ -846,6 +846,10 @@ func (s *ServerService) GetXrayLogs(
entries = append(entries, entry)
}
if err := scanner.Err(); err != nil {
return nil
}
if len(entries) > countInt {
entries = entries[len(entries)-countInt:]
}

View file

@ -83,6 +83,7 @@ var defaultValueMap = map[string]string{
"nord": "",
"externalTrafficInformEnable": "false",
"externalTrafficInformURI": "",
"restartXrayOnClientDisable": "true",
"xrayOutboundTestUrl": "https://www.google.com/generate_204",
// LDAP defaults
@ -628,6 +629,14 @@ func (s *SettingService) SetExternalTrafficInformURI(InformURI string) error {
return s.setString("externalTrafficInformURI", InformURI)
}
func (s *SettingService) GetRestartXrayOnClientDisable() (bool, error) {
return s.getBool("restartXrayOnClientDisable")
}
func (s *SettingService) SetRestartXrayOnClientDisable(value bool) error {
return s.setBool("restartXrayOnClientDisable", value)
}
func (s *SettingService) GetIpLimitEnable() (bool, error) {
accessLogPath, err := xray.GetAccessLogPath()
if err != nil {

View file

@ -103,7 +103,7 @@ func (s *XrayService) GetXrayConfig() (*xray.Config, error) {
return nil, err
}
s.inboundService.AddTraffic(nil, nil)
_, _, _ = s.inboundService.AddTraffic(nil, nil)
inbounds, err := s.inboundService.GetAllInbounds()
if err != nil {

View file

@ -477,6 +477,8 @@
"externalTrafficInformEnableDesc" = "يبعت تنبيه لـ API خارجي مع كل تحديث للترافيك."
"externalTrafficInformURI" = "مسار تنبيه الترافيك الخارجي"
"externalTrafficInformURIDesc" = "تحديثات الترافيك هتتبعت للمسار ده."
"restartXrayOnClientDisable" = "إعادة تشغيل Xray بعد التعطيل التلقائي"
"restartXrayOnClientDisableDesc" = "عند تعطيل العميل تلقائيا بسبب انتهاء الصلاحية أو حد حركة المرور، أعد تشغيل Xray."
"fragment" = "تجزئة"
"fragmentDesc" = "يفعل تجزئة لحزمة TLS hello."
"fragmentSett" = "إعدادات التجزئة"

View file

@ -477,6 +477,8 @@
"externalTrafficInformEnableDesc" = "Inform external API on every traffic update."
"externalTrafficInformURI" = "External Traffic Inform URI"
"externalTrafficInformURIDesc" = "Traffic updates are sent to this URI."
"restartXrayOnClientDisable" = "Restart Xray After Auto Disable"
"restartXrayOnClientDisableDesc" = "When a client is automatically disabled due to expiration or traffic limit, restart Xray."
"fragment" = "Fragmentation"
"fragmentDesc" = "Enable fragmentation for TLS hello packet."
"fragmentSett" = "Fragmentation Settings"

View file

@ -476,6 +476,8 @@
"externalTrafficInformEnableDesc" = "Informar a la API externa sobre cada actualización de tráfico."
"externalTrafficInformURI" = "URI de información de tráfico externo"
"externalTrafficInformURIDesc" = "Las actualizaciones de tráfico se envían a este URI."
"restartXrayOnClientDisable" = "Reiniciar Xray tras desactivación automática"
"restartXrayOnClientDisableDesc" = "Cuando un cliente se desactive automáticamente por vencimiento o límite de tráfico, reiniciar Xray."
"subURIDesc" = "Cambiar el URI base de la URL de suscripción para usar detrás de los servidores proxy"
"fragment" = "Fragmentación"
"fragmentDesc" = "Habilitar la fragmentación para el paquete de saludo de TLS"

View file

@ -477,6 +477,8 @@
"externalTrafficInformEnableDesc" = "مصرف ترافیک به سرویس خارجی ارسال می شود"
"externalTrafficInformURI" = "لینک اطلاع رسانی خارجی مصرف ترافیک"
"externalTrafficInformURIDesc" = "ترافیک های مصرفی به این لینک هم ارسال می شود"
"restartXrayOnClientDisable" = "ری‌استارت Xray بعد از غیرفعال‌سازی خودکار"
"restartXrayOnClientDisableDesc" = "وقتی کاربر به‌صورت خودکار به‌دلیل اتمام زمان یا ترافیک غیرفعال می‌شود، Xray ری‌استارت شود."
"fragment" = "فرگمنت"
"fragmentDesc" = "فعال کردن فرگمنت برای بسته‌ی نخست تی‌ال‌اس"
"fragmentSett" = "تنظیمات فرگمنت"

View file

@ -477,6 +477,8 @@
"externalTrafficInformEnableDesc" = "Inform external API on every traffic update."
"externalTrafficInformURI" = "Lalu Lintas Eksternal Menginformasikan URI"
"externalTrafficInformURIDesc" = "Pembaruan lalu lintas dikirim ke URI ini."
"restartXrayOnClientDisable" = "Nyalakan Ulang Xray Setelah Nonaktif Otomatis"
"restartXrayOnClientDisableDesc" = "Saat klien otomatis dinonaktifkan karena kedaluwarsa atau batas trafik, mulai ulang Xray."
"fragment" = "Fragmentasi"
"fragmentDesc" = "Aktifkan fragmentasi untuk paket hello TLS"
"fragmentSett" = "Pengaturan Fragmentasi"

View file

@ -477,6 +477,8 @@
"externalTrafficInformEnableDesc" = "トラフィックの更新ごとに外部 API に通知します。"
"externalTrafficInformURI" = "外部トラフィック通知 URI"
"externalTrafficInformURIDesc" = "トラフィックの更新ごとに外部 API に通知します。"
"restartXrayOnClientDisable" = "自動無効化後に Xray を再起動"
"restartXrayOnClientDisableDesc" = "有効期限切れまたはトラフィック上限でクライアントが自動的に無効化されたとき、Xray を再起動します。"
"fragment" = "フラグメント"
"fragmentDesc" = "TLS helloパケットのフラグメントを有効にする"
"fragmentSett" = "設定"

View file

@ -477,6 +477,8 @@
"externalTrafficInformEnableDesc" = "Informar a API externa sobre cada atualização de tráfego."
"externalTrafficInformURI" = "URI de informação de tráfego externo"
"externalTrafficInformURIDesc" = "As atualizações de tráfego são enviadas para este URI."
"restartXrayOnClientDisable" = "Reiniciar Xray Após Desativação Automática"
"restartXrayOnClientDisableDesc" = "Quando um cliente for desativado automaticamente por expiração ou limite de tráfego, reinicie o Xray."
"fragment" = "Fragmentação"
"fragmentDesc" = "Ativa a fragmentação para o pacote TLS hello."
"fragmentSett" = "Configurações de Fragmentação"

View file

@ -477,6 +477,8 @@
"externalTrafficInformEnableDesc" = "Информировать внешний API о каждом обновлении трафика"
"externalTrafficInformURI" = "URI информации о внешнем трафике"
"externalTrafficInformURIDesc" = "Обновления трафика отправляются на этот URI"
"restartXrayOnClientDisable" = "Перезапускать Xray после автоотключения"
"restartXrayOnClientDisableDesc" = "Когда клиент автоматически отключается из-за окончания срока действия или лимита трафика, перезапускать Xray."
"fragment" = "Фрагментация"
"fragmentDesc" = "Включить фрагментацию TLS-хэндшейка"
"fragmentSett" = "Настройки фрагментации"

View file

@ -477,6 +477,8 @@
"externalTrafficInformEnableDesc" = "Her trafik güncellemesinde harici API'yi bilgilendirin."
"externalTrafficInformURI" = "Harici Trafik Bilgisi URI'si"
"externalTrafficInformURIDesc" = "Trafik güncellemeleri bu URI'ye gönderildi."
"restartXrayOnClientDisable" = "Otomatik Devre Dışı Sonrası Xray'i Yeniden Başlat"
"restartXrayOnClientDisableDesc" = "Bir istemci süre dolumu veya trafik limiti nedeniyle otomatik devre dışı bırakıldığında Xray'i yeniden başlat."
"fragment" = "Parçalama"
"fragmentDesc" = "TLS merhaba paketinin parçalanmasını etkinleştir."
"fragmentSett" = "Parçalama Ayarları"

View file

@ -477,6 +477,8 @@
"externalTrafficInformEnableDesc" = "Інформувати зовнішній API про кожне оновлення трафіку."
"externalTrafficInformURI" = "Інформаційний URI зовнішнього трафіку"
"externalTrafficInformURIDesc" = "Оновлення трафіку надсилаються на цей URI."
"restartXrayOnClientDisable" = "Перезапускати Xray після авто-вимкнення"
"restartXrayOnClientDisableDesc" = "Коли клієнт автоматично вимикається через закінчення терміну дії або ліміт трафіку, перезапускати Xray."
"fragment" = "Фрагментація"
"fragmentDesc" = "Увімкнути фрагментацію для пакету привітання TLS"
"fragmentSett" = "Параметри фрагментації"

View file

@ -477,6 +477,8 @@
"externalTrafficInformEnableDesc" = "Thông báo cho API bên ngoài về mọi cập nhật lưu lượng truy cập."
"externalTrafficInformURI" = "URI thông báo lưu lượng truy cập bên ngoài"
"externalTrafficInformURIDesc" = "Cập nhật lưu lượng truy cập được gửi tới URI này."
"restartXrayOnClientDisable" = "Khởi Động Lại Xray Sau Khi Tự Động Vô Hiệu Hóa"
"restartXrayOnClientDisableDesc" = "Khi người dùng bị vô hiệu hóa tự động do hết hạn hoặc chạm giới hạn lưu lượng, hãy khởi động lại Xray."
"fragment" = "Sự phân mảnh"
"fragmentDesc" = "Kích hoạt phân mảnh cho gói TLS hello"
"fragmentSett" = "Cài đặt phân mảnh"

View file

@ -477,6 +477,8 @@
"externalTrafficInformEnableDesc" = "每次流量更新时通知外部 API"
"externalTrafficInformURI" = "外部流量通知 URI"
"externalTrafficInformURIDesc" = "流量更新将发送到此 URI"
"restartXrayOnClientDisable" = "客户端自动禁用后重启 Xray"
"restartXrayOnClientDisableDesc" = "当客户端因到期或流量超限被自动禁用时,重启 Xray。"
"fragment" = "分片"
"fragmentDesc" = "启用 TLS hello 数据包分片"
"fragmentSett" = "设置"

View file

@ -477,6 +477,8 @@
"externalTrafficInformEnableDesc" = "每次流量更新時通知外部 API"
"externalTrafficInformURI" = "外部流量通知 URI"
"externalTrafficInformURIDesc" = "流量更新將會傳送到此 URI"
"restartXrayOnClientDisable" = "用戶自動停用後重新啟動 Xray"
"restartXrayOnClientDisableDesc" = "當用戶因到期或流量上限而被自動停用時,重新啟動 Xray。"
"fragment" = "分片"
"fragmentDesc" = "啟用 TLS hello 資料包分片"
"fragmentSett" = "設定"

1427
x-ui.sh

File diff suppressed because it is too large Load diff