From e2daf0f012a21928ceeba03e5754b5a2233f4016 Mon Sep 17 00:00:00 2001 From: tobi <31960611+tsmethurst@users.noreply.github.com> Date: Sat, 11 Dec 2021 17:50:00 +0100 Subject: [PATCH] Add `Accept` header negotiation to relevant API endpoints (#337) * start centralizing negotiation logic for API * swagger document nodeinfo endpoint * go fmt * document negotiate function * use content negotiation * tidy up negotiation logic * negotiate content throughout client api * swagger * remove attachment on Content * add accept header to test requests --- docs/api/swagger.yaml | 169 ++++++++++++++++++ internal/api/client/account/account_test.go | 2 + internal/api/client/account/accountcreate.go | 6 + internal/api/client/account/accountget.go | 6 + internal/api/client/account/accountupdate.go | 9 +- internal/api/client/account/accountverify.go | 9 +- internal/api/client/account/block.go | 6 + internal/api/client/account/follow.go | 6 + internal/api/client/account/followers.go | 6 + internal/api/client/account/following.go | 6 + internal/api/client/account/relationships.go | 9 +- internal/api/client/account/statuses.go | 9 +- internal/api/client/account/unblock.go | 6 + internal/api/client/account/unfollow.go | 9 +- .../api/client/admin/domainblockcreate.go | 6 + .../api/client/admin/domainblockdelete.go | 6 + internal/api/client/admin/domainblockget.go | 6 + internal/api/client/admin/domainblocksget.go | 6 + internal/api/client/admin/emojicreate.go | 6 + internal/api/client/app/appcreate.go | 6 + internal/api/client/auth/authorize.go | 9 +- internal/api/client/auth/signin.go | 10 +- internal/api/client/auth/token.go | 9 +- internal/api/client/blocks/blocksget.go | 9 +- internal/api/client/emoji/emojisget.go | 6 + .../api/client/favourites/favouritesget.go | 9 +- internal/api/client/fileserver/servefile.go | 11 +- .../api/client/fileserver/servefile_test.go | 1 + internal/api/client/filter/filtersget.go | 6 + .../api/client/followrequest/authorize.go | 9 +- .../followrequest/followrequest_test.go | 1 + internal/api/client/followrequest/get.go | 9 +- internal/api/client/followrequest/reject.go | 6 + internal/api/client/instance/instanceget.go | 6 + internal/api/client/instance/instancepatch.go | 9 +- internal/api/client/list/listsgets.go | 6 + internal/api/client/media/mediacreate.go | 6 + internal/api/client/media/mediacreate_test.go | 1 + internal/api/client/media/mediaget.go | 9 +- internal/api/client/media/mediaupdate.go | 6 + .../client/notification/notificationsget.go | 6 + internal/api/client/search/searchget.go | 6 + internal/api/client/status/statusboost.go | 8 +- .../api/client/status/statusboost_test.go | 3 + internal/api/client/status/statuscontext.go | 6 + internal/api/client/status/statuscreate.go | 8 +- .../api/client/status/statuscreate_test.go | 6 + internal/api/client/status/statusdelete.go | 8 +- internal/api/client/status/statusfave.go | 8 +- internal/api/client/status/statusfave_test.go | 2 + internal/api/client/status/statusfavedby.go | 8 +- .../api/client/status/statusfavedby_test.go | 1 + internal/api/client/status/statusget.go | 8 +- internal/api/client/status/statusunboost.go | 8 +- internal/api/client/status/statusunfave.go | 8 +- .../api/client/status/statusunfave_test.go | 2 + internal/api/client/timeline/home.go | 9 +- internal/api/client/timeline/public.go | 9 +- internal/api/client/user/passwordchange.go | 6 + .../api/client/user/passwordchange_test.go | 4 + internal/api/model/well-known.go | 10 +- internal/api/negotiate.go | 96 ++++++++++ internal/api/s2s/nodeinfo/nodeinfoget.go | 33 +++- internal/api/s2s/nodeinfo/wellknownget.go | 29 ++- internal/api/s2s/user/common.go | 16 -- internal/api/s2s/user/followers.go | 5 +- internal/api/s2s/user/following.go | 5 +- internal/api/s2s/user/outboxget.go | 5 +- internal/api/s2s/user/outboxget_test.go | 3 + internal/api/s2s/user/publickeyget.go | 5 +- internal/api/s2s/user/repliesget.go | 5 +- internal/api/s2s/user/repliesget_test.go | 3 + internal/api/s2s/user/statusget.go | 5 +- internal/api/s2s/user/userget.go | 5 +- internal/api/s2s/user/userget_test.go | 1 + internal/api/s2s/webfinger/webfingerget.go | 36 +++- .../api/s2s/webfinger/webfingerget_test.go | 4 + mkdocs.yml | 2 +- 78 files changed, 752 insertions(+), 72 deletions(-) create mode 100644 internal/api/negotiate.go diff --git a/docs/api/swagger.yaml b/docs/api/swagger.yaml index 8f1623cc9..f8d1114e7 100644 --- a/docs/api/swagger.yaml +++ b/docs/api/swagger.yaml @@ -12,6 +12,24 @@ definitions: title: A FileHeader describes a file part of a multipart request. type: object x-go-package: mime/multipart + Link: + description: See https://webfinger.net/ + properties: + href: + type: string + x-go-name: Href + rel: + type: string + x-go-name: Rel + template: + type: string + x-go-name: Template + type: + type: string + x-go-name: Type + title: Link represents one 'link' in a slice of links returned from a lookup request. + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model MIMEHeader: additionalProperties: items: @@ -49,6 +67,48 @@ definitions: title: Mention represents a mention of another account. type: object x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + NodeInfoServices: + properties: + inbound: + items: + type: string + type: array + x-go-name: Inbound + outbound: + items: + type: string + type: array + x-go-name: Outbound + title: NodeInfoServices represents inbound and outbound services that this node + offers connections to. + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + NodeInfoSoftware: + properties: + name: + example: gotosocial + type: string + x-go-name: Name + version: + example: 0.1.2 1234567 + type: string + x-go-name: Version + title: NodeInfoSoftware represents the name and version number of the software + of this node. + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + NodeInfoUsage: + properties: + users: + $ref: '#/definitions/NodeInfoUsers' + title: NodeInfoUsage represents usage information about this server, such as number + of users. + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + NodeInfoUsers: + title: NodeInfoUsers is a stub for usage information, currently empty. + type: object + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model Source: description: Returned as an additional entity when verifying and updated credentials, as an attribute of Account. @@ -1122,6 +1182,42 @@ definitions: type: object x-go-name: MediaMeta x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + nodeinfo: + description: 'See: https://nodeinfo.diaspora.software/schema.html' + properties: + metadata: + additionalProperties: + type: object + description: Free form key value pairs for software specific values. Clients + should not rely on any specific key present. + type: object + x-go-name: Metadata + openRegistrations: + description: Whether this server allows open self-registration. + example: false + type: boolean + x-go-name: OpenRegistrations + protocols: + description: The protocols supported on this server. + items: + type: string + type: array + x-go-name: Protocols + services: + $ref: '#/definitions/NodeInfoServices' + software: + $ref: '#/definitions/NodeInfoSoftware' + usage: + $ref: '#/definitions/NodeInfoUsage' + version: + description: The schema version + example: "2.0" + type: string + x-go-name: Version + title: Nodeinfo represents a version 2.1 or version 2.0 nodeinfo schema. + type: object + x-go-name: Nodeinfo + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model oauthToken: properties: access_token: @@ -1670,6 +1766,28 @@ definitions: type: object x-go-name: UpdateSource x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model + wellKnownResponse: + description: See https://webfinger.net/ + properties: + aliases: + items: + type: string + type: array + x-go-name: Aliases + links: + items: + $ref: '#/definitions/Link' + type: array + x-go-name: Links + subject: + type: string + x-go-name: Subject + title: |- + WellKnownResponse represents the response to either a webfinger request for an 'acct' resource, or a request to nodeinfo. + For example, it would be returned from https://example.org/.well-known/webfinger?resource=acct:some_username@example.org + type: object + x-go-name: WellKnownResponse + x-go-package: github.com/superseriousbusiness/gotosocial/internal/api/model host: example.org info: contact: @@ -1682,6 +1800,43 @@ info: title: GoToSocial version: 0.0.1 paths: + /.well-known/nodeinfo: + get: + description: |- + 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 + operationId: nodeInfoWellKnownGet + produces: + - application/json + responses: + "200": + description: "" + schema: + $ref: '#/definitions/wellKnownResponse' + summary: Directs callers to /nodeinfo/2.0. + tags: + - nodeinfo + /.well-known/webfinger: + get: + description: |- + 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/ + operationId: webfingerGet + produces: + - application/json + responses: + "200": + description: "" + schema: + $ref: '#/definitions/wellKnownResponse' + summary: Handles webfinger account lookup requests. + tags: + - webfinger /api/v1/accounts: post: consumes: @@ -3529,6 +3684,20 @@ paths: summary: Change the password of authenticated user. tags: - user + /nodeinfo/2.0: + get: + description: 'See: https://nodeinfo.diaspora.software/schema.html' + operationId: nodeInfoGet + produces: + - application/json; profile="http://nodeinfo.diaspora.software/ns/schema/2.0#" + responses: + "200": + description: "" + schema: + $ref: '#/definitions/nodeinfo' + summary: Returns a compliant nodeinfo response to node info queries. + tags: + - nodeinfo /users/{username}/outbox: get: description: |- diff --git a/internal/api/client/account/account_test.go b/internal/api/client/account/account_test.go index f7dfa4520..b642dbcb4 100644 --- a/internal/api/client/account/account_test.go +++ b/internal/api/client/account/account_test.go @@ -95,5 +95,7 @@ func (suite *AccountStandardTestSuite) newContext(recorder *httptest.ResponseRec ctx.Request.Header.Set("Content-Type", bodyContentType) } + ctx.Request.Header.Set("accept", "application/json") + return ctx } diff --git a/internal/api/client/account/accountcreate.go b/internal/api/client/account/accountcreate.go index ae9d7b0d7..7219fae94 100644 --- a/internal/api/client/account/accountcreate.go +++ b/internal/api/client/account/accountcreate.go @@ -27,6 +27,7 @@ "github.com/spf13/viper" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -78,6 +79,11 @@ func (m *Module) AccountCreatePOSTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + l.Trace("parsing request form") form := &model.AccountCreateRequest{} if err := c.ShouldBind(form); err != nil || form == nil { diff --git a/internal/api/client/account/accountget.go b/internal/api/client/account/accountget.go index 8bac1360b..2bda3c0bd 100644 --- a/internal/api/client/account/accountget.go +++ b/internal/api/client/account/accountget.go @@ -22,6 +22,7 @@ "net/http" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -64,6 +65,11 @@ func (m *Module) AccountGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetAcctID := c.Param(IDKey) if targetAcctID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) diff --git a/internal/api/client/account/accountupdate.go b/internal/api/client/account/accountupdate.go index 8534f5805..2a3882206 100644 --- a/internal/api/client/account/accountupdate.go +++ b/internal/api/client/account/accountupdate.go @@ -20,11 +20,13 @@ import ( "fmt" - "github.com/sirupsen/logrus" "net/http" "strconv" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -110,6 +112,11 @@ func (m *Module) AccountUpdateCredentialsPATCHHandler(c *gin.Context) { } l.Tracef("retrieved account %+v", authed.Account.ID) + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + form, err := parseUpdateAccountForm(c) if err != nil { c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()}) diff --git a/internal/api/client/account/accountverify.go b/internal/api/client/account/accountverify.go index 6b8d1f92c..1da0251a3 100644 --- a/internal/api/client/account/accountverify.go +++ b/internal/api/client/account/accountverify.go @@ -19,10 +19,12 @@ package account import ( - "github.com/sirupsen/logrus" "net/http" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -60,6 +62,11 @@ func (m *Module) AccountVerifyGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + acctSensitive, err := m.processor.AccountGet(c.Request.Context(), authed, authed.Account.ID) if err != nil { l.Debugf("error getting account from processor: %s", err) diff --git a/internal/api/client/account/block.go b/internal/api/client/account/block.go index 243f90c5e..2de754a33 100644 --- a/internal/api/client/account/block.go +++ b/internal/api/client/account/block.go @@ -22,6 +22,7 @@ "net/http" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -66,6 +67,11 @@ func (m *Module) AccountBlockPOSTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetAcctID := c.Param(IDKey) if targetAcctID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) diff --git a/internal/api/client/account/follow.go b/internal/api/client/account/follow.go index 8f3f7ad0a..afdb6d23d 100644 --- a/internal/api/client/account/follow.go +++ b/internal/api/client/account/follow.go @@ -22,6 +22,7 @@ "net/http" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -87,6 +88,11 @@ func (m *Module) AccountFollowPOSTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetAcctID := c.Param(IDKey) if targetAcctID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) diff --git a/internal/api/client/account/followers.go b/internal/api/client/account/followers.go index 4f30e9939..3ccb3d393 100644 --- a/internal/api/client/account/followers.go +++ b/internal/api/client/account/followers.go @@ -22,6 +22,7 @@ "net/http" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -68,6 +69,11 @@ func (m *Module) AccountFollowersGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetAcctID := c.Param(IDKey) if targetAcctID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) diff --git a/internal/api/client/account/following.go b/internal/api/client/account/following.go index baac2c9d3..78ab610c8 100644 --- a/internal/api/client/account/following.go +++ b/internal/api/client/account/following.go @@ -22,6 +22,7 @@ "net/http" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -68,6 +69,11 @@ func (m *Module) AccountFollowingGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetAcctID := c.Param(IDKey) if targetAcctID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) diff --git a/internal/api/client/account/relationships.go b/internal/api/client/account/relationships.go index bb942d18a..22ae835f0 100644 --- a/internal/api/client/account/relationships.go +++ b/internal/api/client/account/relationships.go @@ -1,10 +1,12 @@ package account import ( - "github.com/sirupsen/logrus" "net/http" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -57,6 +59,11 @@ func (m *Module) AccountRelationshipsGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetAccountIDs := c.QueryArray("id[]") if len(targetAccountIDs) == 0 { // check fallback -- let's be generous and see if maybe it's just set as 'id'? diff --git a/internal/api/client/account/statuses.go b/internal/api/client/account/statuses.go index 9a60e80ee..49c15987e 100644 --- a/internal/api/client/account/statuses.go +++ b/internal/api/client/account/statuses.go @@ -19,11 +19,13 @@ package account import ( - "github.com/sirupsen/logrus" "net/http" "strconv" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -118,6 +120,11 @@ func (m *Module) AccountStatusesGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetAcctID := c.Param(IDKey) if targetAcctID == "" { l.Debug("no account id specified in query") diff --git a/internal/api/client/account/unblock.go b/internal/api/client/account/unblock.go index 7b16ac887..f075d14ed 100644 --- a/internal/api/client/account/unblock.go +++ b/internal/api/client/account/unblock.go @@ -22,6 +22,7 @@ "net/http" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -66,6 +67,11 @@ func (m *Module) AccountUnblockPOSTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetAcctID := c.Param(IDKey) if targetAcctID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no account id specified"}) diff --git a/internal/api/client/account/unfollow.go b/internal/api/client/account/unfollow.go index b18903c8a..2ee812f5a 100644 --- a/internal/api/client/account/unfollow.go +++ b/internal/api/client/account/unfollow.go @@ -19,10 +19,12 @@ package account import ( - "github.com/sirupsen/logrus" "net/http" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -69,6 +71,11 @@ func (m *Module) AccountUnfollowPOSTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetAcctID := c.Param(IDKey) if targetAcctID == "" { l.Debug(err) diff --git a/internal/api/client/admin/domainblockcreate.go b/internal/api/client/admin/domainblockcreate.go index e1b6bb032..dd5623a1c 100644 --- a/internal/api/client/admin/domainblockcreate.go +++ b/internal/api/client/admin/domainblockcreate.go @@ -8,6 +8,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -110,6 +111,11 @@ func (m *Module) DomainBlocksPOSTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + imp := false importString := c.Query(ImportQueryKey) if importString != "" { diff --git a/internal/api/client/admin/domainblockdelete.go b/internal/api/client/admin/domainblockdelete.go index 9c6f97ca2..8d41ec072 100644 --- a/internal/api/client/admin/domainblockdelete.go +++ b/internal/api/client/admin/domainblockdelete.go @@ -5,6 +5,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -62,6 +63,11 @@ func (m *Module) DomainBlockDELETEHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + domainBlockID := c.Param(IDKey) if domainBlockID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"}) diff --git a/internal/api/client/admin/domainblockget.go b/internal/api/client/admin/domainblockget.go index b9dabcd80..49a0795d7 100644 --- a/internal/api/client/admin/domainblockget.go +++ b/internal/api/client/admin/domainblockget.go @@ -6,6 +6,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -63,6 +64,11 @@ func (m *Module) DomainBlockGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + domainBlockID := c.Param(IDKey) if domainBlockID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no domain block id provided"}) diff --git a/internal/api/client/admin/domainblocksget.go b/internal/api/client/admin/domainblocksget.go index 979fc4a69..94b1c89ce 100644 --- a/internal/api/client/admin/domainblocksget.go +++ b/internal/api/client/admin/domainblocksget.go @@ -6,6 +6,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -69,6 +70,11 @@ func (m *Module) DomainBlocksGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + export := false exportString := c.Query(ExportQueryKey) if exportString != "" { diff --git a/internal/api/client/admin/emojicreate.go b/internal/api/client/admin/emojicreate.go index 89325c47a..044e8b643 100644 --- a/internal/api/client/admin/emojicreate.go +++ b/internal/api/client/admin/emojicreate.go @@ -25,6 +25,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/media" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -94,6 +95,11 @@ func (m *Module) emojiCreatePOSTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + // extract the media create form from the request context l.Tracef("parsing request form: %+v", c.Request.Form) form := &model.EmojiCreateRequest{} diff --git a/internal/api/client/app/appcreate.go b/internal/api/client/app/appcreate.go index c23a838f4..07ff5a889 100644 --- a/internal/api/client/app/appcreate.go +++ b/internal/api/client/app/appcreate.go @@ -25,6 +25,7 @@ "github.com/sirupsen/logrus" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -81,6 +82,11 @@ func (m *Module) AppsPOSTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + form := &model.ApplicationCreateRequest{} if err := c.ShouldBind(form); err != nil { c.JSON(http.StatusUnprocessableEntity, gin.H{"error": err.Error()}) diff --git a/internal/api/client/auth/authorize.go b/internal/api/client/auth/authorize.go index 6382f473d..35a681114 100644 --- a/internal/api/client/auth/authorize.go +++ b/internal/api/client/auth/authorize.go @@ -21,14 +21,16 @@ import ( "errors" "fmt" - "github.com/sirupsen/logrus" "net/http" "net/url" "strings" + "github.com/sirupsen/logrus" + "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" "github.com/google/uuid" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" @@ -41,6 +43,11 @@ func (m *Module) AuthorizeGETHandler(c *gin.Context) { l := logrus.WithField("func", "AuthorizeGETHandler") s := sessions.Default(c) + if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + // UserID will be set in the session by AuthorizePOSTHandler if the caller has already gone through the authentication flow // If it's not set, then we don't know yet who the user is, so we need to redirect them to the sign in page. userID, ok := s.Get(sessionUserID).(string) diff --git a/internal/api/client/auth/signin.go b/internal/api/client/auth/signin.go index 68944226f..cc60a3b52 100644 --- a/internal/api/client/auth/signin.go +++ b/internal/api/client/auth/signin.go @@ -21,11 +21,13 @@ import ( "context" "errors" - "github.com/sirupsen/logrus" "net/http" + "github.com/sirupsen/logrus" + "github.com/gin-contrib/sessions" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "golang.org/x/crypto/bcrypt" @@ -43,6 +45,12 @@ type login struct { func (m *Module) SignInGETHandler(c *gin.Context) { l := logrus.WithField("func", "SignInGETHandler") l.Trace("entering sign in handler") + + if _, err := api.NegotiateAccept(c, api.HTMLAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + if m.idp != nil { s := sessions.Default(c) diff --git a/internal/api/client/auth/token.go b/internal/api/client/auth/token.go index f24840c9f..d3e362f8f 100644 --- a/internal/api/client/auth/token.go +++ b/internal/api/client/auth/token.go @@ -19,10 +19,12 @@ package auth import ( - "github.com/sirupsen/logrus" "net/http" "net/url" + "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" + "github.com/gin-gonic/gin" ) @@ -41,6 +43,11 @@ func (m *Module) TokenPOSTHandler(c *gin.Context) { l := logrus.WithField("func", "TokenPOSTHandler") l.Trace("entered TokenPOSTHandler") + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + form := &tokenBody{} if err := c.ShouldBind(form); err == nil { c.Request.Form = url.Values{} diff --git a/internal/api/client/blocks/blocksget.go b/internal/api/client/blocks/blocksget.go index b00a6e198..d53feabfe 100644 --- a/internal/api/client/blocks/blocksget.go +++ b/internal/api/client/blocks/blocksget.go @@ -19,11 +19,13 @@ package blocks import ( - "github.com/sirupsen/logrus" "net/http" "strconv" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -94,6 +96,11 @@ func (m *Module) BlocksGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + maxID := "" maxIDString := c.Query(MaxIDKey) if maxIDString != "" { diff --git a/internal/api/client/emoji/emojisget.go b/internal/api/client/emoji/emojisget.go index 0feb5d9cc..62223e992 100644 --- a/internal/api/client/emoji/emojisget.go +++ b/internal/api/client/emoji/emojisget.go @@ -4,9 +4,15 @@ "net/http" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" ) // EmojisGETHandler returns a list of custom emojis enabled on the instance func (m *Module) EmojisGETHandler(c *gin.Context) { + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, []string{}) } diff --git a/internal/api/client/favourites/favouritesget.go b/internal/api/client/favourites/favouritesget.go index 22ed03b39..d112e9b95 100644 --- a/internal/api/client/favourites/favouritesget.go +++ b/internal/api/client/favourites/favouritesget.go @@ -1,11 +1,13 @@ package favourites import ( - "github.com/sirupsen/logrus" "net/http" "strconv" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -20,6 +22,11 @@ func (m *Module) FavouritesGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + maxID := "" maxIDString := c.Query(MaxIDKey) if maxIDString != "" { diff --git a/internal/api/client/fileserver/servefile.go b/internal/api/client/fileserver/servefile.go index 0840f1d96..287d8af80 100644 --- a/internal/api/client/fileserver/servefile.go +++ b/internal/api/client/fileserver/servefile.go @@ -24,6 +24,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -90,14 +91,14 @@ func (m *FileServer) ServeFile(c *gin.Context) { return } - // TODO: do proper content negotiation here -- if the requester only accepts text/html we should try to serve them *something* + // TODO: if the requester only accepts text/html we should try to serve them *something*. // This is mostly needed because when sharing a link to a gts-hosted file on something like mastodon, the masto servers will // attempt to look up the content to provide a preview of the link, and they ask for text/html. - if c.NegotiateFormat(content.ContentType) == "" { - l.Debugf("couldn't negotiate content for Accept headers %+v: we have content type %s", c.Request.Header.Get("Accepted"), content.ContentType) - c.AbortWithStatus(http.StatusNotAcceptable) + format, err := api.NegotiateAccept(c, api.Offer(content.ContentType)) + if err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) return } - c.DataFromReader(http.StatusOK, content.ContentLength, content.ContentType, bytes.NewReader(content.Content), nil) + c.DataFromReader(http.StatusOK, content.ContentLength, format, bytes.NewReader(content.Content), nil) } diff --git a/internal/api/client/fileserver/servefile_test.go b/internal/api/client/fileserver/servefile_test.go index b93f97606..cf05ebbf1 100644 --- a/internal/api/client/fileserver/servefile_test.go +++ b/internal/api/client/fileserver/servefile_test.go @@ -123,6 +123,7 @@ func (suite *ServeFileTestSuite) TestServeOriginalFileSuccessful() { recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) ctx.Request = httptest.NewRequest(http.MethodGet, targetAttachment.URL, nil) + ctx.Request.Header.Set("accept", "*/*") // normally the router would populate these params from the path values, // but because we're calling the ServeFile function directly, we need to set them manually. diff --git a/internal/api/client/filter/filtersget.go b/internal/api/client/filter/filtersget.go index 079d39f35..f0367d40c 100644 --- a/internal/api/client/filter/filtersget.go +++ b/internal/api/client/filter/filtersget.go @@ -4,9 +4,15 @@ "net/http" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" ) // FiltersGETHandler returns a list of filters set by/for the authed account func (m *Module) FiltersGETHandler(c *gin.Context) { + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, []string{}) } diff --git a/internal/api/client/followrequest/authorize.go b/internal/api/client/followrequest/authorize.go index 1ab7891a6..1e1a46711 100644 --- a/internal/api/client/followrequest/authorize.go +++ b/internal/api/client/followrequest/authorize.go @@ -19,10 +19,12 @@ package followrequest import ( - "github.com/sirupsen/logrus" "net/http" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -76,6 +78,11 @@ func (m *Module) FollowRequestAuthorizePOSTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { l.Debugf("couldn't auth: %s", err) c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) diff --git a/internal/api/client/followrequest/followrequest_test.go b/internal/api/client/followrequest/followrequest_test.go index e7dccc210..27b8d3db2 100644 --- a/internal/api/client/followrequest/followrequest_test.go +++ b/internal/api/client/followrequest/followrequest_test.go @@ -106,6 +106,7 @@ func (suite *FollowRequestStandardTestSuite) newContext(recorder *httptest.Respo if bodyContentType != "" { ctx.Request.Header.Set("Content-Type", bodyContentType) } + ctx.Request.Header.Set("accept", "application/json") return ctx } diff --git a/internal/api/client/followrequest/get.go b/internal/api/client/followrequest/get.go index de8c83d9a..036e51c8d 100644 --- a/internal/api/client/followrequest/get.go +++ b/internal/api/client/followrequest/get.go @@ -19,10 +19,12 @@ package followrequest import ( - "github.com/sirupsen/logrus" "net/http" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -83,6 +85,11 @@ func (m *Module) FollowRequestGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { l.Debugf("couldn't auth: %s", err) c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) diff --git a/internal/api/client/followrequest/reject.go b/internal/api/client/followrequest/reject.go index a7fa12e7f..28459b9a6 100644 --- a/internal/api/client/followrequest/reject.go +++ b/internal/api/client/followrequest/reject.go @@ -23,6 +23,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -74,6 +75,11 @@ func (m *Module) FollowRequestRejectPOSTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { l.Debugf("couldn't auth: %s", err) c.JSON(http.StatusForbidden, gin.H{"error": "account is disabled, not yet approved, or suspended"}) diff --git a/internal/api/client/instance/instanceget.go b/internal/api/client/instance/instanceget.go index f2f496cb5..4f9afdb38 100644 --- a/internal/api/client/instance/instanceget.go +++ b/internal/api/client/instance/instanceget.go @@ -5,6 +5,7 @@ "github.com/sirupsen/logrus" "github.com/spf13/viper" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/gin-gonic/gin" @@ -35,6 +36,11 @@ func (m *Module) InstanceInformationGETHandler(c *gin.Context) { l := logrus.WithField("func", "InstanceInformationGETHandler") + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + host := viper.GetString(config.Keys.Host) instance, err := m.processor.InstanceGet(c.Request.Context(), host) diff --git a/internal/api/client/instance/instancepatch.go b/internal/api/client/instance/instancepatch.go index 50923dd90..e0200cb0f 100644 --- a/internal/api/client/instance/instancepatch.go +++ b/internal/api/client/instance/instancepatch.go @@ -1,10 +1,12 @@ package instance import ( - "github.com/sirupsen/logrus" "net/http" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -93,6 +95,11 @@ func (m *Module) InstanceUpdatePATCHHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + // only admins can update instance settings if !authed.User.Admin { l.Debug("user is not an admin so cannot update instance settings") diff --git a/internal/api/client/list/listsgets.go b/internal/api/client/list/listsgets.go index 5d8d7d194..184830290 100644 --- a/internal/api/client/list/listsgets.go +++ b/internal/api/client/list/listsgets.go @@ -4,9 +4,15 @@ "net/http" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" ) // ListsGETHandler returns a list of lists created by/for the authed account func (m *Module) ListsGETHandler(c *gin.Context) { + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + c.JSON(http.StatusOK, []string{}) } diff --git a/internal/api/client/media/mediacreate.go b/internal/api/client/media/mediacreate.go index f808b554c..47f63561b 100644 --- a/internal/api/client/media/mediacreate.go +++ b/internal/api/client/media/mediacreate.go @@ -27,6 +27,7 @@ "github.com/spf13/viper" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -93,6 +94,11 @@ func (m *Module) MediaCreatePOSTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + // extract the media create form from the request context l.Tracef("parsing request form: %s", c.Request.Form) form := &model.AttachmentRequest{} diff --git a/internal/api/client/media/mediacreate_test.go b/internal/api/client/media/mediacreate_test.go index f8a17c9de..e87306977 100644 --- a/internal/api/client/media/mediacreate_test.go +++ b/internal/api/client/media/mediacreate_test.go @@ -149,6 +149,7 @@ func (suite *MediaCreateTestSuite) TestStatusCreatePOSTImageHandlerSuccessful() } ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", mediamodule.BasePath), bytes.NewReader(buf.Bytes())) // the endpoint we're hitting ctx.Request.Header.Set("Content-Type", w.FormDataContentType()) + ctx.Request.Header.Set("accept", "application/json") // do the actual request suite.mediaModule.MediaCreatePOSTHandler(ctx) diff --git a/internal/api/client/media/mediaget.go b/internal/api/client/media/mediaget.go index 0ddccc4e4..42af19000 100644 --- a/internal/api/client/media/mediaget.go +++ b/internal/api/client/media/mediaget.go @@ -19,10 +19,12 @@ package media import ( - "github.com/sirupsen/logrus" "net/http" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -70,6 +72,11 @@ func (m *Module) MediaGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + attachmentID := c.Param(IDKey) if attachmentID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"}) diff --git a/internal/api/client/media/mediaupdate.go b/internal/api/client/media/mediaupdate.go index c22e4e919..97d1e6b6e 100644 --- a/internal/api/client/media/mediaupdate.go +++ b/internal/api/client/media/mediaupdate.go @@ -27,6 +27,7 @@ "github.com/spf13/viper" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -102,6 +103,11 @@ func (m *Module) MediaPUTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + attachmentID := c.Param(IDKey) if attachmentID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no attachment ID given in request"}) diff --git a/internal/api/client/notification/notificationsget.go b/internal/api/client/notification/notificationsget.go index 98d5c2471..5d5aed7a5 100644 --- a/internal/api/client/notification/notificationsget.go +++ b/internal/api/client/notification/notificationsget.go @@ -24,6 +24,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -44,6 +45,11 @@ func (m *Module) NotificationsGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + limit := 20 limitString := c.Query(LimitKey) if limitString != "" { diff --git a/internal/api/client/search/searchget.go b/internal/api/client/search/searchget.go index f842e5044..0a33e86ce 100644 --- a/internal/api/client/search/searchget.go +++ b/internal/api/client/search/searchget.go @@ -25,6 +25,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -71,6 +72,11 @@ func (m *Module) SearchGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + accountID := c.Query(AccountIDKey) maxID := c.Query(MaxIDKey) minID := c.Query(MinIDKey) diff --git a/internal/api/client/status/statusboost.go b/internal/api/client/status/statusboost.go index bc950d358..ada791788 100644 --- a/internal/api/client/status/statusboost.go +++ b/internal/api/client/status/statusboost.go @@ -23,6 +23,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -74,13 +75,18 @@ func (m *Module) StatusBoostPOSTHandler(c *gin.Context) { }) l.Debugf("entering function") - authed, err := oauth.Authed(c, true, false, true, true) // we don't really need an app here but we want everything else + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { l.Debug("not authed so can't boost status") c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetStatusID := c.Param(IDKey) if targetStatusID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) diff --git a/internal/api/client/status/statusboost_test.go b/internal/api/client/status/statusboost_test.go index b5a377565..3fc27becc 100644 --- a/internal/api/client/status/statusboost_test.go +++ b/internal/api/client/status/statusboost_test.go @@ -51,6 +51,7 @@ func (suite *StatusBoostTestSuite) TestPostBoost() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") // 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. @@ -117,6 +118,7 @@ func (suite *StatusBoostTestSuite) TestPostUnboostable() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") // 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. @@ -155,6 +157,7 @@ func (suite *StatusBoostTestSuite) TestPostNotVisible() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_2"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_2"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.ReblogPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") // 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. diff --git a/internal/api/client/status/statuscontext.go b/internal/api/client/status/statuscontext.go index f9d7fcc35..7dc23f570 100644 --- a/internal/api/client/status/statuscontext.go +++ b/internal/api/client/status/statuscontext.go @@ -23,6 +23,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -80,6 +81,11 @@ func (m *Module) StatusContextGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetStatusID := c.Param(IDKey) if targetStatusID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) diff --git a/internal/api/client/status/statuscreate.go b/internal/api/client/status/statuscreate.go index 629a325c5..40a437564 100644 --- a/internal/api/client/status/statuscreate.go +++ b/internal/api/client/status/statuscreate.go @@ -27,6 +27,7 @@ "github.com/spf13/viper" "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/oauth" @@ -71,13 +72,18 @@ // description: internal error func (m *Module) StatusCreatePOSTHandler(c *gin.Context) { l := logrus.WithField("func", "statusCreatePOSTHandler") - authed, err := oauth.Authed(c, true, true, true, true) // posting a status is serious business so we want *everything* + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { l.Debugf("couldn't auth: %s", err) c.JSON(http.StatusForbidden, gin.H{"error": err.Error()}) return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + // First check this user/account is permitted to post new statuses. // There's no point continuing otherwise. if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { diff --git a/internal/api/client/status/statuscreate_test.go b/internal/api/client/status/statuscreate_test.go index 776b25769..cbbce681f 100644 --- a/internal/api/client/status/statuscreate_test.go +++ b/internal/api/client/status/statuscreate_test.go @@ -65,6 +65,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatus() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {"this is a brand new status! #helloworld"}, "spoiler_text": {"hello hello"}, @@ -119,6 +120,7 @@ func (suite *StatusCreateTestSuite) TestPostAnotherNewStatus() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {statusWithLinksAndTags}, } @@ -154,6 +156,7 @@ func (suite *StatusCreateTestSuite) TestPostNewStatusWithEmoji() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {"here is a rainbow emoji a few times! :rainbow: :rainbow: :rainbow: \n here's an emoji that isn't in the db: :test_emoji: "}, } @@ -195,6 +198,7 @@ func (suite *StatusCreateTestSuite) TestReplyToNonexistentStatus() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {"this is a reply to a status that doesn't exist"}, "spoiler_text": {"don't open cuz it won't work"}, @@ -226,6 +230,7 @@ func (suite *StatusCreateTestSuite) TestReplyToLocalStatus() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {fmt.Sprintf("hello @%s this reply should work!", testrig.NewTestAccounts()["local_account_2"].Username)}, "in_reply_to_id": {testrig.NewTestStatuses()["local_account_2_status_1"].ID}, @@ -268,6 +273,7 @@ func (suite *StatusCreateTestSuite) TestAttachNewMediaSuccess() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", status.BasePath), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "status": {"here's an image attachment"}, "media_ids": {attachment.ID}, diff --git a/internal/api/client/status/statusdelete.go b/internal/api/client/status/statusdelete.go index 5a6b2928e..8e2a81ecb 100644 --- a/internal/api/client/status/statusdelete.go +++ b/internal/api/client/status/statusdelete.go @@ -23,6 +23,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -73,13 +74,18 @@ func (m *Module) StatusDELETEHandler(c *gin.Context) { }) l.Debugf("entering function") - authed, err := oauth.Authed(c, true, false, true, true) // we don't really need an app here but we want everything else + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { l.Debug("not authed so can't delete status") c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetStatusID := c.Param(IDKey) if targetStatusID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) diff --git a/internal/api/client/status/statusfave.go b/internal/api/client/status/statusfave.go index 7d4879832..3ced93f3a 100644 --- a/internal/api/client/status/statusfave.go +++ b/internal/api/client/status/statusfave.go @@ -23,6 +23,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -70,13 +71,18 @@ func (m *Module) StatusFavePOSTHandler(c *gin.Context) { }) l.Debugf("entering function") - authed, err := oauth.Authed(c, true, false, true, true) // we don't really need an app here but we want everything else + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { l.Debug("not authed so can't fave status") c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetStatusID := c.Param(IDKey) if targetStatusID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) diff --git a/internal/api/client/status/statusfave_test.go b/internal/api/client/status/statusfave_test.go index 5b877a291..4c924e7a2 100644 --- a/internal/api/client/status/statusfave_test.go +++ b/internal/api/client/status/statusfave_test.go @@ -55,6 +55,7 @@ func (suite *StatusFaveTestSuite) TestPostFave() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") // 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. @@ -103,6 +104,7 @@ func (suite *StatusFaveTestSuite) TestPostUnfaveable() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.FavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") // 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. diff --git a/internal/api/client/status/statusfavedby.go b/internal/api/client/status/statusfavedby.go index 4de66d65d..a20b7bb8c 100644 --- a/internal/api/client/status/statusfavedby.go +++ b/internal/api/client/status/statusfavedby.go @@ -23,6 +23,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -71,13 +72,18 @@ func (m *Module) StatusFavedByGETHandler(c *gin.Context) { }) l.Debugf("entering function") - authed, err := oauth.Authed(c, false, false, false, false) // we don't really need an app here but we want everything else + authed, err := oauth.Authed(c, true, true, true, true) // we don't really need an app here but we want everything else if err != nil { l.Errorf("error authing status faved by request: %s", err) c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetStatusID := c.Param(IDKey) if targetStatusID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) diff --git a/internal/api/client/status/statusfavedby_test.go b/internal/api/client/status/statusfavedby_test.go index 0f10d8449..ca85c6ccf 100644 --- a/internal/api/client/status/statusfavedby_test.go +++ b/internal/api/client/status/statusfavedby_test.go @@ -53,6 +53,7 @@ func (suite *StatusFavedByTestSuite) TestGetFavedBy() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_2"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_2"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.FavouritedPath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") // 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. diff --git a/internal/api/client/status/statusget.go b/internal/api/client/status/statusget.go index b246002d8..061434457 100644 --- a/internal/api/client/status/statusget.go +++ b/internal/api/client/status/statusget.go @@ -23,6 +23,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -70,13 +71,18 @@ func (m *Module) StatusGETHandler(c *gin.Context) { }) l.Debugf("entering function") - authed, err := oauth.Authed(c, false, false, false, false) // we don't really need an app here but we want everything else + authed, err := oauth.Authed(c, false, false, false, false) if err != nil { l.Errorf("error authing status faved by request: %s", err) c.JSON(http.StatusBadRequest, gin.H{"error": "not authed"}) return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetStatusID := c.Param(IDKey) if targetStatusID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) diff --git a/internal/api/client/status/statusunboost.go b/internal/api/client/status/statusunboost.go index 7ddb274e0..bb50b3879 100644 --- a/internal/api/client/status/statusunboost.go +++ b/internal/api/client/status/statusunboost.go @@ -23,6 +23,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -71,13 +72,18 @@ func (m *Module) StatusUnboostPOSTHandler(c *gin.Context) { }) l.Debugf("entering function") - authed, err := oauth.Authed(c, true, false, true, true) // we don't really need an app here but we want everything else + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { l.Debug("not authed so can't unboost status") c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetStatusID := c.Param(IDKey) if targetStatusID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) diff --git a/internal/api/client/status/statusunfave.go b/internal/api/client/status/statusunfave.go index 29d3daa47..0b0cebc73 100644 --- a/internal/api/client/status/statusunfave.go +++ b/internal/api/client/status/statusunfave.go @@ -23,6 +23,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -70,13 +71,18 @@ func (m *Module) StatusUnfavePOSTHandler(c *gin.Context) { }) l.Debugf("entering function") - authed, err := oauth.Authed(c, true, false, true, true) // we don't really need an app here but we want everything else + authed, err := oauth.Authed(c, true, true, true, true) if err != nil { l.Debug("not authed so can't unfave status") c.JSON(http.StatusUnauthorized, gin.H{"error": "not authorized"}) return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + targetStatusID := c.Param(IDKey) if targetStatusID == "" { c.JSON(http.StatusBadRequest, gin.H{"error": "no status id provided"}) diff --git a/internal/api/client/status/statusunfave_test.go b/internal/api/client/status/statusunfave_test.go index 0809840da..3355c6326 100644 --- a/internal/api/client/status/statusunfave_test.go +++ b/internal/api/client/status/statusunfave_test.go @@ -56,6 +56,7 @@ func (suite *StatusUnfaveTestSuite) TestPostUnfave() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.UnfavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") // 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. @@ -105,6 +106,7 @@ func (suite *StatusUnfaveTestSuite) TestPostAlreadyNotFaved() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080%s", strings.Replace(status.UnfavouritePath, ":id", targetStatus.ID, 1)), nil) // the endpoint we're hitting + ctx.Request.Header.Set("accept", "application/json") // 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. diff --git a/internal/api/client/timeline/home.go b/internal/api/client/timeline/home.go index ef5aa48b6..2dedd9c9e 100644 --- a/internal/api/client/timeline/home.go +++ b/internal/api/client/timeline/home.go @@ -19,11 +19,13 @@ package timeline import ( - "github.com/sirupsen/logrus" "net/http" "strconv" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -112,6 +114,11 @@ func (m *Module) HomeTimelineGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + maxID := "" maxIDString := c.Query(MaxIDKey) if maxIDString != "" { diff --git a/internal/api/client/timeline/public.go b/internal/api/client/timeline/public.go index 049078e4d..ae2922a25 100644 --- a/internal/api/client/timeline/public.go +++ b/internal/api/client/timeline/public.go @@ -19,11 +19,13 @@ package timeline import ( - "github.com/sirupsen/logrus" "net/http" "strconv" + "github.com/sirupsen/logrus" + "github.com/gin-gonic/gin" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -112,6 +114,11 @@ func (m *Module) PublicTimelineGETHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + maxID := "" maxIDString := c.Query(MaxIDKey) if maxIDString != "" { diff --git a/internal/api/client/user/passwordchange.go b/internal/api/client/user/passwordchange.go index 581abe526..f17ec7e85 100644 --- a/internal/api/client/user/passwordchange.go +++ b/internal/api/client/user/passwordchange.go @@ -23,6 +23,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/api/model" "github.com/superseriousbusiness/gotosocial/internal/oauth" ) @@ -71,6 +72,11 @@ func (m *Module) PasswordChangePOSTHandler(c *gin.Context) { return } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + // First check this user/account is active. if authed.User.Disabled || !authed.User.Approved || !authed.Account.SuspendedAt.IsZero() { l.Debugf("couldn't auth: %s", err) diff --git a/internal/api/client/user/passwordchange_test.go b/internal/api/client/user/passwordchange_test.go index bdbeb3e42..ae513babe 100644 --- a/internal/api/client/user/passwordchange_test.go +++ b/internal/api/client/user/passwordchange_test.go @@ -50,6 +50,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordChangePOST() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil) + ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "old_password": {"password"}, "new_password": {"peepeepoopoopassword"}, @@ -83,6 +84,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordMissingOldPassword() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil) + ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "new_password": {"peepeepoopoopassword"}, } @@ -109,6 +111,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordIncorrectOldPassword() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil) + ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "old_password": {"notright"}, "new_password": {"peepeepoopoopassword"}, @@ -136,6 +139,7 @@ func (suite *PasswordChangeTestSuite) TestPasswordWeakNewPassword() { ctx.Set(oauth.SessionAuthorizedUser, suite.testUsers["local_account_1"]) ctx.Set(oauth.SessionAuthorizedAccount, suite.testAccounts["local_account_1"]) ctx.Request = httptest.NewRequest(http.MethodPost, fmt.Sprintf("http://localhost:8080/%s", user.PasswordChangePath), nil) + ctx.Request.Header.Set("accept", "application/json") ctx.Request.Form = url.Values{ "old_password": {"password"}, "new_password": {"peepeepoopoo"}, diff --git a/internal/api/model/well-known.go b/internal/api/model/well-known.go index 945215e4e..49c43d278 100644 --- a/internal/api/model/well-known.go +++ b/internal/api/model/well-known.go @@ -22,6 +22,8 @@ // For example, it would be returned from https://example.org/.well-known/webfinger?resource=acct:some_username@example.org // // See https://webfinger.net/ +// +// swagger:model wellKnownResponse type WellKnownResponse struct { Subject string `json:"subject,omitempty"` Aliases []string `json:"aliases,omitempty"` @@ -40,8 +42,11 @@ type Link struct { // Nodeinfo represents a version 2.1 or version 2.0 nodeinfo schema. // See: https://nodeinfo.diaspora.software/schema.html +// +// swagger:model nodeinfo type Nodeinfo struct { // The schema version + // example: 2.0 Version string `json:"version"` // Metadata about server software in use. Software NodeInfoSoftware `json:"software"` @@ -50,6 +55,7 @@ type Nodeinfo struct { // The third party sites this server can connect to via their application API. Services NodeInfoServices `json:"services"` // Whether this server allows open self-registration. + // example: false OpenRegistrations bool `json:"openRegistrations"` // Usage statistics for this server. Usage NodeInfoUsage `json:"usage"` @@ -59,7 +65,9 @@ type Nodeinfo struct { // NodeInfoSoftware represents the name and version number of the software of this node. type NodeInfoSoftware struct { - Name string `json:"name"` + // example: gotosocial + Name string `json:"name"` + // example: 0.1.2 1234567 Version string `json:"version"` } diff --git a/internal/api/negotiate.go b/internal/api/negotiate.go new file mode 100644 index 000000000..4d529b3df --- /dev/null +++ b/internal/api/negotiate.go @@ -0,0 +1,96 @@ +/* + GoToSocial + Copyright (C) 2021 GoToSocial Authors admin@gotosocial.org + + This program is free software: you can redistribute it and/or modify + it under the terms of the GNU Affero General Public License as published by + the Free Software Foundation, either version 3 of the License, or + (at your option) any later version. + + This program is distributed in the hope that it will be useful, + but WITHOUT ANY WARRANTY; without even the implied warranty of + MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the + GNU Affero General Public License for more details. + + You should have received a copy of the GNU Affero General Public License + along with this program. If not, see . +*/ + +package api + +import ( + "errors" + "fmt" + + "github.com/gin-gonic/gin" +) + +// Offer represents an offered mime-type. +type Offer string + +const ( + AppJSON Offer = `application/json` // AppJSON is the mime type for 'application/json'. + AppActivityJSON Offer = `application/activity+json` // AppActivityJSON is the mime type for 'application/activity+json'. + AppActivityLDJSON Offer = `application/ld+json; profile="https://www.w3.org/ns/activitystreams"` // AppActivityLDJSON is the mime type for 'application/ld+json; profile="https://www.w3.org/ns/activitystreams"' + TextHTML Offer = `text/html` // TextHTML is the mime type for 'text/html'. +) + +// ActivityPubAcceptHeaders represents the Accept headers mentioned here: +// https://www.w3.org/TR/activitypub/#retrieving-objects +var ActivityPubAcceptHeaders = []Offer{ + AppActivityJSON, + AppActivityLDJSON, +} + +// JSONAcceptHeaders is a slice of offers that just contains application/json types. +var JSONAcceptHeaders = []Offer{ + AppJSON, +} + +// HTMLAcceptHeaders is a slice of offers that just contains text/html types. +var HTMLAcceptHeaders = []Offer{ + TextHTML, +} + +// NegotiateAccept takes the *gin.Context from an incoming request, and a +// slice of Offers, and performs content negotiation for the given request +// with the given content-type offers. It will return a string representation +// of the first suitable content-type, or an error if something goes wrong or +// a suiteable content-type cannot be matched. +// +// For example, if the request in the *gin.Context has Accept headers of value +// [application/json, text/html], and the provided offers are of value +// [application/json, application/xml], then the returned string will be +// 'application/json', which indicates the content-type that should be returned. +// +// If there are no Accept headers in the request, or the length of offers is 0, +// then an error will be returned, so this function should only be called in places +// where format negotiation is actually needed and headers are expected to be present +// on incoming requests. +// +// Callers can use the offer slices exported in this package as shortcuts for +// often-used Accept types. +// +// See https://developer.mozilla.org/en-US/docs/Web/HTTP/Content_negotiation#server-driven_content_negotiation +func NegotiateAccept(c *gin.Context, offers ...Offer) (string, error) { + if len(offers) == 0 { + return "", errors.New("no format offered") + } + + accepts := c.Request.Header.Values("Accept") + if len(accepts) == 0 { + return "", fmt.Errorf("no Accept header(s) set on incoming request; this endpoint offers %s", offers) + } + + strings := []string{} + for _, o := range offers { + strings = append(strings, string(o)) + } + + format := c.NegotiateFormat(strings...) + if format == "" { + return "", fmt.Errorf("no format can be offered for requested Accept header(s) %s; this endpoint offers %s", accepts, offers) + } + + return format, nil +} diff --git a/internal/api/s2s/nodeinfo/nodeinfoget.go b/internal/api/s2s/nodeinfo/nodeinfoget.go index 547aa2cf9..61247b1df 100644 --- a/internal/api/s2s/nodeinfo/nodeinfoget.go +++ b/internal/api/s2s/nodeinfo/nodeinfoget.go @@ -19,20 +19,42 @@ package nodeinfo import ( + "encoding/json" "net/http" "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" ) -// NodeInfoGETHandler returns a compliant nodeinfo response to node info queries. -// See: https://nodeinfo.diaspora.software/ +// 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) { l := logrus.WithFields(logrus.Fields{ "func": "NodeInfoGETHandler", "user-agent": c.Request.UserAgent(), }) + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + ni, err := m.processor.GetNodeInfo(c.Request.Context(), c.Request) if err != nil { l.Debugf("error with get node info request: %s", err) @@ -40,5 +62,10 @@ func (m *Module) NodeInfoGETHandler(c *gin.Context) { return } - c.JSON(http.StatusOK, ni) + b, jsonErr := json.Marshal(ni) + if jsonErr != nil { + c.JSON(http.StatusInternalServerError, gin.H{"error": jsonErr.Error()}) + } + + 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 index e5aa39aad..aab89a994 100644 --- a/internal/api/s2s/nodeinfo/wellknownget.go +++ b/internal/api/s2s/nodeinfo/wellknownget.go @@ -23,16 +23,37 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" ) -// NodeInfoWellKnownGETHandler returns a well known response to a query to /.well-known/nodeinfo, -// directing (but not redirecting...) callers to the NodeInfoGETHandler. +// 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) { l := logrus.WithFields(logrus.Fields{ - "func": "NodeInfoWellKnownGETHandler", - "user-agent": c.Request.UserAgent(), + "func": "NodeInfoWellKnownGETHandler", }) + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + niRel, err := m.processor.GetNodeInfoRel(c.Request.Context(), c.Request) if err != nil { l.Debugf("error with get node info rel request: %s", err) diff --git a/internal/api/s2s/user/common.go b/internal/api/s2s/user/common.go index 9f426274d..c03765bfb 100644 --- a/internal/api/s2s/user/common.go +++ b/internal/api/s2s/user/common.go @@ -20,19 +20,11 @@ import ( "context" - "fmt" "github.com/gin-gonic/gin" "github.com/superseriousbusiness/gotosocial/internal/util" ) -// ActivityPubAcceptHeaders represents the Accept headers mentioned here: -// https://www.w3.org/TR/activitypub/#retrieving-objects -var ActivityPubAcceptHeaders = []string{ - `application/activity+json`, - `application/ld+json; profile="https://www.w3.org/ns/activitystreams"`, -} - // 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() @@ -50,14 +42,6 @@ func transferContext(c *gin.Context) context.Context { return ctx } -func negotiateFormat(c *gin.Context) (string, error) { - format := c.NegotiateFormat(ActivityPubAcceptHeaders...) - if format == "" { - return "", fmt.Errorf("no format can be offered for Accept headers %s", c.Request.Header.Get("Accept")) - } - return format, nil -} - // SwaggerCollection represents an activitypub collection. // swagger:model swaggerCollection type SwaggerCollection struct { diff --git a/internal/api/s2s/user/followers.go b/internal/api/s2s/user/followers.go index 288e60f30..4d5ecc728 100644 --- a/internal/api/s2s/user/followers.go +++ b/internal/api/s2s/user/followers.go @@ -25,6 +25,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" ) // FollowersGETHandler returns a collection of URIs for followers of the target user, formatted so that other AP servers can understand it. @@ -40,9 +41,9 @@ func (m *Module) FollowersGETHandler(c *gin.Context) { return } - format, err := negotiateFormat(c) + format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": fmt.Sprintf("could not negotiate format with given Accept header(s): %s", err)}) + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) return } l.Tracef("negotiated format: %s", format) diff --git a/internal/api/s2s/user/following.go b/internal/api/s2s/user/following.go index 6c7b60d32..3471eefa3 100644 --- a/internal/api/s2s/user/following.go +++ b/internal/api/s2s/user/following.go @@ -25,6 +25,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" ) // FollowingGETHandler returns a collection of URIs for accounts that the target user follows, formatted so that other AP servers can understand it. @@ -40,9 +41,9 @@ func (m *Module) FollowingGETHandler(c *gin.Context) { return } - format, err := negotiateFormat(c) + format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": fmt.Sprintf("could not negotiate format with given Accept header(s): %s", err)}) + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) return } l.Tracef("negotiated format: %s", format) diff --git a/internal/api/s2s/user/outboxget.go b/internal/api/s2s/user/outboxget.go index 8b3fecfdd..7401109bb 100644 --- a/internal/api/s2s/user/outboxget.go +++ b/internal/api/s2s/user/outboxget.go @@ -26,6 +26,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" ) // OutboxGETHandler swagger:operation GET /users/{username}/outbox s2sOutboxGet @@ -113,9 +114,9 @@ func (m *Module) OutboxGETHandler(c *gin.Context) { maxID = maxIDString } - format, err := negotiateFormat(c) + format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": fmt.Sprintf("could not negotiate format with given Accept header(s): %s", err)}) + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) return } l.Tracef("negotiated format: %s", format) diff --git a/internal/api/s2s/user/outboxget_test.go b/internal/api/s2s/user/outboxget_test.go index 4cd556bbe..8b345b9e7 100644 --- a/internal/api/s2s/user/outboxget_test.go +++ b/internal/api/s2s/user/outboxget_test.go @@ -54,6 +54,7 @@ func (suite *OutboxGetTestSuite) TestGetOutbox() { recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) 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) @@ -108,6 +109,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxFirstPage() { recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) 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) @@ -162,6 +164,7 @@ func (suite *OutboxGetTestSuite) TestGetOutboxNextPage() { recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) 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) diff --git a/internal/api/s2s/user/publickeyget.go b/internal/api/s2s/user/publickeyget.go index bf7116833..462f66b07 100644 --- a/internal/api/s2s/user/publickeyget.go +++ b/internal/api/s2s/user/publickeyget.go @@ -25,6 +25,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" ) // PublicKeyGETHandler should be served at eg https://example.org/users/:username/main-key. @@ -44,9 +45,9 @@ func (m *Module) PublicKeyGETHandler(c *gin.Context) { return } - format, err := negotiateFormat(c) + format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": fmt.Sprintf("could not negotiate format with given Accept header(s): %s", err)}) + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) return } l.Tracef("negotiated format: %s", format) diff --git a/internal/api/s2s/user/repliesget.go b/internal/api/s2s/user/repliesget.go index fd3c680f1..fef0392e8 100644 --- a/internal/api/s2s/user/repliesget.go +++ b/internal/api/s2s/user/repliesget.go @@ -26,6 +26,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" ) // StatusRepliesGETHandler swagger:operation GET /users/{username}/statuses/{status}/replies s2sRepliesGet @@ -131,9 +132,9 @@ func (m *Module) StatusRepliesGETHandler(c *gin.Context) { minID = minIDString } - format, err := negotiateFormat(c) + format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": fmt.Sprintf("could not negotiate format with given Accept header(s): %s", err)}) + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) return } l.Tracef("negotiated format: %s", format) diff --git a/internal/api/s2s/user/repliesget_test.go b/internal/api/s2s/user/repliesget_test.go index a4229bb21..636eca8eb 100644 --- a/internal/api/s2s/user/repliesget_test.go +++ b/internal/api/s2s/user/repliesget_test.go @@ -57,6 +57,7 @@ func (suite *RepliesGetTestSuite) TestGetReplies() { recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) 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) @@ -117,6 +118,7 @@ func (suite *RepliesGetTestSuite) TestGetRepliesNext() { recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) 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) @@ -180,6 +182,7 @@ func (suite *RepliesGetTestSuite) TestGetRepliesLast() { recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) 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) diff --git a/internal/api/s2s/user/statusget.go b/internal/api/s2s/user/statusget.go index 91c29e3cb..f865ecbd7 100644 --- a/internal/api/s2s/user/statusget.go +++ b/internal/api/s2s/user/statusget.go @@ -25,6 +25,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" ) // StatusGETHandler serves the target status as an activitystreams NOTE so that other AP servers can parse it. @@ -46,9 +47,9 @@ func (m *Module) StatusGETHandler(c *gin.Context) { return } - format, err := negotiateFormat(c) + format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": fmt.Sprintf("could not negotiate format with given Accept header(s): %s", err)}) + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) return } l.Tracef("negotiated format: %s", format) diff --git a/internal/api/s2s/user/userget.go b/internal/api/s2s/user/userget.go index 2af692b3c..915c45ba4 100644 --- a/internal/api/s2s/user/userget.go +++ b/internal/api/s2s/user/userget.go @@ -25,6 +25,7 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" + "github.com/superseriousbusiness/gotosocial/internal/api" ) // UsersGETHandler should be served at https://example.org/users/:username. @@ -48,9 +49,9 @@ func (m *Module) UsersGETHandler(c *gin.Context) { return } - format, err := negotiateFormat(c) + format, err := api.NegotiateAccept(c, api.ActivityPubAcceptHeaders...) if err != nil { - c.JSON(http.StatusNotAcceptable, gin.H{"error": fmt.Sprintf("could not negotiate format with given Accept header(s): %s", err)}) + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) return } l.Tracef("negotiated format: %s", format) diff --git a/internal/api/s2s/user/userget_test.go b/internal/api/s2s/user/userget_test.go index 8ad4a8151..8aa0ab450 100644 --- a/internal/api/s2s/user/userget_test.go +++ b/internal/api/s2s/user/userget_test.go @@ -55,6 +55,7 @@ func (suite *UserGetTestSuite) TestGetUser() { recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) 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) diff --git a/internal/api/s2s/webfinger/webfingerget.go b/internal/api/s2s/webfinger/webfingerget.go index b7f3c714d..6b0de69a9 100644 --- a/internal/api/s2s/webfinger/webfingerget.go +++ b/internal/api/s2s/webfinger/webfingerget.go @@ -27,26 +27,54 @@ "github.com/gin-gonic/gin" "github.com/sirupsen/logrus" "github.com/spf13/viper" + "github.com/superseriousbusiness/gotosocial/internal/api" "github.com/superseriousbusiness/gotosocial/internal/config" "github.com/superseriousbusiness/gotosocial/internal/util" ) -// WebfingerGETRequest handles requests to, for example, https://example.org/.well-known/webfinger?resource=acct:some_user@example.org +// 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 := logrus.WithFields(logrus.Fields{ "func": "WebfingerGETRequest", "user-agent": c.Request.UserAgent(), }) - q, set := c.GetQuery("resource") - if !set || q == "" { + 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 } + if _, err := api.NegotiateAccept(c, api.JSONAcceptHeaders...); err != nil { + c.JSON(http.StatusNotAcceptable, gin.H{"error": err.Error()}) + return + } + // remove the acct: prefix if it's present - trimAcct := strings.TrimPrefix(q, "acct:") + trimAcct := strings.TrimPrefix(resourceQuery, "acct:") // remove the first @ in @whatever@example.org if it's present namestring := strings.TrimPrefix(trimAcct, "@") diff --git a/internal/api/s2s/webfinger/webfingerget_test.go b/internal/api/s2s/webfinger/webfingerget_test.go index 8314972d6..c10f7e186 100644 --- a/internal/api/s2s/webfinger/webfingerget_test.go +++ b/internal/api/s2s/webfinger/webfingerget_test.go @@ -50,6 +50,7 @@ func (suite *WebfingerGetTestSuite) TestFingerUser() { recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) 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) @@ -83,6 +84,7 @@ func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByHo recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) 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) @@ -116,6 +118,7 @@ func (suite *WebfingerGetTestSuite) TestFingerUserWithDifferentAccountDomainByAc recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) 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) @@ -141,6 +144,7 @@ func (suite *WebfingerGetTestSuite) TestFingerUserWithoutAcct() { recorder := httptest.NewRecorder() ctx, _ := gin.CreateTestContext(recorder) 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) diff --git a/mkdocs.yml b/mkdocs.yml index c61e0f132..59965ce22 100644 --- a/mkdocs.yml +++ b/mkdocs.yml @@ -21,7 +21,7 @@ nav: - "configuration/index.md" - "configuration/general.md" - "configuration/database.md" - - "configuration/template.md" + - "configuration/web.md" - "configuration/accounts.md" - "configuration/media.md" - "configuration/storage.md"