mirror of
https://github.com/yu-i-i/overleaf-cep.git
synced 2026-05-23 17:19:37 +02:00
[web] last infrastructure conversions GitOrigin-RevId: ad1aff9b7df0610ed0303157d9e2c8032f32c02b
145 lines
4.0 KiB
JavaScript
145 lines
4.0 KiB
JavaScript
import Settings from '@overleaf/settings'
|
|
import Metrics from '@overleaf/metrics'
|
|
import logger from '@overleaf/logger'
|
|
import RedisWrapper from './RedisWrapper.mjs'
|
|
import RateLimiterFlexible from 'rate-limiter-flexible'
|
|
import OError from '@overleaf/o-error'
|
|
|
|
const rclient = RedisWrapper.client('ratelimiter')
|
|
|
|
/**
|
|
* Wrapper over the RateLimiterRedis class
|
|
*/
|
|
export class RateLimiter {
|
|
#opts
|
|
|
|
/**
|
|
* Create a rate limiter.
|
|
*
|
|
* @param name {string} The name that identifies this rate limiter. Different
|
|
* rate limiters must have different names.
|
|
* @param opts {object} Options to pass to RateLimiterRedis
|
|
*
|
|
* Some useful options:
|
|
*
|
|
* points - number of points that can be consumed over the given duration
|
|
* (default: 4)
|
|
* subnetPoints - number of points that can be consumed over the given
|
|
* duration accross a sub-network. This should only be used
|
|
* ip-based rate limits.
|
|
* duration - duration of the fixed window in seconds (default: 1)
|
|
* blockDuration - additional seconds to block after all points are consumed
|
|
* (default: 0)
|
|
*/
|
|
constructor(name, opts = {}) {
|
|
this.name = name
|
|
this.#opts = Object.assign({}, opts)
|
|
this._rateLimiter = new RateLimiterFlexible.RateLimiterRedis({
|
|
...opts,
|
|
keyPrefix: `rate-limit:${name}`,
|
|
storeClient: rclient,
|
|
})
|
|
if (opts.subnetPoints && !Settings.rateLimit?.subnetRateLimiterDisabled) {
|
|
this._subnetRateLimiter = new RateLimiterFlexible.RateLimiterRedis({
|
|
...opts,
|
|
points: opts.subnetPoints,
|
|
keyPrefix: `rate-limit:${name}`,
|
|
storeClient: rclient,
|
|
})
|
|
}
|
|
}
|
|
|
|
// Readonly access to the options, useful for aligning rate-limits.
|
|
getOptions() {
|
|
return Object.assign({}, this.#opts)
|
|
}
|
|
|
|
async consume(key, points = 1, options = { method: 'unknown' }) {
|
|
if (Settings.disableRateLimits) {
|
|
// Return a fake result in case it's used somewhere
|
|
return {
|
|
msBeforeNext: 0,
|
|
remainingPoints: 100,
|
|
consumedPoints: 0,
|
|
isFirstInDuration: false,
|
|
}
|
|
}
|
|
|
|
await this.consumeForRateLimiter(this._rateLimiter, key, options, points)
|
|
|
|
if (options.method === 'ip' && this._subnetRateLimiter) {
|
|
const subnetKey = this.getSubnetKeyFromIp(key)
|
|
await this.consumeForRateLimiter(
|
|
this._subnetRateLimiter,
|
|
subnetKey,
|
|
options,
|
|
points,
|
|
'ip-subnet'
|
|
)
|
|
}
|
|
}
|
|
|
|
async consumeForRateLimiter(rateLimiter, key, options, points, method) {
|
|
try {
|
|
const res = await rateLimiter.consume(key, points, options)
|
|
return res
|
|
} catch (err) {
|
|
if (err instanceof Error) {
|
|
throw err
|
|
} else {
|
|
// Only log the first time we exceed the rate limit for a given key and
|
|
// duration. This happens when the previous amount of consumed points
|
|
// was below the threshold.
|
|
if (err.consumedPoints - points <= rateLimiter.points) {
|
|
logger.warn({ path: this.name, key }, 'rate limit exceeded')
|
|
}
|
|
Metrics.inc('rate-limit-hit', 1, {
|
|
path: this.name,
|
|
method: method || options.method,
|
|
})
|
|
throw err
|
|
}
|
|
}
|
|
}
|
|
|
|
getSubnetKeyFromIp(ip) {
|
|
if (!/^(?:[0-9]{1,3}\.){3}[0-9]{1,3}$/.test(ip)) {
|
|
throw new OError(
|
|
'Cannot generate subnet key as the ip address is not of the expected format.',
|
|
{ ip }
|
|
)
|
|
}
|
|
|
|
return ip.split('.').slice(0, 3).join('.')
|
|
}
|
|
|
|
async delete(key) {
|
|
return await this._rateLimiter.delete(key)
|
|
}
|
|
}
|
|
|
|
/*
|
|
* Shared rate limiters
|
|
*/
|
|
|
|
export const openProjectRateLimiter = new RateLimiter('open-project', {
|
|
points: 15,
|
|
duration: 60,
|
|
})
|
|
|
|
// Keep in sync with the can-skip-captcha options.
|
|
export const overleafLoginRateLimiter = new RateLimiter(
|
|
'overleaf-login',
|
|
Settings.rateLimit?.login?.ip || {
|
|
points: 20,
|
|
subnetPoints: 200,
|
|
duration: 60,
|
|
}
|
|
)
|
|
|
|
export default {
|
|
RateLimiter,
|
|
openProjectRateLimiter,
|
|
overleafLoginRateLimiter,
|
|
}
|