diff --git a/cmd/gotosocial/action/server/server.go b/cmd/gotosocial/action/server/server.go index 7e07b76ab..7a4b315da 100644 --- a/cmd/gotosocial/action/server/server.go +++ b/cmd/gotosocial/action/server/server.go @@ -183,14 +183,21 @@ var Start action.GTSAction = func(ctx context.Context) error { 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 @@ var Start action.GTSAction = func(ctx context.Context) error { // 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 diff --git a/internal/api/activitypub.go b/internal/api/activitypub.go index 68c3b81e0..70b696834 100644 --- a/internal/api/activitypub.go +++ b/internal/api/activitypub.go @@ -22,6 +22,7 @@ import ( "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) diff --git a/internal/api/auth.go b/internal/api/auth.go index 472c6922a..48131dc3b 100644 --- a/internal/api/auth.go +++ b/internal/api/auth.go @@ -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) diff --git a/internal/api/client.go b/internal/api/client.go index 7736a99f4..0874664a4 100644 --- a/internal/api/client.go +++ b/internal/api/client.go @@ -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 ) diff --git a/internal/api/fileserver.go b/internal/api/fileserver.go index 8784a8663..83aceadd1 100644 --- a/internal/api/fileserver.go +++ b/internal/api/fileserver.go @@ -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 diff --git a/internal/api/nodeinfo.go b/internal/api/nodeinfo.go index 906703434..cfab289c8 100644 --- a/internal/api/nodeinfo.go +++ b/internal/api/nodeinfo.go @@ -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) diff --git a/internal/api/wellknown.go b/internal/api/wellknown.go index e4fe15f94..62532f292 100644 --- a/internal/api/wellknown.go +++ b/internal/api/wellknown.go @@ -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"), ) diff --git a/internal/middleware/gzip.go b/internal/middleware/gzip.go index ddea62b63..4523b4ea3 100644 --- a/internal/middleware/gzip.go +++ b/internal/middleware/gzip.go @@ -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) } diff --git a/internal/middleware/ratelimit.go b/internal/middleware/ratelimit.go index ab947a124..48f15eae5 100644 --- a/internal/middleware/ratelimit.go +++ b/internal/middleware/ratelimit.go @@ -24,7 +24,6 @@ import ( "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 @@ const rateLimitPeriod = 5 * time.Minute // // 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 } diff --git a/internal/web/web.go b/internal/web/web.go index f263c4655..f8d355ffe 100644 --- a/internal/web/web.go +++ b/internal/web/web.go @@ -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/"