From 045c279d47df6f73af3e1274788829ce6a783a2c Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Sun, 28 May 2023 21:05:15 +0200 Subject: [PATCH] [bugfix/chore] Inbox post updates (#1821) Co-authored-by: kim --- internal/api/activitypub/users/inboxpost.go | 36 +- .../api/activitypub/users/inboxpost_test.go | 711 ++++++++++-------- internal/federation/federatingactor.go | 281 ++++--- internal/federation/federatingactor_test.go | 48 ++ 4 files changed, 620 insertions(+), 456 deletions(-) diff --git a/internal/api/activitypub/users/inboxpost.go b/internal/api/activitypub/users/inboxpost.go index 9f74d9c31..4f535f534 100644 --- a/internal/api/activitypub/users/inboxpost.go +++ b/internal/api/activitypub/users/inboxpost.go @@ -19,32 +19,34 @@ package users import ( "errors" - "strings" + "net/http" "github.com/gin-gonic/gin" apiutil "github.com/superseriousbusiness/gotosocial/internal/api/util" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck + "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/log" ) // InboxPOSTHandler deals with incoming POST requests to an actor's inbox. // Eg., POST to https://example.org/users/whatever/inbox. func (m *Module) InboxPOSTHandler(c *gin.Context) { - // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) - if requestedUsername == "" { - err := errors.New("no username specified in request") - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + _, err := m.processor.Fedi().InboxPost(apiutil.TransferSignatureContext(c), c.Writer, c.Request) + if err != nil { + errWithCode := new(gtserror.WithCode) + + if !errors.As(err, errWithCode) { + // Something else went wrong, and someone forgot to return + // an errWithCode! It's chill though. Log the error but don't + // return it as-is to the caller, to avoid leaking internals. + log.Errorf(c.Request.Context(), "returning Bad Request to caller, err was: %q", err) + *errWithCode = gtserror.NewErrorBadRequest(err) + } + + // Pass along confirmed error with code to the main error handler + apiutil.ErrorHandler(c, *errWithCode, m.processor.InstanceGetV1) return } - if posted, err := m.processor.Fedi().InboxPost(apiutil.TransferSignatureContext(c), c.Writer, c.Request); err != nil { - if withCode, ok := err.(gtserror.WithCode); ok { - apiutil.ErrorHandler(c, withCode, m.processor.InstanceGetV1) - } else { - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) - } - } else if !posted { - err := errors.New("unable to process request") - apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) - } + // Inbox POST body was Accepted for processing. + c.JSON(http.StatusAccepted, gin.H{"status": http.StatusText(http.StatusAccepted)}) } diff --git a/internal/api/activitypub/users/inboxpost_test.go b/internal/api/activitypub/users/inboxpost_test.go index 26c4029a2..82e86fb9c 100644 --- a/internal/api/activitypub/users/inboxpost_test.go +++ b/internal/api/activitypub/users/inboxpost_test.go @@ -21,7 +21,9 @@ import ( "bytes" "context" "encoding/json" - "io/ioutil" + "errors" + "fmt" + "io" "net/http" "net/http/httptest" "testing" @@ -35,6 +37,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/ap" "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/id" "github.com/superseriousbusiness/gotosocial/testrig" @@ -44,11 +47,82 @@ type InboxPostTestSuite struct { UserStandardTestSuite } -func (suite *InboxPostTestSuite) TestPostBlock() { - blockingAccount := suite.testAccounts["remote_account_1"] - blockedAccount := suite.testAccounts["local_account_1"] - blockURI := testrig.URLMustParse("http://fossbros-anonymous.io/users/foss_satan/blocks/01FG9C441MCTW3R2W117V2PQK3") +func (suite *InboxPostTestSuite) inboxPost( + activity pub.Activity, + requestingAccount *gtsmodel.Account, + targetAccount *gtsmodel.Account, + expectedHTTPStatus int, + expectedBody string, + middlewares ...func(*gin.Context), +) { + var ( + recorder = httptest.NewRecorder() + ctx, _ = testrig.CreateGinTestContext(recorder, nil) + ) + // Prepare the requst body bytes. + bodyI, err := ap.Serialize(activity) + if err != nil { + suite.FailNow(err.Error()) + } + + b, err := json.MarshalIndent(bodyI, "", " ") + if err != nil { + suite.FailNow(err.Error()) + } + suite.T().Logf("prepared POST body:\n%s", string(b)) + + // Prepare signature headers for this Activity. + signature, digestHeader, dateHeader := testrig.GetSignatureForActivity( + activity, + requestingAccount.PublicKeyURI, + requestingAccount.PrivateKey, + testrig.URLMustParse(targetAccount.InboxURI), + ) + + // Put the request together. + ctx.AddParam(users.UsernameKey, targetAccount.Username) + ctx.Request = httptest.NewRequest(http.MethodPost, targetAccount.InboxURI, bytes.NewReader(b)) + ctx.Request.Header.Set("Signature", signature) + ctx.Request.Header.Set("Date", dateHeader) + ctx.Request.Header.Set("Digest", digestHeader) + ctx.Request.Header.Set("Content-Type", "application/activity+json") + + // Pass the context through provided middlewares. + for _, middleware := range middlewares { + middleware(ctx) + } + + // Trigger the function being tested. + suite.userModule.InboxPOSTHandler(ctx) + + // Read the result. + result := recorder.Result() + defer result.Body.Close() + + b, err = io.ReadAll(result.Body) + if err != nil { + suite.FailNow(err.Error()) + } + + errs := gtserror.MultiError{} + + // Check expected code + body. + if resultCode := recorder.Code; expectedHTTPStatus != resultCode { + errs = append(errs, fmt.Sprintf("expected %d got %d", expectedHTTPStatus, resultCode)) + } + + // If we got an expected body, return early. + if expectedBody != "" && string(b) != expectedBody { + errs = append(errs, fmt.Sprintf("expected %s got %s", expectedBody, string(b))) + } + + if err := errs.Combine(); err != nil { + suite.FailNow("", "%v (body %s)", err, string(b)) + } +} + +func (suite *InboxPostTestSuite) newBlock(blockID string, blockingAccount *gtsmodel.Account, blockedAccount *gtsmodel.Account) vocab.ActivityStreamsBlock { block := streams.NewActivityStreamsBlock() // set the actor property to the block-ing account's URI @@ -59,7 +133,7 @@ func (suite *InboxPostTestSuite) TestPostBlock() { // set the ID property to the blocks's URI idProp := streams.NewJSONLDIdProperty() - idProp.Set(blockURI) + idProp.Set(testrig.URLMustParse(blockID)) block.SetJSONLDId(idProp) // set the object property to the target account's URI @@ -74,188 +148,48 @@ func (suite *InboxPostTestSuite) TestPostBlock() { toProp.AppendIRI(toIRI) block.SetActivityStreamsTo(toProp) - targetURI := testrig.URLMustParse(blockedAccount.InboxURI) - - signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(block, blockingAccount.PublicKeyURI, blockingAccount.PrivateKey, targetURI) - bodyI, err := ap.Serialize(block) - suite.NoError(err) - - bodyJson, err := json.Marshal(bodyI) - suite.NoError(err) - body := bytes.NewReader(bodyJson) - - tc := testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")) - federator := testrig.NewTestFederator(&suite.state, tc, suite.mediaManager) - emailSender := testrig.NewEmailSender("../../../../web/template/", nil) - processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager) - userModule := users.New(processor) - suite.NoError(processor.Start()) - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting - ctx.Request.Header.Set("Signature", signature) - ctx.Request.Header.Set("Date", dateHeader) - ctx.Request.Header.Set("Digest", digestHeader) - ctx.Request.Header.Set("Content-Type", "application/activity+json") - - // we need to pass the context through signature check first to set appropriate values on it - suite.signatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: users.UsernameKey, - Value: blockedAccount.Username, - }, - } - - // trigger the function being tested - userModule.InboxPOSTHandler(ctx) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Empty(b) - - // there should be a block in the database now between the accounts - dbBlock, err := suite.db.GetBlock(context.Background(), blockingAccount.ID, blockedAccount.ID) - suite.NoError(err) - suite.NotNil(dbBlock) - suite.WithinDuration(time.Now(), dbBlock.CreatedAt, 30*time.Second) - suite.WithinDuration(time.Now(), dbBlock.UpdatedAt, 30*time.Second) - suite.Equal("http://fossbros-anonymous.io/users/foss_satan/blocks/01FG9C441MCTW3R2W117V2PQK3", dbBlock.URI) + return block } -// TestPostUnblock verifies that a remote account with a block targeting one of our instance users should be able to undo that block. -func (suite *InboxPostTestSuite) TestPostUnblock() { - blockingAccount := suite.testAccounts["remote_account_1"] - blockedAccount := suite.testAccounts["local_account_1"] - - // first put a block in the database so we have something to undo - blockURI := "http://fossbros-anonymous.io/users/foss_satan/blocks/01FG9C441MCTW3R2W117V2PQK3" - dbBlockID, err := id.NewRandomULID() - suite.NoError(err) - - dbBlock := >smodel.Block{ - ID: dbBlockID, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - URI: blockURI, - AccountID: blockingAccount.ID, - TargetAccountID: blockedAccount.ID, - } - - err = suite.db.PutBlock(context.Background(), dbBlock) - suite.NoError(err) - - asBlock, err := suite.tc.BlockToAS(context.Background(), dbBlock) - suite.NoError(err) - - targetAccountURI := testrig.URLMustParse(blockedAccount.URI) - - // create an Undo and set the appropriate actor on it +func (suite *InboxPostTestSuite) newUndo( + originalActivity pub.Activity, + objectF func() vocab.ActivityStreamsObjectProperty, + to string, + undoIRI string, +) vocab.ActivityStreamsUndo { undo := streams.NewActivityStreamsUndo() - undo.SetActivityStreamsActor(asBlock.GetActivityStreamsActor()) - // Set the block as the 'object' property. - undoObject := streams.NewActivityStreamsObjectProperty() - undoObject.AppendActivityStreamsBlock(asBlock) - undo.SetActivityStreamsObject(undoObject) + // Set the appropriate actor. + undo.SetActivityStreamsActor(originalActivity.GetActivityStreamsActor()) - // Set the To of the undo as the target of the block + // Set the original activity uri as the 'object' property. + undo.SetActivityStreamsObject(objectF()) + + // Set the To of the undo as the target of the activity. undoTo := streams.NewActivityStreamsToProperty() - undoTo.AppendIRI(targetAccountURI) + undoTo.AppendIRI(testrig.URLMustParse(to)) undo.SetActivityStreamsTo(undoTo) + // Set the ID property to the undo's URI. undoID := streams.NewJSONLDIdProperty() - undoID.SetIRI(testrig.URLMustParse("http://fossbros-anonymous.io/72cc96a3-f742-4daf-b9f5-3407667260c5")) + undoID.SetIRI(testrig.URLMustParse(undoIRI)) undo.SetJSONLDId(undoID) - targetURI := testrig.URLMustParse(blockedAccount.InboxURI) - - signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(undo, blockingAccount.PublicKeyURI, blockingAccount.PrivateKey, targetURI) - bodyI, err := ap.Serialize(undo) - suite.NoError(err) - - bodyJson, err := json.Marshal(bodyI) - suite.NoError(err) - body := bytes.NewReader(bodyJson) - - tc := testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")) - federator := testrig.NewTestFederator(&suite.state, tc, suite.mediaManager) - emailSender := testrig.NewEmailSender("../../../../web/template/", nil) - processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager) - userModule := users.New(processor) - suite.NoError(processor.Start()) - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting - ctx.Request.Header.Set("Signature", signature) - ctx.Request.Header.Set("Date", dateHeader) - ctx.Request.Header.Set("Digest", digestHeader) - ctx.Request.Header.Set("Content-Type", "application/activity+json") - - // we need to pass the context through signature check first to set appropriate values on it - suite.signatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: users.UsernameKey, - Value: blockedAccount.Username, - }, - } - - // trigger the function being tested - userModule.InboxPOSTHandler(ctx) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Empty(b) - suite.Equal(http.StatusOK, result.StatusCode) - - // the block should be undone - block, err := suite.db.GetBlock(context.Background(), blockingAccount.ID, blockedAccount.ID) - suite.ErrorIs(err, db.ErrNoEntries) - suite.Nil(block) + return undo } -func (suite *InboxPostTestSuite) TestPostUpdate() { - receivingAccount := suite.testAccounts["local_account_1"] - updatedAccount := *suite.testAccounts["remote_account_1"] - updatedAccount.DisplayName = "updated display name!" - - // add an emoji to the account; because we're serializing this remote - // account from our own instance, we need to cheat a bit to get the emoji - // to work properly, just for this test - testEmoji := >smodel.Emoji{} - *testEmoji = *testrig.NewTestEmojis()["yell"] - testEmoji.ImageURL = testEmoji.ImageRemoteURL // <- here's the cheat - updatedAccount.Emojis = []*gtsmodel.Emoji{testEmoji} - - asAccount, err := suite.tc.AccountToAS(context.Background(), &updatedAccount) - suite.NoError(err) - +func (suite *InboxPostTestSuite) newUpdatePerson(person vocab.ActivityStreamsPerson, cc string, updateIRI string) vocab.ActivityStreamsUpdate { // create an update update := streams.NewActivityStreamsUpdate() // set the appropriate actor on it updateActor := streams.NewActivityStreamsActorProperty() - updateActor.AppendIRI(testrig.URLMustParse(updatedAccount.URI)) + updateActor.AppendIRI(person.GetJSONLDId().Get()) update.SetActivityStreamsActor(updateActor) - // Set the account as the 'object' property. + // Set the person as the 'object' property. updateObject := streams.NewActivityStreamsObjectProperty() - updateObject.AppendActivityStreamsPerson(asAccount) + updateObject.AppendActivityStreamsPerson(person) update.SetActivityStreamsObject(updateObject) // Set the To of the update as public @@ -265,77 +199,179 @@ func (suite *InboxPostTestSuite) TestPostUpdate() { // set the cc of the update to the receivingAccount updateCC := streams.NewActivityStreamsCcProperty() - updateCC.AppendIRI(testrig.URLMustParse(receivingAccount.URI)) + updateCC.AppendIRI(testrig.URLMustParse(cc)) update.SetActivityStreamsCc(updateCC) // set some random-ass ID for the activity - undoID := streams.NewJSONLDIdProperty() - undoID.SetIRI(testrig.URLMustParse("http://fossbros-anonymous.io/d360613a-dc8d-4563-8f0b-b6161caf0f2b")) - update.SetJSONLDId(undoID) + updateID := streams.NewJSONLDIdProperty() + updateID.SetIRI(testrig.URLMustParse(updateIRI)) + update.SetJSONLDId(updateID) - targetURI := testrig.URLMustParse(receivingAccount.InboxURI) + return update +} - signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(update, updatedAccount.PublicKeyURI, updatedAccount.PrivateKey, targetURI) - bodyI, err := ap.Serialize(update) - suite.NoError(err) +func (suite *InboxPostTestSuite) newDelete(actorIRI string, objectIRI string, deleteIRI string) vocab.ActivityStreamsDelete { + // create a delete + delete := streams.NewActivityStreamsDelete() - bodyJson, err := json.Marshal(bodyI) - suite.NoError(err) - body := bytes.NewReader(bodyJson) + // set the appropriate actor on it + deleteActor := streams.NewActivityStreamsActorProperty() + deleteActor.AppendIRI(testrig.URLMustParse(actorIRI)) + delete.SetActivityStreamsActor(deleteActor) - // use a different version of the mock http client which serves the updated - // version of the remote account, as though it had been updated there too; - // this is needed so it can be dereferenced + updated properly - mockHTTPClient := testrig.NewMockHTTPClient(nil, "../../../../testrig/media") - mockHTTPClient.TestRemotePeople = map[string]vocab.ActivityStreamsPerson{ - updatedAccount.URI: asAccount, + // Set 'object' property. + deleteObject := streams.NewActivityStreamsObjectProperty() + deleteObject.AppendIRI(testrig.URLMustParse(objectIRI)) + delete.SetActivityStreamsObject(deleteObject) + + // Set the To of the delete as public + deleteTo := streams.NewActivityStreamsToProperty() + deleteTo.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) + delete.SetActivityStreamsTo(deleteTo) + + // set some random-ass ID for the activity + deleteID := streams.NewJSONLDIdProperty() + deleteID.SetIRI(testrig.URLMustParse(deleteIRI)) + delete.SetJSONLDId(deleteID) + + return delete +} + +// TestPostBlock verifies that a remote account can block one of +// our instance users. +func (suite *InboxPostTestSuite) TestPostBlock() { + var ( + requestingAccount = suite.testAccounts["remote_account_1"] + targetAccount = suite.testAccounts["local_account_1"] + activityID = requestingAccount.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3" + ) + + block := suite.newBlock(activityID, requestingAccount, targetAccount) + + // Block. + suite.inboxPost( + block, + requestingAccount, + targetAccount, + http.StatusAccepted, + `{"status":"Accepted"}`, + suite.signatureCheck, + ) + + // Ensure block created in the database. + var ( + dbBlock *gtsmodel.Block + err error + ) + + if !testrig.WaitFor(func() bool { + dbBlock, err = suite.db.GetBlock(context.Background(), requestingAccount.ID, targetAccount.ID) + return err == nil && dbBlock != nil + }) { + suite.FailNow("timed out waiting for block to be created") + } +} + +// TestPostUnblock verifies that a remote account who blocks +// one of our instance users should be able to undo that block. +func (suite *InboxPostTestSuite) TestPostUnblock() { + var ( + ctx = context.Background() + requestingAccount = suite.testAccounts["remote_account_1"] + targetAccount = suite.testAccounts["local_account_1"] + blockID = "http://fossbros-anonymous.io/blocks/01H1462TPRTVG2RTQCTSQ7N6Q0" + undoID = "http://fossbros-anonymous.io/some-activity/01H1463RDQNG5H98F29BXYHW6B" + ) + + // Put a block in the database so we have something to undo. + block := >smodel.Block{ + ID: id.NewULID(), + URI: blockID, + AccountID: requestingAccount.ID, + TargetAccountID: targetAccount.ID, + } + if err := suite.db.PutBlock(ctx, block); err != nil { + suite.FailNow(err.Error()) } - tc := testrig.NewTestTransportController(&suite.state, mockHTTPClient) - federator := testrig.NewTestFederator(&suite.state, tc, suite.mediaManager) - emailSender := testrig.NewEmailSender("../../../../web/template/", nil) - processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager) - userModule := users.New(processor) - suite.NoError(processor.Start()) - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting - ctx.Request.Header.Set("Signature", signature) - ctx.Request.Header.Set("Date", dateHeader) - ctx.Request.Header.Set("Digest", digestHeader) - ctx.Request.Header.Set("Content-Type", "application/activity+json") - - // we need to pass the context through signature check first to set appropriate values on it - suite.signatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: users.UsernameKey, - Value: receivingAccount.Username, - }, + // Create the undo from the AS model block. + asBlock, err := suite.tc.BlockToAS(ctx, block) + if err != nil { + suite.FailNow(err.Error()) } - // trigger the function being tested - userModule.InboxPOSTHandler(ctx) + undo := suite.newUndo(asBlock, func() vocab.ActivityStreamsObjectProperty { + // Append the whole block as Object. + op := streams.NewActivityStreamsObjectProperty() + op.AppendActivityStreamsBlock(asBlock) + return op + }, targetAccount.URI, undoID) - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Empty(b) - suite.Equal(http.StatusOK, result.StatusCode) + // Undo. + suite.inboxPost( + undo, + requestingAccount, + targetAccount, + http.StatusAccepted, + `{"status":"Accepted"}`, + suite.signatureCheck, + ) + + // Ensure block removed from the database. + if !testrig.WaitFor(func() bool { + _, err := suite.db.GetBlockByID(ctx, block.ID) + return errors.Is(err, db.ErrNoEntries) + }) { + suite.FailNow("timed out waiting for block to be removed") + } +} + +func (suite *InboxPostTestSuite) TestPostUpdate() { + var ( + requestingAccount = new(gtsmodel.Account) + targetAccount = suite.testAccounts["local_account_1"] + activityID = "http://fossbros-anonymous.io/72cc96a3-f742-4daf-b9f5-3407667260c5" + updatedDisplayName = "updated display name!" + ) + + // Copy the requesting account, since we'll be changing it. + *requestingAccount = *suite.testAccounts["remote_account_1"] + + // Update the account's display name. + requestingAccount.DisplayName = updatedDisplayName + + // Add an emoji to the account; because we're serializing this + // remote account from our own instance, we need to cheat a bit + // to get the emoji to work properly, just for this test. + testEmoji := >smodel.Emoji{} + *testEmoji = *testrig.NewTestEmojis()["yell"] + testEmoji.ImageURL = testEmoji.ImageRemoteURL // <- here's the cheat + requestingAccount.Emojis = []*gtsmodel.Emoji{testEmoji} + + // Create an update from the account. + asAccount, err := suite.tc.AccountToAS(context.Background(), requestingAccount) + if err != nil { + suite.FailNow(err.Error()) + } + update := suite.newUpdatePerson(asAccount, targetAccount.URI, activityID) + + // Update. + suite.inboxPost( + update, + requestingAccount, + targetAccount, + http.StatusAccepted, + `{"status":"Accepted"}`, + suite.signatureCheck, + ) // account should be changed in the database now var dbUpdatedAccount *gtsmodel.Account if !testrig.WaitFor(func() bool { // displayName should be updated - dbUpdatedAccount, _ = suite.db.GetAccountByID(context.Background(), updatedAccount.ID) - return dbUpdatedAccount.DisplayName == "updated display name!" + dbUpdatedAccount, _ = suite.db.GetAccountByID(context.Background(), requestingAccount.ID) + return dbUpdatedAccount.DisplayName == updatedDisplayName }) { suite.FailNow("timed out waiting for account update") } @@ -347,134 +383,82 @@ func (suite *InboxPostTestSuite) TestPostUpdate() { suite.WithinDuration(time.Now(), dbUpdatedAccount.FetchedAt, 10*time.Second) // everything else should be the same as it was before - suite.EqualValues(updatedAccount.Username, dbUpdatedAccount.Username) - suite.EqualValues(updatedAccount.Domain, dbUpdatedAccount.Domain) - suite.EqualValues(updatedAccount.AvatarMediaAttachmentID, dbUpdatedAccount.AvatarMediaAttachmentID) - suite.EqualValues(updatedAccount.AvatarMediaAttachment, dbUpdatedAccount.AvatarMediaAttachment) - suite.EqualValues(updatedAccount.AvatarRemoteURL, dbUpdatedAccount.AvatarRemoteURL) - suite.EqualValues(updatedAccount.HeaderMediaAttachmentID, dbUpdatedAccount.HeaderMediaAttachmentID) - suite.EqualValues(updatedAccount.HeaderMediaAttachment, dbUpdatedAccount.HeaderMediaAttachment) - suite.EqualValues(updatedAccount.HeaderRemoteURL, dbUpdatedAccount.HeaderRemoteURL) - suite.EqualValues(updatedAccount.Note, dbUpdatedAccount.Note) - suite.EqualValues(updatedAccount.Memorial, dbUpdatedAccount.Memorial) - suite.EqualValues(updatedAccount.AlsoKnownAs, dbUpdatedAccount.AlsoKnownAs) - suite.EqualValues(updatedAccount.MovedToAccountID, dbUpdatedAccount.MovedToAccountID) - suite.EqualValues(updatedAccount.Bot, dbUpdatedAccount.Bot) - suite.EqualValues(updatedAccount.Reason, dbUpdatedAccount.Reason) - suite.EqualValues(updatedAccount.Locked, dbUpdatedAccount.Locked) - suite.EqualValues(updatedAccount.Discoverable, dbUpdatedAccount.Discoverable) - suite.EqualValues(updatedAccount.Privacy, dbUpdatedAccount.Privacy) - suite.EqualValues(updatedAccount.Sensitive, dbUpdatedAccount.Sensitive) - suite.EqualValues(updatedAccount.Language, dbUpdatedAccount.Language) - suite.EqualValues(updatedAccount.URI, dbUpdatedAccount.URI) - suite.EqualValues(updatedAccount.URL, dbUpdatedAccount.URL) - suite.EqualValues(updatedAccount.InboxURI, dbUpdatedAccount.InboxURI) - suite.EqualValues(updatedAccount.OutboxURI, dbUpdatedAccount.OutboxURI) - suite.EqualValues(updatedAccount.FollowingURI, dbUpdatedAccount.FollowingURI) - suite.EqualValues(updatedAccount.FollowersURI, dbUpdatedAccount.FollowersURI) - suite.EqualValues(updatedAccount.FeaturedCollectionURI, dbUpdatedAccount.FeaturedCollectionURI) - suite.EqualValues(updatedAccount.ActorType, dbUpdatedAccount.ActorType) - suite.EqualValues(updatedAccount.PublicKey, dbUpdatedAccount.PublicKey) - suite.EqualValues(updatedAccount.PublicKeyURI, dbUpdatedAccount.PublicKeyURI) - suite.EqualValues(updatedAccount.SensitizedAt, dbUpdatedAccount.SensitizedAt) - suite.EqualValues(updatedAccount.SilencedAt, dbUpdatedAccount.SilencedAt) - suite.EqualValues(updatedAccount.SuspendedAt, dbUpdatedAccount.SuspendedAt) - suite.EqualValues(updatedAccount.HideCollections, dbUpdatedAccount.HideCollections) - suite.EqualValues(updatedAccount.SuspensionOrigin, dbUpdatedAccount.SuspensionOrigin) + suite.EqualValues(requestingAccount.Username, dbUpdatedAccount.Username) + suite.EqualValues(requestingAccount.Domain, dbUpdatedAccount.Domain) + suite.EqualValues(requestingAccount.AvatarMediaAttachmentID, dbUpdatedAccount.AvatarMediaAttachmentID) + suite.EqualValues(requestingAccount.AvatarMediaAttachment, dbUpdatedAccount.AvatarMediaAttachment) + suite.EqualValues(requestingAccount.AvatarRemoteURL, dbUpdatedAccount.AvatarRemoteURL) + suite.EqualValues(requestingAccount.HeaderMediaAttachmentID, dbUpdatedAccount.HeaderMediaAttachmentID) + suite.EqualValues(requestingAccount.HeaderMediaAttachment, dbUpdatedAccount.HeaderMediaAttachment) + suite.EqualValues(requestingAccount.HeaderRemoteURL, dbUpdatedAccount.HeaderRemoteURL) + suite.EqualValues(requestingAccount.Note, dbUpdatedAccount.Note) + suite.EqualValues(requestingAccount.Memorial, dbUpdatedAccount.Memorial) + suite.EqualValues(requestingAccount.AlsoKnownAs, dbUpdatedAccount.AlsoKnownAs) + suite.EqualValues(requestingAccount.MovedToAccountID, dbUpdatedAccount.MovedToAccountID) + suite.EqualValues(requestingAccount.Bot, dbUpdatedAccount.Bot) + suite.EqualValues(requestingAccount.Reason, dbUpdatedAccount.Reason) + suite.EqualValues(requestingAccount.Locked, dbUpdatedAccount.Locked) + suite.EqualValues(requestingAccount.Discoverable, dbUpdatedAccount.Discoverable) + suite.EqualValues(requestingAccount.Privacy, dbUpdatedAccount.Privacy) + suite.EqualValues(requestingAccount.Sensitive, dbUpdatedAccount.Sensitive) + suite.EqualValues(requestingAccount.Language, dbUpdatedAccount.Language) + suite.EqualValues(requestingAccount.URI, dbUpdatedAccount.URI) + suite.EqualValues(requestingAccount.URL, dbUpdatedAccount.URL) + suite.EqualValues(requestingAccount.InboxURI, dbUpdatedAccount.InboxURI) + suite.EqualValues(requestingAccount.OutboxURI, dbUpdatedAccount.OutboxURI) + suite.EqualValues(requestingAccount.FollowingURI, dbUpdatedAccount.FollowingURI) + suite.EqualValues(requestingAccount.FollowersURI, dbUpdatedAccount.FollowersURI) + suite.EqualValues(requestingAccount.FeaturedCollectionURI, dbUpdatedAccount.FeaturedCollectionURI) + suite.EqualValues(requestingAccount.ActorType, dbUpdatedAccount.ActorType) + suite.EqualValues(requestingAccount.PublicKey, dbUpdatedAccount.PublicKey) + suite.EqualValues(requestingAccount.PublicKeyURI, dbUpdatedAccount.PublicKeyURI) + suite.EqualValues(requestingAccount.SensitizedAt, dbUpdatedAccount.SensitizedAt) + suite.EqualValues(requestingAccount.SilencedAt, dbUpdatedAccount.SilencedAt) + suite.EqualValues(requestingAccount.SuspendedAt, dbUpdatedAccount.SuspendedAt) + suite.EqualValues(requestingAccount.HideCollections, dbUpdatedAccount.HideCollections) + suite.EqualValues(requestingAccount.SuspensionOrigin, dbUpdatedAccount.SuspensionOrigin) } func (suite *InboxPostTestSuite) TestPostDelete() { - deletedAccount := *suite.testAccounts["remote_account_1"] - receivingAccount := suite.testAccounts["local_account_1"] + var ( + ctx = context.Background() + requestingAccount = suite.testAccounts["remote_account_1"] + targetAccount = suite.testAccounts["local_account_1"] + activityID = requestingAccount.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3" + ) - // create a delete - delete := streams.NewActivityStreamsDelete() + delete := suite.newDelete(requestingAccount.URI, requestingAccount.URI, activityID) - // set the appropriate actor on it - deleteActor := streams.NewActivityStreamsActorProperty() - deleteActor.AppendIRI(testrig.URLMustParse(deletedAccount.URI)) - delete.SetActivityStreamsActor(deleteActor) - - // Set the account iri as the 'object' property. - deleteObject := streams.NewActivityStreamsObjectProperty() - deleteObject.AppendIRI(testrig.URLMustParse(deletedAccount.URI)) - delete.SetActivityStreamsObject(deleteObject) - - // Set the To of the delete as public - deleteTo := streams.NewActivityStreamsToProperty() - deleteTo.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) - delete.SetActivityStreamsTo(deleteTo) - - // set some random-ass ID for the activity - deleteID := streams.NewJSONLDIdProperty() - deleteID.SetIRI(testrig.URLMustParse("http://fossbros-anonymous.io/d360613a-dc8d-4563-8f0b-b6161caf0f2b")) - delete.SetJSONLDId(deleteID) - - targetURI := testrig.URLMustParse(receivingAccount.InboxURI) - - signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(delete, deletedAccount.PublicKeyURI, deletedAccount.PrivateKey, targetURI) - bodyI, err := ap.Serialize(delete) - suite.NoError(err) - - bodyJson, err := json.Marshal(bodyI) - suite.NoError(err) - body := bytes.NewReader(bodyJson) - - tc := testrig.NewTestTransportController(&suite.state, testrig.NewMockHTTPClient(nil, "../../../../testrig/media")) - federator := testrig.NewTestFederator(&suite.state, tc, suite.mediaManager) - emailSender := testrig.NewEmailSender("../../../../web/template/", nil) - processor := testrig.NewTestProcessor(&suite.state, federator, emailSender, suite.mediaManager) - userModule := users.New(processor) - suite.NoError(processor.Start()) - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting - ctx.Request.Header.Set("Signature", signature) - ctx.Request.Header.Set("Date", dateHeader) - ctx.Request.Header.Set("Digest", digestHeader) - ctx.Request.Header.Set("Content-Type", "application/activity+json") - - // we need to pass the context through signature check first to set appropriate values on it - suite.signatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: users.UsernameKey, - Value: receivingAccount.Username, - }, - } - - // trigger the function being tested - userModule.InboxPOSTHandler(ctx) - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Empty(b) - suite.Equal(http.StatusOK, result.StatusCode) + // Delete. + suite.inboxPost( + delete, + requestingAccount, + targetAccount, + http.StatusAccepted, + `{"status":"Accepted"}`, + suite.signatureCheck, + ) if !testrig.WaitFor(func() bool { // local account 2 blocked foss_satan, that block should be gone now testBlock := suite.testBlocks["local_account_2_block_remote_account_1"] - dbBlock := >smodel.Block{} - err = suite.db.GetByID(ctx, testBlock.ID, dbBlock) + _, err := suite.db.GetBlockByID(ctx, testBlock.ID) return suite.ErrorIs(err, db.ErrNoEntries) }) { suite.FailNow("timed out waiting for block to be removed") } - // no statuses from foss satan should be left in the database - dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false) - suite.ErrorIs(err, db.ErrNoEntries) - suite.Empty(dbStatuses) + if !testrig.WaitFor(func() bool { + // no statuses from foss satan should be left in the database + dbStatuses, err := suite.db.GetAccountStatuses(ctx, requestingAccount.ID, 0, false, false, "", "", false, false) + return len(dbStatuses) == 0 && errors.Is(err, db.ErrNoEntries) + }) { + suite.FailNow("timed out waiting for statuses to be removed") + } - dbAccount, err := suite.db.GetAccountByID(ctx, deletedAccount.ID) + // Account should be stubbified. + dbAccount, err := suite.db.GetAccountByID(ctx, requestingAccount.ID) suite.NoError(err) - suite.Empty(dbAccount.Note) suite.Empty(dbAccount.DisplayName) suite.Empty(dbAccount.AvatarMediaAttachmentID) @@ -489,6 +473,69 @@ func (suite *InboxPostTestSuite) TestPostDelete() { suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin) } +func (suite *InboxPostTestSuite) TestPostEmptyCreate() { + var ( + requestingAccount = suite.testAccounts["remote_account_1"] + targetAccount = suite.testAccounts["local_account_1"] + ) + + // Post a create with no object. + create := streams.NewActivityStreamsCreate() + + suite.inboxPost( + create, + requestingAccount, + targetAccount, + http.StatusBadRequest, + `{"error":"Bad Request: incoming Activity Create did not have required id property set"}`, + suite.signatureCheck, + ) +} + +func (suite *InboxPostTestSuite) TestPostFromBlockedAccount() { + var ( + requestingAccount = suite.testAccounts["remote_account_1"] + targetAccount = suite.testAccounts["local_account_2"] + activityID = requestingAccount.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3" + ) + + person, err := suite.tc.AccountToAS(context.Background(), requestingAccount) + if err != nil { + suite.FailNow(err.Error()) + } + + // Post an update from foss satan to turtle, who blocks him. + update := suite.newUpdatePerson(person, targetAccount.URI, activityID) + + suite.inboxPost( + update, + requestingAccount, + targetAccount, + http.StatusForbidden, + `{"error":"Forbidden"}`, + suite.signatureCheck, + ) +} + +func (suite *InboxPostTestSuite) TestPostUnauthorized() { + var ( + requestingAccount = suite.testAccounts["remote_account_1"] + targetAccount = suite.testAccounts["local_account_1"] + ) + + // Post an empty create. + create := streams.NewActivityStreamsCreate() + + suite.inboxPost( + create, + requestingAccount, + targetAccount, + http.StatusUnauthorized, + `{"error":"Unauthorized"}`, + // Omit signature check middleware. + ) +} + func TestInboxPostTestSuite(t *testing.T) { suite.Run(t, &InboxPostTestSuite{}) } diff --git a/internal/federation/federatingactor.go b/internal/federation/federatingactor.go index 33ae38220..25282235a 100644 --- a/internal/federation/federatingactor.go +++ b/internal/federation/federatingactor.go @@ -20,6 +20,7 @@ package federation import ( "context" "encoding/json" + "errors" "fmt" "io" "net/http" @@ -31,21 +32,56 @@ import ( "github.com/superseriousbusiness/activity/streams" "github.com/superseriousbusiness/activity/streams/vocab" "github.com/superseriousbusiness/gotosocial/internal/ap" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtserror" "github.com/superseriousbusiness/gotosocial/internal/log" ) -// Potential incoming Content-Type header values; be -// lenient with whitespace and quotation mark placement. -var activityStreamsMediaTypes = []string{ - "application/activity+json", - "application/ld+json;profile=https://www.w3.org/ns/activitystreams", - "application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"", - "application/ld+json ;profile=https://www.w3.org/ns/activitystreams", - "application/ld+json ;profile=\"https://www.w3.org/ns/activitystreams\"", - "application/ld+json ; profile=https://www.w3.org/ns/activitystreams", - "application/ld+json ; profile=\"https://www.w3.org/ns/activitystreams\"", - "application/ld+json; profile=https://www.w3.org/ns/activitystreams", - "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", +// IsASMediaType will return whether the given content-type string +// matches one of the 2 possible ActivityStreams incoming content types: +// - application/activity+json +// - application/ld+json;profile=https://w3.org/ns/activitystreams +// +// Where for the above we are leniant with whitespace and quotes. +func IsASMediaType(ct string) bool { + var ( + // First content-type part, + // contains the application/... + p1 string = ct //nolint:revive + + // Second content-type part, + // contains AS IRI if provided + p2 string + ) + + // Split content-type by semi-colon. + sep := strings.IndexByte(ct, ';') + if sep >= 0 { + p1 = ct[:sep] + p2 = ct[sep+1:] + } + + // Trim any ending space from the + // main content-type part of string. + p1 = strings.TrimRight(p1, " ") + + switch p1 { + case "application/activity+json": + return p2 == "" + + case "application/ld+json": + // Trim all start/end space. + p2 = strings.Trim(p2, " ") + + // Drop any quotes around the URI str. + p2 = strings.ReplaceAll(p2, "\"", "") + + // End part must be a ref to the main AS namespace IRI. + return p2 == "profile=https://www.w3.org/ns/activitystreams" + + default: + return false + } } // federatingActor wraps the pub.FederatingActor interface @@ -67,99 +103,165 @@ func newFederatingActor(c pub.CommonBehavior, s2s pub.FederatingProtocol, db pub } } -func (f *federatingActor) Send(c context.Context, outbox *url.URL, t vocab.Type) (pub.Activity, error) { - log.Infof(c, "send activity %s via outbox %s", t.GetTypeName(), outbox) - return f.wrapped.Send(c, outbox, t) -} - -func (f *federatingActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { - return f.PostInboxScheme(c, w, r, "https") -} - // PostInboxScheme is a reimplementation of the default baseActor // implementation of PostInboxScheme in pub/base_actor.go. // // Key differences from that implementation: // - More explicit debug logging when a request is not processed. // - Normalize content of activity object. +// - *ALWAYS* return gtserror.WithCode if there's an issue, to +// provide more helpful messages to remote callers. // - Return code 202 instead of 200 on successful POST, to reflect // that we process most side effects asynchronously. func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) { - l := log. - WithContext(ctx). + l := log.WithContext(ctx). WithFields([]kv.Field{ {"userAgent", r.UserAgent()}, {"path", r.URL.Path}, }...) - // Do nothing if this is not an ActivityPub POST request. - if !func() bool { - if r.Method != http.MethodPost { - l.Debugf("inbox request was %s rather than required POST", r.Method) - return false - } - - contentType := r.Header.Get("Content-Type") - for _, mediaType := range activityStreamsMediaTypes { - if strings.Contains(contentType, mediaType) { - return true - } - } - - l.Debugf("inbox POST request content-type %s was not recognized", contentType) - return false - }() { - return false, nil + // Ensure valid ActivityPub Content-Type. + // https://www.w3.org/TR/activitypub/#server-to-server-interactions + if ct := r.Header.Get("Content-Type"); !IsASMediaType(ct) { + const ct1 = "application/activity+json" + const ct2 = "application/ld+json;profile=https://w3.org/ns/activitystreams" + err := fmt.Errorf("Content-Type %s not acceptable, this endpoint accepts: [%q %q]", ct, ct1, ct2) + return false, gtserror.NewErrorNotAcceptable(err) } - // Check the peer request is authentic. + // Authenticate request by checking http signature. ctx, authenticated, err := f.sideEffectActor.AuthenticatePostInbox(ctx, w, r) if err != nil { - return true, err + return false, gtserror.NewErrorInternalError(err) } else if !authenticated { - return true, nil + return false, gtserror.NewErrorUnauthorized(errors.New("unauthorized")) } - // Begin processing the request, but note that we have - // not yet applied authorization (ex: blocks). + /* + Begin processing the request, but note that we + have not yet applied authorization (ie., blocks). + */ + + // Obtain the activity; reject unknown activities. + activity, errWithCode := resolveActivity(ctx, r) + if errWithCode != nil { + return false, errWithCode + } + + // Set additional context data. + ctx, err = f.sideEffectActor.PostInboxRequestBodyHook(ctx, r, activity) + if err != nil { + return false, gtserror.NewErrorInternalError(err) + } + + // Check authorization of the activity. + authorized, err := f.sideEffectActor.AuthorizePostInbox(ctx, w, activity) + if err != nil { + return false, gtserror.NewErrorInternalError(err) + } + + if !authorized { + return false, gtserror.NewErrorForbidden(errors.New("blocked")) + } + + // Copy existing URL + add request host and scheme. + inboxID := func() *url.URL { + u := new(url.URL) + *u = *r.URL + u.Host = r.Host + u.Scheme = scheme + return u + }() + + // At this point we have everything we need, and have verified that + // the POST request is authentic (properly signed) and authorized + // (permitted to interact with the target inbox). // - // Obtain the activity and reject unknown activities. + // Post the activity to the Actor's inbox and trigger side effects . + if err := f.sideEffectActor.PostInbox(ctx, inboxID, activity); err != nil { + // Special case: We know it is a bad request if the object or + // target properties needed to be populated, but weren't. + // Send the rejection to the peer. + if errors.Is(err, pub.ErrObjectRequired) || errors.Is(err, pub.ErrTargetRequired) { + // Log the original error but return something a bit more generic. + l.Debugf("malformed incoming Activity: %q", err) + err = errors.New("malformed incoming Activity: an Object and/or Target was required but not set") + return false, gtserror.NewErrorBadRequest(err, err.Error()) + } + + // There's been some real error. + err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.PostInbox: %w", err) + return false, gtserror.NewErrorInternalError(err) + } + + // Side effects are complete. Now delegate determining whether + // to do inbox forwarding, as well as the action to do it. + if err := f.sideEffectActor.InboxForwarding(ctx, inboxID, activity); err != nil { + // As a not-ideal side-effect, InboxForwarding will try + // to create entries if the federatingDB returns `false` + // when calling `Exists()` to determine whether the Activity + // is in the database. + // + // Since our `Exists()` function currently *always* + // returns false, it will *always* attempt to insert + // the Activity. Therefore, we ignore AlreadyExists + // errors. + // + // This check may be removed when the `Exists()` func + // is updated, and/or federating callbacks are handled + // properly. + if !errors.Is(err, db.ErrAlreadyExists) { + err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.InboxForwarding: %w", err) + return false, gtserror.NewErrorInternalError(err) + } + } + + // Request is now undergoing processing. Caller + // of this function will handle writing Accepted. + return true, nil +} + +// resolveActivity is a util function for pulling a +// pub.Activity type out of an incoming POST request. +func resolveActivity(ctx context.Context, r *http.Request) (pub.Activity, gtserror.WithCode) { + // Tidy up when done. + defer r.Body.Close() + b, err := io.ReadAll(r.Body) if err != nil { - err = fmt.Errorf("PostInboxScheme: error reading request body: %w", err) - return true, err + err = fmt.Errorf("error reading request body: %w", err) + return nil, gtserror.NewErrorInternalError(err) } var rawActivity map[string]interface{} if err := json.Unmarshal(b, &rawActivity); err != nil { - err = fmt.Errorf("PostInboxScheme: error unmarshalling request body: %w", err) - return true, err + err = fmt.Errorf("error unmarshalling request body: %w", err) + return nil, gtserror.NewErrorInternalError(err) } t, err := streams.ToType(ctx, rawActivity) if err != nil { if !streams.IsUnmatchedErr(err) { // Real error. - err = fmt.Errorf("PostInboxScheme: error matching json to type: %w", err) - return true, err + err = fmt.Errorf("error matching json to type: %w", err) + return nil, gtserror.NewErrorInternalError(err) } + // Respond with bad request; we just couldn't // match the type to one that we know about. - l.Debug("json could not be resolved to ActivityStreams value") - w.WriteHeader(http.StatusBadRequest) - return true, nil + err = errors.New("body json could not be resolved to ActivityStreams value") + return nil, gtserror.NewErrorBadRequest(err, err.Error()) } activity, ok := t.(pub.Activity) if !ok { err = fmt.Errorf("ActivityStreams value with type %T is not a pub.Activity", t) - return true, err + return nil, gtserror.NewErrorBadRequest(err, err.Error()) } if activity.GetJSONLDId() == nil { - l.Debugf("incoming Activity %s did not have required id property set", activity.GetTypeName()) - w.WriteHeader(http.StatusBadRequest) - return true, nil + err = fmt.Errorf("incoming Activity %s did not have required id property set", activity.GetTypeName()) + return nil, gtserror.NewErrorBadRequest(err, err.Error()) } // If activity Object is a Statusable, we'll want to replace the @@ -168,56 +270,21 @@ func (f *federatingActor) PostInboxScheme(ctx context.Context, w http.ResponseWr // Likewise, if it's an Accountable, we'll normalize some fields on it. ap.NormalizeIncomingActivityObject(activity, rawActivity) - // Allow server implementations to set context data with a hook. - ctx, err = f.sideEffectActor.PostInboxRequestBodyHook(ctx, r, activity) - if err != nil { - return true, err - } + return activity, nil +} - // Check authorization of the activity. - authorized, err := f.sideEffectActor.AuthorizePostInbox(ctx, w, activity) - if err != nil { - return true, err - } else if !authorized { - return true, nil - } +/* + Functions below are just lightly wrapped versions + of the original go-fed federatingActor functions. +*/ - // Copy existing URL + add request host and scheme. - inboxID := func() *url.URL { - id := &url.URL{} - *id = *r.URL - id.Host = r.Host - id.Scheme = scheme - return id - }() +func (f *federatingActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { + return f.PostInboxScheme(c, w, r, "https") +} - // Post the activity to the actor's inbox and trigger side effects for - // that particular Activity type. It is up to the delegate to resolve - // the given map. - if err := f.sideEffectActor.PostInbox(ctx, inboxID, activity); err != nil { - // Special case: We know it is a bad request if the object or - // target properties needed to be populated, but weren't. - // - // Send the rejection to the peer. - if err == pub.ErrObjectRequired || err == pub.ErrTargetRequired { - l.Debugf("malformed incoming Activity: %s", err) - w.WriteHeader(http.StatusBadRequest) - return true, nil - } - err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.PostInbox: %w", err) - return true, err - } - - // Our side effects are complete, now delegate determining whether to do inbox forwarding, as well as the action to do it. - if err := f.sideEffectActor.InboxForwarding(ctx, inboxID, activity); err != nil { - err = fmt.Errorf("PostInboxScheme: error calling sideEffectActor.InboxForwarding: %w", err) - return true, err - } - - // Request is now undergoing processing. - // Respond with an Accepted status. - w.WriteHeader(http.StatusAccepted) - return true, nil +func (f *federatingActor) Send(c context.Context, outbox *url.URL, t vocab.Type) (pub.Activity, error) { + log.Infof(c, "send activity %s via outbox %s", t.GetTypeName(), outbox) + return f.wrapped.Send(c, outbox, t) } func (f *federatingActor) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { diff --git a/internal/federation/federatingactor_test.go b/internal/federation/federatingactor_test.go index 604f458f5..a8f71af4c 100644 --- a/internal/federation/federatingactor_test.go +++ b/internal/federation/federatingactor_test.go @@ -151,3 +151,51 @@ func (suite *FederatingActorTestSuite) TestSendRemoteFollower() { func TestFederatingActorTestSuite(t *testing.T) { suite.Run(t, new(FederatingActorTestSuite)) } + +func TestIsASMediaType(t *testing.T) { + for _, test := range []struct { + Input string + Expect bool + }{ + { + Input: "application/activity+json", + Expect: true, + }, + { + Input: "application/ld+json;profile=https://www.w3.org/ns/activitystreams", + Expect: true, + }, + { + Input: "application/ld+json;profile=\"https://www.w3.org/ns/activitystreams\"", + Expect: true, + }, + { + Input: "application/ld+json ;profile=https://www.w3.org/ns/activitystreams", + Expect: true, + }, + { + Input: "application/ld+json ;profile=\"https://www.w3.org/ns/activitystreams\"", + Expect: true, + }, + { + Input: "application/ld+json ; profile=https://www.w3.org/ns/activitystreams", + Expect: true, + }, + { + Input: "application/ld+json ; profile=\"https://www.w3.org/ns/activitystreams\"", + Expect: true, + }, + { + Input: "application/ld+json; profile=https://www.w3.org/ns/activitystreams", + Expect: true, + }, + { + Input: "application/ld+json; profile=\"https://www.w3.org/ns/activitystreams\"", + Expect: true, + }, + } { + if federation.IsASMediaType(test.Input) != test.Expect { + t.Errorf("did not get expected result %v for input: %s", test.Expect, test.Input) + } + } +}