diff --git a/internal/apimodule/apimodule.go b/internal/apimodule/apimodule.go
index 6d7dbdb83..52304fc45 100644
--- a/internal/apimodule/apimodule.go
+++ b/internal/apimodule/apimodule.go
@@ -31,3 +31,11 @@ type ClientAPIModule interface {
Route(s router.Router) error
CreateTables(db db.DB) error
}
+
+// FederationAPIModule represents a chunk of code (usually contained in a single package) that adds a set
+// of functionalities and/or side effects to a router, by mapping routes and/or middlewares onto it--in other words, a REST API ;)
+// Unlike ClientAPIModule, federation API module is not intended to be interacted with by clients directly -- it is primarily a server-to-server interface.
+type FederationAPIModule interface {
+ Route(s router.Router) error
+ CreateTables(db db.DB) error
+}
diff --git a/internal/apimodule/federation/federation.go b/internal/apimodule/federation/federation.go
new file mode 100644
index 000000000..82744085d
--- /dev/null
+++ b/internal/apimodule/federation/federation.go
@@ -0,0 +1,90 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ 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 .
+*/
+
+package federation
+
+import (
+ "net/http"
+
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/apimodule"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/federation"
+ "github.com/superseriousbusiness/gotosocial/internal/router"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
+ "github.com/superseriousbusiness/gotosocial/internal/util"
+)
+
+const (
+ // UsernameKey is for account usernames.
+ UsernameKey = "username"
+ // UsersBasePath is the base path for serving information about Users eg https://example.org/users
+ UsersBasePath = "/" + util.UsersPath
+ // UsersBasePathWithID is just the users base path with the Username key in it.
+ // Use this anywhere you need to know the username of the user being queried.
+ // Eg https://example.org/users/:username
+ UsersBasePathWithID = UsersBasePath + "/:" + UsernameKey
+)
+
+// ActivityPubAcceptHeaders represents the Accept headers mentioned here:
+// https://www.w3.org/TR/activitypub/#retrieving-objects
+var ActivityPubAcceptHeaders = []string{
+ `application/activity+json`,
+ `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`,
+}
+
+// Module implements the FederationAPIModule interface
+type Module struct {
+ federator federation.Federator
+ config *config.Config
+ db db.DB
+ tc typeutils.TypeConverter
+ log *logrus.Logger
+}
+
+// New returns a new auth module
+func New(db db.DB, federator federation.Federator, tc typeutils.TypeConverter, config *config.Config, log *logrus.Logger) apimodule.FederationAPIModule {
+ return &Module{
+ federator: federator,
+ config: config,
+ db: db,
+ tc: tc,
+ log: log,
+ }
+}
+
+// Route satisfies the RESTAPIModule interface
+func (m *Module) Route(s router.Router) error {
+ s.AttachHandler(http.MethodGet, UsersBasePathWithID, m.UsersGETHandler)
+ return nil
+}
+
+// CreateTables populates necessary tables in the given DB
+func (m *Module) CreateTables(db db.DB) error {
+ // models := []interface{}{
+ // >smodel.MediaAttachment{},
+ // }
+
+ // for _, m := range models {
+ // if err := db.CreateTable(m); err != nil {
+ // return fmt.Errorf("error creating table: %s", err)
+ // }
+ // }
+ return nil
+}
diff --git a/internal/apimodule/federation/users.go b/internal/apimodule/federation/users.go
new file mode 100644
index 000000000..668e51f82
--- /dev/null
+++ b/internal/apimodule/federation/users.go
@@ -0,0 +1,122 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ 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 .
+*/
+
+package federation
+
+import (
+ "encoding/json"
+ "errors"
+ "fmt"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/sirupsen/logrus"
+ "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/federation"
+)
+
+// UsersGETHandler should be served at https://example.org/users/:username.
+//
+// The goal here is to return the activitypub representation of an account
+// in the form of a vocab.ActivityStreamsPerson. This should only be served
+// to REMOTE SERVERS that present a valid signature on the GET request, on
+// behalf of a user, otherwise we risk leaking information about users publicly.
+//
+// And of course, the request should be refused if the account or server making the
+// request is blocked.
+func (m *Module) UsersGETHandler(c *gin.Context) {
+ l := m.log.WithFields(logrus.Fields{
+ "func": "UsersGETHandler",
+ "url": c.Request.RequestURI,
+ })
+ requestedUsername := c.Param(UsernameKey)
+ if requestedUsername == "" {
+ err := errors.New("no username specified in request")
+ l.Debug(err)
+ c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
+ return
+ }
+
+ // make sure this actually an AP request
+ format := c.NegotiateFormat(ActivityPubAcceptHeaders...)
+ if format == "" {
+ err := errors.New("could not negotiate format with given Accept header(s)")
+ l.Debug(err)
+ c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()})
+ return
+ }
+ l.Tracef("negotiated format: %s", format)
+
+ // get the account the request is referring to
+ requestedAccount := >smodel.Account{}
+ if err := m.db.GetLocalAccountByUsername(requestedUsername, requestedAccount); err != nil {
+ l.Errorf("database error getting account with username %s: %s", requestedUsername, err)
+ // we'll just return not authorized here to avoid giving anything away
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
+ return
+ }
+
+ // and create a transport for it
+ transport, err := m.federator.TransportController().NewTransport(requestedAccount.PublicKeyURI, requestedAccount.PrivateKey)
+ if err != nil {
+ l.Errorf("error creating transport for username %s: %s", requestedUsername, err)
+ // we'll just return not authorized here to avoid giving anything away
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
+ return
+ }
+
+ // authenticate the request
+ authentication, err := federation.AuthenticateFederatedRequest(transport, c.Request)
+ if err != nil {
+ l.Errorf("error authenticating GET user request: %s", err)
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
+ return
+ }
+
+ if !authentication.Authenticated {
+ l.Debug("request not authorized")
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
+ return
+ }
+
+
+ requestingAccount := >smodel.Account{}
+ if authentication.RequestingPublicKeyID != nil {
+ if err := m.db.GetWhere("public_key_uri", authentication.RequestingPublicKeyID.String(), requestingAccount); err != nil {
+
+ }
+ }
+
+ authorization, err := federation.AuthorizeFederatedRequest
+
+ person, err := m.tc.AccountToAS(requestedAccount)
+ if err != nil {
+ l.Errorf("error converting account to ap person: %s", err)
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
+ return
+ }
+
+ data, err := person.Serialize()
+ if err != nil {
+ l.Errorf("error serializing user: %s", err)
+ c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"})
+ return
+ }
+
+ c.JSON(http.StatusOK, data)
+}
diff --git a/internal/federation/clock.go b/internal/federation/clock.go
index 1d85df097..2bd82e34e 100644
--- a/internal/federation/clock.go
+++ b/internal/federation/clock.go
@@ -18,7 +18,11 @@
package federation
-import "time"
+import (
+ "time"
+
+ "github.com/go-fed/activity/pub"
+)
/*
GOFED CLOCK INTERFACE
@@ -32,3 +36,7 @@ type Clock struct{}
func (c *Clock) Now() time.Time {
return time.Now()
}
+
+func NewClock() pub.Clock {
+ return &Clock{}
+}
diff --git a/internal/federation/commonbehavior.go b/internal/federation/commonbehavior.go
index cbda9e7ef..cb8b2b613 100644
--- a/internal/federation/commonbehavior.go
+++ b/internal/federation/commonbehavior.go
@@ -34,7 +34,7 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/util"
)
-// commonBehavior implements the go-fed common behavior interface
+// commonBehavior implements the GTSCommonBehavior interface
type commonBehavior struct {
db db.DB
log *logrus.Logger
@@ -42,7 +42,8 @@ type commonBehavior struct {
transportController transport.Controller
}
-// newCommonBehavior returns an implementation of the pub.CommonBehavior interface that uses the given db, log, config, and transportController
+// newCommonBehavior returns an implementation of the GTSCommonBehavior interface that uses the given db, log, config, and transportController.
+// This interface is a superset of the pub.CommonBehavior interface, so it can be used anywhere that interface would be used.
func newCommonBehavior(db db.DB, log *logrus.Logger, config *config.Config, transportController transport.Controller) pub.CommonBehavior {
return &commonBehavior{
db: db,
@@ -172,3 +173,10 @@ func (c *commonBehavior) NewTransport(ctx context.Context, actorBoxIRI *url.URL,
return c.transportController.NewTransport(account.PublicKeyURI, account.PrivateKey)
}
+
+// GetUser returns the activitypub representation of the user specified in the path of r, eg https://example.org/users/example_user.
+// AuthenticateGetUser should be called first, to make sure the requester has permission to view the requested user.
+// The returned user should be a translation from a *gtsmodel.Account to a serializable ActivityStreamsPerson.
+func (c *commonBehavior) GetUser(ctx context.Context, r *http.Request) (vocab.ActivityStreamsPerson, error) {
+ return nil, nil
+}
diff --git a/internal/federation/federatingactor.go b/internal/federation/federatingactor.go
new file mode 100644
index 000000000..f105d9125
--- /dev/null
+++ b/internal/federation/federatingactor.go
@@ -0,0 +1,136 @@
+/*
+ GoToSocial
+ Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org
+
+ 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 .
+*/
+
+package federation
+
+import (
+ "context"
+ "net/http"
+ "net/url"
+
+ "github.com/go-fed/activity/pub"
+ "github.com/go-fed/activity/streams/vocab"
+)
+
+// federatingActor implements the go-fed federating protocol interface
+type federatingActor struct {
+ actor pub.FederatingActor
+}
+
+// newFederatingProtocol returns the gotosocial implementation of the GTSFederatingProtocol interface
+func newFederatingActor(c pub.CommonBehavior, s2s pub.FederatingProtocol, db pub.Database, clock pub.Clock) pub.FederatingActor {
+ actor := pub.NewFederatingActor(c, s2s, db, clock)
+
+ return &federatingActor{
+ actor: actor,
+ }
+}
+
+// Send a federated activity.
+//
+// The provided url must be the outbox of the sender. All processing of
+// the activity occurs similarly to the C2S flow:
+// - If t is not an Activity, it is wrapped in a Create activity.
+// - A new ID is generated for the activity.
+// - The activity is added to the specified outbox.
+// - The activity is prepared and delivered to recipients.
+//
+// Note that this function will only behave as expected if the
+// implementation has been constructed to support federation. This
+// method will guaranteed work for non-custom Actors. For custom actors,
+// care should be used to not call this method if only C2S is supported.
+func (f *federatingActor) Send(c context.Context, outbox *url.URL, t vocab.Type) (pub.Activity, error) {
+ return f.actor.Send(c, outbox, t)
+}
+
+// PostInbox returns true if the request was handled as an ActivityPub
+// POST to an actor's inbox. If false, the request was not an
+// ActivityPub request and may still be handled by the caller in
+// another way, such as serving a web page.
+//
+// If the error is nil, then the ResponseWriter's headers and response
+// has already been written. If a non-nil error is returned, then no
+// response has been written.
+//
+// If the Actor was constructed with the Federated Protocol enabled,
+// side effects will occur.
+//
+// If the Federated Protocol is not enabled, writes the
+// http.StatusMethodNotAllowed status code in the response. No side
+// effects occur.
+func (f *federatingActor) PostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
+ return f.actor.PostInbox(c, w, r)
+}
+
+// GetInbox returns true if the request was handled as an ActivityPub
+// GET to an actor's inbox. If false, the request was not an ActivityPub
+// request and may still be handled by the caller in another way, such
+// as serving a web page.
+//
+// If the error is nil, then the ResponseWriter's headers and response
+// has already been written. If a non-nil error is returned, then no
+// response has been written.
+//
+// If the request is an ActivityPub request, the Actor will defer to the
+// application to determine the correct authorization of the request and
+// the resulting OrderedCollection to respond with. The Actor handles
+// serializing this OrderedCollection and responding with the correct
+// headers and http.StatusOK.
+func (f *federatingActor) GetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
+ return f.actor.GetInbox(c, w, r)
+}
+
+// PostOutbox returns true if the request was handled as an ActivityPub
+// POST to an actor's outbox. If false, the request was not an
+// ActivityPub request and may still be handled by the caller in another
+// way, such as serving a web page.
+//
+// If the error is nil, then the ResponseWriter's headers and response
+// has already been written. If a non-nil error is returned, then no
+// response has been written.
+//
+// If the Actor was constructed with the Social Protocol enabled, side
+// effects will occur.
+//
+// If the Social Protocol is not enabled, writes the
+// http.StatusMethodNotAllowed status code in the response. No side
+// effects occur.
+//
+// If the Social and Federated Protocol are both enabled, it will handle
+// the side effects of receiving an ActivityStream Activity, and then
+// federate the Activity to peers.
+func (f *federatingActor) PostOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
+ return f.actor.PostOutbox(c, w, r)
+}
+
+// GetOutbox returns true if the request was handled as an ActivityPub
+// GET to an actor's outbox. If false, the request was not an
+// ActivityPub request.
+//
+// If the error is nil, then the ResponseWriter's headers and response
+// has already been written. If a non-nil error is returned, then no
+// response has been written.
+//
+// If the request is an ActivityPub request, the Actor will defer to the
+// application to determine the correct authorization of the request and
+// the resulting OrderedCollection to respond with. The Actor handles
+// serializing this OrderedCollection and responding with the correct
+// headers and http.StatusOK.
+func (f *federatingActor) GetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (bool, error) {
+ return f.actor.GetOutbox(c, w, r)
+}
diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go
index 035d0b437..0f1bd830e 100644
--- a/internal/federation/federatingprotocol.go
+++ b/internal/federation/federatingprotocol.go
@@ -27,30 +27,32 @@ import (
"github.com/go-fed/activity/pub"
"github.com/go-fed/activity/streams/vocab"
- "github.com/go-fed/httpsig"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/transport"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/util"
)
-// FederatingProtocol implements the go-fed federating protocol interface
-type FederatingProtocol struct {
+// federatingProtocol implements the go-fed federating protocol interface
+type federatingProtocol struct {
db db.DB
log *logrus.Logger
config *config.Config
transportController transport.Controller
+ typeConverter typeutils.TypeConverter
}
-// NewFederatingProtocol returns the gotosocial implementation of the go-fed FederatingProtocol interface
-func NewFederatingProtocol(db db.DB, log *logrus.Logger, config *config.Config, transportController transport.Controller) pub.FederatingProtocol {
- return &FederatingProtocol{
+// newFederatingProtocol returns the gotosocial implementation of the GTSFederatingProtocol interface
+func newFederatingProtocol(db db.DB, log *logrus.Logger, config *config.Config, transportController transport.Controller, typeConverter typeutils.TypeConverter) pub.FederatingProtocol {
+ return &federatingProtocol{
db: db,
log: log,
config: config,
transportController: transportController,
+ typeConverter: typeConverter,
}
}
@@ -80,7 +82,7 @@ func NewFederatingProtocol(db db.DB, log *logrus.Logger, config *config.Config,
// PostInbox. In this case, the DelegateActor implementation must not
// write a response to the ResponseWriter as is expected that the caller
// to PostInbox will do so when handling the error.
-func (f *FederatingProtocol) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) {
+func (f *federatingProtocol) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) {
l := f.log.WithFields(logrus.Fields{
"func": "PostInboxRequestBodyHook",
"useragent": r.UserAgent(),
@@ -93,33 +95,7 @@ func (f *FederatingProtocol) PostInboxRequestBodyHook(ctx context.Context, r *ht
return nil, err
}
- if !util.IsInboxPath(r.URL) {
- err := fmt.Errorf("url %s did not correspond to inbox path", r.URL.String())
- l.Debug(err)
- return nil, err
- }
-
- inboxUsername, err := util.ParseInboxPath(r.URL)
- if err != nil {
- err := fmt.Errorf("could not parse username from url: %s", r.URL.String())
- l.Debug(err)
- return nil, err
- }
- l.Tracef("parsed username %s from %s", inboxUsername, r.URL.String())
- l.Tracef("signature: %s", r.Header.Get("Signature"))
-
- // get the gts account from the username
- inboxAccount := >smodel.Account{}
- if err := f.db.GetLocalAccountByUsername(inboxUsername, inboxAccount); err != nil {
- err := fmt.Errorf("AuthenticateGetInbox: error fetching inbox account for %s from database: %s", r.URL.String(), err)
- l.Error(err)
- // return an abridged version of the error so we don't leak anything to the caller
- return nil, errors.New("database error")
- }
-
- ctxWithUsername := context.WithValue(ctx, util.APUsernameKey, inboxUsername)
- ctxWithAccount := context.WithValue(ctxWithUsername, util.APAccountKey, inboxAccount)
- ctxWithActivity := context.WithValue(ctxWithAccount, util.APActivityKey, activity)
+ ctxWithActivity := context.WithValue(ctx, util.APActivity, activity)
return ctxWithActivity, nil
}
@@ -139,17 +115,7 @@ func (f *FederatingProtocol) PostInboxRequestBodyHook(ctx context.Context, r *ht
// Finally, if the authentication and authorization succeeds, then
// authenticated must be true and error nil. The request will continue
// to be processed.
-//
-// IMPLEMENTATION NOTES:
-// AuthenticatePostInbox validates an incoming federation request (!!) by deriving the public key
-// of the requester from the request, checking the owner of the inbox that's being requested, and doing
-// some fiddling around with http signatures.
-//
-// A *side effect* of calling this function is that the name of the host making the request will be set
-// onto the returned context, using APRequestingHostKey. If known to us already, the remote account making
-// the request will also be set on the context, using APRequestingAccountKey. If not known to us already,
-// the value of this key will be set to nil and the account will have to be fetched further down the line.
-func (f *FederatingProtocol) AuthenticatePostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
+func (f *federatingProtocol) AuthenticatePostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) {
l := f.log.WithFields(logrus.Fields{
"func": "AuthenticatePostInbox",
"useragent": r.UserAgent(),
@@ -157,64 +123,47 @@ func (f *FederatingProtocol) AuthenticatePostInbox(ctx context.Context, w http.R
})
l.Trace("received request to authenticate")
- // account should have been set in PostInboxRequestBodyHook
- // if it's not set, we should bail because we can't do anything
- i := ctx.Value(util.APAccountKey)
- if i == nil {
- return nil, false, errors.New("could not retrieve inbox owner")
- }
- requestedAccount, ok := i.(*gtsmodel.Account)
- if !ok {
- return nil, false, errors.New("could not cast inbox owner")
+ requestedAccountI := ctx.Value(util.APAccount)
+ if requestedAccountI == nil {
+ return ctx, false, errors.New("requested account not set in context")
}
- v, err := httpsig.NewVerifier(r)
- if err != nil {
- return ctx, false, fmt.Errorf("could not create http sig verifier: %s", err)
- }
-
- requestingPublicKeyID, err := url.Parse(v.KeyId())
- if err != nil {
- return ctx, false, fmt.Errorf("could not create parse key id into a url: %s", err)
+ requestedAccount, ok := requestedAccountI.(*gtsmodel.Account)
+ if !ok || requestedAccount == nil {
+ return ctx, false, errors.New("requested account not parsebale from context")
}
transport, err := f.transportController.NewTransport(requestedAccount.PublicKeyURI, requestedAccount.PrivateKey)
if err != nil {
- return ctx, false, fmt.Errorf("error creating new transport: %s", err)
+ return ctx, false, fmt.Errorf("error creating transport: %s", err)
}
- b, err := transport.Dereference(ctx, requestingPublicKeyID)
+ requestingPublicKeyID, err := AuthenticateFederatedRequest(transport, r)
if err != nil {
- return ctx, false, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err)
+ l.Debugf("request not authenticated: %s", err)
+ return ctx, false, fmt.Errorf("not authenticated: %s", err)
}
- requestingPublicKey, err := getPublicKeyFromResponse(ctx, b, requestingPublicKeyID)
- if err != nil {
- return ctx, false, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err)
- }
-
- algo := httpsig.RSA_SHA256
- if err := v.Verify(requestingPublicKey, algo); err != nil {
- return ctx, false, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err)
- }
-
- var requestingAccount *gtsmodel.Account
- a := >smodel.Account{}
- if err := f.db.GetWhere("public_key_uri", requestingPublicKeyID.String(), a); err == nil {
- // we know about this account already so we can set it on the context
- requestingAccount = a
- } else {
+ requestingAccount := >smodel.Account{}
+ if err := f.db.GetWhere("public_key_uri", requestingPublicKeyID.String(), requestingAccount); err != nil {
+ // there's been a proper error so return it
if _, ok := err.(db.ErrNoEntries); !ok {
- return ctx, false, fmt.Errorf("database error finding account with public key uri %s: %s", requestingPublicKeyID.String(), err)
+ return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", requestingPublicKeyID.String(), err)
}
- // do nothing here, requestingAccount will stay nil and we'll have to figure it out further down the line
+ // we just don't know this account (yet) so try to dereference it
+ // TODO: slow-fed
+ person, err := DereferenceAccount(transport, requestingPublicKeyID)
+ if err != nil {
+ return ctx, false, fmt.Errorf("error dereferencing account with public key id %s: %s", requestingPublicKeyID.String(), err)
+ }
+ a, err := f.typeConverter.ASPersonToAccount(person)
+ if err != nil {
+ return ctx, false, fmt.Errorf("error converting person with public key id %s to account: %s", requestingPublicKeyID.String(), err)
+ }
+ requestingAccount = a
}
- // all good at this point, so just set some stuff on the context
- contextWithHost := context.WithValue(ctx, util.APRequestingHostKey, requestingPublicKeyID.Host)
- contextWithRequestingAccount := context.WithValue(contextWithHost, util.APRequestingAccountKey, requestingAccount)
-
- return contextWithRequestingAccount, true, nil
+ return newContext, true, nil
}
// Blocked should determine whether to permit a set of actors given by
@@ -231,7 +180,7 @@ func (f *FederatingProtocol) AuthenticatePostInbox(ctx context.Context, w http.R
// Finally, if the authentication and authorization succeeds, then
// blocked must be false and error nil. The request will continue
// to be processed.
-func (f *FederatingProtocol) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) {
+func (f *federatingProtocol) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) {
// TODO
return false, nil
}
@@ -255,7 +204,7 @@ func (f *FederatingProtocol) Blocked(ctx context.Context, actorIRIs []*url.URL)
//
// Applications are not expected to handle every single ActivityStreams
// type and extension. The unhandled ones are passed to DefaultCallback.
-func (f *FederatingProtocol) FederatingCallbacks(ctx context.Context) (pub.FederatingWrappedCallbacks, []interface{}, error) {
+func (f *federatingProtocol) FederatingCallbacks(ctx context.Context) (pub.FederatingWrappedCallbacks, []interface{}, error) {
// TODO
return pub.FederatingWrappedCallbacks{}, nil, nil
}
@@ -267,7 +216,7 @@ func (f *FederatingProtocol) FederatingCallbacks(ctx context.Context) (pub.Feder
// Applications are not expected to handle every single ActivityStreams
// type and extension, so the unhandled ones are passed to
// DefaultCallback.
-func (f *FederatingProtocol) DefaultCallback(ctx context.Context, activity pub.Activity) error {
+func (f *federatingProtocol) DefaultCallback(ctx context.Context, activity pub.Activity) error {
l := f.log.WithFields(logrus.Fields{
"func": "DefaultCallback",
"aptype": activity.GetTypeName(),
@@ -280,7 +229,7 @@ func (f *FederatingProtocol) DefaultCallback(ctx context.Context, activity pub.A
// an activity to determine if inbox forwarding needs to occur.
//
// Zero or negative numbers indicate infinite recursion.
-func (f *FederatingProtocol) MaxInboxForwardingRecursionDepth(ctx context.Context) int {
+func (f *federatingProtocol) MaxInboxForwardingRecursionDepth(ctx context.Context) int {
// TODO
return 0
}
@@ -290,7 +239,7 @@ func (f *FederatingProtocol) MaxInboxForwardingRecursionDepth(ctx context.Contex
// delivery.
//
// Zero or negative numbers indicate infinite recursion.
-func (f *FederatingProtocol) MaxDeliveryRecursionDepth(ctx context.Context) int {
+func (f *federatingProtocol) MaxDeliveryRecursionDepth(ctx context.Context) int {
// TODO
return 0
}
@@ -302,7 +251,7 @@ func (f *FederatingProtocol) MaxDeliveryRecursionDepth(ctx context.Context) int
//
// The activity is provided as a reference for more intelligent
// logic to be used, but the implementation must not modify it.
-func (f *FederatingProtocol) FilterForwarding(ctx context.Context, potentialRecipients []*url.URL, a pub.Activity) ([]*url.URL, error) {
+func (f *federatingProtocol) FilterForwarding(ctx context.Context, potentialRecipients []*url.URL, a pub.Activity) ([]*url.URL, error) {
// TODO
return nil, nil
}
@@ -315,7 +264,7 @@ func (f *FederatingProtocol) FilterForwarding(ctx context.Context, potentialReci
//
// Always called, regardless whether the Federated Protocol or Social
// API is enabled.
-func (f *FederatingProtocol) GetInbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
+func (f *federatingProtocol) GetInbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
// IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through
// the CLIENT API, not through the federation API, so we just do nothing here.
return nil, nil
diff --git a/internal/federation/federator.go b/internal/federation/federator.go
index a1c27b0ea..a38e2d09a 100644
--- a/internal/federation/federator.go
+++ b/internal/federation/federator.go
@@ -19,292 +19,62 @@
package federation
import (
- "context"
- "net/http"
- "net/url"
-
"github.com/go-fed/activity/pub"
- "github.com/go-fed/activity/streams/vocab"
"github.com/sirupsen/logrus"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/distributor"
"github.com/superseriousbusiness/gotosocial/internal/transport"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
)
-// Federator wraps everything needed to manage activitypub federation from gotosocial
+// Federator wraps various interfaces and functions to manage activitypub federation from gotosocial
type Federator interface {
- // Send a federated activity.
- //
- // The provided url must be the outbox of the sender. All processing of
- // the activity occurs similarly to the C2S flow:
- // - If t is not an Activity, it is wrapped in a Create activity.
- // - A new ID is generated for the activity.
- // - The activity is added to the specified outbox.
- // - The activity is prepared and delivered to recipients.
- //
- // Note that this function will only behave as expected if the
- // implementation has been constructed to support federation. This
- // method will guaranteed work for non-custom Actors. For custom actors,
- // care should be used to not call this method if only C2S is supported.
- Send(c context.Context, outbox *url.URL, t vocab.Type) (pub.Activity, error)
- // Hook callback after parsing the request body for a federated request
- // to the Actor's inbox.
- //
- // Can be used to set contextual information based on the Activity
- // received.
- //
- // Only called if the Federated Protocol is enabled.
- //
- // Warning: Neither authentication nor authorization has taken place at
- // this time. Doing anything beyond setting contextual information is
- // strongly discouraged.
- //
- // If an error is returned, it is passed back to the caller of
- // PostInbox. In this case, the DelegateActor implementation must not
- // write a response to the ResponseWriter as is expected that the caller
- // to PostInbox will do so when handling the error.
- PostInboxRequestBodyHook(c context.Context, r *http.Request, activity pub.Activity) (context.Context, error)
- // AuthenticatePostInbox delegates the authentication of a POST to an
- // inbox.
- //
- // If an error is returned, it is passed back to the caller of
- // PostInbox. In this case, the implementation must not write a
- // response to the ResponseWriter as is expected that the client will
- // do so when handling the error. The 'authenticated' is ignored.
- //
- // If no error is returned, but authentication or authorization fails,
- // then authenticated must be false and error nil. It is expected that
- // the implementation handles writing to the ResponseWriter in this
- // case.
- //
- // Finally, if the authentication and authorization succeeds, then
- // authenticated must be true and error nil. The request will continue
- // to be processed.
- AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
- // Blocked should determine whether to permit a set of actors given by
- // their ids are able to interact with this particular end user due to
- // being blocked or other application-specific logic.
- //
- // If an error is returned, it is passed back to the caller of
- // PostInbox.
- //
- // If no error is returned, but authentication or authorization fails,
- // then blocked must be true and error nil. An http.StatusForbidden
- // will be written in the wresponse.
- //
- // Finally, if the authentication and authorization succeeds, then
- // blocked must be false and error nil. The request will continue
- // to be processed.
- Blocked(c context.Context, actorIRIs []*url.URL) (blocked bool, err error)
- // FederatingCallbacks returns the application logic that handles
- // ActivityStreams received from federating peers.
- //
- // Note that certain types of callbacks will be 'wrapped' with default
- // behaviors supported natively by the library. Other callbacks
- // compatible with streams.TypeResolver can be specified by 'other'.
- //
- // For example, setting the 'Create' field in the
- // FederatingWrappedCallbacks lets an application dependency inject
- // additional behaviors they want to take place, including the default
- // behavior supplied by this library. This is guaranteed to be compliant
- // with the ActivityPub Social protocol.
- //
- // To override the default behavior, instead supply the function in
- // 'other', which does not guarantee the application will be compliant
- // with the ActivityPub Social Protocol.
- //
- // Applications are not expected to handle every single ActivityStreams
- // type and extension. The unhandled ones are passed to DefaultCallback.
- FederatingCallbacks(c context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}, err error)
- // DefaultCallback is called for types that go-fed can deserialize but
- // are not handled by the application's callbacks returned in the
- // Callbacks method.
- //
- // Applications are not expected to handle every single ActivityStreams
- // type and extension, so the unhandled ones are passed to
- // DefaultCallback.
- DefaultCallback(c context.Context, activity pub.Activity) error
- // MaxInboxForwardingRecursionDepth determines how deep to search within
- // an activity to determine if inbox forwarding needs to occur.
- //
- // Zero or negative numbers indicate infinite recursion.
- MaxInboxForwardingRecursionDepth(c context.Context) int
- // MaxDeliveryRecursionDepth determines how deep to search within
- // collections owned by peers when they are targeted to receive a
- // delivery.
- //
- // Zero or negative numbers indicate infinite recursion.
- MaxDeliveryRecursionDepth(c context.Context) int
- // FilterForwarding allows the implementation to apply business logic
- // such as blocks, spam filtering, and so on to a list of potential
- // Collections and OrderedCollections of recipients when inbox
- // forwarding has been triggered.
- //
- // The activity is provided as a reference for more intelligent
- // logic to be used, but the implementation must not modify it.
- FilterForwarding(c context.Context, potentialRecipients []*url.URL, a pub.Activity) (filteredRecipients []*url.URL, err error)
- // GetInbox returns the OrderedCollection inbox of the actor for this
- // context. It is up to the implementation to provide the correct
- // collection for the kind of authorization given in the request.
- //
- // AuthenticateGetInbox will be called prior to this.
- //
- // Always called, regardless whether the Federated Protocol or Social
- // API is enabled.
- GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error)
- // AuthenticateGetInbox delegates the authentication of a GET to an
- // inbox.
- //
- // Always called, regardless whether the Federated Protocol or Social
- // API is enabled.
- //
- // If an error is returned, it is passed back to the caller of
- // GetInbox. In this case, the implementation must not write a
- // response to the ResponseWriter as is expected that the client will
- // do so when handling the error. The 'authenticated' is ignored.
- //
- // If no error is returned, but authentication or authorization fails,
- // then authenticated must be false and error nil. It is expected that
- // the implementation handles writing to the ResponseWriter in this
- // case.
- //
- // Finally, if the authentication and authorization succeeds, then
- // authenticated must be true and error nil. The request will continue
- // to be processed.
- AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
- // AuthenticateGetOutbox delegates the authentication of a GET to an
- // outbox.
- //
- // Always called, regardless whether the Federated Protocol or Social
- // API is enabled.
- //
- // If an error is returned, it is passed back to the caller of
- // GetOutbox. In this case, the implementation must not write a
- // response to the ResponseWriter as is expected that the client will
- // do so when handling the error. The 'authenticated' is ignored.
- //
- // If no error is returned, but authentication or authorization fails,
- // then authenticated must be false and error nil. It is expected that
- // the implementation handles writing to the ResponseWriter in this
- // case.
- //
- // Finally, if the authentication and authorization succeeds, then
- // authenticated must be true and error nil. The request will continue
- // to be processed.
- AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error)
- // GetOutbox returns the OrderedCollection inbox of the actor for this
- // context. It is up to the implementation to provide the correct
- // collection for the kind of authorization given in the request.
- //
- // AuthenticateGetOutbox will be called prior to this.
- //
- // Always called, regardless whether the Federated Protocol or Social
- // API is enabled.
- GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error)
- // NewTransport returns a new Transport on behalf of a specific actor.
- //
- // The actorBoxIRI will be either the inbox or outbox of an actor who is
- // attempting to do the dereferencing or delivery. Any authentication
- // scheme applied on the request must be based on this actor. The
- // request must contain some sort of credential of the user, such as a
- // HTTP Signature.
- //
- // The gofedAgent passed in should be used by the Transport
- // implementation in the User-Agent, as well as the application-specific
- // user agent string. The gofedAgent will indicate this library's use as
- // well as the library's version number.
- //
- // Any server-wide rate-limiting that needs to occur should happen in a
- // Transport implementation. This factory function allows this to be
- // created, so peer servers are not DOS'd.
- //
- // Any retry logic should also be handled by the Transport
- // implementation.
- //
- // Note that the library will not maintain a long-lived pointer to the
- // returned Transport so that any private credentials are able to be
- // garbage collected.
- NewTransport(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t pub.Transport, err error)
+ FederatingActor() pub.FederatingActor
+ TransportController() transport.Controller
+ FederatingProtocol() pub.FederatingProtocol
+ CommonBehavior() pub.CommonBehavior
}
type federator struct {
- actor pub.FederatingActor
- distributor distributor.Distributor
- federatingProtocol pub.FederatingProtocol
- commonBehavior pub.CommonBehavior
- clock pub.Clock
+ actor pub.FederatingActor
+ distributor distributor.Distributor
+ federatingProtocol pub.FederatingProtocol
+ commonBehavior pub.CommonBehavior
+ clock pub.Clock
+ transportController transport.Controller
}
// NewFederator returns a new federator
-func NewFederator(db db.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, distributor distributor.Distributor) Federator {
+func NewFederator(db db.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, distributor distributor.Distributor, typeConverter typeutils.TypeConverter) Federator {
clock := &Clock{}
- federatingProtocol := NewFederatingProtocol(db, log, config, transportController)
+ federatingProtocol := newFederatingProtocol(db, log, config, transportController, typeConverter)
commonBehavior := newCommonBehavior(db, log, config, transportController)
- actor := pub.NewFederatingActor(commonBehavior, federatingProtocol, db.Federation(), clock)
+ actor := newFederatingActor(commonBehavior, federatingProtocol, db.Federation(), clock)
return &federator{
- actor: actor,
- distributor: distributor,
- federatingProtocol: federatingProtocol,
- commonBehavior: commonBehavior,
- clock: clock,
+ actor: actor,
+ distributor: distributor,
+ federatingProtocol: federatingProtocol,
+ commonBehavior: commonBehavior,
+ clock: clock,
+ transportController: transportController,
}
}
-func (f *federator) Send(c context.Context, outbox *url.URL, t vocab.Type) (pub.Activity, error) {
- return f.actor.Send(c, outbox, t)
+func (f *federator) FederatingActor() pub.FederatingActor {
+ return f.actor
}
-func (f *federator) PostInboxRequestBodyHook(c context.Context, r *http.Request, activity pub.Activity) (context.Context, error) {
- return f.federatingProtocol.PostInboxRequestBodyHook(c, r, activity)
+func (f *federator) TransportController() transport.Controller {
+ return f.transportController
}
-func (f *federator) AuthenticatePostInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) {
- return f.federatingProtocol.AuthenticatePostInbox(c, w, r)
+func (f *federator) FederatingProtocol() pub.FederatingProtocol {
+ return f.federatingProtocol
}
-func (f *federator) Blocked(c context.Context, actorIRIs []*url.URL) (blocked bool, err error) {
- return f.federatingProtocol.Blocked(c, actorIRIs)
-}
-
-func (f *federator) FederatingCallbacks(c context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}, err error) {
- return f.federatingProtocol.FederatingCallbacks(c)
-}
-
-func (f *federator) DefaultCallback(c context.Context, activity pub.Activity) error {
- return f.federatingProtocol.DefaultCallback(c, activity)
-}
-
-func (f *federator) MaxInboxForwardingRecursionDepth(c context.Context) int {
- return f.federatingProtocol.MaxInboxForwardingRecursionDepth(c)
-}
-
-func (f *federator) MaxDeliveryRecursionDepth(c context.Context) int {
- return f.federatingProtocol.MaxDeliveryRecursionDepth(c)
-}
-
-func (f *federator) FilterForwarding(c context.Context, potentialRecipients []*url.URL, a pub.Activity) (filteredRecipients []*url.URL, err error) {
- return f.federatingProtocol.FilterForwarding(c, potentialRecipients, a)
-}
-
-func (f *federator) GetInbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
- return f.federatingProtocol.GetInbox(c, r)
-}
-
-func (f *federator) AuthenticateGetInbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) {
- return f.commonBehavior.AuthenticateGetInbox(c, w, r)
-}
-
-func (f *federator) AuthenticateGetOutbox(c context.Context, w http.ResponseWriter, r *http.Request) (out context.Context, authenticated bool, err error) {
- return f.commonBehavior.AuthenticateGetOutbox(c, w, r)
-}
-
-func (f *federator) GetOutbox(c context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) {
- return f.commonBehavior.GetOutbox(c, r)
-}
-
-func (f *federator) NewTransport(c context.Context, actorBoxIRI *url.URL, gofedAgent string) (t pub.Transport, err error) {
- return f.commonBehavior.NewTransport(c, actorBoxIRI, gofedAgent)
+func (f *federator) CommonBehavior() pub.CommonBehavior {
+ return f.commonBehavior
}
diff --git a/internal/federation/federator_test.go b/internal/federation/federator_test.go
index 8563e21f8..3081d0604 100644
--- a/internal/federation/federator_test.go
+++ b/internal/federation/federator_test.go
@@ -40,18 +40,20 @@ import (
"github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/distributor"
"github.com/superseriousbusiness/gotosocial/internal/federation"
+ "github.com/superseriousbusiness/gotosocial/internal/typeutils"
"github.com/superseriousbusiness/gotosocial/internal/util"
"github.com/superseriousbusiness/gotosocial/testrig"
)
type ProtocolTestSuite struct {
suite.Suite
- config *config.Config
- db db.DB
- log *logrus.Logger
- distributor distributor.Distributor
- accounts map[string]*gtsmodel.Account
- activities map[string]testrig.ActivityWithSignature
+ config *config.Config
+ db db.DB
+ log *logrus.Logger
+ distributor distributor.Distributor
+ typeConverter typeutils.TypeConverter
+ accounts map[string]*gtsmodel.Account
+ activities map[string]testrig.ActivityWithSignature
}
// SetupSuite sets some variables on the suite that we can use as consts (more or less) throughout
@@ -61,6 +63,7 @@ func (suite *ProtocolTestSuite) SetupSuite() {
suite.db = testrig.NewTestDB()
suite.log = testrig.NewTestLog()
suite.distributor = testrig.NewTestDistributor()
+ suite.typeConverter = testrig.NewTestTypeConverter(suite.db)
suite.accounts = testrig.NewTestAccounts()
suite.activities = testrig.NewTestActivities(suite.accounts)
}
@@ -86,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.distributor)
+ federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.distributor, suite.typeConverter)
// setup request
ctx := context.Background()
@@ -94,20 +97,12 @@ func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() {
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)
+ newContext, err := federator.FederatingProtocol().PostInboxRequestBodyHook(ctx, request, activity.Activity)
assert.NoError(suite.T(), err)
assert.NotNil(suite.T(), newContext)
- // username should be set on context now
- usernameI := newContext.Value(util.APUsernameKey)
- assert.NotNil(suite.T(), usernameI)
- username, ok := usernameI.(string)
- assert.True(suite.T(), ok)
- assert.NotEmpty(suite.T(), username)
- assert.Equal(suite.T(), "the_mighty_zork", username)
-
// activity should be set on context now
- activityI := newContext.Value(util.APActivityKey)
+ activityI := newContext.Value(util.APActivity)
assert.NotNil(suite.T(), activityI)
returnedActivity, ok := activityI.(pub.Activity)
assert.True(suite.T(), ok)
@@ -160,15 +155,14 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() {
}))
// now setup module being tested, with the mock transport controller
- federator := federation.NewFederatingProtocol(suite.db, suite.log, suite.config, tc)
+ federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.distributor, suite.typeConverter)
// setup request
ctx := context.Background()
// 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:
- ctxWithUsername := context.WithValue(ctx, util.APUsernameKey, inboxAccount.Username)
- ctxWithAccount := context.WithValue(ctxWithUsername, util.APAccountKey, inboxAccount)
- ctxWithActivity := context.WithValue(ctxWithAccount, util.APActivityKey, activity)
+ ctxWithAccount := context.WithValue(ctx, util.APAccount, inboxAccount)
+ ctxWithActivity := context.WithValue(ctxWithAccount, util.APActivity, activity)
request := httptest.NewRequest(http.MethodPost, "http://localhost:8080/users/the_mighty_zork/inbox", nil) // the endpoint we're hitting
// we need these headers for the request to be validated
@@ -179,23 +173,16 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() {
recorder := httptest.NewRecorder()
// trigger the function being tested, and return the new context it creates
- newContext, authed, err := federator.AuthenticatePostInbox(ctxWithActivity, recorder, request)
+ newContext, authed, err := federator.FederatingProtocol().AuthenticatePostInbox(ctxWithActivity, recorder, request)
assert.NoError(suite.T(), err)
assert.True(suite.T(), authed)
// since we know this account already it should be set on the context
- requestingAccountI := newContext.Value(util.APRequestingAccountKey)
+ requestingAccountI := newContext.Value(util.APRequestingAccount)
assert.NotNil(suite.T(), requestingAccountI)
requestingAccount, ok := requestingAccountI.(*gtsmodel.Account)
assert.True(suite.T(), ok)
assert.Equal(suite.T(), sendingAccount.Username, requestingAccount.Username)
-
- // the host making the request should also be set on the context
- requestingHostI := newContext.Value(util.APRequestingHostKey)
- assert.NotNil(suite.T(), requestingHostI)
- requestingHost, ok := requestingHostI.(string)
- assert.True(suite.T(), ok)
- assert.Equal(suite.T(), sendingAccount.Domain, requestingHost)
}
func TestProtocolTestSuite(t *testing.T) {
diff --git a/internal/federation/util.go b/internal/federation/util.go
index 7c43c58b5..997550b9f 100644
--- a/internal/federation/util.go
+++ b/internal/federation/util.go
@@ -24,12 +24,16 @@ import (
"crypto/x509"
"encoding/json"
"encoding/pem"
+ "errors"
"fmt"
+ "net/http"
"net/url"
"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/gtsmodel"
)
/*
@@ -101,3 +105,84 @@ func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (p cr
p, err = x509.ParsePKIXPublicKey(block.Bytes)
return
}
+
+// AuthenticateFederatedRequest authenticates any kind of federated request from a remote server. This includes things like
+// GET requests for dereferencing users or statuses etc and POST requests for delivering new Activities.
+//
+// Error means the request did not pass authentication. No error means it's authentic.
+//
+// Authenticate in this case is defined as just making sure that the http request is actually signed by whoever claims
+// to have signed it, by fetching the public key from the signature and checking it against the remote public key.
+//
+// The provided transport will be used to dereference the public key ID of the request signature. Ideally you should pass in a transport
+// with the credentials of the user *being requested*, so that the remote server can decide how to handle the request based on who's making it.
+// Ie., if the request on this server is for https://example.org/users/some_username then you should pass in a transport that's been initialized with
+// the keys belonging to local user 'some_username'. The remote server will then know that this is the user making the
+// dereferencing request, and they can decide to allow or deny the request depending on their settings.
+//
+// Note that this function *does not* dereference the remote account that the signature key is associated with, but it will
+// return the public key URL associated with that account, so that other functions can dereference it with that, as required.
+func AuthenticateFederatedRequest(transport pub.Transport, r *http.Request) (*url.URL, error) {
+ verifier, err := httpsig.NewVerifier(r)
+ if err != nil {
+ return nil, fmt.Errorf("could not create http sig verifier: %s", err)
+ }
+
+ // The key ID should be given in the signature so that we know where to fetch it from the remote server.
+ // This will be something like https://example.org/users/whatever_requesting_user#main-key
+ requestingPublicKeyID, err := url.Parse(verifier.KeyId())
+ if err != nil {
+ return nil, fmt.Errorf("could not parse key id into a url: %s", err)
+ }
+
+ // use the new transport to fetch the requesting public key from the remote server
+ 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)
+ }
+
+ // do the actual authentication here!
+ algo := httpsig.RSA_SHA256 // TODO: make this more robust
+ if err := verifier.Verify(requestingPublicKey, algo); err != nil {
+ return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err)
+ }
+
+ // all good!
+ return requestingPublicKeyID, nil
+}
+
+func DereferenceAccount(transport pub.Transport, publicKeyID *url.URL) (vocab.ActivityStreamsPerson, error) {
+ b, err := transport.Dereference(context.Background(), publicKeyID)
+ if err != nil {
+ return nil, fmt.Errorf("error deferencing %s: %s", publicKeyID.String(), err)
+ }
+
+ m := make(map[string]interface{})
+ if err := json.Unmarshal(b, &m); err != nil {
+ return nil, fmt.Errorf("error unmarshalling bytes into json: %s", err)
+ }
+
+ t, err := streams.ToType(context.Background(), m)
+ if err != nil {
+ return nil, fmt.Errorf("error resolving json into ap vocab type: %s", err)
+ }
+
+ switch t.GetTypeName() {
+ case string(gtsmodel.ActivityStreamsPerson):
+ p, ok := t.(vocab.ActivityStreamsPerson)
+ if !ok {
+ return nil, errors.New("error resolving type as activitystreams person")
+ }
+ return p, nil
+ case string(gtsmodel.ActivityStreamsApplication):
+ // TODO: convert application into person
+ }
+
+ return nil, fmt.Errorf("type name %s not supported", t.GetTypeName())
+}
diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go
index bc580718b..e39a697e4 100644
--- a/internal/gotosocial/actions.go
+++ b/internal/gotosocial/actions.go
@@ -67,6 +67,9 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr
return fmt.Errorf("error creating storage backend: %s", err)
}
+ // build converters and util
+ typeConverter := typeutils.NewConverter(c, dbService)
+
// build backend handlers
mediaHandler := media.New(c, dbService, storageBackend, log)
oauthServer := oauth.New(dbService, log)
@@ -75,19 +78,16 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr
return fmt.Errorf("error starting distributor: %s", err)
}
transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log)
- federator := federation.NewFederator(dbService, transportController, c, log, distributor)
-
- // build converters and util
- ic := typeutils.NewConverter(c, dbService)
+ federator := federation.NewFederator(dbService, transportController, c, log, distributor, typeConverter)
// build client api modules
authModule := auth.New(oauthServer, dbService, log)
- accountModule := account.New(c, dbService, oauthServer, mediaHandler, ic, log)
- appsModule := app.New(oauthServer, dbService, ic, log)
- mm := mediaModule.New(dbService, mediaHandler, ic, c, log)
+ accountModule := account.New(c, dbService, oauthServer, mediaHandler, typeConverter, log)
+ appsModule := app.New(oauthServer, dbService, typeConverter, log)
+ mm := mediaModule.New(dbService, mediaHandler, typeConverter, c, log)
fileServerModule := fileserver.New(c, dbService, storageBackend, log)
- adminModule := admin.New(c, dbService, mediaHandler, ic, log)
- statusModule := status.New(c, dbService, mediaHandler, ic, distributor, log)
+ adminModule := admin.New(c, dbService, mediaHandler, typeConverter, log)
+ statusModule := status.New(c, dbService, mediaHandler, typeConverter, distributor, log)
securityModule := security.New(c, log)
apiModules := []apimodule.ClientAPIModule{
diff --git a/internal/typeutils/astointernal.go b/internal/typeutils/astointernal.go
new file mode 100644
index 000000000..9b1615645
--- /dev/null
+++ b/internal/typeutils/astointernal.go
@@ -0,0 +1,10 @@
+package typeutils
+
+import (
+ "github.com/go-fed/activity/streams/vocab"
+ "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel"
+)
+
+func (c *converter) ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsmodel.Account, error) {
+ return nil, nil
+}
diff --git a/internal/typeutils/converter.go b/internal/typeutils/converter.go
index 4f19e07b2..c44e0d71c 100644
--- a/internal/typeutils/converter.go
+++ b/internal/typeutils/converter.go
@@ -85,6 +85,9 @@ type TypeConverter interface {
ACTIVITYSTREAMS MODEL TO INTERNAL (gts) MODEL
*/
+ // ASPersonToAccount converts an activitystreams person into a gts model account
+ ASPersonToAccount(person vocab.ActivityStreamsPerson) (*gtsmodel.Account, error)
+
/*
INTERNAL (gts) MODEL TO ACTIVITYSTREAMS MODEL
*/
diff --git a/internal/util/uri.go b/internal/util/uri.go
index ef7e66c2b..9b96edc61 100644
--- a/internal/util/uri.go
+++ b/internal/util/uri.go
@@ -53,19 +53,14 @@ const (
type APContextKey string
const (
- // APActivityKey can be used to set and retrieve the actual go-fed pub.Activity within a context.
- APActivityKey APContextKey = "activity"
- // APUsernameKey can be used to set and retrieve the username of the user being interacted with.
- APUsernameKey APContextKey = "username"
- // APAccountKey can be used the set and retrieve the account being interacted with
- APAccountKey APContextKey = "account"
- // APPrivateAccessKey can be used to set and retrieve whether or not the requesting account
- // OWNS the inbox or outbox being interacted with. If so, it should be able to see everything in that box.
- APPrivateAccessKey APContextKey = "privateAccess"
- // APRequestingHostKey can be used to set and retrieve the host of an incoming federation request.
- APRequestingHostKey APContextKey = "requestingHost"
- // APRequestingAccountKey can be used to set and retrieve the account of an incoming federation request.
- APRequestingAccountKey APContextKey = "requestingAccount"
+ // APActivity can be used to set and retrieve the actual go-fed pub.Activity within a context.
+ APActivity APContextKey = "activity"
+ // APAccount can be used the set and retrieve the account being interacted with
+ APAccount APContextKey = "account"
+ // APRequestingAccount can be used to set and retrieve the account of an incoming federation request.
+ APRequestingAccount APContextKey = "requestingAccount"
+ // APRequestingPublicKeyID can be used to set and retrieve the public key ID of an incoming federation request.
+ APRequestingPublicKeyID APContextKey = "requestingPublicKeyID"
)
type ginContextKey struct{}
diff --git a/testrig/actions.go b/testrig/actions.go
index 63645c796..07ffe7184 100644
--- a/testrig/actions.go
+++ b/testrig/actions.go
@@ -57,7 +57,7 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr
if err := distributor.Start(); err != nil {
return fmt.Errorf("error starting distributor: %s", err)
}
- mastoConverter := NewTestTypeConverter(dbService)
+ typeConverter := NewTestTypeConverter(dbService)
transportController := NewTestTransportController(NewMockHTTPClient(func(req *http.Request) (*http.Response, error) {
r := ioutil.NopCloser(bytes.NewReader([]byte{}))
return &http.Response{
@@ -65,19 +65,19 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr
Body: r,
}, nil
}))
- federator := federation.NewFederator(dbService, transportController, c, log, distributor)
+ federator := federation.NewFederator(dbService, transportController, c, log, distributor, typeConverter)
StandardDBSetup(dbService)
StandardStorageSetup(storageBackend, "./testrig/media")
// build client api modules
authModule := auth.New(oauthServer, dbService, log)
- accountModule := account.New(c, dbService, oauthServer, mediaHandler, mastoConverter, log)
- appsModule := app.New(oauthServer, dbService, mastoConverter, log)
- mm := mediaModule.New(dbService, mediaHandler, mastoConverter, c, log)
+ accountModule := account.New(c, dbService, oauthServer, mediaHandler, typeConverter, log)
+ appsModule := app.New(oauthServer, dbService, typeConverter, log)
+ mm := mediaModule.New(dbService, mediaHandler, typeConverter, c, log)
fileServerModule := fileserver.New(c, dbService, storageBackend, log)
- adminModule := admin.New(c, dbService, mediaHandler, mastoConverter, log)
- statusModule := status.New(c, dbService, mediaHandler, mastoConverter, distributor, log)
+ adminModule := admin.New(c, dbService, mediaHandler, typeConverter, log)
+ statusModule := status.New(c, dbService, mediaHandler, typeConverter, distributor, log)
securityModule := security.New(c, log)
apiModules := []apimodule.ClientAPIModule{