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
This commit is contained in:
tobi 2021-12-11 17:50:00 +01:00 committed by GitHub
parent 0884f89431
commit e2daf0f012
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
78 changed files with 752 additions and 72 deletions

View File

@ -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: |-

View File

@ -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
}

View File

@ -27,6 +27,7 @@ import (
"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 {

View File

@ -22,6 +22,7 @@ import (
"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"})

View File

@ -20,11 +20,13 @@ package account
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()})

View File

@ -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)

View File

@ -22,6 +22,7 @@ import (
"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"})

View File

@ -22,6 +22,7 @@ import (
"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"})

View File

@ -22,6 +22,7 @@ import (
"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"})

View File

@ -22,6 +22,7 @@ import (
"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"})

View File

@ -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'?

View File

@ -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")

View File

@ -22,6 +22,7 @@ import (
"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"})

View File

@ -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)

View File

@ -8,6 +8,7 @@ import (
"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 != "" {

View File

@ -5,6 +5,7 @@ import (
"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"})

View File

@ -6,6 +6,7 @@ import (
"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"})

View File

@ -6,6 +6,7 @@ import (
"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 != "" {

View File

@ -25,6 +25,7 @@ import (
"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{}

View File

@ -25,6 +25,7 @@ import (
"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()})

View File

@ -21,14 +21,16 @@ package auth
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)

View File

@ -21,11 +21,13 @@ package auth
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)

View File

@ -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{}

View File

@ -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 != "" {

View File

@ -4,9 +4,15 @@ import (
"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{})
}

View File

@ -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 != "" {

View File

@ -24,6 +24,7 @@ import (
"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)
}

View File

@ -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.

View File

@ -4,9 +4,15 @@ import (
"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{})
}

View File

@ -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"})

View File

@ -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
}

View File

@ -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"})

View File

@ -23,6 +23,7 @@ import (
"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"})

View File

@ -5,6 +5,7 @@ import (
"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 @@ import (
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)

View File

@ -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")

View File

@ -4,9 +4,15 @@ import (
"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{})
}

View File

@ -27,6 +27,7 @@ import (
"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{}

View File

@ -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)

View File

@ -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"})

View File

@ -27,6 +27,7 @@ import (
"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"})

View File

@ -24,6 +24,7 @@ import (
"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 != "" {

View File

@ -25,6 +25,7 @@ import (
"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)

View File

@ -23,6 +23,7 @@ import (
"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"})

View File

@ -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.

View File

@ -23,6 +23,7 @@ import (
"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"})

View File

@ -27,6 +27,7 @@ import (
"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 @@ import (
// 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() {

View File

@ -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},

View File

@ -23,6 +23,7 @@ import (
"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"})

View File

@ -23,6 +23,7 @@ import (
"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"})

View File

@ -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.

View File

@ -23,6 +23,7 @@ import (
"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"})

View File

@ -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.

View File

@ -23,6 +23,7 @@ import (
"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"})

View File

@ -23,6 +23,7 @@ import (
"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"})

View File

@ -23,6 +23,7 @@ import (
"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"})

View File

@ -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.

View File

@ -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 != "" {

View File

@ -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 != "" {

View File

@ -23,6 +23,7 @@ import (
"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)

View File

@ -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"},

View File

@ -22,6 +22,8 @@ package model
// 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"`
}

96
internal/api/negotiate.go Normal file
View File

@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

View File

@ -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)
}

View File

@ -23,16 +23,37 @@ import (
"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)

View File

@ -20,19 +20,11 @@ package user
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 {

View File

@ -25,6 +25,7 @@ import (
"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)

View File

@ -25,6 +25,7 @@ import (
"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)

View File

@ -26,6 +26,7 @@ import (
"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)

View File

@ -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)

View File

@ -25,6 +25,7 @@ import (
"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)

View File

@ -26,6 +26,7 @@ import (
"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)

View File

@ -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)

View File

@ -25,6 +25,7 @@ import (
"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)

View File

@ -25,6 +25,7 @@ import (
"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)

View File

@ -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)

View File

@ -27,26 +27,54 @@ import (
"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, "@")

View File

@ -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)

View File

@ -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"