Skip to content

Commit b0d9e19

Browse files
authored
Merge pull request #31 from ivanitskiy/master
[add] http/rate-limit/simple example
2 parents 337866f + ba33771 commit b0d9e19

File tree

3 files changed

+204
-0
lines changed

3 files changed

+204
-0
lines changed

README.rst

+111
Original file line numberDiff line numberDiff line change
@@ -1349,6 +1349,117 @@ Checking:
13491349
127.0.0.2 [22/Nov/2021:18:20:24 +0000] 1
13501350
127.0.0.2 [22/Nov/2021:18:20:25 +0000] 2
13511351
1352+
Shared Dictionary
1353+
-----------------
1354+
1355+
HTTP Rate limit[http/rate-limit/simple]
1356+
~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
1357+
1358+
In this example `js_shared_dict_zone <https://nginx.org/en/docs/http/ngx_http_js_module.html#js_shared_dict_zone>`_ is used to implement a simple rate limit and can be set in different contexts.
1359+
The rate limit is implemented using a shared dictionary zone and a simple javascript function that is called for each request and increments the counter for the current window.
1360+
If the counter exceeds the limit, the function returns the number of seconds until the end of the window. The function is called using
1361+
`js_set <https://nginx.org/en/docs/http/ngx_http_js_module.html#js_set>`_ and the result is stored in a variable that is used to return a 429 response if the limit is exceeded.
1362+
1363+
nginx.conf:
1364+
1365+
.. code-block:: nginx
1366+
1367+
http {
1368+
js_path "/etc/nginx/njs/";
1369+
js_import main from http/rate-limit/simple.js;
1370+
# optionally set timeout so NJS resets and deletes all data for ratelimit counters
1371+
js_shared_dict_zone zone=kv:1M timeout=3600s evict;
1372+
1373+
server {
1374+
listen 80;
1375+
server_name www.example.com;
1376+
# access_log off;
1377+
js_var $rl_zone_name kv; # shared dict zone name; requred variable
1378+
js_var $rl_windows_ms 30000; # optional window in miliseconds; default 1 minute window if not set
1379+
js_var $rl_limit 10; # optional limit for the window; default 10 requests if not set
1380+
js_var $rl_key $remote_addr; # rate limit key; default remote_addr if not set
1381+
js_set $rl_result main.ratelimit; # call ratelimit function that returns retry-after value if limit is exceeded
1382+
1383+
location = / {
1384+
# test rate limit result
1385+
if ($rl_result != "0") {
1386+
add_header Retry-After $rl_result always;
1387+
return 429 "Too Many Requests.";
1388+
}
1389+
# Your normal processing here
1390+
return 200 "hello world";
1391+
}
1392+
}
1393+
}
1394+
1395+
example.js:
1396+
1397+
.. code-block:: js
1398+
1399+
const defaultResponse = "0";
1400+
function ratelimit(r) {
1401+
const zone = r.variables['rl_zone_name'];
1402+
const kv = zone && ngx.shared && ngx.shared[zone];
1403+
if (!kv) {
1404+
r.log(`ratelimit: ${zone} js_shared_dict_zone not found`);
1405+
return defaultResponse;
1406+
}
1407+
1408+
const key = r.variables['rl_key'] || r.variables['remote_addr'];
1409+
const window = Number(r.variables['rl_windows_ms']) || 60000;
1410+
const limit = Number(r.variables['rl_limit']) || 10;
1411+
const now = Date.now();
1412+
1413+
let requestData = kv.get(key);
1414+
if (requestData === undefined || requestData.length === 0) {
1415+
requestData = { timestamp: now, count: 1 }
1416+
kv.set(key, JSON.stringify(requestData));
1417+
return defaultResponse;
1418+
}
1419+
try {
1420+
requestData = JSON.parse(requestData);
1421+
} catch (e) {
1422+
requestData = { timestamp: now, count: 1 }
1423+
kv.set(key, JSON.stringify(requestData));
1424+
return defaultResponse;
1425+
}
1426+
if (!requestData) {
1427+
requestData = { timestamp: now, count: 1 }
1428+
kv.set(key, JSON.stringify(requestData));
1429+
return defaultResponse;
1430+
}
1431+
if (now - requestData.timestamp >= window) {
1432+
requestData.timestamp = now;
1433+
requestData.count = 1;
1434+
} else {
1435+
requestData.count++;
1436+
}
1437+
const elapsed = now - requestData.timestamp;
1438+
r.log(`limit: ${limit} window: ${window} elapsed: ${elapsed} count: ${requestData.count} timestamp: ${requestData.timestamp}`)
1439+
let retryAfter = 0;
1440+
if (requestData.count > limit) {
1441+
retryAfter = Math.ceil((window - elapsed) / 1000);
1442+
}
1443+
kv.set(key, JSON.stringify(requestData));
1444+
return retryAfter.toString();
1445+
}
1446+
1447+
export default { ratelimit };
1448+
1449+
1450+
.. code-block:: shell
1451+
1452+
curl http://localhost
1453+
200 hello world
1454+
1455+
curl http://localhost
1456+
200 hello world
1457+
1458+
# 3rd request should fail according to the rate limit $rl_limit=2
1459+
curl http://localhost
1460+
429 rate limit exceeded
1461+
1462+
13521463
NGINX-PLUS API
13531464
--------------
13541465

conf/http/rate-limit/simple.conf

+33
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
load_module modules/ngx_http_js_module.so;
2+
3+
error_log /dev/stdout debug;
4+
5+
events { }
6+
7+
http {
8+
js_path "/etc/nginx/njs/";
9+
js_import main from http/rate-limit/simple.js;
10+
# optionally set timeout so NJS resets and deletes all data for ratelimit counters
11+
js_shared_dict_zone zone=kv:1M timeout=3600s evict;
12+
13+
server {
14+
listen 80;
15+
server_name www.example.com;
16+
# access_log off;
17+
js_var $rl_zone_name kv; # shared dict zone name; requred variable
18+
js_var $rl_windows_ms 30000; # optional window in miliseconds; default 1 minute window if not set
19+
js_var $rl_limit 10; # optional limit for the window; default 10 requests if not set
20+
js_var $rl_key $remote_addr; # rate limit key; default remote_addr if not set
21+
js_set $rl_result main.ratelimit; # call ratelimit function that returns retry-after value if limit is exceeded
22+
23+
location = / {
24+
# test rate limit result
25+
if ($rl_result != "0") {
26+
add_header Retry-After $rl_result always;
27+
return 429 "Too Many Requests.";
28+
}
29+
# Your normal processing here
30+
return 200 "hello world";
31+
}
32+
}
33+
}

njs/http/rate-limit/simple.js

+60
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,60 @@
1+
const defaultResponse = "0";
2+
3+
/**
4+
* Applies rate limiting logic for the request.
5+
*
6+
* @param {Object} r - The request object.
7+
* @returns {string} - The retry-after value in seconds as a string. '0' means no reate limit.
8+
*/
9+
function ratelimit(r) {
10+
const zone = r.variables['rl_zone_name'];
11+
const kv = zone && ngx.shared && ngx.shared[zone];
12+
if (!kv) {
13+
r.log(`ratelimit: ${zone} js_shared_dict_zone not found`);
14+
return defaultResponse;
15+
}
16+
17+
const key = r.variables['rl_key'] || r.variables['remote_addr'];
18+
const window = Number(r.variables['rl_windows_ms']) || 60000;
19+
const limit = Number(r.variables['rl_limit']) || 10;
20+
const now = Date.now();
21+
22+
let requestData = kv.get(key);
23+
if (requestData === undefined || requestData.length === 0) {
24+
r.log(`ratelimit: setting initial value for ${key}`);
25+
requestData = { timestamp: now, count: 1 }
26+
kv.set(key, JSON.stringify(requestData));
27+
return defaultResponse;
28+
}
29+
try {
30+
requestData = JSON.parse(requestData);
31+
} catch (e) {
32+
r.log(`ratelimit: failed to parse value for ${key}`);
33+
requestData = { timestamp: now, count: 1 }
34+
kv.set(key, JSON.stringify(requestData));
35+
return defaultResponse;
36+
}
37+
if (!requestData) {
38+
// remember the first request
39+
r.log(`ratelimit: value for ${key} was not set`);
40+
requestData = { timestamp: now, count: 1 }
41+
kv.set(key, JSON.stringify(requestData));
42+
return defaultResponse;
43+
}
44+
if (now - requestData.timestamp >= window) {
45+
requestData.timestamp = now;
46+
requestData.count = 1;
47+
} else {
48+
requestData.count++;
49+
}
50+
const elapsed = now - requestData.timestamp;
51+
r.log(`limit: ${limit} window: ${window} elapsed: ${elapsed} count: ${requestData.count} timestamp: ${requestData.timestamp}`)
52+
let retryAfter = 0;
53+
if (requestData.count > limit) {
54+
retryAfter = Math.ceil((window - elapsed) / 1000);
55+
}
56+
kv.set(key, JSON.stringify(requestData));
57+
return retryAfter.toString();
58+
}
59+
60+
export default { ratelimit };

0 commit comments

Comments
 (0)