Files
speckle-server/packages/server/modules/core/services/ratelimits.js
T
Gergő Jedlicska 3d6653f73b hotfix/2.9.2 (#1175)
* Update to new specklepy (#1173)

* Publish images for all branches but limit tagging
* only tag 'latest' and '2' when 'SHOULD_PUBLISH' variable is 'true'

* Publishing helm chart should check for `SHOULD_PUBLISH`

* Move blocking step to publish-helm chart, and allow images to be published

* Pin python requirements and bump to latest versions

* Fix EOL whitespace

* use valid version for psycopg2-binary (the clue is in the 2!)

* fix(fileimports): add exception printing to file imports

* fix(fileimports): bump specklepy version

move to a specklepy version that contains a fix for send without writing to disk

Co-authored-by: Iain Sproat <68657+iainsproat@users.noreply.github.com>

* Fixes liveness and readiness checks to prevent CSRF error message (#1169)

- provides content-type header
- check that status code is 200

* Fixes broken helm template by adding quotation marks around liveness probe command (#1171)

* fix(server activities): make sure the stream events are properly dispatched

* feat(server webhooks): add scheduled orphaned webhook cleanup

* test(server webhooks): add test to webhook cleanup service

* feat(server webhooks): drop foreign key reference for webhooks schema to streams

* refactor(server req context): refactor req context to have the ip attribute for all requests

* feat(server objects rest api): add ratelimits to objects rest api endpoints

* fix(server rest api): properly handle returning 419

Co-authored-by: Iain Sproat <68657+iainsproat@users.noreply.github.com>
2022-11-09 13:23:32 +01:00

117 lines
4.0 KiB
JavaScript

'use strict'
const knex = require('@/db/knex')
const RatelimitActions = () => knex('ratelimit_actions')
const prometheusClient = require('prom-client')
const limitsReached = new prometheusClient.Counter({
name: 'speckle_server_blocked_ratelimit',
help: 'Number of time the requests were blocked',
labelNames: ['actionName']
})
const LIMITS = {
// rate limits:
USER_CREATE: parseInt(process.env.RATELIMIT_USER_CREATE) || 1000, // per week
STREAM_CREATE: parseInt(process.env.RATELIMIT_STREAM_CREATE) || 10000, // per week (1 stream / minute average)
COMMIT_CREATE: parseInt(process.env.RATELIMIT_COMMIT_CREATE) || 86400, // per day (1 commit every second average)
// unused:
SUBSCRIPTION: parseInt(process.env.RATELIMIT_SUBSCRIPTION) || 600, // per minute
REST_API: parseInt(process.env.RATELIMIT_REST_API) || 2400, // per minute
WEBHOOKS: parseInt(process.env.RATELIMIT_WEBHOOKS) || 1000, // per day
PREVIEWS: parseInt(process.env.RATELIMIT_PREVIEWS) || 1000, // per day
FILE_UPLOADS: parseInt(process.env.RATELIMIT_FILE_UPLOADS) || 1000, // per day
// static limits:
BRANCHES: parseInt(process.env.LIMIT_BRANCHES) || 1000, // per stream
TOKENS: parseInt(process.env.LIMIT_TOKENS) || 1000, // per user
ACTIVE_SUBSCRIPTIONS: parseInt(process.env.LIMIT_ACTIVE_SUBSCRIPTIONS) || 100, // per user
ACTIVE_CONNECTIONS: parseInt(process.env.LIMIT_ACTIVE_CONNECTIONS) || 100, // per source ip
'POST /api/getobjects/:streamId': 200, // for 1 minute
'POST /api/diff/:streamId': 200, // for 1 minute
'POST /objects/:streamId': 200, // for 1 minute
'GET /objects/:streamId/:objectId': 200, // for 1 minute
'GET /objects/:streamId/:objectId/single': 200 // for 1 minute
}
const LIMIT_INTERVAL = {
// rate limits
USER_CREATE: 7 * 24 * 3600,
STREAM_CREATE: 7 * 24 * 3600,
COMMIT_CREATE: 24 * 3600,
SUBSCRIPTION: 60,
REST_API: 60,
WEBHOOKS: 24 * 3600,
PREVIEWS: 24 * 3600,
FILE_UPLOADS: 24 * 3600,
// static limits:
BRANCHES: 0,
TOKENS: 0,
ACTIVE_SUBSCRIPTIONS: 0,
ACTIVE_CONNECTIONS: 0,
'POST /api/getobjects/:streamId': 60,
'POST /api/diff/:streamId': 60,
'POST /objects/:streamId': 60,
'GET /objects/:streamId/:objectId': 60,
'GET /objects/:streamId/:objectId/single': 60
}
const rateLimitedCache = {}
async function shouldRateLimitNext({ action, source }) {
if (!source) return false
const limit = LIMITS[action]
const checkInterval = LIMIT_INTERVAL[action]
if (limit === undefined || checkInterval === undefined) {
return false
}
let startTimeMs
if (checkInterval === 0) startTimeMs = 0
else startTimeMs = Date.now() - checkInterval * 1000
const [res] = await RatelimitActions()
.count()
.where({ action, source })
.andWhere('timestamp', '>', new Date(startTimeMs))
const count = parseInt(res.count) + 1 // plus this request
const shouldRateLimit = count >= limit
if (!shouldRateLimit) {
await RatelimitActions().insert({ action, source })
}
return shouldRateLimit
}
// returns true if the action is fine, false if it should be blocked because of exceeding limit
async function respectsLimits({ action, source }) {
const rateLimitKey = `${action} ${source}`
const promise = shouldRateLimitNext({ action, source }).then((shouldRateLimit) => {
if (shouldRateLimit) rateLimitedCache[rateLimitKey] = true
else delete rateLimitedCache[rateLimitKey]
})
if (rateLimitedCache[rateLimitKey]) {
await promise
}
if (rateLimitedCache[rateLimitKey]) limitsReached.labels(action).inc()
return !rateLimitedCache[rateLimitKey]
}
async function rejectsRequestWithRatelimitStatusIfNeeded({ action, req, res }) {
const source = req.context.userId || req.context.ip
if (!(await respectsLimits({ action, source })))
return res.status(429).set('X-Speckle-Meditation', 'https://http.cat/429').send({
err: 'You are sending too many requests. You have been rate limited. Please try again later.'
})
}
module.exports = {
LIMITS,
LIMIT_INTERVAL,
respectsLimits,
rejectsRequestWithRatelimitStatusIfNeeded
}