Book a Demo!
CoCalc Logo Icon
StoreFeaturesDocsShareSupportNewsAboutPoliciesSign UpSign In
sagemathinc
GitHub Repository: sagemathinc/cocalc
Path: blob/master/src/packages/util/api/throttle.ts
1447 views
1
/*
2
Generic throttling protocol for rate limiting api requests.
3
4
It limits the number of requests per second, minute and hour using a TTL
5
data structure and keeping track of all access of times during the interval.
6
*/
7
8
import TTLCache from "@isaacs/ttlcache";
9
import { plural } from "@cocalc/util/misc";
10
11
/*
12
We specify non-default throttling parameters for an endpoint *here* rather than in @cocalc/server,
13
so that we can enforce them in various places. E.g., by specifying them here,
14
we can enforce them both on the frontend and the backend with different semantics,
15
so the backend enforcement is only needed if the frontend client is somehow abusive
16
(i.e., not our client but one written by somebody else).
17
18
CAREFUL: if you make a change it won't be reflected in all clients since they use
19
this hardcoded value, rather than an api endoint to get this.
20
*/
21
22
const THROTTLE = {
23
"/accounts/get-names": {
24
second: 3,
25
minute: 50,
26
hour: 500,
27
},
28
"compute/get-servers": {
29
second: 5,
30
minute: 50,
31
hour: 500,
32
},
33
"compute/is-dns-available": {
34
second: 3,
35
minute: 80,
36
hour: 500,
37
},
38
"compute/get-servers-by-id": {
39
second: 15,
40
minute: 100,
41
hour: 1000,
42
},
43
"purchases/is-purchase-allowed": {
44
second: 7,
45
minute: 30,
46
hour: 300,
47
},
48
"purchases/stripe/get-payments": {
49
second: 3,
50
minute: 20,
51
hour: 150,
52
},
53
"purchases/stripe/get-customer-session": {
54
second: 1,
55
minute: 3,
56
hour: 40,
57
},
58
"purchases/get-purchases-admin": {
59
// extra generous for admin
60
second: 5,
61
minute: 100,
62
hour: 1000,
63
},
64
// i'm worried about abuse/bugs with message sending for now, so
65
// pretty aggressive throttling:
66
"user_query-messages": {
67
minute: 6,
68
hour: 100,
69
},
70
71
// pretty limiting for now -- this only applies to sending messages via the api
72
"messages/send": {
73
second: 1,
74
minute: 5,
75
hour: 60,
76
},
77
} as const;
78
79
const DEFAULTS = {
80
second: 3,
81
minute: 15,
82
hour: 200,
83
} as const;
84
85
type Interval = keyof typeof DEFAULTS;
86
87
const INTERVALS: Interval[] = ["second", "minute", "hour"] as const;
88
89
const cache = {
90
second: new TTLCache<string, number[]>({
91
max: 100000,
92
ttl: 1000,
93
updateAgeOnGet: true,
94
}),
95
minute: new TTLCache<string, number[]>({
96
max: 100000,
97
ttl: 1000 * 60,
98
updateAgeOnGet: true,
99
}),
100
hour: new TTLCache<string, number[]>({
101
max: 100000,
102
ttl: 1000 * 1000 * 60,
103
updateAgeOnGet: true,
104
}),
105
};
106
107
export default function throttle({
108
endpoint,
109
account_id,
110
}: {
111
endpoint: string;
112
// if not given, viewed as global
113
account_id?: string;
114
}) {
115
if (process["env"]?.["JEST_WORKER_ID"]) {
116
// do not throttle when testing.
117
return;
118
}
119
const key = `${account_id ? account_id : ""}:${endpoint}`;
120
const m = maxPerInterval(endpoint);
121
const now = Date.now();
122
for (const interval of INTERVALS) {
123
const c = cache[interval];
124
if (c == null) continue; // can't happen
125
const v = c.get(key);
126
if (v == null) {
127
c.set(key, [now]);
128
continue;
129
}
130
// process mutates v in place, so efficient
131
process(v, now, interval, m[interval], endpoint);
132
}
133
}
134
135
const TO_MS = {
136
second: 1000,
137
minute: 1000 * 60,
138
hour: 1000 * 60 * 60,
139
} as const;
140
141
function process(
142
v: number[],
143
now: number,
144
interval: Interval,
145
maxPerInterval: number,
146
endpoint: string,
147
) {
148
const cutoff = now - TO_MS[interval];
149
// mutate v so all numbers in it are >= cutoff:
150
for (let i = 0; i < v.length; i++) {
151
if (v[i] < cutoff) {
152
v.splice(i, 1);
153
i--; // Adjust index due to array mutation
154
}
155
}
156
if (v.length >= maxPerInterval) {
157
const wait = Math.ceil((v[0] - cutoff) / 1000);
158
const mesg = `too many requests to ${endpoint}; try again in ${wait} ${plural(wait, "second")} (rule: at most ${maxPerInterval} ${plural(maxPerInterval, "request")} per ${interval})`;
159
// console.trace(mesg);
160
throw Error(mesg);
161
}
162
v.push(now);
163
}
164
165
function maxPerInterval(endpoint): {
166
second: number;
167
minute: number;
168
hour: number;
169
} {
170
const a = THROTTLE[endpoint];
171
if (a == null) {
172
return DEFAULTS;
173
}
174
return {
175
second: a["second"] ?? DEFAULTS.second,
176
minute: a["minute"] ?? DEFAULTS.minute,
177
hour: a["hour"] ?? DEFAULTS.hour,
178
};
179
}
180
181