[security] Check all involved IRIs during block checking (#593)
* tidy up context keys, add otherInvolvedIRIs * add ReplyToable interface * skip block check if we own the requesting domain * add block check for other involved IRIs * use cacheable status fetch * remove unused ContextActivity * remove unused ContextActivity * add helper for unique URIs * check through CCs and clean slice * add GetAccountIDForStatusURI * add GetAccountIDForAccountURI * check blocks on involved account * add statuses to tests * add some blocked tests * go fmt * extract Tos as well as CCs * test PostInboxRequestBodyHook * add some more testActivities * deduplicate involvedAccountIDs * go fmt * use cacheable db functions, remove new functions
This commit is contained in:
parent
d6abe105b3
commit
469da93678
|
@ -22,20 +22,16 @@ package ap
|
||||||
type ContextKey string
|
type ContextKey string
|
||||||
|
|
||||||
const (
|
const (
|
||||||
// ContextActivity can be used to set and retrieve the actual go-fed pub.Activity within a context.
|
|
||||||
ContextActivity ContextKey = "activity"
|
|
||||||
// ContextReceivingAccount can be used the set and retrieve the account being interacted with / receiving an activity in their inbox.
|
// ContextReceivingAccount can be used the set and retrieve the account being interacted with / receiving an activity in their inbox.
|
||||||
ContextReceivingAccount ContextKey = "account"
|
ContextReceivingAccount ContextKey = "receivingAccount"
|
||||||
// ContextRequestingAccount can be used to set and retrieve the account of an incoming federation request.
|
// ContextRequestingAccount can be used to set and retrieve the account of an incoming federation request.
|
||||||
// This will often be the actor of the instance that's posting the request.
|
// This will often be the actor of the instance that's posting the request.
|
||||||
ContextRequestingAccount ContextKey = "requestingAccount"
|
ContextRequestingAccount ContextKey = "requestingAccount"
|
||||||
// ContextRequestingActorIRI can be used to set and retrieve the actor of an incoming federation request.
|
// ContextOtherInvolvedIRIs can be used to set and retrieve a slice of all IRIs that are 'involved' in an Activity without being
|
||||||
// This will usually be the owner of whatever activity is being posted.
|
// the receivingAccount or the requestingAccount. In other words, people or notes who are CC'ed or Replied To by an Activity.
|
||||||
ContextRequestingActorIRI ContextKey = "requestingActorIRI"
|
ContextOtherInvolvedIRIs ContextKey = "otherInvolvedIRIs"
|
||||||
// ContextRequestingPublicKeyVerifier can be used to set and retrieve the public key verifier of an incoming federation request.
|
// ContextRequestingPublicKeyVerifier can be used to set and retrieve the public key verifier of an incoming federation request.
|
||||||
ContextRequestingPublicKeyVerifier ContextKey = "requestingPublicKeyVerifier"
|
ContextRequestingPublicKeyVerifier ContextKey = "requestingPublicKeyVerifier"
|
||||||
// ContextRequestingPublicKeySignature can be used to set and retrieve the value of the signature header of an incoming federation request.
|
// ContextRequestingPublicKeySignature can be used to set and retrieve the value of the signature header of an incoming federation request.
|
||||||
ContextRequestingPublicKeySignature ContextKey = "requestingPublicKeySignature"
|
ContextRequestingPublicKeySignature ContextKey = "requestingPublicKeySignature"
|
||||||
// ContextFromFederatorChan can be used to pass a pointer to the fromFederator channel into the federator for use in callbacks.
|
|
||||||
ContextFromFederatorChan ContextKey = "fromFederatorChan"
|
|
||||||
)
|
)
|
||||||
|
|
|
@ -140,6 +140,11 @@ type Addressable interface {
|
||||||
WithCC
|
WithCC
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ReplyToable represents the minimum interface for an Activity that can be InReplyTo another activity.
|
||||||
|
type ReplyToable interface {
|
||||||
|
WithInReplyTo
|
||||||
|
}
|
||||||
|
|
||||||
// CollectionPageable represents the minimum interface for an activitystreams 'CollectionPage' object.
|
// CollectionPageable represents the minimum interface for an activitystreams 'CollectionPage' object.
|
||||||
type CollectionPageable interface {
|
type CollectionPageable interface {
|
||||||
WithJSONLDId
|
WithJSONLDId
|
||||||
|
|
|
@ -23,6 +23,8 @@ import (
|
||||||
"net/url"
|
"net/url"
|
||||||
"strings"
|
"strings"
|
||||||
|
|
||||||
|
"github.com/spf13/viper"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/config"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/util"
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
|
@ -33,7 +35,7 @@ type domainDB struct {
|
||||||
}
|
}
|
||||||
|
|
||||||
func (d *domainDB) IsDomainBlocked(ctx context.Context, domain string) (bool, db.Error) {
|
func (d *domainDB) IsDomainBlocked(ctx context.Context, domain string) (bool, db.Error) {
|
||||||
if domain == "" {
|
if domain == "" || domain == viper.GetString(config.Keys.Host) {
|
||||||
return false, nil
|
return false, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -33,6 +33,7 @@ import (
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/db"
|
"github.com/superseriousbusiness/gotosocial/internal/db"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
"github.com/superseriousbusiness/gotosocial/internal/uris"
|
||||||
|
"github.com/superseriousbusiness/gotosocial/internal/util"
|
||||||
)
|
)
|
||||||
|
|
||||||
/*
|
/*
|
||||||
|
@ -62,19 +63,60 @@ import (
|
||||||
// write a response to the ResponseWriter as is expected that the caller
|
// write a response to the ResponseWriter as is expected that the caller
|
||||||
// to PostInbox will do so when handling the error.
|
// to PostInbox will do so when handling the error.
|
||||||
func (f *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) {
|
func (f *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) {
|
||||||
l := logrus.WithFields(logrus.Fields{
|
// extract any other IRIs involved in this activity
|
||||||
"func": "PostInboxRequestBodyHook",
|
otherInvolvedIRIs := []*url.URL{}
|
||||||
"useragent": r.UserAgent(),
|
|
||||||
"url": r.URL.String(),
|
|
||||||
})
|
|
||||||
|
|
||||||
if activity == nil {
|
// check if the Activity itself has an 'inReplyTo'
|
||||||
err := errors.New("nil activity in PostInboxRequestBodyHook")
|
if replyToable, ok := activity.(ap.ReplyToable); ok {
|
||||||
l.Debug(err)
|
if inReplyToURI := ap.ExtractInReplyToURI(replyToable); inReplyToURI != nil {
|
||||||
return nil, err
|
otherInvolvedIRIs = append(otherInvolvedIRIs, inReplyToURI)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
// set the activity on the context for use later on
|
|
||||||
return context.WithValue(ctx, ap.ContextActivity, activity), nil
|
// now check if the Object of the Activity (usually a Note or something) has an 'inReplyTo'
|
||||||
|
if object := activity.GetActivityStreamsObject(); object != nil {
|
||||||
|
if replyToable, ok := object.(ap.ReplyToable); ok {
|
||||||
|
if inReplyToURI := ap.ExtractInReplyToURI(replyToable); inReplyToURI != nil {
|
||||||
|
otherInvolvedIRIs = append(otherInvolvedIRIs, inReplyToURI)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// check for Tos and CCs on Activity itself
|
||||||
|
if addressable, ok := activity.(ap.Addressable); ok {
|
||||||
|
if ccURIs, err := ap.ExtractCCs(addressable); err == nil {
|
||||||
|
otherInvolvedIRIs = append(otherInvolvedIRIs, ccURIs...)
|
||||||
|
}
|
||||||
|
if toURIs, err := ap.ExtractTos(addressable); err == nil {
|
||||||
|
otherInvolvedIRIs = append(otherInvolvedIRIs, toURIs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// and on the Object itself
|
||||||
|
if object := activity.GetActivityStreamsObject(); object != nil {
|
||||||
|
if addressable, ok := object.(ap.Addressable); ok {
|
||||||
|
if ccURIs, err := ap.ExtractCCs(addressable); err == nil {
|
||||||
|
otherInvolvedIRIs = append(otherInvolvedIRIs, ccURIs...)
|
||||||
|
}
|
||||||
|
if toURIs, err := ap.ExtractTos(addressable); err == nil {
|
||||||
|
otherInvolvedIRIs = append(otherInvolvedIRIs, toURIs...)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// remove any duplicate entries in the slice we put together
|
||||||
|
deduped := util.UniqueURIs(otherInvolvedIRIs)
|
||||||
|
|
||||||
|
// clean any instances of the public URI since we don't care about that in this context
|
||||||
|
cleaned := []*url.URL{}
|
||||||
|
for _, u := range deduped {
|
||||||
|
if !pub.IsPublic(u.String()) {
|
||||||
|
cleaned = append(cleaned, u)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
withOtherInvolvedIRIs := context.WithValue(ctx, ap.ContextOtherInvolvedIRIs, cleaned)
|
||||||
|
return withOtherInvolvedIRIs, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// AuthenticatePostInbox delegates the authentication of a POST to an
|
// AuthenticatePostInbox delegates the authentication of a POST to an
|
||||||
|
@ -185,40 +227,85 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
|
||||||
})
|
})
|
||||||
l.Debugf("entering BLOCKED function with IRI list: %+v", actorIRIs)
|
l.Debugf("entering BLOCKED function with IRI list: %+v", actorIRIs)
|
||||||
|
|
||||||
|
// check domain blocks first for the given actor IRIs
|
||||||
|
blocked, err := f.db.AreURIsBlocked(ctx, actorIRIs)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error checking domain blocks of actorIRIs: %s", err)
|
||||||
|
}
|
||||||
|
if blocked {
|
||||||
|
return blocked, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// check domain blocks for any other involved IRIs
|
||||||
|
otherInvolvedIRIsI := ctx.Value(ap.ContextOtherInvolvedIRIs)
|
||||||
|
otherInvolvedIRIs, ok := otherInvolvedIRIsI.([]*url.URL)
|
||||||
|
if !ok {
|
||||||
|
l.Errorf("other involved IRIs not set on request context")
|
||||||
|
return false, errors.New("other involved IRIs not set on request context, so couldn't determine blocks")
|
||||||
|
}
|
||||||
|
blocked, err = f.db.AreURIsBlocked(ctx, otherInvolvedIRIs)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error checking domain blocks of otherInvolvedIRIs: %s", err)
|
||||||
|
}
|
||||||
|
if blocked {
|
||||||
|
return blocked, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// now check for user-level block from receiving against requesting account
|
||||||
receivingAccountI := ctx.Value(ap.ContextReceivingAccount)
|
receivingAccountI := ctx.Value(ap.ContextReceivingAccount)
|
||||||
receivingAccount, ok := receivingAccountI.(*gtsmodel.Account)
|
receivingAccount, ok := receivingAccountI.(*gtsmodel.Account)
|
||||||
if !ok {
|
if !ok {
|
||||||
l.Errorf("receiving account not set on request context")
|
l.Errorf("receiving account not set on request context")
|
||||||
return false, errors.New("receiving account not set on request context, so couldn't determine blocks")
|
return false, errors.New("receiving account not set on request context, so couldn't determine blocks")
|
||||||
}
|
}
|
||||||
|
requestingAccountI := ctx.Value(ap.ContextRequestingAccount)
|
||||||
blocked, err := f.db.AreURIsBlocked(ctx, actorIRIs)
|
requestingAccount, ok := requestingAccountI.(*gtsmodel.Account)
|
||||||
|
if !ok {
|
||||||
|
l.Errorf("requesting account not set on request context")
|
||||||
|
return false, errors.New("requesting account not set on request context, so couldn't determine blocks")
|
||||||
|
}
|
||||||
|
// the receiver shouldn't block the sender
|
||||||
|
blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, requestingAccount.ID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("error checking domain blocks: %s", err)
|
return false, fmt.Errorf("error checking user-level blocks: %s", err)
|
||||||
}
|
}
|
||||||
if blocked {
|
if blocked {
|
||||||
return blocked, nil
|
return blocked, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, uri := range actorIRIs {
|
// get account IDs for other involved accounts
|
||||||
requestingAccount, err := f.db.GetAccountByURI(ctx, uri.String())
|
var involvedAccountIDs []string
|
||||||
if err != nil {
|
for _, iri := range otherInvolvedIRIs {
|
||||||
if err == db.ErrNoEntries {
|
var involvedAccountID string
|
||||||
// we don't have an entry for this account so it's not blocked
|
if involvedStatus, err := f.db.GetStatusByURI(ctx, iri.String()); err == nil {
|
||||||
// TODO: allow a different default to be set for this behavior
|
involvedAccountID = involvedStatus.AccountID
|
||||||
l.Tracef("no entry for account with URI %s so it can't be blocked", uri)
|
} else if involvedAccount, err := f.db.GetAccountByURI(ctx, iri.String()); err == nil {
|
||||||
continue
|
involvedAccountID = involvedAccount.ID
|
||||||
}
|
|
||||||
return false, fmt.Errorf("error getting account with uri %s: %s", uri.String(), err)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, requestingAccount.ID, false)
|
if involvedAccountID != "" {
|
||||||
|
involvedAccountIDs = append(involvedAccountIDs, involvedAccountID)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
deduped := util.UniqueStrings(involvedAccountIDs)
|
||||||
|
|
||||||
|
for _, involvedAccountID := range deduped {
|
||||||
|
// the involved account shouldn't block whoever is making this request
|
||||||
|
blocked, err = f.db.IsBlocked(ctx, involvedAccountID, requestingAccount.ID, false)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, fmt.Errorf("error checking account block: %s", err)
|
return false, fmt.Errorf("error checking user-level otherInvolvedIRI blocks: %s", err)
|
||||||
}
|
}
|
||||||
if blocked {
|
if blocked {
|
||||||
l.Tracef("local account %s blocks account with uri %s", receivingAccount.Username, uri)
|
return blocked, nil
|
||||||
return true, nil
|
}
|
||||||
|
|
||||||
|
// whoever is receiving this request shouldn't block the involved account
|
||||||
|
blocked, err = f.db.IsBlocked(ctx, receivingAccount.ID, involvedAccountID, false)
|
||||||
|
if err != nil {
|
||||||
|
return false, fmt.Errorf("error checking user-level otherInvolvedIRI blocks: %s", err)
|
||||||
|
}
|
||||||
|
if blocked {
|
||||||
|
return blocked, nil
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -22,11 +22,11 @@ import (
|
||||||
"context"
|
"context"
|
||||||
"net/http"
|
"net/http"
|
||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
|
"net/url"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"github.com/go-fed/httpsig"
|
"github.com/go-fed/httpsig"
|
||||||
"github.com/stretchr/testify/suite"
|
"github.com/stretchr/testify/suite"
|
||||||
"github.com/superseriousbusiness/activity/pub"
|
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
"github.com/superseriousbusiness/gotosocial/internal/ap"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
|
"github.com/superseriousbusiness/gotosocial/internal/concurrency"
|
||||||
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
"github.com/superseriousbusiness/gotosocial/internal/federation"
|
||||||
|
@ -39,8 +39,7 @@ type FederatingProtocolTestSuite struct {
|
||||||
FederatorStandardTestSuite
|
FederatorStandardTestSuite
|
||||||
}
|
}
|
||||||
|
|
||||||
// make sure PostInboxRequestBodyHook properly sets the inbox username and activity on the context
|
func (suite *FederatingProtocolTestSuite) TestPostInboxRequestBodyHook1() {
|
||||||
func (suite *FederatingProtocolTestSuite) TestPostInboxRequestBodyHook() {
|
|
||||||
// the activity we're gonna use
|
// the activity we're gonna use
|
||||||
activity := suite.testActivities["dm_for_zork"]
|
activity := suite.testActivities["dm_for_zork"]
|
||||||
|
|
||||||
|
@ -63,13 +62,82 @@ func (suite *FederatingProtocolTestSuite) TestPostInboxRequestBodyHook() {
|
||||||
suite.NoError(err)
|
suite.NoError(err)
|
||||||
suite.NotNil(newContext)
|
suite.NotNil(newContext)
|
||||||
|
|
||||||
// activity should be set on context now
|
involvedIRIsI := newContext.Value(ap.ContextOtherInvolvedIRIs)
|
||||||
activityI := newContext.Value(ap.ContextActivity)
|
involvedIRIs, ok := involvedIRIsI.([]*url.URL)
|
||||||
suite.NotNil(activityI)
|
if !ok {
|
||||||
returnedActivity, ok := activityI.(pub.Activity)
|
suite.FailNow("couldn't get involved IRIs from context")
|
||||||
suite.True(ok)
|
}
|
||||||
suite.NotNil(returnedActivity)
|
|
||||||
suite.EqualValues(activity.Activity, returnedActivity)
|
suite.Len(involvedIRIs, 1)
|
||||||
|
suite.Contains(involvedIRIs, testrig.URLMustParse("http://localhost:8080/users/the_mighty_zork"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *FederatingProtocolTestSuite) TestPostInboxRequestBodyHook2() {
|
||||||
|
// the activity we're gonna use
|
||||||
|
activity := suite.testActivities["reply_to_turtle_for_zork"]
|
||||||
|
|
||||||
|
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||||
|
|
||||||
|
// setup transport controller with a no-op client so we don't make external calls
|
||||||
|
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
|
return nil, nil
|
||||||
|
}), suite.db, fedWorker)
|
||||||
|
// setup module being tested
|
||||||
|
federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db, fedWorker), tc, suite.tc, testrig.NewTestMediaManager(suite.db, suite.storage))
|
||||||
|
|
||||||
|
// setup request
|
||||||
|
ctx := context.Background()
|
||||||
|
request := httptest.NewRequest(http.MethodPost, "http://localhost:8080/users/the_mighty_zork/inbox", nil) // the endpoint we're hitting
|
||||||
|
request.Header.Set("Signature", activity.SignatureHeader)
|
||||||
|
|
||||||
|
// trigger the function being tested, and return the new context it creates
|
||||||
|
newContext, err := federator.PostInboxRequestBodyHook(ctx, request, activity.Activity)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(newContext)
|
||||||
|
|
||||||
|
involvedIRIsI := newContext.Value(ap.ContextOtherInvolvedIRIs)
|
||||||
|
involvedIRIs, ok := involvedIRIsI.([]*url.URL)
|
||||||
|
if !ok {
|
||||||
|
suite.FailNow("couldn't get involved IRIs from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Len(involvedIRIs, 2)
|
||||||
|
suite.Contains(involvedIRIs, testrig.URLMustParse("http://localhost:8080/users/1happyturtle"))
|
||||||
|
suite.Contains(involvedIRIs, testrig.URLMustParse("http://fossbros-anonymous.io/users/foss_satan/followers"))
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *FederatingProtocolTestSuite) TestPostInboxRequestBodyHook3() {
|
||||||
|
// the activity we're gonna use
|
||||||
|
activity := suite.testActivities["reply_to_turtle_for_turtle"]
|
||||||
|
|
||||||
|
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||||
|
|
||||||
|
// setup transport controller with a no-op client so we don't make external calls
|
||||||
|
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(func(req *http.Request) (*http.Response, error) {
|
||||||
|
return nil, nil
|
||||||
|
}), suite.db, fedWorker)
|
||||||
|
// setup module being tested
|
||||||
|
federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db, fedWorker), tc, suite.tc, testrig.NewTestMediaManager(suite.db, suite.storage))
|
||||||
|
|
||||||
|
// setup request
|
||||||
|
ctx := context.Background()
|
||||||
|
request := httptest.NewRequest(http.MethodPost, "http://localhost:8080/users/1happyturtle/inbox", nil) // the endpoint we're hitting
|
||||||
|
request.Header.Set("Signature", activity.SignatureHeader)
|
||||||
|
|
||||||
|
// trigger the function being tested, and return the new context it creates
|
||||||
|
newContext, err := federator.PostInboxRequestBodyHook(ctx, request, activity.Activity)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.NotNil(newContext)
|
||||||
|
|
||||||
|
involvedIRIsI := newContext.Value(ap.ContextOtherInvolvedIRIs)
|
||||||
|
involvedIRIs, ok := involvedIRIsI.([]*url.URL)
|
||||||
|
if !ok {
|
||||||
|
suite.FailNow("couldn't get involved IRIs from context")
|
||||||
|
}
|
||||||
|
|
||||||
|
suite.Len(involvedIRIs, 2)
|
||||||
|
suite.Contains(involvedIRIs, testrig.URLMustParse("http://localhost:8080/users/1happyturtle"))
|
||||||
|
suite.Contains(involvedIRIs, testrig.URLMustParse("http://fossbros-anonymous.io/users/foss_satan/followers"))
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FederatingProtocolTestSuite) TestAuthenticatePostInbox() {
|
func (suite *FederatingProtocolTestSuite) TestAuthenticatePostInbox() {
|
||||||
|
@ -97,8 +165,7 @@ func (suite *FederatingProtocolTestSuite) TestAuthenticatePostInbox() {
|
||||||
// by the time AuthenticatePostInbox is called, PostInboxRequestBodyHook should have already been called,
|
// by the time AuthenticatePostInbox is called, PostInboxRequestBodyHook should have already been called,
|
||||||
// which should have set the account and username onto the request. We can replicate that behavior here:
|
// which should have set the account and username onto the request. We can replicate that behavior here:
|
||||||
ctxWithAccount := context.WithValue(ctx, ap.ContextReceivingAccount, inboxAccount)
|
ctxWithAccount := context.WithValue(ctx, ap.ContextReceivingAccount, inboxAccount)
|
||||||
ctxWithActivity := context.WithValue(ctxWithAccount, ap.ContextActivity, activity)
|
ctxWithVerifier := context.WithValue(ctxWithAccount, ap.ContextRequestingPublicKeyVerifier, verifier)
|
||||||
ctxWithVerifier := context.WithValue(ctxWithActivity, ap.ContextRequestingPublicKeyVerifier, verifier)
|
|
||||||
ctxWithSignature := context.WithValue(ctxWithVerifier, ap.ContextRequestingPublicKeySignature, activity.SignatureHeader)
|
ctxWithSignature := context.WithValue(ctxWithVerifier, ap.ContextRequestingPublicKeySignature, activity.SignatureHeader)
|
||||||
|
|
||||||
// we can pass this recorder as a writer and read it back after
|
// we can pass this recorder as a writer and read it back after
|
||||||
|
@ -117,6 +184,125 @@ func (suite *FederatingProtocolTestSuite) TestAuthenticatePostInbox() {
|
||||||
suite.Equal(sendingAccount.Username, requestingAccount.Username)
|
suite.Equal(sendingAccount.Username, requestingAccount.Username)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (suite *FederatingProtocolTestSuite) TestBlocked1() {
|
||||||
|
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||||
|
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker)
|
||||||
|
federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db, fedWorker), tc, suite.tc, testrig.NewTestMediaManager(suite.db, suite.storage))
|
||||||
|
|
||||||
|
sendingAccount := suite.testAccounts["remote_account_1"]
|
||||||
|
inboxAccount := suite.testAccounts["local_account_1"]
|
||||||
|
otherInvolvedIRIs := []*url.URL{}
|
||||||
|
actorIRIs := []*url.URL{
|
||||||
|
testrig.URLMustParse(sendingAccount.URI),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
ctxWithReceivingAccount := context.WithValue(ctx, ap.ContextReceivingAccount, inboxAccount)
|
||||||
|
ctxWithRequestingAccount := context.WithValue(ctxWithReceivingAccount, ap.ContextRequestingAccount, sendingAccount)
|
||||||
|
ctxWithOtherInvolvedIRIs := context.WithValue(ctxWithRequestingAccount, ap.ContextOtherInvolvedIRIs, otherInvolvedIRIs)
|
||||||
|
|
||||||
|
blocked, err := federator.Blocked(ctxWithOtherInvolvedIRIs, actorIRIs)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.False(blocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *FederatingProtocolTestSuite) TestBlocked2() {
|
||||||
|
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||||
|
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker)
|
||||||
|
federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db, fedWorker), tc, suite.tc, testrig.NewTestMediaManager(suite.db, suite.storage))
|
||||||
|
|
||||||
|
sendingAccount := suite.testAccounts["remote_account_1"]
|
||||||
|
inboxAccount := suite.testAccounts["local_account_1"]
|
||||||
|
otherInvolvedIRIs := []*url.URL{}
|
||||||
|
actorIRIs := []*url.URL{
|
||||||
|
testrig.URLMustParse(sendingAccount.URI),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
ctxWithReceivingAccount := context.WithValue(ctx, ap.ContextReceivingAccount, inboxAccount)
|
||||||
|
ctxWithRequestingAccount := context.WithValue(ctxWithReceivingAccount, ap.ContextRequestingAccount, sendingAccount)
|
||||||
|
ctxWithOtherInvolvedIRIs := context.WithValue(ctxWithRequestingAccount, ap.ContextOtherInvolvedIRIs, otherInvolvedIRIs)
|
||||||
|
|
||||||
|
// insert a block from inboxAccount targeting sendingAccount
|
||||||
|
if err := suite.db.Put(context.Background(), >smodel.Block{
|
||||||
|
ID: "01G3KBEMJD4VQ2D615MPV7KTRD",
|
||||||
|
URI: "whatever",
|
||||||
|
AccountID: inboxAccount.ID,
|
||||||
|
TargetAccountID: sendingAccount.ID,
|
||||||
|
}); err != nil {
|
||||||
|
suite.Fail(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
// request should be blocked now
|
||||||
|
blocked, err := federator.Blocked(ctxWithOtherInvolvedIRIs, actorIRIs)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.True(blocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *FederatingProtocolTestSuite) TestBlocked3() {
|
||||||
|
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||||
|
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker)
|
||||||
|
federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db, fedWorker), tc, suite.tc, testrig.NewTestMediaManager(suite.db, suite.storage))
|
||||||
|
|
||||||
|
sendingAccount := suite.testAccounts["remote_account_1"]
|
||||||
|
inboxAccount := suite.testAccounts["local_account_1"]
|
||||||
|
ccedAccount := suite.testAccounts["remote_account_2"]
|
||||||
|
|
||||||
|
otherInvolvedIRIs := []*url.URL{
|
||||||
|
testrig.URLMustParse(ccedAccount.URI),
|
||||||
|
}
|
||||||
|
actorIRIs := []*url.URL{
|
||||||
|
testrig.URLMustParse(sendingAccount.URI),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
ctxWithReceivingAccount := context.WithValue(ctx, ap.ContextReceivingAccount, inboxAccount)
|
||||||
|
ctxWithRequestingAccount := context.WithValue(ctxWithReceivingAccount, ap.ContextRequestingAccount, sendingAccount)
|
||||||
|
ctxWithOtherInvolvedIRIs := context.WithValue(ctxWithRequestingAccount, ap.ContextOtherInvolvedIRIs, otherInvolvedIRIs)
|
||||||
|
|
||||||
|
// insert a block from inboxAccount targeting CCed account
|
||||||
|
if err := suite.db.Put(context.Background(), >smodel.Block{
|
||||||
|
ID: "01G3KBEMJD4VQ2D615MPV7KTRD",
|
||||||
|
URI: "whatever",
|
||||||
|
AccountID: inboxAccount.ID,
|
||||||
|
TargetAccountID: ccedAccount.ID,
|
||||||
|
}); err != nil {
|
||||||
|
suite.Fail(err.Error())
|
||||||
|
}
|
||||||
|
|
||||||
|
blocked, err := federator.Blocked(ctxWithOtherInvolvedIRIs, actorIRIs)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.True(blocked)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (suite *FederatingProtocolTestSuite) TestBlocked4() {
|
||||||
|
fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1)
|
||||||
|
tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil), suite.db, fedWorker)
|
||||||
|
federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db, fedWorker), tc, suite.tc, testrig.NewTestMediaManager(suite.db, suite.storage))
|
||||||
|
|
||||||
|
sendingAccount := suite.testAccounts["remote_account_1"]
|
||||||
|
inboxAccount := suite.testAccounts["local_account_1"]
|
||||||
|
repliedStatus := suite.testStatuses["local_account_2_status_1"]
|
||||||
|
|
||||||
|
otherInvolvedIRIs := []*url.URL{
|
||||||
|
testrig.URLMustParse(repliedStatus.URI), // this status is involved because the hypothetical activity is a reply to this status
|
||||||
|
}
|
||||||
|
actorIRIs := []*url.URL{
|
||||||
|
testrig.URLMustParse(sendingAccount.URI),
|
||||||
|
}
|
||||||
|
|
||||||
|
ctx := context.Background()
|
||||||
|
ctxWithReceivingAccount := context.WithValue(ctx, ap.ContextReceivingAccount, inboxAccount)
|
||||||
|
ctxWithRequestingAccount := context.WithValue(ctxWithReceivingAccount, ap.ContextRequestingAccount, sendingAccount)
|
||||||
|
ctxWithOtherInvolvedIRIs := context.WithValue(ctxWithRequestingAccount, ap.ContextOtherInvolvedIRIs, otherInvolvedIRIs)
|
||||||
|
|
||||||
|
// local account 2 (replied status account) blocks sending account already so we don't need to add a block here
|
||||||
|
|
||||||
|
blocked, err := federator.Blocked(ctxWithOtherInvolvedIRIs, actorIRIs)
|
||||||
|
suite.NoError(err)
|
||||||
|
suite.True(blocked)
|
||||||
|
}
|
||||||
|
|
||||||
func TestFederatingProtocolTestSuite(t *testing.T) {
|
func TestFederatingProtocolTestSuite(t *testing.T) {
|
||||||
suite.Run(t, new(FederatingProtocolTestSuite))
|
suite.Run(t, new(FederatingProtocolTestSuite))
|
||||||
}
|
}
|
||||||
|
|
|
@ -34,6 +34,7 @@ type FederatorStandardTestSuite struct {
|
||||||
storage *kv.KVStore
|
storage *kv.KVStore
|
||||||
tc typeutils.TypeConverter
|
tc typeutils.TypeConverter
|
||||||
testAccounts map[string]*gtsmodel.Account
|
testAccounts map[string]*gtsmodel.Account
|
||||||
|
testStatuses map[string]*gtsmodel.Status
|
||||||
testActivities map[string]testrig.ActivityWithSignature
|
testActivities map[string]testrig.ActivityWithSignature
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -43,6 +44,7 @@ func (suite *FederatorStandardTestSuite) SetupSuite() {
|
||||||
suite.storage = testrig.NewTestStorage()
|
suite.storage = testrig.NewTestStorage()
|
||||||
suite.tc = testrig.NewTestTypeConverter(suite.db)
|
suite.tc = testrig.NewTestTypeConverter(suite.db)
|
||||||
suite.testAccounts = testrig.NewTestAccounts()
|
suite.testAccounts = testrig.NewTestAccounts()
|
||||||
|
suite.testStatuses = testrig.NewTestStatuses()
|
||||||
}
|
}
|
||||||
|
|
||||||
func (suite *FederatorStandardTestSuite) SetupTest() {
|
func (suite *FederatorStandardTestSuite) SetupTest() {
|
||||||
|
|
|
@ -393,9 +393,9 @@ func (c *converter) StatusToAS(ctx context.Context, s *gtsmodel.Status) (vocab.A
|
||||||
if s.InReplyToID != "" {
|
if s.InReplyToID != "" {
|
||||||
// fetch the replied status if we don't have it on hand already
|
// fetch the replied status if we don't have it on hand already
|
||||||
if s.InReplyTo == nil {
|
if s.InReplyTo == nil {
|
||||||
rs := >smodel.Status{}
|
rs, err := c.db.GetStatusByID(ctx, s.InReplyToID)
|
||||||
if err := c.db.GetByID(ctx, s.InReplyToID, rs); err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("StatusToAS: error retrieving replied-to status from db: %s", err)
|
return nil, fmt.Errorf("StatusToAS: error getting replied to status %s: %s", s.InReplyToID, err)
|
||||||
}
|
}
|
||||||
s.InReplyTo = rs
|
s.InReplyTo = rs
|
||||||
}
|
}
|
||||||
|
|
|
@ -18,6 +18,8 @@
|
||||||
|
|
||||||
package util
|
package util
|
||||||
|
|
||||||
|
import "net/url"
|
||||||
|
|
||||||
// UniqueStrings returns a deduplicated version of a given string slice.
|
// UniqueStrings returns a deduplicated version of a given string slice.
|
||||||
func UniqueStrings(s []string) []string {
|
func UniqueStrings(s []string) []string {
|
||||||
keys := make(map[string]bool, len(s))
|
keys := make(map[string]bool, len(s))
|
||||||
|
@ -30,3 +32,16 @@ func UniqueStrings(s []string) []string {
|
||||||
}
|
}
|
||||||
return list
|
return list
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// UniqueURIs returns a deduplicated version of a given *url.URL slice.
|
||||||
|
func UniqueURIs(s []*url.URL) []*url.URL {
|
||||||
|
keys := make(map[string]bool, len(s))
|
||||||
|
list := []*url.URL{}
|
||||||
|
for _, entry := range s {
|
||||||
|
if _, value := keys[entry.String()]; !value {
|
||||||
|
keys[entry.String()] = true
|
||||||
|
list = append(list, entry)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return list
|
||||||
|
}
|
||||||
|
|
|
@ -1601,6 +1601,30 @@ func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]Activit
|
||||||
dmForZork)
|
dmForZork)
|
||||||
createDmForZorkSig, createDmForZorkDigest, creatDmForZorkDate := GetSignatureForActivity(createDmForZork, accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_1"].InboxURI))
|
createDmForZorkSig, createDmForZorkDigest, creatDmForZorkDate := GetSignatureForActivity(createDmForZork, accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_1"].InboxURI))
|
||||||
|
|
||||||
|
replyToTurtle := NewAPNote(
|
||||||
|
URLMustParse("http://fossbros-anonymous.io/users/foss_satan/statuses/2f1195a6-5cb0-4475-adf5-92ab9a0147fe"),
|
||||||
|
URLMustParse("http://fossbros-anonymous.io/@foss_satan/2f1195a6-5cb0-4475-adf5-92ab9a0147fe"),
|
||||||
|
time.Now(),
|
||||||
|
"@1happyturtle@localhost:8080 u suck lol",
|
||||||
|
"",
|
||||||
|
URLMustParse("http://fossbros-anonymous.io/users/foss_satan"),
|
||||||
|
[]*url.URL{URLMustParse("http://fossbros-anonymous.io/users/foss_satan/followers")},
|
||||||
|
[]*url.URL{URLMustParse("http://localhost:8080/users/1happyturtle")},
|
||||||
|
false,
|
||||||
|
[]vocab.ActivityStreamsMention{newAPMention(
|
||||||
|
URLMustParse("http://localhost:8080/users/1happyturtle"),
|
||||||
|
"@1happyturtle@localhost:8080",
|
||||||
|
)},
|
||||||
|
nil,
|
||||||
|
)
|
||||||
|
createReplyToTurtle := WrapAPNoteInCreate(
|
||||||
|
URLMustParse("http://fossbros-anonymous.io/users/foss_satan/statuses/2f1195a6-5cb0-4475-adf5-92ab9a0147fe"),
|
||||||
|
URLMustParse("http://fossbros-anonymous.io/users/foss_satan"),
|
||||||
|
time.Now(),
|
||||||
|
replyToTurtle)
|
||||||
|
createReplyToTurtleForZorkSig, createReplyToTurtleForZorkDigest, createReplyToTurtleForZorkDate := GetSignatureForActivity(createReplyToTurtle, accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_1"].InboxURI))
|
||||||
|
createReplyToTurtleForTurtleSig, createReplyToTurtleForTurtleDigest, createReplyToTurtleForTurtleDate := GetSignatureForActivity(createReplyToTurtle, accounts["remote_account_1"].PublicKeyURI, accounts["remote_account_1"].PrivateKey, URLMustParse(accounts["local_account_2"].InboxURI))
|
||||||
|
|
||||||
forwardedMessage := NewAPNote(
|
forwardedMessage := NewAPNote(
|
||||||
URLMustParse("http://example.org/users/some_user/statuses/afaba698-5740-4e32-a702-af61aa543bc1"),
|
URLMustParse("http://example.org/users/some_user/statuses/afaba698-5740-4e32-a702-af61aa543bc1"),
|
||||||
URLMustParse("http://example.org/@some_user/afaba698-5740-4e32-a702-af61aa543bc1"),
|
URLMustParse("http://example.org/@some_user/afaba698-5740-4e32-a702-af61aa543bc1"),
|
||||||
|
@ -1628,6 +1652,18 @@ func NewTestActivities(accounts map[string]*gtsmodel.Account) map[string]Activit
|
||||||
DigestHeader: createDmForZorkDigest,
|
DigestHeader: createDmForZorkDigest,
|
||||||
DateHeader: creatDmForZorkDate,
|
DateHeader: creatDmForZorkDate,
|
||||||
},
|
},
|
||||||
|
"reply_to_turtle_for_zork": {
|
||||||
|
Activity: createReplyToTurtle,
|
||||||
|
SignatureHeader: createReplyToTurtleForZorkSig,
|
||||||
|
DigestHeader: createReplyToTurtleForZorkDigest,
|
||||||
|
DateHeader: createReplyToTurtleForZorkDate,
|
||||||
|
},
|
||||||
|
"reply_to_turtle_for_turtle": {
|
||||||
|
Activity: createReplyToTurtle,
|
||||||
|
SignatureHeader: createReplyToTurtleForTurtleSig,
|
||||||
|
DigestHeader: createReplyToTurtleForTurtleDigest,
|
||||||
|
DateHeader: createReplyToTurtleForTurtleDate,
|
||||||
|
},
|
||||||
"forwarded_message": {
|
"forwarded_message": {
|
||||||
Activity: createForwardedMessage,
|
Activity: createForwardedMessage,
|
||||||
SignatureHeader: createForwardedMessageSig,
|
SignatureHeader: createForwardedMessageSig,
|
||||||
|
|
Loading…
Reference in New Issue