diff options
Diffstat (limited to 'internal/federation')
-rw-r--r-- | internal/federation/dereferencing/status_permitted.go | 291 | ||||
-rw-r--r-- | internal/federation/federatingdb/accept.go | 173 |
2 files changed, 384 insertions, 80 deletions
diff --git a/internal/federation/dereferencing/status_permitted.go b/internal/federation/dereferencing/status_permitted.go index 5d05c5de4..86d391a59 100644 --- a/internal/federation/dereferencing/status_permitted.go +++ b/internal/federation/dereferencing/status_permitted.go @@ -119,10 +119,10 @@ func (d *Dereferencer) isPermittedReply( ) (bool, error) { var ( - replyURI = reply.URI // Definitely set. - inReplyToURI = reply.InReplyToURI // Definitely set. - inReplyTo = reply.InReplyTo // Might not be set. - acceptIRI = reply.ApprovedByURI // Might not be set. + replyURI = reply.URI // Definitely set. + inReplyToURI = reply.InReplyToURI // Definitely set. + inReplyTo = reply.InReplyTo // Might not be set. + approvedByURI = reply.ApprovedByURI // Might not be set. ) // Check if we have a stored interaction request for parent status. @@ -165,7 +165,7 @@ func (d *Dereferencer) isPermittedReply( // If it was, and it doesn't now claim to // be approved, then we should just reject it // again, as nothing's changed since last time. - if thisRejected && acceptIRI == "" { + if thisRejected && approvedByURI == "" { // Nothing changed, // still rejected. @@ -224,16 +224,17 @@ func (d *Dereferencer) isPermittedReply( // If this reply claims to be approved, // validate this by dereferencing the - // Accept and checking the return value. + // approval and checking the return value. // No further checks are required. - if acceptIRI != "" { - return d.isPermittedByAcceptIRI( + if approvedByURI != "" { + return d.isPermittedByApprovedByIRI( ctx, + gtsmodel.InteractionReply, requestUser, reply, inReplyTo, thisReq, - acceptIRI, + approvedByURI, ) } @@ -269,7 +270,7 @@ func (d *Dereferencer) isPermittedReply( // Reply is permitted and match was *not* made // based on inclusion in a followers/following // collection. Just permit the reply full stop - // as no approval / accept URI is necessary. + // as no explicit approval is necessary. return true, nil } @@ -285,7 +286,7 @@ func (d *Dereferencer) isPermittedReply( // we can't verify the presence of a remote account // in one of another remote account's collections. // - // It's possible we'll get an Accept from the replied- + // It's possible we'll get an approval from the replied- // to account later, and we can store this reply then. return false, nil } @@ -385,30 +386,36 @@ func (d *Dereferencer) unpermittedByParent( return nil } -// isPermittedByAcceptIRI checks whether the given acceptIRI -// permits the given reply to the given inReplyTo status. -// If yes, then thisReq will be updated to reflect the -// acceptance, if it's not nil. -func (d *Dereferencer) isPermittedByAcceptIRI( +// isPermittedByApprovedByIRI checks whether the given URI +// can be dereferenced, and whether it returns either an +// Accept activity or an approval object which permits the +// given reply to the given inReplyTo status. +// +// If yes, then thisReq will be updated to +// reflect the approval, if it's not nil. +func (d *Dereferencer) isPermittedByApprovedByIRI( ctx context.Context, + interactionType gtsmodel.InteractionType, requestUser string, reply *gtsmodel.Status, inReplyTo *gtsmodel.Status, thisReq *gtsmodel.InteractionRequest, - acceptIRI string, + approvedByIRI string, ) (bool, error) { - permitted, err := d.isValidAccept( + permitted, err := d.isValidApprovedByIRI( ctx, + interactionType, requestUser, - acceptIRI, - reply.URI, - inReplyTo.AccountURI, + approvedByIRI, // approval iri + inReplyTo.AccountURI, // actor + reply.URI, // object + reply.InReplyToURI, // target ) if err != nil { // Error dereferencing means we couldn't - // get the Accept right now or it wasn't + // get the approval right now or it wasn't // valid, so we shouldn't store this status. - err := gtserror.Newf("undereferencable ApprovedByURI: %w", err) + err := gtserror.Newf("undereferencable approvedByURI: %w", err) return false, err } @@ -418,12 +425,12 @@ func (d *Dereferencer) isPermittedByAcceptIRI( return false, nil } - // Reply is permitted by this Accept. + // Reply is permitted by this approval. // If it was previously rejected or // pending approval, clear that now. reply.PendingApproval = util.Ptr(false) if thisReq != nil { - thisReq.URI = acceptIRI + thisReq.URI = approvedByIRI thisReq.AcceptedAt = time.Now() thisReq.RejectedAt = time.Time{} err := d.state.DB.UpdateInteractionRequest( @@ -576,7 +583,7 @@ func (d *Dereferencer) isPermittedBoost( // permitted but matched on a collection. // // Check if we can dereference - // an Accept that grants approval. + // an IRI that grants approval. if status.ApprovedByURI == "" { // Status doesn't claim to be approved. @@ -602,18 +609,20 @@ func (d *Dereferencer) isPermittedBoost( } // Boost claims to be approved, check - // this by dereferencing the Accept and - // inspecting the return value. - permitted, err := d.isValidAccept( + // this by dereferencing the approvedBy + // and inspecting the return value. + permitted, err := d.isValidApprovedByIRI( ctx, + gtsmodel.InteractionAnnounce, requestUser, - status.ApprovedByURI, - status.URI, - boostOf.AccountURI, + status.ApprovedByURI, // approval uri + boostOf.AccountURI, // actor + status.URI, // object + status.BoostOfURI, // target ) if err != nil { // Error dereferencing means we couldn't - // get the Accept right now or it wasn't + // get the approval right now or it wasn't // valid, so we shouldn't store this status. err := gtserror.Newf("undereferencable ApprovedByURI: %w", err) return false, err @@ -628,34 +637,36 @@ func (d *Dereferencer) isPermittedBoost( return true, nil } -// isValidAccept dereferences the activitystreams Accept at the -// specified IRI, and checks the Accept for validity against the -// provided expectedObject and expectedActor. +// isValidApprovedByIRI dereferences the activitystreams Accept or approval +// at the specified IRI, and checks the Accept or approval for validity +// against the provided expectedActor, expectedObject, and expectedTarget. // // Will return either (true, nil) if everything looked OK, an error // if something went wrong internally during deref, or (false, nil) -// if the dereferenced Accept did not meet expectations. -func (d *Dereferencer) isValidAccept( +// if the dereferenced Accept/Approval did not meet expectations. +func (d *Dereferencer) isValidApprovedByIRI( ctx context.Context, + interactionType gtsmodel.InteractionType, requestUser string, - acceptIRIStr string, // Eg., "https://example.org/users/someone/accepts/01J2736AWWJ3411CPR833F6D03" - expectObjectURIStr string, // Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R" - expectActorURIStr string, // Eg., "https://example.org/users/someone" + approvedByIRIStr string, // approval uri Eg., "https://example.org/users/someone/accepts/01J2736AWWJ3411CPR833F6D03" + expectActorURIStr string, // actor Eg., "https://example.org/users/someone" + expectObjectURIStr string, // object Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R" + expectTargetURIStr string, // target Eg., "https://example.org/users/someone/statuses/01JM4REQTJ1BZ1R4BPYP1W4R9E" ) (bool, error) { l := log. WithContext(ctx). - WithField("acceptIRI", acceptIRIStr) + WithField("approvedByIRI", approvedByIRIStr) - acceptIRI, err := url.Parse(acceptIRIStr) + approvedByIRI, err := url.Parse(approvedByIRIStr) if err != nil { // Real returnable error. - err := gtserror.Newf("error parsing acceptIRI: %w", err) + err := gtserror.Newf("error parsing approvedByIRI: %w", err) return false, err } - // Don't make calls to the Accept IRI - // if it's blocked, just return false. - blocked, err := d.state.DB.IsDomainBlocked(ctx, acceptIRI.Host) + // Don't make calls to the IRI if its + // domain is blocked, just return false. + blocked, err := d.state.DB.IsDomainBlocked(ctx, approvedByIRI.Host) if err != nil { // Real returnable error. err := gtserror.Newf("error checking domain block: %w", err) @@ -663,7 +674,7 @@ func (d *Dereferencer) isValidAccept( } if blocked { - l.Info("Accept host is blocked") + l.Info("approvedByIRI host is blocked") return false, nil } @@ -674,51 +685,52 @@ func (d *Dereferencer) isValidAccept( return false, err } - // Make the call to resolve into an Acceptable. + // Make the call to the approvedByURI. // Log any error encountered here but don't // return it as it's not *our* error. - rsp, err := tsport.Dereference(ctx, acceptIRI) + rsp, err := tsport.Dereference(ctx, approvedByIRI) if err != nil { - l.Errorf("error dereferencing Accept: %v", err) + l.Errorf("error dereferencing approvedByIRI: %v", err) return false, nil } - acceptable, err := ap.ResolveAcceptable(ctx, rsp.Body) + // Try to parse response as an AP type. + t, err := ap.DecodeType(ctx, rsp.Body) // Tidy up rsp body. _ = rsp.Body.Close() if err != nil { - l.Errorf("error resolving to Accept: %v", err) + l.Errorf("error resolving to type: %v", err) return false, err } - // Extract the URI/ID of the Accept. - acceptID := ap.GetJSONLDId(acceptable) - acceptIDStr := acceptID.String() + // Extract the URI/ID of the type. + approvedByID := ap.GetJSONLDId(t) + approvedByIDStr := approvedByID.String() // Check whether input URI and final returned URI // have changed (i.e. we followed some redirects). rspURL := rsp.Request.URL rspURLStr := rspURL.String() - if rspURLStr != acceptIRIStr { - // If rspURLStr != acceptIRIStr, make sure final + if rspURLStr != approvedByIRIStr { + // If rspURLStr != approvedByIRI, make sure final // response URL is at least on the same host as // what we expected (ie., we weren't redirected // across domains), and make sure it's the same // as the ID of the Accept we were returned. switch { - case rspURL.Host != acceptIRI.Host: + case rspURL.Host != approvedByIRI.Host: l.Errorf( - "final deref host %s did not match acceptIRI host", + "final deref host %s did not match approvedByIRI host", rspURL.Host, ) return false, nil - case acceptIDStr != rspURLStr: + case approvedByIDStr != rspURLStr: l.Errorf( - "final deref uri %s did not match returned Accept ID %s", - rspURLStr, acceptIDStr, + "final deref uri %s did not match returned ID %s", + rspURLStr, approvedByIDStr, ) return false, nil } @@ -727,6 +739,52 @@ func (d *Dereferencer) isValidAccept( // Response is superficially OK, // check in more detail now. + // First try to parse type as Approval stamp. + if approvable, ok := ap.ToApprovable(t); ok { + return isValidApprovable( + ctx, + interactionType, + approvable, + approvedByID, + expectActorURIStr, // actor + expectObjectURIStr, // object + expectTargetURIStr, // target + ) + } + + // Fall back to parsing as a simple Accept. + if acceptable, ok := ap.ToAcceptable(t); ok { + return isValidAcceptable( + ctx, + acceptable, + approvedByID, + expectActorURIStr, // actor + expectObjectURIStr, // object + expectTargetURIStr, // target + ) + } + + // Type wasn't something we + // could do anything with! + l.Errorf( + "%T at %s not approvable or acceptable", + t, approvedByIRIStr, + ) + return false, nil +} + +func isValidAcceptable( + ctx context.Context, + acceptable ap.Acceptable, + acceptID *url.URL, + expectActorURIStr string, // actor Eg., "https://example.org/users/someone" + expectObjectURIStr string, // object Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R" + expectTargetURIStr string, // target Eg., "https://example.org/users/someone/statuses/01JM4REQTJ1BZ1R4BPYP1W4R9E" +) (bool, error) { + l := log. + WithContext(ctx). + WithField("accept", acceptID.String()) + // Extract the actor IRI and string from Accept. actorIRIs := ap.GetActorIRIs(acceptable) actorIRI, actorIRIStr := extractIRI(actorIRIs) @@ -775,6 +833,113 @@ func (d *Dereferencer) isValidAccept( return false, nil } + // If there's a Target set then verify it's + // what we expect it to be, ie., it should point + // back to the post that's being interacted with. + targetIRIs := ap.GetTargetIRIs(acceptable) + _, targetIRIStr := extractIRI(targetIRIs) + if targetIRIStr != "" && targetIRIStr != expectTargetURIStr { + l.Errorf( + "resolved Accept target IRI %s was not the same as expected target %s", + targetIRIStr, expectTargetURIStr, + ) + return false, nil + } + + // Everything looks OK. + return true, nil +} + +func isValidApprovable( + ctx context.Context, + interactionType gtsmodel.InteractionType, + approvable ap.Approvable, + approvalID *url.URL, + expectActorURIStr string, // actor Eg., "https://example.org/users/someone" + expectObjectURIStr string, // object Eg., "https://some.instance.example.org/users/someone_else/statuses/01J27414TWV9F7DC39FN8ABB5R" + expectTargetURIStr string, // target Eg., "https://example.org/users/someone/statuses/01JM4REQTJ1BZ1R4BPYP1W4R9E" +) (bool, error) { + l := log. + WithContext(ctx). + WithField("approval", approvalID.String()) + + // Check that the type of the Approval + // matches the interaction it's approving. + switch tn := approvable.GetTypeName(); { + case (tn == ap.ObjectLikeApproval && interactionType == gtsmodel.InteractionLike), + (tn == ap.ObjectReplyApproval && interactionType == gtsmodel.InteractionReply), + (tn == ap.ObjectAnnounceApproval && interactionType == gtsmodel.InteractionAnnounce): + // All good baby! + default: + // There's a mismatch. + l.Errorf( + "approval type %s cannot approve %s", + tn, interactionType.String(), + ) + return false, nil + } + + // Extract the actor IRI and string from Approval. + actorIRIs := ap.GetAttributedTo(approvable) + actorIRI, actorIRIStr := extractIRI(actorIRIs) + switch { + case actorIRIStr == "": + l.Error("Approval missing attributedTo IRI") + return false, nil + + // Ensure the Approval actor is on + // the instance hosting the Approval. + case actorIRI.Host != approvalID.Host: + l.Errorf( + "actor %s not on the same host as Approval", + actorIRIStr, + ) + return false, nil + + // Ensure the Approval actor is who we expect + // it to be, and not someone else trying to + // do an Approval for an interaction with a + // statusable they don't own. + case actorIRIStr != expectActorURIStr: + l.Errorf( + "actor %s was not the same as expected actor %s", + actorIRIStr, expectActorURIStr, + ) + return false, nil + } + + // Extract the object IRI string from Approval. + objectIRIs := ap.GetObjectIRIs(approvable) + _, objectIRIStr := extractIRI(objectIRIs) + switch { + case objectIRIStr == "": + l.Error("missing Approval object IRI") + return false, nil + + // Ensure the Approval Object is what we expect + // it to be, ie., it's approving the interaction + // we need it to approve, and not something else. + case objectIRIStr != expectObjectURIStr: + l.Errorf( + "resolved Approval object IRI %s was not the same as expected object %s", + objectIRIStr, expectObjectURIStr, + ) + return false, nil + } + + // If there's a Target set then verify it's + // what we expect it to be, ie., it should point + // back to the post that's being interacted with. + targetIRIs := ap.GetTargetIRIs(approvable) + _, targetIRIStr := extractIRI(targetIRIs) + if targetIRIStr != "" && targetIRIStr != expectTargetURIStr { + l.Errorf( + "resolved Approval target IRI %s was not the same as expected target %s", + targetIRIStr, expectTargetURIStr, + ) + return false, nil + } + // Everything looks OK. return true, nil } diff --git a/internal/federation/federatingdb/accept.go b/internal/federation/federatingdb/accept.go index 0274fd9d7..09bbde97b 100644 --- a/internal/federation/federatingdb/accept.go +++ b/internal/federation/federatingdb/accept.go @@ -20,6 +20,7 @@ package federatingdb import ( "context" "errors" + "fmt" "net/url" "github.com/superseriousbusiness/activity/streams/vocab" @@ -62,8 +63,8 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA return nil } - activityID := ap.GetJSONLDId(accept) - if activityID == nil { + acceptID := ap.GetJSONLDId(accept) + if acceptID == nil { // We need an ID. const text = "Accept had no id property" return gtserror.NewErrorBadRequest(errors.New(text), text) @@ -87,12 +88,11 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA // handling the ones we know how to handle. for _, object := range ap.ExtractObjects(accept) { if asType := object.GetType(); asType != nil { - // Check and handle any vocab.Type objects. - switch name := asType.GetTypeName(); name { + switch name := asType.GetTypeName(); { // ACCEPT FOLLOW - case ap.ActivityFollow: + case name == ap.ActivityFollow: if err := f.acceptFollowType( ctx, asType, @@ -102,6 +102,50 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA return err } + // ACCEPT TYPE-HINTED LIKE + // + // ie., a Like with just `id` + // and `type` properties set. + case name == ap.ActivityLike: + objIRI := ap.GetJSONLDId(asType) + if objIRI == nil { + log.Debugf(ctx, "could not retrieve id of inlined Accept object %s", name) + continue + } + + if err := f.acceptLikeIRI( + ctx, + acceptID, + accept, + objIRI.String(), + receivingAcct, + requestingAcct, + ); err != nil { + return err + } + + // ACCEPT TYPE-HINTED REPLY OR ANNOUNCE. + // + // ie., a statusable or Announce with + // just `id` and `type` properties set. + case name == ap.ActivityAnnounce || ap.IsStatusable(name): + objIRI := ap.GetJSONLDId(asType) + if objIRI == nil { + log.Debugf(ctx, "could not retrieve id of inlined Accept object %s", name) + continue + } + + if err := f.acceptOtherIRI( + ctx, + acceptID, + accept, + objIRI, + receivingAcct, + requestingAcct, + ); err != nil { + return err + } + // UNHANDLED default: log.Debugf(ctx, "unhandled object type: %s", name) @@ -127,7 +171,8 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA case uris.IsLikePath(objIRI): if err := f.acceptLikeIRI( ctx, - activityID.String(), + acceptID, + accept, objIRI.String(), receivingAcct, requestingAcct, @@ -135,14 +180,15 @@ func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsA return err } - // ACCEPT OTHER (reply? boost?) + // ACCEPT OTHER (reply? announce?) // // Don't check on IsStatusesPath // as this may be a remote status. default: if err := f.acceptOtherIRI( ctx, - activityID, + acceptID, + accept, objIRI, receivingAcct, requestingAcct, @@ -292,7 +338,8 @@ func (f *federatingDB) acceptFollowIRI( func (f *federatingDB) acceptOtherIRI( ctx context.Context, - activityID *url.URL, + acceptID *url.URL, + accept vocab.ActivityStreamsAccept, objectIRI *url.URL, receivingAcct *gtsmodel.Account, requestingAcct *gtsmodel.Account, @@ -309,7 +356,8 @@ func (f *federatingDB) acceptOtherIRI( // objectIRI, proceed to accept it. return f.acceptStoredStatus( ctx, - activityID, + acceptID, + accept, status, receivingAcct, requestingAcct, @@ -348,13 +396,21 @@ func (f *federatingDB) acceptOtherIRI( // This may be a reply, or it may be a boost, // we can't know yet without dereferencing it, // but let the processor worry about that. + // + // TODO: do something with type hinting here. apObjectType := ap.ObjectUnknown + // Extract appropriate approvedByURI from the Accept. + approvedByURI, err := approvedByURI(acceptID, accept) + if err != nil { + return gtserror.NewErrorForbidden(err, err.Error()) + } + // Pass to the processor and let them handle side effects. f.state.Workers.Federator.Queue.Push(&messages.FromFediAPI{ APObjectType: apObjectType, APActivityType: ap.ActivityAccept, - APIRI: activityID, + APIRI: approvedByURI, APObject: objectIRI, Receiving: receivingAcct, Requesting: requestingAcct, @@ -365,7 +421,8 @@ func (f *federatingDB) acceptOtherIRI( func (f *federatingDB) acceptStoredStatus( ctx context.Context, - activityID *url.URL, + acceptID *url.URL, + accept vocab.ActivityStreamsAccept, status *gtsmodel.Status, receivingAcct *gtsmodel.Account, requestingAcct *gtsmodel.Account, @@ -391,9 +448,15 @@ func (f *federatingDB) acceptStoredStatus( return gtserror.NewErrorForbidden(errors.New(text), text) } - // Mark the status as approved by this Accept URI. + // Extract appropriate approvedByURI from the Accept. + approvedByURI, err := approvedByURI(acceptID, accept) + if err != nil { + return gtserror.NewErrorForbidden(err, err.Error()) + } + + // Mark the status as approved by this URI. status.PendingApproval = util.Ptr(false) - status.ApprovedByURI = activityID.String() + status.ApprovedByURI = approvedByURI.String() if err := f.state.DB.UpdateStatus( ctx, status, @@ -428,7 +491,8 @@ func (f *federatingDB) acceptStoredStatus( func (f *federatingDB) acceptLikeIRI( ctx context.Context, - activityID string, + acceptID *url.URL, + accept vocab.ActivityStreamsAccept, objectIRI string, receivingAcct *gtsmodel.Account, requestingAcct *gtsmodel.Account, @@ -482,9 +546,15 @@ func (f *federatingDB) acceptLikeIRI( return gtserror.NewErrorForbidden(errors.New(text), text) } - // Mark the fave as approved by this Accept URI. + // Extract appropriate approvedByURI from the Accept. + approvedByURI, err := approvedByURI(acceptID, accept) + if err != nil { + return gtserror.NewErrorForbidden(err, err.Error()) + } + + // Mark the fave as approved by this URI. fave.PendingApproval = util.Ptr(false) - fave.ApprovedByURI = activityID + fave.ApprovedByURI = approvedByURI.String() if err := f.state.DB.UpdateStatusFave( ctx, fave, @@ -507,3 +577,72 @@ func (f *federatingDB) acceptLikeIRI( return nil } + +// approvedByURI extracts the appropriate *url.URL +// to use as an interaction's approvedBy value by +// checking to see if the Accept has a result URL set. +// If that result URL exists, is an IRI (not a type), +// and is on the same host as the Accept ID, then the +// result URI will be returned. In all other cases, +// the Accept ID is returned unchanged. +// +// Error is only returned if the result URI is set +// but the host differs from the Accept ID host. +// +// TODO: This function should be updated at some point +// to check for inlined result type, and see if type is +// a LikeApproval, ReplyApproval, or AnnounceApproval, +// and check the attributedTo, object, and target of +// the approval as well. But this'll do for now. +func approvedByURI( + acceptID *url.URL, + accept vocab.ActivityStreamsAccept, +) (*url.URL, error) { + // Check if the Accept has a `result` property + // set on it (which should be an approval). + resultProp := accept.GetActivityStreamsResult() + if resultProp == nil { + // No result, + // use AcceptID. + return acceptID, nil + } + + if resultProp.Len() != 1 { + // Result was unexpected + // length, can't use this. + return acceptID, nil + } + + result := resultProp.At(0) + if result == nil { + // Result entry + // was nil, huh! + return acceptID, nil + } + + if !result.IsIRI() { + // Can't handle + // inlined yet. + return acceptID, nil + } + + resultIRI := result.GetIRI() + if resultIRI == nil { + // Result entry + // was nil, huh! + return acceptID, nil + } + + if resultIRI.Host != acceptID.Host { + // What the boobs is this? + err := fmt.Errorf( + "host of result %s differed from host of Accept %s", + resultIRI, accept, + ) + return nil, err + } + + // Use the result IRI we've been + // given instead of the acceptID. + return resultIRI, nil +} |