magnet-v2.go 3.5 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147
  1. package metainfo
  2. import (
  3. "encoding/hex"
  4. "errors"
  5. "fmt"
  6. "net/url"
  7. "strings"
  8. g "github.com/anacrolix/generics"
  9. "github.com/multiformats/go-multihash"
  10. infohash_v2 "github.com/anacrolix/torrent/types/infohash-v2"
  11. )
  12. // Magnet link components.
  13. type MagnetV2 struct {
  14. InfoHash g.Option[Hash] // Expected in this implementation
  15. V2InfoHash g.Option[infohash_v2.T]
  16. Trackers []string // "tr" values
  17. DisplayName string // "dn" value, if not empty
  18. Params url.Values // All other values, such as "x.pe", "as", "xs" etc.
  19. }
  20. const (
  21. btmhPrefix = "urn:btmh:"
  22. )
  23. func (m MagnetV2) String() string {
  24. // Deep-copy m.Params
  25. vs := make(url.Values, len(m.Params)+len(m.Trackers)+2)
  26. for k, v := range m.Params {
  27. vs[k] = append([]string(nil), v...)
  28. }
  29. for _, tr := range m.Trackers {
  30. vs.Add("tr", tr)
  31. }
  32. if m.DisplayName != "" {
  33. vs.Add("dn", m.DisplayName)
  34. }
  35. // Transmission and Deluge both expect "urn:btih:" to be unescaped. Deluge wants it to be at the
  36. // start of the magnet link. The InfoHash field is expected to be BitTorrent in this
  37. // implementation.
  38. u := url.URL{
  39. Scheme: "magnet",
  40. }
  41. var queryParts []string
  42. if m.InfoHash.Ok {
  43. queryParts = append(queryParts, "xt="+btihPrefix+m.InfoHash.Value.HexString())
  44. }
  45. if m.V2InfoHash.Ok {
  46. queryParts = append(
  47. queryParts,
  48. "xt="+btmhPrefix+infohash_v2.ToMultihash(m.V2InfoHash.Value).HexString(),
  49. )
  50. }
  51. if rem := vs.Encode(); rem != "" {
  52. queryParts = append(queryParts, rem)
  53. }
  54. u.RawQuery = strings.Join(queryParts, "&")
  55. return u.String()
  56. }
  57. // ParseMagnetUri parses Magnet-formatted URIs into a Magnet instance
  58. func ParseMagnetV2Uri(uri string) (m MagnetV2, err error) {
  59. u, err := url.Parse(uri)
  60. if err != nil {
  61. err = fmt.Errorf("error parsing uri: %w", err)
  62. return
  63. }
  64. if u.Scheme != "magnet" {
  65. err = fmt.Errorf("unexpected scheme %q", u.Scheme)
  66. return
  67. }
  68. q := u.Query()
  69. for _, xt := range q["xt"] {
  70. if hashStr, found := strings.CutPrefix(xt, btihPrefix); found {
  71. if m.InfoHash.Ok {
  72. err = errors.New("more than one infohash found in magnet link")
  73. return
  74. }
  75. m.InfoHash.Value, err = parseEncodedV1Infohash(hashStr)
  76. if err != nil {
  77. err = fmt.Errorf("error parsing infohash %q: %w", hashStr, err)
  78. return
  79. }
  80. m.InfoHash.Ok = true
  81. } else if hashStr, found := strings.CutPrefix(xt, btmhPrefix); found {
  82. if m.V2InfoHash.Ok {
  83. err = errors.New("more than one infohash found in magnet link")
  84. return
  85. }
  86. m.V2InfoHash.Value, err = parseV2Infohash(hashStr)
  87. if err != nil {
  88. err = fmt.Errorf("error parsing infohash %q: %w", hashStr, err)
  89. return
  90. }
  91. m.V2InfoHash.Ok = true
  92. } else {
  93. lazyAddParam(&m.Params, "xt", xt)
  94. }
  95. }
  96. q.Del("xt")
  97. m.DisplayName = popFirstValue(q, "dn").UnwrapOrZeroValue()
  98. m.Trackers = q["tr"]
  99. q.Del("tr")
  100. // Add everything we haven't consumed.
  101. copyParams(&m.Params, q)
  102. return
  103. }
  104. func lazyAddParam(vs *url.Values, k, v string) {
  105. if *vs == nil {
  106. g.MakeMap(vs)
  107. }
  108. vs.Add(k, v)
  109. }
  110. func copyParams(dest *url.Values, src url.Values) {
  111. for k, vs := range src {
  112. for _, v := range vs {
  113. lazyAddParam(dest, k, v)
  114. }
  115. }
  116. }
  117. func parseV2Infohash(encoded string) (ih infohash_v2.T, err error) {
  118. b, err := hex.DecodeString(encoded)
  119. if err != nil {
  120. return
  121. }
  122. mh, err := multihash.Decode(b)
  123. if err != nil {
  124. return
  125. }
  126. if mh.Code != multihash.SHA2_256 || mh.Length != infohash_v2.Size || len(mh.Digest) != infohash_v2.Size {
  127. err = errors.New("bad multihash")
  128. return
  129. }
  130. n := copy(ih[:], mh.Digest)
  131. if n != infohash_v2.Size {
  132. panic(n)
  133. }
  134. return
  135. }