From 941893a774c83802afdc4cc76e1d30c59b6c5585 Mon Sep 17 00:00:00 2001
From: tobi <31960611+tsmethurst@users.noreply.github.com>
Date: Mon, 2 Jan 2023 13:10:50 +0100
Subject: [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
---
internal/router/attach.go | 23 +++------
internal/router/cors.go | 87 -------------------------------
internal/router/gzip.go | 30 -----------
internal/router/logger.go | 96 ----------------------------------
internal/router/router.go | 67 +++++++++++-------------
internal/router/session.go | 111 ----------------------------------------
internal/router/session_test.go | 95 ----------------------------------
internal/router/template.go | 16 +++---
8 files changed, 43 insertions(+), 482 deletions(-)
delete mode 100644 internal/router/cors.go
delete mode 100644 internal/router/gzip.go
delete mode 100644 internal/router/logger.go
delete mode 100644 internal/router/session.go
delete mode 100644 internal/router/session_test.go
(limited to 'internal/router')
diff --git a/internal/router/attach.go b/internal/router/attach.go
index 7c20b33d8..c65d88717 100644
--- a/internal/router/attach.go
+++ b/internal/router/attach.go
@@ -20,29 +20,18 @@ package router
import "github.com/gin-gonic/gin"
-// AttachHandler attaches the given gin.HandlerFunc to the router with the specified method and path.
-// If the path is set to ANY, then the handlerfunc will be used for ALL methods at its given path.
-func (r *router) AttachHandler(method string, path string, handler gin.HandlerFunc) {
- if method == "ANY" {
- r.engine.Any(path, handler)
- } else {
- r.engine.Handle(method, path, handler)
- }
-}
-
-// AttachMiddleware attaches a gin middleware to the router that will be used globally
-func (r *router) AttachMiddleware(middleware gin.HandlerFunc) {
- r.engine.Use(middleware)
+func (r *router) AttachGlobalMiddleware(handlers ...gin.HandlerFunc) gin.IRoutes {
+ return r.engine.Use(handlers...)
}
-// AttachNoRouteHandler attaches a gin.HandlerFunc to NoRoute to handle 404's
func (r *router) AttachNoRouteHandler(handler gin.HandlerFunc) {
r.engine.NoRoute(handler)
}
-// AttachGroup attaches the given handlers into a group with the given relativePath as
-// base path for that group. It then returns the *gin.RouterGroup so that the caller
-// can add any extra middlewares etc specific to that group, as desired.
func (r *router) AttachGroup(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup {
return r.engine.Group(relativePath, handlers...)
}
+
+func (r *router) AttachHandler(method string, path string, handler gin.HandlerFunc) {
+ r.engine.Handle(method, path, handler)
+}
diff --git a/internal/router/cors.go b/internal/router/cors.go
deleted file mode 100644
index c8ef040d8..000000000
--- a/internal/router/cors.go
+++ /dev/null
@@ -1,87 +0,0 @@
-/*
- 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 .
-*/
-
-package router
-
-import (
- "time"
-
- "github.com/gin-contrib/cors"
- "github.com/gin-gonic/gin"
-)
-
-var corsConfig = cors.Config{
- // TODO: make this customizable so instance admins can specify an origin for CORS requests
- 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,
-}
-
-// useCors attaches the corsConfig above to the given gin engine
-func useCors(engine *gin.Engine) error {
- c := cors.New(corsConfig)
- engine.Use(c)
- return nil
-}
diff --git a/internal/router/gzip.go b/internal/router/gzip.go
deleted file mode 100644
index 4c5c99eee..000000000
--- a/internal/router/gzip.go
+++ /dev/null
@@ -1,30 +0,0 @@
-/*
- 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 .
-*/
-
-package router
-
-import (
- ginGzip "github.com/gin-contrib/gzip"
- "github.com/gin-gonic/gin"
-)
-
-func useGzip(engine *gin.Engine) error {
- gzipMiddleware := ginGzip.Gzip(ginGzip.DefaultCompression)
- engine.Use(gzipMiddleware)
- return nil
-}
diff --git a/internal/router/logger.go b/internal/router/logger.go
deleted file mode 100644
index 6eb271a84..000000000
--- a/internal/router/logger.go
+++ /dev/null
@@ -1,96 +0,0 @@
-/*
- 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 .
-*/
-
-package router
-
-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"
-)
-
-// loggingMiddleware provides a request logging and panic recovery gin handler.
-func loggingMiddleware(c *gin.Context) {
- // Initialize the logging fields
- fields := make(kv.Fields, 6, 7)
-
- // Determine pre-handler time
- before := time.Now()
-
- 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()
-}
diff --git a/internal/router/router.go b/internal/router/router.go
index d66e5f9cd..dd3ff61d7 100644
--- a/internal/router/router.go
+++ b/internal/router/router.go
@@ -25,34 +25,39 @@ import (
"net/http"
"time"
+ "codeberg.org/gruf/go-bytesize"
"codeberg.org/gruf/go-debug"
"github.com/gin-gonic/gin"
"github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/db"
"github.com/superseriousbusiness/gotosocial/internal/log"
"golang.org/x/crypto/acme/autocert"
)
const (
- readTimeout = 60 * time.Second
- writeTimeout = 30 * time.Second
- idleTimeout = 30 * time.Second
- readHeaderTimeout = 30 * time.Second
- shutdownTimeout = 30 * time.Second
+ readTimeout = 60 * time.Second
+ writeTimeout = 30 * time.Second
+ idleTimeout = 30 * time.Second
+ readHeaderTimeout = 30 * time.Second
+ shutdownTimeout = 30 * time.Second
+ maxMultipartMemory = int64(8 * bytesize.MiB)
)
// Router provides the REST interface for gotosocial, using gin.
type Router interface {
- // Attach a gin handler to the router with the given method and path
- AttachHandler(method string, path string, f gin.HandlerFunc)
- // Attach a gin middleware to the router that will be used globally
- AttachMiddleware(handler gin.HandlerFunc)
+ // Attach global gin middlewares to this router.
+ AttachGlobalMiddleware(handlers ...gin.HandlerFunc) gin.IRoutes
+ // AttachGroup attaches the given handlers into a group with the given relativePath as
+ // base path for that group. It then returns the *gin.RouterGroup so that the caller
+ // can add any extra middlewares etc specific to that group, as desired.
+ AttachGroup(relativePath string, handlers ...gin.HandlerFunc) *gin.RouterGroup
+ // Attach a single gin handler to the router with the given method and path.
+ // To make middleware management easier, AttachGroup should be preferred where possible.
+ // However, this function can be used for attaching single handlers that only require
+ // global middlewares.
+ AttachHandler(method string, path string, handler gin.HandlerFunc)
+
// Attach 404 NoRoute handler
AttachNoRouteHandler(handler gin.HandlerFunc)
- // Attach a router group, and receive that group back.
- // More middlewares and handlers can then be attached on
- // the group by the caller.
- AttachGroup(path string, handlers ...gin.HandlerFunc) *gin.RouterGroup
// Start the router
Start()
// Stop the router
@@ -142,19 +147,20 @@ func (r *router) Stop(ctx context.Context) error {
return nil
}
-// New returns a new Router with the specified configuration.
+// New returns a new Router.
+//
+// The router's Attach functions should be used *before* the router is Started.
//
-// The given DB is only used in the New function for parsing config values, and is not otherwise
-// pinned to the router.
-func New(ctx context.Context, db db.DB) (Router, error) {
- gin.SetMode(gin.ReleaseMode)
+// When the router's work is finished, Stop should be called on it to close connections gracefully.
+//
+// The provided context will be used as the base context for all requests passing
+// through the underlying http.Server, so this should be a long-running context.
+func New(ctx context.Context) (Router, error) {
+ gin.SetMode(gin.TestMode)
// create the actual engine here -- this is the core request routing handler for gts
engine := gin.New()
- engine.Use(loggingMiddleware)
-
- // 8 MiB
- engine.MaxMultipartMemory = 8 << 20
+ engine.MaxMultipartMemory = maxMultipartMemory
// set up IP forwarding via x-forward-* headers.
trustedProxies := config.GetTrustedProxies()
@@ -162,21 +168,6 @@ func New(ctx context.Context, db db.DB) (Router, error) {
return nil, err
}
- // enable cors on the engine
- if err := useCors(engine); err != nil {
- return nil, err
- }
-
- // enable gzip compression on the engine
- if err := useGzip(engine); err != nil {
- return nil, err
- }
-
- // enable session store middleware on the engine
- if err := useSession(ctx, db, engine); err != nil {
- return nil, err
- }
-
// set template functions
LoadTemplateFunctions(engine)
diff --git a/internal/router/session.go b/internal/router/session.go
deleted file mode 100644
index eb4b83874..000000000
--- a/internal/router/session.go
+++ /dev/null
@@ -1,111 +0,0 @@
-/*
- 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 .
-*/
-
-package router
-
-import (
- "context"
- "errors"
- "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/db"
- "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
-}
-
-func useSession(ctx context.Context, sessionDB db.Session, engine *gin.Engine) error {
- // check if we have a saved router session already
- rs, err := sessionDB.GetSession(ctx)
- if err != nil {
- return fmt.Errorf("error using session: %s", err)
- }
- if rs == nil || rs.Auth == nil || rs.Crypt == nil {
- return errors.New("router session was nil")
- }
-
- store := memstore.NewStore(rs.Auth, rs.Crypt)
- store.Options(SessionOptions())
-
- sessionName, err := SessionName()
- if err != nil {
- return err
- }
-
- engine.Use(sessions.Sessions(sessionName, store))
- return nil
-}
diff --git a/internal/router/session_test.go b/internal/router/session_test.go
deleted file mode 100644
index 5f4777a48..000000000
--- a/internal/router/session_test.go
+++ /dev/null
@@ -1,95 +0,0 @@
-/*
- 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 .
-*/
-
-package router_test
-
-import (
- "testing"
-
- "github.com/stretchr/testify/suite"
- "github.com/superseriousbusiness/gotosocial/internal/config"
- "github.com/superseriousbusiness/gotosocial/internal/router"
- "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 := router.SessionName()
- suite.NoError(err)
- suite.Equal("gotosocial-localhost", sessionName)
-}
-
-func (suite *SessionTestSuite) TestDeriveSessionNameLocalhost() {
- config.SetProtocol("http")
- config.SetHost("localhost")
-
- sessionName, err := router.SessionName()
- suite.NoError(err)
- suite.Equal("gotosocial-localhost", sessionName)
-}
-
-func (suite *SessionTestSuite) TestDeriveSessionNoProtocol() {
- config.SetProtocol("")
- config.SetHost("localhost")
-
- sessionName, err := router.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 := router.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 := router.SessionName()
- suite.NoError(err)
- suite.Equal("gotosocial-example.org", sessionName)
-}
-
-func (suite *SessionTestSuite) TestDeriveSessionIDNOK() {
- config.SetProtocol("https")
- config.SetHost("fóid.org")
-
- sessionName, err := router.SessionName()
- suite.NoError(err)
- suite.Equal("gotosocial-xn--fid-gna.org", sessionName)
-}
-
-func TestSessionTestSuite(t *testing.T) {
- suite.Run(t, &SessionTestSuite{})
-}
diff --git a/internal/router/template.go b/internal/router/template.go
index e87f6b69e..e2deb9ca8 100644
--- a/internal/router/template.go
+++ b/internal/router/template.go
@@ -26,7 +26,7 @@ import (
"time"
"github.com/gin-gonic/gin"
- "github.com/superseriousbusiness/gotosocial/internal/api/model"
+ apimodel "github.com/superseriousbusiness/gotosocial/internal/api/model"
"github.com/superseriousbusiness/gotosocial/internal/config"
"github.com/superseriousbusiness/gotosocial/internal/log"
"github.com/superseriousbusiness/gotosocial/internal/text"
@@ -130,19 +130,19 @@ type iconWithLabel struct {
label string
}
-func visibilityIcon(visibility model.Visibility) template.HTML {
+func visibilityIcon(visibility apimodel.Visibility) template.HTML {
var icon iconWithLabel
switch visibility {
- case model.VisibilityPublic:
+ case apimodel.VisibilityPublic:
icon = iconWithLabel{"globe", "public"}
- case model.VisibilityUnlisted:
+ case apimodel.VisibilityUnlisted:
icon = iconWithLabel{"unlock", "unlisted"}
- case model.VisibilityPrivate:
+ case apimodel.VisibilityPrivate:
icon = iconWithLabel{"lock", "private"}
- case model.VisibilityMutualsOnly:
+ case apimodel.VisibilityMutualsOnly:
icon = iconWithLabel{"handshake-o", "mutuals only"}
- case model.VisibilityDirect:
+ case apimodel.VisibilityDirect:
icon = iconWithLabel{"envelope", "direct"}
}
@@ -151,7 +151,7 @@ func visibilityIcon(visibility model.Visibility) template.HTML {
}
// text is a template.HTML to affirm that the input of this function is already escaped
-func emojify(emojis []model.Emoji, inputText template.HTML) template.HTML {
+func emojify(emojis []apimodel.Emoji, inputText template.HTML) template.HTML {
out := text.Emojify(emojis, string(inputText))
/* #nosec G203 */
--
cgit v1.2.3