[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

@@ -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)
}
func (r *router) AttachGlobalMiddleware(handlers ...gin.HandlerFunc) gin.IRoutes {
return r.engine.Use(handlers...)
}
// AttachMiddleware attaches a gin middleware to the router that will be used globally
func (r *router) AttachMiddleware(middleware gin.HandlerFunc) {
r.engine.Use(middleware)
}
// 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)
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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()
}

View File

@@ -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 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)
// The router's Attach functions should be used *before* the router is Started.
//
// 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)

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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
}

View File

@@ -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 <http://www.gnu.org/licenses/>.
*/
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{})
}

View File

@@ -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 */