[chore] The Big Middleware and API Refactor (tm) (#1250)

* interim commit: start refactoring middlewares into package under router

* another interim commit, this is becoming a big job

* another fucking massive interim commit

* refactor bookmarks to new style

* ambassador, wiz zeze commits you are spoiling uz

* she compiles, we're getting there

* we're just normal men; we're just innocent men

* apiutil

* whoopsie

* i'm glad noone reads commit msgs haha :blob_sweat:

* use that weirdo go-bytesize library for maxMultipartMemory

* fix media module paths
This commit is contained in:
tobi
2023-01-02 13:10:50 +01:00
committed by GitHub
parent 560ff1209d
commit 941893a774
228 changed files with 3188 additions and 3047 deletions

View File

@@ -0,0 +1,35 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package middleware
import (
"strings"
"github.com/gin-gonic/gin"
)
// CacheControl returns a new gin middleware which allows callers to control cache settings on response headers.
//
// For directives, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
func CacheControl(directives ...string) gin.HandlerFunc {
ccHeader := strings.Join(directives, ", ")
return func(c *gin.Context) {
c.Header("Cache-Control", ccHeader)
}
}

View File

@@ -0,0 +1,86 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package middleware
import (
"time"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
// CORS returns a new gin middleware which allows CORS requests to be processed.
// This is necessary in order for web/browser-based clients like Pinafore to work.
func CORS() gin.HandlerFunc {
cfg := cors.Config{
// todo: use config to customize this
AllowAllOrigins: true,
// adds the following:
// "chrome-extension://"
// "safari-extension://"
// "moz-extension://"
// "ms-browser-extension://"
AllowBrowserExtensions: true,
AllowMethods: []string{
"POST",
"PUT",
"DELETE",
"GET",
"PATCH",
"OPTIONS",
},
AllowHeaders: []string{
// basic cors stuff
"Origin",
"Content-Length",
"Content-Type",
// needed to pass oauth bearer tokens
"Authorization",
// needed for websocket upgrade requests
"Upgrade",
"Sec-WebSocket-Extensions",
"Sec-WebSocket-Key",
"Sec-WebSocket-Protocol",
"Sec-WebSocket-Version",
"Connection",
},
AllowWebSockets: true,
ExposeHeaders: []string{
// needed for accessing next/prev links when making GET timeline requests
"Link",
// needed so clients can handle rate limits
"X-RateLimit-Reset",
"X-RateLimit-Limit",
"X-RateLimit-Remaining",
"X-Request-Id",
// websocket stuff
"Connection",
"Sec-WebSocket-Accept",
"Upgrade",
},
MaxAge: 2 * time.Minute,
}
return cors.New(cfg)
}

View File

@@ -0,0 +1,37 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package middleware
import "github.com/gin-gonic/gin"
// ExtraHeaders returns a new gin middleware which adds various extra headers to the response.
func ExtraHeaders() gin.HandlerFunc {
return func(c *gin.Context) {
// Inform all callers which server implementation this is.
c.Header("Server", "gotosocial")
// Prevent google chrome cohort tracking. Originally this was referred
// to as FlocBlock. Floc was replaced by Topics in 2022 and the spec says
// that interest-cohort will also block Topics (as of 2022-Nov).
//
// See: https://smartframe.io/blog/google-topics-api-everything-you-need-to-know
//
// See: https://github.com/patcg-individual-drafts/topics
c.Header("Permissions-Policy", "browsing-topics=()")
}
}

View File

@@ -0,0 +1,30 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package middleware
import (
ginGzip "github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin"
)
// Gzip returns a gzip gin middleware using default compression.
func Gzip() gin.HandlerFunc {
// todo: make this configurable
return ginGzip.Gzip(ginGzip.DefaultCompression)
}

View File

@@ -0,0 +1,99 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package middleware
import (
"fmt"
"net/http"
"time"
"codeberg.org/gruf/go-bytesize"
"codeberg.org/gruf/go-errors/v2"
"codeberg.org/gruf/go-kv"
"codeberg.org/gruf/go-logger/v2/level"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/log"
)
// Logger returns a gin middleware which provides request logging and panic recovery.
func Logger() gin.HandlerFunc {
return func(c *gin.Context) {
// Initialize the logging fields
fields := make(kv.Fields, 6, 7)
// Determine pre-handler time
before := time.Now()
// defer so that we log *after the request has completed*
defer func() {
code := c.Writer.Status()
path := c.Request.URL.Path
if r := recover(); r != nil {
if c.Writer.Status() == 0 {
// No response was written, send a generic Internal Error
c.Writer.WriteHeader(http.StatusInternalServerError)
}
// Append panic information to the request ctx
err := fmt.Errorf("recovered panic: %v", r)
_ = c.Error(err)
// Dump a stacktrace to error log
callers := errors.GetCallers(3, 10)
log.WithField("stacktrace", callers).Error(err)
}
// NOTE:
// It is very important here that we are ONLY logging
// the request path, and none of the query parameters.
// Query parameters can contain sensitive information
// and could lead to storing plaintext API keys in logs
// Set request logging fields
fields[0] = kv.Field{"latency", time.Since(before)}
fields[1] = kv.Field{"clientIP", c.ClientIP()}
fields[2] = kv.Field{"userAgent", c.Request.UserAgent()}
fields[3] = kv.Field{"method", c.Request.Method}
fields[4] = kv.Field{"statusCode", code}
fields[5] = kv.Field{"path", path}
// Create log entry with fields
l := log.WithFields(fields...)
// Default is info
lvl := level.INFO
if code >= 500 {
// This is a server error
lvl = level.ERROR
l = l.WithField("error", c.Errors)
}
// Generate a nicer looking bytecount
size := bytesize.Size(c.Writer.Size())
// Finally, write log entry with status text body size
l.Logf(lvl, "%s: wrote %s", http.StatusText(code), size)
}()
// Process request
c.Next()
}
}

View File

@@ -0,0 +1,77 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package middleware
import (
"net"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/ulule/limiter/v3"
limitergin "github.com/ulule/limiter/v3/drivers/middleware/gin"
"github.com/ulule/limiter/v3/drivers/store/memory"
)
const rateLimitPeriod = 5 * time.Minute
// RateLimit returns a gin middleware that will automatically rate limit caller (by IP address),
// and enrich the response header with the following headers:
//
// - `x-ratelimit-limit` - maximum number of requests allowed per time period (fixed).
// - `x-ratelimit-remaining` - number of remaining requests that can still be performed.
// - `x-ratelimit-reset` - unix timestamp when the rate limit will reset.
//
// If `x-ratelimit-limit` is exceeded, the request is aborted and an HTTP 429 TooManyRequests
// status is returned.
//
// If the config AdvancedRateLimitRequests value is <= 0, then a noop handler will be returned,
// which performs no rate limiting.
func RateLimit() gin.HandlerFunc {
// only enable rate limit middleware if configured
// advanced-rate-limit-requests is greater than 0
rateLimitRequests := config.GetAdvancedRateLimitRequests()
if rateLimitRequests <= 0 {
// use noop middleware if ratelimiting is disabled
return func(c *gin.Context) {}
}
rate := limiter.Rate{
Period: rateLimitPeriod,
Limit: int64(rateLimitRequests),
}
limiterInstance := limiter.New(
memory.NewStore(),
rate,
limiter.WithIPv6Mask(net.CIDRMask(64, 128)), // apply /64 mask to IPv6 addresses
)
limitReachedHandler := func(c *gin.Context) {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit reached"})
}
middleware := limitergin.NewMiddleware(
limiterInstance,
limitergin.WithLimitReachedHandler(limitReachedHandler), // use custom rate limit reached error
)
return middleware
}

View File

@@ -0,0 +1,95 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package middleware
import (
"fmt"
"net/http"
"net/url"
"strings"
"github.com/gin-contrib/sessions"
"github.com/gin-contrib/sessions/memstore"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/log"
"golang.org/x/net/idna"
)
// SessionOptions returns the standard set of options to use for each session.
func SessionOptions() sessions.Options {
var samesite http.SameSite
switch strings.TrimSpace(strings.ToLower(config.GetAdvancedCookiesSamesite())) {
case "lax":
samesite = http.SameSiteLaxMode
case "strict":
samesite = http.SameSiteStrictMode
default:
log.Warnf("%s set to %s which is not recognized, defaulting to 'lax'", config.AdvancedCookiesSamesiteFlag(), config.GetAdvancedCookiesSamesite())
samesite = http.SameSiteLaxMode
}
return sessions.Options{
Path: "/",
Domain: config.GetHost(),
// 2 minutes
MaxAge: 120,
// only set secure over https
Secure: config.GetProtocol() == "https",
// forbid javascript from inspecting cookie
HttpOnly: true,
// https://datatracker.ietf.org/doc/html/draft-ietf-httpbis-cookie-same-site-00#section-4.1.1
SameSite: samesite,
}
}
// SessionName is a utility function that derives an appropriate session name from the hostname.
func SessionName() (string, error) {
// parse the protocol + host
protocol := config.GetProtocol()
host := config.GetHost()
u, err := url.Parse(fmt.Sprintf("%s://%s", protocol, host))
if err != nil {
return "", err
}
// take the hostname without any port attached
strippedHostname := u.Hostname()
if strippedHostname == "" {
return "", fmt.Errorf("could not derive hostname without port from %s://%s", protocol, host)
}
// make sure IDNs are converted to punycode or the cookie library breaks:
// see https://en.wikipedia.org/wiki/Punycode
punyHostname, err := idna.New().ToASCII(strippedHostname)
if err != nil {
return "", fmt.Errorf("could not convert %s to punycode: %s", strippedHostname, err)
}
return fmt.Sprintf("gotosocial-%s", punyHostname), nil
}
// Session returns a new gin middleware that implements session cookies using the given
// sessionName, authentication key, and encryption key. Session name can be derived from the
// SessionName utility function in this package.
func Session(sessionName string, auth []byte, crypt []byte) gin.HandlerFunc {
store := memstore.NewStore(auth, crypt)
store.Options(SessionOptions())
return sessions.Sessions(sessionName, store)
}

View File

@@ -0,0 +1,95 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package middleware_test
import (
"testing"
"github.com/stretchr/testify/suite"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/middleware"
"github.com/superseriousbusiness/gotosocial/testrig"
)
type SessionTestSuite struct {
suite.Suite
}
func (suite *SessionTestSuite) SetupTest() {
testrig.InitTestConfig()
}
func (suite *SessionTestSuite) TestDeriveSessionNameLocalhostWithPort() {
config.SetProtocol("http")
config.SetHost("localhost:8080")
sessionName, err := middleware.SessionName()
suite.NoError(err)
suite.Equal("gotosocial-localhost", sessionName)
}
func (suite *SessionTestSuite) TestDeriveSessionNameLocalhost() {
config.SetProtocol("http")
config.SetHost("localhost")
sessionName, err := middleware.SessionName()
suite.NoError(err)
suite.Equal("gotosocial-localhost", sessionName)
}
func (suite *SessionTestSuite) TestDeriveSessionNoProtocol() {
config.SetProtocol("")
config.SetHost("localhost")
sessionName, err := middleware.SessionName()
suite.EqualError(err, "parse \"://localhost\": missing protocol scheme")
suite.Equal("", sessionName)
}
func (suite *SessionTestSuite) TestDeriveSessionNoHost() {
config.SetProtocol("https")
config.SetHost("")
config.SetPort(0)
sessionName, err := middleware.SessionName()
suite.EqualError(err, "could not derive hostname without port from https://")
suite.Equal("", sessionName)
}
func (suite *SessionTestSuite) TestDeriveSessionOK() {
config.SetProtocol("https")
config.SetHost("example.org")
sessionName, err := middleware.SessionName()
suite.NoError(err)
suite.Equal("gotosocial-example.org", sessionName)
}
func (suite *SessionTestSuite) TestDeriveSessionIDNOK() {
config.SetProtocol("https")
config.SetHost("fóid.org")
sessionName, err := middleware.SessionName()
suite.NoError(err)
suite.Equal("gotosocial-xn--fid-gna.org", sessionName)
}
func TestSessionTestSuite(t *testing.T) {
suite.Run(t, &SessionTestSuite{})
}

View File

@@ -0,0 +1,93 @@
package middleware
import (
"context"
"fmt"
"net/http"
"net/url"
"github.com/superseriousbusiness/gotosocial/internal/ap"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/gin-gonic/gin"
"github.com/go-fed/httpsig"
)
var (
// this mimics an untyped error returned by httpsig when no signature is present;
// define it here so that we can use it to decide what to log without hitting
// performance too hard
noSignatureError = fmt.Sprintf("neither %q nor %q have signature parameters", httpsig.Signature, httpsig.Authorization)
signatureHeader = string(httpsig.Signature)
authorizationHeader = string(httpsig.Authorization)
)
// SignatureCheck returns a gin middleware for checking http signatures.
//
// The middleware first checks whether an incoming http request has been http-signed with a well-formed signature.
//
// If so, it will check if the domain that signed the request is permitted to access the server, using the provided isURIBlocked function.
//
// If it is permitted, the handler will set the key verifier and the signature in the gin context for use down the line.
//
// If the domain is blocked, the middleware will abort the request chain instead with http code 403 forbidden.
//
// In case of an error, the request will be aborted with http code 500 internal server error.
func SignatureCheck(isURIBlocked func(context.Context, *url.URL) (bool, db.Error)) func(*gin.Context) {
return func(c *gin.Context) {
// create the verifier from the request, this will error if the request wasn't signed
verifier, err := httpsig.NewVerifier(c.Request)
if err != nil {
// Something went wrong, so we need to return regardless, but only actually
// *abort* the request with 401 if a signature was present but malformed
if err.Error() != noSignatureError {
log.Debugf("http signature was present but invalid: %s", err)
c.AbortWithStatus(http.StatusUnauthorized)
}
return
}
// The request was signed!
// The key ID should be given in the signature so that we know where to fetch it from the remote server.
// This will be something like https://example.org/users/whatever_requesting_user#main-key
requestingPublicKeyIDString := verifier.KeyId()
requestingPublicKeyID, err := url.Parse(requestingPublicKeyIDString)
if err != nil {
log.Debugf("http signature requesting public key id %s could not be parsed as a url: %s", requestingPublicKeyIDString, err)
c.AbortWithStatus(http.StatusUnauthorized)
return
} else if requestingPublicKeyID == nil {
// Key can sometimes be nil, according to url parse function:
// 'Trying to parse a hostname and path without a scheme is invalid but may not necessarily return an error, due to parsing ambiguities'
log.Debugf("http signature requesting public key id %s was nil after parsing as a url", requestingPublicKeyIDString)
c.AbortWithStatus(http.StatusUnauthorized)
return
}
// we managed to parse the url!
// if the domain is blocked we want to bail as early as possible
if blocked, err := isURIBlocked(c.Request.Context(), requestingPublicKeyID); err != nil {
log.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err)
c.AbortWithStatus(http.StatusInternalServerError)
return
} else if blocked {
log.Infof("domain %s is blocked", requestingPublicKeyID.Host)
c.AbortWithStatus(http.StatusForbidden)
return
}
// assume signature was set on Signature header (most common behavior),
// but fall back to Authorization header if necessary
var signature string
if s := c.GetHeader(signatureHeader); s != "" {
signature = s
} else {
signature = c.GetHeader(authorizationHeader)
}
// set the verifier and signature on the context here to save some work further down the line
c.Set(string(ap.ContextRequestingPublicKeyVerifier), verifier)
c.Set(string(ap.ContextRequestingPublicKeySignature), signature)
}
}

View File

@@ -0,0 +1,140 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package middleware
import (
"net/http"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/oauth"
"github.com/superseriousbusiness/oauth2/v4"
)
// TokenCheck returns a new gin middleware for validating oauth tokens in requests.
//
// The middleware checks the request Authorization header for a valid oauth Bearer token.
//
// If no token was set in the Authorization header, or the token was invalid, the handler will return.
//
// If a valid oauth Bearer token was provided, it will be set on the gin context for further use.
//
// Then, it will check which *gtsmodel.User the token belongs to. If the user is not confirmed, not approved,
// or has been disabled, then the middleware will return early. Otherwise, the User will be set on the
// gin context for further processing by other functions.
//
// Next, it will look up the *gtsmodel.Account for the User. If the Account has been suspended, then the
// middleware will return early. Otherwise, it will set the Account on the gin context too.
//
// Finally, it will check the client ID of the token to see if a *gtsmodel.Application can be retrieved
// for that client ID. This will also be set on the gin context.
//
// If an invalid token is presented, or a user/account/application can't be found, then this middleware
// won't abort the request, since the server might want to still allow public requests that don't have a
// Bearer token set (eg., for public instance information and so on).
func TokenCheck(dbConn db.DB, validateBearerToken func(r *http.Request) (oauth2.TokenInfo, error)) func(*gin.Context) {
return func(c *gin.Context) {
ctx := c.Request.Context()
if c.Request.Header.Get("Authorization") == "" {
// no token set in the header, we can just bail
return
}
ti, err := validateBearerToken(c.Copy().Request)
if err != nil {
log.Debugf("token was passed in Authorization header but we could not validate it: %s", err)
return
}
c.Set(oauth.SessionAuthorizedToken, ti)
// check for user-level token
if userID := ti.GetUserID(); userID != "" {
log.Tracef("authenticated user %s with bearer token, scope is %s", userID, ti.GetScope())
// fetch user for this token
user, err := dbConn.GetUserByID(ctx, userID)
if err != nil {
if err != db.ErrNoEntries {
log.Errorf("database error looking for user with id %s: %s", userID, err)
return
}
log.Warnf("no user found for userID %s", userID)
return
}
if user.ConfirmedAt.IsZero() {
log.Warnf("authenticated user %s has never confirmed thier email address", userID)
return
}
if !*user.Approved {
log.Warnf("authenticated user %s's account was never approved by an admin", userID)
return
}
if *user.Disabled {
log.Warnf("authenticated user %s's account was disabled'", userID)
return
}
c.Set(oauth.SessionAuthorizedUser, user)
// fetch account for this token
if user.Account == nil {
acct, err := dbConn.GetAccountByID(ctx, user.AccountID)
if err != nil {
if err != db.ErrNoEntries {
log.Errorf("database error looking for account with id %s: %s", user.AccountID, err)
return
}
log.Warnf("no account found for userID %s", userID)
return
}
user.Account = acct
}
if !user.Account.SuspendedAt.IsZero() {
log.Warnf("authenticated user %s's account (accountId=%s) has been suspended", userID, user.AccountID)
return
}
c.Set(oauth.SessionAuthorizedAccount, user.Account)
}
// check for application token
if clientID := ti.GetClientID(); clientID != "" {
log.Tracef("authenticated client %s with bearer token, scope is %s", clientID, ti.GetScope())
// fetch app for this token
app := &gtsmodel.Application{}
if err := dbConn.GetWhere(ctx, []db.Where{{Key: "client_id", Value: clientID}}, app); err != nil {
if err != db.ErrNoEntries {
log.Errorf("database error looking for application with clientID %s: %s", clientID, err)
return
}
log.Warnf("no app found for client %s", clientID)
return
}
c.Set(oauth.SessionAuthorizedApplication, app)
}
}
}

View File

@@ -0,0 +1,39 @@
/*
GoToSocial
Copyright (C) 2021-2022 GoToSocial Authors admin@gotosocial.org
This program is free software: you can redistribute it and/or modify
it under the terms of the GNU Affero General Public License as published by
the Free Software Foundation, either version 3 of the License, or
(at your option) any later version.
This program is distributed in the hope that it will be useful,
but WITHOUT ANY WARRANTY; without even the implied warranty of
MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
GNU Affero General Public License for more details.
You should have received a copy of the GNU Affero General Public License
along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
package middleware
import (
"errors"
"net/http"
"github.com/gin-gonic/gin"
)
// UserAgent returns a gin middleware which aborts requests with
// empty user agent strings, returning code 418 - I'm a teapot.
func UserAgent() gin.HandlerFunc {
// todo: make this configurable
return func(c *gin.Context) {
if ua := c.Request.UserAgent(); ua == "" {
code := http.StatusTeapot
err := errors.New(http.StatusText(code) + ": no user-agent sent with request")
c.AbortWithStatusJSON(code, gin.H{"error": err.Error()})
}
}
}