Bug 836206 - Remove AITC client. r=gps

This commit is contained in:
Richard Newman 2013-02-03 00:51:26 -08:00
parent 082460c692
commit eab35762ba
34 changed files with 4 additions and 4047 deletions

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -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

View File

@ -14,10 +14,6 @@ PARALLEL_DIRS += \
crypto \
$(NULL)
ifdef MOZ_SERVICES_AITC
PARALLEL_DIRS += aitc
endif
ifdef MOZ_SERVICES_HEALTHREPORT
PARALLEL_DIRS += healthreport
endif

View File

@ -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);

View File

@ -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/

View File

@ -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

View File

@ -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();
});

View File

@ -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");
}
},
};

View File

@ -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;
}
},
};

View File

@ -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
});
},
};

View File

@ -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();
});

View File

@ -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");

View File

@ -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

View File

@ -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();
}

View File

@ -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>

View File

@ -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);
}

View File

@ -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);
});
});
});

View File

@ -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());
});
}
});

View File

@ -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, {});
}
}

View File

@ -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();
});
});
});

View File

@ -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();
});
});

View File

@ -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]

View File

@ -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)

View File

@ -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;
},
};

View File

@ -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();

View File

@ -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);

View File

@ -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);
});
});
});

View File

@ -14,7 +14,6 @@ const modules = [
];
const test_modules = [
"aitcserver.js",
"bagheeraserver.js",
"logging.js",
"storageserver.js",

View File

@ -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]

View File

@ -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

View File

@ -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]

View File

@ -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