mirror of
https://github.com/mozilla/gecko-dev.git
synced 2025-01-27 07:34:20 +00:00
Bug 836206 - Remove AITC client. r=gps
This commit is contained in:
parent
082460c692
commit
eab35762ba
@ -25,7 +25,6 @@ fi
|
||||
|
||||
MOZ_CHROME_FILE_FORMAT=omni
|
||||
MOZ_SAFE_BROWSING=1
|
||||
MOZ_SERVICES_AITC=1
|
||||
MOZ_SERVICES_COMMON=1
|
||||
MOZ_SERVICES_CRYPTO=1
|
||||
MOZ_SERVICES_HEALTHREPORT=1
|
||||
|
@ -460,10 +460,6 @@
|
||||
@BINPATH@/components/nsINIProcessor.js
|
||||
@BINPATH@/components/nsPrompter.manifest
|
||||
@BINPATH@/components/nsPrompter.js
|
||||
#ifdef MOZ_SERVICES_AITC
|
||||
@BINPATH@/components/AitcComponents.manifest
|
||||
@BINPATH@/components/Aitc.js
|
||||
#endif
|
||||
#ifdef MOZ_DATA_REPORTING
|
||||
@BINPATH@/components/DataReporting.manifest
|
||||
@BINPATH@/components/DataReportingService.js
|
||||
@ -583,9 +579,6 @@
|
||||
#endif
|
||||
|
||||
; Services (gre) prefs
|
||||
#ifdef MOZ_SERVICES_AITC
|
||||
@BINPATH@/defaults/pref/services-aitc.js
|
||||
#endif
|
||||
#ifdef MOZ_SERVICES_NOTIFICATIONS
|
||||
@BINPATH@/defaults/pref/services-notifications.js
|
||||
#endif
|
||||
|
@ -96,6 +96,7 @@ defaults/pref/bug259708.js
|
||||
defaults/pref/bug307259.js
|
||||
defaults/pref/reporter.js
|
||||
defaults/pref/security-prefs.js
|
||||
defaults/pref/services-aitc.js
|
||||
defaults/pref/winpref.js
|
||||
defaults/pref/xpinstall.js
|
||||
defaults/preferences/services-aitc.js
|
||||
@ -882,6 +883,8 @@ xpicleanup@BIN_SUFFIX@
|
||||
chrome/pippki.jar
|
||||
chrome/toolkit.jar
|
||||
components/addonManager.js
|
||||
components/Aitc.js
|
||||
components/AitcComponents.manifest
|
||||
components/amContentHandler.js
|
||||
components/amWebInstallListener.js
|
||||
components/binary.manifest
|
||||
|
@ -8343,12 +8343,6 @@ if test "$MOZ_PLACES"; then
|
||||
AC_DEFINE(MOZ_PLACES)
|
||||
fi
|
||||
|
||||
dnl Build Apps in the Cloud (AITC) if required
|
||||
AC_SUBST(MOZ_SERVICES_AITC)
|
||||
if test -n "$MOZ_SERVICES_AITC"; then
|
||||
AC_DEFINE(MOZ_SERVICES_AITC)
|
||||
fi
|
||||
|
||||
dnl Build Common JS modules provided by services.
|
||||
AC_SUBST(MOZ_SERVICES_COMMON)
|
||||
if test -n "$MOZ_SERVICES_COMMON"; then
|
||||
|
@ -14,10 +14,6 @@ PARALLEL_DIRS += \
|
||||
crypto \
|
||||
$(NULL)
|
||||
|
||||
ifdef MOZ_SERVICES_AITC
|
||||
PARALLEL_DIRS += aitc
|
||||
endif
|
||||
|
||||
ifdef MOZ_SERVICES_HEALTHREPORT
|
||||
PARALLEL_DIRS += healthreport
|
||||
endif
|
||||
|
@ -1,104 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
|
||||
function AitcService() {
|
||||
this.aitc = null;
|
||||
this.wrappedJSObject = this;
|
||||
}
|
||||
AitcService.prototype = {
|
||||
classID: Components.ID("{a3d387ca-fd26-44ca-93be-adb5fda5a78d}"),
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsIObserver,
|
||||
Ci.nsISupportsWeakReference]),
|
||||
|
||||
observe: function observe(subject, topic, data) {
|
||||
switch (topic) {
|
||||
case "app-startup":
|
||||
// We listen for this event beacause Aitc won't work until there is
|
||||
// atleast 1 visible top-level XUL window.
|
||||
Services.obs.addObserver(this, "sessionstore-windows-restored", true);
|
||||
break;
|
||||
case "sessionstore-windows-restored":
|
||||
Services.obs.removeObserver(this, "sessionstore-windows-restored");
|
||||
|
||||
// Don't start AITC if classic sync is on.
|
||||
if (Preferences.get("services.sync.engine.apps", false)) {
|
||||
return;
|
||||
}
|
||||
// Start AITC only if it is enabled.
|
||||
if (!Preferences.get("services.aitc.enabled", false)) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Start AITC service only if apps.enabled is true. If false, setup
|
||||
// an observer in case the value changes as a result of an access to
|
||||
// the DOM API.
|
||||
if (Preferences.get("dom.mozApps.used", false)) {
|
||||
this.start();
|
||||
return;
|
||||
}
|
||||
|
||||
// Wait and see if the user wants anything apps related.
|
||||
Preferences.observe("dom.mozApps.used", function checkIfEnabled() {
|
||||
if (Preferences.get("dom.mozApps.used", false)) {
|
||||
Preferences.ignore("dom.mozApps.used", checkIfEnabled, this);
|
||||
this.start();
|
||||
}
|
||||
}, this);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
start: function start() {
|
||||
if (this.aitc) {
|
||||
return;
|
||||
}
|
||||
|
||||
// Log to stdout if enabled.
|
||||
Cu.import("resource://services-aitc/main.js");
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
let root = Log4Moz.repository.getLogger("Service.AITC");
|
||||
root.level = Log4Moz.Level[Preferences.get("services.aitc.log.level")];
|
||||
if (Preferences.get("services.aitc.log.dump")) {
|
||||
root.addAppender(new Log4Moz.DumpAppender());
|
||||
}
|
||||
this.aitc = new Aitc();
|
||||
Services.obs.notifyObservers(null, "service:aitc:started", null);
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
function AboutApps() {
|
||||
}
|
||||
AboutApps.prototype = {
|
||||
classID: Components.ID("{1de7cbe8-60f1-493e-b56b-9d099b3c018e}"),
|
||||
|
||||
QueryInterface: XPCOMUtils.generateQI([Ci.nsISupports,
|
||||
Ci.nsIAboutModule]),
|
||||
|
||||
getURIFlags: function(aURI) {
|
||||
return Ci.nsIAboutModule.ALLOW_SCRIPT;
|
||||
},
|
||||
|
||||
newChannel: function(aURI) {
|
||||
let channel = Services.io.newChannel(
|
||||
Preferences.get("services.aitc.dashboard.url"), null, null
|
||||
);
|
||||
channel.originalURI = aURI;
|
||||
return channel;
|
||||
}
|
||||
};
|
||||
|
||||
const components = [AitcService, AboutApps];
|
||||
this.NSGetFactory = XPCOMUtils.generateNSGetFactory(components);
|
@ -1,10 +0,0 @@
|
||||
# Aitc.js
|
||||
component {a3d387ca-fd26-44ca-93be-adb5fda5a78d} Aitc.js
|
||||
contract @mozilla.org/services/aitc;1 {a3d387ca-fd26-44ca-93be-adb5fda5a78d}
|
||||
category app-startup AitcService service,@mozilla.org/services/aitc;1
|
||||
|
||||
component {1de7cbe8-60f1-493e-b56b-9d099b3c018e} Aitc.js
|
||||
contract @mozilla.org/network/protocol/about;1?what=apps {1de7cbe8-60f1-493e-b56b-9d099b3c018e}
|
||||
|
||||
# Register resource aliases
|
||||
resource services-aitc resource://gre/modules/services-aitc/
|
@ -1,33 +0,0 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DEPTH = @DEPTH@
|
||||
topsrcdir = @top_srcdir@
|
||||
srcdir = @srcdir@
|
||||
VPATH = @srcdir@
|
||||
|
||||
include $(DEPTH)/config/autoconf.mk
|
||||
|
||||
aitc_modules := \
|
||||
browserid.js \
|
||||
client.js \
|
||||
main.js \
|
||||
manager.js \
|
||||
storage.js \
|
||||
$(NULL)
|
||||
|
||||
EXTRA_COMPONENTS = \
|
||||
AitcComponents.manifest \
|
||||
Aitc.js \
|
||||
$(NULL)
|
||||
|
||||
PREF_JS_EXPORTS = $(srcdir)/services-aitc.js
|
||||
|
||||
AITC_MODULE_FILES := $(addprefix modules/,$(aitc_modules))
|
||||
AITC_MODULE_DEST = $(FINAL_TARGET)/modules/services-aitc
|
||||
INSTALL_TARGETS += AITC_MODULE
|
||||
|
||||
TEST_DIRS += tests
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
@ -1,472 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["BrowserID"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
|
||||
const PREFS = new Preferences("services.aitc.browserid.");
|
||||
|
||||
/**
|
||||
* This implementation will be replaced with native crypto and assertion
|
||||
* generation goodness. See bug 753238.
|
||||
*/
|
||||
function BrowserIDService() {
|
||||
this._log = Log4Moz.repository.getLogger("Service.AITC.BrowserID");
|
||||
this._log.level = Log4Moz.Level[PREFS.get("log")];
|
||||
}
|
||||
BrowserIDService.prototype = {
|
||||
/**
|
||||
* Getter that returns the freshest value for ID_URI.
|
||||
*/
|
||||
get ID_URI() {
|
||||
return PREFS.get("url");
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtain a BrowserID assertion with the specified characteristics.
|
||||
*
|
||||
* @param cb
|
||||
* (Function) Callback to be called with (err, assertion) where 'err'
|
||||
* can be an Error or NULL, and 'assertion' can be NULL or a valid
|
||||
* BrowserID assertion. If no callback is provided, an exception is
|
||||
* thrown.
|
||||
*
|
||||
* @param options
|
||||
* (Object) An object that may contain the following properties:
|
||||
*
|
||||
* "requiredEmail" : An email for which the assertion is to be
|
||||
* issued. If one could not be obtained, the call
|
||||
* will fail. If this property is not specified,
|
||||
* the default email as set by the user will be
|
||||
* chosen. If both this property and "sameEmailAs"
|
||||
* are set, an exception will be thrown.
|
||||
*
|
||||
* "sameEmailAs" : If set, instructs the function to issue an
|
||||
* assertion for the same email that was provided
|
||||
* to the domain specified by this value. If this
|
||||
* information could not be obtained, the call
|
||||
* will fail. If both this property and
|
||||
* "requiredEmail" are set, an exception will be
|
||||
* thrown.
|
||||
*
|
||||
* "audience" : The audience for which the assertion is to be
|
||||
* issued. If this property is not set an exception
|
||||
* will be thrown.
|
||||
*
|
||||
* Any properties not listed above will be ignored.
|
||||
*
|
||||
* (This function could use some love in terms of what arguments it accepts.
|
||||
* See bug 746401.)
|
||||
*/
|
||||
getAssertion: function getAssertion(cb, options) {
|
||||
if (!cb) {
|
||||
throw new Error("getAssertion called without a callback");
|
||||
}
|
||||
if (!options) {
|
||||
throw new Error("getAssertion called without any options");
|
||||
}
|
||||
if (!options.audience) {
|
||||
throw new Error("getAssertion called without an audience");
|
||||
}
|
||||
if (options.sameEmailAs && options.requiredEmail) {
|
||||
throw new Error(
|
||||
"getAssertion sameEmailAs and requiredEmail are mutually exclusive"
|
||||
);
|
||||
}
|
||||
|
||||
new Sandbox(this._getEmails.bind(this, cb, options), this.ID_URI);
|
||||
},
|
||||
|
||||
/**
|
||||
* Obtain a BrowserID assertion by asking the user to login and select an
|
||||
* email address.
|
||||
*
|
||||
* @param cb
|
||||
* (Function) Callback to be called with (err, assertion) where 'err'
|
||||
* can be an Error or NULL, and 'assertion' can be NULL or a valid
|
||||
* BrowserID assertion. If no callback is provided, an exception is
|
||||
* thrown.
|
||||
*
|
||||
* @param win
|
||||
* (Window) A contentWindow that has a valid document loaded. If this
|
||||
* argument is provided the user will be asked to login in the context
|
||||
* of the document currently loaded in this window.
|
||||
*
|
||||
* The audience of the assertion will be set to the domain of the
|
||||
* loaded document, and the "audience" property in the "options"
|
||||
* argument (if provided), will be ignored. The email to which this
|
||||
* assertion issued will be selected by the user when they login (and
|
||||
* "requiredEmail" or "sameEmailAs", if provided, will be ignored). If
|
||||
* the user chooses to not login, this call will fail.
|
||||
*
|
||||
* Be aware! The provided contentWindow must also have loaded the
|
||||
* BrowserID include.js shim for this to work! This behavior is
|
||||
* temporary until we implement native support for navigator.id.
|
||||
*
|
||||
* @param options
|
||||
* (Object) Currently an empty object. Present for future compatiblity
|
||||
* when options for a login case may be added. Any properties, if
|
||||
* present, are ignored.
|
||||
*/
|
||||
getAssertionWithLogin: function getAssertionWithLogin(cb, win, options) {
|
||||
if (!cb) {
|
||||
throw new Error("getAssertionWithLogin called without a callback");
|
||||
}
|
||||
if (!win) {
|
||||
throw new Error("getAssertionWithLogin called without a window");
|
||||
}
|
||||
this._getAssertionWithLogin(cb, win);
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Internal implementation methods begin here
|
||||
*/
|
||||
|
||||
// Try to get the user's email(s). If user isn't logged in, this will be empty
|
||||
_getEmails: function _getEmails(cb, options, sandbox) {
|
||||
let self = this;
|
||||
|
||||
if (!sandbox) {
|
||||
cb(new Error("Sandbox not created"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
function callback(res) {
|
||||
let emails = {};
|
||||
try {
|
||||
emails = JSON.parse(res);
|
||||
} catch (e) {
|
||||
self._log.error("Exception in JSON.parse for _getAssertion: " + e);
|
||||
}
|
||||
self._gotEmails(emails, sandbox, cb, options);
|
||||
}
|
||||
sandbox.box.importFunction(callback);
|
||||
|
||||
let scriptText =
|
||||
"var list = window.BrowserID.User.getStoredEmailKeypairs();" +
|
||||
"callback(JSON.stringify(list));";
|
||||
Cu.evalInSandbox(scriptText, sandbox.box, "1.8", this.ID_URI, 1);
|
||||
},
|
||||
|
||||
// Received a list of emails from BrowserID for current user
|
||||
_gotEmails: function _gotEmails(emails, sandbox, cb, options) {
|
||||
let keys = Object.keys(emails);
|
||||
|
||||
// If list is empty, user is not logged in, or doesn't have a default email.
|
||||
if (!keys.length) {
|
||||
sandbox.free();
|
||||
|
||||
let err = "User is not logged in, or no emails were found";
|
||||
this._log.error(err);
|
||||
try {
|
||||
cb(new Error(err), null);
|
||||
} catch(e) {
|
||||
this._log.warn("Callback threw in _gotEmails " +
|
||||
CommonUtils.exceptionStr(e));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// User is logged in. For which email shall we get an assertion?
|
||||
|
||||
// Case 1: Explicitely provided
|
||||
if (options.requiredEmail) {
|
||||
this._getAssertionWithEmail(
|
||||
sandbox, cb, options.requiredEmail, options.audience
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 2: Derive from a given domain
|
||||
if (options.sameEmailAs) {
|
||||
this._getAssertionWithDomain(
|
||||
sandbox, cb, options.sameEmailAs, options.audience
|
||||
);
|
||||
return;
|
||||
}
|
||||
|
||||
// Case 3: Default email
|
||||
this._getAssertionWithEmail(
|
||||
sandbox, cb, keys[0], options.audience
|
||||
);
|
||||
return;
|
||||
},
|
||||
|
||||
/**
|
||||
* Open a login window and ask the user to login, returning the assertion
|
||||
* generated as a result to the caller.
|
||||
*/
|
||||
_getAssertionWithLogin: function _getAssertionWithLogin(cb, win) {
|
||||
// We're executing navigator.id.get as a content script in win.
|
||||
// This results in a popup that we will temporarily unblock.
|
||||
let pm = Services.perms;
|
||||
let principal = win.document.nodePrincipal;
|
||||
|
||||
let oldPerm = pm.testExactPermissionFromPrincipal(principal, "popup");
|
||||
try {
|
||||
pm.addFromPrincipal(principal, "popup", pm.ALLOW_ACTION);
|
||||
} catch(e) {
|
||||
this._log.warn("Setting popup blocking to false failed " + e);
|
||||
}
|
||||
|
||||
// Open sandbox and execute script. This sandbox will be GC'ed.
|
||||
let sandbox = new Cu.Sandbox(win, {
|
||||
wantXrays: false,
|
||||
sandboxPrototype: win
|
||||
});
|
||||
|
||||
let self = this;
|
||||
function callback(val) {
|
||||
// Set popup blocker permission to original value.
|
||||
try {
|
||||
pm.addFromPrincipal(principal, "popup", oldPerm);
|
||||
} catch(e) {
|
||||
this._log.warn("Setting popup blocking to original value failed " + e);
|
||||
}
|
||||
|
||||
if (val) {
|
||||
self._log.info("_getAssertionWithLogin succeeded");
|
||||
try {
|
||||
cb(null, val);
|
||||
} catch(e) {
|
||||
self._log.warn("Callback threw in _getAssertionWithLogin " +
|
||||
CommonUtils.exceptionStr(e));
|
||||
}
|
||||
} else {
|
||||
let msg = "Could not obtain assertion in _getAssertionWithLogin";
|
||||
self._log.error(msg);
|
||||
try {
|
||||
cb(new Error(msg), null);
|
||||
} catch(e) {
|
||||
self._log.warn("Callback threw in _getAssertionWithLogin " +
|
||||
CommonUtils.exceptionStr(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
sandbox.importFunction(callback);
|
||||
|
||||
function doGetAssertion() {
|
||||
self._log.info("_getAssertionWithLogin Started");
|
||||
let scriptText = "window.navigator.id.get(" +
|
||||
" callback, {allowPersistent: true}" +
|
||||
");";
|
||||
Cu.evalInSandbox(scriptText, sandbox, "1.8", self.ID_URI, 1);
|
||||
}
|
||||
|
||||
// Sometimes the provided win hasn't fully loaded yet
|
||||
if (!win.document || (win.document.readyState != "complete")) {
|
||||
win.addEventListener("DOMContentLoaded", function _contentLoaded() {
|
||||
win.removeEventListener("DOMContentLoaded", _contentLoaded, false);
|
||||
doGetAssertion();
|
||||
}, false);
|
||||
} else {
|
||||
doGetAssertion();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets an assertion for the specified 'email' and 'audience'
|
||||
*/
|
||||
_getAssertionWithEmail: function _getAssertionWithEmail(sandbox, cb, email,
|
||||
audience) {
|
||||
let self = this;
|
||||
|
||||
function onSuccess(res) {
|
||||
// Cleanup first.
|
||||
sandbox.free();
|
||||
|
||||
// The internal API sometimes calls onSuccess even though no assertion
|
||||
// could be obtained! Double check:
|
||||
if (!res) {
|
||||
let msg = "BrowserID.User.getAssertion empty assertion for " + email;
|
||||
self._log.error(msg);
|
||||
try {
|
||||
cb(new Error(msg), null);
|
||||
} catch(e) {
|
||||
self._log.warn("Callback threw in _getAssertionWithEmail " +
|
||||
CommonUtils.exceptionStr(e));
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
// Success
|
||||
self._log.info("BrowserID.User.getAssertion succeeded");
|
||||
try {
|
||||
cb(null, res);
|
||||
} catch(e) {
|
||||
self._log.warn("Callback threw in _getAssertionWithEmail " +
|
||||
CommonUtils.exceptionStr(e));
|
||||
}
|
||||
}
|
||||
|
||||
function onError(err) {
|
||||
sandbox.free();
|
||||
|
||||
self._log.info("BrowserID.User.getAssertion failed");
|
||||
try {
|
||||
cb(err, null);
|
||||
} catch(e) {
|
||||
self._log.warn("Callback threw in _getAssertionWithEmail " +
|
||||
CommonUtils.exceptionStr(e));
|
||||
}
|
||||
}
|
||||
sandbox.box.importFunction(onSuccess);
|
||||
sandbox.box.importFunction(onError);
|
||||
|
||||
self._log.info("_getAssertionWithEmail Started");
|
||||
let scriptText =
|
||||
"window.BrowserID.User.getAssertion(" +
|
||||
"'" + email + "', " +
|
||||
"'" + audience + "', " +
|
||||
"onSuccess, " +
|
||||
"onError" +
|
||||
");";
|
||||
Cu.evalInSandbox(scriptText, sandbox.box, "1.8", this.ID_URI, 1);
|
||||
},
|
||||
|
||||
/**
|
||||
* Gets the email which was used to login to 'domain'. If one was found,
|
||||
* _getAssertionWithEmail is called to obtain the assertion.
|
||||
*/
|
||||
_getAssertionWithDomain: function _getAssertionWithDomain(sandbox, cb, domain,
|
||||
audience) {
|
||||
let self = this;
|
||||
|
||||
function onDomainSuccess(email) {
|
||||
if (email) {
|
||||
self._getAssertionWithEmail(sandbox, cb, email, audience);
|
||||
} else {
|
||||
sandbox.free();
|
||||
try {
|
||||
cb(new Error("No email found for _getAssertionWithDomain"), null);
|
||||
} catch(e) {
|
||||
self._log.warn("Callback threw in _getAssertionWithDomain " +
|
||||
CommonUtils.exceptionStr(e));
|
||||
}
|
||||
}
|
||||
}
|
||||
sandbox.box.importFunction(onDomainSuccess);
|
||||
|
||||
// This wil tell us which email was used to login to "domain", if any.
|
||||
self._log.info("_getAssertionWithDomain Started");
|
||||
let scriptText =
|
||||
"onDomainSuccess(window.BrowserID.Storage.site.get(" +
|
||||
"'" + domain + "', " +
|
||||
"'email'" +
|
||||
"));";
|
||||
Cu.evalInSandbox(scriptText, sandbox.box, "1.8", this.ID_URI, 1);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* An object that represents a sandbox in an iframe loaded with uri. The
|
||||
* callback provided to the constructor will be invoked when the sandbox is
|
||||
* ready to be used. The callback will receive this object as its only argument
|
||||
* and the prepared sandbox may be accessed via the "box" property.
|
||||
*
|
||||
* Please call free() when you are finished with the sandbox to explicitely free
|
||||
* up all associated resources.
|
||||
*
|
||||
* @param cb
|
||||
* (function) Callback to be invoked with a Sandbox, when ready.
|
||||
* @param uri
|
||||
* (String) URI to be loaded in the Sandbox.
|
||||
*/
|
||||
function Sandbox(cb, uri) {
|
||||
this._uri = uri;
|
||||
|
||||
// Put in a try/catch block because Services.wm.getMostRecentWindow, called in
|
||||
// _createFrame will be null in XPCShell.
|
||||
try {
|
||||
this._createFrame();
|
||||
this._createSandbox(cb, uri);
|
||||
} catch(e) {
|
||||
this._log = Log4Moz.repository.getLogger("Service.AITC.BrowserID.Sandbox");
|
||||
this._log.level = Log4Moz.Level[PREFS.get("log")];
|
||||
this._log.error("Could not create Sandbox " + e);
|
||||
cb(null);
|
||||
}
|
||||
}
|
||||
Sandbox.prototype = {
|
||||
/**
|
||||
* Frees the sandbox and releases the iframe created to host it.
|
||||
*/
|
||||
free: function free() {
|
||||
delete this.box;
|
||||
this._container.removeChild(this._frame);
|
||||
this._frame = null;
|
||||
this._container = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Creates an empty, hidden iframe and sets it to the _iframe
|
||||
* property of this object.
|
||||
*
|
||||
* @return frame
|
||||
* (iframe) An empty, hidden iframe
|
||||
*/
|
||||
_createFrame: function _createFrame() {
|
||||
let doc = Services.wm.getMostRecentWindow("navigator:browser").document;
|
||||
|
||||
// Insert iframe in to create docshell.
|
||||
let frame = doc.createElement("iframe");
|
||||
frame.setAttribute("type", "content");
|
||||
frame.setAttribute("collapsed", "true");
|
||||
doc.documentElement.appendChild(frame);
|
||||
|
||||
// Stop about:blank from being loaded.
|
||||
let webNav = frame.docShell.QueryInterface(Ci.nsIWebNavigation);
|
||||
webNav.stop(Ci.nsIWebNavigation.STOP_NETWORK);
|
||||
|
||||
// Set instance properties.
|
||||
this._frame = frame;
|
||||
this._container = doc.documentElement;
|
||||
},
|
||||
|
||||
_createSandbox: function _createSandbox(cb, uri) {
|
||||
let self = this;
|
||||
this._frame.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
function _makeSandboxContentLoaded(event) {
|
||||
if (event.target.location.toString() != uri) {
|
||||
return;
|
||||
}
|
||||
event.target.removeEventListener(
|
||||
"DOMContentLoaded", _makeSandboxContentLoaded, false
|
||||
);
|
||||
let workerWindow = self._frame.contentWindow;
|
||||
self.box = new Cu.Sandbox(workerWindow, {
|
||||
wantXrays: false,
|
||||
sandboxPrototype: workerWindow
|
||||
});
|
||||
cb(self);
|
||||
},
|
||||
true
|
||||
);
|
||||
|
||||
// Load the iframe.
|
||||
this._frame.docShell.loadURI(
|
||||
uri,
|
||||
this._frame.docShell.LOAD_FLAGS_NONE,
|
||||
null, // referrer
|
||||
null, // postData
|
||||
null // headers
|
||||
);
|
||||
},
|
||||
};
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "BrowserID", function() {
|
||||
return new BrowserIDService();
|
||||
});
|
@ -1,417 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["AitcClient"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Webapps.jsm");
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
Cu.import("resource://services-common/rest.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
|
||||
const DEFAULT_INITIAL_BACKOFF = 600000; // 10min
|
||||
const DEFAULT_MAX_BACKOFF = 21600000; // 6h
|
||||
const DEFAULT_REQUEST_FAILURE_THRESHOLD = 3;
|
||||
|
||||
/**
|
||||
* First argument is a token as returned by CommonUtils.TokenServerClient.
|
||||
* This is a convenience, so you don't have to call updateToken immediately
|
||||
* after making a new client (though you must when the token expires and you
|
||||
* intend to reuse the same client instance).
|
||||
*
|
||||
* Second argument is a key-value store object that exposes two methods:
|
||||
* set(key, value); Sets the value of a given key
|
||||
* get(key, default); Returns the value of key, if it doesn't exist,
|
||||
* return default
|
||||
* The values should be stored persistently. The Preferences object from
|
||||
* services-common/preferences.js is an example of such an object.
|
||||
*/
|
||||
this.AitcClient = function AitcClient(token, state) {
|
||||
this.updateToken(token);
|
||||
|
||||
this._log = Log4Moz.repository.getLogger("Service.AITC.Client");
|
||||
this._log.level = Log4Moz.Level[
|
||||
Preferences.get("services.aitc.client.log.level")
|
||||
];
|
||||
|
||||
this._state = state;
|
||||
this._backoff = !!state.get("backoff", false);
|
||||
this._backoffTime = 0;
|
||||
this._consecutiveFailures = 0;
|
||||
this._maxFailures = state.get("requestFailureThreshold",
|
||||
DEFAULT_REQUEST_FAILURE_THRESHOLD);
|
||||
|
||||
this._timeout = state.get("timeout", 120);
|
||||
this._appsLastModified = parseInt(state.get("lastModified", "0"), 10);
|
||||
this._log.info("Client initialized with token endpoint: " + this.uri);
|
||||
}
|
||||
AitcClient.prototype = {
|
||||
_requiredLocalKeys: [
|
||||
"origin", "receipts", "manifestURL", "installOrigin"
|
||||
],
|
||||
_requiredRemoteKeys: [
|
||||
"origin", "name", "receipts", "manifestPath", "installOrigin",
|
||||
"installedAt", "modifiedAt"
|
||||
],
|
||||
|
||||
/**
|
||||
* Updates the token the client must use to authenticate outgoing requests.
|
||||
*
|
||||
* @param token
|
||||
* (Object) A token as returned by CommonUtils.TokenServerClient.
|
||||
*/
|
||||
updateToken: function updateToken(token) {
|
||||
this.uri = token.endpoint.replace(/\/+$/, "");
|
||||
this.token = {id: token.id, key: token.key};
|
||||
},
|
||||
|
||||
/**
|
||||
* Initiates an update of a newly installed app to the AITC server. Call this
|
||||
* when an application is installed locally.
|
||||
*
|
||||
* @param app
|
||||
* (Object) The app record of the application that was just installed.
|
||||
*/
|
||||
remoteInstall: function remoteInstall(app, cb) {
|
||||
if (!cb) {
|
||||
throw new Error("remoteInstall called without callback");
|
||||
}
|
||||
|
||||
// Fetch the name of the app because it's not in the event app object.
|
||||
let self = this;
|
||||
DOMApplicationRegistry.getManifestFor(app.origin, function gotManifest(m) {
|
||||
if (m) {
|
||||
app.name = m.name;
|
||||
}
|
||||
self._putApp(self._makeRemoteApp(app), cb);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Initiates an update of an uinstalled app to the AITC server. Call this
|
||||
* when an application is uninstalled locally.
|
||||
*
|
||||
* @param app
|
||||
* (Object) The app record of the application that was uninstalled.
|
||||
*/
|
||||
remoteUninstall: function remoteUninstall(app, cb) {
|
||||
if (!cb) {
|
||||
throw new Error("remoteUninstall called without callback");
|
||||
}
|
||||
|
||||
app.name = "Uninstalled"; // Bug 760262
|
||||
let record = this._makeRemoteApp(app);
|
||||
record.hidden = true;
|
||||
this._putApp(record, cb);
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch remote apps from server with GET. The provided callback will receive
|
||||
* an array of app objects in the format expected by DOMApplicationRegistry,
|
||||
* if successful, or an Error; in the usual (err, result) way.
|
||||
*/
|
||||
getApps: function getApps(cb) {
|
||||
if (!cb) {
|
||||
throw new Error("getApps called but no callback provided");
|
||||
}
|
||||
|
||||
if (!this._isRequestAllowed()) {
|
||||
cb(null, null);
|
||||
return;
|
||||
}
|
||||
|
||||
let uri = this.uri + "/apps/?full=1";
|
||||
let req = new TokenAuthenticatedRESTRequest(uri, this.token);
|
||||
req.timeout = this._timeout;
|
||||
req.setHeader("Content-Type", "application/json");
|
||||
|
||||
if (this._appsLastModified) {
|
||||
req.setHeader("X-If-Modified-Since", this._appsLastModified);
|
||||
}
|
||||
|
||||
let self = this;
|
||||
req.get(function _getAppsCb(err) {
|
||||
self._processGetApps(err, cb, req);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* GET request returned from getApps, process.
|
||||
*/
|
||||
_processGetApps: function _processGetApps(err, cb, req) {
|
||||
// Set X-Backoff or Retry-After, if needed.
|
||||
this._setBackoff(req);
|
||||
|
||||
if (err) {
|
||||
this._log.error("getApps request error " + err);
|
||||
cb(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Bubble auth failure back up so new token can be acquired.
|
||||
if (req.response.status == 401) {
|
||||
let msg = new Error("getApps failed due to 401 authentication failure");
|
||||
this._log.info(msg);
|
||||
msg.authfailure = true;
|
||||
cb(msg, null);
|
||||
return;
|
||||
}
|
||||
// Process response.
|
||||
if (req.response.status == 304) {
|
||||
this._log.info("getApps returned 304");
|
||||
cb(null, null);
|
||||
return;
|
||||
}
|
||||
if (req.response.status != 200) {
|
||||
this._log.error(req);
|
||||
cb(new Error("Unexpected error with getApps"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
let apps;
|
||||
try {
|
||||
let tmp = JSON.parse(req.response.body);
|
||||
tmp = tmp["apps"];
|
||||
// Convert apps from remote to local format.
|
||||
apps = tmp.map(this._makeLocalApp, this);
|
||||
this._log.info("getApps succeeded and got " + apps.length);
|
||||
} catch (e) {
|
||||
this._log.error(CommonUtils.exceptionStr(e));
|
||||
cb(new Error("Exception in getApps " + e), null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Return success.
|
||||
try {
|
||||
cb(null, apps);
|
||||
// Don't update lastModified until we know cb succeeded.
|
||||
this._appsLastModified = parseInt(req.response.headers["x-timestamp"], 10);
|
||||
this._state.set("lastModified", "" + this._appsLastModified);
|
||||
} catch (e) {
|
||||
this._log.error("Exception in getApps callback " + e);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Change a given app record to match what the server expects.
|
||||
* Change manifestURL to manifestPath, and trim out manifests since we
|
||||
* don't store them on the server.
|
||||
*/
|
||||
_makeRemoteApp: function _makeRemoteApp(app) {
|
||||
for each (let key in this.requiredLocalKeys) {
|
||||
if (!(key in app)) {
|
||||
throw new Error("Local app missing key " + key);
|
||||
}
|
||||
}
|
||||
|
||||
let record = {
|
||||
name: app.name,
|
||||
origin: app.origin,
|
||||
receipts: app.receipts,
|
||||
manifestPath: app.manifestURL,
|
||||
installOrigin: app.installOrigin
|
||||
};
|
||||
if ("modifiedAt" in app) {
|
||||
record.modifiedAt = app.modifiedAt;
|
||||
}
|
||||
if ("installedAt" in app) {
|
||||
record.installedAt = app.installedAt;
|
||||
}
|
||||
return record;
|
||||
},
|
||||
|
||||
/**
|
||||
* Change a given app record received from the server to match what the local
|
||||
* registry expects. (Inverse of _makeRemoteApp)
|
||||
*/
|
||||
_makeLocalApp: function _makeLocalApp(app) {
|
||||
for each (let key in this._requiredRemoteKeys) {
|
||||
if (!(key in app)) {
|
||||
throw new Error("Remote app missing key " + key);
|
||||
}
|
||||
}
|
||||
|
||||
let record = {
|
||||
origin: app.origin,
|
||||
installOrigin: app.installOrigin,
|
||||
installedAt: app.installedAt,
|
||||
modifiedAt: app.modifiedAt,
|
||||
manifestURL: app.manifestPath,
|
||||
receipts: app.receipts
|
||||
};
|
||||
if ("hidden" in app) {
|
||||
record.hidden = app.hidden;
|
||||
}
|
||||
return record;
|
||||
},
|
||||
|
||||
/**
|
||||
* Try PUT for an app on the server and determine if we should retry
|
||||
* if it fails.
|
||||
*/
|
||||
_putApp: function _putApp(app, cb) {
|
||||
if (!this._isRequestAllowed()) {
|
||||
// PUT requests may qualify as the "minimum number of additional requests
|
||||
// required to maintain consistency of their stored data". However, it's
|
||||
// better to keep server load low, even if it means user's apps won't
|
||||
// reach their other devices during the early days of AITC. We should
|
||||
// revisit this when we have a better of idea of server load curves.
|
||||
let err = new Error("Backoff in effect, aborting PUT");
|
||||
err.processed = false;
|
||||
cb(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
let uri = this._makeAppURI(app.origin);
|
||||
let req = new TokenAuthenticatedRESTRequest(uri, this.token);
|
||||
req.timeout = this._timeout;
|
||||
req.setHeader("Content-Type", "application/json");
|
||||
|
||||
if (app.modifiedAt) {
|
||||
req.setHeader("X-If-Unmodified-Since", "" + app.modified);
|
||||
}
|
||||
|
||||
let self = this;
|
||||
this._log.info("Trying to _putApp to " + uri);
|
||||
req.put(JSON.stringify(app), function _putAppCb(err) {
|
||||
self._processPutApp(err, cb, req);
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* PUT from _putApp finished, process.
|
||||
*/
|
||||
_processPutApp: function _processPutApp(error, cb, req) {
|
||||
this._setBackoff(req);
|
||||
|
||||
if (error) {
|
||||
this._log.error("_putApp request error " + error);
|
||||
cb(error, null);
|
||||
return;
|
||||
}
|
||||
|
||||
let err = null;
|
||||
switch (req.response.status) {
|
||||
case 201:
|
||||
case 204:
|
||||
this._log.info("_putApp succeeded");
|
||||
cb(null, true);
|
||||
break;
|
||||
|
||||
case 401:
|
||||
// Bubble auth failure back up so new token can be acquired.
|
||||
err = new Error("_putApp failed due to 401 authentication failure");
|
||||
this._log.warn(err);
|
||||
err.authfailure = true;
|
||||
cb(err, null);
|
||||
break;
|
||||
|
||||
case 409:
|
||||
// Retry on server conflict.
|
||||
err = new Error("_putApp failed due to 409 conflict");
|
||||
this._log.warn(err);
|
||||
cb(err,null);
|
||||
break;
|
||||
|
||||
case 400:
|
||||
case 412:
|
||||
case 413:
|
||||
let msg = "_putApp returned: " + req.response.status;
|
||||
this._log.warn(msg);
|
||||
err = new Error(msg);
|
||||
err.processed = true;
|
||||
cb(err, null);
|
||||
break;
|
||||
|
||||
default:
|
||||
this._error(req);
|
||||
err = new Error("Unexpected error with _putApp");
|
||||
err.processed = false;
|
||||
cb(err, null);
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Utility methods.
|
||||
*/
|
||||
_error: function _error(req) {
|
||||
this._log.error("Catch-all error for request: " +
|
||||
req.uri.asciiSpec + " " + req.response.status + " with: " +
|
||||
req.response.body);
|
||||
},
|
||||
|
||||
_makeAppURI: function _makeAppURI(origin) {
|
||||
let part = CommonUtils.encodeBase64URL(
|
||||
CryptoUtils.UTF8AndSHA1(origin)
|
||||
).replace("=", "");
|
||||
return this.uri + "/apps/" + part;
|
||||
},
|
||||
|
||||
// Before making a request, check if we are allowed to.
|
||||
_isRequestAllowed: function _isRequestAllowed() {
|
||||
if (!this._backoff) {
|
||||
return true;
|
||||
}
|
||||
|
||||
let time = Date.now();
|
||||
let backoff = parseInt(this._state.get("backoff", 0), 10);
|
||||
|
||||
if (time < backoff) {
|
||||
this._log.warn(backoff - time + "ms left for backoff, aborting request");
|
||||
return false;
|
||||
}
|
||||
|
||||
this._backoff = false;
|
||||
this._state.set("backoff", "0");
|
||||
return true;
|
||||
},
|
||||
|
||||
// Set values from X-Backoff and Retry-After headers, if present.
|
||||
_setBackoff: function _setBackoff(req) {
|
||||
let backoff = 0;
|
||||
let statusCodesWithoutBackoff = [200, 201, 204, 304, 401];
|
||||
|
||||
let val;
|
||||
if (req.response.headers["Retry-After"]) {
|
||||
val = req.response.headers["Retry-After"];
|
||||
backoff = parseInt(val, 10);
|
||||
this._log.warn("Retry-Header header was seen: " + val);
|
||||
} else if (req.response.headers["X-Backoff"]) {
|
||||
val = req.response.headers["X-Backoff"];
|
||||
backoff = parseInt(val, 10);
|
||||
this._log.warn("X-Backoff header was seen: " + val);
|
||||
} else if (statusCodesWithoutBackoff.indexOf(req.response.status) === -1) {
|
||||
// Bad status code.
|
||||
this._consecutiveFailures++;
|
||||
if (this._consecutiveFailures === this._maxFailures) {
|
||||
// Initialize the backoff.
|
||||
backoff = this._state.get("backoff.initial", DEFAULT_INITIAL_BACKOFF);
|
||||
} else if (this._consecutiveFailures > this._maxFailures) {
|
||||
// Set the backoff to the smallest of either:
|
||||
backoff = Math.min(
|
||||
// double the current backoff
|
||||
this._backoffTime * 2,
|
||||
// or the max backoff.
|
||||
this._state.get("backoff.max", DEFAULT_MAX_BACKOFF)
|
||||
);
|
||||
}
|
||||
} else {
|
||||
// Good Status Code.
|
||||
this._consecutiveFailures = 0;
|
||||
}
|
||||
if (backoff) {
|
||||
this._backoff = true;
|
||||
let time = Date.now();
|
||||
this._state.set("backoff", "" + (time + backoff));
|
||||
this._backoffTime = backoff;
|
||||
this._log.info("Client setting backoff to: " + backoff + "ms");
|
||||
}
|
||||
},
|
||||
};
|
@ -1,174 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["Aitc"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Webapps.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
Cu.import("resource://services-aitc/manager.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
|
||||
this.Aitc = function Aitc() {
|
||||
this._log = Log4Moz.repository.getLogger("Service.AITC");
|
||||
this._log.level = Log4Moz.Level[Preferences.get(
|
||||
"services.aitc.service.log.level"
|
||||
)];
|
||||
this._log.info("Loading AitC");
|
||||
|
||||
this.DASHBOARD_ORIGIN = CommonUtils.makeURI(
|
||||
Preferences.get("services.aitc.dashboard.url")
|
||||
).prePath;
|
||||
|
||||
let self = this;
|
||||
this._manager = new AitcManager(function managerDone() {
|
||||
CommonUtils.nextTick(self._init, self);
|
||||
});
|
||||
}
|
||||
Aitc.prototype = {
|
||||
// The goal of the init function is to be ready to activate the AITC
|
||||
// client whenever the user is looking at the dashboard. It also calls
|
||||
// the initialSchedule function on the manager.
|
||||
_init: function _init() {
|
||||
let self = this;
|
||||
|
||||
// Do an initial upload.
|
||||
this._manager.initialSchedule(function queueDone(num) {
|
||||
if (num == -1) {
|
||||
self._log.debug("No initial upload was required");
|
||||
return;
|
||||
}
|
||||
self._log.debug(num + " initial apps queued successfully");
|
||||
});
|
||||
|
||||
// This is called iff the user is currently looking the dashboard.
|
||||
function dashboardLoaded(browser) {
|
||||
let win = browser.contentWindow;
|
||||
self._log.info("Dashboard was accessed " + win);
|
||||
|
||||
// If page is ready to go, fire immediately.
|
||||
if (win.document && win.document.readyState == "complete") {
|
||||
self._manager.userActive(win);
|
||||
return;
|
||||
}
|
||||
|
||||
// Only fire event after the page fully loads.
|
||||
browser.contentWindow.addEventListener(
|
||||
"DOMContentLoaded",
|
||||
function _contentLoaded(event) {
|
||||
self._manager.userActive(win);
|
||||
},
|
||||
false
|
||||
);
|
||||
}
|
||||
|
||||
// This is called when the user's attention is elsewhere.
|
||||
function dashboardUnloaded() {
|
||||
self._log.info("Dashboard closed or in background");
|
||||
self._manager.userIdle();
|
||||
}
|
||||
|
||||
// Called when a URI is loaded in any tab. We have to listen for this
|
||||
// because tabSelected is not called if I open a new tab which loads
|
||||
// about:home and then navigate to the dashboard, or navigation via
|
||||
// links on the currently open tab.
|
||||
let listener = {
|
||||
onLocationChange: function onLocationChange(browser, pr, req, loc, flag) {
|
||||
let win = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
if (win.gBrowser.selectedBrowser == browser) {
|
||||
if (loc.prePath == self.DASHBOARD_ORIGIN) {
|
||||
dashboardLoaded(browser);
|
||||
}
|
||||
}
|
||||
}
|
||||
};
|
||||
// Called when the current tab selection changes.
|
||||
function tabSelected(event) {
|
||||
let browser = event.target.linkedBrowser;
|
||||
if (browser.currentURI.prePath == self.DASHBOARD_ORIGIN) {
|
||||
dashboardLoaded(browser);
|
||||
} else {
|
||||
dashboardUnloaded();
|
||||
}
|
||||
}
|
||||
|
||||
// Add listeners for all windows opened in the future.
|
||||
function winWatcher(subject, topic) {
|
||||
if (topic != "domwindowopened") {
|
||||
return;
|
||||
}
|
||||
subject.addEventListener("load", function winWatcherLoad() {
|
||||
subject.removeEventListener("load", winWatcherLoad, false);
|
||||
let doc = subject.document.documentElement;
|
||||
if (doc.getAttribute("windowtype") == "navigator:browser") {
|
||||
let browser = subject.gBrowser;
|
||||
browser.addTabsProgressListener(listener);
|
||||
browser.tabContainer.addEventListener("TabSelect", tabSelected);
|
||||
}
|
||||
}, false);
|
||||
}
|
||||
Services.ww.registerNotification(winWatcher);
|
||||
|
||||
// Add listeners for all current open windows.
|
||||
let enumerator = Services.wm.getEnumerator("navigator:browser");
|
||||
while (enumerator.hasMoreElements()) {
|
||||
let browser = enumerator.getNext().gBrowser;
|
||||
browser.addTabsProgressListener(listener);
|
||||
browser.tabContainer.addEventListener("TabSelect", tabSelected);
|
||||
|
||||
// Also check the currently open URI.
|
||||
if (browser.currentURI.prePath == this.DASHBOARD_ORIGIN) {
|
||||
dashboardLoaded(browser);
|
||||
}
|
||||
}
|
||||
|
||||
// Add listeners for app installs/uninstall.
|
||||
Services.obs.addObserver(this, "webapps-sync-install", false);
|
||||
Services.obs.addObserver(this, "webapps-sync-uninstall", false);
|
||||
|
||||
// Add listener for idle service.
|
||||
let idleSvc = Cc["@mozilla.org/widget/idleservice;1"].
|
||||
getService(Ci.nsIIdleService);
|
||||
idleSvc.addIdleObserver(this,
|
||||
Preferences.get("services.aitc.main.idleTime"));
|
||||
},
|
||||
|
||||
observe: function(subject, topic, data) {
|
||||
let app;
|
||||
switch (topic) {
|
||||
case "webapps-sync-install":
|
||||
app = JSON.parse(data);
|
||||
this._log.info(app.origin + " was installed, initiating PUT");
|
||||
this._manager.appEvent("install", app);
|
||||
break;
|
||||
case "webapps-sync-uninstall":
|
||||
app = JSON.parse(data);
|
||||
this._log.info(app.origin + " was uninstalled, initiating PUT");
|
||||
this._manager.appEvent("uninstall", app);
|
||||
break;
|
||||
case "idle":
|
||||
this._log.info("User went idle");
|
||||
if (this._manager) {
|
||||
this._manager.userIdle();
|
||||
}
|
||||
break;
|
||||
case "back":
|
||||
this._log.info("User is no longer idle");
|
||||
let win = Services.wm.getMostRecentWindow("navigator:browser");
|
||||
if (win && win.gBrowser.currentURI.prePath == this.DASHBOARD_ORIGIN &&
|
||||
this._manager) {
|
||||
this._manager.userActive();
|
||||
}
|
||||
break;
|
||||
}
|
||||
},
|
||||
|
||||
};
|
@ -1,694 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["AitcManager"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/Webapps.jsm");
|
||||
Cu.import("resource://gre/modules/Services.jsm");
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
|
||||
Cu.import("resource://services-aitc/client.js");
|
||||
Cu.import("resource://services-aitc/browserid.js");
|
||||
Cu.import("resource://services-aitc/storage.js");
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
Cu.import("resource://services-common/tokenserverclient.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
const PREFS = new Preferences("services.aitc.");
|
||||
const INITIAL_TOKEN_DURATION = 240000; // 4 minutes
|
||||
const DASHBOARD_URL = PREFS.get("dashboard.url");
|
||||
const MARKETPLACE_URL = PREFS.get("marketplace.url");
|
||||
|
||||
/**
|
||||
* The constructor for the manager takes a callback, which will be invoked when
|
||||
* the manager is ready (construction is asynchronous). *DO NOT* call any
|
||||
* methods on this object until the callback has been invoked, doing so will
|
||||
* lead to undefined behaviour. The premadeClient and premadeToken are used
|
||||
* to bypass BrowserID for xpcshell tests, since the window object in not
|
||||
* available.
|
||||
*/
|
||||
this.AitcManager = function AitcManager(cb, premadeClient, premadeToken) {
|
||||
this._client = null;
|
||||
this._getTimer = null;
|
||||
this._putTimer = null;
|
||||
|
||||
this._lastTokenTime = 0;
|
||||
this._tokenDuration = INITIAL_TOKEN_DURATION;
|
||||
this._premadeToken = premadeToken || null;
|
||||
this._invalidTokenFlag = false;
|
||||
|
||||
this._lastEmail = null;
|
||||
this._dashboardWindow = null;
|
||||
|
||||
this._log = Log4Moz.repository.getLogger("Service.AITC.Manager");
|
||||
this._log.level = Log4Moz.Level[Preferences.get("manager.log.level")];
|
||||
this._log.info("Loading AitC manager module");
|
||||
|
||||
// Check if we have pending PUTs from last time.
|
||||
let self = this;
|
||||
this._pending = new AitcQueue("webapps-pending.json", function _queueDone() {
|
||||
// Inform the AitC service that we're good to go!
|
||||
self._log.info("AitC manager has finished loading");
|
||||
try {
|
||||
cb(true);
|
||||
} catch (e) {
|
||||
self._log.error(new Error("AitC manager callback threw " + e));
|
||||
}
|
||||
|
||||
// Used for testing.
|
||||
if (premadeClient) {
|
||||
self._client = premadeClient;
|
||||
cb(null, true);
|
||||
return;
|
||||
}
|
||||
|
||||
// Caller will invoke initialSchedule which will process any items in the
|
||||
// queue, if present.
|
||||
});
|
||||
}
|
||||
AitcManager.prototype = {
|
||||
/**
|
||||
* State of the user. ACTIVE implies user is looking at the dashboard,
|
||||
* PASSIVE means either not at the dashboard or the idle timer started.
|
||||
*/
|
||||
_ACTIVE: 1,
|
||||
_PASSIVE: 2,
|
||||
|
||||
/**
|
||||
* Smart setter that will only call _setPoll is the value changes.
|
||||
*/
|
||||
_clientState: null,
|
||||
get _state() {
|
||||
return this._clientState;
|
||||
},
|
||||
set _state(value) {
|
||||
if (this._clientState == value) {
|
||||
return;
|
||||
}
|
||||
this._clientState = value;
|
||||
this._setPoll();
|
||||
},
|
||||
|
||||
/**
|
||||
* Local app was just installed or uninstalled, ask client to PUT if user
|
||||
* is logged in.
|
||||
*/
|
||||
appEvent: function appEvent(type, app) {
|
||||
// Add this to the equeue.
|
||||
let self = this;
|
||||
let obj = {type: type, app: app, retries: 0, lastTime: 0};
|
||||
this._pending.enqueue(obj, function _enqueued(err, rec) {
|
||||
if (err) {
|
||||
self._log.error("Could not add " + type + " " + app + " to queue");
|
||||
return;
|
||||
}
|
||||
|
||||
// If we already have a client (i.e. user is logged in), attempt to PUT.
|
||||
if (self._client) {
|
||||
self._processQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
// If not, try a silent client creation.
|
||||
self._makeClient(function(err, client) {
|
||||
if (!err && client) {
|
||||
self._client = client;
|
||||
self._processQueue();
|
||||
}
|
||||
// If user is not logged in, we'll just have to try later.
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* User is looking at dashboard. Start polling actively, but if user isn't
|
||||
* logged in, prompt for them to login via a dialog.
|
||||
*/
|
||||
userActive: function userActive(win) {
|
||||
// Stash a reference to the dashboard window in case we need to prompt
|
||||
this._dashboardWindow = win;
|
||||
|
||||
if (this._client) {
|
||||
this._state = this._ACTIVE;
|
||||
return;
|
||||
}
|
||||
|
||||
// Make client will first try silent login, if it doesn't work, a popup
|
||||
// will be shown in the context of the dashboard. We shouldn't be
|
||||
// trying to make a client every time this function is called, there is
|
||||
// room for optimization (Bug 750607).
|
||||
let self = this;
|
||||
this._makeClient(function(err, client) {
|
||||
if (err) {
|
||||
// Notify user of error (Bug 750610).
|
||||
self._log.error("Client not created at Dashboard");
|
||||
return;
|
||||
}
|
||||
self._client = client;
|
||||
self._state = self._ACTIVE;
|
||||
}, true, win);
|
||||
},
|
||||
|
||||
/**
|
||||
* User is idle, (either by idle observer, or by not being on the dashboard).
|
||||
* When the user is no longer idle and the dashboard is the current active
|
||||
* page, a call to userActive MUST be made.
|
||||
*/
|
||||
userIdle: function userIdle() {
|
||||
this._state = this._PASSIVE;
|
||||
this._dashboardWindow = null;
|
||||
},
|
||||
|
||||
/**
|
||||
* Initial schedule for the manager. It is the responsibility of the
|
||||
* caller who created this object to call this function if it wants to
|
||||
* do an initial sync (i.e. upload local apps on a device that has never
|
||||
* communicated with AITC before).
|
||||
*
|
||||
* The callback will be invoked with the number of local apps that were
|
||||
* queued to be uploaded, or -1 if this client has already synced and a
|
||||
* local upload is not required.
|
||||
*
|
||||
* Try to schedule PUTs but only if we can get a silent assertion, and if
|
||||
* the queue in non-empty, or we've never done a GET (first run).
|
||||
*/
|
||||
initialSchedule: function initialSchedule(cb) {
|
||||
let self = this;
|
||||
|
||||
function startProcessQueue(num) {
|
||||
self._makeClient(function(err, client) {
|
||||
if (!err && client) {
|
||||
self._client = client;
|
||||
self._processQueue();
|
||||
return;
|
||||
}
|
||||
});
|
||||
cb(num);
|
||||
}
|
||||
|
||||
// If we've already done a sync with AITC, it means we've already done
|
||||
// an initial upload. Resume processing the queue, if there are items in it.
|
||||
if (Preferences.get("services.aitc.client.lastModified", "0") != "0") {
|
||||
if (this._pending.length) {
|
||||
startProcessQueue(-1);
|
||||
} else {
|
||||
cb(-1);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
DOMApplicationRegistry.getAllWithoutManifests(function gotAllApps(apps) {
|
||||
let done = 0;
|
||||
let appids = Object.keys(apps);
|
||||
let total = appids.length;
|
||||
self._log.info("First run, queuing all local apps: " + total + " found");
|
||||
|
||||
function appQueued(err) {
|
||||
if (err) {
|
||||
self._log.error("Error queuing app " + apps[appids[done]].origin);
|
||||
}
|
||||
|
||||
if (done == total) {
|
||||
self._log.info("Finished queuing all initial local apps");
|
||||
startProcessQueue(total);
|
||||
return;
|
||||
}
|
||||
|
||||
let app = apps[appids[done]];
|
||||
let obj = {type: "install", app: app, retries: 0, lastTime: 0};
|
||||
|
||||
done += 1;
|
||||
self._pending.enqueue(obj, appQueued);
|
||||
}
|
||||
appQueued();
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Poll the AITC server for any changes and process them. It is safe to call
|
||||
* this function multiple times. Last caller wins. The function will
|
||||
* grab the current user state from _state and act accordingly.
|
||||
*
|
||||
* Invalid states will cause this function to throw.
|
||||
*/
|
||||
_setPoll: function _setPoll() {
|
||||
if (this._state == this._ACTIVE && !this._client) {
|
||||
throw new Error("_setPoll(ACTIVE) called without client");
|
||||
}
|
||||
if (this._state != this._ACTIVE && this._state != this._PASSIVE) {
|
||||
throw new Error("_state is invalid " + this._state);
|
||||
}
|
||||
|
||||
if (!this._client) {
|
||||
// User is not logged in, we cannot do anything.
|
||||
self._log.warn("_setPoll called but user not logged in, ignoring");
|
||||
return;
|
||||
}
|
||||
|
||||
// Check if there are any PUTs pending first.
|
||||
if (this._pending.length && !(this._putTimer)) {
|
||||
// There are pending PUTs and no timer, so let's process them. GETs will
|
||||
// resume after the PUTs finish (see processQueue)
|
||||
this._processQueue();
|
||||
return;
|
||||
}
|
||||
|
||||
// Do one GET soon, but only if user is active.
|
||||
let getFreq;
|
||||
if (this._state == this._ACTIVE) {
|
||||
CommonUtils.nextTick(this._checkServer, this);
|
||||
getFreq = PREFS.get("manager.getActiveFreq");
|
||||
} else {
|
||||
getFreq = PREFS.get("manager.getPassiveFreq");
|
||||
}
|
||||
|
||||
// Cancel existing timer, if any.
|
||||
if (this._getTimer) {
|
||||
this._getTimer.cancel();
|
||||
this._getTimer = null;
|
||||
}
|
||||
|
||||
// Start the timer for GETs.
|
||||
let self = this;
|
||||
this._log.info("Starting GET timer");
|
||||
this._getTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
|
||||
this._getTimer.initWithCallback({notify: this._checkServer.bind(this)},
|
||||
getFreq, Ci.nsITimer.TYPE_REPEATING_SLACK);
|
||||
|
||||
this._log.info("GET timer set, next attempt in " + getFreq + "ms");
|
||||
},
|
||||
|
||||
/**
|
||||
* Checks if the current token we hold is valid. If not, we obtain a new one
|
||||
* and execute the provided func. If a token could not be obtained, func will
|
||||
* not be called and an error will be logged.
|
||||
*/
|
||||
_validateToken: function _validateToken(func) {
|
||||
let timeSinceLastToken = Date.now() - this._lastTokenTime;
|
||||
if (!this._invalidTokenFlag && timeSinceLastToken < this._tokenDuration) {
|
||||
this._log.info("Current token is valid");
|
||||
func();
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.info("Current token is invalid");
|
||||
let win;
|
||||
if (this._state == this.ACTIVE) {
|
||||
win = this._dashboardWindow;
|
||||
}
|
||||
|
||||
let self = this;
|
||||
this._refreshToken(function(err, done) {
|
||||
if (!done) {
|
||||
self._log.warn("_checkServer could not refresh token, aborting");
|
||||
return;
|
||||
}
|
||||
func(err);
|
||||
}, win);
|
||||
},
|
||||
|
||||
/**
|
||||
* Do a GET check on the server to see if we have any new apps. Abort if
|
||||
* there are pending PUTs. If we GET some apps, send to storage for
|
||||
* further processing.
|
||||
*/
|
||||
_checkServer: function _checkServer() {
|
||||
if (!this._client) {
|
||||
throw new Error("_checkServer called without a client");
|
||||
}
|
||||
|
||||
if (this._pending.length) {
|
||||
this._log.warn("_checkServer aborted because of pending PUTs");
|
||||
return;
|
||||
}
|
||||
|
||||
let self = this;
|
||||
this._validateToken(function validation(err) {
|
||||
if (err) {
|
||||
self._log.error(err);
|
||||
} else {
|
||||
self._getApps();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_getApps: function _getApps() {
|
||||
// Do a GET
|
||||
this._log.info("Attempting to getApps");
|
||||
|
||||
let self = this;
|
||||
this._client.getApps(function gotApps(err, apps) {
|
||||
if (err) {
|
||||
// Error was logged in client.
|
||||
if (err.authfailure) {
|
||||
self._invalidTokenFlag = true;
|
||||
self._validateToken(function revalidated(err) {
|
||||
if (!err) {
|
||||
self._getApps();
|
||||
}
|
||||
});
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
if (!apps) {
|
||||
// No changes, got 304.
|
||||
return;
|
||||
}
|
||||
if (!apps.length) {
|
||||
// Empty array, nothing to process
|
||||
self._log.info("No apps found on remote server");
|
||||
return;
|
||||
}
|
||||
|
||||
// Send list of remote apps to storage to apply locally
|
||||
AitcStorage.processApps(apps, function processedApps() {
|
||||
self._log.info("processApps completed successfully, changes applied");
|
||||
});
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* Go through list of apps to PUT and attempt each one. If we fail, try
|
||||
* again in PUT_FREQ. Will throw if called with an empty, _reschedule()
|
||||
* makes sure that we don't.
|
||||
*/
|
||||
_processQueue: function _processQueue() {
|
||||
if (!this._client) {
|
||||
throw new Error("_processQueue called without a client");
|
||||
}
|
||||
if (!this._pending.length) {
|
||||
throw new Error("_processQueue called with an empty queue");
|
||||
}
|
||||
|
||||
if (this._putInProgress) {
|
||||
// The network request sent out as a result to the last call to
|
||||
// _processQueue still isn't done. A timer is created they all
|
||||
// finish to make sure this function is called again if neccessary.
|
||||
return;
|
||||
}
|
||||
|
||||
let self = this;
|
||||
this._validateToken(function validation(err) {
|
||||
if (err) {
|
||||
self._log.error(err);
|
||||
} else {
|
||||
self._putApps();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
_putApps: function _putApps() {
|
||||
this._putInProgress = true;
|
||||
let record = this._pending.peek();
|
||||
|
||||
this._log.info("Processing record type " + record.type);
|
||||
|
||||
let self = this;
|
||||
function _clientCallback(err, done) {
|
||||
// Send to end of queue if unsuccessful or err.removeFromQueue is false.
|
||||
if (err && !err.removeFromQueue) {
|
||||
self._log.info("PUT failed, re-adding to queue");
|
||||
// Error was logged in client.
|
||||
if (err.authfailure) {
|
||||
self._invalidTokenFlag = true;
|
||||
self._validateToken(function validation(err) {
|
||||
if (err) {
|
||||
self._log.error("Failed to obtain an updated token");
|
||||
}
|
||||
_reschedule();
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Update retries and time
|
||||
record.retries += 1;
|
||||
record.lastTime = new Date().getTime();
|
||||
|
||||
// Add updated record to the end of the queue.
|
||||
self._pending.enqueue(record, function(err, done) {
|
||||
if (err) {
|
||||
self._log.error("Enqueue failed " + err);
|
||||
_reschedule();
|
||||
return;
|
||||
}
|
||||
// If record was successfully added, remove old record.
|
||||
self._pending.dequeue(function(err, done) {
|
||||
if (err) {
|
||||
self._log.error("Dequeue failed " + err);
|
||||
}
|
||||
_reschedule();
|
||||
return;
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// If succeeded or client told us to remove from queue
|
||||
self._log.info("_putApp asked us to remove it from queue");
|
||||
self._pending.dequeue(function(err, done) {
|
||||
if (err) {
|
||||
self._log.error("Dequeue failed " + e);
|
||||
}
|
||||
_reschedule();
|
||||
});
|
||||
}
|
||||
|
||||
function _reschedule() {
|
||||
// Release PUT lock
|
||||
self._putInProgress = false;
|
||||
|
||||
// We just finished PUTting an object, try the next one immediately,
|
||||
// but only if haven't tried it already in the last putFreq (ms).
|
||||
if (!self._pending.length) {
|
||||
// Start GET timer now that we're done with PUTs.
|
||||
self._setPoll();
|
||||
return;
|
||||
}
|
||||
|
||||
let obj = self._pending.peek();
|
||||
let cTime = new Date().getTime();
|
||||
let freq = PREFS.get("manager.putFreq");
|
||||
|
||||
// We tried this object recently, we'll come back to it later.
|
||||
if (obj.lastTime && ((cTime - obj.lastTime) < freq)) {
|
||||
self._log.info("Scheduling next processQueue in " + freq);
|
||||
CommonUtils.namedTimer(self._processQueue, freq, self, "_putTimer");
|
||||
return;
|
||||
}
|
||||
|
||||
// Haven't tried this PUT yet, do it immediately.
|
||||
self._log.info("Queue non-empty, processing next PUT");
|
||||
self._processQueue();
|
||||
}
|
||||
|
||||
switch (record.type) {
|
||||
case "install":
|
||||
this._client.remoteInstall(record.app, _clientCallback);
|
||||
break;
|
||||
case "uninstall":
|
||||
record.app.hidden = true;
|
||||
this._client.remoteUninstall(record.app, _clientCallback);
|
||||
break;
|
||||
default:
|
||||
this._log.warn(
|
||||
"Unrecognized type " + record.type + " in queue, removing"
|
||||
);
|
||||
let self = this;
|
||||
this._pending.dequeue(function _dequeued(err) {
|
||||
if (err) {
|
||||
self._log.error("Dequeue of unrecognized app type failed");
|
||||
}
|
||||
_reschedule();
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/* Obtain a (new) token from the Sagrada token server. If win is is specified,
|
||||
* the user will be asked to login via UI, if required. The callback's
|
||||
* signature is cb(err, done). If a token is obtained successfully, done will
|
||||
* be true and err will be null.
|
||||
*/
|
||||
_refreshToken: function _refreshToken(cb, win) {
|
||||
if (!this._client) {
|
||||
throw new Error("_refreshToken called without an active client");
|
||||
}
|
||||
|
||||
this._log.info("Token refresh requested");
|
||||
|
||||
let self = this;
|
||||
function refreshedAssertion(err, assertion) {
|
||||
if (!err) {
|
||||
self._getToken(assertion, function(err, token) {
|
||||
if (err) {
|
||||
cb(err, null);
|
||||
return;
|
||||
}
|
||||
self._lastTokenTime = Date.now();
|
||||
self._client.updateToken(token);
|
||||
self._invalidTokenFlag = false;
|
||||
cb(null, true);
|
||||
});
|
||||
return;
|
||||
}
|
||||
|
||||
// Silent refresh was asked for.
|
||||
if (!win) {
|
||||
cb(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Prompt user to login.
|
||||
self._makeClient(function(err, client) {
|
||||
if (err) {
|
||||
cb(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// makeClient sets an updated token.
|
||||
self._client = client;
|
||||
self._invalidTokenFlag = false;
|
||||
cb(null, true);
|
||||
}, win);
|
||||
}
|
||||
|
||||
let options = { audience: DASHBOARD_URL };
|
||||
if (this._lastEmail) {
|
||||
options.requiredEmail = this._lastEmail;
|
||||
} else {
|
||||
options.sameEmailAs = MARKETPLACE_URL;
|
||||
}
|
||||
if (this._premadeToken) {
|
||||
this._client.updateToken(this._premadeToken);
|
||||
this._tokenDuration = parseInt(this._premadeToken.duration, 10);
|
||||
this._lastTokenTime = Date.now();
|
||||
this._invalidTokenFlag = false;
|
||||
cb(null, true);
|
||||
} else {
|
||||
BrowserID.getAssertion(refreshedAssertion, options);
|
||||
}
|
||||
},
|
||||
|
||||
/* Obtain a token from Sagrada token server, given a BrowserID assertion
|
||||
* cb(err, token) will be invoked on success or failure.
|
||||
*/
|
||||
_getToken: function _getToken(assertion, cb) {
|
||||
let url = PREFS.get("tokenServer.url") + "/1.0/aitc/1.0";
|
||||
let client = new TokenServerClient();
|
||||
|
||||
this._log.info("Obtaining token from " + url);
|
||||
|
||||
let self = this;
|
||||
try {
|
||||
client.getTokenFromBrowserIDAssertion(url, assertion, function(err, tok) {
|
||||
self._gotToken(err, tok, cb);
|
||||
});
|
||||
} catch (e) {
|
||||
cb(new Error(e), null);
|
||||
}
|
||||
},
|
||||
|
||||
// Token recieved from _getToken.
|
||||
_gotToken: function _gotToken(err, tok, cb) {
|
||||
if (!err) {
|
||||
this._log.info("Got token from server: " + JSON.stringify(tok));
|
||||
this._tokenDuration = parseInt(tok.duration, 10);
|
||||
cb(null, tok);
|
||||
return;
|
||||
}
|
||||
|
||||
let msg = "Error in _getToken: " + err;
|
||||
this._log.error(msg);
|
||||
cb(msg, null);
|
||||
},
|
||||
|
||||
// Extract the email address from a BrowserID assertion.
|
||||
_extractEmail: function _extractEmail(assertion) {
|
||||
// Please look the other way while I do this. Thanks.
|
||||
let chain = assertion.split("~");
|
||||
let len = chain.length;
|
||||
if (len < 2) {
|
||||
return null;
|
||||
}
|
||||
|
||||
try {
|
||||
// We need CommonUtils.decodeBase64URL.
|
||||
let cert = JSON.parse(atob(
|
||||
chain[0].split(".")[1].replace("-", "+", "g").replace("_", "/", "g")
|
||||
));
|
||||
return cert.principal.email;
|
||||
} catch (e) {
|
||||
return null;
|
||||
}
|
||||
},
|
||||
|
||||
/* To start the AitcClient we need a token, for which we need a BrowserID
|
||||
* assertion. If login is true, makeClient will ask the user to login in
|
||||
* the context of win. cb is called with (err, client).
|
||||
*/
|
||||
_makeClient: function makeClient(cb, login, win) {
|
||||
if (!cb) {
|
||||
throw new Error("makeClient called without callback");
|
||||
}
|
||||
if (login && !win) {
|
||||
throw new Error("makeClient called with login as true but no win");
|
||||
}
|
||||
|
||||
let self = this;
|
||||
let ctxWin = win;
|
||||
function processAssertion(val) {
|
||||
// Store the email we got the token for so we can refresh.
|
||||
self._lastEmail = self._extractEmail(val);
|
||||
self._log.info("Got assertion from BrowserID, creating token");
|
||||
self._getToken(val, function(err, token) {
|
||||
if (err) {
|
||||
cb(err, null);
|
||||
return;
|
||||
}
|
||||
|
||||
// Store when we got the token so we can refresh it as needed.
|
||||
self._lastTokenTime = Date.now();
|
||||
|
||||
// We only create one client instance, store values in a pref tree
|
||||
cb(null, new AitcClient(
|
||||
token, new Preferences("services.aitc.client.")
|
||||
));
|
||||
});
|
||||
}
|
||||
function gotSilentAssertion(err, val) {
|
||||
self._log.info("gotSilentAssertion called");
|
||||
if (err) {
|
||||
// If we were asked to let the user login, do the popup method.
|
||||
if (login) {
|
||||
self._log.info("Could not obtain silent assertion, retrying login");
|
||||
BrowserID.getAssertionWithLogin(function gotAssertion(err, val) {
|
||||
if (err) {
|
||||
self._log.error(err);
|
||||
cb(err, false);
|
||||
return;
|
||||
}
|
||||
processAssertion(val);
|
||||
}, ctxWin);
|
||||
return;
|
||||
}
|
||||
self._log.warn("Could not obtain assertion in _makeClient");
|
||||
cb(err, false);
|
||||
} else {
|
||||
processAssertion(val);
|
||||
}
|
||||
}
|
||||
|
||||
// Check if we can get assertion silently first
|
||||
self._log.info("Attempting to obtain assertion silently")
|
||||
BrowserID.getAssertion(gotSilentAssertion, {
|
||||
audience: DASHBOARD_URL,
|
||||
sameEmailAs: MARKETPLACE_URL
|
||||
});
|
||||
},
|
||||
|
||||
};
|
@ -1,450 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
this.EXPORTED_SYMBOLS = ["AitcStorage", "AitcQueue"];
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
|
||||
|
||||
Cu.import("resource://gre/modules/NetUtil.jsm");
|
||||
Cu.import("resource://gre/modules/Webapps.jsm");
|
||||
Cu.import("resource://gre/modules/FileUtils.jsm");
|
||||
Cu.import("resource://gre/modules/XPCOMUtils.jsm");
|
||||
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
Cu.import("resource://services-common/rest.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
/**
|
||||
* Provides a file-backed queue. Currently used by manager.js as persistent
|
||||
* storage to manage pending installs and uninstalls.
|
||||
*
|
||||
* @param filename
|
||||
* (String) The file backing this queue will be named as this string.
|
||||
*
|
||||
* @param cb
|
||||
* (Function) This function will be called when the queue is ready to
|
||||
* use. *DO NOT* call any methods on this object until the
|
||||
* callback is invoked, if you do so, none of your operations
|
||||
* will be persisted on disk.
|
||||
*
|
||||
*/
|
||||
this.AitcQueue = function AitcQueue(filename, cb) {
|
||||
if (!cb) {
|
||||
throw new Error("AitcQueue constructor called without callback");
|
||||
}
|
||||
|
||||
this._log = Log4Moz.repository.getLogger("Service.AITC.Storage.Queue");
|
||||
this._log.level = Log4Moz.Level[Preferences.get(
|
||||
"services.aitc.storage.log.level"
|
||||
)];
|
||||
|
||||
this._queue = [];
|
||||
this._writeLock = false;
|
||||
this._filePath = "webapps/" + filename;
|
||||
|
||||
this._log.info("AitcQueue instance loading");
|
||||
|
||||
CommonUtils.jsonLoad(this._filePath, this, function jsonLoaded(data) {
|
||||
if (data && Array.isArray(data)) {
|
||||
this._queue = data;
|
||||
}
|
||||
this._log.info("AitcQueue instance created");
|
||||
cb(true);
|
||||
});
|
||||
}
|
||||
AitcQueue.prototype = {
|
||||
/**
|
||||
* Add an object to the queue, and data is saved to disk.
|
||||
*/
|
||||
enqueue: function enqueue(obj, cb) {
|
||||
this._log.info("Adding to queue " + obj);
|
||||
|
||||
if (!cb) {
|
||||
throw new Error("enqueue called without callback");
|
||||
}
|
||||
|
||||
let self = this;
|
||||
this._queue.push(obj);
|
||||
|
||||
try {
|
||||
this._putFile(this._queue, function _enqueuePutFile(err) {
|
||||
if (err) {
|
||||
// Write unsuccessful, don't add to queue.
|
||||
self._queue.pop();
|
||||
cb(err, false);
|
||||
return;
|
||||
}
|
||||
// Successful write.
|
||||
cb(null, true);
|
||||
return;
|
||||
});
|
||||
} catch (e) {
|
||||
self._queue.pop();
|
||||
cb(e, false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Remove the object at the head of the queue, and data is saved to disk.
|
||||
*/
|
||||
dequeue: function dequeue(cb) {
|
||||
this._log.info("Removing head of queue");
|
||||
|
||||
if (!cb) {
|
||||
throw new Error("dequeue called without callback");
|
||||
}
|
||||
if (!this._queue.length) {
|
||||
throw new Error("Queue is empty");
|
||||
}
|
||||
|
||||
let self = this;
|
||||
let obj = this._queue.shift();
|
||||
|
||||
try {
|
||||
this._putFile(this._queue, function _dequeuePutFile(err) {
|
||||
if (!err) {
|
||||
// Successful write.
|
||||
cb(null, true);
|
||||
return;
|
||||
}
|
||||
// Unsuccessful write, put back in queue.
|
||||
self._queue.unshift(obj);
|
||||
cb(err, false);
|
||||
});
|
||||
} catch (e) {
|
||||
self._queue.unshift(obj);
|
||||
cb(e, false);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Return the object at the front of the queue without removing it.
|
||||
*/
|
||||
peek: function peek() {
|
||||
this._log.info("Peek called when length " + this._queue.length);
|
||||
if (!this._queue.length) {
|
||||
throw new Error("Queue is empty");
|
||||
}
|
||||
return this._queue[0];
|
||||
},
|
||||
|
||||
/**
|
||||
* Find out the length of the queue.
|
||||
*/
|
||||
get length() {
|
||||
return this._queue.length;
|
||||
},
|
||||
|
||||
/**
|
||||
* Put an array into the cache file. Will throw an exception if there is
|
||||
* an error while trying to write to the file.
|
||||
*/
|
||||
_putFile: function _putFile(value, cb) {
|
||||
if (this._writeLock) {
|
||||
throw new Error("_putFile already in progress");
|
||||
}
|
||||
|
||||
this._writeLock = true;
|
||||
this._log.info("Writing queue to disk");
|
||||
CommonUtils.jsonSave(this._filePath, this, value, function jsonSaved(err) {
|
||||
if (err) {
|
||||
let msg = new Error("_putFile failed with " + err);
|
||||
this._writeLock = false;
|
||||
cb(msg);
|
||||
return;
|
||||
}
|
||||
this._log.info("_putFile succeeded");
|
||||
this._writeLock = false;
|
||||
cb(null);
|
||||
});
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* An interface to DOMApplicationRegistry, used by manager.js to process
|
||||
* remote changes received and apply them to the local registry.
|
||||
*/
|
||||
function AitcStorageImpl() {
|
||||
this._log = Log4Moz.repository.getLogger("Service.AITC.Storage");
|
||||
this._log.level = Log4Moz.Level[Preferences.get(
|
||||
"services.aitc.storage.log.level"
|
||||
)];
|
||||
this._log.info("Loading AitC storage module");
|
||||
}
|
||||
AitcStorageImpl.prototype = {
|
||||
/**
|
||||
* Determines what changes are to be made locally, given a list of
|
||||
* remote apps.
|
||||
*
|
||||
* @param remoteApps
|
||||
* (Array) An array of app records fetched from the AITC server.
|
||||
*
|
||||
* @param callback
|
||||
* (function) A callback to be invoked when processing is finished.
|
||||
*/
|
||||
processApps: function processApps(remoteApps, callback) {
|
||||
let self = this;
|
||||
this._log.info("Server check got " + remoteApps.length + " apps");
|
||||
|
||||
// Get the set of local apps, and then pass to _processApps.
|
||||
// _processApps will check for the validity of remoteApps.
|
||||
DOMApplicationRegistry.getAllWithoutManifests(
|
||||
function _processAppsGotLocalApps(localApps) {
|
||||
let changes = self._determineLocalChanges(localApps, remoteApps);
|
||||
self._processChanges(changes, callback);
|
||||
}
|
||||
);
|
||||
},
|
||||
|
||||
/**
|
||||
* Determine the set of changes needed to reconcile local with remote data.
|
||||
*
|
||||
* The return value is a mapping describing the actions that need to be
|
||||
* performed. It has the following keys:
|
||||
*
|
||||
* deleteIDs
|
||||
* (Array) String app IDs of applications that need to be uninstalled.
|
||||
* installs
|
||||
* (Object) Maps app ID to the remote app record. The app ID may exist
|
||||
* locally. If the app did not exist previously, a new ID will be
|
||||
* generated and used here.
|
||||
*
|
||||
* @param localApps
|
||||
* (Object) Mapping of App ID to minimal application record (from
|
||||
* DOMApplicationRegistry.getAllWithoutManifests()).
|
||||
* @param remoteApps
|
||||
* (Array) Application records from the server.
|
||||
*/
|
||||
_determineLocalChanges: function _determineChanges(localApps, remoteApps) {
|
||||
let changes = new Map();
|
||||
changes.deleteIDs = [];
|
||||
changes.installs = {};
|
||||
|
||||
// If there are no remote apps, do nothing.
|
||||
//
|
||||
// Arguably, the correct thing to do is to delete all local apps. The
|
||||
// server is the authoritative source, after all. But, we play it safe.
|
||||
if (!Object.keys(remoteApps).length) {
|
||||
this._log.warn("Empty set of remote apps. Not taking any action.");
|
||||
return changes;
|
||||
}
|
||||
|
||||
// This is all to avoid potential duplicates. Once JS Sets are iterable, we
|
||||
// should switch everything to use them.
|
||||
let deletes = {};
|
||||
let remoteOrigins = {};
|
||||
|
||||
let localOrigins = {};
|
||||
for (let [id, app] in Iterator(localApps)) {
|
||||
localOrigins[app.origin] = id;
|
||||
}
|
||||
|
||||
for (let remoteApp of remoteApps) {
|
||||
let origin = remoteApp.origin;
|
||||
remoteOrigins[origin] = true;
|
||||
|
||||
// If the app is hidden on the remote server, that translates to a local
|
||||
// delete/uninstall, but only if the app is present locally.
|
||||
if (remoteApp.hidden) {
|
||||
if (origin in localOrigins) {
|
||||
deletes[localOrigins[origin]] = true;
|
||||
}
|
||||
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the remote app isn't present locally, we install it under a
|
||||
// newly-generated ID.
|
||||
if (!localApps[origin]) {
|
||||
changes.installs[DOMApplicationRegistry.makeAppId()] = remoteApp;
|
||||
continue;
|
||||
}
|
||||
|
||||
// If the remote app is newer, we force a re-install using the existing
|
||||
// ID.
|
||||
if (localApps[origin].installTime < remoteApp.installTime) {
|
||||
changes.installs[localApps[origin]] = remoteApp;
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
// If we have local apps not on the server, we need to delete them, as the
|
||||
// server is authoritative.
|
||||
for (let [id, app] in Iterator(localApps)) {
|
||||
if (!(app.origin in remoteOrigins)) {
|
||||
deletes[id] = true;
|
||||
}
|
||||
}
|
||||
|
||||
changes.deleteIDs = Object.keys(deletes);
|
||||
|
||||
return changes;
|
||||
},
|
||||
|
||||
/**
|
||||
* Process changes so local client is in sync with server.
|
||||
*
|
||||
* This takes the output from _determineLocalChanges() and applies it.
|
||||
*
|
||||
* The supplied callback is invoked with no arguments when all operations
|
||||
* have completed.
|
||||
*/
|
||||
_processChanges: function _processChanges(changes, cb) {
|
||||
|
||||
if (!changes.deleteIDs.length && !Object.keys(changes.installs).length) {
|
||||
this._log.info("No changes to be applied.");
|
||||
cb();
|
||||
return;
|
||||
}
|
||||
|
||||
// First, we assemble all the changes in the right format.
|
||||
let installs = [];
|
||||
for (let [id, record] in Iterator(changes.installs)) {
|
||||
installs.push({id: id, value: record});
|
||||
}
|
||||
|
||||
let uninstalls = [];
|
||||
for (let id of changes.deleteIDs) {
|
||||
this._log.info("Uninstalling app: " + id);
|
||||
uninstalls.push({id: id, hidden: true});
|
||||
}
|
||||
|
||||
// Now we need to perform actions.
|
||||
//
|
||||
// We want to perform all the uninstalls followed by all the installs.
|
||||
// However, this is somewhat complicated because uninstalls are
|
||||
// asynchronous and there may not even be any uninstalls. So, we simply
|
||||
// define a clojure to invoke installation and we call it whenever we're
|
||||
// ready.
|
||||
let doInstalls = function doInstalls() {
|
||||
if (!installs.length) {
|
||||
if (cb) {
|
||||
try {
|
||||
cb();
|
||||
} catch (ex) {
|
||||
this._log.warn("Exception when invoking callback: " +
|
||||
CommonUtils.exceptionStr(ex));
|
||||
} finally {
|
||||
cb = null;
|
||||
}
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
this._applyInstalls(installs, cb);
|
||||
|
||||
// Prevent double invoke, just in case.
|
||||
installs = [];
|
||||
cb = null;
|
||||
}.bind(this);
|
||||
|
||||
if (uninstalls.length) {
|
||||
DOMApplicationRegistry.updateApps(uninstalls, function onComplete() {
|
||||
doInstalls();
|
||||
return;
|
||||
});
|
||||
} else {
|
||||
doInstalls();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Apply a set of installs to the local registry. Fetch each app's manifest
|
||||
* in parallel (don't retry on failure) and insert into registry.
|
||||
*/
|
||||
_applyInstalls: function _applyInstalls(toInstall, callback) {
|
||||
let done = 0;
|
||||
let total = toInstall.length;
|
||||
this._log.info("Applying " + total + " installs to registry");
|
||||
|
||||
/**
|
||||
* The purpose of _checkIfDone is to invoke the callback after all the
|
||||
* installs have been applied. They all fire in parallel, and each will
|
||||
* check-in when it is done.
|
||||
*/
|
||||
let self = this;
|
||||
function _checkIfDone() {
|
||||
done += 1;
|
||||
self._log.debug(done + "/" + total + " apps processed");
|
||||
if (done == total) {
|
||||
callback();
|
||||
}
|
||||
}
|
||||
|
||||
function _makeManifestCallback(appObj) {
|
||||
return function(err, manifest) {
|
||||
if (err) {
|
||||
self._log.warn("Could not fetch manifest for " + appObj.name);
|
||||
_checkIfDone();
|
||||
return;
|
||||
}
|
||||
appObj.value.manifest = manifest;
|
||||
DOMApplicationRegistry.updateApps([appObj], _checkIfDone);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Now we get a manifest for each record, and add it to the local registry
|
||||
* when we receive it. If a manifest GET times out, we will not add
|
||||
* the app to the registry but count as "success" anyway. The app will
|
||||
* be added on the next GET poll, hopefully the manifest will be
|
||||
* available then.
|
||||
*/
|
||||
for each (let app in toInstall) {
|
||||
let url = app.value.manifestURL;
|
||||
if (url[0] == "/") {
|
||||
url = app.value.origin + app.value.manifestURL;
|
||||
}
|
||||
this._getManifest(url, _makeManifestCallback(app));
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Fetch a manifest from given URL. No retries are made on failure. We'll
|
||||
* timeout after 20 seconds.
|
||||
*/
|
||||
_getManifest: function _getManifest(url, callback) {
|
||||
let req = new RESTRequest(url);
|
||||
req.timeout = 20;
|
||||
|
||||
let self = this;
|
||||
req.get(function(error) {
|
||||
if (error) {
|
||||
callback(error, null);
|
||||
return;
|
||||
}
|
||||
if (!req.response.success) {
|
||||
callback(new Error("Non-200 while fetching manifest"), null);
|
||||
return;
|
||||
}
|
||||
|
||||
let err = null;
|
||||
let manifest = null;
|
||||
try {
|
||||
manifest = JSON.parse(req.response.body);
|
||||
if (!manifest.name) {
|
||||
self._log.warn(
|
||||
"_getManifest got invalid manifest: " + req.response.body
|
||||
);
|
||||
err = new Error("Invalid manifest fetched");
|
||||
}
|
||||
} catch (e) {
|
||||
self._log.warn(
|
||||
"_getManifest got invalid JSON response: " + req.response.body
|
||||
);
|
||||
err = new Error("Invalid manifest fetched");
|
||||
}
|
||||
|
||||
callback(err, manifest);
|
||||
});
|
||||
},
|
||||
|
||||
};
|
||||
|
||||
XPCOMUtils.defineLazyGetter(this, "AitcStorage", function() {
|
||||
return new AitcStorageImpl();
|
||||
});
|
@ -1,28 +0,0 @@
|
||||
pref("dom.mozApps.used", false); // Set to true by DOMApplicationRegistry
|
||||
|
||||
pref("services.aitc.log.dump", false); // Root logger
|
||||
pref("services.aitc.log.level", "All");
|
||||
|
||||
pref("services.aitc.browserid.url", "https://login.persona.org/sign_in");
|
||||
pref("services.aitc.browserid.log.level", "Debug");
|
||||
|
||||
pref("services.aitc.client.log.level", "Debug");
|
||||
pref("services.aitc.client.timeout", 120); // 120 seconds
|
||||
|
||||
pref("services.aitc.dashboard.url", "https://myapps.mozillalabs.com");
|
||||
|
||||
pref("services.aitc.main.idleTime", 120000); // 2 minutes
|
||||
|
||||
pref("services.aitc.manager.putFreq", 10000); // 10 seconds
|
||||
pref("services.aitc.manager.getActiveFreq", 120000); // 2 minutes
|
||||
pref("services.aitc.manager.getPassiveFreq", 7200000); // 2 hours
|
||||
pref("services.aitc.manager.log.level", "Debug");
|
||||
|
||||
pref("services.aitc.marketplace.url", "https://marketplace.mozilla.org");
|
||||
|
||||
pref("services.aitc.service.log.level", "Debug");
|
||||
|
||||
// TODO: Temporary value. Change to the production server (bug 760903)
|
||||
pref("services.aitc.tokenServer.url", "https://stage-token.services.mozilla.com");
|
||||
|
||||
pref("services.aitc.storage.log.level", "Debug");
|
@ -1,23 +0,0 @@
|
||||
# This Source Code Form is subject to the terms of the Mozilla Public
|
||||
# License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
# You can obtain one at http://mozilla.org/MPL/2.0/.
|
||||
|
||||
DEPTH = @DEPTH@
|
||||
topsrcdir = @top_srcdir@
|
||||
srcdir = @srcdir@
|
||||
VPATH = @srcdir@
|
||||
relativesrcdir = @relativesrcdir@
|
||||
|
||||
include $(DEPTH)/config/autoconf.mk
|
||||
|
||||
MODULE = test_services_aitc
|
||||
XPCSHELL_TESTS = unit
|
||||
|
||||
|
||||
MOCHITEST_BROWSER_FILES = \
|
||||
mochitest/head.js \
|
||||
mochitest/browser_id_simple.js \
|
||||
mochitest/file_browser_id_mock.html \
|
||||
$(NULL)
|
||||
|
||||
include $(topsrcdir)/config/rules.mk
|
@ -1,44 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
const AUD = "http://foo.net";
|
||||
|
||||
function test() {
|
||||
waitForExplicitFinish();
|
||||
setEndpoint("browser_id_mock");
|
||||
|
||||
// Get an assertion for default email.
|
||||
BrowserID.getAssertion(gotDefaultAssertion, {audience: AUD});
|
||||
}
|
||||
|
||||
function gotDefaultAssertion(err, ast) {
|
||||
is(err, null, "gotDefaultAssertion failed with " + err);
|
||||
is(ast, "default@example.org_assertion_" + AUD,
|
||||
"gotDefaultAssertion returned wrong assertion");
|
||||
|
||||
// Get an assertion for a specific email.
|
||||
BrowserID.getAssertion(gotSpecificAssertion, {
|
||||
requiredEmail: "specific@example.org",
|
||||
audience: AUD
|
||||
});
|
||||
}
|
||||
|
||||
function gotSpecificAssertion(err, ast) {
|
||||
is(err, null, "gotSpecificAssertion failed with " + err);
|
||||
is(ast, "specific@example.org_assertion_" + AUD,
|
||||
"gotSpecificAssertion returned wrong assertion");
|
||||
|
||||
// Get an assertion using sameEmailAs for another domain.
|
||||
BrowserID.getAssertion(gotSameEmailAssertion, {
|
||||
sameEmailAs: "http://zombo.com",
|
||||
audience: AUD
|
||||
});
|
||||
}
|
||||
|
||||
function gotSameEmailAssertion(err, ast) {
|
||||
is(err, null, "gotSameEmailAssertion failed with " + err);
|
||||
is(ast, "assertion_for_sameEmailAs",
|
||||
"gotSameEmailAssertion returned wrong assertion");
|
||||
|
||||
finish();
|
||||
}
|
@ -1,52 +0,0 @@
|
||||
<html>
|
||||
<head>
|
||||
<meta http-equiv="Content-Type" content="text/html; charset=utf-8">
|
||||
</head>
|
||||
<body>
|
||||
<p>Mock BrowserID endpoint for a logged-in user</p>
|
||||
</body>
|
||||
<script>
|
||||
|
||||
/**
|
||||
* Object containing valid email/key paris for this user. An assertion is simply
|
||||
* the string "_assertion_$audience" appended to the email. The exception is
|
||||
* when the email address is "sameEmailAs@example.org" the assertion will
|
||||
* be "assertion_for_sameEmailAs".
|
||||
*/
|
||||
var _emails = {
|
||||
"default@example.org": "default@example.org_key",
|
||||
"specific@example.org": "specific@example.org_key",
|
||||
"sameEmailAs@example.org": "sameEmailAs@example.org_key"
|
||||
};
|
||||
var _sameEmailAs = "sameEmailAs@example.org";
|
||||
|
||||
// Mock internal API
|
||||
window.BrowserID = {};
|
||||
window.BrowserID.User = {
|
||||
getStoredEmailKeypairs: function() {
|
||||
return _emails;
|
||||
},
|
||||
getAssertion: function(email, audience, success, error) {
|
||||
if (email == _sameEmailAs) {
|
||||
success("assertion_for_sameEmailAs");
|
||||
return;
|
||||
}
|
||||
if (email in _emails) {
|
||||
success(email + "_assertion_" + audience);
|
||||
return;
|
||||
}
|
||||
error("invalid email specified");
|
||||
}
|
||||
};
|
||||
window.BrowserID.Storage = {
|
||||
site: {
|
||||
get: function(domain, key) {
|
||||
if (key == "email") {
|
||||
return _sameEmailAs;
|
||||
}
|
||||
return "";
|
||||
}
|
||||
}
|
||||
};
|
||||
</script>
|
||||
</html>
|
@ -1,23 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
let tmp = {};
|
||||
Components.utils.import("resource://gre/modules/Services.jsm");
|
||||
Components.utils.import("resource://services-aitc/browserid.js", tmp);
|
||||
|
||||
const BrowserID = tmp.BrowserID;
|
||||
const testPath = "http://mochi.test:8888/browser/services/aitc/tests/";
|
||||
|
||||
function loadURL(aURL, aCB) {
|
||||
gBrowser.selectedBrowser.addEventListener("load", function () {
|
||||
gBrowser.selectedBrowser.removeEventListener("load", arguments.callee, true);
|
||||
is(gBrowser.currentURI.spec, aURL, "loaded expected URL");
|
||||
aCB();
|
||||
}, true);
|
||||
gBrowser.loadURI(aURL);
|
||||
}
|
||||
|
||||
function setEndpoint(name) {
|
||||
let fullPath = testPath + "file_" + name + ".html";
|
||||
Services.prefs.setCharPref("services.aitc.browserid.url", fullPath);
|
||||
}
|
@ -1,163 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://services-aitc/client.js");
|
||||
|
||||
Cu.import("resource://services-common/rest.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
|
||||
Cu.import("resource://testing-common/services-common/aitcserver.js");
|
||||
|
||||
const PREFS = new Preferences("services.aitc.client.")
|
||||
|
||||
function run_test() {
|
||||
initTestLogging("Trace");
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function get_aitc_server() {
|
||||
_("Create new server.");
|
||||
|
||||
let server = new AITCServer10Server();
|
||||
server.start(get_server_port());
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function get_server_with_user(username) {
|
||||
_("Create server user for User " + username);
|
||||
|
||||
let server = get_aitc_server();
|
||||
server.createUser(username);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function get_mock_app(remote) {
|
||||
|
||||
let app = {
|
||||
name: "Mocking Birds",
|
||||
origin: "http://example.com",
|
||||
installOrigin: "http://example.com",
|
||||
installedAt: Date.now(),
|
||||
modifiedAt: Date.now(),
|
||||
receipts: []
|
||||
};
|
||||
|
||||
app[remote ? 'manifestPath' : 'manifestURL'] = "/manifest.webapp";
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
function get_client_for_server(username, server) {
|
||||
_("Create server user for User " + username);
|
||||
|
||||
let token = {
|
||||
endpoint: server.url + username,
|
||||
id: 'ID-HERE',
|
||||
key: 'KEY-HERE'
|
||||
};
|
||||
|
||||
let client = new AitcClient(token, PREFS);
|
||||
|
||||
return client;
|
||||
}
|
||||
|
||||
// Clean up code - backoff is preserved between requests in a pref
|
||||
function advance(server) {
|
||||
PREFS.set("backoff", "0");
|
||||
if (server) {
|
||||
server.stop(run_next_test);
|
||||
} else {
|
||||
run_next_test();
|
||||
}
|
||||
}
|
||||
|
||||
add_test(function test_getapps_empty() {
|
||||
_("Ensure client request for empty user has appropriate content.");
|
||||
|
||||
const username = "123";
|
||||
|
||||
let server = get_server_with_user(username);
|
||||
let client = get_client_for_server(username, server);
|
||||
|
||||
client.getApps(function(error, apps) {
|
||||
_("Got response");
|
||||
do_check_null(error);
|
||||
|
||||
do_check_true(Array.isArray(apps));
|
||||
do_check_eq(apps.length, 0);
|
||||
|
||||
advance(server);
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_install_app() {
|
||||
_("Ensure client request for installing an app has appropriate content.");
|
||||
|
||||
const username = "123";
|
||||
const app = get_mock_app();
|
||||
|
||||
let server = get_server_with_user(username);
|
||||
|
||||
let client = get_client_for_server(username, server);
|
||||
|
||||
// TODO _putApp instead of, as install requires app in registry
|
||||
client._putApp(client._makeRemoteApp(app), function(error, status) {
|
||||
_("Got response");
|
||||
do_check_null(error);
|
||||
|
||||
do_check_true(status);
|
||||
|
||||
client.getApps(function(error, apps) {
|
||||
_("Got response");
|
||||
do_check_null(error);
|
||||
|
||||
do_check_true(Array.isArray(apps));
|
||||
do_check_eq(apps.length, 1);
|
||||
|
||||
let first = apps[0];
|
||||
|
||||
do_check_eq(first.origin, app.origin);
|
||||
|
||||
advance(server);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_uninstall_app() {
|
||||
_("Ensure client request for un-installing an app has appropriate content.");
|
||||
|
||||
const username = "123";
|
||||
const app = get_mock_app();
|
||||
|
||||
let server = get_server_with_user(username);
|
||||
let client = get_client_for_server(username, server);
|
||||
|
||||
server.users[username].addApp(get_mock_app(true));
|
||||
|
||||
client.remoteUninstall(app, function(error, status) {
|
||||
_("Got response");
|
||||
do_check_null(error);
|
||||
|
||||
do_check_true(status);
|
||||
|
||||
client.getApps(function(error, apps) {
|
||||
_("Got response");
|
||||
do_check_eq(error);
|
||||
|
||||
do_check_true(Array.isArray(apps));
|
||||
do_check_eq(apps.length, 1);
|
||||
|
||||
let first = apps[0];
|
||||
|
||||
do_check_eq(first.origin, app.origin);
|
||||
do_check_true(first.hidden);
|
||||
|
||||
advance(server);
|
||||
});
|
||||
});
|
||||
});
|
@ -1,263 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://gre/modules/Webapps.jsm");
|
||||
|
||||
Cu.import("resource://services-aitc/client.js");
|
||||
Cu.import("resource://services-aitc/manager.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
Cu.import("resource://services-common/preferences.js");
|
||||
|
||||
Cu.import("resource://testing-common/services-common/aitcserver.js");
|
||||
|
||||
const PREFS = new Preferences("services.aitc.");
|
||||
|
||||
let count = 0;
|
||||
|
||||
function run_test() {
|
||||
initTestLogging("Trace");
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function get_aitc_server() {
|
||||
_("Create new server.");
|
||||
|
||||
let server = new AITCServer10Server();
|
||||
server.start(get_server_port());
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function get_server_with_user(username) {
|
||||
_("Create server user for User " + username);
|
||||
|
||||
let server = get_aitc_server();
|
||||
server.createUser(username);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function get_mock_app() {
|
||||
|
||||
let app = {
|
||||
name: "Mocking Birds",
|
||||
origin: "http://example" + ++count + ".com",
|
||||
installOrigin: "http://example.com",
|
||||
installedAt: Date.now(),
|
||||
modifiedAt: Date.now(),
|
||||
receipts: [],
|
||||
manifestURL: "/manifest.webapp"
|
||||
};
|
||||
|
||||
return app;
|
||||
}
|
||||
|
||||
function get_mock_queue_element() {
|
||||
return {
|
||||
type: "install",
|
||||
app: get_mock_app(),
|
||||
retries: 0,
|
||||
lastTime: 0
|
||||
}
|
||||
}
|
||||
|
||||
function get_client_for_server(username, server) {
|
||||
_("Create server user for User " + username);
|
||||
|
||||
let token = {
|
||||
endpoint: server.url + username,
|
||||
id: "ID-HERE",
|
||||
key: "KEY-HERE"
|
||||
};
|
||||
|
||||
return new AitcClient(token, new Preferences("services.aitc.client."));
|
||||
}
|
||||
|
||||
// Check that a is less than b.
|
||||
function do_check_lt(a, b) {
|
||||
do_check_true(a < b);
|
||||
}
|
||||
|
||||
add_test(function test_manager_localapps() {
|
||||
// Install two fake apps into the DOM registry.
|
||||
let fakeApp1 = get_mock_app();
|
||||
fakeApp1.manifest = {
|
||||
name: "Appasaurus 1",
|
||||
description: "One of the best fake apps ever",
|
||||
launch_path: "/",
|
||||
fullscreen: true,
|
||||
required_features: ["webgl"]
|
||||
};
|
||||
|
||||
let fakeApp2 = get_mock_app();
|
||||
fakeApp2.manifest = {
|
||||
name: "Appasaurus 2",
|
||||
description: "The other best fake app ever",
|
||||
launch_path: "/",
|
||||
fullscreen: true,
|
||||
required_features: ["geolocation"]
|
||||
};
|
||||
|
||||
DOMApplicationRegistry.confirmInstall({app: fakeApp1});
|
||||
DOMApplicationRegistry.confirmInstall({app: fakeApp2});
|
||||
|
||||
// Create an instance of the manager and check if it put the app in the queue.
|
||||
// We put doInitialUpload in nextTick, because maanger will not be defined
|
||||
// in the callback. This pattern is used everywhere, AitcManager is created.
|
||||
let manager = new AitcManager(function() {
|
||||
CommonUtils.nextTick(doInitialUpload);
|
||||
});
|
||||
|
||||
function doInitialUpload() {
|
||||
manager.initialSchedule(function(num) {
|
||||
// 2 apps should have been queued.
|
||||
do_check_eq(num, 2);
|
||||
do_check_eq(manager._pending.length, 2);
|
||||
|
||||
let entry = manager._pending.peek();
|
||||
do_check_eq(entry.type, "install");
|
||||
do_check_eq(entry.app.origin, fakeApp1.origin);
|
||||
|
||||
// Remove one app from queue.
|
||||
manager._pending.dequeue(run_next_test);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
add_test(function test_manager_alreadysynced() {
|
||||
// The manager should ignore any local apps if we've already synced before.
|
||||
Preferences.set("services.aitc.client.lastModified", "" + Date.now());
|
||||
|
||||
let manager = new AitcManager(function() {
|
||||
CommonUtils.nextTick(doCheck);
|
||||
});
|
||||
|
||||
function doCheck() {
|
||||
manager.initialSchedule(function(num) {
|
||||
do_check_eq(num, -1);
|
||||
do_check_eq(manager._pending.length, 1);
|
||||
// Clear queue for next test.
|
||||
manager._pending.dequeue(run_next_test);
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
add_test(function test_401_responses() {
|
||||
PREFS.set("client.backoff", "50");
|
||||
PREFS.set("manager.putFreq", 50);
|
||||
const app = get_mock_app();
|
||||
const username = "123";
|
||||
const premadeToken = {
|
||||
id: "testtest",
|
||||
key: "testtest",
|
||||
endpoint: "http://localhost:8080/1.0/123",
|
||||
uid: "uid",
|
||||
duration: "5000"
|
||||
};
|
||||
|
||||
let server = get_server_with_user(username);
|
||||
let client = get_client_for_server(username, server);
|
||||
|
||||
server.mockStatus = {
|
||||
code: 401,
|
||||
method: "Unauthorized"
|
||||
};
|
||||
|
||||
let mockRequestCount = 0;
|
||||
let clientFirstToken = null;
|
||||
server.onRequest = function mockstatus() {
|
||||
mockRequestCount++;
|
||||
switch (mockRequestCount) {
|
||||
case 1:
|
||||
clientFirstToken = client.token;
|
||||
// Switch to using mock 201s.
|
||||
this.mockStatus = {
|
||||
code: 201,
|
||||
method: "Created"
|
||||
};
|
||||
break;
|
||||
case 2:
|
||||
// Check that the client obtained a different token.
|
||||
do_check_neq(client.token.id, clientFirstToken.id);
|
||||
do_check_neq(client.token.key, clientFirstToken.key);
|
||||
server.stop(run_next_test);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
||||
let manager = new AitcManager(function() {
|
||||
CommonUtils.nextTick(gotManager);
|
||||
}, client, premadeToken);
|
||||
|
||||
function gotManager() {
|
||||
// Assume first token is not outdated.
|
||||
manager._lastTokenTime = Date.now();
|
||||
manager.appEvent("install", get_mock_app());
|
||||
}
|
||||
});
|
||||
|
||||
add_test(function test_client_exponential_backoff() {
|
||||
_("Test that the client is properly setting the backoff");
|
||||
|
||||
// Use prefs to speed up tests.
|
||||
const putFreq = 50;
|
||||
const initialBackoff = 50;
|
||||
const username = "123";
|
||||
PREFS.set("manager.putFreq", putFreq);
|
||||
PREFS.set("client.backoff.initial", initialBackoff);
|
||||
PREFS.set("client.backoff.max", 100000);
|
||||
|
||||
let mockRequestCount = 0;
|
||||
let lastRequestTime = Date.now();
|
||||
// Create server that returns failure codes.
|
||||
let server = get_server_with_user(username);
|
||||
server.mockStatus = {
|
||||
code: 399,
|
||||
method: "Self Destruct"
|
||||
}
|
||||
|
||||
server.onRequest = function onRequest() {
|
||||
mockRequestCount++;
|
||||
let timeDiff, timeNow = Date.now();
|
||||
if (mockRequestCount !== 1) {
|
||||
timeDiff = timeNow - lastRequestTime;
|
||||
}
|
||||
lastRequestTime = timeNow;
|
||||
|
||||
// The time between the 3rd and 4th request should be atleast the
|
||||
// initial backoff.
|
||||
if (mockRequestCount === 4) {
|
||||
do_check_lt(initialBackoff, timeDiff);
|
||||
// The time beween the 4th and 5th request should be atleast double
|
||||
// the intial backoff.
|
||||
} else if (mockRequestCount === 5) {
|
||||
do_check_lt(initialBackoff * 2, timeDiff);
|
||||
server.stop(run_next_test);
|
||||
}
|
||||
}
|
||||
|
||||
// Create dummy client and manager.
|
||||
let client = get_client_for_server(username, server);
|
||||
let manager = new AitcManager(function() {
|
||||
CommonUtils.nextTick(gotManager);
|
||||
}, client);
|
||||
|
||||
function gotManager() {
|
||||
manager._lastTokenTime = Date.now();
|
||||
// Create a bunch of dummy apps for the queue to cycle through.
|
||||
manager._pending._queue = [
|
||||
get_mock_queue_element(),
|
||||
get_mock_queue_element(),
|
||||
get_mock_queue_element(),
|
||||
get_mock_queue_element(),
|
||||
];
|
||||
// Add the dummy apps to the queue, then start the polling cycle with an app
|
||||
// event.
|
||||
manager._pending._putFile(manager._pending._queue, function (err) {
|
||||
manager.appEvent("install", get_mock_app());
|
||||
});
|
||||
}
|
||||
|
||||
});
|
@ -1,13 +0,0 @@
|
||||
const modules = [
|
||||
"client.js",
|
||||
"browserid.js",
|
||||
"main.js",
|
||||
"manager.js",
|
||||
"storage.js"
|
||||
];
|
||||
|
||||
function run_test() {
|
||||
for each (let m in modules) {
|
||||
Cu.import("resource://services-aitc/" + m, {});
|
||||
}
|
||||
}
|
@ -1,119 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://services-aitc/storage.js");
|
||||
Cu.import("resource://services-common/async.js");
|
||||
|
||||
let queue = null;
|
||||
|
||||
function run_test() {
|
||||
initTestLogging();
|
||||
queue = new AitcQueue("test", run_next_test);
|
||||
}
|
||||
|
||||
add_test(function test_queue_create() {
|
||||
do_check_eq(queue._queue.length, 0);
|
||||
do_check_eq(queue._writeLock, false);
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_queue_enqueue() {
|
||||
// Add to queue.
|
||||
let testObj = {foo: "bar"};
|
||||
queue.enqueue(testObj, function(err, done) {
|
||||
do_check_eq(err, null);
|
||||
do_check_true(done);
|
||||
|
||||
// Check if peek value is correct.
|
||||
do_check_eq(queue.peek(), testObj);
|
||||
// Peek should be idempotent.
|
||||
do_check_eq(queue.peek(), testObj);
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_queue_dequeue() {
|
||||
// Remove an item and see if queue is empty.
|
||||
queue.dequeue(function(err, done) {
|
||||
do_check_eq(err, null);
|
||||
do_check_true(done);
|
||||
do_check_eq(queue.length, 0);
|
||||
try {
|
||||
queue.peek();
|
||||
} catch (e) {
|
||||
do_check_eq(e.toString(), "Error: Queue is empty");
|
||||
run_next_test();
|
||||
}
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_queue_multiaddremove() {
|
||||
// Queues should handle objects, strings and numbers.
|
||||
let items = [{test:"object"}, "teststring", 42];
|
||||
|
||||
// Two random numbers: how many items to queue and how many to remove.
|
||||
// The next test relies on rem > 0.
|
||||
let num = Math.floor(Math.random() * 100 + 20);
|
||||
let rem = Math.floor(Math.random() * 15 + 5);
|
||||
do_check_true(rem < num);
|
||||
do_check_true(rem > 0);
|
||||
|
||||
// First insert all the items we will remove later.
|
||||
for (let i = 0; i < rem; i++) {
|
||||
let ins = items[Math.round(Math.random() * 2)];
|
||||
let cb = Async.makeSpinningCallback();
|
||||
queue.enqueue(ins, cb);
|
||||
do_check_true(cb.wait());
|
||||
}
|
||||
|
||||
do_check_eq(queue.length, rem);
|
||||
|
||||
// Now insert the items we won't remove.
|
||||
let check = [];
|
||||
for (let i = 0; i < (num - rem); i++) {
|
||||
check.push(items[Math.round(Math.random() * 2)]);
|
||||
let cb = Async.makeSpinningCallback();
|
||||
queue.enqueue(check[check.length - 1], cb);
|
||||
do_check_true(cb.wait());
|
||||
}
|
||||
|
||||
do_check_eq(queue.length, num);
|
||||
|
||||
// Now dequeue rem items.
|
||||
for (let i = 0; i < rem; i++) {
|
||||
let cb = Async.makeSpinningCallback();
|
||||
queue.dequeue(cb);
|
||||
do_check_true(cb.wait());
|
||||
}
|
||||
|
||||
do_check_eq(queue.length, num - rem);
|
||||
|
||||
// Check that the items left are the right ones.
|
||||
do_check_eq(JSON.stringify(queue._queue), JSON.stringify(check));
|
||||
|
||||
// Another instance of the same queue should return correct data.
|
||||
let queue2 = new AitcQueue("test", function(done) {
|
||||
do_check_true(done);
|
||||
do_check_eq(queue2.length, queue.length);
|
||||
do_check_eq(JSON.stringify(queue._queue), JSON.stringify(queue2._queue));
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_queue_writelock() {
|
||||
// Queue should not enqueue or dequeue if lock is enabled.
|
||||
queue._writeLock = true;
|
||||
let len = queue.length;
|
||||
|
||||
queue.enqueue("writeLock test", function(err, done) {
|
||||
do_check_eq(err.toString(), "Error: _putFile already in progress");
|
||||
do_check_eq(queue.length, len);
|
||||
|
||||
queue.dequeue(function(err, done) {
|
||||
do_check_eq(err.toString(), "Error: _putFile already in progress");
|
||||
do_check_eq(queue.length, len);
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
});
|
@ -1,151 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
Cu.import("resource://gre/modules/Webapps.jsm");
|
||||
Cu.import("resource://services-common/async.js");
|
||||
Cu.import("resource://services-aitc/storage.js");
|
||||
|
||||
const START_PORT = 8080;
|
||||
const SERVER = "http://localhost";
|
||||
|
||||
let fakeApp1 = {
|
||||
origin: SERVER + ":" + START_PORT,
|
||||
receipts: [],
|
||||
manifestURL: "/manifest.webapp",
|
||||
installOrigin: "http://localhost",
|
||||
installedAt: Date.now(),
|
||||
modifiedAt: Date.now()
|
||||
};
|
||||
|
||||
// Valid manifest for app1
|
||||
let manifest1 = {
|
||||
name: "Appasaurus",
|
||||
description: "Best fake app ever",
|
||||
launch_path: "/",
|
||||
fullscreen: true,
|
||||
required_features: ["webgl"]
|
||||
};
|
||||
|
||||
let fakeApp2 = {
|
||||
origin: SERVER + ":" + (START_PORT + 1),
|
||||
receipts: ["fake.jwt.token"],
|
||||
manifestURL: "/manifest.webapp",
|
||||
installOrigin: "http://localhost",
|
||||
installedAt: Date.now(),
|
||||
modifiedAt: Date.now()
|
||||
};
|
||||
|
||||
// Invalid manifest for app2
|
||||
let manifest2_bad = {
|
||||
not: "a manifest",
|
||||
fullscreen: true
|
||||
};
|
||||
|
||||
// Valid manifest for app2
|
||||
let manifest2_good = {
|
||||
name: "Supercalifragilisticexpialidocious",
|
||||
description: "Did we blow your mind yet?",
|
||||
launch_path: "/"
|
||||
};
|
||||
|
||||
let fakeApp3 = {
|
||||
origin: SERVER + ":" + (START_PORT + 3), // 8082 is for the good manifest2
|
||||
receipts: [],
|
||||
manifestURL: "/manifest.webapp",
|
||||
installOrigin: "http://localhost",
|
||||
installedAt: Date.now(),
|
||||
modifiedAt: Date.now()
|
||||
};
|
||||
|
||||
let manifest3 = {
|
||||
name: "Taumatawhakatangihangakoauauotamateapokaiwhenuakitanatahu",
|
||||
description: "Find your way around this beautiful hill",
|
||||
launch_path: "/"
|
||||
};
|
||||
|
||||
function create_servers() {
|
||||
// Setup servers to server manifests at each port
|
||||
let manifests = [manifest1, manifest2_bad, manifest2_good, manifest3];
|
||||
for (let i = 0; i < manifests.length; i++) {
|
||||
let response = JSON.stringify(manifests[i]);
|
||||
httpd_setup({"/manifest.webapp": function(req, res) {
|
||||
res.setStatusLine(req.httpVersion, 200, "OK");
|
||||
res.setHeader("Content-Type", "application/x-web-app-manifest+json");
|
||||
res.bodyOutputStream.write(response, response.length);
|
||||
}}, START_PORT + i);
|
||||
}
|
||||
}
|
||||
|
||||
function run_test() {
|
||||
initTestLogging();
|
||||
create_servers();
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
add_test(function test_storage_install() {
|
||||
let apps = [fakeApp1, fakeApp2];
|
||||
AitcStorage.processApps(apps, function() {
|
||||
// Verify that app1 got added to registry
|
||||
let id = DOMApplicationRegistry._appId(fakeApp1.origin);
|
||||
do_check_true(DOMApplicationRegistry.itemExists(id));
|
||||
|
||||
// app2 should be missing because of bad manifest
|
||||
do_check_null(DOMApplicationRegistry._appId(fakeApp2.origin));
|
||||
|
||||
// Now associate fakeApp2 with a good manifest and process again
|
||||
fakeApp2.origin = SERVER + ":8082";
|
||||
AitcStorage.processApps([fakeApp1, fakeApp2], function() {
|
||||
// Both apps must be installed
|
||||
let id1 = DOMApplicationRegistry._appId(fakeApp1.origin);
|
||||
let id2 = DOMApplicationRegistry._appId(fakeApp2.origin);
|
||||
do_check_true(DOMApplicationRegistry.itemExists(id1));
|
||||
do_check_true(DOMApplicationRegistry.itemExists(id2));
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_storage_uninstall() {
|
||||
_("Ensure explicit uninstalls through hidden are honored.");
|
||||
do_check_neq(DOMApplicationRegistry._appId(fakeApp1.origin), null);
|
||||
|
||||
// Set app1 as hidden.
|
||||
fakeApp1.hidden = true;
|
||||
AitcStorage.processApps([fakeApp1], function() {
|
||||
// It should be missing.
|
||||
do_check_null(DOMApplicationRegistry._appId(fakeApp1.origin));
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_storage_uninstall_missing() {
|
||||
_("Ensure a local app with no remote record is uninstalled.");
|
||||
|
||||
// If the remote server has data, any local apps not on the remote server
|
||||
// should be deleted.
|
||||
let cb = Async.makeSpinningCallback();
|
||||
AitcStorage.processApps([fakeApp2], cb);
|
||||
cb.wait();
|
||||
do_check_neq(DOMApplicationRegistry._appId(fakeApp2.origin), null);
|
||||
|
||||
AitcStorage.processApps([fakeApp3], function() {
|
||||
let id3 = DOMApplicationRegistry._appId(fakeApp3.origin);
|
||||
do_check_true(DOMApplicationRegistry.itemExists(id3));
|
||||
do_check_null(DOMApplicationRegistry._appId(fakeApp2.origin));
|
||||
run_next_test();
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_uninstall_noop() {
|
||||
_("Ensure that an empty set of remote records does nothing.");
|
||||
|
||||
let id = DOMApplicationRegistry._appId(fakeApp3.origin);
|
||||
do_check_neq(id, null);
|
||||
do_check_true(DOMApplicationRegistry.itemExists(id));
|
||||
|
||||
AitcStorage.processApps([], function onComplete() {
|
||||
do_check_true(DOMApplicationRegistry.itemExists(id));
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
});
|
@ -1,11 +0,0 @@
|
||||
[DEFAULT]
|
||||
head = ../../../common/tests/unit/head_global.js ../../../common/tests/unit/head_helpers.js ../../../common/tests/unit/head_http.js
|
||||
tail =
|
||||
|
||||
[test_load_modules.js]
|
||||
[test_aitc_client.js]
|
||||
# Bug 752243: Profile cleanup frequently fails
|
||||
skip-if = os == "mac" || os == "linux"
|
||||
[test_aitc_manager.js]
|
||||
[test_storage_queue.js]
|
||||
[test_storage_registry.js]
|
@ -26,7 +26,6 @@ pp_modules := \
|
||||
$(NULL)
|
||||
|
||||
testing_modules := \
|
||||
aitcserver.js \
|
||||
bagheeraserver.js \
|
||||
logging.js \
|
||||
storageserver.js \
|
||||
@ -65,10 +64,6 @@ storage-server:
|
||||
$(PYTHON) $(srcdir)/tests/run_server.py $(topsrcdir) \
|
||||
$(MOZ_BUILD_ROOT) run_storage_server.js --port $(server_port)
|
||||
|
||||
aitc-server:
|
||||
$(PYTHON) $(srcdir)/tests/run_server.py $(topsrcdir) \
|
||||
$(MOZ_BUILD_ROOT) run_aitc_server.js --port $(server_port)
|
||||
|
||||
bagheera-server:
|
||||
$(PYTHON) $(srcdir)/tests/run_server.py $(topsrcdir) \
|
||||
$(MOZ_BUILD_ROOT) run_bagheera_server.js --port $(server_port)
|
||||
|
@ -1,555 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
"use strict";
|
||||
|
||||
const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
|
||||
|
||||
this.EXPORTED_SYMBOLS = [
|
||||
"AITCServer10User",
|
||||
"AITCServer10Server",
|
||||
];
|
||||
|
||||
Cu.import("resource://testing-common/httpd.js");
|
||||
Cu.import("resource://services-crypto/utils.js");
|
||||
Cu.import("resource://services-common/log4moz.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
/**
|
||||
* Represents an individual user on an AITC 1.0 server.
|
||||
*
|
||||
* This type provides convenience APIs for interacting with an individual
|
||||
* user's data.
|
||||
*/
|
||||
this.AITCServer10User = function AITCServer10User() {
|
||||
this._log = Log4Moz.repository.getLogger("Services.Common.AITCServer");
|
||||
this.apps = {};
|
||||
}
|
||||
AITCServer10User.prototype = {
|
||||
appRecordProperties: {
|
||||
origin: true,
|
||||
manifestPath: true,
|
||||
installOrigin: true,
|
||||
installedAt: true,
|
||||
modifiedAt: true,
|
||||
receipts: true,
|
||||
name: true,
|
||||
hidden: true,
|
||||
},
|
||||
|
||||
requiredAppProperties: [
|
||||
"origin",
|
||||
"manifestPath",
|
||||
"installOrigin",
|
||||
"installedAt",
|
||||
"modifiedAt",
|
||||
"name",
|
||||
"receipts",
|
||||
],
|
||||
|
||||
/**
|
||||
* Obtain the apps for this user.
|
||||
*
|
||||
* This is a generator of objects representing the apps. Returns the original
|
||||
* apps object normally or an abbreviated version if `minimal` is truthy.
|
||||
*/
|
||||
getApps: function getApps(minimal) {
|
||||
let result;
|
||||
|
||||
for (let id in this.apps) {
|
||||
let app = this.apps[id];
|
||||
|
||||
if (!minimal) {
|
||||
yield app;
|
||||
continue;
|
||||
}
|
||||
|
||||
yield {origin: app.origin, modifiedAt: app.modifiedAt};
|
||||
}
|
||||
},
|
||||
|
||||
getAppByID: function getAppByID(id) {
|
||||
return this.apps[id];
|
||||
},
|
||||
|
||||
/**
|
||||
* Adds an app to this user.
|
||||
*
|
||||
* The app record should be an object (likely from decoded JSON).
|
||||
*/
|
||||
addApp: function addApp(app) {
|
||||
for (let k in app) {
|
||||
if (!(k in this.appRecordProperties)) {
|
||||
throw new Error("Unexpected property in app record: " + k);
|
||||
}
|
||||
}
|
||||
|
||||
for each (let k in this.requiredAppProperties) {
|
||||
if (!(k in app)) {
|
||||
throw new Error("Required property not in app record: " + k);
|
||||
}
|
||||
}
|
||||
|
||||
this.apps[this.originToID(app.origin)] = app;
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns whether a user has an app with the specified ID.
|
||||
*/
|
||||
hasAppID: function hasAppID(id) {
|
||||
return id in this.apps;
|
||||
},
|
||||
|
||||
/**
|
||||
* Delete an app having the specified ID.
|
||||
*/
|
||||
deleteAppWithID: function deleteAppWithID(id) {
|
||||
delete this.apps[id];
|
||||
},
|
||||
|
||||
/**
|
||||
* Convert an origin string to an ID.
|
||||
*/
|
||||
originToID: function originToID(origin) {
|
||||
let hash = CryptoUtils.UTF8AndSHA1(origin);
|
||||
return CommonUtils.encodeBase64URL(hash, false);
|
||||
},
|
||||
};
|
||||
|
||||
/**
|
||||
* A fully-functional AITC 1.0 server implementation.
|
||||
*
|
||||
* Each server instance is capable of serving requests for multiple users.
|
||||
* By default, users do not exist and requests to URIs for a specific user
|
||||
* will result in 404s. To register a new user with an empty account, call
|
||||
* createUser(). If you wish for HTTP requests for non-existing users to
|
||||
* work, set autoCreateUsers to true and am empty user will be
|
||||
* provisioned at request time.
|
||||
*/
|
||||
this.AITCServer10Server = function AITCServer10Server() {
|
||||
this._log = Log4Moz.repository.getLogger("Services.Common.AITCServer");
|
||||
|
||||
this.server = new HttpServer();
|
||||
this.port = null;
|
||||
this.users = {};
|
||||
this.autoCreateUsers = false;
|
||||
this.mockStatus = {
|
||||
code: null,
|
||||
method: null
|
||||
};
|
||||
this.onRequest = null;
|
||||
|
||||
this._appsAppHandlers = {
|
||||
GET: this._appsAppGetHandler,
|
||||
PUT: this._appsAppPutHandler,
|
||||
DELETE: this._appsAppDeleteHandler,
|
||||
};
|
||||
}
|
||||
AITCServer10Server.prototype = {
|
||||
ID_REGEX: /^[a-zA-Z0-9_-]{27}$/,
|
||||
VERSION_PATH: "/1.0/",
|
||||
|
||||
/**
|
||||
* Obtain the base URL the server can be accessed at as a string.
|
||||
*/
|
||||
get url() {
|
||||
// Is this available on the nsHttpServer instance?
|
||||
return "http://localhost:" + this.port + this.VERSION_PATH;
|
||||
},
|
||||
|
||||
/**
|
||||
* Start the server on a specified port.
|
||||
*/
|
||||
start: function start(port) {
|
||||
if (!port) {
|
||||
throw new Error("port argument must be specified.");
|
||||
}
|
||||
|
||||
this.port = port;
|
||||
|
||||
this.server.registerPrefixHandler(this.VERSION_PATH,
|
||||
this._generalHandler.bind(this));
|
||||
this.server.start(port);
|
||||
},
|
||||
|
||||
/**
|
||||
* Stop the server.
|
||||
*
|
||||
* Calls the specified callback when the server is stopped.
|
||||
*/
|
||||
stop: function stop(cb) {
|
||||
let handler = {onStopped: cb};
|
||||
|
||||
this.server.stop(handler);
|
||||
},
|
||||
|
||||
createUser: function createUser(username) {
|
||||
if (username in this.users) {
|
||||
throw new Error("User already exists: " + username);
|
||||
}
|
||||
|
||||
this._log.info("Registering user: " + username);
|
||||
|
||||
this.users[username] = new AITCServer10User();
|
||||
this.server.registerPrefixHandler(this.VERSION_PATH + username + "/",
|
||||
this._userHandler.bind(this, username));
|
||||
|
||||
return this.users[username];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns information for an individual user.
|
||||
*
|
||||
* The returned object contains functions to access and manipulate an
|
||||
* individual user.
|
||||
*/
|
||||
getUser: function getUser(username) {
|
||||
if (!(username in this.users)) {
|
||||
throw new Error("user is not present in server: " + username);
|
||||
}
|
||||
|
||||
return this.users[username];
|
||||
},
|
||||
|
||||
/**
|
||||
* Returns a specific status code for testing.
|
||||
*/
|
||||
_respondWithMockStatus: function _respondWithMockStatus(request, response) {
|
||||
response.setStatusLine(request.httpVersion, this.mockStatus.code,
|
||||
this.mockStatus.method);
|
||||
this._onRequest();
|
||||
},
|
||||
|
||||
_onRequest: function _onRequest() {
|
||||
if (typeof this.onRequest === 'function') {
|
||||
this.onRequest();
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* HTTP handler for requests to /1.0/ which don't have a specific user
|
||||
* registered.
|
||||
*/
|
||||
_generalHandler: function _generalHandler(request, response) {
|
||||
if (this.mockStatus.code && this.mockStatus.method) {
|
||||
this._respondWithMockStatus(request, response);
|
||||
return;
|
||||
}
|
||||
this._onRequest();
|
||||
let path = request.path;
|
||||
this._log.info("Request: " + request.method + " " + path);
|
||||
|
||||
if (path.indexOf(this.VERSION_PATH) != 0) {
|
||||
throw new Error("generalHandler invoked improperly.");
|
||||
}
|
||||
|
||||
let rest = request.path.substr(this.VERSION_PATH.length);
|
||||
if (!rest.length) {
|
||||
throw HTTP_404;
|
||||
}
|
||||
|
||||
if (!this.autoCreateUsers) {
|
||||
throw HTTP_404;
|
||||
}
|
||||
|
||||
let username;
|
||||
let index = rest.indexOf("/");
|
||||
if (index == -1) {
|
||||
username = rest;
|
||||
} else {
|
||||
username = rest.substr(0, index);
|
||||
}
|
||||
|
||||
this.createUser(username);
|
||||
this._userHandler(username, request, response);
|
||||
},
|
||||
|
||||
/**
|
||||
* HTTP handler for requests for a specific user.
|
||||
*
|
||||
* This handles request routing to the appropriate handler.
|
||||
*/
|
||||
_userHandler: function _userHandler(username, request, response) {
|
||||
if (this.mockStatus.code && this.mockStatus.method) {
|
||||
this._respondWithMockStatus(request, response);
|
||||
return;
|
||||
}
|
||||
this._onRequest();
|
||||
this._log.info("Request: " + request.method + " " + request.path);
|
||||
let path = request.path;
|
||||
let prefix = this.VERSION_PATH + username + "/";
|
||||
|
||||
if (path.indexOf(prefix) != 0) {
|
||||
throw new Error("userHandler invoked improperly.");
|
||||
}
|
||||
|
||||
let user = this.users[username];
|
||||
if (!user) {
|
||||
throw new Error("User handler should not have been invoked for an " +
|
||||
"unknown user!");
|
||||
}
|
||||
|
||||
let requestTime = Date.now();
|
||||
response.dispatchTime = requestTime;
|
||||
response.setHeader("X-Timestamp", "" + requestTime);
|
||||
|
||||
let handler;
|
||||
let remaining = path.substr(prefix.length);
|
||||
|
||||
if (remaining == "apps" || remaining == "apps/") {
|
||||
this._log.info("Dispatching to apps index handler.");
|
||||
handler = this._appsIndexHandler.bind(this, user, request, response);
|
||||
} else if (!remaining.indexOf("apps/")) {
|
||||
let id = remaining.substr("apps/".length);
|
||||
|
||||
this._log.info("Dispatching to app handler.");
|
||||
handler = this._appsAppHandler.bind(this, user, id, request, response);
|
||||
} else if (remaining == "devices" || !remaining.indexOf("devices/")) {
|
||||
this._log.info("Dispatching to devices handler.");
|
||||
handler = this._devicesHandler.bind(this, user,
|
||||
remaining.substr("devices".length),
|
||||
request, response);
|
||||
} else {
|
||||
throw HTTP_404;
|
||||
}
|
||||
|
||||
try {
|
||||
handler();
|
||||
} catch (ex) {
|
||||
if (ex instanceof HttpError) {
|
||||
response.setStatusLine(request.httpVersion, ex.code, ex.description);
|
||||
return;
|
||||
}
|
||||
|
||||
this._log.warn("Exception when processing request: " +
|
||||
CommonUtils.exceptionStr(ex));
|
||||
throw ex;
|
||||
}
|
||||
},
|
||||
|
||||
_appsIndexHandler: function _appsIndexHandler(user, request, response) {
|
||||
if (request.method != "GET") {
|
||||
response.setStatusLine(request.httpVersion, 405, "Method Not Allowed");
|
||||
response.setHeader("Accept", "GET");
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let options = this._getQueryStringParams(request);
|
||||
for (let key in options) {
|
||||
let value = options[key];
|
||||
|
||||
switch (key) {
|
||||
case "after":
|
||||
let time = parseInt(value, 10);
|
||||
if (isNaN(time)) {
|
||||
throw HTTP_400;
|
||||
}
|
||||
|
||||
options.after = time;
|
||||
break;
|
||||
|
||||
case "full":
|
||||
// Value is irrelevant.
|
||||
break;
|
||||
|
||||
default:
|
||||
this._log.info("Unknown query string parameter: " + key);
|
||||
throw HTTP_400;
|
||||
}
|
||||
}
|
||||
|
||||
let apps = [];
|
||||
let newest = 0;
|
||||
for each (let app in user.getApps(!("full" in options))) {
|
||||
if (app.modifiedAt > newest) {
|
||||
newest = app.modifiedAt;
|
||||
}
|
||||
|
||||
if ("after" in options && app.modifiedAt <= options.after) {
|
||||
continue;
|
||||
}
|
||||
|
||||
apps.push(app);
|
||||
}
|
||||
|
||||
if (request.hasHeader("X-If-Modified-Since")) {
|
||||
let modified = parseInt(request.getHeader("X-If-Modified-Since"), 10);
|
||||
if (modified >= newest) {
|
||||
response.setStatusLine(request.httpVersion, 304, "Not Modified");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let body = JSON.stringify({apps: apps});
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.setHeader("X-Last-Modified", "" + newest);
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
},
|
||||
|
||||
_appsAppHandler: function _appAppHandler(user, id, request, response) {
|
||||
if (!(request.method in this._appsAppHandlers)) {
|
||||
response.setStatusLine(request.httpVersion, 405, "Method Not Allowed");
|
||||
response.setHeader("Accept", Object.keys(this._appsAppHandlers).join(","));
|
||||
|
||||
return;
|
||||
}
|
||||
|
||||
let handler = this._appsAppHandlers[request.method];
|
||||
return handler.call(this, user, id, request, response);
|
||||
},
|
||||
|
||||
_appsAppGetHandler: function _appsAppGetHandler(user, id, request, response) {
|
||||
if (!user.hasAppID(id)) {
|
||||
throw HTTP_404;
|
||||
}
|
||||
|
||||
let app = user.getAppByID(id);
|
||||
|
||||
if (request.hasHeader("X-If-Modified-Since")) {
|
||||
let modified = parseInt(request.getHeader("X-If-Modified-Since"), 10);
|
||||
|
||||
this._log.debug("Client time: " + modified + "; Server time: " +
|
||||
app.modifiedAt);
|
||||
|
||||
if (modified >= app.modifiedAt) {
|
||||
response.setStatusLine(request.httpVersion, 304, "Not Modified");
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
let body = JSON.stringify(app);
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.setHeader("X-Last-Modified", "" + response.dispatchTime);
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
},
|
||||
|
||||
_appsAppPutHandler: function _appsAppPutHandler(user, id, request, response) {
|
||||
if (!request.hasHeader("Content-Type")) {
|
||||
this._log.info("Request does not have Content-Type header.");
|
||||
throw HTTP_400;
|
||||
}
|
||||
|
||||
let ct = request.getHeader("Content-Type");
|
||||
if (ct != "application/json" && ct.indexOf("application/json;") !== 0) {
|
||||
this._log.info("Unknown media type: " + ct);
|
||||
// TODO proper response headers.
|
||||
throw HTTP_415;
|
||||
}
|
||||
|
||||
let requestBody = CommonUtils.readBytesFromInputStream(request.bodyInputStream);
|
||||
this._log.debug("Request body: " + requestBody);
|
||||
if (requestBody.length > 8192) {
|
||||
this._log.info("Request body too long: " + requestBody.length);
|
||||
throw HTTP_413;
|
||||
}
|
||||
|
||||
let hadApp = user.hasAppID(id);
|
||||
|
||||
let app;
|
||||
try {
|
||||
app = JSON.parse(requestBody);
|
||||
} catch (e) {
|
||||
this._log.info("JSON parse error.");
|
||||
throw HTTP_400;
|
||||
}
|
||||
|
||||
// URL and record mismatch.
|
||||
if (user.originToID(app.origin) != id) {
|
||||
this._log.warn("URL ID and origin mismatch. URL: " + id + "; Record: " +
|
||||
user.originToID(app.origin));
|
||||
throw HTTP_403;
|
||||
}
|
||||
|
||||
if (request.hasHeader("X-If-Unmodified-Since") && hadApp) {
|
||||
let modified = parseInt(request.getHeader("X-If-Unmodified-Since"), 10);
|
||||
let existing = user.getAppByID(id);
|
||||
|
||||
if (existing.modifiedAt > modified) {
|
||||
this._log.info("Server modified after client.");
|
||||
throw HTTP_412;
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
app.modifiedAt = response.dispatchTime;
|
||||
|
||||
if (hadApp) {
|
||||
app.installedAt = user.getAppByID(id).installedAt;
|
||||
} else {
|
||||
app.installedAt = response.dispatchTime;
|
||||
}
|
||||
|
||||
user.addApp(app);
|
||||
} catch (e) {
|
||||
this._log.info("Error adding app: " + CommonUtils.exceptionStr(e));
|
||||
throw HTTP_400;
|
||||
}
|
||||
|
||||
let code = 201;
|
||||
let status = "Created";
|
||||
|
||||
if (hadApp) {
|
||||
code = 204;
|
||||
status = "No Content";
|
||||
}
|
||||
|
||||
response.setHeader("X-Last-Modified", "" + response.dispatchTime);
|
||||
response.setStatusLine(request.httpVersion, code, status);
|
||||
},
|
||||
|
||||
_appsAppDeleteHandler: function _appsAppDeleteHandler(user, id, request,
|
||||
response) {
|
||||
if (!user.hasAppID(id)) {
|
||||
throw HTTP_404;
|
||||
}
|
||||
|
||||
let existing = user.getAppByID(id);
|
||||
if (request.hasHeader("X-If-Unmodified-Since")) {
|
||||
let modified = parseInt(request.getHeader("X-If-Unmodified-Since"), 10);
|
||||
|
||||
if (existing.modifiedAt > modified) {
|
||||
throw HTTP_412;
|
||||
}
|
||||
}
|
||||
|
||||
user.deleteAppWithID(id);
|
||||
|
||||
response.setHeader("X-Last-Modified", "" + response.dispatchTime);
|
||||
response.setStatusLine(request.httpVersion, 204, "No Content");
|
||||
},
|
||||
|
||||
_devicesHandler: function _devicesHandler(user, path, request, response) {
|
||||
// TODO need to support full API.
|
||||
// For now, we just assume it is a request for /.
|
||||
response.setHeader("Content-Type", "application/json");
|
||||
let body = JSON.stringify({devices: []});
|
||||
|
||||
response.setStatusLine(request.httpVersion, 200, "OK");
|
||||
response.bodyOutputStream.write(body, body.length);
|
||||
},
|
||||
|
||||
// Surely this exists elsewhere in the Mozilla source tree...
|
||||
_getQueryStringParams: function _getQueryStringParams(request) {
|
||||
let params = {};
|
||||
for each (let chunk in request.queryString.split("&")) {
|
||||
if (!chunk) {
|
||||
continue;
|
||||
}
|
||||
|
||||
let parts = chunk.split("=");
|
||||
// TODO URL decode key and value.
|
||||
if (parts.length == 1) {
|
||||
params[parts[0]] = "";
|
||||
} else {
|
||||
params[parts[0]] = parts[1];
|
||||
}
|
||||
}
|
||||
|
||||
return params;
|
||||
},
|
||||
};
|
||||
|
@ -1,26 +0,0 @@
|
||||
/* This Source Code Form is subject to the terms of the Mozilla Public
|
||||
* License, v. 2.0. If a copy of the MPL was not distributed with this file,
|
||||
* You can obtain one at http://mozilla.org/MPL/2.0/. */
|
||||
|
||||
/**
|
||||
* This file runs a standalone AITC server.
|
||||
*
|
||||
* It is meant to be executed with an xpcshell.
|
||||
*
|
||||
* The Makefile in this directory contains a target to run it:
|
||||
*
|
||||
* $ make aitc-server
|
||||
*/
|
||||
|
||||
Cu.import("resource://testing-common/services-common/aitcserver.js");
|
||||
|
||||
initTestLogging();
|
||||
|
||||
let server = new AITCServer10Server();
|
||||
server.autoCreateUsers = true;
|
||||
server.start(SERVER_PORT);
|
||||
|
||||
_("AITC server started on port " + SERVER_PORT);
|
||||
|
||||
// Launch the thread manager.
|
||||
_do_main();
|
@ -44,7 +44,7 @@ function addResourceAlias() {
|
||||
const handler = Services.io.getProtocolHandler("resource")
|
||||
.QueryInterface(Ci.nsIResProtocolHandler);
|
||||
|
||||
let modules = ["aitc", "common", "crypto"];
|
||||
let modules = ["common", "crypto"];
|
||||
for each (let module in modules) {
|
||||
let uri = Services.io.newURI("resource://gre/modules/services-" + module + "/",
|
||||
null, null);
|
||||
|
@ -1,190 +0,0 @@
|
||||
/* Any copyright is dedicated to the Public Domain.
|
||||
http://creativecommons.org/publicdomain/zero/1.0/ */
|
||||
|
||||
"use strict";
|
||||
|
||||
Cu.import("resource://services-common/rest.js");
|
||||
Cu.import("resource://services-common/utils.js");
|
||||
|
||||
Cu.import("resource://testing-common/services-common/aitcserver.js");
|
||||
|
||||
function run_test() {
|
||||
initTestLogging("Trace");
|
||||
run_next_test();
|
||||
}
|
||||
|
||||
function get_aitc_server() {
|
||||
let server = new AITCServer10Server();
|
||||
server.start(get_server_port());
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
function get_server_with_user(username) {
|
||||
let server = get_aitc_server();
|
||||
server.createUser(username);
|
||||
|
||||
return server;
|
||||
}
|
||||
|
||||
add_test(function test_origin_conversion() {
|
||||
let mapping = {
|
||||
"www.mozilla.org": "xSMmiFEpg4b4TRtzJZd6Mvy4hGc",
|
||||
"foo": "C-7Hteo_D9vJXQ3UfzxbwnXaijM",
|
||||
};
|
||||
|
||||
for (let k in mapping) {
|
||||
do_check_eq(AITCServer10User.prototype.originToID(k), mapping[k]);
|
||||
}
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_empty_user() {
|
||||
_("Ensure user instances can be created.");
|
||||
|
||||
let user = new AITCServer10User();
|
||||
|
||||
let apps = user.getApps();
|
||||
do_check_eq([app for (app in apps)].length, 0);
|
||||
do_check_false(user.hasAppID("foobar"));
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_user_add_app() {
|
||||
_("Ensure apps can be added to users.");
|
||||
|
||||
let user = new AITCServer10User();
|
||||
let threw = false;
|
||||
try {
|
||||
user.addApp({});
|
||||
} catch (ex) {
|
||||
threw = true;
|
||||
} finally {
|
||||
do_check_true(threw);
|
||||
threw = false;
|
||||
}
|
||||
|
||||
run_next_test();
|
||||
});
|
||||
|
||||
add_test(function test_server_run() {
|
||||
_("Ensure server can be started properly.");
|
||||
|
||||
let server = new AITCServer10Server();
|
||||
server.start(get_server_port());
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
|
||||
add_test(function test_create_user() {
|
||||
_("Ensure users can be created properly.");
|
||||
|
||||
let server = get_aitc_server();
|
||||
|
||||
let u1 = server.createUser("123");
|
||||
do_check_true(u1 instanceof AITCServer10User);
|
||||
|
||||
let u2 = server.getUser("123");
|
||||
do_check_eq(u1, u2);
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
|
||||
add_test(function test_empty_server_404() {
|
||||
_("Ensure empty server returns 404.");
|
||||
|
||||
let server = get_aitc_server();
|
||||
let request = new RESTRequest(server.url + "123/");
|
||||
request.get(function onComplete(error) {
|
||||
do_check_eq(this.response.status, 404);
|
||||
|
||||
let request = new RESTRequest(server.url + "123/apps/");
|
||||
request.get(function onComplete(error) {
|
||||
do_check_eq(this.response.status, 404);
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_empty_user_apps() {
|
||||
_("Ensure apps request for empty user has appropriate content.");
|
||||
|
||||
const username = "123";
|
||||
|
||||
let server = get_server_with_user(username);
|
||||
let request = new RESTRequest(server.url + username + "/apps/");
|
||||
_("Performing request...");
|
||||
request.get(function onComplete(error) {
|
||||
_("Got response");
|
||||
do_check_eq(error, null);
|
||||
|
||||
do_check_eq(200, this.response.status);
|
||||
let headers = this.response.headers;
|
||||
do_check_true("content-type" in headers);
|
||||
do_check_eq(headers["content-type"], "application/json");
|
||||
do_check_true("x-timestamp" in headers);
|
||||
|
||||
let body = this.response.body;
|
||||
let parsed = JSON.parse(body);
|
||||
do_check_attribute_count(parsed, 1);
|
||||
do_check_true("apps" in parsed);
|
||||
do_check_true(Array.isArray(parsed.apps));
|
||||
do_check_eq(parsed.apps.length, 0);
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
});
|
||||
|
||||
add_test(function test_invalid_request_method() {
|
||||
_("Ensure HTTP 405 works as expected.");
|
||||
|
||||
const username = "12345";
|
||||
|
||||
let server = get_server_with_user(username);
|
||||
let request = new RESTRequest(server.url + username + "/apps/foobar");
|
||||
request.dispatch("SILLY", null, function onComplete(error) {
|
||||
do_check_eq(error, null);
|
||||
do_check_eq(this.response.status, 405);
|
||||
|
||||
let headers = this.response.headers;
|
||||
do_check_true("accept" in headers);
|
||||
|
||||
let allowed = new Set();
|
||||
|
||||
for (let method of headers["accept"].split(",")) {
|
||||
allowed.add(method);
|
||||
}
|
||||
|
||||
do_check_eq(allowed.size, 3);
|
||||
for (let method of ["GET", "PUT", "DELETE"]) {
|
||||
do_check_true(allowed.has(method));
|
||||
}
|
||||
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
});
|
||||
add_test(function test_respond_with_mock_status() {
|
||||
let username = "123"
|
||||
let server = get_server_with_user(username);
|
||||
server.mockStatus = {
|
||||
code: 405,
|
||||
method: "Method Not Allowed"
|
||||
};
|
||||
let request = new RESTRequest(server.url);
|
||||
request.dispatch("GET", null, function onComplete(error){
|
||||
do_check_eq(this.response.status, 405);
|
||||
|
||||
server.mockStatus = {
|
||||
code: 399,
|
||||
method: "Self Destruct"
|
||||
};
|
||||
let request2 = new RESTRequest(server.url);
|
||||
request2.dispatch("GET", null, function onComplete(error){
|
||||
do_check_eq(this.response.status, 399);
|
||||
server.stop(run_next_test);
|
||||
});
|
||||
});
|
||||
});
|
@ -14,7 +14,6 @@ const modules = [
|
||||
];
|
||||
|
||||
const test_modules = [
|
||||
"aitcserver.js",
|
||||
"bagheeraserver.js",
|
||||
"logging.js",
|
||||
"storageserver.js",
|
||||
|
@ -19,9 +19,6 @@ tail =
|
||||
[test_utils_utf8.js]
|
||||
[test_utils_uuid.js]
|
||||
|
||||
[test_aitc_server.js]
|
||||
# Bug 752243: Profile cleanup frequently fails
|
||||
skip-if = os == "mac" || os == "linux"
|
||||
[test_async_chain.js]
|
||||
[test_async_querySpinningly.js]
|
||||
[test_bagheera_server.js]
|
||||
|
@ -4,7 +4,6 @@
|
||||
|
||||
add_makefiles "
|
||||
services/Makefile
|
||||
services/aitc/Makefile
|
||||
services/common/Makefile
|
||||
services/crypto/Makefile
|
||||
services/crypto/component/Makefile
|
||||
@ -17,7 +16,6 @@ add_makefiles "
|
||||
|
||||
if [ "$ENABLE_TESTS" ]; then
|
||||
add_makefiles "
|
||||
services/aitc/tests/Makefile
|
||||
services/common/tests/Makefile
|
||||
services/crypto/tests/Makefile
|
||||
services/healthreport/tests/Makefile
|
||||
|
@ -85,7 +85,6 @@ skip-if = os == "android"
|
||||
[include:content/base/test/unit/xpcshell.ini]
|
||||
[include:content/test/unit/xpcshell.ini]
|
||||
[include:toolkit/components/url-classifier/tests/unit/xpcshell.ini]
|
||||
[include:services/aitc/tests/unit/xpcshell.ini]
|
||||
[include:services/common/tests/unit/xpcshell.ini]
|
||||
[include:services/crypto/tests/unit/xpcshell.ini]
|
||||
[include:services/crypto/components/tests/unit/xpcshell.ini]
|
||||
|
@ -12,7 +12,6 @@ MOZ_APP_VERSION=$MOZILLA_VERSION
|
||||
MOZ_PLACES=1
|
||||
MOZ_EXTENSIONS_DEFAULT=" gio"
|
||||
MOZ_URL_CLASSIFIER=1
|
||||
MOZ_SERVICES_AITC=1
|
||||
MOZ_SERVICES_COMMON=1
|
||||
MOZ_SERVICES_CRYPTO=1
|
||||
MOZ_SERVICES_METRICS=1
|
||||
|
Loading…
x
Reference in New Issue
Block a user