diff options
Diffstat (limited to 'internal/api')
27 files changed, 448 insertions, 34 deletions
diff --git a/internal/api/client/account/accountupdate_test.go b/internal/api/client/account/accountupdate_test.go index ba7faa794..341b865ff 100644 --- a/internal/api/client/account/accountupdate_test.go +++ b/internal/api/client/account/accountupdate_test.go @@ -53,7 +53,7 @@ func (suite *AccountUpdateTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) suite.accountModule = account.New(suite.config, suite.processor, suite.log).(*account.Module) testrig.StandardDBSetup(suite.db) diff --git a/internal/api/client/account/statuses.go b/internal/api/client/account/statuses.go index f03a942f3..c92e85cee 100644 --- a/internal/api/client/account/statuses.go +++ b/internal/api/client/account/statuses.go @@ -82,7 +82,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { maxID = maxIDString } - pinned := false + pinnedOnly := false pinnedString := c.Query(PinnedKey) if pinnedString != "" { i, err := strconv.ParseBool(pinnedString) @@ -91,7 +91,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse pinned query param"}) return } - pinned = i + pinnedOnly = i } mediaOnly := false @@ -106,7 +106,7 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { mediaOnly = i } - statuses, errWithCode := m.processor.AccountStatusesGet(authed, targetAcctID, limit, excludeReplies, maxID, pinned, mediaOnly) + statuses, errWithCode := m.processor.AccountStatusesGet(authed, targetAcctID, limit, excludeReplies, maxID, pinnedOnly, mediaOnly) if errWithCode != nil { l.Debugf("error from processor account statuses get: %s", errWithCode) c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) diff --git a/internal/api/client/admin/admin.go b/internal/api/client/admin/admin.go index b33813a7d..b8b94be76 100644 --- a/internal/api/client/admin/admin.go +++ b/internal/api/client/admin/admin.go @@ -29,10 +29,19 @@ import ( ) const ( - // BasePath is the base API path for this module + // BasePath is the base API path for this module. BasePath = "/api/v1/admin" - // EmojiPath is used for posting/deleting custom emojis + // EmojiPath is used for posting/deleting custom emojis. EmojiPath = BasePath + "/custom_emojis" + // DomainBlocksPath is used for posting domain blocks. + DomainBlocksPath = BasePath + "/domain_blocks" + // DomainBlockPath is used for interacting with a single domain block. + DomainBlockPath = DomainBlocksPath + "/:" + IDKey + + // ExportQueryKey is for requesting a public export of some data. + ExportQueryKey = "export" + // IDKey specifies the ID of a single item being interacted with. + IDKey = "id" ) // Module implements the ClientAPIModule interface for admin-related actions (reports, emojis, etc) @@ -54,5 +63,9 @@ func New(config *config.Config, processor processing.Processor, log *logrus.Logg // Route attaches all routes from this module to the given router func (m *Module) Route(r router.Router) error { r.AttachHandler(http.MethodPost, EmojiPath, m.emojiCreatePOSTHandler) + r.AttachHandler(http.MethodPost, DomainBlocksPath, m.DomainBlocksPOSTHandler) + r.AttachHandler(http.MethodGet, DomainBlocksPath, m.DomainBlocksGETHandler) + r.AttachHandler(http.MethodGet, DomainBlockPath, m.DomainBlockGETHandler) + r.AttachHandler(http.MethodDelete, DomainBlockPath, m.DomainBlockDELETEHandler) return nil } diff --git a/internal/api/client/admin/domainblockcreate.go b/internal/api/client/admin/domainblockcreate.go new file mode 100644 index 000000000..5d3df58de --- /dev/null +++ b/internal/api/client/admin/domainblockcreate.go @@ -0,0 +1,70 @@ +package admin + +import ( + "errors" + "fmt" + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api/model" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// DomainBlocksPOSTHandler deals with the creation of a new domain block. +func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "DomainBlocksPOSTHandler", + "request_uri": c.Request.RequestURI, + "user_agent": c.Request.UserAgent(), + "origin_ip": c.ClientIP(), + }) + + // make sure we're authed with an admin account + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + return + } + if !authed.User.Admin { + l.Debugf("user %s not an admin", authed.User.ID) + c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) + return + } + + // extract the media create form from the request context + l.Tracef("parsing request form: %+v", c.Request.Form) + form := &model.DomainBlockCreateRequest{} + if err := c.ShouldBind(form); err != nil { + l.Debugf("error parsing form %+v: %s", c.Request.Form, err) + c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("could not parse form: %s", err)}) + return + } + + // Give the fields on the request form a first pass to make sure the request is superficially valid. + l.Tracef("validating form %+v", form) + if err := validateCreateDomainBlock(form); err != nil { + l.Debugf("error validating form: %s", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + domainBlock, err := m.processor.AdminDomainBlockCreate(authed, form) + if err != nil { + l.Debugf("error creating domain block: %s", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, domainBlock) +} + +func validateCreateDomainBlock(form *model.DomainBlockCreateRequest) error { + // add some more validation here later if necessary + if form.Domain == "" { + return errors.New("empty domain provided") + } + + return nil +} diff --git a/internal/api/client/admin/domainblockdelete.go b/internal/api/client/admin/domainblockdelete.go new file mode 100644 index 000000000..d8f4586f9 --- /dev/null +++ b/internal/api/client/admin/domainblockdelete.go @@ -0,0 +1,47 @@ +package admin + +import ( + "net/http" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// DomainBlockDELETEHandler deals with the delete of an existing domain block. +func (m *Module) DomainBlockDELETEHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "DomainBlockDELETEHandler", + "request_uri": c.Request.RequestURI, + "user_agent": c.Request.UserAgent(), + "origin_ip": c.ClientIP(), + }) + + // make sure we're authed with an admin account + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + return + } + if !authed.User.Admin { + l.Debugf("user %s not an admin", authed.User.ID) + c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) + return + } + + domainBlockID := c.Param(IDKey) + if domainBlockID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"}) + return + } + + domainBlock, errWithCode := m.processor.AdminDomainBlockDelete(authed, domainBlockID) + if errWithCode != nil { + l.Debugf("error deleting domain block: %s", errWithCode.Error()) + c.JSON(errWithCode.Code(), gin.H{"error": errWithCode.Safe()}) + return + } + + c.JSON(http.StatusOK, domainBlock) +} diff --git a/internal/api/client/admin/domainblockget.go b/internal/api/client/admin/domainblockget.go new file mode 100644 index 000000000..009794f8a --- /dev/null +++ b/internal/api/client/admin/domainblockget.go @@ -0,0 +1,60 @@ +package admin + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// DomainBlockGETHandler returns one existing domain block, identified by its id. +func (m *Module) DomainBlockGETHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "DomainBlockGETHandler", + "request_uri": c.Request.RequestURI, + "user_agent": c.Request.UserAgent(), + "origin_ip": c.ClientIP(), + }) + + // make sure we're authed with an admin account + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + return + } + if !authed.User.Admin { + l.Debugf("user %s not an admin", authed.User.ID) + c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) + return + } + + domainBlockID := c.Param(IDKey) + if domainBlockID == "" { + c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"}) + return + } + + export := false + exportString := c.Query(ExportQueryKey) + if exportString != "" { + i, err := strconv.ParseBool(exportString) + if err != nil { + l.Debugf("error parsing export string: %s", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"}) + return + } + export = i + } + + domainBlock, err := m.processor.AdminDomainBlockGet(authed, domainBlockID, export) + if err != nil { + l.Debugf("error getting domain block: %s", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, domainBlock) +} diff --git a/internal/api/client/admin/domainblocksget.go b/internal/api/client/admin/domainblocksget.go new file mode 100644 index 000000000..1e873a302 --- /dev/null +++ b/internal/api/client/admin/domainblocksget.go @@ -0,0 +1,54 @@ +package admin + +import ( + "net/http" + "strconv" + + "github.com/gin-gonic/gin" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/oauth" +) + +// DomainBlocksGETHandler returns a list of all existing domain blocks. +func (m *Module) DomainBlocksGETHandler(c *gin.Context) { + l := m.log.WithFields(logrus.Fields{ + "func": "DomainBlocksGETHandler", + "request_uri": c.Request.RequestURI, + "user_agent": c.Request.UserAgent(), + "origin_ip": c.ClientIP(), + }) + + // make sure we're authed with an admin account + authed, err := oauth.Authed(c, true, true, true, true) + if err != nil { + l.Debugf("couldn't auth: %s", err) + c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) + return + } + if !authed.User.Admin { + l.Debugf("user %s not an admin", authed.User.ID) + c.JSON(http.StatusForbidden, gin.H{"error": "not an admin"}) + return + } + + export := false + exportString := c.Query(ExportQueryKey) + if exportString != "" { + i, err := strconv.ParseBool(exportString) + if err != nil { + l.Debugf("error parsing export string: %s", err) + c.JSON(http.StatusBadRequest, gin.H{"error": "couldn't parse export query param"}) + return + } + export = i + } + + domainBlocks, err := m.processor.AdminDomainBlocksGet(authed, export) + if err != nil { + l.Debugf("error getting domain blocks: %s", err) + c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) + return + } + + c.JSON(http.StatusOK, domainBlocks) +} diff --git a/internal/api/client/fileserver/servefile_test.go b/internal/api/client/fileserver/servefile_test.go index 2646da24d..cb503facb 100644 --- a/internal/api/client/fileserver/servefile_test.go +++ b/internal/api/client/fileserver/servefile_test.go @@ -78,7 +78,7 @@ func (suite *ServeFileTestSuite) SetupSuite() { suite.db = testrig.NewTestDB() suite.log = testrig.NewTestLog() suite.storage = testrig.NewTestStorage() - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) suite.tc = testrig.NewTestTypeConverter(suite.db) suite.mediaHandler = testrig.NewTestMediaHandler(suite.db, suite.storage) diff --git a/internal/api/client/instance/instancepatch.go b/internal/api/client/instance/instancepatch.go index ace7674c0..250e836be 100644 --- a/internal/api/client/instance/instancepatch.go +++ b/internal/api/client/instance/instancepatch.go @@ -8,6 +8,7 @@ import ( "github.com/superseriousbusiness/gotosocial/internal/oauth" ) +// InstanceUpdatePATCHHandler allows an admin to update the instance information served at /api/v1/instance func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) { l := m.log.WithField("func", "InstanceUpdatePATCHHandler") authed, err := oauth.Authed(c, true, true, true, true) diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index bed588017..89a77a729 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -84,7 +84,7 @@ 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.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) // setup module being tested diff --git a/internal/api/client/status/statusboost_test.go b/internal/api/client/status/statusboost_test.go index 24ed8c361..9400aeddc 100644 --- a/internal/api/client/status/statusboost_test.go +++ b/internal/api/client/status/statusboost_test.go @@ -52,7 +52,7 @@ func (suite *StatusBoostTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) 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) diff --git a/internal/api/client/status/statuscreate_test.go b/internal/api/client/status/statuscreate_test.go index a78374fe8..b19323869 100644 --- a/internal/api/client/status/statuscreate_test.go +++ b/internal/api/client/status/statuscreate_test.go @@ -57,7 +57,7 @@ func (suite *StatusCreateTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) 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) diff --git a/internal/api/client/status/statusfave_test.go b/internal/api/client/status/statusfave_test.go index 2f779baed..b1cafc2fb 100644 --- a/internal/api/client/status/statusfave_test.go +++ b/internal/api/client/status/statusfave_test.go @@ -55,7 +55,7 @@ func (suite *StatusFaveTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) 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) diff --git a/internal/api/client/status/statusfavedby_test.go b/internal/api/client/status/statusfavedby_test.go index 7b72df7bc..b6e1591e0 100644 --- a/internal/api/client/status/statusfavedby_test.go +++ b/internal/api/client/status/statusfavedby_test.go @@ -55,7 +55,7 @@ func (suite *StatusFavedByTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) 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) diff --git a/internal/api/client/status/statusget_test.go b/internal/api/client/status/statusget_test.go index b31acebca..1bbf48a91 100644 --- a/internal/api/client/status/statusget_test.go +++ b/internal/api/client/status/statusget_test.go @@ -45,7 +45,7 @@ func (suite *StatusGetTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) 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) diff --git a/internal/api/client/status/statusunfave_test.go b/internal/api/client/status/statusunfave_test.go index 44b1dd3a6..36144c5ce 100644 --- a/internal/api/client/status/statusunfave_test.go +++ b/internal/api/client/status/statusunfave_test.go @@ -55,7 +55,7 @@ func (suite *StatusUnfaveTestSuite) SetupTest() { suite.db = testrig.NewTestDB() suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) 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) diff --git a/internal/api/model/domainblock.go b/internal/api/model/domainblock.go new file mode 100644 index 000000000..aaa28f34d --- /dev/null +++ b/internal/api/model/domainblock.go @@ -0,0 +1,43 @@ +/* + 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 <http://www.gnu.org/licenses/>. +*/ + +package model + +// DomainBlock represents a block on one domain +type DomainBlock struct { + ID string `json:"id,omitempty"` + Domain string `json:"domain"` + Obfuscate bool `json:"obfuscate,omitempty"` + PrivateComment string `json:"private_comment,omitempty"` + PublicComment string `json:"public_comment,omitempty"` + SubscriptionID string `json:"subscription_id,omitempty"` + CreatedBy string `json:"created_by,omitempty"` + CreatedAt string `json:"created_at,omitempty"` +} + +// DomainBlockCreateRequest is the form submitted as a POST to /api/v1/admin/domain_blocks to create a new block. +type DomainBlockCreateRequest struct { + // hostname/domain to block + Domain string `form:"domain" json:"domain" xml:"domain" validation:"required"` + // whether the domain should be obfuscated when being displayed publicly + Obfuscate bool `form:"obfuscate" json:"obfuscate" xml:"obfuscate"` + // private comment for other admins on why the domain was blocked + PrivateComment string `form:"private_comment" json:"private_comment" xml:"private_comment"` + // public comment on the reason for the domain block + PublicComment string `form:"public_comment" json:"public_comment" xml:"public_comment"` +} diff --git a/internal/api/s2s/user/followers.go b/internal/api/s2s/user/followers.go index 9ccf9c4d5..6e33407d0 100644 --- a/internal/api/s2s/user/followers.go +++ b/internal/api/s2s/user/followers.go @@ -19,10 +19,12 @@ package user import ( + "context" "net/http" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it. @@ -46,9 +48,14 @@ func (m *Module) FollowersGETHandler(c *gin.Context) { } l.Tracef("negotiated format: %s", format) - // make a copy of the context to pass along so we don't break anything - cp := c.Copy() - user, err := m.processor.GetFediFollowers(requestedUsername, cp.Request) // GetFediUser handles auth as well + // transfer the signature verifier from the gin context to the request context + ctx := c.Request.Context() + verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier)) + if signed { + ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier) + } + + user, err := m.processor.GetFediFollowers(ctx, requestedUsername, c.Request.URL) // GetFediUser handles auth as well if err != nil { l.Info(err.Error()) c.JSON(err.Code(), gin.H{"error": err.Safe()}) diff --git a/internal/api/s2s/user/following.go b/internal/api/s2s/user/following.go index f19965c26..bdf815b05 100644 --- a/internal/api/s2s/user/following.go +++ b/internal/api/s2s/user/following.go @@ -19,10 +19,12 @@ package user import ( + "context" "net/http" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it. @@ -46,9 +48,14 @@ func (m *Module) FollowingGETHandler(c *gin.Context) { } l.Tracef("negotiated format: %s", format) - // make a copy of the context to pass along so we don't break anything - cp := c.Copy() - user, err := m.processor.GetFediFollowing(requestedUsername, cp.Request) // handles auth as well + // transfer the signature verifier from the gin context to the request context + ctx := c.Request.Context() + verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier)) + if signed { + ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier) + } + + user, err := m.processor.GetFediFollowing(ctx, requestedUsername, c.Request.URL) // handles auth as well if err != nil { l.Info(err.Error()) c.JSON(err.Code(), gin.H{"error": err.Safe()}) diff --git a/internal/api/s2s/user/inboxpost.go b/internal/api/s2s/user/inboxpost.go index a51cd8add..98442af13 100644 --- a/internal/api/s2s/user/inboxpost.go +++ b/internal/api/s2s/user/inboxpost.go @@ -19,11 +19,13 @@ package user import ( + "context" "net/http" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "github.com/superseriousbusiness/gotosocial/internal/gtserror" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // InboxPOSTHandler deals with incoming POST requests to an actor's inbox. @@ -40,7 +42,14 @@ func (m *Module) InboxPOSTHandler(c *gin.Context) { return } - posted, err := m.processor.InboxPost(c.Request.Context(), c.Writer, c.Request) + // transfer the signature verifier from the gin context to the request context + ctx := c.Request.Context() + verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier)) + if signed { + ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier) + } + + posted, err := m.processor.InboxPost(ctx, c.Writer, c.Request) if err != nil { if withCode, ok := err.(gtserror.WithCode); ok { l.Debug(withCode.Error()) diff --git a/internal/api/s2s/user/publickeyget.go b/internal/api/s2s/user/publickeyget.go index b6aadedb2..bb1844e0e 100644 --- a/internal/api/s2s/user/publickeyget.go +++ b/internal/api/s2s/user/publickeyget.go @@ -1,10 +1,12 @@ package user import ( + "context" "net/http" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // PublicKeyGETHandler should be served at eg https://example.org/users/:username/main-key. @@ -32,9 +34,14 @@ func (m *Module) PublicKeyGETHandler(c *gin.Context) { } l.Tracef("negotiated format: %s", format) - // make a copy of the context to pass along so we don't break anything - cp := c.Copy() - user, err := m.processor.GetFediUser(requestedUsername, cp.Request) // GetFediUser handles auth as well + // transfer the signature verifier from the gin context to the request context + ctx := c.Request.Context() + verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier)) + if signed { + ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier) + } + + user, err := m.processor.GetFediUser(ctx, requestedUsername, c.Request.URL) // GetFediUser handles auth as well if err != nil { l.Info(err.Error()) c.JSON(err.Code(), gin.H{"error": err.Safe()}) diff --git a/internal/api/s2s/user/statusget.go b/internal/api/s2s/user/statusget.go index 22774ae2c..37621d1de 100644 --- a/internal/api/s2s/user/statusget.go +++ b/internal/api/s2s/user/statusget.go @@ -1,10 +1,12 @@ package user import ( + "context" "net/http" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // StatusGETHandler serves the target status as an activitystreams NOTE so that other AP servers can parse it. @@ -34,9 +36,14 @@ func (m *Module) StatusGETHandler(c *gin.Context) { } l.Tracef("negotiated format: %s", format) - // make a copy of the context to pass along so we don't break anything - cp := c.Copy() - status, err := m.processor.GetFediStatus(requestedUsername, requestedStatusID, cp.Request) // handles auth as well + // transfer the signature verifier from the gin context to the request context + ctx := c.Request.Context() + verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier)) + if signed { + ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier) + } + + status, err := m.processor.GetFediStatus(ctx, requestedUsername, requestedStatusID, c.Request.URL) // handles auth as well if err != nil { l.Info(err.Error()) c.JSON(err.Code(), gin.H{"error": err.Safe()}) diff --git a/internal/api/s2s/user/userget.go b/internal/api/s2s/user/userget.go index 9d268e121..ac49b1529 100644 --- a/internal/api/s2s/user/userget.go +++ b/internal/api/s2s/user/userget.go @@ -19,10 +19,12 @@ package user import ( + "context" "net/http" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // UsersGETHandler should be served at https://example.org/users/:username. @@ -54,9 +56,14 @@ func (m *Module) UsersGETHandler(c *gin.Context) { } l.Tracef("negotiated format: %s", format) - // make a copy of the context to pass along so we don't break anything - cp := c.Copy() - user, err := m.processor.GetFediUser(requestedUsername, cp.Request) // GetFediUser handles auth as well + // transfer the signature verifier from the gin context to the request context + ctx := c.Request.Context() + verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier)) + if signed { + ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier) + } + + user, err := m.processor.GetFediUser(ctx, requestedUsername, c.Request.URL) // GetFediUser handles auth as well if err != nil { l.Info(err.Error()) c.JSON(err.Code(), gin.H{"error": err.Safe()}) diff --git a/internal/api/s2s/user/userget_test.go b/internal/api/s2s/user/userget_test.go index fab490767..d20148802 100644 --- a/internal/api/s2s/user/userget_test.go +++ b/internal/api/s2s/user/userget_test.go @@ -42,7 +42,7 @@ func (suite *UserGetTestSuite) SetupTest() { suite.tc = testrig.NewTestTypeConverter(suite.db) suite.storage = testrig.NewTestStorage() suite.log = testrig.NewTestLog() - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil))) + suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil)), suite.storage) suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator) suite.userModule = user.New(suite.config, suite.processor, suite.log).(*user.Module) testrig.StandardDBSetup(suite.db) @@ -98,7 +98,7 @@ func (suite *UserGetTestSuite) TestGetUser() { }, nil })) // get this transport controller embedded right in the user module we're testing - federator := testrig.NewTestFederator(suite.db, tc) + federator := testrig.NewTestFederator(suite.db, tc, suite.storage) processor := testrig.NewTestProcessor(suite.db, suite.storage, federator) userModule := user.New(suite.config, processor, suite.log).(*user.Module) diff --git a/internal/api/s2s/webfinger/webfingerget.go b/internal/api/s2s/webfinger/webfingerget.go index 30e089162..416a75f3b 100644 --- a/internal/api/s2s/webfinger/webfingerget.go +++ b/internal/api/s2s/webfinger/webfingerget.go @@ -19,12 +19,14 @@ package webfinger import ( + "context" "fmt" "net/http" "strings" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/util" ) // WebfingerGETRequest handles requests to, for example, https://example.org/.well-known/webfinger?resource=acct:some_user@example.org @@ -68,7 +70,14 @@ func (m *Module) WebfingerGETRequest(c *gin.Context) { return } - resp, err := m.processor.GetWebfingerAccount(username, c.Request) + // transfer the signature verifier from the gin context to the request context + ctx := c.Request.Context() + verifier, signed := c.Get(string(util.APRequestingPublicKeyVerifier)) + if signed { + ctx = context.WithValue(ctx, util.APRequestingPublicKeyVerifier, verifier) + } + + resp, err := m.processor.GetWebfingerAccount(ctx, username, c.Request.URL) if err != nil { l.Debugf("aborting request with an error: %s", err.Error()) c.JSON(err.Code(), gin.H{"error": err.Safe()}) diff --git a/internal/api/security/security.go b/internal/api/security/security.go index 7298bc7cb..d8f6b0fe3 100644 --- a/internal/api/security/security.go +++ b/internal/api/security/security.go @@ -24,6 +24,7 @@ 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/router" ) @@ -33,18 +34,21 @@ const robotsPath = "/robots.txt" type Module struct { config *config.Config log *logrus.Logger + db db.DB } // New returns a new security module -func New(config *config.Config, log *logrus.Logger) api.ClientModule { +func New(config *config.Config, db db.DB, log *logrus.Logger) api.ClientModule { return &Module{ config: config, log: log, + db: db, } } // Route attaches security middleware to the given router func (m *Module) Route(s router.Router) error { + s.AttachMiddleware(m.SignatureCheck) s.AttachMiddleware(m.FlocBlock) s.AttachMiddleware(m.ExtraHeaders) s.AttachMiddleware(m.UserAgentBlock) diff --git a/internal/api/security/signaturecheck.go b/internal/api/security/signaturecheck.go new file mode 100644 index 000000000..b852c92ab --- /dev/null +++ b/internal/api/security/signaturecheck.go @@ -0,0 +1,69 @@ +package security + +import ( + "net/http" + "net/url" + + "github.com/gin-gonic/gin" + "github.com/go-fed/httpsig" + "github.com/superseriousbusiness/gotosocial/internal/db" + "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" + "github.com/superseriousbusiness/gotosocial/internal/util" +) + +// SignatureCheck checks whether an incoming http request has been signed. If so, it will check if the domain +// that signed the request is permitted to access the server. If it is permitted, the handler will set the key +// verifier in the gin context for use down the line. +func (m *Module) SignatureCheck(c *gin.Context) { + l := m.log.WithField("func", "DomainBlockChecker") + + // set this extra field for signature validation + c.Request.Header.Set("host", m.config.Host) + + // create the verifier from the request + // if the request is signed, it will have a signature header + verifier, err := httpsig.NewVerifier(c.Request) + if err == nil { + // the request was signed! + + // The key ID should be given in the signature so that we know where to fetch it from the remote server. + // This will be something like https://example.org/users/whatever_requesting_user#main-key + requestingPublicKeyID, err := url.Parse(verifier.KeyId()) + if err == nil && requestingPublicKeyID != nil { + // we managed to parse the url! + + // if the domain is blocked we want to bail as early as possible + blockedDomain, err := m.blockedDomain(requestingPublicKeyID.Host) + if err != nil { + l.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err) + c.AbortWithStatus(http.StatusInternalServerError) + return + } + if blockedDomain { + l.Infof("domain %s is blocked", requestingPublicKeyID.Host) + c.AbortWithStatus(http.StatusForbidden) + return + } + + // set the verifier on the context here to save some work further down the line + c.Set(string(util.APRequestingPublicKeyVerifier), verifier) + } + } +} + +func (m *Module) blockedDomain(host string) (bool, error) { + b := >smodel.DomainBlock{} + err := m.db.GetWhere([]db.Where{{Key: "domain", Value: host, CaseInsensitive: true}}, b) + if err == nil { + // block exists + return true, nil + } + + if _, ok := err.(db.ErrNoEntries); ok { + // there are no entries so there's no block + return false, nil + } + + // there's an actual error + return false, err +} |