From c70817c146df550906c6346feeefdc108dc7697c Mon Sep 17 00:00:00 2001 From: Lloyd Hilaiel <lloyd@hilaiel.com> Date: Wed, 13 Apr 2011 15:43:57 -0600 Subject: [PATCH] use sqlite for persistence in the authority --- authority/.gitignore | 1 + authority/server/db.js | 202 ++++++++++++++++++++++++++++---------- authority/server/email.js | 6 +- authority/server/wsapi.js | 45 +++++---- 4 files changed, 182 insertions(+), 72 deletions(-) create mode 100644 authority/.gitignore diff --git a/authority/.gitignore b/authority/.gitignore new file mode 100644 index 000000000..ae9f5955b --- /dev/null +++ b/authority/.gitignore @@ -0,0 +1 @@ +/authdb.sqlite diff --git a/authority/server/db.js b/authority/server/db.js index de6fcff41..390e6cc10 100644 --- a/authority/server/db.js +++ b/authority/server/db.js @@ -1,7 +1,27 @@ -// Registered users. This is a horribly inefficient data structure -// which only exists for prototype purposes. -var g_users = [ -]; +const sqlite = require('sqlite'), + path = require('path'); + +var db = new sqlite.Database(); + +db.open(path.join(path.dirname(__dirname), "authdb.sqlite"), function (error) { + if (error) { + console.log("Couldn't open database: " + error); + throw error; + } + + function createTable(name, sql) { + db.execute(sql, function (error, rows) { + if (error) { + console.log("Couldn't create " + name + " table: " + error); + throw error; + } + }); + } + + createTable('users', "CREATE TABLE IF NOT EXISTS users ( id INTEGER PRIMARY KEY, password TEXT )"); + createTable('emails', "CREATE TABLE IF NOT EXISTS emails ( id INTEGER PRIMARY KEY, user INTEGER, address TEXT UNIQUE )"); + createTable('keys', "CREATE TABLE IF NOT EXISTS keys ( id INTEGER PRIMARY KEY, email INTEGER, key TEXT, expires INTEGER )"); +}); // half created user accounts (pending email verification) // OR @@ -9,6 +29,46 @@ var g_users = [ var g_staged = { }; +// an email to secret map for efficient fulfillment of isStaged queries +var g_stagedEmails = { +}; + +function executeTransaction(statements, cb) { + function executeTransaction2(statements, cb) { + if (statements.length == 0) cb(); + else { + var s = statements.shift(); + db.execute(s[0], s[1], function(err, rows) { + if (err) cb(err); + else executeTransaction2(statements, cb); + }); + } + } + + db.execute('BEGIN', function(err, rows) { + executeTransaction2(statements, function(err) { + if (err) cb(err); + else db.execute('COMMIT', function(err, rows) { + cb(err); + }); + }); + }); +} + +function emailToUserID(email, cb) { + db.execute( + 'SELECT users.id FROM emails, users WHERE emails.address = ? AND users.id == emails.user', + [ email ], + function (err, rows) { + if (rows && rows.length == 1) { + cb(rows[0].id); + } else { + if (err) console.log("database error: " + err); + cb(undefined); + } + }); +} + exports.findByEmail = function(email) { for (var i = 0; i < g_users.length; i++) { for (var j = 0; j < g_users[i].emails.length; j++) { @@ -18,12 +78,17 @@ exports.findByEmail = function(email) { return undefined; }; +exports.emailKnown = function(email, cb) { + db.execute( + "SELECT id FROM emails WHERE address = ?", + [ email ], + function(error, rows) { + cb(rows.length > 0); + }); +}; + exports.isStaged = function(email) { - // XXX: not efficient - for (var k in g_staged) { - if (g_staged[k].email === email) return true; - } - return false; + return g_stagedEmails.hasOwnProperty(email); }; function generateSecret() { @@ -35,12 +100,22 @@ function generateSecret() { return str; } -exports.addEmailToAccount = function(existing_email, email, pubkey) { - var acct = exports.findByEmail(existing_email); - if (acct === undefined) throw "no such email: " + existing_email; - if (acct.emails.indexOf(email) == -1) acct.emails.push(email); - acct.keys.push(email); - return; +exports.addEmailToAccount = function(existing_email, email, pubkey, cb) { + emailToUserID(existing_email, function(userID) { + if (userID == undefined) { + cb("no such email: " + existing_email, undefined); + } else { + executeTransaction([ + [ "INSERT INTO emails (user, address) VALUES(?,?)", [ userID, email ] ], + [ "INSERT INTO keys (email, key, expires) VALUES(last_insert_rowid(),?,?)", + [ pubkey, ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) ] + ] + ], function (error) { + if (error) cb(error); + else cb(); + }); + } + }); } /* takes an argument object including email, pass, and pubkey. */ @@ -53,6 +128,7 @@ exports.stageUser = function(obj) { pubkey: obj.pubkey, pass: obj.pass }; + g_stagedEmails[obj.email] = secret; return secret; }; @@ -66,39 +142,48 @@ exports.stageEmail = function(existing_email, new_email, pubkey) { email: new_email, pubkey: pubkey }; + g_stagedEmails[new_email] = secret; return secret; }; /* invoked when a user clicks on a verification URL in their email */ -exports.gotVerificationSecret = function(secret) { - if (!g_staged.hasOwnProperty(secret)) return false; +exports.gotVerificationSecret = function(secret, cb) { + if (!g_staged.hasOwnProperty(secret)) cb("unknown secret"); // simply move from staged over to the emails "database" var o = g_staged[secret]; delete g_staged[secret]; + delete g_stagedEmails[o.email]; if (o.type === 'add_account') { - if (undefined != exports.findByEmail(o.email)) { - throw "email already exists!"; - } - g_users.push({ - emails: [ o.email ], - keys: [ o.pubkey ], - pass: o.pass + exports.emailKnown(o.email, function(known) { + if (known) cb("email already exists!"); + else { + executeTransaction([ + [ "INSERT INTO users (password) VALUES(?)", [ o.pass ] ] , + [ "INSERT INTO emails (user, address) VALUES(last_insert_rowid(),?)", [ o.email ] ], + [ "INSERT INTO keys (email, key, expires) VALUES(last_insert_rowid(),?,?)", + [ o.pubkey, ((new Date()).getTime() + (14 * 24 * 60 * 60 * 1000)) ] + ] + ], function (error) { + if (error) cb(error); + else cb(); + }); + } }); } else if (o.type === 'add_email') { - exports.addEmailToAccount(o.existing_email, o.email, o.pubkey); + exports.addEmailToAccount(o.existing_email, o.email, o.pubkey, cb); } else { - return false; + cb("internal error"); } - - return true; }; /* takes an argument object including email, pass, and pubkey. */ -exports.checkAuth = function(email, pass) { - var acct = exports.findByEmail(email); - if (acct === undefined) return false; - return pass === acct.pass; +exports.checkAuth = function(email, pass, cb) { + db.execute("SELECT users.id FROM emails, users WHERE users.id = emails.user AND emails.address = ? AND users.password = ?", + [ email, pass ], + function (error, rows) { + cb(rows.length === 1); + }); }; /* a high level operation that attempts to sync a client's view with that of the @@ -112,28 +197,43 @@ exports.checkAuth = function(email, pass) { * NOTE: it's not neccesary to differentiate between #2 and #3, as the client action * is the same (regen keypair and tell us about it). */ -exports.getSyncResponse = function(email, identities) { +exports.getSyncResponse = function(email, identities, cb) { var respBody = { unknown_emails: [ ], key_refresh: [ ] }; - // fetch user acct - var acct = exports.findByEmail(email); - - // #1 - for (var e in identities) { - if (acct.emails.indexOf(e) == -1) respBody.unknown_emails.push(e); - } - - // #2 - for (var e in acct.emails) { - e = acct.emails[e]; - if (!identities.hasOwnProperty(e)) respBody.key_refresh.push(e); - } - - // #3 - // XXX todo - - return respBody; + // get the user id associated with this account + emailToUserID(email, function(userID) { + if (userID === undefined) { + cb("no such email: " + email); + return; + } + db.execute( + 'SELECT address FROM emails WHERE ? = user', + [ userID ], + function (err, rows) { + if (err) cb(err); + else { + var emails = [ ]; + for (var i = 0; i < rows.length; i++) emails.push(rows[i].address); + + // #1 + for (var e in identities) { + if (emails.indexOf(e) == -1) respBody.unknown_emails.push(e); + } + + // #2 + for (var e in emails) { + e = emails[e]; + if (!identities.hasOwnProperty(e)) respBody.key_refresh.push(e); + } + + // #3 + // XXX todo + + cb(undefined, respBody); + } + }); + }); }; diff --git a/authority/server/email.js b/authority/server/email.js index edd8c02c1..2777ab6ca 100644 --- a/authority/server/email.js +++ b/authority/server/email.js @@ -6,6 +6,10 @@ exports.sendVerificationEmail = function(email, secret) { // we'll just wait 5 seconds and manually feed the secret back into the // system, as if a user had clicked a link setTimeout(function() { - db.gotVerificationSecret(secret); + db.gotVerificationSecret(secret, function(e) { + if (e) { + console.log("error completing the verification: " + e); + } + }); }, 5000); }; \ No newline at end of file diff --git a/authority/server/wsapi.js b/authority/server/wsapi.js index 103a5962c..9b8e22dc0 100644 --- a/authority/server/wsapi.js +++ b/authority/server/wsapi.js @@ -38,7 +38,9 @@ exports.have_email = function(req, resp) { // get inputs from get data! var email = url.parse(req.url, true).query['email']; logRequest("have_email", {email: email}); - httputils.jsonResponse(resp, undefined != db.findByEmail(email)); + db.emailKnown(email, function(known) { + httputils.jsonResponse(resp, known); + }); }; /* First half of account creation. Stages a user account for creation. @@ -74,15 +76,17 @@ exports.registration_status = function(req, resp) { logRequest("registration_status", req.session); var email = req.session.pendingRegistration; - if (undefined != db.findByEmail(email)) { - delete req.session.pendingRegistration; - req.session.authenticatedUser = email; - httputils.jsonResponse(resp, "complete"); - } else if (db.isStaged(email)) { - httputils.jsonResponse(resp, "pending"); - } else { - httputils.jsonResponse(resp, "noRegistration"); - } + db.emailKnown(email, function(known) { + if (known) { + delete req.session.pendingRegistration; + req.session.authenticatedUser = email; + httputils.jsonResponse(resp, "complete"); + } else if (db.isStaged(email)) { + httputils.jsonResponse(resp, "pending"); + } else { + httputils.jsonResponse(resp, "noRegistration"); + } + }); }; exports.authenticate_user = function(req, resp) { @@ -91,12 +95,10 @@ exports.authenticate_user = function(req, resp) { if (!checkParams(getArgs, resp, [ "email", "pass" ])) return; - if (db.checkAuth(getArgs.email, getArgs.pass)) { - req.session.authenticatedUser = getArgs.email; - httputils.jsonResponse(resp, true); - } else { - httputils.jsonResponse(resp, false); - } + db.checkAuth(getArgs.email, getArgs.pass, function(rv) { + if (rv) req.session.authenticatedUser = getArgs.email; + httputils.jsonResponse(resp, rv); + }); }; exports.add_email = function (req, resp) { @@ -132,8 +134,9 @@ exports.set_key = function (req, resp) { if (!checkParams(getArgs, resp, [ "email", "pubkey" ])) return; if (!isAuthed(req, resp)) return; logRequest("set_key", getArgs); - db.addEmailToAccount(req.session.authenticatedUser, getArgs.email, getArgs.pubkey); - httputils.jsonResponse(resp, true); + db.addEmailToAccount(req.session.authenticatedUser, getArgs.email, getArgs.pubkey, function (rv) { + httputils.jsonResponse(resp, rv); + }); }; exports.am_authed = function(req,resp) { @@ -152,10 +155,12 @@ exports.sync_emails = function(req,resp) { logRequest("sync_emails", requestBody); try { var emails = JSON.parse(requestBody); - var syncResponse = db.getSyncResponse(req.session.authenticatedUser, emails); - httputils.jsonResponse(resp, syncResponse); } catch(e) { httputils.badRequest(resp, "malformed payload: " + e); } + db.getSyncResponse(req.session.authenticatedUser, emails, function(err, syncResponse) { + if (err) httputils.serverError(resp, err); + else httputils.jsonResponse(resp, syncResponse); + }); }); }; -- GitLab