-
Notifications
You must be signed in to change notification settings - Fork 57
/
Copy pathindex.js
114 lines (95 loc) · 3.06 KB
/
index.js
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
'use strict'
/**
* Module dependencies.
*/
const debug = require('debug')('koa-ratelimit')
const RedisLimiter = require('./limiter/redis')
const MemoryLimiter = require('./limiter/memory')
const ms = require('ms')
/**
* Expose `ratelimit()`.
*
* Initialize ratelimit middleware with the given `opts`:
*
* - `driver` redis or memory [redis]
* - `duration` limit duration in milliseconds [1 hour]
* - `max` max requests per `id` [2500]
* - `db` database connection if redis. Map instance if memory
* - `id` id to compare requests [ip]
* - `headers` custom header names
* - `remaining` remaining number of requests ['X-RateLimit-Remaining']
* - `reset` reset timestamp ['X-RateLimit-Reset']
* - `total` total number of requests ['X-RateLimit-Limit']
* - `whitelist` whitelist function [false]
* - `blacklist` blacklist function [false]
* - `throw` call ctx.throw if true
*
* @param {Object} opts
* @return {Function}
* @api public
*/
module.exports = function ratelimit (opts = {}) {
const defaultOpts = {
driver: 'redis',
duration: 60 * 60 * 1000, // 1 hour
max: 2500,
id: ctx => ctx.ip,
headers: {
remaining: 'X-RateLimit-Remaining',
reset: 'X-RateLimit-Reset',
total: 'X-RateLimit-Limit'
}
}
opts = { ...defaultOpts, ...opts }
const {
remaining = 'X-RateLimit-Remaining',
reset = 'X-RateLimit-Reset',
total = 'X-RateLimit-Limit'
} = opts.headers
return async function ratelimit (ctx, next) {
const id = opts.id(ctx)
const { driver } = opts
const whitelisted = typeof opts.whitelist === 'function' && await opts.whitelist(ctx)
const blacklisted = typeof opts.blacklist === 'function' && await opts.blacklist(ctx)
if (blacklisted) {
ctx.throw(403, 'Forbidden')
}
if (id === false || whitelisted) return await next()
// initialize limiter
let limiter
if (driver === 'memory') {
limiter = new MemoryLimiter({ ...opts, id })
} else if (driver === 'redis') {
limiter = new RedisLimiter({ ...opts, id })
} else {
throw new Error(`invalid driver. Expecting memory or redis, got ${driver}`)
}
// check limit
const limit = await limiter.get()
// check if current call is legit
const calls = limit.remaining > 0 ? limit.remaining - 1 : 0
// check if header disabled
const disableHeader = opts.disableHeader || false
let headers = {}
if (!disableHeader) {
// header fields
headers = {
[remaining]: calls,
[reset]: limit.reset,
[total]: limit.total
}
ctx.set(headers)
}
debug('remaining %s/%s %s', remaining, limit.total, id)
if (limit.remaining) return await next()
const delta = (limit.reset * 1000) - Date.now() | 0
const after = limit.reset - (Date.now() / 1000) | 0
ctx.set('Retry-After', after)
ctx.status = opts.status || 429
ctx.body = opts.errorMessage || `Rate limit exceeded, retry in ${ms(delta, { long: true })}.`
if (opts.throw) {
headers['Retry-After'] = after
ctx.throw(ctx.status, ctx.body, { headers })
}
}
}