1
0

inbound_clients.go 14 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494
  1. package service
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "strings"
  7. "time"
  8. "github.com/google/uuid"
  9. "github.com/mhsanaei/3x-ui/v3/internal/database"
  10. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  11. "github.com/mhsanaei/3x-ui/v3/internal/logger"
  12. "github.com/mhsanaei/3x-ui/v3/internal/util/common"
  13. "github.com/mhsanaei/3x-ui/v3/internal/xray"
  14. "gorm.io/gorm"
  15. )
  16. type CopyClientsResult struct {
  17. Added []string `json:"added"`
  18. Skipped []string `json:"skipped"`
  19. Errors []string `json:"errors"`
  20. }
  21. // enrichClientStats parses each inbound's clients once, fills in the
  22. // UUID/SubId fields on the preloaded ClientStats, and tops up rows owned by
  23. // a sibling inbound (shared-email mode — the row is keyed on email so it
  24. // only preloads on its owning inbound).
  25. func (s *InboundService) enrichClientStats(db *gorm.DB, inbounds []*model.Inbound) {
  26. if len(inbounds) == 0 {
  27. return
  28. }
  29. clientsByInbound := s.backfillClientStats(db, inbounds)
  30. for i, inbound := range inbounds {
  31. clients := clientsByInbound[i]
  32. if len(clients) == 0 || len(inbound.ClientStats) == 0 {
  33. continue
  34. }
  35. cMap := make(map[string]model.Client, len(clients))
  36. for _, c := range clients {
  37. cMap[strings.ToLower(c.Email)] = c
  38. }
  39. for j := range inbound.ClientStats {
  40. email := strings.ToLower(inbound.ClientStats[j].Email)
  41. if c, ok := cMap[email]; ok {
  42. inbound.ClientStats[j].UUID = c.ID
  43. inbound.ClientStats[j].SubId = c.SubID
  44. }
  45. }
  46. }
  47. }
  48. // backfillClientStats tops up each inbound's preloaded ClientStats with rows
  49. // owned by a sibling inbound: client_traffics is keyed on email, so a client
  50. // attached to several inbounds has one row that only preloads on the inbound
  51. // it was created on. Returns the parsed clients per inbound for reuse.
  52. func (s *InboundService) backfillClientStats(db *gorm.DB, inbounds []*model.Inbound) [][]model.Client {
  53. clientsByInbound := make([][]model.Client, len(inbounds))
  54. seenByInbound := make([]map[string]struct{}, len(inbounds))
  55. missing := make(map[string]struct{})
  56. for i, inbound := range inbounds {
  57. clients, _ := s.GetClients(inbound)
  58. clientsByInbound[i] = clients
  59. seen := make(map[string]struct{}, len(inbound.ClientStats))
  60. for _, st := range inbound.ClientStats {
  61. if st.Email != "" {
  62. seen[strings.ToLower(st.Email)] = struct{}{}
  63. }
  64. }
  65. seenByInbound[i] = seen
  66. for _, c := range clients {
  67. if c.Email == "" {
  68. continue
  69. }
  70. if _, ok := seen[strings.ToLower(c.Email)]; !ok {
  71. missing[c.Email] = struct{}{}
  72. }
  73. }
  74. }
  75. if len(missing) > 0 {
  76. emails := make([]string, 0, len(missing))
  77. for e := range missing {
  78. emails = append(emails, e)
  79. }
  80. var extra []xray.ClientTraffic
  81. var loadErr error
  82. for _, batch := range chunkStrings(emails, sqlInChunk) {
  83. var page []xray.ClientTraffic
  84. if err := db.Model(xray.ClientTraffic{}).Where("email IN ?", batch).Find(&page).Error; err != nil {
  85. loadErr = err
  86. break
  87. }
  88. extra = append(extra, page...)
  89. }
  90. if loadErr != nil {
  91. logger.Warning("backfillClientStats:", loadErr)
  92. } else {
  93. byEmail := make(map[string]xray.ClientTraffic, len(extra))
  94. for _, st := range extra {
  95. byEmail[strings.ToLower(st.Email)] = st
  96. }
  97. for i, inbound := range inbounds {
  98. for _, c := range clientsByInbound[i] {
  99. if c.Email == "" {
  100. continue
  101. }
  102. key := strings.ToLower(c.Email)
  103. if _, ok := seenByInbound[i][key]; ok {
  104. continue
  105. }
  106. if st, ok := byEmail[key]; ok {
  107. inbound.ClientStats = append(inbound.ClientStats, st)
  108. seenByInbound[i][key] = struct{}{}
  109. }
  110. }
  111. }
  112. }
  113. }
  114. return clientsByInbound
  115. }
  116. // emailUsedByOtherInbounds reports whether email lives in any inbound other
  117. // than exceptInboundId. Empty email returns false.
  118. func (s *InboundService) emailUsedByOtherInbounds(email string, exceptInboundId int) (bool, error) {
  119. if email == "" {
  120. return false, nil
  121. }
  122. var count int64
  123. err := database.GetDB().Table("client_inbounds").
  124. Joins("JOIN clients ON clients.id = client_inbounds.client_id").
  125. Where("client_inbounds.inbound_id != ? AND LOWER(clients.email) = ?",
  126. exceptInboundId, strings.ToLower(strings.TrimSpace(email))).
  127. Count(&count).Error
  128. if err != nil {
  129. return false, err
  130. }
  131. return count > 0, nil
  132. }
  133. func (s *InboundService) emailsUsedByOtherInbounds(emails []string, exceptInboundId int) (map[string]bool, error) {
  134. shared := make(map[string]bool, len(emails))
  135. want := make(map[string]struct{}, len(emails))
  136. for _, e := range emails {
  137. e = strings.ToLower(strings.TrimSpace(e))
  138. if e != "" {
  139. want[e] = struct{}{}
  140. }
  141. }
  142. if len(want) == 0 {
  143. return shared, nil
  144. }
  145. lowered := make([]string, 0, len(want))
  146. for e := range want {
  147. lowered = append(lowered, e)
  148. }
  149. db := database.GetDB()
  150. for _, batch := range chunkStrings(lowered, sqlInChunk) {
  151. var rows []struct{ Email string }
  152. err := db.Table("client_inbounds").
  153. Joins("JOIN clients ON clients.id = client_inbounds.client_id").
  154. Select("DISTINCT LOWER(clients.email) AS email").
  155. Where("client_inbounds.inbound_id != ? AND LOWER(clients.email) IN ?", exceptInboundId, batch).
  156. Scan(&rows).Error
  157. if err != nil {
  158. return nil, err
  159. }
  160. for _, r := range rows {
  161. shared[r.Email] = true
  162. }
  163. }
  164. return shared, nil
  165. }
  166. func (s *InboundService) writeBackClientSubID(sourceInboundID int, client model.Client, subID string) (bool, error) {
  167. client.SubID = subID
  168. client.UpdatedAt = time.Now().UnixMilli()
  169. if client.Email == "" {
  170. return false, common.NewError("empty client email")
  171. }
  172. settingsBytes, err := json.Marshal(map[string][]model.Client{
  173. "clients": {client},
  174. })
  175. if err != nil {
  176. return false, err
  177. }
  178. updatePayload := &model.Inbound{
  179. Id: sourceInboundID,
  180. Settings: string(settingsBytes),
  181. }
  182. return s.clientService.UpdateInboundClient(s, updatePayload, client.Email)
  183. }
  184. func (s *InboundService) generateRandomCredential(targetProtocol model.Protocol) string {
  185. switch targetProtocol {
  186. case model.VMESS, model.VLESS:
  187. return uuid.NewString()
  188. default:
  189. return strings.ReplaceAll(uuid.NewString(), "-", "")
  190. }
  191. }
  192. func (s *InboundService) buildTargetClientFromSource(source model.Client, targetInbound *model.Inbound, email string, flow string) (model.Client, error) {
  193. nowTs := time.Now().UnixMilli()
  194. target := source
  195. target.Email = email
  196. target.CreatedAt = nowTs
  197. target.UpdatedAt = nowTs
  198. target.ID = ""
  199. target.Password = ""
  200. target.Auth = ""
  201. target.Flow = ""
  202. target.Secret = ""
  203. targetProtocol := targetInbound.Protocol
  204. switch targetProtocol {
  205. case model.VMESS:
  206. target.ID = s.generateRandomCredential(targetProtocol)
  207. case model.VLESS:
  208. target.ID = s.generateRandomCredential(targetProtocol)
  209. if (flow == "xtls-rprx-vision" || flow == "xtls-rprx-vision-udp443") &&
  210. !targetInbound.DisableFlow &&
  211. inboundCanEnableTlsFlow(string(targetProtocol), targetInbound.StreamSettings, targetInbound.Settings) {
  212. target.Flow = flow
  213. }
  214. case model.Trojan, model.Shadowsocks:
  215. target.Password = s.generateRandomCredential(targetProtocol)
  216. case model.Hysteria:
  217. target.Auth = s.generateRandomCredential(targetProtocol)
  218. case model.MTProto:
  219. target.Secret = model.GenerateFakeTLSSecret(mtprotoDomainFromSettings(targetInbound.Settings))
  220. default:
  221. target.ID = s.generateRandomCredential(targetProtocol)
  222. }
  223. return target, nil
  224. }
  225. func (s *InboundService) nextAvailableCopiedEmail(originalEmail string, targetID int, occupied map[string]struct{}) string {
  226. base := fmt.Sprintf("%s_%d", originalEmail, targetID)
  227. candidate := base
  228. suffix := 0
  229. for {
  230. if _, exists := occupied[strings.ToLower(candidate)]; !exists {
  231. occupied[strings.ToLower(candidate)] = struct{}{}
  232. return candidate
  233. }
  234. suffix++
  235. candidate = fmt.Sprintf("%s_%d", base, suffix)
  236. }
  237. }
  238. func (s *InboundService) CopyInboundClients(targetInboundID int, sourceInboundID int, clientEmails []string, flow string) (*CopyClientsResult, bool, error) {
  239. result := &CopyClientsResult{
  240. Added: []string{},
  241. Skipped: []string{},
  242. Errors: []string{},
  243. }
  244. if targetInboundID == sourceInboundID {
  245. return result, false, common.NewError("source and target inbounds must be different")
  246. }
  247. targetInbound, err := s.GetInbound(targetInboundID)
  248. if err != nil {
  249. return result, false, err
  250. }
  251. sourceInbound, err := s.GetInbound(sourceInboundID)
  252. if err != nil {
  253. return result, false, err
  254. }
  255. sourceClients, err := s.GetClients(sourceInbound)
  256. if err != nil {
  257. return result, false, err
  258. }
  259. if len(sourceClients) == 0 {
  260. return result, false, nil
  261. }
  262. allowedEmails := map[string]struct{}{}
  263. if len(clientEmails) > 0 {
  264. for _, email := range clientEmails {
  265. allowedEmails[strings.ToLower(strings.TrimSpace(email))] = struct{}{}
  266. }
  267. }
  268. occupiedEmails := map[string]struct{}{}
  269. allEmails, err := s.GetAllEmails()
  270. if err != nil {
  271. return result, false, err
  272. }
  273. for _, email := range allEmails {
  274. clean := strings.Trim(email, "\"")
  275. if clean != "" {
  276. occupiedEmails[strings.ToLower(clean)] = struct{}{}
  277. }
  278. }
  279. newClients := make([]model.Client, 0)
  280. needRestart := false
  281. for _, sourceClient := range sourceClients {
  282. originalEmail := strings.TrimSpace(sourceClient.Email)
  283. if originalEmail == "" {
  284. continue
  285. }
  286. if len(allowedEmails) > 0 {
  287. if _, ok := allowedEmails[strings.ToLower(originalEmail)]; !ok {
  288. continue
  289. }
  290. }
  291. if sourceClient.SubID == "" {
  292. newSubID := uuid.NewString()
  293. subNeedRestart, subErr := s.writeBackClientSubID(sourceInbound.Id, sourceClient, newSubID)
  294. if subErr != nil {
  295. result.Errors = append(result.Errors, fmt.Sprintf("%s: failed to write source subId: %v", originalEmail, subErr))
  296. continue
  297. }
  298. if subNeedRestart {
  299. needRestart = true
  300. }
  301. sourceClient.SubID = newSubID
  302. }
  303. targetEmail := s.nextAvailableCopiedEmail(originalEmail, targetInboundID, occupiedEmails)
  304. targetClient, buildErr := s.buildTargetClientFromSource(sourceClient, targetInbound, targetEmail, flow)
  305. if buildErr != nil {
  306. result.Errors = append(result.Errors, fmt.Sprintf("%s: %v", originalEmail, buildErr))
  307. continue
  308. }
  309. newClients = append(newClients, targetClient)
  310. result.Added = append(result.Added, targetEmail)
  311. }
  312. if len(newClients) == 0 {
  313. return result, needRestart, nil
  314. }
  315. settingsPayload, err := json.Marshal(map[string][]model.Client{
  316. "clients": newClients,
  317. })
  318. if err != nil {
  319. return result, needRestart, err
  320. }
  321. addNeedRestart, err := s.clientService.AddInboundClient(s, &model.Inbound{
  322. Id: targetInboundID,
  323. Settings: string(settingsPayload),
  324. })
  325. if err != nil {
  326. return result, needRestart, err
  327. }
  328. if addNeedRestart {
  329. needRestart = true
  330. }
  331. return result, needRestart, nil
  332. }
  333. func (s *InboundService) GetClientInboundByTrafficID(trafficId int) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
  334. db := database.GetDB()
  335. var traffics []*xray.ClientTraffic
  336. err = db.Model(xray.ClientTraffic{}).Where("id = ?", trafficId).Find(&traffics).Error
  337. if err != nil {
  338. logger.Warningf("Error retrieving ClientTraffic with trafficId %d: %v", trafficId, err)
  339. return nil, nil, err
  340. }
  341. if len(traffics) == 0 {
  342. return nil, nil, nil
  343. }
  344. traffic = traffics[0]
  345. inbound, err = s.GetInbound(traffic.InboundId)
  346. if errors.Is(err, gorm.ErrRecordNotFound) {
  347. // client_traffics.inbound_id goes stale when an inbound is deleted and
  348. // recreated; fall back to the authoritative client_inbounds link by email.
  349. ids, idErr := s.clientService.GetInboundIdsForEmail(db, traffic.Email)
  350. if idErr != nil {
  351. return traffic, nil, idErr
  352. }
  353. if len(ids) > 0 {
  354. inbound, err = s.GetInbound(ids[0])
  355. }
  356. }
  357. return traffic, inbound, err
  358. }
  359. func (s *InboundService) GetClientInboundByEmail(email string) (traffic *xray.ClientTraffic, inbound *model.Inbound, err error) {
  360. db := database.GetDB()
  361. var traffics []*xray.ClientTraffic
  362. err = db.Model(xray.ClientTraffic{}).Where("email = ?", email).Find(&traffics).Error
  363. if err != nil {
  364. logger.Warningf("Error retrieving ClientTraffic with email %s: %v", email, err)
  365. return nil, nil, err
  366. }
  367. if len(traffics) == 0 {
  368. return nil, nil, nil
  369. }
  370. traffic = traffics[0]
  371. inbound, err = s.GetInbound(traffic.InboundId)
  372. if errors.Is(err, gorm.ErrRecordNotFound) {
  373. // client_traffics.inbound_id is a legacy single-inbound pointer that goes
  374. // stale when an inbound is deleted and recreated: the email-keyed traffic
  375. // row survives but still references the missing inbound. Fall back to the
  376. // authoritative client_inbounds link so email lookups (reset, info, …) work.
  377. ids, idErr := s.clientService.GetInboundIdsForEmail(db, email)
  378. if idErr != nil {
  379. return traffic, nil, idErr
  380. }
  381. if len(ids) > 0 {
  382. inbound, err = s.GetInbound(ids[0])
  383. }
  384. }
  385. if err == nil && inbound != nil && !s.inboundHasClientEmail(inbound, email) {
  386. // The pointed-at inbound still exists but no longer carries the client —
  387. // the client was moved to another inbound (#6059). Resolve through the
  388. // client_inbounds link to the inbound that actually hosts it now.
  389. ids, idErr := s.clientService.GetInboundIdsForEmail(db, email)
  390. if idErr == nil {
  391. for _, id := range ids {
  392. if id == inbound.Id {
  393. continue
  394. }
  395. if other, oErr := s.GetInbound(id); oErr == nil && s.inboundHasClientEmail(other, email) {
  396. inbound = other
  397. break
  398. }
  399. }
  400. }
  401. }
  402. return traffic, inbound, err
  403. }
  404. func (s *InboundService) inboundHasClientEmail(inbound *model.Inbound, email string) bool {
  405. clients, err := s.GetClients(inbound)
  406. if err != nil {
  407. return false
  408. }
  409. for _, client := range clients {
  410. if client.Email == email {
  411. return true
  412. }
  413. }
  414. return false
  415. }
  416. func (s *InboundService) GetClientByEmail(clientEmail string) (*xray.ClientTraffic, *model.Client, error) {
  417. traffic, inbound, err := s.GetClientInboundByEmail(clientEmail)
  418. if err != nil {
  419. return nil, nil, err
  420. }
  421. if inbound == nil {
  422. return nil, nil, common.NewError("Inbound Not Found For Email:", clientEmail)
  423. }
  424. clients, err := s.GetClients(inbound)
  425. if err != nil {
  426. return nil, nil, err
  427. }
  428. for _, client := range clients {
  429. if client.Email == clientEmail {
  430. return traffic, &client, nil
  431. }
  432. }
  433. return nil, nil, common.NewError("Client Not Found In Inbound For Email:", clientEmail)
  434. }
  435. // EmailsByInbound returns the list of client emails currently configured on
  436. // an inbound's settings.clients[]. Used by the "delete all clients" flow on
  437. // the inbounds page, which then feeds the list into ClientService.BulkDelete.
  438. func (s *InboundService) EmailsByInbound(inboundId int) ([]string, error) {
  439. inbound, err := s.GetInbound(inboundId)
  440. if err != nil {
  441. return nil, err
  442. }
  443. clients, err := s.GetClients(inbound)
  444. if err != nil {
  445. return nil, err
  446. }
  447. emails := make([]string, 0, len(clients))
  448. for _, c := range clients {
  449. if e := strings.TrimSpace(c.Email); e != "" {
  450. emails = append(emails, e)
  451. }
  452. }
  453. return emails, nil
  454. }