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