ldap_sync_job.go 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393
  1. package job
  2. import (
  3. "time"
  4. "github.com/mhsanaei/3x-ui/v2/database/model"
  5. "github.com/mhsanaei/3x-ui/v2/logger"
  6. ldaputil "github.com/mhsanaei/3x-ui/v2/util/ldap"
  7. "github.com/mhsanaei/3x-ui/v2/web/service"
  8. "strings"
  9. "github.com/google/uuid"
  10. "strconv"
  11. )
  12. var DefaultTruthyValues = []string{"true", "1", "yes", "on"}
  13. type LdapSyncJob struct {
  14. settingService service.SettingService
  15. inboundService service.InboundService
  16. xrayService service.XrayService
  17. }
  18. // --- Helper functions for mustGet ---
  19. func mustGetString(fn func() (string, error)) string {
  20. v, err := fn()
  21. if err != nil {
  22. panic(err)
  23. }
  24. return v
  25. }
  26. func mustGetInt(fn func() (int, error)) int {
  27. v, err := fn()
  28. if err != nil {
  29. panic(err)
  30. }
  31. return v
  32. }
  33. func mustGetBool(fn func() (bool, error)) bool {
  34. v, err := fn()
  35. if err != nil {
  36. panic(err)
  37. }
  38. return v
  39. }
  40. func mustGetStringOr(fn func() (string, error), fallback string) string {
  41. v, err := fn()
  42. if err != nil || v == "" {
  43. return fallback
  44. }
  45. return v
  46. }
  47. func NewLdapSyncJob() *LdapSyncJob {
  48. return new(LdapSyncJob)
  49. }
  50. func (j *LdapSyncJob) Run() {
  51. logger.Info("LDAP sync job started")
  52. enabled, err := j.settingService.GetLdapEnable()
  53. if err != nil || !enabled {
  54. logger.Warning("LDAP disabled or failed to fetch flag")
  55. return
  56. }
  57. // --- LDAP fetch ---
  58. cfg := ldaputil.Config{
  59. Host: mustGetString(j.settingService.GetLdapHost),
  60. Port: mustGetInt(j.settingService.GetLdapPort),
  61. UseTLS: mustGetBool(j.settingService.GetLdapUseTLS),
  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. clientsToCreate := map[string][]model.Client{} // tag -> []new clients
  99. clientsToEnable := map[string][]string{} // tag -> []email
  100. clientsToDisable := map[string][]string{} // tag -> []email
  101. for email, allowed := range flags {
  102. exists := allClients[email] != nil
  103. for _, tag := range inboundTags {
  104. if !exists && allowed && autoCreate {
  105. newClient := j.buildClient(inboundMap[tag], email, defGB, defExpiryDays, defLimitIP)
  106. clientsToCreate[tag] = append(clientsToCreate[tag], newClient)
  107. } else if exists {
  108. if allowed && !allClients[email].Enable {
  109. clientsToEnable[tag] = append(clientsToEnable[tag], email)
  110. } else if !allowed && allClients[email].Enable {
  111. clientsToDisable[tag] = append(clientsToDisable[tag], email)
  112. }
  113. }
  114. }
  115. }
  116. // --- Execute batch create ---
  117. for tag, newClients := range clientsToCreate {
  118. if len(newClients) == 0 {
  119. continue
  120. }
  121. payload := &model.Inbound{Id: inboundMap[tag].Id}
  122. payload.Settings = j.clientsToJSON(newClients)
  123. if _, err := j.inboundService.AddInboundClient(payload); err != nil {
  124. logger.Warningf("Failed to add clients for tag %s: %v", tag, err)
  125. } else {
  126. logger.Infof("LDAP auto-create: %d clients for %s", len(newClients), tag)
  127. j.xrayService.SetToNeedRestart()
  128. }
  129. }
  130. // --- Execute enable/disable batch ---
  131. for tag, emails := range clientsToEnable {
  132. j.batchSetEnable(inboundMap[tag], emails, true)
  133. }
  134. for tag, emails := range clientsToDisable {
  135. j.batchSetEnable(inboundMap[tag], emails, false)
  136. }
  137. // --- Auto delete clients not in LDAP ---
  138. autoDelete := mustGetBool(j.settingService.GetLdapAutoDelete)
  139. if autoDelete {
  140. ldapEmailSet := map[string]struct{}{}
  141. for e := range flags {
  142. ldapEmailSet[e] = struct{}{}
  143. }
  144. for _, tag := range inboundTags {
  145. j.deleteClientsNotInLDAP(tag, ldapEmailSet)
  146. }
  147. }
  148. }
  149. func splitCsv(s string) []string {
  150. if s == "" {
  151. return DefaultTruthyValues
  152. }
  153. parts := strings.Split(s, ",")
  154. out := make([]string, 0, len(parts))
  155. for _, p := range parts {
  156. v := strings.TrimSpace(p)
  157. if v != "" {
  158. out = append(out, v)
  159. }
  160. }
  161. return out
  162. }
  163. // buildClient creates a new client for auto-create
  164. func (j *LdapSyncJob) buildClient(ib *model.Inbound, email string, defGB, defExpiryDays, defLimitIP int) model.Client {
  165. c := model.Client{
  166. Email: email,
  167. Enable: true,
  168. LimitIP: defLimitIP,
  169. TotalGB: int64(defGB),
  170. }
  171. if defExpiryDays > 0 {
  172. c.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
  173. }
  174. switch ib.Protocol {
  175. case model.Trojan, model.Shadowsocks:
  176. c.Password = uuid.NewString()
  177. default:
  178. c.ID = uuid.NewString()
  179. }
  180. return c
  181. }
  182. // batchSetEnable enables/disables clients in batch through a single call
  183. func (j *LdapSyncJob) batchSetEnable(ib *model.Inbound, emails []string, enable bool) {
  184. if len(emails) == 0 {
  185. return
  186. }
  187. // Подготовка JSON для массового обновления
  188. clients := make([]model.Client, 0, len(emails))
  189. for _, email := range emails {
  190. clients = append(clients, model.Client{
  191. Email: email,
  192. Enable: enable,
  193. })
  194. }
  195. payload := &model.Inbound{
  196. Id: ib.Id,
  197. Settings: j.clientsToJSON(clients),
  198. }
  199. // Use a single AddInboundClient call to update enable
  200. if _, err := j.inboundService.AddInboundClient(payload); err != nil {
  201. logger.Warningf("Batch set enable failed for inbound %s: %v", ib.Tag, err)
  202. return
  203. }
  204. logger.Infof("Batch set enable=%v for %d clients in inbound %s", enable, len(emails), ib.Tag)
  205. j.xrayService.SetToNeedRestart()
  206. }
  207. // deleteClientsNotInLDAP performs batch deletion of clients not in LDAP
  208. func (j *LdapSyncJob) deleteClientsNotInLDAP(inboundTag string, ldapEmails map[string]struct{}) {
  209. inbounds, err := j.inboundService.GetAllInbounds()
  210. if err != nil {
  211. logger.Warning("Failed to get inbounds for deletion:", err)
  212. return
  213. }
  214. for _, ib := range inbounds {
  215. if ib.Tag != inboundTag {
  216. continue
  217. }
  218. clients, err := j.inboundService.GetClients(ib)
  219. if err != nil {
  220. continue
  221. }
  222. // Сбор клиентов для удаления
  223. toDelete := []model.Client{}
  224. for _, c := range clients {
  225. if _, ok := ldapEmails[c.Email]; !ok {
  226. // Use appropriate field depending on protocol
  227. client := model.Client{Email: c.Email, ID: c.ID, Password: c.Password}
  228. toDelete = append(toDelete, client)
  229. }
  230. }
  231. if len(toDelete) == 0 {
  232. continue
  233. }
  234. payload := &model.Inbound{
  235. Id: ib.Id,
  236. Settings: j.clientsToJSON(toDelete),
  237. }
  238. if _, err := j.inboundService.DelInboundClient(payload.Id, payload.Settings); err != nil {
  239. logger.Warningf("Batch delete failed for inbound %s: %v", ib.Tag, err)
  240. } else {
  241. logger.Infof("Batch deleted %d clients from inbound %s", len(toDelete), ib.Tag)
  242. j.xrayService.SetToNeedRestart()
  243. }
  244. }
  245. }
  246. // clientsToJSON сериализует массив клиентов в JSON
  247. func (j *LdapSyncJob) clientsToJSON(clients []model.Client) string {
  248. b := strings.Builder{}
  249. b.WriteString("{\"clients\":[")
  250. for i, c := range clients {
  251. if i > 0 { b.WriteString(",") }
  252. b.WriteString(j.clientToJSON(c))
  253. }
  254. b.WriteString("]}")
  255. return b.String()
  256. }
  257. // ensureClientExists adds client with defaults to inbound tag if not present
  258. func (j *LdapSyncJob) ensureClientExists(inboundTag string, email string, defGB int, defExpiryDays int, defLimitIP int) {
  259. inbounds, err := j.inboundService.GetAllInbounds()
  260. if err != nil {
  261. logger.Warning("ensureClientExists: get inbounds failed:", err)
  262. return
  263. }
  264. var target *model.Inbound
  265. for _, ib := range inbounds {
  266. if ib.Tag == inboundTag {
  267. target = ib
  268. break
  269. }
  270. }
  271. if target == nil {
  272. logger.Debugf("ensureClientExists: inbound tag %s not found", inboundTag)
  273. return
  274. }
  275. // check if email already exists in this inbound
  276. clients, err := j.inboundService.GetClients(target)
  277. if err == nil {
  278. for _, c := range clients {
  279. if c.Email == email {
  280. return
  281. }
  282. }
  283. }
  284. // build new client according to protocol
  285. newClient := model.Client{
  286. Email: email,
  287. Enable: true,
  288. LimitIP: defLimitIP,
  289. TotalGB: int64(defGB),
  290. }
  291. if defExpiryDays > 0 {
  292. newClient.ExpiryTime = time.Now().Add(time.Duration(defExpiryDays) * 24 * time.Hour).UnixMilli()
  293. }
  294. switch target.Protocol {
  295. case model.Trojan:
  296. newClient.Password = uuid.NewString()
  297. case model.Shadowsocks:
  298. newClient.Password = uuid.NewString()
  299. default: // VMESS/VLESS and others using ID
  300. newClient.ID = uuid.NewString()
  301. }
  302. // prepare inbound payload with only the new client
  303. payload := &model.Inbound{Id: target.Id}
  304. payload.Settings = `{"clients":[` + j.clientToJSON(newClient) + `]}`
  305. if _, err := j.inboundService.AddInboundClient(payload); err != nil {
  306. logger.Warning("ensureClientExists: add client failed:", err)
  307. } else {
  308. j.xrayService.SetToNeedRestart()
  309. logger.Infof("LDAP auto-create: %s in %s", email, inboundTag)
  310. }
  311. }
  312. // clientToJSON serializes minimal client fields to JSON object string without extra deps
  313. func (j *LdapSyncJob) clientToJSON(c model.Client) string {
  314. // construct minimal JSON manually to avoid importing json for simple case
  315. b := strings.Builder{}
  316. b.WriteString("{")
  317. if c.ID != "" {
  318. b.WriteString("\"id\":\"")
  319. b.WriteString(c.ID)
  320. b.WriteString("\",")
  321. }
  322. if c.Password != "" {
  323. b.WriteString("\"password\":\"")
  324. b.WriteString(c.Password)
  325. b.WriteString("\",")
  326. }
  327. b.WriteString("\"email\":\"")
  328. b.WriteString(c.Email)
  329. b.WriteString("\",")
  330. b.WriteString("\"enable\":")
  331. if c.Enable { b.WriteString("true") } else { b.WriteString("false") }
  332. b.WriteString(",")
  333. b.WriteString("\"limitIp\":")
  334. b.WriteString(strconv.Itoa(c.LimitIP))
  335. b.WriteString(",")
  336. b.WriteString("\"totalGB\":")
  337. b.WriteString(strconv.FormatInt(c.TotalGB, 10))
  338. if c.ExpiryTime > 0 {
  339. b.WriteString(",\"expiryTime\":")
  340. b.WriteString(strconv.FormatInt(c.ExpiryTime, 10))
  341. }
  342. b.WriteString("}")
  343. return b.String()
  344. }