Merge services-central into mozilla-central
authorGregory Szorc <gps@mozilla.com>
Wed, 11 Apr 2012 11:46:19 -0700
changeset 94701 1711e06ca9f7b8cba7ecb27a48b9fd8ad1980a26
parent 94694 3930e47f14ae61a8c72c07e45cb28a5ce3ecbe51 (current diff)
parent 94700 e9bfc7255afde4d7629210b699c3887b5ccb415a (diff)
child 94702 8eb367e5b0531480085fe5c96c0cbae7ad4ada28
child 94805 b774546790d72c97debc66afc69ebd9f8a6bcfa9
child 97501 86eef96edae3f90585f373046ca123b4c26b0ec0
child 112409 8ee84aca3656e2a9314b8320882497e24c506889
push id886
push userlsblakk@mozilla.com
push dateMon, 04 Jun 2012 19:57:52 +0000
treeherdermozilla-beta@bbd8d5efd6d1 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone14.0a1
first release with
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
last release without
nightly linux32
nightly linux64
nightly mac
nightly win32
nightly win64
Merge services-central into mozilla-central
services/sync/modules/async.js
services/sync/modules/ext/Observers.js
services/sync/modules/ext/Preferences.js
services/sync/modules/ext/StringBundle.js
services/sync/modules/log4moz.js
services/sync/tests/unit/test_Observers.js
services/sync/tests/unit/test_Preferences.js
services/sync/tests/unit/test_async_chain.js
services/sync/tests/unit/test_async_querySpinningly.js
services/sync/tests/unit/test_log4moz.js
services/sync/tests/unit/test_restrequest.js
services/sync/tests/unit/test_utils_atob.js
services/sync/tests/unit/test_utils_makeURI.js
services/sync/tests/unit/test_utils_namedTimer.js
services/sync/tests/unit/test_utils_stackTrace.js
services/sync/tests/unit/test_utils_utf8.js
--- a/browser/base/content/sync/notification.xml
+++ b/browser/base/content/sync/notification.xml
@@ -52,27 +52,27 @@
         <children includes="notification"/>
       </xul:vbox>
       <children/>
     </content>
 
     <implementation>
       <constructor><![CDATA[
         let temp = {};
-        Cu.import("resource://services-sync/ext/Observers.js", temp);
+        Cu.import("resource://services-common/observers.js", temp);
         temp.Observers.add("weave:notification:added", this.onNotificationAdded, this);
         temp.Observers.add("weave:notification:removed", this.onNotificationRemoved, this);
 
         for each (var notification in Weave.Notifications.notifications)
           this._appendNotification(notification);
       ]]></constructor>
 
       <destructor><![CDATA[
         let temp = {};
-        Cu.import("resource://services-sync/ext/Observers.js", temp);
+        Cu.import("resource://services-common/observers.js", temp);
         temp.Observers.remove("weave:notification:added", this.onNotificationAdded, this);
         temp.Observers.remove("weave:notification:removed", this.onNotificationRemoved, this);
       ]]></destructor>
 
       <method name="onNotificationAdded">
         <parameter name="subject"/>
         <parameter name="data"/>
         <body><![CDATA[
--- a/browser/installer/removed-files.in
+++ b/browser/installer/removed-files.in
@@ -1026,16 +1026,23 @@ xpicleanup@BIN_SUFFIX@
   modules/PlacesUIUtils.jsm
   modules/PlacesUtils.jsm
   modules/PluginProvider.jsm
   modules/PluralForm.jsm
   modules/PopupNotifications.jsm
   modules/PropertyPanel.jsm
   modules/reflect.jsm
   modules/Services.jsm
+  modules/services-common/async.js
+  modules/services-common/log4moz.js
+  modules/services-common/observers.js
+  modules/services-common/preferences.js
+  modules/services-common/rest.js
+  modules/services-common/stringbundle.js
+  modules/services-common/utils.js
   modules/services-sync/auth.js
   modules/services-sync/base_records/collection.js
   modules/services-sync/base_records/crypto.js
   modules/services-sync/base_records/keys.js
   modules/services-sync/base_records/wbo.js
   modules/services-sync/constants.js
   modules/services-sync/engines/bookmarks.js
   modules/services-sync/engines/clients.js
--- a/services/Makefile.in
+++ b/services/Makefile.in
@@ -38,12 +38,12 @@
 DEPTH     = ..
 topsrcdir = @top_srcdir@
 srcdir    = @srcdir@
 VPATH     = @srcdir@
 
 include $(DEPTH)/config/autoconf.mk
 
 ifdef MOZ_SERVICES_SYNC
-PARALLEL_DIRS += crypto sync
+PARALLEL_DIRS += common crypto sync
 endif
 
 include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/services/common/Makefile.in
@@ -0,0 +1,34 @@
+# 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     = ../..
+topsrcdir = @top_srcdir@
+srcdir    = @srcdir@
+VPATH     = @srcdir@
+
+include $(DEPTH)/config/autoconf.mk
+
+PREF_JS_EXPORTS = $(srcdir)/services-common.js
+
+modules := \
+  async.js \
+  log4moz.js \
+  observers.js \
+  preferences.js \
+  rest.js \
+  stringbundle.js \
+  tokenserverclient.js \
+  utils.js \
+  $(NULL)
+
+source_modules = $(foreach module,$(modules),$(srcdir)/$(module))
+module_dir = $(FINAL_TARGET)/modules/services-common
+
+libs::
+	$(NSINSTALL) -D $(module_dir)
+	$(NSINSTALL) -l $(source_modules) $(module_dir)
+
+TEST_DIRS += tests
+
+include $(topsrcdir)/config/rules.mk
rename from services/sync/modules/async.js
rename to services/common/async.js
--- a/services/sync/modules/async.js
+++ b/services/common/async.js
@@ -1,51 +1,15 @@
-/* ***** BEGIN LICENSE BLOCK *****
- * Version: MPL 1.1/GPL 2.0/LGPL 2.1
- *
- * The contents of this file are subject to the Mozilla Public License Version
- * 1.1 (the "License"); you may not use this file except in compliance with
- * the License. You may obtain a copy of the License at
- * http://www.mozilla.org/MPL/
- *
- * Software distributed under the License is distributed on an "AS IS" basis,
- * WITHOUT WARRANTY OF ANY KIND, either express or implied. See the License
- * for the specific language governing rights and limitations under the
- * License.
- *
- * The Original Code is Firefox Sync.
- *
- * The Initial Developer of the Original Code is
- * the Mozilla Foundation.
- * Portions created by the Initial Developer are Copyright (C) 2011
- * the Initial Developer. All Rights Reserved.
- *
- * Contributor(s):
- *  Richard Newman <rnewman@mozilla.com>
- *
- * Alternatively, the contents of this file may be used under the terms of
- * either the GNU General Public License Version 2 or later (the "GPL"), or
- * the GNU Lesser General Public License Version 2.1 or later (the "LGPL"),
- * in which case the provisions of the GPL or the LGPL are applicable instead
- * of those above. If you wish to allow use of your version of this file only
- * under the terms of either the GPL or the LGPL, and not to allow others to
- * use your version of this file under the terms of the MPL, indicate your
- * decision by deleting the provisions above and replace them with the notice
- * and other provisions required by the GPL or the LGPL. If you do not delete
- * the provisions above, a recipient may use your version of this file under
- * the terms of any one of the MPL, the GPL or the LGPL.
- *
- * ***** END LICENSE BLOCK ***** */
+/* 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/. */
 
-const EXPORTED_SYMBOLS = ['Async'];
+const EXPORTED_SYMBOLS = ["Async"];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
 // Constants for makeSyncCallback, waitForSyncCallback.
 const CB_READY = {};
 const CB_COMPLETE = {};
 const CB_FAIL = {};
 
 const REASON_ERROR = Ci.mozIStorageStatementCallback.REASON_ERROR;
 
@@ -56,20 +20,20 @@ Cu.import("resource://gre/modules/Servic
  */
 let Async = {
 
   /**
    * Execute an arbitrary number of asynchronous functions one after the
    * other, passing the callback arguments on to the next one.  All functions
    * must take a callback function as their last argument.  The 'this' object
    * will be whatever chain()'s is.
-   * 
+   *
    * @usage this._chain = Async.chain;
    *        this._chain(this.foo, this.bar, this.baz)(args, for, foo)
-   * 
+   *
    * This is equivalent to:
    *
    *   let self = this;
    *   self.foo(args, for, foo, function (bars, args) {
    *     self.bar(bars, args, function (baz, params) {
    *       self.baz(baz, params);
    *     });
    *   });
rename from services/sync/modules/log4moz.js
rename to services/common/log4moz.js
--- a/services/sync/modules/log4moz.js
+++ b/services/common/log4moz.js
@@ -34,20 +34,17 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 const EXPORTED_SYMBOLS = ['Log4Moz'];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
 const ONE_BYTE = 1;
 const ONE_KILOBYTE = 1024 * ONE_BYTE;
 const ONE_MEGABYTE = 1024 * ONE_KILOBYTE;
 
 const STREAM_SEGMENT_SIZE = 4096;
 const PR_UINT32_MAX = 0xffffffff;
 
@@ -368,17 +365,17 @@ Formatter.prototype = {
 function BasicFormatter(dateFormat) {
   if (dateFormat)
     this.dateFormat = dateFormat;
 }
 BasicFormatter.prototype = {
   __proto__: Formatter.prototype,
 
   format: function BF_format(message) {
-    return message.time + "\t" + message.loggerName + "\t" + message.levelDesc 
+    return message.time + "\t" + message.loggerName + "\t" + message.levelDesc
            + "\t" + message.message + "\n";
   }
 };
 
 /*
  * Appenders
  * These can be attached to Loggers to log to different places
  * Simply subclass and override doAppend to implement a new one
@@ -437,17 +434,17 @@ ConsoleAppender.prototype = {
     }
     Cc["@mozilla.org/consoleservice;1"].
       getService(Ci.nsIConsoleService).logStringMessage(message);
   }
 };
 
 /**
  * Base implementation for stream based appenders.
- * 
+ *
  * Caution: This writes to the output stream synchronously, thus logging calls
  * block as the data is written to the stream. This can have negligible impact
  * for in-memory streams, but should be taken into account for I/O streams
  * (files, network, etc.)
  */
 function BlockingStreamAppender(formatter) {
   this._name = "BlockingStreamAppender";
   Appender.call(this, formatter);
@@ -455,17 +452,17 @@ function BlockingStreamAppender(formatte
 BlockingStreamAppender.prototype = {
   __proto__: Appender.prototype,
 
   _converterStream: null, // holds the nsIConverterOutputStream
   _outputStream: null,    // holds the underlying nsIOutputStream
 
   /**
    * Output stream to write to.
-   * 
+   *
    * This will automatically open the stream if it doesn't exist yet by
    * calling newOutputStream. The resulting raw stream is wrapped in a
    * nsIConverterOutputStream to ensure text is written as UTF-8.
    */
   get outputStream() {
     if (!this._outputStream) {
       // First create a raw stream. We can bail out early if that fails.
       this._outputStream = this.newOutputStream();
@@ -476,17 +473,17 @@ BlockingStreamAppender.prototype = {
       // Wrap the raw stream in an nsIConverterOutputStream. We can reuse
       // the instance if we already have one.
       if (!this._converterStream) {
         this._converterStream = Cc["@mozilla.org/intl/converter-output-stream;1"]
                                   .createInstance(Ci.nsIConverterOutputStream);
       }
       this._converterStream.init(
         this._outputStream, "UTF-8", STREAM_SEGMENT_SIZE,
-        Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);      
+        Ci.nsIConverterInputStream.DEFAULT_REPLACEMENT_CHARACTER);
     }
     return this._converterStream;
   },
 
   newOutputStream: function newOutputStream() {
     throw "Stream-based appenders need to implement newOutputStream()!";
   },
 
@@ -516,27 +513,27 @@ BlockingStreamAppender.prototype = {
         }
       }
     }
   }
 };
 
 /**
  * Append to an nsIStorageStream
- * 
+ *
  * This writes logging output to an in-memory stream which can later be read
  * back as an nsIInputStream. It can be used to avoid expensive I/O operations
  * during logging. Instead, one can periodically consume the input stream and
  * e.g. write it to disk asynchronously.
  */
 function StorageStreamAppender(formatter) {
   this._name = "StorageStreamAppender";
   BlockingStreamAppender.call(this, formatter);
 }
-StorageStreamAppender.prototype = { 
+StorageStreamAppender.prototype = {
   __proto__: BlockingStreamAppender.prototype,
 
   _ss: null,
   newOutputStream: function newOutputStream() {
     let ss = this._ss = Cc["@mozilla.org/storagestream;1"]
                           .createInstance(Ci.nsIStorageStream);
     ss.init(STREAM_SEGMENT_SIZE, PR_UINT32_MAX, null);
     return ss.getOutputStream(0);
@@ -585,17 +582,17 @@ FileAppender.prototype = {
     } catch (e) {
       // File didn't exist in the first place, or we're on Windows. Meh.
     }
   }
 };
 
 /**
  * Rotating file appender (discouraged)
- * 
+ *
  * Similar to FileAppender, but rotates logs when they become too large.
  */
 function RotatingFileAppender(file, formatter, maxSize, maxBackups) {
   if (maxSize === undefined)
     maxSize = ONE_MEGABYTE * 2;
 
   if (maxBackups === undefined)
     maxBackups = 0;
rename from services/sync/modules/ext/Observers.js
rename to services/common/observers.js
rename from services/sync/modules/ext/Preferences.js
rename to services/common/preferences.js
new file mode 100644
--- /dev/null
+++ b/services/common/rest.js
@@ -0,0 +1,578 @@
+/* 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/. */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const EXPORTED_SYMBOLS = ["RESTRequest", "RESTResponse"];
+
+Cu.import("resource://gre/modules/Services.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/utils.js");
+
+const Prefs = new Preferences("services.common.rest.");
+
+/**
+ * Single use HTTP requests to RESTish resources.
+ *
+ * @param uri
+ *        URI for the request. This can be an nsIURI object or a string
+ *        that can be used to create one. An exception will be thrown if
+ *        the string is not a valid URI.
+ *
+ * Examples:
+ *
+ * (1) Quick GET request:
+ *
+ *   new RESTRequest("http://server/rest/resource").get(function (error) {
+ *     if (error) {
+ *       // Deal with a network error.
+ *       processNetworkErrorCode(error.result);
+ *       return;
+ *     }
+ *     if (!this.response.success) {
+ *       // Bail out if we're not getting an HTTP 2xx code.
+ *       processHTTPError(this.response.status);
+ *       return;
+ *     }
+ *     processData(this.response.body);
+ *   });
+ *
+ * (2) Quick PUT request (non-string data is automatically JSONified)
+ *
+ *   new RESTRequest("http://server/rest/resource").put(data, function (error) {
+ *     ...
+ *   });
+ *
+ * (3) Streaming GET
+ *
+ *   let request = new RESTRequest("http://server/rest/resource");
+ *   request.setHeader("Accept", "application/newlines");
+ *   request.onComplete = function (error) {
+ *     if (error) {
+ *       // Deal with a network error.
+ *       processNetworkErrorCode(error.result);
+ *       return;
+ *     }
+ *     callbackAfterRequestHasCompleted()
+ *   });
+ *   request.onProgress = function () {
+ *     if (!this.response.success) {
+ *       // Bail out if we're not getting an HTTP 2xx code.
+ *       return;
+ *     }
+ *     // Process body data and reset it so we don't process the same data twice.
+ *     processIncrementalData(this.response.body);
+ *     this.response.body = "";
+ *   });
+ *   request.get();
+ */
+function RESTRequest(uri) {
+  this.status = this.NOT_SENT;
+
+  // If we don't have an nsIURI object yet, make one. This will throw if
+  // 'uri' isn't a valid URI string.
+  if (!(uri instanceof Ci.nsIURI)) {
+    uri = Services.io.newURI(uri, null, null);
+  }
+  this.uri = uri;
+
+  this._headers = {};
+  this._log = Log4Moz.repository.getLogger(this._logName);
+  this._log.level =
+    Log4Moz.Level[Prefs.get("log.logger.rest.request")];
+}
+RESTRequest.prototype = {
+
+  _logName: "Services.Common.RESTRequest",
+
+  QueryInterface: XPCOMUtils.generateQI([
+    Ci.nsIBadCertListener2,
+    Ci.nsIInterfaceRequestor,
+    Ci.nsIChannelEventSink
+  ]),
+
+  /*** Public API: ***/
+
+  /**
+   * URI for the request (an nsIURI object).
+   */
+  uri: null,
+
+  /**
+   * HTTP method (e.g. "GET")
+   */
+  method: null,
+
+  /**
+   * RESTResponse object
+   */
+  response: null,
+
+  /**
+   * nsIRequest load flags. Don't do any caching by default.
+   */
+  loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING,
+
+  /**
+   * nsIHttpChannel
+   */
+  channel: null,
+
+  /**
+   * Flag to indicate the status of the request.
+   *
+   * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED.
+   */
+  status: null,
+
+  NOT_SENT:    0,
+  SENT:        1,
+  IN_PROGRESS: 2,
+  COMPLETED:   4,
+  ABORTED:     8,
+
+  /**
+   * Request timeout (in seconds, though decimal values can be used for
+   * up to millisecond granularity.)
+   *
+   * 0 for no timeout.
+   */
+  timeout: null,
+
+  /**
+   * Called when the request has been completed, including failures and
+   * timeouts.
+   *
+   * @param error
+   *        Error that occurred while making the request, null if there
+   *        was no error.
+   */
+  onComplete: function onComplete(error) {
+  },
+
+  /**
+   * Called whenever data is being received on the channel. If this throws an
+   * exception, the request is aborted and the exception is passed as the
+   * error to onComplete().
+   */
+  onProgress: function onProgress() {
+  },
+
+  /**
+   * Set a request header.
+   */
+  setHeader: function setHeader(name, value) {
+    this._headers[name.toLowerCase()] = value;
+  },
+
+  /**
+   * Perform an HTTP GET.
+   *
+   * @param onComplete
+   *        Short-circuit way to set the 'onComplete' method. Optional.
+   * @param onProgress
+   *        Short-circuit way to set the 'onProgress' method. Optional.
+   *
+   * @return the request object.
+   */
+  get: function get(onComplete, onProgress) {
+    return this.dispatch("GET", null, onComplete, onProgress);
+  },
+
+  /**
+   * Perform an HTTP PUT.
+   *
+   * @param data
+   *        Data to be used as the request body. If this isn't a string
+   *        it will be JSONified automatically.
+   * @param onComplete
+   *        Short-circuit way to set the 'onComplete' method. Optional.
+   * @param onProgress
+   *        Short-circuit way to set the 'onProgress' method. Optional.
+   *
+   * @return the request object.
+   */
+  put: function put(data, onComplete, onProgress) {
+    return this.dispatch("PUT", data, onComplete, onProgress);
+  },
+
+  /**
+   * Perform an HTTP POST.
+   *
+   * @param data
+   *        Data to be used as the request body. If this isn't a string
+   *        it will be JSONified automatically.
+   * @param onComplete
+   *        Short-circuit way to set the 'onComplete' method. Optional.
+   * @param onProgress
+   *        Short-circuit way to set the 'onProgress' method. Optional.
+   *
+   * @return the request object.
+   */
+  post: function post(data, onComplete, onProgress) {
+    return this.dispatch("POST", data, onComplete, onProgress);
+  },
+
+  /**
+   * Perform an HTTP DELETE.
+   *
+   * @param onComplete
+   *        Short-circuit way to set the 'onComplete' method. Optional.
+   * @param onProgress
+   *        Short-circuit way to set the 'onProgress' method. Optional.
+   *
+   * @return the request object.
+   */
+  delete: function delete_(onComplete, onProgress) {
+    return this.dispatch("DELETE", null, onComplete, onProgress);
+  },
+
+  /**
+   * Abort an active request.
+   */
+  abort: function abort() {
+    if (this.status != this.SENT && this.status != this.IN_PROGRESS) {
+      throw "Can only abort a request that has been sent.";
+    }
+
+    this.status = this.ABORTED;
+    this.channel.cancel(Cr.NS_BINDING_ABORTED);
+
+    if (this.timeoutTimer) {
+      // Clear the abort timer now that the channel is done.
+      this.timeoutTimer.clear();
+    }
+  },
+
+  /*** Implementation stuff ***/
+
+  dispatch: function dispatch(method, data, onComplete, onProgress) {
+    if (this.status != this.NOT_SENT) {
+      throw "Request has already been sent!";
+    }
+
+    this.method = method;
+    if (onComplete) {
+      this.onComplete = onComplete;
+    }
+    if (onProgress) {
+      this.onProgress = onProgress;
+    }
+
+    // Create and initialize HTTP channel.
+    let channel = Services.io.newChannelFromURI(this.uri, null, null)
+                          .QueryInterface(Ci.nsIRequest)
+                          .QueryInterface(Ci.nsIHttpChannel);
+    this.channel = channel;
+    channel.loadFlags |= this.loadFlags;
+    channel.notificationCallbacks = this;
+
+    // Set request headers.
+    let headers = this._headers;
+    for (let key in headers) {
+      if (key == 'authorization') {
+        this._log.trace("HTTP Header " + key + ": ***** (suppressed)");
+      } else {
+        this._log.trace("HTTP Header " + key + ": " + headers[key]);
+      }
+      channel.setRequestHeader(key, headers[key], false);
+    }
+
+    // Set HTTP request body.
+    if (method == "PUT" || method == "POST") {
+      // Convert non-string bodies into JSON.
+      if (typeof data != "string") {
+        data = JSON.stringify(data);
+      }
+
+      this._log.debug(method + " Length: " + data.length);
+      if (this._log.level <= Log4Moz.Level.Trace) {
+        this._log.trace(method + " Body: " + data);
+      }
+
+      let stream = Cc["@mozilla.org/io/string-input-stream;1"]
+                     .createInstance(Ci.nsIStringInputStream);
+      stream.setData(data, data.length);
+
+      let type = headers["content-type"] || "text/plain";
+      channel.QueryInterface(Ci.nsIUploadChannel);
+      channel.setUploadStream(stream, type, data.length);
+    }
+    // We must set this after setting the upload stream, otherwise it
+    // will always be 'PUT'. Yeah, I know.
+    channel.requestMethod = method;
+
+    // Blast off!
+    channel.asyncOpen(this, null);
+    this.status = this.SENT;
+    this.delayTimeout();
+    return this;
+  },
+
+  /**
+   * Create or push back the abort timer that kills this request.
+   */
+  delayTimeout: function delayTimeout() {
+    if (this.timeout) {
+      CommonUtils.namedTimer(this.abortTimeout, this.timeout * 1000, this,
+                             "timeoutTimer");
+    }
+  },
+
+  /**
+   * Abort the request based on a timeout.
+   */
+  abortTimeout: function abortTimeout() {
+    this.abort();
+    let error = Components.Exception("Aborting due to channel inactivity.",
+                                     Cr.NS_ERROR_NET_TIMEOUT);
+    if (!this.onComplete) {
+      this._log.error("Unexpected error: onComplete not defined in " +
+                      "abortTimeout.")
+      return;
+    }
+    this.onComplete(error);
+  },
+
+  /*** nsIStreamListener ***/
+
+  onStartRequest: function onStartRequest(channel) {
+    if (this.status == this.ABORTED) {
+      this._log.trace("Not proceeding with onStartRequest, request was aborted.");
+      return;
+    }
+
+    try {
+      channel.QueryInterface(Ci.nsIHttpChannel);
+    } catch (ex) {
+      this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
+      this.status = this.ABORTED;
+      channel.cancel(Cr.NS_BINDING_ABORTED);
+      return;
+    }
+
+    this.status = this.IN_PROGRESS;
+
+    this._log.trace("onStartRequest: " + channel.requestMethod + " " +
+                    channel.URI.spec);
+
+    // Create a response object and fill it with some data.
+    let response = this.response = new RESTResponse();
+    response.request = this;
+    response.body = "";
+
+    // Define this here so that we don't have make a new one each time
+    // onDataAvailable() gets called.
+    this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
+                          .createInstance(Ci.nsIScriptableInputStream);
+
+    this.delayTimeout();
+  },
+
+  onStopRequest: function onStopRequest(channel, context, statusCode) {
+    if (this.timeoutTimer) {
+      // Clear the abort timer now that the channel is done.
+      this.timeoutTimer.clear();
+    }
+
+    // We don't want to do anything for a request that's already been aborted.
+    if (this.status == this.ABORTED) {
+      this._log.trace("Not proceeding with onStopRequest, request was aborted.");
+      return;
+    }
+
+    try {
+      channel.QueryInterface(Ci.nsIHttpChannel);
+    } catch (ex) {
+      this._log.error("Unexpected error: channel not nsIHttpChannel!");
+      this.status = this.ABORTED;
+      return;
+    }
+    this.status = this.COMPLETED;
+
+    let statusSuccess = Components.isSuccessCode(statusCode);
+    let uri = channel && channel.URI && channel.URI.spec || "<unknown>";
+    this._log.trace("Channel for " + channel.requestMethod + " " + uri +
+                    " returned status code " + statusCode);
+
+    if (!this.onComplete) {
+      this._log.error("Unexpected error: onComplete not defined in " +
+                      "abortRequest.");
+      this.onProgress = null;
+      return;
+    }
+
+    // Throw the failure code and stop execution.  Use Components.Exception()
+    // instead of Error() so the exception is QI-able and can be passed across
+    // XPCOM borders while preserving the status code.
+    if (!statusSuccess) {
+      let message = Components.Exception("", statusCode).name;
+      let error = Components.Exception(message, statusCode);
+      this.onComplete(error);
+      this.onComplete = this.onProgress = null;
+      return;
+    }
+
+    this._log.debug(this.method + " " + uri + " " + this.response.status);
+
+    // Additionally give the full response body when Trace logging.
+    if (this._log.level <= Log4Moz.Level.Trace) {
+      this._log.trace(this.method + " body: " + this.response.body);
+    }
+
+    delete this._inputStream;
+
+    this.onComplete(null);
+    this.onComplete = this.onProgress = null;
+  },
+
+  onDataAvailable: function onDataAvailable(req, cb, stream, off, count) {
+    this._inputStream.init(stream);
+    try {
+      this.response.body += this._inputStream.read(count);
+    } catch (ex) {
+      this._log.warn("Exception thrown reading " + count +
+                     " bytes from the channel.");
+      this._log.debug(CommonUtils.exceptionStr(ex));
+      throw ex;
+    }
+
+    try {
+      this.onProgress();
+    } catch (ex) {
+      this._log.warn("Got exception calling onProgress handler, aborting " +
+                     this.method + " " + req.URI.spec);
+      this._log.debug("Exception: " + CommonUtils.exceptionStr(ex));
+      this.abort();
+
+      if (!this.onComplete) {
+        this._log.error("Unexpected error: onComplete not defined in " +
+                        "onDataAvailable.");
+        this.onProgress = null;
+        return;
+      }
+
+      this.onComplete(ex);
+      this.onComplete = this.onProgress = null;
+      return;
+    }
+
+    this.delayTimeout();
+  },
+
+  /*** nsIInterfaceRequestor ***/
+
+  getInterface: function(aIID) {
+    return this.QueryInterface(aIID);
+  },
+
+  /*** nsIBadCertListener2 ***/
+
+  notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) {
+    this._log.warn("Invalid HTTPS certificate encountered!");
+    // Suppress invalid HTTPS certificate warnings in the UI.
+    // (The request will still fail.)
+    return true;
+  },
+
+  /*** nsIChannelEventSink ***/
+  asyncOnChannelRedirect:
+    function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
+
+    try {
+      newChannel.QueryInterface(Ci.nsIHttpChannel);
+    } catch (ex) {
+      this._log.error("Unexpected error: channel not nsIHttpChannel!");
+      callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE);
+      return;
+    }
+
+    this.channel = newChannel;
+
+    // We let all redirects proceed.
+    callback.onRedirectVerifyCallback(Cr.NS_OK);
+  }
+};
+
+/**
+ * Response object for a RESTRequest. This will be created automatically by
+ * the RESTRequest.
+ */
+function RESTResponse() {
+  this._log = Log4Moz.repository.getLogger(this._logName);
+  this._log.level =
+    Log4Moz.Level[Prefs.get("log.logger.rest.response")];
+}
+RESTResponse.prototype = {
+
+  _logName: "Sync.RESTResponse",
+
+  /**
+   * Corresponding REST request
+   */
+  request: null,
+
+  /**
+   * HTTP status code
+   */
+  get status() {
+    let status;
+    try {
+      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
+      status = channel.responseStatus;
+    } catch (ex) {
+      this._log.debug("Caught exception fetching HTTP status code:" +
+                      CommonUtils.exceptionStr(ex));
+      return null;
+    }
+    delete this.status;
+    return this.status = status;
+  },
+
+  /**
+   * Boolean flag that indicates whether the HTTP status code is 2xx or not.
+   */
+  get success() {
+    let success;
+    try {
+      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
+      success = channel.requestSucceeded;
+    } catch (ex) {
+      this._log.debug("Caught exception fetching HTTP success flag:" +
+                      CommonUtils.exceptionStr(ex));
+      return null;
+    }
+    delete this.success;
+    return this.success = success;
+  },
+
+  /**
+   * Object containing HTTP headers (keyed as lower case)
+   */
+  get headers() {
+    let headers = {};
+    try {
+      this._log.trace("Processing response headers.");
+      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
+      channel.visitResponseHeaders(function (header, value) {
+        headers[header.toLowerCase()] = value;
+      });
+    } catch (ex) {
+      this._log.debug("Caught exception processing response headers:" +
+                      CommonUtils.exceptionStr(ex));
+      return null;
+    }
+
+    delete this.headers;
+    return this.headers = headers;
+  },
+
+  /**
+   * HTTP body (string)
+   */
+  body: null
+
+};
new file mode 100644
--- /dev/null
+++ b/services/common/services-common.js
@@ -0,0 +1,7 @@
+// This file contains default preference values for components in
+// services-common.
+
+pref("services.common.log.logger.rest.request", "Debug");
+pref("services.common.log.logger.rest.response", "Debug");
+
+pref("services.common.tokenserverclient.logger.level", "Info");
rename from services/sync/modules/ext/StringBundle.js
rename to services/common/stringbundle.js
--- a/services/sync/modules/ext/StringBundle.js
+++ b/services/common/stringbundle.js
@@ -29,22 +29,19 @@
  * use your version of this file under the terms of the MPL, indicate your
  * decision by deleting the provisions above and replace them with the notice
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
-let EXPORTED_SYMBOLS = ["StringBundle"];
+const EXPORTED_SYMBOLS = ["StringBundle"];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
 /**
  * A string bundle.
  *
  * This object presents two APIs: a deprecated one that is equivalent to the API
  * for the stringbundle XBL binding, to make it easy to switch from that binding
  * to this module, and a new one that is simpler and easier to use.
  *
new file mode 100644
--- /dev/null
+++ b/services/common/tests/Makefile.in
@@ -0,0 +1,16 @@
+# 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          = ../../..
+topsrcdir      = @top_srcdir@
+srcdir         = @srcdir@
+VPATH          = @srcdir@
+relativesrcdir = services/common/tests
+
+include $(DEPTH)/config/autoconf.mk
+
+MODULE = test_services_common
+XPCSHELL_TESTS = unit
+
+include $(topsrcdir)/config/rules.mk
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/head_global.js
@@ -0,0 +1,54 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+// Where to bind test HTTP servers to.
+const TEST_SERVER_URL = "http://localhost:8080/";
+
+// This has the side-effect of populating Cc, Ci, Cu, Cr. It's best not to
+// ask questions and just accept it.
+do_load_httpd_js();
+const Cm = Components.manager;
+
+let gSyncProfile = do_get_profile();
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+let XULAppInfo = {
+  vendor: "Mozilla",
+  name: "XPCShell",
+  ID: "xpcshell@tests.mozilla.org",
+  version: "1",
+  appBuildID: "20100621",
+  platformVersion: "",
+  platformBuildID: "20100621",
+  inSafeMode: false,
+  logConsoleErrors: true,
+  OS: "XPCShell",
+  XPCOMABI: "noarch-spidermonkey",
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIXULAppInfo, Ci.nsIXULRuntime]),
+  invalidateCachesOnRestart: function invalidateCachesOnRestart() { }
+};
+
+let XULAppInfoFactory = {
+  createInstance: function (outer, iid) {
+    if (outer != null)
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    return XULAppInfo.QueryInterface(iid);
+  }
+};
+
+let registrar = Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+registrar.registerFactory(Components.ID("{fbfae60b-64a4-44ef-a911-08ceb70b9f31}"),
+                          "XULAppInfo", "@mozilla.org/xre/app-info;1",
+                          XULAppInfoFactory);
+
+function addResourceAlias() {
+  Cu.import("resource://gre/modules/Services.jsm");
+  const handler = Services.io.getProtocolHandler("resource")
+                  .QueryInterface(Ci.nsIResProtocolHandler);
+
+  let uri = Services.io.newURI("resource:///modules/services-common/", null,
+                               null);
+  handler.setSubstitution("services-common", uri);
+}
+addResourceAlias();
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/head_helpers.js
@@ -0,0 +1,173 @@
+/* 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/. */
+
+Cu.import("resource://services-common/log4moz.js");
+
+function do_check_empty(obj) {
+  do_check_attribute_count(obj, 0);
+}
+
+function do_check_attribute_count(obj, c) {
+  do_check_eq(c, Object.keys(obj).length);
+}
+
+function do_check_throws(aFunc, aResult, aStack) {
+  if (!aStack) {
+    try {
+      // We might not have a 'Components' object.
+      aStack = Components.stack.caller;
+    } catch (e) {}
+  }
+
+  try {
+    aFunc();
+  } catch (e) {
+    do_check_eq(e.result, aResult, aStack);
+    return;
+  }
+  do_throw("Expected result " + aResult + ", none thrown.", aStack);
+}
+
+/**
+ * Print some debug message to the console. All arguments will be printed,
+ * separated by spaces.
+ *
+ * @param [arg0, arg1, arg2, ...]
+ *        Any number of arguments to print out
+ * @usage _("Hello World") -> prints "Hello World"
+ * @usage _(1, 2, 3) -> prints "1 2 3"
+ */
+let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" "));
+
+function initTestLogging(level) {
+  function LogStats() {
+    this.errorsLogged = 0;
+  }
+  LogStats.prototype = {
+    format: function BF_format(message) {
+      if (message.level == Log4Moz.Level.Error)
+        this.errorsLogged += 1;
+      return message.loggerName + "\t" + message.levelDesc + "\t" +
+        message.message + "\n";
+    }
+  };
+  LogStats.prototype.__proto__ = new Log4Moz.Formatter();
+
+  var log = Log4Moz.repository.rootLogger;
+  var logStats = new LogStats();
+  var appender = new Log4Moz.DumpAppender(logStats);
+
+  if (typeof(level) == "undefined")
+    level = "Debug";
+  getTestLogger().level = Log4Moz.Level[level];
+
+  log.level = Log4Moz.Level.Trace;
+  appender.level = Log4Moz.Level.Trace;
+  // Overwrite any other appenders (e.g. from previous incarnations)
+  log.ownAppenders = [appender];
+  log.updateAppenders();
+
+  return logStats;
+}
+
+function getTestLogger(component) {
+  return Log4Moz.repository.getLogger("Testing");
+}
+
+function httpd_setup (handlers, port) {
+  let port   = port || 8080;
+  let server = new nsHttpServer();
+  for (let path in handlers) {
+    server.registerPathHandler(path, handlers[path]);
+  }
+  try {
+    server.start(port);
+  } catch (ex) {
+    _("==========================================");
+    _("Got exception starting HTTP server on port " + port);
+    _("Error: " + Utils.exceptionStr(ex));
+    _("Is there a process already listening on port " + port + "?");
+    _("==========================================");
+    do_throw(ex);
+  }
+
+  return server;
+}
+
+function httpd_handler(statusCode, status, body) {
+  return function handler(request, response) {
+    _("Processing request");
+    // Allow test functions to inspect the request.
+    request.body = readBytesFromInputStream(request.bodyInputStream);
+    handler.request = request;
+
+    response.setStatusLine(request.httpVersion, statusCode, status);
+    if (body) {
+      response.bodyOutputStream.write(body, body.length);
+    }
+  };
+}
+
+/*
+ * Read bytes string from an nsIInputStream.  If 'count' is omitted,
+ * all available input is read.
+ */
+function readBytesFromInputStream(inputStream, count) {
+  var BinaryInputStream = Components.Constructor(
+      "@mozilla.org/binaryinputstream;1",
+      "nsIBinaryInputStream",
+      "setInputStream");
+  if (!count) {
+    count = inputStream.available();
+  }
+  return new BinaryInputStream(inputStream).readBytes(count);
+}
+
+/**
+ * Proxy auth helpers.
+ */
+
+/**
+ * Fake a PAC to prompt a channel replacement.
+ */
+let PACSystemSettings = {
+  CID: Components.ID("{5645d2c1-d6d8-4091-b117-fe7ee4027db7}"),
+  contractID: "@mozilla.org/system-proxy-settings;1",
+
+  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory,
+                                         Ci.nsISystemProxySettings]),
+
+  createInstance: function createInstance(outer, iid) {
+    if (outer) {
+      throw Cr.NS_ERROR_NO_AGGREGATION;
+    }
+    return this.QueryInterface(iid);
+  },
+
+  lockFactory: function lockFactory(lock) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  },
+
+  // Replace this URI for each test to avoid caching. We want to ensure that
+  // each test gets a completely fresh setup.
+  PACURI: null,
+  getProxyForURI: function getProxyForURI(aURI) {
+    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
+  }
+};
+
+function installFakePAC() {
+  _("Installing fake PAC.");
+  Cm.nsIComponentRegistrar
+    .registerFactory(PACSystemSettings.CID,
+                     "Fake system proxy-settings",
+                     PACSystemSettings.contractID,
+                     PACSystemSettings);
+}
+
+function uninstallFakePAC() {
+  _("Uninstalling fake PAC.");
+  let CID = PACSystemSettings.CID;
+  Cm.nsIComponentRegistrar.unregisterFactory(CID, PACSystemSettings);
+}
rename from services/sync/tests/unit/test_async_chain.js
rename to services/common/tests/unit/test_async_chain.js
--- a/services/sync/tests/unit/test_async_chain.js
+++ b/services/common/tests/unit/test_async_chain.js
@@ -1,9 +1,12 @@
-Cu.import("resource://services-sync/async.js");
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/async.js");
 
 function run_test() {
   _("Chain a few async methods, making sure the 'this' object is correct.");
 
   let methods = {
     save: function(x, callback) {
       this.x = x;
       callback(x);
rename from services/sync/tests/unit/test_async_querySpinningly.js
rename to services/common/tests/unit/test_async_querySpinningly.js
--- a/services/sync/tests/unit/test_async_querySpinningly.js
+++ b/services/common/tests/unit/test_async_querySpinningly.js
@@ -1,26 +1,37 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/utils.js");
+
 _("Make sure querySpinningly will synchronously fetch rows for a query asyncly");
-Cu.import("resource://services-sync/async.js");
 
 const SQLITE_CONSTRAINT_VIOLATION = 19;  // http://www.sqlite.org/c3ref/c_abort.html
 
+let Svc = {};
+XPCOMUtils.defineLazyServiceGetter(Svc, "Form",
+                                   "@mozilla.org/satchel/form-history;1",
+                                   "nsIFormHistory2");
+
 function querySpinningly(query, names) {
   let q = Svc.Form.DBConnection.createStatement(query);
   let r = Async.querySpinningly(q, names);
-  q.finalize();    
+  q.finalize();
   return r;
 }
 
 function run_test() {
   initTestLogging("Trace");
 
   _("Make sure the call is async and allows other events to process");
   let isAsync = false;
-  Utils.nextTick(function() { isAsync = true; });
+  CommonUtils.nextTick(function() { isAsync = true; });
   do_check_false(isAsync);
 
   _("Empty out the formhistory table");
   let r0 = querySpinningly("DELETE FROM moz_formhistory");
   do_check_eq(r0, null);
 
   _("Make sure there's nothing there");
   let r1 = querySpinningly("SELECT 1 FROM moz_formhistory");
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/test_load_modules.js
@@ -0,0 +1,19 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+const modules = [
+  "async.js",
+  "log4moz.js",
+  "preferences.js",
+  "rest.js",
+  "stringbundle.js",
+  "tokenserverclient.js",
+  "utils.js",
+];
+
+function run_test() {
+  for each (let m in modules) {
+    let resource = "resource://services-common/" + m;
+    Components.utils.import(resource, {});
+  }
+}
rename from services/sync/tests/unit/test_log4moz.js
rename to services/common/tests/unit/test_log4moz.js
--- a/services/sync/tests/unit/test_log4moz.js
+++ b/services/common/tests/unit/test_log4moz.js
@@ -1,13 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Components.utils.import("resource://services-sync/log4moz.js");
-Components.utils.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/FileUtils.jsm");
+Cu.import("resource://gre/modules/NetUtil.jsm");
+
+Cu.import("resource://services-common/log4moz.js");
 
 let testFormatter = {
   format: function format(message) {
     return message.loggerName + "\t" + message.levelDesc + "\t" +
            message.message + "\n";
   }
 };
 
rename from services/sync/tests/unit/test_Observers.js
rename to services/common/tests/unit/test_observers.js
--- a/services/sync/tests/unit/test_Observers.js
+++ b/services/common/tests/unit/test_observers.js
@@ -1,13 +1,20 @@
-Components.utils.import("resource://services-sync/ext/Observers.js");
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Components.utils.import("resource://services-common/observers.js");
 
 let gSubject = {};
 
-function test_function_observer() {
+function run_test() {
+  run_next_test();
+}
+
+add_test(function test_function_observer() {
   let foo = false;
 
   let onFoo = function(subject, data) {
     foo = !foo;
     do_check_eq(subject, gSubject);
     do_check_eq(data, "some data");
   };
 
@@ -17,19 +24,21 @@ function test_function_observer() {
   // The observer was notified after being added.
   do_check_true(foo);
 
   Observers.remove("foo", onFoo);
   Observers.notify("foo");
 
   // The observer was not notified after being removed.
   do_check_true(foo);
-}
 
-function test_method_observer() {
+  run_next_test();
+});
+
+add_test(function test_method_observer() {
   let obj = {
     foo: false,
     onFoo: function(subject, data) {
       this.foo = !this.foo;
       do_check_eq(subject, gSubject);
       do_check_eq(data, "some data");
     }
   };
@@ -38,19 +47,21 @@ function test_method_observer() {
   Observers.add("foo", obj.onFoo, obj);
   Observers.notify("foo", gSubject, "some data");
   do_check_true(obj.foo);
 
   // The observer is not notified after being removed.
   Observers.remove("foo", obj.onFoo, obj);
   Observers.notify("foo");
   do_check_true(obj.foo);
-}
 
-function test_object_observer() {
+  run_next_test();
+});
+
+add_test(function test_object_observer() {
   let obj = {
     foo: false,
     observe: function(subject, topic, data) {
       this.foo = !this.foo;
 
       do_check_eq(subject, gSubject);
       do_check_eq(topic, "foo");
       do_check_eq(data, "some data");
@@ -63,15 +74,11 @@ function test_object_observer() {
   // The observer is notified after being added.
   do_check_true(obj.foo);
 
   Observers.remove("foo", obj);
   Observers.notify("foo");
 
   // The observer is not notified after being removed.
   do_check_true(obj.foo);
-}
 
-function run_test() {
-  test_function_observer();
-  test_method_observer();
-  test_object_observer();
-}
+  run_next_test();
+});
rename from services/sync/tests/unit/test_Preferences.js
rename to services/common/tests/unit/test_preferences.js
--- a/services/sync/tests/unit/test_Preferences.js
+++ b/services/common/tests/unit/test_preferences.js
@@ -1,119 +1,144 @@
-Components.utils.import("resource://services-sync/ext/Preferences.js");
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/preferences.js");
 
-function test_set_get_pref() {
+function run_test() {
+  run_next_test();
+}
+
+add_test(function test_set_get_pref() {
   Preferences.set("test_set_get_pref.integer", 1);
   do_check_eq(Preferences.get("test_set_get_pref.integer"), 1);
 
   Preferences.set("test_set_get_pref.string", "foo");
   do_check_eq(Preferences.get("test_set_get_pref.string"), "foo");
 
   Preferences.set("test_set_get_pref.boolean", true);
   do_check_eq(Preferences.get("test_set_get_pref.boolean"), true);
 
   // Clean up.
   Preferences.resetBranch("test_set_get_pref.");
-}
 
-function test_set_get_branch_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_get_branch_pref() {
   let prefs = new Preferences("test_set_get_branch_pref.");
 
   prefs.set("something", 1);
   do_check_eq(prefs.get("something"), 1);
   do_check_false(Preferences.has("something"));
 
   // Clean up.
   prefs.reset("something");
-}
 
-function test_set_get_multiple_prefs() {
+  run_next_test();
+});
+
+add_test(function test_set_get_multiple_prefs() {
   Preferences.set({ "test_set_get_multiple_prefs.integer":  1,
                     "test_set_get_multiple_prefs.string":   "foo",
                     "test_set_get_multiple_prefs.boolean":  true });
 
   let [i, s, b] = Preferences.get(["test_set_get_multiple_prefs.integer",
                                    "test_set_get_multiple_prefs.string",
                                    "test_set_get_multiple_prefs.boolean"]);
 
   do_check_eq(i, 1);
   do_check_eq(s, "foo");
   do_check_eq(b, true);
 
   // Clean up.
   Preferences.resetBranch("test_set_get_multiple_prefs.");
-}
 
-function test_get_multiple_prefs_with_default_value() {
+  run_next_test();
+});
+
+add_test(function test_get_multiple_prefs_with_default_value() {
   Preferences.set({ "test_get_multiple_prefs_with_default_value.a":  1,
                     "test_get_multiple_prefs_with_default_value.b":  2 });
 
   let [a, b, c] = Preferences.get(["test_get_multiple_prefs_with_default_value.a",
                                    "test_get_multiple_prefs_with_default_value.b",
                                    "test_get_multiple_prefs_with_default_value.c"],
                                   0);
 
   do_check_eq(a, 1);
   do_check_eq(b, 2);
   do_check_eq(c, 0);
 
   // Clean up.
   Preferences.resetBranch("test_get_multiple_prefs_with_default_value.");
-}
 
-function test_set_get_unicode_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_get_unicode_pref() {
   Preferences.set("test_set_get_unicode_pref", String.fromCharCode(960));
   do_check_eq(Preferences.get("test_set_get_unicode_pref"), String.fromCharCode(960));
 
   // Clean up.
   Preferences.reset("test_set_get_unicode_pref");
-}
 
-function test_set_null_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_null_pref() {
   try {
     Preferences.set("test_set_null_pref", null);
     // We expect this to throw, so the test is designed to fail if it doesn't.
     do_check_true(false);
   }
   catch(ex) {}
-}
 
-function test_set_undefined_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_undefined_pref() {
   try {
     Preferences.set("test_set_undefined_pref");
     // We expect this to throw, so the test is designed to fail if it doesn't.
     do_check_true(false);
   }
   catch(ex) {}
-}
 
-function test_set_unsupported_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_unsupported_pref() {
   try {
     Preferences.set("test_set_unsupported_pref", new Array());
     // We expect this to throw, so the test is designed to fail if it doesn't.
     do_check_true(false);
   }
   catch(ex) {}
-}
+
+  run_next_test();
+});
 
 // Make sure that we can get a string pref that we didn't set ourselves
 // (i.e. that the way we get a string pref using getComplexValue doesn't
 // hork us getting a string pref that wasn't set using setComplexValue).
-function test_get_string_pref() {
+add_test(function test_get_string_pref() {
   let svc = Cc["@mozilla.org/preferences-service;1"].
             getService(Ci.nsIPrefService).
             getBranch("");
   svc.setCharPref("test_get_string_pref", "a normal string");
   do_check_eq(Preferences.get("test_get_string_pref"), "a normal string");
 
   // Clean up.
   Preferences.reset("test_get_string_pref");
-}
 
-function test_set_get_number_pref() {
+  run_next_test();
+});
+
+add_test(function test_set_get_number_pref() {
   Preferences.set("test_set_get_number_pref", 5);
   do_check_eq(Preferences.get("test_set_get_number_pref"), 5);
 
   // Non-integer values get converted to integers.
   Preferences.set("test_set_get_number_pref", 3.14159);
   do_check_eq(Preferences.get("test_set_get_number_pref"), 3);
 
   // Values outside the range -(2^31-1) to 2^31-1 overflow.
@@ -121,61 +146,73 @@ function test_set_get_number_pref() {
     Preferences.set("test_set_get_number_pref", Math.pow(2, 31));
     // We expect this to throw, so the test is designed to fail if it doesn't.
     do_check_true(false);
   }
   catch(ex) {}
 
   // Clean up.
   Preferences.reset("test_set_get_number_pref");
-}
 
-function test_reset_pref() {
+  run_next_test();
+});
+
+add_test(function test_reset_pref() {
   Preferences.set("test_reset_pref", 1);
   Preferences.reset("test_reset_pref");
   do_check_eq(Preferences.get("test_reset_pref"), undefined);
-}
 
-function test_reset_pref_branch() {
+  run_next_test();
+});
+
+add_test(function test_reset_pref_branch() {
   Preferences.set("test_reset_pref_branch.foo", 1);
   Preferences.set("test_reset_pref_branch.bar", 2);
   Preferences.resetBranch("test_reset_pref_branch.");
   do_check_eq(Preferences.get("test_reset_pref_branch.foo"), undefined);
   do_check_eq(Preferences.get("test_reset_pref_branch.bar"), undefined);
-}
+
+  run_next_test();
+});
 
 // Make sure the module doesn't throw an exception when asked to reset
 // a nonexistent pref.
-function test_reset_nonexistent_pref() {
+add_test(function test_reset_nonexistent_pref() {
   Preferences.reset("test_reset_nonexistent_pref");
-}
+
+  run_next_test();
+});
 
 // Make sure the module doesn't throw an exception when asked to reset
 // a nonexistent pref branch.
-function test_reset_nonexistent_pref_branch() {
+add_test(function test_reset_nonexistent_pref_branch() {
   Preferences.resetBranch("test_reset_nonexistent_pref_branch.");
-}
 
-function test_observe_prefs_function() {
+  run_next_test();
+});
+
+add_test(function test_observe_prefs_function() {
   let observed = false;
   let observer = function() { observed = !observed };
 
   Preferences.observe("test_observe_prefs_function", observer);
   Preferences.set("test_observe_prefs_function", "something");
   do_check_true(observed);
 
   Preferences.ignore("test_observe_prefs_function", observer);
   Preferences.set("test_observe_prefs_function", "something else");
   do_check_true(observed);
 
   // Clean up.
   Preferences.reset("test_observe_prefs_function");
-}
 
-function test_observe_prefs_object() {
+  run_next_test();
+});
+
+add_test(function test_observe_prefs_object() {
   let observer = {
     observed: false,
     observe: function() {
       this.observed = !this.observed;
     }
   };
 
   Preferences.observe("test_observe_prefs_object", observer.observe, observer);
@@ -183,19 +220,21 @@ function test_observe_prefs_object() {
   do_check_true(observer.observed);
 
   Preferences.ignore("test_observe_prefs_object", observer.observe, observer);
   Preferences.set("test_observe_prefs_object", "something else");
   do_check_true(observer.observed);
 
   // Clean up.
   Preferences.reset("test_observe_prefs_object");
-}
 
-function test_observe_prefs_nsIObserver() {
+  run_next_test();
+});
+
+add_test(function test_observe_prefs_nsIObserver() {
   let observer = {
     observed: false,
     observe: function(subject, topic, data) {
       this.observed = !this.observed;
       do_check_true(subject instanceof Ci.nsIPrefBranch);
       do_check_eq(topic, "nsPref:changed");
       do_check_eq(data, "test_observe_prefs_nsIObserver");
     }
@@ -206,84 +245,99 @@ function test_observe_prefs_nsIObserver(
   do_check_true(observer.observed);
 
   Preferences.ignore("test_observe_prefs_nsIObserver", observer);
   Preferences.set("test_observe_prefs_nsIObserver", "something else");
   do_check_true(observer.observed);
 
   // Clean up.
   Preferences.reset("test_observe_prefs_nsIObserver");
-}
 
-function test_observe_exact_pref() {
+  run_next_test();
+});
+
+/*
+add_test(function test_observe_exact_pref() {
   let observed = false;
   let observer = function() { observed = !observed };
 
   Preferences.observe("test_observe_exact_pref", observer);
   Preferences.set("test_observe_exact_pref.sub-pref", "something");
   do_check_false(observed);
 
   // Clean up.
   Preferences.ignore("test_observe_exact_pref", observer);
   Preferences.reset("test_observe_exact_pref.sub-pref");
-}
 
-function test_observe_value_of_set_pref() {
+  run_next_test();
+});
+*/
+
+add_test(function test_observe_value_of_set_pref() {
   let observer = function(newVal) { do_check_eq(newVal, "something") };
 
   Preferences.observe("test_observe_value_of_set_pref", observer);
   Preferences.set("test_observe_value_of_set_pref", "something");
 
   // Clean up.
   Preferences.ignore("test_observe_value_of_set_pref", observer);
   Preferences.reset("test_observe_value_of_set_pref");
-}
 
-function test_observe_value_of_reset_pref() {
+  run_next_test();
+});
+
+add_test(function test_observe_value_of_reset_pref() {
   let observer = function(newVal) { do_check_true(typeof newVal == "undefined") };
 
   Preferences.set("test_observe_value_of_reset_pref", "something");
   Preferences.observe("test_observe_value_of_reset_pref", observer);
   Preferences.reset("test_observe_value_of_reset_pref");
 
   // Clean up.
   Preferences.ignore("test_observe_value_of_reset_pref", observer);
-}
 
-function test_has_pref() {
+  run_next_test();
+});
+
+add_test(function test_has_pref() {
   do_check_false(Preferences.has("test_has_pref"));
   Preferences.set("test_has_pref", "foo");
   do_check_true(Preferences.has("test_has_pref"));
 
   Preferences.set("test_has_pref.foo", "foo");
   Preferences.set("test_has_pref.bar", "bar");
   let [hasFoo, hasBar, hasBaz] = Preferences.has(["test_has_pref.foo",
                                                   "test_has_pref.bar",
                                                   "test_has_pref.baz"]);
   do_check_true(hasFoo);
   do_check_true(hasBar);
   do_check_false(hasBaz);
 
   // Clean up.
   Preferences.resetBranch("test_has_pref");
-}
 
-function test_isSet_pref() {
+  run_next_test();
+});
+
+add_test(function test_isSet_pref() {
   // Use a pref that we know has a default value but no user-set value.
   // This feels dangerous; perhaps we should create some other default prefs
   // that we can use for testing.
   do_check_false(Preferences.isSet("toolkit.defaultChromeURI"));
   Preferences.set("toolkit.defaultChromeURI", "foo");
   do_check_true(Preferences.isSet("toolkit.defaultChromeURI"));
 
   // Clean up.
   Preferences.reset("toolkit.defaultChromeURI");
-}
 
-function test_lock_prefs() {
+  run_next_test();
+});
+
+/*
+add_test(function test_lock_prefs() {
   // Use a pref that we know has a default value.
   // This feels dangerous; perhaps we should create some other default prefs
   // that we can use for testing.
   do_check_false(Preferences.locked("toolkit.defaultChromeURI"));
   Preferences.lock("toolkit.defaultChromeURI");
   do_check_true(Preferences.locked("toolkit.defaultChromeURI"));
   Preferences.unlock("toolkit.defaultChromeURI");
   do_check_false(Preferences.locked("toolkit.defaultChromeURI"));
@@ -293,19 +347,22 @@ function test_lock_prefs() {
   do_check_eq(Preferences.get("toolkit.defaultChromeURI"), "test_lock_prefs");
   Preferences.lock("toolkit.defaultChromeURI");
   do_check_eq(Preferences.get("toolkit.defaultChromeURI"), val);
   Preferences.unlock("toolkit.defaultChromeURI");
   do_check_eq(Preferences.get("toolkit.defaultChromeURI"), "test_lock_prefs");
 
   // Clean up.
   Preferences.reset("toolkit.defaultChromeURI");
-}
 
-function test_site_prefs() {
+  run_next_test();
+});
+*/
+
+add_test(function test_site_prefs() {
   let prefs = Preferences.site("www.example.com");
 
   prefs.set("test_site_prefs.integer", 1);
   do_check_eq(prefs.get("test_site_prefs.integer"), 1);
   do_check_true(prefs.has("test_site_prefs.integer"));
   do_check_false(Preferences.has("test_site_prefs.integer"));
   prefs.reset("test_site_prefs.integer");
   do_check_false(prefs.has("test_site_prefs.integer"));
@@ -318,37 +375,11 @@ function test_site_prefs() {
   do_check_false(prefs.has("test_site_prefs.string"));
 
   prefs.set("test_site_prefs.boolean", true);
   do_check_eq(prefs.get("test_site_prefs.boolean"), true);
   do_check_true(prefs.has("test_site_prefs.boolean"));
   do_check_false(Preferences.has("test_site_prefs.boolean"));
   prefs.reset("test_site_prefs.boolean");
   do_check_false(prefs.has("test_site_prefs.boolean"));
-}
 
-
-function run_test() {
-  test_set_get_pref();
-  test_set_get_branch_pref();
-  test_set_get_multiple_prefs();
-  test_get_multiple_prefs_with_default_value();
-  test_set_get_unicode_pref();
-  test_set_null_pref();
-  test_set_undefined_pref();
-  test_set_unsupported_pref();
-  test_get_string_pref();
-  test_set_get_number_pref();
-  test_reset_pref();
-  test_reset_pref_branch();
-  test_reset_nonexistent_pref();
-  test_reset_nonexistent_pref_branch();
-  test_observe_prefs_function();
-  test_observe_prefs_object();
-  test_observe_prefs_nsIObserver();
-  //test_observe_exact_pref();
-  test_observe_value_of_set_pref();
-  test_observe_value_of_reset_pref();
-  test_has_pref();
-  test_isSet_pref();
-  //test_lock_prefs();
-  test_site_prefs();
-}
+  run_next_test();
+});
rename from services/sync/tests/unit/test_restrequest.js
rename to services/common/tests/unit/test_restrequest.js
--- a/services/sync/tests/unit/test_restrequest.js
+++ b/services/common/tests/unit/test_restrequest.js
@@ -1,19 +1,23 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/rest.js");
-Cu.import("resource://services-sync/log4moz.js");
 Cu.import("resource://gre/modules/NetUtil.jsm");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/rest.js");
+Cu.import("resource://services-common/utils.js");
 
 const TEST_RESOURCE_URL = TEST_SERVER_URL + "resource";
 
+//DEBUG = true;
+
 function run_test() {
-  Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
+  Log4Moz.repository.getLogger("Services.Common.RESTRequest").level =
+    Log4Moz.Level.Trace;
   initTestLogging();
 
   run_next_test();
 }
 
 /**
  * Initializing a RESTRequest with an invalid URI throws
  * NS_ERROR_MALFORMED_URI.
@@ -135,17 +139,17 @@ add_test(function test_get() {
 
     do_check_eq(this.status, this.COMPLETED);
     do_check_true(this.response.success);
     do_check_eq(this.response.status, 200);
     do_check_eq(this.response.body, "Huzzah!");
     do_check_eq(handler.request.method, "GET");
 
     do_check_true(onProgress_called);
-    Utils.nextTick(function () {
+    CommonUtils.nextTick(function () {
       do_check_eq(request.onComplete, null);
       do_check_eq(request.onProgress, null);
       server.stop(run_next_test);
     });
   };
 
   do_check_eq(request.get(onComplete, onProgress), request);
   do_check_eq(request.status, request.SENT);
@@ -184,17 +188,17 @@ add_test(function test_put() {
     do_check_eq(this.response.status, 200);
     do_check_eq(this.response.body, "Got it!");
 
     do_check_eq(handler.request.method, "PUT");
     do_check_eq(handler.request.body, "Hullo?");
     do_check_eq(handler.request.getHeader("Content-Type"), "text/plain");
 
     do_check_true(onProgress_called);
-    Utils.nextTick(function () {
+    CommonUtils.nextTick(function () {
       do_check_eq(request.onComplete, null);
       do_check_eq(request.onProgress, null);
       server.stop(run_next_test);
     });
   };
 
   do_check_eq(request.put("Hullo?", onComplete, onProgress), request);
   do_check_eq(request.status, request.SENT);
@@ -233,17 +237,17 @@ add_test(function test_post() {
     do_check_eq(this.response.status, 200);
     do_check_eq(this.response.body, "Got it!");
 
     do_check_eq(handler.request.method, "POST");
     do_check_eq(handler.request.body, "Hullo?");
     do_check_eq(handler.request.getHeader("Content-Type"), "text/plain");
 
     do_check_true(onProgress_called);
-    Utils.nextTick(function () {
+    CommonUtils.nextTick(function () {
       do_check_eq(request.onComplete, null);
       do_check_eq(request.onProgress, null);
       server.stop(run_next_test);
     });
   };
 
   do_check_eq(request.post("Hullo?", onComplete, onProgress), request);
   do_check_eq(request.status, request.SENT);
@@ -279,17 +283,17 @@ add_test(function test_delete() {
 
     do_check_eq(this.status, this.COMPLETED);
     do_check_true(this.response.success);
     do_check_eq(this.response.status, 200);
     do_check_eq(this.response.body, "Got it!");
     do_check_eq(handler.request.method, "DELETE");
 
     do_check_true(onProgress_called);
-    Utils.nextTick(function () {
+    CommonUtils.nextTick(function () {
       do_check_eq(request.onComplete, null);
       do_check_eq(request.onProgress, null);
       server.stop(run_next_test);
     });
   };
 
   do_check_eq(request.delete(onComplete, onProgress), request);
   do_check_eq(request.status, request.SENT);
@@ -462,17 +466,17 @@ add_test(function test_get_no_headers() 
 /**
  * Test changing the URI after having created the request.
  */
 add_test(function test_changing_uri() {
   let handler = httpd_handler(200, "OK");
   let server = httpd_setup({"/resource": handler});
 
   let request = new RESTRequest("http://localhost:8080/the-wrong-resource");
-  request.uri = Utils.makeURI(TEST_RESOURCE_URL);
+  request.uri = CommonUtils.makeURI(TEST_RESOURCE_URL);
   request.get(function (error) {
     do_check_eq(error, null);
     do_check_eq(this.response.status, 200);
     server.stop(run_next_test);
   });
 });
 
 /**
@@ -567,17 +571,17 @@ add_test(function test_abort() {
   request.abort();
 
   // Aborting an already aborted request is pointless and will throw.
   do_check_throws(function () {
     request.abort();
   });
 
   do_check_eq(request.status, request.ABORTED);
-  Utils.nextTick(function () {
+  CommonUtils.nextTick(function () {
     server.stop(run_next_test);
   });
 });
 
 /**
  * A non-zero 'timeout' property specifies the amount of seconds to wait after
  * channel activity until the request is automatically canceled.
  */
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/test_tokenserverclient.js
@@ -0,0 +1,166 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/tokenserverclient.js");
+
+function run_test() {
+  initTestLogging("Trace");
+
+  run_next_test();
+}
+
+add_test(function test_working_bid_exchange() {
+  _("Ensure that working BrowserID token exchange works as expected.");
+
+  let service = "http://example.com/foo";
+
+  let server = httpd_setup({
+    "/1.0/foo/1.0": function(request, response) {
+      do_check_true(request.hasHeader("accept"));
+      do_check_eq("application/json", request.getHeader("accept"));
+
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "application/json");
+
+      let body = JSON.stringify({
+        id:           "id",
+        secret:       "key",
+        api_endpoint: service,
+        uid:          "uid",
+      });
+      response.bodyOutputStream.write(body, body.length);
+    }
+  });
+
+  let client = new TokenServerClient();
+  let cb = Async.makeSpinningCallback();
+  let url = TEST_SERVER_URL + "1.0/foo/1.0";
+  client.getTokenFromBrowserIDAssertion(url, "assertion", cb);
+  let result = cb.wait();
+  do_check_eq("object", typeof(result));
+  do_check_attribute_count(result, 4);
+  do_check_eq(service, result.endpoint);
+  do_check_eq("id", result.id);
+  do_check_eq("key", result.key);
+  do_check_eq("uid", result.uid);
+
+  server.stop(run_next_test);
+});
+
+add_test(function test_invalid_arguments() {
+  _("Ensure invalid arguments to APIs are rejected.");
+
+  let args = [
+    [null, "assertion", function() {}],
+    ["http://example.com/", null, function() {}],
+    ["http://example.com/", "assertion", null]
+  ];
+
+  for each (let arg in args) {
+    try {
+      let client = new TokenServerClient();
+      client.getTokenFromBrowserIDAssertion(arg[0], arg[1], arg[2]);
+      do_throw("Should never get here.");
+    } catch (ex) {
+      do_check_true(ex instanceof TokenServerClientError);
+    }
+  }
+
+  run_next_test();
+});
+
+add_test(function test_error_404() {
+  _("Ensure that 404 responses result in error.");
+
+  let server = httpd_setup();
+
+  let client = new TokenServerClient();
+  let url = TEST_SERVER_URL + "foo";
+  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+    do_check_neq(null, error);
+    do_check_eq("TokenServerClientServerError", error.name);
+    do_check_neq(null, error.response);
+    do_check_eq(null, r);
+
+    server.stop(run_next_test);
+  });
+});
+
+add_test(function test_bad_json() {
+  _("Ensure that malformed JSON is handled properly.");
+
+  let server = httpd_setup({
+    "/1.0/foo/1.0": function(request, response) {
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "application/json");
+
+      let body = '{"id": "id", baz}'
+      response.bodyOutputStream.write(body, body.length);
+    }
+  });
+
+  let client = new TokenServerClient();
+  let url = TEST_SERVER_URL + "1.0/foo/1.0";
+  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+    _(error);
+    do_check_neq(null, error);
+    do_check_eq("TokenServerClientServerError", error.name);
+    do_check_neq(null, error.response);
+    do_check_eq(null, r);
+
+    server.stop(run_next_test);
+  });
+});
+
+add_test(function test_unhandled_media_type() {
+  _("Ensure that unhandled media types throw an error.");
+
+  let server = httpd_setup({
+    "/1.0/foo/1.0": function(request, response) {
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "text/plain");
+
+      let body = "hello, world";
+      response.bodyOutputStream.write(body, body.length);
+    }
+  });
+
+  let url = TEST_SERVER_URL + "1.0/foo/1.0";
+  let client = new TokenServerClient();
+  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+    do_check_neq(null, error);
+    do_check_eq("TokenServerClientError", error.name);
+    do_check_neq(null, error.response);
+    do_check_eq(null, r);
+
+    server.stop(run_next_test);
+  });
+});
+
+add_test(function test_rich_media_types() {
+  _("Ensure that extra tokens in the media type aren't rejected.");
+
+  let server = httpd_setup({
+    "/foo": function(request, response) {
+      response.setStatusLine(request.httpVersion, 200, "OK");
+      response.setHeader("Content-Type", "application/json; foo=bar; bar=foo");
+
+      let body = JSON.stringify({
+        id:           "id",
+        secret:       "key",
+        api_endpoint: "foo",
+        uid:          "uid",
+      });
+      response.bodyOutputStream.write(body, body.length);
+    }
+  });
+
+  let url = TEST_SERVER_URL + "foo";
+  let client = new TokenServerClient();
+  client.getTokenFromBrowserIDAssertion(url, "assertion", function(error, r) {
+    do_check_eq(null, error);
+
+    server.stop(run_next_test);
+  });
+});
rename from services/sync/tests/unit/test_utils_atob.js
rename to services/common/tests/unit/test_utils_atob.js
--- a/services/sync/tests/unit/test_utils_atob.js
+++ b/services/common/tests/unit/test_utils_atob.js
@@ -1,8 +1,11 @@
-Cu.import("resource://services-sync/util.js");
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   let data = ["Zm9vYmE=", "Zm9vYmE==", "Zm9vYmE==="];
   for (let d in data) {
-    do_check_eq(Utils.safeAtoB(data[d]), "fooba");
+    do_check_eq(CommonUtils.safeAtoB(data[d]), "fooba");
   }
 }
rename from services/sync/tests/unit/test_utils_makeURI.js
rename to services/common/tests/unit/test_utils_makeURI.js
--- a/services/sync/tests/unit/test_utils_makeURI.js
+++ b/services/common/tests/unit/test_utils_makeURI.js
@@ -1,66 +1,66 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 _("Make sure uri strings are converted to nsIURIs");
-Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   _test_makeURI();
 }
 
 function _test_makeURI() {
   _("Check http uris");
   let uri1 = "http://mozillalabs.com/";
-  do_check_eq(Utils.makeURI(uri1).spec, uri1);
+  do_check_eq(CommonUtils.makeURI(uri1).spec, uri1);
   let uri2 = "http://www.mozillalabs.com/";
-  do_check_eq(Utils.makeURI(uri2).spec, uri2);
+  do_check_eq(CommonUtils.makeURI(uri2).spec, uri2);
   let uri3 = "http://mozillalabs.com/path";
-  do_check_eq(Utils.makeURI(uri3).spec, uri3);
+  do_check_eq(CommonUtils.makeURI(uri3).spec, uri3);
   let uri4 = "http://mozillalabs.com/multi/path";
-  do_check_eq(Utils.makeURI(uri4).spec, uri4);
+  do_check_eq(CommonUtils.makeURI(uri4).spec, uri4);
   let uri5 = "http://mozillalabs.com/?query";
-  do_check_eq(Utils.makeURI(uri5).spec, uri5);
+  do_check_eq(CommonUtils.makeURI(uri5).spec, uri5);
   let uri6 = "http://mozillalabs.com/#hash";
-  do_check_eq(Utils.makeURI(uri6).spec, uri6);
+  do_check_eq(CommonUtils.makeURI(uri6).spec, uri6);
 
   _("Check https uris");
   let uris1 = "https://mozillalabs.com/";
-  do_check_eq(Utils.makeURI(uris1).spec, uris1);
+  do_check_eq(CommonUtils.makeURI(uris1).spec, uris1);
   let uris2 = "https://www.mozillalabs.com/";
-  do_check_eq(Utils.makeURI(uris2).spec, uris2);
+  do_check_eq(CommonUtils.makeURI(uris2).spec, uris2);
   let uris3 = "https://mozillalabs.com/path";
-  do_check_eq(Utils.makeURI(uris3).spec, uris3);
+  do_check_eq(CommonUtils.makeURI(uris3).spec, uris3);
   let uris4 = "https://mozillalabs.com/multi/path";
-  do_check_eq(Utils.makeURI(uris4).spec, uris4);
+  do_check_eq(CommonUtils.makeURI(uris4).spec, uris4);
   let uris5 = "https://mozillalabs.com/?query";
-  do_check_eq(Utils.makeURI(uris5).spec, uris5);
+  do_check_eq(CommonUtils.makeURI(uris5).spec, uris5);
   let uris6 = "https://mozillalabs.com/#hash";
-  do_check_eq(Utils.makeURI(uris6).spec, uris6);
+  do_check_eq(CommonUtils.makeURI(uris6).spec, uris6);
 
   _("Check chrome uris");
   let uric1 = "chrome://browser/content/browser.xul";
-  do_check_eq(Utils.makeURI(uric1).spec, uric1);
+  do_check_eq(CommonUtils.makeURI(uric1).spec, uric1);
   let uric2 = "chrome://browser/skin/browser.css";
-  do_check_eq(Utils.makeURI(uric2).spec, uric2);
+  do_check_eq(CommonUtils.makeURI(uric2).spec, uric2);
   let uric3 = "chrome://browser/locale/browser.dtd";
-  do_check_eq(Utils.makeURI(uric3).spec, uric3);
+  do_check_eq(CommonUtils.makeURI(uric3).spec, uric3);
 
   _("Check about uris");
   let uria1 = "about:weave";
-  do_check_eq(Utils.makeURI(uria1).spec, uria1);
+  do_check_eq(CommonUtils.makeURI(uria1).spec, uria1);
   let uria2 = "about:weave/";
-  do_check_eq(Utils.makeURI(uria2).spec, uria2);
+  do_check_eq(CommonUtils.makeURI(uria2).spec, uria2);
   let uria3 = "about:weave/path";
-  do_check_eq(Utils.makeURI(uria3).spec, uria3);
+  do_check_eq(CommonUtils.makeURI(uria3).spec, uria3);
   let uria4 = "about:weave/multi/path";
-  do_check_eq(Utils.makeURI(uria4).spec, uria4);
+  do_check_eq(CommonUtils.makeURI(uria4).spec, uria4);
   let uria5 = "about:weave/?query";
-  do_check_eq(Utils.makeURI(uria5).spec, uria5);
+  do_check_eq(CommonUtils.makeURI(uria5).spec, uria5);
   let uria6 = "about:weave/#hash";
-  do_check_eq(Utils.makeURI(uria6).spec, uria6);
+  do_check_eq(CommonUtils.makeURI(uria6).spec, uria6);
 
   _("Invalid uris are undefined");
-  do_check_eq(Utils.makeURI("mozillalabs.com"), undefined);
-  do_check_eq(Utils.makeURI("chrome://badstuff"), undefined);
-  do_check_eq(Utils.makeURI("this is a test"), undefined);
+  do_check_eq(CommonUtils.makeURI("mozillalabs.com"), undefined);
+  do_check_eq(CommonUtils.makeURI("chrome://badstuff"), undefined);
+  do_check_eq(CommonUtils.makeURI("this is a test"), undefined);
 }
rename from services/sync/tests/unit/test_utils_namedTimer.js
rename to services/common/tests/unit/test_utils_namedTimer.js
--- a/services/sync/tests/unit/test_utils_namedTimer.js
+++ b/services/common/tests/unit/test_utils_namedTimer.js
@@ -1,69 +1,69 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   run_next_test();
 }
 
 add_test(function test_required_args() {
   try {
-    Utils.namedTimer(function callback() {
+    CommonUtils.namedTimer(function callback() {
       do_throw("Shouldn't fire.");
     }, 0);
     do_throw("Should have thrown!");
   } catch(ex) {
     run_next_test();
   }
 });
 
 add_test(function test_simple() {
-  _("Test basic properties of Utils.namedTimer.");
+  _("Test basic properties of CommonUtils.namedTimer.");
 
   const delay = 200;
   let that = {};
   let t0 = Date.now();
-  Utils.namedTimer(function callback(timer) {
+  CommonUtils.namedTimer(function callback(timer) {
     do_check_eq(this, that);
     do_check_eq(this._zetimer, null);
     do_check_true(timer instanceof Ci.nsITimer);
     // Difference should be ~delay, but hard to predict on all platforms,
     // particularly Windows XP.
     do_check_true(Date.now() > t0);
     run_next_test();
   }, delay, that, "_zetimer");
 });
 
 add_test(function test_delay() {
   _("Test delaying a timer that hasn't fired yet.");
-  
+
   const delay = 100;
   let that = {};
   let t0 = Date.now();
   function callback(timer) {
     // Difference should be ~2*delay, but hard to predict on all platforms,
     // particularly Windows XP.
     do_check_true((Date.now() - t0) > delay);
     run_next_test();
   }
-  Utils.namedTimer(callback, delay, that, "_zetimer");
-  Utils.namedTimer(callback, 2 * delay, that, "_zetimer");
+  CommonUtils.namedTimer(callback, delay, that, "_zetimer");
+  CommonUtils.namedTimer(callback, 2 * delay, that, "_zetimer");
   run_next_test();
 });
 
 add_test(function test_clear() {
   _("Test clearing a timer that hasn't fired yet.");
 
   const delay = 0;
   let that = {};
-  Utils.namedTimer(function callback(timer) {
+  CommonUtils.namedTimer(function callback(timer) {
     do_throw("Shouldn't fire!");
   }, delay, that, "_zetimer");
 
   that._zetimer.clear();
   do_check_eq(that._zetimer, null);
-  Utils.nextTick(run_next_test);
+  CommonUtils.nextTick(run_next_test);
 
   run_next_test();
 });
rename from services/sync/tests/unit/test_utils_stackTrace.js
rename to services/common/tests/unit/test_utils_stackTrace.js
--- a/services/sync/tests/unit/test_utils_stackTrace.js
+++ b/services/common/tests/unit/test_utils_stackTrace.js
@@ -1,30 +1,33 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
 _("Define some functions in well defined line positions for the test");
 function foo(v) bar(v + 1); // line 2
 function bar(v) baz(v + 1); // line 3
 function baz(v) { throw new Error(v + 1); } // line 4
 
 _("Make sure lazy constructor calling/assignment works");
-Cu.import("resource://services-sync/util.js");
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   _("Make sure functions, arguments, files are pretty printed in the trace");
   let trace = "";
   try {
     foo(0);
   }
   catch(ex) {
-    trace = Utils.stackTrace(ex);
+    trace = CommonUtils.stackTrace(ex);
   }
   _("Got trace:", trace);
   do_check_neq(trace, "");
 
-  let bazPos = trace.indexOf("baz(2)@test_utils_stackTrace.js:4");
-  let barPos = trace.indexOf("bar(1)@test_utils_stackTrace.js:3");
-  let fooPos = trace.indexOf("foo(0)@test_utils_stackTrace.js:2");
+  let bazPos = trace.indexOf("baz(2)@test_utils_stackTrace.js:7");
+  let barPos = trace.indexOf("bar(1)@test_utils_stackTrace.js:6");
+  let fooPos = trace.indexOf("foo(0)@test_utils_stackTrace.js:5");
   _("String positions:", bazPos, barPos, fooPos);
 
   _("Make sure the desired messages show up");
   do_check_true(bazPos >= 0);
   do_check_true(barPos > bazPos);
   do_check_true(fooPos > barPos);
 }
rename from services/sync/tests/unit/test_utils_utf8.js
rename to services/common/tests/unit/test_utils_utf8.js
--- a/services/sync/tests/unit/test_utils_utf8.js
+++ b/services/common/tests/unit/test_utils_utf8.js
@@ -1,8 +1,11 @@
-Cu.import("resource://services-sync/util.js");
+/* Any copyright is dedicated to the Public Domain.
+   http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/utils.js");
 
 function run_test() {
   let str = "Umlaute: \u00FC \u00E4\n"; // Umlaute: ü ä
-  let encoded = Utils.encodeUTF8(str);
-  let decoded = Utils.decodeUTF8(encoded);
+  let encoded = CommonUtils.encodeUTF8(str);
+  let decoded = CommonUtils.decodeUTF8(encoded);
   do_check_eq(decoded, str);
 }
new file mode 100644
--- /dev/null
+++ b/services/common/tests/unit/xpcshell.ini
@@ -0,0 +1,20 @@
+[DEFAULT]
+head = head_global.js head_helpers.js
+tail =
+
+# Test load modules first so syntax failures are caught early.
+[test_load_modules.js]
+
+[test_utils_atob.js]
+[test_utils_makeURI.js]
+[test_utils_namedTimer.js]
+[test_utils_stackTrace.js]
+[test_utils_utf8.js]
+
+[test_async_chain.js]
+[test_async_querySpinningly.js]
+[test_log4moz.js]
+[test_observers.js]
+[test_preferences.js]
+[test_restrequest.js]
+[test_tokenserverclient.js]
new file mode 100644
--- /dev/null
+++ b/services/common/tokenserverclient.js
@@ -0,0 +1,238 @@
+/* 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 EXPORTED_SYMBOLS = [
+  "TokenServerClient",
+  "TokenServerClientError",
+  "TokenServerClientNetworkError",
+  "TokenServerClientServerError"
+];
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/preferences.js");
+Cu.import("resource://services-common/rest.js");
+
+const Prefs = new Preferences("services.common.tokenserverclient.");
+
+/**
+ * Represents a TokenServerClient error that occurred on the client.
+ *
+ * This is the base type for all errors raised by client operations.
+ *
+ * @param message
+ *        (string) Error message.
+ */
+function TokenServerClientError(message) {
+  this.name = "TokenServerClientError";
+  this.message = message || "Client error.";
+}
+TokenServerClientError.prototype = new Error();
+TokenServerClientError.prototype.constructor = TokenServerClientError;
+
+/**
+ * Represents a TokenServerClient error that occurred in the network layer.
+ *
+ * @param error
+ *        The underlying error thrown by the network layer.
+ */
+function TokenServerClientNetworkError(error) {
+  this.name = "TokenServerClientNetworkError";
+  this.error = error;
+}
+TokenServerClientNetworkError.prototype = new TokenServerClientError();
+TokenServerClientNetworkError.prototype.constructor =
+  TokenServerClientNetworkError;
+
+/**
+ * Represents a TokenServerClient error that occurred on the server.
+ *
+ * This type will be encountered for all non-200 response codes from the
+ * server.
+ *
+ * @param message
+ *        (string) Error message.
+ */
+function TokenServerClientServerError(message) {
+  this.name = "TokenServerClientServerError";
+  this.message = message || "Server error.";
+}
+TokenServerClientServerError.prototype = new TokenServerClientError();
+TokenServerClientServerError.prototype.constructor =
+  TokenServerClientServerError;
+
+/**
+ * Represents a client to the Token Server.
+ *
+ * http://docs.services.mozilla.com/token/index.html
+ *
+ * The Token Server supports obtaining tokens for arbitrary apps by
+ * constructing URI paths of the form <app>/<app_version>. However, the service
+ * discovery mechanism emphasizes the use of full URIs and tries to not force
+ * the client to manipulate URIs. This client currently enforces this practice
+ * by not implementing an API which would perform URI manipulation.
+ *
+ * If you are tempted to implement this API in the future, consider this your
+ * warning that you may be doing it wrong and that you should store full URIs
+ * instead.
+ *
+ * Areas to Improve:
+ *
+ *  - The server sends a JSON response on error. The client does not currently
+ *    parse this. It might be convenient if it did.
+ *  - Currently all non-200 status codes are rolled into one error type. It
+ *    might be helpful if callers had a richer API that communicated who was
+ *    at fault (e.g. differentiating a 503 from a 401).
+ */
+function TokenServerClient() {
+  this._log = Log4Moz.repository.getLogger("Common.TokenServerClient");
+  this._log.level = Log4Moz.Level[Prefs.get("logger.level")];
+}
+TokenServerClient.prototype = {
+  /**
+   * Logger instance.
+   */
+  _log: null,
+
+  /**
+   * Obtain a token from a BrowserID assertion against a specific URL.
+   *
+   * This asynchronously obtains the token. The callback receives 2 arguments.
+   * The first signifies an error and is a TokenServerClientError (or derived)
+   * type when an error occurs. If an HTTP response was seen, a RESTResponse
+   * instance will be stored in the "response" property of this object.
+   *
+   * The second argument to the callback is a map containing the results from
+   * the server. This map has the following keys:
+   *
+   *   id       (string) HTTP MAC public key identifier.
+   *   key      (string) HTTP MAC shared symmetric key.
+   *   endpoint (string) URL where service can be connected to.
+   *   uid      (string) user ID for requested service.
+   *
+   * e.g.
+   *
+   *   let client = new TokenServerClient();
+   *   let assertion = getBrowserIDAssertionFromSomewhere();
+   *   let url = "https://token.services.mozilla.com/1.0/sync/2.0";
+   *
+   *   client.getTokenFromBrowserIDAssertion(url, assertion,
+   *                                         function(error, result) {
+   *     if (error) {
+   *       // Do error handling.
+   *       return;
+   *     }
+   *
+   *     let {id: id, key: key, uid: uid, endpoint: endpoint} = result;
+   *     // Do stuff with data and carry on.
+   *   });
+   *
+   * @param  url
+   *         (string) URL to fetch token from.
+   * @param  assertion
+   *         (string) BrowserID assertion to exchange token for.
+   * @param  cb
+   *         (function) Callback to be invoked with result of operation.
+   */
+  getTokenFromBrowserIDAssertion:
+    function getTokenFromBrowserIDAssertion(url, assertion, cb) {
+    if (!url) {
+      throw new TokenServerClientError("url argument is not valid.");
+    }
+
+    if (!assertion) {
+      throw new TokenServerClientError("assertion argument is not valid.");
+    }
+
+    if (!cb) {
+      throw new TokenServerClientError("cb argument is not valid.");
+    }
+
+    this._log.debug("Beginning BID assertion exchange: " + url);
+
+    let req = new RESTRequest(url);
+    req.setHeader("accept", "application/json");
+    req.setHeader("authorization", "Browser-ID " + assertion);
+    let client = this;
+    req.get(function onResponse(error) {
+      if (error) {
+        cb(new TokenServerClientNetworkError(error), null);
+        return;
+      }
+
+      try {
+        client._processTokenResponse(this.response, cb);
+      } catch (ex) {
+        let error = new TokenServerClientError(ex);
+        error.response = this.response;
+        cb(error, null);
+        return;
+      }
+    });
+  },
+
+  /**
+   * Handler to process token request responses.
+   *
+   * @param response
+   *        RESTResponse from token HTTP request.
+   * @param cb
+   *        The original callback passed to the public API.
+   */
+  _processTokenResponse: function processTokenResponse(response, cb) {
+    this._log.debug("Got token response.");
+
+    if (!response.success) {
+      this._log.info("Non-200 response code to token request: " +
+                     response.status);
+      this._log.debug("Response body: " + response.body);
+      let error = new TokenServerClientServerError("Non 200 response code: " +
+                                                   response.status);
+      error.response = response;
+      cb(error, null);
+      return;
+    }
+
+    let ct = response.headers["content-type"];
+    if (ct != "application/json" && ct.indexOf("application/json;") != 0) {
+      let error =  new TokenServerClientError("Unsupported media type: " + ct);
+      error.response = response;
+      cb(error, null);
+      return;
+    }
+
+    let result;
+    try {
+      result = JSON.parse(response.body);
+    } catch (ex) {
+      let error = new TokenServerClientServerError("Invalid JSON returned " +
+                                                   "from server.");
+      error.response = response;
+      cb(error, null);
+      return;
+    }
+
+    for each (let k in ["id", "secret", "api_endpoint", "uid"]) {
+      if (!(k in result)) {
+        let error = new TokenServerClientServerError("Expected key not " +
+                                                     " present in result: " +
+                                                     k);
+        error.response = response;
+        cb(error, null);
+        return;
+      }
+    }
+
+    this._log.debug("Successful token response: " + result.id);
+    cb(null, {
+      id:       result.id,
+      key:      result.secret,
+      endpoint: result.api_endpoint,
+      uid:      result.uid,
+    });
+  }
+};
new file mode 100644
--- /dev/null
+++ b/services/common/utils.js
@@ -0,0 +1,161 @@
+/* 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/. */
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+const EXPORTED_SYMBOLS = ["CommonUtils"];
+
+Cu.import("resource://gre/modules/Services.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+Cu.import("resource://services-common/log4moz.js");
+
+let CommonUtils = {
+  exceptionStr: function exceptionStr(e) {
+    let message = e.message ? e.message : e;
+    return message + " " + CommonUtils.stackTrace(e);
+  },
+
+  stackTrace: function stackTrace(e) {
+    // Wrapped nsIException
+    if (e.location) {
+      let frame = e.location;
+      let output = [];
+      while (frame) {
+        // Works on frames or exceptions, munges file:// URIs to shorten the paths
+        // FIXME: filename munging is sort of hackish, might be confusing if
+        // there are multiple extensions with similar filenames
+        let str = "<file:unknown>";
+
+        let file = frame.filename || frame.fileName;
+        if (file){
+          str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
+        }
+
+        if (frame.lineNumber){
+          str += ":" + frame.lineNumber;
+        }
+        if (frame.name){
+          str = frame.name + "()@" + str;
+        }
+
+        if (str){
+          output.push(str);
+        }
+        frame = frame.caller;
+      }
+      return "Stack trace: " + output.join(" < ");
+    }
+    // Standard JS exception
+    if (e.stack){
+      return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
+        replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
+    }
+
+    return "No traceback available";
+  },
+
+  /**
+   * Create a nsIURI instance from a string.
+   */
+  makeURI: function makeURI(URIString) {
+    if (!URIString)
+      return null;
+    try {
+      return Services.io.newURI(URIString, null, null);
+    } catch (e) {
+      let log = Log4Moz.repository.getLogger("Common.Utils");
+      log.debug("Could not create URI: " + CommonUtils.exceptionStr(e));
+      return null;
+    }
+  },
+
+  /**
+   * Execute a function on the next event loop tick.
+   *
+   * @param callback
+   *        Function to invoke.
+   * @param thisObj [optional]
+   *        Object to bind the callback to.
+   */
+  nextTick: function nextTick(callback, thisObj) {
+    if (thisObj) {
+      callback = callback.bind(thisObj);
+    }
+    Services.tm.currentThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
+  },
+
+  /**
+   * Return a timer that is scheduled to call the callback after waiting the
+   * provided time or as soon as possible. The timer will be set as a property
+   * of the provided object with the given timer name.
+   */
+  namedTimer: function namedTimer(callback, wait, thisObj, name) {
+    if (!thisObj || !name) {
+      throw "You must provide both an object and a property name for the timer!";
+    }
+
+    // Delay an existing timer if it exists
+    if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
+      thisObj[name].delay = wait;
+      return;
+    }
+
+    // Create a special timer that we can add extra properties
+    let timer = {};
+    timer.__proto__ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
+    // Provide an easy way to clear out the timer
+    timer.clear = function() {
+      thisObj[name] = null;
+      timer.cancel();
+    };
+
+    // Initialize the timer with a smart callback
+    timer.initWithCallback({
+      notify: function notify() {
+        // Clear out the timer once it's been triggered
+        timer.clear();
+        callback.call(thisObj, timer);
+      }
+    }, wait, timer.TYPE_ONE_SHOT);
+
+    return thisObj[name] = timer;
+  },
+
+  encodeUTF8: function encodeUTF8(str) {
+    try {
+      str = this._utf8Converter.ConvertFromUnicode(str);
+      return str + this._utf8Converter.Finish();
+    } catch (ex) {
+      return null;
+    }
+  },
+
+  decodeUTF8: function decodeUTF8(str) {
+    try {
+      str = this._utf8Converter.ConvertToUnicode(str);
+      return str + this._utf8Converter.Finish();
+    } catch (ex) {
+      return null;
+    }
+  },
+
+  /**
+   * Trim excess padding from a Base64 string and atob().
+   *
+   * See bug 562431 comment 4.
+   */
+  safeAtoB: function safeAtoB(b64) {
+    let len = b64.length;
+    let over = len % 4;
+    return over ? atob(b64.substr(0, len - over)) : atob(b64);
+  },
+};
+
+XPCOMUtils.defineLazyGetter(CommonUtils, "_utf8Converter", function() {
+  let converter = Cc["@mozilla.org/intl/scriptableunicodeconverter"]
+                    .createInstance(Ci.nsIScriptableUnicodeConverter);
+  converter.charset = "UTF-8";
+  return converter;
+});
--- a/services/makefiles.sh
+++ b/services/makefiles.sh
@@ -30,30 +30,24 @@
 # use your version of this file under the terms of the MPL, indicate your
 # decision by deleting the provisions above and replace them with the notice
 # and other provisions required by the GPL or the LGPL. If you do not delete
 # the provisions above, a recipient may use your version of this file under
 # the terms of any one of the MPL, the GPL or the LGPL.
 #
 # ***** END LICENSE BLOCK *****
 
-MAKEFILES_crypto="
+add_makefiles "
+  services/Makefile
+  services/common/Makefile
   services/crypto/Makefile
   services/crypto/component/Makefile
-"
-
-MAKEFILES_sync="
   services/sync/Makefile
   services/sync/locales/Makefile
 "
 
-add_makefiles "
-  services/Makefile
-  $MAKEFILES_crypto
-  $MAKEFILES_sync
-"
-
 if [ "$ENABLE_TESTS" ]; then
   add_makefiles "
+    services/common/tests/Makefile
     services/crypto/tests/Makefile
     services/sync/tests/Makefile
   "
 fi
--- a/services/sync/SyncComponents.manifest
+++ b/services/sync/SyncComponents.manifest
@@ -1,9 +1,10 @@
 # Weave.js
 component {74b89fb0-f200-4ae8-a3ec-dd164117f6de} Weave.js
 contract @mozilla.org/weave/service;1 {74b89fb0-f200-4ae8-a3ec-dd164117f6de}
 category app-startup WeaveService service,@mozilla.org/weave/service;1
 component {d28f8a0b-95da-48f4-b712-caf37097be41} Weave.js
 contract @mozilla.org/network/protocol/about;1?what=sync-log {d28f8a0b-95da-48f4-b712-caf37097be41}
 # Register resource aliases
 resource services-sync resource:///modules/services-sync/
+resource services-common resource:///modules/services-common/
 resource services-crypto resource:///modules/services-crypto/
--- a/services/sync/modules/addonsreconciler.js
+++ b/services/sync/modules/addonsreconciler.js
@@ -47,17 +47,17 @@
  * standalone file so it could be more easily understood, tested, and
  * hopefully ported.
  */
 
 "use strict";
 
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://gre/modules/AddonManager.jsm");
 
 const DEFAULT_STATE_FILE = "addonsreconciler";
 
 const CHANGE_INSTALLED   = 1;
 const CHANGE_UNINSTALLED = 2;
--- a/services/sync/modules/engines.js
+++ b/services/sync/modules/engines.js
@@ -41,22 +41,22 @@
 const EXPORTED_SYMBOLS = ['Engines', 'Engine', 'SyncEngine',
                           'Tracker', 'Store'];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://services-sync/main.js");    // So we can get to Service for callbacks.
 
 /*
  * Trackers are associated with a single engine and deal with
  * listening for changes to their particular data type.
--- a/services/sync/modules/engines/addons.js
+++ b/services/sync/modules/engines/addons.js
@@ -67,18 +67,18 @@
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://services-sync/addonsreconciler.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/async.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/async.js");
+Cu.import("resource://services-common/preferences.js");
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/AddonRepository.jsm");
 
 const EXPORTED_SYMBOLS = ["AddonsEngine"];
 
 // 7 days in milliseconds.
 const PRUNE_ADDON_CHANGES_THRESHOLD = 60 * 60 * 24 * 7 * 1000;
--- a/services/sync/modules/engines/bookmarks.js
+++ b/services/sync/modules/engines/bookmarks.js
@@ -46,17 +46,17 @@ const EXPORTED_SYMBOLS = ['BookmarksEngi
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
 
 Cu.import("resource://services-sync/main.js");      // For access to Service.
 
 const ALLBOOKMARKS_ANNO    = "AllBookmarks";
 const DESCRIPTION_ANNO     = "bookmarkProperties/description";
 const SIDEBAR_ANNO         = "bookmarkProperties/loadInSidebar";
--- a/services/sync/modules/engines/clients.js
+++ b/services/sync/modules/engines/clients.js
@@ -37,19 +37,19 @@
  * ***** END LICENSE BLOCK ***** */
 
 const EXPORTED_SYMBOLS = ["Clients", "ClientsRec"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
+Cu.import("resource://services-common/stringbundle.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/ext/StringBundle.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/main.js");
 
 const CLIENTS_TTL = 1814400; // 21 days
 const CLIENTS_TTL_REFRESH = 604800; // 7 days
 
--- a/services/sync/modules/engines/forms.js
+++ b/services/sync/modules/engines/forms.js
@@ -38,20 +38,20 @@ const EXPORTED_SYMBOLS = ['FormEngine', 
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 const FORMS_TTL = 5184000; // 60 days
 
 function FormRec(collection, id) {
   CryptoWrapper.call(this, collection, id);
 }
 FormRec.prototype = {
   __proto__: CryptoWrapper.prototype,
--- a/services/sync/modules/engines/history.js
+++ b/services/sync/modules/engines/history.js
@@ -46,19 +46,19 @@ const Cu = Components.utils;
 const Cr = Components.results;
 
 const HISTORY_TTL = 5184000; // 60 days
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function HistoryRec(collection, id) {
   CryptoWrapper.call(this, collection, id);
 }
 HistoryRec.prototype = {
   __proto__: CryptoWrapper.prototype,
   _logName: "Sync.Record.History",
   ttl: HISTORY_TTL
--- a/services/sync/modules/engines/prefs.js
+++ b/services/sync/modules/engines/prefs.js
@@ -42,17 +42,17 @@ const Ci = Components.interfaces;
 const Cu = Components.utils;
 
 const WEAVE_SYNC_PREFS = "services.sync.prefs.sync.";
 
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
 
 const PREFS_GUID = Utils.encodeBase64url(Services.appinfo.ID);
 
 function PrefRec(collection, id) {
   CryptoWrapper.call(this, collection, id);
 }
 PrefRec.prototype = {
--- a/services/sync/modules/engines/tabs.js
+++ b/services/sync/modules/engines/tabs.js
@@ -46,17 +46,17 @@ const TABS_TTL = 604800; // 7 days
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 
 // It is safer to inspect the private browsing preferences rather than
 // the flags of nsIPrivateBrowsingService.  The user may have turned on
 // "Never remember history" in the same session, or Firefox was started
 // with the -private command line argument.  In both cases, the
 // "autoStarted" flag of nsIPrivateBrowsingService will be wrong.
 const PBPrefs = new Preferences("browser.privatebrowsing.");
 
--- a/services/sync/modules/identity.js
+++ b/services/sync/modules/identity.js
@@ -5,17 +5,17 @@
 "use strict";
 
 const EXPORTED_SYMBOLS = ["Identity", "IdentityManager"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 XPCOMUtils.defineLazyGetter(this, "Identity", function() {
   return new IdentityManager();
 });
 
 /**
  * Manages identity and authentication for Sync.
--- a/services/sync/modules/jpakeclient.js
+++ b/services/sync/modules/jpakeclient.js
@@ -35,18 +35,18 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/log4moz.js");
-Cu.import("resource://services-sync/rest.js");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/util.js");
 
 const EXPORTED_SYMBOLS = ["JPAKEClient"];
 
 const REQUEST_TIMEOUT         = 60; // 1 minute
 const KEYEXCHANGE_VERSION     = 3;
 
--- a/services/sync/modules/keys.js
+++ b/services/sync/modules/keys.js
@@ -7,17 +7,17 @@
 const EXPORTED_SYMBOLS = [
   "BulkKeyBundle",
   "SyncKeyBundle"
 ];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 /**
  * Represents a pair of keys.
  *
  * Each key stored in a key bundle is 256 bits. One key is used for symmetric
  * encryption. The other is used for HMAC.
  *
--- a/services/sync/modules/notifications.js
+++ b/services/sync/modules/notifications.js
@@ -36,18 +36,18 @@
 
 const EXPORTED_SYMBOLS = ["Notifications", "Notification", "NotificationButton"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/ext/Observers.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/observers.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 let Notifications = {
   // Match the referenced values in toolkit/content/widgets/notification.xml.
   get PRIORITY_INFO()     1, // PRIORITY_INFO_LOW
   get PRIORITY_WARNING()  4, // PRIORITY_WARNING_LOW
   get PRIORITY_ERROR()    7, // PRIORITY_CRITICAL_LOW
 
--- a/services/sync/modules/policies.js
+++ b/services/sync/modules/policies.js
@@ -40,17 +40,17 @@
 
 const EXPORTED_SYMBOLS = ["SyncScheduler",
                           "ErrorHandler",
                           "SendCredentialsController"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
 Cu.import("resource://services-sync/status.js");
 
 Cu.import("resource://services-sync/main.js");    // So we can get to Service for callbacks.
 
 let SyncScheduler = {
--- a/services/sync/modules/record.js
+++ b/services/sync/modules/record.js
@@ -46,17 +46,17 @@ const Cr = Components.results;
 const Cu = Components.utils;
 
 const CRYPTO_COLLECTION = "crypto";
 const KEYS_WBO = "keys";
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 function WBORecord(collection, id) {
   this.data = {};
   this.payload = {};
   this.collection = collection;      // Optional.
   this.id = id;                      // Optional.
--- a/services/sync/modules/resource.js
+++ b/services/sync/modules/resource.js
@@ -42,22 +42,22 @@ const EXPORTED_SYMBOLS = [
   "Resource"
 ];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Observers.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/observers.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 /*
  * AsyncResource represents a remote network resource, identified by a URI.
  * Create an instance like so:
  * 
  *   let resource = new AsyncResource("http://foobar.com/path/to/resource");
  * 
--- a/services/sync/modules/rest.js
+++ b/services/sync/modules/rest.js
@@ -35,592 +35,27 @@
  * and other provisions required by the GPL or the LGPL. If you do not delete
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
 
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/constants.js");
 
-const EXPORTED_SYMBOLS = ["RESTRequest", "SyncStorageRequest"];
+const EXPORTED_SYMBOLS = ["SyncStorageRequest"];
 
 const STORAGE_REQUEST_TIMEOUT = 5 * 60; // 5 minutes
 
 /**
- * Single use HTTP requests to RESTish resources.
- * 
- * @param uri
- *        URI for the request. This can be an nsIURI object or a string
- *        that can be used to create one. An exception will be thrown if
- *        the string is not a valid URI.
- *
- * Examples:
- *
- * (1) Quick GET request:
- *
- *   new RESTRequest("http://server/rest/resource").get(function (error) {
- *     if (error) {
- *       // Deal with a network error.
- *       processNetworkErrorCode(error.result);
- *       return;
- *     }
- *     if (!this.response.success) {
-
- *  *       // Bail out if we're not getting an HTTP 2xx code.
- *       processHTTPError(this.response.status);
- *       return;
- *     }
- *     processData(this.response.body);
- *   });
- *
- * (2) Quick PUT request (non-string data is automatically JSONified)
- *
- *   new RESTRequest("http://server/rest/resource").put(data, function (error) {
- *     ...
- *   });
- *
- * (3) Streaming GET
- *
- *   let request = new RESTRequest("http://server/rest/resource");
- *   request.setHeader("Accept", "application/newlines");
- *   request.onComplete = function (error) {
- *     if (error) {
- *       // Deal with a network error.
- *       processNetworkErrorCode(error.result);
- *       return;
- *     }
- *     callbackAfterRequestHasCompleted()
- *   });
- *   request.onProgress = function () {
- *     if (!this.response.success) {
- *       // Bail out if we're not getting an HTTP 2xx code.
- *       return;
- *     }
- *     // Process body data and reset it so we don't process the same data twice.
- *     processIncrementalData(this.response.body);
- *     this.response.body = "";
- *   });
- *   request.get();
- */
-function RESTRequest(uri) {
-  this.status = this.NOT_SENT;
-
-  // If we don't have an nsIURI object yet, make one. This will throw if
-  // 'uri' isn't a valid URI string.
-  if (!(uri instanceof Ci.nsIURI)) {
-    uri = Services.io.newURI(uri, null, null);
-  }
-  this.uri = uri;
-
-  this._headers = {};
-  this._log = Log4Moz.repository.getLogger(this._logName);
-  this._log.level =
-    Log4Moz.Level[Svc.Prefs.get("log.logger.network.resources")];
-}
-RESTRequest.prototype = {
-
-  _logName: "Sync.RESTRequest",
-
-  QueryInterface: XPCOMUtils.generateQI([
-    Ci.nsIBadCertListener2,
-    Ci.nsIInterfaceRequestor,
-    Ci.nsIChannelEventSink
-  ]),
-
-  /*** Public API: ***/
-
-  /**
-   * URI for the request (an nsIURI object).
-   */
-  uri: null,
-
-  /**
-   * HTTP method (e.g. "GET")
-   */
-  method: null,
-
-  /**
-   * RESTResponse object
-   */
-  response: null,
-
-  /**
-   * nsIRequest load flags. Don't do any caching by default.
-   */
-  loadFlags: Ci.nsIRequest.LOAD_BYPASS_CACHE | Ci.nsIRequest.INHIBIT_CACHING,
-
-  /**
-   * nsIHttpChannel
-   */
-  channel: null,
-
-  /**
-   * Flag to indicate the status of the request.
-   *
-   * One of NOT_SENT, SENT, IN_PROGRESS, COMPLETED, ABORTED.
-   */
-  status: null,
-
-  NOT_SENT:    0,
-  SENT:        1,
-  IN_PROGRESS: 2,
-  COMPLETED:   4,
-  ABORTED:     8,
-
-  /**
-   * Request timeout (in seconds, though decimal values can be used for
-   * up to millisecond granularity.)
-   *
-   * 0 for no timeout.
-   */
-  timeout: null,
-
-  /**
-   * Called when the request has been completed, including failures and
-   * timeouts.
-   * 
-   * @param error
-   *        Error that occurred while making the request, null if there
-   *        was no error.
-   */
-  onComplete: function onComplete(error) {
-  },
-
-  /**
-   * Called whenever data is being received on the channel. If this throws an
-   * exception, the request is aborted and the exception is passed as the
-   * error to onComplete().
-   */
-  onProgress: function onProgress() {
-  },
-
-  /**
-   * Set a request header.
-   */
-  setHeader: function setHeader(name, value) {
-    this._headers[name.toLowerCase()] = value;
-  },
-
-  /**
-   * Perform an HTTP GET.
-   *
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
-   */
-  get: function get(onComplete, onProgress) {
-    return this.dispatch("GET", null, onComplete, onProgress);
-  },
-
-  /**
-   * Perform an HTTP PUT.
-   *
-   * @param data
-   *        Data to be used as the request body. If this isn't a string
-   *        it will be JSONified automatically.
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
-   */
-  put: function put(data, onComplete, onProgress) {
-    return this.dispatch("PUT", data, onComplete, onProgress);
-  },
-
-  /**
-   * Perform an HTTP POST.
-   *
-   * @param data
-   *        Data to be used as the request body. If this isn't a string
-   *        it will be JSONified automatically.
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
-   */
-  post: function post(data, onComplete, onProgress) {
-    return this.dispatch("POST", data, onComplete, onProgress);
-  },
-
-  /**
-   * Perform an HTTP DELETE.
-   *
-   * @param onComplete
-   *        Short-circuit way to set the 'onComplete' method. Optional.
-   * @param onProgress
-   *        Short-circuit way to set the 'onProgress' method. Optional.
-   *
-   * @return the request object.
-   */
-  delete: function delete_(onComplete, onProgress) {
-    return this.dispatch("DELETE", null, onComplete, onProgress);
-  },
-
-  /**
-   * Abort an active request.
-   */
-  abort: function abort() {
-    if (this.status != this.SENT && this.status != this.IN_PROGRESS) {
-      throw "Can only abort a request that has been sent.";
-    }
-
-    this.status = this.ABORTED;
-    this.channel.cancel(Cr.NS_BINDING_ABORTED);
-
-    if (this.timeoutTimer) {
-      // Clear the abort timer now that the channel is done.
-      this.timeoutTimer.clear();
-    }
-  },
-
-  /*** Implementation stuff ***/
-
-  dispatch: function dispatch(method, data, onComplete, onProgress) {
-    if (this.status != this.NOT_SENT) {
-      throw "Request has already been sent!";
-    }
-
-    this.method = method;
-    if (onComplete) {
-      this.onComplete = onComplete;
-    }
-    if (onProgress) {
-      this.onProgress = onProgress;
-    }
-
-    // Create and initialize HTTP channel.
-    let channel = Services.io.newChannelFromURI(this.uri, null, null)
-                          .QueryInterface(Ci.nsIRequest)
-                          .QueryInterface(Ci.nsIHttpChannel);
-    this.channel = channel;
-    channel.loadFlags |= this.loadFlags;
-    channel.notificationCallbacks = this;
-
-    // Set request headers.
-    let headers = this._headers;
-    for (let key in headers) {
-      if (key == 'authorization') {
-        this._log.trace("HTTP Header " + key + ": ***** (suppressed)");
-      } else {
-        this._log.trace("HTTP Header " + key + ": " + headers[key]);
-      }
-      channel.setRequestHeader(key, headers[key], false);
-    }
-
-    // Set HTTP request body.
-    if (method == "PUT" || method == "POST") {
-      // Convert non-string bodies into JSON.
-      if (typeof data != "string") {
-        data = JSON.stringify(data);
-      }
-
-      this._log.debug(method + " Length: " + data.length);
-      if (this._log.level <= Log4Moz.Level.Trace) {
-        this._log.trace(method + " Body: " + data);
-      }
-
-      let stream = Cc["@mozilla.org/io/string-input-stream;1"]
-                     .createInstance(Ci.nsIStringInputStream);
-      stream.setData(data, data.length);
-
-      let type = headers["content-type"] || "text/plain";
-      channel.QueryInterface(Ci.nsIUploadChannel);
-      channel.setUploadStream(stream, type, data.length);
-    }
-    // We must set this after setting the upload stream, otherwise it
-    // will always be 'PUT'. Yeah, I know.
-    channel.requestMethod = method;
-
-    // Blast off!
-    channel.asyncOpen(this, null);
-    this.status = this.SENT;
-    this.delayTimeout();
-    return this;
-  },
-
-  /**
-   * Create or push back the abort timer that kills this request.
-   */
-  delayTimeout: function delayTimeout() {
-    if (this.timeout) {
-      Utils.namedTimer(this.abortTimeout, this.timeout * 1000, this,
-                       "timeoutTimer");
-    }
-  },
-
-  /**
-   * Abort the request based on a timeout.
-   */
-  abortTimeout: function abortTimeout() {
-    this.abort();
-    let error = Components.Exception("Aborting due to channel inactivity.",
-                                     Cr.NS_ERROR_NET_TIMEOUT);
-    if (!this.onComplete) {
-      this._log.error("Unexpected error: onComplete not defined in " +
-                      "abortTimeout.")
-      return;
-    }
-    this.onComplete(error);
-  },
-
-  /*** nsIStreamListener ***/
-
-  onStartRequest: function onStartRequest(channel) {
-    if (this.status == this.ABORTED) {
-      this._log.trace("Not proceeding with onStartRequest, request was aborted.");
-      return;
-    }
-
-    try {
-      channel.QueryInterface(Ci.nsIHttpChannel);
-    } catch (ex) {
-      this._log.error("Unexpected error: channel is not a nsIHttpChannel!");
-      this.status = this.ABORTED;
-      channel.cancel(Cr.NS_BINDING_ABORTED);
-      return;
-    }
-
-    this.status = this.IN_PROGRESS;
-
-    this._log.trace("onStartRequest: " + channel.requestMethod + " " +
-                    channel.URI.spec);
-
-    // Create a response object and fill it with some data.
-    let response = this.response = new RESTResponse();
-    response.request = this;
-    response.body = "";
-
-    // Define this here so that we don't have make a new one each time
-    // onDataAvailable() gets called.
-    this._inputStream = Cc["@mozilla.org/scriptableinputstream;1"]
-                          .createInstance(Ci.nsIScriptableInputStream);
-
-    this.delayTimeout();
-  },
-
-  onStopRequest: function onStopRequest(channel, context, statusCode) {
-    if (this.timeoutTimer) {
-      // Clear the abort timer now that the channel is done.
-      this.timeoutTimer.clear();
-    }
-
-    // We don't want to do anything for a request that's already been aborted.
-    if (this.status == this.ABORTED) {
-      this._log.trace("Not proceeding with onStopRequest, request was aborted.");
-      return;
-    }
-
-    try {
-      channel.QueryInterface(Ci.nsIHttpChannel);
-    } catch (ex) {
-      this._log.error("Unexpected error: channel not nsIHttpChannel!");
-      this.status = this.ABORTED;
-      return;
-    }
-    this.status = this.COMPLETED;
-
-    let statusSuccess = Components.isSuccessCode(statusCode);
-    let uri = channel && channel.URI && channel.URI.spec || "<unknown>";
-    this._log.trace("Channel for " + channel.requestMethod + " " + uri +
-                    " returned status code " + statusCode);
-
-    if (!this.onComplete) {
-      this._log.error("Unexpected error: onComplete not defined in " +
-                      "abortRequest.");
-      this.onProgress = null;
-      return;
-    }
-
-    // Throw the failure code and stop execution.  Use Components.Exception()
-    // instead of Error() so the exception is QI-able and can be passed across
-    // XPCOM borders while preserving the status code.
-    if (!statusSuccess) {
-      let message = Components.Exception("", statusCode).name;
-      let error = Components.Exception(message, statusCode);
-      this.onComplete(error);
-      this.onComplete = this.onProgress = null;
-      return;
-    }
-
-    this._log.debug(this.method + " " + uri + " " + this.response.status);
-
-    // Additionally give the full response body when Trace logging.
-    if (this._log.level <= Log4Moz.Level.Trace) {
-      this._log.trace(this.method + " body: " + this.response.body);
-    }
-
-    delete this._inputStream;
-
-    this.onComplete(null);
-    this.onComplete = this.onProgress = null;
-  },
-
-  onDataAvailable: function onDataAvailable(req, cb, stream, off, count) {
-    this._inputStream.init(stream);
-    try {
-      this.response.body += this._inputStream.read(count);
-    } catch (ex) {
-      this._log.warn("Exception thrown reading " + count +
-                     " bytes from the channel.");
-      this._log.debug(Utils.exceptionStr(ex));
-      throw ex;
-    }
-
-    try {
-      this.onProgress();
-    } catch (ex) {
-      this._log.warn("Got exception calling onProgress handler, aborting " +
-                     this.method + " " + req.URI.spec);
-      this._log.debug("Exception: " + Utils.exceptionStr(ex));
-      this.abort();
-
-      if (!this.onComplete) {
-        this._log.error("Unexpected error: onComplete not defined in " +
-                        "onDataAvailable.");
-        this.onProgress = null;
-        return;
-      }
-
-      this.onComplete(ex);
-      this.onComplete = this.onProgress = null;
-      return;
-    }
-
-    this.delayTimeout();
-  },
-
-  /*** nsIInterfaceRequestor ***/
-
-  getInterface: function(aIID) {
-    return this.QueryInterface(aIID);
-  },
-
-  /*** nsIBadCertListener2 ***/
-
-  notifyCertProblem: function notifyCertProblem(socketInfo, sslStatus, targetHost) {
-    this._log.warn("Invalid HTTPS certificate encountered!");
-    // Suppress invalid HTTPS certificate warnings in the UI.
-    // (The request will still fail.)
-    return true;
-  },
-
-  /*** nsIChannelEventSink ***/
-  asyncOnChannelRedirect:
-    function asyncOnChannelRedirect(oldChannel, newChannel, flags, callback) {
-
-    try {
-      newChannel.QueryInterface(Ci.nsIHttpChannel);
-    } catch (ex) {
-      this._log.error("Unexpected error: channel not nsIHttpChannel!");
-      callback.onRedirectVerifyCallback(Cr.NS_ERROR_NO_INTERFACE);
-      return;
-    }
-
-    this.channel = newChannel;
-
-    // We let all redirects proceed.
-    callback.onRedirectVerifyCallback(Cr.NS_OK);
-  }
-};
-
-
-/**
- * Response object for a RESTRequest. This will be created automatically by
- * the RESTRequest.
- */
-function RESTResponse() {
-  this._log = Log4Moz.repository.getLogger(this._logName);
-  this._log.level =
-    Log4Moz.Level[Svc.Prefs.get("log.logger.network.resources")];
-}
-RESTResponse.prototype = {
-
-  _logName: "Sync.RESTResponse",
-
-  /**
-   * Corresponding REST request
-   */
-  request: null,
-
-  /**
-   * HTTP status code
-   */
-  get status() {
-    let status;
-    try {
-      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
-      status = channel.responseStatus;
-    } catch (ex) {
-      this._log.debug("Caught exception fetching HTTP status code:" +
-                      Utils.exceptionStr(ex));
-      return null;
-    }
-    delete this.status;
-    return this.status = status;
-  },
-
-  /**
-   * Boolean flag that indicates whether the HTTP status code is 2xx or not.
-   */
-  get success() {
-    let success;
-    try {
-      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
-      success = channel.requestSucceeded;
-    } catch (ex) {
-      this._log.debug("Caught exception fetching HTTP success flag:" +
-                      Utils.exceptionStr(ex));
-      return null;
-    }
-    delete this.success;
-    return this.success = success;
-  },
-
-  /**
-   * Object containing HTTP headers (keyed as lower case)
-   */
-  get headers() {
-    let headers = {};
-    try {
-      this._log.trace("Processing response headers.");
-      let channel = this.request.channel.QueryInterface(Ci.nsIHttpChannel);
-      channel.visitResponseHeaders(function (header, value) {
-        headers[header.toLowerCase()] = value;
-      });
-    } catch (ex) {
-      this._log.debug("Caught exception processing response headers:" +
-                      Utils.exceptionStr(ex));
-      return null;
-    }
-
-    delete this.headers;
-    return this.headers = headers;
-  },
-
-  /**
-   * HTTP body (string)
-   */
-  body: null
-
-};
-
-
-/**
  * RESTRequest variant for use against a Sync storage server.
  */
 function SyncStorageRequest(uri) {
   RESTRequest.call(this, uri);
 }
 SyncStorageRequest.prototype = {
 
   __proto__: RESTRequest.prototype,
--- a/services/sync/modules/service.js
+++ b/services/sync/modules/service.js
@@ -58,19 +58,19 @@ const KEYS_WBO = "keys";
 
 const LOG_DATE_FORMAT = "%Y-%m-%d %H:%M:%S";
 
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/clients.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/policies.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/main.js");
 
 const STORAGE_INFO_TYPES = [INFO_COLLECTIONS,
--- a/services/sync/modules/status.js
+++ b/services/sync/modules/status.js
@@ -36,17 +36,17 @@
 const EXPORTED_SYMBOLS = ["Status"];
 
 const Cc = Components.classes;
 const Ci = Components.interfaces;
 const Cr = Components.results;
 const Cu = Components.utils;
 
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://gre/modules/Services.jsm");
 
 let Status = {
   _log: Log4Moz.repository.getLogger("Sync.Status"),
   _authManager: Identity,
   ready: false,
 
--- a/services/sync/modules/util.js
+++ b/services/sync/modules/util.js
@@ -33,38 +33,47 @@
  * the provisions above, a recipient may use your version of this file under
  * the terms of any one of the MPL, the GPL or the LGPL.
  *
  * ***** END LICENSE BLOCK ***** */
 
 const EXPORTED_SYMBOLS = ["XPCOMUtils", "Services", "NetUtil", "PlacesUtils",
                           "FileUtils", "Utils", "Async", "Svc", "Str"];
 
-const Cc = Components.classes;
-const Ci = Components.interfaces;
-const Cr = Components.results;
-const Cu = Components.utils;
+const {classes: Cc, interfaces: Ci, results: Cr, utils: Cu} = Components;
 
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/preferences.js");
+Cu.import("resource://services-common/stringbundle.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Observers.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
-Cu.import("resource://services-sync/ext/StringBundle.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/FileUtils.jsm");
 
 /*
  * Utility functions
  */
 
 let Utils = {
+  // Alias in functions from CommonUtils. These previously were defined here.
+  // In the ideal world, references to these would be removed.
+  nextTick: CommonUtils.nextTick,
+  namedTimer: CommonUtils.namedTimer,
+  exceptionStr: CommonUtils.exceptionStr,
+  stackTrace: CommonUtils.stackTrace,
+  makeURI: CommonUtils.makeURI,
+  encodeUTF8: CommonUtils.encodeUTF8,
+  decodeUTF8: CommonUtils.decodeUTF8,
+  safeAtoB: CommonUtils.safeAtoB,
+
   /**
    * Wrap a function to catch all exceptions and log them
    *
    * @usage MyObj._catch = Utils.catch;
    *        MyObj.foo = function() { this._catch(func)(); }
    *        
    * Optionally pass a function which will be called if an
    * exception occurs.
@@ -264,72 +273,28 @@ let Utils = {
     // Do the same for b's keys but skip those that we already checked
     for (let k in b)
       if (!(k in a) && !eq(a[k], b[k]))
         return false;
 
     return true;
   },
 
-  exceptionStr: function Weave_exceptionStr(e) {
-    let message = e.message ? e.message : e;
-    return message + " " + Utils.stackTrace(e);
-  },
-  
-  stackTrace: function Weave_stackTrace(e) {
-    // Wrapped nsIException
-    if (e.location){
-      let frame = e.location; 
-      let output = [];
-      while (frame) {
-      	// Works on frames or exceptions, munges file:// URIs to shorten the paths
-        // FIXME: filename munging is sort of hackish, might be confusing if
-        // there are multiple extensions with similar filenames
-        let str = "<file:unknown>";
-
-        let file = frame.filename || frame.fileName;
-        if (file){
-          str = file.replace(/^(?:chrome|file):.*?([^\/\.]+\.\w+)$/, "$1");
-        }
-
-        if (frame.lineNumber){
-          str += ":" + frame.lineNumber;
-        }
-        if (frame.name){
-          str = frame.name + "()@" + str;
-        }
-
-        if (str){
-          output.push(str);
-        }
-        frame = frame.caller;
-      }
-      return "Stack trace: " + output.join(" < ");
-    }
-    // Standard JS exception
-    if (e.stack){
-      return "JS Stack trace: " + e.stack.trim().replace(/\n/g, " < ").
-        replace(/@[^@]*?([^\/\.]+\.\w+:)/g, "@$1");
-    }
-
-    return "No traceback available";
-  },
-  
   // Generator and discriminator for HMAC exceptions.
-  // Split these out in case we want to make them richer in future, and to 
+  // Split these out in case we want to make them richer in future, and to
   // avoid inevitable confusion if the message changes.
   throwHMACMismatch: function throwHMACMismatch(shouldBe, is) {
     throw "Record SHA256 HMAC mismatch: should be " + shouldBe + ", is " + is;
   },
-  
+
   isHMACMismatch: function isHMACMismatch(ex) {
     const hmacFail = "Record SHA256 HMAC mismatch: ";
     return ex && ex.indexOf && (ex.indexOf(hmacFail) == 0);
   },
-  
+
   /**
    * UTF8-encode a message and hash it with the given hasher. Returns a
    * string containing bytes. The hasher is reset if it's an HMAC hasher.
    */
   digestUTF8: function digestUTF8(message, hasher) {
     let data = this._utf8Converter.convertToByteArray(message, {});
     hasher.update(data, data.length);
     let result = hasher.finish(false);
@@ -827,28 +792,16 @@ let Utils = {
 
     if (!ext) {
       return header;
     }
 
     return header += ', ext="' + ext +'"';
   },
 
-  makeURI: function Weave_makeURI(URIString) {
-    if (!URIString)
-      return null;
-    try {
-      return Services.io.newURI(URIString, null, null);
-    } catch (e) {
-      let log = Log4Moz.repository.getLogger("Sync.Utils");
-      log.debug("Could not create URI: " + Utils.exceptionStr(e));
-      return null;
-    }
-  },
-
   /**
    * Load a json object from disk
    *
    * @param filePath
    *        Json file path load from weave/[filePath].json
    * @param that
    *        Object to use for logging and "this" for callback
    * @param callback
@@ -912,69 +865,16 @@ let Utils = {
     let is = this._utf8Converter.convertToInputStream(out);
     NetUtil.asyncCopy(is, fos, function (result) {
       if (typeof callback == "function") {
         callback.call(that);        
       }
     });
   },
 
-  /**
-   * Execute a function on the next event loop tick.
-   * 
-   * @param callback
-   *        Function to invoke.
-   * @param thisObj [optional]
-   *        Object to bind the callback to.
-   */
-  nextTick: function nextTick(callback, thisObj) {
-    if (thisObj) {
-      callback = callback.bind(thisObj);
-    }
-    Services.tm.currentThread.dispatch(callback, Ci.nsIThread.DISPATCH_NORMAL);
-  },
-
-  /**
-   * Return a timer that is scheduled to call the callback after waiting the
-   * provided time or as soon as possible. The timer will be set as a property
-   * of the provided object with the given timer name.
-   */
-  namedTimer: function delay(callback, wait, thisObj, name) {
-    if (!thisObj || !name) {
-      throw "You must provide both an object and a property name for the timer!";
-    }
-
-    // Delay an existing timer if it exists
-    if (name in thisObj && thisObj[name] instanceof Ci.nsITimer) {
-      thisObj[name].delay = wait;
-      return;
-    }
-
-    // Create a special timer that we can add extra properties
-    let timer = {};
-    timer.__proto__ = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
-
-    // Provide an easy way to clear out the timer
-    timer.clear = function() {
-      thisObj[name] = null;
-      timer.cancel();
-    };
-
-    // Initialize the timer with a smart callback
-    timer.initWithCallback({
-      notify: function notify() {
-        // Clear out the timer once it's been triggered
-        timer.clear();
-        callback.call(thisObj, timer);
-      }
-    }, wait, timer.TYPE_ONE_SHOT);
-
-    return thisObj[name] = timer;
-  },
-
   getIcon: function(iconUri, defaultIcon) {
     try {
       let iconURI = Utils.makeURI(iconUri);
       return PlacesUtils.favicons.getFaviconLinkForIcon(iconURI).spec;
     }
     catch(ex) {}
 
     // Just give the provided default icon or the system's default
@@ -985,34 +885,16 @@ let Utils = {
     try {
       return Str.errors.get(error, args || null);
     } catch (e) {}
 
     // basically returns "Unknown Error"
     return Str.errors.get("error.reason.unknown");
   },
 
-  encodeUTF8: function(str) {
-    try {
-      str = this._utf8Converter.ConvertFromUnicode(str);
-      return str + this._utf8Converter.Finish();
-    } catch(ex) {
-      return null;
-    }
-  },
-
-  decodeUTF8: function(str) {
-    try {
-      str = this._utf8Converter.ConvertToUnicode(str);
-      return str + this._utf8Converter.Finish();
-    } catch(ex) {
-      return null;
-    }
-  },
-
   /**
    * Generate 26 characters.
    */
   generatePassphrase: function generatePassphrase() {
     // Note that this is a different base32 alphabet to the one we use for
     // other tasks. It's lowercase, uses different letters, and needs to be
     // decoded with decodeKeyBase32, not just decodeBase32.
     return Utils.encodeKeyBase32(Utils.generateRandomBytes(16));
@@ -1103,25 +985,16 @@ let Utils = {
     // Something else -- just return.
     return pp;
   },
   
   normalizeAccount: function normalizeAccount(acc) {
     return acc.trim();
   },
 
-  // WeaveCrypto returns bad base64 strings. Truncate excess padding
-  // and decode.
-  // See Bug 562431, comment 4.
-  safeAtoB: function safeAtoB(b64) {
-    let len = b64.length;
-    let over = len % 4;
-    return over ? atob(b64.substr(0, len - over)) : atob(b64);
-  },
-
   /**
    * Create an array like the first but without elements of the second. Reuse
    * arrays if possible.
    */
   arraySub: function arraySub(minuend, subtrahend) {
     if (!minuend.length || !subtrahend.length)
       return minuend;
     return minuend.filter(function(i) subtrahend.indexOf(i) == -1);
--- a/services/sync/tests/unit/head_appinfo.js
+++ b/services/sync/tests/unit/head_appinfo.js
@@ -44,15 +44,15 @@ registrar.registerFactory(Components.ID(
                           XULAppInfoFactory);
 
 
 // Register resource aliases. Normally done in SyncComponents.manifest.
 function addResourceAlias() {
   Cu.import("resource://gre/modules/Services.jsm");
   const resProt = Services.io.getProtocolHandler("resource")
                           .QueryInterface(Ci.nsIResProtocolHandler);
-  let uri;
-  uri = Services.io.newURI("resource:///modules/services-sync/", null, null);
-  resProt.setSubstitution("services-sync", uri);
-  uri = Services.io.newURI("resource:///modules/services-crypto/", null, null);
-  resProt.setSubstitution("services-crypto", uri);
+  for each (let s in ["common", "sync", "crypto"]) {
+    let uri = Services.io.newURI("resource:///modules/services-" + s + "/", null,
+                                 null);
+    resProt.setSubstitution("services-" + s, uri);
+  }
 }
 addResourceAlias();
--- a/services/sync/tests/unit/head_helpers.js
+++ b/services/sync/tests/unit/head_helpers.js
@@ -1,12 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/utils.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines.js");
 let btoa;
 let atob;
 
 let provider = {
@@ -35,52 +36,18 @@ function waitForZeroTimer(callback) {
       Utils.nextTick(wait);
       return;
     }
     callback();
   }
   timer = Utils.namedTimer(wait, 150, {}, "timer");
 }
 
-btoa = Cu.import("resource://services-sync/log4moz.js").btoa;
-atob = Cu.import("resource://services-sync/log4moz.js").atob;
-function getTestLogger(component) {
-  return Log4Moz.repository.getLogger("Testing");
-}
-
-function initTestLogging(level) {
-  function LogStats() {
-    this.errorsLogged = 0;
-  }
-  LogStats.prototype = {
-    format: function BF_format(message) {
-      if (message.level == Log4Moz.Level.Error)
-        this.errorsLogged += 1;
-      return message.loggerName + "\t" + message.levelDesc + "\t" +
-        message.message + "\n";
-    }
-  };
-  LogStats.prototype.__proto__ = new Log4Moz.Formatter();
-
-  var log = Log4Moz.repository.rootLogger;
-  var logStats = new LogStats();
-  var appender = new Log4Moz.DumpAppender(logStats);
-
-  if (typeof(level) == "undefined")
-    level = "Debug";
-  getTestLogger().level = Log4Moz.Level[level];
-
-  log.level = Log4Moz.Level.Trace;
-  appender.level = Log4Moz.Level.Trace;
-  // Overwrite any other appenders (e.g. from previous incarnations)
-  log.ownAppenders = [appender];
-  log.updateAppenders();
-
-  return logStats;
-}
+btoa = Cu.import("resource://services-common/log4moz.js").btoa;
+atob = Cu.import("resource://services-common/log4moz.js").atob;
 
 // This is needed for loadAddonTestFunctions().
 let gGlobalScope = this;
 
 function ExtensionsTestPath(path) {
   if (path[0] != "/") {
     throw Error("Path must begin with '/': " + path);
   }
@@ -293,31 +260,19 @@ function ensureThrows(func) {
       func.apply(this, arguments);
     } catch (ex) {
       do_throw(ex);
     }
   };
 }
 
 
-/**
- * Print some debug message to the console. All arguments will be printed,
- * separated by spaces.
- *
- * @param [arg0, arg1, arg2, ...]
- *        Any number of arguments to print out
- * @usage _("Hello World") -> prints "Hello World"
- * @usage _(1, 2, 3) -> prints "1 2 3"
- */
-let _ = function(some, debug, text, to) print(Array.slice(arguments).join(" "));
-
 _("Setting the identity for passphrase");
 Cu.import("resource://services-sync/identity.js");
 
-
 /*
  * Test setup helpers.
  */
 
 // Turn WBO cleartext into fake "encrypted" payload as it goes over the wire.
 function encryptPayload(cleartext) {
   if (typeof cleartext == "object") {
     cleartext = JSON.stringify(cleartext);
@@ -329,42 +284,16 @@ function encryptPayload(cleartext) {
 }
 
 function generateNewKeys(collections) {
   let wbo = CollectionKeys.generateNewKeysWBO(collections);
   let modified = new_timestamp();
   CollectionKeys.setContents(wbo.cleartext, modified);
 }
 
-function do_check_empty(obj) {
-  do_check_attribute_count(obj, 0);
-}
-
-function do_check_attribute_count(obj, c) {
-  do_check_eq(c, Object.keys(obj).length);
-}
-
-function do_check_throws(aFunc, aResult, aStack)
-{
-  if (!aStack) {
-    try {
-      // We might not have a 'Components' object.
-      aStack = Components.stack.caller;
-    } catch (e) {}
-  }
-
-  try {
-    aFunc();
-  } catch (e) {
-    do_check_eq(e.result, aResult, aStack);
-    return;
-  }
-  do_throw("Expected result " + aResult + ", none thrown.", aStack);
-}
-
 /*
  * A fake engine implementation.
  * This is used all over the place.
  *
  * Complete with record, store, and tracker implementations.
  */
 
 function RotaryRecord(collection, id) {
@@ -482,9 +411,9 @@ deepCopy: function deepCopy(thing, noSor
     let props = [p for (p in thing)];
     if (!noSort){
       props = props.sort();
     }
     props.forEach(function(k) ret[k] = deepCopy(thing[k], noSort));
   }
 
   return ret;
-};
\ No newline at end of file
+};
--- a/services/sync/tests/unit/head_http_server.js
+++ b/services/sync/tests/unit/head_http_server.js
@@ -1,15 +1,15 @@
 const Cm = Components.manager;
 
 const TEST_CLUSTER_URL = "http://localhost:8080/";
 const TEST_SERVER_URL  = "http://localhost:8080/";
 
 // Shared logging for all HTTP server functions.
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 const SYNC_HTTP_LOGGER = "Sync.Test.Server";
 const SYNC_API_VERSION = "1.1";
 
 // Use the same method that record.js does, which mirrors the server.
 // The server returns timestamps with 1/100 sec granularity. Note that this is
 // subject to change: see Bug 650435.
 function new_timestamp() {
   return Math.round(Date.now() / 10) / 100;
@@ -21,49 +21,16 @@ function return_timestamp(request, respo
   }
   let body = "" + timestamp;
   response.setHeader("X-Weave-Timestamp", body);
   response.setStatusLine(request.httpVersion, 200, "OK");
   response.bodyOutputStream.write(body, body.length);
   return timestamp;
 }
 
-function httpd_setup (handlers, port) {
-  let port   = port || 8080;
-  let server = new nsHttpServer();
-  for (let path in handlers) {
-    server.registerPathHandler(path, handlers[path]);
-  }
-  try {
-    server.start(port);
-  } catch (ex) {
-    _("==========================================");
-    _("Got exception starting HTTP server on port " + port);
-    _("Error: " + Utils.exceptionStr(ex));
-    _("Is there a process already listening on port " + port + "?");
-    _("==========================================");
-    do_throw(ex);
-  }
-
-  return server;
-}
-
-function httpd_handler(statusCode, status, body) {
-  return function handler(request, response) {
-    // Allow test functions to inspect the request.
-    request.body = readBytesFromInputStream(request.bodyInputStream);
-    handler.request = request;
-
-    response.setStatusLine(request.httpVersion, statusCode, status);
-    if (body) {
-      response.bodyOutputStream.write(body, body.length);
-    }
-  };
-}
-
 function basic_auth_header(user, password) {
   return "Basic " + btoa(user + ":" + Utils.encodeUTF8(password));
 }
 
 function basic_auth_matches(req, user, password) {
   if (!req.hasHeader("Authorization")) {
     return false;
   }
@@ -80,31 +47,16 @@ function httpd_basic_auth_handler(body, 
     body = "This path exists and is protected - failed";
     response.setStatusLine(metadata.httpVersion, 401, "Unauthorized");
     response.setHeader("WWW-Authenticate", 'Basic realm="secret"', false);
   }
   response.bodyOutputStream.write(body, body.length);
 }
 
 /*
- * Read bytes string from an nsIInputStream.  If 'count' is omitted,
- * all available input is read.
- */
-function readBytesFromInputStream(inputStream, count) {
-  var BinaryInputStream = Components.Constructor(
-      "@mozilla.org/binaryinputstream;1",
-      "nsIBinaryInputStream",
-      "setInputStream");
-  if (!count) {
-    count = inputStream.available();
-  }
-  return new BinaryInputStream(inputStream).readBytes(count);
-}
-
-/*
  * Represent a WBO on the server
  */
 function ServerWBO(id, initialPayload, modified) {
   if (!id) {
     throw "No ID for ServerWBO!";
   }
   this.id = id;
   if (!initialPayload) {
@@ -1039,56 +991,8 @@ function serverForUsers(users, contents,
   let server = new SyncServer(callback);
   for (let [user, pass] in Iterator(users)) {
     server.registerUser(user, pass);
     server.createContents(user, contents);
   }
   server.start();
   return server;
 }
-
-/**
- * Proxy auth helpers.
- */
-
-/**
- * Fake a PAC to prompt a channel replacement.
- */
-let PACSystemSettings = {
-  CID: Components.ID("{5645d2c1-d6d8-4091-b117-fe7ee4027db7}"),
-  contractID: "@mozilla.org/system-proxy-settings;1",
-
-  QueryInterface: XPCOMUtils.generateQI([Ci.nsIFactory,
-                                         Ci.nsISystemProxySettings]),
-
-  createInstance: function createInstance(outer, iid) {
-    if (outer) {
-      throw Cr.NS_ERROR_NO_AGGREGATION;
-    }
-    return this.QueryInterface(iid);
-  },
-
-  lockFactory: function lockFactory(lock) {
-    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-  },
-  
-  // Replace this URI for each test to avoid caching. We want to ensure that
-  // each test gets a completely fresh setup.
-  PACURI: null,
-  getProxyForURI: function getProxyForURI(aURI) {
-    throw Cr.NS_ERROR_NOT_IMPLEMENTED;
-  }
-};
-
-function installFakePAC() {
-  _("Installing fake PAC.");
-  Cm.nsIComponentRegistrar
-    .registerFactory(PACSystemSettings.CID,
-                     "Fake system proxy-settings",
-                     PACSystemSettings.contractID,
-                     PACSystemSettings);
-}
-
-function uninstallFakePAC() {
-  _("Uninstalling fake PAC.");
-  let CID = PACSystemSettings.CID;
-  Cm.nsIComponentRegistrar.unregisterFactory(CID, PACSystemSettings);
-}
--- a/services/sync/tests/unit/test_addons_engine.js
+++ b/services/sync/tests/unit/test_addons_engine.js
@@ -1,19 +1,19 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://services-sync/addonsreconciler.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/engines/addons.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://services-sync/service.js");
 
 let prefs = new Preferences();
 prefs.set("extensions.getAddons.get.url",
           "http://localhost:8888/search/guid:%IDS%");
 
 loadAddonTestFunctions();
 startupManager();
--- a/services/sync/tests/unit/test_addons_store.js
+++ b/services/sync/tests/unit/test_addons_store.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 "use strict";
 
 Cu.import("resource://services-sync/engines/addons.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 
 const HTTP_PORT = 8888;
 
 let prefs = new Preferences();
 
 Svc.Prefs.set("addons.ignoreRepositoryChecking", true);
 prefs.set("extensions.getAddons.get.url", "http://localhost:8888/search/guid:%IDS%");
 loadAddonTestFunctions();
--- a/services/sync/tests/unit/test_bookmark_engine.js
+++ b/services/sync/tests/unit/test_bookmark_engine.js
@@ -1,13 +1,13 @@
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/log4moz.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/log4moz.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 Engines.register(BookmarksEngine);
 var syncTesting = new SyncTestingInfrastructure();
 
--- a/services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js
+++ b/services/sync/tests/unit/test_bookmark_legacy_microsummaries_support.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 // Tests that Sync can correctly handle a legacy microsummary record
 
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://gre/modules/Services.jsm");
 Cu.import("resource://gre/modules/NetUtil.jsm");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 const GENERATORURI_ANNO = "microsummary/generatorURI";
 const STATICTITLE_ANNO = "bookmarks/staticTitle";
--- a/services/sync/tests/unit/test_bookmark_livemarks.js
+++ b/services/sync/tests/unit/test_bookmark_livemarks.js
@@ -1,9 +1,9 @@
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
--- a/services/sync/tests/unit/test_bookmark_record.js
+++ b/services/sync/tests/unit/test_bookmark_record.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 function prepareBookmarkItem(collection, id) {
   let b = new Bookmark(collection, id);
   b.cleartext.stuff = "my payload here";
   return b;
 }
 
--- a/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
+++ b/services/sync/tests/unit/test_bookmark_smart_bookmarks.js
@@ -1,12 +1,12 @@
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 Cu.import("resource://services-sync/record.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://gre/modules/PlacesUtils.jsm");
 
 const SMART_BOOKMARKS_ANNO = "Places/SmartBookmark";
 var IOService = Cc["@mozilla.org/network/io-service;1"]
                 .getService(Ci.nsIIOService);
--- a/services/sync/tests/unit/test_corrupt_keys.js
+++ b/services/sync/tests/unit/test_corrupt_keys.js
@@ -2,17 +2,17 @@ Cu.import("resource://services-sync/main
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/engines/tabs.js");
 Cu.import("resource://services-sync/engines/history.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
   
 add_test(function test_locally_changed_keys() {
   let passphrase = "abcdeabcdeabcdeabcdeabcdea";
 
   let hmacErrorCount = 0;
   function counting(f) {
     return function() {
       hmacErrorCount++;
--- a/services/sync/tests/unit/test_engine.js
+++ b/services/sync/tests/unit/test_engine.js
@@ -1,10 +1,10 @@
 Cu.import("resource://services-sync/engines.js");
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/util.js");
 
 
 function SteamStore() {
   Store.call(this, "Steam");
   this.wasWiped = false;
 }
 SteamStore.prototype = {
--- a/services/sync/tests/unit/test_errorhandler_filelog.js
+++ b/services/sync/tests/unit/test_errorhandler_filelog.js
@@ -1,15 +1,15 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/policies.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 const logsdir            = FileUtils.getDir("ProfD", ["weave", "logs"], true);
 const LOG_PREFIX_SUCCESS = "success-";
 const LOG_PREFIX_ERROR   = "error-";
 const CLEANUP_DELAY      = 1000; // delay to age files for cleanup (ms)
 const DELAY_BUFFER       = 50; // buffer for timers on different OS platforms
 
 const PROLONGED_ERROR_DURATION =
--- a/services/sync/tests/unit/test_forms_tracker.js
+++ b/services/sync/tests/unit/test_forms_tracker.js
@@ -1,11 +1,11 @@
 Cu.import("resource://services-sync/engines/forms.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function run_test() {
   _("Verify we've got an empty tracker to work with.");
   let tracker = new FormEngine()._tracker;
   do_check_empty(tracker.changedIDs);
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   try {
--- a/services/sync/tests/unit/test_history_store.js
+++ b/services/sync/tests/unit/test_history_store.js
@@ -1,11 +1,11 @@
 Cu.import("resource://gre/modules/XPCOMUtils.jsm");
 Cu.import("resource://services-sync/engines/history.js");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 
 const TIMESTAMP1 = (Date.now() - 103406528) * 1000;
 const TIMESTAMP2 = (Date.now() - 6592903) * 1000;
 const TIMESTAMP3 = (Date.now() - 123894) * 1000;
 
 function queryPlaces(uri, options) {
   let query = PlacesUtils.history.getNewQuery();
--- a/services/sync/tests/unit/test_httpd_sync_server.js
+++ b/services/sync/tests/unit/test_httpd_sync_server.js
@@ -54,17 +54,17 @@ add_test(function test_url_parsing() {
   parts = server.storageRE.exec("storage");
   let [all, storage, collection, id] = parts;
   do_check_eq(all, "storage");
   do_check_eq(collection, undefined);
 
   run_next_test();
 });
 
-Cu.import("resource://services-sync/rest.js");
+Cu.import("resource://services-common/rest.js");
 function localRequest(path) {
   _("localRequest: " + path);
   let url = "http://127.0.0.1:8080" + path;
   _("url: " + url);
   return new RESTRequest(url);
 }
 
 add_test(function test_basic_http() {
--- a/services/sync/tests/unit/test_jpakeclient.js
+++ b/services/sync/tests/unit/test_jpakeclient.js
@@ -1,9 +1,9 @@
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/jpakeclient.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/util.js");
 
 const JPAKE_LENGTH_SECRET     = 8;
 const JPAKE_LENGTH_CLIENTID   = 256;
 const KEYEXCHANGE_VERSION     = 3;
@@ -185,17 +185,18 @@ function run_test() {
   // sure the J-PAKE requests don't include those data.
   setBasicCredentials("johndoe", "ilovejane");
 
   server = httpd_setup({"/new_channel": server_new_channel,
                         "/report":      server_report});
 
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.JPAKEClient").level = Log4Moz.Level.Trace;
-  Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
+  Log4Moz.repository.getLogger("Common.RESTRequest").level =
+    Log4Moz.Level.Trace;
   run_next_test();
 }
 
 
 add_test(function test_success_receiveNoPIN() {
   _("Test a successful exchange started by receiveNoPIN().");
 
   let snd = new JPAKEClient({
--- a/services/sync/tests/unit/test_load_modules.js
+++ b/services/sync/tests/unit/test_load_modules.js
@@ -1,27 +1,23 @@
 const modules = [
                  "addonsreconciler.js",
-                 "async.js",
                  "constants.js",
                  "engines/addons.js",
                  "engines/bookmarks.js",
                  "engines/clients.js",
                  "engines/forms.js",
                  "engines/history.js",
                  "engines/passwords.js",
                  "engines/prefs.js",
                  "engines/tabs.js",
                  "engines.js",
-                 "ext/Observers.js",
-                 "ext/Preferences.js",
                  "identity.js",
                  "jpakeclient.js",
                  "keys.js",
-                 "log4moz.js",
                  "main.js",
                  "notifications.js",
                  "policies.js",
                  "record.js",
                  "resource.js",
                  "rest.js",
                  "service.js",
                  "status.js",
--- a/services/sync/tests/unit/test_node_reassignment.js
+++ b/services/sync/tests/unit/test_node_reassignment.js
@@ -2,22 +2,22 @@
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 _("Test that node reassignment responses are respected on all kinds of " +
   "requests.");
 
 // Don't sync any engines by default.
 Svc.DefaultPrefs.set("registerEngines", "")
 
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/policies.js");
-Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function run_test() {
   Log4Moz.repository.getLogger("Sync.AsyncResource").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.ErrorHandler").level  = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Resource").level      = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.RESTRequest").level   = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Service").level       = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.SyncScheduler").level = Log4Moz.Level.Trace;
--- a/services/sync/tests/unit/test_places_guid_downgrade.js
+++ b/services/sync/tests/unit/test_places_guid_downgrade.js
@@ -1,9 +1,9 @@
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/engines/history.js");
 Cu.import("resource://services-sync/engines/bookmarks.js");
 
 const kDBName = "places.sqlite";
 const storageSvc = Cc["@mozilla.org/storage/service;1"]
                      .getService(Ci.mozIStorageService);
--- a/services/sync/tests/unit/test_prefs_store.js
+++ b/services/sync/tests/unit/test_prefs_store.js
@@ -1,11 +1,11 @@
 Cu.import("resource://services-sync/engines/prefs.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 Cu.import("resource://gre/modules/LightweightThemeManager.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
 
 const PREFS_GUID = Utils.encodeBase64url(Services.appinfo.ID);
 
 function makePersona(id) {
   return {
     id: id || Math.random().toString(),
--- a/services/sync/tests/unit/test_prefs_tracker.js
+++ b/services/sync/tests/unit/test_prefs_tracker.js
@@ -1,12 +1,12 @@
 Cu.import("resource://services-sync/engines/prefs.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 
 function run_test() {
   let engine = new PrefsEngine();
   let tracker = engine._tracker;
   let prefs = new Preferences();
 
   try {
 
--- a/services/sync/tests/unit/test_records_crypto.js
+++ b/services/sync/tests/unit/test_records_crypto.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/resource.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/util.js");
 
 let cryptoWrap;
 
 function crypted_resource_handler(metadata, response) {
   let obj = {id: "resource",
              modified: cryptoWrap.modified,
              payload: JSON.stringify(cryptoWrap.payload)};
--- a/services/sync/tests/unit/test_resource.js
+++ b/services/sync/tests/unit/test_resource.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 let logger;
 
 let fetched = false;
 function server_open(metadata, response) {
   let body;
--- a/services/sync/tests/unit/test_resource_async.js
+++ b/services/sync/tests/unit/test_resource_async.js
@@ -1,14 +1,14 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/identity.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/resource.js");
 Cu.import("resource://services-sync/util.js");
 
 const RES_UPLOAD_URL = "http://localhost:8080/upload";
 const RES_HEADERS_URL = "http://localhost:8080/headers";
 
 let logger;
 
--- a/services/sync/tests/unit/test_service_changePassword.js
+++ b/services/sync/tests/unit/test_service_changePassword.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/main.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/constants.js");
 
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function run_test() {
   initTestLogging("Trace");
   Log4Moz.repository.getLogger("Sync.AsyncResource").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Resource").level = Log4Moz.Level.Trace;
   Log4Moz.repository.getLogger("Sync.Service").level = Log4Moz.Level.Trace;
 
   run_next_test();
--- a/services/sync/tests/unit/test_service_detect_upgrade.js
+++ b/services/sync/tests/unit/test_service_detect_upgrade.js
@@ -5,17 +5,17 @@ Cu.import("resource://services-sync/main
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/record.js");
 Cu.import("resource://services-sync/keys.js");
 Cu.import("resource://services-sync/engines/tabs.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
   
 Engines.register(TabEngine);
 
 add_test(function v4_upgrade() {
   let passphrase = "abcdeabcdeabcdeabcdeabcdea";
 
   let clients = new ServerCollection();
   let meta_global = new ServerWBO('global');
--- a/services/sync/tests/unit/test_service_getStorageInfo.js
+++ b/services/sync/tests/unit/test_service_getStorageInfo.js
@@ -1,13 +1,13 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
+Cu.import("resource://services-common/rest.js");
 Cu.import("resource://services-sync/service.js");
-Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/util.js");
 
 let collections = {steam:  65.11328,
                    petrol: 82.488281,
                    diesel: 2.25488281};
 
 function run_test() {
--- a/services/sync/tests/unit/test_service_login.js
+++ b/services/sync/tests/unit/test_service_login.js
@@ -1,10 +1,10 @@
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/policies.js");
 
 function login_handling(handler) {
   return function (request, response) {
     if (basic_auth_matches(request, "johndoe", "ilovejane") ||
--- a/services/sync/tests/unit/test_service_migratePrefs.js
+++ b/services/sync/tests/unit/test_service_migratePrefs.js
@@ -1,9 +1,9 @@
-Cu.import("resource://services-sync/ext/Preferences.js");
+Cu.import("resource://services-common/preferences.js");
 
 function test_migrate_logging() {
   _("Testing log pref migration.");
   Svc.Prefs.set("log.appender.debugLog", "Warn");
   Svc.Prefs.set("log.appender.debugLog.enabled", true);
   do_check_true(Svc.Prefs.get("log.appender.debugLog.enabled"));
   do_check_eq(Svc.Prefs.get("log.appender.file.level"), "Trace");
   do_check_eq(Svc.Prefs.get("log.appender.file.logOnSuccess"), false);
--- a/services/sync/tests/unit/test_service_startup.js
+++ b/services/sync/tests/unit/test_service_startup.js
@@ -1,12 +1,12 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
-Cu.import("resource://services-sync/ext/Observers.js");
+Cu.import("resource://services-common/observers.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/engines.js");
 
 function run_test() {
   _("When imported, Service.onStartup is called");
 
--- a/services/sync/tests/unit/test_service_sync_remoteSetup.js
+++ b/services/sync/tests/unit/test_service_sync_remoteSetup.js
@@ -1,17 +1,17 @@
 /* Any copyright is dedicated to the Public Domain.
  * http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/main.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/constants.js");
 Cu.import("resource://services-sync/keys.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 function run_test() {
   let logger = Log4Moz.repository.rootLogger;
   Log4Moz.repository.rootLogger.addAppender(new Log4Moz.DumpAppender());
 
   let guidSvc = new FakeGUIDService();
   let clients = new ServerCollection();
   let meta_global = new ServerWBO('global');
--- a/services/sync/tests/unit/test_service_verifyLogin.js
+++ b/services/sync/tests/unit/test_service_verifyLogin.js
@@ -1,10 +1,10 @@
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 Cu.import("resource://services-sync/service.js");
 Cu.import("resource://services-sync/status.js");
 Cu.import("resource://services-sync/util.js");
 
 function login_handling(handler) {
   return function (request, response) {
     if (basic_auth_matches(request, "johndoe", "ilovejane")) {
       handler(request, response);
--- a/services/sync/tests/unit/test_syncstoragerequest.js
+++ b/services/sync/tests/unit/test_syncstoragerequest.js
@@ -1,16 +1,16 @@
 /* Any copyright is dedicated to the Public Domain.
    http://creativecommons.org/publicdomain/zero/1.0/ */
 
 Cu.import("resource://services-sync/rest.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://services-sync/identity.js");
 Cu.import("resource://services-sync/constants.js");
-Cu.import("resource://services-sync/log4moz.js");
+Cu.import("resource://services-common/log4moz.js");
 
 const STORAGE_REQUEST_RESOURCE_URL = TEST_SERVER_URL + "resource";
 
 function run_test() {
   Log4Moz.repository.getLogger("Sync.RESTRequest").level = Log4Moz.Level.Trace;
   initTestLogging();
 
   run_next_test();
--- a/services/sync/tests/unit/test_utils_lazyStrings.js
+++ b/services/sync/tests/unit/test_utils_lazyStrings.js
@@ -1,10 +1,13 @@
+/* Any copyright is dedicated to the Public Domain.
+ * http://creativecommons.org/publicdomain/zero/1.0/ */
+
+Cu.import("resource://services-common/stringbundle.js");
 Cu.import("resource://services-sync/util.js");
-Cu.import("resource://services-sync/ext/StringBundle.js");
 
 function run_test() {
     let fn = Utils.lazyStrings("sync");
     do_check_eq(typeof fn, "function");
     let bundle = fn();
     do_check_true(bundle instanceof StringBundle);
     let url = bundle.url;
     do_check_eq(url, "chrome://weave/locale/services/sync.properties");
--- a/services/sync/tests/unit/xpcshell.ini
+++ b/services/sync/tests/unit/xpcshell.ini
@@ -1,54 +1,41 @@
 [DEFAULT]
-head = head_appinfo.js head_helpers.js head_http_server.js
+head = head_appinfo.js ../../../common/tests/unit/head_helpers.js head_helpers.js head_http_server.js
 tail =
 
-[test_load_modules.js]
-
 # The manifest is roughly ordered from low-level to high-level. When making
 # systemic sweeping changes, this makes it easier to identify errors closer to
 # the source.
 
 # Ensure we can import everything.
 [test_load_modules.js]
 
 # util contains a bunch of functionality used throughout.
-[test_utils_atob.js]
 [test_utils_catch.js]
 [test_utils_deepCopy.js]
 [test_utils_deepEquals.js]
 [test_utils_deferGetSet.js]
 [test_utils_deriveKey.js]
 [test_utils_encodeBase32.js]
 [test_utils_getErrorString.js]
 [test_utils_getIcon.js]
 [test_utils_hkdfExpand.js]
 [test_utils_httpmac.js]
 [test_utils_json.js]
 [test_utils_lazyStrings.js]
 [test_utils_lock.js]
 [test_utils_makeGUID.js]
-[test_utils_makeURI.js]
-[test_utils_namedTimer.js]
 [test_utils_notify.js]
 [test_utils_passphrase.js]
 [test_utils_pbkdf2.js]
 [test_utils_sha1.js]
-[test_utils_stackTrace.js]
-[test_utils_utf8.js]
 
 # We have a number of other libraries that are pretty much standalone.
-[test_Observers.js]
-[test_Preferences.js]
-[test_async_chain.js]
-[test_async_querySpinningly.js]
 [test_httpd_sync_server.js]
-[test_log4moz.js]
-[test_restrequest.js]
 [test_jpakeclient.js]
 # Bug 618233: this test produces random failures on Windows 7.
 # Bug 676978: test hangs on Android (see also testing/xpcshell/xpcshell.ini)
 skip-if = os == "win" || os == "android"
 
 # HTTP layers.
 [test_resource.js]
 [test_resource_async.js]
--- a/services/sync/tps/extensions/tps/modules/addons.jsm
+++ b/services/sync/tps/extensions/tps/modules/addons.jsm
@@ -37,17 +37,17 @@
 
 let EXPORTED_SYMBOLS = ["Addon", "STATE_ENABLED", "STATE_DISABLED"];
 
 const {classes: Cc, interfaces: Ci, utils: Cu} = Components;
 
 Cu.import("resource://gre/modules/AddonManager.jsm");
 Cu.import("resource://gre/modules/AddonRepository.jsm");
 Cu.import("resource://gre/modules/Services.jsm");
-Cu.import("resource://services-sync/async.js");
+Cu.import("resource://services-common/async.js");
 Cu.import("resource://services-sync/engines.js");
 Cu.import("resource://services-sync/util.js");
 Cu.import("resource://tps/logger.jsm");
 
 const ADDONSGETURL = 'http://127.0.0.1:4567/';
 const STATE_ENABLED = 1;
 const STATE_DISABLED = 2;
 
--- a/services/sync/tps/extensions/tps/modules/bookmarks.jsm
+++ b/services/sync/tps/extensions/tps/modules/bookmarks.jsm
@@ -45,17 +45,17 @@ var EXPORTED_SYMBOLS = ["PlacesItem", "B
 
 const CC = Components.classes;
 const CI = Components.interfaces;
 const CU = Components.utils;
 
 CU.import("resource://tps/logger.jsm");
 CU.import("resource://gre/modules/Services.jsm");
 CU.import("resource://gre/modules/PlacesUtils.jsm");
-CU.import("resource://services-sync/async.js");
+CU.import("resource://services-common/async.js");
 
 var DumpBookmarks = function TPS_Bookmarks__DumpBookmarks() {
   let writer = {
     value: "",
     write: function PlacesItem__dump__write(aStr, aLen) {
       this.value += aStr;
     }
   };
--- a/services/sync/tps/extensions/tps/modules/history.jsm
+++ b/services/sync/tps/extensions/tps/modules/history.jsm
@@ -44,17 +44,17 @@ var EXPORTED_SYMBOLS = ["HistoryEntry", 
 
 const CC = Components.classes;
 const CI = Components.interfaces;
 const CU = Components.utils;
 
 CU.import("resource://gre/modules/Services.jsm");
 CU.import("resource://gre/modules/PlacesUtils.jsm");
 CU.import("resource://tps/logger.jsm");
-CU.import("resource://services-sync/async.js");
+CU.import("resource://services-common/async.js");
 
 var DumpHistory = function TPS_History__DumpHistory() {
   let writer = {
     value: "",
     write: function PlacesItem__dump__write(aStr, aLen) {
       this.value += aStr;
     }
   };
--- a/services/sync/tps/extensions/tps/modules/tps.jsm
+++ b/services/sync/tps/extensions/tps/modules/tps.jsm
@@ -42,17 +42,17 @@
 
 let EXPORTED_SYMBOLS = ["TPS"];
 
 const {classes: CC, interfaces: CI, utils: CU} = Components;
 
 CU.import("resource://services-sync/service.js");
 CU.import("resource://services-sync/constants.js");
 CU.import("resource://services-sync/engines.js");
-CU.import("resource://services-sync/async.js");
+CU.import("resource://services-common/async.js");
 CU.import("resource://services-sync/util.js");
 CU.import("resource://gre/modules/XPCOMUtils.jsm");
 CU.import("resource://gre/modules/Services.jsm");
 CU.import("resource://tps/addons.jsm");
 CU.import("resource://tps/bookmarks.jsm");
 CU.import("resource://tps/logger.jsm");
 CU.import("resource://tps/passwords.jsm");
 CU.import("resource://tps/history.jsm");
--- a/testing/tps/setup.py
+++ b/testing/tps/setup.py
@@ -35,17 +35,17 @@
 #
 # ***** END LICENSE BLOCK *****
 
 import sys
 from setuptools import setup, find_packages
 
 version = '0.2.40'
 
-deps = ['pulsebuildmonitor >= 0.2', 'MozillaPulse == .4', 
+deps = ['pulsebuildmonitor >= 0.62', 'MozillaPulse == 0.61', 
         'mozinfo == 0.3.1', 'mozprofile == 0.1t',
         'mozprocess == 0.1a', 'mozrunner == 3.0a', 'mozregression == 0.3',
         'mozautolog >= 0.2.1']
 
 # we only support python 2.6+ right now
 assert sys.version_info[0] == 2
 assert sys.version_info[1] >= 6
 
--- a/testing/tps/tps/pulse.py
+++ b/testing/tps/tps/pulse.py
@@ -46,55 +46,52 @@ from tps.thread import TPSTestThread
 class TPSPulseMonitor(PulseBuildMonitor):
   """Listens to pulse messages, and initiates a TPS test run when
      a relevant 'build complete' message is received.
   """
 
   def __init__(self, extensionDir, platform='linux', config=None,
                autolog=False, emailresults=False, testfile=None,
                logfile=None, rlock=None, **kwargs):
-    self.buildtype = 'opt'
+    self.buildtype = ['opt']
     self.autolog = autolog
     self.emailresults = emailresults
     self.testfile = testfile
     self.logfile = logfile
     self.rlock = rlock
     self.extensionDir = extensionDir
     self.config = config
-    self.tree = self.config.get('tree', ['services-central', 'places'])
-    self.platform = self.config.get('platform', 'linux')
+    self.tree = self.config.get('tree', ['services-central'])
+    self.platform = [self.config.get('platform', 'linux')]
     self.label=('crossweave@mozilla.com|tps_build_monitor_' +
                 socket.gethostname())
 
     self.logger = logging.getLogger('tps_pulse')
     self.logger.setLevel(logging.DEBUG)
     handler = logging.FileHandler('tps_pulse.log')
     self.logger.addHandler(handler)
 
     PulseBuildMonitor.__init__(self,
-                               tree=self.tree,
+                               trees=self.tree,
                                label=self.label,
-                               mobile=False,
                                logger=self.logger,
+                               platforms=self.platform,
+                               buildtypes=self.buildtype,
+                               builds=True,
                                **kwargs)
 
   def onPulseMessage(self, data):
     key = data['_meta']['routing_key']
     #print key
 
   def onBuildComplete(self, builddata):
     print "================================================================="
     print json.dumps(builddata)
     print "================================================================="
-    try:
-      if not (builddata['platform'] == self.platform and
-              builddata['buildtype'] == self.buildtype):
-        return
-    except KeyError:
-      return
+
     thread = TPSTestThread(self.extensionDir,
                            builddata=builddata,
                            emailresults=self.emailresults,
                            autolog=self.autolog,
                            testfile=self.testfile,
                            logfile=self.logfile,
                            rlock=self.rlock,
                            config=self.config)
--- a/testing/xpcshell/xpcshell.ini
+++ b/testing/xpcshell/xpcshell.ini
@@ -62,16 +62,17 @@ skip-if = os == "android"
 [include:extensions/cookie/test/unit/xpcshell.ini]
 [include:storage/test/unit/xpcshell.ini]
 [include:rdf/tests/unit/xpcshell.ini]
 [include:gfx/tests/unit/xpcshell.ini]
 [include:widget/tests/unit/xpcshell.ini]
 [include:content/base/test/unit/xpcshell.ini]
 [include:content/test/unit/xpcshell.ini]
 [include:toolkit/components/url-classifier/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]
 [include:services/sync/tests/unit/xpcshell.ini]
 # Bug 676978: tests hang on Android 
 skip-if = os == "android"
 [include:browser/components/dirprovider/tests/unit/xpcshell.ini]
 [include:browser/components/feeds/test/unit/xpcshell.ini]
 [include:browser/components/migration/tests/unit/xpcshell.ini]