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:
Francois Marier 2014-06-03 22:50:51 +12:00
Родитель 6f7e1ed55d
Коммит f8409fb6c6
6 изменённых файлов: 138 добавлений и 15 удалений

Просмотреть файл

@ -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(