Commit ·
adae8b1
1
Parent(s): de10cbd
update rate-limit
Browse files
ndcc/server/index.js
CHANGED
|
@@ -599,7 +599,7 @@ module.exports = async (app) =>{
|
|
| 599 |
}
|
| 600 |
},
|
| 601 |
{
|
| 602 |
-
plugin:require('./plugins/rateLimit.js'), //upload
|
| 603 |
options:{}
|
| 604 |
},
|
| 605 |
{
|
|
|
|
| 599 |
}
|
| 600 |
},
|
| 601 |
{
|
| 602 |
+
plugin:require('./plugins/rateLimit/index.js'), //upload
|
| 603 |
options:{}
|
| 604 |
},
|
| 605 |
{
|
ndcc/server/lib/index.js
CHANGED
|
@@ -136,8 +136,12 @@ exports.plugin = {
|
|
| 136 |
path: '/test',
|
| 137 |
options:{
|
| 138 |
plugins:{
|
| 139 |
-
'
|
| 140 |
-
|
|
|
|
|
|
|
|
|
|
|
|
|
| 141 |
}
|
| 142 |
}
|
| 143 |
},
|
|
@@ -226,7 +230,7 @@ exports.plugin = {
|
|
| 226 |
server.route({
|
| 227 |
method: 'GET',
|
| 228 |
path: '/_next/{p*}',
|
| 229 |
-
options:{plugins: {'
|
| 230 |
handler: async ({raw},h)=>{
|
| 231 |
await handler(raw.req,raw.res);
|
| 232 |
if (raw.res.headersSent) { return h.close;}
|
|
@@ -238,7 +242,7 @@ exports.plugin = {
|
|
| 238 |
server.route({
|
| 239 |
method: 'GET',
|
| 240 |
path: '/pwa/{p*}',
|
| 241 |
-
options:{plugins: {'
|
| 242 |
handler: async ({ url}, h) => {
|
| 243 |
try {
|
| 244 |
let pathname = url.pathname;
|
|
@@ -257,7 +261,7 @@ exports.plugin = {
|
|
| 257 |
server.route({
|
| 258 |
method: 'GET',
|
| 259 |
path: '/sw.js',
|
| 260 |
-
options:{plugins: {'
|
| 261 |
handler: async ({ url}, h) => {
|
| 262 |
try {
|
| 263 |
let pathname = url.pathname;
|
|
@@ -615,10 +619,10 @@ exports.plugin = {
|
|
| 615 |
auth: 'auth',
|
| 616 |
plugins: {
|
| 617 |
//限制用户每5s只能上传一次
|
| 618 |
-
"
|
| 619 |
"userPathLimit":Number(uploadRequestsPerSecond.split('/')[0]),
|
| 620 |
"userPathCache":{
|
| 621 |
-
"segment":`
|
| 622 |
"expiresIn":Number(uploadRequestsPerSecond.split('/')[1])
|
| 623 |
}
|
| 624 |
}
|
|
|
|
| 136 |
path: '/test',
|
| 137 |
options:{
|
| 138 |
plugins:{
|
| 139 |
+
'rate-limit': {
|
| 140 |
+
"userPathLimit":1,
|
| 141 |
+
"userPathCache":{
|
| 142 |
+
"segment":"ZhIWjVHdVLruss7eFn",
|
| 143 |
+
"expiresIn":1000
|
| 144 |
+
}
|
| 145 |
}
|
| 146 |
}
|
| 147 |
},
|
|
|
|
| 230 |
server.route({
|
| 231 |
method: 'GET',
|
| 232 |
path: '/_next/{p*}',
|
| 233 |
+
options:{plugins: {'rate-limit': {enabled: false } }},
|
| 234 |
handler: async ({raw},h)=>{
|
| 235 |
await handler(raw.req,raw.res);
|
| 236 |
if (raw.res.headersSent) { return h.close;}
|
|
|
|
| 242 |
server.route({
|
| 243 |
method: 'GET',
|
| 244 |
path: '/pwa/{p*}',
|
| 245 |
+
options:{plugins: {'rate-limit': { enabled: false}}},
|
| 246 |
handler: async ({ url}, h) => {
|
| 247 |
try {
|
| 248 |
let pathname = url.pathname;
|
|
|
|
| 261 |
server.route({
|
| 262 |
method: 'GET',
|
| 263 |
path: '/sw.js',
|
| 264 |
+
options:{plugins: {'rate-limit': { enabled: false}}},
|
| 265 |
handler: async ({ url}, h) => {
|
| 266 |
try {
|
| 267 |
let pathname = url.pathname;
|
|
|
|
| 619 |
auth: 'auth',
|
| 620 |
plugins: {
|
| 621 |
//限制用户每5s只能上传一次
|
| 622 |
+
"rate-limit": {
|
| 623 |
"userPathLimit":Number(uploadRequestsPerSecond.split('/')[0]),
|
| 624 |
"userPathCache":{
|
| 625 |
+
"segment":`rate-limit-user-upload-${server.info.port}`,
|
| 626 |
"expiresIn":Number(uploadRequestsPerSecond.split('/')[1])
|
| 627 |
}
|
| 628 |
}
|
ndcc/server/plugins/{rateLimit.js → rateLimit/index.js}
RENAMED
|
@@ -1,6 +1,6 @@
|
|
| 1 |
|
| 2 |
'use strict';
|
| 3 |
-
const {Error} = require('../lib/think');
|
| 4 |
exports.plugin = {
|
| 5 |
pkg: {
|
| 6 |
"name": "rateLimit",
|
|
@@ -9,8 +9,7 @@ exports.plugin = {
|
|
| 9 |
register: async function (server, options) {
|
| 10 |
const {requestsPerSecond} = server.settings.app.systemConfig.Config
|
| 11 |
|
| 12 |
-
//使用的hapi-rate-limit插件,再次封装而已 参考 https://www.npmjs.com/package/
|
| 13 |
-
const pkgName = "hapi-rate-limit"
|
| 14 |
await server.ext('onPreAuth', function (request, h) {
|
| 15 |
// if(request.route.path === "/api/{action*}"){
|
| 16 |
// // console.log(requestsPerSecond,'============')
|
|
@@ -19,10 +18,10 @@ exports.plugin = {
|
|
| 19 |
//无需单独写,后台路由配置直接覆盖即可,参考
|
| 20 |
// "options":{
|
| 21 |
// "plugins": {
|
| 22 |
-
// "
|
| 23 |
// "userPathLimit":1,
|
| 24 |
// "userPathCache":{
|
| 25 |
-
// "segment":"ZhIWjVHdVLru7eFn",
|
| 26 |
// "expiresIn":1000 //过期时间
|
| 27 |
// }
|
| 28 |
// }
|
|
@@ -34,7 +33,7 @@ exports.plugin = {
|
|
| 34 |
});
|
| 35 |
|
| 36 |
await server.register({
|
| 37 |
-
plugin:require('
|
| 38 |
options: {
|
| 39 |
enabled:true,
|
| 40 |
trustProxy:true,
|
|
@@ -42,7 +41,7 @@ exports.plugin = {
|
|
| 42 |
userPathLimit:false, //每个用户每个时间段在给定路径上可以发出的请求总数
|
| 43 |
userLimit:Number(requestsPerSecond.split('/')[0]), //用户每个时间段可以发出的总请求数
|
| 44 |
userCache:{
|
| 45 |
-
segment:`
|
| 46 |
expiresIn:Number(requestsPerSecond.split('/')[1])
|
| 47 |
},
|
| 48 |
}
|
|
|
|
| 1 |
|
| 2 |
'use strict';
|
| 3 |
+
const {Error} = require('../../lib/think');
|
| 4 |
exports.plugin = {
|
| 5 |
pkg: {
|
| 6 |
"name": "rateLimit",
|
|
|
|
| 9 |
register: async function (server, options) {
|
| 10 |
const {requestsPerSecond} = server.settings.app.systemConfig.Config
|
| 11 |
|
| 12 |
+
//使用的hapi-rate-limit插件,再次封装而已,pak name吸修改为rate-limit 参考 https://www.npmjs.com/package/rate-limit
|
|
|
|
| 13 |
await server.ext('onPreAuth', function (request, h) {
|
| 14 |
// if(request.route.path === "/api/{action*}"){
|
| 15 |
// // console.log(requestsPerSecond,'============')
|
|
|
|
| 18 |
//无需单独写,后台路由配置直接覆盖即可,参考
|
| 19 |
// "options":{
|
| 20 |
// "plugins": {
|
| 21 |
+
// "rate-limit": {
|
| 22 |
// "userPathLimit":1,
|
| 23 |
// "userPathCache":{
|
| 24 |
+
// "segment":"ZhIWjVHdVLru7eFn", //填写路由id就行了
|
| 25 |
// "expiresIn":1000 //过期时间
|
| 26 |
// }
|
| 27 |
// }
|
|
|
|
| 33 |
});
|
| 34 |
|
| 35 |
await server.register({
|
| 36 |
+
plugin:require('./lib/index'),
|
| 37 |
options: {
|
| 38 |
enabled:true,
|
| 39 |
trustProxy:true,
|
|
|
|
| 41 |
userPathLimit:false, //每个用户每个时间段在给定路径上可以发出的请求总数
|
| 42 |
userLimit:Number(requestsPerSecond.split('/')[0]), //用户每个时间段可以发出的总请求数
|
| 43 |
userCache:{
|
| 44 |
+
segment:`ndcc-rate-limit-user-${server.info.port}`,
|
| 45 |
expiresIn:Number(requestsPerSecond.split('/')[1])
|
| 46 |
},
|
| 47 |
}
|
ndcc/server/plugins/rateLimit/lib/index.js
ADDED
|
@@ -0,0 +1,120 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
'use strict'
|
| 2 |
+
|
| 3 |
+
const Joi = require('joi')
|
| 4 |
+
const Pkg = {
|
| 5 |
+
name:"rate-limit"
|
| 6 |
+
}
|
| 7 |
+
|
| 8 |
+
const internals = require('./internals')
|
| 9 |
+
|
| 10 |
+
const register = function (plugin, options) {
|
| 11 |
+
const pluginSettings = Joi.attempt(Object.assign({}, options), internals.schema)
|
| 12 |
+
|
| 13 |
+
// we call toString on the user attribute in getUser, so we have to do it here too.
|
| 14 |
+
pluginSettings.userWhitelist = pluginSettings.userWhitelist.map(user => user.toString())
|
| 15 |
+
|
| 16 |
+
const userCache = plugin.cache(pluginSettings.userCache)
|
| 17 |
+
const pathCache = plugin.cache(pluginSettings.pathCache)
|
| 18 |
+
const userPathCache = plugin.cache(pluginSettings.userPathCache)
|
| 19 |
+
const authCache = plugin.cache(pluginSettings.authCache)
|
| 20 |
+
|
| 21 |
+
// called regardless if authentication is performed, before authentication is performed
|
| 22 |
+
plugin.ext('onPreAuth', async (request, h) => {
|
| 23 |
+
const routeSettings = request.route.settings.plugins[internals.pluginName] || {}
|
| 24 |
+
|
| 25 |
+
delete routeSettings.userCache
|
| 26 |
+
|
| 27 |
+
if (routeSettings.userLimit !== false) {
|
| 28 |
+
delete routeSettings.userLimit
|
| 29 |
+
}
|
| 30 |
+
|
| 31 |
+
const settings = { ...pluginSettings, ...routeSettings }
|
| 32 |
+
|
| 33 |
+
request.plugins[internals.pluginName] = { settings }
|
| 34 |
+
|
| 35 |
+
if (settings.enabled === false) {
|
| 36 |
+
return h.continue
|
| 37 |
+
}
|
| 38 |
+
|
| 39 |
+
const remaining = await internals.authCheck(authCache, request)
|
| 40 |
+
|
| 41 |
+
if (remaining < 0) {
|
| 42 |
+
return settings.limitExceededResponse(request, h)
|
| 43 |
+
}
|
| 44 |
+
|
| 45 |
+
return h.continue
|
| 46 |
+
})
|
| 47 |
+
|
| 48 |
+
// called regardless if authentication is performed, but not if authentication fails
|
| 49 |
+
plugin.ext('onPostAuth', async (request, h) => {
|
| 50 |
+
const { settings } = request.plugins[internals.pluginName]
|
| 51 |
+
|
| 52 |
+
if (settings.enabled === false) {
|
| 53 |
+
return h.continue
|
| 54 |
+
}
|
| 55 |
+
|
| 56 |
+
const remaining = await Promise.all([
|
| 57 |
+
internals.userCheck(userCache, request),
|
| 58 |
+
internals.userPathCheck(userPathCache, request),
|
| 59 |
+
internals.pathCheck(pathCache, request)
|
| 60 |
+
])
|
| 61 |
+
|
| 62 |
+
if (remaining.some(r => r < 0)) {
|
| 63 |
+
return settings.limitExceededResponse(request, h)
|
| 64 |
+
}
|
| 65 |
+
|
| 66 |
+
return h.continue
|
| 67 |
+
})
|
| 68 |
+
|
| 69 |
+
// always called, unless the request is aborted
|
| 70 |
+
plugin.ext('onPreResponse', async (request, h) => {
|
| 71 |
+
const requestPlugin = request.plugins[internals.pluginName]
|
| 72 |
+
|
| 73 |
+
if (!requestPlugin) {
|
| 74 |
+
// We never even made it to onPreAuth
|
| 75 |
+
return h.continue
|
| 76 |
+
}
|
| 77 |
+
|
| 78 |
+
const { response } = request
|
| 79 |
+
|
| 80 |
+
// Non 401s can include authToken in their data and if it's there it counts
|
| 81 |
+
if (response.isBoom) {
|
| 82 |
+
await internals.authFailure(authCache, request)
|
| 83 |
+
}
|
| 84 |
+
|
| 85 |
+
const { settings } = requestPlugin
|
| 86 |
+
|
| 87 |
+
if (settings.headers !== false) {
|
| 88 |
+
let headers = response.headers
|
| 89 |
+
|
| 90 |
+
if (response.isBoom) {
|
| 91 |
+
headers = response.output.headers
|
| 92 |
+
}
|
| 93 |
+
|
| 94 |
+
if (settings.pathLimit !== false) {
|
| 95 |
+
headers['X-RateLimit-PathLimit'] = requestPlugin.pathLimit
|
| 96 |
+
headers['X-RateLimit-PathRemaining'] = requestPlugin.pathRemaining
|
| 97 |
+
headers['X-RateLimit-PathReset'] = requestPlugin.pathReset
|
| 98 |
+
}
|
| 99 |
+
|
| 100 |
+
if (settings.userPathLimit !== false) {
|
| 101 |
+
headers['X-RateLimit-UserPathLimit'] = requestPlugin.userPathLimit
|
| 102 |
+
headers['X-RateLimit-UserPathRemaining'] = requestPlugin.userPathRemaining
|
| 103 |
+
headers['X-RateLimit-UserPathReset'] = requestPlugin.userPathReset
|
| 104 |
+
}
|
| 105 |
+
|
| 106 |
+
if (settings.userLimit !== false) {
|
| 107 |
+
headers['X-RateLimit-UserLimit'] = requestPlugin.userLimit
|
| 108 |
+
headers['X-RateLimit-UserRemaining'] = requestPlugin.userRemaining
|
| 109 |
+
headers['X-RateLimit-UserReset'] = requestPlugin.userReset
|
| 110 |
+
}
|
| 111 |
+
}
|
| 112 |
+
|
| 113 |
+
return h.continue
|
| 114 |
+
})
|
| 115 |
+
}
|
| 116 |
+
|
| 117 |
+
module.exports = {
|
| 118 |
+
register,
|
| 119 |
+
pkg: Pkg
|
| 120 |
+
}
|
ndcc/server/plugins/rateLimit/lib/internals.js
ADDED
|
@@ -0,0 +1,304 @@
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
| 1 |
+
const Crypto = require('crypto')
|
| 2 |
+
const Boom = require('@hapi/boom')
|
| 3 |
+
const Hoek = require('@hapi/hoek')
|
| 4 |
+
const Joi = require('joi')
|
| 5 |
+
|
| 6 |
+
const pluginName = 'rate-limit'
|
| 7 |
+
|
| 8 |
+
const schema = Joi.object({
|
| 9 |
+
enabled: Joi.boolean().default(true),
|
| 10 |
+
addressOnly: Joi.boolean().default(false),
|
| 11 |
+
headers: Joi.boolean().default(true),
|
| 12 |
+
ipWhitelist: Joi.array().default([]),
|
| 13 |
+
authCache: Joi.object({
|
| 14 |
+
getDecoratedValue: Joi.boolean().default(true),
|
| 15 |
+
cache: Joi.string().optional(),
|
| 16 |
+
segment: Joi.string().default(`${pluginName}-auth`),
|
| 17 |
+
expiresIn: Joi.number().default(1 * 60 * 1000) // 1 minute
|
| 18 |
+
}).default(),
|
| 19 |
+
authToken: Joi.string().default('authToken'),
|
| 20 |
+
authLimit: Joi.alternatives()
|
| 21 |
+
.try(Joi.boolean(), Joi.number())
|
| 22 |
+
.default(5),
|
| 23 |
+
pathCache: Joi.object({
|
| 24 |
+
getDecoratedValue: Joi.boolean().default(true),
|
| 25 |
+
cache: Joi.string().optional(),
|
| 26 |
+
segment: Joi.string().default(`${pluginName}-path`),
|
| 27 |
+
expiresIn: Joi.number().default(1 * 60 * 1000) // 1 minute
|
| 28 |
+
}).default(),
|
| 29 |
+
pathLimit: Joi.alternatives()
|
| 30 |
+
.try(Joi.boolean(), Joi.number())
|
| 31 |
+
.default(50),
|
| 32 |
+
ignorePathParams: Joi.boolean().default(false),
|
| 33 |
+
trustProxy: Joi.boolean().default(false),
|
| 34 |
+
getIpFromProxyHeader: Joi.func().default(null),
|
| 35 |
+
proxyHeaderName: Joi.string().default('x-forwarded-for'),
|
| 36 |
+
userAttribute: Joi.string().default('id'),
|
| 37 |
+
userCache: Joi.object({
|
| 38 |
+
getDecoratedValue: Joi.boolean().default(true),
|
| 39 |
+
cache: Joi.string().optional(),
|
| 40 |
+
segment: Joi.string().default(`${pluginName}-user`),
|
| 41 |
+
expiresIn: Joi.number().default(10 * 60 * 1000) // 10 minutes
|
| 42 |
+
}).default(),
|
| 43 |
+
userLimit: Joi.alternatives()
|
| 44 |
+
.try(Joi.boolean(), Joi.number())
|
| 45 |
+
.default(300),
|
| 46 |
+
userWhitelist: Joi.array().default([]),
|
| 47 |
+
userPathCache: Joi.object({
|
| 48 |
+
getDecoratedValue: Joi.boolean().default(true),
|
| 49 |
+
cache: Joi.string().optional(),
|
| 50 |
+
segment: Joi.string().default(`${pluginName}-userPath`),
|
| 51 |
+
expiresIn: Joi.number().default(1 * 60 * 1000) // 1 minute
|
| 52 |
+
}).default(),
|
| 53 |
+
userPathLimit: Joi.alternatives()
|
| 54 |
+
.try(Joi.boolean(), Joi.number())
|
| 55 |
+
.default(false),
|
| 56 |
+
limitExceededResponse: Joi.func().default(() => {
|
| 57 |
+
return limitExceededResponse
|
| 58 |
+
})
|
| 59 |
+
})
|
| 60 |
+
|
| 61 |
+
function getUser (request, settings) {
|
| 62 |
+
if (request.auth.isAuthenticated) {
|
| 63 |
+
const user = Hoek.reach(request.auth.credentials, settings.userAttribute)
|
| 64 |
+
if (user !== undefined) {
|
| 65 |
+
return user.toString()
|
| 66 |
+
}
|
| 67 |
+
}
|
| 68 |
+
}
|
| 69 |
+
|
| 70 |
+
function getIP (request, settings) {
|
| 71 |
+
let ip
|
| 72 |
+
|
| 73 |
+
if (settings.trustProxy && request.headers[settings.proxyHeaderName]) {
|
| 74 |
+
if (settings.getIpFromProxyHeader) {
|
| 75 |
+
ip = settings.getIpFromProxyHeader(request.headers[settings.proxyHeaderName])
|
| 76 |
+
} else {
|
| 77 |
+
const ips = request.headers[settings.proxyHeaderName].split(',')
|
| 78 |
+
ip = ips[0]
|
| 79 |
+
}
|
| 80 |
+
}
|
| 81 |
+
|
| 82 |
+
if (ip === undefined) {
|
| 83 |
+
ip = request.info.remoteAddress
|
| 84 |
+
}
|
| 85 |
+
|
| 86 |
+
return ip
|
| 87 |
+
}
|
| 88 |
+
|
| 89 |
+
async function authFailure (authCache, request) {
|
| 90 |
+
const requestPlugin = request.plugins[pluginName]
|
| 91 |
+
const settings = requestPlugin.settings
|
| 92 |
+
if (settings.authLimit === false) {
|
| 93 |
+
return
|
| 94 |
+
}
|
| 95 |
+
|
| 96 |
+
const ip = getIP(request, settings)
|
| 97 |
+
|
| 98 |
+
const { value, cached } = await authCache.get(ip)
|
| 99 |
+
|
| 100 |
+
let token
|
| 101 |
+
|
| 102 |
+
token = Hoek.reach(request, `auth.artifacts.${settings.authToken}`)
|
| 103 |
+
|
| 104 |
+
if (!token) {
|
| 105 |
+
token = Hoek.reach(request, `auth.error.data.${settings.authToken}`)
|
| 106 |
+
}
|
| 107 |
+
|
| 108 |
+
if (!token) {
|
| 109 |
+
return
|
| 110 |
+
}
|
| 111 |
+
|
| 112 |
+
const tokenHash = Crypto.createHash('sha1')
|
| 113 |
+
.update(token)
|
| 114 |
+
.digest('hex')
|
| 115 |
+
.slice(0, 6)
|
| 116 |
+
|
| 117 |
+
let tokens
|
| 118 |
+
let ttl = settings.userPathCache.expiresIn
|
| 119 |
+
|
| 120 |
+
/* $lab:coverage:off$ */
|
| 121 |
+
if (value === null || cached.isStale) {
|
| 122 |
+
/* $lab:coverage:on$ */
|
| 123 |
+
tokens = new Set([tokenHash])
|
| 124 |
+
} else {
|
| 125 |
+
ttl = cached.ttl
|
| 126 |
+
tokens = new Set([...value, tokenHash])
|
| 127 |
+
}
|
| 128 |
+
|
| 129 |
+
// Sets don't stringify so we cast to an array before storing in the cache
|
| 130 |
+
await authCache.set(ip, Array.from(tokens), ttl)
|
| 131 |
+
}
|
| 132 |
+
|
| 133 |
+
async function authCheck (authCache, request) {
|
| 134 |
+
const requestPlugin = request.plugins[pluginName]
|
| 135 |
+
const settings = requestPlugin.settings
|
| 136 |
+
if (settings.authLimit === false) {
|
| 137 |
+
requestPlugin.authLimit = false
|
| 138 |
+
return
|
| 139 |
+
}
|
| 140 |
+
|
| 141 |
+
const ip = getIP(request, settings)
|
| 142 |
+
const { value, cached } = await authCache.get(ip)
|
| 143 |
+
|
| 144 |
+
/* $lab:coverage:off$ */
|
| 145 |
+
if (value === null || cached.isStale) {
|
| 146 |
+
/* $lab:coverage:on$ */
|
| 147 |
+
return
|
| 148 |
+
}
|
| 149 |
+
|
| 150 |
+
const badIps = new Set(value)
|
| 151 |
+
const remaining = settings.authLimit - badIps.size
|
| 152 |
+
|
| 153 |
+
return remaining
|
| 154 |
+
}
|
| 155 |
+
|
| 156 |
+
async function pathCheck (pathCache, request) {
|
| 157 |
+
const requestPlugin = request.plugins[pluginName]
|
| 158 |
+
const settings = requestPlugin.settings
|
| 159 |
+
const path = settings.ignorePathParams ? request.route.path : request.path
|
| 160 |
+
|
| 161 |
+
if (settings.pathLimit === false) {
|
| 162 |
+
requestPlugin.pathLimit = false
|
| 163 |
+
return
|
| 164 |
+
}
|
| 165 |
+
|
| 166 |
+
const { value, cached } = await pathCache.get(path)
|
| 167 |
+
let count
|
| 168 |
+
let ttl = settings.pathCache.expiresIn
|
| 169 |
+
|
| 170 |
+
/* $lab:coverage:off$ */
|
| 171 |
+
if (value === null || cached.isStale) {
|
| 172 |
+
/* $lab:coverage:on$ */
|
| 173 |
+
count = 1
|
| 174 |
+
} else {
|
| 175 |
+
count = value + 1
|
| 176 |
+
ttl = cached.ttl
|
| 177 |
+
}
|
| 178 |
+
|
| 179 |
+
let remaining = settings.pathLimit - count
|
| 180 |
+
if (remaining < 0) {
|
| 181 |
+
remaining = -1
|
| 182 |
+
}
|
| 183 |
+
|
| 184 |
+
await pathCache.set(path, count, ttl)
|
| 185 |
+
|
| 186 |
+
requestPlugin.pathLimit = settings.pathLimit
|
| 187 |
+
requestPlugin.pathRemaining = remaining
|
| 188 |
+
requestPlugin.pathReset = Date.now() + ttl
|
| 189 |
+
|
| 190 |
+
return remaining
|
| 191 |
+
}
|
| 192 |
+
|
| 193 |
+
async function userCheck (userCache, request) {
|
| 194 |
+
const requestPlugin = request.plugins[pluginName]
|
| 195 |
+
const settings = requestPlugin.settings
|
| 196 |
+
const ip = getIP(request, settings)
|
| 197 |
+
let user = getUser(request, settings)
|
| 198 |
+
if (
|
| 199 |
+
settings.ipWhitelist.indexOf(ip) > -1 ||
|
| 200 |
+
(user && settings.userWhitelist.indexOf(user) > -1) ||
|
| 201 |
+
settings.userLimit === false
|
| 202 |
+
) {
|
| 203 |
+
requestPlugin.userLimit = false
|
| 204 |
+
return
|
| 205 |
+
}
|
| 206 |
+
|
| 207 |
+
if (settings.addressOnly || user === undefined) {
|
| 208 |
+
user = ip
|
| 209 |
+
}
|
| 210 |
+
|
| 211 |
+
const { value, cached } = await userCache.get(user)
|
| 212 |
+
|
| 213 |
+
let count
|
| 214 |
+
let ttl = settings.userCache.expiresIn
|
| 215 |
+
|
| 216 |
+
/* $lab:coverage:off$ */
|
| 217 |
+
if (value === null || cached.isStale) {
|
| 218 |
+
/* $lab:coverage:on$ */
|
| 219 |
+
count = 1
|
| 220 |
+
} else {
|
| 221 |
+
count = value + 1
|
| 222 |
+
ttl = cached.ttl
|
| 223 |
+
}
|
| 224 |
+
|
| 225 |
+
let remaining = settings.userLimit - count
|
| 226 |
+
if (remaining < 0) {
|
| 227 |
+
remaining = -1
|
| 228 |
+
}
|
| 229 |
+
|
| 230 |
+
await userCache.set(user, count, ttl)
|
| 231 |
+
|
| 232 |
+
requestPlugin.userLimit = settings.userLimit
|
| 233 |
+
requestPlugin.userRemaining = remaining
|
| 234 |
+
requestPlugin.userReset = Date.now() + ttl
|
| 235 |
+
|
| 236 |
+
return remaining
|
| 237 |
+
}
|
| 238 |
+
|
| 239 |
+
async function userPathCheck (userPathCache, request) {
|
| 240 |
+
const requestPlugin = request.plugins[pluginName]
|
| 241 |
+
const settings = requestPlugin.settings
|
| 242 |
+
const ip = getIP(request, settings)
|
| 243 |
+
let user = getUser(request, settings)
|
| 244 |
+
const path = settings.ignorePathParams ? request.route.path : request.path
|
| 245 |
+
|
| 246 |
+
if (
|
| 247 |
+
settings.ipWhitelist.indexOf(ip) > -1 ||
|
| 248 |
+
(user && settings.userWhitelist.indexOf(user) > -1) ||
|
| 249 |
+
settings.userPathLimit === false
|
| 250 |
+
) {
|
| 251 |
+
requestPlugin.userPathLimit = false
|
| 252 |
+
return
|
| 253 |
+
}
|
| 254 |
+
|
| 255 |
+
if (settings.addressOnly || user === undefined) {
|
| 256 |
+
user = ip
|
| 257 |
+
}
|
| 258 |
+
|
| 259 |
+
const userPath = `${user}:${path}`
|
| 260 |
+
|
| 261 |
+
const { value, cached } = await userPathCache.get(userPath)
|
| 262 |
+
|
| 263 |
+
let count
|
| 264 |
+
let ttl = settings.userPathCache.expiresIn
|
| 265 |
+
|
| 266 |
+
/* $lab:coverage:off$ */
|
| 267 |
+
if (value === null || cached.isStale) {
|
| 268 |
+
/* $lab:coverage:on$ */
|
| 269 |
+
count = 1
|
| 270 |
+
} else {
|
| 271 |
+
count = value + 1
|
| 272 |
+
ttl = cached.ttl
|
| 273 |
+
}
|
| 274 |
+
|
| 275 |
+
let remaining = settings.userPathLimit - count
|
| 276 |
+
if (remaining < 0) {
|
| 277 |
+
remaining = -1
|
| 278 |
+
}
|
| 279 |
+
|
| 280 |
+
await userPathCache.set(userPath, count, ttl)
|
| 281 |
+
|
| 282 |
+
requestPlugin.userPathLimit = settings.userPathLimit
|
| 283 |
+
requestPlugin.userPathRemaining = remaining
|
| 284 |
+
requestPlugin.userPathReset = Date.now() + ttl
|
| 285 |
+
|
| 286 |
+
return remaining
|
| 287 |
+
}
|
| 288 |
+
|
| 289 |
+
function limitExceededResponse () {
|
| 290 |
+
return Boom.tooManyRequests('Rate limit exceeded')
|
| 291 |
+
}
|
| 292 |
+
|
| 293 |
+
module.exports = {
|
| 294 |
+
authCheck,
|
| 295 |
+
authFailure,
|
| 296 |
+
getIP,
|
| 297 |
+
getUser,
|
| 298 |
+
limitExceededResponse,
|
| 299 |
+
pathCheck,
|
| 300 |
+
pluginName,
|
| 301 |
+
schema,
|
| 302 |
+
userCheck,
|
| 303 |
+
userPathCheck
|
| 304 |
+
}
|
ndcc/server/plugins/route.js
CHANGED
|
@@ -97,7 +97,7 @@ exports.plugin = {
|
|
| 97 |
collection === "plugins" ? server.match('POST', `/plugins/${_id}`) : server.match('GET', `/${_id}`); //添加时默认是这样
|
| 98 |
if(!route){
|
| 99 |
const options = collection === "api" ? {auth: 'auth'} :
|
| 100 |
-
collection === 'plugins' ? { isInternal:true,payload: {parse: false, output: 'stream'},plugins: {'
|
| 101 |
{}
|
| 102 |
|
| 103 |
//添加插件需要注入到server.settings.app.plugins
|
|
@@ -242,7 +242,7 @@ exports.plugin = {
|
|
| 242 |
server.route({
|
| 243 |
method: ['POST'],
|
| 244 |
path: `/plugins/${_id}`,
|
| 245 |
-
options:{ isInternal:true,payload: {parse: false, output: 'stream'},plugins: {'
|
| 246 |
handler: pluginsHandler(_id)
|
| 247 |
});
|
| 248 |
})
|
|
|
|
| 97 |
collection === "plugins" ? server.match('POST', `/plugins/${_id}`) : server.match('GET', `/${_id}`); //添加时默认是这样
|
| 98 |
if(!route){
|
| 99 |
const options = collection === "api" ? {auth: 'auth'} :
|
| 100 |
+
collection === 'plugins' ? { isInternal:true,payload: {parse: false, output: 'stream'},plugins: {'rate-limit': {enabled: false } }} :
|
| 101 |
{}
|
| 102 |
|
| 103 |
//添加插件需要注入到server.settings.app.plugins
|
|
|
|
| 242 |
server.route({
|
| 243 |
method: ['POST'],
|
| 244 |
path: `/plugins/${_id}`,
|
| 245 |
+
options:{ isInternal:true,payload: {parse: false, output: 'stream'},plugins: {'rate-limit': {enabled: false } }},
|
| 246 |
handler: pluginsHandler(_id)
|
| 247 |
});
|
| 248 |
})
|