diff --git a/go.mod b/go.mod index d1cefcf78..65dcf0490 100644 --- a/go.mod +++ b/go.mod @@ -7,7 +7,7 @@ require ( github.com/cpuguy83/go-md2man/v2 v2.0.0 // indirect github.com/gin-contrib/sessions v0.0.3 github.com/gin-gonic/gin v1.6.3 - github.com/go-fed/activity v1.0.0 + github.com/go-fed/activity v1.0.1-0.20210426194615-e0de0863dcc1 github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5 github.com/go-pg/pg/extra/pgdebug v0.2.0 github.com/go-pg/pg/v10 v10.8.0 diff --git a/go.sum b/go.sum index 7a8514c2a..341494d79 100644 --- a/go.sum +++ b/go.sum @@ -59,8 +59,8 @@ github.com/globalsign/mgo v0.0.0-20181015135952-eeefdecb41b8/go.mod h1:xkRDCp4j0 github.com/go-errors/errors v1.0.1/go.mod h1:f4zRHt4oKfwPJE5k8C9vpYG+aDHdBFUsgrm6/TyX73Q= github.com/go-errors/errors v1.0.2 h1:xMxH9j2fNg/L4hLn/4y3M0IUsn0M6Wbu/Uh9QlOfBh4= github.com/go-errors/errors v1.0.2/go.mod h1:psDX2osz5VnTOnFWbDeWwS7yejl+uV3FEWEp4lssFEs= -github.com/go-fed/activity v1.0.0 h1:j7w3auHZnVCjUcgA1mE+UqSOjFBhvW2Z2res3vNol+o= -github.com/go-fed/activity v1.0.0/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q= +github.com/go-fed/activity v1.0.1-0.20210426194615-e0de0863dcc1 h1:go9MogQW0eTLwdOs/ZfNCGpwUkVcr7IMUbI3u8wYQxw= +github.com/go-fed/activity v1.0.1-0.20210426194615-e0de0863dcc1/go.mod h1:v4QoPaAzjWZ8zN2VFVGL5ep9C02mst0hQYHUpQwso4Q= github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5 h1:WLvFZqoXnuVTBKA6U/1FnEHNQ0Rq0QM0rGhY8Tx6R1g= github.com/go-fed/httpsig v0.1.1-0.20190914113940-c2de3672e5b5/go.mod h1:T56HUNYZUQ1AGUzhAYPugZfp36sKApVnGBgKlIY+aIE= github.com/go-pg/pg/extra/pgdebug v0.2.0 h1:t62UhMiV6KYAxSWojwIJiyX06TdepkzCeIzdeb00184= diff --git a/internal/api/s2s/user/inboxpost.go b/internal/api/s2s/user/inboxpost.go new file mode 100644 index 000000000..7be36590a --- /dev/null +++ b/internal/api/s2s/user/inboxpost.go @@ -0,0 +1,56 @@ +/* + 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 user + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/message" +) + +func (m *Module) InboxPOSTHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "InboxPOSTHandler", + "url": c.Request.RequestURI, + }) + + requestedUsername := c.Param(UsernameKey) + if requestedUsername == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + return + } + + posted, err := m.processor.InboxPost(c.Request.Context(), c.Writer, c.Request) + if err != nil { + if withCode, ok := err.(message.ErrorWithCode); ok { + l.Debug(withCode.Error()) + c.JSON(withCode.Code(), withCode.Safe()) + return + } + l.Debug(err) + c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"}) + return + } + + if !posted { + c.JSON(http.StatusBadRequest, gin.H{"error": "unable to process request"}) + } +} diff --git a/internal/api/s2s/user/user.go b/internal/api/s2s/user/user.go index 693fac7c3..36c1389d1 100644 --- a/internal/api/s2s/user/user.go +++ b/internal/api/s2s/user/user.go @@ -38,6 +38,7 @@ const ( // Use this anywhere you need to know the username of the user being queried. // Eg https://example.org/users/:username UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey + UsersInboxPath = UsersBasePathWithUsername + "/" + util.InboxPath ) // ActivityPubAcceptHeaders represents the Accept headers mentioned here: @@ -66,5 +67,6 @@ func New(config *config.Config, processor message.Processor, log *logrus.Logger) // Route satisfies the RESTAPIModule interface func (m *Module) Route(s router.Router) error { s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler) + s.AttachHandler(http.MethodPost, UsersInboxPath, m.InboxPOSTHandler) return nil } diff --git a/internal/db/federating_db.go b/internal/db/federating_db.go index ab66b19de..50f74b407 100644 --- a/internal/db/federating_db.go +++ b/internal/db/federating_db.go @@ -104,6 +104,13 @@ func (f *federatingDB) Unlock(c context.Context, id *url.URL) error { // // The library makes this call only after acquiring a lock first. func (f *federatingDB) InboxContains(c context.Context, inbox, id *url.URL) (contains bool, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "InboxContains", + "id": id.String(), + }, + ) + l.Debug("entering INBOXCONTAINS function") if !util.IsInboxPath(inbox) { return false, fmt.Errorf("%s is not an inbox URI", inbox.String()) @@ -151,6 +158,14 @@ func (f *federatingDB) SetInbox(c context.Context, inbox vocab.ActivityStreamsOr // the database has an entry for the IRI. // The library makes this call only after acquiring a lock first. func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "Owns", + "id": id.String(), + }, + ) + l.Debug("entering OWNS function") + // if the id host isn't this instance host, we don't own this IRI if id.Host != f.config.Host { return false, nil @@ -199,6 +214,14 @@ func (f *federatingDB) Owns(c context.Context, id *url.URL) (bool, error) { // // The library makes this call only after acquiring a lock first. func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (actorIRI *url.URL, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "ActorForOutbox", + "inboxIRI": outboxIRI.String(), + }, + ) + l.Debug("entering ACTORFOROUTBOX function") + if !util.IsOutboxPath(outboxIRI) { return nil, fmt.Errorf("%s is not an outbox URI", outboxIRI.String()) } @@ -216,6 +239,14 @@ func (f *federatingDB) ActorForOutbox(c context.Context, outboxIRI *url.URL) (ac // // The library makes this call only after acquiring a lock first. func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (actorIRI *url.URL, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "ActorForInbox", + "inboxIRI": inboxIRI.String(), + }, + ) + l.Debug("entering ACTORFORINBOX function") + if !util.IsInboxPath(inboxIRI) { return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) } @@ -234,6 +265,14 @@ func (f *federatingDB) ActorForInbox(c context.Context, inboxIRI *url.URL) (acto // // The library makes this call only after acquiring a lock first. func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (outboxIRI *url.URL, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "OutboxForInbox", + "inboxIRI": inboxIRI.String(), + }, + ) + l.Debug("entering OUTBOXFORINBOX function") + if !util.IsInboxPath(inboxIRI) { return nil, fmt.Errorf("%s is not an inbox URI", inboxIRI.String()) } @@ -252,6 +291,14 @@ func (f *federatingDB) OutboxForInbox(c context.Context, inboxIRI *url.URL) (out // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Exists(c context.Context, id *url.URL) (exists bool, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "Exists", + "id": id.String(), + }, + ) + l.Debug("entering EXISTS function") + return false, nil } @@ -259,6 +306,13 @@ func (f *federatingDB) Exists(c context.Context, id *url.URL) (exists bool, err // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, err error) { + l := f.log.WithFields( + logrus.Fields{ + "func": "Get", + "id": id.String(), + }, + ) + l.Debug("entering GET function") return nil, nil } @@ -275,6 +329,13 @@ func (f *federatingDB) Get(c context.Context, id *url.URL) (value vocab.Type, er // Under certain conditions and network activities, Create may be called // multiple times for the same ActivityStreams object. func (f *federatingDB) Create(c context.Context, asType vocab.Type) error { + l := f.log.WithFields( + logrus.Fields{ + "func": "Create", + "asType": asType.GetTypeName(), + }, + ) + l.Debugf("received CREATE asType %+v", asType) return nil } @@ -288,6 +349,13 @@ func (f *federatingDB) Create(c context.Context, asType vocab.Type) error { // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Update(c context.Context, asType vocab.Type) error { + l := f.log.WithFields( + logrus.Fields{ + "func": "Update", + "asType": asType.GetTypeName(), + }, + ) + l.Debugf("received UPDATE asType %+v", asType) return nil } @@ -298,6 +366,13 @@ func (f *federatingDB) Update(c context.Context, asType vocab.Type) error { // // The library makes this call only after acquiring a lock first. func (f *federatingDB) Delete(c context.Context, id *url.URL) error { + l := f.log.WithFields( + logrus.Fields{ + "func": "Delete", + "id": id.String(), + }, + ) + l.Debugf("received DELETE id %s", id.String()) return nil } @@ -325,6 +400,14 @@ func (f *federatingDB) SetOutbox(c context.Context, outbox vocab.ActivityStreams // The go-fed library will handle setting the 'id' property on the // activity or object provided with the value returned. func (f *federatingDB) NewID(c context.Context, t vocab.Type) (id *url.URL, err error) { + + l := f.log.WithFields( + logrus.Fields{ + "func": "NewID", + "asType": t.GetTypeName(), + }, + ) + l.Debugf("received NEWID request for asType %+v", t) return nil, nil } diff --git a/internal/federation/commonbehavior.go b/internal/federation/commonbehavior.go index 9274e78b4..8ed6fd2cb 100644 --- a/internal/federation/commonbehavior.go +++ b/internal/federation/commonbehavior.go @@ -57,7 +57,7 @@ import ( // authenticated must be true and error nil. The request will continue // to be processed. func (f *federator) AuthenticateGetInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { - // IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through + // IMPLEMENTATION NOTE: For GoToSocial, we serve GETS to outboxes and inboxes through // the CLIENT API, not through the federation API, so we just do nothing here. return nil, false, nil } @@ -82,7 +82,7 @@ func (f *federator) AuthenticateGetInbox(ctx context.Context, w http.ResponseWri // authenticated must be true and error nil. The request will continue // to be processed. func (f *federator) AuthenticateGetOutbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { - // IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through + // IMPLEMENTATION NOTE: For GoToSocial, we serve GETS to outboxes and inboxes through // the CLIENT API, not through the federation API, so we just do nothing here. return nil, false, nil } @@ -96,7 +96,7 @@ func (f *federator) AuthenticateGetOutbox(ctx context.Context, w http.ResponseWr // Always called, regardless whether the Federated Protocol or Social // API is enabled. func (f *federator) GetOutbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) { - // IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through + // IMPLEMENTATION NOTE: For GoToSocial, we serve GETS to 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/federatingactor.go b/internal/federation/federatingactor.go index f105d9125..65a16efaf 100644 --- a/internal/federation/federatingactor.go +++ b/internal/federation/federatingactor.go @@ -77,6 +77,13 @@ func (f *federatingActor) PostInbox(c context.Context, w http.ResponseWriter, r return f.actor.PostInbox(c, w, r) } +// PostInboxScheme is similar to PostInbox, except clients are able to +// specify which protocol scheme to handle the incoming request and the +// data stored within the application (HTTP, HTTPS, etc). +func (f *federatingActor) PostInboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) { + return f.actor.PostInboxScheme(c, w, r, scheme) +} + // 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 @@ -118,6 +125,13 @@ func (f *federatingActor) PostOutbox(c context.Context, w http.ResponseWriter, r return f.actor.PostOutbox(c, w, r) } +// PostOutboxScheme is similar to PostOutbox, except clients are able to +// specify which protocol scheme to handle the incoming request and the +// data stored within the application (HTTP, HTTPS, etc). +func (f *federatingActor) PostOutboxScheme(c context.Context, w http.ResponseWriter, r *http.Request, scheme string) (bool, error) { + return f.actor.PostOutboxScheme(c, w, r, scheme) +} + // 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. diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index 1764eb791..22769317a 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -72,6 +72,7 @@ func (f *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Reques return nil, err } + ctxWithActivity := context.WithValue(ctx, util.APActivity, activity) return ctxWithActivity, nil } @@ -100,14 +101,22 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr }) l.Trace("received request to authenticate") - requestedAccountI := ctx.Value(util.APAccount) - if requestedAccountI == nil { - return ctx, false, errors.New("requested account not set in context") + if !util.IsInboxPath(r.URL) { + return nil, false, fmt.Errorf("path %s was not an inbox path", r.URL.String()) } - requestedAccount, ok := requestedAccountI.(*gtsmodel.Account) - if !ok || requestedAccount == nil { - return ctx, false, errors.New("requested account not parsebale from context") + username, err := util.ParseInboxPath(r.URL) + if err != nil { + return nil, false, fmt.Errorf("could not parse path %s: %s", r.URL.String(), err) + } + + if username == "" { + return nil, false, errors.New("username was empty") + } + + requestedAccount := >smodel.Account{} + if err := f.db.GetLocalAccountByUsername(username, requestedAccount); err != nil { + return nil, false, fmt.Errorf("could not fetch requested account with username %s: %s", username, err) } publicKeyOwnerURI, err := f.AuthenticateFederatedRequest(requestedAccount.Username, r) @@ -124,7 +133,6 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr } // we don't know this account (yet) so let's dereference it right now - // TODO: slow-fed person, err := f.DereferenceRemoteAccount(requestedAccount.Username, publicKeyOwnerURI) if err != nil { return ctx, false, fmt.Errorf("error dereferencing account with public key id %s: %s", publicKeyOwnerURI.String(), err) @@ -138,7 +146,6 @@ func (f *federator) AuthenticatePostInbox(ctx context.Context, w http.ResponseWr } contextWithRequestingAccount := context.WithValue(ctx, util.APRequestingAccount, requestingAccount) - return contextWithRequestingAccount, true, nil } @@ -180,9 +187,9 @@ func (f *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, er // // Applications are not expected to handle every single ActivityStreams // type and extension. The unhandled ones are passed to DefaultCallback. -func (f *federator) FederatingCallbacks(ctx context.Context) (pub.FederatingWrappedCallbacks, []interface{}, error) { - // TODO - return pub.FederatingWrappedCallbacks{}, nil, nil +func (f *federator) FederatingCallbacks(ctx context.Context) (wrapped pub.FederatingWrappedCallbacks, other []interface{}, err error) { + + return } // DefaultCallback is called for types that go-fed can deserialize but @@ -207,7 +214,7 @@ func (f *federator) DefaultCallback(ctx context.Context, activity pub.Activity) // Zero or negative numbers indicate infinite recursion. func (f *federator) MaxInboxForwardingRecursionDepth(ctx context.Context) int { // TODO - return 0 + return 4 } // MaxDeliveryRecursionDepth determines how deep to search within @@ -217,7 +224,7 @@ func (f *federator) MaxInboxForwardingRecursionDepth(ctx context.Context) int { // Zero or negative numbers indicate infinite recursion. func (f *federator) MaxDeliveryRecursionDepth(ctx context.Context) int { // TODO - return 0 + return 4 } // FilterForwarding allows the implementation to apply business logic @@ -241,7 +248,7 @@ func (f *federator) FilterForwarding(ctx context.Context, potentialRecipients [] // Always called, regardless whether the Federated Protocol or Social // API is enabled. func (f *federator) GetInbox(ctx context.Context, r *http.Request) (vocab.ActivityStreamsOrderedCollectionPage, error) { - // IMPLEMENTATION NOTE: For GoToSocial, we serve outboxes and inboxes through + // IMPLEMENTATION NOTE: For GoToSocial, we serve GETS to 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/message/fediprocess.go b/internal/message/fediprocess.go index dad1e848c..133e7dbaa 100644 --- a/internal/message/fediprocess.go +++ b/internal/message/fediprocess.go @@ -1,6 +1,7 @@ package message import ( + "context" "fmt" "net/http" @@ -8,6 +9,7 @@ import ( apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // authenticateAndDereferenceFediRequest authenticates the HTTP signature of an incoming federation request, using the given @@ -130,3 +132,9 @@ func (p *processor) GetWebfingerAccount(requestedUsername string, request *http. }, }, nil } + +func (p *processor) InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) { + contextWithChannel := context.WithValue(ctx, util.APFromFederatorChanKey, p.fromFederator) + posted, err := p.federator.FederatingActor().PostInbox(contextWithChannel, w, r) + return posted, err +} diff --git a/internal/message/processor.go b/internal/message/processor.go index c8d58a346..9d7f0c6c9 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -19,6 +19,7 @@ package message import ( + "context" "net/http" "github.com/sirupsen/logrus" @@ -116,6 +117,18 @@ type Processor interface { // GetWebfingerAccount handles the GET for a webfinger resource. Most commonly, it will be used for returning account lookups. GetWebfingerAccount(requestedUsername string, request *http.Request) (*apimodel.WebfingerAccountResponse, ErrorWithCode) + + // InboxPost handles POST requests to a user's inbox for new activitypub messages. + // + // InboxPost 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. + InboxPost(ctx context.Context, w http.ResponseWriter, r *http.Request) (bool, error) } // processor just implements the Processor interface diff --git a/internal/transport/controller.go b/internal/transport/controller.go index 2ee23f141..108f749cb 100644 --- a/internal/transport/controller.go +++ b/internal/transport/controller.go @@ -54,8 +54,8 @@ func NewController(config *config.Config, clock pub.Clock, client pub.HttpClient func (c *controller) NewTransport(pubKeyID string, privkey crypto.PrivateKey) (pub.Transport, error) { prefs := []httpsig.Algorithm{httpsig.RSA_SHA256, httpsig.RSA_SHA512} digestAlgo := httpsig.DigestSha256 - getHeaders := []string{"(request-target)", "date", "accept"} - postHeaders := []string{"(request-target)", "date", "accept", "digest"} + getHeaders := []string{"(request-target)", "host", "date"} + postHeaders := []string{"(request-target)", "host", "date", "digest"} getSigner, _, err := httpsig.NewSigner(prefs, digestAlgo, getHeaders, httpsig.Signature) if err != nil { diff --git a/internal/util/uri.go b/internal/util/uri.go index 538df9210..2b0020b4e 100644 --- a/internal/util/uri.go +++ b/internal/util/uri.go @@ -61,6 +61,8 @@ const ( APRequestingAccount APContextKey = "requestingAccount" // APRequestingPublicKeyID can be used to set and retrieve the public key ID of an incoming federation request. APRequestingPublicKeyID APContextKey = "requestingPublicKeyID" + // APFromFederatorChanKey can be used to pass a pointer to the fromFederator channel into the federator for use in callbacks. + APFromFederatorChanKey APContextKey = "fromFederatorChan" ) type ginContextKey struct{}