gtsocial-umbx

Unnamed repository; edit this file 'description' to name the repository.
Log | Files | Refs | README | LICENSE

commit 469da93678b3f738f65372d13dcd1ea7de390063
parent d6abe105b3aeb0dd35442f913df5082db9983aae
Author: tobi <31960611+tsmethurst@users.noreply.github.com>
Date:   Mon, 23 May 2022 11:46:50 +0200

[security] Check all involved IRIs during block checking (#593)

* tidy up context keys, add otherInvolvedIRIs

* add ReplyToable interface

* skip block check if we own the requesting domain

* add block check for other involved IRIs

* use cacheable status fetch

* remove unused ContextActivity

* remove unused ContextActivity

* add helper for unique URIs

* check through CCs and clean slice

* add GetAccountIDForStatusURI

* add GetAccountIDForAccountURI

* check blocks on involved account

* add statuses to tests

* add some blocked tests

* go fmt

* extract Tos as well as CCs

* test PostInboxRequestBodyHook

* add some more testActivities

* deduplicate involvedAccountIDs

* go fmt

* use cacheable db functions, remove new functions
Diffstat:
Minternal/ap/contextkey.go | 12++++--------
Minternal/ap/interfaces.go | 5+++++
Minternal/db/bundb/domain.go | 4+++-
Minternal/federation/federatingprotocol.go | 141++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++---------------
Minternal/federation/federatingprotocol_test.go | 210++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++++-----
Minternal/federation/federator_test.go | 2++
Minternal/typeutils/internaltoas.go | 6+++---
Minternal/util/unique.go | 15+++++++++++++++
Mtestrig/testmodels.go | 36++++++++++++++++++++++++++++++++++++
9 files changed, 380 insertions(+), 51 deletions(-)

diff --git a/internal/ap/contextkey.go b/internal/ap/contextkey.go @@ -22,20 +22,16 @@ package ap type ContextKey string const ( - // ContextActivity can be used to set and retrieve the actual go-fed pub.Activity within a context. - ContextActivity ContextKey = "activity" // ContextReceivingAccount can be used the set and retrieve the account being interacted with / receiving an activity in their inbox. - ContextReceivingAccount ContextKey = "account" + ContextReceivingAccount ContextKey = "receivingAccount" // ContextRequestingAccount can be used to set and retrieve the account of an incoming federation request. // This will often be the actor of the instance that's posting the request. ContextRequestingAccount ContextKey = "requestingAccount" - // ContextRequestingActorIRI can be used to set and retrieve the actor of an incoming federation request. - // This will usually be the owner of whatever activity is being posted. - ContextRequestingActorIRI ContextKey = "requestingActorIRI" + // ContextOtherInvolvedIRIs can be used to set and retrieve a slice of all IRIs that are 'involved' in an Activity without being + // the receivingAccount or the requestingAccount. In other words, people or notes who are CC'ed or Replied To by an Activity. + ContextOtherInvolvedIRIs ContextKey = "otherInvolvedIRIs" // ContextRequestingPublicKeyVerifier can be used to set and retrieve the public key verifier of an incoming federation request. ContextRequestingPublicKeyVerifier ContextKey = "requestingPublicKeyVerifier" // ContextRequestingPublicKeySignature can be used to set and retrieve the value of the signature header of an incoming federation request. ContextRequestingPublicKeySignature ContextKey = "requestingPublicKeySignature" - // ContextFromFederatorChan can be used to pass a pointer to the fromFederator channel into the federator for use in callbacks. - ContextFromFederatorChan ContextKey = "fromFederatorChan" ) diff --git a/internal/ap/interfaces.go b/internal/ap/interfaces.go @@ -140,6 +140,11 @@ type Addressable interface { WithCC } +// ReplyToable represents the minimum interface for an Activity that can be InReplyTo another activity. +type ReplyToable interface { + WithInReplyTo +} + // CollectionPageable represents the minimum interface for an activitystreams 'CollectionPage' object. type CollectionPageable interface { WithJSONLDId diff --git a/internal/db/bundb/domain.go b/internal/db/bundb/domain.go @@ -23,6 +23,8 @@ import ( "net/url" "strings" + "github.com/spf13/viper" + "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/util" @@ -33,7 +35,7 @@ type domainDB struct { } func (d *domainDB) IsDomainBlocked(ctx context.Context, domain string) (bool, db.Error) { - if domain == "" { + if domain == "" || domain == viper.GetString(config.Keys.Host) { return false, nil } diff --git 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 @@ -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(), &gtsmodel.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(), &gtsmodel.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 @@ -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() { diff --git a/internal/typeutils/internaltoas.go b/internal/typeutils/internaltoas.go @@ -393,9 +393,9 @@ func (c *converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (vocab.A if s.InReplyToID != "" { // fetch the replied status if we don't have it on hand already if s.InReplyTo == nil { - rs := &gtsmodel.Status{} - if err := c.db.GetByID(ctx, s.InReplyToID, rs); err != nil { - return nil, fmt.Errorf("StatusToAS: error retrieving replied-to status from db: %s", err) + rs, err := c.db.GetStatusByID(ctx, s.InReplyToID) + if err != nil { + return nil, fmt.Errorf("StatusToAS: error getting replied to status %s: %s", s.InReplyToID, err) } s.InReplyTo = rs } diff --git a/internal/util/unique.go b/internal/util/unique.go @@ -18,6 +18,8 @@ package util +import "net/url" + // UniqueStrings returns a deduplicated version of a given string slice. func UniqueStrings(s []string) []string { keys := make(map[string]bool, len(s)) @@ -30,3 +32,16 @@ func UniqueStrings(s []string) []string { } return list } + +// UniqueURIs returns a deduplicated version of a given *url.URL slice. +func UniqueURIs(s []*url.URL) []*url.URL { + keys := make(map[string]bool, len(s)) + list := []*url.URL{} + for _, entry := range s { + if _, value := keys[entry.String()]; !value { + keys[entry.String()] = true + list = append(list, entry) + } + } + return list +} diff --git a/testrig/testmodels.go b/testrig/testmodels.go @@ -1601,6 +1601,30 @@ func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]Activit dmForZork) createDmForZorkSig, createDmForZorkDigest, creatDmForZorkDate := GetSignatureForActivity(createDmForZork, accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_1"].InboxURI)) + replyToTurtle := NewAPNote( + URLMustParse("http://fossbros-anonymous.io/users/foss_satan/statuses/2f1195a6-5cb0-4475-adf5-92ab9a0147fe"), + URLMustParse("http://fossbros-anonymous.io/@foss_satan/2f1195a6-5cb0-4475-adf5-92ab9a0147fe"), + time.Now(), + "@1happyturtle@localhost:8080 u suck lol", + "", + URLMustParse("http://fossbros-anonymous.io/users/foss_satan"), + []*url.URL{URLMustParse("http://fossbros-anonymous.io/users/foss_satan/followers")}, + []*url.URL{URLMustParse("http://localhost:8080/users/1happyturtle")}, + false, + []vocab.ActivityStreamsMention{newAPMention( + URLMustParse("http://localhost:8080/users/1happyturtle"), + "@1happyturtle@localhost:8080", + )}, + nil, + ) + createReplyToTurtle := WrapAPNoteInCreate( + URLMustParse("http://fossbros-anonymous.io/users/foss_satan/statuses/2f1195a6-5cb0-4475-adf5-92ab9a0147fe"), + URLMustParse("http://fossbros-anonymous.io/users/foss_satan"), + time.Now(), + replyToTurtle) + createReplyToTurtleForZorkSig, createReplyToTurtleForZorkDigest, createReplyToTurtleForZorkDate := GetSignatureForActivity(createReplyToTurtle, accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_1"].InboxURI)) + createReplyToTurtleForTurtleSig, createReplyToTurtleForTurtleDigest, createReplyToTurtleForTurtleDate := GetSignatureForActivity(createReplyToTurtle, accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_2"].InboxURI)) + forwardedMessage := NewAPNote( URLMustParse("http://example.org/users/some_user/statuses/afaba698-5740-4e32-a702-af61aa543bc1"), URLMustParse("http://example.org/@some_user/afaba698-5740-4e32-a702-af61aa543bc1"), @@ -1628,6 +1652,18 @@ func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]Activit DigestHeader: createDmForZorkDigest, DateHeader: creatDmForZorkDate, }, + "reply_to_turtle_for_zork": { + Activity: createReplyToTurtle, + SignatureHeader: createReplyToTurtleForZorkSig, + DigestHeader: createReplyToTurtleForZorkDigest, + DateHeader: createReplyToTurtleForZorkDate, + }, + "reply_to_turtle_for_turtle": { + Activity: createReplyToTurtle, + SignatureHeader: createReplyToTurtleForTurtleSig, + DigestHeader: createReplyToTurtleForTurtleDigest, + DateHeader: createReplyToTurtleForTurtleDate, + }, "forwarded_message": { Activity: createForwardedMessage, SignatureHeader: createForwardedMessageSig,