diff options
Diffstat (limited to 'internal/router')
-rw-r--r-- | internal/router/attach.go | 23 | ||||
-rw-r--r-- | internal/router/cors.go | 87 | ||||
-rw-r--r-- | internal/router/gzip.go | 30 | ||||
-rw-r--r-- | internal/router/logger.go | 96 | ||||
-rw-r--r-- | internal/router/router.go | 67 | ||||
-rw-r--r-- | internal/router/session.go | 111 | ||||
-rw-r--r-- | internal/router/session_test.go | 95 | ||||
-rw-r--r-- | internal/router/template.go | 16 |
8 files changed, 43 insertions, 482 deletions
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 <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 -} 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 <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 -} 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 <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() -} 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 <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 -} 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 <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{}) -} 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 */ |