diff options
Diffstat (limited to 'internal/federation/dereferencing')
| -rw-r--r-- | internal/federation/dereferencing/status_permitted.go | 262 | 
1 files changed, 256 insertions, 6 deletions
diff --git a/internal/federation/dereferencing/status_permitted.go b/internal/federation/dereferencing/status_permitted.go index 5c16f9f15..97cd61e93 100644 --- a/internal/federation/dereferencing/status_permitted.go +++ b/internal/federation/dereferencing/status_permitted.go @@ -19,10 +19,13 @@ package dereferencing  import (  	"context" +	"net/url" +	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/gtserror"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/log" +	"github.com/superseriousbusiness/gotosocial/internal/util"  )  // isPermittedStatus returns whether the given status @@ -147,12 +150,67 @@ func (d *Dereferencer) isPermittedReply(  		return onFalse()  	} -	// TODO in next PR: check conditional / -	// with approval and deref Accept. -	if !replyable.Permitted() { +	if replyable.Permitted() && +		!replyable.MatchedOnCollection() { +		// Replier is permitted to do this +		// interaction, and didn't match on +		// a collection so we don't need to +		// do further checking. +		return true, nil +	} + +	// Replier is permitted to do this +	// interaction pending approval, or +	// permitted but matched on a collection. +	// +	// Check if we can dereference +	// an Accept that grants approval. + +	if status.ApprovedByURI == "" { +		// Status doesn't claim to be approved. +		// +		// For replies to local statuses that's +		// fine, we can put it in the DB pending +		// approval, and continue processing it. +		// +		// If permission was granted based on a match +		// with a followers or following collection, +		// we can mark it as PreApproved so the processor +		// sends an accept out for it immediately. +		// +		// For replies to remote statuses, though +		// we should be polite and just drop it. +		if inReplyTo.IsLocal() { +			status.PendingApproval = util.Ptr(true) +			status.PreApproved = replyable.MatchedOnCollection() +			return true, nil +		} + +		return onFalse() +	} + +	// Status claims to be approved, check +	// this by dereferencing the Accept and +	// inspecting the return value. +	if err := d.validateApprovedBy( +		ctx, +		requestUser, +		status.ApprovedByURI, +		status.URI, +		inReplyTo.AccountURI, +	); err != nil { +		// Error dereferencing means we couldn't +		// get the Accept right now or it wasn't +		// valid, so we shouldn't store this status. +		// +		// Do log the error though as it may be +		// interesting for admins to see. +		log.Info(ctx, "rejecting reply with undereferenceable ApprovedByURI: %v", err)  		return onFalse()  	} +	// Status has been approved. +	status.PendingApproval = util.Ptr(false)  	return true, nil  } @@ -206,11 +264,203 @@ func (d *Dereferencer) isPermittedBoost(  		return onFalse()  	} -	// TODO in next PR: check conditional / -	// with approval and deref Accept. -	if !boostable.Permitted() { +	if boostable.Permitted() && +		!boostable.MatchedOnCollection() { +		// Booster is permitted to do this +		// interaction, and didn't match on +		// a collection so we don't need to +		// do further checking. +		return true, nil +	} + +	// Booster is permitted to do this +	// interaction pending approval, or +	// permitted but matched on a collection. +	// +	// Check if we can dereference +	// an Accept that grants approval. + +	if status.ApprovedByURI == "" { +		// Status doesn't claim to be approved. +		// +		// For boosts of local statuses that's +		// fine, we can put it in the DB pending +		// approval, and continue processing it. +		// +		// If permission was granted based on a match +		// with a followers or following collection, +		// we can mark it as PreApproved so the processor +		// sends an accept out for it immediately. +		// +		// For boosts of remote statuses, though +		// we should be polite and just drop it. +		if boostOf.IsLocal() { +			status.PendingApproval = util.Ptr(true) +			status.PreApproved = boostable.MatchedOnCollection() +			return true, nil +		} +  		return onFalse()  	} +	// Boost claims to be approved, check +	// this by dereferencing the Accept and +	// inspecting the return value. +	if err := d.validateApprovedBy( +		ctx, +		requestUser, +		status.ApprovedByURI, +		status.URI, +		boostOf.AccountURI, +	); err != nil { +		// Error dereferencing means we couldn't +		// get the Accept right now or it wasn't +		// valid, so we shouldn't store this status. +		// +		// Do log the error though as it may be +		// interesting for admins to see. +		log.Info(ctx, "rejecting boost with undereferenceable ApprovedByURI: %v", err) +		return onFalse() +	} + +	// Status has been approved. +	status.PendingApproval = util.Ptr(false)  	return true, nil  } + +// validateApprovedBy dereferences the activitystreams Accept at +// the specified IRI, and checks the Accept for validity against +// the provided expectedObject and expectedActor. +// +// Will return either nil if everything looked OK, or an error if +// something went wrong during deref, or if the dereffed Accept +// did not meet expectations. +func (d *Dereferencer) validateApprovedBy( +	ctx context.Context, +	requestUser string, +	approvedByURIStr string, // Eg., "https://example.org/users/someone/accepts/01J2736AWWJ3411CPR833F6D03" +	expectedObject string, // Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R" +	expectedActor string, // Eg., "https://example.org/users/someone" +) error { +	approvedByURI, err := url.Parse(approvedByURIStr) +	if err != nil { +		err := gtserror.Newf("error parsing approvedByURI: %w", err) +		return err +	} + +	// Don't make calls to the remote if it's blocked. +	if blocked, err := d.state.DB.IsDomainBlocked(ctx, approvedByURI.Host); blocked || err != nil { +		err := gtserror.Newf("domain %s is blocked", approvedByURI.Host) +		return err +	} + +	transport, err := d.transportController.NewTransportForUsername(ctx, requestUser) +	if err != nil { +		err := gtserror.Newf("error creating transport: %w", err) +		return err +	} + +	// Make the call to resolve into an Acceptable. +	rsp, err := transport.Dereference(ctx, approvedByURI) +	if err != nil { +		err := gtserror.Newf("error dereferencing %s: %w", approvedByURIStr, err) +		return err +	} + +	acceptable, err := ap.ResolveAcceptable(ctx, rsp.Body) + +	// Tidy up rsp body. +	_ = rsp.Body.Close() + +	if err != nil { +		err := gtserror.Newf("error resolving Accept %s: %w", approvedByURIStr, err) +		return err +	} + +	// Extract the URI/ID of the Accept. +	acceptURI := ap.GetJSONLDId(acceptable) +	acceptURIStr := acceptURI.String() + +	// Check whether input URI and final returned URI +	// have changed (i.e. we followed some redirects). +	rspURL := rsp.Request.URL +	rspURLStr := rspURL.String() +	if rspURLStr != approvedByURIStr { +		// Final URI was different from approvedByURIStr. +		// +		// Make sure it's at least on the same host as +		// what we expected (ie., we weren't redirected +		// across domains), and make sure it's the same +		// as the ID of the Accept we were returned. +		if rspURL.Host != approvedByURI.Host { +			err := gtserror.Newf( +				"final dereference host %s did not match approvedByURI host %s", +				rspURL.Host, approvedByURI.Host, +			) +			return err +		} + +		if acceptURIStr != rspURLStr { +			err := gtserror.Newf( +				"final dereference uri %s did not match returned Accept ID/URI %s", +				rspURLStr, acceptURIStr, +			) +			return err +		} +	} + +	// Ensure the Accept URI has the same host +	// as the Accept Actor, so we know we're +	// not dealing with someone on a different +	// domain just pretending to be the Actor. +	actorIRIs := ap.GetActorIRIs(acceptable) +	if len(actorIRIs) != 1 { +		err := gtserror.New("resolved Accept actor(s) length was not 1") +		return gtserror.SetMalformed(err) +	} + +	actorIRI := actorIRIs[0] +	actorStr := actorIRI.String() + +	if actorIRI.Host != acceptURI.Host { +		err := gtserror.Newf( +			"Accept Actor %s was not the same host as Accept %s", +			actorStr, acceptURIStr, +		) +		return err +	} + +	// Ensure the Accept Actor is who we expect +	// it to be, and not someone else trying to +	// do an Accept for an interaction with a +	// statusable they don't own. +	if actorStr != expectedActor { +		err := gtserror.Newf( +			"Accept Actor %s was not the same as expected actor %s", +			actorStr, expectedActor, +		) +		return err +	} + +	// Ensure the Accept Object is what we expect +	// it to be, ie., it's Accepting the interaction +	// we need it to Accept, and not something else. +	objectIRIs := ap.GetObjectIRIs(acceptable) +	if len(objectIRIs) != 1 { +		err := gtserror.New("resolved Accept object(s) length was not 1") +		return err +	} + +	objectIRI := objectIRIs[0] +	objectStr := objectIRI.String() + +	if objectStr != expectedObject { +		err := gtserror.Newf( +			"resolved Accept Object uri %s was not the same as expected object %s", +			objectStr, expectedObject, +		) +		return err +	} + +	return nil +}  | 
