You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
478 lines
13 KiB
478 lines
13 KiB
// 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 workers |
|
|
|
import ( |
|
"context" |
|
"errors" |
|
"time" |
|
|
|
"github.com/superseriousbusiness/gotosocial/internal/db" |
|
"github.com/superseriousbusiness/gotosocial/internal/federation/dereferencing" |
|
"github.com/superseriousbusiness/gotosocial/internal/gtscontext" |
|
"github.com/superseriousbusiness/gotosocial/internal/gtserror" |
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" |
|
"github.com/superseriousbusiness/gotosocial/internal/id" |
|
"github.com/superseriousbusiness/gotosocial/internal/log" |
|
"github.com/superseriousbusiness/gotosocial/internal/messages" |
|
) |
|
|
|
// ShouldProcessMove checks whether we should attempt |
|
// to process a move with the given object and target, |
|
// based on whether or not a move with those values |
|
// was attempted or succeeded recently. |
|
func (p *fediAPI) ShouldProcessMove( |
|
ctx context.Context, |
|
object string, |
|
target string, |
|
) (bool, error) { |
|
// If a Move has been *attempted* within last 5m, |
|
// that involved the origin and target in any way, |
|
// then we shouldn't try to reprocess immediately. |
|
// |
|
// This avoids the potential DDOS vector of a given |
|
// origin account spamming out moves to various |
|
// target accounts, causing loads of dereferences. |
|
latestMoveAttempt, err := p.state.DB.GetLatestMoveAttemptInvolvingURIs( |
|
ctx, object, target, |
|
) |
|
if err != nil { |
|
return false, gtserror.Newf( |
|
"error checking latest Move attempt involving object %s and target %s: %w", |
|
object, target, err, |
|
) |
|
} |
|
|
|
if !latestMoveAttempt.IsZero() && |
|
time.Since(latestMoveAttempt) < 5*time.Minute { |
|
log.Infof(ctx, |
|
"object %s or target %s have been involved in a Move attempt within the last 5 minutes, will not process Move", |
|
object, target, |
|
) |
|
return false, nil |
|
} |
|
|
|
// If a Move has *succeeded* within the last week |
|
// that involved the origin and target in any way, |
|
// then we shouldn't process again for a while. |
|
latestMoveSuccess, err := p.state.DB.GetLatestMoveSuccessInvolvingURIs( |
|
ctx, object, target, |
|
) |
|
if err != nil { |
|
return false, gtserror.Newf( |
|
"error checking latest Move success involving object %s and target %s: %w", |
|
object, target, err, |
|
) |
|
} |
|
|
|
if !latestMoveSuccess.IsZero() && |
|
time.Since(latestMoveSuccess) < 168*time.Hour { |
|
log.Infof(ctx, |
|
"object %s or target %s have been involved in a successful Move within the last 7 days, will not process Move", |
|
object, target, |
|
) |
|
return false, nil |
|
} |
|
|
|
return true, nil |
|
} |
|
|
|
// GetOrCreateMove takes a stub move created by the |
|
// requesting account, and either retrieves or creates |
|
// a corresponding move in the database. If a move is |
|
// created in this way, requestingAcct will be updated |
|
// with the correct moveID. |
|
func (p *fediAPI) GetOrCreateMove( |
|
ctx context.Context, |
|
requestingAcct *gtsmodel.Account, |
|
stubMove *gtsmodel.Move, |
|
) (*gtsmodel.Move, error) { |
|
var ( |
|
moveURIStr = stubMove.URI |
|
objectStr = stubMove.OriginURI |
|
object = stubMove.Origin |
|
targetStr = stubMove.TargetURI |
|
target = stubMove.Target |
|
|
|
move *gtsmodel.Move |
|
err error |
|
) |
|
|
|
// See if we have a move with |
|
// this ID/URI stored already. |
|
move, err = p.state.DB.GetMoveByURI(ctx, moveURIStr) |
|
if err != nil && !errors.Is(err, db.ErrNoEntries) { |
|
return nil, gtserror.Newf( |
|
"db error retrieving move with URI %s: %w", |
|
moveURIStr, err, |
|
) |
|
} |
|
|
|
if move != nil { |
|
// We had a Move with this ID/URI. |
|
// |
|
// Make sure the Move we already had |
|
// stored has the same origin + target. |
|
if move.OriginURI != objectStr || |
|
move.TargetURI != targetStr { |
|
return nil, gtserror.Newf( |
|
"Move object %s and/or target %s differ from stored object and target for this ID (%s)", |
|
objectStr, targetStr, moveURIStr, |
|
) |
|
} |
|
} |
|
|
|
// If we didn't have a move stored for |
|
// this ID/URI, then see if we have a |
|
// Move with this origin and target |
|
// already (but a different ID/URI). |
|
if move == nil { |
|
move, err = p.state.DB.GetMoveByOriginTarget(ctx, objectStr, targetStr) |
|
if err != nil && !errors.Is(err, db.ErrNoEntries) { |
|
return nil, gtserror.Newf( |
|
"db error retrieving Move with object %s and target %s: %w", |
|
objectStr, targetStr, err, |
|
) |
|
} |
|
|
|
if move != nil { |
|
// We had a move for this object and |
|
// target, but the ID/URI has changed. |
|
// Update the Move's URI in the db to |
|
// reflect that this is but the latest |
|
// attempt with this origin + target. |
|
// |
|
// The remote may be trying to retry |
|
// the Move but their server might |
|
// not reuse the same Activity URIs, |
|
// and we don't want to store a brand |
|
// new Move for each attempt! |
|
move.URI = moveURIStr |
|
if err := p.state.DB.UpdateMove(ctx, move, "uri"); err != nil { |
|
return nil, gtserror.Newf( |
|
"db error updating Move with object %s and target %s: %w", |
|
objectStr, targetStr, err, |
|
) |
|
} |
|
} |
|
} |
|
|
|
if move == nil { |
|
// If Move is still nil then |
|
// we didn't have this Move |
|
// stored yet, so it's new. |
|
// Store it now! |
|
move = >smodel.Move{ |
|
ID: id.NewULID(), |
|
AttemptedAt: time.Now(), |
|
OriginURI: objectStr, |
|
Origin: object, |
|
TargetURI: targetStr, |
|
Target: target, |
|
URI: moveURIStr, |
|
} |
|
if err := p.state.DB.PutMove(ctx, move); err != nil { |
|
return nil, gtserror.Newf( |
|
"db error storing move %s: %w", |
|
moveURIStr, err, |
|
) |
|
} |
|
} |
|
|
|
// If move_id isn't set on the requesting |
|
// account yet, set it so other processes |
|
// know there's a Move in progress. |
|
if requestingAcct.MoveID != move.ID { |
|
requestingAcct.Move = move |
|
requestingAcct.MoveID = move.ID |
|
if err := p.state.DB.UpdateAccount(ctx, |
|
requestingAcct, "move_id", |
|
); err != nil { |
|
return nil, gtserror.Newf( |
|
"db error updating move_id on account: %w", |
|
err, |
|
) |
|
} |
|
} |
|
|
|
return move, nil |
|
} |
|
|
|
// MoveAccount processes the given |
|
// Move FromFediAPI message: |
|
// |
|
// APObjectType: "Profile" |
|
// APActivityType: "Move" |
|
// GTSModel: stub *gtsmodel.Move. |
|
// ReceivingAccount: Account of inbox owner receiving the Move. |
|
func (p *fediAPI) MoveAccount(ctx context.Context, fMsg *messages.FromFediAPI) error { |
|
// *gtsmodel.Move activity. |
|
stubMove, ok := fMsg.GTSModel.(*gtsmodel.Move) |
|
if !ok { |
|
return gtserror.Newf( |
|
"%T not parseable as *gtsmodel.Move", |
|
fMsg.GTSModel, |
|
) |
|
} |
|
|
|
// Move origin and target info. |
|
var ( |
|
originAcctURIStr = stubMove.OriginURI |
|
originAcct = fMsg.Requesting |
|
targetAcctURIStr = stubMove.TargetURI |
|
targetAcctURI = stubMove.Target |
|
) |
|
|
|
// Assemble log context. |
|
l := log. |
|
WithContext(ctx). |
|
WithField("originAcct", originAcctURIStr). |
|
WithField("targetAcct", targetAcctURIStr) |
|
|
|
// We can't/won't validate Move activities |
|
// to domains we have blocked, so check this. |
|
targetDomainBlocked, err := p.state.DB.IsDomainBlocked(ctx, targetAcctURI.Host) |
|
if err != nil { |
|
return gtserror.Newf( |
|
"db error checking if target domain %s blocked: %w", |
|
targetAcctURI.Host, err, |
|
) |
|
} |
|
|
|
if targetDomainBlocked { |
|
l.Info("target domain is blocked, will not process Move") |
|
return nil |
|
} |
|
|
|
// Next steps require making calls to remote + |
|
// setting values that may be attempted by other |
|
// in-process Moves. To avoid race conditions, |
|
// ensure we're only trying to process this |
|
// Move combo one attempt at a time. |
|
// |
|
// We use a custom lock because remotes might |
|
// try to send the same Move several times with |
|
// different IDs (you never know), but we only |
|
// want to process them based on origin + target. |
|
unlock := p.state.FedLocks.Lock( |
|
"move:" + originAcctURIStr + ":" + targetAcctURIStr, |
|
) |
|
defer unlock() |
|
|
|
// Check if Move is rate limited based |
|
// on previous attempts / successes. |
|
shouldProcess, err := p.ShouldProcessMove(ctx, |
|
originAcctURIStr, targetAcctURIStr, |
|
) |
|
if err != nil { |
|
return gtserror.Newf( |
|
"error checking if Move should be processed now: %w", |
|
err, |
|
) |
|
} |
|
|
|
if !shouldProcess { |
|
// Move is rate limited, so don't process. |
|
// Reason why should already be logged. |
|
return nil |
|
} |
|
|
|
// Store new or retrieve existing Move. This will |
|
// also update moveID on originAcct if necessary. |
|
move, err := p.GetOrCreateMove(ctx, originAcct, stubMove) |
|
if err != nil { |
|
return gtserror.Newf( |
|
"error refreshing target account %s: %w", |
|
targetAcctURIStr, err, |
|
) |
|
} |
|
|
|
// Account to which the Move is taking place. |
|
targetAcct, targetAcctable, err := p.federate.GetAccountByURI( |
|
ctx, |
|
fMsg.Receiving.Username, |
|
targetAcctURI, |
|
) |
|
if err != nil { |
|
return gtserror.Newf( |
|
"error getting target account %s: %w", |
|
targetAcctURIStr, err, |
|
) |
|
} |
|
|
|
// If target is suspended from this instance, |
|
// then we can't/won't process any move side |
|
// effects to that account, because: |
|
// |
|
// 1. We can't verify that it's aliased correctly |
|
// back to originAcct without dereferencing it. |
|
// 2. We can't/won't forward follows to a suspended |
|
// account, since suspension would remove follows |
|
// etc. targeting the new account anyways. |
|
// 3. If someone is moving to a suspended account |
|
// they probably totally suck ass (according to |
|
// the moderators of this instance, anyway) so |
|
// to hell with it. |
|
if targetAcct.IsSuspended() { |
|
l.Info("target account is suspended, will not process Move") |
|
return nil |
|
} |
|
|
|
if targetAcct.IsRemote() { |
|
// Force refresh Move target account |
|
// to ensure we have up-to-date version. |
|
targetAcct, _, err = p.federate.RefreshAccount(ctx, |
|
fMsg.Receiving.Username, |
|
targetAcct, |
|
targetAcctable, |
|
dereferencing.Freshest, |
|
) |
|
if err != nil { |
|
return gtserror.Newf( |
|
"error refreshing target account %s: %w", |
|
targetAcctURIStr, err, |
|
) |
|
} |
|
} |
|
|
|
// Target must not itself have moved somewhere. |
|
// You can't move to an already-moved account. |
|
targetAcctMovedTo := targetAcct.MovedToURI |
|
if targetAcctMovedTo != "" { |
|
l.Infof( |
|
"target account has, itself, already moved to %s, will not process Move", |
|
targetAcctMovedTo, |
|
) |
|
return nil |
|
} |
|
|
|
// Target must be aliased back to origin account. |
|
// Ie., its alsoKnownAs values must include the |
|
// origin account, so we know it's for real. |
|
if !targetAcct.IsAliasedTo(originAcctURIStr) { |
|
l.Info("target account is not aliased back to origin account, will not process Move") |
|
return nil |
|
} |
|
|
|
/* |
|
At this point we know that the move |
|
looks valid and we should process it. |
|
*/ |
|
|
|
// Transfer originAcct's followers |
|
// on this instance to targetAcct. |
|
redirectOK := p.utils.redirectFollowers( |
|
ctx, |
|
originAcct, |
|
targetAcct, |
|
) |
|
|
|
// Remove follows on this |
|
// instance owned by originAcct. |
|
removeFollowingOK := p.RemoveAccountFollowing( |
|
ctx, |
|
originAcct, |
|
) |
|
|
|
// Whatever happened above, error or |
|
// not, we've just at least attempted |
|
// the Move so we'll need to update it. |
|
move.AttemptedAt = time.Now() |
|
updateColumns := []string{"attempted_at"} |
|
|
|
if redirectOK && removeFollowingOK { |
|
// All OK means we can mark the |
|
// Move as definitively succeeded. |
|
// |
|
// Take same time so SucceededAt |
|
// isn't 0.0001s later or something. |
|
move.SucceededAt = move.AttemptedAt |
|
updateColumns = append(updateColumns, "succeeded_at") |
|
} |
|
|
|
// Update whatever columns we need to update. |
|
if err := p.state.DB.UpdateMove(ctx, |
|
move, updateColumns..., |
|
); err != nil { |
|
return gtserror.Newf( |
|
"db error updating Move %s: %w", |
|
move.URI, err, |
|
) |
|
} |
|
|
|
return nil |
|
} |
|
|
|
// RemoveAccountFollowing removes all |
|
// follows owned by the move originAcct. |
|
// |
|
// originAcct must be fully dereferenced |
|
// already, and the Move must be valid. |
|
// |
|
// Callers to this function MUST have obtained |
|
// a lock already by calling FedLocks.Lock. |
|
// |
|
// Return bool will be true if all goes OK. |
|
func (p *fediAPI) RemoveAccountFollowing( |
|
ctx context.Context, |
|
originAcct *gtsmodel.Account, |
|
) bool { |
|
// Any follows owned by originAcct which target |
|
// accounts on our instance should be removed. |
|
// |
|
// We should rely on the target instance |
|
// to send out new follows from targetAcct. |
|
following, err := p.state.DB.GetAccountLocalFollows( |
|
gtscontext.SetBarebones(ctx), |
|
originAcct.ID, |
|
) |
|
if err != nil && !errors.Is(err, db.ErrNoEntries) { |
|
log.Errorf(ctx, |
|
"db error getting follows owned by originAcct: %v", |
|
err, |
|
) |
|
return false |
|
} |
|
|
|
for _, follow := range following { |
|
// Ditch it. This is a one-way action |
|
// from our side so we don't need to |
|
// send any messages this time. |
|
if err := p.state.DB.DeleteFollowByID(ctx, follow.ID); err != nil { |
|
log.Errorf(ctx, |
|
"error removing old follow owned by account %s: %v", |
|
follow.AccountID, err, |
|
) |
|
return false |
|
} |
|
} |
|
|
|
// Finally delete any follow requests |
|
// owned by or targeting the originAcct. |
|
if err := p.state.DB.DeleteAccountFollowRequests( |
|
ctx, originAcct.ID, |
|
); err != nil { |
|
log.Errorf(ctx, |
|
"db error deleting follow requests involving originAcct %s: %v", |
|
originAcct.URI, err, |
|
) |
|
return false |
|
} |
|
|
|
return true |
|
}
|
|
|