diff options
Diffstat (limited to 'internal/federation')
| -rw-r--r-- | internal/federation/federatingprotocol.go | 141 | ||||
| -rw-r--r-- | internal/federation/federatingprotocol_test.go | 210 | ||||
| -rw-r--r-- | internal/federation/federator_test.go | 2 | 
3 files changed, 314 insertions, 39 deletions
| diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index e1ca3e7e5..a41d1ae80 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -33,6 +33,7 @@ import (  	"github.com/superseriousbusiness/gotosocial/internal/db"  	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"  	"github.com/superseriousbusiness/gotosocial/internal/uris" +	"github.com/superseriousbusiness/gotosocial/internal/util"  )  /* @@ -62,19 +63,60 @@ import (  // write a response to the ResponseWriter as is expected that the caller  // to PostInbox will do so when handling the error.  func (f *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) { -	l := logrus.WithFields(logrus.Fields{ -		"func":      "PostInboxRequestBodyHook", -		"useragent": r.UserAgent(), -		"url":       r.URL.String(), -	}) +	// extract any other IRIs involved in this activity +	otherInvolvedIRIs := []*url.URL{} + +	// check if the Activity itself has an 'inReplyTo' +	if replyToable, ok := activity.(ap.ReplyToable); ok { +		if inReplyToURI := ap.ExtractInReplyToURI(replyToable); inReplyToURI != nil { +			otherInvolvedIRIs = append(otherInvolvedIRIs, inReplyToURI) +		} +	} -	if activity == nil { -		err := errors.New("nil activity in PostInboxRequestBodyHook") -		l.Debug(err) -		return nil, err +	// now check if the Object of the Activity (usually a Note or something) has an 'inReplyTo' +	if object := activity.GetActivityStreamsObject(); object != nil { +		if replyToable, ok := object.(ap.ReplyToable); ok { +			if inReplyToURI := ap.ExtractInReplyToURI(replyToable); inReplyToURI != nil { +				otherInvolvedIRIs = append(otherInvolvedIRIs, inReplyToURI) +			} +		} +	} + +	// check for Tos and CCs on Activity itself +	if addressable, ok := activity.(ap.Addressable); ok { +		if ccURIs, err := ap.ExtractCCs(addressable); err == nil { +			otherInvolvedIRIs = append(otherInvolvedIRIs, ccURIs...) +		} +		if toURIs, err := ap.ExtractTos(addressable); err == nil { +			otherInvolvedIRIs = append(otherInvolvedIRIs, toURIs...) +		}  	} -	// set the activity on the context for use later on -	return context.WithValue(ctx, ap.ContextActivity, activity), nil + +	// and on the Object itself +	if object := activity.GetActivityStreamsObject(); object != nil { +		if addressable, ok := object.(ap.Addressable); ok { +			if ccURIs, err := ap.ExtractCCs(addressable); err == nil { +				otherInvolvedIRIs = append(otherInvolvedIRIs, ccURIs...) +			} +			if toURIs, err := ap.ExtractTos(addressable); err == nil { +				otherInvolvedIRIs = append(otherInvolvedIRIs, toURIs...) +			} +		} +	} + +	// remove any duplicate entries in the slice we put together +	deduped := util.UniqueURIs(otherInvolvedIRIs) + +	// clean any instances of the public URI since we don't care about that in this context +	cleaned := []*url.URL{} +	for _, u := range deduped { +		if !pub.IsPublic(u.String()) { +			cleaned = append(cleaned, u) +		} +	} + +	withOtherInvolvedIRIs := context.WithValue(ctx, ap.ContextOtherInvolvedIRIs, cleaned) +	return withOtherInvolvedIRIs, nil  }  // AuthenticatePostInbox delegates the authentication of a POST to an @@ -185,40 +227,85 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er  	})  	l.Debugf("entering BLOCKED function with IRI list: %+v", actorIRIs) +	// check domain blocks first for the given actor IRIs +	blocked, err := f.db.AreURIsBlocked(ctx, actorIRIs) +	if err != nil { +		return false, fmt.Errorf("error checking domain blocks of actorIRIs: %s", err) +	} +	if blocked { +		return blocked, nil +	} + +	// check domain blocks for any other involved IRIs +	otherInvolvedIRIsI := ctx.Value(ap.ContextOtherInvolvedIRIs) +	otherInvolvedIRIs, ok := otherInvolvedIRIsI.([]*url.URL) +	if !ok { +		l.Errorf("other involved IRIs not set on request context") +		return false, errors.New("other involved IRIs not set on request context, so couldn't determine blocks") +	} +	blocked, err = f.db.AreURIsBlocked(ctx, otherInvolvedIRIs) +	if err != nil { +		return false, fmt.Errorf("error checking domain blocks of otherInvolvedIRIs: %s", err) +	} +	if blocked { +		return blocked, nil +	} + +	// now check for user-level block from receiving against requesting account  	receivingAccountI := ctx.Value(ap.ContextReceivingAccount)  	receivingAccount, ok := receivingAccountI.(*gtsmodel.Account)  	if !ok {  		l.Errorf("receiving account not set on request context")  		return false, errors.New("receiving account not set on request context, so couldn't determine blocks")  	} - -	blocked, err := f.db.AreURIsBlocked(ctx, actorIRIs) +	requestingAccountI := ctx.Value(ap.ContextRequestingAccount) +	requestingAccount, ok := requestingAccountI.(*gtsmodel.Account) +	if !ok { +		l.Errorf("requesting account not set on request context") +		return false, errors.New("requesting account not set on request context, so couldn't determine blocks") +	} +	// the receiver shouldn't block the sender +	blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, requestingAccount.ID, false)  	if err != nil { -		return false, fmt.Errorf("error checking domain blocks: %s", err) +		return false, fmt.Errorf("error checking user-level blocks: %s", err)  	}  	if blocked {  		return blocked, nil  	} -	for _, uri := range actorIRIs { -		requestingAccount, err := f.db.GetAccountByURI(ctx, uri.String()) +	// get account IDs for other involved accounts +	var involvedAccountIDs []string +	for _, iri := range otherInvolvedIRIs { +		var involvedAccountID string +		if involvedStatus, err := f.db.GetStatusByURI(ctx, iri.String()); err == nil { +			involvedAccountID = involvedStatus.AccountID +		} else if involvedAccount, err := f.db.GetAccountByURI(ctx, iri.String()); err == nil { +			involvedAccountID = involvedAccount.ID +		} + +		if involvedAccountID != "" { +			involvedAccountIDs = append(involvedAccountIDs, involvedAccountID) +		} +	} +	deduped := util.UniqueStrings(involvedAccountIDs) + +	for _, involvedAccountID := range deduped { +		// the involved account shouldn't block whoever is making this request +		blocked, err = f.db.IsBlocked(ctx, involvedAccountID, requestingAccount.ID, false)  		if err != nil { -			if err == db.ErrNoEntries { -				// we don't have an entry for this account so it's not blocked -				// TODO: allow a different default to be set for this behavior -				l.Tracef("no entry for account with URI %s so it can't be blocked", uri) -				continue -			} -			return false, fmt.Errorf("error getting account with uri %s: %s", uri.String(), err) +			return false, fmt.Errorf("error checking user-level otherInvolvedIRI blocks: %s", err) +		} +		if blocked { +			return blocked, nil  		} -		blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, requestingAccount.ID, false) +		// whoever is receiving this request shouldn't block the involved account +		blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, involvedAccountID, false)  		if err != nil { -			return false, fmt.Errorf("error checking account block: %s", err) +			return false, fmt.Errorf("error checking user-level otherInvolvedIRI blocks: %s", err)  		}  		if blocked { -			l.Tracef("local account %s blocks account with uri %s", receivingAccount.Username, uri) -			return true, nil +			return blocked, nil  		}  	} diff --git a/internal/federation/federatingprotocol_test.go b/internal/federation/federatingprotocol_test.go index b4769a70f..992a55e6b 100644 --- a/internal/federation/federatingprotocol_test.go +++ b/internal/federation/federatingprotocol_test.go @@ -22,11 +22,11 @@ import (  	"context"  	"net/http"  	"net/http/httptest" +	"net/url"  	"testing"  	"github.com/go-fed/httpsig"  	"github.com/stretchr/testify/suite" -	"github.com/superseriousbusiness/activity/pub"  	"github.com/superseriousbusiness/gotosocial/internal/ap"  	"github.com/superseriousbusiness/gotosocial/internal/concurrency"  	"github.com/superseriousbusiness/gotosocial/internal/federation" @@ -39,8 +39,7 @@ type FederatingProtocolTestSuite struct {  	FederatorStandardTestSuite  } -// make sure PostInboxRequestBodyHook properly sets the inbox username and activity on the context -func (suite *FederatingProtocolTestSuite) TestPostInboxRequestBodyHook() { +func (suite *FederatingProtocolTestSuite) TestPostInboxRequestBodyHook1() {  	// the activity we're gonna use  	activity := suite.testActivities["dm_for_zork"] @@ -63,13 +62,82 @@ func (suite *FederatingProtocolTestSuite) TestPostInboxRequestBodyHook() {  	suite.NoError(err)  	suite.NotNil(newContext) -	// activity should be set on context now -	activityI := newContext.Value(ap.ContextActivity) -	suite.NotNil(activityI) -	returnedActivity, ok := activityI.(pub.Activity) -	suite.True(ok) -	suite.NotNil(returnedActivity) -	suite.EqualValues(activity.Activity, returnedActivity) +	involvedIRIsI := newContext.Value(ap.ContextOtherInvolvedIRIs) +	involvedIRIs, ok := involvedIRIsI.([]*url.URL) +	if !ok { +		suite.FailNow("couldn't get involved IRIs from context") +	} + +	suite.Len(involvedIRIs, 1) +	suite.Contains(involvedIRIs, testrig.URLMustParse("http://localhost:8080/users/the_mighty_zork")) +} + +func (suite *FederatingProtocolTestSuite) TestPostInboxRequestBodyHook2() { +	// the activity we're gonna use +	activity := suite.testActivities["reply_to_turtle_for_zork"] + +	fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) + +	// setup transport controller with a no-op client so we don't make external calls +	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) { +		return nil, nil +	}), suite.db, fedWorker) +	// setup module being tested +	federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db, fedWorker), tc, suite.tc, testrig.NewTestMediaManager(suite.db, suite.storage)) + +	// setup request +	ctx := context.Background() +	request := httptest.NewRequest(http.MethodPost, "http://localhost:8080/users/the_mighty_zork/inbox", nil) // the endpoint we're hitting +	request.Header.Set("Signature", activity.SignatureHeader) + +	// trigger the function being tested, and return the new context it creates +	newContext, err := federator.PostInboxRequestBodyHook(ctx, request, activity.Activity) +	suite.NoError(err) +	suite.NotNil(newContext) + +	involvedIRIsI := newContext.Value(ap.ContextOtherInvolvedIRIs) +	involvedIRIs, ok := involvedIRIsI.([]*url.URL) +	if !ok { +		suite.FailNow("couldn't get involved IRIs from context") +	} + +	suite.Len(involvedIRIs, 2) +	suite.Contains(involvedIRIs, testrig.URLMustParse("http://localhost:8080/users/1happyturtle")) +	suite.Contains(involvedIRIs, testrig.URLMustParse("http://fossbros-anonymous.io/users/foss_satan/followers")) +} + +func (suite *FederatingProtocolTestSuite) TestPostInboxRequestBodyHook3() { +	// the activity we're gonna use +	activity := suite.testActivities["reply_to_turtle_for_turtle"] + +	fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) + +	// setup transport controller with a no-op client so we don't make external calls +	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) { +		return nil, nil +	}), suite.db, fedWorker) +	// setup module being tested +	federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db, fedWorker), tc, suite.tc, testrig.NewTestMediaManager(suite.db, suite.storage)) + +	// setup request +	ctx := context.Background() +	request := httptest.NewRequest(http.MethodPost, "http://localhost:8080/users/1happyturtle/inbox", nil) // the endpoint we're hitting +	request.Header.Set("Signature", activity.SignatureHeader) + +	// trigger the function being tested, and return the new context it creates +	newContext, err := federator.PostInboxRequestBodyHook(ctx, request, activity.Activity) +	suite.NoError(err) +	suite.NotNil(newContext) + +	involvedIRIsI := newContext.Value(ap.ContextOtherInvolvedIRIs) +	involvedIRIs, ok := involvedIRIsI.([]*url.URL) +	if !ok { +		suite.FailNow("couldn't get involved IRIs from context") +	} + +	suite.Len(involvedIRIs, 2) +	suite.Contains(involvedIRIs, testrig.URLMustParse("http://localhost:8080/users/1happyturtle")) +	suite.Contains(involvedIRIs, testrig.URLMustParse("http://fossbros-anonymous.io/users/foss_satan/followers"))  }  func (suite *FederatingProtocolTestSuite) TestAuthenticatePostInbox() { @@ -97,8 +165,7 @@ func (suite *FederatingProtocolTestSuite) TestAuthenticatePostInbox() {  	// by the time AuthenticatePostInbox is called, PostInboxRequestBodyHook should have already been called,  	// which should have set the account and username onto the request. We can replicate that behavior here:  	ctxWithAccount := context.WithValue(ctx, ap.ContextReceivingAccount, inboxAccount) -	ctxWithActivity := context.WithValue(ctxWithAccount, ap.ContextActivity, activity) -	ctxWithVerifier := context.WithValue(ctxWithActivity, ap.ContextRequestingPublicKeyVerifier, verifier) +	ctxWithVerifier := context.WithValue(ctxWithAccount, ap.ContextRequestingPublicKeyVerifier, verifier)  	ctxWithSignature := context.WithValue(ctxWithVerifier, ap.ContextRequestingPublicKeySignature, activity.SignatureHeader)  	// we can pass this recorder as a writer and read it back after @@ -117,6 +184,125 @@ func (suite *FederatingProtocolTestSuite) TestAuthenticatePostInbox() {  	suite.Equal(sendingAccount.Username, requestingAccount.Username)  } +func (suite *FederatingProtocolTestSuite) TestBlocked1() { +	fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) +	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker) +	federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db, fedWorker), tc, suite.tc, testrig.NewTestMediaManager(suite.db, suite.storage)) + +	sendingAccount := suite.testAccounts["remote_account_1"] +	inboxAccount := suite.testAccounts["local_account_1"] +	otherInvolvedIRIs := []*url.URL{} +	actorIRIs := []*url.URL{ +		testrig.URLMustParse(sendingAccount.URI), +	} + +	ctx := context.Background() +	ctxWithReceivingAccount := context.WithValue(ctx, ap.ContextReceivingAccount, inboxAccount) +	ctxWithRequestingAccount := context.WithValue(ctxWithReceivingAccount, ap.ContextRequestingAccount, sendingAccount) +	ctxWithOtherInvolvedIRIs := context.WithValue(ctxWithRequestingAccount, ap.ContextOtherInvolvedIRIs, otherInvolvedIRIs) + +	blocked, err := federator.Blocked(ctxWithOtherInvolvedIRIs, actorIRIs) +	suite.NoError(err) +	suite.False(blocked) +} + +func (suite *FederatingProtocolTestSuite) TestBlocked2() { +	fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) +	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker) +	federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db, fedWorker), tc, suite.tc, testrig.NewTestMediaManager(suite.db, suite.storage)) + +	sendingAccount := suite.testAccounts["remote_account_1"] +	inboxAccount := suite.testAccounts["local_account_1"] +	otherInvolvedIRIs := []*url.URL{} +	actorIRIs := []*url.URL{ +		testrig.URLMustParse(sendingAccount.URI), +	} + +	ctx := context.Background() +	ctxWithReceivingAccount := context.WithValue(ctx, ap.ContextReceivingAccount, inboxAccount) +	ctxWithRequestingAccount := context.WithValue(ctxWithReceivingAccount, ap.ContextRequestingAccount, sendingAccount) +	ctxWithOtherInvolvedIRIs := context.WithValue(ctxWithRequestingAccount, ap.ContextOtherInvolvedIRIs, otherInvolvedIRIs) + +	// insert a block from inboxAccount targeting sendingAccount +	if err := suite.db.Put(context.Background(), >smodel.Block{ +		ID:              "01G3KBEMJD4VQ2D615MPV7KTRD", +		URI:             "whatever", +		AccountID:       inboxAccount.ID, +		TargetAccountID: sendingAccount.ID, +	}); err != nil { +		suite.Fail(err.Error()) +	} + +	// request should be blocked now +	blocked, err := federator.Blocked(ctxWithOtherInvolvedIRIs, actorIRIs) +	suite.NoError(err) +	suite.True(blocked) +} + +func (suite *FederatingProtocolTestSuite) TestBlocked3() { +	fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) +	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker) +	federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db, fedWorker), tc, suite.tc, testrig.NewTestMediaManager(suite.db, suite.storage)) + +	sendingAccount := suite.testAccounts["remote_account_1"] +	inboxAccount := suite.testAccounts["local_account_1"] +	ccedAccount := suite.testAccounts["remote_account_2"] + +	otherInvolvedIRIs := []*url.URL{ +		testrig.URLMustParse(ccedAccount.URI), +	} +	actorIRIs := []*url.URL{ +		testrig.URLMustParse(sendingAccount.URI), +	} + +	ctx := context.Background() +	ctxWithReceivingAccount := context.WithValue(ctx, ap.ContextReceivingAccount, inboxAccount) +	ctxWithRequestingAccount := context.WithValue(ctxWithReceivingAccount, ap.ContextRequestingAccount, sendingAccount) +	ctxWithOtherInvolvedIRIs := context.WithValue(ctxWithRequestingAccount, ap.ContextOtherInvolvedIRIs, otherInvolvedIRIs) + +	// insert a block from inboxAccount targeting CCed account +	if err := suite.db.Put(context.Background(), >smodel.Block{ +		ID:              "01G3KBEMJD4VQ2D615MPV7KTRD", +		URI:             "whatever", +		AccountID:       inboxAccount.ID, +		TargetAccountID: ccedAccount.ID, +	}); err != nil { +		suite.Fail(err.Error()) +	} + +	blocked, err := federator.Blocked(ctxWithOtherInvolvedIRIs, actorIRIs) +	suite.NoError(err) +	suite.True(blocked) +} + +func (suite *FederatingProtocolTestSuite) TestBlocked4() { +	fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) +	tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker) +	federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db, fedWorker), tc, suite.tc, testrig.NewTestMediaManager(suite.db, suite.storage)) + +	sendingAccount := suite.testAccounts["remote_account_1"] +	inboxAccount := suite.testAccounts["local_account_1"] +	repliedStatus := suite.testStatuses["local_account_2_status_1"] + +	otherInvolvedIRIs := []*url.URL{ +		testrig.URLMustParse(repliedStatus.URI), // this status is involved because the hypothetical activity is a reply to this status +	} +	actorIRIs := []*url.URL{ +		testrig.URLMustParse(sendingAccount.URI), +	} + +	ctx := context.Background() +	ctxWithReceivingAccount := context.WithValue(ctx, ap.ContextReceivingAccount, inboxAccount) +	ctxWithRequestingAccount := context.WithValue(ctxWithReceivingAccount, ap.ContextRequestingAccount, sendingAccount) +	ctxWithOtherInvolvedIRIs := context.WithValue(ctxWithRequestingAccount, ap.ContextOtherInvolvedIRIs, otherInvolvedIRIs) + +	// local account 2 (replied status account) blocks sending account already so we don't need to add a block here + +	blocked, err := federator.Blocked(ctxWithOtherInvolvedIRIs, actorIRIs) +	suite.NoError(err) +	suite.True(blocked) +} +  func TestFederatingProtocolTestSuite(t *testing.T) {  	suite.Run(t, new(FederatingProtocolTestSuite))  } diff --git a/internal/federation/federator_test.go b/internal/federation/federator_test.go index 0988178d2..0984b405b 100644 --- a/internal/federation/federator_test.go +++ b/internal/federation/federator_test.go @@ -34,6 +34,7 @@ type FederatorStandardTestSuite struct {  	storage        *kv.KVStore  	tc             typeutils.TypeConverter  	testAccounts   map[string]*gtsmodel.Account +	testStatuses   map[string]*gtsmodel.Status  	testActivities map[string]testrig.ActivityWithSignature  } @@ -43,6 +44,7 @@ func (suite *FederatorStandardTestSuite) SetupSuite() {  	suite.storage = testrig.NewTestStorage()  	suite.tc = testrig.NewTestTypeConverter(suite.db)  	suite.testAccounts = testrig.NewTestAccounts() +	suite.testStatuses = testrig.NewTestStatuses()  }  func (suite *FederatorStandardTestSuite) SetupTest() { | 
