mirror of
https://github.com/superseriousbusiness/gotosocial.git
synced 2024-11-22 11:46:40 +00:00
[feature] HTTP request throttling middleware (#1297)
* [feature] Add throttling middleware to AP endpoints * refactor a lil bit * use config setting, start updating docs * doc updates * use relative links in faq doc * small docs fixes * return code 503 instead of 429 when throttled * throttle other endpoints too * simplify token channel prefills
This commit is contained in:
parent
0b8eafec5c
commit
90a14abb0c
|
@ -184,20 +184,29 @@
|
||||||
)
|
)
|
||||||
|
|
||||||
// create required middleware
|
// create required middleware
|
||||||
|
// rate limiting
|
||||||
limit := config.GetAdvancedRateLimitRequests()
|
limit := config.GetAdvancedRateLimitRequests()
|
||||||
gzip := middleware.Gzip() // all except fileserver
|
|
||||||
clLimit := middleware.RateLimit(limit) // client api
|
clLimit := middleware.RateLimit(limit) // client api
|
||||||
s2sLimit := middleware.RateLimit(limit) // server-to-server (AP)
|
s2sLimit := middleware.RateLimit(limit) // server-to-server (AP)
|
||||||
fsLimit := middleware.RateLimit(limit) // fileserver / web templates
|
fsLimit := middleware.RateLimit(limit) // fileserver / web templates
|
||||||
|
|
||||||
// these should be routed in order
|
// throttling
|
||||||
authModule.Route(router, clLimit, gzip)
|
cpuMultiplier := config.GetAdvancedThrottlingMultiplier()
|
||||||
clientModule.Route(router, clLimit, gzip)
|
clThrottle := middleware.Throttle(cpuMultiplier) // client api
|
||||||
fileserverModule.Route(router, fsLimit)
|
s2sThrottle := middleware.Throttle(cpuMultiplier) // server-to-server (AP)
|
||||||
wellKnownModule.Route(router, gzip, s2sLimit)
|
fsThrottle := middleware.Throttle(cpuMultiplier) // fileserver / web templates
|
||||||
nodeInfoModule.Route(router, s2sLimit, gzip)
|
|
||||||
activityPubModule.Route(router, s2sLimit, gzip)
|
gzip := middleware.Gzip() // applied to all except fileserver
|
||||||
webModule.Route(router, fsLimit, gzip)
|
|
||||||
|
// these should be routed in order;
|
||||||
|
// apply throttling *after* rate limiting
|
||||||
|
authModule.Route(router, clLimit, clThrottle, gzip)
|
||||||
|
clientModule.Route(router, clLimit, clThrottle, gzip)
|
||||||
|
fileserverModule.Route(router, fsLimit, fsThrottle)
|
||||||
|
wellKnownModule.Route(router, gzip, s2sLimit, s2sThrottle)
|
||||||
|
nodeInfoModule.Route(router, s2sLimit, s2sThrottle, gzip)
|
||||||
|
activityPubModule.Route(router, s2sLimit, s2sThrottle, gzip)
|
||||||
|
webModule.Route(router, fsLimit, fsThrottle, gzip)
|
||||||
|
|
||||||
gts, err := gotosocial.NewServer(dbService, router, federator, mediaManager)
|
gts, err := gotosocial.NewServer(dbService, router, federator, mediaManager)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
|
|
|
@ -1,4 +1,4 @@
|
||||||
# Rate Limit
|
# Request Rate Limiting
|
||||||
|
|
||||||
To mitigate abuse + scraping of your instance, IP-based HTTP rate limiting is in place.
|
To mitigate abuse + scraping of your instance, IP-based HTTP rate limiting is in place.
|
||||||
|
|
||||||
|
|
35
docs/api/throttling.md
Normal file
35
docs/api/throttling.md
Normal file
|
@ -0,0 +1,35 @@
|
||||||
|
# Request Throttling
|
||||||
|
|
||||||
|
GoToSocial uses request throttling to limit the number of open connections to the API of your instance. This is designed to prevent your instance from accidentally being DDOS'd (aka [the hug of death](https://en.wikipedia.org/wiki/Slashdot_effect)) if a post gets boosted or replied to by an account with many thousands of followers.
|
||||||
|
|
||||||
|
Throttling means that only a limited number of HTTP requests to the API will be handled concurrently, in order to provide a snappy response to each request and move on quickly. The rationale is that it's better to handle fewer requests quickly, than to try to handle all incoming requests at once and take multiple seconds per request.
|
||||||
|
|
||||||
|
Throttling limits are applied across router groups, similar to the way that [rate limiting](./ratelimiting.md) is organized, so if one part of the API is currently being throttled, that doesn't mean they all are.
|
||||||
|
|
||||||
|
Throttling limits are calculated based on the number of CPUs available to GoToSocial, and the configuration value `advanced-throttling-multiplier`. The calculation is performed as follows:
|
||||||
|
|
||||||
|
- In-process queue limit = number of CPUs * CPU multiplier.
|
||||||
|
- Backlog queue limit = in-process queue limit * CPU multiplier.
|
||||||
|
|
||||||
|
This leads to the following values for the default multiplier (8):
|
||||||
|
|
||||||
|
```text
|
||||||
|
1 cpu = 08 in-process, 064 backlog
|
||||||
|
2 cpu = 16 in-process, 128 backlog
|
||||||
|
4 cpu = 32 in-process, 256 backlog
|
||||||
|
8 cpu = 64 in-process, 512 backlog
|
||||||
|
```
|
||||||
|
|
||||||
|
New requests that overflow the in-process limit are held in the backlog queue, and processed as soon as a spot is freed up (ie., when a currently in-process request is finished). Requests that cannot be processed, and cannot fit in the backlog queue will be responded to with http code [503 - Service Unavailable](https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503), and the `Retry-After` header will be set to `30` (seconds), to indicate that the caller should try again later.
|
||||||
|
|
||||||
|
Requests are not held in the backlog queue indefinitely: if requests in the backlog cannot be processed within 30 seconds of being received, they will also receive a code 503 and a 30s retry-after.
|
||||||
|
|
||||||
|
## Throttling FAQs
|
||||||
|
|
||||||
|
### Can I tune the request throttling?
|
||||||
|
|
||||||
|
Yes, just change the value of `advanced-throttling-multiplier` higher (if you have very powerful CPUs) or lower (if you have relatively less powerful CPUs).
|
||||||
|
|
||||||
|
### Can I disable the request throttling?
|
||||||
|
|
||||||
|
Yes. To do so, just set `advanced-throttling-multiplier` to `0` or less. This will disable HTTP request throttling entirely, and instead attempt to process all incoming requests at once. This is useful in cases where you want to do request throttling using an external service or a reverse-proxy, and you don't want GoToSocial to interfere with your setup.
|
|
@ -36,9 +36,8 @@ These are set to sensible defaults, so most server admins won't need to touch th
|
||||||
# Default: "lax"
|
# Default: "lax"
|
||||||
advanced-cookies-samesite: "lax"
|
advanced-cookies-samesite: "lax"
|
||||||
|
|
||||||
# Int. Amount of requests to permit from a single IP address within a span of 5 minutes.
|
# Int. Amount of requests to permit per router grouping from a single IP address within
|
||||||
# If this amount is exceeded, a 429 HTTP error code will be returned.
|
# a span of 5 minutes. If this amount is exceeded, a 429 HTTP error code will be returned.
|
||||||
# See https://docs.gotosocial.org/en/latest/api/swagger/#rate-limit.
|
|
||||||
#
|
#
|
||||||
# If you find yourself adjusting this limit because it's regularly being exceeded,
|
# If you find yourself adjusting this limit because it's regularly being exceeded,
|
||||||
# you should first verify that your settings for `trusted-proxies` (above) are correct.
|
# you should first verify that your settings for `trusted-proxies` (above) are correct.
|
||||||
|
@ -50,6 +49,34 @@ advanced-cookies-samesite: "lax"
|
||||||
# If you set this to 0 or less, rate limiting will be disabled entirely.
|
# If you set this to 0 or less, rate limiting will be disabled entirely.
|
||||||
#
|
#
|
||||||
# Examples: [1000, 500, 0]
|
# Examples: [1000, 500, 0]
|
||||||
# Default: 1000
|
# Default: 300
|
||||||
advanced-rate-limit-requests: 1000
|
advanced-rate-limit-requests: 300
|
||||||
|
|
||||||
|
# Int. Amount of open requests to permit per CPU, per router grouping, before applying http
|
||||||
|
# request throttling. Any requests beyond the calculated limit are held in a backlog queue for
|
||||||
|
# up to 30 seconds before either being processed or timing out. Requests that don't fit in the backlog
|
||||||
|
# queue will have status 503 returned to them, and the header 'Retry-After' will be set to 30 seconds.
|
||||||
|
#
|
||||||
|
# Open request limit is available CPUs * multiplier; backlog queue limit is limit * multiplier.
|
||||||
|
#
|
||||||
|
# Example values for multiplier 8:
|
||||||
|
#
|
||||||
|
# 1 cpu = 08 open, 064 backlog
|
||||||
|
# 2 cpu = 16 open, 128 backlog
|
||||||
|
# 4 cpu = 32 open, 256 backlog
|
||||||
|
#
|
||||||
|
# Example values for multiplier 4:
|
||||||
|
#
|
||||||
|
# 1 cpu = 04 open, 016 backlog
|
||||||
|
# 2 cpu = 08 open, 032 backlog
|
||||||
|
# 4 cpu = 16 open, 064 backlog
|
||||||
|
#
|
||||||
|
# A multiplier of 8 is a sensible default, but you may wish to increase this for instances
|
||||||
|
# running on very performant hardware, or decrease it for instances using v. slow CPUs.
|
||||||
|
#
|
||||||
|
# If you set this to 0 or less, http request throttling will be disabled entirely.
|
||||||
|
#
|
||||||
|
# Examples: [8, 4, 9, 0]
|
||||||
|
# Default: 8
|
||||||
|
advanced-throttling-multiplier: 8
|
||||||
```
|
```
|
||||||
|
|
|
@ -8,7 +8,9 @@
|
||||||
|
|
||||||
- **Why aren't my posts showing up on other servers?** First check the visibility as noted above. TODO: explain how to debug common federation issues
|
- **Why aren't my posts showing up on other servers?** First check the visibility as noted above. TODO: explain how to debug common federation issues
|
||||||
|
|
||||||
- **Why am I getting frequent error responses?** GoToSocial is configured to use per-IP [rate limiting](https://docs.gotosocial.org/en/latest/api/ratelimiting/) by default, but in certain situations it can't accurately identify the remote IP and will treat all connections as coming from the same place. In those cases, the rate limiting needs to be disabled or reconfigured.
|
- **Why am I getting frequent http 429 error responses?** GoToSocial is configured to use per-IP [rate limiting](./api/ratelimiting.md) by default, but in certain situations it can't accurately identify the remote IP and will treat all connections as coming from the same place. In those cases, the rate limiting needs to be disabled or reconfigured.
|
||||||
|
|
||||||
|
- **Why am I getting frequent http 503 error responses?** Code 503 is returned to callers when your instance is under heavy load and requests are being throttled. This behavior can be tuned as desired, or turned off entirely, see [here](./api/throttling.md).
|
||||||
|
|
||||||
- **My instance is deployed and I'm logged in to a client but my timelines are empty, what's up there?** To see posts, you have to start following people! Once you've followed a few people and they've posted or boosted things, you'll start seeing them in your timelines. Right now GoToSocial doesn't have a way of 'backfilling' posts -- that is, fetching previous posts from other instances -- so you'll only see new posts of people you follow. If you want to interact with an older post of theirs, you can copy the link to the post from their web profile, and paste it in to your client's search bar.
|
- **My instance is deployed and I'm logged in to a client but my timelines are empty, what's up there?** To see posts, you have to start following people! Once you've followed a few people and they've posted or boosted things, you'll start seeing them in your timelines. Right now GoToSocial doesn't have a way of 'backfilling' posts -- that is, fetching previous posts from other instances -- so you'll only see new posts of people you follow. If you want to interact with an older post of theirs, you can copy the link to the post from their web profile, and paste it in to your client's search bar.
|
||||||
|
|
||||||
|
|
3
docs/federation/behaviors/request_throttling.md
Normal file
3
docs/federation/behaviors/request_throttling.md
Normal file
|
@ -0,0 +1,3 @@
|
||||||
|
# Request Throttling and Rate Limiting
|
||||||
|
|
||||||
|
GoToSocial applies rate limiting and http request throttling to the ActivityPub API endpoints (inboxes, user endpoints, emojis, etc). For more details on this, please see the [throttling](../../api/throttling.md) and [rate limiting](../../api/ratelimiting.md) documents.
|
|
@ -34,7 +34,7 @@ It began as a solo project, and then picked up steam as more developers became i
|
||||||
|
|
||||||
## Known Issues
|
## Known Issues
|
||||||
|
|
||||||
Since GoToSocial is still in alpha, there are plenty of bugs. We use [GitHub issues](https://github.com/superseriousbusiness/gotosocial/issues?q=is%3Aissue+is%3Aopen+label%3Abug) to track these. The [FAQ](docs/faq.md) also describes some of the features that haven't been implemented yet.
|
Since GoToSocial is still in alpha, there are plenty of bugs. We use [GitHub issues](https://github.com/superseriousbusiness/gotosocial/issues?q=is%3Aissue+is%3Aopen+label%3Abug) to track these. The [FAQ](./faq.md) also describes some of the features that haven't been implemented yet.
|
||||||
|
|
||||||
### Client App Issues
|
### Client App Issues
|
||||||
|
|
||||||
|
|
|
@ -644,9 +644,6 @@ advanced-cookies-samesite: "lax"
|
||||||
# Int. Amount of requests to permit per router grouping from a single IP address within
|
# Int. Amount of requests to permit per router grouping from a single IP address within
|
||||||
# a span of 5 minutes. If this amount is exceeded, a 429 HTTP error code will be returned.
|
# a span of 5 minutes. If this amount is exceeded, a 429 HTTP error code will be returned.
|
||||||
#
|
#
|
||||||
# Router groupings and rate limit headers are described here:
|
|
||||||
# https://docs.gotosocial.org/en/latest/api/swagger/#rate-limit.
|
|
||||||
#
|
|
||||||
# If you find yourself adjusting this limit because it's regularly being exceeded,
|
# If you find yourself adjusting this limit because it's regularly being exceeded,
|
||||||
# you should first verify that your settings for `trusted-proxies` (above) are correct.
|
# you should first verify that your settings for `trusted-proxies` (above) are correct.
|
||||||
# In many cases, when the rate limit is exceeded it is because your instance sees all
|
# In many cases, when the rate limit is exceeded it is because your instance sees all
|
||||||
|
@ -659,3 +656,31 @@ advanced-cookies-samesite: "lax"
|
||||||
# Examples: [1000, 500, 0]
|
# Examples: [1000, 500, 0]
|
||||||
# Default: 300
|
# Default: 300
|
||||||
advanced-rate-limit-requests: 300
|
advanced-rate-limit-requests: 300
|
||||||
|
|
||||||
|
# Int. Amount of open requests to permit per CPU, per router grouping, before applying http
|
||||||
|
# request throttling. Any requests beyond the calculated limit are held in a backlog queue for
|
||||||
|
# up to 30 seconds before either being processed or timing out. Requests that don't fit in the backlog
|
||||||
|
# queue will have status 503 returned to them, and the header 'Retry-After' will be set to 30 seconds.
|
||||||
|
#
|
||||||
|
# Open request limit is available CPUs * multiplier; backlog queue limit is limit * multiplier.
|
||||||
|
#
|
||||||
|
# Example values for multiplier 8:
|
||||||
|
#
|
||||||
|
# 1 cpu = 08 open, 064 backlog
|
||||||
|
# 2 cpu = 16 open, 128 backlog
|
||||||
|
# 4 cpu = 32 open, 256 backlog
|
||||||
|
#
|
||||||
|
# Example values for multiplier 4:
|
||||||
|
#
|
||||||
|
# 1 cpu = 04 open, 016 backlog
|
||||||
|
# 2 cpu = 08 open, 032 backlog
|
||||||
|
# 4 cpu = 16 open, 064 backlog
|
||||||
|
#
|
||||||
|
# A multiplier of 8 is a sensible default, but you may wish to increase this for instances
|
||||||
|
# running on very performant hardware, or decrease it for instances using v. slow CPUs.
|
||||||
|
#
|
||||||
|
# If you set this to 0 or less, http request throttling will be disabled entirely.
|
||||||
|
#
|
||||||
|
# Examples: [8, 4, 9, 0]
|
||||||
|
# Default: 8
|
||||||
|
advanced-throttling-multiplier: 8
|
||||||
|
|
|
@ -127,8 +127,9 @@ type Configuration struct {
|
||||||
SyslogProtocol string `name:"syslog-protocol" usage:"Protocol to use when directing logs to syslog. Leave empty to connect to local syslog."`
|
SyslogProtocol string `name:"syslog-protocol" usage:"Protocol to use when directing logs to syslog. Leave empty to connect to local syslog."`
|
||||||
SyslogAddress string `name:"syslog-address" usage:"Address:port to send syslog logs to. Leave empty to connect to local syslog."`
|
SyslogAddress string `name:"syslog-address" usage:"Address:port to send syslog logs to. Leave empty to connect to local syslog."`
|
||||||
|
|
||||||
AdvancedCookiesSamesite string `name:"advanced-cookies-samesite" usage:"'strict' or 'lax', see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite"`
|
AdvancedCookiesSamesite string `name:"advanced-cookies-samesite" usage:"'strict' or 'lax', see https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Set-Cookie/SameSite"`
|
||||||
AdvancedRateLimitRequests int `name:"advanced-rate-limit-requests" usage:"Amount of HTTP requests to permit within a 5 minute window. 0 or less turns rate limiting off."`
|
AdvancedRateLimitRequests int `name:"advanced-rate-limit-requests" usage:"Amount of HTTP requests to permit within a 5 minute window. 0 or less turns rate limiting off."`
|
||||||
|
AdvancedThrottlingMultiplier int `name:"advanced-throttling-multiplier" usage:"Multiplier to use per cpu for http request throttling. 0 or less turns throttling off."`
|
||||||
|
|
||||||
// Cache configuration vars.
|
// Cache configuration vars.
|
||||||
Cache CacheConfiguration `name:"cache"`
|
Cache CacheConfiguration `name:"cache"`
|
||||||
|
|
|
@ -104,8 +104,9 @@
|
||||||
SyslogProtocol: "udp",
|
SyslogProtocol: "udp",
|
||||||
SyslogAddress: "localhost:514",
|
SyslogAddress: "localhost:514",
|
||||||
|
|
||||||
AdvancedCookiesSamesite: "lax",
|
AdvancedCookiesSamesite: "lax",
|
||||||
AdvancedRateLimitRequests: 300, // 1 per second per 5 minutes
|
AdvancedRateLimitRequests: 300, // 1 per second per 5 minutes
|
||||||
|
AdvancedThrottlingMultiplier: 8, // 8 open requests per CPU
|
||||||
|
|
||||||
Cache: CacheConfiguration{
|
Cache: CacheConfiguration{
|
||||||
GTS: GTSCacheConfiguration{
|
GTS: GTSCacheConfiguration{
|
||||||
|
|
|
@ -132,6 +132,7 @@ func (s *ConfigState) AddServerFlags(cmd *cobra.Command) {
|
||||||
// Advanced flags
|
// Advanced flags
|
||||||
cmd.Flags().String(AdvancedCookiesSamesiteFlag(), cfg.AdvancedCookiesSamesite, fieldtag("AdvancedCookiesSamesite", "usage"))
|
cmd.Flags().String(AdvancedCookiesSamesiteFlag(), cfg.AdvancedCookiesSamesite, fieldtag("AdvancedCookiesSamesite", "usage"))
|
||||||
cmd.Flags().Int(AdvancedRateLimitRequestsFlag(), cfg.AdvancedRateLimitRequests, fieldtag("AdvancedRateLimitRequests", "usage"))
|
cmd.Flags().Int(AdvancedRateLimitRequestsFlag(), cfg.AdvancedRateLimitRequests, fieldtag("AdvancedRateLimitRequests", "usage"))
|
||||||
|
cmd.Flags().Int(AdvancedThrottlingMultiplierFlag(), cfg.AdvancedThrottlingMultiplier, fieldtag("AdvancedThrottlingMultiplier", "usage"))
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
|
@ -1824,6 +1824,31 @@ func GetAdvancedRateLimitRequests() int { return global.GetAdvancedRateLimitRequ
|
||||||
// SetAdvancedRateLimitRequests safely sets the value for global configuration 'AdvancedRateLimitRequests' field
|
// SetAdvancedRateLimitRequests safely sets the value for global configuration 'AdvancedRateLimitRequests' field
|
||||||
func SetAdvancedRateLimitRequests(v int) { global.SetAdvancedRateLimitRequests(v) }
|
func SetAdvancedRateLimitRequests(v int) { global.SetAdvancedRateLimitRequests(v) }
|
||||||
|
|
||||||
|
// GetAdvancedThrottlingMultiplier safely fetches the Configuration value for state's 'AdvancedThrottlingMultiplier' field
|
||||||
|
func (st *ConfigState) GetAdvancedThrottlingMultiplier() (v int) {
|
||||||
|
st.mutex.Lock()
|
||||||
|
v = st.config.AdvancedThrottlingMultiplier
|
||||||
|
st.mutex.Unlock()
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetAdvancedThrottlingMultiplier safely sets the Configuration value for state's 'AdvancedThrottlingMultiplier' field
|
||||||
|
func (st *ConfigState) SetAdvancedThrottlingMultiplier(v int) {
|
||||||
|
st.mutex.Lock()
|
||||||
|
defer st.mutex.Unlock()
|
||||||
|
st.config.AdvancedThrottlingMultiplier = v
|
||||||
|
st.reloadToViper()
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdvancedThrottlingMultiplierFlag returns the flag name for the 'AdvancedThrottlingMultiplier' field
|
||||||
|
func AdvancedThrottlingMultiplierFlag() string { return "advanced-throttling-multiplier" }
|
||||||
|
|
||||||
|
// GetAdvancedThrottlingMultiplier safely fetches the value for global configuration 'AdvancedThrottlingMultiplier' field
|
||||||
|
func GetAdvancedThrottlingMultiplier() int { return global.GetAdvancedThrottlingMultiplier() }
|
||||||
|
|
||||||
|
// SetAdvancedThrottlingMultiplier safely sets the value for global configuration 'AdvancedThrottlingMultiplier' field
|
||||||
|
func SetAdvancedThrottlingMultiplier(v int) { global.SetAdvancedThrottlingMultiplier(v) }
|
||||||
|
|
||||||
// GetCacheGTSAccountMaxSize safely fetches the Configuration value for state's 'Cache.GTS.AccountMaxSize' field
|
// GetCacheGTSAccountMaxSize safely fetches the Configuration value for state's 'Cache.GTS.AccountMaxSize' field
|
||||||
func (st *ConfigState) GetCacheGTSAccountMaxSize() (v int) {
|
func (st *ConfigState) GetCacheGTSAccountMaxSize() (v int) {
|
||||||
st.mutex.Lock()
|
st.mutex.Lock()
|
||||||
|
|
153
internal/middleware/throttling.go
Normal file
153
internal/middleware/throttling.go
Normal file
|
@ -0,0 +1,153 @@
|
||||||
|
/*
|
||||||
|
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/>.
|
||||||
|
*/
|
||||||
|
|
||||||
|
/*
|
||||||
|
The code in this file is adapted from MIT-licensed code in github.com/go-chi/chi. Thanks chi (thi)!
|
||||||
|
|
||||||
|
See: https://github.com/go-chi/chi/blob/e6baba61759b26ddf7b14d1e02d1da81a4d76c08/middleware/throttle.go
|
||||||
|
|
||||||
|
And: https://github.com/sponsors/pkieltyka
|
||||||
|
*/
|
||||||
|
|
||||||
|
package middleware
|
||||||
|
|
||||||
|
import (
|
||||||
|
"net/http"
|
||||||
|
"runtime"
|
||||||
|
"time"
|
||||||
|
|
||||||
|
"github.com/gin-gonic/gin"
|
||||||
|
)
|
||||||
|
|
||||||
|
const (
|
||||||
|
errCapacityExceeded = "server capacity exceeded"
|
||||||
|
errTimedOut = "timed out while waiting for a pending request to complete"
|
||||||
|
errContextCanceled = "context canceled"
|
||||||
|
)
|
||||||
|
|
||||||
|
// token represents a request that is being processed.
|
||||||
|
type token struct{}
|
||||||
|
|
||||||
|
// Throttle returns a gin middleware that performs throttling of incoming requests,
|
||||||
|
// ensuring that only a certain number of requests are handled concurrently, to reduce
|
||||||
|
// congestion of the server.
|
||||||
|
//
|
||||||
|
// Limits are configured using available CPUs and the given cpuMultiplier value.
|
||||||
|
// Open request limit is available CPUs * multiplier; backlog limit is limit * multiplier.
|
||||||
|
//
|
||||||
|
// Example values for multiplier 8:
|
||||||
|
//
|
||||||
|
// 1 cpu = 08 open, 064 backlog
|
||||||
|
// 2 cpu = 16 open, 128 backlog
|
||||||
|
// 4 cpu = 32 open, 256 backlog
|
||||||
|
//
|
||||||
|
// Example values for multiplier 4:
|
||||||
|
//
|
||||||
|
// 1 cpu = 04 open, 016 backlog
|
||||||
|
// 2 cpu = 08 open, 032 backlog
|
||||||
|
// 4 cpu = 16 open, 064 backlog
|
||||||
|
//
|
||||||
|
// Callers will first attempt to get a backlog token. Once they have that, they will
|
||||||
|
// wait in the backlog queue until they can get a token to allow their request to be
|
||||||
|
// processed.
|
||||||
|
//
|
||||||
|
// If the backlog queue is full, the request context is closed, or the caller has been
|
||||||
|
// waiting in the backlog for too long, this function will abort the request chain,
|
||||||
|
// write a JSON error into the response, set an appropriate Retry-After value, and set
|
||||||
|
// the HTTP response code to 503: Service Unavailable.
|
||||||
|
//
|
||||||
|
// If the multiplier is <= 0, a noop middleware will be returned instead.
|
||||||
|
//
|
||||||
|
// Useful links:
|
||||||
|
//
|
||||||
|
// - https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Retry-After
|
||||||
|
// - https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/503
|
||||||
|
func Throttle(cpuMultiplier int) gin.HandlerFunc {
|
||||||
|
if cpuMultiplier <= 0 {
|
||||||
|
// throttling is disabled, return a noop middleware
|
||||||
|
return func(c *gin.Context) {}
|
||||||
|
}
|
||||||
|
|
||||||
|
var (
|
||||||
|
limit = runtime.GOMAXPROCS(0) * cpuMultiplier
|
||||||
|
backlogLimit = limit * cpuMultiplier
|
||||||
|
backlogChannelSize = limit + backlogLimit
|
||||||
|
tokens = make(chan token, limit)
|
||||||
|
backlogTokens = make(chan token, backlogChannelSize)
|
||||||
|
retryAfter = "30" // seconds
|
||||||
|
backlogDuration = 30 * time.Second
|
||||||
|
)
|
||||||
|
|
||||||
|
// prefill token channels
|
||||||
|
for i := 0; i < limit; i++ {
|
||||||
|
tokens <- token{}
|
||||||
|
}
|
||||||
|
|
||||||
|
for i := 0; i < backlogChannelSize; i++ {
|
||||||
|
backlogTokens <- token{}
|
||||||
|
}
|
||||||
|
|
||||||
|
// bail instructs the requester to return after retryAfter seconds, returns a 503,
|
||||||
|
// and writes the given message into the "error" field of a returned json object
|
||||||
|
bail := func(c *gin.Context, msg string) {
|
||||||
|
c.Header("Retry-After", retryAfter)
|
||||||
|
c.JSON(http.StatusServiceUnavailable, gin.H{"error": msg})
|
||||||
|
c.Abort()
|
||||||
|
}
|
||||||
|
|
||||||
|
return func(c *gin.Context) {
|
||||||
|
// inside this select, the caller tries to get a backlog token
|
||||||
|
select {
|
||||||
|
case <-c.Request.Context().Done():
|
||||||
|
// request context has been canceled already
|
||||||
|
bail(c, errContextCanceled)
|
||||||
|
case btok := <-backlogTokens:
|
||||||
|
// take a backlog token and wait
|
||||||
|
timer := time.NewTimer(backlogDuration)
|
||||||
|
defer func() {
|
||||||
|
// when we're finished, return the backlog token to the bucket
|
||||||
|
backlogTokens <- btok
|
||||||
|
}()
|
||||||
|
|
||||||
|
// inside *this* select, the caller has a backlog token,
|
||||||
|
// and they're waiting for their turn to be processed
|
||||||
|
select {
|
||||||
|
case <-timer.C:
|
||||||
|
// waiting too long in the backlog
|
||||||
|
bail(c, errTimedOut)
|
||||||
|
case <-c.Request.Context().Done():
|
||||||
|
// the request context has been canceled already
|
||||||
|
timer.Stop()
|
||||||
|
bail(c, errContextCanceled)
|
||||||
|
case tok := <-tokens:
|
||||||
|
// the caller gets a token, so their request can now be processed
|
||||||
|
timer.Stop()
|
||||||
|
defer func() {
|
||||||
|
// whatever happens to the request, put the
|
||||||
|
// token back in the bucket when we're finished
|
||||||
|
tokens <- tok
|
||||||
|
}()
|
||||||
|
c.Next() // <- finally process the caller's request
|
||||||
|
}
|
||||||
|
|
||||||
|
default:
|
||||||
|
// we don't have space in the backlog queue
|
||||||
|
bail(c, errCapacityExceeded)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
|
@ -59,6 +59,8 @@ nav:
|
||||||
- "federation/security.md"
|
- "federation/security.md"
|
||||||
- "federation/behaviors/outbox.md"
|
- "federation/behaviors/outbox.md"
|
||||||
- "federation/behaviors/conversation_threads.md"
|
- "federation/behaviors/conversation_threads.md"
|
||||||
|
- "federation/behaviors/request_throttling.md"
|
||||||
- "API Documentation":
|
- "API Documentation":
|
||||||
- "api/swagger.md"
|
- "api/swagger.md"
|
||||||
- "api/ratelimiting.md"
|
- "api/ratelimiting.md"
|
||||||
|
- "api/throttling.md"
|
||||||
|
|
|
@ -2,7 +2,7 @@
|
||||||
|
|
||||||
set -eu
|
set -eu
|
||||||
|
|
||||||
EXPECT='{"account-domain":"peepee","accounts-allow-custom-css":true,"accounts-approval-required":false,"accounts-reason-required":false,"accounts-registration-open":true,"advanced-cookies-samesite":"strict","advanced-rate-limit-requests":6969,"application-name":"gts","bind-address":"127.0.0.1","cache":{"gts":{"account-max-size":99,"account-sweep-freq":1000000000,"account-ttl":10800000000000,"block-max-size":100,"block-sweep-freq":10000000000,"block-ttl":300000000000,"domain-block-max-size":1000,"domain-block-sweep-freq":60000000000,"domain-block-ttl":86400000000000,"emoji-category-max-size":100,"emoji-category-sweep-freq":10000000000,"emoji-category-ttl":300000000000,"emoji-max-size":500,"emoji-sweep-freq":10000000000,"emoji-ttl":300000000000,"mention-max-size":500,"mention-sweep-freq":10000000000,"mention-ttl":300000000000,"notification-max-size":500,"notification-sweep-freq":10000000000,"notification-ttl":300000000000,"status-max-size":500,"status-sweep-freq":10000000000,"status-ttl":300000000000,"tombstone-max-size":100,"tombstone-sweep-freq":10000000000,"tombstone-ttl":300000000000,"user-max-size":100,"user-sweep-freq":10000000000,"user-ttl":300000000000}},"config-path":"internal/config/testdata/test.yaml","db-address":":memory:","db-database":"gotosocial_prod","db-password":"hunter2","db-port":6969,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"sqlite","db-user":"sex-haver","dry-run":false,"email":"","host":"example.com","instance-deliver-to-shared-inboxes":false,"instance-expose-peers":true,"instance-expose-public-timeline":true,"instance-expose-suspended":true,"landing-page-user":"admin","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":true,"log-level":"info","media-description-max-chars":5000,"media-description-min-chars":69,"media-emoji-local-max-size":420,"media-emoji-remote-max-size":420,"media-image-max-size":420,"media-remote-cache-days":30,"media-video-max-size":420,"oidc-client-id":"1234","oidc-client-secret":"shhhh its a secret","oidc-enabled":true,"oidc-idp-name":"sex-haver","oidc-issuer":"whoknows","oidc-link-existing":true,"oidc-scopes":["read","write"],"oidc-skip-verification":true,"password":"","path":"","port":6969,"protocol":"http","smtp-from":"queen.rip.in.piss@terfisland.org","smtp-host":"example.com","smtp-password":"hunter2","smtp-port":4269,"smtp-username":"sex-haver","software-version":"","statuses-cw-max-chars":420,"statuses-max-chars":69,"statuses-media-max-files":1,"statuses-poll-max-options":1,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/root/store","storage-s3-access-key":"minio","storage-s3-bucket":"gts","storage-s3-endpoint":"localhost:9000","storage-s3-proxy":true,"storage-s3-secret-key":"miniostorage","storage-s3-use-ssl":false,"syslog-address":"127.0.0.1:6969","syslog-enabled":true,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","docker.host.local"],"username":"","web-asset-base-dir":"/root","web-template-base-dir":"/root"}'
|
EXPECT='{"account-domain":"peepee","accounts-allow-custom-css":true,"accounts-approval-required":false,"accounts-reason-required":false,"accounts-registration-open":true,"advanced-cookies-samesite":"strict","advanced-rate-limit-requests":6969,"advanced-throttling-multiplier":-1,"application-name":"gts","bind-address":"127.0.0.1","cache":{"gts":{"account-max-size":99,"account-sweep-freq":1000000000,"account-ttl":10800000000000,"block-max-size":100,"block-sweep-freq":10000000000,"block-ttl":300000000000,"domain-block-max-size":1000,"domain-block-sweep-freq":60000000000,"domain-block-ttl":86400000000000,"emoji-category-max-size":100,"emoji-category-sweep-freq":10000000000,"emoji-category-ttl":300000000000,"emoji-max-size":500,"emoji-sweep-freq":10000000000,"emoji-ttl":300000000000,"mention-max-size":500,"mention-sweep-freq":10000000000,"mention-ttl":300000000000,"notification-max-size":500,"notification-sweep-freq":10000000000,"notification-ttl":300000000000,"status-max-size":500,"status-sweep-freq":10000000000,"status-ttl":300000000000,"tombstone-max-size":100,"tombstone-sweep-freq":10000000000,"tombstone-ttl":300000000000,"user-max-size":100,"user-sweep-freq":10000000000,"user-ttl":300000000000}},"config-path":"internal/config/testdata/test.yaml","db-address":":memory:","db-database":"gotosocial_prod","db-password":"hunter2","db-port":6969,"db-tls-ca-cert":"","db-tls-mode":"disable","db-type":"sqlite","db-user":"sex-haver","dry-run":false,"email":"","host":"example.com","instance-deliver-to-shared-inboxes":false,"instance-expose-peers":true,"instance-expose-public-timeline":true,"instance-expose-suspended":true,"landing-page-user":"admin","letsencrypt-cert-dir":"/gotosocial/storage/certs","letsencrypt-email-address":"","letsencrypt-enabled":true,"letsencrypt-port":80,"log-db-queries":true,"log-level":"info","media-description-max-chars":5000,"media-description-min-chars":69,"media-emoji-local-max-size":420,"media-emoji-remote-max-size":420,"media-image-max-size":420,"media-remote-cache-days":30,"media-video-max-size":420,"oidc-client-id":"1234","oidc-client-secret":"shhhh its a secret","oidc-enabled":true,"oidc-idp-name":"sex-haver","oidc-issuer":"whoknows","oidc-link-existing":true,"oidc-scopes":["read","write"],"oidc-skip-verification":true,"password":"","path":"","port":6969,"protocol":"http","smtp-from":"queen.rip.in.piss@terfisland.org","smtp-host":"example.com","smtp-password":"hunter2","smtp-port":4269,"smtp-username":"sex-haver","software-version":"","statuses-cw-max-chars":420,"statuses-max-chars":69,"statuses-media-max-files":1,"statuses-poll-max-options":1,"statuses-poll-option-max-chars":50,"storage-backend":"local","storage-local-base-path":"/root/store","storage-s3-access-key":"minio","storage-s3-bucket":"gts","storage-s3-endpoint":"localhost:9000","storage-s3-proxy":true,"storage-s3-secret-key":"miniostorage","storage-s3-use-ssl":false,"syslog-address":"127.0.0.1:6969","syslog-enabled":true,"syslog-protocol":"udp","trusted-proxies":["127.0.0.1/32","docker.host.local"],"username":"","web-asset-base-dir":"/root","web-template-base-dir":"/root"}'
|
||||||
|
|
||||||
# Set all the environment variables to
|
# Set all the environment variables to
|
||||||
# ensure that these are parsed without panic
|
# ensure that these are parsed without panic
|
||||||
|
@ -76,6 +76,7 @@ GTS_SYSLOG_PROTOCOL='udp' \
|
||||||
GTS_SYSLOG_ADDRESS='127.0.0.1:6969' \
|
GTS_SYSLOG_ADDRESS='127.0.0.1:6969' \
|
||||||
GTS_ADVANCED_COOKIES_SAMESITE='strict' \
|
GTS_ADVANCED_COOKIES_SAMESITE='strict' \
|
||||||
GTS_ADVANCED_RATE_LIMIT_REQUESTS=6969 \
|
GTS_ADVANCED_RATE_LIMIT_REQUESTS=6969 \
|
||||||
|
GTS_ADVANCED_THROTTLING_MULTIPLIER=-1 \
|
||||||
go run ./cmd/gotosocial/... --config-path internal/config/testdata/test.yaml debug config)
|
go run ./cmd/gotosocial/... --config-path internal/config/testdata/test.yaml debug config)
|
||||||
|
|
||||||
OUTPUT_OUT=$(mktemp)
|
OUTPUT_OUT=$(mktemp)
|
||||||
|
|
|
@ -106,8 +106,9 @@ func InitTestConfig() {
|
||||||
SyslogProtocol: "udp",
|
SyslogProtocol: "udp",
|
||||||
SyslogAddress: "localhost:514",
|
SyslogAddress: "localhost:514",
|
||||||
|
|
||||||
AdvancedCookiesSamesite: "lax",
|
AdvancedCookiesSamesite: "lax",
|
||||||
AdvancedRateLimitRequests: 0, // disabled
|
AdvancedRateLimitRequests: 0, // disabled
|
||||||
|
AdvancedThrottlingMultiplier: 0, // disabled
|
||||||
|
|
||||||
SoftwareVersion: "0.0.0-testrig",
|
SoftwareVersion: "0.0.0-testrig",
|
||||||
|
|
||||||
|
|
Loading…
Reference in a new issue