start adding remote instance dereference

This commit is contained in:
tsmethurst 2021-06-24 19:03:30 +02:00
parent c1e107266f
commit 18c39ad23d
4 changed files with 115 additions and 1 deletions

View File

@ -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 := &gtsmodel.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 := &gtsmodel.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

View File

@ -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.
//

View File

@ -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 := &gtsmodel.Account{}
requestingLocalAccount := &gtsmodel.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.

View File

@ -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")
}