| 123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160 |
- package sub
- import (
- "encoding/base64"
- "net/url"
- "strings"
- "github.com/goccy/go-json"
- "github.com/mhsanaei/3x-ui/v3/internal/database"
- "github.com/mhsanaei/3x-ui/v3/internal/database/model"
- "github.com/mhsanaei/3x-ui/v3/internal/util/json_util"
- "github.com/mhsanaei/3x-ui/v3/internal/util/link"
- )
- // externalLinkEntry is one client × external-link row, resolved for a
- // subscription request. Email/Enable come from the owning client.
- type externalLinkEntry struct {
- Kind string
- Value string
- Remark string
- Email string
- Enable bool
- }
- // expandedLink is a single share link contributed by an entry, with the display
- // name to use (empty → keep the link's own remark / fall back to the email).
- type expandedLink struct {
- Link string
- Name string
- }
- // getClientExternalLinksBySubId returns every external-link row attached to a
- // client that carries the given subId, in stable order. Stays inside
- // internal/sub + database + util/link — no dependency on the panel service layer.
- func (s *SubService) getClientExternalLinksBySubId(subId string) ([]externalLinkEntry, error) {
- db := database.GetDB()
- var recs []model.ClientRecord
- if err := db.Where("sub_id = ?", subId).Find(&recs).Error; err != nil {
- return nil, err
- }
- if len(recs) == 0 {
- return nil, nil
- }
- clientIds := make([]int, 0, len(recs))
- byId := make(map[int]model.ClientRecord, len(recs))
- for _, rec := range recs {
- clientIds = append(clientIds, rec.Id)
- byId[rec.Id] = rec
- }
- var rows []model.ClientExternalLink
- if err := db.Where("client_id IN ?", clientIds).
- Order("client_id ASC, sort_index ASC, id ASC").
- Find(&rows).Error; err != nil {
- return nil, err
- }
- if len(rows) == 0 {
- return nil, nil
- }
- out := make([]externalLinkEntry, 0, len(rows))
- for _, r := range rows {
- rec := byId[r.ClientId]
- out = append(out, externalLinkEntry{
- Kind: r.Kind,
- Value: r.Value,
- Remark: r.Remark,
- Email: rec.Email,
- Enable: rec.Enable,
- })
- }
- return out, nil
- }
- // expandEntry turns one entry into the concrete share links it contributes. A
- // "subscription" entry is fetched (cached) and its links are kept with their own
- // names; a "link" entry yields the single link with the row's remark.
- func expandEntry(e externalLinkEntry) []expandedLink {
- if e.Kind == model.ExternalLinkKindSubscription {
- links := fetchSubscriptionLinks(e.Value)
- out := make([]expandedLink, 0, len(links))
- for _, l := range links {
- out = append(out, expandedLink{Link: l, Name: ""})
- }
- return out
- }
- return []expandedLink{{Link: e.Value, Name: e.Remark}}
- }
- // applyRemarkToLink rewrites a share link's display name to remark (when set),
- // leaving everything else byte-for-byte. vmess carries its remark in the base64
- // JSON `ps`; every other scheme carries it in the URL #fragment.
- func applyRemarkToLink(rawLink, remark string) string {
- rawLink = strings.TrimSpace(rawLink)
- if remark == "" {
- return rawLink
- }
- if strings.HasPrefix(rawLink, "vmess://") {
- return applyVmessRemark(rawLink, remark)
- }
- if i := strings.IndexByte(rawLink, '#'); i >= 0 {
- rawLink = rawLink[:i]
- }
- return rawLink + "#" + url.PathEscape(remark)
- }
- func applyVmessRemark(rawLink, remark string) string {
- b64 := strings.TrimPrefix(rawLink, "vmess://")
- raw, err := base64.StdEncoding.DecodeString(padBase64Sub(b64))
- if err != nil {
- raw, err = base64.RawURLEncoding.DecodeString(strings.TrimRight(b64, "="))
- }
- if err != nil {
- return rawLink
- }
- var j map[string]any
- if err := json.Unmarshal(raw, &j); err != nil {
- return rawLink
- }
- j["ps"] = remark
- nb, err := json.Marshal(j)
- if err != nil {
- return rawLink
- }
- return "vmess://" + base64.StdEncoding.EncodeToString(nb)
- }
- func padBase64Sub(s string) string {
- for len(s)%4 != 0 {
- s += "="
- }
- return s
- }
- // parsedExternalOutbound turns a pasted share link into a structured Xray
- // outbound (tagged "proxy") for the JSON subscription. Returns nil when the
- // link can't be parsed — the caller skips it.
- func parsedExternalOutbound(rawLink string) json_util.RawMessage {
- ob := parseExternalLink(rawLink)
- if ob == nil {
- return nil
- }
- ob["tag"] = "proxy"
- b, err := json.MarshalIndent(ob, "", " ")
- if err != nil {
- return nil
- }
- return b
- }
- // parseExternalLink parses a share link into the Xray outbound wire shape
- // (map), or nil if unsupported/invalid.
- func parseExternalLink(rawLink string) map[string]any {
- res, err := link.ParseLink(strings.TrimSpace(rawLink))
- if err != nil || res == nil || res.Outbound == nil {
- return nil
- }
- return map[string]any(res.Outbound)
- }
|