client_lookup.go 9.2 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305
  1. package service
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "strings"
  6. "github.com/mhsanaei/3x-ui/v3/internal/database"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  8. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  9. "gorm.io/gorm"
  10. )
  11. func (s *ClientService) GetRecordByEmail(tx *gorm.DB, email string) (*model.ClientRecord, error) {
  12. if tx == nil {
  13. tx = database.GetDB()
  14. }
  15. row := &model.ClientRecord{}
  16. err := tx.Where("email = ?", email).First(row).Error
  17. if err != nil {
  18. return nil, err
  19. }
  20. return row, nil
  21. }
  22. // EffectiveFlow returns the client's flow from the first flow-capable inbound
  23. // it is attached to (lowest inbound_id with a non-empty flow_override). The
  24. // canonical clients.Flow column is unreliable for multi-inbound clients: a
  25. // non-flow inbound (Hysteria, WS, gRPC, …) carries an empty flow and, when its
  26. // SyncInbound runs last, overwrites the column to "" even though a VLESS Reality
  27. // inbound stored a real flow. The per-inbound flow_override is always correct,
  28. // so derive the display flow from it (order-independent). See issue #4792.
  29. func (s *ClientService) EffectiveFlow(tx *gorm.DB, recordId int) (string, error) {
  30. if tx == nil {
  31. tx = database.GetDB()
  32. }
  33. var flows []string
  34. err := tx.Model(&model.ClientInbound{}).
  35. Where("client_id = ? AND flow_override <> ?", recordId, "").
  36. Order("inbound_id ASC").
  37. Limit(1).
  38. Pluck("flow_override", &flows).Error
  39. if err != nil {
  40. return "", err
  41. }
  42. if len(flows) == 0 {
  43. return "", nil
  44. }
  45. return flows[0], nil
  46. }
  47. // EffectiveFlowsByEmails resolves the intended flow (non-empty flow_override,
  48. // lowest inbound_id first — same rule as EffectiveFlow) for many clients in one
  49. // query, keyed by email. Emails absent from the result carry no flow anywhere.
  50. // Batched so flow restoration on an inbound with many clients is O(1) queries
  51. // instead of O(clients). Used to restore a stripped flow onto an inbound that
  52. // has just become flow-eligible.
  53. func (s *ClientService) EffectiveFlowsByEmails(tx *gorm.DB, emails []string) (map[string]string, error) {
  54. if tx == nil {
  55. tx = database.GetDB()
  56. }
  57. out := make(map[string]string, len(emails))
  58. if len(emails) == 0 {
  59. return out, nil
  60. }
  61. type row struct {
  62. Email string
  63. Flow string `gorm:"column:flow_override"`
  64. }
  65. for _, batch := range chunkStrings(emails, sqlInChunk) {
  66. var rows []row
  67. err := tx.Table("client_inbounds").
  68. Select("clients.email AS email, client_inbounds.flow_override AS flow_override").
  69. Joins("JOIN clients ON clients.id = client_inbounds.client_id").
  70. Where("clients.email IN ? AND client_inbounds.flow_override <> ?", batch, "").
  71. Order("client_inbounds.inbound_id ASC").
  72. Scan(&rows).Error
  73. if err != nil {
  74. return nil, err
  75. }
  76. for _, r := range rows {
  77. if _, seen := out[r.Email]; !seen { // ordered by inbound_id ASC → first = lowest
  78. out[r.Email] = r.Flow
  79. }
  80. }
  81. }
  82. return out, nil
  83. }
  84. func (s *ClientService) GetInboundIdsForEmail(tx *gorm.DB, email string) ([]int, error) {
  85. if tx == nil {
  86. tx = database.GetDB()
  87. }
  88. var ids []int
  89. err := tx.Table("client_inbounds").
  90. Select("client_inbounds.inbound_id").
  91. Joins("JOIN clients ON clients.id = client_inbounds.client_id").
  92. Where("clients.email = ?", email).
  93. Scan(&ids).Error
  94. if err != nil {
  95. return nil, err
  96. }
  97. return ids, nil
  98. }
  99. // sub_id carries a plain index, not a unique one: one subscription can cover
  100. // several clients, so callers acting on a subId must handle all of them.
  101. func (s *ClientService) GetRecordsBySubID(subId string) ([]*model.ClientRecord, error) {
  102. if subId == "" {
  103. return nil, errors.New("sub_id must not be empty")
  104. }
  105. var rows []*model.ClientRecord
  106. err := database.GetDB().Where("sub_id = ?", subId).Order("id ASC").Find(&rows).Error
  107. return rows, err
  108. }
  109. func (s *ClientService) GetRecordsByTgID(tgId int64) ([]*model.ClientRecord, error) {
  110. if tgId <= 0 {
  111. return nil, errors.New("tg_id must be a positive integer")
  112. }
  113. var rows []*model.ClientRecord
  114. err := database.GetDB().Where("tg_id = ?", tgId).Find(&rows).Error
  115. return rows, err
  116. }
  117. func (s *ClientService) GetByID(id int) (*model.ClientRecord, error) {
  118. row := &model.ClientRecord{}
  119. if err := database.GetDB().Where("id = ?", id).First(row).Error; err != nil {
  120. return nil, err
  121. }
  122. return row, nil
  123. }
  124. func (s *ClientService) GetInboundIdsForRecord(id int) ([]int, error) {
  125. var ids []int
  126. err := database.GetDB().Table("client_inbounds").
  127. Where("client_id = ?", id).
  128. Order("inbound_id ASC").
  129. Pluck("inbound_id", &ids).Error
  130. if err != nil {
  131. return nil, err
  132. }
  133. return ids, nil
  134. }
  135. // TunnelAllowedIPsByInbound returns, for each given WireGuard/AmneziaWG
  136. // inbound id, the real AllowedIPs this email currently has on that specific
  137. // inbound's own settings JSON -- joined comma-separated, matching the form
  138. // value shape a single AllowedIPs field already uses. Non-tunnel inbounds
  139. // and ids the email isn't actually attached to are simply absent from the
  140. // result (not an error): callers use this to seed a per-protocol display
  141. // field, and ClientRecord's own single AllowedIPs column can't tell two
  142. // different protocol addresses apart, which is exactly the gap this closes.
  143. func (s *ClientService) TunnelAllowedIPsByInbound(inboundSvc *InboundService, email string, inboundIds []int) (map[int]string, error) {
  144. result := make(map[int]string, len(inboundIds))
  145. for _, ibId := range inboundIds {
  146. inbound, err := inboundSvc.GetInbound(ibId)
  147. if err != nil {
  148. if errors.Is(err, gorm.ErrRecordNotFound) {
  149. continue
  150. }
  151. return nil, err
  152. }
  153. if inbound.Protocol != model.WireGuard && inbound.Protocol != model.AmneziaWG {
  154. continue
  155. }
  156. clients, err := inboundSvc.GetClients(inbound)
  157. if err != nil {
  158. return nil, err
  159. }
  160. for i := range clients {
  161. if strings.EqualFold(clients[i].Email, email) {
  162. result[ibId] = strings.Join(clients[i].AllowedIPs, ",")
  163. break
  164. }
  165. }
  166. }
  167. return result, nil
  168. }
  169. func (s *ClientService) List() ([]ClientWithAttachments, error) {
  170. db := database.GetDB()
  171. var rows []model.ClientRecord
  172. if err := db.Order("id ASC").Find(&rows).Error; err != nil {
  173. return nil, err
  174. }
  175. if len(rows) == 0 {
  176. return []ClientWithAttachments{}, nil
  177. }
  178. clientIds := make([]int, 0, len(rows))
  179. emails := make([]string, 0, len(rows))
  180. for i := range rows {
  181. clientIds = append(clientIds, rows[i].Id)
  182. if rows[i].Email != "" {
  183. emails = append(emails, rows[i].Email)
  184. }
  185. }
  186. attachments := make(map[int][]int, len(rows))
  187. for _, batch := range chunkInts(clientIds, sqlInChunk) {
  188. var links []model.ClientInbound
  189. if err := db.Where("client_id IN ?", batch).Find(&links).Error; err != nil {
  190. return nil, err
  191. }
  192. for _, l := range links {
  193. attachments[l.ClientId] = append(attachments[l.ClientId], l.InboundId)
  194. }
  195. }
  196. trafficByEmail := make(map[string]*xray.ClientTraffic, len(emails))
  197. if len(emails) > 0 {
  198. var stats []xray.ClientTraffic
  199. for _, batch := range chunkStrings(emails, sqlInChunk) {
  200. var batchStats []xray.ClientTraffic
  201. if err := db.Where("email IN ?", batch).Find(&batchStats).Error; err != nil {
  202. return nil, err
  203. }
  204. stats = append(stats, batchStats...)
  205. }
  206. overlayGlobalTrafficValues(db, stats)
  207. for i := range stats {
  208. trafficByEmail[stats[i].Email] = &stats[i]
  209. }
  210. }
  211. out := make([]ClientWithAttachments, 0, len(rows))
  212. for i := range rows {
  213. out = append(out, ClientWithAttachments{
  214. ClientRecord: rows[i],
  215. InboundIds: attachments[rows[i].Id],
  216. Traffic: trafficByEmail[rows[i].Email],
  217. })
  218. }
  219. return out, nil
  220. }
  221. func (s *ClientService) HasPendingNode(inboundSvc *InboundService, email string) bool {
  222. if strings.TrimSpace(email) == "" {
  223. return false
  224. }
  225. ids, err := s.GetInboundIdsForEmail(nil, email)
  226. if err != nil {
  227. return false
  228. }
  229. return inboundSvc.AnyNodePending(ids)
  230. }
  231. // findInboundIdsByClientEmail returns every inbound whose settings.clients[]
  232. // JSON contains an entry with the given email. Driver-portable (no JSON
  233. // operators) by parsing in Go — fine for the rare fallback path.
  234. func (s *ClientService) findInboundIdsByClientEmail(email string) ([]int, error) {
  235. var inbounds []model.Inbound
  236. if err := database.GetDB().
  237. Select("id, settings").
  238. Where("settings LIKE ?", "%"+email+"%").
  239. Find(&inbounds).Error; err != nil {
  240. return nil, err
  241. }
  242. out := make([]int, 0, len(inbounds))
  243. for _, ib := range inbounds {
  244. var settings map[string]any
  245. if err := json.Unmarshal([]byte(ib.Settings), &settings); err != nil {
  246. continue
  247. }
  248. clients, ok := settings["clients"].([]any)
  249. if !ok {
  250. continue
  251. }
  252. for _, c := range clients {
  253. cm, ok := c.(map[string]any)
  254. if !ok {
  255. continue
  256. }
  257. if cEmail, _ := cm["email"].(string); cEmail == email {
  258. out = append(out, ib.Id)
  259. break
  260. }
  261. }
  262. }
  263. return out, nil
  264. }
  265. // clientRecordsByEmail batch-loads client rows for emails, keyed by email.
  266. // Callers pass an already-deduplicated list; absent addresses are simply
  267. // missing from the map.
  268. func clientRecordsByEmail(tx *gorm.DB, emails []string) (map[string]*model.ClientRecord, error) {
  269. if tx == nil {
  270. tx = database.GetDB()
  271. }
  272. var records []model.ClientRecord
  273. for _, batch := range chunkStrings(emails, sqlInChunk) {
  274. var rows []model.ClientRecord
  275. if err := tx.Where("email IN ?", batch).Find(&rows).Error; err != nil {
  276. return nil, err
  277. }
  278. records = append(records, rows...)
  279. }
  280. byEmail := make(map[string]*model.ClientRecord, len(records))
  281. for i := range records {
  282. byEmail[records[i].Email] = &records[i]
  283. }
  284. return byEmail, nil
  285. }