Add a new blockEmail API endpoint
This allows a third-party to ban a given email address for a given period of time.
This commit is contained in:
Родитель
6f7e1ed55d
Коммит
f8409fb6c6
|
@ -13,7 +13,7 @@ var BLOCK_INTERVAL_MS = config.blockIntervalSeconds * 1000
|
|||
var RATE_LIMIT_INTERVAL_MS = config.rateLimitIntervalSeconds * 1000
|
||||
|
||||
var IpEmailRecord = require('../ip_email_record')(RATE_LIMIT_INTERVAL_MS, config.maxBadLogins)
|
||||
var EmailRecord = require('../email_record')(RATE_LIMIT_INTERVAL_MS, config.maxEmails)
|
||||
var EmailRecord = require('../email_record')(RATE_LIMIT_INTERVAL_MS, BLOCK_INTERVAL_MS, config.maxEmails)
|
||||
var IpRecord = require('../ip_record')(BLOCK_INTERVAL_MS)
|
||||
|
||||
var P = require('bluebird')
|
||||
|
@ -190,6 +190,39 @@ api.post(
|
|||
}
|
||||
)
|
||||
|
||||
api.post(
|
||||
'/blockEmail',
|
||||
function (req, res, next) {
|
||||
var email = req.body.email
|
||||
if (!email) {
|
||||
var err = {code: 'MissingParameters', message: 'email is required'}
|
||||
log.error({ op: 'request.blockEmail', email: email, err: err })
|
||||
res.send(500, err)
|
||||
next()
|
||||
}
|
||||
|
||||
mc.getAsync(email)
|
||||
.then(EmailRecord.parse, EmailRecord.parse)
|
||||
.then(
|
||||
function (emailRecord) {
|
||||
emailRecord.block()
|
||||
return mc.setAsync(email, emailRecord, LIFETIME).caught(ignore)
|
||||
}
|
||||
)
|
||||
.then(
|
||||
function () {
|
||||
log.info({ op: 'request.blockEmail', email: email })
|
||||
res.send({})
|
||||
},
|
||||
function (err) {
|
||||
log.error({ op: 'request.blockEmail', email: email, err: err })
|
||||
res.send(500, err)
|
||||
}
|
||||
)
|
||||
.done(next, next)
|
||||
}
|
||||
)
|
||||
|
||||
api.get(
|
||||
'/',
|
||||
function (req, res, next) {
|
||||
|
|
|
@ -3,7 +3,7 @@
|
|||
* file, You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
// Keep track of events tied to just email addresses
|
||||
module.exports = function (RATE_LIMIT_INTERVAL_MS, MAX_EMAILS, now) {
|
||||
module.exports = function (RATE_LIMIT_INTERVAL_MS, BLOCK_INTERVAL_MS, MAX_EMAILS, now) {
|
||||
|
||||
now = now || Date.now
|
||||
|
||||
|
@ -21,6 +21,7 @@ module.exports = function (RATE_LIMIT_INTERVAL_MS, MAX_EMAILS, now) {
|
|||
EmailRecord.parse = function (object) {
|
||||
var rec = new EmailRecord()
|
||||
object = object || {}
|
||||
rec.bk = object.bk // timestamp when the account was banned
|
||||
rec.rl = object.rl // timestamp when the account was rate-limited
|
||||
rec.xs = object.xs || [] // timestamps when emails were sent
|
||||
rec.pr = object.pr // timestamp of the last password reset
|
||||
|
@ -52,7 +53,13 @@ module.exports = function (RATE_LIMIT_INTERVAL_MS, MAX_EMAILS, now) {
|
|||
}
|
||||
|
||||
EmailRecord.prototype.isBlocked = function () {
|
||||
return !!(this.rl && (now() - this.rl < RATE_LIMIT_INTERVAL_MS))
|
||||
var rateLimited = !!(this.rl && (now() - this.rl < RATE_LIMIT_INTERVAL_MS))
|
||||
var banned = !!(this.bk && (now() - this.bk < BLOCK_INTERVAL_MS))
|
||||
return rateLimited || banned
|
||||
}
|
||||
|
||||
EmailRecord.prototype.block = function () {
|
||||
this.bk = now()
|
||||
}
|
||||
|
||||
EmailRecord.prototype.rateLimit = function () {
|
||||
|
@ -65,7 +72,9 @@ module.exports = function (RATE_LIMIT_INTERVAL_MS, MAX_EMAILS, now) {
|
|||
}
|
||||
|
||||
EmailRecord.prototype.retryAfter = function () {
|
||||
return Math.max(0, Math.floor(((this.rl || 0) + RATE_LIMIT_INTERVAL_MS - now()) / 1000))
|
||||
var rateLimitAfter = Math.floor(((this.rl || 0) + RATE_LIMIT_INTERVAL_MS - now()) / 1000)
|
||||
var banAfter = Math.floor(((this.bk || 0) + BLOCK_INTERVAL_MS - now()) / 1000)
|
||||
return Math.max(0, rateLimitAfter, banAfter)
|
||||
}
|
||||
|
||||
EmailRecord.prototype.update = function (action) {
|
||||
|
|
|
@ -10,7 +10,7 @@ function now() {
|
|||
}
|
||||
|
||||
function simpleEmailRecord() {
|
||||
return new (emailRecord(500, 2, now))()
|
||||
return new (emailRecord(500, 800, 2, now))()
|
||||
}
|
||||
|
||||
test(
|
||||
|
@ -23,6 +23,13 @@ test(
|
|||
t.equal(er.isBlocked(), false, 'blockedAt is older than rate-limit interval')
|
||||
er.rl = 501
|
||||
t.equal(er.isBlocked(), true, 'blockedAt is within the rate-limit interval')
|
||||
delete er.rl
|
||||
t.equal(er.isBlocked(), false, 'record is no longer blocked')
|
||||
|
||||
er.bk = 199
|
||||
t.equal(er.isBlocked(), false, 'blockedAt is older than block interval')
|
||||
er.bk = 201
|
||||
t.equal(er.isBlocked(), true, 'blockedAt is within the block interval')
|
||||
t.end()
|
||||
}
|
||||
)
|
||||
|
@ -110,18 +117,26 @@ test(
|
|||
test(
|
||||
'retryAfter works',
|
||||
function (t) {
|
||||
var er = simpleEmailRecord()
|
||||
er.now = function () {
|
||||
var er = new (emailRecord(5000, 8000, 2, function () {
|
||||
return 10000
|
||||
}
|
||||
}))()
|
||||
|
||||
t.equal(er.retryAfter(), 0, 'unblocked records can be retried now')
|
||||
er.rl = 100
|
||||
er.rl = 1000
|
||||
t.equal(er.retryAfter(), 0, 'long expired blocks can be retried immediately')
|
||||
er.rl = 500
|
||||
er.rl = 5000
|
||||
t.equal(er.retryAfter(), 0, 'just expired blocks can be retried immediately')
|
||||
er.rl = 6000
|
||||
t.equal(er.retryAfter(), 5, 'unexpired blocks can be retried in a bit')
|
||||
t.equal(er.retryAfter(), 1, 'unexpired blocks can be retried in a bit')
|
||||
|
||||
delete er.rl
|
||||
t.equal(er.retryAfter(), 0, 'unblocked records can be retried now')
|
||||
er.bk = 1000
|
||||
t.equal(er.retryAfter(), 0, 'long expired blocks can be retried immediately')
|
||||
er.bk = 2000
|
||||
t.equal(er.retryAfter(), 0, 'just expired blocks can be retried immediately')
|
||||
er.bk = 6000
|
||||
t.equal(er.retryAfter(), 4, 'unexpired blocks can be retried in a bit') // TODO?
|
||||
t.end()
|
||||
}
|
||||
)
|
||||
|
@ -145,7 +160,7 @@ test(
|
|||
t.equal(er.isBlocked(), false, 'original object is not blocked')
|
||||
t.equal(er.xs.length, 0, 'original object has no hits')
|
||||
|
||||
var erCopy1 = (emailRecord(50, 2, now)).parse(er)
|
||||
var erCopy1 = (emailRecord(50, 50, 2, now)).parse(er)
|
||||
t.equal(erCopy1.isBlocked(), false, 'copied object is not blocked')
|
||||
t.equal(erCopy1.xs.length, 0, 'copied object has no hits')
|
||||
|
||||
|
@ -154,7 +169,7 @@ test(
|
|||
t.equal(er.isBlocked(), true, 'original object is now blocked')
|
||||
t.equal(er.xs.length, 1, 'original object now has one hit')
|
||||
|
||||
var erCopy2 = (emailRecord(50, 2, now)).parse(er)
|
||||
var erCopy2 = (emailRecord(50, 50, 2, now)).parse(er)
|
||||
t.equal(erCopy2.isBlocked(), true, 'copied object is blocked')
|
||||
t.equal(erCopy2.xs.length, 1, 'copied object has one hit')
|
||||
t.end()
|
||||
|
|
|
@ -0,0 +1,64 @@
|
|||
/* Any copyright is dedicated to the Public Domain.
|
||||
* http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
var test = require('tap').test
|
||||
var restify = require('restify')
|
||||
var TestServer = require('../test_server')
|
||||
|
||||
var TEST_EMAIL = 'test@example.com'
|
||||
|
||||
var config = {
|
||||
port: 7000
|
||||
}
|
||||
var testServer = new TestServer(config)
|
||||
|
||||
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()
|
||||
})
|
||||
}
|
||||
)
|
||||
|
||||
var client = restify.createJsonClient({
|
||||
url: 'http://127.0.0.1:' + config.port
|
||||
});
|
||||
|
||||
test(
|
||||
'well-formed request',
|
||||
function (t) {
|
||||
client.post('/blockEmail', { email: TEST_EMAIL },
|
||||
function (err, req, res, obj) {
|
||||
t.notOk(err, 'good request is successful')
|
||||
t.equal(res.statusCode, 200, 'good request returns a 200')
|
||||
t.end()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'missing email',
|
||||
function (t) {
|
||||
client.post('/blockEmail', {},
|
||||
function (err, req, res, obj) {
|
||||
t.equal(res.statusCode, 500, 'bad request returns a 500')
|
||||
t.type(obj.code, 'string', 'bad request returns an error code')
|
||||
t.type(obj.message, 'string', 'bad request returns an error message')
|
||||
t.end()
|
||||
}
|
||||
)
|
||||
}
|
||||
)
|
||||
|
||||
test(
|
||||
'teardown',
|
||||
function (t) {
|
||||
testServer.stop()
|
||||
t.equal(testServer.server.killed, true, 'test server has been killed')
|
||||
t.end()
|
||||
}
|
||||
)
|
|
@ -12,12 +12,13 @@ var TEST_IP = '192.0.2.1'
|
|||
var config = {
|
||||
port: 7000,
|
||||
memcached: '127.0.0.1:11211',
|
||||
blockIntervalSeconds: 1,
|
||||
rateLimitIntervalSeconds: 1,
|
||||
maxEmails: 3,
|
||||
maxBadLogins: 2
|
||||
}
|
||||
|
||||
var EmailRecord = require('../../email_record')(config.rateLimitIntervalSeconds * 1000, config.maxEmails)
|
||||
var EmailRecord = require('../../email_record')(config.rateLimitIntervalSeconds * 1000, config.blockIntervalSeconds * 1000, config.maxEmails)
|
||||
var IpEmailRecord = require('../../ip_email_record')(config.rateLimitIntervalSeconds * 1000, config.maxBadLogins)
|
||||
var testServer = new TestServer(config)
|
||||
|
||||
|
|
|
@ -12,11 +12,12 @@ var TEST_IP = '192.0.2.1'
|
|||
var config = {
|
||||
port: 7000,
|
||||
memcached: '127.0.0.1:11211',
|
||||
blockIntervalSeconds: 1,
|
||||
rateLimitIntervalSeconds: 1,
|
||||
maxEmails: 3
|
||||
}
|
||||
|
||||
var EmailRecord = require('../../email_record')(config.rateLimitIntervalSeconds * 1000, config.maxEmails)
|
||||
var EmailRecord = require('../../email_record')(config.rateLimitIntervalSeconds * 1000, config.blockIntervalSeconds * 1000, config.maxEmails)
|
||||
var testServer = new TestServer(config)
|
||||
|
||||
var mc = new Memcached(
|
||||
|
|
Загрузка…
Ссылка в новой задаче