inbound_clients.go 13 KB

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