Compare commits

..

3 commits

Author SHA1 Message Date
fpointsstar
d16e67e462
Merge branch 'main' into patch-1 2025-10-07 21:44:42 +09:00
mhsanaei
b578a33518
update dependencies 2025-10-07 13:49:08 +02:00
mhsanaei
8153e0ac05
fragment : MaxSplit 2025-10-07 13:46:30 +02:00
5 changed files with 403 additions and 386 deletions

19
go.mod
View file

@ -6,7 +6,7 @@ require (
github.com/gin-contrib/gzip v1.2.3 github.com/gin-contrib/gzip v1.2.3
github.com/gin-contrib/sessions v1.0.4 github.com/gin-contrib/sessions v1.0.4
github.com/gin-gonic/gin v1.11.0 github.com/gin-gonic/gin v1.11.0
github.com/go-ldap/ldap/v3 v3.4.11 github.com/go-ldap/ldap/v3 v3.4.12
github.com/goccy/go-json v0.10.5 github.com/goccy/go-json v0.10.5
github.com/google/uuid v1.6.0 github.com/google/uuid v1.6.0
github.com/joho/godotenv v1.5.1 github.com/joho/godotenv v1.5.1
@ -15,7 +15,7 @@ require (
github.com/op/go-logging v0.0.0-20160315200505-970db520ece7 github.com/op/go-logging v0.0.0-20160315200505-970db520ece7
github.com/pelletier/go-toml/v2 v2.2.4 github.com/pelletier/go-toml/v2 v2.2.4
github.com/robfig/cron/v3 v3.0.1 github.com/robfig/cron/v3 v3.0.1
github.com/shirou/gopsutil/v4 v4.25.8 github.com/shirou/gopsutil/v4 v4.25.9
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e
github.com/valyala/fasthttp v1.66.0 github.com/valyala/fasthttp v1.66.0
github.com/xlzd/gotp v0.1.0 github.com/xlzd/gotp v0.1.0
@ -24,7 +24,7 @@ require (
golang.org/x/crypto v0.42.0 golang.org/x/crypto v0.42.0
golang.org/x/sys v0.36.0 golang.org/x/sys v0.36.0
golang.org/x/text v0.29.0 golang.org/x/text v0.29.0
google.golang.org/grpc v1.75.1 google.golang.org/grpc v1.76.0
gorm.io/driver/sqlite v1.6.0 gorm.io/driver/sqlite v1.6.0
gorm.io/gorm v1.31.0 gorm.io/gorm v1.31.0
) )
@ -45,7 +45,7 @@ require (
github.com/go-ole/go-ole v1.3.0 // indirect github.com/go-ole/go-ole v1.3.0 // indirect
github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/locales v0.14.1 // indirect
github.com/go-playground/universal-translator v0.18.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect
github.com/go-playground/validator/v10 v10.27.0 // indirect github.com/go-playground/validator/v10 v10.28.0 // indirect
github.com/goccy/go-yaml v1.18.0 // indirect github.com/goccy/go-yaml v1.18.0 // indirect
github.com/google/btree v1.1.3 // indirect github.com/google/btree v1.1.3 // indirect
github.com/gorilla/context v1.1.2 // indirect github.com/gorilla/context v1.1.2 // indirect
@ -70,11 +70,11 @@ require (
github.com/pires/go-proxyproto v0.8.1 // indirect github.com/pires/go-proxyproto v0.8.1 // indirect
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 // indirect
github.com/quic-go/qpack v0.5.1 // indirect github.com/quic-go/qpack v0.5.1 // indirect
github.com/quic-go/quic-go v0.54.0 // indirect github.com/quic-go/quic-go v0.55.0 // indirect
github.com/refraction-networking/utls v1.8.0 // indirect github.com/refraction-networking/utls v1.8.0 // indirect
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 // indirect
github.com/rogpeppe/go-internal v1.14.1 // indirect github.com/rogpeppe/go-internal v1.14.1 // indirect
github.com/sagernet/sing v0.7.10 // indirect github.com/sagernet/sing v0.7.12 // indirect
github.com/sagernet/sing-shadowsocks v0.2.9 // indirect github.com/sagernet/sing-shadowsocks v0.2.9 // indirect
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771 // indirect
github.com/tklauser/go-sysconf v0.3.15 // indirect github.com/tklauser/go-sysconf v0.3.15 // indirect
@ -86,9 +86,8 @@ require (
github.com/valyala/fastjson v1.6.4 // indirect github.com/valyala/fastjson v1.6.4 // indirect
github.com/vishvananda/netlink v1.3.1 // indirect github.com/vishvananda/netlink v1.3.1 // indirect
github.com/vishvananda/netns v0.0.5 // indirect github.com/vishvananda/netns v0.0.5 // indirect
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c // indirect github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196 // indirect
github.com/yusufpapurcu/wmi v1.2.4 // 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 go4.org/netipx v0.0.0-20231129151722-fdeea329fbba // indirect
golang.org/x/arch v0.21.0 // indirect golang.org/x/arch v0.21.0 // indirect
golang.org/x/mod v0.28.0 // indirect golang.org/x/mod v0.28.0 // indirect
@ -98,8 +97,8 @@ require (
golang.org/x/tools v0.37.0 // indirect golang.org/x/tools v0.37.0 // indirect
golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect golang.zx2c4.com/wintun v0.0.0-20230126152724-0fa3db229ce2 // indirect
golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb // indirect
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 // indirect google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87 // indirect
google.golang.org/protobuf v1.36.9 // indirect google.golang.org/protobuf v1.36.10 // indirect
gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect gvisor.dev/gvisor v0.0.0-20250503011706-39ed1f5ac29c // indirect
lukechampine.com/blake3 v1.4.1 // indirect lukechampine.com/blake3 v1.4.1 // indirect
) )

40
go.sum
View file

@ -2,8 +2,8 @@ github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358 h1:mFRzDkZVAjdal+
github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU= github.com/Azure/go-ntlmssp v0.0.0-20221128193559-754e69321358/go.mod h1:chxPXzSsl7ZWRAuOIE23GDNzjWuZquvFlgA8xmpunjU=
github.com/BurntSushi/toml v1.5.0 h1:W5quZX/G/csjUnuI8SUYlsHs9M38FC7znL0lIO+DvMg= 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/BurntSushi/toml v1.5.0/go.mod h1:ukJfTF/6rtPPRCnwkur4qwRxa8vTRFBF0uk2lLoLwho=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa h1:LHTHcTQiSGT7VVbI0o4wBRNQIgn917usHWOd6VAffYI= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e h1:4dAU9FXIyQktpoUAgOJK3OTFc/xug0PCXYCqU0FgDKI=
github.com/alexbrainman/sspi v0.0.0-20231016080023-1a75b4708caa/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4= github.com/alexbrainman/sspi v0.0.0-20250919150558-7d374ff0d59e/go.mod h1:cEWa1LVoE5KvSD9ONXsZrj0z6KqySlCCNKHlLzbqAt4=
github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ= github.com/andybalholm/brotli v1.2.0 h1:ukwgCxwYrmACq68yiUqwIWnGY0cTPox/M94sVwToPjQ=
github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY= github.com/andybalholm/brotli v1.2.0/go.mod h1:rzTDkvFWvIrjDXZHkuS16NPggd91W3kUSvPlQ1pLaKY=
github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M= github.com/bytedance/gopkg v0.1.3 h1:TPBSwH8RsouGCBcMBktLt1AymVo2TVsBVCY4b6TnZ/M=
@ -39,8 +39,8 @@ github.com/gin-gonic/gin v1.11.0 h1:OW/6PLjyusp2PPXtyxKHU0RbX6I/l28FTdDlae5ueWk=
github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls= github.com/gin-gonic/gin v1.11.0/go.mod h1:+iq/FyxlGzII0KHiBGjuNn4UNENUlKbGlNmc+W50Dls=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667 h1:BP4M0CvQ4S3TGls2FvczZtj5Re/2ZzkV9VwqPHH/3Bo=
github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0= github.com/go-asn1-ber/asn1-ber v1.5.8-0.20250403174932-29230038a667/go.mod h1:hEBeB/ic+5LoWskz+yKT7vGhhPYkProFKoKdwZRWMe0=
github.com/go-ldap/ldap/v3 v3.4.11 h1:4k0Yxweg+a3OyBLjdYn5OKglv18JNvfDykSoI8bW0gU= github.com/go-ldap/ldap/v3 v3.4.12 h1:1b81mv7MagXZ7+1r7cLTWmyuTqVqdwbtJSjC0DAp9s4=
github.com/go-ldap/ldap/v3 v3.4.11/go.mod h1:bY7t0FLK8OAVpp/vV6sSlpz3EQDGcQwc8pF0ujLgKvM= github.com/go-ldap/ldap/v3 v3.4.12/go.mod h1:+SPAGcTtOfmGsCb3h1RFiq4xpp4N636G75OEace8lNo=
github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI= github.com/go-logr/logr v1.4.3 h1:CjnDlHq8ikf6E492q6eKboGOC0T8CDaOvkHCIg8idEI=
github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY= github.com/go-logr/logr v1.4.3/go.mod h1:9T104GzyrTigFIr8wt5mBrctHMim0Nb2HLGrmQ40KvY=
github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag= github.com/go-logr/stdr v1.2.2 h1:hSWxHoqTgW2S2qGc0LTAI563KZ5YKYRhT3MFKZMbjag=
@ -54,8 +54,8 @@ github.com/go-playground/locales v0.14.1 h1:EWaQ/wswjilfKLTECiXz7Rh+3BjFhfDFKv/o
github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY= github.com/go-playground/locales v0.14.1/go.mod h1:hxrqLVvrK65+Rwrd5Fc6F2O76J/NuW9t0sjnWqG1slY=
github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY= github.com/go-playground/universal-translator v0.18.1 h1:Bcnm0ZwsGyWbCzImXv+pAJnYK9S473LQFuzCbDbfSFY=
github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY= github.com/go-playground/universal-translator v0.18.1/go.mod h1:xekY+UJKNuX9WP91TpwSH2VMlDf28Uj24BCp08ZFTUY=
github.com/go-playground/validator/v10 v10.27.0 h1:w8+XrWVMhGkxOaaowyKH35gFydVHOvC0/uWoy2Fzwn4= github.com/go-playground/validator/v10 v10.28.0 h1:Q7ibns33JjyW48gHkuFT91qX48KG0ktULL6FgHdG688=
github.com/go-playground/validator/v10 v10.27.0/go.mod h1:I5QpIEbmr8On7W0TktmJAumgzX4CA1XNl4ZmDuVHKKo= github.com/go-playground/validator/v10 v10.28.0/go.mod h1:GoI6I1SjPBh9p7ykNE/yj3fFYbyDOpwMn5KXd+m2hUU=
github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4=
github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M=
github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw= github.com/goccy/go-yaml v1.18.0 h1:8W7wMFS12Pcas7KU+VVkaiCng+kG8QiFeFwzFb+rwuw=
@ -148,8 +148,8 @@ github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55 h1:o4JXh1EVt
github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE= github.com/power-devops/perfstat v0.0.0-20240221224432-82ca36839d55/go.mod h1:OmDBASR4679mdNQnz2pUhc2G8CO2JrUAVFDRBDP/hJE=
github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI= github.com/quic-go/qpack v0.5.1 h1:giqksBPnT/HDtZ6VhtFKgoLOWmlyo9Ei6u9PqzIMbhI=
github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg= github.com/quic-go/qpack v0.5.1/go.mod h1:+PC4XFrEskIVkcLzpEkbLqq1uCoxPhQuvK5rH1ZgaEg=
github.com/quic-go/quic-go v0.54.0 h1:6s1YB9QotYI6Ospeiguknbp2Znb/jZYjZLRXn9kMQBg= github.com/quic-go/quic-go v0.55.0 h1:zccPQIqYCXDt5NmcEabyYvOnomjs8Tlwl7tISjJh9Mk=
github.com/quic-go/quic-go v0.54.0/go.mod h1:e68ZEaCdyviluZmy44P6Iey98v/Wfz6HCjQEm+l8zTY= github.com/quic-go/quic-go v0.55.0/go.mod h1:DR51ilwU1uE164KuWXhinFcKWGlEjzys2l8zUl5Ss1U=
github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE= github.com/refraction-networking/utls v1.8.0 h1:L38krhiTAyj9EeiQQa2sg+hYb4qwLCqdMcpZrRfbONE=
github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM= github.com/refraction-networking/utls v1.8.0/go.mod h1:jkSOEkLqn+S/jtpEHPOsVv/4V4EVnelwbMQl4vCWXAM=
github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg= github.com/riobard/go-bloom v0.0.0-20200614022211-cdc8013cb5b3 h1:f/FNXud6gA3MNr8meMVVGxhp+QBTqY91tM8HjEuMjGg=
@ -158,14 +158,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/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 h1:UQB4HGPB6osV0SQTLymcB4TgvyWu6ZyliaW0tI/otEQ=
github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc= github.com/rogpeppe/go-internal v1.14.1/go.mod h1:MaRKkUm5W0goXpeCfT7UZI6fk/L7L7so1lCWt35ZSgc=
github.com/sagernet/sing v0.7.10 h1:2yPhZFx+EkyHPH8hXNezgyRSHyGY12CboId7CtwLROw= github.com/sagernet/sing v0.7.12 h1:MpMbO56crPRZTbltoj1wGk4Xj9+GiwH1wTO4s3fz1EA=
github.com/sagernet/sing v0.7.10/go.mod h1:ARkL0gM13/Iv5VCZmci/NuoOlePoIsW0m7BWfln/Hak= github.com/sagernet/sing v0.7.12/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 h1:Paep5zCszRKsEn8587O0MnhFWKJwDW1Y4zOYYlIxMkM=
github.com/sagernet/sing-shadowsocks v0.2.9/go.mod h1:TE/Z6401Pi8tgr0nBZcM/xawAI6u3F6TTbz4nH/qw+8= 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 h1:emzAzMZ1L9iaKCTxdy3Em8Wv4ChIAGnfiz18Cda70g4=
github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg= github.com/seiflotfy/cuckoofilter v0.0.0-20240715131351-a2f2c23f1771/go.mod h1:bR6DqgcAl1zTcOX8/pE2Qkj9XO00eCNqmKb7lXP8EAg=
github.com/shirou/gopsutil/v4 v4.25.8 h1:NnAsw9lN7587WHxjJA9ryDnqhJpFH6A+wagYWTOH970= github.com/shirou/gopsutil/v4 v4.25.9 h1:JImNpf6gCVhKgZhtaAHJ0serfFGtlfIlSC08eaKdTrU=
github.com/shirou/gopsutil/v4 v4.25.8/go.mod h1:q9QdMmfAOVIw7a+eF86P7ISEU6ka+NLgkUxlopV4RwI= github.com/shirou/gopsutil/v4 v4.25.9/go.mod h1:gxIxoC+7nQRwUl/xNhutXlD8lq+jxTgpIkEf3rADHL8=
github.com/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e h1:MRM5ITcdelLK2j1vwZ3Je0FKVCfqOLp5zO6trqMLYs0= 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/skip2/go-qrcode v0.0.0-20200617195104-da1b6568686e/go.mod h1:XV66xRDqSt+GTGFMVlhk3ULuV0y9ZmzeVGR4mloJI3M=
github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME= github.com/stretchr/objx v0.1.0/go.mod h1:HFkY916IF+rwdDfMAkV7OtwuqBVzrE8GR6GFx+wExME=
@ -200,8 +200,8 @@ github.com/vishvananda/netns v0.0.5 h1:DfiHV+j8bA32MFM7bfEunvT8IAqQ/NzSJHtcmW5zd
github.com/vishvananda/netns v0.0.5/go.mod h1:SpkAiCQRtJ6TvvxPnOSyH3BMl6unz3xZlaprSwhNNJM= 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 h1:37blvlKCh38s+fkem+fFh7sMnceltoIEBYTVXyoa5Po=
github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg= github.com/xlzd/gotp v0.1.0/go.mod h1:ndLJ3JKzi3xLmUProq4LLxCuECL93dG9WASNLpHz8qg=
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c h1:LHLhQY3mKXSpTcQAkjFR4/6ar3rXjQryNeM7khK3AHU= github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196 h1:jb1y+Rm6UBW/CEV0FehsKlQ/2dnLsQjyUjn3UfWwbic=
github.com/xtls/reality v0.0.0-20250904214705-431b6ff8c67c/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0= github.com/xtls/reality v0.0.0-20251005124704-8f4f0a188196/go.mod h1:XxvnCCgBee4WWE0bc4E+a7wbk8gkJ/rS0vNVNtC5qp0=
github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno= github.com/xtls/xray-core v1.250911.0 h1:KMN8zVurAjHFixiUoFV/jwmzYohf27dQRntjV+8LQno=
github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k= github.com/xtls/xray-core v1.250911.0/go.mod h1:LkqA/BFVtPS2e5fRzg/bkYas9nQu4Uztlx+/fjlLM9k=
github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU= github.com/xyproto/randomstring v1.0.5 h1:YtlWPoRdgMu3NZtP45drfy1GKoojuR7hmRcnhZqKjWU=
@ -256,12 +256,12 @@ 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= golang.zx2c4.com/wireguard v0.0.0-20250521234502-f333402bd9cb/go.mod h1:rpwXGsirqLqN2L0JDJQlwOboGHmptD5ZD6T2VmcqhTw=
gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk= gonum.org/v1/gonum v0.16.0 h1:5+ul4Swaf3ESvrOnidPp4GZbzf0mxVQpDCYUQE7OJfk=
gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E= gonum.org/v1/gonum v0.16.0/go.mod h1:fef3am4MQ93R2HHpKnLk4/Tbh/s0+wqD5nfa6Pnwy4E=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9 h1:V1jCN2HBa8sySkR5vLcCSqJSTMv093Rw9EJefhQGP7M= google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87 h1:WgGZrMngVRRve7T3P5gbXdmedSmUpkf8uIUu1fg+biY=
google.golang.org/genproto/googleapis/rpc v0.0.0-20250922171735-9219d122eba9/go.mod h1:HSkG/KdJWusxU1F6CNrwNDjBMgisKxGnc5dAZfT0mjQ= google.golang.org/genproto/googleapis/rpc v0.0.0-20251006185510-65f7160b3a87/go.mod h1:7i2o+ce6H/6BluujYR+kqX3GKH+dChPTQU19wjRPiGk=
google.golang.org/grpc v1.75.1 h1:/ODCNEuf9VghjgO3rqLcfg8fiOP0nSluljWFlDxELLI= google.golang.org/grpc v1.76.0 h1:UnVkv1+uMLYXoIz6o7chp59WfQUYA2ex/BXQ9rHZu7A=
google.golang.org/grpc v1.75.1/go.mod h1:JtPAzKiq4v1xcAB2hydNlWI2RnF85XXcV0mhKXr2ecQ= google.golang.org/grpc v1.76.0/go.mod h1:Ju12QI8M6iQJtbcsV+awF5a4hfJMLi4X0JLo94ULZ6c=
google.golang.org/protobuf v1.36.9 h1:w2gp2mA27hUeUzj9Ex9FBjsBm40zfaDtEWow293U7Iw= google.golang.org/protobuf v1.36.10 h1:AYd7cD/uASjIL6Q9LiTjz8JLcrh/88q5UObnmY3aOOE=
google.golang.org/protobuf v1.36.9/go.mod h1:fuxRtAxBytpl4zzqUh6/eyUujkJdNiuEkXntxiD/uRU= google.golang.org/protobuf v1.36.10/go.mod h1:HTf+CrKn2C3g5S8VImy6tdcUvCska2kB7j23XfzDpco=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c h1:Hei/4ADfdWqJk1ZMxUNpqntNwaWcugrBjAiHlqqRiVk=
gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q= gopkg.in/check.v1 v1.0.0-20201130134442-10cb98267c6c/go.mod h1:JHkPIbrfpd72SG/EVd6muEfDQjcINNoR0C8j2r3qZ4Q=

View file

@ -9,19 +9,20 @@
<a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'> <a-spin :spinning="loadingStates.spinning" :delay="500" tip='{{ i18n "loading"}}'>
<transition name="list" appear> <transition name="list" appear>
<a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }" <a-alert type="error" v-if="confAlerts.length>0 && loadingStates.fetched" :style="{ marginBottom: '10px' }"
message='{{ i18n "secAlertTitle" }}' message='{{ i18n "secAlertTitle" }}' color="red" show-icon closable>
color="red"
show-icon closable>
<template slot="description"> <template slot="description">
<b>{{ i18n "secAlertConf" }}</b> <b>{{ i18n "secAlertConf" }}</b>
<ul><li v-for="a in confAlerts">[[ a ]]</li></ul> <ul>
<li v-for="a in confAlerts">[[ a ]]</li>
</ul>
</template> </template>
</a-alert> </a-alert>
</transition> </transition>
<transition name="list" appear> <transition name="list" appear>
<template> <template>
<a-row v-if="!loadingStates.fetched"> <a-row v-if="!loadingStates.fetched">
<a-card :style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }"> <a-card
:style="{ textAlign: 'center', padding: '30px 0', marginTop: '10px', background: 'transparent', border: 'none' }">
<a-spin tip='{{ i18n "loading" }}'></a-spin> <a-spin tip='{{ i18n "loading" }}'></a-spin>
</a-card> </a-card>
</a-row> </a-row>
@ -31,17 +32,19 @@
<a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }"> <a-row :style="{ display: 'flex', flexWrap: 'wrap', alignItems: 'center' }">
<a-col :xs="24" :sm="10" :style="{ padding: '4px' }"> <a-col :xs="24" :sm="10" :style="{ padding: '4px' }">
<a-space direction="horizontal"> <a-space direction="horizontal">
<a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n "pages.settings.save" }}</a-button> <a-button type="primary" :disabled="saveBtnDisable" @click="updateAllSetting">{{ i18n
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n "pages.settings.restartPanel" }}</a-button> "pages.settings.save" }}</a-button>
<a-button type="danger" :disabled="!saveBtnDisable" @click="restartPanel">{{ i18n
"pages.settings.restartPanel" }}</a-button>
</a-space> </a-space>
</a-col> </a-col>
<a-col :xs="24" :sm="14"> <a-col :xs="24" :sm="14">
<template> <template>
<div> <div>
<a-back-top :target="() => document.getElementById('content-layout')" visibility-height="200"></a-back-top> <a-back-top :target="() => document.getElementById('content-layout')"
visibility-height="200"></a-back-top>
<a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }" <a-alert type="warning" :style="{ float: 'right', width: 'fit-content' }"
message='{{ i18n "pages.settings.infoDesc" }}' message='{{ i18n "pages.settings.infoDesc" }}' show-icon>
show-icon>
</a-alert> </a-alert>
</div> </div>
</template> </template>
@ -132,7 +135,8 @@
fragment: { fragment: {
packets: "tlshello", packets: "tlshello",
length: "100-200", length: "100-200",
interval: "10-20" interval: "10-20",
maxSplit: "300-400"
} }
}, },
streamSettings: { streamSettings: {
@ -381,11 +385,11 @@
}, },
computed: { computed: {
ldapInboundTagList: { ldapInboundTagList: {
get: function() { get: function () {
const csv = this.allSetting.ldapInboundTags || ""; const csv = this.allSetting.ldapInboundTags || "";
return csv.length ? csv.split(',').map(s => s.trim()).filter(Boolean) : []; 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(',') : ''; this.allSetting.ldapInboundTags = Array.isArray(list) ? list.join(',') : '';
} }
}, },
@ -425,6 +429,16 @@
} }
} }
}, },
fragmentMaxSplit: {
get: function () { return this.fragment ? JSON.parse(this.allSetting.subJsonFragment).settings.fragment.maxSplit : ""; },
set: function (v) {
if (v != "") {
newFragment = JSON.parse(this.allSetting.subJsonFragment);
newFragment.settings.fragment.maxSplit = v;
this.allSetting.subJsonFragment = JSON.stringify(newFragment);
}
}
},
noises: { noises: {
get() { get() {
return this.allSetting?.subJsonNoises != ""; return this.allSetting?.subJsonNoises != "";

View file

@ -5,13 +5,10 @@
<template #title>{{ i18n "pages.settings.subPath"}}</template> <template #title>{{ i18n "pages.settings.subPath"}}</template>
<template #description>{{ i18n "pages.settings.subPathDesc"}}</template> <template #description>{{ i18n "pages.settings.subPathDesc"}}</template>
<template #control> <template #control>
<a-input <a-input type="text" v-model="allSetting.subJsonPath"
type="text"
v-model="allSetting.subJsonPath"
@input="allSetting.subJsonPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')" @input="allSetting.subJsonPath = ((typeof $event === 'string' ? $event : ($event && $event.target ? $event.target.value : '')) || '').replace(/[:*]/g, '')"
@blur="allSetting.subJsonPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subJsonPath)" @blur="allSetting.subJsonPath = (p => { p = p || '/'; if (!p.startsWith('/')) p='/' + p; if (!p.endsWith('/')) p += '/'; return p.replace(/\/+/g,'/'); })(allSetting.subJsonPath)"
placeholder="/json/" placeholder="/json/"></a-input>
></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small"> <a-setting-list-item paddings="small">
@ -53,6 +50,12 @@
<a-input type="text" v-model="fragmentInterval" placeholder="10-20"></a-input> <a-input type="text" v-model="fragmentInterval" placeholder="10-20"></a-input>
</template> </template>
</a-setting-list-item> </a-setting-list-item>
<a-setting-list-item paddings="small">
<template #title>MaxSplit</template>
<template #control>
<a-input type="text" v-model="fragmentMaxSplit" placeholder="300-400"></a-input>
</template>
</a-setting-list-item>
</a-collapse-panel> </a-collapse-panel>
</a-collapse> </a-collapse>
</a-list-item> </a-list-item>
@ -74,7 +77,8 @@
<a-select :value="noise.type" :style="{ width: '100%' }" <a-select :value="noise.type" :style="{ width: '100%' }"
:dropdown-class-name="themeSwitcher.currentTheme" :dropdown-class-name="themeSwitcher.currentTheme"
@change="(value) => updateNoiseType(index, value)"> @change="(value) => updateNoiseType(index, value)">
<a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']" :key="p"> <a-select-option :value="p" :label="p" v-for="p in ['rand', 'base64', 'str', 'hex']"
:key="p">
<span>[[ p ]]</span> <span>[[ p ]]</span>
</a-select-option> </a-select-option>
</a-select> </a-select>

View file

@ -1,421 +1,421 @@
package job package job
import ( import (
"time" "time"
"github.com/mhsanaei/3x-ui/v2/database/model" "strings"
"github.com/mhsanaei/3x-ui/v2/logger"
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
"github.com/mhsanaei/3x-ui/v2/web/service"
"strings"
"github.com/google/uuid" "github.com/mhsanaei/3x-ui/v2/database/model"
"strconv" "github.com/mhsanaei/3x-ui/v2/logger"
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
"github.com/mhsanaei/3x-ui/v2/web/service"
"strconv"
"github.com/google/uuid"
) )
var DefaultTruthyValues = []string{"true", "1", "yes", "on"} var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
type LdapSyncJob struct { type LdapSyncJob struct {
settingService service.SettingService settingService service.SettingService
inboundService service.InboundService inboundService service.InboundService
xrayService service.XrayService xrayService service.XrayService
} }
// --- Helper functions for mustGet --- // --- Helper functions for mustGet ---
func mustGetString(fn func() (string, error)) string { func mustGetString(fn func() (string, error)) string {
v, err := fn() v, err := fn()
if err != nil { if err != nil {
panic(err) panic(err)
} }
return v return v
} }
func mustGetInt(fn func() (int, error)) int { func mustGetInt(fn func() (int, error)) int {
v, err := fn() v, err := fn()
if err != nil { if err != nil {
panic(err) panic(err)
} }
return v return v
} }
func mustGetBool(fn func() (bool, error)) bool { func mustGetBool(fn func() (bool, error)) bool {
v, err := fn() v, err := fn()
if err != nil { if err != nil {
panic(err) panic(err)
} }
return v return v
} }
func mustGetStringOr(fn func() (string, error), fallback string) string { func mustGetStringOr(fn func() (string, error), fallback string) string {
v, err := fn() v, err := fn()
if err != nil || v == "" { if err != nil || v == "" {
return fallback return fallback
} }
return v return v
} }
func NewLdapSyncJob() *LdapSyncJob { func NewLdapSyncJob() *LdapSyncJob {
return new(LdapSyncJob) return new(LdapSyncJob)
} }
func (j *LdapSyncJob) Run() { func (j *LdapSyncJob) Run() {
logger.Info("LDAP sync job started") logger.Info("LDAP sync job started")
enabled, err := j.settingService.GetLdapEnable() enabled, err := j.settingService.GetLdapEnable()
if err != nil || !enabled { if err != nil || !enabled {
logger.Warning("LDAP disabled or failed to fetch flag") logger.Warning("LDAP disabled or failed to fetch flag")
return return
} }
// --- LDAP fetch --- // --- LDAP fetch ---
cfg := ldaputil.Config{ cfg := ldaputil.Config{
Host: mustGetString(j.settingService.GetLdapHost), Host: mustGetString(j.settingService.GetLdapHost),
Port: mustGetInt(j.settingService.GetLdapPort), Port: mustGetInt(j.settingService.GetLdapPort),
UseTLS: mustGetBool(j.settingService.GetLdapUseTLS), UseTLS: mustGetBool(j.settingService.GetLdapUseTLS),
BindDN: mustGetString(j.settingService.GetLdapBindDN), BindDN: mustGetString(j.settingService.GetLdapBindDN),
Password: mustGetString(j.settingService.GetLdapPassword), Password: mustGetString(j.settingService.GetLdapPassword),
BaseDN: mustGetString(j.settingService.GetLdapBaseDN), BaseDN: mustGetString(j.settingService.GetLdapBaseDN),
UserFilter: mustGetString(j.settingService.GetLdapUserFilter), UserFilter: mustGetString(j.settingService.GetLdapUserFilter),
UserAttr: mustGetString(j.settingService.GetLdapUserAttr), UserAttr: mustGetString(j.settingService.GetLdapUserAttr),
FlagField: mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)), FlagField: mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)),
TruthyVals: splitCsv(mustGetString(j.settingService.GetLdapTruthyValues)), TruthyVals: splitCsv(mustGetString(j.settingService.GetLdapTruthyValues)),
Invert: mustGetBool(j.settingService.GetLdapInvertFlag), Invert: mustGetBool(j.settingService.GetLdapInvertFlag),
} }
flags, err := ldaputil.FetchVlessFlags(cfg) flags, err := ldaputil.FetchVlessFlags(cfg)
if err != nil { if err != nil {
logger.Warning("LDAP fetch failed:", err) logger.Warning("LDAP fetch failed:", err)
return return
} }
logger.Infof("Fetched %d LDAP flags", len(flags)) logger.Infof("Fetched %d LDAP flags", len(flags))
// --- Load all inbounds and all clients once --- // --- Load all inbounds and all clients once ---
inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags)) inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags))
inbounds, err := j.inboundService.GetAllInbounds() inbounds, err := j.inboundService.GetAllInbounds()
if err != nil { if err != nil {
logger.Warning("Failed to get inbounds:", err) logger.Warning("Failed to get inbounds:", err)
return return
} }
allClients := map[string]*model.Client{} // email -> client allClients := map[string]*model.Client{} // email -> client
inboundMap := map[string]*model.Inbound{} // tag -> inbound inboundMap := map[string]*model.Inbound{} // tag -> inbound
for _, ib := range inbounds { for _, ib := range inbounds {
inboundMap[ib.Tag] = ib inboundMap[ib.Tag] = ib
clients, _ := j.inboundService.GetClients(ib) clients, _ := j.inboundService.GetClients(ib)
for i := range clients { for i := range clients {
allClients[clients[i].Email] = &clients[i] allClients[clients[i].Email] = &clients[i]
} }
} }
// --- Prepare batch operations --- // --- Prepare batch operations ---
autoCreate := mustGetBool(j.settingService.GetLdapAutoCreate) autoCreate := mustGetBool(j.settingService.GetLdapAutoCreate)
defGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB) defGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB)
defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays) defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays)
defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP) defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP)
clientsToCreate := map[string][]model.Client{} // tag -> []new clients clientsToCreate := map[string][]model.Client{} // tag -> []new clients
clientsToEnable := map[string][]string{} // tag -> []email clientsToEnable := map[string][]string{} // tag -> []email
clientsToDisable := map[string][]string{} // tag -> []email clientsToDisable := map[string][]string{} // tag -> []email
for email, allowed := range flags { for email, allowed := range flags {
exists := allClients[email] != nil exists := allClients[email] != nil
for _, tag := range inboundTags { for _, tag := range inboundTags {
if !exists && allowed && autoCreate { if !exists && allowed && autoCreate {
newClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP) newClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP)
clientsToCreate[tag] = append(clientsToCreate[tag], newClient) clientsToCreate[tag] = append(clientsToCreate[tag], newClient)
} else if exists { } else if exists {
if allowed && !allClients[email].Enable { if allowed && !allClients[email].Enable {
clientsToEnable[tag] = append(clientsToEnable[tag], email) clientsToEnable[tag] = append(clientsToEnable[tag], email)
} else if !allowed && allClients[email].Enable { } else if !allowed && allClients[email].Enable {
clientsToDisable[tag] = append(clientsToDisable[tag], email) clientsToDisable[tag] = append(clientsToDisable[tag], email)
} }
} }
} }
} }
// --- Execute batch create --- // --- Execute batch create ---
for tag, newClients := range clientsToCreate { for tag, newClients := range clientsToCreate {
if len(newClients) == 0 { if len(newClients) == 0 {
continue continue
} }
payload := &model.Inbound{Id: inboundMap[tag].Id} payload := &model.Inbound{Id: inboundMap[tag].Id}
payload.Settings = j.clientsToJSON(newClients) payload.Settings = j.clientsToJSON(newClients)
if _, err := j.inboundService.AddInboundClient(payload); err != nil { if _, err := j.inboundService.AddInboundClient(payload); err != nil {
logger.Warningf("Failed to add clients for tag %s: %v", tag, err) logger.Warningf("Failed to add clients for tag %s: %v", tag, err)
} else { } else {
logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag) logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag)
j.xrayService.SetToNeedRestart() j.xrayService.SetToNeedRestart()
} }
} }
// --- Execute enable/disable batch --- // --- Execute enable/disable batch ---
for tag, emails := range clientsToEnable { for tag, emails := range clientsToEnable {
j.batchSetEnable(inboundMap[tag], emails, true) j.batchSetEnable(inboundMap[tag], emails, true)
} }
for tag, emails := range clientsToDisable { for tag, emails := range clientsToDisable {
j.batchSetEnable(inboundMap[tag], emails, false) j.batchSetEnable(inboundMap[tag], emails, false)
} }
// --- Auto delete clients not in LDAP --- // --- Auto delete clients not in LDAP ---
autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete) autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete)
if autoDelete { if autoDelete {
ldapEmailSet := map[string]struct{}{} ldapEmailSet := map[string]struct{}{}
for e := range flags { for e := range flags {
ldapEmailSet[e] = struct{}{} ldapEmailSet[e] = struct{}{}
} }
for _, tag := range inboundTags { for _, tag := range inboundTags {
j.deleteClientsNotInLDAP(tag, ldapEmailSet) j.deleteClientsNotInLDAP(tag, ldapEmailSet)
} }
} }
} }
func splitCsv(s string) []string { func splitCsv(s string) []string {
if s == "" { if s == "" {
return DefaultTruthyValues return DefaultTruthyValues
} }
parts := strings.Split(s, ",") parts := strings.Split(s, ",")
out := make([]string, 0, len(parts)) out := make([]string, 0, len(parts))
for _, p := range parts { for _, p := range parts {
v := strings.TrimSpace(p) v := strings.TrimSpace(p)
if v != "" { if v != "" {
out = append(out, v) out = append(out, v)
} }
} }
return out return out
} }
// buildClient creates a new client for auto-create // buildClient creates a new client for auto-create
func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client { func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client {
c := model.Client{ c := model.Client{
Email: email, Email: email,
Enable: true, Enable: true,
LimitIP: defLimitIP, LimitIP: defLimitIP,
TotalGB: int64(defGB), TotalGB: int64(defGB),
} }
if defExpiryDays > 0 { if defExpiryDays > 0 {
c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli() c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
} }
switch ib.Protocol { switch ib.Protocol {
case model.Trojan, model.Shadowsocks: case model.Trojan, model.Shadowsocks:
c.Password = uuid.NewString() c.Password = uuid.NewString()
default: default:
c.ID = uuid.NewString() c.ID = uuid.NewString()
} }
return c return c
} }
// batchSetEnable enables/disables clients in batch through a single call // batchSetEnable enables/disables clients in batch through a single call
func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) { func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
if len(emails) == 0 { if len(emails) == 0 {
return return
} }
// Prepare JSON for mass update // Prepare JSON for mass update
clients := make([]model.Client, 0, len(emails)) clients := make([]model.Client, 0, len(emails))
for _, email := range emails { for _, email := range emails {
clients = append(clients, model.Client{ clients = append(clients, model.Client{
Email: email, Email: email,
Enable: enable, Enable: enable,
}) })
} }
payload := &model.Inbound{ payload := &model.Inbound{
Id: ib.Id, Id: ib.Id,
Settings: j.clientsToJSON(clients), Settings: j.clientsToJSON(clients),
} }
// Use a single AddInboundClient call to update enable // Use a single AddInboundClient call to update enable
if _, err := j.inboundService.AddInboundClient(payload); err != nil { if _, err := j.inboundService.AddInboundClient(payload); err != nil {
logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err) logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err)
return return
} }
logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag) logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag)
j.xrayService.SetToNeedRestart() j.xrayService.SetToNeedRestart()
} }
// deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart // deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart
func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) { func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {
inbounds, err := j.inboundService.GetAllInbounds() inbounds, err := j.inboundService.GetAllInbounds()
if err != nil { if err != nil {
logger.Warning("Failed to get inbounds for deletion:", err) logger.Warning("Failed to get inbounds for deletion:", err)
return return
} }
batchSize := 50 // clients in 1 batch batchSize := 50 // clients in 1 batch
restartNeeded := false restartNeeded := false
for _, ib := range inbounds { for _, ib := range inbounds {
if ib.Tag != inboundTag { if ib.Tag != inboundTag {
continue continue
} }
clients, err := j.inboundService.GetClients(ib) clients, err := j.inboundService.GetClients(ib)
if err != nil { if err != nil {
logger.Warningf("Failed to get clients for inbound %s: %v", ib.Tag, err) logger.Warningf("Failed to get clients for inbound %s: %v", ib.Tag, err)
continue continue
} }
// Collect clients for deletion // Collect clients for deletion
toDelete := []model.Client{} toDelete := []model.Client{}
for _, c := range clients { for _, c := range clients {
if _, ok := ldapEmails[c.Email]; !ok { if _, ok := ldapEmails[c.Email]; !ok {
toDelete = append(toDelete, c) toDelete = append(toDelete, c)
} }
} }
if len(toDelete) == 0 { if len(toDelete) == 0 {
continue continue
} }
// Delete in batches // Delete in batches
for i := 0; i < len(toDelete); i += batchSize { for i := 0; i < len(toDelete); i += batchSize {
end := i + batchSize end := i + batchSize
if end > len(toDelete) { if end > len(toDelete) {
end = len(toDelete) end = len(toDelete)
} }
batch := toDelete[i:end] batch := toDelete[i:end]
for _, c := range batch { for _, c := range batch {
var clientKey string var clientKey string
switch ib.Protocol { switch ib.Protocol {
case model.Trojan: case model.Trojan:
clientKey = c.Password clientKey = c.Password
case model.Shadowsocks: case model.Shadowsocks:
clientKey = c.Email clientKey = c.Email
default: // vless/vmess default: // vless/vmess
clientKey = c.ID clientKey = c.ID
} }
if _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); err != nil { if _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); err != nil {
logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v", logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v",
c.Email, ib.Id, ib.Tag, err) c.Email, ib.Id, ib.Tag, err)
} else { } else {
logger.Infof("Deleted client %s from inbound id=%d(tag=%s)", logger.Infof("Deleted client %s from inbound id=%d(tag=%s)",
c.Email, ib.Id, ib.Tag) c.Email, ib.Id, ib.Tag)
// do not restart here // do not restart here
restartNeeded = true restartNeeded = true
} }
} }
} }
} }
// One time after all batches // One time after all batches
if restartNeeded { if restartNeeded {
j.xrayService.SetToNeedRestart() j.xrayService.SetToNeedRestart()
logger.Info("Xray restart scheduled after batch deletion") logger.Info("Xray restart scheduled after batch deletion")
} }
} }
// clientsToJSON serializes an array of clients to JSON // clientsToJSON serializes an array of clients to JSON
func (j *LdapSyncJob) clientsToJSON(clients []model.Client) string { func (j *LdapSyncJob) clientsToJSON(clients []model.Client) string {
b := strings.Builder{} b := strings.Builder{}
b.WriteString("{\"clients\":[") b.WriteString("{\"clients\":[")
for i, c := range clients { for i, c := range clients {
if i > 0 { b.WriteString(",") } if i > 0 {
b.WriteString(j.clientToJSON(c)) b.WriteString(",")
} }
b.WriteString("]}") b.WriteString(j.clientToJSON(c))
return b.String() }
b.WriteString("]}")
return b.String()
} }
// ensureClientExists adds client with defaults to inbound tag if not present // ensureClientExists adds client with defaults to inbound tag if not present
func (j *LdapSyncJob) ensureClientExists(inboundTag string, email string, defGB int, defExpiryDays int, defLimitIP int) { func (j *LdapSyncJob) ensureClientExists(inboundTag string, email string, defGB int, defExpiryDays int, defLimitIP int) {
inbounds, err := j.inboundService.GetAllInbounds() inbounds, err := j.inboundService.GetAllInbounds()
if err != nil { if err != nil {
logger.Warning("ensureClientExists: get inbounds failed:", err) logger.Warning("ensureClientExists: get inbounds failed:", err)
return return
} }
var target *model.Inbound var target *model.Inbound
for _, ib := range inbounds { for _, ib := range inbounds {
if ib.Tag == inboundTag { if ib.Tag == inboundTag {
target = ib target = ib
break break
} }
} }
if target == nil { if target == nil {
logger.Debugf("ensureClientExists: inbound tag %s not found", inboundTag) logger.Debugf("ensureClientExists: inbound tag %s not found", inboundTag)
return return
} }
// check if email already exists in this inbound // check if email already exists in this inbound
clients, err := j.inboundService.GetClients(target) clients, err := j.inboundService.GetClients(target)
if err == nil { if err == nil {
for _, c := range clients { for _, c := range clients {
if c.Email == email { if c.Email == email {
return return
} }
} }
} }
// build new client according to protocol // build new client according to protocol
newClient := model.Client{ newClient := model.Client{
Email: email, Email: email,
Enable: true, Enable: true,
LimitIP: defLimitIP, LimitIP: defLimitIP,
TotalGB: int64(defGB), TotalGB: int64(defGB),
} }
if defExpiryDays > 0 { if defExpiryDays > 0 {
newClient.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli() newClient.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
} }
switch target.Protocol { switch target.Protocol {
case model.Trojan: case model.Trojan:
newClient.Password = uuid.NewString() newClient.Password = uuid.NewString()
case model.Shadowsocks: case model.Shadowsocks:
newClient.Password = uuid.NewString() newClient.Password = uuid.NewString()
default: // VMESS/VLESS and others using ID default: // VMESS/VLESS and others using ID
newClient.ID = uuid.NewString() newClient.ID = uuid.NewString()
} }
// prepare inbound payload with only the new client // prepare inbound payload with only the new client
payload := &model.Inbound{Id: target.Id} payload := &model.Inbound{Id: target.Id}
payload.Settings = `{"clients":[` + j.clientToJSON(newClient) + `]}` payload.Settings = `{"clients":[` + j.clientToJSON(newClient) + `]}`
if _, err := j.inboundService.AddInboundClient(payload); err != nil { if _, err := j.inboundService.AddInboundClient(payload); err != nil {
logger.Warning("ensureClientExists: add client failed:", err) logger.Warning("ensureClientExists: add client failed:", err)
} else { } else {
j.xrayService.SetToNeedRestart() j.xrayService.SetToNeedRestart()
logger.Infof("LDAP auto-create: %s in %s", email, inboundTag) logger.Infof("LDAP auto-create: %s in %s", email, inboundTag)
} }
} }
// clientToJSON serializes minimal client fields to JSON object string without extra deps // clientToJSON serializes minimal client fields to JSON object string without extra deps
func (j *LdapSyncJob) clientToJSON(c model.Client) string { func (j *LdapSyncJob) clientToJSON(c model.Client) string {
// construct minimal JSON manually to avoid importing json for simple case // construct minimal JSON manually to avoid importing json for simple case
b := strings.Builder{} b := strings.Builder{}
b.WriteString("{") b.WriteString("{")
if c.ID != "" { if c.ID != "" {
b.WriteString("\"id\":\"") b.WriteString("\"id\":\"")
b.WriteString(c.ID) b.WriteString(c.ID)
b.WriteString("\",") b.WriteString("\",")
} }
if c.Password != "" { if c.Password != "" {
b.WriteString("\"password\":\"") b.WriteString("\"password\":\"")
b.WriteString(c.Password) b.WriteString(c.Password)
b.WriteString("\",") b.WriteString("\",")
} }
b.WriteString("\"email\":\"") b.WriteString("\"email\":\"")
b.WriteString(c.Email) b.WriteString(c.Email)
b.WriteString("\",") b.WriteString("\",")
b.WriteString("\"enable\":") b.WriteString("\"enable\":")
if c.Enable { b.WriteString("true") } else { b.WriteString("false") } if c.Enable {
b.WriteString(",") b.WriteString("true")
b.WriteString("\"limitIp\":") } else {
b.WriteString(strconv.Itoa(c.LimitIP)) b.WriteString("false")
b.WriteString(",") }
b.WriteString("\"totalGB\":") b.WriteString(",")
b.WriteString(strconv.FormatInt(c.TotalGB, 10)) b.WriteString("\"limitIp\":")
if c.ExpiryTime > 0 { b.WriteString(strconv.Itoa(c.LimitIP))
b.WriteString(",\"expiryTime\":") b.WriteString(",")
b.WriteString(strconv.FormatInt(c.ExpiryTime, 10)) b.WriteString("\"totalGB\":")
} b.WriteString(strconv.FormatInt(c.TotalGB, 10))
b.WriteString("}") if c.ExpiryTime > 0 {
return b.String() b.WriteString(",\"expiryTime\":")
b.WriteString(strconv.FormatInt(c.ExpiryTime, 10))
}
b.WriteString("}")
return b.String()
} }