mirror of
https://github.com/MHSanaei/3x-ui.git
synced 2025-09-12 13:10:05 +00:00
Merge branch 'main' into periodic-traffic-reset
This commit is contained in:
commit
a7f460213d
41 changed files with 434 additions and 174 deletions
12
.github/workflows/release.yml
vendored
12
.github/workflows/release.yml
vendored
|
@ -7,8 +7,9 @@ on:
|
|||
push:
|
||||
branches:
|
||||
- main
|
||||
tags:
|
||||
- "v*.*.*"
|
||||
paths:
|
||||
- '.github/workflows/release.yml'
|
||||
- '**.js'
|
||||
- '**.css'
|
||||
- '**.html'
|
||||
|
@ -38,7 +39,7 @@ jobs:
|
|||
uses: actions/checkout@v5
|
||||
|
||||
- name: Setup Go
|
||||
uses: actions/setup-go@v5
|
||||
uses: actions/setup-go@v6
|
||||
with:
|
||||
go-version-file: go.mod
|
||||
check-latest: true
|
||||
|
@ -84,7 +85,7 @@ jobs:
|
|||
cd x-ui/bin
|
||||
|
||||
# Download dependencies
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.8.29/"
|
||||
Xray_URL="https://github.com/XTLS/Xray-core/releases/download/v25.9.5/"
|
||||
if [ "${{ matrix.platform }}" == "amd64" ]; then
|
||||
wget -q ${Xray_URL}Xray-linux-64.zip
|
||||
unzip Xray-linux-64.zip
|
||||
|
@ -135,10 +136,13 @@ jobs:
|
|||
|
||||
- name: Upload files to GH release
|
||||
uses: svenstaro/upload-release-action@v2
|
||||
if: github.event_name == 'release' && github.event.action == 'published'
|
||||
if: |
|
||||
(github.event_name == 'release' && github.event.action == 'published') ||
|
||||
(github.event_name == 'push' && startsWith(github.ref, 'refs/tags/'))
|
||||
with:
|
||||
repo_token: ${{ secrets.GITHUB_TOKEN }}
|
||||
tag: ${{ github.ref }}
|
||||
file: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
asset_name: x-ui-linux-${{ matrix.platform }}.tar.gz
|
||||
overwrite: true
|
||||
prerelease: true
|
||||
|
|
|
@ -27,7 +27,7 @@ case $1 in
|
|||
esac
|
||||
mkdir -p build/bin
|
||||
cd build/bin
|
||||
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.8.29/Xray-linux-${ARCH}.zip"
|
||||
wget -q "https://github.com/XTLS/Xray-core/releases/download/v25.9.5/Xray-linux-${ARCH}.zip"
|
||||
unzip "Xray-linux-${ARCH}.zip"
|
||||
rm -f "Xray-linux-${ARCH}.zip" geoip.dat geosite.dat
|
||||
mv xray "xray-linux-${FNAME}"
|
||||
|
|
|
@ -1 +1 @@
|
|||
2.6.7
|
||||
2.6.8
|
|
@ -107,3 +107,10 @@ type Client struct {
|
|||
CreatedAt int64 `json:"created_at,omitempty"`
|
||||
UpdatedAt int64 `json:"updated_at,omitempty"`
|
||||
}
|
||||
|
||||
type VLESSSettings struct {
|
||||
Clients []Client `json:"clients"`
|
||||
Decryption string `json:"decryption"`
|
||||
Encryption string `json:"encryption"`
|
||||
Fallbacks []any `json:"fallbacks"`
|
||||
}
|
||||
|
|
21
go.mod
21
go.mod
|
@ -14,10 +14,10 @@ require (
|
|||
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
|
||||
github.com/pelletier/go-toml/v2 v2.2.4
|
||||
github.com/robfig/cron/v3 v3.0.1
|
||||
github.com/shirou/gopsutil/v4 v4.25.7
|
||||
github.com/shirou/gopsutil/v4 v4.25.8
|
||||
github.com/valyala/fasthttp v1.65.0
|
||||
github.com/xlzd/gotp v0.1.0
|
||||
github.com/xtls/xray-core v1.250803.1-0.20250829143322-81b7cd718ad5
|
||||
github.com/xtls/xray-core v1.250905.0
|
||||
go.uber.org/atomic v1.11.0
|
||||
golang.org/x/crypto v0.41.0
|
||||
golang.org/x/text v0.28.0
|
||||
|
@ -28,7 +28,8 @@ require (
|
|||
|
||||
require (
|
||||
github.com/andybalholm/brotli v1.2.0 // indirect
|
||||
github.com/bytedance/sonic v1.14.0 // indirect
|
||||
github.com/bytedance/gopkg v0.1.3 // indirect
|
||||
github.com/bytedance/sonic v1.14.1 // indirect
|
||||
github.com/bytedance/sonic/loader v0.3.0 // indirect
|
||||
github.com/cloudflare/circl v1.6.1 // indirect
|
||||
github.com/cloudwego/base64x v0.1.6 // indirect
|
||||
|
@ -67,8 +68,8 @@ require (
|
|||
github.com/refraction-networking/utls v1.8.0 // indirect
|
||||
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
|
||||
github.com/rogpeppe/go-internal v1.14.1 // indirect
|
||||
github.com/sagernet/sing v0.7.5 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 // indirect
|
||||
github.com/sagernet/sing v0.7.7 // indirect
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
|
||||
github.com/tklauser/go-sysconf v0.3.15 // indirect
|
||||
github.com/tklauser/numcpus v0.10.0 // indirect
|
||||
|
@ -79,16 +80,16 @@ require (
|
|||
github.com/valyala/fastjson v1.6.4 // indirect
|
||||
github.com/vishvananda/netlink v1.3.1 // indirect
|
||||
github.com/vishvananda/netns v0.0.5 // indirect
|
||||
github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f // indirect
|
||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c // indirect
|
||||
github.com/yusufpapurcu/wmi v1.2.4 // indirect
|
||||
go.uber.org/mock v0.6.0 // indirect
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
|
||||
golang.org/x/arch v0.20.0 // indirect
|
||||
golang.org/x/arch v0.21.0 // indirect
|
||||
golang.org/x/mod v0.27.0 // indirect
|
||||
golang.org/x/net v0.43.0 // indirect
|
||||
golang.org/x/sync v0.16.0 // indirect
|
||||
golang.org/x/sys v0.35.0 // indirect
|
||||
golang.org/x/time v0.12.0 // indirect
|
||||
golang.org/x/sync v0.17.0 // indirect
|
||||
golang.org/x/sys v0.36.0 // indirect
|
||||
golang.org/x/time v0.13.0 // indirect
|
||||
golang.org/x/tools v0.36.0 // indirect
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
|
||||
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
|
||||
|
|
42
go.sum
42
go.sum
|
@ -2,8 +2,10 @@ github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg
|
|||
github.com/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
|
||||
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
|
||||
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
|
||||
github.com/bytedance/sonic v1.14.0 h1:/OfKt8HFw0kh2rj8N0F6C/qPGRESq0BbaNZgcNXXzQQ=
|
||||
github.com/bytedance/sonic v1.14.0/go.mod h1:WoEbx8WTcFJfzCe0hbmyTGrfjt8PzNEBdxlNUO24NhA=
|
||||
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
|
||||
github.com/bytedance/gopkg v0.1.3/go.mod h1:576VvJ+eJgyCzdjS+c4+77QF3p7ubbtiKARP3TxducM=
|
||||
github.com/bytedance/sonic v1.14.1 h1:FBMC0zVz5XUmE4z9wF4Jey0An5FueFvOsTKKKtwIl7w=
|
||||
github.com/bytedance/sonic v1.14.1/go.mod h1:gi6uhQLMbTdeP0muCnrjHLeCUPyb70ujhnNlhOylAFc=
|
||||
github.com/bytedance/sonic/loader v0.3.0 h1:dskwH8edlzNMctoruo8FPTJDF3vLtDT0sXZwvZJyqeA=
|
||||
github.com/bytedance/sonic/loader v0.3.0/go.mod h1:N8A3vUdtUebEY2/VQC0MyhYeKUFosQU6FxH2JmUe6VI=
|
||||
github.com/cloudflare/circl v1.6.1 h1:zqIqSPIndyBh1bjLVVDHMPpVKqp8Su/V+6MeDzzQBQ0=
|
||||
|
@ -132,14 +134,14 @@ github.com/robfig/cron/v3 v3.0.1 h1:WdRxkvbJztn8LMz/QEvLN5sBU+xKpSqwwUO1Pjr4qDs=
|
|||
github.com/robfig/cron/v3 v3.0.1/go.mod h1:eQICP3HwyT7UooqI/z+Ov+PtYAWygg1TEWWzGIFLtro=
|
||||
github.com/rogpeppe/go-internal v1.14.1 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
|
||||
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
|
||||
github.com/sagernet/sing v0.7.5 h1:gNMwZCLPqR+4e0g6dwi0sSsrvOmoMjpZgqxKsuJZatc=
|
||||
github.com/sagernet/sing v0.7.5/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8 h1:PURj5PRoAkqeHh2ZW205RWzN9E9RtKCVCzByXruQWfE=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.8/go.mod h1:lo7TWEMDcN5/h5B8S0ew+r78ZODn6SwVaFhvB6H+PTI=
|
||||
github.com/sagernet/sing v0.7.7 h1:o46FzVZS+wKbBMEkMEdEHoVZxyM9jvfRpKXc7pEgS/c=
|
||||
github.com/sagernet/sing v0.7.7/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
|
||||
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
|
||||
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7 h1:bNb2JuqKuAu3tRlPv5piSmBZyMfecwQ+t/ILq+1JqVM=
|
||||
github.com/shirou/gopsutil/v4 v4.25.7/go.mod h1:XV/egmwJtd3ZQjBpJVY5kndsiOO4IRqy9TQnmm6VP7U=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970=
|
||||
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI=
|
||||
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
|
||||
github.com/stretchr/objx v0.4.0/go.mod h1:YvHI0jy2hoMjB+UWwv71VJQ9isScKT/TqJzVSSt89Yw=
|
||||
github.com/stretchr/objx v0.5.0/go.mod h1:Yh+to48EsGEfYuaHDzXPcE3xhTkx73EhmCGUpEOglKo=
|
||||
|
@ -172,10 +174,10 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
|
|||
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM=
|
||||
github.com/xlzd/gotp v0.1.0 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
|
||||
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
|
||||
github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f h1:o1Kryl9qEYYzNep9RId9DM1kBn8tBrcK5UJnti/l0NI=
|
||||
github.com/xtls/reality v0.0.0-20250828044527-046fad5ab64f/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
||||
github.com/xtls/xray-core v1.250803.1-0.20250829143322-81b7cd718ad5 h1:rBqCVgic8yIUVHB4h26K8JNuwJuNj45egsdXxwEvA7E=
|
||||
github.com/xtls/xray-core v1.250803.1-0.20250829143322-81b7cd718ad5/go.mod h1:WB/73DmN9Vs7lxtx4Xc/D0Ub1VUu06hAh1mMh8JN2uM=
|
||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU=
|
||||
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
|
||||
github.com/xtls/xray-core v1.250905.0 h1:VNL3l/6fcwyeYXJTRbf+TYqPfJYkk0Wmmz7qoQNkxY8=
|
||||
github.com/xtls/xray-core v1.250905.0/go.mod h1:WB/73DmN9Vs7lxtx4Xc/D0Ub1VUu06hAh1mMh8JN2uM=
|
||||
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
|
||||
github.com/xyproto/randomstring v1.0.5/go.mod h1:rgmS5DeNXLivK7YprL0pY+lTuhNQW3iGxZ18UQApw/E=
|
||||
github.com/yusufpapurcu/wmi v1.2.4 h1:zFUKzehAFReQwLys1b/iSMl+JQGSCSjtVqQn9bBrPo0=
|
||||
|
@ -198,28 +200,28 @@ go.uber.org/mock v0.6.0 h1:hyF9dfmbgIX5EfOdasqLsWD6xqpNZlXblLB/Dbnwv3Y=
|
|||
go.uber.org/mock v0.6.0/go.mod h1:KiVJ4BqZJaMj4svdfmHM0AUx4NJYO8ZNpPnZn1Z+BBU=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba h1:0b9z3AuHCjxk0x/opv64kcgZLBseWJUpBw5I82+2U4M=
|
||||
go4.org/netipx v0.0.0-20231129151722-fdeea329fbba/go.mod h1:PLyyIXexvUFg3Owu6p/WfdlivPbZJsZdgWZlrGope/Y=
|
||||
golang.org/x/arch v0.20.0 h1:dx1zTU0MAE98U+TQ8BLl7XsJbgze2WnNKF/8tGp/Q6c=
|
||||
golang.org/x/arch v0.20.0/go.mod h1:bdwinDaKcfZUGpH09BB7ZmOfhalA8lQdzl62l8gGWsk=
|
||||
golang.org/x/arch v0.21.0 h1:iTC9o7+wP6cPWpDWkivCvQFGAHDQ59SrSxsLPcnkArw=
|
||||
golang.org/x/arch v0.21.0/go.mod h1:dNHoOeKiyja7GTvF9NJS1l3Z2yntpQNzgrjh1cU103A=
|
||||
golang.org/x/crypto v0.41.0 h1:WKYxWedPGCTVVl5+WHSSrOBT0O8lx32+zxmHxijgXp4=
|
||||
golang.org/x/crypto v0.41.0/go.mod h1:pO5AFd7FA68rFak7rOAGVuygIISepHftHnr8dr6+sUc=
|
||||
golang.org/x/mod v0.27.0 h1:kb+q2PyFnEADO2IEF935ehFUXlWiNjJWtRNgBLSfbxQ=
|
||||
golang.org/x/mod v0.27.0/go.mod h1:rWI627Fq0DEoudcK+MBkNkCe0EetEaDSwJJkCcjpazc=
|
||||
golang.org/x/net v0.43.0 h1:lat02VYK2j4aLzMzecihNvTlJNQUq316m2Mr9rnM6YE=
|
||||
golang.org/x/net v0.43.0/go.mod h1:vhO1fvI4dGsIjh73sWfUVjj3N7CA9WkKJNQm2svM6Jg=
|
||||
golang.org/x/sync v0.16.0 h1:ycBJEhp9p4vXvUZNszeOq0kGTPghopOL8q0fq3vstxw=
|
||||
golang.org/x/sync v0.16.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA=
|
||||
golang.org/x/sync v0.17.0 h1:l60nONMj9l5drqw6jlhIELNv9I0A4OFgRsG9k2oT9Ug=
|
||||
golang.org/x/sync v0.17.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
|
||||
golang.org/x/sys v0.0.0-20190916202348-b4ddaad3f8a3/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.0.0-20201204225414-ed752295db88/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
|
||||
golang.org/x/sys v0.1.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.2.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.10.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
|
||||
golang.org/x/sys v0.35.0 h1:vz1N37gP5bs89s7He8XuIYXpyY0+QlsKmzipCbUtyxI=
|
||||
golang.org/x/sys v0.35.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k=
|
||||
golang.org/x/sys v0.36.0 h1:KVRy2GtZBrk1cBYA7MKu5bEZFxQk4NIDV6RLVcC8o0k=
|
||||
golang.org/x/sys v0.36.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
|
||||
golang.org/x/text v0.28.0 h1:rhazDwis8INMIwQ4tpjLDzUhx6RlXqZNPEM0huQojng=
|
||||
golang.org/x/text v0.28.0/go.mod h1:U8nCwOR8jO/marOQ0QbDiOngZVEBB7MAiitBuMjXiNU=
|
||||
golang.org/x/time v0.12.0 h1:ScB/8o8olJvc+CQPWrK3fPZNfh7qgwCrY0zJmoEQLSE=
|
||||
golang.org/x/time v0.12.0/go.mod h1:CDIdPxbZBQxdj6cxyCIdrNogrJKMJ7pr37NYpMcMDSg=
|
||||
golang.org/x/time v0.13.0 h1:eUlYslOIt32DgYD6utsuUeHs4d7AsEYLuIAdg7FlYgI=
|
||||
golang.org/x/time v0.13.0/go.mod h1:eL/Oa2bBBK0TkX57Fyni+NgnyQQN4LitPmob2Hjnqw4=
|
||||
golang.org/x/tools v0.36.0 h1:kWS0uv/zsvHEle1LbV5LE8QujrxB3wfQyxHfhOk0Qkg=
|
||||
golang.org/x/tools v0.36.0/go.mod h1:WBDiHKJK8YgLHlcQPYQzNCkUxUypCaa5ZegCVutKm+s=
|
||||
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 h1:B82qJJgjvYKsXS9jeunTOisW56dUokqW/FOteYJJ/yg=
|
||||
|
|
|
@ -53,7 +53,6 @@ func (a *SUBController) initRouter(g *gin.RouterGroup) {
|
|||
gJson := g.Group(a.subJsonPath)
|
||||
|
||||
gLink.GET(":subid", a.subs)
|
||||
|
||||
gJson.GET(":subid", a.subJsons)
|
||||
}
|
||||
|
||||
|
@ -85,7 +84,7 @@ func (a *SUBController) subs(c *gin.Context) {
|
|||
// Add headers
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
|
||||
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
|
||||
|
||||
if a.subEncrypt {
|
||||
c.String(200, base64.StdEncoding.EncodeToString([]byte(result)))
|
||||
|
@ -119,7 +118,7 @@ func (a *SUBController) subJsons(c *gin.Context) {
|
|||
// Add headers
|
||||
c.Writer.Header().Set("Subscription-Userinfo", header)
|
||||
c.Writer.Header().Set("Profile-Update-Interval", a.updateInterval)
|
||||
c.Writer.Header().Set("Profile-Title", "base64:" + base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
|
||||
c.Writer.Header().Set("Profile-Title", "base64:"+base64.StdEncoding.EncodeToString([]byte(a.subTitle)))
|
||||
|
||||
c.String(200, jsonSub)
|
||||
}
|
||||
|
|
|
@ -184,8 +184,14 @@ func (s *SubJsonService) getConfig(inbound *model.Inbound, client model.Client,
|
|||
var newOutbounds []json_util.RawMessage
|
||||
|
||||
switch inbound.Protocol {
|
||||
case "vmess", "vless":
|
||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client))
|
||||
case "vmess":
|
||||
newOutbounds = append(newOutbounds, s.genVnext(inbound, streamSettings, client, ""))
|
||||
case "vless":
|
||||
var vlessSettings model.VLESSSettings
|
||||
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
|
||||
|
||||
newOutbounds = append(newOutbounds,
|
||||
s.genVnext(inbound, streamSettings, client, vlessSettings.Encryption))
|
||||
case "trojan", "shadowsocks":
|
||||
newOutbounds = append(newOutbounds, s.genServer(inbound, streamSettings, client))
|
||||
}
|
||||
|
@ -284,7 +290,7 @@ func (s *SubJsonService) realityData(rData map[string]any) map[string]any {
|
|||
return rltyData
|
||||
}
|
||||
|
||||
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client) json_util.RawMessage {
|
||||
func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_util.RawMessage, client model.Client, encryption string) json_util.RawMessage {
|
||||
outbound := Outbound{}
|
||||
usersData := make([]UserVnext, 1)
|
||||
|
||||
|
@ -295,7 +301,7 @@ func (s *SubJsonService) genVnext(inbound *model.Inbound, streamSettings json_ut
|
|||
}
|
||||
if inbound.Protocol == model.VLESS {
|
||||
usersData[0].Flow = client.Flow
|
||||
usersData[0].Encryption = "none"
|
||||
usersData[0].Encryption = encryption
|
||||
}
|
||||
|
||||
vnextData := make([]VnextSetting, 1)
|
||||
|
|
|
@ -313,6 +313,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||
if inbound.Protocol != model.VLESS {
|
||||
return ""
|
||||
}
|
||||
var vlessSettings model.VLESSSettings
|
||||
_ = json.Unmarshal([]byte(inbound.Settings), &vlessSettings)
|
||||
|
||||
var stream map[string]any
|
||||
json.Unmarshal([]byte(inbound.StreamSettings), &stream)
|
||||
clients, _ := s.inboundService.GetClients(inbound)
|
||||
|
@ -327,6 +330,9 @@ func (s *SubService) genVlessLink(inbound *model.Inbound, email string) string {
|
|||
port := inbound.Port
|
||||
streamNetwork := stream["network"].(string)
|
||||
params := make(map[string]string)
|
||||
if vlessSettings.Encryption != "" {
|
||||
params["encryption"] = vlessSettings.Encryption
|
||||
}
|
||||
params["type"] = streamNetwork
|
||||
|
||||
switch streamNetwork {
|
||||
|
|
|
@ -11,10 +11,8 @@ const Protocols = {
|
|||
|
||||
const SSMethods = {
|
||||
AES_256_GCM: 'aes-256-gcm',
|
||||
AES_128_GCM: 'aes-128-gcm',
|
||||
CHACHA20_POLY1305: 'chacha20-poly1305',
|
||||
CHACHA20_IETF_POLY1305: 'chacha20-ietf-poly1305',
|
||||
XCHACHA20_POLY1305: 'xchacha20-poly1305',
|
||||
XCHACHA20_IETF_POLY1305: 'xchacha20-ietf-poly1305',
|
||||
BLAKE3_AES_128_GCM: '2022-blake3-aes-128-gcm',
|
||||
BLAKE3_AES_256_GCM: '2022-blake3-aes-256-gcm',
|
||||
|
@ -1301,6 +1299,7 @@ class Inbound extends XrayCommonClass {
|
|||
const security = forceTls == 'same' ? this.stream.security : forceTls;
|
||||
const params = new Map();
|
||||
params.set("type", this.stream.network);
|
||||
params.set("encryption", this.settings.encryption);
|
||||
switch (type) {
|
||||
case "tcp":
|
||||
const tcp = this.stream.tcp;
|
||||
|
@ -1859,13 +1858,17 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||
constructor(
|
||||
protocol,
|
||||
vlesses = [new Inbound.VLESSSettings.VLESS()],
|
||||
decryption = 'none',
|
||||
fallbacks = []
|
||||
decryption = "none",
|
||||
encryption = "none",
|
||||
fallbacks = [],
|
||||
selectedAuth = undefined,
|
||||
) {
|
||||
super(protocol);
|
||||
this.vlesses = vlesses;
|
||||
this.decryption = decryption;
|
||||
this.encryption = encryption;
|
||||
this.fallbacks = fallbacks;
|
||||
this.selectedAuth = selectedAuth;
|
||||
}
|
||||
|
||||
addFallback() {
|
||||
|
@ -1876,22 +1879,43 @@ Inbound.VLESSSettings = class extends Inbound.Settings {
|
|||
this.fallbacks.splice(index, 1);
|
||||
}
|
||||
|
||||
// decryption should be set to static value
|
||||
static fromJson(json = {}) {
|
||||
return new Inbound.VLESSSettings(
|
||||
const obj = new Inbound.VLESSSettings(
|
||||
Protocols.VLESS,
|
||||
json.clients.map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
||||
json.decryption || 'none',
|
||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks),);
|
||||
(json.clients || []).map(client => Inbound.VLESSSettings.VLESS.fromJson(client)),
|
||||
json.decryption,
|
||||
json.encryption,
|
||||
Inbound.VLESSSettings.Fallback.fromJson(json.fallbacks || []),
|
||||
json.selectedAuth
|
||||
);
|
||||
return obj;
|
||||
}
|
||||
|
||||
|
||||
toJson() {
|
||||
return {
|
||||
const json = {
|
||||
clients: Inbound.VLESSSettings.toJsonArray(this.vlesses),
|
||||
decryption: this.decryption,
|
||||
fallbacks: Inbound.VLESSSettings.toJsonArray(this.fallbacks),
|
||||
};
|
||||
|
||||
if (this.decryption) {
|
||||
json.decryption = this.decryption;
|
||||
}
|
||||
|
||||
if (this.encryption) {
|
||||
json.encryption = this.encryption;
|
||||
}
|
||||
|
||||
if (this.fallbacks && this.fallbacks.length > 0) {
|
||||
json.fallbacks = Inbound.VLESSSettings.toJsonArray(this.fallbacks);
|
||||
}
|
||||
if (this.selectedAuth) {
|
||||
json.selectedAuth = this.selectedAuth;
|
||||
}
|
||||
|
||||
return json;
|
||||
}
|
||||
|
||||
|
||||
};
|
||||
|
||||
Inbound.VLESSSettings.VLESS = class extends XrayCommonClass {
|
||||
|
|
|
@ -813,7 +813,7 @@ class Outbound extends CommonClass {
|
|||
var settings;
|
||||
switch (protocol) {
|
||||
case Protocols.VLESS:
|
||||
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '');
|
||||
settings = new Outbound.VLESSSettings(address, port, userData, url.searchParams.get('flow') ?? '', url.searchParams.get('encryption') ?? 'none');
|
||||
break;
|
||||
case Protocols.Trojan:
|
||||
settings = new Outbound.TrojanSettings(address, port, userData);
|
||||
|
@ -1046,13 +1046,13 @@ Outbound.VmessSettings = class extends CommonClass {
|
|||
}
|
||||
};
|
||||
Outbound.VLESSSettings = class extends CommonClass {
|
||||
constructor(address, port, id, flow, encryption = 'none') {
|
||||
constructor(address, port, id, flow, encryption) {
|
||||
super();
|
||||
this.address = address;
|
||||
this.port = port;
|
||||
this.id = id;
|
||||
this.flow = flow;
|
||||
this.encryption = encryption
|
||||
this.encryption = encryption;
|
||||
}
|
||||
|
||||
static fromJson(json = {}) {
|
||||
|
@ -1071,7 +1071,7 @@ Outbound.VLESSSettings = class extends CommonClass {
|
|||
vnext: [{
|
||||
address: this.address,
|
||||
port: this.port,
|
||||
users: [{ id: this.id, flow: this.flow, encryption: 'none', }],
|
||||
users: [{ id: this.id, flow: this.flow, encryption: this.encryption }],
|
||||
}],
|
||||
};
|
||||
}
|
||||
|
|
|
@ -134,7 +134,7 @@ class DateUtil {
|
|||
}
|
||||
|
||||
static formatMillis(millis) {
|
||||
return moment(millis).format('YYYY-M-D H:m:s');
|
||||
return moment(millis).format('YYYY-M-D HH:mm:ss');
|
||||
}
|
||||
|
||||
static firstDayOfMonth() {
|
||||
|
|
|
@ -47,6 +47,7 @@ func (a *APIController) initRouter(g *gin.RouterGroup) {
|
|||
{"POST", "/resetAllClientTraffics/:id", a.inboundController.resetAllClientTraffics},
|
||||
{"POST", "/delDepletedClients/:id", a.inboundController.delDepletedClients},
|
||||
{"POST", "/onlines", a.inboundController.onlines},
|
||||
{"POST", "/lastOnline", a.inboundController.lastOnline},
|
||||
{"POST", "/updateClientTraffic/:email", a.inboundController.updateClientTraffic},
|
||||
}
|
||||
|
||||
|
|
|
@ -340,6 +340,11 @@ func (a *InboundController) onlines(c *gin.Context) {
|
|||
jsonObj(c, a.inboundService.GetOnlineClients(), nil)
|
||||
}
|
||||
|
||||
func (a *InboundController) lastOnline(c *gin.Context) {
|
||||
data, err := a.inboundService.GetClientsLastOnline()
|
||||
jsonObj(c, data, err)
|
||||
}
|
||||
|
||||
func (a *InboundController) updateClientTraffic(c *gin.Context) {
|
||||
email := c.Param("email")
|
||||
|
||||
|
|
|
@ -55,6 +55,7 @@ func (a *ServerController) initRouter(g *gin.RouterGroup) {
|
|||
g.POST("/getNewX25519Cert", a.getNewX25519Cert)
|
||||
g.POST("/getNewmldsa65", a.getNewmldsa65)
|
||||
g.POST("/getNewEchCert", a.getNewEchCert)
|
||||
g.POST("/getNewVlessEnc", a.getNewVlessEnc)
|
||||
}
|
||||
|
||||
func (a *ServerController) refreshStatus() {
|
||||
|
@ -266,3 +267,12 @@ func (a *ServerController) getNewEchCert(c *gin.Context) {
|
|||
}
|
||||
jsonObj(c, cert, nil)
|
||||
}
|
||||
|
||||
func (a *ServerController) getNewVlessEnc(c *gin.Context) {
|
||||
out, err := a.serverService.GetNewVlessEnc()
|
||||
if err != nil {
|
||||
jsonMsg(c, I18nWeb(c, "pages.inbounds.toasts.getNewVlessEncError"), err)
|
||||
return
|
||||
}
|
||||
jsonObj(c, out, nil)
|
||||
}
|
||||
|
|
|
@ -33,12 +33,17 @@
|
|||
<a-switch v-model="client.enable" @change="switchEnableClient(record.id,client)"></a-switch>
|
||||
</template>
|
||||
<template slot="online" slot-scope="text, client, index">
|
||||
<template v-if="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme">
|
||||
<template slot="content" >
|
||||
{{ i18n "lastOnline" }}: [[ formatLastOnline(client.email) ]]
|
||||
</template>
|
||||
<template v-if="client.enable && isClientOnline(client.email)">
|
||||
<a-tag color="green">{{ i18n "online" }}</a-tag>
|
||||
</template>
|
||||
<template v-else>
|
||||
<a-tag>{{ i18n "offline" }}</a-tag>
|
||||
</template>
|
||||
</a-popover>
|
||||
</template>
|
||||
<template slot="client" slot-scope="text, client">
|
||||
<a-space direction="horizontal" :size="2">
|
||||
|
|
|
@ -226,6 +226,11 @@
|
|||
</template>
|
||||
|
||||
<!-- vless settings -->
|
||||
<template v-if="outbound.protocol === Protocols.VLESS">
|
||||
<a-form-item label='encryption'>
|
||||
<a-input v-model.trim="outbound.settings.encryption"></a-input>
|
||||
</a-form-item>
|
||||
</template>
|
||||
<template v-if="outbound.canEnableTlsFlow()">
|
||||
<a-form-item label='Flow'>
|
||||
<a-select v-model="outbound.settings.flow" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
|
|
|
@ -18,7 +18,30 @@
|
|||
</table>
|
||||
</a-collapse-panel>
|
||||
</a-collapse>
|
||||
<template v-if="inbound.isTcp">
|
||||
<template v-if="!inbound.stream.isTLS || !inbound.stream.isReality">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label="Authentication">
|
||||
<a-select v-model="inbound.settings.selectedAuth" @change="getNewVlessEnc" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="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" disabled></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="clearKeys">Clear</a-button>
|
||||
</a-space>
|
||||
</a-form-item>
|
||||
</a-form>
|
||||
</template>
|
||||
<template v-if="inbound.isTcp && !inbound.settings.encryption">
|
||||
<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>
|
||||
|
|
|
@ -5,13 +5,13 @@
|
|||
<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 value="tls">TLS</a-radio-button>
|
||||
<a-radio-button v-if="inbound.canEnableReality() && !inbound.settings.encryption" value="reality">Reality</a-radio-button>
|
||||
<a-radio-button v-if="!inbound.settings.encryption" value="tls">TLS</a-radio-button>
|
||||
</a-radio-group>
|
||||
</a-form-item>
|
||||
|
||||
<!-- tls settings -->
|
||||
<template v-if="inbound.stream.isTls">
|
||||
<template v-if="inbound.stream.isTls && !inbound.settings.encryption">
|
||||
<a-form-item label="SNI" placeholder="Server Name Indication">
|
||||
<a-input v-model.trim="inbound.stream.tls.sni"></a-input>
|
||||
</a-form-item>
|
||||
|
@ -121,7 +121,7 @@
|
|||
</template>
|
||||
|
||||
<!-- reality settings -->
|
||||
<template v-if="inbound.stream.isReality">
|
||||
<template v-if="inbound.stream.isReality && !inbound.settings.encryption">
|
||||
{{template "form/realitySettings"}}
|
||||
</template>
|
||||
</a-form>
|
||||
|
|
|
@ -712,7 +712,7 @@
|
|||
}, {
|
||||
title: '{{ i18n "pages.inbounds.enable" }}',
|
||||
align: 'center',
|
||||
width: 30,
|
||||
width: 35,
|
||||
scopedSlots: { customRender: 'enable' },
|
||||
}, {
|
||||
title: '{{ i18n "pages.inbounds.remark" }}',
|
||||
|
@ -776,8 +776,8 @@
|
|||
|
||||
const innerColumns = [
|
||||
{ title: '{{ i18n "pages.inbounds.operate" }}', width: 65, scopedSlots: { customRender: 'actions' } },
|
||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 30, scopedSlots: { customRender: 'enable' } },
|
||||
{ title: '{{ i18n "online" }}', width: 30, scopedSlots: { customRender: 'online' } },
|
||||
{ title: '{{ i18n "pages.inbounds.enable" }}', width: 35, scopedSlots: { customRender: 'enable' } },
|
||||
{ title: '{{ i18n "online" }}', width: 32, scopedSlots: { customRender: 'online' } },
|
||||
{ title: '{{ i18n "pages.inbounds.client" }}', width: 80, scopedSlots: { customRender: 'client' } },
|
||||
{ title: '{{ i18n "pages.inbounds.traffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'traffic' } },
|
||||
{ title: '{{ i18n "pages.inbounds.allTimeTraffic" }}', width: 80, align: 'center', scopedSlots: { customRender: 'allTime' } },
|
||||
|
@ -813,6 +813,7 @@
|
|||
defaultKey: '',
|
||||
clientCount: [],
|
||||
onlineClients: [],
|
||||
lastOnlineMap: {},
|
||||
isRefreshEnabled: localStorage.getItem("isRefreshEnabled") === "true" ? true : false,
|
||||
refreshing: false,
|
||||
refreshInterval: Number(localStorage.getItem("refreshInterval")) || 5000,
|
||||
|
@ -841,6 +842,7 @@
|
|||
return;
|
||||
}
|
||||
|
||||
await this.getLastOnlineMap();
|
||||
await this.getOnlineUsers();
|
||||
|
||||
this.setInbounds(msg.obj);
|
||||
|
@ -855,6 +857,11 @@
|
|||
}
|
||||
this.onlineClients = msg.obj != null ? msg.obj : [];
|
||||
},
|
||||
async getLastOnlineMap() {
|
||||
const msg = await HttpUtil.post('/panel/api/inbounds/lastOnline');
|
||||
if (!msg.success || !msg.obj) return;
|
||||
this.lastOnlineMap = msg.obj || {}
|
||||
},
|
||||
async getDefaultSettings() {
|
||||
const msg = await HttpUtil.post('/panel/setting/defaultSettings');
|
||||
if (!msg.success) {
|
||||
|
@ -1502,6 +1509,17 @@
|
|||
isClientOnline(email) {
|
||||
return this.onlineClients.includes(email);
|
||||
},
|
||||
getLastOnline(email) {
|
||||
return this.lastOnlineMap[email] || null
|
||||
},
|
||||
formatLastOnline(email) {
|
||||
const ts = this.getLastOnline(email)
|
||||
if (!ts) return '-'
|
||||
if (this.datepicker === 'gregorian') {
|
||||
return DateUtil.formatMillis(ts)
|
||||
}
|
||||
return DateUtil.convertToJalalian(moment(ts))
|
||||
},
|
||||
isRemovable(dbInboundId) {
|
||||
return this.getInboundClients(this.dbInbounds.find(row => row.id === dbInboundId)).length > 1;
|
||||
},
|
||||
|
|
|
@ -466,71 +466,83 @@
|
|||
</g>
|
||||
</svg>
|
||||
</div>
|
||||
<a-row type="flex" justify="center" align="middle" :style="{ height: '100%', overflow: 'auto', overflowX: 'hidden' }">
|
||||
<a-row type="flex" justify="center" align="middle"
|
||||
:style="{ height: '100%', overflow: 'auto', overflowX: 'hidden' }">
|
||||
<a-col :xs="22" :sm="12" :md="10" :lg="8" :xl="6" :xxl="5" id="login" :style="{ margin: '3rem 0' }">
|
||||
<div class="setting-section">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}' placement="bottomRight" trigger="click">
|
||||
<template slot="content">
|
||||
<a-space direction="vertical" :size="10">
|
||||
<a-theme-switch-login></a-theme-switch-login>
|
||||
<span>{{ i18n "pages.settings.language" }}</span>
|
||||
<a-select ref="selectLang" :style="{ width: '100%' }" v-model="lang" @change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-button shape="circle" icon="setting"></a-button>
|
||||
</a-popover>
|
||||
</div>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col :style="{ width: '100%' }">
|
||||
<h2 class="title headline zoom">
|
||||
<span class="words-wrapper">
|
||||
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
|
||||
<b>{{ i18n "pages.login.title" }}</b>
|
||||
</span>
|
||||
</h2>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col span="24">
|
||||
<a-form>
|
||||
<a-space direction="vertical" size="middle">
|
||||
<a-form-item>
|
||||
<a-input autocomplete="username" name="username" v-model.trim="user.username"
|
||||
placeholder='{{ i18n "username" }}' @keydown.enter.native="login" autofocus>
|
||||
<a-icon slot="prefix" type="user" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' @keydown.enter.native="login">
|
||||
<a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="twoFactorEnable">
|
||||
<a-input autocomplete="one-time-code" name="twoFactorCode" v-model.trim="user.twoFactorCode"
|
||||
placeholder='{{ i18n "twoFactorCode" }}' @keydown.enter.native="login">
|
||||
<a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<div :style="{ height: '50px', marginTop: '1rem', ...loading ? { width: '52px' } : { display: 'inline-block' } }" class="wave-btn-bg wave-btn-bg-cl">
|
||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loading" @click="login"
|
||||
:icon="loading ? 'poweroff' : undefined">
|
||||
[[ loading ? '' : '{{ i18n "login" }}' ]]
|
||||
</a-button>
|
||||
</div>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<template v-if="!loadingStates.fetched">
|
||||
<div :style="{ textAlign: 'center' }">
|
||||
<a-spin size="large" />
|
||||
</div>
|
||||
</template>
|
||||
<template v-else>
|
||||
<div class="setting-section">
|
||||
<a-popover :overlay-class-name="themeSwitcher.currentTheme" title='{{ i18n "menu.settings" }}'
|
||||
placement="bottomRight" trigger="click">
|
||||
<template slot="content">
|
||||
<a-space direction="vertical" :size="10">
|
||||
<a-theme-switch-login></a-theme-switch-login>
|
||||
<span>{{ i18n "pages.settings.language" }}</span>
|
||||
<a-select ref="selectLang" :style="{ width: '100%' }" v-model="lang"
|
||||
@change="LanguageManager.setLanguage(lang)" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option :value="l.value" label="English" v-for="l in LanguageManager.supportedLanguages">
|
||||
<span role="img" aria-label="l.name" v-text="l.icon"></span>
|
||||
<span v-text="l.name"></span>
|
||||
</a-select-option>
|
||||
</a-select>
|
||||
</a-space>
|
||||
</template>
|
||||
<a-button shape="circle" icon="setting"></a-button>
|
||||
</a-popover>
|
||||
</div>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col :style="{ width: '100%' }">
|
||||
<h2 class="title headline zoom">
|
||||
<span class="words-wrapper">
|
||||
<b class="is-visible">{{ i18n "pages.login.hello" }}</b>
|
||||
<b>{{ i18n "pages.login.title" }}</b>
|
||||
</span>
|
||||
</h2>
|
||||
</a-col>
|
||||
</a-row>
|
||||
<a-row type="flex" justify="center">
|
||||
<a-col span="24">
|
||||
<a-form @submit.prevent="login">
|
||||
<a-space direction="vertical" size="middle">
|
||||
<a-form-item>
|
||||
<a-input autocomplete="username" name="username" v-model.trim="user.username"
|
||||
placeholder='{{ i18n "username" }}' autofocus required>
|
||||
<a-icon slot="prefix" type="user" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-input-password autocomplete="password" name="password" v-model.trim="user.password"
|
||||
placeholder='{{ i18n "password" }}' required>
|
||||
<a-icon slot="prefix" type="lock" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input-password>
|
||||
</a-form-item>
|
||||
<a-form-item v-if="twoFactorEnable">
|
||||
<a-input autocomplete="one-time-code" name="twoFactorCode" v-model.trim="user.twoFactorCode"
|
||||
placeholder='{{ i18n "twoFactorCode" }}' required>
|
||||
<a-icon slot="prefix" type="key" :style="{ fontSize: '1rem' }"></a-icon>
|
||||
</a-input>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<a-row justify="center" class="centered">
|
||||
<div
|
||||
:style="{ height: '50px', marginTop: '1rem', ...loadingStates.spinning ? { width: '52px' } : { display: 'inline-block' } }"
|
||||
class="wave-btn-bg wave-btn-bg-cl">
|
||||
<a-button class="ant-btn-primary-login" type="primary" :loading="loadingStates.spinning"
|
||||
:icon="loadingStates.spinning ? 'poweroff' : undefined" html-type="submit">
|
||||
[[ loadingStates.spinning ? '' : '{{ i18n "login" }}' ]]
|
||||
</a-button>
|
||||
</div>
|
||||
</a-row>
|
||||
</a-form-item>
|
||||
</a-space>
|
||||
</a-form>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</template>
|
||||
</a-col>
|
||||
</a-row>
|
||||
</a-layout-content>
|
||||
|
@ -544,7 +556,10 @@
|
|||
el: '#app',
|
||||
data: {
|
||||
themeSwitcher,
|
||||
loading: false,
|
||||
loadingStates: {
|
||||
fetched: false,
|
||||
spinning: false
|
||||
},
|
||||
user: {
|
||||
username: "",
|
||||
password: "",
|
||||
|
@ -559,19 +574,23 @@
|
|||
},
|
||||
methods: {
|
||||
async login() {
|
||||
this.loading = true;
|
||||
this.loadingStates.spinning = true;
|
||||
|
||||
const msg = await HttpUtil.post('/login', this.user);
|
||||
this.loading = false;
|
||||
|
||||
if (msg.success) {
|
||||
location.href = basePath + 'panel/';
|
||||
}
|
||||
|
||||
this.loadingStates.spinning = false;
|
||||
},
|
||||
async getTwoFactorEnable() {
|
||||
this.loading = true;
|
||||
const msg = await HttpUtil.post('/getTwoFactorEnable');
|
||||
this.loading = false;
|
||||
|
||||
if (msg.success) {
|
||||
this.twoFactorEnable = msg.obj;
|
||||
this.loadingStates.fetched = true;
|
||||
|
||||
return msg.obj;
|
||||
}
|
||||
},
|
||||
|
@ -615,4 +634,4 @@
|
|||
}
|
||||
});
|
||||
</script>
|
||||
{{ template "page/body_end" .}}
|
||||
{{ template "page/body_end" .}}
|
|
@ -101,6 +101,12 @@
|
|||
{{ i18n "security" }}
|
||||
<a-tag :color="inbound.stream.security == 'none' ? 'red' : 'green'">[[ inbound.stream.security ]]</a-tag>
|
||||
<br />
|
||||
<td>Authentication</td>
|
||||
<a-tag :color="inbound.settings.selectedAuth ? 'green' : 'red'">[[ inbound.settings.selectedAuth ? inbound.settings.selectedAuth : '' ]]</a-tag>
|
||||
<br />
|
||||
{{ i18n "encryption" }}
|
||||
<a-tag :color="inbound.settings.encryption ? 'green' : 'red'">[[ inbound.settings.encryption ? inbound.settings.encryption : '' ]]</a-tag>
|
||||
<br />
|
||||
<template v-if="inbound.stream.security != 'none'">
|
||||
{{ i18n "domainName" }}
|
||||
<a-tag v-if="inbound.serverName" :color="inbound.serverName ? 'green' : 'orange'">[[ inbound.serverName ? inbound.serverName : '' ]]</a-tag>
|
||||
|
@ -217,6 +223,12 @@
|
|||
</template>
|
||||
</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>{{ i18n "lastOnline" }}</td>
|
||||
<td>
|
||||
<a-tag>[[ app.formatLastOnline(infoModal.clientSettings && infoModal.clientSettings.email ? infoModal.clientSettings.email : '') ]]</a-tag>
|
||||
</td>
|
||||
</tr>
|
||||
<tr v-if="infoModal.clientSettings.comment">
|
||||
<td>{{ i18n "comment" }}</td>
|
||||
<td>
|
||||
|
|
|
@ -1,9 +1,7 @@
|
|||
{{define "modals/inboundModal"}}
|
||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title"
|
||||
:dialog-style="{ top: '20px' }" @ok="inModal.ok"
|
||||
:confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.currentTheme"
|
||||
:ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
<a-modal id="inbound-modal" v-model="inModal.visible" :title="inModal.title" :dialog-style="{ top: '20px' }"
|
||||
@ok="inModal.ok" :confirm-loading="inModal.confirmLoading" :closable="true" :mask-closable="false"
|
||||
:class="themeSwitcher.currentTheme" :ok-text="inModal.okText" cancel-text='{{ i18n "close" }}'>
|
||||
{{template "form/inbound"}}
|
||||
</a-modal>
|
||||
<script>
|
||||
|
@ -20,7 +18,7 @@
|
|||
ok() {
|
||||
ObjectUtil.execute(inModal.confirm, inModal.inbound, inModal.dbInbound);
|
||||
},
|
||||
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => {}, isEdit = false }) {
|
||||
show({ title = '', okText = '{{ i18n "sure" }}', inbound = null, dbInbound = null, confirm = (inbound, dbInbound) => { }, isEdit = false }) {
|
||||
this.title = title;
|
||||
this.okText = okText;
|
||||
if (inbound) {
|
||||
|
@ -41,7 +39,7 @@
|
|||
inModal.visible = false;
|
||||
inModal.loading(false);
|
||||
},
|
||||
loading(loading=true) {
|
||||
loading(loading = true) {
|
||||
inModal.confirmLoading = loading;
|
||||
},
|
||||
};
|
||||
|
@ -105,9 +103,9 @@
|
|||
},
|
||||
SSMethodChange() {
|
||||
this.inModal.inbound.settings.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
|
||||
|
||||
|
||||
if (this.inModal.inbound.isSSMultiUser) {
|
||||
if (this.inModal.inbound.settings.shadowsockses.length ==0){
|
||||
if (this.inModal.inbound.settings.shadowsockses.length == 0) {
|
||||
this.inModal.inbound.settings.shadowsockses = [new Inbound.ShadowsocksSettings.Shadowsocks()];
|
||||
}
|
||||
if (!this.inModal.inbound.isSS2022) {
|
||||
|
@ -123,7 +121,7 @@
|
|||
client.password = RandomUtil.randomShadowsocksPassword(this.inModal.inbound.settings.method)
|
||||
})
|
||||
} else {
|
||||
if (this.inModal.inbound.settings.shadowsockses.length > 0){
|
||||
if (this.inModal.inbound.settings.shadowsockses.length > 0) {
|
||||
this.inModal.inbound.settings.shadowsockses = [];
|
||||
}
|
||||
}
|
||||
|
@ -154,7 +152,7 @@
|
|||
},
|
||||
async getNewEchCert() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.post('/server/getNewEchCert', {sni: inModal.inbound.stream.tls.sni});
|
||||
const msg = await HttpUtil.post('/server/getNewEchCert', { sni: inModal.inbound.stream.tls.sni });
|
||||
inModal.loading(false);
|
||||
if (!msg.success) {
|
||||
return;
|
||||
|
@ -162,8 +160,35 @@
|
|||
inModal.inbound.stream.tls.echServerKeys = msg.obj.echServerKeys;
|
||||
inModal.inbound.stream.tls.settings.echConfigList = msg.obj.echConfigList;
|
||||
},
|
||||
async getNewVlessEnc() {
|
||||
inModal.loading(true);
|
||||
const msg = await HttpUtil.post('/server/getNewVlessEnc');
|
||||
inModal.loading(false);
|
||||
|
||||
if (!msg.success) {
|
||||
return;
|
||||
}
|
||||
|
||||
const auths = msg.obj.auths || [];
|
||||
const selected = inModal.inbound.settings.selectedAuth;
|
||||
const block = auths.find(a => a.label === selected);
|
||||
|
||||
if (!block) {
|
||||
console.error("No auth block for", selected);
|
||||
return;
|
||||
}
|
||||
|
||||
inModal.inbound.settings.decryption = block.decryption;
|
||||
inModal.inbound.settings.encryption = block.encryption;
|
||||
},
|
||||
clearKeys() {
|
||||
this.inbound.settings.decryption = 'none';
|
||||
this.inbound.settings.encryption = 'none';
|
||||
this.inbound.settings.selectedAuth = undefined;
|
||||
}
|
||||
|
||||
},
|
||||
});
|
||||
|
||||
</script>
|
||||
{{end}}
|
||||
{{end}}
|
|
@ -1,11 +1,6 @@
|
|||
{{define "modals/ruleModal"}}
|
||||
<a-modal id="rule-modal" v-model="ruleModal.visible" :title="ruleModal.title" @ok="ruleModal.ok" :confirm-loading="ruleModal.confirmLoading" :closable="true" :mask-closable="false" :ok-text="ruleModal.okText" cancel-text='{{ i18n "close" }}' :class="themeSwitcher.currentTheme">
|
||||
<a-form :colon="false" :label-col="{ md: {span:8} }" :wrapper-col="{ md: {span:14} }">
|
||||
<a-form-item label='Domain Matcher'>
|
||||
<a-select v-model="ruleModal.rule.domainMatcher" :dropdown-class-name="themeSwitcher.currentTheme">
|
||||
<a-select-option v-for="dm in ['','hybrid','linear']" :value="dm">[[ dm ]]</a-select-option>
|
||||
</a-select>
|
||||
</a-form-item>
|
||||
<a-form-item>
|
||||
<template slot="label">
|
||||
<a-tooltip>
|
||||
|
@ -123,7 +118,6 @@
|
|||
confirm: null,
|
||||
rule: {
|
||||
type: "field",
|
||||
domainMatcher: "",
|
||||
domain: "",
|
||||
ip: "",
|
||||
port: "",
|
||||
|
@ -157,7 +151,6 @@
|
|||
this.confirm = confirm;
|
||||
this.visible = true;
|
||||
if (isEdit) {
|
||||
this.rule.domainMatcher = rule.domainMatcher;
|
||||
this.rule.domain = rule.domain ? rule.domain.join(',') : [];
|
||||
this.rule.ip = rule.ip ? rule.ip.join(',') : [];
|
||||
this.rule.port = rule.port;
|
||||
|
@ -172,7 +165,6 @@
|
|||
this.rule.balancerTag = rule.balancerTag ? rule.balancerTag : "";
|
||||
} else {
|
||||
this.rule = {
|
||||
domainMatcher: "",
|
||||
domain: "",
|
||||
ip: "",
|
||||
port: "",
|
||||
|
@ -214,7 +206,6 @@
|
|||
rule = {};
|
||||
newRule = {};
|
||||
rule.type = "field";
|
||||
rule.domainMatcher = value.domainMatcher;
|
||||
rule.domain = value.domain.length > 0 ? value.domain.split(',') : [];
|
||||
rule.ip = value.ip.length > 0 ? value.ip.split(',') : [];
|
||||
rule.port = value.port;
|
||||
|
|
|
@ -359,6 +359,7 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
|||
var oldSettings map[string]any
|
||||
_ = json.Unmarshal([]byte(oldInbound.Settings), &oldSettings)
|
||||
emailToCreated := map[string]int64{}
|
||||
emailToUpdated := map[string]int64{}
|
||||
if oldSettings != nil {
|
||||
if oc, ok := oldSettings["clients"].([]any); ok {
|
||||
for _, it := range oc {
|
||||
|
@ -370,6 +371,12 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
|||
case int64:
|
||||
emailToCreated[email] = v
|
||||
}
|
||||
switch v := m["updated_at"].(type) {
|
||||
case float64:
|
||||
emailToUpdated[email] = int64(v)
|
||||
case int64:
|
||||
emailToUpdated[email] = v
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -389,7 +396,12 @@ func (s *InboundService) UpdateInbound(inbound *model.Inbound) (*model.Inbound,
|
|||
m["created_at"] = now
|
||||
}
|
||||
}
|
||||
m["updated_at"] = now
|
||||
// Preserve client's updated_at if present; do not bump on parent inbound update
|
||||
if _, hasUpdated := m["updated_at"]; !hasUpdated {
|
||||
if v, ok4 := emailToUpdated[email]; ok4 && v > 0 {
|
||||
m["updated_at"] = v
|
||||
}
|
||||
}
|
||||
nSlice[i] = m
|
||||
}
|
||||
}
|
||||
|
@ -978,6 +990,7 @@ func (s *InboundService) addClientTraffic(tx *gorm.DB, traffics []*xray.ClientTr
|
|||
// Add user in onlineUsers array on traffic
|
||||
if traffics[traffic_index].Up+traffics[traffic_index].Down > 0 {
|
||||
onlineClients = append(onlineClients, traffics[traffic_index].Email)
|
||||
dbClientTraffics[dbTraffic_index].LastOnline = time.Now().UnixMilli()
|
||||
}
|
||||
break
|
||||
}
|
||||
|
@ -2198,6 +2211,20 @@ func (s *InboundService) GetOnlineClients() []string {
|
|||
return p.GetOnlineClients()
|
||||
}
|
||||
|
||||
func (s *InboundService) GetClientsLastOnline() (map[string]int64, error) {
|
||||
db := database.GetDB()
|
||||
var rows []xray.ClientTraffic
|
||||
err := db.Model(&xray.ClientTraffic{}).Select("email, last_online").Find(&rows).Error
|
||||
if err != nil && err != gorm.ErrRecordNotFound {
|
||||
return nil, err
|
||||
}
|
||||
result := make(map[string]int64, len(rows))
|
||||
for _, r := range rows {
|
||||
result[r.Email] = r.LastOnline
|
||||
}
|
||||
return result, nil
|
||||
}
|
||||
|
||||
func (s *InboundService) FilterAndSortClientEmails(emails []string) ([]string, []string, error) {
|
||||
db := database.GetDB()
|
||||
|
||||
|
|
|
@ -871,3 +871,53 @@ func (s *ServerService) GetNewEchCert(sni string) (interface{}, error) {
|
|||
"echConfigList": configList,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type AuthBlock struct {
|
||||
Label string `json:"label"`
|
||||
Decryption string `json:"decryption"`
|
||||
Encryption string `json:"encryption"`
|
||||
}
|
||||
|
||||
func (s *ServerService) GetNewVlessEnc() (any, error) {
|
||||
cmd := exec.Command(xray.GetBinaryPath(), "vlessenc")
|
||||
var out bytes.Buffer
|
||||
cmd.Stdout = &out
|
||||
if err := cmd.Run(); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
lines := strings.Split(out.String(), "\n")
|
||||
|
||||
var blocks []AuthBlock
|
||||
var current *AuthBlock
|
||||
|
||||
for _, line := range lines {
|
||||
line = strings.TrimSpace(line)
|
||||
if strings.HasPrefix(line, "Authentication:") {
|
||||
if current != nil {
|
||||
blocks = append(blocks, *current)
|
||||
}
|
||||
current = &AuthBlock{Label: strings.TrimSpace(strings.TrimPrefix(line, "Authentication:"))}
|
||||
} else if strings.HasPrefix(line, `"decryption"`) || strings.HasPrefix(line, `"encryption"`) {
|
||||
parts := strings.SplitN(line, ":", 2)
|
||||
if len(parts) == 2 && current != nil {
|
||||
key := strings.Trim(parts[0], `" `)
|
||||
val := strings.Trim(parts[1], `" `)
|
||||
switch key {
|
||||
case "decryption":
|
||||
current.Decryption = val
|
||||
case "encryption":
|
||||
current.Encryption = val
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if current != nil {
|
||||
blocks = append(blocks, *current)
|
||||
}
|
||||
|
||||
return map[string]any{
|
||||
"auths": blocks,
|
||||
}, nil
|
||||
}
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "فشل"
|
||||
"comment" = "تعليق"
|
||||
"success" = "تم بنجاح"
|
||||
"lastOnline" = "آخر متصل"
|
||||
"getVersion" = "جيب النسخة"
|
||||
"install" = "تثبيت"
|
||||
"clients" = "عملاء"
|
||||
|
@ -274,6 +275,7 @@
|
|||
"trafficGetError" = "خطأ في الحصول على حركات المرور"
|
||||
"getNewX25519CertError" = "حدث خطأ أثناء الحصول على شهادة X25519."
|
||||
"getNewmldsa65Error" = "حدث خطاء في الحصول على mldsa65."
|
||||
"getNewVlessEncError" = "حدث خطأ أثناء الحصول على VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "طلب"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Failed"
|
||||
"comment" = "Comment"
|
||||
"success" = "Successfully"
|
||||
"lastOnline" = "Last Online"
|
||||
"getVersion" = "Get Version"
|
||||
"install" = "Install"
|
||||
"clients" = "Clients"
|
||||
|
@ -274,6 +275,7 @@
|
|||
"trafficGetError" = "Error getting traffics."
|
||||
"getNewX25519CertError" = "Error while obtaining the X25519 certificate."
|
||||
"getNewmldsa65Error" = "Error while obtaining mldsa65."
|
||||
"getNewVlessEncError" = "Error while obtaining VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Request"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Falló"
|
||||
"comment" = "Comentario"
|
||||
"success" = "Éxito"
|
||||
"lastOnline" = "Última conexión"
|
||||
"getVersion" = "Obtener versión"
|
||||
"install" = "Instalar"
|
||||
"clients" = "Clientes"
|
||||
|
@ -274,6 +275,7 @@
|
|||
"trafficGetError" = "Error al obtener los tráficos"
|
||||
"getNewX25519CertError" = "Error al obtener el certificado X25519."
|
||||
"getNewmldsa65Error" = "Error al obtener el certificado mldsa65."
|
||||
"getNewVlessEncError" = "Error al obtener el certificado VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Pedido"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "ناموفق"
|
||||
"comment" = "توضیحات"
|
||||
"success" = "موفق"
|
||||
"lastOnline" = "آخرین فعالیت"
|
||||
"getVersion" = "دریافت نسخه"
|
||||
"install" = "نصب"
|
||||
"clients" = "کاربران"
|
||||
|
@ -274,6 +275,7 @@
|
|||
"trafficGetError" = "خطا در دریافت ترافیکها"
|
||||
"getNewX25519CertError" = "خطا در دریافت گواهی X25519."
|
||||
"getNewmldsa65Error" = "خطا در دریافت گواهی mldsa65."
|
||||
"getNewVlessEncError" = "خطا در دریافت گواهی VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "درخواست"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Gagal"
|
||||
"comment" = "Komentar"
|
||||
"success" = "Berhasil"
|
||||
"lastOnline" = "Terakhir online"
|
||||
"getVersion" = "Dapatkan Versi"
|
||||
"install" = "Instal"
|
||||
"clients" = "Klien"
|
||||
|
@ -274,6 +275,7 @@
|
|||
"trafficGetError" = "Gagal mendapatkan data lalu lintas"
|
||||
"getNewX25519CertError" = "Terjadi kesalahan saat mendapatkan sertifikat X25519."
|
||||
"getNewmldsa65Error" = "Terjadi kesalahan saat mendapatkan sertifikat mldsa65."
|
||||
"getNewVlessEncError" = "Terjadi kesalahan saat mendapatkan sertifikat VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Permintaan"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "失敗"
|
||||
"comment" = "コメント"
|
||||
"success" = "成功"
|
||||
"lastOnline" = "最終オンライン"
|
||||
"getVersion" = "バージョン取得"
|
||||
"install" = "インストール"
|
||||
"clients" = "クライアント"
|
||||
|
@ -274,6 +275,7 @@
|
|||
"trafficGetError" = "トラフィックの取得中にエラーが発生しました"
|
||||
"getNewX25519CertError" = "X25519証明書の取得中にエラーが発生しました。"
|
||||
"getNewmldsa65Error" = "mldsa65証明書の取得中にエラーが発生しました。"
|
||||
"getNewVlessEncError" = "VlessEnc証明書の取得中にエラーが発生しました。"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "リクエスト"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Falhou"
|
||||
"comment" = "Comentário"
|
||||
"success" = "Com Sucesso"
|
||||
"lastOnline" = "Última vez online"
|
||||
"getVersion" = "Obter Versão"
|
||||
"install" = "Instalar"
|
||||
"clients" = "Clientes"
|
||||
|
@ -274,6 +275,7 @@
|
|||
"trafficGetError" = "Erro ao obter tráfegos"
|
||||
"getNewX25519CertError" = "Erro ao obter o certificado X25519."
|
||||
"getNewmldsa65Error" = "Erro ao obter o certificado mldsa65."
|
||||
"getNewVlessEncError" = "Erro ao obter o certificado VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Requisição"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Ошибка"
|
||||
"comment" = "Комментарий"
|
||||
"success" = "Успешно"
|
||||
"lastOnline" = "Был(а) в сети"
|
||||
"getVersion" = "Узнать версию"
|
||||
"install" = "Установка"
|
||||
"clients" = "Клиенты"
|
||||
|
@ -274,6 +275,7 @@
|
|||
"trafficGetError" = "Ошибка получения данных о трафике"
|
||||
"getNewX25519CertError" = "Ошибка при получении сертификата X25519."
|
||||
"getNewmldsa65Error" = "Ошибка при получении сертификата mldsa65."
|
||||
"getNewVlessEncError" = "Ошибка при получении сертификата VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Запрос"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Başarısız"
|
||||
"comment" = "Yorum"
|
||||
"success" = "Başarılı"
|
||||
"lastOnline" = "Son çevrimiçi"
|
||||
"getVersion" = "Sürümü Al"
|
||||
"install" = "Yükle"
|
||||
"clients" = "Müşteriler"
|
||||
|
@ -274,6 +275,7 @@
|
|||
"trafficGetError" = "Trafik bilgisi alınırken hata oluştu"
|
||||
"getNewX25519CertError" = "X25519 sertifikası alınırken hata oluştu."
|
||||
"getNewmldsa65Error" = "mldsa65 sertifikası alınırken hata oluştu."
|
||||
"getNewVlessEncError" = "VlessEnc sertifikası alınırken hata oluştu."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "İstek"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Помилка"
|
||||
"comment" = "Коментар"
|
||||
"success" = "Успішно"
|
||||
"lastOnline" = "Був(ла) онлайн"
|
||||
"getVersion" = "Отримати версію"
|
||||
"install" = "Встановити"
|
||||
"clients" = "Клієнти"
|
||||
|
@ -274,6 +275,7 @@
|
|||
"trafficGetError" = "Помилка отримання даних про трафік"
|
||||
"getNewX25519CertError" = "Помилка при отриманні сертифіката X25519."
|
||||
"getNewmldsa65Error" = "Помилка при отриманні сертифіката mldsa65."
|
||||
"getNewVlessEncError" = "Помилка при отриманні сертифіката VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Запит"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "Thất bại"
|
||||
"comment" = "Bình luận"
|
||||
"success" = "Thành công"
|
||||
"lastOnline" = "Lần online gần nhất"
|
||||
"getVersion" = "Lấy phiên bản"
|
||||
"install" = "Cài đặt"
|
||||
"clients" = "Các khách hàng"
|
||||
|
@ -273,7 +274,8 @@
|
|||
"resetInboundClientTrafficSuccess" = "Đã đặt lại lưu lượng"
|
||||
"trafficGetError" = "Lỗi khi lấy thông tin lưu lượng"
|
||||
"getNewX25519CertError" = "Lỗi khi lấy chứng chỉ X25519."
|
||||
"getNewmldsa65Error" = "Lỗi khi lấy chúng tôi mldsa65."
|
||||
"getNewmldsa65Error" = "Lỗi khi lấy chứng chỉ mldsa65."
|
||||
"getNewVlessEncError" = "Lỗi khi lấy chứng chỉ VlessEnc."
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "Lời yêu cầu"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "失败"
|
||||
"comment" = "评论"
|
||||
"success" = "成功"
|
||||
"lastOnline" = "上次在线"
|
||||
"getVersion" = "获取版本"
|
||||
"install" = "安装"
|
||||
"clients" = "客户端"
|
||||
|
@ -274,6 +275,7 @@
|
|||
"trafficGetError" = "获取流量数据时出错"
|
||||
"getNewX25519CertError" = "获取X25519证书时出错。"
|
||||
"getNewmldsa65Error" = "获取mldsa65证书时出错。"
|
||||
"getNewVlessEncError" = "获取VlessEnc证书时出错。"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "请求"
|
||||
|
|
|
@ -50,6 +50,7 @@
|
|||
"fail" = "失敗"
|
||||
"comment" = "評論"
|
||||
"success" = "成功"
|
||||
"lastOnline" = "上次上線"
|
||||
"getVersion" = "獲取版本"
|
||||
"install" = "安裝"
|
||||
"clients" = "客戶端"
|
||||
|
@ -274,6 +275,7 @@
|
|||
"trafficGetError" = "取得流量資料時發生錯誤"
|
||||
"getNewX25519CertError" = "取得X25519憑證時發生錯誤。"
|
||||
"getNewmldsa65Error" = "取得mldsa65憑證時發生錯誤。"
|
||||
"getNewVlessEncError" = "取得VlessEnc憑證時發生錯誤。"
|
||||
|
||||
[pages.inbounds.stream.general]
|
||||
"request" = "請求"
|
||||
|
|
|
@ -10,13 +10,6 @@ WorkingDirectory=/usr/local/x-ui/
|
|||
ExecStart=/usr/local/x-ui/x-ui
|
||||
Restart=on-failure
|
||||
RestartSec=5s
|
||||
ProtectHome=tmpfs
|
||||
ProtectKernelModules=true
|
||||
ProtectControlGroups=true
|
||||
ProtectKernelLogs=true
|
||||
ProtectHostname=true
|
||||
ProtectClock=true
|
||||
MemoryDenyWriteExecute=true
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target
|
||||
|
|
|
@ -11,4 +11,5 @@ type ClientTraffic struct {
|
|||
ExpiryTime int64 `json:"expiryTime" form:"expiryTime"`
|
||||
Total int64 `json:"total" form:"total"`
|
||||
Reset int `json:"reset" form:"reset" gorm:"default:0"`
|
||||
LastOnline int64 `json:"lastOnline" form:"lastOnline" gorm:"default:0"`
|
||||
}
|
||||
|
|
Loading…
Reference in a new issue