diff options
Diffstat (limited to 'internal/api/s2s')
26 files changed, 0 insertions, 3188 deletions
diff --git a/internal/api/s2s/emoji/emoji.go b/internal/api/s2s/emoji/emoji.go deleted file mode 100644 index d448d2105..000000000 --- a/internal/api/s2s/emoji/emoji.go +++ /dev/null @@ -1,53 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 emoji - -import ( - "net/http" - - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" - "github.com/superseriousbusiness/gotosocial/internal/uris" -) - -const ( - // EmojiIDKey is for emoji IDs - EmojiIDKey = "id" - // EmojiBasePath is the base path for serving information about Emojis eg https://example.org/emoji - EmojiWithIDPath = "/" + uris.EmojiPath + "/:" + EmojiIDKey -) - -// Module implements the FederationModule interface -type Module struct { - processor processing.Processor -} - -// New returns a emoji module -func New(processor processing.Processor) api.FederationModule { - return &Module{ - processor: processor, - } -} - -// Route satisfies the RESTAPIModule interface -func (m *Module) Route(s router.Router) error { - s.AttachHandler(http.MethodGet, EmojiWithIDPath, m.EmojiGetHandler) - return nil -} diff --git a/internal/api/s2s/emoji/emojiget.go b/internal/api/s2s/emoji/emojiget.go deleted file mode 100644 index 28a737f9a..000000000 --- a/internal/api/s2s/emoji/emojiget.go +++ /dev/null @@ -1,74 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 emoji - -import ( - "context" - "encoding/json" - "errors" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -// EmojiGetHandler -func (m *Module) EmojiGetHandler(c *gin.Context) { - // usernames on our instance are always lowercase - requestedEmojiID := strings.ToUpper(c.Param(EmojiIDKey)) - if requestedEmojiID == "" { - err := errors.New("no emoji id specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - - format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) - return - } - - ctx := c.Request.Context() - verifier, signed := c.Get(string(ap.ContextRequestingPublicKeyVerifier)) - if signed { - ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier) - } - - signature, signed := c.Get(string(ap.ContextRequestingPublicKeySignature)) - if signed { - ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeySignature, signature) - } - - resp, errWithCode := m.processor.GetFediEmoji(ctx, requestedEmojiID, c.Request.URL) - if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) - return - } - - b, err := json.Marshal(resp) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) - return - } - - c.Data(http.StatusOK, format, b) -} diff --git a/internal/api/s2s/emoji/emojiget_test.go b/internal/api/s2s/emoji/emojiget_test.go deleted file mode 100644 index 16bc9dc1a..000000000 --- a/internal/api/s2s/emoji/emojiget_test.go +++ /dev/null @@ -1,138 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 emoji_test - -import ( - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/emoji" - "github.com/superseriousbusiness/gotosocial/internal/api/security" - "github.com/superseriousbusiness/gotosocial/internal/concurrency" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/email" - "github.com/superseriousbusiness/gotosocial/internal/federation" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/media" - "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/storage" - "github.com/superseriousbusiness/gotosocial/internal/typeutils" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type EmojiGetTestSuite struct { - suite.Suite - db db.DB - tc typeutils.TypeConverter - mediaManager media.Manager - federator federation.Federator - emailSender email.Sender - processor processing.Processor - storage *storage.Driver - oauthServer oauth.Server - securityModule *security.Module - - testEmojis map[string]*gtsmodel.Emoji - testAccounts map[string]*gtsmodel.Account - - emojiModule *emoji.Module -} - -func (suite *EmojiGetTestSuite) SetupSuite() { - suite.testAccounts = testrig.NewTestAccounts() - suite.testEmojis = testrig.NewTestEmojis() -} - -func (suite *EmojiGetTestSuite) SetupTest() { - testrig.InitTestConfig() - testrig.InitTestLog() - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - - suite.db = testrig.NewTestDB() - suite.tc = testrig.NewTestTypeConverter(suite.db) - suite.storage = testrig.NewInMemoryStorage() - suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage) - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) - suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) - suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.emojiModule = emoji.New(suite.processor).(*emoji.Module) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) - suite.securityModule = security.New(suite.db, suite.oauthServer).(*security.Module) - testrig.StandardDBSetup(suite.db, suite.testAccounts) - testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") - - suite.NoError(suite.processor.Start()) -} - -func (suite *EmojiGetTestSuite) TearDownTest() { - testrig.StandardDBTeardown(suite.db) - testrig.StandardStorageTeardown(suite.storage) -} - -func (suite *EmojiGetTestSuite) TestGetEmoji() { - // the dereference we're gonna use - derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) - signedRequest := derefRequests["foss_satan_dereference_emoji"] - targetEmoji := suite.testEmojis["rainbow"] - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, targetEmoji.URI, nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/activity+json") - ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) - ctx.Request.Header.Set("Date", signedRequest.DateHeader) - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: emoji.EmojiIDKey, - Value: targetEmoji.ID, - }, - } - - // trigger the function being tested - suite.emojiModule.EmojiGetHandler(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - - suite.Contains(string(b), `"icon":{"mediaType":"image/png","type":"Image","url":"http://localhost:8080/fileserver/01AY6P665V14JJR0AFVRT7311Y/emoji/original/01F8MH9H8E4VG3KDYJR9EGPXCQ.png"},"id":"http://localhost:8080/emoji/01F8MH9H8E4VG3KDYJR9EGPXCQ","name":":rainbow:","type":"Emoji"`) -} - -func TestEmojiGetTestSuite(t *testing.T) { - suite.Run(t, new(EmojiGetTestSuite)) -} diff --git a/internal/api/s2s/nodeinfo/nodeinfo.go b/internal/api/s2s/nodeinfo/nodeinfo.go deleted file mode 100644 index 539dcc2d1..000000000 --- a/internal/api/s2s/nodeinfo/nodeinfo.go +++ /dev/null @@ -1,53 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 nodeinfo - -import ( - "net/http" - - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" -) - -const ( - // NodeInfoWellKnownPath is the base path for serving responses to nodeinfo lookup requests. - NodeInfoWellKnownPath = ".well-known/nodeinfo" - // NodeInfoBasePath is the path for serving nodeinfo responses. - NodeInfoBasePath = "/nodeinfo/2.0" -) - -// Module implements the FederationModule interface -type Module struct { - processor processing.Processor -} - -// New returns a new nodeinfo module -func New(processor processing.Processor) api.FederationModule { - return &Module{ - processor: processor, - } -} - -// Route satisfies the FederationModule interface -func (m *Module) Route(s router.Router) error { - s.AttachHandler(http.MethodGet, NodeInfoWellKnownPath, m.NodeInfoWellKnownGETHandler) - s.AttachHandler(http.MethodGet, NodeInfoBasePath, m.NodeInfoGETHandler) - return nil -} diff --git a/internal/api/s2s/nodeinfo/nodeinfoget.go b/internal/api/s2s/nodeinfo/nodeinfoget.go deleted file mode 100644 index 6cb5e1ebf..000000000 --- a/internal/api/s2s/nodeinfo/nodeinfoget.go +++ /dev/null @@ -1,66 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 nodeinfo - -import ( - "encoding/json" - "net/http" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -// NodeInfoGETHandler swagger:operation GET /nodeinfo/2.0 nodeInfoGet -// -// Returns a compliant nodeinfo response to node info queries. -// -// See: https://nodeinfo.diaspora.software/schema.html -// -// --- -// tags: -// - nodeinfo -// -// produces: -// - application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#" -// -// responses: -// '200': -// schema: -// "$ref": "#/definitions/nodeinfo" -func (m *Module) NodeInfoGETHandler(c *gin.Context) { - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) - return - } - - ni, errWithCode := m.processor.GetNodeInfo(c.Request.Context(), c.Request) - if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) - return - } - - b, err := json.Marshal(ni) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) - return - } - - c.Data(http.StatusOK, `application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#"`, b) -} diff --git a/internal/api/s2s/nodeinfo/wellknownget.go b/internal/api/s2s/nodeinfo/wellknownget.go deleted file mode 100644 index dc14e43a3..000000000 --- a/internal/api/s2s/nodeinfo/wellknownget.go +++ /dev/null @@ -1,60 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 nodeinfo - -import ( - "net/http" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -// NodeInfoWellKnownGETHandler swagger:operation GET /.well-known/nodeinfo nodeInfoWellKnownGet -// -// Directs callers to /nodeinfo/2.0. -// -// eg. `{"links":[{"rel":"http://nodeinfo.diaspora.software/ns/schema/2.0","href":"http://example.org/nodeinfo/2.0"}]}` -// See: https://nodeinfo.diaspora.software/protocol.html -// -// --- -// tags: -// - nodeinfo -// -// produces: -// - application/json -// -// responses: -// '200': -// schema: -// "$ref": "#/definitions/wellKnownResponse" -func (m *Module) NodeInfoWellKnownGETHandler(c *gin.Context) { - if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) - return - } - - niRel, errWithCode := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request) - if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) - return - } - - c.JSON(http.StatusOK, niRel) -} diff --git a/internal/api/s2s/user/common.go b/internal/api/s2s/user/common.go deleted file mode 100644 index 0a215ebbd..000000000 --- a/internal/api/s2s/user/common.go +++ /dev/null @@ -1,81 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user - -import ( - "context" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/ap" -) - -// transferContext transfers the signature verifier and signature from the gin context to the request context -func transferContext(c *gin.Context) context.Context { - ctx := c.Request.Context() - - verifier, signed := c.Get(string(ap.ContextRequestingPublicKeyVerifier)) - if signed { - ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier) - } - - signature, signed := c.Get(string(ap.ContextRequestingPublicKeySignature)) - if signed { - ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeySignature, signature) - } - - return ctx -} - -// SwaggerCollection represents an activitypub collection. -// swagger:model swaggerCollection -type SwaggerCollection struct { - // ActivityStreams context. - // example: https://www.w3.org/ns/activitystreams - Context string `json:"@context"` - // ActivityStreams ID. - // example: https://example.org/users/some_user/statuses/106717595988259568/replies - ID string `json:"id"` - // ActivityStreams type. - // example: Collection - Type string `json:"type"` - // ActivityStreams first property. - First SwaggerCollectionPage `json:"first"` - // ActivityStreams last property. - Last SwaggerCollectionPage `json:"last,omitempty"` -} - -// SwaggerCollectionPage represents one page of a collection. -// swagger:model swaggerCollectionPage -type SwaggerCollectionPage struct { - // ActivityStreams ID. - // example: https://example.org/users/some_user/statuses/106717595988259568/replies?page=true - ID string `json:"id"` - // ActivityStreams type. - // example: CollectionPage - Type string `json:"type"` - // Link to the next page. - // example: https://example.org/users/some_user/statuses/106717595988259568/replies?only_other_accounts=true&page=true - Next string `json:"next"` - // Collection this page belongs to. - // example: https://example.org/users/some_user/statuses/106717595988259568/replies - PartOf string `json:"partOf"` - // Items on this page. - // example: ["https://example.org/users/some_other_user/statuses/086417595981111564", "https://another.example.com/users/another_user/statuses/01FCN8XDV3YG7B4R42QA6YQZ9R"] - Items []string `json:"items"` -} diff --git a/internal/api/s2s/user/followers.go b/internal/api/s2s/user/followers.go deleted file mode 100644 index 675688311..000000000 --- a/internal/api/s2s/user/followers.go +++ /dev/null @@ -1,67 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user - -import ( - "encoding/json" - "errors" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -// FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it. -func (m *Module) FollowersGETHandler(c *gin.Context) { - // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) - if requestedUsername == "" { - err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) - return - } - - if format == string(api.TextHTML) { - // redirect to the user's profile - c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) - return - } - - resp, errWithCode := m.processor.GetFediFollowers(transferContext(c), requestedUsername, c.Request.URL) - if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) - return - } - - b, err := json.Marshal(resp) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) - return - } - - c.Data(http.StatusOK, format, b) -} diff --git a/internal/api/s2s/user/following.go b/internal/api/s2s/user/following.go deleted file mode 100644 index d4404ea08..000000000 --- a/internal/api/s2s/user/following.go +++ /dev/null @@ -1,67 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user - -import ( - "encoding/json" - "errors" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -// FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it. -func (m *Module) FollowingGETHandler(c *gin.Context) { - // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) - if requestedUsername == "" { - err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) - return - } - - if format == string(api.TextHTML) { - // redirect to the user's profile - c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) - return - } - - resp, errWithCode := m.processor.GetFediFollowing(transferContext(c), requestedUsername, c.Request.URL) - if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) - return - } - - b, err := json.Marshal(resp) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) - return - } - - c.Data(http.StatusOK, format, b) -} diff --git a/internal/api/s2s/user/inboxpost.go b/internal/api/s2s/user/inboxpost.go deleted file mode 100644 index fa6792cd2..000000000 --- a/internal/api/s2s/user/inboxpost.go +++ /dev/null @@ -1,51 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user - -import ( - "errors" - "strings" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" //nolint:typecheck -) - -// InboxPOSTHandler deals with incoming POST requests to an actor's inbox. -// Eg., POST to https://example.org/users/whatever/inbox. -func (m *Module) InboxPOSTHandler(c *gin.Context) { - // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) - if requestedUsername == "" { - err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - - if posted, err := m.processor.InboxPost(transferContext(c), c.Writer, c.Request); err != nil { - if withCode, ok := err.(gtserror.WithCode); ok { - api.ErrorHandler(c, withCode, m.processor.InstanceGet) - } else { - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - } - } else if !posted { - err := errors.New("unable to process request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - } -} diff --git a/internal/api/s2s/user/inboxpost_test.go b/internal/api/s2s/user/inboxpost_test.go deleted file mode 100644 index 3821406be..000000000 --- a/internal/api/s2s/user/inboxpost_test.go +++ /dev/null @@ -1,500 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user_test - -import ( - "bytes" - "context" - "encoding/json" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - "time" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/activity/pub" - "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" - "github.com/superseriousbusiness/gotosocial/internal/concurrency" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/id" - "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type InboxPostTestSuite struct { - UserStandardTestSuite -} - -func (suite *InboxPostTestSuite) TestPostBlock() { - blockingAccount := suite.testAccounts["remote_account_1"] - blockedAccount := suite.testAccounts["local_account_1"] - blockURI := testrig.URLMustParse("http://fossbros-anonymous.io/users/foss_satan/blocks/01FG9C441MCTW3R2W117V2PQK3") - - block := streams.NewActivityStreamsBlock() - - // set the actor property to the block-ing account's URI - actorProp := streams.NewActivityStreamsActorProperty() - actorIRI := testrig.URLMustParse(blockingAccount.URI) - actorProp.AppendIRI(actorIRI) - block.SetActivityStreamsActor(actorProp) - - // set the ID property to the blocks's URI - idProp := streams.NewJSONLDIdProperty() - idProp.Set(blockURI) - block.SetJSONLDId(idProp) - - // set the object property to the target account's URI - objectProp := streams.NewActivityStreamsObjectProperty() - targetIRI := testrig.URLMustParse(blockedAccount.URI) - objectProp.AppendIRI(targetIRI) - block.SetActivityStreamsObject(objectProp) - - // set the TO property to the target account's IRI - toProp := streams.NewActivityStreamsToProperty() - toIRI := testrig.URLMustParse(blockedAccount.URI) - toProp.AppendIRI(toIRI) - block.SetActivityStreamsTo(toProp) - - targetURI := testrig.URLMustParse(blockedAccount.InboxURI) - - signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(block, blockingAccount.PublicKeyURI, blockingAccount.PrivateKey, targetURI) - bodyI, err := streams.Serialize(block) - suite.NoError(err) - - bodyJson, err := json.Marshal(bodyI) - suite.NoError(err) - body := bytes.NewReader(bodyJson) - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - - tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker) - federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) - emailSender := testrig.NewEmailSender("../../../../web/template/", nil) - processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) - suite.NoError(processor.Start()) - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting - ctx.Request.Header.Set("Signature", signature) - ctx.Request.Header.Set("Date", dateHeader) - ctx.Request.Header.Set("Digest", digestHeader) - ctx.Request.Header.Set("Content-Type", "application/activity+json") - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: blockedAccount.Username, - }, - } - - // trigger the function being tested - userModule.InboxPOSTHandler(ctx) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Empty(b) - - // there should be a block in the database now between the accounts - dbBlock, err := suite.db.GetBlock(context.Background(), blockingAccount.ID, blockedAccount.ID) - suite.NoError(err) - suite.NotNil(dbBlock) - suite.WithinDuration(time.Now(), dbBlock.CreatedAt, 30*time.Second) - suite.WithinDuration(time.Now(), dbBlock.UpdatedAt, 30*time.Second) - suite.Equal("http://fossbros-anonymous.io/users/foss_satan/blocks/01FG9C441MCTW3R2W117V2PQK3", dbBlock.URI) -} - -// TestPostUnblock verifies that a remote account with a block targeting one of our instance users should be able to undo that block. -func (suite *InboxPostTestSuite) TestPostUnblock() { - blockingAccount := suite.testAccounts["remote_account_1"] - blockedAccount := suite.testAccounts["local_account_1"] - - // first put a block in the database so we have something to undo - blockURI := "http://fossbros-anonymous.io/users/foss_satan/blocks/01FG9C441MCTW3R2W117V2PQK3" - dbBlockID, err := id.NewRandomULID() - suite.NoError(err) - - dbBlock := >smodel.Block{ - ID: dbBlockID, - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - URI: blockURI, - AccountID: blockingAccount.ID, - TargetAccountID: blockedAccount.ID, - } - - err = suite.db.PutBlock(context.Background(), dbBlock) - suite.NoError(err) - - asBlock, err := suite.tc.BlockToAS(context.Background(), dbBlock) - suite.NoError(err) - - targetAccountURI := testrig.URLMustParse(blockedAccount.URI) - - // create an Undo and set the appropriate actor on it - undo := streams.NewActivityStreamsUndo() - undo.SetActivityStreamsActor(asBlock.GetActivityStreamsActor()) - - // Set the block as the 'object' property. - undoObject := streams.NewActivityStreamsObjectProperty() - undoObject.AppendActivityStreamsBlock(asBlock) - undo.SetActivityStreamsObject(undoObject) - - // Set the To of the undo as the target of the block - undoTo := streams.NewActivityStreamsToProperty() - undoTo.AppendIRI(targetAccountURI) - undo.SetActivityStreamsTo(undoTo) - - undoID := streams.NewJSONLDIdProperty() - undoID.SetIRI(testrig.URLMustParse("http://fossbros-anonymous.io/72cc96a3-f742-4daf-b9f5-3407667260c5")) - undo.SetJSONLDId(undoID) - - targetURI := testrig.URLMustParse(blockedAccount.InboxURI) - - signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(undo, blockingAccount.PublicKeyURI, blockingAccount.PrivateKey, targetURI) - bodyI, err := streams.Serialize(undo) - suite.NoError(err) - - bodyJson, err := json.Marshal(bodyI) - suite.NoError(err) - body := bytes.NewReader(bodyJson) - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - - tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker) - federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) - emailSender := testrig.NewEmailSender("../../../../web/template/", nil) - processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) - suite.NoError(processor.Start()) - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting - ctx.Request.Header.Set("Signature", signature) - ctx.Request.Header.Set("Date", dateHeader) - ctx.Request.Header.Set("Digest", digestHeader) - ctx.Request.Header.Set("Content-Type", "application/activity+json") - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: blockedAccount.Username, - }, - } - - // trigger the function being tested - userModule.InboxPOSTHandler(ctx) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Empty(b) - suite.Equal(http.StatusOK, result.StatusCode) - - // the block should be undone - block, err := suite.db.GetBlock(context.Background(), blockingAccount.ID, blockedAccount.ID) - suite.ErrorIs(err, db.ErrNoEntries) - suite.Nil(block) -} - -func (suite *InboxPostTestSuite) TestPostUpdate() { - updatedAccount := *suite.testAccounts["remote_account_1"] - updatedAccount.DisplayName = "updated display name!" - - // ad an emoji to the account; because we're serializing this remote - // account from our own instance, we need to cheat a bit to get the emoji - // to work properly, just for this test - testEmoji := >smodel.Emoji{} - *testEmoji = *testrig.NewTestEmojis()["yell"] - testEmoji.ImageURL = testEmoji.ImageRemoteURL // <- here's the cheat - updatedAccount.Emojis = []*gtsmodel.Emoji{testEmoji} - - asAccount, err := suite.tc.AccountToAS(context.Background(), &updatedAccount) - suite.NoError(err) - - receivingAccount := suite.testAccounts["local_account_1"] - - // create an update - update := streams.NewActivityStreamsUpdate() - - // set the appropriate actor on it - updateActor := streams.NewActivityStreamsActorProperty() - updateActor.AppendIRI(testrig.URLMustParse(updatedAccount.URI)) - update.SetActivityStreamsActor(updateActor) - - // Set the account as the 'object' property. - updateObject := streams.NewActivityStreamsObjectProperty() - updateObject.AppendActivityStreamsPerson(asAccount) - update.SetActivityStreamsObject(updateObject) - - // Set the To of the update as public - updateTo := streams.NewActivityStreamsToProperty() - updateTo.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) - update.SetActivityStreamsTo(updateTo) - - // set the cc of the update to the receivingAccount - updateCC := streams.NewActivityStreamsCcProperty() - updateCC.AppendIRI(testrig.URLMustParse(receivingAccount.URI)) - update.SetActivityStreamsCc(updateCC) - - // set some random-ass ID for the activity - undoID := streams.NewJSONLDIdProperty() - undoID.SetIRI(testrig.URLMustParse("http://fossbros-anonymous.io/d360613a-dc8d-4563-8f0b-b6161caf0f2b")) - update.SetJSONLDId(undoID) - - targetURI := testrig.URLMustParse(receivingAccount.InboxURI) - - signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(update, updatedAccount.PublicKeyURI, updatedAccount.PrivateKey, targetURI) - bodyI, err := streams.Serialize(update) - suite.NoError(err) - - bodyJson, err := json.Marshal(bodyI) - suite.NoError(err) - body := bytes.NewReader(bodyJson) - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - - tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker) - federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) - emailSender := testrig.NewEmailSender("../../../../web/template/", nil) - processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) - suite.NoError(processor.Start()) - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting - ctx.Request.Header.Set("Signature", signature) - ctx.Request.Header.Set("Date", dateHeader) - ctx.Request.Header.Set("Digest", digestHeader) - ctx.Request.Header.Set("Content-Type", "application/activity+json") - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: receivingAccount.Username, - }, - } - - // trigger the function being tested - userModule.InboxPOSTHandler(ctx) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Empty(b) - suite.Equal(http.StatusOK, result.StatusCode) - - // account should be changed in the database now - var dbUpdatedAccount *gtsmodel.Account - - if !testrig.WaitFor(func() bool { - // displayName should be updated - dbUpdatedAccount, _ = suite.db.GetAccountByID(context.Background(), updatedAccount.ID) - return dbUpdatedAccount.DisplayName == "updated display name!" - }) { - suite.FailNow("timed out waiting for account update") - } - - // emojis should be updated - suite.Contains(dbUpdatedAccount.EmojiIDs, testEmoji.ID) - - // account should be freshly webfingered - suite.WithinDuration(time.Now(), dbUpdatedAccount.LastWebfingeredAt, 10*time.Second) - - // everything else should be the same as it was before - suite.EqualValues(updatedAccount.Username, dbUpdatedAccount.Username) - suite.EqualValues(updatedAccount.Domain, dbUpdatedAccount.Domain) - suite.EqualValues(updatedAccount.AvatarMediaAttachmentID, dbUpdatedAccount.AvatarMediaAttachmentID) - suite.EqualValues(updatedAccount.AvatarMediaAttachment, dbUpdatedAccount.AvatarMediaAttachment) - suite.EqualValues(updatedAccount.AvatarRemoteURL, dbUpdatedAccount.AvatarRemoteURL) - suite.EqualValues(updatedAccount.HeaderMediaAttachmentID, dbUpdatedAccount.HeaderMediaAttachmentID) - suite.EqualValues(updatedAccount.HeaderMediaAttachment, dbUpdatedAccount.HeaderMediaAttachment) - suite.EqualValues(updatedAccount.HeaderRemoteURL, dbUpdatedAccount.HeaderRemoteURL) - suite.EqualValues(updatedAccount.Note, dbUpdatedAccount.Note) - suite.EqualValues(updatedAccount.Memorial, dbUpdatedAccount.Memorial) - suite.EqualValues(updatedAccount.AlsoKnownAs, dbUpdatedAccount.AlsoKnownAs) - suite.EqualValues(updatedAccount.MovedToAccountID, dbUpdatedAccount.MovedToAccountID) - suite.EqualValues(updatedAccount.Bot, dbUpdatedAccount.Bot) - suite.EqualValues(updatedAccount.Reason, dbUpdatedAccount.Reason) - suite.EqualValues(updatedAccount.Locked, dbUpdatedAccount.Locked) - suite.EqualValues(updatedAccount.Discoverable, dbUpdatedAccount.Discoverable) - suite.EqualValues(updatedAccount.Privacy, dbUpdatedAccount.Privacy) - suite.EqualValues(updatedAccount.Sensitive, dbUpdatedAccount.Sensitive) - suite.EqualValues(updatedAccount.Language, dbUpdatedAccount.Language) - suite.EqualValues(updatedAccount.URI, dbUpdatedAccount.URI) - suite.EqualValues(updatedAccount.URL, dbUpdatedAccount.URL) - suite.EqualValues(updatedAccount.InboxURI, dbUpdatedAccount.InboxURI) - suite.EqualValues(updatedAccount.OutboxURI, dbUpdatedAccount.OutboxURI) - suite.EqualValues(updatedAccount.FollowingURI, dbUpdatedAccount.FollowingURI) - suite.EqualValues(updatedAccount.FollowersURI, dbUpdatedAccount.FollowersURI) - suite.EqualValues(updatedAccount.FeaturedCollectionURI, dbUpdatedAccount.FeaturedCollectionURI) - suite.EqualValues(updatedAccount.ActorType, dbUpdatedAccount.ActorType) - suite.EqualValues(updatedAccount.PublicKey, dbUpdatedAccount.PublicKey) - suite.EqualValues(updatedAccount.PublicKeyURI, dbUpdatedAccount.PublicKeyURI) - suite.EqualValues(updatedAccount.SensitizedAt, dbUpdatedAccount.SensitizedAt) - suite.EqualValues(updatedAccount.SilencedAt, dbUpdatedAccount.SilencedAt) - suite.EqualValues(updatedAccount.SuspendedAt, dbUpdatedAccount.SuspendedAt) - suite.EqualValues(updatedAccount.HideCollections, dbUpdatedAccount.HideCollections) - suite.EqualValues(updatedAccount.SuspensionOrigin, dbUpdatedAccount.SuspensionOrigin) -} - -func (suite *InboxPostTestSuite) TestPostDelete() { - deletedAccount := *suite.testAccounts["remote_account_1"] - receivingAccount := suite.testAccounts["local_account_1"] - - // create a delete - delete := streams.NewActivityStreamsDelete() - - // set the appropriate actor on it - deleteActor := streams.NewActivityStreamsActorProperty() - deleteActor.AppendIRI(testrig.URLMustParse(deletedAccount.URI)) - delete.SetActivityStreamsActor(deleteActor) - - // Set the account iri as the 'object' property. - deleteObject := streams.NewActivityStreamsObjectProperty() - deleteObject.AppendIRI(testrig.URLMustParse(deletedAccount.URI)) - delete.SetActivityStreamsObject(deleteObject) - - // Set the To of the delete as public - deleteTo := streams.NewActivityStreamsToProperty() - deleteTo.AppendIRI(testrig.URLMustParse(pub.PublicActivityPubIRI)) - delete.SetActivityStreamsTo(deleteTo) - - // set some random-ass ID for the activity - deleteID := streams.NewJSONLDIdProperty() - deleteID.SetIRI(testrig.URLMustParse("http://fossbros-anonymous.io/d360613a-dc8d-4563-8f0b-b6161caf0f2b")) - delete.SetJSONLDId(deleteID) - - targetURI := testrig.URLMustParse(receivingAccount.InboxURI) - - signature, digestHeader, dateHeader := testrig.GetSignatureForActivity(delete, deletedAccount.PublicKeyURI, deletedAccount.PrivateKey, targetURI) - bodyI, err := streams.Serialize(delete) - suite.NoError(err) - - bodyJson, err := json.Marshal(bodyI) - suite.NoError(err) - body := bytes.NewReader(bodyJson) - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - - tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker) - federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) - emailSender := testrig.NewEmailSender("../../../../web/template/", nil) - processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.NoError(processor.Start()) - userModule := user.New(processor).(*user.Module) - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodPost, targetURI.String(), body) // the endpoint we're hitting - ctx.Request.Header.Set("Signature", signature) - ctx.Request.Header.Set("Date", dateHeader) - ctx.Request.Header.Set("Digest", digestHeader) - ctx.Request.Header.Set("Content-Type", "application/activity+json") - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: receivingAccount.Username, - }, - } - - // trigger the function being tested - userModule.InboxPOSTHandler(ctx) - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Empty(b) - suite.Equal(http.StatusOK, result.StatusCode) - - if !testrig.WaitFor(func() bool { - // local account 2 blocked foss_satan, that block should be gone now - testBlock := suite.testBlocks["local_account_2_block_remote_account_1"] - dbBlock := >smodel.Block{} - err = suite.db.GetByID(ctx, testBlock.ID, dbBlock) - return suite.ErrorIs(err, db.ErrNoEntries) - }) { - suite.FailNow("timed out waiting for block to be removed") - } - - // no statuses from foss satan should be left in the database - dbStatuses, err := suite.db.GetAccountStatuses(ctx, deletedAccount.ID, 0, false, false, "", "", false, false, false) - suite.ErrorIs(err, db.ErrNoEntries) - suite.Empty(dbStatuses) - - dbAccount, err := suite.db.GetAccountByID(ctx, deletedAccount.ID) - suite.NoError(err) - - suite.Empty(dbAccount.Note) - suite.Empty(dbAccount.DisplayName) - suite.Empty(dbAccount.AvatarMediaAttachmentID) - suite.Empty(dbAccount.AvatarRemoteURL) - suite.Empty(dbAccount.HeaderMediaAttachmentID) - suite.Empty(dbAccount.HeaderRemoteURL) - suite.Empty(dbAccount.Reason) - suite.Empty(dbAccount.Fields) - suite.True(*dbAccount.HideCollections) - suite.False(*dbAccount.Discoverable) - suite.WithinDuration(time.Now(), dbAccount.SuspendedAt, 30*time.Second) - suite.Equal(dbAccount.ID, dbAccount.SuspensionOrigin) -} - -func TestInboxPostTestSuite(t *testing.T) { - suite.Run(t, &InboxPostTestSuite{}) -} diff --git a/internal/api/s2s/user/outboxget.go b/internal/api/s2s/user/outboxget.go deleted file mode 100644 index 726f86237..000000000 --- a/internal/api/s2s/user/outboxget.go +++ /dev/null @@ -1,145 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "strconv" - "strings" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -// OutboxGETHandler swagger:operation GET /users/{username}/outbox s2sOutboxGet -// -// Get the public outbox collection for an actor. -// -// Note that the response will be a Collection with a page as `first`, as shown below, if `page` is `false`. -// -// If `page` is `true`, then the response will be a single `CollectionPage` without the wrapping `Collection`. -// -// HTTP signature is required on the request. -// -// --- -// tags: -// - s2s/federation -// -// produces: -// - application/activity+json -// -// parameters: -// - -// name: username -// type: string -// description: Username of the account. -// in: path -// required: true -// - -// name: page -// type: boolean -// description: Return response as a CollectionPage. -// in: query -// default: false -// - -// name: min_id -// type: string -// description: Minimum ID of the next status, used for paging. -// in: query -// - -// name: max_id -// type: string -// description: Maximum ID of the next status, used for paging. -// in: query -// -// responses: -// '200': -// in: body -// schema: -// "$ref": "#/definitions/swaggerCollection" -// '400': -// description: bad request -// '401': -// description: unauthorized -// '403': -// description: forbidden -// '404': -// description: not found -func (m *Module) OutboxGETHandler(c *gin.Context) { - // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) - if requestedUsername == "" { - err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) - return - } - - if format == string(api.TextHTML) { - // redirect to the user's profile - c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) - return - } - - var page bool - if pageString := c.Query(PageKey); pageString != "" { - i, err := strconv.ParseBool(pageString) - if err != nil { - err := fmt.Errorf("error parsing %s: %s", PageKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - page = i - } - - minID := "" - minIDString := c.Query(MinIDKey) - if minIDString != "" { - minID = minIDString - } - - maxID := "" - maxIDString := c.Query(MaxIDKey) - if maxIDString != "" { - maxID = maxIDString - } - - resp, errWithCode := m.processor.GetFediOutbox(transferContext(c), requestedUsername, page, maxID, minID, c.Request.URL) - if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) - return - } - - b, err := json.Marshal(resp) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) - return - } - - c.Data(http.StatusOK, format, b) -} diff --git a/internal/api/s2s/user/outboxget_test.go b/internal/api/s2s/user/outboxget_test.go deleted file mode 100644 index 35a048323..000000000 --- a/internal/api/s2s/user/outboxget_test.go +++ /dev/null @@ -1,216 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user_test - -import ( - "context" - "encoding/json" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/activity/streams/vocab" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" - "github.com/superseriousbusiness/gotosocial/internal/concurrency" - "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type OutboxGetTestSuite struct { - UserStandardTestSuite -} - -func (suite *OutboxGetTestSuite) TestGetOutbox() { - // the dereference we're gonna use - derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) - signedRequest := derefRequests["foss_satan_dereference_zork_outbox"] - targetAccount := suite.testAccounts["local_account_1"] - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.OutboxURI, nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/activity+json") - ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) - ctx.Request.Header.Set("Date", signedRequest.DateHeader) - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: targetAccount.Username, - }, - } - - // trigger the function being tested - suite.userModule.OutboxGETHandler(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","first":"http://localhost:8080/users/the_mighty_zork/outbox?page=true","id":"http://localhost:8080/users/the_mighty_zork/outbox","type":"OrderedCollection"}`, string(b)) - - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - suite.NoError(err) - - t, err := streams.ToType(context.Background(), m) - suite.NoError(err) - - _, ok := t.(vocab.ActivityStreamsOrderedCollection) - suite.True(ok) -} - -func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() { - // the dereference we're gonna use - derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) - signedRequest := derefRequests["foss_satan_dereference_zork_outbox_first"] - targetAccount := suite.testAccounts["local_account_1"] - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - - tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker) - federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) - emailSender := testrig.NewEmailSender("../../../../web/template/", nil) - processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) - suite.NoError(processor.Start()) - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.OutboxURI+"?page=true", nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/activity+json") - ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) - ctx.Request.Header.Set("Date", signedRequest.DateHeader) - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: targetAccount.Username, - }, - } - - // trigger the function being tested - userModule.OutboxGETHandler(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/outbox?page=true","next":"http://localhost:8080/users/the_mighty_zork/outbox?page=true\u0026max_id=01F8MHAMCHF6Y650WCRSCP4WMY","orderedItems":{"actor":"http://localhost:8080/users/the_mighty_zork","cc":"http://localhost:8080/users/the_mighty_zork/followers","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/activity","object":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY","published":"2021-10-20T10:40:37Z","to":"https://www.w3.org/ns/activitystreams#Public","type":"Create"},"partOf":"http://localhost:8080/users/the_mighty_zork/outbox","prev":"http://localhost:8080/users/the_mighty_zork/outbox?page=true\u0026min_id=01F8MHAMCHF6Y650WCRSCP4WMY","type":"OrderedCollectionPage"}`, string(b)) - - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - suite.NoError(err) - - t, err := streams.ToType(context.Background(), m) - suite.NoError(err) - - _, ok := t.(vocab.ActivityStreamsOrderedCollectionPage) - suite.True(ok) -} - -func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() { - // the dereference we're gonna use - derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) - signedRequest := derefRequests["foss_satan_dereference_zork_outbox_next"] - targetAccount := suite.testAccounts["local_account_1"] - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - - tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker) - federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) - emailSender := testrig.NewEmailSender("../../../../web/template/", nil) - processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) - suite.NoError(processor.Start()) - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.OutboxURI+"?page=true&max_id=01F8MHAMCHF6Y650WCRSCP4WMY", nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/activity+json") - ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) - ctx.Request.Header.Set("Date", signedRequest.DateHeader) - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: targetAccount.Username, - }, - gin.Param{ - Key: user.MaxIDKey, - Value: "01F8MHAMCHF6Y650WCRSCP4WMY", - }, - } - - // trigger the function being tested - userModule.OutboxGETHandler(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - suite.Equal(`{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/outbox?page=true\u0026maxID=01F8MHAMCHF6Y650WCRSCP4WMY","orderedItems":[],"partOf":"http://localhost:8080/users/the_mighty_zork/outbox","type":"OrderedCollectionPage"}`, string(b)) - - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - suite.NoError(err) - - t, err := streams.ToType(context.Background(), m) - suite.NoError(err) - - _, ok := t.(vocab.ActivityStreamsOrderedCollectionPage) - suite.True(ok) -} - -func TestOutboxGetTestSuite(t *testing.T) { - suite.Run(t, new(OutboxGetTestSuite)) -} diff --git a/internal/api/s2s/user/publickeyget.go b/internal/api/s2s/user/publickeyget.go deleted file mode 100644 index 265c01ee5..000000000 --- a/internal/api/s2s/user/publickeyget.go +++ /dev/null @@ -1,71 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user - -import ( - "encoding/json" - "errors" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -// PublicKeyGETHandler should be served at eg https://example.org/users/:username/main-key. -// -// The goal here is to return a MINIMAL activitypub representation of an account -// in the form of a vocab.ActivityStreamsPerson. The account will only contain the id, -// public key, username, and type of the account. -func (m *Module) PublicKeyGETHandler(c *gin.Context) { - // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) - if requestedUsername == "" { - err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) - return - } - - if format == string(api.TextHTML) { - // redirect to the user's profile - c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) - return - } - - resp, errWithCode := m.processor.GetFediUser(transferContext(c), requestedUsername, c.Request.URL) - if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) - return - } - - b, err := json.Marshal(resp) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) - return - } - - c.Data(http.StatusOK, format, b) -} diff --git a/internal/api/s2s/user/repliesget.go b/internal/api/s2s/user/repliesget.go deleted file mode 100644 index b3b20d0c2..000000000 --- a/internal/api/s2s/user/repliesget.go +++ /dev/null @@ -1,166 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user - -import ( - "encoding/json" - "errors" - "fmt" - "net/http" - "strconv" - "strings" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -// StatusRepliesGETHandler swagger:operation GET /users/{username}/statuses/{status}/replies s2sRepliesGet -// -// Get the replies collection for a status. -// -// Note that the response will be a Collection with a page as `first`, as shown below, if `page` is `false`. -// -// If `page` is `true`, then the response will be a single `CollectionPage` without the wrapping `Collection`. -// -// HTTP signature is required on the request. -// -// --- -// tags: -// - s2s/federation -// -// produces: -// - application/activity+json -// -// parameters: -// - -// name: username -// type: string -// description: Username of the account. -// in: path -// required: true -// - -// name: status -// type: string -// description: ID of the status. -// in: path -// required: true -// - -// name: page -// type: boolean -// description: Return response as a CollectionPage. -// in: query -// default: false -// - -// name: only_other_accounts -// type: boolean -// description: Return replies only from accounts other than the status owner. -// in: query -// default: false -// - -// name: min_id -// type: string -// description: Minimum ID of the next status, used for paging. -// in: query -// -// responses: -// '200': -// in: body -// schema: -// "$ref": "#/definitions/swaggerCollection" -// '400': -// description: bad request -// '401': -// description: unauthorized -// '403': -// description: forbidden -// '404': -// description: not found -func (m *Module) StatusRepliesGETHandler(c *gin.Context) { - // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) - if requestedUsername == "" { - err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - - // status IDs on our instance are always uppercase - requestedStatusID := strings.ToUpper(c.Param(StatusIDKey)) - if requestedStatusID == "" { - err := errors.New("no status id specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) - return - } - - if format == string(api.TextHTML) { - // redirect to the status - c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID) - return - } - - var page bool - if pageString := c.Query(PageKey); pageString != "" { - i, err := strconv.ParseBool(pageString) - if err != nil { - err := fmt.Errorf("error parsing %s: %s", PageKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - page = i - } - - onlyOtherAccounts := false - onlyOtherAccountsString := c.Query(OnlyOtherAccountsKey) - if onlyOtherAccountsString != "" { - i, err := strconv.ParseBool(onlyOtherAccountsString) - if err != nil { - err := fmt.Errorf("error parsing %s: %s", OnlyOtherAccountsKey, err) - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - onlyOtherAccounts = i - } - - minID := "" - minIDString := c.Query(MinIDKey) - if minIDString != "" { - minID = minIDString - } - - resp, errWithCode := m.processor.GetFediStatusReplies(transferContext(c), requestedUsername, requestedStatusID, page, onlyOtherAccounts, minID, c.Request.URL) - if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) - return - } - - b, err := json.Marshal(resp) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) - return - } - - c.Data(http.StatusOK, format, b) -} diff --git a/internal/api/s2s/user/repliesget_test.go b/internal/api/s2s/user/repliesget_test.go deleted file mode 100644 index 5ab7f1ebd..000000000 --- a/internal/api/s2s/user/repliesget_test.go +++ /dev/null @@ -1,239 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user_test - -import ( - "context" - "encoding/json" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/activity/streams/vocab" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" - "github.com/superseriousbusiness/gotosocial/internal/concurrency" - "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type RepliesGetTestSuite struct { - UserStandardTestSuite -} - -func (suite *RepliesGetTestSuite) TestGetReplies() { - // the dereference we're gonna use - derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) - signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1_replies"] - targetAccount := suite.testAccounts["local_account_1"] - targetStatus := suite.testStatuses["local_account_1_status_1"] - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI+"/replies", nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/activity+json") - ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) - ctx.Request.Header.Set("Date", signedRequest.DateHeader) - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: targetAccount.Username, - }, - gin.Param{ - Key: user.StatusIDKey, - Value: targetStatus.ID, - }, - } - - // trigger the function being tested - suite.userModule.StatusRepliesGETHandler(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) - assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","first":{"id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true","next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"},"id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"Collection"}`, string(b)) - - // should be a Collection - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - assert.NoError(suite.T(), err) - - t, err := streams.ToType(context.Background(), m) - assert.NoError(suite.T(), err) - - _, ok := t.(vocab.ActivityStreamsCollection) - assert.True(suite.T(), ok) -} - -func (suite *RepliesGetTestSuite) TestGetRepliesNext() { - // the dereference we're gonna use - derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) - signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1_replies_next"] - targetAccount := suite.testAccounts["local_account_1"] - targetStatus := suite.testStatuses["local_account_1_status_1"] - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - - tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker) - federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) - emailSender := testrig.NewEmailSender("../../../../web/template/", nil) - processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) - suite.NoError(processor.Start()) - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI+"/replies?only_other_accounts=false&page=true", nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/activity+json") - ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) - ctx.Request.Header.Set("Date", signedRequest.DateHeader) - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: targetAccount.Username, - }, - gin.Param{ - Key: user.StatusIDKey, - Value: targetStatus.ID, - }, - } - - // trigger the function being tested - userModule.StatusRepliesGETHandler(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) - - assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true\u0026only_other_accounts=false","items":"http://localhost:8080/users/admin/statuses/01FF25D5Q0DH7CHD57CTRS6WK0","next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true\u0026min_id=01FF25D5Q0DH7CHD57CTRS6WK0","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"}`, string(b)) - - // should be a Collection - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - assert.NoError(suite.T(), err) - - t, err := streams.ToType(context.Background(), m) - assert.NoError(suite.T(), err) - - page, ok := t.(vocab.ActivityStreamsCollectionPage) - assert.True(suite.T(), ok) - - assert.Equal(suite.T(), page.GetActivityStreamsItems().Len(), 1) -} - -func (suite *RepliesGetTestSuite) TestGetRepliesLast() { - // the dereference we're gonna use - derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) - signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1_replies_last"] - targetAccount := suite.testAccounts["local_account_1"] - targetStatus := suite.testStatuses["local_account_1_status_1"] - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - - tc := testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker) - federator := testrig.NewTestFederator(suite.db, tc, suite.storage, suite.mediaManager, fedWorker) - emailSender := testrig.NewEmailSender("../../../../web/template/", nil) - processor := testrig.NewTestProcessor(suite.db, suite.storage, federator, emailSender, suite.mediaManager, clientWorker, fedWorker) - userModule := user.New(processor).(*user.Module) - suite.NoError(processor.Start()) - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI+"/replies?only_other_accounts=false&page=true&min_id=01FF25D5Q0DH7CHD57CTRS6WK0", nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/activity+json") - ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) - ctx.Request.Header.Set("Date", signedRequest.DateHeader) - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: targetAccount.Username, - }, - gin.Param{ - Key: user.StatusIDKey, - Value: targetStatus.ID, - }, - } - - // trigger the function being tested - userModule.StatusRepliesGETHandler(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) - - fmt.Println(string(b)) - assert.Equal(suite.T(), `{"@context":"https://www.w3.org/ns/activitystreams","id":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?page=true\u0026only_other_accounts=false\u0026min_id=01FF25D5Q0DH7CHD57CTRS6WK0","items":[],"next":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies?only_other_accounts=false\u0026page=true","partOf":"http://localhost:8080/users/the_mighty_zork/statuses/01F8MHAMCHF6Y650WCRSCP4WMY/replies","type":"CollectionPage"}`, string(b)) - - // should be a Collection - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - assert.NoError(suite.T(), err) - - t, err := streams.ToType(context.Background(), m) - assert.NoError(suite.T(), err) - - page, ok := t.(vocab.ActivityStreamsCollectionPage) - assert.True(suite.T(), ok) - - assert.Equal(suite.T(), page.GetActivityStreamsItems().Len(), 0) -} - -func TestRepliesGetTestSuite(t *testing.T) { - suite.Run(t, new(RepliesGetTestSuite)) -} diff --git a/internal/api/s2s/user/statusget.go b/internal/api/s2s/user/statusget.go deleted file mode 100644 index 3e3d6ea56..000000000 --- a/internal/api/s2s/user/statusget.go +++ /dev/null @@ -1,75 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user - -import ( - "encoding/json" - "errors" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -// StatusGETHandler serves the target status as an activitystreams NOTE so that other AP servers can parse it. -func (m *Module) StatusGETHandler(c *gin.Context) { - // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) - if requestedUsername == "" { - err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - - // status IDs on our instance are always uppercase - requestedStatusID := strings.ToUpper(c.Param(StatusIDKey)) - if requestedStatusID == "" { - err := errors.New("no status id specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) - return - } - - if format == string(api.TextHTML) { - // redirect to the status - c.Redirect(http.StatusSeeOther, "/@"+requestedUsername+"/statuses/"+requestedStatusID) - return - } - - resp, errWithCode := m.processor.GetFediStatus(transferContext(c), requestedUsername, requestedStatusID, c.Request.URL) - if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) - return - } - - b, err := json.Marshal(resp) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) - return - } - - c.Data(http.StatusOK, format, b) -} diff --git a/internal/api/s2s/user/statusget_test.go b/internal/api/s2s/user/statusget_test.go deleted file mode 100644 index 42f7dbb1b..000000000 --- a/internal/api/s2s/user/statusget_test.go +++ /dev/null @@ -1,162 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user_test - -import ( - "context" - "encoding/json" - "io/ioutil" - "net/http" - "net/http/httptest" - "strings" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/activity/streams/vocab" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type StatusGetTestSuite struct { - UserStandardTestSuite -} - -func (suite *StatusGetTestSuite) TestGetStatus() { - // the dereference we're gonna use - derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) - signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1"] - targetAccount := suite.testAccounts["local_account_1"] - targetStatus := suite.testStatuses["local_account_1_status_1"] - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, targetStatus.URI, nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/activity+json") - ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) - ctx.Request.Header.Set("Date", signedRequest.DateHeader) - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: targetAccount.Username, - }, - gin.Param{ - Key: user.StatusIDKey, - Value: targetStatus.ID, - }, - } - - // trigger the function being tested - suite.userModule.StatusGETHandler(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - - // should be a Note - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - suite.NoError(err) - - t, err := streams.ToType(context.Background(), m) - suite.NoError(err) - - note, ok := t.(vocab.ActivityStreamsNote) - suite.True(ok) - - // convert note to status - a, err := suite.tc.ASStatusToStatus(context.Background(), note) - suite.NoError(err) - suite.EqualValues(targetStatus.Content, a.Content) -} - -func (suite *StatusGetTestSuite) TestGetStatusLowercase() { - // the dereference we're gonna use - derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) - signedRequest := derefRequests["foss_satan_dereference_local_account_1_status_1_lowercase"] - targetAccount := suite.testAccounts["local_account_1"] - targetStatus := suite.testStatuses["local_account_1_status_1"] - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, strings.ToLower(targetStatus.URI), nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/activity+json") - ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) - ctx.Request.Header.Set("Date", signedRequest.DateHeader) - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: strings.ToLower(targetAccount.Username), - }, - gin.Param{ - Key: user.StatusIDKey, - Value: strings.ToLower(targetStatus.ID), - }, - } - - // trigger the function being tested - suite.userModule.StatusGETHandler(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - - // should be a Note - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - suite.NoError(err) - - t, err := streams.ToType(context.Background(), m) - suite.NoError(err) - - note, ok := t.(vocab.ActivityStreamsNote) - suite.True(ok) - - // convert note to status - a, err := suite.tc.ASStatusToStatus(context.Background(), note) - suite.NoError(err) - suite.EqualValues(targetStatus.Content, a.Content) -} - -func TestStatusGetTestSuite(t *testing.T) { - suite.Run(t, new(StatusGetTestSuite)) -} diff --git a/internal/api/s2s/user/user.go b/internal/api/s2s/user/user.go deleted file mode 100644 index 1daa53ad8..000000000 --- a/internal/api/s2s/user/user.go +++ /dev/null @@ -1,89 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user - -import ( - "net/http" - - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" - "github.com/superseriousbusiness/gotosocial/internal/uris" -) - -const ( - // UsernameKey is for account usernames. - UsernameKey = "username" - // StatusIDKey is for status IDs - StatusIDKey = "status" - // OnlyOtherAccountsKey is for filtering status responses. - OnlyOtherAccountsKey = "only_other_accounts" - // MinIDKey is for filtering status responses. - MinIDKey = "min_id" - // MaxIDKey is for filtering status responses. - MaxIDKey = "max_id" - // PageKey is for filtering status responses. - PageKey = "page" - - // UsersBasePath is the base path for serving information about Users eg https://example.org/users - UsersBasePath = "/" + uris.UsersPath - // UsersBasePathWithUsername is just the users base path with the Username key in it. - // Use this anywhere you need to know the username of the user being queried. - // Eg https://example.org/users/:username - UsersBasePathWithUsername = UsersBasePath + "/:" + UsernameKey - // UsersPublicKeyPath is a path to a user's public key, for serving bare minimum AP representations. - UsersPublicKeyPath = UsersBasePathWithUsername + "/" + uris.PublicKeyPath - // UsersInboxPath is for serving POST requests to a user's inbox with the given username key. - UsersInboxPath = UsersBasePathWithUsername + "/" + uris.InboxPath - // UsersOutboxPath is for serving GET requests to a user's outbox with the given username key. - UsersOutboxPath = UsersBasePathWithUsername + "/" + uris.OutboxPath - // UsersFollowersPath is for serving GET request's to a user's followers list, with the given username key. - UsersFollowersPath = UsersBasePathWithUsername + "/" + uris.FollowersPath - // UsersFollowingPath is for serving GET request's to a user's following list, with the given username key. - UsersFollowingPath = UsersBasePathWithUsername + "/" + uris.FollowingPath - // UsersStatusPath is for serving GET requests to a particular status by a user, with the given username key and status ID - UsersStatusPath = UsersBasePathWithUsername + "/" + uris.StatusesPath + "/:" + StatusIDKey - // UsersStatusRepliesPath is for serving the replies collection of a status. - UsersStatusRepliesPath = UsersStatusPath + "/replies" -) - -// Module implements the FederationAPIModule interface -type Module struct { - processor processing.Processor -} - -// New returns a new auth module -func New(processor processing.Processor) api.FederationModule { - return &Module{ - processor: processor, - } -} - -// Route satisfies the RESTAPIModule interface -func (m *Module) Route(s router.Router) error { - s.AttachHandler(http.MethodGet, UsersBasePathWithUsername, m.UsersGETHandler) - s.AttachHandler(http.MethodPost, UsersInboxPath, m.InboxPOSTHandler) - s.AttachHandler(http.MethodGet, UsersFollowersPath, m.FollowersGETHandler) - s.AttachHandler(http.MethodGet, UsersFollowingPath, m.FollowingGETHandler) - s.AttachHandler(http.MethodGet, UsersStatusPath, m.StatusGETHandler) - s.AttachHandler(http.MethodGet, UsersPublicKeyPath, m.PublicKeyGETHandler) - s.AttachHandler(http.MethodGet, UsersStatusRepliesPath, m.StatusRepliesGETHandler) - s.AttachHandler(http.MethodGet, UsersOutboxPath, m.OutboxGETHandler) - return nil -} diff --git a/internal/api/s2s/user/user_test.go b/internal/api/s2s/user/user_test.go deleted file mode 100644 index 444e9cab5..000000000 --- a/internal/api/s2s/user/user_test.go +++ /dev/null @@ -1,103 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user_test - -import ( - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" - "github.com/superseriousbusiness/gotosocial/internal/api/security" - "github.com/superseriousbusiness/gotosocial/internal/concurrency" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/email" - "github.com/superseriousbusiness/gotosocial/internal/federation" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/media" - "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/storage" - "github.com/superseriousbusiness/gotosocial/internal/typeutils" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type UserStandardTestSuite struct { - // standard suite interfaces - suite.Suite - db db.DB - tc typeutils.TypeConverter - mediaManager media.Manager - federator federation.Federator - emailSender email.Sender - processor processing.Processor - storage *storage.Driver - oauthServer oauth.Server - securityModule *security.Module - - // standard suite models - testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client - testApplications map[string]*gtsmodel.Application - testUsers map[string]*gtsmodel.User - testAccounts map[string]*gtsmodel.Account - testAttachments map[string]*gtsmodel.MediaAttachment - testStatuses map[string]*gtsmodel.Status - testBlocks map[string]*gtsmodel.Block - - // module being tested - userModule *user.Module -} - -func (suite *UserStandardTestSuite) SetupSuite() { - suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() - suite.testApplications = testrig.NewTestApplications() - suite.testUsers = testrig.NewTestUsers() - suite.testAccounts = testrig.NewTestAccounts() - suite.testAttachments = testrig.NewTestAttachments() - suite.testStatuses = testrig.NewTestStatuses() - suite.testBlocks = testrig.NewTestBlocks() -} - -func (suite *UserStandardTestSuite) SetupTest() { - testrig.InitTestConfig() - testrig.InitTestLog() - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - - suite.db = testrig.NewTestDB() - suite.tc = testrig.NewTestTypeConverter(suite.db) - suite.storage = testrig.NewInMemoryStorage() - suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage) - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) - suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) - suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.userModule = user.New(suite.processor).(*user.Module) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) - suite.securityModule = security.New(suite.db, suite.oauthServer).(*security.Module) - testrig.StandardDBSetup(suite.db, suite.testAccounts) - testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") - - suite.NoError(suite.processor.Start()) -} - -func (suite *UserStandardTestSuite) TearDownTest() { - testrig.StandardDBTeardown(suite.db) - testrig.StandardStorageTeardown(suite.storage) -} diff --git a/internal/api/s2s/user/userget.go b/internal/api/s2s/user/userget.go deleted file mode 100644 index 508c8be7d..000000000 --- a/internal/api/s2s/user/userget.go +++ /dev/null @@ -1,75 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user - -import ( - "encoding/json" - "errors" - "net/http" - "strings" - - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/gtserror" -) - -// 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) { - // usernames on our instance are always lowercase - requestedUsername := strings.ToLower(c.Param(UsernameKey)) - if requestedUsername == "" { - err := errors.New("no username specified in request") - api.ErrorHandler(c, gtserror.NewErrorBadRequest(err, err.Error()), m.processor.InstanceGet) - return - } - - format, err := api.NegotiateAccept(c, api.HTMLOrActivityPubHeaders...) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorNotAcceptable(err, err.Error()), m.processor.InstanceGet) - return - } - - if format == string(api.TextHTML) { - // redirect to the user's profile - c.Redirect(http.StatusSeeOther, "/@"+requestedUsername) - return - } - - resp, errWithCode := m.processor.GetFediUser(transferContext(c), requestedUsername, c.Request.URL) - if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) - return - } - - b, err := json.Marshal(resp) - if err != nil { - api.ErrorHandler(c, gtserror.NewErrorInternalError(err), m.processor.InstanceGet) - return - } - - c.Data(http.StatusOK, format, b) -} diff --git a/internal/api/s2s/user/userget_test.go b/internal/api/s2s/user/userget_test.go deleted file mode 100644 index c656911d7..000000000 --- a/internal/api/s2s/user/userget_test.go +++ /dev/null @@ -1,177 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 user_test - -import ( - "context" - "encoding/json" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/gin-gonic/gin" - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/activity/streams" - "github.com/superseriousbusiness/activity/streams/vocab" - apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/user" - "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type UserGetTestSuite struct { - UserStandardTestSuite -} - -func (suite *UserGetTestSuite) TestGetUser() { - // the dereference we're gonna use - derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) - signedRequest := derefRequests["foss_satan_dereference_zork"] - targetAccount := suite.testAccounts["local_account_1"] - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.URI, nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/activity+json") - ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) - ctx.Request.Header.Set("Date", signedRequest.DateHeader) - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: targetAccount.Username, - }, - } - - // trigger the function being tested - suite.userModule.UsersGETHandler(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - - // should be a Person - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - suite.NoError(err) - - t, err := streams.ToType(context.Background(), m) - suite.NoError(err) - - person, ok := t.(vocab.ActivityStreamsPerson) - suite.True(ok) - - // convert person to account - // since this account is already known, we should get a pretty full model of it from the conversion - a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, "", false) - suite.NoError(err) - suite.EqualValues(targetAccount.Username, a.Username) -} - -// TestGetUserPublicKeyDeleted checks whether the public key of a deleted account can still be dereferenced. -// This is needed by remote instances for authenticating delete requests and stuff like that. -func (suite *UserGetTestSuite) TestGetUserPublicKeyDeleted() { - userModule := user.New(suite.processor).(*user.Module) - targetAccount := suite.testAccounts["local_account_1"] - - // first delete the account, as though zork had deleted himself - authed := &oauth.Auth{ - Application: suite.testApplications["local_account_1"], - User: suite.testUsers["local_account_1"], - Account: suite.testAccounts["local_account_1"], - } - suite.processor.AccountDeleteLocal(context.Background(), authed, &apimodel.AccountDeleteRequest{ - Password: "password", - DeleteOriginID: targetAccount.ID, - }) - - // wait for the account delete to be processed - if !testrig.WaitFor(func() bool { - a, _ := suite.db.GetAccountByID(context.Background(), targetAccount.ID) - return !a.SuspendedAt.IsZero() - }) { - suite.FailNow("delete of account timed out") - } - - // the dereference we're gonna use - derefRequests := testrig.NewTestDereferenceRequests(suite.testAccounts) - signedRequest := derefRequests["foss_satan_dereference_zork_public_key"] - - // setup request - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, targetAccount.PublicKeyURI, nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/activity+json") - ctx.Request.Header.Set("Signature", signedRequest.SignatureHeader) - ctx.Request.Header.Set("Date", signedRequest.DateHeader) - - // we need to pass the context through signature check first to set appropriate values on it - suite.securityModule.SignatureCheck(ctx) - - // normally the router would populate these params from the path values, - // but because we're calling the function directly, we need to set them manually. - ctx.Params = gin.Params{ - gin.Param{ - Key: user.UsernameKey, - Value: targetAccount.Username, - }, - } - - // trigger the function being tested - userModule.UsersGETHandler(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - suite.NoError(err) - - // should be a Person - m := make(map[string]interface{}) - err = json.Unmarshal(b, &m) - suite.NoError(err) - - t, err := streams.ToType(context.Background(), m) - suite.NoError(err) - - person, ok := t.(vocab.ActivityStreamsPerson) - suite.True(ok) - - // convert person to account - a, err := suite.tc.ASRepresentationToAccount(context.Background(), person, "", false) - suite.NoError(err) - suite.EqualValues(targetAccount.Username, a.Username) -} - -func TestUserGetTestSuite(t *testing.T) { - suite.Run(t, new(UserGetTestSuite)) -} diff --git a/internal/api/s2s/webfinger/webfinger.go b/internal/api/s2s/webfinger/webfinger.go deleted file mode 100644 index c46ca7260..000000000 --- a/internal/api/s2s/webfinger/webfinger.go +++ /dev/null @@ -1,50 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 webfinger - -import ( - "net/http" - - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/router" -) - -const ( - // WebfingerBasePath is the base path for serving webfinger lookup requests - WebfingerBasePath = ".well-known/webfinger" -) - -// Module implements the FederationModule interface -type Module struct { - processor processing.Processor -} - -// New returns a new webfinger module -func New(processor processing.Processor) api.FederationModule { - return &Module{ - processor: processor, - } -} - -// Route satisfies the FederationModule interface -func (m *Module) Route(s router.Router) error { - s.AttachHandler(http.MethodGet, WebfingerBasePath, m.WebfingerGETRequest) - return nil -} diff --git a/internal/api/s2s/webfinger/webfinger_test.go b/internal/api/s2s/webfinger/webfinger_test.go deleted file mode 100644 index e5d026d06..000000000 --- a/internal/api/s2s/webfinger/webfinger_test.go +++ /dev/null @@ -1,137 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 webfinger_test - -import ( - "crypto/rand" - "crypto/rsa" - "time" - - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger" - "github.com/superseriousbusiness/gotosocial/internal/api/security" - "github.com/superseriousbusiness/gotosocial/internal/concurrency" - "github.com/superseriousbusiness/gotosocial/internal/db" - "github.com/superseriousbusiness/gotosocial/internal/email" - "github.com/superseriousbusiness/gotosocial/internal/federation" - "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" - "github.com/superseriousbusiness/gotosocial/internal/media" - "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/internal/oauth" - "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/internal/storage" - "github.com/superseriousbusiness/gotosocial/internal/typeutils" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type WebfingerStandardTestSuite struct { - // standard suite interfaces - suite.Suite - db db.DB - tc typeutils.TypeConverter - mediaManager media.Manager - federator federation.Federator - emailSender email.Sender - processor processing.Processor - storage *storage.Driver - oauthServer oauth.Server - securityModule *security.Module - - // standard suite models - testTokens map[string]*gtsmodel.Token - testClients map[string]*gtsmodel.Client - testApplications map[string]*gtsmodel.Application - testUsers map[string]*gtsmodel.User - testAccounts map[string]*gtsmodel.Account - testAttachments map[string]*gtsmodel.MediaAttachment - testStatuses map[string]*gtsmodel.Status - - // module being tested - webfingerModule *webfinger.Module -} - -func (suite *WebfingerStandardTestSuite) SetupSuite() { - suite.testTokens = testrig.NewTestTokens() - suite.testClients = testrig.NewTestClients() - suite.testApplications = testrig.NewTestApplications() - suite.testUsers = testrig.NewTestUsers() - suite.testAccounts = testrig.NewTestAccounts() - suite.testAttachments = testrig.NewTestAttachments() - suite.testStatuses = testrig.NewTestStatuses() -} - -func (suite *WebfingerStandardTestSuite) SetupTest() { - testrig.InitTestLog() - testrig.InitTestConfig() - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - - suite.db = testrig.NewTestDB() - suite.tc = testrig.NewTestTypeConverter(suite.db) - suite.storage = testrig.NewInMemoryStorage() - suite.mediaManager = testrig.NewTestMediaManager(suite.db, suite.storage) - suite.federator = testrig.NewTestFederator(suite.db, testrig.NewTestTransportController(testrig.NewMockHTTPClient(nil, "../../../../testrig/media"), suite.db, fedWorker), suite.storage, suite.mediaManager, fedWorker) - suite.emailSender = testrig.NewEmailSender("../../../../web/template/", nil) - suite.processor = testrig.NewTestProcessor(suite.db, suite.storage, suite.federator, suite.emailSender, suite.mediaManager, clientWorker, fedWorker) - suite.webfingerModule = webfinger.New(suite.processor).(*webfinger.Module) - suite.oauthServer = testrig.NewTestOauthServer(suite.db) - suite.securityModule = security.New(suite.db, suite.oauthServer).(*security.Module) - testrig.StandardDBSetup(suite.db, suite.testAccounts) - testrig.StandardStorageSetup(suite.storage, "../../../../testrig/media") - - suite.NoError(suite.processor.Start()) -} - -func (suite *WebfingerStandardTestSuite) TearDownTest() { - testrig.StandardDBTeardown(suite.db) - testrig.StandardStorageTeardown(suite.storage) -} - -func accountDomainAccount() *gtsmodel.Account { - privateKey, err := rsa.GenerateKey(rand.Reader, 2048) - if err != nil { - panic(err) - } - publicKey := &privateKey.PublicKey - - acct := >smodel.Account{ - ID: "01FG1K8EA7SYHEC7V6XKVNC4ZA", - CreatedAt: time.Now(), - UpdatedAt: time.Now(), - Username: "aaaaa", - Domain: "", - Privacy: gtsmodel.VisibilityDefault, - Language: "en", - URI: "http://gts.example.org/users/aaaaa", - URL: "http://gts.example.org/@aaaaa", - InboxURI: "http://gts.example.org/users/aaaaa/inbox", - OutboxURI: "http://gts.example.org/users/aaaaa/outbox", - FollowingURI: "http://gts.example.org/users/aaaaa/following", - FollowersURI: "http://gts.example.org/users/aaaaa/followers", - FeaturedCollectionURI: "http://gts.example.org/users/aaaaa/collections/featured", - ActorType: ap.ActorPerson, - PrivateKey: privateKey, - PublicKey: publicKey, - PublicKeyURI: "http://gts.example.org/users/aaaaa/main-key", - } - - return acct -} diff --git a/internal/api/s2s/webfinger/webfingerget.go b/internal/api/s2s/webfinger/webfingerget.go deleted file mode 100644 index 9949140c1..000000000 --- a/internal/api/s2s/webfinger/webfingerget.go +++ /dev/null @@ -1,102 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 webfinger - -import ( - "context" - "fmt" - "net/http" - - "codeberg.org/gruf/go-kv" - "github.com/gin-gonic/gin" - "github.com/superseriousbusiness/gotosocial/internal/ap" - "github.com/superseriousbusiness/gotosocial/internal/api" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/log" - "github.com/superseriousbusiness/gotosocial/internal/util" -) - -// WebfingerGETRequest swagger:operation GET /.well-known/webfinger webfingerGet -// -// Handles webfinger account lookup requests. -// -// For example, a GET to `https://goblin.technology/.well-known/webfinger?resource=acct:tobi@goblin.technology` would return: -// -// ``` -// -// {"subject":"acct:tobi@goblin.technology","aliases":["https://goblin.technology/users/tobi","https://goblin.technology/@tobi"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"https://goblin.technology/@tobi"},{"rel":"self","type":"application/activity+json","href":"https://goblin.technology/users/tobi"}]} -// -// ``` -// -// See: https://webfinger.net/ -// -// --- -// tags: -// - webfinger -// -// produces: -// - application/json -// -// responses: -// '200': -// schema: -// "$ref": "#/definitions/wellKnownResponse" -func (m *Module) WebfingerGETRequest(c *gin.Context) { - l := log.WithFields(kv.Fields{ - {K: "user-agent", V: c.Request.UserAgent()}, - }...) - - resourceQuery, set := c.GetQuery("resource") - if !set || resourceQuery == "" { - l.Debug("aborting request because no resource was set in query") - c.JSON(http.StatusBadRequest, gin.H{"error": "no 'resource' in request query"}) - return - } - - requestedUsername, requestedHost, err := util.ExtractWebfingerParts(resourceQuery) - if err != nil { - l.Debugf("bad webfinger request with resource query %s: %s", resourceQuery, err) - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("bad webfinger request with resource query %s", resourceQuery)}) - return - } - - accountDomain := config.GetAccountDomain() - host := config.GetHost() - - if requestedHost != host && requestedHost != accountDomain { - l.Debugf("aborting request because requestedHost %s does not belong to this instance", requestedHost) - c.JSON(http.StatusBadRequest, gin.H{"error": fmt.Sprintf("requested host %s does not belong to this instance", requestedHost)}) - return - } - - // transfer the signature verifier from the gin context to the request context - ctx := c.Request.Context() - verifier, signed := c.Get(string(ap.ContextRequestingPublicKeyVerifier)) - if signed { - ctx = context.WithValue(ctx, ap.ContextRequestingPublicKeyVerifier, verifier) - } - - resp, errWithCode := m.processor.GetWebfingerAccount(ctx, requestedUsername) - if errWithCode != nil { - api.ErrorHandler(c, errWithCode, m.processor.InstanceGet) - return - } - - c.JSON(http.StatusOK, resp) -} diff --git a/internal/api/s2s/webfinger/webfingerget_test.go b/internal/api/s2s/webfinger/webfingerget_test.go deleted file mode 100644 index 3e91b8f6a..000000000 --- a/internal/api/s2s/webfinger/webfingerget_test.go +++ /dev/null @@ -1,171 +0,0 @@ -/* - GoToSocial - Copyright (C) 2021-2022 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 webfinger_test - -import ( - "context" - "fmt" - "io/ioutil" - "net/http" - "net/http/httptest" - "testing" - - "github.com/stretchr/testify/assert" - "github.com/stretchr/testify/suite" - "github.com/superseriousbusiness/gotosocial/internal/api/s2s/webfinger" - "github.com/superseriousbusiness/gotosocial/internal/concurrency" - "github.com/superseriousbusiness/gotosocial/internal/config" - "github.com/superseriousbusiness/gotosocial/internal/messages" - "github.com/superseriousbusiness/gotosocial/internal/processing" - "github.com/superseriousbusiness/gotosocial/testrig" -) - -type WebfingerGetTestSuite struct { - WebfingerStandardTestSuite -} - -func (suite *WebfingerGetTestSuite) TestFingerUser() { - targetAccount := suite.testAccounts["local_account_1"] - - // setup request - host := config.GetHost() - requestPath := fmt.Sprintf("/%s?resource=acct:%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, host) - - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, requestPath, nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/json") - - // trigger the function being tested - suite.webfingerModule.WebfingerGETRequest(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) - - suite.Equal(`{"subject":"acct:the_mighty_zork@localhost:8080","aliases":["http://localhost:8080/users/the_mighty_zork","http://localhost:8080/@the_mighty_zork"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"http://localhost:8080/@the_mighty_zork"},{"rel":"self","type":"application/activity+json","href":"http://localhost:8080/users/the_mighty_zork"}]}`, string(b)) -} - -func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByHost() { - config.SetHost("gts.example.org") - config.SetAccountDomain("example.org") - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - suite.processor = processing.NewProcessor(suite.tc, suite.federator, testrig.NewTestOauthServer(suite.db), testrig.NewTestMediaManager(suite.db, suite.storage), suite.storage, suite.db, suite.emailSender, clientWorker, fedWorker) - suite.webfingerModule = webfinger.New(suite.processor).(*webfinger.Module) - - targetAccount := accountDomainAccount() - if err := suite.db.Put(context.Background(), targetAccount); err != nil { - panic(err) - } - - // setup request - host := config.GetHost() - requestPath := fmt.Sprintf("/%s?resource=acct:%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, host) - - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, requestPath, nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/json") - - // trigger the function being tested - suite.webfingerModule.WebfingerGETRequest(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) - - suite.Equal(`{"subject":"acct:aaaaa@example.org","aliases":["http://gts.example.org/users/aaaaa","http://gts.example.org/@aaaaa"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"http://gts.example.org/@aaaaa"},{"rel":"self","type":"application/activity+json","href":"http://gts.example.org/users/aaaaa"}]}`, string(b)) -} - -func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByAccountDomain() { - config.SetHost("gts.example.org") - config.SetAccountDomain("example.org") - - clientWorker := concurrency.NewWorkerPool[messages.FromClientAPI](-1, -1) - fedWorker := concurrency.NewWorkerPool[messages.FromFederator](-1, -1) - suite.processor = processing.NewProcessor(suite.tc, suite.federator, testrig.NewTestOauthServer(suite.db), testrig.NewTestMediaManager(suite.db, suite.storage), suite.storage, suite.db, suite.emailSender, clientWorker, fedWorker) - suite.webfingerModule = webfinger.New(suite.processor).(*webfinger.Module) - - targetAccount := accountDomainAccount() - if err := suite.db.Put(context.Background(), targetAccount); err != nil { - panic(err) - } - - // setup request - accountDomain := config.GetAccountDomain() - requestPath := fmt.Sprintf("/%s?resource=acct:%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, accountDomain) - - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, requestPath, nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/json") - - // trigger the function being tested - suite.webfingerModule.WebfingerGETRequest(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) - - suite.Equal(`{"subject":"acct:aaaaa@example.org","aliases":["http://gts.example.org/users/aaaaa","http://gts.example.org/@aaaaa"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"http://gts.example.org/@aaaaa"},{"rel":"self","type":"application/activity+json","href":"http://gts.example.org/users/aaaaa"}]}`, string(b)) -} - -func (suite *WebfingerGetTestSuite) TestFingerUserWithoutAcct() { - targetAccount := suite.testAccounts["local_account_1"] - - // setup request -- leave out the 'acct:' prefix, which is prettymuch what pixelfed currently does - host := config.GetHost() - requestPath := fmt.Sprintf("/%s?resource=%s@%s", webfinger.WebfingerBasePath, targetAccount.Username, host) - - recorder := httptest.NewRecorder() - ctx, _ := testrig.CreateGinTestContext(recorder, nil) - ctx.Request = httptest.NewRequest(http.MethodGet, requestPath, nil) // the endpoint we're hitting - ctx.Request.Header.Set("accept", "application/json") - - // trigger the function being tested - suite.webfingerModule.WebfingerGETRequest(ctx) - - // check response - suite.EqualValues(http.StatusOK, recorder.Code) - - result := recorder.Result() - defer result.Body.Close() - b, err := ioutil.ReadAll(result.Body) - assert.NoError(suite.T(), err) - - suite.Equal(`{"subject":"acct:the_mighty_zork@localhost:8080","aliases":["http://localhost:8080/users/the_mighty_zork","http://localhost:8080/@the_mighty_zork"],"links":[{"rel":"http://webfinger.net/rel/profile-page","type":"text/html","href":"http://localhost:8080/@the_mighty_zork"},{"rel":"self","type":"application/activity+json","href":"http://localhost:8080/users/the_mighty_zork"}]}`, string(b)) -} - -func TestWebfingerGetTestSuite(t *testing.T) { - suite.Run(t, new(WebfingerGetTestSuite)) -} |