mirror of
				https://github.com/superseriousbusiness/gotosocial.git
				synced 2025-11-04 09:12:24 -06:00 
			
		
		
		
	start adding remote instance dereference
This commit is contained in:
		
					parent
					
						
							
								b6c62309f2
							
						
					
				
			
			
				commit
				
					
						24262b11cf
					
				
			
		
					 4 changed files with 115 additions and 1 deletions
				
			
		| 
						 | 
				
			
			@ -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
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
	//
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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.
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
| 
						 | 
				
			
			@ -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")
 | 
			
		||||
}
 | 
			
		||||
| 
						 | 
				
			
			
 | 
			
		|||
		Loading…
	
	Add table
		Add a link
		
	
		Reference in a new issue