summaryrefslogtreecommitdiff
path: root/internal/federation
diff options
context:
space:
mode:
authorLibravatar Tobi Smethurst <31960611+tsmethurst@users.noreply.github.com>2021-05-21 15:48:26 +0200
committerLibravatar GitHub <noreply@github.com>2021-05-21 15:48:26 +0200
commitd839f27c306eedebdc7cc0311f35b8856cc2bb24 (patch)
tree7a11a3a641f902991d26771c4d3f8e836a2bce7e /internal/federation
parentupdate progress (diff)
downloadgotosocial-d839f27c306eedebdc7cc0311f35b8856cc2bb24.tar.xz
Follows and relationships (#27)
* Follows -- create and undo, both remote and local * Statuses -- federate new posts, including media, attachments, CWs and image descriptions.
Diffstat (limited to 'internal/federation')
-rw-r--r--internal/federation/federating_db.go327
-rw-r--r--internal/federation/federatingprotocol.go45
-rw-r--r--internal/federation/federator.go12
-rw-r--r--internal/federation/federator_test.go4
-rw-r--r--internal/federation/util.go118
5 files changed, 436 insertions, 70 deletions
diff --git a/internal/federation/federating_db.go b/internal/federation/federating_db.go
index f72c5e636..8f203e132 100644
--- a/internal/federation/federating_db.go
+++ b/internal/federation/federating_db.go
@@ -20,6 +20,7 @@ package federation
import (
"context"
+ "encoding/json"
"errors"
"fmt"
"net/url"
@@ -37,6 +38,12 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util"
)
+type FederatingDB interface {
+ pub.Database
+ Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error
+ Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error
+}
+
// FederatingDB uses the underlying DB interface to implement the go-fed pub.Database interface.
// It doesn't care what the underlying implementation of the DB interface is, as long as it works.
type federatingDB struct {
@@ -47,8 +54,8 @@ type federatingDB struct {
typeConverter typeutils.TypeConverter
}
-// NewFederatingDB returns a pub.Database interface using the given database, config, and logger.
-func NewFederatingDB(db db.DB, config *config.Config, log *logrus.Logger) pub.Database {
+// NewFederatingDB returns a FederatingDB interface using the given database, config, and logger.
+func NewFederatingDB(db db.DB, config *config.Config, log *logrus.Logger) FederatingDB {
return &federatingDB{
locks: new(sync.Map),
db: db,
@@ -204,7 +211,7 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) {
if err != nil {
return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err)
}
- if err := f.db.GetWhere("uri", uid, &gtsmodel.Status{}); err != nil {
+ if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uid}}, &gtsmodel.Status{}); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
// there are no entries for this status
return false, nil
@@ -253,7 +260,7 @@ func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (ac
return nil, fmt.Errorf("%s is not an outbox URI", outboxIRI.String())
}
acct := &gtsmodel.Account{}
- if err := f.db.GetWhere("outbox_uri", outboxIRI.String(), acct); err != nil {
+ if err := f.db.GetWhere([]db.Where{{Key: "outbox_uri", Value: outboxIRI.String()}}, acct); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return nil, fmt.Errorf("no actor found that corresponds to outbox %s", outboxIRI.String())
}
@@ -278,7 +285,7 @@ func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (acto
return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String())
}
acct := &gtsmodel.Account{}
- if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil {
+ if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String())
}
@@ -304,7 +311,7 @@ func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (out
return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String())
}
acct := &gtsmodel.Account{}
- if err := f.db.GetWhere("inbox_uri", inboxIRI.String(), acct); err != nil {
+ if err := f.db.GetWhere([]db.Where{{Key: "inbox_uri", Value: inboxIRI.String()}}, acct); err != nil {
if _, ok := err.(db.ErrNoEntries); ok {
return nil, fmt.Errorf("no actor found that corresponds to inbox %s", inboxIRI.String())
}
@@ -343,9 +350,10 @@ func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, er
if util.IsUserPath(id) {
acct := &gtsmodel.Account{}
- if err := f.db.GetWhere("uri", id.String(), acct); err != nil {
+ if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: id.String()}}, acct); err != nil {
return nil, err
}
+ l.Debug("is user path! returning account")
return f.typeConverter.AccountToAS(acct)
}
@@ -371,27 +379,40 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
"asType": asType.GetTypeName(),
},
)
- l.Debugf("received CREATE asType %+v", asType)
+ m, err := streams.Serialize(asType)
+ if err != nil {
+ return err
+ }
+ b, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+
+ l.Debugf("received CREATE asType %s", string(b))
targetAcctI := ctx.Value(util.APAccount)
if targetAcctI == nil {
l.Error("target account wasn't set on context")
+ return nil
}
targetAcct, ok := targetAcctI.(*gtsmodel.Account)
if !ok {
l.Error("target account was set on context but couldn't be parsed")
+ return nil
}
fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
if fromFederatorChanI == nil {
l.Error("from federator channel wasn't set on context")
+ return nil
}
fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator)
if !ok {
l.Error("from federator channel was set on context but couldn't be parsed")
+ return nil
}
- switch gtsmodel.ActivityStreamsActivity(asType.GetTypeName()) {
+ switch asType.GetTypeName() {
case gtsmodel.ActivityStreamsCreate:
create, ok := asType.(vocab.ActivityStreamsCreate)
if !ok {
@@ -399,7 +420,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
}
object := create.GetActivityStreamsObject()
for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() {
- switch gtsmodel.ActivityStreamsObject(objectIter.GetType().GetTypeName()) {
+ switch objectIter.GetType().GetTypeName() {
case gtsmodel.ActivityStreamsNote:
note := objectIter.GetActivityStreamsNote()
status, err := f.typeConverter.ASStatusToStatus(note)
@@ -407,13 +428,17 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
return fmt.Errorf("error converting note to status: %s", err)
}
if err := f.db.Put(status); err != nil {
+ if _, ok := err.(db.ErrAlreadyExists); ok {
+ return nil
+ }
return fmt.Errorf("database error inserting status: %s", err)
}
fromFederatorChan <- gtsmodel.FromFederator{
- APObjectType: gtsmodel.ActivityStreamsNote,
- APActivityType: gtsmodel.ActivityStreamsCreate,
- GTSModel: status,
+ APObjectType: gtsmodel.ActivityStreamsNote,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: status,
+ ReceivingAccount: targetAcct,
}
}
}
@@ -433,7 +458,7 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
}
if !targetAcct.Locked {
- if err := f.db.AcceptFollowRequest(followRequest.AccountID, followRequest.TargetAccountID); err != nil {
+ if _, err := f.db.AcceptFollowRequest(followRequest.AccountID, followRequest.TargetAccountID); err != nil {
return fmt.Errorf("database error accepting follow request: %s", err)
}
}
@@ -450,14 +475,87 @@ func (f *federatingDB) Create(ctx context.Context, asType vocab.Type) error {
// the entire value.
//
// The library makes this call only after acquiring a lock first.
-func (f *federatingDB) Update(c context.Context, asType vocab.Type) error {
+func (f *federatingDB) Update(ctx context.Context, asType vocab.Type) error {
l := f.log.WithFields(
logrus.Fields{
"func": "Update",
"asType": asType.GetTypeName(),
},
)
- l.Debugf("received UPDATE asType %+v", asType)
+ m, err := streams.Serialize(asType)
+ if err != nil {
+ return err
+ }
+ b, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+
+ l.Debugf("received UPDATE asType %s", string(b))
+
+ receivingAcctI := ctx.Value(util.APAccount)
+ if receivingAcctI == nil {
+ l.Error("receiving account wasn't set on context")
+ }
+ receivingAcct, ok := receivingAcctI.(*gtsmodel.Account)
+ if !ok {
+ l.Error("receiving account was set on context but couldn't be parsed")
+ }
+
+ fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
+ if fromFederatorChanI == nil {
+ l.Error("from federator channel wasn't set on context")
+ }
+ fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator)
+ if !ok {
+ l.Error("from federator channel was set on context but couldn't be parsed")
+ }
+
+ switch asType.GetTypeName() {
+ case gtsmodel.ActivityStreamsUpdate:
+ update, ok := asType.(vocab.ActivityStreamsCreate)
+ if !ok {
+ return errors.New("could not convert type to create")
+ }
+ object := update.GetActivityStreamsObject()
+ for objectIter := object.Begin(); objectIter != object.End(); objectIter = objectIter.Next() {
+ switch objectIter.GetType().GetTypeName() {
+ case string(gtsmodel.ActivityStreamsPerson):
+ person := objectIter.GetActivityStreamsPerson()
+ updatedAcct, err := f.typeConverter.ASRepresentationToAccount(person)
+ if err != nil {
+ return fmt.Errorf("error converting person to account: %s", err)
+ }
+ if err := f.db.Put(updatedAcct); err != nil {
+ return fmt.Errorf("database error inserting updated account: %s", err)
+ }
+
+ fromFederatorChan <- gtsmodel.FromFederator{
+ APObjectType: gtsmodel.ActivityStreamsProfile,
+ APActivityType: gtsmodel.ActivityStreamsUpdate,
+ GTSModel: updatedAcct,
+ ReceivingAccount: receivingAcct,
+ }
+
+ case string(gtsmodel.ActivityStreamsApplication):
+ application := objectIter.GetActivityStreamsApplication()
+ updatedAcct, err := f.typeConverter.ASRepresentationToAccount(application)
+ if err != nil {
+ return fmt.Errorf("error converting person to account: %s", err)
+ }
+ if err := f.db.Put(updatedAcct); err != nil {
+ return fmt.Errorf("database error inserting updated account: %s", err)
+ }
+
+ fromFederatorChan <- gtsmodel.FromFederator{
+ APObjectType: gtsmodel.ActivityStreamsProfile,
+ APActivityType: gtsmodel.ActivityStreamsUpdate,
+ GTSModel: updatedAcct,
+ ReceivingAccount: receivingAcct,
+ }
+ }
+ }
+ }
return nil
}
@@ -490,7 +588,7 @@ func (f *federatingDB) GetOutbox(c context.Context, outboxIRI *url.URL) (inbox v
)
l.Debug("entering GETOUTBOX function")
- return nil, nil
+ return streams.NewActivityStreamsOrderedCollectionPage(), nil
}
// SetOutbox saves the outbox value given from GetOutbox, with new items
@@ -522,9 +620,60 @@ func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err
"asType": t.GetTypeName(),
},
)
- l.Debugf("received NEWID request for asType %+v", t)
+ m, err := streams.Serialize(t)
+ if err != nil {
+ return nil, err
+ }
+ b, err := json.Marshal(m)
+ if err != nil {
+ return nil, err
+ }
+ l.Debugf("received NEWID request for asType %s", string(b))
- return url.Parse(fmt.Sprintf("%s://%s/", f.config.Protocol, uuid.NewString()))
+ switch t.GetTypeName() {
+ case gtsmodel.ActivityStreamsFollow:
+ // FOLLOW
+ // ID might already be set on a follow we've created, so check it here and return it if it is
+ follow, ok := t.(vocab.ActivityStreamsFollow)
+ if !ok {
+ return nil, errors.New("newid: follow couldn't be parsed into vocab.ActivityStreamsFollow")
+ }
+ idProp := follow.GetJSONLDId()
+ if idProp != nil {
+ if idProp.IsIRI() {
+ return idProp.GetIRI(), nil
+ }
+ }
+ // it's not set so create one based on the actor set on the follow (ie., the followER not the followEE)
+ actorProp := follow.GetActivityStreamsActor()
+ if actorProp != nil {
+ for iter := actorProp.Begin(); iter != actorProp.End(); iter = iter.Next() {
+ // take the IRI of the first actor we can find (there should only be one)
+ if iter.IsIRI() {
+ actorAccount := &gtsmodel.Account{}
+ if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: iter.GetIRI().String()}}, actorAccount); err == nil { // if there's an error here, just use the fallback behavior -- we don't need to return an error here
+ return url.Parse(util.GenerateURIForFollow(actorAccount.Username, f.config.Protocol, f.config.Host))
+ }
+ }
+ }
+ }
+ case gtsmodel.ActivityStreamsNote:
+ // NOTE aka STATUS
+ // ID might already be set on a note we've created, so check it here and return it if it is
+ note, ok := t.(vocab.ActivityStreamsNote)
+ if !ok {
+ return nil, errors.New("newid: follow couldn't be parsed into vocab.ActivityStreamsNote")
+ }
+ idProp := note.GetJSONLDId()
+ if idProp != nil {
+ if idProp.IsIRI() {
+ return idProp.GetIRI(), nil
+ }
+ }
+ }
+
+ // fallback default behavior: just return a random UUID after our protocol and host
+ return url.Parse(fmt.Sprintf("%s://%s/%s", f.config.Protocol, f.config.Host, uuid.NewString()))
}
// Followers obtains the Followers Collection for an actor with the
@@ -543,7 +692,7 @@ func (f *federatingDB) Followers(c context.Context, actorIRI *url.URL) (follower
l.Debugf("entering FOLLOWERS function with actorIRI %s", actorIRI.String())
acct := &gtsmodel.Account{}
- if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil {
+ if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil {
return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err)
}
@@ -585,7 +734,7 @@ func (f *federatingDB) Following(c context.Context, actorIRI *url.URL) (followin
l.Debugf("entering FOLLOWING function with actorIRI %s", actorIRI.String())
acct := &gtsmodel.Account{}
- if err := f.db.GetWhere("uri", actorIRI.String(), acct); err != nil {
+ if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: actorIRI.String()}}, acct); err != nil {
return nil, fmt.Errorf("db error getting account with uri %s: %s", actorIRI.String(), err)
}
@@ -627,3 +776,139 @@ func (f *federatingDB) Liked(c context.Context, actorIRI *url.URL) (liked vocab.
l.Debugf("entering LIKED function with actorIRI %s", actorIRI.String())
return nil, nil
}
+
+/*
+ CUSTOM FUNCTIONALITY FOR GTS
+*/
+
+func (f *federatingDB) Undo(ctx context.Context, undo vocab.ActivityStreamsUndo) error {
+ l := f.log.WithFields(
+ logrus.Fields{
+ "func": "Undo",
+ "asType": undo.GetTypeName(),
+ },
+ )
+ m, err := streams.Serialize(undo)
+ if err != nil {
+ return err
+ }
+ b, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+ l.Debugf("received UNDO asType %s", string(b))
+
+ targetAcctI := ctx.Value(util.APAccount)
+ if targetAcctI == nil {
+ l.Error("UNDO: target account wasn't set on context")
+ return nil
+ }
+ targetAcct, ok := targetAcctI.(*gtsmodel.Account)
+ if !ok {
+ l.Error("UNDO: target account was set on context but couldn't be parsed")
+ return nil
+ }
+
+ undoObject := undo.GetActivityStreamsObject()
+ if undoObject == nil {
+ return errors.New("UNDO: no object set on vocab.ActivityStreamsUndo")
+ }
+
+ for iter := undoObject.Begin(); iter != undoObject.End(); iter = iter.Next() {
+ switch iter.GetType().GetTypeName() {
+ case string(gtsmodel.ActivityStreamsFollow):
+ // UNDO FOLLOW
+ ASFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
+ if !ok {
+ return errors.New("UNDO: couldn't parse follow into vocab.ActivityStreamsFollow")
+ }
+ // make sure the actor owns the follow
+ if !sameActor(undo.GetActivityStreamsActor(), ASFollow.GetActivityStreamsActor()) {
+ return errors.New("UNDO: follow actor and activity actor not the same")
+ }
+ // convert the follow to something we can understand
+ gtsFollow, err := f.typeConverter.ASFollowToFollow(ASFollow)
+ if err != nil {
+ return fmt.Errorf("UNDO: error converting asfollow to gtsfollow: %s", err)
+ }
+ // make sure the addressee of the original follow is the same as whatever inbox this landed in
+ if gtsFollow.TargetAccountID != targetAcct.ID {
+ return errors.New("UNDO: follow object account and inbox account were not the same")
+ }
+ // delete any existing FOLLOW
+ if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsFollow.URI}}, &gtsmodel.Follow{}); err != nil {
+ return fmt.Errorf("UNDO: db error removing follow: %s", err)
+ }
+ // delete any existing FOLLOW REQUEST
+ if err := f.db.DeleteWhere([]db.Where{{Key: "uri", Value: gtsFollow.URI}}, &gtsmodel.FollowRequest{}); err != nil {
+ return fmt.Errorf("UNDO: db error removing follow request: %s", err)
+ }
+ l.Debug("follow undone")
+ return nil
+ case string(gtsmodel.ActivityStreamsLike):
+ // UNDO LIKE
+ case string(gtsmodel.ActivityStreamsAnnounce):
+ // UNDO BOOST/REBLOG/ANNOUNCE
+ }
+ }
+
+ return nil
+}
+
+func (f *federatingDB) Accept(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
+ l := f.log.WithFields(
+ logrus.Fields{
+ "func": "Accept",
+ "asType": accept.GetTypeName(),
+ },
+ )
+ m, err := streams.Serialize(accept)
+ if err != nil {
+ return err
+ }
+ b, err := json.Marshal(m)
+ if err != nil {
+ return err
+ }
+ l.Debugf("received ACCEPT asType %s", string(b))
+
+ inboxAcctI := ctx.Value(util.APAccount)
+ if inboxAcctI == nil {
+ l.Error("ACCEPT: inbox account wasn't set on context")
+ return nil
+ }
+ inboxAcct, ok := inboxAcctI.(*gtsmodel.Account)
+ if !ok {
+ l.Error("ACCEPT: inbox account was set on context but couldn't be parsed")
+ return nil
+ }
+
+ acceptObject := accept.GetActivityStreamsObject()
+ if acceptObject == nil {
+ return errors.New("ACCEPT: no object set on vocab.ActivityStreamsUndo")
+ }
+
+ for iter := acceptObject.Begin(); iter != acceptObject.End(); iter = iter.Next() {
+ switch iter.GetType().GetTypeName() {
+ case string(gtsmodel.ActivityStreamsFollow):
+ // ACCEPT FOLLOW
+ asFollow, ok := iter.GetType().(vocab.ActivityStreamsFollow)
+ if !ok {
+ return errors.New("ACCEPT: couldn't parse follow into vocab.ActivityStreamsFollow")
+ }
+ // convert the follow to something we can understand
+ gtsFollow, err := f.typeConverter.ASFollowToFollow(asFollow)
+ if err != nil {
+ return fmt.Errorf("ACCEPT: error converting asfollow to gtsfollow: %s", err)
+ }
+ // make sure the addressee of the original follow is the same as whatever inbox this landed in
+ if gtsFollow.AccountID != inboxAcct.ID {
+ return errors.New("ACCEPT: follow object account and inbox account were not the same")
+ }
+ _, err = f.db.AcceptFollowRequest(gtsFollow.AccountID, gtsFollow.TargetAccountID)
+ return err
+ }
+ }
+
+ return nil
+}
diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go
index d8f6eb839..61fecb11a 100644
--- a/internal/federation/federatingprotocol.go
+++ b/internal/federation/federatingprotocol.go
@@ -124,7 +124,7 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
}
requestingAccount := &gtsmodel.Account{}
- if err := f.db.GetWhere("uri", publicKeyOwnerURI.String(), requestingAccount); err != nil {
+ if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: publicKeyOwnerURI.String()}}, requestingAccount); err != nil {
// there's been a proper error so return it
if _, ok := err.(db.ErrNoEntries); !ok {
return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", publicKeyOwnerURI.String(), err)
@@ -146,6 +146,22 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr
}
requestingAccount = a
+
+ // send the newly dereferenced account into the processor channel for further async processing
+ fromFederatorChanI := ctx.Value(util.APFromFederatorChanKey)
+ if fromFederatorChanI == nil {
+ l.Error("from federator channel wasn't set on context")
+ }
+ fromFederatorChan, ok := fromFederatorChanI.(chan gtsmodel.FromFederator)
+ if !ok {
+ l.Error("from federator channel was set on context but couldn't be parsed")
+ }
+
+ fromFederatorChan <- gtsmodel.FromFederator{
+ APObjectType: gtsmodel.ActivityStreamsProfile,
+ APActivityType: gtsmodel.ActivityStreamsCreate,
+ GTSModel: requestingAccount,
+ }
}
withRequester := context.WithValue(ctx, util.APRequestingAccount, requestingAccount)
@@ -184,7 +200,7 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er
for _, uri := range actorIRIs {
a := &gtsmodel.Account{}
- if err := f.db.GetWhere("uri", uri.String(), a); err != nil {
+ if err := f.db.GetWhere([]db.Where{{Key: "uri", Value: uri.String()}}, a); err != nil {
_, ok := err.(db.ErrNoEntries)
if ok {
// we don't have an entry for this account so it's not blocked
@@ -228,17 +244,19 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa
"func": "FederatingCallbacks",
})
- targetAcctI := ctx.Value(util.APAccount)
- if targetAcctI == nil {
- l.Error("target account wasn't set on context")
+ receivingAcctI := ctx.Value(util.APAccount)
+ if receivingAcctI == nil {
+ l.Error("receiving account wasn't set on context")
+ return
}
- targetAcct, ok := targetAcctI.(*gtsmodel.Account)
+ receivingAcct, ok := receivingAcctI.(*gtsmodel.Account)
if !ok {
- l.Error("target account was set on context but couldn't be parsed")
+ l.Error("receiving account was set on context but couldn't be parsed")
+ return
}
var onFollow pub.OnFollowBehavior = pub.OnFollowAutomaticallyAccept
- if targetAcct.Locked {
+ if receivingAcct.Locked {
onFollow = pub.OnFollowDoNothing
}
@@ -248,6 +266,17 @@ func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.Federa
OnFollow: onFollow,
}
+ other = []interface{}{
+ // override default undo behavior
+ func(ctx context.Context, undo vocab.ActivityStreamsUndo) error {
+ return f.FederatingDB().Undo(ctx, undo)
+ },
+ // override default accept behavior
+ func(ctx context.Context, accept vocab.ActivityStreamsAccept) error {
+ return f.FederatingDB().Accept(ctx, accept)
+ },
+ }
+
return
}
diff --git a/internal/federation/federator.go b/internal/federation/federator.go
index a3b1386e4..f09a77279 100644
--- a/internal/federation/federator.go
+++ b/internal/federation/federator.go
@@ -34,6 +34,8 @@ import (
type Federator interface {
// FederatingActor returns the underlying pub.FederatingActor, which can be used to send activities, and serve actors at inboxes/outboxes.
FederatingActor() pub.FederatingActor
+ // FederatingDB returns the underlying FederatingDB interface.
+ FederatingDB() FederatingDB
// AuthenticateFederatedRequest can be used to check the authenticity of incoming http-signed requests for federating resources.
// The given username will be used to create a transport for making outgoing requests. See the implementation for more detailed comments.
AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error)
@@ -52,6 +54,7 @@ type Federator interface {
type federator struct {
config *config.Config
db db.DB
+ federatingDB FederatingDB
clock pub.Clock
typeConverter typeutils.TypeConverter
transportController transport.Controller
@@ -60,18 +63,19 @@ type federator struct {
}
// NewFederator returns a new federator
-func NewFederator(db db.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, typeConverter typeutils.TypeConverter) Federator {
+func NewFederator(db db.DB, federatingDB FederatingDB, transportController transport.Controller, config *config.Config, log *logrus.Logger, typeConverter typeutils.TypeConverter) Federator {
clock := &Clock{}
f := &federator{
config: config,
db: db,
+ federatingDB: federatingDB,
clock: &Clock{},
typeConverter: typeConverter,
transportController: transportController,
log: log,
}
- actor := newFederatingActor(f, f, db.Federation(), clock)
+ actor := newFederatingActor(f, f, federatingDB, clock)
f.actor = actor
return f
}
@@ -79,3 +83,7 @@ func NewFederator(db db.DB, transportController transport.Controller, config *co
func (f *federator) FederatingActor() pub.FederatingActor {
return f.actor
}
+
+func (f *federator) FederatingDB() FederatingDB {
+ return f.federatingDB
+}
diff --git a/internal/federation/federator_test.go b/internal/federation/federator_test.go
index 2eab09507..e5d42b53d 100644
--- a/internal/federation/federator_test.go
+++ b/internal/federation/federator_test.go
@@ -89,7 +89,7 @@ func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() {
return nil, nil
}))
// setup module being tested
- federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.typeConverter)
+ federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.config, suite.log, suite.typeConverter)
// setup request
ctx := context.Background()
@@ -155,7 +155,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() {
}))
// now setup module being tested, with the mock transport controller
- federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.typeConverter)
+ federator := federation.NewFederator(suite.db, testrig.NewTestFederatingDB(suite.db), tc, suite.config, suite.log, suite.typeConverter)
// setup request
ctx := context.Background()
diff --git a/internal/federation/util.go b/internal/federation/util.go
index 14ceaeb1d..3f53ed6a7 100644
--- a/internal/federation/util.go
+++ b/internal/federation/util.go
@@ -27,11 +27,13 @@ import (
"fmt"
"net/http"
"net/url"
+ "strings"
"github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams"
"github.com/go-fed/activity/streams/vocab"
"github.com/go-fed/httpsig"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/transport"
"github.com/superseriousbusiness/gotosocial/internal/typeutils"
@@ -128,57 +130,73 @@ func (f *federator) AuthenticateFederatedRequest(username string, r *http.Reques
return nil, fmt.Errorf("could not parse key id into a url: %s", err)
}
- transport, err := f.GetTransportForUser(username)
- if err != nil {
- return nil, fmt.Errorf("transport err: %s", err)
- }
+ var publicKey interface{}
+ var pkOwnerURI *url.URL
+ if strings.EqualFold(requestingPublicKeyID.Host, f.config.Host) {
+ // the request is coming from INSIDE THE HOUSE so skip the remote dereferencing
+ requestingLocalAccount := &gtsmodel.Account{}
+ if err := f.db.GetWhere([]db.Where{{Key: "public_key_uri", Value: requestingPublicKeyID.String()}}, requestingLocalAccount); err != nil {
+ return nil, fmt.Errorf("couldn't get local account with public key uri %s from the database: %s", requestingPublicKeyID.String(), err)
+ }
+ publicKey = requestingLocalAccount.PublicKey
+ pkOwnerURI, err = url.Parse(requestingLocalAccount.URI)
+ if err != nil {
+ return nil, fmt.Errorf("error parsing url %s: %s", requestingLocalAccount.URI, err)
+ }
+ } else {
+ // the request is remote, so we need to authenticate the request properly by dereferencing the remote key
+ transport, err := f.GetTransportForUser(username)
+ if err != nil {
+ return nil, fmt.Errorf("transport err: %s", err)
+ }
- // The actual http call to the remote server is made right here in the Dereference function.
- b, err := transport.Dereference(context.Background(), requestingPublicKeyID)
- if err != nil {
- return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err)
- }
+ // The actual http call to the remote server is made right here in the Dereference function.
+ b, err := transport.Dereference(context.Background(), requestingPublicKeyID)
+ if err != nil {
+ return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err)
+ }
- // if the key isn't in the response, we can't authenticate the request
- requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID)
- if err != nil {
- return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err)
- }
+ // if the key isn't in the response, we can't authenticate the request
+ requestingPublicKey, err := getPublicKeyFromResponse(context.Background(), b, requestingPublicKeyID)
+ if err != nil {
+ return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err)
+ }
- // we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
- pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
- if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
- return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value")
- }
+ // we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey
+ pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem()
+ if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() {
+ return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value")
+ }
- // and decode the PEM so that we can parse it as a golang public key
- pubKeyPem := pkPemProp.Get()
- block, _ := pem.Decode([]byte(pubKeyPem))
- if block == nil || block.Type != "PUBLIC KEY" {
- return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
- }
+ // and decode the PEM so that we can parse it as a golang public key
+ pubKeyPem := pkPemProp.Get()
+ block, _ := pem.Decode([]byte(pubKeyPem))
+ if block == nil || block.Type != "PUBLIC KEY" {
+ return nil, errors.New("could not decode publicKeyPem to PUBLIC KEY pem block type")
+ }
- p, err := x509.ParsePKIXPublicKey(block.Bytes)
- if err != nil {
- return nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
+ publicKey, err = x509.ParsePKIXPublicKey(block.Bytes)
+ if err != nil {
+ return nil, fmt.Errorf("could not parse public key from block bytes: %s", err)
+ }
+
+ // all good! we just need the URI of the key owner to return
+ pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner()
+ if pkOwnerProp == nil || !pkOwnerProp.IsIRI() {
+ return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value")
+ }
+ pkOwnerURI = pkOwnerProp.GetIRI()
}
- if p == nil {
+ if publicKey == nil {
return nil, errors.New("returned public key was empty")
}
// do the actual authentication here!
algo := httpsig.RSA_SHA256 // TODO: make this more robust
- if err := verifier.Verify(p, algo); err != nil {
+ if err := verifier.Verify(publicKey, algo); err != nil {
return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err)
}
- // all good! we just need the URI of the key owner to return
- pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner()
- if pkOwnerProp == nil || !pkOwnerProp.IsIRI() {
- return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value")
- }
- pkOwnerURI := pkOwnerProp.GetIRI()
-
return pkOwnerURI, nil
}
@@ -217,6 +235,12 @@ func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *u
return nil, errors.New("error resolving type as activitystreams application")
}
return p, nil
+ case string(gtsmodel.ActivityStreamsService):
+ p, ok := t.(vocab.ActivityStreamsService)
+ if !ok {
+ return nil, errors.New("error resolving type as activitystreams service")
+ }
+ return p, nil
}
return nil, fmt.Errorf("type name %s not supported", t.GetTypeName())
@@ -243,3 +267,23 @@ func (f *federator) GetTransportForUser(username string) (transport.Transport, e
}
return transport, nil
}
+
+func sameActor(activityActor vocab.ActivityStreamsActorProperty, followActor vocab.ActivityStreamsActorProperty) bool {
+ if activityActor == nil || followActor == nil {
+ return false
+ }
+ for aIter := activityActor.Begin(); aIter != activityActor.End(); aIter = aIter.Next() {
+ for fIter := followActor.Begin(); fIter != followActor.End(); fIter = fIter.Next() {
+ if aIter.GetIRI() == nil {
+ return false
+ }
+ if fIter.GetIRI() == nil {
+ return false
+ }
+ if aIter.GetIRI().String() == fIter.GetIRI().String() {
+ return true
+ }
+ }
+ }
+ return false
+}