diff --git a/exit b/exit new file mode 100644 index 00000000..988ae8d7 --- /dev/null +++ b/exit @@ -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 diff --git a/util/ldap/ldap.go b/util/ldap/ldap.go index 1c7a20e7..795d0e23 100644 --- a/util/ldap/ldap.go +++ b/util/ldap/ldap.go @@ -1,144 +1,142 @@ package ldaputil import ( - "crypto/tls" - "fmt" + "crypto/tls" + "fmt" - "github.com/go-ldap/ldap/v3" + "github.com/go-ldap/ldap/v3" ) type Config struct { - Host string - Port int - UseTLS bool - BindDN string - Password string - BaseDN string - UserFilter string - UserAttr string - FlagField string - TruthyVals []string - Invert bool + Host string + Port int + UseTLS bool + BindDN string + Password string + BaseDN string + UserFilter string + UserAttr string + FlagField string + TruthyVals []string + Invert bool } // FetchVlessFlags returns map[email]enabled func FetchVlessFlags(cfg Config) (map[string]bool, error) { - addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) - var conn *ldap.Conn - var err error - if cfg.UseTLS { - conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false}) - } else { - conn, err = ldap.Dial("tcp", addr) - } - if err != nil { - return nil, err - } - defer conn.Close() + addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) + var conn *ldap.Conn + var err error + if cfg.UseTLS { + conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false}) + } else { + conn, err = ldap.Dial("tcp", addr) + } + if err != nil { + return nil, err + } + defer conn.Close() - if cfg.BindDN != "" { - if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil { - return nil, err - } - } + if cfg.BindDN != "" { + if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil { + return nil, err + } + } - if cfg.UserFilter == "" { - cfg.UserFilter = "(objectClass=person)" - } - if cfg.UserAttr == "" { - cfg.UserAttr = "mail" - } - // if field not set we fallback to legacy vless_enabled - if cfg.FlagField == "" { - cfg.FlagField = "vless_enabled" - } + if cfg.UserFilter == "" { + cfg.UserFilter = "(objectClass=person)" + } + if cfg.UserAttr == "" { + cfg.UserAttr = "mail" + } + // if field not set we fallback to legacy vless_enabled + if cfg.FlagField == "" { + cfg.FlagField = "vless_enabled" + } - req := ldap.NewSearchRequest( - cfg.BaseDN, - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, - cfg.UserFilter, - []string{cfg.UserAttr, cfg.FlagField}, - nil, - ) + req := ldap.NewSearchRequest( + cfg.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 0, 0, false, + cfg.UserFilter, + []string{cfg.UserAttr, cfg.FlagField}, + nil, + ) - res, err := conn.Search(req) - if err != nil { - return nil, err - } + res, err := conn.Search(req) + if err != nil { + return nil, err + } - result := make(map[string]bool, len(res.Entries)) - for _, e := range res.Entries { - user := e.GetAttributeValue(cfg.UserAttr) - if user == "" { - continue - } - val := e.GetAttributeValue(cfg.FlagField) - enabled := false - for _, t := range cfg.TruthyVals { - if val == t { - enabled = true - break - } - } - if cfg.Invert { - enabled = !enabled - } - result[user] = enabled - } - return result, nil + result := make(map[string]bool, len(res.Entries)) + for _, e := range res.Entries { + user := e.GetAttributeValue(cfg.UserAttr) + if user == "" { + continue + } + val := e.GetAttributeValue(cfg.FlagField) + enabled := false + for _, t := range cfg.TruthyVals { + if val == t { + enabled = true + break + } + } + if cfg.Invert { + enabled = !enabled + } + result[user] = enabled + } + return result, nil } // AuthenticateUser searches user by cfg.UserAttr and attempts to bind with provided password. func AuthenticateUser(cfg Config, username, password string) (bool, error) { - addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) - var conn *ldap.Conn - var err error - if cfg.UseTLS { - conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false}) - } else { - conn, err = ldap.Dial("tcp", addr) - } - if err != nil { - return false, err - } - defer conn.Close() + addr := fmt.Sprintf("%s:%d", cfg.Host, cfg.Port) + var conn *ldap.Conn + var err error + if cfg.UseTLS { + conn, err = ldap.DialTLS("tcp", addr, &tls.Config{InsecureSkipVerify: false}) + } else { + conn, err = ldap.Dial("tcp", addr) + } + if err != nil { + return false, err + } + defer conn.Close() - // Optional initial bind for search - if cfg.BindDN != "" { - if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil { - return false, err - } - } + // Optional initial bind for search + if cfg.BindDN != "" { + if err := conn.Bind(cfg.BindDN, cfg.Password); err != nil { + return false, err + } + } - if cfg.UserFilter == "" { - cfg.UserFilter = "(objectClass=person)" - } - if cfg.UserAttr == "" { - cfg.UserAttr = "uid" - } + if cfg.UserFilter == "" { + cfg.UserFilter = "(objectClass=person)" + } + if cfg.UserAttr == "" { + cfg.UserAttr = "uid" + } - // Build filter to find specific user - filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username)) - req := ldap.NewSearchRequest( - cfg.BaseDN, - ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, - filter, - []string{"dn"}, - nil, - ) - res, err := conn.Search(req) - if err != nil { - return false, err - } - if len(res.Entries) == 0 { - return false, nil - } - userDN := res.Entries[0].DN - // Try to bind as the user - if err := conn.Bind(userDN, password); err != nil { - return false, nil - } - return true, nil + // Build filter to find specific user + filter := fmt.Sprintf("(&%s(%s=%s))", cfg.UserFilter, cfg.UserAttr, ldap.EscapeFilter(username)) + req := ldap.NewSearchRequest( + cfg.BaseDN, + ldap.ScopeWholeSubtree, ldap.NeverDerefAliases, 1, 0, false, + filter, + []string{"dn"}, + nil, + ) + res, err := conn.Search(req) + if err != nil { + return false, err + } + if len(res.Entries) == 0 { + return false, nil + } + userDN := res.Entries[0].DN + // Try to bind as the user + if err := conn.Bind(userDN, password); err != nil { + return false, nil + } + return true, nil } - - diff --git a/web/entity/entity.go b/web/entity/entity.go index de054e2b..42e2df85 100644 --- a/web/entity/entity.go +++ b/web/entity/entity.go @@ -74,30 +74,30 @@ type AllSetting struct { SubJsonFragment string `json:"subJsonFragment" form:"subJsonFragment"` // JSON subscription fragment configuration SubJsonNoises string `json:"subJsonNoises" form:"subJsonNoises"` // JSON subscription noise 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 - LdapEnable bool `json:"ldapEnable" form:"ldapEnable"` - LdapHost string `json:"ldapHost" form:"ldapHost"` - LdapPort int `json:"ldapPort" form:"ldapPort"` - LdapUseTLS bool `json:"ldapUseTLS" form:"ldapUseTLS"` - LdapBindDN string `json:"ldapBindDN" form:"ldapBindDN"` - LdapPassword string `json:"ldapPassword" form:"ldapPassword"` - LdapBaseDN string `json:"ldapBaseDN" form:"ldapBaseDN"` - LdapUserFilter string `json:"ldapUserFilter" form:"ldapUserFilter"` - LdapUserAttr string `json:"ldapUserAttr" form:"ldapUserAttr"` // e.g., mail or uid - LdapVlessField string `json:"ldapVlessField" form:"ldapVlessField"` - LdapSyncCron string `json:"ldapSyncCron" form:"ldapSyncCron"` + LdapEnable bool `json:"ldapEnable" form:"ldapEnable"` + LdapHost string `json:"ldapHost" form:"ldapHost"` + LdapPort int `json:"ldapPort" form:"ldapPort"` + LdapUseTLS bool `json:"ldapUseTLS" form:"ldapUseTLS"` + LdapBindDN string `json:"ldapBindDN" form:"ldapBindDN"` + LdapPassword string `json:"ldapPassword" form:"ldapPassword"` + LdapBaseDN string `json:"ldapBaseDN" form:"ldapBaseDN"` + LdapUserFilter string `json:"ldapUserFilter" form:"ldapUserFilter"` + LdapUserAttr string `json:"ldapUserAttr" form:"ldapUserAttr"` // e.g., mail or uid + LdapVlessField string `json:"ldapVlessField" form:"ldapVlessField"` + LdapSyncCron string `json:"ldapSyncCron" form:"ldapSyncCron"` // Generic flag configuration - LdapFlagField string `json:"ldapFlagField" form:"ldapFlagField"` - LdapTruthyValues string `json:"ldapTruthyValues" form:"ldapTruthyValues"` - LdapInvertFlag bool `json:"ldapInvertFlag" form:"ldapInvertFlag"` - LdapInboundTags string `json:"ldapInboundTags" form:"ldapInboundTags"` - LdapAutoCreate bool `json:"ldapAutoCreate" form:"ldapAutoCreate"` - LdapAutoDelete bool `json:"ldapAutoDelete" form:"ldapAutoDelete"` - LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB"` - LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays"` - LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP"` + LdapFlagField string `json:"ldapFlagField" form:"ldapFlagField"` + LdapTruthyValues string `json:"ldapTruthyValues" form:"ldapTruthyValues"` + LdapInvertFlag bool `json:"ldapInvertFlag" form:"ldapInvertFlag"` + LdapInboundTags string `json:"ldapInboundTags" form:"ldapInboundTags"` + LdapAutoCreate bool `json:"ldapAutoCreate" form:"ldapAutoCreate"` + LdapAutoDelete bool `json:"ldapAutoDelete" form:"ldapAutoDelete"` + LdapDefaultTotalGB int `json:"ldapDefaultTotalGB" form:"ldapDefaultTotalGB"` + LdapDefaultExpiryDays int `json:"ldapDefaultExpiryDays" form:"ldapDefaultExpiryDays"` + LdapDefaultLimitIP int `json:"ldapDefaultLimitIP" form:"ldapDefaultLimitIP"` // JSON subscription routing rules } diff --git a/web/job/ldap_sync_job.go b/web/job/ldap_sync_job.go index cb954932..3a8c8077 100644 --- a/web/job/ldap_sync_job.go +++ b/web/job/ldap_sync_job.go @@ -1,421 +1,419 @@ package job import ( - "time" + "time" - "github.com/mhsanaei/3x-ui/v2/database/model" - "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/mhsanaei/3x-ui/v2/database/model" + "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" - "strconv" + "github.com/google/uuid" + "strconv" ) var DefaultTruthyValues = []string{"true", "1", "yes", "on"} type LdapSyncJob struct { - settingService service.SettingService - inboundService service.InboundService - xrayService service.XrayService + settingService service.SettingService + inboundService service.InboundService + xrayService service.XrayService } // --- Helper functions for mustGet --- func mustGetString(fn func() (string, error)) string { - v, err := fn() - if err != nil { - panic(err) - } - return v + v, err := fn() + if err != nil { + panic(err) + } + return v } func mustGetInt(fn func() (int, error)) int { - v, err := fn() - if err != nil { - panic(err) - } - return v + v, err := fn() + if err != nil { + panic(err) + } + return v } func mustGetBool(fn func() (bool, error)) bool { - v, err := fn() - if err != nil { - panic(err) - } - return v + v, err := fn() + if err != nil { + panic(err) + } + return v } func mustGetStringOr(fn func() (string, error), fallback string) string { - v, err := fn() - if err != nil || v == "" { - return fallback - } - return v + v, err := fn() + if err != nil || v == "" { + return fallback + } + return v } - func NewLdapSyncJob() *LdapSyncJob { - return new(LdapSyncJob) + return new(LdapSyncJob) } func (j *LdapSyncJob) Run() { - logger.Info("LDAP sync job started") + logger.Info("LDAP sync job started") - enabled, err := j.settingService.GetLdapEnable() - if err != nil || !enabled { - logger.Warning("LDAP disabled or failed to fetch flag") - return - } + enabled, err := j.settingService.GetLdapEnable() + if err != nil || !enabled { + logger.Warning("LDAP disabled or failed to fetch flag") + return + } - // --- LDAP fetch --- - cfg := ldaputil.Config{ - Host: mustGetString(j.settingService.GetLdapHost), - Port: mustGetInt(j.settingService.GetLdapPort), - UseTLS: mustGetBool(j.settingService.GetLdapUseTLS), - BindDN: mustGetString(j.settingService.GetLdapBindDN), - Password: mustGetString(j.settingService.GetLdapPassword), - BaseDN: mustGetString(j.settingService.GetLdapBaseDN), - UserFilter: mustGetString(j.settingService.GetLdapUserFilter), - UserAttr: mustGetString(j.settingService.GetLdapUserAttr), - FlagField: mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)), - TruthyVals: splitCsv(mustGetString(j.settingService.GetLdapTruthyValues)), - Invert: mustGetBool(j.settingService.GetLdapInvertFlag), - } + // --- LDAP fetch --- + cfg := ldaputil.Config{ + Host: mustGetString(j.settingService.GetLdapHost), + Port: mustGetInt(j.settingService.GetLdapPort), + UseTLS: mustGetBool(j.settingService.GetLdapUseTLS), + BindDN: mustGetString(j.settingService.GetLdapBindDN), + Password: mustGetString(j.settingService.GetLdapPassword), + BaseDN: mustGetString(j.settingService.GetLdapBaseDN), + UserFilter: mustGetString(j.settingService.GetLdapUserFilter), + UserAttr: mustGetString(j.settingService.GetLdapUserAttr), + FlagField: mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)), + TruthyVals: splitCsv(mustGetString(j.settingService.GetLdapTruthyValues)), + Invert: mustGetBool(j.settingService.GetLdapInvertFlag), + } - flags, err := ldaputil.FetchVlessFlags(cfg) - if err != nil { - logger.Warning("LDAP fetch failed:", err) - return - } - logger.Infof("Fetched %d LDAP flags", len(flags)) + flags, err := ldaputil.FetchVlessFlags(cfg) + if err != nil { + logger.Warning("LDAP fetch failed:", err) + return + } + logger.Infof("Fetched %d LDAP flags", len(flags)) - // --- Load all inbounds and all clients once --- - inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags)) - inbounds, err := j.inboundService.GetAllInbounds() - if err != nil { - logger.Warning("Failed to get inbounds:", err) - return - } + // --- Load all inbounds and all clients once --- + inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags)) + inbounds, err := j.inboundService.GetAllInbounds() + if err != nil { + logger.Warning("Failed to get inbounds:", err) + return + } - allClients := map[string]*model.Client{} // email -> client - inboundMap := map[string]*model.Inbound{} // tag -> inbound - for _, ib := range inbounds { - inboundMap[ib.Tag] = ib - clients, _ := j.inboundService.GetClients(ib) - for i := range clients { - allClients[clients[i].Email] = &clients[i] - } - } + allClients := map[string]*model.Client{} // email -> client + inboundMap := map[string]*model.Inbound{} // tag -> inbound + for _, ib := range inbounds { + inboundMap[ib.Tag] = ib + clients, _ := j.inboundService.GetClients(ib) + for i := range clients { + allClients[clients[i].Email] = &clients[i] + } + } - // --- Prepare batch operations --- - autoCreate := mustGetBool(j.settingService.GetLdapAutoCreate) - defGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB) - defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays) - defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP) + // --- Prepare batch operations --- + autoCreate := mustGetBool(j.settingService.GetLdapAutoCreate) + defGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB) + defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays) + defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP) - clientsToCreate := map[string][]model.Client{} // tag -> []new clients - clientsToEnable := map[string][]string{} // tag -> []email - clientsToDisable := map[string][]string{} // tag -> []email + clientsToCreate := map[string][]model.Client{} // tag -> []new clients + clientsToEnable := map[string][]string{} // tag -> []email + clientsToDisable := map[string][]string{} // tag -> []email - for email, allowed := range flags { - exists := allClients[email] != nil - for _, tag := range inboundTags { - if !exists && allowed && autoCreate { - newClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP) - clientsToCreate[tag] = append(clientsToCreate[tag], newClient) - } else if exists { - if allowed && !allClients[email].Enable { - clientsToEnable[tag] = append(clientsToEnable[tag], email) - } else if !allowed && allClients[email].Enable { - clientsToDisable[tag] = append(clientsToDisable[tag], email) - } - } - } - } + for email, allowed := range flags { + exists := allClients[email] != nil + for _, tag := range inboundTags { + if !exists && allowed && autoCreate { + newClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP) + clientsToCreate[tag] = append(clientsToCreate[tag], newClient) + } else if exists { + if allowed && !allClients[email].Enable { + clientsToEnable[tag] = append(clientsToEnable[tag], email) + } else if !allowed && allClients[email].Enable { + clientsToDisable[tag] = append(clientsToDisable[tag], email) + } + } + } + } - // --- Execute batch create --- - for tag, newClients := range clientsToCreate { - if len(newClients) == 0 { - continue - } - payload := &model.Inbound{Id: inboundMap[tag].Id} - payload.Settings = j.clientsToJSON(newClients) - if _, err := j.inboundService.AddInboundClient(payload); err != nil { - logger.Warningf("Failed to add clients for tag %s: %v", tag, err) - } else { - logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag) - j.xrayService.SetToNeedRestart() - } - } + // --- Execute batch create --- + for tag, newClients := range clientsToCreate { + if len(newClients) == 0 { + continue + } + payload := &model.Inbound{Id: inboundMap[tag].Id} + payload.Settings = j.clientsToJSON(newClients) + if _, err := j.inboundService.AddInboundClient(payload); err != nil { + logger.Warningf("Failed to add clients for tag %s: %v", tag, err) + } else { + logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag) + j.xrayService.SetToNeedRestart() + } + } - // --- Execute enable/disable batch --- - for tag, emails := range clientsToEnable { - j.batchSetEnable(inboundMap[tag], emails, true) - } - for tag, emails := range clientsToDisable { - j.batchSetEnable(inboundMap[tag], emails, false) - } + // --- Execute enable/disable batch --- + for tag, emails := range clientsToEnable { + j.batchSetEnable(inboundMap[tag], emails, true) + } + for tag, emails := range clientsToDisable { + j.batchSetEnable(inboundMap[tag], emails, false) + } - // --- Auto delete clients not in LDAP --- - autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete) - if autoDelete { - ldapEmailSet := map[string]struct{}{} - for e := range flags { - ldapEmailSet[e] = struct{}{} - } - for _, tag := range inboundTags { - j.deleteClientsNotInLDAP(tag, ldapEmailSet) - } - } + // --- Auto delete clients not in LDAP --- + autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete) + if autoDelete { + ldapEmailSet := map[string]struct{}{} + for e := range flags { + ldapEmailSet[e] = struct{}{} + } + for _, tag := range inboundTags { + j.deleteClientsNotInLDAP(tag, ldapEmailSet) + } + } } - - func splitCsv(s string) []string { - if s == "" { - return DefaultTruthyValues - } - parts := strings.Split(s, ",") - out := make([]string, 0, len(parts)) - for _, p := range parts { - v := strings.TrimSpace(p) - if v != "" { - out = append(out, v) - } - } - return out + if s == "" { + return DefaultTruthyValues + } + parts := strings.Split(s, ",") + out := make([]string, 0, len(parts)) + for _, p := range parts { + v := strings.TrimSpace(p) + if v != "" { + out = append(out, v) + } + } + return out } - // buildClient creates a new client for auto-create func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client { - c := model.Client{ - Email: email, - Enable: true, - LimitIP: defLimitIP, - TotalGB: int64(defGB), - } - if defExpiryDays > 0 { - c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli() - } - switch ib.Protocol { - case model.Trojan, model.Shadowsocks: - c.Password = uuid.NewString() - default: - c.ID = uuid.NewString() - } - return c + c := model.Client{ + Email: email, + Enable: true, + LimitIP: defLimitIP, + TotalGB: int64(defGB), + } + if defExpiryDays > 0 { + c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli() + } + switch ib.Protocol { + case model.Trojan, model.Shadowsocks: + c.Password = uuid.NewString() + default: + c.ID = uuid.NewString() + } + return c } // batchSetEnable enables/disables clients in batch through a single call func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) { - if len(emails) == 0 { - return - } + if len(emails) == 0 { + return + } - // Prepare JSON for mass update - clients := make([]model.Client, 0, len(emails)) - for _, email := range emails { - clients = append(clients, model.Client{ - Email: email, - Enable: enable, - }) - } + // Prepare JSON for mass update + clients := make([]model.Client, 0, len(emails)) + for _, email := range emails { + clients = append(clients, model.Client{ + Email: email, + Enable: enable, + }) + } - payload := &model.Inbound{ - Id: ib.Id, - Settings: j.clientsToJSON(clients), - } + payload := &model.Inbound{ + Id: ib.Id, + Settings: j.clientsToJSON(clients), + } - // Use a single AddInboundClient call to update enable - if _, err := j.inboundService.AddInboundClient(payload); err != nil { - logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err) - return - } + // Use a single AddInboundClient call to update enable + if _, err := j.inboundService.AddInboundClient(payload); err != nil { + logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err) + return + } - logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag) - j.xrayService.SetToNeedRestart() + logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag) + j.xrayService.SetToNeedRestart() } // deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) { - inbounds, err := j.inboundService.GetAllInbounds() - if err != nil { - logger.Warning("Failed to get inbounds for deletion:", err) - return - } + inbounds, err := j.inboundService.GetAllInbounds() + if err != nil { + logger.Warning("Failed to get inbounds for deletion:", err) + return + } - batchSize := 50 // clients in 1 batch - restartNeeded := false + batchSize := 50 // clients in 1 batch + restartNeeded := false - for _, ib := range inbounds { - if ib.Tag != inboundTag { - continue - } - clients, err := j.inboundService.GetClients(ib) - if err != nil { - logger.Warningf("Failed to get clients for inbound %s: %v", ib.Tag, err) - continue - } + for _, ib := range inbounds { + if ib.Tag != inboundTag { + continue + } + clients, err := j.inboundService.GetClients(ib) + if err != nil { + logger.Warningf("Failed to get clients for inbound %s: %v", ib.Tag, err) + continue + } - // Collect clients for deletion - toDelete := []model.Client{} - for _, c := range clients { - if _, ok := ldapEmails[c.Email]; !ok { - toDelete = append(toDelete, c) - } - } + // Collect clients for deletion + toDelete := []model.Client{} + for _, c := range clients { + if _, ok := ldapEmails[c.Email]; !ok { + toDelete = append(toDelete, c) + } + } - if len(toDelete) == 0 { - continue - } + if len(toDelete) == 0 { + continue + } - // Delete in batches - for i := 0; i < len(toDelete); i += batchSize { - end := i + batchSize - if end > len(toDelete) { - end = len(toDelete) - } - batch := toDelete[i:end] + // Delete in batches + for i := 0; i < len(toDelete); i += batchSize { + end := i + batchSize + if end > len(toDelete) { + end = len(toDelete) + } + batch := toDelete[i:end] - for _, c := range batch { - var clientKey string - switch ib.Protocol { - case model.Trojan: - clientKey = c.Password - case model.Shadowsocks: - clientKey = c.Email - default: // vless/vmess - clientKey = c.ID - } + for _, c := range batch { + var clientKey string + switch ib.Protocol { + case model.Trojan: + clientKey = c.Password + case model.Shadowsocks: + clientKey = c.Email + default: // vless/vmess + clientKey = c.ID + } - if _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); err != nil { - logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v", - c.Email, ib.Id, ib.Tag, err) - } else { - logger.Infof("Deleted client %s from inbound id=%d(tag=%s)", - c.Email, ib.Id, ib.Tag) - // do not restart here - restartNeeded = true - } - } - } - } + if _, err := j.inboundService.DelInboundClient(ib.Id, clientKey); err != nil { + logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v", + c.Email, ib.Id, ib.Tag, err) + } else { + logger.Infof("Deleted client %s from inbound id=%d(tag=%s)", + c.Email, ib.Id, ib.Tag) + // do not restart here + restartNeeded = true + } + } + } + } - // One time after all batches - if restartNeeded { - j.xrayService.SetToNeedRestart() - logger.Info("Xray restart scheduled after batch deletion") - } + // One time after all batches + if restartNeeded { + j.xrayService.SetToNeedRestart() + logger.Info("Xray restart scheduled after batch deletion") + } } - // clientsToJSON serializes an array of clients to JSON func (j *LdapSyncJob) clientsToJSON(clients []model.Client) string { - b := strings.Builder{} - b.WriteString("{\"clients\":[") - for i, c := range clients { - if i > 0 { b.WriteString(",") } - b.WriteString(j.clientToJSON(c)) - } - b.WriteString("]}") - return b.String() + b := strings.Builder{} + b.WriteString("{\"clients\":[") + for i, c := range clients { + if i > 0 { + b.WriteString(",") + } + b.WriteString(j.clientToJSON(c)) + } + b.WriteString("]}") + return b.String() } - // 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) { - inbounds, err := j.inboundService.GetAllInbounds() - if err != nil { - logger.Warning("ensureClientExists: get inbounds failed:", err) - return - } - var target *model.Inbound - for _, ib := range inbounds { - if ib.Tag == inboundTag { - target = ib - break - } - } - if target == nil { - logger.Debugf("ensureClientExists: inbound tag %s not found", inboundTag) - return - } - // check if email already exists in this inbound - clients, err := j.inboundService.GetClients(target) - if err == nil { - for _, c := range clients { - if c.Email == email { - return - } - } - } + inbounds, err := j.inboundService.GetAllInbounds() + if err != nil { + logger.Warning("ensureClientExists: get inbounds failed:", err) + return + } + var target *model.Inbound + for _, ib := range inbounds { + if ib.Tag == inboundTag { + target = ib + break + } + } + if target == nil { + logger.Debugf("ensureClientExists: inbound tag %s not found", inboundTag) + return + } + // check if email already exists in this inbound + clients, err := j.inboundService.GetClients(target) + if err == nil { + for _, c := range clients { + if c.Email == email { + return + } + } + } - // build new client according to protocol - newClient := model.Client{ - Email: email, - Enable: true, - LimitIP: defLimitIP, - TotalGB: int64(defGB), - } - if defExpiryDays > 0 { - newClient.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli() - } + // build new client according to protocol + newClient := model.Client{ + Email: email, + Enable: true, + LimitIP: defLimitIP, + TotalGB: int64(defGB), + } + if defExpiryDays > 0 { + newClient.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli() + } - switch target.Protocol { - case model.Trojan: - newClient.Password = uuid.NewString() - case model.Shadowsocks: - newClient.Password = uuid.NewString() - default: // VMESS/VLESS and others using ID - newClient.ID = uuid.NewString() - } + switch target.Protocol { + case model.Trojan: + newClient.Password = uuid.NewString() + case model.Shadowsocks: + newClient.Password = uuid.NewString() + default: // VMESS/VLESS and others using ID + newClient.ID = uuid.NewString() + } - // prepare inbound payload with only the new client - payload := &model.Inbound{Id: target.Id} - payload.Settings = `{"clients":[` + j.clientToJSON(newClient) + `]}` + // prepare inbound payload with only the new client + payload := &model.Inbound{Id: target.Id} + payload.Settings = `{"clients":[` + j.clientToJSON(newClient) + `]}` - if _, err := j.inboundService.AddInboundClient(payload); err != nil { - logger.Warning("ensureClientExists: add client failed:", err) - } else { - j.xrayService.SetToNeedRestart() - logger.Infof("LDAP auto-create: %s in %s", email, inboundTag) - } + if _, err := j.inboundService.AddInboundClient(payload); err != nil { + logger.Warning("ensureClientExists: add client failed:", err) + } else { + j.xrayService.SetToNeedRestart() + logger.Infof("LDAP auto-create: %s in %s", email, inboundTag) + } } // clientToJSON serializes minimal client fields to JSON object string without extra deps func (j *LdapSyncJob) clientToJSON(c model.Client) string { - // construct minimal JSON manually to avoid importing json for simple case - b := strings.Builder{} - b.WriteString("{") - if c.ID != "" { - b.WriteString("\"id\":\"") - b.WriteString(c.ID) - b.WriteString("\",") - } - if c.Password != "" { - b.WriteString("\"password\":\"") - b.WriteString(c.Password) - b.WriteString("\",") - } - b.WriteString("\"email\":\"") - b.WriteString(c.Email) - b.WriteString("\",") - b.WriteString("\"enable\":") - if c.Enable { b.WriteString("true") } else { b.WriteString("false") } - b.WriteString(",") - b.WriteString("\"limitIp\":") - b.WriteString(strconv.Itoa(c.LimitIP)) - b.WriteString(",") - b.WriteString("\"totalGB\":") - b.WriteString(strconv.FormatInt(c.TotalGB, 10)) - if c.ExpiryTime > 0 { - b.WriteString(",\"expiryTime\":") - b.WriteString(strconv.FormatInt(c.ExpiryTime, 10)) - } - b.WriteString("}") - return b.String() + // construct minimal JSON manually to avoid importing json for simple case + b := strings.Builder{} + b.WriteString("{") + if c.ID != "" { + b.WriteString("\"id\":\"") + b.WriteString(c.ID) + b.WriteString("\",") + } + if c.Password != "" { + b.WriteString("\"password\":\"") + b.WriteString(c.Password) + b.WriteString("\",") + } + b.WriteString("\"email\":\"") + b.WriteString(c.Email) + b.WriteString("\",") + b.WriteString("\"enable\":") + if c.Enable { + b.WriteString("true") + } else { + b.WriteString("false") + } + b.WriteString(",") + b.WriteString("\"limitIp\":") + b.WriteString(strconv.Itoa(c.LimitIP)) + b.WriteString(",") + b.WriteString("\"totalGB\":") + b.WriteString(strconv.FormatInt(c.TotalGB, 10)) + if c.ExpiryTime > 0 { + b.WriteString(",\"expiryTime\":") + b.WriteString(strconv.FormatInt(c.ExpiryTime, 10)) + } + b.WriteString("}") + return b.String() } - - diff --git a/web/service/inbound.go b/web/service/inbound.go index 93414801..66e87a4f 100644 --- a/web/service/inbound.go +++ b/web/service/inbound.go @@ -1569,21 +1569,20 @@ func (s *InboundService) ToggleClientEnableByEmail(clientEmail string) (bool, bo return !clientOldEnabled, needRestart, nil } - // SetClientEnableByEmail sets client enable state to desired value; returns (changed, needRestart, error) func (s *InboundService) SetClientEnableByEmail(clientEmail string, enable bool) (bool, bool, error) { - current, err := s.checkIsEnabledByEmail(clientEmail) - if err != nil { - return false, false, err - } - if current == enable { - return false, false, nil - } - newEnabled, needRestart, err := s.ToggleClientEnableByEmail(clientEmail) - if err != nil { - return false, needRestart, err - } - return newEnabled == enable, needRestart, nil + current, err := s.checkIsEnabledByEmail(clientEmail) + if err != nil { + return false, false, err + } + if current == enable { + return false, false, nil + } + newEnabled, needRestart, err := s.ToggleClientEnableByEmail(clientEmail) + if err != nil { + return false, needRestart, err + } + return newEnabled == enable, needRestart, nil } func (s *InboundService) ResetClientIpLimitByEmail(clientEmail string, count int) (bool, error) { diff --git a/web/service/setting.go b/web/service/setting.go index fa85d58c..c8ce7896 100644 --- a/web/service/setting.go +++ b/web/service/setting.go @@ -74,26 +74,26 @@ var defaultValueMap = map[string]string{ "externalTrafficInformEnable": "false", "externalTrafficInformURI": "", // LDAP defaults - "ldapEnable": "false", - "ldapHost": "", - "ldapPort": "389", - "ldapUseTLS": "false", - "ldapBindDN": "", - "ldapPassword": "", - "ldapBaseDN": "", - "ldapUserFilter": "(objectClass=person)", - "ldapUserAttr": "mail", - "ldapVlessField": "vless_enabled", - "ldapSyncCron": "@every 1m", - "ldapFlagField": "", - "ldapTruthyValues": "true,1,yes,on", - "ldapInvertFlag": "false", - "ldapInboundTags": "", - "ldapAutoCreate": "false", - "ldapAutoDelete": "false", - "ldapDefaultTotalGB": "0", - "ldapDefaultExpiryDays": "0", - "ldapDefaultLimitIP": "0", + "ldapEnable": "false", + "ldapHost": "", + "ldapPort": "389", + "ldapUseTLS": "false", + "ldapBindDN": "", + "ldapPassword": "", + "ldapBaseDN": "", + "ldapUserFilter": "(objectClass=person)", + "ldapUserAttr": "mail", + "ldapVlessField": "vless_enabled", + "ldapSyncCron": "@every 1m", + "ldapFlagField": "", + "ldapTruthyValues": "true,1,yes,on", + "ldapInvertFlag": "false", + "ldapInboundTags": "", + "ldapAutoCreate": "false", + "ldapAutoDelete": "false", + "ldapDefaultTotalGB": "0", + "ldapDefaultExpiryDays": "0", + "ldapDefaultLimitIP": "0", } // SettingService provides business logic for application settings management. @@ -565,83 +565,83 @@ func (s *SettingService) GetIpLimitEnable() (bool, error) { // LDAP exported getters func (s *SettingService) GetLdapEnable() (bool, error) { - return s.getBool("ldapEnable") + return s.getBool("ldapEnable") } func (s *SettingService) GetLdapHost() (string, error) { - return s.getString("ldapHost") + return s.getString("ldapHost") } func (s *SettingService) GetLdapPort() (int, error) { - return s.getInt("ldapPort") + return s.getInt("ldapPort") } func (s *SettingService) GetLdapUseTLS() (bool, error) { - return s.getBool("ldapUseTLS") + return s.getBool("ldapUseTLS") } func (s *SettingService) GetLdapBindDN() (string, error) { - return s.getString("ldapBindDN") + return s.getString("ldapBindDN") } func (s *SettingService) GetLdapPassword() (string, error) { - return s.getString("ldapPassword") + return s.getString("ldapPassword") } func (s *SettingService) GetLdapBaseDN() (string, error) { - return s.getString("ldapBaseDN") + return s.getString("ldapBaseDN") } func (s *SettingService) GetLdapUserFilter() (string, error) { - return s.getString("ldapUserFilter") + return s.getString("ldapUserFilter") } func (s *SettingService) GetLdapUserAttr() (string, error) { - return s.getString("ldapUserAttr") + return s.getString("ldapUserAttr") } func (s *SettingService) GetLdapVlessField() (string, error) { - return s.getString("ldapVlessField") + return s.getString("ldapVlessField") } func (s *SettingService) GetLdapSyncCron() (string, error) { - return s.getString("ldapSyncCron") + return s.getString("ldapSyncCron") } func (s *SettingService) GetLdapFlagField() (string, error) { - return s.getString("ldapFlagField") + return s.getString("ldapFlagField") } func (s *SettingService) GetLdapTruthyValues() (string, error) { - return s.getString("ldapTruthyValues") + return s.getString("ldapTruthyValues") } func (s *SettingService) GetLdapInvertFlag() (bool, error) { - return s.getBool("ldapInvertFlag") + return s.getBool("ldapInvertFlag") } func (s *SettingService) GetLdapInboundTags() (string, error) { - return s.getString("ldapInboundTags") + return s.getString("ldapInboundTags") } func (s *SettingService) GetLdapAutoCreate() (bool, error) { - return s.getBool("ldapAutoCreate") + return s.getBool("ldapAutoCreate") } func (s *SettingService) GetLdapAutoDelete() (bool, error) { - return s.getBool("ldapAutoDelete") + return s.getBool("ldapAutoDelete") } func (s *SettingService) GetLdapDefaultTotalGB() (int, error) { - return s.getInt("ldapDefaultTotalGB") + return s.getInt("ldapDefaultTotalGB") } func (s *SettingService) GetLdapDefaultExpiryDays() (int, error) { - return s.getInt("ldapDefaultExpiryDays") + return s.getInt("ldapDefaultExpiryDays") } func (s *SettingService) GetLdapDefaultLimitIP() (int, error) { - return s.getInt("ldapDefaultLimitIP") + return s.getInt("ldapDefaultLimitIP") } func (s *SettingService) UpdateAllSetting(allSetting *entity.AllSetting) error { diff --git a/web/service/user.go b/web/service/user.go index 87c46bf2..1bde69f6 100644 --- a/web/service/user.go +++ b/web/service/user.go @@ -7,7 +7,7 @@ import ( "github.com/mhsanaei/3x-ui/v2/database/model" "github.com/mhsanaei/3x-ui/v2/logger" "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" "gorm.io/gorm" ) @@ -49,38 +49,38 @@ func (s *UserService) CheckUser(username string, password string, twoFactorCode return nil } - // If LDAP enabled and local password check fails, attempt LDAP auth - if !crypto.CheckPasswordHash(user.Password, password) { - ldapEnabled, _ := s.settingService.GetLdapEnable() - if !ldapEnabled { - return nil - } + // If LDAP enabled and local password check fails, attempt LDAP auth + if !crypto.CheckPasswordHash(user.Password, password) { + ldapEnabled, _ := s.settingService.GetLdapEnable() + if !ldapEnabled { + return nil + } - host, _ := s.settingService.GetLdapHost() - port, _ := s.settingService.GetLdapPort() - useTLS, _ := s.settingService.GetLdapUseTLS() - bindDN, _ := s.settingService.GetLdapBindDN() - ldapPass, _ := s.settingService.GetLdapPassword() - baseDN, _ := s.settingService.GetLdapBaseDN() - userFilter, _ := s.settingService.GetLdapUserFilter() - userAttr, _ := s.settingService.GetLdapUserAttr() + host, _ := s.settingService.GetLdapHost() + port, _ := s.settingService.GetLdapPort() + useTLS, _ := s.settingService.GetLdapUseTLS() + bindDN, _ := s.settingService.GetLdapBindDN() + ldapPass, _ := s.settingService.GetLdapPassword() + baseDN, _ := s.settingService.GetLdapBaseDN() + userFilter, _ := s.settingService.GetLdapUserFilter() + userAttr, _ := s.settingService.GetLdapUserAttr() - cfg := ldaputil.Config{ - Host: host, - Port: port, - UseTLS: useTLS, - BindDN: bindDN, - Password: ldapPass, - BaseDN: baseDN, - UserFilter: userFilter, - UserAttr: userAttr, - } - ok, err := ldaputil.AuthenticateUser(cfg, username, password) - if err != nil || !ok { - return nil - } - // On successful LDAP auth, continue 2FA checks below - } + cfg := ldaputil.Config{ + Host: host, + Port: port, + UseTLS: useTLS, + BindDN: bindDN, + Password: ldapPass, + BaseDN: baseDN, + UserFilter: userFilter, + UserAttr: userAttr, + } + ok, err := ldaputil.AuthenticateUser(cfg, username, password) + if err != nil || !ok { + return nil + } + // On successful LDAP auth, continue 2FA checks below + } twoFactorEnable, err := s.settingService.GetTwoFactorEnable() if err != nil {