diff options
| -rw-r--r-- | internal/api/activitypub/users/inboxpost.go | 36 | ||||
| -rw-r--r-- | internal/api/activitypub/users/inboxpost_test.go | 709 | ||||
| -rw-r--r-- | internal/federation/federatingactor.go | 281 | ||||
| -rw-r--r-- | internal/federation/federatingactor_test.go | 48 | 
4 files changed, 621 insertions, 453 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) -		return -	} +	_, err := m.processor.Fedi().InboxPost(apiutil.TransferSignatureContext(c), c.Writer, c.Request) +	if err != nil { +		errWithCode := new(gtserror.WithCode) -	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) +		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)  		} -	} else if !posted { -		err := errors.New("unable to process request") -		apiutil.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGetV1) + +		// Pass along confirmed error with code to the main error handler +		apiutil.ErrorHandler(c, *errWithCode, m.processor.InstanceGetV1) +		return  	} + +	// 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 4d494cf64..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,186 +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) - -	// 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 original activity uri as the 'object' property. +	undo.SetActivityStreamsObject(objectF()) -	// Set the To of the undo as the target of the block +	// 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) - -	// 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 @@ -263,76 +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) - -	// 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 the appropriate actor on it +	deleteActor := streams.NewActivityStreamsActorProperty() +	deleteActor.AppendIRI(testrig.URLMustParse(actorIRI)) +	delete.SetActivityStreamsActor(deleteActor) -	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) +	// Set 'object' property. +	deleteObject := streams.NewActivityStreamsObjectProperty() +	deleteObject.AppendIRI(testrig.URLMustParse(objectIRI)) +	delete.SetActivityStreamsObject(deleteObject) -	// 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") +	// 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") +	} +} -	// we need to pass the context through signature check first to set appropriate values on it -	suite.signatureCheck(ctx) +// 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()) +	} -	// 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) + +	// 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") +	} +} -	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) +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")  	} @@ -344,133 +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"] - -	// create a delete -	delete := streams.NewActivityStreamsDelete() - -	// 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) - -	// 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) +	var ( +		ctx               = context.Background() +		requestingAccount = suite.testAccounts["remote_account_1"] +		targetAccount     = suite.testAccounts["local_account_1"] +		activityID        = requestingAccount.URI + "/some-new-activity/01FG9C441MCTW3R2W117V2PQK3" +	) + +	delete := suite.newDelete(requestingAccount.URI, requestingAccount.URI, activityID) + +	// 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) @@ -485,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 (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)  	} -	// Begin processing the request, but note that we have -	// not yet applied authorization (ex: blocks). +	// 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 -	} - -	// 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 -	} +	return activity, nil +} -	// 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 -	}() +/* +	Functions below are just lightly wrapped versions +	of the original go-fed federatingActor functions. +*/ -	// 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 -	} +func (f *federatingActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { +	return f.PostInboxScheme(c, w, r, "https") +} -	// 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) +		} +	} +}  | 
