client.go 4.3 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194
  1. package client
  2. import (
  3. "encoding/json"
  4. "errors"
  5. "fmt"
  6. "golang.org/x/sync/singleflight"
  7. "servitor/jtp"
  8. "servitor/object"
  9. "net/url"
  10. "os"
  11. "strings"
  12. )
  13. const MAX_REDIRECTS = 20
  14. func FetchUnknown(input any, source *url.URL) (object.Object, *url.URL, error) {
  15. var obj object.Object
  16. switch narrowed := input.(type) {
  17. case string:
  18. ref, err := url.Parse(narrowed)
  19. if err != nil {
  20. return nil, nil, err
  21. }
  22. if source != nil {
  23. obj, source, err = FetchURL(source.ResolveReference(ref))
  24. } else {
  25. obj, source, err = FetchURL(ref)
  26. }
  27. if err != nil {
  28. return nil, nil, err
  29. }
  30. case map[string]any:
  31. obj = object.Object(narrowed)
  32. default:
  33. return nil, nil, fmt.Errorf("can't turn non-string, non-object %T into Item", input)
  34. }
  35. id, err := obj.GetURL("id")
  36. if errors.Is(err, object.ErrKeyNotPresent) {
  37. id = nil
  38. } else if err != nil {
  39. return nil, nil, err
  40. }
  41. /* Refetch if necessary */
  42. if id != nil && (source == nil || source.Host != id.Host || len(obj) <= 2) {
  43. obj, source, err = FetchURL(id)
  44. if err != nil {
  45. return nil, nil, err
  46. }
  47. /* Verify that now the id matches the source it came from */
  48. id, err = obj.GetURL("id")
  49. if errors.Is(err, object.ErrKeyNotPresent) {
  50. id = nil
  51. } else if err != nil {
  52. return nil, nil, err
  53. }
  54. if id != nil && source.Host != id.Host {
  55. return nil, nil, errors.New("received response with forged identifier")
  56. }
  57. }
  58. return obj, id, nil
  59. }
  60. var group singleflight.Group
  61. type bundle struct {
  62. item map[string]any
  63. source *url.URL
  64. err error
  65. }
  66. /* A map of mutexes is used to ensure no two requests are made simultaneously.
  67. Instead, the subsequent ones will wait for the first one to finish (and will
  68. then naturally find its result in the cache) */
  69. func FetchURL(uri *url.URL) (object.Object, *url.URL, error) {
  70. uriString := uri.String()
  71. b, _, _ := group.Do(uriString, func() (any, error) {
  72. json, source, err :=
  73. jtp.Get(
  74. uri,
  75. `application/activity+json,`+
  76. `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`,
  77. []string{
  78. "application/activity+json",
  79. "application/ld+json",
  80. "application/json",
  81. },
  82. MAX_REDIRECTS,
  83. )
  84. return bundle{
  85. item: json,
  86. source: source,
  87. err: err,
  88. }, nil
  89. })
  90. /* By this point the result has been cached in the LRU cache,
  91. so it can be dropped from the singleflight cache */
  92. group.Forget(uriString)
  93. return b.(bundle).item, b.(bundle).source, b.(bundle).err
  94. }
  95. /*
  96. converts a webfinger identifier to a url
  97. see: https://datatracker.ietf.org/doc/html/rfc7033
  98. */
  99. func ResolveWebfinger(username string) (string, error) {
  100. split := strings.SplitN(username, "@", 2)
  101. var account, domain string
  102. if len(split) != 2 {
  103. return "", errors.New("webfinger address must have a separating @ symbol")
  104. }
  105. account = split[0]
  106. domain = split[1]
  107. link := &url.URL{
  108. Scheme: "https",
  109. Host: domain,
  110. Path: "/.well-known/webfinger",
  111. RawQuery: (url.Values{
  112. "resource": []string{"acct:" + account + "@" + domain},
  113. }).Encode(),
  114. }
  115. json, _, err := jtp.Get(link, "application/jrd+json", []string{
  116. "application/jrd+json",
  117. "application/json",
  118. }, MAX_REDIRECTS)
  119. if err != nil {
  120. return "", err
  121. }
  122. response := object.Object(json)
  123. jrdLinks, err := response.GetList("links")
  124. if err != nil {
  125. return "", err
  126. }
  127. found := false
  128. var underlyingLink string
  129. for _, el := range jrdLinks {
  130. asMap, ok := el.(map[string]any)
  131. o := object.Object(asMap)
  132. if ok {
  133. rel, err := o.GetString("rel")
  134. if err != nil {
  135. return "", err
  136. }
  137. if rel != "self" {
  138. continue
  139. }
  140. mediaType, err := o.GetMediaType("type")
  141. if errors.Is(err, object.ErrKeyNotPresent) {
  142. continue
  143. } else if err != nil {
  144. return "", err
  145. }
  146. if !mediaType.Matches([]string{
  147. "application/activity+json",
  148. "application/ld+json",
  149. }) {
  150. continue
  151. }
  152. href, err := o.GetString("href")
  153. if err != nil {
  154. return "", err
  155. }
  156. found = true
  157. underlyingLink = href
  158. break
  159. } else {
  160. return "", fmt.Errorf("unrecognized type %T found in webfinger response", el)
  161. }
  162. }
  163. if !found {
  164. return "", errors.New("actor not found in webfinger listing")
  165. }
  166. return underlyingLink, nil
  167. }
  168. func FetchFromFile(name string) (object.Object, error) {
  169. file, err := os.Open(name)
  170. if err != nil {
  171. return nil, err
  172. }
  173. var obj object.Object
  174. json.NewDecoder(file).Decode(&obj)
  175. return obj, nil
  176. }