diff options
Diffstat (limited to 'internal/federation')
-rw-r--r-- | internal/federation/dereferencing/account.go | 9 | ||||
-rw-r--r-- | internal/federation/dereferencing/dereferencer.go | 10 | ||||
-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 | ||||
-rw-r--r-- | internal/federation/federatingprotocol.go | 16 |
9 files changed, 437 insertions, 6 deletions
diff --git a/internal/federation/dereferencing/account.go b/internal/federation/dereferencing/account.go index 10d15bca6..5e81fb445 100644 --- a/internal/federation/dereferencing/account.go +++ b/internal/federation/dereferencing/account.go @@ -64,8 +64,8 @@ func accountFresh( return true } - if !account.SuspendedAt.IsZero() { - // Can't refresh + if account.IsSuspended() { + // Can't/won't refresh // suspended accounts. return true } @@ -388,8 +388,9 @@ func (d *Dereferencer) enrichAccountSafely( account *gtsmodel.Account, accountable ap.Accountable, ) (*gtsmodel.Account, ap.Accountable, error) { - // Noop if account has been suspended. - if !account.SuspendedAt.IsZero() { + // Noop if account suspended; + // we don't want to deref it. + if account.IsSuspended() { return account, nil, nil } diff --git a/internal/federation/dereferencing/dereferencer.go b/internal/federation/dereferencing/dereferencer.go index 24e579408..3fa199345 100644 --- a/internal/federation/dereferencing/dereferencer.go +++ b/internal/federation/dereferencing/dereferencer.go @@ -64,6 +64,16 @@ var ( // This is tuned to be quite fresh without // causing loads of dereferencing calls. Fresh = util.Ptr(FreshnessWindow(5 * time.Minute)) + + // 10 seconds. + // + // Freshest is useful when you want an + // immediately up to date model of something + // that's even fresher than Fresh. + // + // Be careful using this one; it can cause + // lots of unnecessary traffic if used unwisely. + Freshest = util.Ptr(FreshnessWindow(10 * time.Second)) ) // Dereferencer wraps logic and functionality for doing dereferencing 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{}) +} diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index f3ab1ae3c..2c2da7b7b 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -450,7 +450,11 @@ func (f *Federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er // // Applications are not expected to handle every single ActivityStreams // type and extension. The unhandled ones are passed to DefaultCallback. -func (f *Federator) FederatingCallbacks(ctx context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}, err error) { +func (f *Federator) FederatingCallbacks(ctx context.Context) ( + wrapped pub.FederatingWrappedCallbacks, + other []any, + err error, +) { wrapped = pub.FederatingWrappedCallbacks{ // OnFollow determines what action to take for this // particular callback if a Follow Activity is handled. @@ -461,7 +465,7 @@ func (f *Federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa } // Override some default behaviors to trigger our own side effects. - other = []interface{}{ + other = []any{ func(ctx context.Context, undo vocab.ActivityStreamsUndo) error { return f.FederatingDB().Undo(ctx, undo) }, @@ -476,6 +480,14 @@ func (f *Federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa }, } + // Define some of our own behaviors which are not + // overrides of the default pub.FederatingWrappedCallbacks. + other = append(other, []any{ + func(ctx context.Context, move vocab.ActivityStreamsMove) error { + return f.FederatingDB().Move(ctx, move) + }, + }...) + return } |