Merge autoland to m-c, a=merge
authorPhil Ringnalda <philringnalda@gmail.com>
Sat, 26 Nov 2016 17:53:09 -0800
changeset 324344 3d81e7906de637040bb850628487fd8aa921569c
parent 324334 26773c7afa55b6f8ad725d5a00850b379eb0a10f (current diff)
parent 324343 d207581536c0ddb8189c643324ffccf491dbbaf0 (diff)
child 324360 2a0abcff5cfce087c12f3e4820b5e8b773cffaca
push id24
push usermaklebus@msu.edu
push dateTue, 20 Dec 2016 03:11:33 +0000
reviewersmerge
milestone53.0a1
Merge autoland to m-c, a=merge
testing/marionette/harness/marionette/tests/unit/test_httpd.py
--- a/browser/base/content/test/general/browser_web_channel.js
+++ b/browser/base/content/test/general/browser_web_channel.js
@@ -32,16 +32,36 @@ var gTests = [
           resolve();
         });
 
         tab = gBrowser.addTab(HTTP_PATH + HTTP_ENDPOINT + "?generic");
       });
     }
   },
   {
+    desc: "WebChannel generic message in a private window.",
+    run: function* () {
+      let promiseTestDone = new Promise(function(resolve, reject) {
+        let channel = new WebChannel("generic", Services.io.newURI(HTTP_PATH, null, null));
+        channel.listen(function(id, message, target) {
+          is(id, "generic");
+          is(message.something.nested, "hello");
+          channel.stopListening();
+          resolve();
+        });
+      });
+
+      const url = HTTP_PATH + HTTP_ENDPOINT + "?generic";
+      let privateWindow = yield BrowserTestUtils.openNewBrowserWindow({private: true});
+      yield BrowserTestUtils.openNewForegroundTab(privateWindow.gBrowser, url);
+      yield promiseTestDone;
+      yield BrowserTestUtils.closeWindow(privateWindow);
+    }
+  },
+  {
     desc: "WebChannel two way communication",
     run: function* () {
       return new Promise(function(resolve, reject) {
         let tab;
         let channel = new WebChannel("twoway", Services.io.newURI(HTTP_PATH, null, null));
 
         channel.listen(function(id, message, sender) {
           is(id, "twoway", "bad id");
new file mode 100644
--- /dev/null
+++ b/testing/marionette/cert.js
@@ -0,0 +1,140 @@
+/* This Source Code Form is subject to the terms of the Mozilla Public
+ * License, v. 2.0. If a copy of the MPL was not distributed with this file,
+ * You can obtain one at http://mozilla.org/MPL/2.0/. */
+
+"use strict";
+
+const {classes: Cc, interfaces: Ci, utils: Cu, results: Cr} = Components;
+
+Cu.import("resource://gre/modules/Preferences.jsm");
+Cu.import("resource://gre/modules/XPCOMUtils.jsm");
+
+this.EXPORTED_SYMBOLS = ["cert"];
+
+const registrar =
+    Components.manager.QueryInterface(Ci.nsIComponentRegistrar);
+const sss = Cc["@mozilla.org/ssservice;1"]
+    .getService(Ci.nsISiteSecurityService);
+
+const CONTRACT_ID = "@mozilla.org/security/certoverride;1";
+const CERT_PINNING_ENFORCEMENT_PREF =
+    "security.cert_pinning.enforcement_level";
+const HSTS_PRELOAD_LIST_PREF =
+    "network.stricttransportsecurity.preloadlist";
+
+/** TLS certificate service override management for Marionette. */
+this.cert = {
+  Error: {
+    Untrusted: 1,
+    Mismatch: 2,
+    Time: 4,
+  },
+
+  currentOverride: null,
+};
+
+/**
+ * Installs a TLS certificate service override.
+ *
+ * The provided |service| must implement the |register| and |unregister|
+ * functions that causes a new |nsICertOverrideService| interface
+ * implementation to be registered with the |nsIComponentRegistrar|.
+ *
+ * After |service| is registered and made the |cert.currentOverride|,
+ * |nsICertOverrideService| is reinitialised to cause all Gecko components
+ * to pick up the new service.
+ *
+ * If an override is already installed, i.e. when |cert.currentOverride|
+ * is not null, this functions acts as a NOOP.
+ *
+ * @param {cert.Override} service
+ *     Service generator that registers and unregisters the XPCOM service.
+ *
+ * @throws {Components.Exception}
+ *     If unable to register or initialise |service|.
+ */
+cert.installOverride = function(service) {
+  if (this.currentOverride) {
+    return;
+  }
+
+  service.register();
+  cert.currentOverride = service;
+};
+
+/**
+ * Uninstall a TLS certificate service override.
+ *
+ * After the service has been unregistered, |cert.currentOverride|
+ * is reset to null.
+ *
+ * If there no current override installed, i.e. if |cert.currentOverride|
+ * is null, this function acts as a NOOP.
+ */
+cert.uninstallOverride = function() {
+  if (!cert.currentOverride) {
+    return;
+  }
+  cert.currentOverride.unregister();
+  this.currentOverride = null;
+};
+
+/**
+ * Certificate override service that acts in an all-inclusive manner
+ * on TLS certificates.
+ *
+ * When an invalid certificate is encountered, it is overriden
+ * with the |matching| bit level, which is typically a combination of
+ * |cert.Error.Untrusted|, |cert.Error.Mismatch|, and |cert.Error.Time|.
+ *
+ * @type cert.Override
+ *
+ * @throws {Components.Exception}
+ *     If there are any problems registering the service.
+ */
+cert.InsecureSweepingOverride = function() {
+  const CID = Components.ID("{4b67cce0-a51c-11e6-9598-0800200c9a66}");
+  const DESC = "All-encompassing cert service that matches on a bitflag";
+
+  // This needs to be an old-style class with a function constructor
+  // and prototype assignment because... XPCOM.  Any attempt at
+  // modernisation will be met with cryptic error messages which will
+  // make your life miserable.
+  let service = function() {};
+  service.prototype = {
+    hasMatchingOverride: function(
+        aHostName, aPort, aCert, aOverrideBits, aIsTemporary) {
+      aIsTemporary.value = false;
+      aOverrideBits.value =
+          cert.Error.Untrusted | cert.Error.Mismatch | cert.Error.Time;
+
+      return true;
+    },
+
+    QueryInterface: XPCOMUtils.generateQI([Ci.nsICertOverrideService]),
+  };
+  let factory = XPCOMUtils.generateSingletonFactory(service);
+
+  return {
+    register: function() {
+      // make it possible to register certificate overrides for domains
+      // that use HSTS or HPKP
+      Preferences.set(HSTS_PRELOAD_LIST_PREF, false);
+      Preferences.set(CERT_PINNING_ENFORCEMENT_PREF, 0);
+
+      registrar.registerFactory(CID, DESC, CONTRACT_ID, factory);
+    },
+
+    unregister: function() {
+      registrar.unregisterFactory(CID, factory);
+
+      Preferences.reset(HSTS_PRELOAD_LIST_PREF);
+      Preferences.reset(CERT_PINNING_ENFORCEMENT_PREF);
+
+      // clear collected HSTS and HPKP state
+      // through the site security service
+      sss.clearAll();
+      sss.clearPreloads();
+    },
+  };
+};
--- a/testing/marionette/client/marionette_driver/errors.py
+++ b/testing/marionette/client/marionette_driver/errors.py
@@ -50,16 +50,20 @@ class MarionetteException(Exception):
 
         return msg
 
 
 class ElementNotSelectableException(MarionetteException):
     status = "element not selectable"
 
 
+class InsecureCertificateException(MarionetteException):
+    status = "insecure certificate"
+
+
 class InvalidArgumentException(MarionetteException):
     status = "invalid argument"
 
 
 class InvalidSessionIdException(MarionetteException):
     status = "invalid session id"
 
 
--- a/testing/marionette/driver.js
+++ b/testing/marionette/driver.js
@@ -18,16 +18,17 @@ XPCOMUtils.defineLazyServiceGetter(
     this, "cookieManager", "@mozilla.org/cookiemanager;1", "nsICookieManager2");
 
 Cu.import("chrome://marionette/content/accessibility.js");
 Cu.import("chrome://marionette/content/action.js");
 Cu.import("chrome://marionette/content/addon.js");
 Cu.import("chrome://marionette/content/assert.js");
 Cu.import("chrome://marionette/content/atom.js");
 Cu.import("chrome://marionette/content/browser.js");
+Cu.import("chrome://marionette/content/cert.js");
 Cu.import("chrome://marionette/content/element.js");
 Cu.import("chrome://marionette/content/error.js");
 Cu.import("chrome://marionette/content/evaluate.js");
 Cu.import("chrome://marionette/content/event.js");
 Cu.import("chrome://marionette/content/interaction.js");
 Cu.import("chrome://marionette/content/l10n.js");
 Cu.import("chrome://marionette/content/legacyaction.js");
 Cu.import("chrome://marionette/content/logging.js");
@@ -103,55 +104,67 @@ this.GeckoDriver = function(appName, ser
   this.appName = appName;
   this._server = server;
 
   this.sessionId = null;
   this.wins = new browser.Windows();
   this.browsers = {};
   // points to current browser
   this.curBrowser = null;
-  this.context = Context.CONTENT;
-
-  this.scriptTimeout = 30000;  // 30 seconds
-  this.searchTimeout = 0;
-  this.pageTimeout = 300000;  // five minutes
-
-  this.timer = null;
-  this.inactivityTimer = null;
-  this.marionetteLog = new logging.ContentLogger();
   // topmost chrome frame
   this.mainFrame = null;
   // chrome iframe that currently has focus
   this.curFrame = null;
   this.mainContentFrameId = null;
-  this.importedScripts = new evaluate.ScriptStorageService([Context.CHROME, Context.CONTENT]);
+  this.mozBrowserClose = null;
   this.currentFrameElement = null;
-  this.testName = null;
-  this.mozBrowserClose = null;
-  this.sandboxes = new Sandboxes(() => this.getCurrentWindow());
   // frame ID of the current remote frame, used for mozbrowserclose events
   this.oopFrameId = null;
   this.observing = null;
   this._browserIds = new WeakMap();
 
+  // user-defined timeouts
+  this.scriptTimeout = 30000;  // 30 seconds
+  this.searchTimeout = null;
+  this.pageTimeout = 300000;  // five minutes
+
+  // Unsigned or invalid TLS certificates will be ignored if secureTLS
+  // is set to false.
+  this.secureTLS = true;
+
+  // The curent context decides if commands should affect chrome- or
+  // content space.
+  this.context = Context.CONTENT;
+
+  this.importedScripts = new evaluate.ScriptStorageService(
+      [Context.CHROME, Context.CONTENT]);
+  this.sandboxes = new Sandboxes(() => this.getCurrentWindow());
+  this.actions = new action.Chain();
+
+  this.timer = null;
+  this.inactivityTimer = null;
+
+  this.marionetteLog = new logging.ContentLogger();
+  this.testName = null;
+
   this.sessionCapabilities = {
     // mandated capabilities
     "browserName": Services.appinfo.name.toLowerCase(),
     "browserVersion": Services.appinfo.version,
     "platformName": Services.sysinfo.getProperty("name").toLowerCase(),
     "platformVersion": Services.sysinfo.getProperty("version"),
-    "specificationLevel": 0,
+    "acceptInsecureCerts": !this.secureTLS,
 
     // supported features
     "raisesAccessibilityExceptions": false,
     "rotatable": this.appName == "B2G",
-    "acceptSslCerts": false,
     "proxy": {},
 
     // proprietary extensions
+    "specificationLevel": 0,
     "processId" : Services.appinfo.processID,
   };
 
   this.mm = globalMessageManager;
   this.listener = proxy.toListener(() => this.mm, this.sendAsync.bind(this));
 
   // always keep weak reference to current dialogue
   this.dialog = null;
@@ -479,26 +492,34 @@ GeckoDriver.prototype.newSession = funct
     throw new SessionNotCreatedError("Maximum number of active sessions.")
   }
   this.sessionId = cmd.parameters.sessionId ||
       cmd.parameters.session_id ||
       element.generateUUID();
 
   this.newSessionCommandId = cmd.id;
   this.setSessionCapabilities(cmd.parameters.capabilities);
+
+  this.scriptTimeout = 10000;
+
+  this.secureTLS = !this.sessionCapabilities.acceptInsecureCerts;
+  if (!this.secureTLS) {
+    logger.warn("TLS certificate errors will be ignored for this session");
+    let acceptAllCerts = new cert.InsecureSweepingOverride();
+    cert.installOverride(acceptAllCerts);
+  }
+
   // If we are testing accessibility with marionette, start a11y service in
   // chrome first. This will ensure that we do not have any content-only
   // services hanging around.
   if (this.sessionCapabilities.raisesAccessibilityExceptions &&
       accessibility.service) {
     logger.info("Preemptively starting accessibility service in Chrome");
   }
 
-  this.scriptTimeout = 10000;
-
   let registerBrowsers = this.registerPromise();
   let browserListening = this.listeningPromise();
 
   let waitForWindow = function() {
     let win = this.getCurrentWindow();
     if (!win) {
       // if the window isn't even created, just poll wait for it
       let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
@@ -636,16 +657,17 @@ GeckoDriver.prototype.setSessionCapabili
     throw new SessionNotCreatedError(
         `Not all requiredCapabilities could be met: ${JSON.stringify(errors)}`);
   };
 
   // clone, overwrite, and set
   let caps = copy(this.sessionCapabilities);
   caps = copy(newCaps, caps);
   logger.config("Changing capabilities: " + JSON.stringify(caps));
+
   this.sessionCapabilities = caps;
 };
 
 GeckoDriver.prototype.setUpProxy = function(proxy) {
   logger.config("User-provided proxy settings: " + JSON.stringify(proxy));
 
   assert.object(proxy);
   if (!proxy.hasOwnProperty("proxyType")) {
@@ -1388,27 +1410,32 @@ GeckoDriver.prototype.switchToParentFram
  *     A web element reference to the element to switch to.
  * @param {(string|number)} id
  *     If element is not defined, then this holds either the id, name,
  *     or index of the frame to switch to.
  */
 GeckoDriver.prototype.switchToFrame = function*(cmd, resp) {
   let {id, element, focus} = cmd.parameters;
 
-  let checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+  const otherErrorsExpr = /about:.+(error)|(blocked)\?/;
+  const checkTimer = Cc["@mozilla.org/timer;1"].createInstance(Ci.nsITimer);
+
   let curWindow = this.getCurrentWindow();
 
   let checkLoad = function() {
-    let errorRegex = /about:.+(error)|(blocked)\?/;
-    let curWindow = this.getCurrentWindow();
-    if (curWindow.document.readyState == "complete") {
+    let win = this.getCurrentWindow();
+    if (win.document.readyState == "complete") {
       return;
-    } else if (curWindow.document.readyState == "interactive" &&
-        errorRegex.exec(curWindow.document.baseURI)) {
-      throw new UnknownError("Error loading page");
+    } else if (win.document.readyState == "interactive") {
+      let baseURI = win.document.baseURI;
+      if (baseURI.startsWith("about:certerror")) {
+        throw new InsecureCertificateError();
+      } else if (otherErrorsExpr.exec(win.document.baseURI)) {
+        throw new UnknownError("Error loading page");
+      }
     }
 
     checkTimer.initWithCallback(checkLoad.bind(this), 100, Ci.nsITimer.TYPE_ONE_SHOT);
   };
 
   if (this.context == Context.CHROME) {
     let foundFrame = null;
 
@@ -2282,17 +2309,19 @@ GeckoDriver.prototype.sessionTearDown = 
   this.sessionId = null;
 
   if (this.observing !== null) {
     for (let topic in this.observing) {
       Services.obs.removeObserver(this.observing[topic], topic);
     }
     this.observing = null;
   }
+
   this.sandboxes.clear();
+  cert.uninstallOverride();
 };
 
 /**
  * Processes the "deleteSession" request from the client by tearing down
  * the session and responding "ok".
  */
 GeckoDriver.prototype.deleteSession = function(cmd, resp) {
   this.sessionTearDown();
--- a/testing/marionette/error.js
+++ b/testing/marionette/error.js
@@ -4,16 +4,17 @@
 
 "use strict";
 
 const {interfaces: Ci, utils: Cu} = Components;
 
 const ERRORS = new Set([
   "ElementNotAccessibleError",
   "ElementNotVisibleError",
+  "InsecureCertificateError",
   "InvalidArgumentError",
   "InvalidElementStateError",
   "InvalidSelectorError",
   "InvalidSessionIdError",
   "JavaScriptError",
   "NoAlertOpenError",
   "NoSuchElementError",
   "NoSuchFrameError",
@@ -224,16 +225,23 @@ ElementNotAccessibleError.prototype = Ob
 
 this.ElementNotVisibleError = function(msg) {
   WebDriverError.call(this, msg);
   this.name = "ElementNotVisibleError";
   this.status = "element not visible";
 };
 ElementNotVisibleError.prototype = Object.create(WebDriverError.prototype);
 
+this.InsecureCertificateError = function(msg) {
+  WebDriverError.call(this, msg);
+  this.name = "InsecureCertificateError";
+  this.status = "insecure certificate";
+};
+InsecureCertificateError.prototype = Object.create(WebDriverError.prototype);
+
 this.InvalidArgumentError = function(msg) {
   WebDriverError.call(this, msg);
   this.name = "InvalidArgumentError";
   this.status = "invalid argument";
 };
 InvalidArgumentError.prototype = Object.create(WebDriverError.prototype);
 
 this.InvalidElementStateError = function(msg) {
--- a/testing/marionette/harness/MANIFEST.in
+++ b/testing/marionette/harness/MANIFEST.in
@@ -1,6 +1,8 @@
 recursive-include marionette/touch *.js
 recursive-include marionette/www *
 recursive-include marionette/chrome *
 recursive-include marionette/runner/mixins/resources *
 exclude MANIFEST.in
 include requirements.txt
+include marionette/runner/test.cert
+include marionette/runner/test.key
--- a/testing/marionette/harness/marionette/marionette_test/testcases.py
+++ b/testing/marionette/harness/marionette/marionette_test/testcases.py
@@ -69,18 +69,22 @@ class JSTest:
 
 class CommonTestCase(unittest.TestCase):
 
     __metaclass__ = MetaParameterized
     match_re = None
     failureException = AssertionError
     pydebugger = None
 
-    def __init__(self, methodName, **kwargs):
+    def __init__(self, methodName, marionette_weakref, fixtures, **kwargs):
         super(CommonTestCase, self).__init__(methodName)
+        self.methodName = methodName
+
+        self._marionette_weakref = marionette_weakref
+        self.fixtures = fixtures
 
         self.loglines = []
         self.duration = 0
         self.start_time = 0
         self.expected = kwargs.pop('expected', 'pass')
         self.logger = get_default_logger()
 
     def _enter_pm(self):
@@ -220,17 +224,17 @@ class CommonTestCase(unittest.TestCase):
         """
         if not cls.match_re:
             return False
         m = cls.match_re.match(filename)
         return m is not None
 
     @classmethod
     def add_tests_to_suite(cls, mod_name, filepath, suite, testloader, marionette,
-                           httpd, testvars):
+                           fixtures, testvars, **kwargs):
         """Add all the tests in the specified file to the specified suite."""
         raise NotImplementedError
 
     @property
     def test_name(self):
         if hasattr(self, 'jsFile'):
             return os.path.basename(self.jsFile)
         else:
@@ -247,17 +251,16 @@ class CommonTestCase(unittest.TestCase):
 
     def setUp(self):
         # Convert the marionette weakref to an object, just for the
         # duration of the test; this is deleted in tearDown() to prevent
         # a persistent circular reference which in turn would prevent
         # proper garbage collection.
         self.start_time = time.time()
         self.marionette = self._marionette_weakref()
-        self.httpd = self._httpd_weakref()
         if self.marionette.session is None:
             self.marionette.start_session()
         self.marionette.timeout.reset()
 
         super(CommonTestCase, self).setUp()
 
     def cleanTest(self):
         self._deleteSession()
@@ -416,31 +419,27 @@ if (!testUtils.hasOwnProperty("specialPo
                 raise
         self.marionette.test_name = original_test_name
 
 
 class MarionetteTestCase(CommonTestCase):
 
     match_re = re.compile(r"test_(.*)\.py$")
 
-    def __init__(self, marionette_weakref, httpd_weakref, methodName='runTest',
+    def __init__(self, marionette_weakref, fixtures, methodName='runTest',
                  filepath='', **kwargs):
-        self._marionette_weakref = marionette_weakref
-        self._httpd_weakref = httpd_weakref
-        self.methodName = methodName
         self.filepath = filepath
         self.testvars = kwargs.pop('testvars', None)
 
-        self.marionette = None
-
-        super(MarionetteTestCase, self).__init__(methodName, **kwargs)
+        super(MarionetteTestCase, self).__init__(
+            methodName, marionette_weakref=marionette_weakref, fixtures=fixtures, **kwargs)
 
     @classmethod
     def add_tests_to_suite(cls, mod_name, filepath, suite, testloader, marionette,
-                           httpd, testvars, **kwargs):
+                           fixtures, testvars, **kwargs):
         # since we use imp.load_source to load test modules, if a module
         # is loaded with the same name as another one the module would just be
         # reloaded.
         #
         # We may end up by finding too many test in a module then since
         # reload() only update the module dict (so old keys are still there!)
         # see https://docs.python.org/2/library/functions.html#reload
         #
@@ -454,17 +453,17 @@ class MarionetteTestCase(CommonTestCase)
 
         for name in dir(test_mod):
             obj = getattr(test_mod, name)
             if (isinstance(obj, (type, types.ClassType)) and
                     issubclass(obj, unittest.TestCase)):
                 testnames = testloader.getTestCaseNames(obj)
                 for testname in testnames:
                     suite.addTest(obj(weakref.ref(marionette),
-                                      weakref.ref(httpd),
+                                      fixtures,
                                       methodName=testname,
                                       filepath=filepath,
                                       testvars=testvars,
                                       **kwargs))
 
     def setUp(self):
         super(MarionetteTestCase, self).setUp()
         self.marionette.test_name = self.test_name
--- a/testing/marionette/harness/marionette/runner/base.py
+++ b/testing/marionette/harness/marionette/runner/base.py
@@ -6,34 +6,30 @@ import json
 import os
 import random
 import re
 import socket
 import sys
 import time
 import traceback
 import unittest
-import warnings
-
 from argparse import ArgumentParser
 from copy import deepcopy
 
 import mozinfo
-import moznetwork
 import mozprofile
+from marionette_driver.marionette import Marionette
+
 import mozversion
-
+import serve
 from manifestparser import TestManifest
 from manifestparser.filters import tags
-from marionette_driver.marionette import Marionette
-from moztest.adapters.unit import StructuredTestRunner, StructuredTestResult
-from moztest.results import TestResultCollection, TestResult, relevant_line
-
-import httpd
-
+from moztest.adapters.unit import StructuredTestResult, StructuredTestRunner
+from moztest.results import TestResult, TestResultCollection, relevant_line
+from serve import iter_proc, iter_url
 
 here = os.path.abspath(os.path.dirname(__file__))
 
 
 def update_mozinfo(path=None):
     """Walk up directories to find mozinfo.json and update the info."""
     path = path or here
     dirs = set()
@@ -484,16 +480,21 @@ class RemoteMarionetteArguments(object):
         [['--package'],
          {'help': 'Name of Android package, e.g. org.mozilla.fennec',
           'dest': 'package_name',
           }],
 
     ]
 
 
+class Fixtures(object):
+    def where_is(self, uri, on="http"):
+        return serve.where_is(uri, on)
+
+
 class BaseMarionetteTestRunner(object):
 
     textrunnerclass = MarionetteTextTestRunner
     driverclass = Marionette
 
     def __init__(self, address=None,
                  app=None, app_args=None, binary=None, profile=None,
                  logger=None, logdir=None,
@@ -501,34 +502,34 @@ class BaseMarionetteTestRunner(object):
                  symbols_path=None,
                  shuffle=False, shuffle_seed=random.randint(0, sys.maxint), this_chunk=1,
                  total_chunks=1,
                  server_root=None, gecko_log=None, result_callbacks=None,
                  prefs=None, test_tags=None,
                  socket_timeout=BaseMarionetteArguments.socket_timeout_default,
                  startup_timeout=None, addons=None, workspace=None,
                  verbose=0, e10s=True, emulator=False, **kwargs):
-
         self._appinfo = None
         self._appName = None
         self._capabilities = None
         self._filename_pattern = None
         self._version_info = {}
 
+        self.fixture_servers = {}
+        self.fixtures = Fixtures()
         self.extra_kwargs = kwargs
         self.test_kwargs = deepcopy(kwargs)
         self.address = address
         self.app = app
         self.app_args = app_args or []
         self.bin = binary
         self.emulator = emulator
         self.profile = profile
         self.addons = addons
         self.logger = logger
-        self.httpd = None
         self.marionette = None
         self.logdir = logdir
         self.repeat = repeat
         self.symbols_path = symbols_path
         self.socket_timeout = socket_timeout
         self.shuffle = shuffle
         self.shuffle_seed = shuffle_seed
         self.server_root = server_root
@@ -667,17 +668,16 @@ class BaseMarionetteTestRunner(object):
 
     @property
     def bin(self):
         return self._bin
 
     @bin.setter
     def bin(self, path):
         """Set binary and reset parts of runner accordingly.
-
         Intended use: to change binary between calls to run_tests
         """
         self._bin = path
         self.tests = []
         self.cleanup()
 
     @property
     def version_info(self):
@@ -770,40 +770,16 @@ class BaseMarionetteTestRunner(object):
             traceback.print_exc()
         return crash
 
     def _initialize_test_run(self, tests):
         assert len(tests) > 0
         assert len(self.test_handlers) > 0
         self.reset_test_stats()
 
-    def _start_marionette(self):
-        need_external_ip = True
-        if not self.marionette:
-            self.marionette = self.driverclass(**self._build_kwargs())
-            # if we're working against a desktop version, we usually don't need
-            # an external ip
-            if self.appName != 'fennec':
-                need_external_ip = False
-        self.logger.info('Initial Profile Destination is '
-                         '"{}"'.format(self.marionette.profile_path))
-        return need_external_ip
-
-    def _set_baseurl(self, need_external_ip):
-        # Gaia sets server_root and that means we shouldn't spin up our own httpd
-        if not self.httpd:
-            if self.server_root is None or os.path.isdir(self.server_root):
-                self.logger.info("starting httpd")
-                self.start_httpd(need_external_ip)
-                self.marionette.baseurl = self.httpd.get_url()
-                self.logger.info("running httpd on {}".format(self.marionette.baseurl))
-            else:
-                self.marionette.baseurl = self.server_root
-                self.logger.info("using remote content from {}".format(self.marionette.baseurl))
-
     def _add_tests(self, tests):
         for test in tests:
             self.add_test(test)
 
         invalid_tests = [t['filepath'] for t in self.tests
                          if not self._is_filename_valid(t['filepath'])]
         if invalid_tests:
             raise Exception("Test file names must be of the form "
@@ -822,29 +798,43 @@ class BaseMarionetteTestRunner(object):
                                  'SKIP',
                                  message=test['disabled'])
             self.todo += 1
 
     def run_tests(self, tests):
         start_time = time.time()
         self._initialize_test_run(tests)
 
-        need_external_ip = self._start_marionette()
-        self._set_baseurl(need_external_ip)
+        if self.marionette is None:
+            self.marionette = self.driverclass(**self._build_kwargs())
+            self.logger.info("Profile path is %s" % self.marionette.profile_path)
+
+        if len(self.fixture_servers) == 0 or \
+                any(not server.is_alive for _, server in self.fixture_servers):
+            self.logger.info("Starting fixture servers")
+            self.fixture_servers = self.start_fixture_servers()
+            for url in iter_url(self.fixture_servers):
+                self.logger.info("Fixture server listening on %s" % url)
+
+            # backwards compatibility
+            self.marionette.baseurl = serve.where_is("/")
 
         self._add_tests(tests)
 
         device_info = None
         if self.marionette.instance and self.emulator:
             try:
                 device_info = self.marionette.instance.runner.device.dm.getInfo()
             except Exception:
-                self.logger.warning('Could not get device info.')
+                self.logger.warning('Could not get device info', exc_info=True)
 
-        self.logger.info("running with e10s: {}".format(self.e10s))
+        if self.e10s:
+            self.logger.info("e10s is enabled")
+        else:
+            self.logger.info("e10s is disabled")
 
         self.logger.suite_start(self.tests,
                                 version_info=self.version_info,
                                 device_info=device_info)
 
         self._log_skipped_tests()
 
         interrupted = None
@@ -870,19 +860,18 @@ class BaseMarionetteTestRunner(object):
         try:
             self._print_summary(tests)
             self.record_crash()
             self.elapsedtime = time.time() - start_time
 
             for run_tests in self.mixin_run_tests:
                 run_tests(tests)
             if self.shuffle:
-                self.logger.info("Using seed where seed is:{}".format(self.shuffle_seed))
+                self.logger.info("Using shuffle seed: %d" % self.shuffle_seed)
 
-            self.logger.info('mode: {}'.format('e10s' if self.e10s else 'non-e10s'))
             self.logger.suite_end()
         except:
             # raise only the exception if we were not interrupted
             if not interrupted:
                 raise
         finally:
             self.cleanup()
 
@@ -904,29 +893,19 @@ class BaseMarionetteTestRunner(object):
         else:
             self.logger.info('todo: {0} (skipped: {1})'.format(self.todo, self.skipped))
 
         if self.failed > 0:
             self.logger.info('\nFAILED TESTS\n-------')
             for failed_test in self.failures:
                 self.logger.info('{}'.format(failed_test[0]))
 
-    def start_httpd(self, need_external_ip):
-        warnings.warn("start_httpd has been deprecated in favour of create_httpd",
-                      DeprecationWarning)
-        self.httpd = self.create_httpd(need_external_ip)
-
-    def create_httpd(self, need_external_ip):
-        host = "127.0.0.1"
-        if need_external_ip:
-            host = moznetwork.get_ip()
+    def start_fixture_servers(self):
         root = self.server_root or os.path.join(os.path.dirname(here), "www")
-        rv = httpd.FixtureServer(root, host=host)
-        rv.start()
-        return rv
+        return serve.start(root)
 
     def add_test(self, test, expected='pass'):
         filepath = os.path.abspath(test)
 
         if os.path.isdir(filepath):
             for root, dirs, files in os.walk(filepath):
                 for filename in files:
                     if filename.endswith('.ini'):
@@ -976,29 +955,28 @@ class BaseMarionetteTestRunner(object):
                 file_ext = os.path.splitext(os.path.split(i['path'])[-1])[-1]
 
                 self.add_test(i["path"], i["expected"])
             return
 
         self.tests.append({'filepath': filepath, 'expected': expected})
 
     def run_test(self, filepath, expected):
-
         testloader = unittest.TestLoader()
         suite = unittest.TestSuite()
         self.test_kwargs['expected'] = expected
         mod_name = os.path.splitext(os.path.split(filepath)[-1])[0]
         for handler in self.test_handlers:
             if handler.match(os.path.basename(filepath)):
                 handler.add_tests_to_suite(mod_name,
                                            filepath,
                                            suite,
                                            testloader,
                                            self.marionette,
-                                           self.httpd,
+                                           self.fixtures,
                                            self.testvars,
                                            **self.test_kwargs)
                 break
 
         if suite.countTestCases():
             runner = self.textrunnerclass(logger=self.logger,
                                           marionette=self.marionette,
                                           capabilities=self.capabilities,
@@ -1054,21 +1032,22 @@ class BaseMarionetteTestRunner(object):
                              'total of {3})'.format(self.this_chunk, self.total_chunks,
                                                     len(chunks[self.this_chunk - 1]),
                                                     len(self.tests)))
             self.tests = chunks[self.this_chunk - 1]
 
         self.run_test_set(self.tests)
 
     def cleanup(self):
-        if hasattr(self, 'httpd') and self.httpd:
-            self.httpd.stop()
-            self.httpd = None
+        for proc in iter_proc(self.fixture_servers):
+            proc.stop()
+            proc.kill()
+        self.fixture_servers = {}
 
         if hasattr(self, 'marionette') and self.marionette:
-            if self.marionette.instance:
+            if self.marionette.instance is not None:
                 self.marionette.instance.close()
                 self.marionette.instance = None
 
             self.marionette.cleanup()
             self.marionette = None
 
     __del__ = cleanup
--- a/testing/marionette/harness/marionette/runner/httpd.py
+++ b/testing/marionette/harness/marionette/runner/httpd.py
@@ -1,82 +1,142 @@
+#!/usr/bin/env python
+
 # 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/.
 
+"""Specialisation of wptserver.server.WebTestHttpd for testing
+Marionette.
+
+"""
+
+import argparse
 import os
+import select
+import sys
 import time
+import urlparse
 
 from wptserve import server, handlers, routes as default_routes
 
 
-class FixtureServer(object):
-
-    def __init__(self, root, host="127.0.0.1", port=0):
-        if not os.path.isdir(root):
-            raise IOError("Server root is not a valid path: {}".format(root))
-        self.root = root
-        self.host = host
-        self.port = port
-        self._server = None
-
-    def start(self, block=False):
-        if self.alive:
-            return
-        routes = [("POST", "/file_upload", upload_handler),
-                  ("GET", "/slow", slow_loading_document)]
-        routes.extend(default_routes.routes)
-        self._server = server.WebTestHttpd(
-            port=self.port,
-            doc_root=self.root,
-            routes=routes,
-            host=self.host,
-        )
-        self._server.start(block=block)
-        self.port = self._server.httpd.server_port
-        self.base_url = self.get_url()
-
-    def stop(self):
-        if not self.alive:
-            return
-        self._server.stop()
-        self._server = None
-
-    @property
-    def alive(self):
-        return self._server is not None
-
-    def get_url(self, path="/"):
-        if not self.alive:
-            raise Exception("Server not started")
-        return self._server.get_url(path)
-
-    @property
-    def router(self):
-        return self._server.router
-
-    @property
-    def routes(self):
-        return self._server.router.routes
+here = os.path.abspath(os.path.dirname(__file__))
+default_doc_root = os.path.join(os.path.dirname(here), "www")
+default_ssl_cert = os.path.join(here, "test.cert")
+default_ssl_key = os.path.join(here, "test.key")
 
 
 @handlers.handler
 def upload_handler(request, response):
     return 200, [], [request.headers.get("Content-Type")] or []
 
 
 @handlers.handler
 def slow_loading_document(request, response):
     time.sleep(5)
     return """<!doctype html>
 <title>ok</title>
 <p>ok"""
 
 
+class NotAliveError(Exception):
+    """Occurs when attempting to run a function that requires the HTTPD
+    to have been started, and it has not.
+
+    """
+    pass
+
+
+class FixtureServer(object):
+
+    def __init__(self, doc_root, url="http://127.0.0.1:0", use_ssl=False,
+                 ssl_cert=None, ssl_key=None):
+        if not os.path.isdir(doc_root):
+            raise ValueError("Server root is not a directory: %s" % doc_root)
+
+        url = urlparse.urlparse(url)
+        if url.scheme is None:
+            raise ValueError("Server scheme not provided")
+
+        scheme, host, port = url.scheme, url.hostname, url.port
+        if host is None:
+            host = "127.0.0.1"
+        if port is None:
+            port = 0
+
+        routes = [("POST", "/file_upload", upload_handler),
+                  ("GET", "/slow", slow_loading_document)]
+        routes.extend(default_routes.routes)
+
+        self._httpd = server.WebTestHttpd(host=host,
+                                          port=port,
+                                          bind_hostname=True,
+                                          doc_root=doc_root,
+                                          routes=routes,
+                                          use_ssl=True if scheme == "https" else False,
+                                          certificate=ssl_cert,
+                                          key_file=ssl_key)
+
+    def start(self, block=False):
+        if self.is_alive:
+            return
+        self._httpd.start(block=block)
+
+    def wait(self):
+        if not self.is_alive:
+            return
+        try:
+            select.select([], [], [])
+        except KeyboardInterrupt:
+            self.stop()
+
+    def stop(self):
+        if not self.is_alive:
+            return
+        self._httpd.stop()
+
+    def get_url(self, path):
+        if not self.is_alive:
+            raise NotAliveError()
+        return self._httpd.get_url(path)
+
+    @property
+    def doc_root(self):
+        return self._httpd.router.doc_root
+
+    @property
+    def router(self):
+        return self._httpd.router
+
+    @property
+    def routes(self):
+        return self._httpd.router.routes
+
+    @property
+    def is_alive(self):
+        return self._httpd.started
+
+
 if __name__ == "__main__":
-    here = os.path.abspath(os.path.dirname(__file__))
-    doc_root = os.path.join(os.path.dirname(here), "www")
-    httpd = FixtureServer(doc_root, port=2829)
-    print "Started fixture server on http://{0}:{1}/".format(httpd.host, httpd.port)
-    try:
-        httpd.start(True)
-    except KeyboardInterrupt:
-        pass
+    parser = argparse.ArgumentParser(
+        description="Specialised HTTP server for testing Marionette.")
+    parser.add_argument("url", help="""
+service address including scheme, hostname, port, and prefix for document root,
+e.g. \"https://0.0.0.0:0/base/\"""")
+    parser.add_argument(
+        "-r", dest="doc_root", default=default_doc_root,
+        help="path to document root (default %(default)s)")
+    parser.add_argument(
+        "-c", dest="ssl_cert", default=default_ssl_cert,
+        help="path to SSL certificate (default %(default)s)")
+    parser.add_argument(
+        "-k", dest="ssl_key", default=default_ssl_key,
+        help="path to SSL certificate key (default %(default)s)")
+    args = parser.parse_args()
+
+    httpd = FixtureServer(args.doc_root, args.url,
+                          ssl_cert=args.ssl_cert,
+                          ssl_key=args.ssl_key)
+    httpd.start()
+    print >>sys.stderr, "%s: started fixture server on %s" % \
+        (sys.argv[0], httpd.get_url("/"))
+    httpd.wait()
new file mode 100755
--- /dev/null
+++ b/testing/marionette/harness/marionette/runner/serve.py
@@ -0,0 +1,227 @@
+#!/usr/bin/env python
+
+# 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/.
+
+"""Spawns necessary HTTP servers for testing Marionette in child
+processes.
+
+"""
+
+import argparse
+import multiprocessing
+import os
+import sys
+from collections import defaultdict
+
+import moznetwork
+
+import httpd
+
+__all__ = ["default_doc_root",
+           "iter_proc",
+           "iter_url",
+           "registered_servers",
+           "servers",
+           "start",
+           "where_is"]
+here = os.path.abspath(os.path.dirname(__file__))
+
+
+class BlockingChannel(object):
+
+    def __init__(self, channel):
+        self.chan = channel
+        self.lock = multiprocessing.Lock()
+
+    def call(self, func, args=()):
+        self.send((func, args))
+        return self.recv()
+
+    def send(self, *args):
+        try:
+            self.lock.acquire()
+            self.chan.send(args)
+        finally:
+            self.lock.release()
+
+    def recv(self):
+        try:
+            self.lock.acquire()
+            payload = self.chan.recv()
+            if isinstance(payload, tuple) and len(payload) == 1:
+                return payload[0]
+            return payload
+        except KeyboardInterrupt:
+            return ("stop", ())
+        finally:
+            self.lock.release()
+
+
+class ServerProxy(multiprocessing.Process, BlockingChannel):
+
+    def __init__(self, channel, init_func, *init_args, **init_kwargs):
+        multiprocessing.Process.__init__(self)
+        BlockingChannel.__init__(self, channel)
+        self.init_func = init_func
+        self.init_args = init_args
+        self.init_kwargs = init_kwargs
+
+    def run(self):
+        server = self.init_func(*self.init_args, **self.init_kwargs)
+        server.start(block=False)
+
+        try:
+            while True:
+                # ["func", ("arg", ...)]
+                # ["prop", ()]
+                sattr, fargs = self.recv()
+                attr = getattr(server, sattr)
+
+                # apply fargs to attr if it is a function
+                if callable(attr):
+                    rv = attr(*fargs)
+
+                # otherwise attr is a property
+                else:
+                    rv = attr
+
+                self.send(rv)
+
+                if sattr == "stop":
+                    return
+
+        except KeyboardInterrupt:
+            server.stop()
+
+
+class ServerProc(BlockingChannel):
+
+    def __init__(self, init_func):
+        self._init_func = init_func
+        self.proc = None
+
+        parent_chan, self.child_chan = multiprocessing.Pipe()
+        BlockingChannel.__init__(self, parent_chan)
+
+    def start(self, doc_root, ssl_config, **kwargs):
+        self.proc = ServerProxy(
+            self.child_chan, self._init_func, doc_root, ssl_config, **kwargs)
+        self.proc.daemon = True
+        self.proc.start()
+
+    def get_url(self, url):
+        return self.call("get_url", (url,))
+
+    @property
+    def doc_root(self):
+        return self.call("doc_root", ())
+
+    def stop(self):
+        self.call("stop")
+        if not self.is_alive:
+            return
+        self.proc.join()
+
+    def kill(self):
+        if not self.is_alive:
+            return
+        self.proc.terminate()
+        self.proc.join(0)
+
+    @property
+    def is_alive(self):
+        if self.proc is not None:
+            return self.proc.is_alive()
+        return False
+
+
+def http_server(doc_root, ssl_config, **kwargs):
+    return httpd.FixtureServer(doc_root, url="http://%s:0/" % moznetwork.get_ip())
+
+
+def https_server(doc_root, ssl_config, **kwargs):
+    return httpd.FixtureServer(doc_root,
+                               url="https://%s:0/" % moznetwork.get_ip(),
+                               ssl_key=ssl_config["key_path"],
+                               ssl_cert=ssl_config["cert_path"])
+
+
+def start_servers(doc_root, ssl_config, **kwargs):
+    servers = defaultdict()
+    for schema, builder_fn in registered_servers:
+        proc = ServerProc(builder_fn)
+        proc.start(doc_root, ssl_config, **kwargs)
+        servers[schema] = (proc.get_url("/"), proc)
+    return servers
+
+
+def start(doc_root=None, **kwargs):
+    """Start all relevant test servers.
+
+    If no `doc_root` is given the default
+    testing/marionette/harness/marionette/www directory will be used.
+
+    Additional keyword arguments can be given which will be passed on
+    to the individual ``FixtureServer``'s in httpd.py.
+
+    """
+    doc_root = doc_root or default_doc_root
+    ssl_config = {"cert_path": httpd.default_ssl_cert,
+                  "key_path": httpd.default_ssl_key}
+
+    global servers
+    servers = start_servers(doc_root, ssl_config, **kwargs)
+
+    return servers
+
+
+def where_is(uri, on="http"):
+    """Returns the full URL, including scheme, hostname, and port, for
+    a fixture resource from the server associated with the ``on`` key.
+    It will by default look for the resource in the "http" server.
+
+    """
+    return servers.get(on)[1].get_url(uri)
+
+
+def iter_proc(servers):
+    for _, (_, proc) in servers.iteritems():
+        yield proc
+
+
+def iter_url(servers):
+    for _, (url, _) in servers.iteritems():
+        yield url
+
+
+default_doc_root = os.path.join(os.path.dirname(here), "www")
+registered_servers = [("http", http_server),
+                      ("https", https_server)]
+servers = defaultdict()
+
+
+def main(args):
+    global servers
+
+    parser = argparse.ArgumentParser()
+    parser.add_argument("-r", dest="doc_root",
+                        help="Path to document root.  Overrides default.")
+    args = parser.parse_args()
+
+    servers = start(args.doc_root)
+    for url in iter_url(servers):
+        print >>sys.stderr, "%s: listening on %s" % (sys.argv[0], url)
+
+    try:
+        while any(proc.is_alive for proc in iter_proc(servers)):
+            for proc in iter_proc(servers):
+                proc.proc.join(1)
+    except KeyboardInterrupt:
+        for proc in iter_proc(servers):
+            proc.kill()
+
+
+if __name__ == "__main__":
+    main(sys.argv[1:])
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette/runner/test.cert
@@ -0,0 +1,86 @@
+Certificate:
+    Data:
+        Version: 3 (0x2)
+        Serial Number: 2 (0x2)
+    Signature Algorithm: sha256WithRSAEncryption
+        Issuer: CN=web-platform-tests
+        Validity
+            Not Before: Dec 22 12:09:16 2014 GMT
+            Not After : Dec 21 12:09:16 2024 GMT
+        Subject: CN=web-platform.test
+        Subject Public Key Info:
+            Public Key Algorithm: rsaEncryption
+                Public-Key: (2048 bit)
+                Modulus:
+                    00:b3:84:d6:8b:01:59:18:85:d1:dc:32:df:38:f7:
+                    90:85:1b:3e:a5:5e:81:3e:2f:fc:3a:5f:7f:77:ef:
+                    23:bb:3a:88:27:0f:be:25:46:cd:63:7d:cb:95:d8:
+                    a5:50:10:d2:a2:d2:b7:97:d1:0d:6c:fb:f9:05:e8:
+                    6f:a8:4b:bd:95:67:9e:7b:94:58:a9:6d:93:fd:e0:
+                    12:c5:cd:b4:8a:64:52:31:5f:0e:e3:89:84:71:da:
+                    98:dd:4b:ec:02:25:a5:7d:35:fe:63:da:b3:ac:ec:
+                    a5:46:0f:0d:64:23:5c:6d:f3:ec:cc:28:63:23:c0:
+                    4b:9a:ec:8f:c1:ee:b1:a2:3e:72:4d:70:b5:09:c1:
+                    eb:b4:10:55:3c:8b:ea:1b:94:7e:4b:74:e6:f4:9f:
+                    4f:a6:45:30:b5:f0:b8:b4:d1:59:50:65:0a:86:53:
+                    ea:4c:9f:9e:f4:58:6c:31:f5:17:3a:6f:57:8b:cb:
+                    5f:f0:28:0b:45:92:8d:30:20:49:ff:52:e6:2c:cb:
+                    18:9a:d7:e6:ee:3e:4f:34:35:15:13:c5:02:da:c5:
+                    5f:be:fb:5b:ce:8d:bf:b5:35:76:3c:7c:e6:9c:3b:
+                    26:87:4d:8d:80:e6:16:c6:27:f2:50:49:b6:72:74:
+                    43:49:49:44:38:bb:78:43:23:ee:16:3e:d9:62:e6:
+                    a5:d7
+                Exponent: 65537 (0x10001)
+        X509v3 extensions:
+            X509v3 Basic Constraints: 
+                CA:FALSE
+            X509v3 Subject Key Identifier: 
+                2D:98:A3:99:39:1C:FE:E9:9A:6D:17:94:D2:3A:96:EE:C8:9E:04:22
+            X509v3 Authority Key Identifier: 
+                keyid:6A:AB:53:64:92:36:87:23:34:B3:1D:6F:85:4B:F5:DF:5A:5C:74:8F
+
+            X509v3 Key Usage: 
+                Digital Signature, Non Repudiation, Key Encipherment
+            X509v3 Extended Key Usage: 
+                TLS Web Server Authentication
+            X509v3 Subject Alternative Name: 
+                DNS:web-platform.test, DNS:www.web-platform.test, DNS:xn--n8j6ds53lwwkrqhv28a.web-platform.test, DNS:xn--lve-6lad.web-platform.test, DNS:www2.web-platform.test, DNS:www1.web-platform.test
+    Signature Algorithm: sha256WithRSAEncryption
+         33:db:f7:f0:f6:92:16:4f:2d:42:bc:b8:aa:e6:ab:5e:f9:b9:
+         b0:48:ae:b5:8d:cc:02:7b:e9:6f:4e:75:f7:17:a0:5e:7b:87:
+         06:49:48:83:c5:bb:ca:95:07:37:0e:5d:e3:97:de:9e:0c:a4:
+         82:30:11:81:49:5d:50:29:72:92:a5:ca:17:b1:7c:f1:32:11:
+         17:57:e6:59:c1:ac:e3:3b:26:d2:94:97:50:6a:b9:54:88:84:
+         9b:6f:b1:06:f5:80:04:22:10:14:b1:f5:97:25:fc:66:d6:69:
+         a3:36:08:85:23:ff:8e:3c:2b:e0:6d:e7:61:f1:00:8f:61:3d:
+         b0:87:ad:72:21:f6:f0:cc:4f:c9:20:bf:83:11:0f:21:f4:b8:
+         c0:dd:9c:51:d7:bb:27:32:ec:ab:a4:62:14:28:32:da:f2:87:
+         80:68:9c:ea:ac:eb:f5:7f:f5:de:f4:c0:39:91:c8:76:a4:ee:
+         d0:a8:50:db:c1:4b:f9:c4:3d:d9:e8:8e:b6:3f:c0:96:79:12:
+         d8:fa:4d:0a:b3:36:76:aa:4e:b2:82:2f:a2:d4:0d:db:fd:64:
+         77:6f:6e:e9:94:7f:0f:c8:3a:3c:96:3d:cd:4d:6c:ba:66:95:
+         f7:b4:9d:a4:94:9f:97:b3:9a:0d:dc:18:8c:11:0b:56:65:8e:
+         46:4c:e6:5e
+-----BEGIN CERTIFICATE-----
+MIID2jCCAsKgAwIBAgIBAjANBgkqhkiG9w0BAQsFADAdMRswGQYDVQQDDBJ3ZWIt
+cGxhdGZvcm0tdGVzdHMwHhcNMTQxMjIyMTIwOTE2WhcNMjQxMjIxMTIwOTE2WjAc
+MRowGAYDVQQDExF3ZWItcGxhdGZvcm0udGVzdDCCASIwDQYJKoZIhvcNAQEBBQAD
+ggEPADCCAQoCggEBALOE1osBWRiF0dwy3zj3kIUbPqVegT4v/Dpff3fvI7s6iCcP
+viVGzWN9y5XYpVAQ0qLSt5fRDWz7+QXob6hLvZVnnnuUWKltk/3gEsXNtIpkUjFf
+DuOJhHHamN1L7AIlpX01/mPas6zspUYPDWQjXG3z7MwoYyPAS5rsj8HusaI+ck1w
+tQnB67QQVTyL6huUfkt05vSfT6ZFMLXwuLTRWVBlCoZT6kyfnvRYbDH1FzpvV4vL
+X/AoC0WSjTAgSf9S5izLGJrX5u4+TzQ1FRPFAtrFX777W86Nv7U1djx85pw7JodN
+jYDmFsYn8lBJtnJ0Q0lJRDi7eEMj7hY+2WLmpdcCAwEAAaOCASQwggEgMAkGA1Ud
+EwQCMAAwHQYDVR0OBBYEFC2Yo5k5HP7pmm0XlNI6lu7IngQiMB8GA1UdIwQYMBaA
+FGqrU2SSNocjNLMdb4VL9d9aXHSPMAsGA1UdDwQEAwIF4DATBgNVHSUEDDAKBggr
+BgEFBQcDATCBsAYDVR0RBIGoMIGlghF3ZWItcGxhdGZvcm0udGVzdIIVd3d3Lndl
+Yi1wbGF0Zm9ybS50ZXN0gil4bi0tbjhqNmRzNTNsd3drcnFodjI4YS53ZWItcGxh
+dGZvcm0udGVzdIIeeG4tLWx2ZS02bGFkLndlYi1wbGF0Zm9ybS50ZXN0ghZ3d3cy
+LndlYi1wbGF0Zm9ybS50ZXN0ghZ3d3cxLndlYi1wbGF0Zm9ybS50ZXN0MA0GCSqG
+SIb3DQEBCwUAA4IBAQAz2/fw9pIWTy1CvLiq5qte+bmwSK61jcwCe+lvTnX3F6Be
+e4cGSUiDxbvKlQc3Dl3jl96eDKSCMBGBSV1QKXKSpcoXsXzxMhEXV+ZZwazjOybS
+lJdQarlUiISbb7EG9YAEIhAUsfWXJfxm1mmjNgiFI/+OPCvgbedh8QCPYT2wh61y
+IfbwzE/JIL+DEQ8h9LjA3ZxR17snMuyrpGIUKDLa8oeAaJzqrOv1f/Xe9MA5kch2
+pO7QqFDbwUv5xD3Z6I62P8CWeRLY+k0KszZ2qk6ygi+i1A3b/WR3b27plH8PyDo8
+lj3NTWy6ZpX3tJ2klJ+Xs5oN3BiMEQtWZY5GTOZe
+-----END CERTIFICATE-----
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette/runner/test.key
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvgIBADANBgkqhkiG9w0BAQEFAASCBKgwggSkAgEAAoIBAQCzhNaLAVkYhdHc
+Mt8495CFGz6lXoE+L/w6X3937yO7OognD74lRs1jfcuV2KVQENKi0reX0Q1s+/kF
+6G+oS72VZ557lFipbZP94BLFzbSKZFIxXw7jiYRx2pjdS+wCJaV9Nf5j2rOs7KVG
+Dw1kI1xt8+zMKGMjwEua7I/B7rGiPnJNcLUJweu0EFU8i+oblH5LdOb0n0+mRTC1
+8Li00VlQZQqGU+pMn570WGwx9Rc6b1eLy1/wKAtFko0wIEn/UuYsyxia1+buPk80
+NRUTxQLaxV+++1vOjb+1NXY8fOacOyaHTY2A5hbGJ/JQSbZydENJSUQ4u3hDI+4W
+Ptli5qXXAgMBAAECggEBAIcwDQSnIjo2ZECHytQykpG6X6XXEksLhc1Lp0lhPC49
+uNR5pX6a4AcBb3PLr0opMQZO2tUoKA0ff3t0e8loKD+/xXhY0Z/dlioEOP7elwv0
+2nS1mhe9spCuxpk4GGXRhdtR8t2tj8s0do3YvgPgITXoEDX6YBZHNGhZpzSrFPgQ
+/c3eGCVmzWYuLFfdj5OPQ9bwTaY4JSvDLZT0/WTgiica7VySwfz3HP1fFqNykTiK
+ACQREvtxfk5Ym2nT6oni7CM2zOEJL9SXicXI5HO4bERH0ZYh//F3g6mwGiFXUJPd
+NKgaTM1oT9kRGkUaEYsRWrddwR8d5mXLvBuTJbgIsSECgYEA1+2uJSYRW1OqbhYP
+ms59YQHSs3VjpJpnCV2zNa2Wixs57KS2cOH7B6KrQCogJFLtgCDVLtyoErfVkD7E
+FivTgYr1pVCRppJddQzXik31uOINOBVffr7/09g3GcRN+ubHPZPq3K+dD6gHa3Aj
+0nH1EjEEV0QpSTQFn87OF2mc9wcCgYEA1NVqMbbzd+9Xft5FXuSbX6E+S02dOGat
+SgpnkTM80rjqa6eHdQzqk3JqyteHPgdi1vdYRlSPOj/X+6tySY0Ej9sRnYOfddA2
+kpiDiVkmiqVolyJPY69Utj+E3TzJ1vhCQuYknJmB7zP9tDcTxMeq0l/NaWvGshEK
+yC4UTQog1rECgYASOFILfGzWgfbNlzr12xqlRtwanHst9oFfPvLSQrWDQ2bd2wAy
+Aj+GY2mD3oobxouX1i1m6OOdwLlalJFDNauBMNKNgoDnx03vhIfjebSURy7KXrNS
+JJe9rm7n07KoyzRgs8yLlp3wJkOKA0pihY8iW9R78JpzPNqEo5SsURMXnQKBgBlV
+gfuC9H4tPjP6zzUZbyk1701VYsaI6k2q6WMOP0ox+q1v1p7nN7DvaKjWeOG4TVqb
+PKW6gQYE/XeWk9cPcyCQigs+1KdYbnaKsvWRaBYO1GFREzQhdarv6qfPCZOOH40J
+Cgid+Sp4/NULzU2aGspJ3xCSZKdjge4MFhyJfRkxAoGBAJlwqY4nue0MBLGNpqcs
+WwDtSasHvegKAcxGBKL5oWPbLBk7hk+hdqc8f6YqCkCNqv/ooBspL15ESItL+6yT
+zt0YkK4oH9tmLDb+rvqZ7ZdXbWSwKITMoCyyHUtT6OKt/RtA0Vdy9LPnP27oSO/C
+dk8Qf7KgKZLWo0ZNkvw38tEC
+-----END PRIVATE KEY-----
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette/tests/harness_unit/test_httpd.py
@@ -0,0 +1,89 @@
+# 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/.
+
+import json
+import os
+import types
+import urllib2
+
+import pytest
+
+from marionette.runner import httpd
+from wptserve.handlers import json_handler
+
+here = os.path.abspath(os.path.dirname(__file__))
+parent = os.path.dirname(here)
+default_doc_root = os.path.join(os.path.dirname(parent), "www")
+
+
+@pytest.yield_fixture
+def server():
+    server = httpd.FixtureServer(default_doc_root)
+    yield server
+    server.stop()
+
+
+def test_ctor():
+    with pytest.raises(ValueError):
+        httpd.FixtureServer("foo")
+    httpd.FixtureServer(default_doc_root)
+
+
+def test_start_stop(server):
+    server.start()
+    server.stop()
+
+
+def test_get_url(server):
+    server.start()
+    url = server.get_url("/")
+    assert isinstance(url, types.StringTypes)
+    assert "http://" in url
+
+    server.stop()
+    with pytest.raises(httpd.NotAliveError):
+        server.get_url("/")
+
+
+def test_doc_root(server):
+    server.start()
+    assert isinstance(server.doc_root, types.StringTypes)
+    server.stop()
+    assert isinstance(server.doc_root, types.StringTypes)
+
+
+def test_router(server):
+    assert server.router is not None
+
+
+def test_routes(server):
+    assert server.routes is not None
+
+
+def test_is_alive(server):
+    assert server.is_alive == False
+    server.start()
+    assert server.is_alive == True
+
+
+def test_handler(server):
+    counter = 0
+
+    @json_handler
+    def handler(request, response):
+        return {"count": counter}
+
+    route = ("GET", "/httpd/test_handler", handler)
+    server.router.register(*route)
+    server.start()
+
+    url = server.get_url("/httpd/test_handler")
+    body = urllib2.urlopen(url).read()
+    res = json.loads(body)
+    assert res["count"] == counter
+
+
+if __name__ == "__main__":
+    import sys
+    sys.exit(pytest.main(["--verbose", __file__]))
--- a/testing/marionette/harness/marionette/tests/harness_unit/test_marionette_runner.py
+++ b/testing/marionette/harness/marionette/tests/harness_unit/test_marionette_runner.py
@@ -20,17 +20,17 @@ def runner(mach_parsed_kwargs):
 @pytest.fixture
 def mock_runner(runner, mock_marionette, monkeypatch):
     """
     MarionetteTestRunner instance with mocked-out
     self.marionette and other properties,
     to enable testing runner.run_tests().
     """
     runner.driverclass = mock_marionette
-    for attr in ['_set_baseurl', 'run_test_set', '_capabilities']:
+    for attr in ['run_test_set', '_capabilities']:
         setattr(runner, attr, Mock())
     runner._appName = 'fake_app'
     monkeypatch.setattr('marionette.runner.base.mozversion', Mock())
     return runner
 
 
 @pytest.fixture
 def build_kwargs_using(mach_parsed_kwargs):
@@ -347,17 +347,17 @@ def test_cleanup_with_manifest(mock_runn
     monkeypatch.setattr('marionette.runner.base.TestManifest', manifest_with_tests.manifest_class)
     if manifest_with_tests.n_enabled > 0:
         context = patch('marionette.runner.base.os.path.exists', return_value=True)
     else:
         context = pytest.raises(Exception)
     with context:
         mock_runner.run_tests([manifest_with_tests.filepath])
     assert mock_runner.marionette is None
-    assert mock_runner.httpd is None
+    assert mock_runner.fixture_servers == {}
 
 
 def test_reset_test_stats(mock_runner):
     def reset_successful(runner):
         stats = ['passed', 'failed', 'unexpected_successes', 'todo', 'skipped', 'failures']
         return all([((s in vars(runner)) and (not vars(runner)[s])) for s in stats])
     assert reset_successful(mock_runner)
     mock_runner.passed = 1
new file mode 100644
--- /dev/null
+++ b/testing/marionette/harness/marionette/tests/harness_unit/test_serve.py
@@ -0,0 +1,67 @@
+# 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/.
+
+import types
+
+import pytest
+
+from marionette.runner import serve
+from marionette.runner.serve import iter_proc, iter_url
+
+
+def teardown_function(func):
+    for server in [server for server in iter_proc(serve.servers) if server.is_alive]:
+        server.stop()
+        server.kill()
+
+
+def test_registered_servers():
+    # [(name, factory), ...]
+    assert serve.registered_servers[0][0] == "http"
+    assert serve.registered_servers[1][0] == "https"
+
+
+def test_globals():
+    assert serve.default_doc_root is not None
+    assert serve.registered_servers is not None
+    assert serve.servers is not None
+
+
+def test_start():
+    serve.start()
+    assert len(serve.servers) == 2
+    assert "http" in serve.servers
+    assert "https" in serve.servers
+    for url in iter_url(serve.servers):
+        assert isinstance(url, types.StringTypes)
+
+
+def test_start_with_custom_root(tmpdir_factory):
+    tdir = tmpdir_factory.mktemp("foo")
+    serve.start(str(tdir))
+    for server in iter_proc(serve.servers):
+        assert server.doc_root == tdir
+
+
+def test_iter_proc():
+    serve.start()
+    for server in iter_proc(serve.servers):
+        server.stop()
+
+
+def test_iter_url():
+    serve.start()
+    for url in iter_url(serve.servers):
+        assert isinstance(url, types.StringTypes)
+
+
+def test_where_is():
+    serve.start()
+    assert serve.where_is("/") == serve.servers["http"][1].get_url("/")
+    assert serve.where_is("/", on="https") == serve.servers["https"][1].get_url("/")
+
+
+if __name__ == "__main__":
+    import sys
+    sys.exit(pytest.main(["-s", "--verbose", __file__]))
--- a/testing/marionette/harness/marionette/tests/unit/test_capabilities.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_capabilities.py
@@ -29,19 +29,18 @@ class TestCapabilities(MarionetteTestCas
         self.assertEqual(self.caps["browserName"], self.appinfo["name"].lower())
         self.assertEqual(self.caps["browserVersion"], self.appinfo["version"])
         self.assertEqual(self.caps["platformName"], self.os_name)
         self.assertEqual(self.caps["platformVersion"], self.os_version)
         self.assertEqual(self.caps["specificationLevel"], 0)
 
     def test_supported_features(self):
         self.assertIn("rotatable", self.caps)
-        self.assertIn("acceptSslCerts", self.caps)
-
-        self.assertFalse(self.caps["acceptSslCerts"])
+        self.assertIn("acceptInsecureCerts", self.caps)
+        self.assertFalse(self.caps["acceptInsecureCerts"])
 
     def test_additional_capabilities(self):
         self.assertIn("processId", self.caps)
         self.assertEqual(self.caps["processId"], self.appinfo["processID"])
 
     def test_we_can_pass_in_capabilities_on_session_start(self):
         self.marionette.delete_session()
         capabilities = {"desiredCapabilities": {"somethingAwesome": "cake"}}
deleted file mode 100644
--- a/testing/marionette/harness/marionette/tests/unit/test_httpd.py
+++ /dev/null
@@ -1,32 +0,0 @@
-# This Source Code Form is subject to the terms of the Mozilla Public
-# License, v. 2.0. If a copy of the MPL was not distributed with this
-# file, You can obtain one at http://mozilla.org/MPL/2.0/.
-
-from marionette import MarionetteTestCase
-from marionette_driver import By
-
-
-class TestHttpdServer(MarionetteTestCase):
-
-    def test_handler(self):
-        status = {"count": 0}
-
-        def handler(request, response):
-            status["count"] += 1
-
-            response.headers.set("Content-Type", "text/html")
-            response.content = "<html><body><p id=\"count\">{}</p></body></html>".format(
-                status["count"])
-
-            return ()
-
-        route = ("GET", "/httpd/test_handler", handler)
-        self.httpd.router.register(*route)
-
-        url = self.marionette.absolute_url("httpd/test_handler")
-
-        for counter in range(0, 5):
-            self.marionette.navigate(url)
-            self.assertEqual(status["count"], counter + 1)
-            elem = self.marionette.find_element(By.ID, "count")
-            self.assertEqual(elem.text, str(counter + 1))
--- a/testing/marionette/harness/marionette/tests/unit/test_navigation.py
+++ b/testing/marionette/harness/marionette/tests/unit/test_navigation.py
@@ -1,44 +1,52 @@
 # 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/.
 
 import time
 import urllib
+import contextlib
 
 from marionette import MarionetteTestCase
-from marionette_driver.errors import MarionetteException, TimeoutException
-from marionette_driver import By, Wait
+from marionette_driver import errors, By, Wait
 
 
 def inline(doc):
     return "data:text/html;charset=utf-8,%s" % urllib.quote(doc)
 
 
 class TestNavigate(MarionetteTestCase):
+
     def setUp(self):
         MarionetteTestCase.setUp(self)
         self.marionette.navigate("about:")
         self.test_doc = self.marionette.absolute_url("test.html")
         self.iframe_doc = self.marionette.absolute_url("test_iframe.html")
 
+    @property
+    def location_href(self):
+        return self.marionette.execute_script("return window.location.href")
+
     def test_set_location_through_execute_script(self):
-        self.marionette.execute_script("window.location.href = '%s'" % self.test_doc)
-        Wait(self.marionette).until(lambda _: self.test_doc == self.location_href)
+        self.marionette.execute_script(
+            "window.location.href = '%s'" % self.test_doc)
+        Wait(self.marionette).until(
+            lambda _: self.test_doc == self.location_href)
         self.assertEqual("Marionette Test", self.marionette.title)
 
     def test_navigate(self):
         self.marionette.navigate(self.test_doc)
         self.assertNotEqual("about:", self.location_href)
         self.assertEqual("Marionette Test", self.marionette.title)
 
     def test_navigate_chrome_error(self):
         with self.marionette.using_context("chrome"):
-            self.assertRaisesRegexp(MarionetteException, "Cannot navigate in chrome context",
+            self.assertRaisesRegexp(
+                errors.MarionetteException, "Cannot navigate in chrome context",
                                     self.marionette.navigate, "about:blank")
 
     def test_get_current_url_returns_top_level_browsing_context_url(self):
         self.marionette.navigate(self.iframe_doc)
         self.assertEqual(self.iframe_doc, self.location_href)
         frame = self.marionette.find_element(By.CSS_SELECTOR, "#test_iframe")
         self.marionette.switch_to_frame(frame)
         self.assertEqual(self.iframe_doc, self.marionette.get_url())
@@ -91,55 +99,113 @@ class TestNavigate(MarionetteTestCase):
         self.marionette.navigate(self.marionette.absolute_url("test_iframe.html"))
         self.marionette.switch_to_frame(0)
         self.marionette.navigate(self.marionette.absolute_url("empty.html"))
         self.assertTrue('empty.html' in self.marionette.get_url())
         self.marionette.switch_to_frame()
         self.assertTrue('test_iframe.html' in self.marionette.get_url())
     """
 
-    def test_should_not_error_if_nonexistent_url_used(self):
-        try:
+    def test_invalid_protocol(self):
+        with self.assertRaises(errors.MarionetteException):
             self.marionette.navigate("thisprotocoldoesnotexist://")
-            self.fail("Should have thrown a MarionetteException")
-        except TimeoutException:
-            self.fail("The socket shouldn't have timed out when navigating to a non-existent URL")
-        except MarionetteException as e:
-            self.assertIn("Reached error page", str(e))
-        except Exception as e:
-            import traceback
-            print traceback.format_exc()
-            self.fail("Should have thrown a MarionetteException instead of %s" % type(e))
 
     def test_should_navigate_to_requested_about_page(self):
         self.marionette.navigate("about:neterror")
         self.assertEqual(self.marionette.get_url(), "about:neterror")
         self.marionette.navigate(self.marionette.absolute_url("test.html"))
         self.marionette.navigate("about:blocked")
         self.assertEqual(self.marionette.get_url(), "about:blocked")
 
     def test_find_element_state_complete(self):
         self.marionette.navigate(self.test_doc)
-        state = self.marionette.execute_script("return window.document.readyState")
+        state = self.marionette.execute_script(
+            "return window.document.readyState")
         self.assertEqual("complete", state)
         self.assertTrue(self.marionette.find_element(By.ID, "mozLink"))
 
     def test_error_when_exceeding_page_load_timeout(self):
-        with self.assertRaises(TimeoutException):
+        with self.assertRaises(errors.TimeoutException):
             self.marionette.timeout.page_load = 0
             self.marionette.navigate(self.marionette.absolute_url("slow"))
             self.marionette.find_element(By.TAG_NAME, "p")
 
     def test_navigate_iframe(self):
         self.marionette.navigate(self.iframe_doc)
         self.assertTrue('test_iframe.html' in self.marionette.get_url())
         self.assertTrue(self.marionette.find_element(By.ID, "test_iframe"))
 
     def test_fragment(self):
         doc = inline("<p id=foo>")
         self.marionette.navigate(doc)
         self.marionette.execute_script("window.visited = true", sandbox=None)
         self.marionette.navigate("%s#foo" % doc)
-        self.assertTrue(self.marionette.execute_script("return window.visited", sandbox=None))
+        self.assertTrue(self.marionette.execute_script(
+            "return window.visited", sandbox=None))
+
+    def test_error_on_tls_navigation(self):
+        self.assertRaises(errors.InsecureCertificateException,
+                          self.marionette.navigate, self.fixtures.where_is("/test.html", on="https"))
+
+
+class TestTLSNavigation(MarionetteTestCase):
+    insecure_tls = {"acceptInsecureCerts": True}
+    secure_tls = {"acceptInsecureCerts": False}
+
+    def setUp(self):
+        MarionetteTestCase.setUp(self)
+        self.marionette.delete_session()
+        self.capabilities = self.marionette.start_session(
+            desired_capabilities=self.insecure_tls)
+
+    def tearDown(self):
+        try:
+            self.marionette.delete_session()
+        except:
+            pass
+        MarionetteTestCase.tearDown(self)
+
+    @contextlib.contextmanager
+    def safe_session(self):
+        try:
+            self.capabilities = self.marionette.start_session(
+                desired_capabilities=self.secure_tls)
+            yield self.marionette
+        finally:
+            self.marionette.delete_session()
 
-    @property
-    def location_href(self):
-        return self.marionette.execute_script("return window.location.href")
+    @contextlib.contextmanager
+    def unsafe_session(self):
+        try:
+            self.capabilities = self.marionette.start_session(
+                desired_capabilities=self.insecure_tls)
+            yield self.marionette
+        finally:
+            self.marionette.delete_session()
+
+    def test_navigate_by_command(self):
+        self.marionette.navigate(
+            self.fixtures.where_is("/test.html", on="https"))
+        self.assertIn("https", self.marionette.get_url())
+
+    def test_navigate_by_click(self):
+        link_url = self.fixtures.where_is("/test.html", on="https")
+        self.marionette.navigate(
+            inline("<a href=%s>https is the future</a>" % link_url))
+        self.marionette.find_element(By.TAG_NAME, "a").click()
+        self.assertIn("https", self.marionette.get_url())
+
+    def test_deactivation(self):
+        invalid_cert_url = self.fixtures.where_is("/test.html", on="https")
+
+        print "with safe session"
+        with self.safe_session() as session:
+            with self.assertRaises(errors.InsecureCertificateException):
+                session.navigate(invalid_cert_url)
+
+        print "with unsafe session"
+        with self.unsafe_session() as session:
+            session.navigate(invalid_cert_url)
+
+        print "with safe session again"
+        with self.safe_session() as session:
+            with self.assertRaises(errors.InsecureCertificateException):
+                session.navigate(invalid_cert_url)
--- a/testing/marionette/harness/marionette/tests/unit/unit-tests.ini
+++ b/testing/marionette/harness/marionette/tests/unit/unit-tests.ini
@@ -118,10 +118,9 @@ skip-if = appname == 'fennec' || os == "
 
 [test_chrome.py]
 skip-if = appname == 'fennec'
 
 [test_addons.py]
 
 [test_select.py]
 [test_crash.py]
-[test_httpd.py]
 [test_localization.py]
--- a/testing/marionette/harness/requirements.txt
+++ b/testing/marionette/harness/requirements.txt
@@ -1,14 +1,14 @@
-marionette-driver >= 2.1.0
 browsermob-proxy >= 0.6.0
 manifestparser >= 1.1
-wptserve >= 1.3.0
+marionette-driver >= 2.1.0
+mozcrash >= 0.5
+mozdevice >= 0.44
 mozinfo >= 0.8
-mozprocess >= 0.9
-mozrunner >= 6.13
-mozdevice >= 0.44
 mozlog >= 3.0
 moznetwork >= 0.21
-mozcrash >= 0.5
+mozprocess >= 0.9
 mozprofile >= 0.7
+mozrunner >= 6.13
 moztest >= 0.7
 mozversion >= 1.1
+wptserve >= 1.3.0
--- a/testing/marionette/jar.mn
+++ b/testing/marionette/jar.mn
@@ -10,16 +10,17 @@ marionette.jar:
   content/legacyaction.js (legacyaction.js)
   content/browser.js (browser.js)
   content/interaction.js (interaction.js)
   content/accessibility.js (accessibility.js)
   content/listener.js (listener.js)
   content/element.js (element.js)
   content/simpletest.js (simpletest.js)
   content/frame.js (frame.js)
+  content/cert.js (cert.js)
   content/event.js  (event.js)
   content/error.js (error.js)
   content/message.js (message.js)
   content/dispatcher.js (dispatcher.js)
   content/modal.js (modal.js)
   content/proxy.js (proxy.js)
   content/capture.js (capture.js)
   content/cookies.js (cookies.js)
--- a/testing/marionette/listener.js
+++ b/testing/marionette/listener.js
@@ -870,20 +870,25 @@ function pollForReadyState(msg, start = 
     let doc = curContainer.frame.document;
     let now = new Date().getTime();
     if (pageTimeout == null || (now - start) <= pageTimeout) {
       // document fully loaded
       if (doc.readyState == "complete") {
         callback();
         sendOk(command_id);
 
+      // document with an insecure cert
+      } else if (doc.readyState == "interactive" &&
+          doc.baseURI.startsWith("about:certerror")) {
+        callback();
+        sendError(new InsecureCertificateError(), command_id);
+
       // we have reached an error url without requesting it
       } else if (doc.readyState == "interactive" &&
-          /about:.+(error)\?/.exec(doc.baseURI) &&
-          !doc.baseURI.startsWith(url)) {
+          /about:.+(error)\?/.exec(doc.baseURI)) {
         callback();
         sendError(new UnknownError("Reached error page: " + doc.baseURI), command_id);
 
       // return early for about: urls
       } else if (doc.readyState == "interactive" && doc.baseURI.startsWith("about:")) {
         callback();
         sendOk(command_id);
 
--- a/toolkit/modules/WebChannel.jsm
+++ b/toolkit/modules/WebChannel.jsm
@@ -171,29 +171,29 @@ this.WebChannel = function(id, originOrP
   this.id = id;
   // originOrPermission can be either an nsIURI or a string representing a
   // permission name.
   if (typeof originOrPermission == "string") {
     this._originCheckCallback = requestPrincipal => {
       // The permission manager operates on domain names rather than true
       // origins (bug 1066517).  To mitigate that, we explicitly check that
       // the scheme is https://.
-      let uri = Services.io.newURI(requestPrincipal.origin, null, null);
+      let uri = Services.io.newURI(requestPrincipal.originNoSuffix, null, null);
       if (uri.scheme != "https") {
         return false;
       }
       // OK - we have https - now we can check the permission.
       let perm = Services.perms.testExactPermissionFromPrincipal(requestPrincipal,
                                                                  originOrPermission);
       return perm == Ci.nsIPermissionManager.ALLOW_ACTION;
     }
   } else {
     // a simple URI, so just check for an exact match.
     this._originCheckCallback = requestPrincipal => {
-      return originOrPermission.prePath === requestPrincipal.origin;
+      return originOrPermission.prePath === requestPrincipal.originNoSuffix;
     }
   }
   this._originOrPermission = originOrPermission;
 };
 
 this.WebChannel.prototype = {
 
   /**