diff options
| author | 2024-03-12 15:34:08 +0100 | |
|---|---|---|
| committer | 2024-03-12 14:34:08 +0000 | |
| commit | 1bcdf1da3bb10d564a6a56a89af5afa53e5cd78f (patch) | |
| tree | 83716cea30d236c48e1655193c3adfc232e5bc75 /internal/federation/federatingdb | |
| parent | [chore] Update usage of OTEL libraries (#2725) (diff) | |
| download | gotosocial-1bcdf1da3bb10d564a6a56a89af5afa53e5cd78f.tar.xz | |
[feature] Process incoming `Move` activity (#2724)
* [feature] Process incoming account Move activity
* fix targetAcct typo
* put move origin account on fMsg
* shift more move functionality back to the worker fn
* simplify error logic
Diffstat (limited to 'internal/federation/federatingdb')
| -rw-r--r-- | internal/federation/federatingdb/accept.go | 6 | ||||
| -rw-r--r-- | internal/federation/federatingdb/announce.go | 6 | ||||
| -rw-r--r-- | internal/federation/federatingdb/create.go | 6 | ||||
| -rw-r--r-- | internal/federation/federatingdb/db.go | 7 | ||||
| -rw-r--r-- | internal/federation/federatingdb/move.go | 182 | ||||
| -rw-r--r-- | internal/federation/federatingdb/move_test.go | 201 | 
6 files changed, 408 insertions, 0 deletions
| diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go index e1d754f2e..7ec9346e0 100644 --- a/internal/federation/federatingdb/accept.go +++ b/internal/federation/federatingdb/accept.go @@ -49,6 +49,12 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA  	requestingAcct := activityContext.requestingAcct  	receivingAcct := activityContext.receivingAcct +	if requestingAcct.IsMoving() { +		// A Moving account +		// can't do this. +		return nil +	} +  	// Iterate all provided objects in the activity.  	for _, object := range ap.ExtractObjects(accept) { diff --git a/internal/federation/federatingdb/announce.go b/internal/federation/federatingdb/announce.go index 2ce6d1c59..e13e212da 100644 --- a/internal/federation/federatingdb/announce.go +++ b/internal/federation/federatingdb/announce.go @@ -49,6 +49,12 @@ func (f *federatingDB) Announce(ctx context.Context, announce vocab.ActivityStre  	requestingAcct := activityContext.requestingAcct  	receivingAcct := activityContext.receivingAcct +	if requestingAcct.IsMoving() { +		// A Moving account +		// can't do this. +		return nil +	} +  	// Ensure requestingAccount is among  	// the Actors doing the Announce.  	// diff --git a/internal/federation/federatingdb/create.go b/internal/federation/federatingdb/create.go index cfb0f319b..cacaf07cf 100644 --- a/internal/federation/federatingdb/create.go +++ b/internal/federation/federatingdb/create.go @@ -68,6 +68,12 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {  	requestingAcct := activityContext.requestingAcct  	receivingAcct := activityContext.receivingAcct +	if requestingAcct.IsMoving() { +		// A Moving account +		// can't do this. +		return nil +	} +  	switch asType.GetTypeName() {  	case ap.ActivityBlock:  		// BLOCK SOMETHING diff --git a/internal/federation/federatingdb/db.go b/internal/federation/federatingdb/db.go index 2174a8003..12bd5a376 100644 --- a/internal/federation/federatingdb/db.go +++ b/internal/federation/federatingdb/db.go @@ -31,11 +31,18 @@ import (  // DB wraps the pub.Database interface with  // a couple of custom functions for GoToSocial.  type DB interface { +	// Default functionality.  	pub.Database + +	/* +		Overridden functionality for calling from federatingProtocol. +	*/ +  	Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error  	Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error  	Reject(ctx context.Context, reject vocab.ActivityStreamsReject) error  	Announce(ctx context.Context, announce vocab.ActivityStreamsAnnounce) error +	Move(ctx context.Context, move vocab.ActivityStreamsMove) error  }  // FederatingDB uses the given state interface diff --git a/internal/federation/federatingdb/move.go b/internal/federation/federatingdb/move.go new file mode 100644 index 000000000..2e8049e08 --- /dev/null +++ b/internal/federation/federatingdb/move.go @@ -0,0 +1,182 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +// Package gtsmodel contains types used *internally* by GoToSocial and added/removed/selected from the database. +// These types should never be serialized and/or sent out via public APIs, as they contain sensitive information. +// The annotation used on these structs is for handling them via the bun-db ORM. +// See here for more info on bun model annotations: https://bun.uptrace.dev/guide/models.html + +package federatingdb + +import ( +	"context" +	"errors" +	"fmt" + +	"codeberg.org/gruf/go-logger/v2/level" +	"github.com/superseriousbusiness/activity/streams/vocab" +	"github.com/superseriousbusiness/gotosocial/internal/ap" +	"github.com/superseriousbusiness/gotosocial/internal/gtserror" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/log" +	"github.com/superseriousbusiness/gotosocial/internal/messages" +) + +func (f *federatingDB) Move(ctx context.Context, move vocab.ActivityStreamsMove) error { +	if log.Level() >= level.DEBUG { +		i, err := marshalItem(move) +		if err != nil { +			return err +		} +		l := log.WithContext(ctx). +			WithField("move", i) +		l.Debug("entering Move") +	} + +	activityContext := getActivityContext(ctx) +	if activityContext.internal { +		// Already processed. +		return nil +	} + +	requestingAcct := activityContext.requestingAcct +	receivingAcct := activityContext.receivingAcct + +	if requestingAcct.IsLocal() { +		// We should not be processing +		// a Move sent from our own +		// instance in the federatingDB. +		return nil +	} + +	// Basic Move requirements we can +	// check at this point already: +	// +	//   - Move must have ID/URI set. +	//   - Move `object` and `actor` must +	//     be set, and must be the same +	//     as requesting account. +	//   - Move `target` must be set, and +	//     must *not* be the same as +	//     requesting account. +	//   - Move `target` and `object` must +	//     not have been involved in a +	//     successful Move within the +	//     last 7 days. +	// +	// If the Move looks OK at this point, +	// additional requirements and checks +	// will be processed in FromFediAPI. + +	// Ensure ID/URI set. +	moveURI := ap.GetJSONLDId(move) +	if moveURI == nil { +		err := errors.New("Move ID/URI was nil") +		return gtserror.SetMalformed(err) +	} +	moveURIStr := moveURI.String() + +	// Check `object` property. +	objects := ap.GetObjectIRIs(move) +	if l := len(objects); l != 1 { +		err := fmt.Errorf("Move requires exactly 1 object, had %d", l) +		return gtserror.SetMalformed(err) +	} +	object := objects[0] +	objectStr := object.String() + +	if objectStr != requestingAcct.URI { +		err := fmt.Errorf( +			"Move was signed by %s but object was %s", +			requestingAcct.URI, objectStr, +		) +		return gtserror.SetMalformed(err) +	} + +	// Check `actor` property. +	actors := ap.GetActorIRIs(move) +	if l := len(actors); l != 1 { +		err := fmt.Errorf("Move requires exactly 1 actor, had %d", l) +		return gtserror.SetMalformed(err) +	} +	actor := actors[0] +	actorStr := actor.String() + +	if actorStr != requestingAcct.URI { +		err := fmt.Errorf( +			"Move was signed by %s but actor was %s", +			requestingAcct.URI, actorStr, +		) +		return gtserror.SetMalformed(err) +	} + +	// Check `target` property. +	targets := ap.GetTargetIRIs(move) +	if l := len(targets); l != 1 { +		err := fmt.Errorf("Move requires exactly 1 target, had %d", l) +		return gtserror.SetMalformed(err) +	} +	target := targets[0] +	targetStr := target.String() + +	if targetStr == requestingAcct.URI { +		err := fmt.Errorf( +			"Move target and origin were the same (%s)", +			targetStr, +		) +		return gtserror.SetMalformed(err) +	} + +	// If movedToURI is set on requestingAcct, +	// make sure it points to the intended target. +	// +	// If it's not set, that's fine, we don't +	// need it right now. We know by now that the +	// Move was really sent to us by requestingAcct. +	movedToURI := receivingAcct.MovedToURI +	if movedToURI != "" && +		movedToURI != targetStr { +		err := fmt.Errorf( +			"origin account movedTo is set to %s, which differs from Move target; will not process Move", +			movedToURI, +		) +		return gtserror.SetMalformed(err) +	} + +	// Create a stub *gtsmodel.Move with relevant +	// values. This will be updated / stored by the +	// fedi api worker as necessary. +	stubMove := >smodel.Move{ +		OriginURI: objectStr, +		Origin:    object, +		TargetURI: targetStr, +		Target:    target, +		URI:       moveURIStr, +	} + +	// We had a Move already or stored a new Move. +	// Pass back to a worker for async processing. +	f.state.Workers.EnqueueFediAPI(ctx, messages.FromFediAPI{ +		APObjectType:      ap.ObjectProfile, +		APActivityType:    ap.ActivityMove, +		GTSModel:          stubMove, +		RequestingAccount: requestingAcct, +		ReceivingAccount:  receivingAcct, +	}) + +	return nil +} diff --git a/internal/federation/federatingdb/move_test.go b/internal/federation/federatingdb/move_test.go new file mode 100644 index 000000000..006dcf0dc --- /dev/null +++ b/internal/federation/federatingdb/move_test.go @@ -0,0 +1,201 @@ +// GoToSocial +// Copyright (C) GoToSocial Authors admin@gotosocial.org +// SPDX-License-Identifier: AGPL-3.0-or-later +// +// This program is free software: you can redistribute it and/or modify +// it under the terms of the GNU Affero General Public License as published by +// the Free Software Foundation, either version 3 of the License, or +// (at your option) any later version. +// +// This program is distributed in the hope that it will be useful, +// but WITHOUT ANY WARRANTY; without even the implied warranty of +// MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the +// GNU Affero General Public License for more details. +// +// You should have received a copy of the GNU Affero General Public License +// along with this program.  If not, see <http://www.gnu.org/licenses/>. + +package federatingdb_test + +import ( +	"encoding/json" +	"testing" +	"time" + +	"github.com/stretchr/testify/suite" +	"github.com/superseriousbusiness/activity/streams" +	"github.com/superseriousbusiness/activity/streams/vocab" +	"github.com/superseriousbusiness/gotosocial/internal/ap" +	"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" +	"github.com/superseriousbusiness/gotosocial/internal/messages" +) + +type MoveTestSuite struct { +	FederatingDBTestSuite +} + +func (suite *MoveTestSuite) move( +	receivingAcct *gtsmodel.Account, +	requestingAcct *gtsmodel.Account, +	moveStr string, +) error { +	ctx := createTestContext(receivingAcct, requestingAcct) + +	rawMove := make(map[string]interface{}) +	if err := json.Unmarshal([]byte(moveStr), &rawMove); err != nil { +		suite.FailNow(err.Error()) +	} + +	t, err := streams.ToType(ctx, rawMove) +	if err != nil { +		suite.FailNow(err.Error()) +	} + +	move, ok := t.(vocab.ActivityStreamsMove) +	if !ok { +		suite.FailNow("", "couldn't cast %T to Move", t) +	} + +	return suite.federatingDB.Move(ctx, move) +} + +func (suite *MoveTestSuite) TestMove() { +	var ( +		receivingAcct  = suite.testAccounts["local_account_1"] +		requestingAcct = suite.testAccounts["remote_account_1"] +		moveStr1       = `{ +  "@context": "https://www.w3.org/ns/activitystreams", +  "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9FDFCAGM7JYPMWNTFRDQE9", +  "actor": "http://fossbros-anonymous.io/users/foss_satan", +  "type": "Move", +  "object": "http://fossbros-anonymous.io/users/foss_satan", +  "target": "https://turnip.farm/users/turniplover6969", +  "to": "http://fossbros-anonymous.io/users/foss_satan/followers" +}` +	) + +	// Trigger the move. +	suite.move(receivingAcct, requestingAcct, moveStr1) + +	// Should be a message heading to the processor. +	var msg messages.FromFediAPI +	select { +	case msg = <-suite.fromFederator: +		// Fine. +	case <-time.After(5 * time.Second): +		suite.FailNow("", "timeout waiting for suite.fromFederator") +	} +	suite.Equal(ap.ObjectProfile, msg.APObjectType) +	suite.Equal(ap.ActivityMove, msg.APActivityType) + +	// Stub Move should be on the message. +	move, ok := msg.GTSModel.(*gtsmodel.Move) +	if !ok { +		suite.FailNow("", "could not cast %T to *gtsmodel.Move", msg.GTSModel) +	} +	suite.Equal("http://fossbros-anonymous.io/users/foss_satan", move.OriginURI) +	suite.Equal("https://turnip.farm/users/turniplover6969", move.TargetURI) + +	// Trigger the same move again. +	suite.move(receivingAcct, requestingAcct, moveStr1) + +	// Should be a message heading to the processor +	// since this is just a straight up retry. +	select { +	case msg = <-suite.fromFederator: +		// Fine. +	case <-time.After(5 * time.Second): +		suite.FailNow("", "timeout waiting for suite.fromFederator") +	} +	suite.Equal(ap.ObjectProfile, msg.APObjectType) +	suite.Equal(ap.ActivityMove, msg.APActivityType) + +	// Same as the first Move, but with a different ID. +	moveStr2 := `{ +  "@context": "https://www.w3.org/ns/activitystreams", +  "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9XWDD25CKXHW82MYD1GDAR", +  "actor": "http://fossbros-anonymous.io/users/foss_satan", +  "type": "Move", +  "object": "http://fossbros-anonymous.io/users/foss_satan", +  "target": "https://turnip.farm/users/turniplover6969", +  "to": "http://fossbros-anonymous.io/users/foss_satan/followers" +}` + +	// Trigger the move. +	suite.move(receivingAcct, requestingAcct, moveStr2) + +	// Should be a message heading to the processor +	// since this is just a retry with a different ID. +	select { +	case msg = <-suite.fromFederator: +		// Fine. +	case <-time.After(5 * time.Second): +		suite.FailNow("", "timeout waiting for suite.fromFederator") +	} +	suite.Equal(ap.ObjectProfile, msg.APObjectType) +	suite.Equal(ap.ActivityMove, msg.APActivityType) +} + +func (suite *MoveTestSuite) TestBadMoves() { +	var ( +		receivingAcct  = suite.testAccounts["local_account_1"] +		requestingAcct = suite.testAccounts["remote_account_1"] +	) + +	type testStruct struct { +		moveStr string +		err     string +	} + +	for _, t := range []testStruct{ +		{ +			// Move signed by someone else. +			moveStr: `{ +  "@context": "https://www.w3.org/ns/activitystreams", +  "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9FDFCAGM7JYPMWNTFRDQE9", +  "actor": "http://fossbros-anonymous.io/users/someone_else", +  "type": "Move", +  "object": "http://fossbros-anonymous.io/users/foss_satan", +  "target": "https://turnip.farm/users/turniplover6969", +  "to": "http://fossbros-anonymous.io/users/foss_satan/followers" +}`, +			err: "Move was signed by http://fossbros-anonymous.io/users/foss_satan but actor was http://fossbros-anonymous.io/users/someone_else", +		}, +		{ +			// Actor and object not the same. +			moveStr: `{ +  "@context": "https://www.w3.org/ns/activitystreams", +  "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9FDFCAGM7JYPMWNTFRDQE9", +  "actor": "http://fossbros-anonymous.io/users/foss_satan", +  "type": "Move", +  "object": "http://fossbros-anonymous.io/users/someone_else", +  "target": "https://turnip.farm/users/turniplover6969", +  "to": "http://fossbros-anonymous.io/users/foss_satan/followers" +}`, +			err: "Move was signed by http://fossbros-anonymous.io/users/foss_satan but object was http://fossbros-anonymous.io/users/someone_else", +		}, +		{ +			// Object and target the same. +			moveStr: `{ +  "@context": "https://www.w3.org/ns/activitystreams", +  "id": "http://fossbros-anonymous.io/users/foss_satan/moves/01HR9FDFCAGM7JYPMWNTFRDQE9", +  "actor": "http://fossbros-anonymous.io/users/foss_satan", +  "type": "Move", +  "object": "http://fossbros-anonymous.io/users/foss_satan", +  "target": "http://fossbros-anonymous.io/users/foss_satan", +  "to": "http://fossbros-anonymous.io/users/foss_satan/followers" +}`, +			err: "Move target and origin were the same (http://fossbros-anonymous.io/users/foss_satan)", +		}, +	} { +		// Trigger the move. +		err := suite.move(receivingAcct, requestingAcct, t.moveStr) +		if t.err != "" { +			suite.EqualError(err, t.err) +		} +	} +} + +func TestMoveTestSuite(t *testing.T) { +	suite.Run(t, &MoveTestSuite{}) +} | 
