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{