diff --git a/internal/apimodule/account/accountcreate_test.go b/internal/apimodule/account/accountcreate_test.go index 6e5c3a8eb..e84b02583 100644 --- a/internal/apimodule/account/accountcreate_test.go +++ b/internal/apimodule/account/accountcreate_test.go @@ -274,7 +274,7 @@ func (suite *AccountCreateTestSuite) TestAccountCreatePOSTHandlerSuccessful() { // 1. we should be able to get the new account from the db acct := >smodel.Account{} - err = suite.db.GetWhere("username", "test_user", acct) + err = suite.db.GetLocalAccountByUsername("test_user", acct) assert.NoError(suite.T(), err) assert.NotNil(suite.T(), acct) // 2. reason should be set diff --git a/internal/apimodule/fileserver/servefile.go b/internal/apimodule/fileserver/servefile.go index 0421c5095..59b06177f 100644 --- a/internal/apimodule/fileserver/servefile.go +++ b/internal/apimodule/fileserver/servefile.go @@ -204,7 +204,7 @@ func (m *FileServer) serveEmoji(c *gin.Context, accountID string, mediaType stri // make sure the instance account id owns the requested emoji instanceAccount := >smodel.Account{} - if err := m.db.GetWhere("username", m.config.Host, instanceAccount); err != nil { + if err := m.db.GetLocalAccountByUsername(m.config.Host, instanceAccount); err != nil { l.Debugf("error fetching instance account: %s", err) c.String(http.StatusNotFound, "404 page not found") return diff --git a/internal/db/db.go b/internal/db/db.go index 69ad7b822..4e3bc87e2 100644 --- a/internal/db/db.go +++ b/internal/db/db.go @@ -126,6 +126,12 @@ type DB interface { // In case of no entries, a 'no entries' error will be returned GetAccountByUserID(userID string, account *gtsmodel.Account) error + // GetLocalAccountByUsername is a shortcut for the common action of fetching an account ON THIS INSTANCE + // according to its username, which should be unique. + // The given account pointer will be set to the result of the query, whatever it is. + // In case of no entries, a 'no entries' error will be returned + GetLocalAccountByUsername(username string, account *gtsmodel.Account) error + // GetFollowRequestsForAccountID is a shortcut for the common action of fetching a list of follow requests targeting the given account ID. // The given slice 'followRequests' will be set to the result of the query, whatever it is. // In case of no entries, a 'no entries' error will be returned diff --git a/internal/db/federating_db.go b/internal/db/federating_db.go index f25d32ed7..401a0edf5 100644 --- a/internal/db/federating_db.go +++ b/internal/db/federating_db.go @@ -147,17 +147,13 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) } acct := >smodel.Account{} - if err := f.db.GetWhere("username", username, acct); err != nil { + if err := f.db.GetLocalAccountByUsername(username, acct); err != nil { if _, ok := err.(ErrNoEntries); ok { // there are no entries for this username return false, nil } return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) } - if acct.Domain != "" { - // this is a remote account so we don't own it after all - return false, nil - } status := >smodel.Status{} if err := f.db.GetByID(uid, status); err != nil { if _, ok := err.(ErrNoEntries); ok { @@ -177,17 +173,13 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { return false, fmt.Errorf("error parsing statuses path for url %s: %s", id.String(), err) } acct := >smodel.Account{} - if err := f.db.GetWhere("username", username, acct); err != nil { + if err := f.db.GetLocalAccountByUsername(username, acct); err != nil { if _, ok := err.(ErrNoEntries); ok { // there are no entries for this username return false, nil } return false, fmt.Errorf("database error fetching account with username %s: %s", username, err) } - if acct.Domain != "" { - // this is a remote account so we don't own it after all - return false, nil - } // the user exists, we own it, we're good return true, nil } diff --git a/internal/db/pg.go b/internal/db/pg.go index 33ce26ba7..46ee680f8 100644 --- a/internal/db/pg.go +++ b/internal/db/pg.go @@ -341,6 +341,16 @@ func (ps *postgresService) GetAccountByUserID(userID string, account *gtsmodel.A return nil } +func (ps *postgresService) GetLocalAccountByUsername(username string, account *gtsmodel.Account) error { + if err := ps.conn.Model(account).Where("username = ?", username).Where("? IS NULL", pg.Ident("domain")).Select(); err != nil { + if err == pg.ErrNoRows { + return ErrNoEntries{} + } + return err + } + return nil +} + func (ps *postgresService) GetFollowRequestsForAccountID(accountID string, followRequests *[]gtsmodel.FollowRequest) error { if err := ps.conn.Model(followRequests).Where("target_account_id = ?", accountID).Select(); err != nil { if err == pg.ErrNoRows { diff --git a/internal/federation/actor.go b/internal/federation/actor.go deleted file mode 100644 index 061f4709f..000000000 --- a/internal/federation/actor.go +++ /dev/null @@ -1,37 +0,0 @@ -/* - 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 provides ActivityPub/federation functionality for GoToSocial -package federation - -import ( - "github.com/go-fed/activity/pub" - "github.com/sirupsen/logrus" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/transport" -) - -// NewFederatingActor returns a go-fed compatible federating actor -func NewFederatingActor(db db.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger) pub.FederatingActor { - c := NewCommonBehavior(db, log, config, transportController) - f := NewFederatingProtocol(db, log, config, transportController) - pubDatabase := db.Federation() - clock := &Clock{} - return pub.NewFederatingActor(c, f, pubDatabase, clock) -} diff --git a/internal/federation/common.go b/internal/federation/commonbehavior.go similarity index 78% rename from internal/federation/common.go rename to internal/federation/commonbehavior.go index 92cd962eb..cbda9e7ef 100644 --- a/internal/federation/common.go +++ b/internal/federation/commonbehavior.go @@ -20,19 +20,16 @@ package federation import ( "context" - "errors" "fmt" "net/http" "net/url" - "github.com/gin-gonic/gin" "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/db/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/transport" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -45,8 +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 -func NewCommonBehavior(db db.DB, log *logrus.Logger, config *config.Config, transportController transport.Controller) pub.CommonBehavior { +// newCommonBehavior returns an implementation of the pub.CommonBehavior interface that uses the given db, log, config, and transportController +func newCommonBehavior(db db.DB, log *logrus.Logger, config *config.Config, transportController transport.Controller) pub.CommonBehavior { return &commonBehavior{ db: db, log: log, @@ -82,44 +79,8 @@ func NewCommonBehavior(db db.DB, log *logrus.Logger, config *config.Config, tran // authenticated must be true and error nil. The request will continue // to be processed. func (c *commonBehavior) AuthenticateGetInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { - l := c.log.WithFields(logrus.Fields{ - "func": "AuthenticateGetInbox", - "url": r.URL.String(), - }) - - if !util.IsInboxPath(r.URL) { - err := errors.New("url %s was not for an inbox", r.URL.String()) - } - - // Earlier in the chain before this function was called, we set a *copy* of the *gin.Context as a value on the context.Context, - // this means that we can retrieve that and use it to check whether we're authorized or not. - - // retrieve what should be a copy of a *gin.Context from the context.Context - gctxI := ctx.Value(util.GinContextKey) - if gctxI == nil { - err := errors.New("AuthenticateGetInbox: nothing was set on the gincontext key of context.Context") - l.Error(err) - return nil, false, err - } - - // cast it to what is hopefully a *gin.Context - gctx, ok := gctxI.(*gin.Context) - if !ok { - err := errors.New("AuthenticateGetInbox: something was set on context.Context but it wasn't a *gin.Context") - l.Error(err) - return nil, false, err - } - - authed, err := oauth.MustAuth(gctx, true, false, true, true) // we need a token, user, and account to be considered 'authed' - if err != nil { - // whatever happened, we're not authorized -- we don't care so much about an error at this point so just log it and move on - l.Debugf("not authed: %s", err) - return ctx, false, nil - } - - // we need the check now that the authed user is the same as the user that the inbox belongs to - - + // 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, false, nil } @@ -143,7 +104,8 @@ func (c *commonBehavior) AuthenticateGetInbox(ctx context.Context, w http.Respon // authenticated must be true and error nil. The request will continue // to be processed. func (c *commonBehavior) AuthenticateGetOutbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { - // TODO + // 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, false, nil } @@ -156,7 +118,8 @@ func (c *commonBehavior) AuthenticateGetOutbox(ctx context.Context, w http.Respo // Always called, regardless whether the Federated Protocol or Social // API is enabled. func (c *commonBehavior) GetOutbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) { - // TODO + // 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 } @@ -203,8 +166,8 @@ func (c *commonBehavior) NewTransport(ctx context.Context, actorBoxIRI *url.URL, } account := >smodel.Account{} - if err := c.db.GetWhere("username", username, account); err != nil { - return nil, err + if err := c.db.GetLocalAccountByUsername(username, account); err != nil { + return nil, fmt.Errorf("error getting account with username %s from the db: %s", username, err) } return c.transportController.NewTransport(account.PublicKeyURI, account.PrivateKey) diff --git a/internal/federation/protocol.go b/internal/federation/federatingprotocol.go similarity index 68% rename from internal/federation/protocol.go rename to internal/federation/federatingprotocol.go index 84cae3ac1..035d0b437 100644 --- a/internal/federation/protocol.go +++ b/internal/federation/federatingprotocol.go @@ -27,9 +27,11 @@ 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/util" ) @@ -92,23 +94,32 @@ func (f *FederatingProtocol) PostInboxRequestBodyHook(ctx context.Context, r *ht } if !util.IsInboxPath(r.URL) { - err := fmt.Errorf("url %s did not corresponding to inbox path", r.URL.String()) + err := fmt.Errorf("url %s did not correspond to inbox path", r.URL.String()) l.Debug(err) return nil, err } - username, err := util.ParseInboxPath(r.URL) + 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", username, r.URL.String()) - + l.Tracef("parsed username %s from %s", inboxUsername, r.URL.String()) l.Tracef("signature: %s", r.Header.Get("Signature")) - ctxWithUsername := context.WithValue(ctx, util.APUsernameKey, username) - ctxWithActivity := context.WithValue(ctxWithUsername, util.APActivityKey, activity) + // 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) return ctxWithActivity, nil } @@ -128,6 +139,16 @@ 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) { l := f.log.WithFields(logrus.Fields{ "func": "AuthenticatePostInbox", @@ -136,21 +157,64 @@ func (f *FederatingProtocol) AuthenticatePostInbox(ctx context.Context, w http.R }) l.Trace("received request to authenticate") - if !util.IsInboxPath(r.URL) { - err := fmt.Errorf("url %s did not corresponding to inbox path", r.URL.String()) - l.Debug(err) - return nil, false, err + // 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") } - username, err := util.ParseInboxPath(r.URL) + v, err := httpsig.NewVerifier(r) if err != nil { - err := fmt.Errorf("could not parse username from url: %s", r.URL.String()) - l.Debug(err) - return nil, false, err + return ctx, false, fmt.Errorf("could not create http sig verifier: %s", err) } - l.Tracef("parsed username %s from %s", username, r.URL.String()) - return validateInboundFederationRequest(ctx, r, f.db, username, f.transportController) + 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) + } + + transport, err := f.transportController.NewTransport(requestedAccount.PublicKeyURI, requestedAccount.PrivateKey) + if err != nil { + return ctx, false, fmt.Errorf("error creating new transport: %s", err) + } + + b, err := transport.Dereference(ctx, requestingPublicKeyID) + if err != nil { + return ctx, false, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), 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 { + if _, ok := err.(db.ErrNoEntries); !ok { + return ctx, false, fmt.Errorf("database error finding account with public key uri %s: %s", requestingPublicKeyID.String(), err) + } + // do nothing here, requestingAccount will stay nil and we'll have to figure it out further down the line + } + + // 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 } // Blocked should determine whether to permit a set of actors given by @@ -252,6 +316,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) { - // TODO + // 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 ee6c1f327..a1c27b0ea 100644 --- a/internal/federation/federator.go +++ b/internal/federation/federator.go @@ -19,23 +19,292 @@ 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" ) // Federator wraps everything needed 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) } type federator struct { - actor pub.FederatingActor - distributor distributor.Distributor + actor pub.FederatingActor + distributor distributor.Distributor + federatingProtocol pub.FederatingProtocol + commonBehavior pub.CommonBehavior + clock pub.Clock } -// NewFederator returns a new federator that uses the given actor and distributor -func NewFederator(actor pub.FederatingActor, distributor distributor.Distributor) Federator { +// NewFederator returns a new federator +func NewFederator(db db.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, distributor distributor.Distributor) Federator { + + clock := &Clock{} + federatingProtocol := NewFederatingProtocol(db, log, config, transportController) + commonBehavior := newCommonBehavior(db, log, config, transportController) + actor := pub.NewFederatingActor(commonBehavior, federatingProtocol, db.Federation(), clock) + return &federator{ - actor: actor, - distributor: distributor, + actor: actor, + distributor: distributor, + federatingProtocol: federatingProtocol, + commonBehavior: commonBehavior, + clock: clock, } } + +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) PostInboxRequestBodyHook(c context.Context, r *http.Request, activity pub.Activity) (context.Context, error) { + return f.federatingProtocol.PostInboxRequestBodyHook(c, r, activity) +} + +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) 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) +} diff --git a/internal/federation/protocol_test.go b/internal/federation/federator_test.go similarity index 87% rename from internal/federation/protocol_test.go rename to internal/federation/federator_test.go index d83c0e3c5..91d1cef7f 100644 --- a/internal/federation/protocol_test.go +++ b/internal/federation/federator_test.go @@ -38,6 +38,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/distributor" "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/util" "github.com/superseriousbusiness/gotosocial/testrig" @@ -48,6 +49,7 @@ type ProtocolTestSuite struct { config *config.Config db db.DB log *logrus.Logger + distributor distributor.Distributor accounts map[string]*gtsmodel.Account activities map[string]testrig.ActivityWithSignature } @@ -58,6 +60,7 @@ func (suite *ProtocolTestSuite) SetupSuite() { suite.config = testrig.NewTestConfig() suite.db = testrig.NewTestDB() suite.log = testrig.NewTestLog() + suite.distributor = testrig.NewTestDistributor() suite.accounts = testrig.NewTestAccounts() suite.activities = testrig.NewTestActivities(suite.accounts) } @@ -83,7 +86,7 @@ func (suite *ProtocolTestSuite) TestPostInboxRequestBodyHook() { return nil, nil })) // setup module being tested - federator := federation.NewFederatingProtocol(suite.db, suite.log, suite.config, tc).(*federation.FederatingProtocol) + federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.distributor) // setup request ctx := context.Background() @@ -117,6 +120,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() { // the activity we're gonna use activity := suite.activities["dm_for_zork"] sendingAccount := suite.accounts["remote_account_1"] + inboxAccount := suite.accounts["local_account_1"] encodedPublicKey, err := x509.MarshalPKIXPublicKey(sendingAccount.PublicKey) assert.NoError(suite.T(), err) @@ -160,14 +164,22 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() { // 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) + 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 request.Header.Set("Signature", activity.SignatureHeader) request.Header.Set("Date", activity.DateHeader) request.Header.Set("Digest", activity.DigestHeader) + // we can pass this recorder as a writer and read it back after + recorder := httptest.NewRecorder() // trigger the function being tested, and return the new context it creates - newContext, authed, err := federator.AuthenticatePostInbox(ctx, nil, request) + newContext, authed, err := federator.AuthenticatePostInbox(ctxWithActivity, recorder, request) assert.NoError(suite.T(), err) assert.True(suite.T(), authed) diff --git a/internal/federation/util.go b/internal/federation/util.go index dff73cae7..7c43c58b5 100644 --- a/internal/federation/util.go +++ b/internal/federation/util.go @@ -25,17 +25,11 @@ import ( "encoding/json" "encoding/pem" "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" - "github.com/superseriousbusiness/gotosocial/internal/db/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/transport" - "github.com/superseriousbusiness/gotosocial/internal/util" ) /* @@ -107,66 +101,3 @@ func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (p cr p, err = x509.ParsePKIXPublicKey(block.Bytes) return } - -// validateInboundFederationRequest 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 validateInboundFederationRequest(ctx context.Context, request *http.Request, dbConn db.DB, inboxUsername string, transportController transport.Controller) (context.Context, bool, error) { - v, err := httpsig.NewVerifier(request) - 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 := >smodel.Account{} - if err := dbConn.GetWhere("username", inboxUsername, requestedAccount); err != nil { - return ctx, false, fmt.Errorf("could not fetch username %s from the database: %s", inboxUsername, err) - } - - transport, err := transportController.NewTransport(requestedAccount.PublicKeyURI, requestedAccount.PrivateKey) - if err != nil { - return ctx, false, fmt.Errorf("error creating new transport: %s", err) - } - - b, err := transport.Dereference(ctx, requestingPublicKeyID) - if err != nil { - return ctx, false, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), 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 := dbConn.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 { - if _, ok := err.(db.ErrNoEntries); !ok { - return ctx, false, fmt.Errorf("database error finding account with public key uri %s: %s", requestingPublicKeyID.String(), err) - } - // do nothing here, requestingAccount will stay nil and we'll have to figure it out further down the line - } - - // 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 -} diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go index 89b65f5e4..bc580718b 100644 --- a/internal/gotosocial/actions.go +++ b/internal/gotosocial/actions.go @@ -75,8 +75,7 @@ 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) - federatingActor := federation.NewFederatingActor(dbService, transportController, c, log) - federator := federation.NewFederator(federatingActor, distributor) + federator := federation.NewFederator(dbService, transportController, c, log, distributor) // build converters and util ic := typeutils.NewConverter(c, dbService) diff --git a/internal/media/media.go b/internal/media/media.go index df8c01e48..07b1d027c 100644 --- a/internal/media/media.go +++ b/internal/media/media.go @@ -218,7 +218,7 @@ func (mh *mediaHandler) ProcessLocalEmoji(emojiBytes []byte, shortcode string) ( // (ie., fileserver/ACCOUNT_ID/etc etc) we need to fetch the INSTANCE ACCOUNT from the database. That is, the account that's created // with the same username as the instance hostname, which doesn't belong to any particular user. instanceAccount := >smodel.Account{} - if err := mh.db.GetWhere("username", mh.config.Host, instanceAccount); err != nil { + if err := mh.db.GetLocalAccountByUsername(mh.config.Host, instanceAccount); err != nil { return nil, fmt.Errorf("error fetching instance account: %s", err) } diff --git a/internal/util/uri.go b/internal/util/uri.go index e99fa7762..ef7e66c2b 100644 --- a/internal/util/uri.go +++ b/internal/util/uri.go @@ -57,6 +57,11 @@ const ( 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. @@ -64,6 +69,7 @@ const ( ) type ginContextKey struct{} + // GinContextKey is used solely for setting and retrieving the gin context from a context.Context var GinContextKey = &ginContextKey{} diff --git a/testrig/actions.go b/testrig/actions.go index 906a2bdc8..63645c796 100644 --- a/testrig/actions.go +++ b/testrig/actions.go @@ -65,8 +65,7 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr Body: r, }, nil })) - federatingActor := federation.NewFederatingActor(dbService, transportController, c, log) - federator := federation.NewFederator(federatingActor, distributor) + federator := federation.NewFederator(dbService, transportController, c, log, distributor) StandardDBSetup(dbService) StandardStorageSetup(storageBackend, "./testrig/media")