ldap_sync_job.go 8.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312
  1. package job
  2. import (
  3. "strings"
  4. "time"
  5. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  6. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  7. ldaputil "github.com/mhsanaei/3x-ui/v3/internal/util/ldap"
  8. "github.com/mhsanaei/3x-ui/v3/internal/web/service"
  9. )
  10. var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
  11. type LdapSyncJob struct {
  12. settingService service.SettingService
  13. inboundService service.InboundService
  14. clientService service.ClientService
  15. xrayService service.XrayService
  16. }
  17. // --- Helper functions for mustGet ---
  18. func mustGetString(fn func() (string, error)) string {
  19. v, err := fn()
  20. if err != nil {
  21. panic(err)
  22. }
  23. return v
  24. }
  25. func mustGetInt(fn func() (int, error)) int {
  26. v, err := fn()
  27. if err != nil {
  28. panic(err)
  29. }
  30. return v
  31. }
  32. func mustGetBool(fn func() (bool, error)) bool {
  33. v, err := fn()
  34. if err != nil {
  35. panic(err)
  36. }
  37. return v
  38. }
  39. func mustGetStringOr(fn func() (string, error), fallback string) string {
  40. v, err := fn()
  41. if err != nil || v == "" {
  42. return fallback
  43. }
  44. return v
  45. }
  46. func NewLdapSyncJob() *LdapSyncJob {
  47. return new(LdapSyncJob)
  48. }
  49. func (j *LdapSyncJob) Run() {
  50. logger.Info("LDAP sync job started")
  51. enabled, err := j.settingService.GetLdapEnable()
  52. if err != nil || !enabled {
  53. logger.Warning("LDAP disabled or failed to fetch flag")
  54. return
  55. }
  56. // --- LDAP fetch ---
  57. cfg := ldaputil.Config{
  58. Host: mustGetString(j.settingService.GetLdapHost),
  59. Port: mustGetInt(j.settingService.GetLdapPort),
  60. UseTLS: mustGetBool(j.settingService.GetLdapUseTLS),
  61. InsecureSkipVerify: mustGetBool(j.settingService.GetLdapInsecureSkipVerify),
  62. BindDN: mustGetString(j.settingService.GetLdapBindDN),
  63. Password: mustGetString(j.settingService.GetLdapPassword),
  64. BaseDN: mustGetString(j.settingService.GetLdapBaseDN),
  65. UserFilter: mustGetString(j.settingService.GetLdapUserFilter),
  66. UserAttr: mustGetString(j.settingService.GetLdapUserAttr),
  67. FlagField: mustGetStringOr(j.settingService.GetLdapFlagField, mustGetString(j.settingService.GetLdapVlessField)),
  68. TruthyVals: splitCsv(mustGetString(j.settingService.GetLdapTruthyValues)),
  69. Invert: mustGetBool(j.settingService.GetLdapInvertFlag),
  70. }
  71. flags, err := ldaputil.FetchVlessFlags(cfg)
  72. if err != nil {
  73. logger.Warning("LDAP fetch failed:", err)
  74. return
  75. }
  76. logger.Infof("Fetched %d LDAP flags", len(flags))
  77. // --- Load all inbounds and all clients once ---
  78. inboundTags := splitCsv(mustGetString(j.settingService.GetLdapInboundTags))
  79. inbounds, err := j.inboundService.GetAllInbounds()
  80. if err != nil {
  81. logger.Warning("Failed to get inbounds:", err)
  82. return
  83. }
  84. allClients := map[string]*model.Client{} // email -> client
  85. inboundMap := map[string]*model.Inbound{} // tag -> inbound
  86. for _, ib := range inbounds {
  87. inboundMap[ib.Tag] = ib
  88. clients, _ := j.inboundService.GetClients(ib)
  89. for i := range clients {
  90. allClients[clients[i].Email] = &clients[i]
  91. }
  92. }
  93. // --- Prepare batch operations ---
  94. autoCreate := mustGetBool(j.settingService.GetLdapAutoCreate)
  95. defGB := mustGetInt(j.settingService.GetLdapDefaultTotalGB)
  96. defExpiryDays := mustGetInt(j.settingService.GetLdapDefaultExpiryDays)
  97. defLimitIP := mustGetInt(j.settingService.GetLdapDefaultLimitIP)
  98. resolvedInboundIds := make([]int, 0, len(inboundTags))
  99. resolvedTags := make([]string, 0, len(inboundTags))
  100. for _, tag := range inboundTags {
  101. ib := inboundMap[tag]
  102. if ib == nil {
  103. logger.Warningf("LDAP inbound tag %s does not match any inbound", tag)
  104. continue
  105. }
  106. resolvedInboundIds = append(resolvedInboundIds, ib.Id)
  107. resolvedTags = append(resolvedTags, tag)
  108. }
  109. clientsToCreate := []model.Client{}
  110. clientsToEnable := map[string][]string{} // tag -> []email
  111. clientsToDisable := map[string][]string{} // tag -> []email
  112. for email, allowed := range flags {
  113. existing := allClients[email]
  114. if existing == nil {
  115. if allowed && autoCreate {
  116. clientsToCreate = append(clientsToCreate, j.buildClient(email, defGB, defExpiryDays, defLimitIP))
  117. }
  118. continue
  119. }
  120. for _, tag := range resolvedTags {
  121. if allowed && !existing.Enable {
  122. clientsToEnable[tag] = append(clientsToEnable[tag], email)
  123. } else if !allowed && existing.Enable {
  124. clientsToDisable[tag] = append(clientsToDisable[tag], email)
  125. }
  126. }
  127. }
  128. j.createClients(clientsToCreate, resolvedInboundIds, resolvedTags)
  129. // --- Execute enable/disable batch ---
  130. for tag, emails := range clientsToEnable {
  131. j.batchSetEnable(inboundMap[tag], emails, true)
  132. }
  133. for tag, emails := range clientsToDisable {
  134. j.batchSetEnable(inboundMap[tag], emails, false)
  135. }
  136. // --- Auto delete clients not in LDAP ---
  137. autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete)
  138. if autoDelete {
  139. ldapEmailSet := map[string]struct{}{}
  140. for e := range flags {
  141. ldapEmailSet[e] = struct{}{}
  142. }
  143. for _, tag := range inboundTags {
  144. j.deleteClientsNotInLDAP(tag, ldapEmailSet)
  145. }
  146. }
  147. }
  148. func splitCsv(s string) []string {
  149. if s == "" {
  150. return DefaultTruthyValues
  151. }
  152. parts := strings.Split(s, ",")
  153. out := make([]string, 0, len(parts))
  154. for _, p := range parts {
  155. v := strings.TrimSpace(p)
  156. if v != "" {
  157. out = append(out, v)
  158. }
  159. }
  160. return out
  161. }
  162. // buildClient creates a new client for auto-create; ClientService.Create fills per-protocol credentials
  163. func (j *LdapSyncJob) buildClient(email string, defGB, defExpiryDays, defLimitIP int) model.Client {
  164. c := model.Client{
  165. Email: email,
  166. Enable: true,
  167. LimitIP: defLimitIP,
  168. TotalGB: int64(defGB),
  169. }
  170. if defExpiryDays > 0 {
  171. c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
  172. }
  173. return c
  174. }
  175. // createClients adds each new LDAP client once, attached to every configured inbound
  176. func (j *LdapSyncJob) createClients(newClients []model.Client, inboundIds []int, tags []string) {
  177. if len(newClients) == 0 || len(inboundIds) == 0 {
  178. return
  179. }
  180. tagList := strings.Join(tags, ",")
  181. created := 0
  182. restartNeeded := false
  183. for _, c := range newClients {
  184. nr, err := j.clientService.Create(&j.inboundService, &service.ClientCreatePayload{Client: c, InboundIds: inboundIds})
  185. if err != nil {
  186. logger.Warningf("Failed to add client %s for tags %s: %v", c.Email, tagList, err)
  187. continue
  188. }
  189. created++
  190. if nr {
  191. restartNeeded = true
  192. }
  193. }
  194. if created == 0 {
  195. return
  196. }
  197. logger.Infof("LDAP auto-create: %d clients for %s", created, tagList)
  198. if restartNeeded {
  199. j.xrayService.SetToNeedRestart()
  200. }
  201. }
  202. func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
  203. if len(emails) == 0 {
  204. return
  205. }
  206. restartNeeded := false
  207. changed := 0
  208. for _, email := range emails {
  209. ok, needRestart, err := j.clientService.SetClientEnableByEmail(&j.inboundService, email, enable)
  210. if err != nil {
  211. logger.Warningf("Batch set enable failed for %s in inbound %s: %v", email, ib.Tag, err)
  212. continue
  213. }
  214. if ok {
  215. changed++
  216. }
  217. if needRestart {
  218. restartNeeded = true
  219. }
  220. }
  221. if changed > 0 {
  222. logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, changed, ib.Tag)
  223. }
  224. if restartNeeded {
  225. j.xrayService.SetToNeedRestart()
  226. }
  227. }
  228. // deleteClientsNotInLDAP deletes clients not in LDAP using batches and a single restart
  229. func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {
  230. inbounds, err := j.inboundService.GetAllInbounds()
  231. if err != nil {
  232. logger.Warning("Failed to get inbounds for deletion:", err)
  233. return
  234. }
  235. batchSize := 50 // clients in 1 batch
  236. restartNeeded := false
  237. for _, ib := range inbounds {
  238. if ib.Tag != inboundTag {
  239. continue
  240. }
  241. clients, err := j.inboundService.GetClients(ib)
  242. if err != nil {
  243. logger.Warningf("Failed to get clients for inbound %s: %v", ib.Tag, err)
  244. continue
  245. }
  246. // Collect clients for deletion
  247. toDelete := []model.Client{}
  248. for _, c := range clients {
  249. if _, ok := ldapEmails[c.Email]; !ok {
  250. toDelete = append(toDelete, c)
  251. }
  252. }
  253. if len(toDelete) == 0 {
  254. continue
  255. }
  256. for i := 0; i < len(toDelete); i += batchSize {
  257. end := min(i+batchSize, len(toDelete))
  258. batch := toDelete[i:end]
  259. for _, c := range batch {
  260. nr, err := j.clientService.DetachByEmail(&j.inboundService, ib.Id, c.Email)
  261. if err != nil {
  262. logger.Warningf("Failed to delete client %s from inbound id=%d(tag=%s): %v",
  263. c.Email, ib.Id, ib.Tag, err)
  264. continue
  265. }
  266. logger.Infof("Deleted client %s from inbound id=%d(tag=%s)",
  267. c.Email, ib.Id, ib.Tag)
  268. if nr {
  269. restartNeeded = true
  270. }
  271. }
  272. }
  273. }
  274. if restartNeeded {
  275. j.xrayService.SetToNeedRestart()
  276. logger.Info("Xray restart scheduled after batch deletion")
  277. }
  278. }