From 18c39ad23d7d6e6cb5681a40d424c6981c2b2323 Mon Sep 17 00:00:00 2001 From: tsmethurst Date: Thu, 24 Jun 2021 19:03:30 +0200 Subject: [PATCH] start adding remote instance dereference --- internal/federation/federatingprotocol.go | 12 ++++ internal/federation/federator.go | 3 + internal/federation/util.go | 13 +++- internal/transport/transport.go | 88 +++++++++++++++++++++++ 4 files changed, 115 insertions(+), 1 deletion(-) diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index 8784c32..fc80fb6 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -125,6 +125,18 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr return ctx, false, fmt.Errorf("not authenticated: %s", err) } + // authentication has passed, so add an instance entry for this instance if it hasn't been done already + i := >smodel.Instance{} + if err := f.db.GetWhere([]db.Where{{Key: "domain", Value: publicKeyOwnerURI.Host, CaseInsensitive: true}}, i); err != nil { + if _, ok := err.(db.ErrNoEntries); !ok { + // there's been an actual error + return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", publicKeyOwnerURI.String(), err) + } + // we don't have an entry for this instance yet so create it + var err error + i, err := f.DereferenceRemoteInstance() + } + requestingAccount := >smodel.Account{} if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: publicKeyOwnerURI.String()}}, requestingAccount); err != nil { // there's been a proper error so return it diff --git a/internal/federation/federator.go b/internal/federation/federator.go index 016a6fb..1d113df 100644 --- a/internal/federation/federator.go +++ b/internal/federation/federator.go @@ -24,6 +24,7 @@ import ( "github.com/go-fed/activity/pub" "github.com/sirupsen/logrus" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation/federatingdb" @@ -49,6 +50,8 @@ type Federator interface { // DereferenceRemoteStatus can be used to get the representation of a remote status, based on its ID (which is a URI). // The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments. DereferenceRemoteStatus(username string, remoteStatusID *url.URL) (typeutils.Statusable, error) + // DereferenceRemoteInstance + DereferenceRemoteInstance(username string, remoteInstanceURI *url.URL) (*apimodel.Instance, error) // GetTransportForUser returns a new transport initialized with the key credentials belonging to the given username. // This can be used for making signed http requests. // diff --git a/internal/federation/util.go b/internal/federation/util.go index 7be92e1..46fdbd4 100644 --- a/internal/federation/util.go +++ b/internal/federation/util.go @@ -37,6 +37,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/transport" "github.com/superseriousbusiness/gotosocial/internal/typeutils" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" ) /* @@ -134,7 +135,8 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques var pkOwnerURI *url.URL requestingRemoteAccount := >smodel.Account{} requestingLocalAccount := >smodel.Account{} - if strings.EqualFold(requestingPublicKeyID.Host, f.config.Host) { + requestingHost := requestingPublicKeyID.Host + if strings.EqualFold(requestingHost, f.config.Host) { // LOCAL ACCOUNT REQUEST // the request is coming from INSIDE THE HOUSE so skip the remote dereferencing if err := f.db.GetWhere([]db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingLocalAccount); err != nil { @@ -340,6 +342,15 @@ func (f *federator) DereferenceRemoteStatus(username string, remoteStatusID *url return nil, fmt.Errorf("type name %s not supported", t.GetTypeName()) } +func (f *federator) DereferenceRemoteInstance(username string, remoteInstanceURI *url.URL) (*apimodel.Instance, error) { + transport, err := f.GetTransportForUser(username) + if err != nil { + return nil, fmt.Errorf("transport err: %s", err) + } + + return transport.DereferenceInstance(context.Background(), remoteInstanceURI) +} + func (f *federator) GetTransportForUser(username string) (transport.Transport, error) { // We need an account to use to create a transport for dereferecing the signature. // If a username has been given, we can fetch the account with that username and use it. diff --git a/internal/transport/transport.go b/internal/transport/transport.go index 8df74f5..5267396 100644 --- a/internal/transport/transport.go +++ b/internal/transport/transport.go @@ -3,6 +3,8 @@ package transport import ( "context" "crypto" + "encoding/json" + "errors" "fmt" "io/ioutil" "net/http" @@ -12,13 +14,17 @@ import ( "github.com/go-fed/activity/pub" "github.com/go-fed/httpsig" "github.com/sirupsen/logrus" + apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" ) // 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) (*apimodel.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) } @@ -124,3 +130,85 @@ func (t *transport) Finger(c context.Context, targetUsername string, targetDomai } return ioutil.ReadAll(resp.Body) } + +func (t *transport) DereferenceInstance(c context.Context, iri *url.URL) (*apimodel.Instance, error) { + l := t.log.WithField("func", "DereferenceInstance") + + var i *apimodel.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(t, c, 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(t, c, 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) + + return nil, fmt.Errorf("couldn't dereference instance %s using either /api/v1/instance or /.well-known/nodeinfo", iri.Host) +} + +func dereferenceByAPIV1Instance(t *transport, c context.Context, iri *url.URL) (*apimodel.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 + } + + // try to parse the returned bytes directly into an Instance model + i := &apimodel.Instance{} + if err := json.Unmarshal(b, i); err != nil { + return nil, err + } + + return i, nil +} + +func dereferenceByNodeInfo(t *transport, c context.Context, iri *url.URL) (*apimodel.Instance, error) { + return nil, errors.New("not yet implemented") +}