summaryrefslogtreecommitdiff
path: root/internal/transport
diff options
context:
space:
mode:
Diffstat (limited to 'internal/transport')
-rw-r--r--internal/transport/deliver.go16
-rw-r--r--internal/transport/dereference.go12
-rw-r--r--internal/transport/derefinstance.go326
-rw-r--r--internal/transport/derefmedia.go42
-rw-r--r--internal/transport/finger.go48
-rw-r--r--internal/transport/transport.go95
6 files changed, 448 insertions, 91 deletions
diff --git a/internal/transport/deliver.go b/internal/transport/deliver.go
new file mode 100644
index 000000000..844cb6bea
--- /dev/null
+++ b/internal/transport/deliver.go
@@ -0,0 +1,16 @@
+package transport
+
+import (
+ "context"
+ "net/url"
+)
+
+func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error {
+ return t.sigTransport.BatchDeliver(c, b, recipients)
+}
+
+func (t *transport) Deliver(c context.Context, b []byte, to *url.URL) error {
+ l := t.log.WithField("func", "Deliver")
+ l.Debugf("performing POST to %s", to.String())
+ return t.sigTransport.Deliver(c, b, to)
+}
diff --git a/internal/transport/dereference.go b/internal/transport/dereference.go
new file mode 100644
index 000000000..d7a28fe17
--- /dev/null
+++ b/internal/transport/dereference.go
@@ -0,0 +1,12 @@
+package transport
+
+import (
+ "context"
+ "net/url"
+)
+
+func (t *transport) Dereference(c context.Context, iri *url.URL) ([]byte, error) {
+ l := t.log.WithField("func", "Dereference")
+ l.Debugf("performing GET to %s", iri.String())
+ return t.sigTransport.Dereference(c, iri)
+}
diff --git a/internal/transport/derefinstance.go b/internal/transport/derefinstance.go
new file mode 100644
index 000000000..a8b2ddfc7
--- /dev/null
+++ b/internal/transport/derefinstance.go
@@ -0,0 +1,326 @@
+package transport
+
+import (
+ "context"
+ "encoding/json"
+ "errors"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+ "strings"
+
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/id"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+func (t *transport) DereferenceInstance(c context.Context, iri *url.URL) (*gtsmodel.Instance, error) {
+ l := t.log.WithField("func", "DereferenceInstance")
+
+ var i *gtsmodel.Instance
+ var err error
+
+ // First try to dereference using /api/v1/instance.
+ // This will provide the most complete picture of an instance, and avoid unnecessary api calls.
+ //
+ // This will only work with Mastodon-api compatible instances: Mastodon, some Pleroma instances, GoToSocial.
+ l.Debugf("trying to dereference instance %s by /api/v1/instance", iri.Host)
+ i, err = dereferenceByAPIV1Instance(c, t, iri)
+ if err == nil {
+ l.Debugf("successfully dereferenced instance using /api/v1/instance")
+ return i, nil
+ }
+ l.Debugf("couldn't dereference instance using /api/v1/instance: %s", err)
+
+ // If that doesn't work, try to dereference using /.well-known/nodeinfo.
+ // This will involve two API calls and return less info overall, but should be more widely compatible.
+ l.Debugf("trying to dereference instance %s by /.well-known/nodeinfo", iri.Host)
+ i, err = dereferenceByNodeInfo(c, t, iri)
+ if err == nil {
+ l.Debugf("successfully dereferenced instance using /.well-known/nodeinfo")
+ return i, nil
+ }
+ l.Debugf("couldn't dereference instance using /.well-known/nodeinfo: %s", err)
+
+ // we couldn't dereference the instance using any of the known methods, so just return a minimal representation
+ l.Debugf("returning minimal representation of instance %s", iri.Host)
+ id, err := id.NewRandomULID()
+ if err != nil {
+ return nil, fmt.Errorf("error creating new id for instance %s: %s", iri.Host, err)
+ }
+
+ return &gtsmodel.Instance{
+ ID: id,
+ Domain: iri.Host,
+ URI: iri.String(),
+ }, nil
+}
+
+func dereferenceByAPIV1Instance(c context.Context, t *transport, iri *url.URL) (*gtsmodel.Instance, error) {
+ l := t.log.WithField("func", "dereferenceByAPIV1Instance")
+
+ cleanIRI := &url.URL{
+ Scheme: iri.Scheme,
+ Host: iri.Host,
+ Path: "api/v1/instance",
+ }
+
+ l.Debugf("performing GET to %s", cleanIRI.String())
+ req, err := http.NewRequest("GET", cleanIRI.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(c)
+ req.Header.Add("Accept", "application/json")
+ req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
+ req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
+ req.Header.Set("Host", cleanIRI.Host)
+ t.getSignerMu.Lock()
+ err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
+ t.getSignerMu.Unlock()
+ if err != nil {
+ return nil, err
+ }
+ resp, err := t.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("GET request to %s failed (%d): %s", cleanIRI.String(), resp.StatusCode, resp.Status)
+ }
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(b) == 0 {
+ return nil, errors.New("response bytes was len 0")
+ }
+
+ // try to parse the returned bytes directly into an Instance model
+ apiResp := &apimodel.Instance{}
+ if err := json.Unmarshal(b, apiResp); err != nil {
+ return nil, err
+ }
+
+ var contactUsername string
+ if apiResp.ContactAccount != nil {
+ contactUsername = apiResp.ContactAccount.Username
+ }
+
+ ulid, err := id.NewRandomULID()
+ if err != nil {
+ return nil, err
+ }
+
+ i := &gtsmodel.Instance{
+ ID: ulid,
+ Domain: iri.Host,
+ Title: apiResp.Title,
+ URI: fmt.Sprintf("%s://%s", iri.Scheme, iri.Host),
+ ShortDescription: apiResp.ShortDescription,
+ Description: apiResp.Description,
+ ContactEmail: apiResp.Email,
+ ContactAccountUsername: contactUsername,
+ Version: apiResp.Version,
+ }
+
+ return i, nil
+}
+
+func dereferenceByNodeInfo(c context.Context, t *transport, iri *url.URL) (*gtsmodel.Instance, error) {
+ niIRI, err := callNodeInfoWellKnown(c, t, iri)
+ if err != nil {
+ return nil, fmt.Errorf("dereferenceByNodeInfo: error during initial call to well-known nodeinfo: %s", err)
+ }
+
+ ni, err := callNodeInfo(c, t, niIRI)
+ if err != nil {
+ return nil, fmt.Errorf("dereferenceByNodeInfo: error doing second call to nodeinfo uri %s: %s", niIRI.String(), err)
+ }
+
+ // we got a response of some kind! take what we can from it...
+ id, err := id.NewRandomULID()
+ if err != nil {
+ return nil, fmt.Errorf("dereferenceByNodeInfo: error creating new id for instance %s: %s", iri.Host, err)
+ }
+
+ // this is the bare minimum instance we'll return, and we'll add more stuff to it if we can
+ i := &gtsmodel.Instance{
+ ID: id,
+ Domain: iri.Host,
+ URI: iri.String(),
+ }
+
+ var title string
+ if i, present := ni.Metadata["nodeName"]; present {
+ // it's present, check it's a string
+ if v, ok := i.(string); ok {
+ // it is a string!
+ title = v
+ }
+ }
+ i.Title = title
+
+ var shortDescription string
+ if i, present := ni.Metadata["nodeDescription"]; present {
+ // it's present, check it's a string
+ if v, ok := i.(string); ok {
+ // it is a string!
+ shortDescription = v
+ }
+ }
+ i.ShortDescription = shortDescription
+
+ var contactEmail string
+ var contactAccountUsername string
+ if i, present := ni.Metadata["maintainer"]; present {
+ // it's present, check it's a map
+ if v, ok := i.(map[string]string); ok {
+ // see if there's an email in the map
+ if email, present := v["email"]; present {
+ if err := util.ValidateEmail(email); err == nil {
+ // valid email address
+ contactEmail = email
+ }
+ }
+ // see if there's a 'name' in the map
+ if name, present := v["name"]; present {
+ // name could be just a username, or could be a mention string eg @whatever@aaaa.com
+ username, _, err := util.ExtractMentionParts(name)
+ if err == nil {
+ // it was a mention string
+ contactAccountUsername = username
+ } else {
+ // not a mention string
+ contactAccountUsername = name
+ }
+ }
+ }
+ }
+ i.ContactEmail = contactEmail
+ i.ContactAccountUsername = contactAccountUsername
+
+ var software string
+ if ni.Software.Name != "" {
+ software = ni.Software.Name
+ }
+ if ni.Software.Version != "" {
+ software = software + " " + ni.Software.Version
+ }
+ i.Version = software
+
+ return i, nil
+}
+
+func callNodeInfoWellKnown(c context.Context, t *transport, iri *url.URL) (*url.URL, error) {
+ l := t.log.WithField("func", "callNodeInfoWellKnown")
+
+ cleanIRI := &url.URL{
+ Scheme: iri.Scheme,
+ Host: iri.Host,
+ Path: ".well-known/nodeinfo",
+ }
+
+ l.Debugf("performing GET to %s", cleanIRI.String())
+ req, err := http.NewRequest("GET", cleanIRI.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(c)
+ req.Header.Add("Accept", "application/json")
+ req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
+ req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
+ req.Header.Set("Host", cleanIRI.Host)
+ t.getSignerMu.Lock()
+ err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
+ t.getSignerMu.Unlock()
+ if err != nil {
+ return nil, err
+ }
+ resp, err := t.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("callNodeInfoWellKnown: GET request to %s failed (%d): %s", cleanIRI.String(), resp.StatusCode, resp.Status)
+ }
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(b) == 0 {
+ return nil, errors.New("callNodeInfoWellKnown: response bytes was len 0")
+ }
+
+ wellKnownResp := &apimodel.WellKnownResponse{}
+ if err := json.Unmarshal(b, wellKnownResp); err != nil {
+ return nil, fmt.Errorf("callNodeInfoWellKnown: could not unmarshal server response as WellKnownResponse: %s", err)
+ }
+
+ // look through the links for the first one that matches the nodeinfo schema, this is what we need
+ var nodeinfoHref *url.URL
+ for _, l := range wellKnownResp.Links {
+ if l.Href == "" || !strings.HasPrefix(l.Rel, "http://nodeinfo.diaspora.software/ns/schema/2") {
+ continue
+ }
+ nodeinfoHref, err = url.Parse(l.Href)
+ if err != nil {
+ return nil, fmt.Errorf("callNodeInfoWellKnown: couldn't parse url %s: %s", l.Href, err)
+ }
+ }
+ if nodeinfoHref == nil {
+ return nil, errors.New("callNodeInfoWellKnown: could not find nodeinfo rel in well known response")
+ }
+
+ return nodeinfoHref, nil
+}
+
+func callNodeInfo(c context.Context, t *transport, iri *url.URL) (*apimodel.Nodeinfo, error) {
+ l := t.log.WithField("func", "callNodeInfo")
+
+ l.Debugf("performing GET to %s", iri.String())
+ req, err := http.NewRequest("GET", iri.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(c)
+ req.Header.Add("Accept", "application/json")
+ req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
+ req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
+ req.Header.Set("Host", iri.Host)
+ t.getSignerMu.Lock()
+ err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
+ t.getSignerMu.Unlock()
+ if err != nil {
+ return nil, err
+ }
+ resp, err := t.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("callNodeInfo: GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
+ }
+ b, err := ioutil.ReadAll(resp.Body)
+ if err != nil {
+ return nil, err
+ }
+
+ if len(b) == 0 {
+ return nil, errors.New("callNodeInfo: response bytes was len 0")
+ }
+
+ niResp := &apimodel.Nodeinfo{}
+ if err := json.Unmarshal(b, niResp); err != nil {
+ return nil, fmt.Errorf("callNodeInfo: could not unmarshal server response as Nodeinfo: %s", err)
+ }
+
+ return niResp, nil
+}
diff --git a/internal/transport/derefmedia.go b/internal/transport/derefmedia.go
new file mode 100644
index 000000000..5fa901100
--- /dev/null
+++ b/internal/transport/derefmedia.go
@@ -0,0 +1,42 @@
+package transport
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+)
+
+func (t *transport) DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error) {
+ l := t.log.WithField("func", "DereferenceMedia")
+ l.Debugf("performing GET to %s", iri.String())
+ req, err := http.NewRequest("GET", iri.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(c)
+ if expectedContentType == "" {
+ req.Header.Add("Accept", "*/*")
+ } else {
+ req.Header.Add("Accept", expectedContentType)
+ }
+ req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
+ req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
+ req.Header.Set("Host", iri.Host)
+ t.getSignerMu.Lock()
+ err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
+ t.getSignerMu.Unlock()
+ if err != nil {
+ return nil, err
+ }
+ resp, err := t.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
+ }
+ return ioutil.ReadAll(resp.Body)
+}
diff --git a/internal/transport/finger.go b/internal/transport/finger.go
new file mode 100644
index 000000000..12cd2fb64
--- /dev/null
+++ b/internal/transport/finger.go
@@ -0,0 +1,48 @@
+package transport
+
+import (
+ "context"
+ "fmt"
+ "io/ioutil"
+ "net/http"
+ "net/url"
+)
+
+func (t *transport) Finger(c context.Context, targetUsername string, targetDomain string) ([]byte, error) {
+ l := t.log.WithField("func", "Finger")
+ urlString := fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s@%s", targetDomain, targetUsername, targetDomain)
+ l.Debugf("performing GET to %s", urlString)
+
+ iri, err := url.Parse(urlString)
+ if err != nil {
+ return nil, fmt.Errorf("Finger: error parsing url %s: %s", urlString, err)
+ }
+
+ l.Debugf("performing GET to %s", iri.String())
+
+ req, err := http.NewRequest("GET", iri.String(), nil)
+ if err != nil {
+ return nil, err
+ }
+ req = req.WithContext(c)
+ req.Header.Add("Accept", "application/json")
+ req.Header.Add("Accept", "application/jrd+json")
+ req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
+ req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
+ req.Header.Set("Host", iri.Host)
+ t.getSignerMu.Lock()
+ err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
+ t.getSignerMu.Unlock()
+ if err != nil {
+ return nil, err
+ }
+ resp, err := t.client.Do(req)
+ if err != nil {
+ return nil, err
+ }
+ defer resp.Body.Close()
+ if resp.StatusCode != http.StatusOK {
+ return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
+ }
+ return ioutil.ReadAll(resp.Body)
+}
diff --git a/internal/transport/transport.go b/internal/transport/transport.go
index 8df74f575..04c72de5c 100644
--- a/internal/transport/transport.go
+++ b/internal/transport/transport.go
@@ -3,22 +3,23 @@ package transport
import (
"context"
"crypto"
- "fmt"
- "io/ioutil"
- "net/http"
"net/url"
"sync"
"github.com/go-fed/activity/pub"
"github.com/go-fed/httpsig"
"github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
)
// Transport wraps the pub.Transport interface with some additional
// functionality for fetching remote media.
type Transport interface {
pub.Transport
+ // DereferenceMedia fetches the bytes of the given media attachment IRI, with the expectedContentType.
DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error)
+ // DereferenceInstance dereferences remote instance information, first by checking /api/v1/instance, and then by checking /.well-known/nodeinfo.
+ DereferenceInstance(c context.Context, iri *url.URL) (*gtsmodel.Instance, error)
// Finger performs a webfinger request with the given username and domain, and returns the bytes from the response body.
Finger(c context.Context, targetUsername string, targetDomains string) ([]byte, error)
}
@@ -36,91 +37,3 @@ type transport struct {
getSignerMu *sync.Mutex
log *logrus.Logger
}
-
-func (t *transport) BatchDeliver(c context.Context, b []byte, recipients []*url.URL) error {
- return t.sigTransport.BatchDeliver(c, b, recipients)
-}
-
-func (t *transport) Deliver(c context.Context, b []byte, to *url.URL) error {
- l := t.log.WithField("func", "Deliver")
- l.Debugf("performing POST to %s", to.String())
- return t.sigTransport.Deliver(c, b, to)
-}
-
-func (t *transport) Dereference(c context.Context, iri *url.URL) ([]byte, error) {
- l := t.log.WithField("func", "Dereference")
- l.Debugf("performing GET to %s", iri.String())
- return t.sigTransport.Dereference(c, iri)
-}
-
-func (t *transport) DereferenceMedia(c context.Context, iri *url.URL, expectedContentType string) ([]byte, error) {
- l := t.log.WithField("func", "DereferenceMedia")
- l.Debugf("performing GET to %s", iri.String())
- req, err := http.NewRequest("GET", iri.String(), nil)
- if err != nil {
- return nil, err
- }
- req = req.WithContext(c)
- if expectedContentType == "" {
- req.Header.Add("Accept", "*/*")
- } else {
- req.Header.Add("Accept", expectedContentType)
- }
- req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
- req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
- req.Header.Set("Host", iri.Host)
- t.getSignerMu.Lock()
- err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
- t.getSignerMu.Unlock()
- if err != nil {
- return nil, err
- }
- resp, err := t.client.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
- }
- return ioutil.ReadAll(resp.Body)
-}
-
-func (t *transport) Finger(c context.Context, targetUsername string, targetDomain string) ([]byte, error) {
- l := t.log.WithField("func", "Finger")
- urlString := fmt.Sprintf("https://%s/.well-known/webfinger?resource=acct:%s@%s", targetDomain, targetUsername, targetDomain)
- l.Debugf("performing GET to %s", urlString)
-
- iri, err := url.Parse(urlString)
- if err != nil {
- return nil, fmt.Errorf("Finger: error parsing url %s: %s", urlString, err)
- }
-
- l.Debugf("performing GET to %s", iri.String())
-
- req, err := http.NewRequest("GET", iri.String(), nil)
- if err != nil {
- return nil, err
- }
- req = req.WithContext(c)
- req.Header.Add("Accept", "application/json")
- req.Header.Add("Accept", "application/jrd+json")
- req.Header.Add("Date", t.clock.Now().UTC().Format("Mon, 02 Jan 2006 15:04:05")+" GMT")
- req.Header.Add("User-Agent", fmt.Sprintf("%s %s", t.appAgent, t.gofedAgent))
- req.Header.Set("Host", iri.Host)
- t.getSignerMu.Lock()
- err = t.getSigner.SignRequest(t.privkey, t.pubKeyID, req, nil)
- t.getSignerMu.Unlock()
- if err != nil {
- return nil, err
- }
- resp, err := t.client.Do(req)
- if err != nil {
- return nil, err
- }
- defer resp.Body.Close()
- if resp.StatusCode != http.StatusOK {
- return nil, fmt.Errorf("GET request to %s failed (%d): %s", iri.String(), resp.StatusCode, resp.Status)
- }
- return ioutil.ReadAll(resp.Body)
-}