Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Real-time collaboration for Jupyter Notebooks, Linux Terminals, LaTeX, VS Code, R IDE, and more,
all in one place. Commercial Alternative to JupyterHub.
Path: blob/master/src/packages/util/api/throttle.ts
Views: 791
/*1Generic throttling protocol for rate limiting api requests.23It limits the number of requests per second, minute and hour using a TTL4data structure and keeping track of all access of times during the interval.5*/67import TTLCache from "@isaacs/ttlcache";8import { plural } from "@cocalc/util/misc";910/*11We specify non-default throttling parameters for an endpoint *here* rather than in @cocalc/server,12so that we can enforce them in various places. E.g., by specifying them here,13we can enforce them both on the frontend and the backend with different semantics,14so the backend enforcement is only needed if the frontend client is somehow abusive15(i.e., not our client but one written by somebody else).1617CAREFUL: if you make a change it won't be reflected in all clients since they use18this hardcoded value, rather than an api endoint to get this.19*/2021const THROTTLE = {22"/accounts/get-names": {23second: 3,24minute: 50,25hour: 500,26},27"purchases/is-purchase-allowed": {28second: 7,29minute: 30,30hour: 300,31},32"purchases/stripe/get-payments": {33second: 3,34minute: 20,35hour: 150,36},37"purchases/stripe/get-customer-session": {38second: 1,39minute: 3,40hour: 40,41},42"purchases/get-purchases-admin": {43// extra generous for admin44second: 5,45minute: 100,46hour: 1000,47},48// i'm worried about abuse/bugs with message sending for now, so49// pretty aggressive throttling:50"user_query-messages": {51minute: 6,52hour: 100,53},5455// pretty limiting for now -- this only applies to sending messages via the api56"messages/send": {57second: 1,58minute: 5,59hour: 60,60},61} as const;6263const DEFAULTS = {64second: 3,65minute: 15,66hour: 200,67} as const;6869type Interval = keyof typeof DEFAULTS;7071const INTERVALS: Interval[] = ["second", "minute", "hour"] as const;7273const cache = {74second: new TTLCache<string, number[]>({75max: 100000,76ttl: 1000,77updateAgeOnGet: true,78}),79minute: new TTLCache<string, number[]>({80max: 100000,81ttl: 1000 * 60,82updateAgeOnGet: true,83}),84hour: new TTLCache<string, number[]>({85max: 100000,86ttl: 1000 * 1000 * 60,87updateAgeOnGet: true,88}),89};9091export default function throttle({92endpoint,93account_id,94}: {95endpoint: string;96// if not given, viewed as global97account_id?: string;98}) {99if (process["env"]?.["JEST_WORKER_ID"]) {100// do not throttle when testing.101return;102}103const key = `${account_id ? account_id : ""}:${endpoint}`;104const m = maxPerInterval(endpoint);105const now = Date.now();106for (const interval of INTERVALS) {107const c = cache[interval];108if (c == null) continue; // can't happen109const v = c.get(key);110if (v == null) {111c.set(key, [now]);112continue;113}114// process mutates v in place, so efficient115process(v, now, interval, m[interval], endpoint);116}117}118119const TO_MS = {120second: 1000,121minute: 1000 * 60,122hour: 1000 * 60 * 60,123} as const;124125function process(126v: number[],127now: number,128interval: Interval,129maxPerInterval: number,130endpoint: string,131) {132const cutoff = now - TO_MS[interval];133// mutate v so all numbers in it are >= cutoff:134for (let i = 0; i < v.length; i++) {135if (v[i] < cutoff) {136v.splice(i, 1);137i--; // Adjust index due to array mutation138}139}140if (v.length >= maxPerInterval) {141const wait = Math.ceil((v[0] - cutoff) / 1000);142const mesg = `too many requests to ${endpoint}; try again in ${wait} ${plural(wait, "second")} (rule: at most ${maxPerInterval} ${plural(maxPerInterval, "request")} per ${interval})`;143// console.trace(mesg);144throw Error(mesg);145}146v.push(now);147}148149function maxPerInterval(endpoint): {150second: number;151minute: number;152hour: number;153} {154const a = THROTTLE[endpoint];155if (a == null) {156return DEFAULTS;157}158return {159second: a["second"] ?? DEFAULTS.second,160minute: a["minute"] ?? DEFAULTS.minute,161hour: a["hour"] ?? DEFAULTS.hour,162};163}164165166