506 строки
18 KiB
JavaScript
506 строки
18 KiB
JavaScript
/* 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/. */
|
|
|
|
'use strict'
|
|
|
|
const ROOT_DIR = '../..'
|
|
|
|
const assert = require('insist')
|
|
const log = {
|
|
trace: () => {},
|
|
activityEvent: () => {},
|
|
flowEvent: () => {},
|
|
error() {}
|
|
}
|
|
const mocks = require('../mocks')
|
|
const error = require(`${ROOT_DIR}/lib/error.js`)
|
|
const P = require(`${ROOT_DIR}/lib/promise.js`)
|
|
var nock = require('nock')
|
|
|
|
const Customs = require(`${ROOT_DIR}/lib/customs.js`)(log, error)
|
|
|
|
var CUSTOMS_URL_REAL = 'http://localhost:7000'
|
|
var CUSTOMS_URL_MISSING = 'http://localhost:7001'
|
|
|
|
var customsNoUrl
|
|
var customsWithUrl
|
|
var customsInvalidUrl
|
|
|
|
var customsServer = nock(CUSTOMS_URL_REAL)
|
|
.defaultReplyHeaders({
|
|
'Content-Type': 'application/json'
|
|
})
|
|
|
|
describe('Customs', () => {
|
|
it(
|
|
"can create a customs object with url as 'none'",
|
|
() => {
|
|
customsNoUrl = new Customs('none')
|
|
|
|
assert.ok(customsNoUrl, 'got a customs object with a none url')
|
|
|
|
var request = newRequest()
|
|
var ip = request.app.clientAddress
|
|
var email = newEmail()
|
|
var action = newAction()
|
|
|
|
return customsNoUrl.check(request, email, action)
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /check succeeds')
|
|
})
|
|
.then(function() {
|
|
return customsNoUrl.flag(ip, { email: email, uid: '12345' })
|
|
})
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /failedLoginAttempt succeeds')
|
|
})
|
|
.then(function() {
|
|
return customsNoUrl.reset(email)
|
|
})
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /passwordReset succeeds')
|
|
})
|
|
.then(() => {
|
|
return customsNoUrl.checkIpOnly(request, action)
|
|
})
|
|
.then(result => {
|
|
assert.equal(result, undefined, 'Nothing is returned when /checkIpOnly succeeds')
|
|
})
|
|
}
|
|
)
|
|
|
|
it(
|
|
'can create a customs object with a url',
|
|
() => {
|
|
customsWithUrl = new Customs(CUSTOMS_URL_REAL)
|
|
|
|
assert.ok(customsWithUrl, 'got a customs object with a valid url')
|
|
|
|
var request = newRequest()
|
|
var ip = request.app.clientAddress
|
|
var email = newEmail()
|
|
var action = newAction()
|
|
|
|
// Mock a check that does not get blocked.
|
|
customsServer.post('/check', function (body) {
|
|
assert.deepEqual(body, {
|
|
ip: ip,
|
|
email: email,
|
|
action: action,
|
|
headers: request.headers,
|
|
query: request.query,
|
|
payload: request.payload,
|
|
}, 'first call to /check had expected request params')
|
|
return true
|
|
}).reply(200, {
|
|
block: false,
|
|
retryAfter: 0
|
|
})
|
|
return customsWithUrl.check(request, email, action)
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /check succeeds')
|
|
})
|
|
.then(function() {
|
|
// Mock a report of a failed login attempt
|
|
customsServer.post('/failedLoginAttempt', function (body) {
|
|
assert.deepEqual(body, {
|
|
ip: ip,
|
|
email: email,
|
|
errno: error.ERRNO.UNEXPECTED_ERROR
|
|
}, 'first call to /failedLoginAttempt had expected request params')
|
|
return true
|
|
}).reply(200, {})
|
|
return customsWithUrl.flag(ip, { email: email, uid: '12345' })
|
|
})
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /failedLoginAttempt succeeds')
|
|
})
|
|
.then(function() {
|
|
// Mock a report of a password reset.
|
|
customsServer.post('/passwordReset', function (body) {
|
|
assert.deepEqual(body, {
|
|
email: email,
|
|
}, 'first call to /passwordReset had expected request params')
|
|
return true
|
|
}).reply(200, {})
|
|
return customsWithUrl.reset(email)
|
|
})
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /passwordReset succeeds')
|
|
})
|
|
.then(function() {
|
|
// Mock a check that does get blocked, with a retryAfter.
|
|
customsServer.post('/check', function (body) {
|
|
assert.deepEqual(body, {
|
|
ip: ip,
|
|
email: email,
|
|
action: action,
|
|
headers: request.headers,
|
|
query: request.query,
|
|
payload: request.payload,
|
|
}, 'second call to /check had expected request params')
|
|
return true
|
|
}).reply(200, {
|
|
block: true,
|
|
retryAfter: 10001
|
|
})
|
|
return customsWithUrl.check(request, email, action)
|
|
})
|
|
.then(function(result) {
|
|
assert(false, 'This should have failed the check since it should be blocked')
|
|
}, function(err) {
|
|
assert.equal(err.errno, error.ERRNO.THROTTLED, 'Error number is correct')
|
|
assert.equal(err.message, 'Client has sent too many requests', 'Error message is correct')
|
|
assert.ok(err.isBoom, 'The error causes a boom')
|
|
assert.equal(err.output.statusCode, 429, 'Status Code is correct')
|
|
assert.equal(err.output.payload.retryAfter, 10001, 'retryAfter is correct')
|
|
assert.equal(err.output.headers['retry-after'], 10001, 'retryAfter header is correct')
|
|
})
|
|
.then(function() {
|
|
// Mock a report of a failed login attempt that does trigger lockout.
|
|
customsServer.post('/failedLoginAttempt', function (body) {
|
|
assert.deepEqual(body, {
|
|
ip: ip,
|
|
email: email,
|
|
errno: error.ERRNO.INCORRECT_PASSWORD
|
|
}, 'second call to /failedLoginAttempt had expected request params')
|
|
return true
|
|
}).reply(200, { })
|
|
return customsWithUrl.flag(ip, {
|
|
email: email,
|
|
errno: error.ERRNO.INCORRECT_PASSWORD
|
|
})
|
|
})
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /failedLoginAttempt succeeds')
|
|
})
|
|
.then(function() {
|
|
// Mock a check that does get blocked, with no retryAfter.
|
|
request.headers['user-agent'] = 'test passing through headers'
|
|
request.payload['foo'] = 'bar'
|
|
customsServer.post('/check', function (body) {
|
|
assert.deepEqual(body, {
|
|
ip: ip,
|
|
email: email,
|
|
action: action,
|
|
headers: request.headers,
|
|
query: request.query,
|
|
payload: request.payload,
|
|
}, 'third call to /check had expected request params')
|
|
return true
|
|
}).reply(200, {
|
|
block: true
|
|
})
|
|
return customsWithUrl.check(request, email, action)
|
|
})
|
|
.then(function(result) {
|
|
assert(false, 'This should have failed the check since it should be blocked')
|
|
}, function(err) {
|
|
assert.equal(err.errno, error.ERRNO.REQUEST_BLOCKED, 'Error number is correct')
|
|
assert.equal(err.message, 'The request was blocked for security reasons', 'Error message is correct')
|
|
assert.ok(err.isBoom, 'The error causes a boom')
|
|
assert.equal(err.output.statusCode, 400, 'Status Code is correct')
|
|
assert(! err.output.payload.retryAfter, 'retryAfter field is not present')
|
|
assert(! err.output.headers['retry-after'], 'retryAfter header is not present')
|
|
})
|
|
.then(() => {
|
|
customsServer.post('/checkIpOnly', function (body) {
|
|
assert.deepEqual(body, {
|
|
ip: ip,
|
|
action: action
|
|
}, 'first call to /check had expected request params')
|
|
return true
|
|
}).reply(200, {
|
|
block: false,
|
|
retryAfter: 0
|
|
})
|
|
return customsWithUrl.checkIpOnly(request, action)
|
|
})
|
|
.then(result => {
|
|
assert.equal(result, undefined, 'Nothing is returned when /check succeeds')
|
|
})
|
|
}
|
|
)
|
|
|
|
it(
|
|
'failed closed when creating a customs object with non-existant customs service',
|
|
() => {
|
|
customsInvalidUrl = new Customs(CUSTOMS_URL_MISSING)
|
|
|
|
assert.ok(customsInvalidUrl, 'got a customs object with a non-existant service url')
|
|
|
|
var request = newRequest()
|
|
var ip = request.app.clientAddress
|
|
var email = newEmail()
|
|
var action = newAction()
|
|
|
|
return P.all([
|
|
customsInvalidUrl.check(request, email, action)
|
|
.then(assert.fail, err => {
|
|
assert.equal(err.errno, error.ERRNO.BACKEND_SERVICE_FAILURE, 'an error is returned from /check')
|
|
}),
|
|
|
|
customsInvalidUrl.flag(ip, { email: email, uid: '12345' })
|
|
.then(assert.fail, err => {
|
|
assert.equal(err.errno, error.ERRNO.BACKEND_SERVICE_FAILURE, 'an error is returned from /flag')
|
|
}),
|
|
|
|
customsInvalidUrl.reset(email)
|
|
.then(assert.fail, err => {
|
|
assert.equal(err.errno, error.ERRNO.BACKEND_SERVICE_FAILURE, 'an error is returned from /passwordReset')
|
|
})
|
|
])
|
|
}
|
|
)
|
|
|
|
it(
|
|
'can rate limit checkAccountStatus /check',
|
|
() => {
|
|
customsWithUrl = new Customs(CUSTOMS_URL_REAL)
|
|
|
|
assert.ok(customsWithUrl, 'can rate limit checkAccountStatus /check')
|
|
|
|
var request = newRequest()
|
|
var ip = request.app.clientAddress
|
|
var email = newEmail()
|
|
var action = 'accountStatusCheck'
|
|
|
|
function checkRequestBody (body) {
|
|
assert.deepEqual(body, {
|
|
ip: ip,
|
|
email: email,
|
|
action: action,
|
|
headers: request.headers,
|
|
query: request.query,
|
|
payload: request.payload,
|
|
}, 'call to /check had expected request params')
|
|
return true
|
|
}
|
|
|
|
customsServer
|
|
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
|
|
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
|
|
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
|
|
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
|
|
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
|
|
.post('/check', checkRequestBody).reply(200, '{"block":true,"retryAfter":10001}')
|
|
|
|
return customsWithUrl.check(request, email, action)
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /check succeeds - 1')
|
|
return customsWithUrl.check(request, email, action)
|
|
})
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /check succeeds - 2')
|
|
return customsWithUrl.check(request, email, action)
|
|
})
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /check succeeds - 3')
|
|
return customsWithUrl.check(request, email, action)
|
|
})
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /check succeeds - 4')
|
|
return customsWithUrl.check(request, email, action)
|
|
})
|
|
.then(function() {
|
|
// request is blocked
|
|
return customsWithUrl.check(request, email, action)
|
|
})
|
|
.then(function() {
|
|
assert(false, 'This should have failed the check since it should be blocked')
|
|
}, function(error) {
|
|
assert.equal(error.errno, 114, 'Error number is correct')
|
|
assert.equal(error.message, 'Client has sent too many requests', 'Error message is correct')
|
|
assert.ok(error.isBoom, 'The error causes a boom')
|
|
assert.equal(error.output.statusCode, 429, 'Status Code is correct')
|
|
assert.equal(error.output.payload.retryAfter, 10001, 'retryAfter is correct')
|
|
assert.equal(error.output.payload.retryAfterLocalized, 'in 3 hours', 'retryAfterLocalized is correct')
|
|
assert.equal(error.output.headers['retry-after'], 10001, 'retryAfter header is correct')
|
|
})
|
|
}
|
|
)
|
|
|
|
it(
|
|
'can rate limit devicesNotify /checkAuthenticated',
|
|
() => {
|
|
customsWithUrl = new Customs(CUSTOMS_URL_REAL)
|
|
|
|
assert.ok(customsWithUrl, 'can rate limit /checkAuthenticated')
|
|
|
|
var request = newRequest()
|
|
var action = 'devicesNotify'
|
|
var ip = request.app.clientAddress
|
|
var uid = 'foo'
|
|
|
|
function checkRequestBody (body) {
|
|
assert.deepEqual(body, {
|
|
action: action,
|
|
ip: ip,
|
|
uid: uid,
|
|
}, 'call to /checkAuthenticated had expected request params')
|
|
return true
|
|
}
|
|
|
|
customsServer
|
|
.post('/checkAuthenticated', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
|
|
.post('/checkAuthenticated', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
|
|
.post('/checkAuthenticated', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
|
|
.post('/checkAuthenticated', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
|
|
.post('/checkAuthenticated', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
|
|
.post('/checkAuthenticated', checkRequestBody).reply(200, '{"block":true,"retryAfter":10001}')
|
|
|
|
return customsWithUrl.checkAuthenticated(action, ip, uid)
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /checkAuthenticated succeeds - 1')
|
|
return customsWithUrl.checkAuthenticated(action, ip, uid)
|
|
})
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /checkAuthenticated succeeds - 2')
|
|
return customsWithUrl.checkAuthenticated(action, ip, uid)
|
|
})
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /checkAuthenticated succeeds - 3')
|
|
return customsWithUrl.checkAuthenticated(action, ip, uid)
|
|
})
|
|
.then(function(result) {
|
|
assert.equal(result, undefined, 'Nothing is returned when /checkAuthenticated succeeds - 4')
|
|
return customsWithUrl.checkAuthenticated(action, ip, uid)
|
|
})
|
|
.then(function() {
|
|
// request is blocked
|
|
return customsWithUrl.checkAuthenticated(action, ip, uid)
|
|
})
|
|
.then(function() {
|
|
assert(false, 'This should have failed the check since it should be blocked')
|
|
}, function(error) {
|
|
assert.equal(error.errno, 114, 'Error number is correct')
|
|
assert.equal(error.message, 'Client has sent too many requests', 'Error message is correct')
|
|
assert.ok(error.isBoom, 'The error causes a boom')
|
|
assert.equal(error.output.statusCode, 429, 'Status Code is correct')
|
|
assert.equal(error.output.payload.retryAfter, 10001, 'retryAfter is correct')
|
|
assert.equal(error.output.headers['retry-after'], 10001, 'retryAfter header is correct')
|
|
})
|
|
}
|
|
)
|
|
|
|
it('can rate limit verifyTotpCode /check', () => {
|
|
const request = newRequest()
|
|
const action = 'verifyTotpCode'
|
|
const email = 'test@email.com'
|
|
const ip = request.app.clientAddress
|
|
|
|
customsWithUrl = new Customs(CUSTOMS_URL_REAL)
|
|
assert.ok(customsWithUrl, 'can rate limit ')
|
|
|
|
function checkRequestBody(body) {
|
|
assert.deepEqual(body, {
|
|
ip: ip,
|
|
email: email,
|
|
action: action,
|
|
headers: request.headers,
|
|
query: request.query,
|
|
payload: request.payload,
|
|
}, 'call to /check had expected request params')
|
|
return true
|
|
}
|
|
|
|
customsServer
|
|
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
|
|
.post('/check', checkRequestBody).reply(200, '{"block":false,"retryAfter":0}')
|
|
.post('/check', checkRequestBody).reply(200, '{"block":true,"retryAfter":30}')
|
|
|
|
return customsWithUrl.check(request, email, action)
|
|
.then((result) => {
|
|
assert.equal(result, undefined, 'Nothing is returned when /check succeeds - 1')
|
|
return customsWithUrl.check(request, email, action)
|
|
})
|
|
.then((result) => {
|
|
assert.equal(result, undefined, 'Nothing is returned when /check succeeds - 2')
|
|
return customsWithUrl.check(request, email, action)
|
|
})
|
|
.then(assert.fail, (error) => {
|
|
assert.equal(error.errno, 114, 'Error number is correct')
|
|
assert.equal(error.message, 'Client has sent too many requests', 'Error message is correct')
|
|
assert.ok(error.isBoom, 'The error causes a boom')
|
|
assert.equal(error.output.statusCode, 429, 'Status Code is correct')
|
|
assert.equal(error.output.payload.retryAfter, 30, 'retryAfter is correct')
|
|
assert.equal(error.output.headers['retry-after'], 30, 'retryAfter header is correct')
|
|
})
|
|
})
|
|
|
|
it(
|
|
'can scrub customs request object',
|
|
() => {
|
|
customsWithUrl = new Customs(CUSTOMS_URL_REAL)
|
|
|
|
assert.ok(customsWithUrl, 'got a customs object with a valid url')
|
|
|
|
var request = newRequest()
|
|
request.payload.authPW = 'asdfasdfadsf'
|
|
request.payload.oldAuthPW = '012301230123'
|
|
request.payload.notThePW = 'plaintext'
|
|
var ip = request.app.clientAddress
|
|
var email = newEmail()
|
|
var action = newAction()
|
|
|
|
customsServer.post('/check', function (body) {
|
|
assert.deepEqual(body, {
|
|
ip: ip,
|
|
email: email,
|
|
action: action,
|
|
headers: request.headers,
|
|
query: request.query,
|
|
payload: {
|
|
notThePW: 'plaintext'
|
|
}
|
|
}, 'should not have password fields in payload')
|
|
return true
|
|
}).reply(200, {
|
|
block: false,
|
|
retryAfter: 0
|
|
})
|
|
|
|
return customsWithUrl.check(request, email, action)
|
|
.then(function (result) {
|
|
assert.equal(result, undefined, 'nothing is returned when /check succeeds - 1')
|
|
})
|
|
}
|
|
)
|
|
|
|
})
|
|
|
|
function newEmail() {
|
|
return Math.random().toString().substr(2) + '@example.com'
|
|
}
|
|
|
|
function newIp() {
|
|
return [
|
|
'' + Math.floor(Math.random() * 256),
|
|
'' + Math.floor(Math.random() * 256),
|
|
'' + Math.floor(Math.random() * 256),
|
|
'' + Math.floor(Math.random() * 256),
|
|
].join('.')
|
|
}
|
|
|
|
function newRequest() {
|
|
return mocks.mockRequest({
|
|
clientAddress: newIp(),
|
|
headers: {},
|
|
query: {},
|
|
payload: {}
|
|
})
|
|
}
|
|
|
|
|
|
function newAction() {
|
|
var EMAIL_ACTIONS = [
|
|
'accountCreate',
|
|
'recoveryEmailResendCode',
|
|
'passwordForgotSendCode',
|
|
'passwordForgotResendCode'
|
|
]
|
|
|
|
return EMAIL_ACTIONS[Math.floor(Math.random() * EMAIL_ACTIONS.length)]
|
|
}
|