From 2c63bd22863b11340246f5d6901c231790abddc5 Mon Sep 17 00:00:00 2001 From: Chris Karlof Date: Sun, 3 Feb 2013 23:12:13 -0800 Subject: [PATCH] start of sync refactor with tests --- background/background.html | 4 ++ background/firebase_sync.js | 78 ++++++++++++++++++++++ background/gombot.js | 43 ++++++++----- background/main.js | 32 ++------- background/models/user.js | 101 ++++++++++++++--------------- background/modules.js | 29 +++++++++ background/sync_adapter.js | 121 +++++++++++++++++++++++++++++++++++ lib/backbone.localStorage.js | 21 ++++-- manifest.json | 6 +- test/test-local-sync.js | 22 +++++++ 10 files changed, 355 insertions(+), 102 deletions(-) create mode 100644 background/firebase_sync.js create mode 100644 background/modules.js create mode 100644 background/sync_adapter.js create mode 100644 test/test-local-sync.js diff --git a/background/background.html b/background/background.html index 523dcf1..194b47c 100644 --- a/background/background.html +++ b/background/background.html @@ -1,4 +1,6 @@ + + @@ -22,6 +24,8 @@ + + diff --git a/background/firebase_sync.js b/background/firebase_sync.js new file mode 100644 index 0000000..4c79a17 --- /dev/null +++ b/background/firebase_sync.js @@ -0,0 +1,78 @@ +var FirebaseSync = function() { + + var dataRef = new Firebase('https://gombot.firebaseIO.com'); + + var authClient = new FirebaseAuthClient(dataRef, authClientCallback); + + var usersRef = dataRef.child('users'); + + var currentUser; + + chrome.webRequest.onBeforeSendHeaders.addListener( + function(details) { + // Remove Origin header if it exists + for (var i = 0; i < details.requestHeaders.length; ++i) { + if (details.requestHeaders[i].name === 'Origin') { + details.requestHeaders.splice(i, 1); + break; + } + } + // Remove Referer header if it exists + for (var i = 0; i < details.requestHeaders.length; ++i) { + if (details.requestHeaders[i].name === 'Referer') { + details.requestHeaders.splice(i, 1); + } + } + // Add a Referer header for gombot.org + details.requestHeaders.push({ name: 'Referer', value: 'https://gombot.org/'}); + return {requestHeaders: details.requestHeaders}; + }, + { urls: ["https://auth.firebase.com/*"] }, + ["blocking", "requestHeaders"] + ); + + + function authClientCallback(error, user) { + if (error) { + // an error occurred while attempting login + console.log(error); + } else if (user) { + currentUser = user; + usersRef.child(user.id).on('value', getUserData); + // user authenticated with Firebase + console.log('User ID: ' + user.id + ', Provider: ' + user.provider); + } else { + if (currentUser) usersRef.child(currentUser.id).off('value', getUserData); + currentUser = null; + // user is logged out + } + } + + function create(email, password, options) { + authClient.createUser(email, password, function(error, user) { + if (!error) { + console.log('User Id: ' + user.id + ', Email: ' + user.email); + } else { + console.log("FirebaseSync.create error:", error); + } + }); + } + + function login(email, password, options) { + authClient.login('password', { + email: email, + password: password, + rememberMe: true + }); + } + + function getUserData(data) { + console.log("User data:", data.val()); + } + + return { + create: create, + login: login, + getUserData: getUserData + }; +}; \ No newline at end of file diff --git a/background/gombot.js b/background/gombot.js index 197cc1d..c339631 100644 --- a/background/gombot.js +++ b/background/gombot.js @@ -45,9 +45,10 @@ var _Gombot = function(importedModules, Gombot) { Gombot.TldService = getModule("TldService")(getModule("Tld"), getModule("Uri")); Gombot.SiteConfigs = getModule("SiteConfigs"); Gombot.Realms = getModule("Realms")(Gombot, Gombot.SiteConfigs, getModule("Uri")); - Gombot.Storage = getModule("Storage")(Backbone, _, Gombot.LocalStorage); // defined by backbone.localStorage.js - Gombot.GombotClient = getModule("GombotClient"); - Gombot.Sync = getModule("GombotSync")(Gombot, Backbone, _); + Gombot.Storage = getModule("Storage")(Backbone, _, Gombot.LocalStorage); // local sync; defined by backbone.localStorage.js + //Gombot.GombotClient = getModule("GombotClient"); + //Gombot.Sync = getModule("GombotSync")(Gombot, Backbone, _); // original sync using our api + //Gombot.FirebaseSync = getModule("FirebaseSync")(Gombot); // sync using firebase Gombot.LoginCredential = getModule("LoginCredential")(Gombot, Backbone, _); Gombot.LoginCredentialCollection = getModule("LoginCredentialCollection")(Backbone, _, Gombot.LoginCredential); // LoginCredential need to be initialized Gombot.CapturedCredentialStorage = getModule("CapturedCredentialStorage")(Gombot, getModule("Uri")); @@ -55,8 +56,12 @@ var _Gombot = function(importedModules, Gombot) { Gombot.AccountManager = getModule("AccountManager")(Gombot, _); Gombot.CommandHandler = getModule("CommandHandler")(Gombot, Gombot.Messaging, _); Gombot.Pages = getModule("Pages")(Gombot); - Gombot.InfobarManager = getModule("InfobarManager"); - Gombot.Infobars = getModule("Infobars")(Gombot); + Gombot.Crypto = getModule("GombotCrypto"); + Gombot.User = getModule("User")(Backbone, _, Gombot); + if (typeof chrome !== "undefined") { + Gombot.InfobarManager = getModule("InfobarManager"); + Gombot.Infobars = getModule("Infobars")(Gombot); + } var currentUser = null; Gombot.getCurrentUser = function() { @@ -73,19 +78,24 @@ var _Gombot = function(importedModules, Gombot) { currentUser.destroy({ localOnly: true, success: function() { currentUser = null; callback(); }}); }; - new Gombot.Storage("users", function(store) { - Gombot.User = getModule("User")(Backbone, _, Gombot, store); - Gombot.UserCollection = getModule("UserCollection")(Backbone, _, Gombot, store); - checkFirstRun(); - }); - - function checkFirstRun() { - Gombot.LocalStorage.getItem("firstRun", function(firstRun) { - initGombot(firstRun); + Gombot.init = function(options) { + options = options || {}; + options.storeName = options.storeName || "users"; + options.callback = options.callback || checkFirstRun; + new Gombot.Storage(options.storeName, function(store) { + Gombot.SyncAdapter = getModule("SyncAdapter")(Gombot, Gombot.Crypto, store, _); + Gombot.UserCollection = getModule("UserCollection")(Backbone, _, Gombot, store); + options.callback(); }); } - function initGombot(firstRun) { + function checkFirstRun() { + Gombot.LocalStorage.getItem("firstRun", function(firstRun) { + fetchUsers(firstRun); + }); + } + + function fetchUsers(firstRun) { Gombot.users = new Gombot.UserCollection(); Gombot.users.fetch({ success: function() { @@ -102,6 +112,7 @@ var _Gombot = function(importedModules, Gombot) { if (typeof module !== "undefined" && module.exports) { module.exports = _Gombot; // export namespace constructor, for Firefox -} else { // otherwise, just create the global Gombot namespace +} else { // otherwise, just create the global Gombot namespace and init var Gombot = _Gombot({}); + Gombot.init(); } diff --git a/background/main.js b/background/main.js index fd9ab94..fd14c7b 100644 --- a/background/main.js +++ b/background/main.js @@ -47,33 +47,9 @@ windows.on('open', function(window) { addToolbarButton(); }); -/** Load all Gombot modules **/ - -var gombotModules = { - Backbone: require("./lib/backbone"), - _ : require("./lib/underscore"), - Messaging: require("./messaging"), - LocalStorage: require("./local_storage"), - Tld: require("./lib/tld.js"), - Uri: require("./lib/jsuri"), - TldService: require("./tld_service"), - SiteConfigs: require("./site_configs"), - Realms: require("./realms"), - Storage: require("./storage"), - GombotClient: require("./client/client"), - GombotSync: require("./gombot_sync"), - LoginCredential: require("./models/login_credential"), - LoginCredentialCollection: require("./collections/login_credential_collection"), - CapturedCredentialStorage: require("./captured_credential_storage"), - Linker: require("./linker"), - CommandHandler: require("./command_handler"), - User: require("./models/user"), - UserCollection: require("./collections/user_collection"), - AccountManager: require("./account_manager"), - Pages: require("./pages") -}; - -var Gombot = require("./gombot")(gombotModules); +var GombotModules = require("./modules"); +var Gombot = require("./gombot")(GombotModules); +Gombot.init(); /** Tpp panel stuff **/ @@ -109,3 +85,5 @@ pageMod.PageMod({ Gombot.Messaging.registerPageModWorker(worker); } }); + +exports.gombot = Gombot; diff --git a/background/models/user.js b/background/models/user.js index 59e81f3..ad09564 100644 --- a/background/models/user.js +++ b/background/models/user.js @@ -1,12 +1,9 @@ -var User = function(Backbone, _, Gombot, LocalStorage) { +var User = function(Backbone, _, Gombot) { const USER_DATA_VERSIONS = [ "identity.mozilla.com/gombot/v1/userData" ]; - var GombotSync = Gombot.Sync, - LoginCredentialCollection = Gombot.LoginCredentialCollection; - // attributes should be something like: // { // "version": "identity.mozilla.com/gombot/v1/userData", @@ -38,8 +35,6 @@ var User = function(Backbone, _, Gombot, LocalStorage) { disabledSites: {} }, - localStorage: LocalStorage, - initialize: function() { Backbone.Model.prototype.initialize.apply(this, arguments); this.addSyncListener(this.get("logins")); @@ -64,6 +59,7 @@ var User = function(Backbone, _, Gombot, LocalStorage) { }, isAuthenticated: function() { + return false; return this.client && ((this.client.isAuthenticated && this.client.isAuthenticated()) || (this.client.keys && this.client.user)); }, @@ -71,60 +67,61 @@ var User = function(Backbone, _, Gombot, LocalStorage) { // call model.toJSON({ encrypted: true, ciphertext: }) // Other toJSON() creates a standard plaintext representation of a User object toJSON: function(args) { - var result; - args = args || {}; - if (args.ciphertext) { - result = { ciphertext: args.ciphertext, updated: this.updated, id: this.id, email: this.get("email"), version: this.get("version") }; - if (this.isAuthenticated()) _.extend(result, { client: this.client.toJSON() }); - return result; - } - else { - result = Backbone.Model.prototype.toJSON.apply(this, arguments); - return _.extend(result, { logins: this.get("logins").toJSON() }); + var result = Backbone.Model.prototype.toJSON.apply(this, arguments); + return _.extend(result, { logins: this.get("logins").toJSON() }); + }, + + // Returns an object containing key/values of data that will be + // stored in plaintext with an encrypted copy of this model's data. + // The metadata should not contain any information that is intended + // to be stored encrypted at rest. + getMetadata: function() { + return { + id: this.id, + email: this.get("email"), + version: this.get("version"), + updated: this.updated } }, parse: function(resp) { - if (resp.ciphertext) this.ciphertext = resp.ciphertext; if (resp.updated) this.updated = resp.updated; - if (resp.client) this.client = resp.client; - delete resp.ciphertext; delete resp.updated; - delete resp.client; return resp; }, sync: function(method, model, options) { - var self = this; - var success = function(resp) { - var s = options.success; - options.success = function(model, resp, options) { - console.log("User.sync finished method="+method+" resp="+JSON.stringify(resp)+" model="+JSON.stringify(model)); - // resp.data is returned by GombotSync calls with plaintext user data - if (s) s(model, resp.data || {}, options); - } - if (resp.updated) self.updated = resp.updated; - // ciphertext in resp indicates we need to write it out to local storage - if (resp.ciphertext) { - if (method === "read") { - self.save(resp.data, _.extend(options, { localOnly: true, ciphertext: resp.ciphertext })); - } else { - console.log("localSync method="+method); - Backbone.localSync(method, model, _.extend(options, { ciphertext: resp.ciphertext })); - } - } else if (options.success) { - options.success(model, resp, options); - } - }; - var error = function(args) { - if (options.error) options.error(args); - }; - var o = _.clone(options); - if (options.localOnly) { - Backbone.localSync(method, model, options); - } else { - GombotSync.sync(method, model, _.extend(o,{ success: success, error: error })); - } + Gombot.SyncAdapter.sync(method, model, options); + // var self = this; + // var success = function(resp) { + // var s = options.success; + // options.success = function(model, resp, options) { + // console.log("User.sync finished method="+method+" resp="+JSON.stringify(resp)+" model="+JSON.stringify(model)); + // // resp.data is returned by GombotSync calls with plaintext user data + // if (s) s(model, resp.data || {}, options); + // } + // if (resp.updated) self.updated = resp.updated; + // // ciphertext in resp indicates we need to write it out to local storage + // if (resp.ciphertext) { + // if (method === "read") { + // self.save(resp.data, _.extend(options, { localOnly: true, ciphertext: resp.ciphertext })); + // } else { + // console.log("localSync method="+method); + // Backbone.localSync(method, model, _.extend(options, { ciphertext: resp.ciphertext })); + // } + // } else if (options.success) { + // options.success(model, resp, options); + // } + // }; + // var error = function(args) { + // if (options.error) options.error(args); + // }; + // var o = _.clone(options); + // if (options.localOnly) { + // Backbone.localSync(method, model, options); + // } else { + // GombotSync.sync(method, model, _.extend(o,{ success: success, error: error })); + // } }, set: function(key, val, options) { @@ -137,9 +134,9 @@ var User = function(Backbone, _, Gombot, LocalStorage) { } else { (attributes = {})[key] = val; } - if (attributes.logins !== undefined && !(attributes.logins instanceof LoginCredentialCollection)) { + if (attributes.logins !== undefined && !(attributes.logins instanceof Gombot.LoginCredentialCollection)) { logins = attributes.logins; - attributes.logins = this.get("logins") || new LoginCredentialCollection(); + attributes.logins = this.get("logins") || new Gombot.LoginCredentialCollection(); } result = Backbone.Model.prototype.set.call(this, attributes, options); if (result && logins) { diff --git a/background/modules.js b/background/modules.js new file mode 100644 index 0000000..92edfca --- /dev/null +++ b/background/modules.js @@ -0,0 +1,29 @@ +/** Load all Gombot modules **/ + +var GombotModules = { + Backbone: require("./lib/backbone"), + _ : require("./lib/underscore"), + Messaging: require("./messaging"), + LocalStorage: require("./local_storage"), + Tld: require("./lib/tld.js"), + Uri: require("./lib/jsuri"), + TldService: require("./tld_service"), + SiteConfigs: require("./site_configs"), + Realms: require("./realms"), + Storage: require("./storage"), + //GombotClient: require("./client/client"), + //GombotSync: require("./gombot_sync"), + LoginCredential: require("./models/login_credential"), + LoginCredentialCollection: require("./collections/login_credential_collection"), + CapturedCredentialStorage: require("./captured_credential_storage"), + Linker: require("./linker"), + CommandHandler: require("./command_handler"), + User: require("./models/user"), + UserCollection: require("./collections/user_collection"), + AccountManager: require("./account_manager"), + Pages: require("./pages"), + GombotCrypto: require("./client/crypto"), + SyncAdapter: require("./sync_adapter") +}; + +module.exports = GombotModules; \ No newline at end of file diff --git a/background/sync_adapter.js b/background/sync_adapter.js new file mode 100644 index 0000000..eecbc60 --- /dev/null +++ b/background/sync_adapter.js @@ -0,0 +1,121 @@ +var SyncAdapter = function(Gombot, GombotCrypto, SyncStrategy, _) { + + // TODO: seed from an actual entropy source + GombotCrypto.seed("oiqwjeciouqh3c89cnkjasdcnasjf84u9jcuqwiench734fhujhwuqhf73f73fhsdjfhasdf734fhdkcnuf"+(new Date().toString()) ,function(err) { + if (err) console.log("GombotCrypto.seed error:", err); + }); + + function maybeHandleError(handler, err) { + if (err) { + console.log("SyncMediator error", err); + if (handler) handler(err); + return true; + } + else return false; + } + + function encryptModel(model, keys, options) { + GombotCrypto.encrypt(keys, JSON.stringify(model), function(err, ciphertext) { + if (err) return maybeHandleError(options.error, err); + options.success(ciphertext); + }); + } + + function decryptModelData(ciphertext, keys, options) { + GombotCrypto.decrypt(keys, ciphertext, function(err, json) { + var modelData; + try { + if (!err) { + modelData = JSON.parse(json); + } + } catch (e) { + err = new Error("Could not parse decrypted JSON:", json); + } + if (err) return maybeHandleError(options.error, err); + options.success(modelData); + }); + } + + function createCryptoProxyForModel(model, keys) { + var clone = _.clone(model); + return _.extend(clone, { + toJSON: function(options) { + // missing options means synchronous response to underyling object + if (!options || !options.success) return model.toJSON(); + var o = _.clone(options); + encryptModel(model, keys, _.extend(o, { success: function(ciphertext) { + options.success(_.extend(model.getMetadata(), { + ciphertext: ciphertext // encrypted plaintext model + })); + }})); + }, + parse: function(resp, options) { + var o = _.clone(options), + ciphertext = resp.ciphertext; + decryptModelData(ciphertext, keys, _.extend(o, { success: function(modelData) { + delete resp.ciphertext; + options.success(_.extend(resp, modelData)); + }})); + } + }); + } + + function getCryptoProxyForModel(model, options) { + if (model.cryptoProxy) return options.success(model.cryptoProxy); + var o = _.clone(options); + deriveKeysForModel(model, _.extend(o, { success: function(keys) { + model.cryptoProxy = createCryptoProxyForModel(model, keys); + options.success(model.cryptoProxy); + }})); + } + + var kdf = GombotCrypto.derive; + // Special kdf derivation function we'll pass to GombotClient to handle FX slowness bug + if (typeof require !== "undefined") { + kdf = function (args, callback) { + console.log("in derive") + require("gombot-crypto-jetpack").kdf(args.email, args.password).then(function(keys) { + callback(null, keys); + }); + } + } + + // options.password must be present + function deriveKeysForModel(model, options) { + kdf({ + email: model.get("email"), + password: options.password + }, function(err, keys) { + if (err) return maybeHandleError(options.error, err); + options.success(keys); + }); + } + + function setSyncStrategy(strategy) { + SyncStrategy = strategy; + } + + function sync(method, model, options) { + if (!(model instanceof Gombot.User)) { + if (options.error) options.error("sync only supports syncing instances of Gombot.User"); + return false; + } + var o = _.clone(options); + getCryptoProxyForModel(model, _.extend(o, { success: function(cryptoProxyForModel) { + var o = _.clone(options); + // translate model proxy back to original model + SyncStrategy.sync(method, cryptoProxyForModel, _.extend(o, { success: function(modelProxy, resp, modifiedOptions) { + if (options.success) options.success(model, resp, options); + }})); + }})); + } + + return { + sync: sync, + setSyncStrategy: setSyncStrategy + }; +}; + +if (typeof module !== "undefined" && module.exports) { + module.exports = SyncAdapter; +} diff --git a/lib/backbone.localStorage.js b/lib/backbone.localStorage.js index 93d9816..7da7aa2 100644 --- a/lib/backbone.localStorage.js +++ b/lib/backbone.localStorage.js @@ -3,7 +3,7 @@ * * https://github.com/jeromegn/Backbone.localStorage */ -var Storage = function(Backbone, _, LocalStorage) { +var Storage = function(Backbone, _, LocalStorage, store) { return (function (root, factory) { // if (typeof define === "function" && define.amd) { // // AMD. Register as an anonymous module. @@ -62,9 +62,12 @@ _.extend(Backbone.LocalStorage.prototype, { var cb = _.after(2, function() { callback(model.toJSON()); }); - this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(model.toJSON({ ciphertext: options.ciphertext })), cb); - this.records.push(model.id.toString()); - this.save(cb); + var o = _.clone(options); + model.toJSON(_.extend(o, { success: (function(jsonObj) { + this.localStorage().setItem(this.name+"-"+model.id, JSON.stringify(jsonObj), cb); + this.records.push(model.id.toString()); + this.save(cb); + }).bind(this)})); return; }, @@ -116,6 +119,12 @@ _.extend(Backbone.LocalStorage.prototype, { // fix for "illegal access" error on Android when JSON.parse is passed null jsonData: function (data) { return data && JSON.parse(data); + }, + + sync: function(method, model, options) { + var o = _.clone(options); + o.store = this; + Backbone.LocalStorage.sync(method, model, o); } }); @@ -124,7 +133,7 @@ _.extend(Backbone.LocalStorage.prototype, { // *localStorage* property, which should be an instance of `Store`. // window.Store.sync and Backbone.localSync is deprectated, use Backbone.LocalStorage.sync instead Backbone.LocalStorage.sync = Backbone.localSync = function(method, model, options) { - var store = model.localStorage || model.collection.localStorage; + var store = options.store || model.localStorage || model.collection.localStorage; var syncDfd = (typeof $ !== "undefined") && $.Deferred && $.Deferred(); //If $ is having Deferred - use it. @@ -149,6 +158,8 @@ Backbone.LocalStorage.sync = Backbone.localSync = function(method, model, option if (options && options.complete) options.complete(resp); }; + console.log("Backbone.LocalStorage.sync", method, model, options); + switch (method) { case "read": model.id != undefined ? store.find(model, callback, options) : store.findAll(callback, options); break; case "create": store.create(model, callback, options); break; diff --git a/manifest.json b/manifest.json index 9c8c085..025076f 100644 --- a/manifest.json +++ b/manifest.json @@ -8,12 +8,14 @@ "notifications", "storage", "http://*/", - "https://*/" + "https://*/", + "webRequest", + "webRequestBlocking" ], "icons": { "128": "images/gombot-icon-128.png" }, - + "content_security_policy": "script-src 'self' https://cdn.firebase.com https://auth.firebase.com https://*.firebaseio.com; object-src 'self'", "background": { "page": "background/background.html" }, diff --git a/test/test-local-sync.js b/test/test-local-sync.js new file mode 100644 index 0000000..e4e1e09 --- /dev/null +++ b/test/test-local-sync.js @@ -0,0 +1,22 @@ +exports.testCreate = function(test) { + var GombotModules = require("./modules"); + var Gombot = require("./gombot")(GombotModules); + + Gombot.init({ storeName: "testUsers", callback: function() { + var email = "test+"+Math.floor((1+Math.random())*10000)+"@test.com"; + var u = new Gombot.User({ email: email }); + u.save(null, { success: function() { + console.log("user saved", u); + test.pass(); + test.done(); + }, + error: function(err) { + console.log("error:", err); + test.fail(); + test.done(); + }, + password: "foobar" + }); + }}); + test.waitUntilDone(); +} \ No newline at end of file