summaryrefslogtreecommitdiff
path: root/internal/middleware
diff options
context:
space:
mode:
Diffstat (limited to 'internal/middleware')
-rw-r--r--internal/middleware/cachecontrol.go35
-rw-r--r--internal/middleware/cors.go86
-rw-r--r--internal/middleware/extraheaders.go37
-rw-r--r--internal/middleware/gzip.go30
-rw-r--r--internal/middleware/logger.go99
-rw-r--r--internal/middleware/ratelimit.go77
-rw-r--r--internal/middleware/session.go95
-rw-r--r--internal/middleware/session_test.go95
-rw-r--r--internal/middleware/signaturecheck.go93
-rw-r--r--internal/middleware/tokencheck.go140
-rw-r--r--internal/middleware/useragent.go39
11 files changed, 826 insertions, 0 deletions
diff --git a/internal/middleware/cachecontrol.go b/internal/middleware/cachecontrol.go
new file mode 100644
index 000000000..6e88daf1a
--- /dev/null
+++ b/internal/middleware/cachecontrol.go
@@ -0,0 +1,35 @@
+/*
+ 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 middleware
+
+import (
+ "strings"
+
+ "github.com/gin-gonic/gin"
+)
+
+// CacheControl returns a new gin middleware which allows callers to control cache settings on response headers.
+//
+// For directives, see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control
+func CacheControl(directives ...string) gin.HandlerFunc {
+ ccHeader := strings.Join(directives, ", ")
+ return func(c *gin.Context) {
+ c.Header("Cache-Control", ccHeader)
+ }
+}
diff --git a/internal/middleware/cors.go b/internal/middleware/cors.go
new file mode 100644
index 000000000..79b72185f
--- /dev/null
+++ b/internal/middleware/cors.go
@@ -0,0 +1,86 @@
+/*
+ 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 middleware
+
+import (
+ "time"
+
+ "github.com/gin-contrib/cors"
+ "github.com/gin-gonic/gin"
+)
+
+// CORS returns a new gin middleware which allows CORS requests to be processed.
+// This is necessary in order for web/browser-based clients like Pinafore to work.
+func CORS() gin.HandlerFunc {
+ cfg := cors.Config{
+ // todo: use config to customize this
+ 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,
+ }
+
+ return cors.New(cfg)
+}
diff --git a/internal/middleware/extraheaders.go b/internal/middleware/extraheaders.go
new file mode 100644
index 000000000..c7d4dd3e2
--- /dev/null
+++ b/internal/middleware/extraheaders.go
@@ -0,0 +1,37 @@
+/*
+ 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 middleware
+
+import "github.com/gin-gonic/gin"
+
+// ExtraHeaders returns a new gin middleware which adds various extra headers to the response.
+func ExtraHeaders() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // Inform all callers which server implementation this is.
+ c.Header("Server", "gotosocial")
+ // Prevent google chrome cohort tracking. Originally this was referred
+ // to as FlocBlock. Floc was replaced by Topics in 2022 and the spec says
+ // that interest-cohort will also block Topics (as of 2022-Nov).
+ //
+ // See: https://smartframe.io/blog/google-topics-api-everything-you-need-to-know
+ //
+ // See: https://github.com/patcg-individual-drafts/topics
+ c.Header("Permissions-Policy", "browsing-topics=()")
+ }
+}
diff --git a/internal/middleware/gzip.go b/internal/middleware/gzip.go
new file mode 100644
index 000000000..ddea62b63
--- /dev/null
+++ b/internal/middleware/gzip.go
@@ -0,0 +1,30 @@
+/*
+ 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 middleware
+
+import (
+ ginGzip "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)
+}
diff --git a/internal/middleware/logger.go b/internal/middleware/logger.go
new file mode 100644
index 000000000..7474b0fe0
--- /dev/null
+++ b/internal/middleware/logger.go
@@ -0,0 +1,99 @@
+/*
+ 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 middleware
+
+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"
+)
+
+// Logger returns a gin middleware which provides request logging and panic recovery.
+func Logger() gin.HandlerFunc {
+ return func(c *gin.Context) {
+ // Initialize the logging fields
+ fields := make(kv.Fields, 6, 7)
+
+ // Determine pre-handler time
+ before := time.Now()
+
+ // defer so that we log *after the request has completed*
+ 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/middleware/ratelimit.go b/internal/middleware/ratelimit.go
new file mode 100644
index 000000000..ab947a124
--- /dev/null
+++ b/internal/middleware/ratelimit.go
@@ -0,0 +1,77 @@
+/*
+ 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 middleware
+
+import (
+ "net"
+ "net/http"
+ "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"
+)
+
+const rateLimitPeriod = 5 * time.Minute
+
+// RateLimit returns a gin middleware that will automatically rate limit caller (by IP address),
+// and enrich the response header with the following headers:
+//
+// - `x-ratelimit-limit` - maximum number of requests allowed per time period (fixed).
+// - `x-ratelimit-remaining` - number of remaining requests that can still be performed.
+// - `x-ratelimit-reset` - unix timestamp when the rate limit will reset.
+//
+// If `x-ratelimit-limit` is exceeded, the request is aborted and an HTTP 429 TooManyRequests
+// status is returned.
+//
+// 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 {
+ // use noop middleware if ratelimiting is disabled
+ return func(c *gin.Context) {}
+ }
+
+ rate := limiter.Rate{
+ Period: rateLimitPeriod,
+ Limit: int64(rateLimitRequests),
+ }
+
+ limiterInstance := limiter.New(
+ memory.NewStore(),
+ rate,
+ limiter.WithIPv6Mask(net.CIDRMask(64, 128)), // apply /64 mask to IPv6 addresses
+ )
+
+ limitReachedHandler := 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 middleware
+}
diff --git a/internal/middleware/session.go b/internal/middleware/session.go
new file mode 100644
index 000000000..bffbdcd92
--- /dev/null
+++ b/internal/middleware/session.go
@@ -0,0 +1,95 @@
+/*
+ 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 middleware
+
+import (
+ "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/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
+}
+
+// Session returns a new gin middleware that implements session cookies using the given
+// sessionName, authentication key, and encryption key. Session name can be derived from the
+// SessionName utility function in this package.
+func Session(sessionName string, auth []byte, crypt []byte) gin.HandlerFunc {
+ store := memstore.NewStore(auth, crypt)
+ store.Options(SessionOptions())
+ return sessions.Sessions(sessionName, store)
+}
diff --git a/internal/middleware/session_test.go b/internal/middleware/session_test.go
new file mode 100644
index 000000000..d11d8c202
--- /dev/null
+++ b/internal/middleware/session_test.go
@@ -0,0 +1,95 @@
+/*
+ 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 middleware_test
+
+import (
+ "testing"
+
+ "github.com/stretchr/testify/suite"
+ "github.com/superseriousbusiness/gotosocial/internal/config"
+ "github.com/superseriousbusiness/gotosocial/internal/middleware"
+ "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 := middleware.SessionName()
+ suite.NoError(err)
+ suite.Equal("gotosocial-localhost", sessionName)
+}
+
+func (suite *SessionTestSuite) TestDeriveSessionNameLocalhost() {
+ config.SetProtocol("http")
+ config.SetHost("localhost")
+
+ sessionName, err := middleware.SessionName()
+ suite.NoError(err)
+ suite.Equal("gotosocial-localhost", sessionName)
+}
+
+func (suite *SessionTestSuite) TestDeriveSessionNoProtocol() {
+ config.SetProtocol("")
+ config.SetHost("localhost")
+
+ sessionName, err := middleware.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 := middleware.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 := middleware.SessionName()
+ suite.NoError(err)
+ suite.Equal("gotosocial-example.org", sessionName)
+}
+
+func (suite *SessionTestSuite) TestDeriveSessionIDNOK() {
+ config.SetProtocol("https")
+ config.SetHost("fóid.org")
+
+ sessionName, err := middleware.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/middleware/signaturecheck.go b/internal/middleware/signaturecheck.go
new file mode 100644
index 000000000..c1f190eb5
--- /dev/null
+++ b/internal/middleware/signaturecheck.go
@@ -0,0 +1,93 @@
+package middleware
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "net/url"
+
+ "github.com/superseriousbusiness/gotosocial/internal/ap"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+
+ "github.com/gin-gonic/gin"
+ "github.com/go-fed/httpsig"
+)
+
+var (
+ // this mimics an untyped error returned by httpsig when no signature is present;
+ // define it here so that we can use it to decide what to log without hitting
+ // performance too hard
+ noSignatureError = fmt.Sprintf("neither %q nor %q have signature parameters", httpsig.Signature, httpsig.Authorization)
+ signatureHeader = string(httpsig.Signature)
+ authorizationHeader = string(httpsig.Authorization)
+)
+
+// SignatureCheck returns a gin middleware for checking http signatures.
+//
+// The middleware first checks whether an incoming http request has been http-signed with a well-formed signature.
+//
+// If so, it will check if the domain that signed the request is permitted to access the server, using the provided isURIBlocked function.
+//
+// If it is permitted, the handler will set the key verifier and the signature in the gin context for use down the line.
+//
+// If the domain is blocked, the middleware will abort the request chain instead with http code 403 forbidden.
+//
+// In case of an error, the request will be aborted with http code 500 internal server error.
+func SignatureCheck(isURIBlocked func(context.Context, *url.URL) (bool, db.Error)) func(*gin.Context) {
+ return func(c *gin.Context) {
+ // create the verifier from the request, this will error if the request wasn't signed
+ verifier, err := httpsig.NewVerifier(c.Request)
+ if err != nil {
+ // Something went wrong, so we need to return regardless, but only actually
+ // *abort* the request with 401 if a signature was present but malformed
+ if err.Error() != noSignatureError {
+ log.Debugf("http signature was present but invalid: %s", err)
+ c.AbortWithStatus(http.StatusUnauthorized)
+ }
+ return
+ }
+
+ // The request was signed!
+ // The key ID should be given in the signature so that we know where to fetch it from the remote server.
+ // This will be something like https://example.org/users/whatever_requesting_user#main-key
+ requestingPublicKeyIDString := verifier.KeyId()
+ requestingPublicKeyID, err := url.Parse(requestingPublicKeyIDString)
+ if err != nil {
+ log.Debugf("http signature requesting public key id %s could not be parsed as a url: %s", requestingPublicKeyIDString, err)
+ c.AbortWithStatus(http.StatusUnauthorized)
+ return
+ } else if requestingPublicKeyID == nil {
+ // Key can sometimes be nil, according to url parse function:
+ // 'Trying to parse a hostname and path without a scheme is invalid but may not necessarily return an error, due to parsing ambiguities'
+ log.Debugf("http signature requesting public key id %s was nil after parsing as a url", requestingPublicKeyIDString)
+ c.AbortWithStatus(http.StatusUnauthorized)
+ return
+ }
+
+ // we managed to parse the url!
+ // if the domain is blocked we want to bail as early as possible
+ if blocked, err := isURIBlocked(c.Request.Context(), requestingPublicKeyID); err != nil {
+ log.Errorf("could not tell if domain %s was blocked or not: %s", requestingPublicKeyID.Host, err)
+ c.AbortWithStatus(http.StatusInternalServerError)
+ return
+ } else if blocked {
+ log.Infof("domain %s is blocked", requestingPublicKeyID.Host)
+ c.AbortWithStatus(http.StatusForbidden)
+ return
+ }
+
+ // assume signature was set on Signature header (most common behavior),
+ // but fall back to Authorization header if necessary
+ var signature string
+ if s := c.GetHeader(signatureHeader); s != "" {
+ signature = s
+ } else {
+ signature = c.GetHeader(authorizationHeader)
+ }
+
+ // set the verifier and signature on the context here to save some work further down the line
+ c.Set(string(ap.ContextRequestingPublicKeyVerifier), verifier)
+ c.Set(string(ap.ContextRequestingPublicKeySignature), signature)
+ }
+}
diff --git a/internal/middleware/tokencheck.go b/internal/middleware/tokencheck.go
new file mode 100644
index 000000000..ca93f379a
--- /dev/null
+++ b/internal/middleware/tokencheck.go
@@ -0,0 +1,140 @@
+/*
+ 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 middleware
+
+import (
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+ "github.com/superseriousbusiness/gotosocial/internal/db"
+ "github.com/superseriousbusiness/gotosocial/internal/gtsmodel"
+ "github.com/superseriousbusiness/gotosocial/internal/log"
+ "github.com/superseriousbusiness/gotosocial/internal/oauth"
+ "github.com/superseriousbusiness/oauth2/v4"
+)
+
+// TokenCheck returns a new gin middleware for validating oauth tokens in requests.
+//
+// The middleware checks the request Authorization header for a valid oauth Bearer token.
+//
+// If no token was set in the Authorization header, or the token was invalid, the handler will return.
+//
+// If a valid oauth Bearer token was provided, it will be set on the gin context for further use.
+//
+// Then, it will check which *gtsmodel.User the token belongs to. If the user is not confirmed, not approved,
+// or has been disabled, then the middleware will return early. Otherwise, the User will be set on the
+// gin context for further processing by other functions.
+//
+// Next, it will look up the *gtsmodel.Account for the User. If the Account has been suspended, then the
+// middleware will return early. Otherwise, it will set the Account on the gin context too.
+//
+// Finally, it will check the client ID of the token to see if a *gtsmodel.Application can be retrieved
+// for that client ID. This will also be set on the gin context.
+//
+// If an invalid token is presented, or a user/account/application can't be found, then this middleware
+// won't abort the request, since the server might want to still allow public requests that don't have a
+// Bearer token set (eg., for public instance information and so on).
+func TokenCheck(dbConn db.DB, validateBearerToken func(r *http.Request) (oauth2.TokenInfo, error)) func(*gin.Context) {
+ return func(c *gin.Context) {
+ ctx := c.Request.Context()
+
+ if c.Request.Header.Get("Authorization") == "" {
+ // no token set in the header, we can just bail
+ return
+ }
+
+ ti, err := validateBearerToken(c.Copy().Request)
+ if err != nil {
+ log.Debugf("token was passed in Authorization header but we could not validate it: %s", err)
+ return
+ }
+ c.Set(oauth.SessionAuthorizedToken, ti)
+
+ // check for user-level token
+ if userID := ti.GetUserID(); userID != "" {
+ log.Tracef("authenticated user %s with bearer token, scope is %s", userID, ti.GetScope())
+
+ // fetch user for this token
+ user, err := dbConn.GetUserByID(ctx, userID)
+ if err != nil {
+ if err != db.ErrNoEntries {
+ log.Errorf("database error looking for user with id %s: %s", userID, err)
+ return
+ }
+ log.Warnf("no user found for userID %s", userID)
+ return
+ }
+
+ if user.ConfirmedAt.IsZero() {
+ log.Warnf("authenticated user %s has never confirmed thier email address", userID)
+ return
+ }
+
+ if !*user.Approved {
+ log.Warnf("authenticated user %s's account was never approved by an admin", userID)
+ return
+ }
+
+ if *user.Disabled {
+ log.Warnf("authenticated user %s's account was disabled'", userID)
+ return
+ }
+
+ c.Set(oauth.SessionAuthorizedUser, user)
+
+ // fetch account for this token
+ if user.Account == nil {
+ acct, err := dbConn.GetAccountByID(ctx, user.AccountID)
+ if err != nil {
+ if err != db.ErrNoEntries {
+ log.Errorf("database error looking for account with id %s: %s", user.AccountID, err)
+ return
+ }
+ log.Warnf("no account found for userID %s", userID)
+ return
+ }
+ user.Account = acct
+ }
+
+ if !user.Account.SuspendedAt.IsZero() {
+ log.Warnf("authenticated user %s's account (accountId=%s) has been suspended", userID, user.AccountID)
+ return
+ }
+
+ c.Set(oauth.SessionAuthorizedAccount, user.Account)
+ }
+
+ // check for application token
+ if clientID := ti.GetClientID(); clientID != "" {
+ log.Tracef("authenticated client %s with bearer token, scope is %s", clientID, ti.GetScope())
+
+ // fetch app for this token
+ app := &gtsmodel.Application{}
+ if err := dbConn.GetWhere(ctx, []db.Where{{Key: "client_id", Value: clientID}}, app); err != nil {
+ if err != db.ErrNoEntries {
+ log.Errorf("database error looking for application with clientID %s: %s", clientID, err)
+ return
+ }
+ log.Warnf("no app found for client %s", clientID)
+ return
+ }
+ c.Set(oauth.SessionAuthorizedApplication, app)
+ }
+ }
+}
diff --git a/internal/middleware/useragent.go b/internal/middleware/useragent.go
new file mode 100644
index 000000000..e55a19434
--- /dev/null
+++ b/internal/middleware/useragent.go
@@ -0,0 +1,39 @@
+/*
+ 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 middleware
+
+import (
+ "errors"
+ "net/http"
+
+ "github.com/gin-gonic/gin"
+)
+
+// UserAgent returns a gin middleware which aborts requests with
+// empty user agent strings, returning code 418 - I'm a teapot.
+func UserAgent() gin.HandlerFunc {
+ // todo: make this configurable
+ return func(c *gin.Context) {
+ if ua := c.Request.UserAgent(); ua == "" {
+ code := http.StatusTeapot
+ err := errors.New(http.StatusText(code) + ": no user-agent sent with request")
+ c.AbortWithStatusJSON(code, gin.H{"error": err.Error()})
+ }
+ }
+}