diff --git a/package-lock.json b/package-lock.json index 06ca116..158162e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -3788,6 +3788,15 @@ "mimic-response": "^1.0.0" } }, + "cls-bluebird": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/cls-bluebird/-/cls-bluebird-2.1.0.tgz", + "integrity": "sha1-N+8eCAqP+1XC9BZPU28ZGeeWiu4=", + "requires": { + "is-bluebird": "^1.0.2", + "shimmer": "^1.1.0" + } + }, "co": { "version": "4.6.0", "resolved": "https://registry.npmjs.org/co/-/co-4.6.0.tgz", @@ -4875,6 +4884,11 @@ "minimatch": "^3.0.4" } }, + "dottie": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/dottie/-/dottie-2.0.2.tgz", + "integrity": "sha512-fmrwR04lsniq/uSr8yikThDTrM7epXHBAAjH9TbeH3rEA8tdCO7mRzB9hdmdGyJCxF8KERo9CITcm3kGuoyMhg==" + }, "drbg.js": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/drbg.js/-/drbg.js-1.0.1.tgz", @@ -5110,9 +5124,9 @@ } }, "engine.io-client": { - "version": "3.5.1", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.1.tgz", - "integrity": "sha512-oVu9kBkGbcggulyVF0kz6BV3ganqUeqXvD79WOFKa+11oK692w1NyFkuEj4xrkFRpZhn92QOqTk4RQq5LiBXbQ==", + "version": "3.5.2", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-3.5.2.tgz", + "integrity": "sha512-QEqIp+gJ/kMHeUun7f5Vv3bteRHppHH/FMBQX/esFj/fuYfjyUKWGMo3VCvIP/V8bE9KcjHmRZrhIz2Z9oNsDA==", "requires": { "component-emitter": "~1.3.0", "component-inherit": "0.0.3", @@ -5123,15 +5137,8 @@ "parseqs": "0.0.6", "parseuri": "0.0.6", "ws": "~7.4.2", - "xmlhttprequest-ssl": "~1.5.4", + "xmlhttprequest-ssl": "~1.6.2", "yeast": "0.1.2" - }, - "dependencies": { - "ws": { - "version": "7.4.4", - "resolved": "https://registry.npmjs.org/ws/-/ws-7.4.4.tgz", - "integrity": "sha512-Qm8k8ojNQIMx7S+Zp8u/uHOx7Qazv3Yv4q68MiWWWOJhiwG5W3x7iqmRtJo8xxrciZUY4vRxUTJCKuRnF28ZZw==" - } } }, "engine.io-parser": { @@ -6587,8 +6594,7 @@ }, "ini": { "version": "1.3.5", - "resolved": "https://registry.npmjs.org/ini/-/ini-1.3.5.tgz", - "integrity": "sha512-RZY5huIKCMRWDUqZlEi72f/lmXKMvuszcMBduliQ3nnWbx9X/ZBQO7DijMEYS9EhHBb2qacRUMtC7svLwe0lcw==", + "resolved": "", "optional": true }, "is-fullwidth-code-point": { @@ -6791,8 +6797,7 @@ "dependencies": { "minimist": { "version": "1.2.0", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-1.2.0.tgz", - "integrity": "sha1-o1AIsg9BOD7sH7kU9M1d95omQoQ=", + "resolved": "", "optional": true } } @@ -7277,9 +7282,9 @@ } }, "handlebars": { - "version": "4.7.6", - "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.6.tgz", - "integrity": "sha512-1f2BACcBfiwAfStCKZNrUCgqNZkGsAT7UM3kkYtXuLo0KnaVfjKOyf7PRzB6++aK9STyT1Pd2ZCPe3EGOXleXA==", + "version": "4.7.7", + "resolved": "https://registry.npmjs.org/handlebars/-/handlebars-4.7.7.tgz", + "integrity": "sha512-aAcXm5OAfE/8IXkcZvCepKU3VzW1/39Fb5ZuqMtgI/hT8X2YgoMvBY5dLhq/cpOvw7Lk1nK/UF71aLG/ZnVYRA==", "dev": true, "requires": { "minimist": "^1.2.5", @@ -7480,9 +7485,9 @@ "dev": true }, "hosted-git-info": { - "version": "2.8.5", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.5.tgz", - "integrity": "sha512-kssjab8CvdXfcXMXVcvsXum4Hwdq9XGtRD3TteMEvEbq0LXyiNQr6AprqKqfeaDXze7SxWvRxdpwE6ku7ikLkg==" + "version": "2.8.9", + "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.9.tgz", + "integrity": "sha512-mxIDAb9Lsm6DoOJ7xH+5+X4y1LU/4Hi50L9C5sIswK3JzULS4bwk1FvjdBgvYR4bzT4tuUQiC15FE2f5HbLvYw==" }, "html-escaper": { "version": "2.0.2", @@ -7746,6 +7751,11 @@ "resolved": "https://registry.npmjs.org/inflation/-/inflation-2.0.0.tgz", "integrity": "sha1-i0F+R8KPklpFEz2RTKH9OJEH8w8=" }, + "inflection": { + "version": "1.12.0", + "resolved": "https://registry.npmjs.org/inflection/-/inflection-1.12.0.tgz", + "integrity": "sha1-ogCTVlbW9fa8TcdQLhrstwMihBY=" + }, "inflight": { "version": "1.0.6", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", @@ -10742,6 +10752,11 @@ "binary-extensions": "^2.0.0" } }, + "is-bluebird": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/is-bluebird/-/is-bluebird-1.0.2.tgz", + "integrity": "sha1-CWQ5Bg9KpBGr7hkUOoTWpVNG1uI=" + }, "is-boolean-object": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/is-boolean-object/-/is-boolean-object-1.1.0.tgz", @@ -12031,6 +12046,16 @@ "koa-send": "^5.0.0" } }, + "koa2-ratelimit": { + "version": "0.9.0", + "resolved": "https://registry.npmjs.org/koa2-ratelimit/-/koa2-ratelimit-0.9.0.tgz", + "integrity": "sha512-gCjHnjm5Xgc+Qzx3Xr8qb0Iatd9Xw4YC2ZemIpdbkFTpNjfJcoI1Df5lS/H0K+NeSxggxYN0K23ToPS/SPN/Gg==", + "requires": { + "mongoose": "^5.5.13", + "promise-redis": "0.0.5", + "sequelize": "^5.8.7" + } + }, "kuler": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/kuler/-/kuler-2.0.0.tgz", @@ -13779,9 +13804,9 @@ } }, "lodash": { - "version": "4.17.20", - "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.20.tgz", - "integrity": "sha512-PlhdFcillOINfeV7Ni6oF1TAEayyZBoZ8bcshTHqOYJYlrqzRK5hagpagky5o4HfCzzd1TRkXPMFq6cKk9rGmA==" + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, "lodash._baseiteratee": { "version": "4.7.0", @@ -14343,8 +14368,7 @@ "dependencies": { "hosted-git-info": { "version": "2.8.8", - "resolved": "https://registry.npmjs.org/hosted-git-info/-/hosted-git-info-2.8.8.tgz", - "integrity": "sha512-f/wzC2QaWBs7t9IYqB4T3sR1xviIViXJRJTWBlx2Gf3g0Xi5vI7Yy4koXQ1c9OYDGHN9sBy1DQ2AB8fqZBWhUg==", + "resolved": "", "dev": true }, "normalize-package-data": { @@ -14675,6 +14699,14 @@ "resolved": "https://registry.npmjs.org/moment/-/moment-2.29.1.tgz", "integrity": "sha512-kHmoybcPV8Sqy59DwNDY3Jefr64lK/by/da0ViFcuA4DH0vQg5Q6Ze5VimxkfQNSC+Mls/Kx53s7TjP1RhFEDQ==" }, + "moment-timezone": { + "version": "0.5.33", + "resolved": "https://registry.npmjs.org/moment-timezone/-/moment-timezone-0.5.33.tgz", + "integrity": "sha512-PTc2vcT8K9J5/9rDEPe5czSIKgLoGsH8UNpA4qZTVw0Vd/Uz19geE9abbIOQKaAQFcnQ3v5YEXrbSc5BpshH+w==", + "requires": { + "moment": ">= 2.9.0" + } + }, "mongodb": { "version": "3.6.3", "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-3.6.3.tgz", @@ -15036,9 +15068,9 @@ "integrity": "sha512-hZXc7K2e+PgeI1eDBe/10Ard4ekbfrrqG8Ep+8Jmf4JID2bNg7NvCPOZN+kfF574pFQI7mum2AUqDidoKqcTOw==" }, "neo-async": { - "version": "2.6.1", - "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.1.tgz", - "integrity": "sha512-iyam8fBuCUpWeKPGpaNMetEocMt364qkCsfL9JuhjXX6dRnguRVOfk2GZaDpPjcOKiiXCPINZC1GczQ7iTq3Zw==", + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/neo-async/-/neo-async-2.6.2.tgz", + "integrity": "sha512-Yd3UES5mWCSqR+qNT93S3UoYUkqAZ9lLg8a7g9rimsWmYGK8cVToA4/sF3RrshdyV3sAGMXVUmpMYOw+dLpOuw==", "dev": true }, "nerf-dart": { @@ -18702,6 +18734,15 @@ "is-promise": "~1" } }, + "promise-redis": { + "version": "0.0.5", + "resolved": "https://registry.npmjs.org/promise-redis/-/promise-redis-0.0.5.tgz", + "integrity": "sha1-PjziqtkS6SfJam7O2NGw+YJmZgk=", + "requires": { + "redis": "*", + "redis-commands": "*" + } + }, "promise-timeout": { "version": "1.3.0", "resolved": "https://registry.npmjs.org/promise-timeout/-/promise-timeout-1.3.0.tgz", @@ -19066,6 +19107,42 @@ "esprima": "~4.0.0" } }, + "redis": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/redis/-/redis-3.1.2.tgz", + "integrity": "sha512-grn5KoZLr/qrRQVwoSkmzdbw6pwF+/rwODtrOr6vuBRiR/f3rjSTGupbF90Zpqm2oenix8Do6RV7pYEkGwlKkw==", + "requires": { + "denque": "^1.5.0", + "redis-commands": "^1.7.0", + "redis-errors": "^1.2.0", + "redis-parser": "^3.0.0" + }, + "dependencies": { + "denque": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/denque/-/denque-1.5.0.tgz", + "integrity": "sha512-CYiCSgIF1p6EUByQPlGkKnP1M9g0ZV3qMIrqMqZqdwazygIA/YP2vrbcyl1h/WppKJTdl1F85cXIle+394iDAQ==" + } + } + }, + "redis-commands": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/redis-commands/-/redis-commands-1.7.0.tgz", + "integrity": "sha512-nJWqw3bTFy21hX/CPKHth6sfhZbdiHP6bTawSgQBlKOVRG7EZkfHbbHwQJnrE4vsQf0CMNE+3gJ4Fmm16vdVlQ==" + }, + "redis-errors": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/redis-errors/-/redis-errors-1.2.0.tgz", + "integrity": "sha1-62LSrbFeTq9GEMBK/hUpOEJQq60=" + }, + "redis-parser": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/redis-parser/-/redis-parser-3.0.0.tgz", + "integrity": "sha1-tm2CjNyv5rS4pCin3vTGvKwxyLQ=", + "requires": { + "redis-errors": "^1.0.0" + } + }, "regexp-clone": { "version": "1.0.0", "resolved": "https://registry.npmjs.org/regexp-clone/-/regexp-clone-1.0.0.tgz", @@ -19301,6 +19378,14 @@ "resolved": "https://registry.npmjs.org/retry/-/retry-0.12.0.tgz", "integrity": "sha1-G0KmJmoh8HQh0bC1S33BZ7AcATs=" }, + "retry-as-promised": { + "version": "3.2.0", + "resolved": "https://registry.npmjs.org/retry-as-promised/-/retry-as-promised-3.2.0.tgz", + "integrity": "sha512-CybGs60B7oYU/qSQ6kuaFmRd9sTZ6oXSc0toqePvV74Ac6/IFZSI1ReFQmtCN+uvW1Mtqdwpvt/LGOiCBAY2Mg==", + "requires": { + "any-promise": "^1.3.0" + } + }, "reusify": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/reusify/-/reusify-1.0.4.tgz", @@ -19769,6 +19854,53 @@ "upper-case-first": "^2.0.2" } }, + "sequelize": { + "version": "5.22.4", + "resolved": "https://registry.npmjs.org/sequelize/-/sequelize-5.22.4.tgz", + "integrity": "sha512-xFQQ38HPg7EyDRDA+NdzMSRWbo9m6Z/RxpjnkBl3ggyQG+jRrup48x0jaw4Ox42h56wFnXOBC2NZOkTJfZeWCw==", + "requires": { + "bluebird": "^3.5.0", + "cls-bluebird": "^2.1.0", + "debug": "^4.1.1", + "dottie": "^2.0.0", + "inflection": "1.12.0", + "lodash": "^4.17.15", + "moment": "^2.24.0", + "moment-timezone": "^0.5.21", + "retry-as-promised": "^3.2.0", + "semver": "^6.3.0", + "sequelize-pool": "^2.3.0", + "toposort-class": "^1.0.1", + "uuid": "^3.3.3", + "validator": "^10.11.0", + "wkx": "^0.4.8" + }, + "dependencies": { + "debug": { + "version": "4.3.1", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.1.tgz", + "integrity": "sha512-doEwdvm4PCeK4K3RQN2ZC2BYUBaxwLARCqZmMjtF8a51J2Rb0xpVloFRnCODwqjpwnAoao4pelN8l3RJdv3gRQ==", + "requires": { + "ms": "2.1.2" + } + }, + "semver": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/semver/-/semver-6.3.0.tgz", + "integrity": "sha512-b39TBaTSfV6yBrapU89p5fKekE2m/NwnDocOVruQFS1/veMgdzuPcnOM34M6CwxW8jH/lxEa5rBoDeUwu5HHTw==" + }, + "uuid": { + "version": "3.4.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-3.4.0.tgz", + "integrity": "sha512-HjSDRw6gZE5JMggctHBcjVak08+KEVhSIiDzFnT9S9aegmp85S/bReBVTb4QTFaRNptJ9kuYaNhnbNEOkbKb/A==" + } + } + }, + "sequelize-pool": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/sequelize-pool/-/sequelize-pool-2.3.0.tgz", + "integrity": "sha512-Ibz08vnXvkZ8LJTiUOxRcj1Ckdn7qafNZ2t59jYHMX1VIebTAOYefWdRYFt6z6+hy52WGthAHAoLc9hvk3onqA==" + }, "serialize-javascript": { "version": "5.0.1", "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-5.0.1.tgz", @@ -19820,6 +19952,11 @@ "resolved": "https://registry.npmjs.org/shebang-regex/-/shebang-regex-3.0.0.tgz", "integrity": "sha512-7++dFhtcx3353uBaq8DDR4NuxBetBzC7ZQOhmTQInHEd6bSrXdiEyzCvG07Z44UYdLShWUyXt5M/yhz8ekcb1A==" }, + "shimmer": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/shimmer/-/shimmer-1.2.1.tgz", + "integrity": "sha512-sQTKC1Re/rM6XyFM6fIAGHRPVGvyXfgzIDvzoq608vM+jeyVD0Tu1E6Np0Kc2zAIFWIj963V2800iF/9LPieQw==" + }, "side-channel": { "version": "1.0.4", "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", @@ -20028,9 +20165,9 @@ } }, "engine.io-client": { - "version": "4.1.3", - "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.1.3.tgz", - "integrity": "sha512-C3JYYyvna0FvSKOWGWpo0/EQ6MayO8uEKam1oedLZRpxLego9Fk6K3UH/Phieu1xPHauM7YqAygJ+6SraVc0Qg==", + "version": "4.1.4", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-4.1.4.tgz", + "integrity": "sha512-843fqAdKeUMFqKi1sSjnR11tJ4wi8sIefu6+JC1OzkkJBmjtc/gM/rZ53tJfu5Iae/3gApm5veoS+v+gtT0+Fg==", "requires": { "base64-arraybuffer": "0.1.4", "component-emitter": "~1.3.0", @@ -20040,7 +20177,7 @@ "parseqs": "0.0.6", "parseuri": "0.0.6", "ws": "~7.4.2", - "xmlhttprequest-ssl": "~1.5.4", + "xmlhttprequest-ssl": "~1.6.2", "yeast": "0.1.2" } }, @@ -20061,6 +20198,11 @@ "component-emitter": "~1.3.0", "debug": "~4.3.1" } + }, + "xmlhttprequest-ssl": { + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.2.tgz", + "integrity": "sha512-tYOaldF/0BLfKuoA39QMwD4j2m8lq4DIncqj1yuNELX4vz9+z/ieG/vwmctjJce+boFHXstqhWnHSxc4W8f4qg==" } } }, @@ -21081,6 +21223,11 @@ "ieee754": "^1.2.1" } }, + "toposort-class": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toposort-class/-/toposort-class-1.0.1.tgz", + "integrity": "sha1-f/0feMi+KMO6Rc1OGj9e4ZO9mYg=" + }, "touch": { "version": "3.1.0", "resolved": "https://registry.npmjs.org/touch/-/touch-3.1.0.tgz", @@ -21269,24 +21416,11 @@ "dev": true }, "uglify-js": { - "version": "3.8.1", - "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.8.1.tgz", - "integrity": "sha512-W7KxyzeaQmZvUFbGj4+YFshhVrMBGSg2IbcYAjGWGvx8DHvJMclbTDMpffdxFUGPBHjIytk7KJUR/KUXstUGDw==", + "version": "3.13.5", + "resolved": "https://registry.npmjs.org/uglify-js/-/uglify-js-3.13.5.tgz", + "integrity": "sha512-xtB8yEqIkn7zmOyS2zUNBsYCBRhDkvlNxMMY2smuJ/qA8NCHeQvKCF3i9Z4k8FJH4+PJvZRtMrPynfZ75+CSZw==", "dev": true, - "optional": true, - "requires": { - "commander": "~2.20.3", - "source-map": "~0.6.1" - }, - "dependencies": { - "source-map": { - "version": "0.6.1", - "resolved": "https://registry.npmjs.org/source-map/-/source-map-0.6.1.tgz", - "integrity": "sha512-UjgapumWlbMhkBgzT7Ykc5YXUT46F0iKu8SGXq0bcwP5dz/h0Plj6enJqjz1Zbq2l5WaqYnrVbwWOWMyF3F47g==", - "dev": true, - "optional": true - } - } + "optional": true }, "uid-safe": { "version": "2.1.5", @@ -21534,6 +21668,11 @@ "spdx-expression-parse": "^3.0.0" } }, + "validator": { + "version": "10.11.0", + "resolved": "https://registry.npmjs.org/validator/-/validator-10.11.0.tgz", + "integrity": "sha512-X/p3UZerAIsbBfN/IwahhYaBbY68EN/UQBWHtsbXGT5bfrH/p4NQzUCG1kF/rtKaNpnJ7jAu6NGTdSNtyNIXMw==" + }, "varint": { "version": "5.0.2", "resolved": "https://registry.npmjs.org/varint/-/varint-5.0.2.tgz", @@ -21764,6 +21903,14 @@ } } }, + "wkx": { + "version": "0.4.8", + "resolved": "https://registry.npmjs.org/wkx/-/wkx-0.4.8.tgz", + "integrity": "sha512-ikPXMM9IR/gy/LwiOSqWlSL3X/J5uk9EO2hHNRXS41eTLXaUFEVw9fn/593jW/tE5tedNg8YjT5HkCa4FqQZyQ==", + "requires": { + "@types/node": "*" + } + }, "word-wrap": { "version": "1.2.3", "resolved": "https://registry.npmjs.org/word-wrap/-/word-wrap-1.2.3.tgz", @@ -21870,9 +22017,9 @@ "integrity": "sha512-fDlsI/kFEx7gLvbecc0/ohLG50fugQp8ryHzMTuW9vSa1GJ0XYWKnhsUx7oie3G98+r56aTQIUB4kht42R3JvA==" }, "xmlhttprequest-ssl": { - "version": "1.5.5", - "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.5.5.tgz", - "integrity": "sha1-wodrBhaKrcQOV9l+gRkayPQ5iz4=" + "version": "1.6.2", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-1.6.2.tgz", + "integrity": "sha512-tYOaldF/0BLfKuoA39QMwD4j2m8lq4DIncqj1yuNELX4vz9+z/ieG/vwmctjJce+boFHXstqhWnHSxc4W8f4qg==" }, "xor-distance": { "version": "2.0.0", diff --git a/package.json b/package.json index 574f0cd..f8c0e0e 100644 --- a/package.json +++ b/package.json @@ -11,22 +11,12 @@ "test:unit:rest": "export SVC_ENV=test && mocha --exit --timeout 15000 test/unit/rest-api/", "test:unit:jsonrpc": "export SVC_ENV=test && mocha --exit --timeout 15000 test/unit/json-rpc/", "test:e2e:auto": "export SVC_ENV=test && mocha --exit --timeout 15000 test/e2e/automated/", + "test:temp": "export SVC_ENV=test && mocha --exit --timeout 15000 -g '#rate-limit' test/unit/json-rpc/", "lint": "standard --env mocha --fix", "docs": "./node_modules/.bin/apidoc -i src/ -o docs", "coverage": "nyc report --reporter=text-lcov | coveralls", "coverage:report": "export SVC_ENV=test && nyc --reporter=html mocha --exit --timeout 15000 test/unit/biz-logic/ test/unit/json-rpc/ test/unit/rest-api/ test/e2e/automated/" }, - "keywords": [ - "koa-api-boilerplate", - "api", - "koa", - "koa2", - "boilerplate", - "es6", - "mongoose", - "passportjs", - "apidoc" - ], "author": "Chris Troutner ", "license": "MIT", "apidoc": { @@ -53,6 +43,7 @@ "koa-passport": "^4.1.3", "koa-router": "^10.0.0", "koa-static": "^5.0.0", + "koa2-ratelimit": "^0.9.0", "line-reader": "^0.4.0", "mongoose": "^5.11.15", "nodemailer": "^6.4.17", diff --git a/src/rpc/auth/index.js b/src/rpc/auth/index.js index f7f7868..e9f3d67 100644 --- a/src/rpc/auth/index.js +++ b/src/rpc/auth/index.js @@ -9,6 +9,7 @@ const jsonrpc = require('jsonrpc-lite') // const AuthLib = require('../../lib/auth') const UserLib = require('../../lib/users') const wlogger = require('../../lib/wlogger') +const RateLimit = require('../rate-limit') class AuthRPC { constructor (localConfig) { @@ -16,6 +17,7 @@ class AuthRPC { // this.authLib = new AuthLib() this.jsonrpc = jsonrpc this.userLib = new UserLib() + this.rateLimit = new RateLimit() } // Top-level router for this library. All other methods in this class are for @@ -32,6 +34,7 @@ class AuthRPC { // Route the call based on the requested endpoint. switch (endpoint) { case 'authUser': + await this.rateLimit.limiter(rpcData.from) return await this.authUser(rpcData) } } catch (err) { diff --git a/src/rpc/index.js b/src/rpc/index.js index 4ebf083..88f5505 100644 --- a/src/rpc/index.js +++ b/src/rpc/index.js @@ -51,6 +51,10 @@ class JSONRPC { return } + // Added the property "from" to the parsedData object; + // necessary for calculating rate limits (based on the IPFS ID). + parsedData.from = from + // Default return string let retObj = _this.defaultResponse() diff --git a/src/rpc/rate-limit.js b/src/rpc/rate-limit.js new file mode 100644 index 0000000..bc03c69 --- /dev/null +++ b/src/rpc/rate-limit.js @@ -0,0 +1,83 @@ +/* + Rate limit +*/ +/* eslint no-useless-catch: 0 */ + +// Local libraries +const RateLimitLib = require('koa2-ratelimit').RateLimit + +class RateLimit { + constructor (options) { + // Encapsulate dependencies + this.RateLimitLib = RateLimitLib + + // Set default rate limit options. + this.defaultOptions = { + interval: { min: 1 }, + max: 60, + onLimitReached: this.onLimitReached + } + + // ctx obj + this.context = { + state: { + user: '' + }, + request: { + ip: '' + }, + user: '', + set: () => {} + } + + console.log( + `this.defaultOptions: ${JSON.stringify(this.defaultOptions, null, 2)}` + ) + console.log(`options: ${JSON.stringify(options, null, 2)}`) + + // Set rate limit settings. Default values are overwritten if user passes + // in an options object. + this.rateLimitOptions = Object.assign({}, this.defaultOptions, options) + console.log( + `this.rateLimitOptions: ${JSON.stringify(this.rateLimitOptions, null, 2)}` + ) + this.rateLimit = this.RateLimitLib.middleware(this.rateLimitOptions) + } + + // This function is called when the user hits their rate limits. + onLimitReached () { + try { + const error = new Error() // Establish provided options as the default options. + error.message = 'Too many requests, please try again later.' + error.status = 429 + throw error + } catch (error) { + // console.log("Error in onLimitReached()", error) + throw error + } + } + + // This is the middleware function called by the router. + async limiter (from) { + try { + if (!from || typeof from !== 'string') { + throw new Error('from must be a string') + } + + // Set context.limiter + // This overrides the default koa behavior and adapts the rate limiter + // to work with the JSON RPC over IPFS. + this.context.state.user = from + this.context.request.ip = from + this.context.user = from + + await this.rateLimit(this.context, () => {}) + return true + } catch (error) { + console.error('Error in rate-limit.js/limiter()') + throw error + } + } +} + +module.exports = RateLimit diff --git a/src/rpc/users/index.js b/src/rpc/users/index.js index 273fb79..fd4bfa4 100644 --- a/src/rpc/users/index.js +++ b/src/rpc/users/index.js @@ -8,6 +8,7 @@ const jsonrpc = require('jsonrpc-lite') // Local libraries const UserLib = require('../../lib/users') const Validators = require('../validators') +const RateLimit = require('../rate-limit') class UserRPC { constructor (localConfig) { @@ -15,6 +16,7 @@ class UserRPC { this.userLib = new UserLib() this.jsonrpc = jsonrpc this.validators = new Validators() + this.rateLimit = new RateLimit() } // Top-level router for this library. All other methods in this class are for @@ -31,22 +33,27 @@ class UserRPC { // Route the call based on the value of the method property. switch (endpoint) { case 'createUser': + await this.rateLimit.limiter(rpcData.from) return await this.createUser(rpcData) case 'getAllUsers': await this.validators.ensureUser(rpcData) + await this.rateLimit.limiter(rpcData.from) return await this.getAll(rpcData) case 'getUser': user = await this.validators.ensureUser(rpcData) + await this.rateLimit.limiter(rpcData.from) return await this.getUser(rpcData, user) case 'updateUser': user = await this.validators.ensureTargetUserOrAdmin(rpcData) + await this.rateLimit.limiter(rpcData.from) return await this.updateUser(rpcData, user) case 'deleteUser': user = await this.validators.ensureTargetUserOrAdmin(rpcData) + await this.rateLimit.limiter(rpcData.from) return await this.deleteUser(rpcData, user) } } catch (err) { @@ -55,7 +62,7 @@ class UserRPC { return { success: false, - status: 500, + status: err.status || 500, message: err.message, endpoint } diff --git a/test/unit/json-rpc/a11-auth.unit.js b/test/unit/json-rpc/a11-auth.unit.js index 41bff49..76a09ae 100644 --- a/test/unit/json-rpc/a11-auth.unit.js +++ b/test/unit/json-rpc/a11-auth.unit.js @@ -15,6 +15,7 @@ process.env.SVC_ENV = 'test' // Local libraries const config = require('../../../config') const AuthRPC = require('../../../src/rpc/auth') +const RateLimit = require('../../../src/rpc/rate-limit') const UserLib = require('../../../src/lib/users') const userLib = new UserLib() @@ -48,6 +49,7 @@ describe('#AuthRPC', () => { sandbox = sinon.createSandbox() uut = new AuthRPC() + uut.rateLimit = new RateLimit({ max: 100 }) }) afterEach(() => sandbox.restore()) @@ -71,6 +73,7 @@ describe('#AuthRPC', () => { const authCall = jsonrpc.request(id, 'auth', { endpoint: 'authUser' }) const jsonStr = JSON.stringify(authCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.authRouter(rpcData) @@ -87,6 +90,7 @@ describe('#AuthRPC', () => { const authCall = jsonrpc.request(id, 'auth', { endpoint: 'authUser' }) const jsonStr = JSON.stringify(authCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.authRouter(rpcData) diff --git a/test/unit/json-rpc/a13-users.unit.js b/test/unit/json-rpc/a13-users.unit.js index 3dacaf1..aecb427 100644 --- a/test/unit/json-rpc/a13-users.unit.js +++ b/test/unit/json-rpc/a13-users.unit.js @@ -15,6 +15,7 @@ process.env.SVC_ENV = 'test' // Local libraries const config = require('../../../config') const UserRPC = require('../../../src/rpc/users') +const RateLimit = require('../../../src/rpc/rate-limit') const UserModel = require('../../../src/models/users') describe('#UserRPC', () => { @@ -40,6 +41,7 @@ describe('#UserRPC', () => { sandbox = sinon.createSandbox() uut = new UserRPC() + uut.rateLimit = new RateLimit({ max: 100 }) }) afterEach(() => sandbox.restore()) @@ -120,6 +122,7 @@ describe('#UserRPC', () => { const userCall = jsonrpc.request(id, 'users', { endpoint: 'createUser' }) const jsonStr = JSON.stringify(userCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.userRouter(rpcData) @@ -139,9 +142,10 @@ describe('#UserRPC', () => { }) const jsonStr = JSON.stringify(userCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.userRouter(rpcData) - + console.log('result', result) assert.equal(result, true) }) @@ -159,6 +163,7 @@ describe('#UserRPC', () => { }) const jsonStr = JSON.stringify(userCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.userRouter(rpcData) // console.log('result: ', result) @@ -179,7 +184,7 @@ describe('#UserRPC', () => { }) const jsonStr = JSON.stringify(userCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) - + rpcData.from = 'Origin request' const result = await uut.userRouter(rpcData) assert.equal(result, true) @@ -199,6 +204,7 @@ describe('#UserRPC', () => { }) const jsonStr = JSON.stringify(userCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.userRouter(rpcData) // console.log('result: ', result) @@ -216,6 +222,7 @@ describe('#UserRPC', () => { const userCall = jsonrpc.request(id, 'users', { endpoint: 'createUser' }) const jsonStr = JSON.stringify(userCall, null, 2) const rpcData = jsonrpc.parse(jsonStr) + rpcData.from = 'Origin request' const result = await uut.userRouter(rpcData) // console.log('result: ', result) diff --git a/test/unit/json-rpc/a14-rate-limits.js b/test/unit/json-rpc/a14-rate-limits.js new file mode 100644 index 0000000..4b34428 --- /dev/null +++ b/test/unit/json-rpc/a14-rate-limits.js @@ -0,0 +1,109 @@ +/* + Unit tests for the JSON RPC validator middleware. + + TODO: ensureTargetUserOrAdmin: it should exit quietly if user is an admin. +*/ + +// Public npm libraries +const sinon = require('sinon') +const assert = require('chai').assert + +// Set the environment variable to signal this is a test. +process.env.SVC_ENV = 'test' + +// Local libraries +const RateLimit = require('../../../src/rpc/rate-limit') + +describe('#rate-limit', () => { + let uut + let sandbox + + beforeEach(() => { + sandbox = sinon.createSandbox() + + uut = new RateLimit() + }) + + afterEach(() => sandbox.restore()) + + describe('#constructor', () => { + it('should use the options provided', async () => { + try { + const options = { + interval: { min: 10 }, + delayAfter: 1, + timeWait: { sec: 5 }, + max: 2, + onLimitReached: () => { + throw new Error('custom message error') + } + } + const _uut = new RateLimit(options) + + // Assert options + assert.equal(_uut.rateLimitOptions.interval.min, options.interval.min) + assert.equal(_uut.rateLimitOptions.delayAfter, options.delayAfter) + assert.equal(_uut.rateLimitOptions.timeWait.sec, options.timeWait.sec) + + const from = 'constructor test' + const firstRequest = await _uut.limiter(from) + assert.isTrue(firstRequest) + + const secondRequest = await _uut.limiter(from) + assert.isTrue(secondRequest) + + await _uut.limiter(from) + assert.fail('unexpected error') + } catch (error) { + assert.include(error.message, 'custom message error') + } + }) + }) + + describe('#onLimitReached', () => { + it('should throw error', async () => { + try { + uut.onLimitReached() + assert.fail('unexpected error') + } catch (error) { + assert.equal(error.status, 429) + assert.include( + error.message, + 'Too many requests, please try again later.' + ) + } + }) + }) + + describe('#limiter', () => { + it('should throw error if "from" input is not provider', async () => { + try { + await uut.limiter() + assert.fail('unexpected error') + } catch (error) { + assert.include(error.message, 'from must be a string') + } + }) + + it('should throw error 429', async () => { + try { + const _uut = new RateLimit({ max: 1 }) + const from = 'Origin request' + + const firstRequest = await _uut.limiter(from) + assert.isTrue(firstRequest) + + const secondRequest = await _uut.limiter(from) + assert.isTrue(secondRequest) + + await _uut.limiter(from) + assert.fail('unexpected error') + } catch (error) { + assert.include( + error.message, + 'Too many requests, please try again later.' + ) + } + }) + }) +})