chore: apply formatting and db.go fixes

This commit is contained in:
Dikiy13371 2025-10-07 23:46:47 +03:00
parent 97e9aca156
commit b2ae172623
7 changed files with 584 additions and 563 deletions

26
exit Normal file
View file

@ -0,0 +1,26 @@
diff.astextplain.textconv=astextplain
filter.lfs.clean=git-lfs clean -- %f
filter.lfs.smudge=git-lfs smudge -- %f
filter.lfs.process=git-lfs filter-process
filter.lfs.required=true
http.sslbackend=schannel
core.autocrlf=true
core.fscache=true
core.symlinks=false
pull.rebase=false
credential.helper=manager
credential.https://dev.azure.com.usehttppath=true
init.defaultbranch=master
user.name=Dikiy13371
user.email=css81933@gmail.com
core.repositoryformatversion=0
core.filemode=false
core.bare=false
core.logallrefupdates=true
core.symlinks=false
core.ignorecase=true
remote.origin.url=https://github.com/Dikiy13371/3x-uiRuys71.git
remote.origin.fetch=+refs/heads/*:refs/remotes/origin/*
branch.main.remote=origin
branch.main.merge=refs/heads/main
branch.main.vscode-merge-base=origin/main

View file

@ -1,144 +1,142 @@
package ldaputil package ldaputil
import ( import (
"crypto/tls" "crypto/tls"
"fmt" "fmt"
"github.com/go-ldap/ldap/v3" "github.com/go-ldap/ldap/v3"
) )
type Config struct { type Config struct {
Host string Host string
Port int Port int
UseTLS bool UseTLS bool
BindDN string BindDN string
Password string Password string
BaseDN string BaseDN string
UserFilter string UserFilter string
UserAttr string UserAttr string
FlagField string FlagField string
TruthyVals []string TruthyVals []string
Invert bool Invert bool
} }
// FetchVlessFlags returns map[email]enabled // FetchVlessFlags returns map[email]enabled
func FetchVlessFlags(cfg Config) (map[string]bool, error) { func FetchVlessFlags(cfg Config) (map[string]bool, error) {
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn var conn *ldap.Conn
var err error var err error
if cfg.UseTLS { if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false}) conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
} else { } else {
conn, err = ldap.Dial("tcp", addr) conn, err = ldap.Dial("tcp", addr)
} }
if err != nil { if err != nil {
return nil, err return nil, err
} }
defer conn.Close() defer conn.Close()
if cfg.BindDN != "" { if cfg.BindDN != "" {
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil { if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
return nil, err return nil, err
} }
} }
if cfg.UserFilter == "" { if cfg.UserFilter == "" {
cfg.UserFilter = "(objectClass=person)" cfg.UserFilter = "(objectClass=person)"
} }
if cfg.UserAttr == "" { if cfg.UserAttr == "" {
cfg.UserAttr = "mail" cfg.UserAttr = "mail"
} }
// if field not set we fallback to legacy vless_enabled // if field not set we fallback to legacy vless_enabled
if cfg.FlagField == "" { if cfg.FlagField == "" {
cfg.FlagField = "vless_enabled" cfg.FlagField = "vless_enabled"
} }
req := ldap.NewSearchRequest( req := ldap.NewSearchRequest(
cfg.BaseDN, cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false,
cfg.UserFilter, cfg.UserFilter,
[]string{cfg.UserAttr, cfg.FlagField}, []string{cfg.UserAttr, cfg.FlagField},
nil, nil,
) )
res, err := conn.Search(req) res, err := conn.Search(req)
if err != nil { if err != nil {
return nil, err return nil, err
} }
result := make(map[string]bool, len(res.Entries)) result := make(map[string]bool, len(res.Entries))
for _, e := range res.Entries { for _, e := range res.Entries {
user := e.GetAttributeValue(cfg.UserAttr) user := e.GetAttributeValue(cfg.UserAttr)
if user == "" { if user == "" {
continue continue
} }
val := e.GetAttributeValue(cfg.FlagField) val := e.GetAttributeValue(cfg.FlagField)
enabled := false enabled := false
for _, t := range cfg.TruthyVals { for _, t := range cfg.TruthyVals {
if val == t { if val == t {
enabled = true enabled = true
break break
} }
} }
if cfg.Invert { if cfg.Invert {
enabled = !enabled enabled = !enabled
} }
result[user] = enabled result[user] = enabled
} }
return result, nil return result, nil
} }
// AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password. // AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password.
func AuthenticateUser(cfg Config, username, password string) (bool, error) { func AuthenticateUser(cfg Config, username, password string) (bool, error) {
addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port)
var conn *ldap.Conn var conn *ldap.Conn
var err error var err error
if cfg.UseTLS { if cfg.UseTLS {
conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false}) conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false})
} else { } else {
conn, err = ldap.Dial("tcp", addr) conn, err = ldap.Dial("tcp", addr)
} }
if err != nil { if err != nil {
return false, err return false, err
} }
defer conn.Close() defer conn.Close()
// Optional initial bind for search // Optional initial bind for search
if cfg.BindDN != "" { if cfg.BindDN != "" {
if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil { if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil {
return false, err return false, err
} }
} }
if cfg.UserFilter == "" { if cfg.UserFilter == "" {
cfg.UserFilter = "(objectClass=person)" cfg.UserFilter = "(objectClass=person)"
} }
if cfg.UserAttr == "" { if cfg.UserAttr == "" {
cfg.UserAttr = "uid" cfg.UserAttr = "uid"
} }
// Build filter to find specific user // Build filter to find specific user
filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username)) filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username))
req := ldap.NewSearchRequest( req := ldap.NewSearchRequest(
cfg.BaseDN, cfg.BaseDN,
ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false,
filter, filter,
[]string{"dn"}, []string{"dn"},
nil, nil,
) )
res, err := conn.Search(req) res, err := conn.Search(req)
if err != nil { if err != nil {
return false, err return false, err
} }
if len(res.Entries) == 0 { if len(res.Entries) == 0 {
return false, nil return false, nil
} }
userDN := res.Entries[0].DN userDN := res.Entries[0].DN
// Try to bind as the user // Try to bind as the user
if err := conn.Bind(userDN, password); err != nil { if err := conn.Bind(userDN, password); err != nil {
return false, nil return false, nil
} }
return true, nil return true, nil
} }

View file

@ -74,30 +74,30 @@ type AllSetting struct {
SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` // JSON subscription fragment configuration SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` // JSON subscription fragment configuration
SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"` // JSON subscription noise configuration SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"` // JSON subscription noise configuration
SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` // JSON subscription mux configuration SubJsonMux string `json:"subJsonMux" form:"subJsonMux"` // JSON subscription mux configuration
SubJsonRules string `json:"subJsonRules" form:"subJsonRules"` SubJsonRules string `json:"subJsonRules" form:"subJsonRules"`
// LDAP settings // LDAP settings
LdapEnable bool `json:"ldapEnable" form:"ldapEnable"` LdapEnable bool `json:"ldapEnable" form:"ldapEnable"`
LdapHost string `json:"ldapHost" form:"ldapHost"` LdapHost string `json:"ldapHost" form:"ldapHost"`
LdapPort int `json:"ldapPort" form:"ldapPort"` LdapPort int `json:"ldapPort" form:"ldapPort"`
LdapUseTLS bool `json:"ldapUseTLS" form:"ldapUseTLS"` LdapUseTLS bool `json:"ldapUseTLS" form:"ldapUseTLS"`
LdapBindDN string `json:"ldapBindDN" form:"ldapBindDN"` LdapBindDN string `json:"ldapBindDN" form:"ldapBindDN"`
LdapPassword string `json:"ldapPassword" form:"ldapPassword"` LdapPassword string `json:"ldapPassword" form:"ldapPassword"`
LdapBaseDN string `json:"ldapBaseDN" form:"ldapBaseDN"` LdapBaseDN string `json:"ldapBaseDN" form:"ldapBaseDN"`
LdapUserFilter string `json:"ldapUserFilter" form:"ldapUserFilter"` LdapUserFilter string `json:"ldapUserFilter" form:"ldapUserFilter"`
LdapUserAttr string `json:"ldapUserAttr" form:"ldapUserAttr"` // e.g., mail or uid LdapUserAttr string `json:"ldapUserAttr" form:"ldapUserAttr"` // e.g., mail or uid
LdapVlessField string `json:"ldapVlessField" form:"ldapVlessField"` LdapVlessField string `json:"ldapVlessField" form:"ldapVlessField"`
LdapSyncCron string `json:"ldapSyncCron" form:"ldapSyncCron"` LdapSyncCron string `json:"ldapSyncCron" form:"ldapSyncCron"`
// Generic flag configuration // Generic flag configuration
LdapFlagField string `json:"ldapFlagField" form:"ldapFlagField"` LdapFlagField string `json:"ldapFlagField" form:"ldapFlagField"`
LdapTruthyValues string `json:"ldapTruthyValues" form:"ldapTruthyValues"` LdapTruthyValues string `json:"ldapTruthyValues" form:"ldapTruthyValues"`
LdapInvertFlag bool `json:"ldapInvertFlag" form:"ldapInvertFlag"` LdapInvertFlag bool `json:"ldapInvertFlag" form:"ldapInvertFlag"`
LdapInboundTags string `json:"ldapInboundTags" form:"ldapInboundTags"` LdapInboundTags string `json:"ldapInboundTags" form:"ldapInboundTags"`
LdapAutoCreate bool `json:"ldapAutoCreate" form:"ldapAutoCreate"` LdapAutoCreate bool `json:"ldapAutoCreate" form:"ldapAutoCreate"`
LdapAutoDelete bool `json:"ldapAutoDelete" form:"ldapAutoDelete"` LdapAutoDelete bool `json:"ldapAutoDelete" form:"ldapAutoDelete"`
LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB"` LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB"`
LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays"` LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays"`
LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP"` LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP"`
// JSON subscription routing rules // JSON subscription routing rules
} }

View file

@ -1,421 +1,419 @@
package job package job
import ( import (
"time" "time"
"github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/logger"
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap" ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
"github.com/mhsanaei/3x-ui/v2/web/service" "github.com/mhsanaei/3x-ui/v2/web/service"
"strings" "strings"
"github.com/google/uuid" "github.com/google/uuid"
"strconv" "strconv"
) )
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()
} }

View file

@ -1569,21 +1569,20 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo
return !clientOldEnabled, needRestart, nil return !clientOldEnabled, needRestart, nil
} }
// SetClientEnableByEmail sets client enable state to desired value; returns (changed, needRestart, error) // SetClientEnableByEmail sets client enable state to desired value; returns (changed, needRestart, error)
func (s *InboundService) SetClientEnableByEmail(clientEmail string, enable bool) (bool, bool, error) { func (s *InboundService) SetClientEnableByEmail(clientEmail string, enable bool) (bool, bool, error) {
current, err := s.checkIsEnabledByEmail(clientEmail) current, err := s.checkIsEnabledByEmail(clientEmail)
if err != nil { if err != nil {
return false, false, err return false, false, err
} }
if current == enable { if current == enable {
return false, false, nil return false, false, nil
} }
newEnabled, needRestart, err := s.ToggleClientEnableByEmail(clientEmail) newEnabled, needRestart, err := s.ToggleClientEnableByEmail(clientEmail)
if err != nil { if err != nil {
return false, needRestart, err return false, needRestart, err
} }
return newEnabled == enable, needRestart, nil return newEnabled == enable, needRestart, nil
} }
func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) (bool, error) { func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) (bool, error) {

View file

@ -74,26 +74,26 @@ var defaultValueMap = map[string]string{
"externalTrafficInformEnable": "false", "externalTrafficInformEnable": "false",
"externalTrafficInformURI": "", "externalTrafficInformURI": "",
// LDAP defaults // LDAP defaults
"ldapEnable": "false", "ldapEnable": "false",
"ldapHost": "", "ldapHost": "",
"ldapPort": "389", "ldapPort": "389",
"ldapUseTLS": "false", "ldapUseTLS": "false",
"ldapBindDN": "", "ldapBindDN": "",
"ldapPassword": "", "ldapPassword": "",
"ldapBaseDN": "", "ldapBaseDN": "",
"ldapUserFilter": "(objectClass=person)", "ldapUserFilter": "(objectClass=person)",
"ldapUserAttr": "mail", "ldapUserAttr": "mail",
"ldapVlessField": "vless_enabled", "ldapVlessField": "vless_enabled",
"ldapSyncCron": "@every 1m", "ldapSyncCron": "@every 1m",
"ldapFlagField": "", "ldapFlagField": "",
"ldapTruthyValues": "true,1,yes,on", "ldapTruthyValues": "true,1,yes,on",
"ldapInvertFlag": "false", "ldapInvertFlag": "false",
"ldapInboundTags": "", "ldapInboundTags": "",
"ldapAutoCreate": "false", "ldapAutoCreate": "false",
"ldapAutoDelete": "false", "ldapAutoDelete": "false",
"ldapDefaultTotalGB": "0", "ldapDefaultTotalGB": "0",
"ldapDefaultExpiryDays": "0", "ldapDefaultExpiryDays": "0",
"ldapDefaultLimitIP": "0", "ldapDefaultLimitIP": "0",
} }
// SettingService provides business logic for application settings management. // SettingService provides business logic for application settings management.
@ -565,83 +565,83 @@ func (s *SettingService) GetIpLimitEnable() (bool, error) {
// LDAP exported getters // LDAP exported getters
func (s *SettingService) GetLdapEnable() (bool, error) { func (s *SettingService) GetLdapEnable() (bool, error) {
return s.getBool("ldapEnable") return s.getBool("ldapEnable")
} }
func (s *SettingService) GetLdapHost() (string, error) { func (s *SettingService) GetLdapHost() (string, error) {
return s.getString("ldapHost") return s.getString("ldapHost")
} }
func (s *SettingService) GetLdapPort() (int, error) { func (s *SettingService) GetLdapPort() (int, error) {
return s.getInt("ldapPort") return s.getInt("ldapPort")
} }
func (s *SettingService) GetLdapUseTLS() (bool, error) { func (s *SettingService) GetLdapUseTLS() (bool, error) {
return s.getBool("ldapUseTLS") return s.getBool("ldapUseTLS")
} }
func (s *SettingService) GetLdapBindDN() (string, error) { func (s *SettingService) GetLdapBindDN() (string, error) {
return s.getString("ldapBindDN") return s.getString("ldapBindDN")
} }
func (s *SettingService) GetLdapPassword() (string, error) { func (s *SettingService) GetLdapPassword() (string, error) {
return s.getString("ldapPassword") return s.getString("ldapPassword")
} }
func (s *SettingService) GetLdapBaseDN() (string, error) { func (s *SettingService) GetLdapBaseDN() (string, error) {
return s.getString("ldapBaseDN") return s.getString("ldapBaseDN")
} }
func (s *SettingService) GetLdapUserFilter() (string, error) { func (s *SettingService) GetLdapUserFilter() (string, error) {
return s.getString("ldapUserFilter") return s.getString("ldapUserFilter")
} }
func (s *SettingService) GetLdapUserAttr() (string, error) { func (s *SettingService) GetLdapUserAttr() (string, error) {
return s.getString("ldapUserAttr") return s.getString("ldapUserAttr")
} }
func (s *SettingService) GetLdapVlessField() (string, error) { func (s *SettingService) GetLdapVlessField() (string, error) {
return s.getString("ldapVlessField") return s.getString("ldapVlessField")
} }
func (s *SettingService) GetLdapSyncCron() (string, error) { func (s *SettingService) GetLdapSyncCron() (string, error) {
return s.getString("ldapSyncCron") return s.getString("ldapSyncCron")
} }
func (s *SettingService) GetLdapFlagField() (string, error) { func (s *SettingService) GetLdapFlagField() (string, error) {
return s.getString("ldapFlagField") return s.getString("ldapFlagField")
} }
func (s *SettingService) GetLdapTruthyValues() (string, error) { func (s *SettingService) GetLdapTruthyValues() (string, error) {
return s.getString("ldapTruthyValues") return s.getString("ldapTruthyValues")
} }
func (s *SettingService) GetLdapInvertFlag() (bool, error) { func (s *SettingService) GetLdapInvertFlag() (bool, error) {
return s.getBool("ldapInvertFlag") return s.getBool("ldapInvertFlag")
} }
func (s *SettingService) GetLdapInboundTags() (string, error) { func (s *SettingService) GetLdapInboundTags() (string, error) {
return s.getString("ldapInboundTags") return s.getString("ldapInboundTags")
} }
func (s *SettingService) GetLdapAutoCreate() (bool, error) { func (s *SettingService) GetLdapAutoCreate() (bool, error) {
return s.getBool("ldapAutoCreate") return s.getBool("ldapAutoCreate")
} }
func (s *SettingService) GetLdapAutoDelete() (bool, error) { func (s *SettingService) GetLdapAutoDelete() (bool, error) {
return s.getBool("ldapAutoDelete") return s.getBool("ldapAutoDelete")
} }
func (s *SettingService) GetLdapDefaultTotalGB() (int, error) { func (s *SettingService) GetLdapDefaultTotalGB() (int, error) {
return s.getInt("ldapDefaultTotalGB") return s.getInt("ldapDefaultTotalGB")
} }
func (s *SettingService) GetLdapDefaultExpiryDays() (int, error) { func (s *SettingService) GetLdapDefaultExpiryDays() (int, error) {
return s.getInt("ldapDefaultExpiryDays") return s.getInt("ldapDefaultExpiryDays")
} }
func (s *SettingService) GetLdapDefaultLimitIP() (int, error) { func (s *SettingService) GetLdapDefaultLimitIP() (int, error) {
return s.getInt("ldapDefaultLimitIP") return s.getInt("ldapDefaultLimitIP")
} }
func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error { func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error {

View file

@ -7,7 +7,7 @@ import (
"github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/database/model"
"github.com/mhsanaei/3x-ui/v2/logger" "github.com/mhsanaei/3x-ui/v2/logger"
"github.com/mhsanaei/3x-ui/v2/util/crypto" "github.com/mhsanaei/3x-ui/v2/util/crypto"
ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap" ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
"github.com/xlzd/gotp" "github.com/xlzd/gotp"
"gorm.io/gorm" "gorm.io/gorm"
) )
@ -49,38 +49,38 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode
return nil return nil
} }
// If LDAP enabled and local password check fails, attempt LDAP auth // If LDAP enabled and local password check fails, attempt LDAP auth
if !crypto.CheckPasswordHash(user.Password, password) { if !crypto.CheckPasswordHash(user.Password, password) {
ldapEnabled, _ := s.settingService.GetLdapEnable() ldapEnabled, _ := s.settingService.GetLdapEnable()
if !ldapEnabled { if !ldapEnabled {
return nil return nil
} }
host, _ := s.settingService.GetLdapHost() host, _ := s.settingService.GetLdapHost()
port, _ := s.settingService.GetLdapPort() port, _ := s.settingService.GetLdapPort()
useTLS, _ := s.settingService.GetLdapUseTLS() useTLS, _ := s.settingService.GetLdapUseTLS()
bindDN, _ := s.settingService.GetLdapBindDN() bindDN, _ := s.settingService.GetLdapBindDN()
ldapPass, _ := s.settingService.GetLdapPassword() ldapPass, _ := s.settingService.GetLdapPassword()
baseDN, _ := s.settingService.GetLdapBaseDN() baseDN, _ := s.settingService.GetLdapBaseDN()
userFilter, _ := s.settingService.GetLdapUserFilter() userFilter, _ := s.settingService.GetLdapUserFilter()
userAttr, _ := s.settingService.GetLdapUserAttr() userAttr, _ := s.settingService.GetLdapUserAttr()
cfg := ldaputil.Config{ cfg := ldaputil.Config{
Host: host, Host: host,
Port: port, Port: port,
UseTLS: useTLS, UseTLS: useTLS,
BindDN: bindDN, BindDN: bindDN,
Password: ldapPass, Password: ldapPass,
BaseDN: baseDN, BaseDN: baseDN,
UserFilter: userFilter, UserFilter: userFilter,
UserAttr: userAttr, UserAttr: userAttr,
} }
ok, err := ldaputil.AuthenticateUser(cfg, username, password) ok, err := ldaputil.AuthenticateUser(cfg, username, password)
if err != nil || !ok { if err != nil || !ok {
return nil return nil
} }
// On successful LDAP auth, continue 2FA checks below // On successful LDAP auth, continue 2FA checks below
} }
twoFactorEnable, err := s.settingService.GetTwoFactorEnable() twoFactorEnable, err := s.settingService.GetTwoFactorEnable()
if err != nil { if err != nil {