-
Notifications
You must be signed in to change notification settings - Fork 18
/
index.js
135 lines (104 loc) · 3.37 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
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
'use strict';
const Boom = require('boom');
const Joi = require('joi');
const util = require('util');
const RateLimitHeaders = require('./rate_limit_headers');
const schema = Joi.object().keys({
event: Joi.any().valid(['onRequest', 'onPreAuth', 'onPostAuth', 'onPreHandler']),
type: [Joi.string(), Joi.func()],
limitd: Joi.object(),
onError: Joi.func(),
extractKey: Joi.func()
}).requiredKeys('type', 'event', 'limitd', 'extractKey');
function setResponseHeader({ response }, header, value) {
if (!response) { return; }
if (response.isBoom) {
response.output.headers[header] = value;
} else {
response.header(header, value);
}
}
function checkLimitsIfSet(request, h) {
const requestLimit = request.plugins.patova && request.plugins.patova.limit;
if (requestLimit && requestLimit.conformant){
const headers = new RateLimitHeaders(
requestLimit.limit,
requestLimit.remaining,
requestLimit.reset
);
Object
.keys(headers)
.forEach(key => setResponseHeader(request, key, headers[key]));
}
return h.continue;
}
function getMinimumLimit(limit1, limit2) {
if (!limit1) { return limit2; }
if (!limit2) { return limit1; }
if (limit2.remaining > limit1.remaining) {
return limit1;
}
return limit2;
}
function buildRateLimitEvent({ extractKey, limitd, onError, type }) {
const flowControl = { continue: Symbol('CONTINUE') };
async function getType(request) {
if (typeof type !== 'function') {
return type;
}
try {
return await type(request, flowControl);
} catch (err) {
throw Boom.wrap(err, 500, 'cannot get bucket type');
}
}
const takeAsync = limitd && util.promisify(limitd.take.bind(limitd));
return async function onRateLimit(request, h) {
if (!limitd) {
// limitd is not connected, do not fail!
return h.continue;
}
const bucketType = await getType(request);
const key = await extractKey(request, flowControl);
if (bucketType === flowControl.continue || key === flowControl.continue) {
return h.continue;
}
let currentLimitResponse;
try {
currentLimitResponse = await takeAsync(bucketType, key);
} catch (err) {
if (onError) { return onError(err, h); }
// by default we don't fail if limitd is unavailable
return h.continue;
}
const oldMinimumLimitResponse = request.plugins.patova && request.plugins.patova.limit
const newMinimumLimitResponse = getMinimumLimit(currentLimitResponse, oldMinimumLimitResponse)
request.plugins.patova = request.plugins.patova || {};
request.plugins.patova.limit = newMinimumLimitResponse;
if (newMinimumLimitResponse.conformant) {
// We continue only if the request is conformat so far
return h.continue;
}
const error = Boom.tooManyRequests();
error.output.headers = new RateLimitHeaders(
newMinimumLimitResponse.limit,
newMinimumLimitResponse.remaining,
newMinimumLimitResponse.reset);
throw error;
}
}
async function register(server, options) {
const {
error, value: { event, ...rateLimitConfig }
} = await Joi.validate(options, schema, { abortEarly: false });
if (error) {
throw error;
}
server.ext('onPreResponse', checkLimitsIfSet);
server.ext(event, buildRateLimitEvent(rateLimitConfig));
}
module.exports = {
pkg: require('../package.json'),
register,
multiple: true
};