external_config.go 4.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160
  1. package sub
  2. import (
  3. "encoding/base64"
  4. "net/url"
  5. "strings"
  6. "github.com/goccy/go-json"
  7. "github.com/mhsanaei/3x-ui/v3/internal/database"
  8. "github.com/mhsanaei/3x-ui/v3/internal/database/model"
  9. "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
  10. "github.com/mhsanaei/3x-ui/v3/internal/util/link"
  11. )
  12. // externalLinkEntry is one client × external-link row, resolved for a
  13. // subscription request. Email/Enable come from the owning client.
  14. type externalLinkEntry struct {
  15. Kind string
  16. Value string
  17. Remark string
  18. Email string
  19. Enable bool
  20. }
  21. // expandedLink is a single share link contributed by an entry, with the display
  22. // name to use (empty → keep the link's own remark / fall back to the email).
  23. type expandedLink struct {
  24. Link string
  25. Name string
  26. }
  27. // getClientExternalLinksBySubId returns every external-link row attached to a
  28. // client that carries the given subId, in stable order. Stays inside
  29. // internal/sub + database + util/link — no dependency on the panel service layer.
  30. func (s *SubService) getClientExternalLinksBySubId(subId string) ([]externalLinkEntry, error) {
  31. db := database.GetDB()
  32. var recs []model.ClientRecord
  33. if err := db.Where("sub_id = ?", subId).Find(&recs).Error; err != nil {
  34. return nil, err
  35. }
  36. if len(recs) == 0 {
  37. return nil, nil
  38. }
  39. clientIds := make([]int, 0, len(recs))
  40. byId := make(map[int]model.ClientRecord, len(recs))
  41. for _, rec := range recs {
  42. clientIds = append(clientIds, rec.Id)
  43. byId[rec.Id] = rec
  44. }
  45. var rows []model.ClientExternalLink
  46. if err := db.Where("client_id IN ?", clientIds).
  47. Order("client_id ASC, sort_index ASC, id ASC").
  48. Find(&rows).Error; err != nil {
  49. return nil, err
  50. }
  51. if len(rows) == 0 {
  52. return nil, nil
  53. }
  54. out := make([]externalLinkEntry, 0, len(rows))
  55. for _, r := range rows {
  56. rec := byId[r.ClientId]
  57. out = append(out, externalLinkEntry{
  58. Kind: r.Kind,
  59. Value: r.Value,
  60. Remark: r.Remark,
  61. Email: rec.Email,
  62. Enable: rec.Enable,
  63. })
  64. }
  65. return out, nil
  66. }
  67. // expandEntry turns one entry into the concrete share links it contributes. A
  68. // "subscription" entry is fetched (cached) and its links are kept with their own
  69. // names; a "link" entry yields the single link with the row's remark.
  70. func expandEntry(e externalLinkEntry) []expandedLink {
  71. if e.Kind == model.ExternalLinkKindSubscription {
  72. links := fetchSubscriptionLinks(e.Value)
  73. out := make([]expandedLink, 0, len(links))
  74. for _, l := range links {
  75. out = append(out, expandedLink{Link: l, Name: ""})
  76. }
  77. return out
  78. }
  79. return []expandedLink{{Link: e.Value, Name: e.Remark}}
  80. }
  81. // applyRemarkToLink rewrites a share link's display name to remark (when set),
  82. // leaving everything else byte-for-byte. vmess carries its remark in the base64
  83. // JSON `ps`; every other scheme carries it in the URL #fragment.
  84. func applyRemarkToLink(rawLink, remark string) string {
  85. rawLink = strings.TrimSpace(rawLink)
  86. if remark == "" {
  87. return rawLink
  88. }
  89. if strings.HasPrefix(rawLink, "vmess://") {
  90. return applyVmessRemark(rawLink, remark)
  91. }
  92. if i := strings.IndexByte(rawLink, '#'); i >= 0 {
  93. rawLink = rawLink[:i]
  94. }
  95. return rawLink + "#" + url.PathEscape(remark)
  96. }
  97. func applyVmessRemark(rawLink, remark string) string {
  98. b64 := strings.TrimPrefix(rawLink, "vmess://")
  99. raw, err := base64.StdEncoding.DecodeString(padBase64Sub(b64))
  100. if err != nil {
  101. raw, err = base64.RawURLEncoding.DecodeString(strings.TrimRight(b64, "="))
  102. }
  103. if err != nil {
  104. return rawLink
  105. }
  106. var j map[string]any
  107. if err := json.Unmarshal(raw, &j); err != nil {
  108. return rawLink
  109. }
  110. j["ps"] = remark
  111. nb, err := json.Marshal(j)
  112. if err != nil {
  113. return rawLink
  114. }
  115. return "vmess://" + base64.StdEncoding.EncodeToString(nb)
  116. }
  117. func padBase64Sub(s string) string {
  118. for len(s)%4 != 0 {
  119. s += "="
  120. }
  121. return s
  122. }
  123. // parsedExternalOutbound turns a pasted share link into a structured Xray
  124. // outbound (tagged "proxy") for the JSON subscription. Returns nil when the
  125. // link can't be parsed — the caller skips it.
  126. func parsedExternalOutbound(rawLink string) json_util.RawMessage {
  127. ob := parseExternalLink(rawLink)
  128. if ob == nil {
  129. return nil
  130. }
  131. ob["tag"] = "proxy"
  132. b, err := json.MarshalIndent(ob, "", " ")
  133. if err != nil {
  134. return nil
  135. }
  136. return b
  137. }
  138. // parseExternalLink parses a share link into the Xray outbound wire shape
  139. // (map), or nil if unsupported/invalid.
  140. func parseExternalLink(rawLink string) map[string]any {
  141. res, err := link.ParseLink(strings.TrimSpace(rawLink))
  142. if err != nil || res == nil || res.Outbound == nil {
  143. return nil
  144. }
  145. return map[string]any(res.Outbound)
  146. }