[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 @@
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
authModule.Route(router)
clientModule.Route(router)
fileserverModule.Route(router)
wellKnownModule.Route(router)
nodeInfoModule.Route(router)
activityPubModule.Route(router)
webModule.Route(router)
authModule.Route(router, clLimit, gzip)
clientModule.Route(router, clLimit, gzip)
fileserverModule.Route(router, fsLimit)
wellKnownModule.Route(router, gzip, s2sLimit)
nodeInfoModule.Route(router, s2sLimit, gzip)
activityPubModule.Route(router, s2sLimit, gzip)
webModule.Route(router, fsLimit, gzip)
gts, err := gotosocial.NewServer(dbService, router, federator, mediaManager)
if err != nil {
@ -208,8 +215,8 @@
// catch shutdown signals from the operating system
sigs := make(chan os.Signal, 1)
signal.Notify(sigs, os.Interrupt, syscall.SIGTERM)
sig := <-sigs
signal.Notify(sigs, syscall.SIGINT, syscall.SIGTERM)
sig := <-sigs // block until signal received
log.Infof("received signal %s, shutting down", sig)
// close down all running services in order

View file

@ -22,6 +22,7 @@
"context"
"net/url"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/emoji"
"github.com/superseriousbusiness/gotosocial/internal/api/activitypub/users"
"github.com/superseriousbusiness/gotosocial/internal/db"
@ -37,20 +38,20 @@ type ActivityPub struct {
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
emojiGroup := r.AttachGroup("emoji")
usersGroup := r.AttachGroup("users")
// instantiate + attach shared, non-global middlewares to both of these groups
var (
rateLimitMiddleware = middleware.RateLimit() // nolint:contextcheck
signatureCheckMiddleware = middleware.SignatureCheck(a.isURIBlocked)
gzipMiddleware = middleware.Gzip()
cacheControlMiddleware = middleware.CacheControl("no-store")
)
emojiGroup.Use(rateLimitMiddleware, signatureCheckMiddleware, gzipMiddleware, cacheControlMiddleware)
usersGroup.Use(rateLimitMiddleware, signatureCheckMiddleware, gzipMiddleware, cacheControlMiddleware)
emojiGroup.Use(m...)
usersGroup.Use(m...)
emojiGroup.Use(signatureCheckMiddleware, cacheControlMiddleware)
usersGroup.Use(signatureCheckMiddleware, cacheControlMiddleware)
a.emoji.Route(emojiGroup.Handle)
a.users.Route(usersGroup.Handle)

View file

@ -19,6 +19,7 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api/auth"
"github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
@ -36,20 +37,20 @@ type Auth struct {
}
// 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
authGroup := r.AttachGroup("auth")
oauthGroup := r.AttachGroup("oauth")
// instantiate + attach shared, non-global middlewares to both of these groups
var (
rateLimitMiddleware = middleware.RateLimit() // nolint:contextcheck
gzipMiddleware = middleware.Gzip()
cacheControlMiddleware = middleware.CacheControl("private", "max-age=120")
sessionMiddleware = middleware.Session(a.sessionName, a.routerSession.Auth, a.routerSession.Crypt)
)
authGroup.Use(rateLimitMiddleware, gzipMiddleware, cacheControlMiddleware, sessionMiddleware)
oauthGroup.Use(rateLimitMiddleware, gzipMiddleware, cacheControlMiddleware, sessionMiddleware)
authGroup.Use(m...)
oauthGroup.Use(m...)
authGroup.Use(cacheControlMiddleware, sessionMiddleware)
oauthGroup.Use(cacheControlMiddleware, sessionMiddleware)
a.auth.RouteAuth(authGroup.Handle)
a.auth.RouteOauth(oauthGroup.Handle)

View file

@ -19,6 +19,7 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api/client/accounts"
"github.com/superseriousbusiness/gotosocial/internal/api/client/admin"
"github.com/superseriousbusiness/gotosocial/internal/api/client/apps"
@ -67,15 +68,14 @@ type Client struct {
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
apiGroup := r.AttachGroup("api")
// attach non-global middlewares appropriate to the client api
apiGroup.Use(m...)
apiGroup.Use(
middleware.TokenCheck(c.db, c.processor.OAuthValidateBearerToken),
middleware.RateLimit(),
middleware.Gzip(),
middleware.CacheControl("no-store"), // never cache api responses
)

View file

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

View file

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

View file

@ -19,6 +19,7 @@
package api
import (
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/nodeinfo"
"github.com/superseriousbusiness/gotosocial/internal/api/wellknown/webfinger"
"github.com/superseriousbusiness/gotosocial/internal/middleware"
@ -31,14 +32,13 @@ type WellKnown struct {
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
wellKnownGroup := r.AttachGroup(".well-known")
// attach middlewares appropriate for this group
wellKnownGroup.Use(m...)
wellKnownGroup.Use(
middleware.Gzip(),
middleware.RateLimit(),
// allow .well-known responses to be cached for 2 minutes
middleware.CacheControl("public", "max-age=120"),
)

View file

@ -19,12 +19,18 @@
package middleware
import (
ginGzip "github.com/gin-contrib/gzip"
"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)
const enabled = true
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 @@
"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"
@ -44,34 +43,25 @@
//
// 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 {
func RateLimit(limit int) gin.HandlerFunc {
if limit <= 0 {
// use noop middleware if ratelimiting is disabled
return func(c *gin.Context) {}
return func(ctx *gin.Context) {}
}
rate := limiter.Rate{
Period: rateLimitPeriod,
Limit: int64(rateLimitRequests),
}
limiterInstance := limiter.New(
limiter := limiter.New(
memory.NewStore(),
rate,
limiter.Rate{Period: rateLimitPeriod, Limit: int64(limit)},
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"})
}
middleware := limitergin.NewMiddleware(
limiterInstance,
limitergin.WithLimitReachedHandler(limitReachedHandler), // use custom rate limit reached error
return limitergin.NewMiddleware(
limiter,
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
assetsGroup := r.AttachGroup(assetsPathPrefix)
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
assetsGroup.Use(m.assetsCacheControlMiddleware(fs))
assetsGroup.Use(mi...)
// serve static file system in the root of this group,
// will end up being something like "/assets/"