diff --git a/internal/api/apimodule.go b/internal/api/apimodule.go index 5d9fc8d59..096203251 100644 --- a/internal/api/apimodule.go +++ b/internal/api/apimodule.go @@ -34,5 +34,5 @@ type ClientModule interface { // 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 FederationModule interface { - Route(s router.Router) error + Route(s router.Router) error } diff --git a/internal/api/client/auth/auth.go b/internal/api/client/auth/auth.go index 471383a75..793c19f4e 100644 --- a/internal/api/client/auth/auth.go +++ b/internal/api/client/auth/auth.go @@ -24,7 +24,8 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/message" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/oauth" "github.com/superseriousbusiness/gotosocial/internal/router" ) @@ -39,17 +40,19 @@ const ( // Module implements the ClientAPIModule interface for type Module struct { - config *config.Config - processor message.Processor - log *logrus.Logger + config *config.Config + db db.DB + server oauth.Server + log *logrus.Logger } // New returns a new auth module -func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.ClientModule { +func New(config *config.Config, db db.DB, server oauth.Server, log *logrus.Logger) api.ClientModule { return &Module{ - config: config, - processor: processor, - log: log, + config: config, + db: db, + server: server, + log: log, } } diff --git a/internal/api/client/auth/middleware.go b/internal/api/client/auth/middleware.go index 8ae3b49ca..c42ba77fc 100644 --- a/internal/api/client/auth/middleware.go +++ b/internal/api/client/auth/middleware.go @@ -30,7 +30,7 @@ import ( // If user or account can't be found, then the handler won't *fail*, in case the server wants to allow // public requests that don't have a Bearer token set (eg., for public instance information and so on). func (m *Module) OauthTokenMiddleware(c *gin.Context) { - l := m.log.WithField("func", "ValidatePassword") + l := m.log.WithField("func", "OauthTokenMiddleware") l.Trace("entering OauthTokenMiddleware") ti, err := m.server.ValidationBearerToken(c.Request) diff --git a/internal/api/client/fileserver/servefile.go b/internal/api/client/fileserver/servefile.go index ef8787661..9823eb387 100644 --- a/internal/api/client/fileserver/servefile.go +++ b/internal/api/client/fileserver/servefile.go @@ -85,6 +85,7 @@ func (m *FileServer) ServeFile(c *gin.Context) { FileName: fileName, }) if err != nil { + l.Debug(err) c.String(http.StatusNotFound, "404 page not found") return } diff --git a/internal/api/client/fileserver/servefile_test.go b/internal/api/client/fileserver/servefile_test.go index 62849ec9a..09fd8ea43 100644 --- a/internal/api/client/fileserver/servefile_test.go +++ b/internal/api/client/fileserver/servefile_test.go @@ -33,6 +33,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/api/client/fileserver" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/message" @@ -49,6 +50,7 @@ type ServeFileTestSuite struct { db db.DB log *logrus.Logger storage storage.Storage + federator federation.Federator tc typeutils.TypeConverter processor message.Processor mediaHandler media.Handler @@ -76,7 +78,8 @@ func (suite *ServeFileTestSuite) SetupSuite() { suite.db = testrig.NewTestDB() suite.log = testrig.NewTestLog() suite.storage = testrig.NewTestStorage() - suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) suite.tc = testrig.NewTestTypeConverter(suite.db) suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) suite.oauthServer = testrig.NewTestOauthServer(suite.db) diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index 18823ad6c..e86c66021 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -36,6 +36,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/message" @@ -52,6 +53,7 @@ type MediaCreateTestSuite struct { db db.DB log *logrus.Logger storage storage.Storage + federator federation.Federator tc typeutils.TypeConverter mediaHandler media.Handler oauthServer oauth.Server @@ -82,7 +84,8 @@ func (suite *MediaCreateTestSuite) SetupSuite() { suite.tc = testrig.NewTestTypeConverter(suite.db) suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) suite.oauthServer = testrig.NewTestOauthServer(suite.db) - suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) // setup module being tested suite.mediaModule = mediamodule.New(suite.config, suite.processor, suite.log).(*mediamodule.Module) diff --git a/internal/api/client/status/status_test.go b/internal/api/client/status/status_test.go index 1cc3231a0..a18787aef 100644 --- a/internal/api/client/status/status_test.go +++ b/internal/api/client/status/status_test.go @@ -1,3 +1,21 @@ +/* + 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 status_test import ( @@ -6,6 +24,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/api/client/status" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/message" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -20,6 +39,7 @@ type StatusStandardTestSuite struct { db db.DB log *logrus.Logger tc typeutils.TypeConverter + federator federation.Federator processor message.Processor storage storage.Storage diff --git a/internal/api/client/status/statuscreate.go b/internal/api/client/status/statuscreate.go index daf8922b5..02080b042 100644 --- a/internal/api/client/status/statuscreate.go +++ b/internal/api/client/status/statuscreate.go @@ -68,7 +68,7 @@ func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { mastoStatus, err := m.processor.StatusCreate(authed, form) if err != nil { l.Debugf("error processing status create: %s", err) - c.JSON(http.StatusBadRequest, gin.H{"error":"bad request"}) + c.JSON(http.StatusBadRequest, gin.H{"error": "bad request"}) return } diff --git a/internal/api/client/status/statuscreate_test.go b/internal/api/client/status/statuscreate_test.go index 58e301a04..fb9b48f8a 100644 --- a/internal/api/client/status/statuscreate_test.go +++ b/internal/api/client/status/statuscreate_test.go @@ -56,7 +56,8 @@ func (suite *StatusCreateTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) testrig.StandardDBSetup(suite.db) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") diff --git a/internal/api/client/status/statusfave_test.go b/internal/api/client/status/statusfave_test.go index 6c6403988..2f779baed 100644 --- a/internal/api/client/status/statusfave_test.go +++ b/internal/api/client/status/statusfave_test.go @@ -55,7 +55,8 @@ func (suite *StatusFaveTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) testrig.StandardDBSetup(suite.db) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") diff --git a/internal/api/client/status/statusfavedby_test.go b/internal/api/client/status/statusfavedby_test.go index 62c027a39..7b72df7bc 100644 --- a/internal/api/client/status/statusfavedby_test.go +++ b/internal/api/client/status/statusfavedby_test.go @@ -55,7 +55,8 @@ func (suite *StatusFavedByTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) testrig.StandardDBSetup(suite.db) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") diff --git a/internal/api/client/status/statusget_test.go b/internal/api/client/status/statusget_test.go index 9bc12d250..b31acebca 100644 --- a/internal/api/client/status/statusget_test.go +++ b/internal/api/client/status/statusget_test.go @@ -45,7 +45,8 @@ func (suite *StatusGetTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) testrig.StandardDBSetup(suite.db) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") @@ -56,7 +57,6 @@ func (suite *StatusGetTestSuite) TearDownTest() { testrig.StandardStorageTeardown(suite.storage) } - // Post a new status with some custom visibility settings func (suite *StatusGetTestSuite) TestPostNewStatus() { diff --git a/internal/api/client/status/statusunfave_test.go b/internal/api/client/status/statusunfave_test.go index daf4b0696..44b1dd3a6 100644 --- a/internal/api/client/status/statusunfave_test.go +++ b/internal/api/client/status/statusunfave_test.go @@ -55,7 +55,8 @@ func (suite *StatusUnfaveTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) suite.statusModule = status.New(suite.config, suite.processor, suite.log).(*status.Module) testrig.StandardDBSetup(suite.db) testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") diff --git a/internal/api/federation/users.go b/internal/api/federation/users.go deleted file mode 100644 index f43842664..000000000 --- a/internal/api/federation/users.go +++ /dev/null @@ -1,113 +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 - -import ( - "github.com/gin-gonic/gin" -) - -// 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/api/federation/federation.go b/internal/api/s2s/user/user.go similarity index 81% rename from internal/api/federation/federation.go rename to internal/api/s2s/user/user.go index cc3b33599..74e3b555e 100644 --- a/internal/api/federation/federation.go +++ b/internal/api/s2s/user/user.go @@ -16,7 +16,7 @@ along with this program. If not, see . */ -package federation +package user import ( "net/http" @@ -24,10 +24,8 @@ import ( "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/message" "github.com/superseriousbusiness/gotosocial/internal/router" - "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/util" ) @@ -51,20 +49,16 @@ var ActivityPubAcceptHeaders = []string{ // Module implements the FederationAPIModule interface type Module struct { - federator federation.Federator config *config.Config - db db.DB - tc typeutils.TypeConverter + processor message.Processor 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) api.FederationModule { +func New(config *config.Config, processor message.Processor, log *logrus.Logger) api.FederationModule { return &Module{ - federator: federator, config: config, - db: db, - tc: tc, + processor: processor, log: log, } } diff --git a/internal/api/s2s/user/userget.go b/internal/api/s2s/user/userget.go new file mode 100644 index 000000000..d62b4b5e6 --- /dev/null +++ b/internal/api/s2s/user/userget.go @@ -0,0 +1,114 @@ +/* + 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/gtsmodel" +) + +// 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 == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no username specified in request"}) + return + } + + // make sure this actually an AP request + format := c.NegotiateFormat(ActivityPubAcceptHeaders...) + if format == "" { + c.JSON(http.StatusNotAcceptable, gin.H{"error": "could not negotiate format with given Accept header(s)"}) + 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 2bd82e34e..f0d6f5e84 100644 --- a/internal/federation/clock.go +++ b/internal/federation/clock.go @@ -38,5 +38,5 @@ func (c *Clock) Now() time.Time { } func NewClock() pub.Clock { - return &Clock{} + return &Clock{} } diff --git a/internal/federation/commonbehavior.go b/internal/federation/commonbehavior.go index 51db2f640..9274e78b4 100644 --- a/internal/federation/commonbehavior.go +++ b/internal/federation/commonbehavior.go @@ -26,33 +26,10 @@ import ( "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/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/transport" "github.com/superseriousbusiness/gotosocial/internal/util" ) -// commonBehavior implements the GTSCommonBehavior interface -type commonBehavior struct { - db db.DB - log *logrus.Logger - config *config.Config - transportController transport.Controller -} - -// 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, - log: log, - config: config, - transportController: transportController, - } -} - /* GOFED COMMON BEHAVIOR INTERFACE Contains functions required for both the Social API and Federating Protocol. @@ -79,7 +56,7 @@ func newCommonBehavior(db db.DB, log *logrus.Logger, config *config.Config, tran // Finally, if the authentication and authorization succeeds, then // 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) { +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 // the CLIENT API, not through the federation API, so we just do nothing here. return nil, false, nil @@ -104,7 +81,7 @@ func (c *commonBehavior) AuthenticateGetInbox(ctx context.Context, w http.Respon // Finally, if the authentication and authorization succeeds, then // 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) { +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 // the CLIENT API, not through the federation API, so we just do nothing here. return nil, false, nil @@ -118,7 +95,7 @@ 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) { +func (f *federator) GetOutbox(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 @@ -147,7 +124,7 @@ func (c *commonBehavior) GetOutbox(ctx context.Context, r *http.Request) (vocab. // 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. -func (c *commonBehavior) NewTransport(ctx context.Context, actorBoxIRI *url.URL, gofedAgent string) (pub.Transport, error) { +func (f *federator) NewTransport(ctx context.Context, actorBoxIRI *url.URL, gofedAgent string) (pub.Transport, error) { var username string var err error @@ -167,16 +144,9 @@ func (c *commonBehavior) NewTransport(ctx context.Context, actorBoxIRI *url.URL, } account := >smodel.Account{} - if err := c.db.GetLocalAccountByUsername(username, account); err != nil { + if err := f.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) -} - -// 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 + return f.transportController.NewTransport(account.PublicKeyURI, account.PrivateKey) } diff --git a/internal/federation/federatingprotocol.go b/internal/federation/federatingprotocol.go index 657cd8081..299b82586 100644 --- a/internal/federation/federatingprotocol.go +++ b/internal/federation/federatingprotocol.go @@ -28,34 +28,11 @@ import ( "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/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 { - db db.DB - log *logrus.Logger - config *config.Config - transportController transport.Controller - typeConverter typeutils.TypeConverter -} - -// 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, - } -} - /* GO FED FEDERATING PROTOCOL INTERFACE FederatingProtocol contains behaviors an application needs to satisfy for the @@ -82,7 +59,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 *federator) PostInboxRequestBodyHook(ctx context.Context, r *http.Request, activity pub.Activity) (context.Context, error) { l := f.log.WithFields(logrus.Fields{ "func": "PostInboxRequestBodyHook", "useragent": r.UserAgent(), @@ -115,7 +92,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. -func (f *federatingProtocol) AuthenticatePostInbox(ctx context.Context, w http.ResponseWriter, r *http.Request) (context.Context, bool, error) { +func (f *federator) 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(), @@ -133,12 +110,7 @@ func (f *federatingProtocol) AuthenticatePostInbox(ctx context.Context, w http.R 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 transport: %s", err) - } - - publicKeyOwnerURI, err := AuthenticateFederatedRequest(transport, r) + publicKeyOwnerURI, err := f.AuthenticateFederatedRequest(requestedAccount.Username, r) if err != nil { l.Debugf("request not authenticated: %s", err) return ctx, false, fmt.Errorf("not authenticated: %s", err) @@ -151,9 +123,9 @@ func (f *federatingProtocol) AuthenticatePostInbox(ctx context.Context, w http.R return ctx, false, fmt.Errorf("error getting requesting account with public key id %s: %s", publicKeyOwnerURI.String(), err) } - // we just don't know this account (yet) so try to dereference it + // we don't know this account (yet) so let's dereference it right now // TODO: slow-fed - person, err := DereferenceAccount(transport, publicKeyOwnerURI) + 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) } @@ -182,7 +154,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 *federator) Blocked(ctx context.Context, actorIRIs []*url.URL) (bool, error) { // TODO return false, nil } @@ -206,7 +178,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 *federator) FederatingCallbacks(ctx context.Context) (pub.FederatingWrappedCallbacks, []interface{}, error) { // TODO return pub.FederatingWrappedCallbacks{}, nil, nil } @@ -218,7 +190,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 *federator) DefaultCallback(ctx context.Context, activity pub.Activity) error { l := f.log.WithFields(logrus.Fields{ "func": "DefaultCallback", "aptype": activity.GetTypeName(), @@ -231,7 +203,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 *federator) MaxInboxForwardingRecursionDepth(ctx context.Context) int { // TODO return 0 } @@ -241,7 +213,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 *federator) MaxDeliveryRecursionDepth(ctx context.Context) int { // TODO return 0 } @@ -253,7 +225,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 *federator) FilterForwarding(ctx context.Context, potentialRecipients []*url.URL, a pub.Activity) ([]*url.URL, error) { // TODO return nil, nil } @@ -266,7 +238,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 *federator) 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 4db469bde..f280de828 100644 --- a/internal/federation/federator.go +++ b/internal/federation/federator.go @@ -19,11 +19,13 @@ package federation import ( + "net/http" + "net/url" + "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/message" "github.com/superseriousbusiness/gotosocial/internal/transport" "github.com/superseriousbusiness/gotosocial/internal/typeutils" ) @@ -32,35 +34,36 @@ import ( type Federator interface { FederatingActor() pub.FederatingActor TransportController() transport.Controller - FederatingProtocol() pub.FederatingProtocol - CommonBehavior() pub.CommonBehavior + AuthenticateFederatedRequest(username string, r *http.Request) (*url.URL, error) + pub.CommonBehavior + pub.FederatingProtocol } type federator struct { - actor pub.FederatingActor - processor message.Processor - federatingProtocol pub.FederatingProtocol - commonBehavior pub.CommonBehavior + config *config.Config + db db.DB clock pub.Clock + typeConverter typeutils.TypeConverter transportController transport.Controller + actor pub.FederatingActor + log *logrus.Logger } // NewFederator returns a new federator -func NewFederator(db db.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, processor message.Processor, typeConverter typeutils.TypeConverter) Federator { +func NewFederator(db db.DB, transportController transport.Controller, config *config.Config, log *logrus.Logger, typeConverter typeutils.TypeConverter) Federator { clock := &Clock{} - federatingProtocol := newFederatingProtocol(db, log, config, transportController, typeConverter) - commonBehavior := newCommonBehavior(db, log, config, transportController) - actor := newFederatingActor(commonBehavior, federatingProtocol, db.Federation(), clock) - - return &federator{ - actor: actor, - processor: processor, - federatingProtocol: federatingProtocol, - commonBehavior: commonBehavior, - clock: clock, + f := &federator{ + config: config, + db: db, + clock: &Clock{}, + typeConverter: typeConverter, transportController: transportController, + log: log, } + actor := newFederatingActor(f, f, db.Federation(), clock) + f.actor = actor + return f } func (f *federator) FederatingActor() pub.FederatingActor { @@ -70,11 +73,3 @@ func (f *federator) FederatingActor() pub.FederatingActor { func (f *federator) TransportController() transport.Controller { return f.transportController } - -func (f *federator) FederatingProtocol() pub.FederatingProtocol { - return f.federatingProtocol -} - -func (f *federator) CommonBehavior() pub.CommonBehavior { - return f.commonBehavior -} diff --git a/internal/federation/federator_test.go b/internal/federation/federator_test.go index e1b2db062..93989bfd8 100644 --- a/internal/federation/federator_test.go +++ b/internal/federation/federator_test.go @@ -39,7 +39,6 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/message" "github.com/superseriousbusiness/gotosocial/internal/storage" "github.com/superseriousbusiness/gotosocial/internal/typeutils" "github.com/superseriousbusiness/gotosocial/internal/util" @@ -51,7 +50,6 @@ type ProtocolTestSuite struct { config *config.Config db db.DB log *logrus.Logger - processor message.Processor storage storage.Storage typeConverter typeutils.TypeConverter accounts map[string]*gtsmodel.Account @@ -65,7 +63,6 @@ func (suite *ProtocolTestSuite) SetupSuite() { suite.db = testrig.NewTestDB() suite.log = testrig.NewTestLog() suite.storage = testrig.NewTestStorage() - suite.processor = testrig.NewTestProcessor(suite.db, suite.storage) suite.typeConverter = testrig.NewTestTypeConverter(suite.db) suite.accounts = testrig.NewTestAccounts() suite.activities = testrig.NewTestActivities(suite.accounts) @@ -92,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.processor, suite.typeConverter) + federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.typeConverter) // setup request ctx := context.Background() @@ -158,7 +155,7 @@ func (suite *ProtocolTestSuite) TestAuthenticatePostInbox() { })) // now setup module being tested, with the mock transport controller - federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.processor, suite.typeConverter) + federator := federation.NewFederator(suite.db, tc, suite.config, suite.log, suite.typeConverter) // setup request ctx := context.Background() diff --git a/internal/federation/util.go b/internal/federation/util.go index 28fa45826..1efaa54b5 100644 --- a/internal/federation/util.go +++ b/internal/federation/util.go @@ -101,15 +101,16 @@ func getPublicKeyFromResponse(c context.Context, b []byte, keyID *url.URL) (voca // 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. +// The provided username will be used to generate a transport for making remote requests/derefencing the public key ID of the request signature. +// Ideally you should pass in the username 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 the username '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 it is also valid to pass in an empty string here, in which case the keys of the instance account will be used. // // Note that this function *does not* dereference the remote account that the signature key is associated with, but it will // return the owner of the public key, so that other functions can dereference it with that, as required. -func AuthenticateFederatedRequest(transport pub.Transport, r *http.Request) (*url.URL, error) { +func (f *federator) AuthenticateFederatedRequest(username string, 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) @@ -122,7 +123,12 @@ func AuthenticateFederatedRequest(transport pub.Transport, r *http.Request) (*ur 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 + transport, err := f.GetTransportForUser(username) + if err != nil { + return nil, fmt.Errorf("transport err: %s", err) + } + + // The actual http call to the remote server is made right here in the Dereference function. b, err := transport.Dereference(context.Background(), requestingPublicKeyID) if err != nil { return nil, fmt.Errorf("error deferencing key %s: %s", requestingPublicKeyID.String(), err) @@ -134,17 +140,13 @@ func AuthenticateFederatedRequest(transport pub.Transport, r *http.Request) (*ur return nil, fmt.Errorf("error getting key %s from response %s: %s", requestingPublicKeyID.String(), string(b), err) } - pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() - if pkOwnerProp == nil || !pkOwnerProp.IsIRI() { - return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value") - } - pkOwnerURI := pkOwnerProp.GetIRI() - + // we should be able to get the actual key embedded in the vocab.W3IDSecurityV1PublicKey pkPemProp := requestingPublicKey.GetW3IDSecurityV1PublicKeyPem() if pkPemProp == nil || !pkPemProp.IsXMLSchemaString() { return nil, errors.New("publicKeyPem property is not provided or it is not embedded as a value") } + // and decode the PEM so that we can parse it as a golang public key pubKeyPem := pkPemProp.Get() block, _ := pem.Decode([]byte(pubKeyPem)) if block == nil || block.Type != "PUBLIC KEY" { @@ -162,14 +164,26 @@ func AuthenticateFederatedRequest(transport pub.Transport, r *http.Request) (*ur return nil, fmt.Errorf("error verifying key %s: %s", requestingPublicKeyID.String(), err) } - // all good! + // all good! we just need the URI of the key owner to return + pkOwnerProp := requestingPublicKey.GetW3IDSecurityV1Owner() + if pkOwnerProp == nil || !pkOwnerProp.IsIRI() { + return nil, errors.New("publicKeyOwner property is not provided or it is not embedded as a value") + } + pkOwnerURI := pkOwnerProp.GetIRI() + return pkOwnerURI, nil } -func DereferenceAccount(transport pub.Transport, id *url.URL) (vocab.ActivityStreamsPerson, error) { - b, err := transport.Dereference(context.Background(), id) +func (f *federator) DereferenceRemoteAccount(username string, remoteAccountID *url.URL) (vocab.ActivityStreamsPerson, error) { + + transport, err := f.GetTransportForUser(username) if err != nil { - return nil, fmt.Errorf("error deferencing %s: %s", id.String(), err) + return nil, fmt.Errorf("transport err: %s", err) + } + + b, err := transport.Dereference(context.Background(), remoteAccountID) + if err != nil { + return nil, fmt.Errorf("error deferencing %s: %s", remoteAccountID.String(), err) } m := make(map[string]interface{}) @@ -195,3 +209,25 @@ func DereferenceAccount(transport pub.Transport, id *url.URL) (vocab.ActivityStr return nil, fmt.Errorf("type name %s not supported", t.GetTypeName()) } + +func (f *federator) GetTransportForUser(username string) (pub.Transport, error) { + // We need an account to use to create a transport for dereferecing the signature. + // If a username has been given, we can fetch the account with that username and use it. + // Otherwise, we can take the instance account and use those credentials to make the request. + ourAccount := >smodel.Account{} + var u string + if username == "" { + u = f.config.Host + } else { + u = username + } + if err := f.db.GetLocalAccountByUsername(u, ourAccount); err != nil { + return nil, fmt.Errorf("error getting account %s from db: %s", username, err) + } + + transport, err := f.TransportController().NewTransport(ourAccount.PublicKeyURI, ourAccount.PrivateKey) + if err != nil { + return nil, fmt.Errorf("error creating transport for user %s: %s", username, err) + } + return transport, nil +} diff --git a/internal/gotosocial/actions.go b/internal/gotosocial/actions.go index e38e12234..8d3142f84 100644 --- a/internal/gotosocial/actions.go +++ b/internal/gotosocial/actions.go @@ -72,15 +72,15 @@ var Run action.GTSAction = func(ctx context.Context, c *config.Config, log *logr // build backend handlers mediaHandler := media.New(c, dbService, storageBackend, log) oauthServer := oauth.New(dbService, log) - processor := message.NewProcessor(c, typeConverter, oauthServer, mediaHandler, storageBackend, dbService, log) + transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log) + federator := federation.NewFederator(dbService, transportController, c, log, typeConverter) + processor := message.NewProcessor(c, typeConverter, federator, oauthServer, mediaHandler, storageBackend, dbService, log) if err := processor.Start(); err != nil { return fmt.Errorf("error starting processor: %s", err) } - transportController := transport.NewController(c, &federation.Clock{}, http.DefaultClient, log) - federator := federation.NewFederator(dbService, transportController, c, log, processor, typeConverter) // build client api modules - authModule := auth.New(c, processor, log) + authModule := auth.New(c, dbService, oauthServer, log) accountModule := account.New(c, processor, log) appsModule := app.New(c, processor, log) mm := mediaModule.New(c, processor, log) diff --git a/internal/message/apuserprocess.go b/internal/message/apuserprocess.go new file mode 100644 index 000000000..ac85b22e4 --- /dev/null +++ b/internal/message/apuserprocess.go @@ -0,0 +1,63 @@ +package message + +import ( + "net/http" +) + +func (p *processor) GetAPUser(requestHeaders http.Header, username string) (interface{}, error) { + + // // get the account the request is referring to + // requestedAccount := >smodel.Account{} + // if err := m.db.GetLocalAccountByUsername(username, requestedAccount); err != nil { + // return nil, NewErrorNotAuthorized(fmt.Errorf("database error getting account with username %s: %s", username, err)) + // } + + // // and create a transport for it + // transport, err := p.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) + return nil, nil +} diff --git a/internal/message/mediaprocess.go b/internal/message/mediaprocess.go index 65181bef4..77b387df3 100644 --- a/internal/message/mediaprocess.go +++ b/internal/message/mediaprocess.go @@ -130,10 +130,12 @@ func (p *processor) MediaGet(authed *oauth.Auth, form *apimodel.GetContentReques return nil, NewErrorNotFound(fmt.Errorf("block status could not be established between accounts %s and %s: %s", form.AccountID, authed.Account.ID, err)) } if blocked { - return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s: %s", form.AccountID, authed.Account.ID)) + return nil, NewErrorNotFound(fmt.Errorf("block exists between accounts %s and %s", form.AccountID, authed.Account.ID)) } } + // the way we store emojis is a little different from the way we store other attachments, + // so we need to take different steps depending on the media type being requested content := &apimodel.Content{} var storagePath string switch mediaType { @@ -155,7 +157,7 @@ func (p *processor) MediaGet(authed *oauth.Auth, form *apimodel.GetContentReques default: return nil, NewErrorNotFound(fmt.Errorf("media size %s not recognized for emoji", mediaSize)) } - case media.Attachment: + case media.Attachment, media.Header, media.Avatar: a := >smodel.MediaAttachment{} if err := p.db.GetByID(wantedMediaID, a); err != nil { return nil, NewErrorNotFound(fmt.Errorf("attachment %s could not be taken from the db: %s", wantedMediaID, err)) diff --git a/internal/message/processor.go b/internal/message/processor.go index 69728a6a7..2e59a0231 100644 --- a/internal/message/processor.go +++ b/internal/message/processor.go @@ -23,6 +23,7 @@ import ( apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -39,8 +40,12 @@ import ( type Processor interface { // ToClientAPI returns a channel for putting in messages that need to go to the gts client API. ToClientAPI() chan ToClientAPI + // FromClientAPI returns a channel for putting messages in that come from the client api going to the processor + FromClientAPI() chan FromClientAPI // ToFederator returns a channel for putting in messages that need to go to the federator (activitypub). ToFederator() chan ToFederator + // FromFederator returns a channel for putting messages in that come from the federator going into the processor + FromFederator() chan FromFederator /* API-FACING PROCESSING FUNCTIONS @@ -86,53 +91,67 @@ type Processor interface { // processor just implements the Processor interface type processor struct { // federator pub.FederatingActor - toClientAPI chan ToClientAPI - toFederator chan ToFederator - stop chan interface{} - log *logrus.Logger - config *config.Config - tc typeutils.TypeConverter - oauthServer oauth.Server - mediaHandler media.Handler - storage storage.Storage - db db.DB + toClientAPI chan ToClientAPI + fromClientAPI chan FromClientAPI + toFederator chan ToFederator + fromFederator chan FromFederator + federator federation.Federator + stop chan interface{} + log *logrus.Logger + config *config.Config + tc typeutils.TypeConverter + oauthServer oauth.Server + mediaHandler media.Handler + storage storage.Storage + db db.DB } // NewProcessor returns a new Processor that uses the given federator and logger -func NewProcessor(config *config.Config, tc typeutils.TypeConverter, oauthServer oauth.Server, mediaHandler media.Handler, storage storage.Storage, db db.DB, log *logrus.Logger) Processor { +func NewProcessor(config *config.Config, tc typeutils.TypeConverter, federator federation.Federator, oauthServer oauth.Server, mediaHandler media.Handler, storage storage.Storage, db db.DB, log *logrus.Logger) Processor { return &processor{ - toClientAPI: make(chan ToClientAPI, 100), - toFederator: make(chan ToFederator, 100), - stop: make(chan interface{}), - log: log, - config: config, - tc: tc, - oauthServer: oauthServer, - mediaHandler: mediaHandler, - storage: storage, - db: db, + toClientAPI: make(chan ToClientAPI, 100), + fromClientAPI: make(chan FromClientAPI, 100), + toFederator: make(chan ToFederator, 100), + fromFederator: make(chan FromFederator, 100), + federator: federator, + stop: make(chan interface{}), + log: log, + config: config, + tc: tc, + oauthServer: oauthServer, + mediaHandler: mediaHandler, + storage: storage, + db: db, } } -func (d *processor) ToClientAPI() chan ToClientAPI { - return d.toClientAPI +func (p *processor) ToClientAPI() chan ToClientAPI { + return p.toClientAPI } -func (d *processor) ToFederator() chan ToFederator { - return d.toFederator +func (p *processor) FromClientAPI() chan FromClientAPI { + return p.fromClientAPI +} + +func (p *processor) ToFederator() chan ToFederator { + return p.toFederator +} + +func (p *processor) FromFederator() chan FromFederator { + return p.fromFederator } // Start starts the Processor, reading from its channels and passing messages back and forth. -func (d *processor) Start() error { +func (p *processor) Start() error { go func() { DistLoop: for { select { - case clientMsg := <-d.toClientAPI: - d.log.Infof("received message TO client API: %+v", clientMsg) - case federatorMsg := <-d.toFederator: - d.log.Infof("received message TO federator: %+v", federatorMsg) - case <-d.stop: + case clientMsg := <-p.toClientAPI: + p.log.Infof("received message TO client API: %+v", clientMsg) + case federatorMsg := <-p.toFederator: + p.log.Infof("received message TO federator: %+v", federatorMsg) + case <-p.stop: break DistLoop } } @@ -142,8 +161,8 @@ func (d *processor) Start() error { // Stop stops the processor cleanly, finishing handling any remaining messages before closing down. // TODO: empty message buffer properly before stopping otherwise we'll lose federating messages. -func (d *processor) Stop() error { - close(d.stop) +func (p *processor) Stop() error { + close(p.stop) return nil } @@ -154,9 +173,23 @@ type ToClientAPI struct { Activity interface{} } +// FromClientAPI wraps a message that travels from client API into the processor +type FromClientAPI struct { + APObjectType gtsmodel.ActivityStreamsObject + APActivityType gtsmodel.ActivityStreamsActivity + Activity interface{} +} + // ToFederator wraps a message that travels from the processor into the federator type ToFederator struct { APObjectType gtsmodel.ActivityStreamsObject APActivityType gtsmodel.ActivityStreamsActivity Activity interface{} } + +// FromFederator wraps a message that travels from the federator into the processor +type FromFederator struct { + APObjectType gtsmodel.ActivityStreamsObject + APActivityType gtsmodel.ActivityStreamsActivity + Activity interface{} +} diff --git a/internal/oauth/server.go b/internal/oauth/server.go index ca749aece..7877d667e 100644 --- a/internal/oauth/server.go +++ b/internal/oauth/server.go @@ -23,10 +23,8 @@ import ( "fmt" "net/http" - "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/oauth2/v4" "github.com/superseriousbusiness/oauth2/v4/errors" "github.com/superseriousbusiness/oauth2/v4/manage" @@ -66,82 +64,53 @@ type s struct { log *logrus.Logger } -// Auth wraps an authorized token, application, user, and account. -// It is used in the functions GetAuthed and MustAuth. -// Because the user might *not* be authed, any of the fields in this struct -// might be nil, so make sure to check that when you're using this struct anywhere. -type Auth struct { - Token oauth2.TokenInfo - Application *gtsmodel.Application - User *gtsmodel.User - Account *gtsmodel.Account -} +// New returns a new oauth server that implements the Server interface +func New(database db.DB, log *logrus.Logger) Server { + ts := newTokenStore(context.Background(), database, log) + cs := NewClientStore(database) -// Authed is a convenience function for returning an Authed struct from a gin context. -// In essence, it tries to extract a token, application, user, and account from the context, -// and then sets them on a struct for convenience. -// -// If any are not present in the context, they will be set to nil on the returned Authed struct. -// -// If *ALL* are not present, then nil and an error will be returned. -// -// If something goes wrong during parsing, then nil and an error will be returned (consider this not authed). -// Authed is like GetAuthed, but will fail if one of the requirements is not met. -func Authed(c *gin.Context, requireToken bool, requireApp bool, requireUser bool, requireAccount bool) (*Auth, error) { - ctx := c.Copy() - a := &Auth{} - var i interface{} - var ok bool + manager := manage.NewDefaultManager() + manager.MapTokenStorage(ts) + manager.MapClientStorage(cs) + manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) + sc := &server.Config{ + TokenType: "Bearer", + // Must follow the spec. + AllowGetAccessRequest: false, + // Support only the non-implicit flow. + AllowedResponseTypes: []oauth2.ResponseType{oauth2.Code}, + // Allow: + // - Authorization Code (for first & third parties) + // - Client Credentials (for applications) + AllowedGrantTypes: []oauth2.GrantType{ + oauth2.AuthorizationCode, + oauth2.ClientCredentials, + }, + AllowedCodeChallengeMethods: []oauth2.CodeChallengeMethod{oauth2.CodeChallengePlain}, + } - i, ok = ctx.Get(SessionAuthorizedToken) - if ok { - parsed, ok := i.(oauth2.TokenInfo) - if !ok { - return nil, errors.New("could not parse token from session context") + srv := server.NewServer(sc, manager) + srv.SetInternalErrorHandler(func(err error) *errors.Response { + log.Errorf("internal oauth error: %s", err) + return nil + }) + + srv.SetResponseErrorHandler(func(re *errors.Response) { + log.Errorf("internal response error: %s", re.Error) + }) + + srv.SetUserAuthorizationHandler(func(w http.ResponseWriter, r *http.Request) (string, error) { + userID := r.FormValue("userid") + if userID == "" { + return "", errors.New("userid was empty") } - a.Token = parsed + return userID, nil + }) + srv.SetClientInfoHandler(server.ClientFormHandler) + return &s{ + server: srv, + log: log, } - - i, ok = ctx.Get(SessionAuthorizedApplication) - if ok { - parsed, ok := i.(*gtsmodel.Application) - if !ok { - return nil, errors.New("could not parse application from session context") - } - a.Application = parsed - } - - i, ok = ctx.Get(SessionAuthorizedUser) - if ok { - parsed, ok := i.(*gtsmodel.User) - if !ok { - return nil, errors.New("could not parse user from session context") - } - a.User = parsed - } - - i, ok = ctx.Get(SessionAuthorizedAccount) - if ok { - parsed, ok := i.(*gtsmodel.Account) - if !ok { - return nil, errors.New("could not parse account from session context") - } - a.Account = parsed - } - - if requireToken && a.Token == nil { - return nil, errors.New("token not supplied") - } - if requireApp && a.Application == nil { - return nil, errors.New("application not supplied") - } - if requireUser && a.User == nil { - return nil, errors.New("user not supplied") - } - if requireAccount && a.Account == nil { - return nil, errors.New("account not supplied") - } - return a, nil } // HandleTokenRequest wraps the oauth2 library's HandleTokenRequest function @@ -199,52 +168,3 @@ func (s *s) GenerateUserAccessToken(ti oauth2.TokenInfo, clientSecret string, us s.log.Tracef("obtained user-level access token: %+v", accessToken) return accessToken, nil } - -// New returns a new oauth server that implements the Server interface -func New(database db.DB, log *logrus.Logger) Server { - ts := newTokenStore(context.Background(), database, log) - cs := NewClientStore(database) - - manager := manage.NewDefaultManager() - manager.MapTokenStorage(ts) - manager.MapClientStorage(cs) - manager.SetAuthorizeCodeTokenCfg(manage.DefaultAuthorizeCodeTokenCfg) - sc := &server.Config{ - TokenType: "Bearer", - // Must follow the spec. - AllowGetAccessRequest: false, - // Support only the non-implicit flow. - AllowedResponseTypes: []oauth2.ResponseType{oauth2.Code}, - // Allow: - // - Authorization Code (for first & third parties) - // - Client Credentials (for applications) - AllowedGrantTypes: []oauth2.GrantType{ - oauth2.AuthorizationCode, - oauth2.ClientCredentials, - }, - AllowedCodeChallengeMethods: []oauth2.CodeChallengeMethod{oauth2.CodeChallengePlain}, - } - - srv := server.NewServer(sc, manager) - srv.SetInternalErrorHandler(func(err error) *errors.Response { - log.Errorf("internal oauth error: %s", err) - return nil - }) - - srv.SetResponseErrorHandler(func(re *errors.Response) { - log.Errorf("internal response error: %s", re.Error) - }) - - srv.SetUserAuthorizationHandler(func(w http.ResponseWriter, r *http.Request) (string, error) { - userID := r.FormValue("userid") - if userID == "" { - return "", errors.New("userid was empty") - } - return userID, nil - }) - srv.SetClientInfoHandler(server.ClientFormHandler) - return &s{ - server: srv, - log: log, - } -} diff --git a/internal/oauth/util.go b/internal/oauth/util.go new file mode 100644 index 000000000..378b81450 --- /dev/null +++ b/internal/oauth/util.go @@ -0,0 +1,86 @@ +package oauth + +import ( + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/oauth2/v4" + "github.com/superseriousbusiness/oauth2/v4/errors" +) + +// Auth wraps an authorized token, application, user, and account. +// It is used in the functions GetAuthed and MustAuth. +// Because the user might *not* be authed, any of the fields in this struct +// might be nil, so make sure to check that when you're using this struct anywhere. +type Auth struct { + Token oauth2.TokenInfo + Application *gtsmodel.Application + User *gtsmodel.User + Account *gtsmodel.Account +} + +// Authed is a convenience function for returning an Authed struct from a gin context. +// In essence, it tries to extract a token, application, user, and account from the context, +// and then sets them on a struct for convenience. +// +// If any are not present in the context, they will be set to nil on the returned Authed struct. +// +// If *ALL* are not present, then nil and an error will be returned. +// +// If something goes wrong during parsing, then nil and an error will be returned (consider this not authed). +// Authed is like GetAuthed, but will fail if one of the requirements is not met. +func Authed(c *gin.Context, requireToken bool, requireApp bool, requireUser bool, requireAccount bool) (*Auth, error) { + ctx := c.Copy() + a := &Auth{} + var i interface{} + var ok bool + + i, ok = ctx.Get(SessionAuthorizedToken) + if ok { + parsed, ok := i.(oauth2.TokenInfo) + if !ok { + return nil, errors.New("could not parse token from session context") + } + a.Token = parsed + } + + i, ok = ctx.Get(SessionAuthorizedApplication) + if ok { + parsed, ok := i.(*gtsmodel.Application) + if !ok { + return nil, errors.New("could not parse application from session context") + } + a.Application = parsed + } + + i, ok = ctx.Get(SessionAuthorizedUser) + if ok { + parsed, ok := i.(*gtsmodel.User) + if !ok { + return nil, errors.New("could not parse user from session context") + } + a.User = parsed + } + + i, ok = ctx.Get(SessionAuthorizedAccount) + if ok { + parsed, ok := i.(*gtsmodel.Account) + if !ok { + return nil, errors.New("could not parse account from session context") + } + a.Account = parsed + } + + if requireToken && a.Token == nil { + return nil, errors.New("token not supplied") + } + if requireApp && a.Application == nil { + return nil, errors.New("application not supplied") + } + if requireUser && a.User == nil { + return nil, errors.New("user not supplied") + } + if requireAccount && a.Account == nil { + return nil, errors.New("account not supplied") + } + return a, nil +} diff --git a/internal/storage/inmem.go b/internal/storage/inmem.go index 2d88189db..a596c3d97 100644 --- a/internal/storage/inmem.go +++ b/internal/storage/inmem.go @@ -35,7 +35,7 @@ func (s *inMemStorage) RetrieveFileFrom(path string) ([]byte, error) { l := s.log.WithField("func", "RetrieveFileFrom") l.Debugf("retrieving from path %s", path) d, ok := s.stored[path] - if !ok { + if !ok || len(d) == 0 { return nil, fmt.Errorf("no data found at path %s", path) } return d, nil diff --git a/internal/typeutils/internaltofrontend.go b/internal/typeutils/internaltofrontend.go index 6ec2e0509..17ae099d9 100644 --- a/internal/typeutils/internaltofrontend.go +++ b/internal/typeutils/internaltofrontend.go @@ -22,9 +22,9 @@ import ( "fmt" "time" + "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/api/model" ) func (c *converter) AccountToMastoSensitive(a *gtsmodel.Account) (*model.Account, error) { diff --git a/testrig/actions.go b/testrig/actions.go index 87b3cae7d..7ed75b18f 100644 --- a/testrig/actions.go +++ b/testrig/actions.go @@ -50,10 +50,7 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr dbService := NewTestDB() router := NewTestRouter() storageBackend := NewTestStorage() - processor := NewTestProcessor(dbService, storageBackend) - if err := processor.Start(); err != nil { - return fmt.Errorf("error starting processor: %s", err) - } + typeConverter := NewTestTypeConverter(dbService) transportController := NewTestTransportController(NewMockHTTPClient(func(req *http.Request) (*http.Response, error) { r := ioutil.NopCloser(bytes.NewReader([]byte{})) @@ -62,13 +59,17 @@ var Run action.GTSAction = func(ctx context.Context, _ *config.Config, log *logr Body: r, }, nil })) - federator := federation.NewFederator(dbService, transportController, c, log, processor, typeConverter) + federator := federation.NewFederator(dbService, transportController, c, log, typeConverter) + processor := NewTestProcessor(dbService, storageBackend, federator) + if err := processor.Start(); err != nil { + return fmt.Errorf("error starting processor: %s", err) + } StandardDBSetup(dbService) StandardStorageSetup(storageBackend, "./testrig/media") // build client api modules - authModule := auth.New(c, processor, log) + authModule := auth.New(c, dbService, NewTestOauthServer(dbService), log) accountModule := account.New(c, processor, log) appsModule := app.New(c, processor, log) mm := mediaModule.New(c, processor, log) diff --git a/testrig/federator.go b/testrig/federator.go new file mode 100644 index 000000000..63ad520db --- /dev/null +++ b/testrig/federator.go @@ -0,0 +1,29 @@ +/* + 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 testrig + +import ( + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/federation" + "github.com/superseriousbusiness/gotosocial/internal/transport" +) + +func NewTestFederator(db db.DB, tc transport.Controller) federation.Federator { + return federation.NewFederator(db, tc, NewTestConfig(), NewTestLog(), NewTestTypeConverter(db)) +} diff --git a/testrig/processor.go b/testrig/processor.go index 7589108be..9aa8e2509 100644 --- a/testrig/processor.go +++ b/testrig/processor.go @@ -20,11 +20,12 @@ package testrig import ( "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/federation" "github.com/superseriousbusiness/gotosocial/internal/message" "github.com/superseriousbusiness/gotosocial/internal/storage" ) // NewTestProcessor returns a Processor suitable for testing purposes -func NewTestProcessor(db db.DB, storage storage.Storage) message.Processor { - return message.NewProcessor(NewTestConfig(), NewTestTypeConverter(db), NewTestOauthServer(db), NewTestMediaHandler(db, storage), storage, db, NewTestLog()) +func NewTestProcessor(db db.DB, storage storage.Storage, federator federation.Federator) message.Processor { + return message.NewProcessor(NewTestConfig(), NewTestTypeConverter(db), federator, NewTestOauthServer(db), NewTestMediaHandler(db, storage), storage, db, NewTestLog()) } diff --git a/testrig/testmodels.go b/testrig/testmodels.go index 1ce1b6a90..6427630c7 100644 --- a/testrig/testmodels.go +++ b/testrig/testmodels.go @@ -691,25 +691,26 @@ func NewTestAttachments() map[string]*gtsmodel.MediaAttachment { func NewTestEmojis() map[string]*gtsmodel.Emoji { return map[string]*gtsmodel.Emoji{ "rainbow": { - ID: "a96ec4f3-1cae-47e4-a508-f9d66a6b221b", - Shortcode: "rainbow", - Domain: "", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - ImageRemoteURL: "", - ImageStaticRemoteURL: "", - ImageURL: "http://localhost:8080/fileserver/39b745a3-774d-4b65-8bb2-b63d9e20a343/emoji/original/a96ec4f3-1cae-47e4-a508-f9d66a6b221b.png", - ImagePath: "/tmp/gotosocial/39b745a3-774d-4b65-8bb2-b63d9e20a343/emoji/original/a96ec4f3-1cae-47e4-a508-f9d66a6b221b.png", - ImageStaticURL: "http://localhost:8080/fileserver/39b745a3-774d-4b65-8bb2-b63d9e20a343/emoji/static/a96ec4f3-1cae-47e4-a508-f9d66a6b221b.png", - ImageStaticPath: "/tmp/gotosocial/39b745a3-774d-4b65-8bb2-b63d9e20a343/emoji/static/a96ec4f3-1cae-47e4-a508-f9d66a6b221b.png", - ImageContentType: "image/png", - ImageFileSize: 36702, - ImageStaticFileSize: 10413, - ImageUpdatedAt: time.Now(), - Disabled: false, - URI: "http://localhost:8080/emoji/a96ec4f3-1cae-47e4-a508-f9d66a6b221b", - VisibleInPicker: true, - CategoryID: "", + ID: "a96ec4f3-1cae-47e4-a508-f9d66a6b221b", + Shortcode: "rainbow", + Domain: "", + CreatedAt: time.Now(), + UpdatedAt: time.Now(), + ImageRemoteURL: "", + ImageStaticRemoteURL: "", + ImageURL: "http://localhost:8080/fileserver/39b745a3-774d-4b65-8bb2-b63d9e20a343/emoji/original/a96ec4f3-1cae-47e4-a508-f9d66a6b221b.png", + ImagePath: "/tmp/gotosocial/39b745a3-774d-4b65-8bb2-b63d9e20a343/emoji/original/a96ec4f3-1cae-47e4-a508-f9d66a6b221b.png", + ImageStaticURL: "http://localhost:8080/fileserver/39b745a3-774d-4b65-8bb2-b63d9e20a343/emoji/static/a96ec4f3-1cae-47e4-a508-f9d66a6b221b.png", + ImageStaticPath: "/tmp/gotosocial/39b745a3-774d-4b65-8bb2-b63d9e20a343/emoji/static/a96ec4f3-1cae-47e4-a508-f9d66a6b221b.png", + ImageContentType: "image/png", + ImageStaticContentType: "image/png", + ImageFileSize: 36702, + ImageStaticFileSize: 10413, + ImageUpdatedAt: time.Now(), + Disabled: false, + URI: "http://localhost:8080/emoji/a96ec4f3-1cae-47e4-a508-f9d66a6b221b", + VisibleInPicker: true, + CategoryID: "", }, } } diff --git a/testrig/transportcontroller.go b/testrig/transportcontroller.go index 942be7f3a..f2b5b93f7 100644 --- a/testrig/transportcontroller.go +++ b/testrig/transportcontroller.go @@ -19,6 +19,8 @@ package testrig import ( + "bytes" + "io/ioutil" "net/http" "github.com/go-fed/activity/pub" @@ -41,7 +43,22 @@ func NewTestTransportController(client pub.HttpClient) transport.Controller { // NewMockHTTPClient returns a client that conforms to the pub.HttpClient interface, // but will always just execute the given `do` function, allowing responses to be mocked. +// +// If 'do' is nil, then a no-op function will be used instead, that just returns status 200. +// +// Note that you should never ever make ACTUAL http calls with this thing. func NewMockHTTPClient(do func(req *http.Request) (*http.Response, error)) pub.HttpClient { + if do == nil { + return &mockHTTPClient{ + do: func(req *http.Request) (*http.Response, error) { + r := ioutil.NopCloser(bytes.NewReader([]byte{})) + return &http.Response{ + StatusCode: 200, + Body: r, + }, nil + }, + } + } return &mockHTTPClient{ do: do, }