[chore] shuffle middleware to split rate limitting into client/s2s/fileserver, share gzip middleware globally (#1290)

Signed-off-by: kim <grufwub@gmail.com>

Signed-off-by: kim <grufwub@gmail.com>
This commit is contained in:
kim 2023-01-03 10:50:59 +00:00 committed by GitHub
parent 9ecb1c8aa5
commit 71dfea7e47
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
10 changed files with 63 additions and 55 deletions

View file

@ -183,14 +183,21 @@ var Start action.GTSAction = func(ctx context.Context) error {
webModule = web.New(processor) // web pages + user profiles + settings panels etc webModule = web.New(processor) // web pages + user profiles + settings panels etc
) )
// create required middleware
limit := config.GetAdvancedRateLimitRequests()
gzip := middleware.Gzip() // all except fileserver
clLimit := middleware.RateLimit(limit) // client api
s2sLimit := middleware.RateLimit(limit) // server-to-server (AP)
fsLimit := middleware.RateLimit(limit) // fileserver / web templates
// these should be routed in order // these should be routed in order
authModule.Route(router) authModule.Route(router, clLimit, gzip)
clientModule.Route(router) clientModule.Route(router, clLimit, gzip)
fileserverModule.Route(router) fileserverModule.Route(router, fsLimit)
wellKnownModule.Route(router) wellKnownModule.Route(router, gzip, s2sLimit)
nodeInfoModule.Route(router) nodeInfoModule.Route(router, s2sLimit, gzip)
activityPubModule.Route(router) activityPubModule.Route(router, s2sLimit, gzip)
webModule.Route(router) webModule.Route(router, fsLimit, gzip)
gts, err := gotosocial.NewServer(dbService, router, federator, mediaManager) gts, err := gotosocial.NewServer(dbService, router, federator, mediaManager)
if err != nil { if err != nil {
@ -208,8 +215,8 @@ var Start action.GTSAction = func(ctx context.Context) error {
// catch shutdown signals from the operating system // catch shutdown signals from the operating system
sigs := make(chan os.Signal, 1) sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM) signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigs sig := <-sigs // block until signal received
log.Infof("received signal %s, shutting down", sig) log.Infof("received signal %s, shutting down", sig)
// close down all running services in order // close down all running services in order

View file

@ -22,6 +22,7 @@ import (
"context" "context"
"net/url" "net/url"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/emoji" "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/emoji"
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users" "github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
@ -37,20 +38,20 @@ type ActivityPub struct {
isURIBlocked func(context.Context, *url.URL) (bool, db.Error) isURIBlocked func(context.Context, *url.URL) (bool, db.Error)
} }
func (a *ActivityPub) Route(r router.Router) { func (a *ActivityPub) Route(r router.Router, m ...gin.HandlerFunc) {
// create groupings for the 'emoji' and 'users' prefixes // create groupings for the 'emoji' and 'users' prefixes
emojiGroup := r.AttachGroup("emoji") emojiGroup := r.AttachGroup("emoji")
usersGroup := r.AttachGroup("users") usersGroup := r.AttachGroup("users")
// instantiate + attach shared, non-global middlewares to both of these groups // instantiate + attach shared, non-global middlewares to both of these groups
var ( var (
rateLimitMiddleware = middleware.RateLimit() // nolint:contextcheck
signatureCheckMiddleware = middleware.SignatureCheck(a.isURIBlocked) signatureCheckMiddleware = middleware.SignatureCheck(a.isURIBlocked)
gzipMiddleware = middleware.Gzip()
cacheControlMiddleware = middleware.CacheControl("no-store") cacheControlMiddleware = middleware.CacheControl("no-store")
) )
emojiGroup.Use(rateLimitMiddleware, signatureCheckMiddleware, gzipMiddleware, cacheControlMiddleware) emojiGroup.Use(m...)
usersGroup.Use(rateLimitMiddleware, signatureCheckMiddleware, gzipMiddleware, cacheControlMiddleware) usersGroup.Use(m...)
emojiGroup.Use(signatureCheckMiddleware, cacheControlMiddleware)
usersGroup.Use(signatureCheckMiddleware, cacheControlMiddleware)
a.emoji.Route(emojiGroup.Handle) a.emoji.Route(emojiGroup.Handle)
a.users.Route(usersGroup.Handle) a.users.Route(usersGroup.Handle)

View file

@ -19,6 +19,7 @@
package api package api
import ( import (
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api/auth" "github.com/superseriousbusiness/gotosocial/internal/api/auth"
"github.com/superseriousbusiness/gotosocial/internal/db" "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel" "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
@ -36,20 +37,20 @@ type Auth struct {
} }
// Route attaches 'auth' and 'oauth' groups to the given router. // Route attaches 'auth' and 'oauth' groups to the given router.
func (a *Auth) Route(r router.Router) { func (a *Auth) Route(r router.Router, m ...gin.HandlerFunc) {
// create groupings for the 'auth' and 'oauth' prefixes // create groupings for the 'auth' and 'oauth' prefixes
authGroup := r.AttachGroup("auth") authGroup := r.AttachGroup("auth")
oauthGroup := r.AttachGroup("oauth") oauthGroup := r.AttachGroup("oauth")
// instantiate + attach shared, non-global middlewares to both of these groups // instantiate + attach shared, non-global middlewares to both of these groups
var ( var (
rateLimitMiddleware = middleware.RateLimit() // nolint:contextcheck
gzipMiddleware = middleware.Gzip()
cacheControlMiddleware = middleware.CacheControl("private", "max-age=120") cacheControlMiddleware = middleware.CacheControl("private", "max-age=120")
sessionMiddleware = middleware.Session(a.sessionName, a.routerSession.Auth, a.routerSession.Crypt) sessionMiddleware = middleware.Session(a.sessionName, a.routerSession.Auth, a.routerSession.Crypt)
) )
authGroup.Use(rateLimitMiddleware, gzipMiddleware, cacheControlMiddleware, sessionMiddleware) authGroup.Use(m...)
oauthGroup.Use(rateLimitMiddleware, gzipMiddleware, cacheControlMiddleware, sessionMiddleware) oauthGroup.Use(m...)
authGroup.Use(cacheControlMiddleware, sessionMiddleware)
oauthGroup.Use(cacheControlMiddleware, sessionMiddleware)
a.auth.RouteAuth(authGroup.Handle) a.auth.RouteAuth(authGroup.Handle)
a.auth.RouteOauth(oauthGroup.Handle) a.auth.RouteOauth(oauthGroup.Handle)

View file

@ -19,6 +19,7 @@
package api package api
import ( import (
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api/client/accounts" "github.com/superseriousbusiness/gotosocial/internal/api/client/accounts"
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin" "github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
"github.com/superseriousbusiness/gotosocial/internal/api/client/apps" "github.com/superseriousbusiness/gotosocial/internal/api/client/apps"
@ -67,15 +68,14 @@ type Client struct {
user *user.Module // api/v1/user user *user.Module // api/v1/user
} }
func (c *Client) Route(r router.Router) { func (c *Client) Route(r router.Router, m ...gin.HandlerFunc) {
// create a new group on the top level client 'api' prefix // create a new group on the top level client 'api' prefix
apiGroup := r.AttachGroup("api") apiGroup := r.AttachGroup("api")
// attach non-global middlewares appropriate to the client api // attach non-global middlewares appropriate to the client api
apiGroup.Use(m...)
apiGroup.Use( apiGroup.Use(
middleware.TokenCheck(c.db, c.processor.OAuthValidateBearerToken), middleware.TokenCheck(c.db, c.processor.OAuthValidateBearerToken),
middleware.RateLimit(),
middleware.Gzip(),
middleware.CacheControl("no-store"), // never cache api responses middleware.CacheControl("no-store"), // never cache api responses
) )

View file

@ -19,6 +19,7 @@
package api package api
import ( import (
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api/fileserver" "github.com/superseriousbusiness/gotosocial/internal/api/fileserver"
"github.com/superseriousbusiness/gotosocial/internal/middleware" "github.com/superseriousbusiness/gotosocial/internal/middleware"
"github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/internal/processing"
@ -29,12 +30,12 @@ type Fileserver struct {
fileserver *fileserver.Module fileserver *fileserver.Module
} }
func (f *Fileserver) Route(r router.Router) { func (f *Fileserver) Route(r router.Router, m ...gin.HandlerFunc) {
fileserverGroup := r.AttachGroup("fileserver") fileserverGroup := r.AttachGroup("fileserver")
// attach middlewares appropriate for this group // attach middlewares appropriate for this group
fileserverGroup.Use(m...)
fileserverGroup.Use( fileserverGroup.Use(
middleware.RateLimit(),
// Since we'll never host different files at the same // Since we'll never host different files at the same
// URL (bc the ULIDs are generated per piece of media), // URL (bc the ULIDs are generated per piece of media),
// it's sensible and safe to use a long cache here, so // it's sensible and safe to use a long cache here, so

View file

@ -19,6 +19,7 @@
package api package api
import ( import (
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api/nodeinfo" "github.com/superseriousbusiness/gotosocial/internal/api/nodeinfo"
"github.com/superseriousbusiness/gotosocial/internal/middleware" "github.com/superseriousbusiness/gotosocial/internal/middleware"
"github.com/superseriousbusiness/gotosocial/internal/processing" "github.com/superseriousbusiness/gotosocial/internal/processing"
@ -29,15 +30,15 @@ type NodeInfo struct {
nodeInfo *nodeinfo.Module nodeInfo *nodeinfo.Module
} }
func (w *NodeInfo) Route(r router.Router) { func (w *NodeInfo) Route(r router.Router, m ...gin.HandlerFunc) {
// group nodeinfo endpoints together // group nodeinfo endpoints together
nodeInfoGroup := r.AttachGroup("nodeinfo") nodeInfoGroup := r.AttachGroup("nodeinfo")
// attach middlewares appropriate for this group // attach middlewares appropriate for this group
nodeInfoGroup.Use(m...)
nodeInfoGroup.Use( nodeInfoGroup.Use(
middleware.Gzip(), // allow cache for 2 minutes
middleware.RateLimit(), middleware.CacheControl("public", "max-age=120"),
middleware.CacheControl("public", "max-age=120"), // allow cache for 2 minutes
) )
w.nodeInfo.Route(nodeInfoGroup.Handle) w.nodeInfo.Route(nodeInfoGroup.Handle)

View file

@ -19,6 +19,7 @@
package api package api
import ( import (
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/nodeinfo" "github.com/superseriousbusiness/gotosocial/internal/api/wellknown/nodeinfo"
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger" "github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"
"github.com/superseriousbusiness/gotosocial/internal/middleware" "github.com/superseriousbusiness/gotosocial/internal/middleware"
@ -31,14 +32,13 @@ type WellKnown struct {
webfinger *webfinger.Module webfinger *webfinger.Module
} }
func (w *WellKnown) Route(r router.Router) { func (w *WellKnown) Route(r router.Router, m ...gin.HandlerFunc) {
// group .well-known endpoints together // group .well-known endpoints together
wellKnownGroup := r.AttachGroup(".well-known") wellKnownGroup := r.AttachGroup(".well-known")
// attach middlewares appropriate for this group // attach middlewares appropriate for this group
wellKnownGroup.Use(m...)
wellKnownGroup.Use( wellKnownGroup.Use(
middleware.Gzip(),
middleware.RateLimit(),
// allow .well-known responses to be cached for 2 minutes // allow .well-known responses to be cached for 2 minutes
middleware.CacheControl("public", "max-age=120"), middleware.CacheControl("public", "max-age=120"),
) )

View file

@ -19,12 +19,18 @@
package middleware package middleware
import ( import (
ginGzip "github.com/gin-contrib/gzip" "github.com/gin-contrib/gzip"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
) )
// Gzip returns a gzip gin middleware using default compression. // Gzip returns a gzip gin middleware using default compression.
func Gzip() gin.HandlerFunc { func Gzip() gin.HandlerFunc {
// todo: make this configurable const enabled = true
return ginGzip.Gzip(ginGzip.DefaultCompression)
if !enabled {
// use noop middleware if gzip is disabled
return func(ctx *gin.Context) {}
}
return gzip.Gzip(gzip.DefaultCompression)
} }

View file

@ -24,7 +24,6 @@ import (
"time" "time"
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/ulule/limiter/v3" "github.com/ulule/limiter/v3"
limitergin "github.com/ulule/limiter/v3/drivers/middleware/gin" limitergin "github.com/ulule/limiter/v3/drivers/middleware/gin"
"github.com/ulule/limiter/v3/drivers/store/memory" "github.com/ulule/limiter/v3/drivers/store/memory"
@ -44,34 +43,25 @@ const rateLimitPeriod = 5 * time.Minute
// //
// If the config AdvancedRateLimitRequests value is <= 0, then a noop handler will be returned, // If the config AdvancedRateLimitRequests value is <= 0, then a noop handler will be returned,
// which performs no rate limiting. // which performs no rate limiting.
func RateLimit() gin.HandlerFunc { func RateLimit(limit int) gin.HandlerFunc {
// only enable rate limit middleware if configured if limit <= 0 {
// advanced-rate-limit-requests is greater than 0
rateLimitRequests := config.GetAdvancedRateLimitRequests()
if rateLimitRequests <= 0 {
// use noop middleware if ratelimiting is disabled // use noop middleware if ratelimiting is disabled
return func(c *gin.Context) {} return func(ctx *gin.Context) {}
} }
rate := limiter.Rate{ limiter := limiter.New(
Period: rateLimitPeriod,
Limit: int64(rateLimitRequests),
}
limiterInstance := limiter.New(
memory.NewStore(), memory.NewStore(),
rate, limiter.Rate{Period: rateLimitPeriod, Limit: int64(limit)},
limiter.WithIPv6Mask(net.CIDRMask(64, 128)), // apply /64 mask to IPv6 addresses limiter.WithIPv6Mask(net.CIDRMask(64, 128)), // apply /64 mask to IPv6 addresses
) )
limitReachedHandler := func(c *gin.Context) { // use custom rate limit reached error
handler := func(c *gin.Context) {
c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit reached"}) c.AbortWithStatusJSON(http.StatusTooManyRequests, gin.H{"error": "rate limit reached"})
} }
middleware := limitergin.NewMiddleware( return limitergin.NewMiddleware(
limiterInstance, limiter,
limitergin.WithLimitReachedHandler(limitReachedHandler), // use custom rate limit reached error limitergin.WithLimitReachedHandler(handler),
) )
return middleware
} }

View file

@ -68,7 +68,7 @@ func New(processor processing.Processor) *Module {
} }
} }
func (m *Module) Route(r router.Router) { func (m *Module) Route(r router.Router, mi ...gin.HandlerFunc) {
// serve static files from assets dir at /assets // serve static files from assets dir at /assets
assetsGroup := r.AttachGroup(assetsPathPrefix) assetsGroup := r.AttachGroup(assetsPathPrefix)
webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir()) webAssetsAbsFilePath, err := filepath.Abs(config.GetWebAssetBaseDir())
@ -80,6 +80,7 @@ func (m *Module) Route(r router.Router) {
// use the cache middleware on all handlers in this group // use the cache middleware on all handlers in this group
assetsGroup.Use(m.assetsCacheControlMiddleware(fs)) assetsGroup.Use(m.assetsCacheControlMiddleware(fs))
assetsGroup.Use(mi...)
// serve static file system in the root of this group, // serve static file system in the root of this group,
// will end up being something like "/assets/" // will end up being something like "/assets/"