From 9b94bd1bdd012217e322d2e008e16f3d7e57c22c Mon Sep 17 00:00:00 2001 From: Greg Guthe Date: Mon, 28 Nov 2016 09:08:23 -0500 Subject: [PATCH] Send violations to ip service (#148), r=@vbudhram This adds the tigerblood-js-client to the customs server. This is used to report when a request has been blocked to the Tigerblood service. It is disabled by default. --- docs/api.md | 7 +- lib/config/config.js | 38 ++ lib/server.js | 49 ++ npm-shrinkwrap.json | 511 +++++++++++++++++-- package.json | 1 + test/memcache-helper.js | 11 +- test/remote/send_violations_tests.js | 317 ++++++++++++ test/remote/too_many_authenticated_checks.js | 48 ++ test/test_reputation_server.js | 69 +++ test/test_reputation_server_stub.js | 46 ++ 10 files changed, 1035 insertions(+), 62 deletions(-) create mode 100644 test/remote/send_violations_tests.js create mode 100644 test/test_reputation_server.js create mode 100644 test/test_reputation_server_stub.js diff --git a/docs/api.md b/docs/api.md index 13dffe7..7be6f65 100644 --- a/docs/api.md +++ b/docs/api.md @@ -216,13 +216,10 @@ curl -v \ ### Response -Successful requests will produce a "200 OK" response with the lockout -advice in the JSON body: +Successful requests will produce a "200 OK" response: ```json -{ - "lockout": false -} +{} ``` `lockout` indicates whether or not the account should be locked out. diff --git a/lib/config/config.js b/lib/config/config.js index 53cd1c7..40e905a 100644 --- a/lib/config/config.js +++ b/lib/config/config.js @@ -211,6 +211,44 @@ module.exports = function (fs, path, url, convict) { format: 'nat', env: 'IP_BLOCKLIST_POLL_INTERVAL_SECONDS' } + }, + reputationService: { + enable: { + doc: 'Flag to enable using the IP Reputation Service', + format: Boolean, + default: false, + env: 'REPUTATION_SERVICE_ENABLE' + }, + host: { + doc: 'The reputation service IP address', + default: '127.0.0.1', + format: 'ipaddress', + env: 'REPUTATION_SERVICE_IP_ADDRESS' + }, + port: { + doc: 'The reputation service port', + default: 8080, + format: 'port', + env: 'REPUTATION_SERVICE_PORT' + }, + hawkId: { + doc: 'HAWK ID for sending blocked IPs to the IP Reputation Service', + default: 'root', + format: String, + env: 'REPUTATION_SERVICE_HAWK_ID' + }, + hawkKey: { + doc: 'HAWK key for sending blocked IPs to the IP Reputation Service', + default: 'toor', + format: String, + env: 'REPUTATION_SERVICE_HAWK_KEY' + }, + timeout: { + doc: 'timeout in ms to wait for requests sent to the IP Reputation Service', + default: 50, + format: 'int', + env: 'REPUTATION_SERVICE_TIMEOUT' + } } }) diff --git a/lib/server.js b/lib/server.js index 737cdd6..354409e 100755 --- a/lib/server.js +++ b/lib/server.js @@ -37,6 +37,16 @@ module.exports = function createServer(config, log) { } ) + if (config.reputationService.enable) { + var IPReputationClient = require('ip-reputation-js-client') + var ipClient = new IPReputationClient({ + host: config.reputationService.host, + port: config.reputationService.port, + id: config.reputationService.hawkId, + key: config.reputationService.hawkKey, + timeout: config.reputationService.timeout + }) + } var limits = require('./limits')(config, mc, log) var allowedIPs = require('./allowed_ips')(config, mc, log) var allowedEmailDomains = require('./allowed_email_domains')(config, mc, log) @@ -215,6 +225,13 @@ module.exports = function createServer(config, log) { suspect: result.suspect }) res.send(result) + + if (config.reputationService.enable && result.block) { + ipClient.sendViolation(ip, 'fxa:request.check.block.' + action) + .catch(function (err) { + log.error({ op: 'request.check.sendViolation.block.' + action, ip: ip, err: err }) + }) + } }, function (err) { log.error({ op: 'request.check', email: email, ip: ip, action: action, err: err }) @@ -266,6 +283,13 @@ module.exports = function createServer(config, log) { function (result) { log.info({ op: 'request.checkAuthenticated', block: result.block }) res.send(result) + + if (config.reputationService.enable && result.block) { + ipClient.sendViolation(ip, 'fxa:request.checkAuthenticated.block.' + action) + .catch(function (err) { + log.error({ op: 'request.checkAuthenticated.sendViolation.block.' + action, ip: ip, err: err }) + }) + } }, function (err) { log.error({ op: 'request.checkAuthenticated', err: err }) @@ -274,6 +298,13 @@ module.exports = function createServer(config, log) { block: true, retryAfter: limits.blockIntervalSeconds }) + + if (config.reputationService.enable) { + ipClient.sendViolation(ip, 'fxa:request.checkAuthenticated.block.' + action) + .catch(function (err) { + log.error({ op: 'request.checkAuthenticated.sendViolation.block.' + action, ip: ip, err: err }) + }) + } } ) .done(next, next) @@ -299,6 +330,14 @@ module.exports = function createServer(config, log) { function (emailRecord, ipRecord, ipEmailRecord) { ipRecord.addBadLogin({ email: email, errno: errno }) ipEmailRecord.addBadLogin() + + if (config.reputationService.enable && ipRecord.isOverBadLogins()) { + ipClient.sendViolation(ip, 'fxa:request.failedLoginAttempt.isOverBadLogins') + .catch(function (err) { + log.error({ op: 'request.failedLoginAttempt.sendViolation.rateLimited', ip: ip, err: err }) + }) + } + return setRecords(email, ip, emailRecord, ipRecord, ipEmailRecord) .then( function () { @@ -408,6 +447,16 @@ module.exports = function createServer(config, log) { res.send(500, err) } ) + .then( + function () { + if (config.reputationService.enable) { + ipClient.sendViolation(ip, 'fxa:request.blockIp') + .catch(function (err) { + log.error({ op: 'request.blockIp.sendViolation', ip: ip, err: err }) + }) + } + } + ) .done(next, next) } ) diff --git a/npm-shrinkwrap.json b/npm-shrinkwrap.json index 1702471..fc5a9b7 100644 --- a/npm-shrinkwrap.json +++ b/npm-shrinkwrap.json @@ -2,11 +2,41 @@ "name": "fxa-customs-server", "version": "0.72.1", "dependencies": { + "ansi-regex": { + "version": "2.0.0", + "from": "ansi-regex@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-2.0.0.tgz" + }, + "ansi-styles": { + "version": "2.2.1", + "from": "ansi-styles@>=2.2.1 <3.0.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-2.2.1.tgz" + }, + "asn1": { + "version": "0.2.3", + "from": "asn1@>=0.2.3 <0.3.0", + "resolved": "https://registry.npmjs.org/asn1/-/asn1-0.2.3.tgz" + }, + "assert-plus": { + "version": "0.2.0", + "from": "assert-plus@>=0.2.0 <0.3.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-0.2.0.tgz" + }, + "asynckit": { + "version": "0.4.0", + "from": "asynckit@>=0.4.0 <0.5.0", + "resolved": "https://registry.npmjs.org/asynckit/-/asynckit-0.4.0.tgz" + }, "aws-sdk": { "version": "2.3.3", "from": "aws-sdk@2.3.3", "resolved": "https://registry.npmjs.org/aws-sdk/-/aws-sdk-2.3.3.tgz", "dependencies": { + "jmespath": { + "version": "0.15.0", + "from": "jmespath@0.15.0", + "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz" + }, "sax": { "version": "1.1.5", "from": "sax@1.1.5", @@ -28,14 +58,25 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-3.5.0.tgz" } } - }, - "jmespath": { - "version": "0.15.0", - "from": "jmespath@0.15.0", - "resolved": "https://registry.npmjs.org/jmespath/-/jmespath-0.15.0.tgz" } } }, + "aws-sign2": { + "version": "0.6.0", + "from": "aws-sign2@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/aws-sign2/-/aws-sign2-0.6.0.tgz" + }, + "aws4": { + "version": "1.5.0", + "from": "aws4@>=1.2.1 <2.0.0", + "resolved": "https://registry.npmjs.org/aws4/-/aws4-1.5.0.tgz" + }, + "bcrypt-pbkdf": { + "version": "1.0.0", + "from": "bcrypt-pbkdf@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/bcrypt-pbkdf/-/bcrypt-pbkdf-1.0.0.tgz", + "optional": true + }, "bl": { "version": "1.1.2", "from": "bl@1.1.2", @@ -85,6 +126,11 @@ "from": "bluebird@3.3.4", "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.3.4.tgz" }, + "boom": { + "version": "2.10.1", + "from": "boom@2.x.x", + "resolved": "https://registry.npmjs.org/boom/-/boom-2.10.1.tgz" + }, "bunyan": { "version": "1.8.0", "from": "bunyan@1.8.0", @@ -94,82 +140,103 @@ "version": "0.6.0", "from": "dtrace-provider@>=0.6.0 <0.7.0", "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", + "optional": true, "dependencies": { "nan": { "version": "2.4.0", "from": "nan@>=2.0.8 <3.0.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz" + "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz", + "optional": true } } }, + "moment": { + "version": "2.15.0", + "from": "moment@>=2.10.6 <3.0.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.15.0.tgz", + "optional": true + }, "mv": { "version": "2.1.1", "from": "mv@>=2.0.0 <3.0.0", "resolved": "https://registry.npmjs.org/mv/-/mv-2.1.1.tgz", + "optional": true, "dependencies": { "mkdirp": { "version": "0.5.1", "from": "mkdirp@>=0.5.1 <0.6.0", "resolved": "https://registry.npmjs.org/mkdirp/-/mkdirp-0.5.1.tgz", + "optional": true, "dependencies": { "minimist": { "version": "0.0.8", "from": "minimist@0.0.8", - "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz" + "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.8.tgz", + "optional": true } } }, "ncp": { "version": "2.0.0", "from": "ncp@>=2.0.0 <2.1.0", - "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz" + "resolved": "https://registry.npmjs.org/ncp/-/ncp-2.0.0.tgz", + "optional": true }, "rimraf": { "version": "2.4.5", "from": "rimraf@>=2.4.0 <2.5.0", "resolved": "https://registry.npmjs.org/rimraf/-/rimraf-2.4.5.tgz", + "optional": true, "dependencies": { "glob": { "version": "6.0.4", "from": "glob@>=6.0.1 <7.0.0", "resolved": "https://registry.npmjs.org/glob/-/glob-6.0.4.tgz", + "optional": true, "dependencies": { "inflight": { "version": "1.0.5", "from": "inflight@>=1.0.4 <2.0.0", "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.5.tgz", + "optional": true, "dependencies": { "wrappy": { "version": "1.0.2", "from": "wrappy@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz" + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "optional": true } } }, "inherits": { "version": "2.0.3", "from": "inherits@>=2.0.0 <3.0.0", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz" + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.3.tgz", + "optional": true }, "minimatch": { "version": "3.0.3", "from": "minimatch@>=2.0.0 <3.0.0||>=3.0.0 <4.0.0", "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-3.0.3.tgz", + "optional": true, "dependencies": { "brace-expansion": { "version": "1.1.6", "from": "brace-expansion@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-1.1.6.tgz", + "optional": true, "dependencies": { "balanced-match": { "version": "0.4.2", "from": "balanced-match@>=0.4.1 <0.5.0", - "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz" + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-0.4.2.tgz", + "optional": true }, "concat-map": { "version": "0.0.1", "from": "concat-map@0.0.1", - "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz" + "resolved": "https://registry.npmjs.org/concat-map/-/concat-map-0.0.1.tgz", + "optional": true } } } @@ -190,7 +257,8 @@ "path-is-absolute": { "version": "1.0.0", "from": "path-is-absolute@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz" + "resolved": "https://registry.npmjs.org/path-is-absolute/-/path-is-absolute-1.0.0.tgz", + "optional": true } } } @@ -201,15 +269,31 @@ "safe-json-stringify": { "version": "1.0.3", "from": "safe-json-stringify@>=1.0.0 <2.0.0", - "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.3.tgz" - }, - "moment": { - "version": "2.15.0", - "from": "moment@>=2.10.6 <3.0.0", - "resolved": "https://registry.npmjs.org/moment/-/moment-2.15.0.tgz" + "resolved": "https://registry.npmjs.org/safe-json-stringify/-/safe-json-stringify-1.0.3.tgz", + "optional": true } } }, + "caseless": { + "version": "0.11.0", + "from": "caseless@>=0.11.0 <0.12.0", + "resolved": "https://registry.npmjs.org/caseless/-/caseless-0.11.0.tgz" + }, + "chalk": { + "version": "1.1.3", + "from": "chalk@>=1.1.1 <1.2.0", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-1.1.3.tgz" + }, + "combined-stream": { + "version": "1.0.5", + "from": "combined-stream@>=1.0.5 <1.1.0", + "resolved": "https://registry.npmjs.org/combined-stream/-/combined-stream-1.0.5.tgz" + }, + "commander": { + "version": "2.9.0", + "from": "commander@>=2.9.0 <3.0.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-2.9.0.tgz" + }, "convict": { "version": "1.2.0", "from": "convict@1.2.0", @@ -249,15 +333,15 @@ "from": "optimist@0.6.1", "resolved": "https://registry.npmjs.org/optimist/-/optimist-0.6.1.tgz", "dependencies": { - "wordwrap": { - "version": "0.0.3", - "from": "wordwrap@>=0.0.2 <0.1.0", - "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" - }, "minimist": { "version": "0.0.10", "from": "minimist@>=0.0.1 <0.1.0", "resolved": "https://registry.npmjs.org/minimist/-/minimist-0.0.10.tgz" + }, + "wordwrap": { + "version": "0.0.3", + "from": "wordwrap@>=0.0.2 <0.1.0", + "resolved": "https://registry.npmjs.org/wordwrap/-/wordwrap-0.0.3.tgz" } } }, @@ -271,11 +355,6 @@ "from": "varify@0.1.1", "resolved": "https://registry.npmjs.org/varify/-/varify-0.1.1.tgz", "dependencies": { - "through": { - "version": "2.3.8", - "from": "through@>=2.3.4 <2.4.0", - "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" - }, "redeyed": { "version": "0.4.4", "from": "redeyed@>=0.4.2 <0.5.0", @@ -287,26 +366,239 @@ "resolved": "https://registry.npmjs.org/esprima/-/esprima-1.0.4.tgz" } } + }, + "through": { + "version": "2.3.8", + "from": "through@>=2.3.4 <2.4.0", + "resolved": "https://registry.npmjs.org/through/-/through-2.3.8.tgz" } } } } }, + "cryptiles": { + "version": "2.0.5", + "from": "cryptiles@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/cryptiles/-/cryptiles-2.0.5.tgz" + }, "csv-parse": { "version": "1.0.4", "from": "csv-parse@1.0.4", "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-1.0.4.tgz" }, + "dashdash": { + "version": "1.14.0", + "from": "dashdash@>=1.12.0 <2.0.0", + "resolved": "https://registry.npmjs.org/dashdash/-/dashdash-1.14.0.tgz", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "from": "assert-plus@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + } + } + }, "deep-equal": { "version": "1.0.1", "from": "deep-equal@1.0.1", "resolved": "https://registry.npmjs.org/deep-equal/-/deep-equal-1.0.1.tgz" }, + "delayed-stream": { + "version": "1.0.0", + "from": "delayed-stream@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/delayed-stream/-/delayed-stream-1.0.0.tgz" + }, + "ecc-jsbn": { + "version": "0.1.1", + "from": "ecc-jsbn@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/ecc-jsbn/-/ecc-jsbn-0.1.1.tgz", + "optional": true + }, + "escape-string-regexp": { + "version": "1.0.5", + "from": "escape-string-regexp@>=1.0.2 <2.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-1.0.5.tgz" + }, + "extend": { + "version": "3.0.0", + "from": "extend@~3.0.0", + "resolved": "https://registry.npmjs.org/extend/-/extend-3.0.0.tgz" + }, + "extsprintf": { + "version": "1.0.2", + "from": "extsprintf@1.0.2", + "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.0.2.tgz" + }, + "forever-agent": { + "version": "0.6.1", + "from": "forever-agent@>=0.6.1 <0.7.0", + "resolved": "https://registry.npmjs.org/forever-agent/-/forever-agent-0.6.1.tgz" + }, + "generate-function": { + "version": "2.0.0", + "from": "generate-function@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.0.0.tgz" + }, + "generate-object-property": { + "version": "1.2.0", + "from": "generate-object-property@>=1.1.0 <2.0.0", + "resolved": "https://registry.npmjs.org/generate-object-property/-/generate-object-property-1.2.0.tgz" + }, + "getpass": { + "version": "0.1.6", + "from": "getpass@>=0.1.1 <0.2.0", + "resolved": "https://registry.npmjs.org/getpass/-/getpass-0.1.6.tgz", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "from": "assert-plus@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + } + } + }, + "graceful-readlink": { + "version": "1.0.1", + "from": "graceful-readlink@>=1.0.0", + "resolved": "https://registry.npmjs.org/graceful-readlink/-/graceful-readlink-1.0.1.tgz" + }, + "har-validator": { + "version": "2.0.6", + "from": "har-validator@>=2.0.6 <2.1.0", + "resolved": "https://registry.npmjs.org/har-validator/-/har-validator-2.0.6.tgz" + }, + "has-ansi": { + "version": "2.0.0", + "from": "has-ansi@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/has-ansi/-/has-ansi-2.0.0.tgz" + }, + "hawk": { + "version": "3.1.3", + "from": "hawk@>=3.1.3 <3.2.0", + "resolved": "https://registry.npmjs.org/hawk/-/hawk-3.1.3.tgz" + }, + "hoek": { + "version": "2.16.3", + "from": "hoek@2.x.x", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-2.16.3.tgz" + }, + "http-signature": { + "version": "1.1.1", + "from": "http-signature@>=1.1.0 <1.2.0", + "resolved": "https://registry.npmjs.org/http-signature/-/http-signature-1.1.1.tgz" + }, "ip": { "version": "1.1.3", "from": "ip@1.1.3", "resolved": "https://registry.npmjs.org/ip/-/ip-1.1.3.tgz" }, + "ip-reputation-js-client": { + "version": "1.0.1", + "from": "ip-reputation-js-client@1.0.1", + "resolved": "https://registry.npmjs.org/ip-reputation-js-client/-/ip-reputation-js-client-1.0.1.tgz", + "dependencies": { + "bluebird": { + "version": "3.4.6", + "from": "bluebird@3.4.6", + "resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.4.6.tgz" + }, + "form-data": { + "version": "2.0.0", + "from": "form-data@>=2.0.0 <2.1.0", + "resolved": "https://registry.npmjs.org/form-data/-/form-data-2.0.0.tgz" + }, + "request": { + "version": "2.75.0", + "from": "request@2.75.0", + "resolved": "https://registry.npmjs.org/request/-/request-2.75.0.tgz" + }, + "tough-cookie": { + "version": "2.3.2", + "from": "tough-cookie@>=2.3.0 <2.4.0", + "resolved": "https://registry.npmjs.org/tough-cookie/-/tough-cookie-2.3.2.tgz" + } + } + }, + "is-my-json-valid": { + "version": "2.15.0", + "from": "is-my-json-valid@>=2.10.0 <3.0.0", + "resolved": "https://registry.npmjs.org/is-my-json-valid/-/is-my-json-valid-2.15.0.tgz", + "dependencies": { + "xtend": { + "version": "4.0.1", + "from": "xtend@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/xtend/-/xtend-4.0.1.tgz" + } + } + }, + "is-property": { + "version": "1.0.2", + "from": "is-property@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz" + }, + "is-typedarray": { + "version": "1.0.0", + "from": "is-typedarray@>=1.0.0 <1.1.0", + "resolved": "https://registry.npmjs.org/is-typedarray/-/is-typedarray-1.0.0.tgz" + }, + "isemail": { + "version": "2.2.1", + "from": "isemail@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/isemail/-/isemail-2.2.1.tgz" + }, + "isstream": { + "version": "0.1.2", + "from": "isstream@>=0.1.2 <0.2.0", + "resolved": "https://registry.npmjs.org/isstream/-/isstream-0.1.2.tgz" + }, + "items": { + "version": "2.1.1", + "from": "items@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/items/-/items-2.1.1.tgz" + }, + "jodid25519": { + "version": "1.0.2", + "from": "jodid25519@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/jodid25519/-/jodid25519-1.0.2.tgz", + "optional": true + }, + "joi": { + "version": "9.2.0", + "from": "joi@9.2.0", + "resolved": "https://registry.npmjs.org/joi/-/joi-9.2.0.tgz", + "dependencies": { + "hoek": { + "version": "4.1.0", + "from": "hoek@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.1.0.tgz" + } + } + }, + "jsbn": { + "version": "0.1.0", + "from": "jsbn@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/jsbn/-/jsbn-0.1.0.tgz", + "optional": true + }, + "json-schema": { + "version": "0.2.3", + "from": "json-schema@0.2.3", + "resolved": "https://registry.npmjs.org/json-schema/-/json-schema-0.2.3.tgz" + }, + "json-stringify-safe": { + "version": "5.0.1", + "from": "json-stringify-safe@>=5.0.1 <5.1.0", + "resolved": "https://registry.npmjs.org/json-stringify-safe/-/json-stringify-safe-5.0.1.tgz" + }, + "jsonpointer": { + "version": "4.0.0", + "from": "jsonpointer@>=4.0.0 <5.0.0", + "resolved": "https://registry.npmjs.org/jsonpointer/-/jsonpointer-4.0.0.tgz" + }, + "jsprim": { + "version": "1.3.1", + "from": "jsprim@>=1.2.2 <2.0.0", + "resolved": "https://registry.npmjs.org/jsprim/-/jsprim-1.3.1.tgz" + }, "memcached": { "version": "2.2.1", "from": "memcached@2.2.1", @@ -343,6 +635,21 @@ } } }, + "mime-db": { + "version": "1.25.0", + "from": "mime-db@>=1.25.0 <1.26.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.25.0.tgz" + }, + "mime-types": { + "version": "2.1.13", + "from": "mime-types@>=2.1.7 <2.2.0", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.13.tgz" + }, + "moment": { + "version": "2.17.0", + "from": "moment@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/moment/-/moment-2.17.0.tgz" + }, "newrelic": { "version": "1.30.1", "from": "newrelic@1.30.1", @@ -358,11 +665,6 @@ "from": "inherits@>=2.0.1 <2.1.0", "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" }, - "typedarray": { - "version": "0.0.6", - "from": "typedarray@>=0.0.5 <0.1.0", - "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" - }, "readable-stream": { "version": "2.0.6", "from": "readable-stream@>=2.0.0 <2.1.0", @@ -394,6 +696,11 @@ "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz" } } + }, + "typedarray": { + "version": "0.0.6", + "from": "typedarray@>=0.0.5 <0.1.0", + "resolved": "https://registry.npmjs.org/typedarray/-/typedarray-0.0.6.tgz" } } }, @@ -441,6 +748,11 @@ "from": "core-util-is@>=1.0.0 <1.1.0", "resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.2.tgz" }, + "inherits": { + "version": "2.0.1", + "from": "inherits@>=2.0.1 <2.1.0", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" + }, "isarray": { "version": "0.0.1", "from": "isarray@0.0.1", @@ -450,11 +762,6 @@ "version": "0.10.31", "from": "string_decoder@>=0.10.0 <0.11.0", "resolved": "https://registry.npmjs.org/string_decoder/-/string_decoder-0.10.31.tgz" - }, - "inherits": { - "version": "2.0.1", - "from": "inherits@>=2.0.1 <2.1.0", - "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.1.tgz" } } }, @@ -470,6 +777,36 @@ } } }, + "node-uuid": { + "version": "1.4.7", + "from": "node-uuid@>=1.4.7 <1.5.0", + "resolved": "https://registry.npmjs.org/node-uuid/-/node-uuid-1.4.7.tgz" + }, + "oauth-sign": { + "version": "0.8.2", + "from": "oauth-sign@>=0.8.1 <0.9.0", + "resolved": "https://registry.npmjs.org/oauth-sign/-/oauth-sign-0.8.2.tgz" + }, + "pinkie": { + "version": "2.0.4", + "from": "pinkie@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pinkie/-/pinkie-2.0.4.tgz" + }, + "pinkie-promise": { + "version": "2.0.1", + "from": "pinkie-promise@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/pinkie-promise/-/pinkie-promise-2.0.1.tgz" + }, + "punycode": { + "version": "1.4.1", + "from": "punycode@>=1.4.1 <2.0.0", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-1.4.1.tgz" + }, + "qs": { + "version": "6.2.1", + "from": "qs@>=6.2.0 <6.3.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.2.1.tgz" + }, "restify": { "version": "4.1.1", "from": "restify@4.1.1", @@ -507,15 +844,29 @@ "from": "csv-parse@>=1.0.0 <2.0.0", "resolved": "https://registry.npmjs.org/csv-parse/-/csv-parse-1.1.7.tgz" }, - "stream-transform": { - "version": "0.1.1", - "from": "stream-transform@>=0.1.0 <0.2.0", - "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-0.1.1.tgz" - }, "csv-stringify": { "version": "0.0.8", "from": "csv-stringify@>=0.0.8 <0.0.9", "resolved": "https://registry.npmjs.org/csv-stringify/-/csv-stringify-0.0.8.tgz" + }, + "stream-transform": { + "version": "0.1.1", + "from": "stream-transform@>=0.1.0 <0.2.0", + "resolved": "https://registry.npmjs.org/stream-transform/-/stream-transform-0.1.1.tgz" + } + } + }, + "dtrace-provider": { + "version": "0.6.0", + "from": "dtrace-provider@>=0.6.0 <0.7.0", + "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", + "optional": true, + "dependencies": { + "nan": { + "version": "2.4.0", + "from": "nan@>=2.0.8 <3.0.0", + "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz", + "optional": true } } }, @@ -762,20 +1113,68 @@ "resolved": "https://registry.npmjs.org/extsprintf/-/extsprintf-1.3.0.tgz" } } - }, - "dtrace-provider": { - "version": "0.6.0", - "from": "dtrace-provider@>=0.6.0 <0.7.0", - "resolved": "https://registry.npmjs.org/dtrace-provider/-/dtrace-provider-0.6.0.tgz", - "dependencies": { - "nan": { - "version": "2.4.0", - "from": "nan@>=2.0.8 <3.0.0", - "resolved": "https://registry.npmjs.org/nan/-/nan-2.4.0.tgz" - } - } } } + }, + "sntp": { + "version": "1.0.9", + "from": "sntp@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sntp/-/sntp-1.0.9.tgz" + }, + "sshpk": { + "version": "1.10.1", + "from": "sshpk@>=1.7.0 <2.0.0", + "resolved": "https://registry.npmjs.org/sshpk/-/sshpk-1.10.1.tgz", + "dependencies": { + "assert-plus": { + "version": "1.0.0", + "from": "assert-plus@>=1.0.0 <2.0.0", + "resolved": "https://registry.npmjs.org/assert-plus/-/assert-plus-1.0.0.tgz" + } + } + }, + "stringstream": { + "version": "0.0.5", + "from": "stringstream@>=0.0.4 <0.1.0", + "resolved": "https://registry.npmjs.org/stringstream/-/stringstream-0.0.5.tgz" + }, + "strip-ansi": { + "version": "3.0.1", + "from": "strip-ansi@>=3.0.0 <4.0.0", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-3.0.1.tgz" + }, + "supports-color": { + "version": "2.0.0", + "from": "supports-color@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-2.0.0.tgz" + }, + "topo": { + "version": "2.0.2", + "from": "topo@>=2.0.0 <3.0.0", + "resolved": "https://registry.npmjs.org/topo/-/topo-2.0.2.tgz", + "dependencies": { + "hoek": { + "version": "4.1.0", + "from": "hoek@4.x.x", + "resolved": "https://registry.npmjs.org/hoek/-/hoek-4.1.0.tgz" + } + } + }, + "tunnel-agent": { + "version": "0.4.3", + "from": "tunnel-agent@>=0.4.1 <0.5.0", + "resolved": "https://registry.npmjs.org/tunnel-agent/-/tunnel-agent-0.4.3.tgz" + }, + "tweetnacl": { + "version": "0.14.3", + "from": "tweetnacl@>=0.14.0 <0.15.0", + "resolved": "https://registry.npmjs.org/tweetnacl/-/tweetnacl-0.14.3.tgz", + "optional": true + }, + "verror": { + "version": "1.3.6", + "from": "verror@1.3.6", + "resolved": "https://registry.npmjs.org/verror/-/verror-1.3.6.tgz" } } } diff --git a/package.json b/package.json index 5fc7332..56a072e 100644 --- a/package.json +++ b/package.json @@ -26,6 +26,7 @@ "csv-parse": "1.0.4", "deep-equal": "1.0.1", "ip": "1.1.3", + "ip-reputation-js-client": "1.0.1", "memcached": "2.2.1", "newrelic": "1.30.1", "restify": "4.1.1" diff --git a/test/memcache-helper.js b/test/memcache-helper.js index 6ac1109..c23d3df 100644 --- a/test/memcache-helper.js +++ b/test/memcache-helper.js @@ -46,7 +46,10 @@ var mc = new Memcached( module.exports.mc = mc var TEST_EMAIL = 'test@example.com' +var TEST_EMAIL_2 = 'test+2@example.com' var TEST_IP = '192.0.2.1' +var ALLOWED_IP = '192.0.3.1' +var TEST_UID = 'test-uid' var limits = require('../lib/limits')(config, mc, console) var allowedIPs = require('../lib/allowed_ips')(config, mc, console) @@ -118,8 +121,14 @@ function clearEverything(cb) { mc.delAsync('allowedEmailDomains'), mc.delAsync('requestChecks'), mc.delAsync(TEST_EMAIL), + mc.delAsync(TEST_EMAIL_2), + mc.delAsync(TEST_IP), + mc.delAsync(ALLOWED_IP), + mc.delAsync(ALLOWED_IP + TEST_EMAIL), + mc.delAsync(ALLOWED_IP + TEST_EMAIL_2), mc.delAsync(TEST_IP + TEST_EMAIL), - mc.delAsync(TEST_IP) + mc.delAsync(TEST_IP + TEST_EMAIL_2), + mc.delAsync(TEST_UID) ]) .then(function () { mc.end() diff --git a/test/remote/send_violations_tests.js b/test/remote/send_violations_tests.js new file mode 100644 index 0000000..2dba948 --- /dev/null +++ b/test/remote/send_violations_tests.js @@ -0,0 +1,317 @@ +/* Any copyright is dedicated to the Public Domain. + * http://creativecommons.org/publicdomain/zero/1.0/ */ + +var test = require('tap').test +var TestServer = require('../test_server') +var ReputationServerStub = require('../test_reputation_server') +var Promise = require('bluebird') +var restify = require('restify') +var mcHelper = require('../memcache-helper') + +var TEST_EMAIL = 'test@example.com' +var TEST_EMAIL_2 = 'test+2@example.com' +var TEST_IP = '192.0.2.1' +var ALLOWED_IP = '192.0.3.1' +var TEST_UID = 'test-uid' +var TEST_ACTION = 'action1' +var TEST_CHECK_ACTION = 'recoveryEmailVerifyCode' + +// wait for the violation to be sent for endpoints that respond +// before sending violation +var TEST_DELAY_MS = 500 + +var config = { + listen: { + port: 7000 + }, + limits: { + rateLimitIntervalSeconds: 1 + }, + reputationService: { + enable: true, + host: '127.0.0.1', + port: 9009, + timeout: 25 + } +} + +// Override limit values for testing +process.env.ALLOWED_IPS = ALLOWED_IP +process.env.MAX_VERIFY_CODES = 2 +process.env.MAX_BAD_LOGINS_PER_IP = 1 +process.env.UID_RATE_LIMIT = 3 +process.env.UID_RATE_LIMIT_INTERVAL_SECONDS = 2 +process.env.UID_RATE_LIMIT_BAN_DURATION_SECONDS = 2 +process.env.RATE_LIMIT_INTERVAL_SECONDS = config.limits.rateLimitIntervalSeconds + +// Enable reputation test server +process.env.REPUTATION_SERVICE_ENABLE = config.reputationService.enable +process.env.REPUTATION_SERVICE_IP_ADDRESS = config.reputationService.host +process.env.REPUTATION_SERVICE_PORT = config.reputationService.port +process.env.REPUTATION_SERVICE_TIMEOUT = config.reputationService.timeout + +var testServer = new TestServer(config) +var reputationServer = new ReputationServerStub(config) + +var client = restify.createJsonClient({ + url: 'http://127.0.0.1:' + config.listen.port +}) +var reputationClient = restify.createJsonClient({ + url: 'http://' + config.reputationService.host + ':' + config.reputationService.port +}) + +Promise.promisifyAll(client, { multiArgs: true }) +Promise.promisifyAll(reputationClient, { multiArgs: true }) + +test( + 'startup', + function (t) { + testServer.start(function (err) { + t.type(testServer.server, 'object', 'test server was started') + t.notOk(err, 'no errors were returned') + t.end() + }) + } +) + +test( + 'clear everything', + function (t) { + mcHelper.clearEverything( + function (err) { + t.notOk(err, 'no errors were returned') + t.end() + } + ) + } +) + +test( + '/check resulting in lockout runs when send violation fails', + function (t) { + return client.postAsync('/check', { email: TEST_EMAIL, ip: TEST_IP, action: TEST_CHECK_ACTION }) + .spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'first action noted') + t.equal(obj.block, false, 'first action not blocked') + return client.postAsync('/check', { email: TEST_EMAIL, ip: TEST_IP, action: TEST_CHECK_ACTION }) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'second action noted') + t.equal(obj.block, false, 'second action not blocked') + return client.postAsync('/check', { email: TEST_EMAIL, ip: TEST_IP, action: TEST_CHECK_ACTION }) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'third action attempt noted') + t.equal(obj.block, true, 'third action blocked') + }).catch(function(err) { + t.fail(err) + t.end() + }) + } +) + +test( + '/checkAuthenticated rate limited runs when sends violation fails', + function (t) { + return client.postAsync('/checkAuthenticated', { action: TEST_ACTION, ip: TEST_IP, uid: TEST_UID }) + .spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'returns 200 for /checkAuthenticated 1') + t.equal(obj.block, false, 'not rate limited') + return client.postAsync('/checkAuthenticated', { action: TEST_ACTION, ip: TEST_IP, uid: TEST_UID }) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'returns 200 for /checkAuthenticated 2') + t.equal(obj.block, false, 'not rate limited') + return client.postAsync('/checkAuthenticated', { action: TEST_ACTION, ip: TEST_IP, uid: TEST_UID }) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'returns 200 for /checkAuthenticated 3') + t.equal(obj.block, true, 'rate limited') + t.end() + }).catch(function(err) { + t.fail(err) + t.end() + }) + } +) + +test( + '/failedLoginAttempt resulting in lockout runs when send violation fails', + function (t) { + return client.postAsync('/failedLoginAttempt', { email: TEST_EMAIL, ip: TEST_IP }) + .spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'first login attempt noted') + return client.postAsync('/failedLoginAttempt', { email: TEST_EMAIL_2, ip: TEST_IP }) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'second login attempt noted') + return client.postAsync('/failedLoginAttempt', { email: TEST_EMAIL, ip: TEST_IP }) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'third login attempt noted') + return client.postAsync('/failedLoginAttempt', { email: TEST_EMAIL, ip: TEST_IP }) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'fourth login attempt noted') + return mcHelper.badLoginCheck() + }).then(function (records) { + t.equal(records.ipEmailRecord.isOverBadLogins(), true, 'is now over bad logins') + t.end() + }).catch(function(err) { + t.fail(err) + t.end() + }) + } +) + +test( + '/blockIp returns when send violation fails', + function (t) { + return client.postAsync('/blockIp', { ip: TEST_IP }) + .spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'blockIp returns 200') + t.end() + }) + } +) + +test( + 'clear everything again', + function (t) { + mcHelper.clearEverything( + function (err) { + t.notOk(err, 'no errors were returned') + t.end() + } + ) + } +) + +test( + 'startup reputation service', + function (t) { + reputationServer.start(function (err) { + t.type(reputationServer.server, 'object', 'test server was started') + t.notOk(err, 'no errors were returned') + t.end() + }) + } +) + +test( + 'sends violation /check resulting in lockout', + function (t) { + return client.postAsync('/check', { email: TEST_EMAIL, ip: TEST_IP, action: TEST_CHECK_ACTION }) + .spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'first action noted') + t.equal(obj.block, false, 'first action not blocked') + return client.postAsync('/check', { email: TEST_EMAIL, ip: TEST_IP, action: TEST_CHECK_ACTION }) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'second action noted') + t.equal(obj.block, false, 'second action not blocked') + return client.postAsync('/check', { email: TEST_EMAIL, ip: TEST_IP, action: TEST_CHECK_ACTION }) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'third action attempt noted') + t.equal(obj.block, true, 'third action blocked') + return Promise.delay(TEST_DELAY_MS) + }).then(function () { + return reputationClient.getAsync('/mostRecentViolation/' + TEST_IP) + }).spread(function (req, res, obj) { + t.equal(res.body, '"fxa:request.check.block.' + TEST_CHECK_ACTION + '"', 'sends violation when /check') + return reputationClient.delAsync('/mostRecentViolation/' + TEST_IP) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'Failed to clear sent violation from test server.') + t.end() + }).catch(function(err) { + t.fail(err) + t.end() + }) + } +) + +test( + 'sends violation when /checkAuthenticated rate limited', + function (t) { + return client.postAsync('/checkAuthenticated', { action: TEST_ACTION, ip: TEST_IP, uid: TEST_UID }) + .spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'returns 200 for /checkAuthenticated 1') + t.equal(obj.block, false, 'not rate limited') + return client.postAsync('/checkAuthenticated', { action: TEST_ACTION, ip: TEST_IP, uid: TEST_UID }) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'returns 200 for /checkAuthenticated 2') + t.equal(obj.block, false, 'not rate limited') + return client.postAsync('/checkAuthenticated', { action: TEST_ACTION, ip: TEST_IP, uid: TEST_UID }) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'returns 200 for /checkAuthenticated 3') + t.equal(obj.block, true, 'rate limited') + return Promise.delay(TEST_DELAY_MS) + }).then(function () { + return reputationClient.getAsync('/mostRecentViolation/' + TEST_IP) + }).spread(function (req, res, obj) { + t.equal(res.body, '"fxa:request.checkAuthenticated.block.action1"', 'Violation sent.') + return reputationClient.delAsync('/mostRecentViolation/' + TEST_IP) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'Failed to clear sent violation from test server.') + t.end() + }).catch(function(err) { + t.fail(err) + t.end() + }) + } +) + +test( + 'sends violation /failedLoginAttempt results in lockout', + function (t) { + return client.postAsync('/failedLoginAttempt', { email: TEST_EMAIL, ip: TEST_IP }) + .spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'first login attempt noted') + return client.postAsync('/failedLoginAttempt', { email: TEST_EMAIL_2, ip: TEST_IP }) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'second login attempt noted') + return reputationClient.getAsync('/mostRecentViolation/' + TEST_IP) + }).spread(function (req, res, obj) { + t.equal(res.body, '"fxa:request.failedLoginAttempt.isOverBadLogins"', 'sends violation.') + return reputationClient.delAsync('/mostRecentViolation/' + TEST_IP) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'Failed to clear sent violation from test server.') + t.end() + }).catch(function(err) { + t.fail(err) + t.end() + }) + } +) + +test( + 'sends violation for blocked IP from /blockIp request', + function (t) { + return client.postAsync('/blockIp', { ip: TEST_IP }) + .spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'blockIp returns 200') + return Promise.delay(TEST_DELAY_MS) + }).then(function () { + return reputationClient.getAsync('/mostRecentViolation/' + TEST_IP) + }).spread(function (req, res, obj) { + t.equal(res.body, '"fxa:request.blockIp"', 'sends violation when IP blocked') + return reputationClient.delAsync('/mostRecentViolation/' + TEST_IP) + }).spread(function (req, res, obj) { + t.equal(res.statusCode, 200, 'Failed to clear sent violation from test server.') + t.end() + }).catch(function(err) { + t.fail(err) + t.end() + }) + } +) + +test( + 'teardown test reptuation server', + function (t) { + reputationServer.stop() + t.equal(reputationServer.server.killed, true, 'test reputation server killed') + t.end() + } +) + +test( + 'teardown test server', + function (t) { + testServer.stop() + t.equal(testServer.server.killed, true, 'test server killed') + t.end() + } +) diff --git a/test/remote/too_many_authenticated_checks.js b/test/remote/too_many_authenticated_checks.js index de980c6..437efd1 100644 --- a/test/remote/too_many_authenticated_checks.js +++ b/test/remote/too_many_authenticated_checks.js @@ -55,6 +55,54 @@ test( } ) +test( + '/checkAuthenticated requires action', + function (t) { + return client.postAsync('/checkAuthenticated', { ip: TEST_IP, uid: TEST_UID}) + .spread(function(req, res, obj){ + t.fail('Success response from request missing param.') + t.end() + }) + .catch(function(err){ + t.equal(err.statusCode, 400, 'returns a 400') + t.equal(err.body.code, 'MissingParameters') + t.end() + }) + } +) + +test( + '/checkAuthenticated requires ip', + function (t) { + return client.postAsync('/checkAuthenticated', { action: ACTION_ONE, uid: TEST_UID}) + .spread(function(req, res, obj){ + t.fail('Success response from request missing param.') + t.end() + }) + .catch(function(err){ + t.equal(err.statusCode, 400, 'returns a 400') + t.equal(err.body.code, 'MissingParameters') + t.end() + }) + } +) + +test( + '/checkAuthenticated requires uid', + function (t) { + return client.postAsync('/checkAuthenticated', { action: ACTION_ONE, ip: TEST_IP}) + .spread(function(req, res, obj){ + t.fail('Success response from request missing param.') + t.end() + }) + .catch(function(err){ + t.equal(err.statusCode, 400, 'returns a 400') + t.equal(err.body.code, 'MissingParameters') + t.end() + }) + } +) + test( '/checkAuthenticated with same action', function (t) { diff --git a/test/test_reputation_server.js b/test/test_reputation_server.js new file mode 100644 index 0000000..2c70202 --- /dev/null +++ b/test/test_reputation_server.js @@ -0,0 +1,69 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-disable no-console */ + +var cp = require('child_process') +var request = require('request') + +function ReputationServerStub(config) { + this.url = 'http://' + config.reputationService.host + ':' + config.reputationService.port + this.server = null +} + + +function waitLoop(testServer, url, cb) { + request( + url + '/', + function (err, res/*, body*/) { + if (err) { + if (err.errno !== 'ECONNREFUSED') { + console.log('ERROR: unexpected result from ' + url) + console.log(err) + return cb(err) + } + return setTimeout(waitLoop.bind(null, testServer, url, cb), 100) + } + if (res.statusCode !== 200) { + console.log('ERROR: bad status code: ' + res.statusCode) + return cb(res.statusCode) + } + return cb() + } + ) +} + +ReputationServerStub.prototype.start = function (cb) { + if (!this.server) { + this.server = cp.spawn( + 'node', + ['./test_reputation_server_stub.js'], + { + cwd: __dirname, + stdio: 'ignore' + } + ) + } + + this.server.on('error', function (err) { + console.log('Failed to start child process.') + cb(err) + }) + + waitLoop(this, this.url, function (err) { + if (err) { + cb(err) + } else { + cb(null) + } + }) +} + +ReputationServerStub.prototype.stop = function () { + if (this.server) { + this.server.kill('SIGINT') + } +} + +module.exports = ReputationServerStub diff --git a/test/test_reputation_server_stub.js b/test/test_reputation_server_stub.js new file mode 100644 index 0000000..352efe2 --- /dev/null +++ b/test/test_reputation_server_stub.js @@ -0,0 +1,46 @@ +/* This Source Code Form is subject to the terms of the Mozilla Public + * License, v. 2.0. If a copy of the MPL was not distributed with this + * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ + +/* eslint-disable no-console */ + +var restify = require('restify') + +// a dummy reputation server to receive violations +var server = restify.createServer() +server.timeout = process.env.REPUTATION_SERVICE_TIMEOUT +server.use(restify.bodyParser()) + +// hashmap of ip -> list of violations +var mostRecentViolationByIp = {} + +server.put('/violations/:ip', function (req, res, next) { + var ip = req.params.ip + mostRecentViolationByIp[ip] = req.body.Violation + console.log('put req', req.url) + res.send(200) + next() +}) + +// not real tigerblood endpoints +server.get('/mostRecentViolation/:ip', function (req, res, next) { + var ip = req.params.ip + console.log('get req', req.url) + res.send(200, mostRecentViolationByIp[ip] || null) + + next() +}) +server.del('/mostRecentViolation/:ip', function (req, res, next) { + var ip = req.params.ip + console.log('delete req', req.url) + delete mostRecentViolationByIp[ip] + res.send(200) + + next() +}) +server.get('/', function (req, res, next) { + res.send(200) + next() +}) + +server.listen(process.env.REPUTATION_SERVICE_PORT)