Skip to content
Snippets Groups Projects
Commit c5684a06 authored by Lloyd Hilaiel's avatar Lloyd Hilaiel
Browse files

implement throttling on outbound emails: don't send emails to the same address...

implement throttling on outbound emails: don't send emails to the same address more than once per minute - issue #430
parent c21c34ad
No related branches found
No related tags found
No related merge requests found
......@@ -47,7 +47,7 @@ bcrypt = require('bcrypt'),
crypto = require('crypto'),
logger = require('logging.js').logger,
ca = require('./ca.js'),
configuration = require('configuration.js');
config = require('configuration.js');
function checkParams(params) {
return function(req, resp, next) {
......@@ -79,7 +79,6 @@ function clearAuthenticatedUser(session) {
});
}
function setAuthenticatedUser(session, email) {
session.authenticatedUser = email;
session.authenticatedAt = new Date();
......@@ -91,7 +90,7 @@ function isAuthed(req) {
if (req.session.authenticatedUser) {
if (!Date.parse(req.session.authenticatedAt) > 0) throw "bad timestamp";
if (new Date() - new Date(req.session.authenticatedAt) >
configuration.get('authentication_duration_ms'))
config.get('authentication_duration_ms'))
{
throw "expired";
}
......@@ -177,27 +176,35 @@ function setup(app) {
// staging a user logs you out.
clearAuthenticatedUser(req.session);
try {
// upon success, stage_user returns a secret (that'll get baked into a url
// and given to the user), on failure it throws
db.stageUser(req.body.email, function(secret) {
// store the email being registered in the session data
if (!req.session) req.session = {};
db.lastStaged(req.body.email, function (last) {
if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) {
logger.warn('throttling request to stage email address ' + req.body.email + ', only ' +
((new Date() - last) / 1000.0) + "s elapsed");
return httputils.forbidden(resp, "throttling. try again later.");
}
// store the secret we're sending via email in the users session, as checking
// that it still exists in the database is the surest way to determine the
// status of the email verification.
req.session.pendingCreation = secret;
try {
// upon success, stage_user returns a secret (that'll get baked into a url
// and given to the user), on failure it throws
db.stageUser(req.body.email, function(secret) {
// store the email being registered in the session data
if (!req.session) req.session = {};
resp.json({ success: true });
// store the secret we're sending via email in the users session, as checking
// that it still exists in the database is the surest way to determine the
// status of the email verification.
req.session.pendingCreation = secret;
// let's now kick out a verification email!
email.sendNewUserEmail(req.body.email, req.body.site, secret);
});
} catch(e) {
// we should differentiate tween' 400 and 500 here.
httputils.badRequest(resp, e.toString());
}
resp.json({ success: true });
// let's now kick out a verification email!
email.sendNewUserEmail(req.body.email, req.body.site, secret);
});
} catch(e) {
// we should differentiate tween' 400 and 500 here.
httputils.badRequest(resp, e.toString());
}
});
});
app.get('/wsapi/user_creation_status', function(req, resp) {
......@@ -233,7 +240,7 @@ function setup(app) {
});
function bcrypt_password(password, cb) {
var bcryptWorkFactor = configuration.get('bcrypt_work_factor');
var bcryptWorkFactor = config.get('bcrypt_work_factor');
bcrypt.gen_salt(bcryptWorkFactor, function (err, salt) {
if (err) {
......@@ -293,22 +300,30 @@ function setup(app) {
});
app.post('/wsapi/stage_email', checkAuthed, checkParams(["email", "site"]), function (req, resp) {
try {
// on failure stageEmail may throw
db.stageEmail(req.session.authenticatedUser, req.body.email, function(secret) {
db.lastStaged(req.body.email, function (last) {
if (last && (new Date() - last) < config.get('min_time_between_emails_ms')) {
logger.warn('throttling request to stage email address ' + req.body.email + ', only ' +
((new Date() - last) / 1000.0) + "s elapsed");
return httputils.forbidden(resp, "throttling. try again later.");
}
// store the email being added in session data
req.session.pendingAddition = secret;
try {
// on failure stageEmail may throw
db.stageEmail(req.session.authenticatedUser, req.body.email, function(secret) {
resp.json({ success: true });
// store the email being added in session data
req.session.pendingAddition = secret;
// let's now kick out a verification email!
email.sendAddAddressEmail(req.body.email, req.body.site, secret);
});
} catch(e) {
// we should differentiate tween' 400 and 500 here.
httputils.badRequest(resp, e.toString());
}
resp.json({ success: true });
// let's now kick out a verification email!
email.sendAddAddressEmail(req.body.email, req.body.site, secret);
});
} catch(e) {
// we should differentiate tween' 400 and 500 here.
httputils.badRequest(resp, e.toString());
}
});
});
app.get('/wsapi/email_for_token', checkParams(["token"]), function(req,resp) {
......@@ -387,7 +402,7 @@ function setup(app) {
// if the work factor has changed, update the hash here. issue #204
// NOTE: this runs asynchronously and will not delay the response
if (configuration.get('bcrypt_work_factor') != bcrypt.get_rounds(hash)) {
if (config.get('bcrypt_work_factor') != bcrypt.get_rounds(hash)) {
logger.info("updating bcrypted password for email " + req.body.email);
bcrypt_password(req.body.pass, function(err, hash) {
db.updatePassword(req.body.email, hash, function(err) {
......@@ -436,7 +451,7 @@ function setup(app) {
// same account, we certify the key
// we certify it for a day for now
var expiration = new Date();
expiration.setTime(new Date().valueOf() + configuration.get('certificate_validity_ms'));
expiration.setTime(new Date().valueOf() + config.get('certificate_validity_ms'));
var cert = ca.certify(req.body.email, pk, expiration);
resp.writeHead(200, {'Content-Type': 'text/plain'});
......
......@@ -84,7 +84,8 @@ g_configs.production = {
},
bcrypt_work_factor: 12,
authentication_duration_ms: (7 * 24 * 60 * 60 * 1000),
certificate_validity_ms: (24 * 60 * 60 * 1000)
certificate_validity_ms: (24 * 60 * 60 * 1000),
min_time_between_emails_ms: (60 * 1000)
};
......@@ -97,7 +98,8 @@ g_configs.local = {
database: { driver: "json" },
bcrypt_work_factor: g_configs.production.bcrypt_work_factor,
authentication_duration_ms: g_configs.production.authentication_duration_ms,
certificate_validity_ms: g_configs.production.certificate_validity_ms
certificate_validity_ms: g_configs.production.certificate_validity_ms,
min_time_between_emails_ms: g_configs.production.min_time_between_emails_ms
};
if (undefined !== process.env['NODE_EXTRA_CONFIG']) {
......@@ -105,16 +107,6 @@ if (undefined !== process.env['NODE_EXTRA_CONFIG']) {
eval(fs.readFileSync(process.env['NODE_EXTRA_CONFIG']) + '');
}
Object.keys(g_configs).forEach(function(config) {
if (!g_configs[config].smtp) {
g_configs[config].smtp = {
host: process.env['SMTP_HOST'],
user: process.env['SMTP_USER'],
pass: process.env['SMTP_PASS']
};
}
});
// test environments are variations on local
g_configs.test_json = JSON.parse(JSON.stringify(g_configs.local));
g_configs.test_json.database = {
......@@ -155,6 +147,15 @@ if (process.env['VERIFIER_URL']) {
g_config.verifier_url = url;
}
// extract smtp params from the environment
if (!g_config.smtp) {
g_config.smtp = {
host: process.env['SMTP_HOST'],
user: process.env['SMTP_USER'],
pass: process.env['SMTP_PASS']
};
}
// now handle ephemeral database configuration. Used in testing.
if (g_config.database.driver === 'mysql') {
if (process.env['MYSQL_DATABASE_NAME']) {
......
......@@ -104,7 +104,8 @@ exports.onReady = function(f) {
'listEmails',
'removeEmail',
'cancelAccount',
'updatePassword'
'updatePassword',
'lastStaged'
].forEach(function(fn) {
exports[fn] = function() {
checkReady();
......
......@@ -129,6 +129,17 @@ exports.isStaged = function(email, cb) {
}
};
exports.lastStaged = function(email, cb) {
if (cb) {
sync();
var d;
if (db.stagedEmails.hasOwnProperty(email)) {
d = new Date(db.staged[db.stagedEmails[email]].when);
}
setTimeout(function() { cb(d); }, 0);
}
};
exports.emailsBelongToSameAccount = function(lhs, rhs, cb) {
sync();
emailToUserID(lhs, function(lhs_uid) {
......@@ -161,7 +172,8 @@ exports.stageUser = function(email, cb) {
sync();
db.staged[secret] = {
type: "add_account",
email: email
email: email,
when: (new Date()).getTime()
};
db.stagedEmails[email] = secret;
flush();
......@@ -176,7 +188,8 @@ exports.stageEmail = function(existing_email, new_email, cb) {
db.staged[secret] = {
type: "add_email",
existing_email: existing_email,
email: new_email
email: new_email,
when: (new Date()).getTime()
};
db.stagedEmails[new_email] = secret;
flush();
......
......@@ -78,7 +78,7 @@ const schemas = [
"CREATE TABLE IF NOT EXISTS email (" +
"id BIGINT AUTO_INCREMENT PRIMARY KEY," +
"user BIGINT NOT NULL," +
"address VARCHAR(255) UNIQUE NOT NULL," +
"address VARCHAR(255) UNIQUE NOT NULL," +
"FOREIGN KEY user_fkey (user) REFERENCES user(id)" +
") ENGINE=InnoDB;",
......@@ -88,7 +88,7 @@ const schemas = [
"new_acct BOOL NOT NULL," +
"existing VARCHAR(255)," +
"email VARCHAR(255) UNIQUE NOT NULL," +
"ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL" +
"ts TIMESTAMP DEFAULT CURRENT_TIMESTAMP NOT NULL" +
") ENGINE=InnoDB;",
];
......@@ -204,6 +204,17 @@ exports.isStaged = function(email, cb) {
);
}
exports.lastStaged = function(email, cb) {
client.query(
"SELECT UNIX_TIMESTAMP(ts) as ts FROM staged WHERE email = ?", [ email ],
function(err, rows) {
if (err) logUnexpectedError(err);
if (!rows || rows.length === 0) cb();
else cb(new Date(rows[0].ts * 1000));
}
);
}
exports.stageUser = function(email, cb) {
var secret = secrets.generate(48);
// overwrite previously staged users
......
......@@ -63,6 +63,16 @@ exports.badRequest = function(resp, reason)
resp.end();
};
exports.forbidden = function(resp, reason)
{
resp.writeHead(403, {"Content-Type": "text/plain"});
resp.write("Forbidden");
if (reason) {
resp.write(": " + reason);
}
resp.end();
};
exports.jsonResponse = function(resp, obj)
{
resp.writeHead(200, {"Content-Type": "application/json"});
......
......@@ -54,7 +54,7 @@ function injectCookies(ctx, headers) {
headers['Cookie'] += k + "=" + ctx.cookieJar[k];
}
}
}
}
function extractCookies(ctx, res) {
if (ctx.cookieJar === undefined) ctx.cookieJar = {};
......
0% Loading or .
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment