webdriver: Add support for the new-style NewSession command in addition to the leacgy one. (#67)
authorjgraham <james@hoppipolla.co.uk>
Thu, 23 Mar 2017 16:26:32 +0000
changeset 428128 dff765c4ab4c5d51eae70b68aa5ab7f4e896fd4c
parent 428127 c245d9ddabf13516a7bb577d7ebf0aaf6ccc0d18
child 428129 e9b1b5d4d1779f77007021848bce43a15e73d8a1
push id7761
push userjlund@mozilla.com
push dateFri, 15 Sep 2017 00:19:52 +0000
treeherdermozilla-beta@c38455951db4 [default view] [failures only]
perfherder[talos] [build metrics] [platform microbench] (compared to previous push)
milestone57.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
webdriver: Add support for the new-style NewSession command in addition to the leacgy one. (#67) This adds a BrowserCapabilities trait. Implementations must call NewSessionParameters::match_browser with an object implementing this trait in order to get the set of capabilites that are supported by the browser and should be used for the current session. Source-Repo: https://github.com/mozilla/webdriver-rust Source-Revision: 3e73b0b6bbbd7bdb438aa54c2e8b249493c4e763 committer: GitHub <noreply@github.com>
testing/webdriver/Cargo.toml
testing/webdriver/src/capabilities.rs
testing/webdriver/src/command.rs
testing/webdriver/src/lib.rs
testing/webdriver/src/response.rs
--- a/testing/webdriver/Cargo.toml
+++ b/testing/webdriver/Cargo.toml
@@ -12,8 +12,9 @@ license = "MPL-2.0"
 [dependencies]
 backtrace = "0.3"
 cookie = {version = "0.6", default-features = false}
 hyper = "0.10"
 log = "0.3"
 regex = "0.2"
 rustc-serialize = "0.3"
 time = "0.1"
+url = "1"
\ No newline at end of file
new file mode 100644
--- /dev/null
+++ b/testing/webdriver/src/capabilities.rs
@@ -0,0 +1,554 @@
+use command::Parameters;
+use error::{WebDriverResult, WebDriverError, ErrorStatus};
+use rustc_serialize::json::{ToJson, Json};
+use std::collections::BTreeMap;
+use url::Url;
+
+pub type Capabilities = BTreeMap<String, Json>;
+
+/// Trait for objects that can be used to inspect browser capabilities
+///
+/// The main methods in this trait are called with a Capabilites object
+/// resulting from a full set of potential capabilites for the session.
+/// Given those Capabilities they return a property of the browser instance
+/// that would be initiated. In many cases this will be independent of the
+/// input, but in the case of e.g. browser version, it might depend on a
+/// path to the binary provided as a capability.
+pub trait BrowserCapabilities {
+    /// Set up the Capabilites object
+    ///
+    /// Typically used to create any internal caches
+    fn init(&mut self, &Capabilities);
+
+    /// Name of the browser
+    fn browser_name(&mut self, &Capabilities) -> WebDriverResult<Option<String>>;
+    /// Version number of the browser
+    fn browser_version(&mut self, &Capabilities) -> WebDriverResult<Option<String>>;
+    /// Compare actual browser version to that provided in a version specifier
+    ///
+    /// Parameters are the actual browser version and the comparison string,
+    /// respectively. The format of the comparison string is implementation-defined.
+    fn compare_browser_version(&mut self, version: &str, comparison: &str) -> WebDriverResult<bool>;
+    /// Name of the platform/OS
+    fn platform_name(&mut self, &Capabilities) -> WebDriverResult<Option<String>>;
+    /// Whether insecure certificates are supported
+    fn accept_insecure_certs(&mut self, &Capabilities) -> WebDriverResult<bool>;
+
+    fn accept_proxy(&mut self, proxy_settings: &BTreeMap<String, Json>, &Capabilities) -> WebDriverResult<bool>;
+
+    /// Type check custom properties
+    ///
+    /// Check that custom properties containing ":" have the correct data types.
+    /// Properties that are unrecognised must be ignored i.e. return without
+    /// error.
+    fn validate_custom(&self, name: &str, value: &Json) -> WebDriverResult<()>;
+    /// Check if custom properties are accepted capabilites
+    ///
+    /// Check that custom properties containing ":" are compatible with
+    /// the implementation.
+    fn accept_custom(&mut self, name: &str, value: &Json, merged: &Capabilities) -> WebDriverResult<bool>;
+}
+
+/// Trait to abstract over various version of the new session parameters
+///
+/// This trait is expected to be implemented on objects holding the capabilities
+/// from a new session command.
+pub trait CapabilitiesMatching {
+    /// Match the BrowserCapabilities against some candidate capabilites
+    ///
+    /// Takes a BrowserCapabilites object and returns a set of capabilites that
+    /// are valid for that browser, if any, or None if there are no matching
+    /// capabilities.
+    fn match_browser<T: BrowserCapabilities>(&self, browser_capabilities: &mut T)
+                                             -> WebDriverResult<Option<Capabilities>>;
+}
+
+#[derive(PartialEq)]
+pub struct SpecNewSessionParameters {
+    pub alwaysMatch: Capabilities,
+    pub firstMatch: Vec<Capabilities>,
+}
+
+impl SpecNewSessionParameters {
+    fn validate<T: BrowserCapabilities>(&self,
+                                        mut capabilities: Capabilities,
+                                        browser_capabilities: &T) -> WebDriverResult<Capabilities> {
+        // Filter out entries with the value `null`
+        let null_entries = capabilities
+            .iter()
+            .filter(|&(_, ref value)| **value == Json::Null)
+            .map(|(k, _)| k.clone())
+            .collect::<Vec<String>>();
+        for key in null_entries {
+            capabilities.remove(&key);
+        }
+
+        for (key, value) in capabilities.iter() {
+            match &**key {
+                "acceptInsecureCerts" => if !value.is_boolean() {
+                        return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                       "acceptInsecureCerts was not a boolean"))
+                    },
+                x @ "browserName" |
+                x @ "browserVersion" |
+                x @ "platformName" => if !value.is_string() {
+                        return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                       format!("{} was not a boolean", x)))
+                    },
+                "pageLoadStrategy" => {
+                    try!(SpecNewSessionParameters::validate_page_load_strategy(value))
+                }
+                "proxy" => {
+                    try!(SpecNewSessionParameters::validate_proxy(value))
+                },
+                "timeouts" => {
+                    try!(SpecNewSessionParameters::validate_timeouts(value))
+                },
+                "unhandledPromptBehaviour" => {
+                    try!(SpecNewSessionParameters::validate_unhandled_prompt_behaviour(value))
+                }
+                x => {
+                    if !x.contains(":") {
+                        return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                       format!("{} was not a the name of a known capability or a valid extension capability", x)))
+                    } else {
+                        try!(browser_capabilities.validate_custom(x, value));
+                    }
+                }
+            }
+        }
+        Ok(capabilities)
+    }
+
+    fn validate_page_load_strategy(value: &Json) -> WebDriverResult<()> {
+        match value {
+            &Json::String(ref x) => {
+                match &**x {
+                    "normal" |
+                    "eager" |
+                    "none" => {},
+                    x => {
+                        return Err(WebDriverError::new(
+                            ErrorStatus::InvalidArgument,
+                            format!("\"{}\" not a valid page load strategy", x)))
+                    }
+                }
+            }
+            _ => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                "pageLoadStrategy was not a string"))
+        }
+        Ok(())
+    }
+
+    fn validate_proxy(proxy_value: &Json) -> WebDriverResult<()> {
+        let obj = try_opt!(proxy_value.as_object(),
+                           ErrorStatus::InvalidArgument,
+                           "proxy was not an object");
+        for (key, value) in obj.iter() {
+            match &**key {
+                "proxyType" => match value.as_string() {
+                    Some("pac") |
+                    Some("noproxy") |
+                    Some("autodetect") |
+                    Some("system") |
+                    Some("manual") => {},
+                    Some(x) => return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        format!("{} was not a valid proxyType value", x))),
+                    None => return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        "proxyType value was not a string")),
+                },
+                "proxyAutoconfigUrl" => match value.as_string() {
+                    Some(x) => {
+                        try!(Url::parse(x).or(Err(WebDriverError::new(
+                            ErrorStatus::InvalidArgument,
+                            "proxyAutoconfigUrl was not a valid url"))));
+                    },
+                    None => return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        "proxyAutoconfigUrl was not a string"
+                    ))
+                },
+                "ftpProxy" => try!(SpecNewSessionParameters::validate_host_domain("ftpProxy", "ftp", obj, value)),
+                "ftpProxyPort" => try!(SpecNewSessionParameters::validate_port("ftpProxyPort", value)),
+                "httpProxy" => try!(SpecNewSessionParameters::validate_host_domain("httpProxy", "http", obj, value)),
+                "httpProxyPort" => try!(SpecNewSessionParameters::validate_port("httpProxyPort", value)),
+                "socksProxy" => try!(SpecNewSessionParameters::validate_host_domain("socksProxy", "ssh", obj, value)),
+                "socksProxyPort" => try!(SpecNewSessionParameters::validate_port("socksProxyPort", value)),
+                "socksUsername" => if !value.is_string() {
+                    return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                   "socksUsername was not a string"))
+                },
+                "socksPassword" => if !value.is_string() {
+                    return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                   "socksPassword was not a string"))
+                },
+                x => return Err(WebDriverError::new(
+                    ErrorStatus::InvalidArgument,
+                    format!("{} was not a valid proxy configuration capability", x)))
+            }
+        }
+        Ok(())
+    }
+
+    /// Validate whether a named capability is JSON value is a string containing a host
+    /// and possible port
+    fn validate_host_domain(name: &str,
+                            scheme: &str,
+                            obj: &Capabilities,
+                            value: &Json) -> WebDriverResult<()> {
+        match value.as_string() {
+            Some(x) => {
+                if x.contains("::/") {
+                    return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        format!("{} contains a scheme", name)))
+                };
+                let mut s = String::with_capacity(scheme.len() + x.len() + 3);
+                s.push_str(scheme);
+                s.push_str("://");
+                s.push_str(x);
+                let url = try!(Url::parse(&*s).or(Err(WebDriverError::new(
+                    ErrorStatus::InvalidArgument,
+                    format!("{} was not a valid url", name)))));
+                if url.username() != "" ||
+                    url.password() != None ||
+                    url.path() != "/" ||
+                    url.query() != None ||
+                    url.fragment() != None {
+                        return Err(WebDriverError::new(
+                            ErrorStatus::InvalidArgument,
+                            format!("{} was not of the form host[:port]", name)));
+                    }
+                let mut port_key = String::with_capacity(name.len() + 4);
+                port_key.push_str(name);
+                port_key.push_str("Port");
+                if url.port() != None &&
+                    obj.contains_key(&*port_key) {
+                        return Err(WebDriverError::new(
+                                    ErrorStatus::InvalidArgument,
+                                    format!("{} supplied with a port as well as {}",
+                                            name, port_key)));
+                    }
+            },
+            None => return Err(WebDriverError::new(
+                ErrorStatus::InvalidArgument,
+                format!("{} was not a string", name)
+            ))
+        }
+        Ok(())
+    }
+
+    fn validate_port(name: &str, value: &Json) -> WebDriverResult<()> {
+        match value {
+            &Json::I64(x) => {
+                if x < 0 || x > 2i64.pow(16) - 1 {
+                    return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        format!("{} is out of range", name)))
+                }
+            }
+            _ => return Err(WebDriverError::new(
+                ErrorStatus::InvalidArgument,
+                format!("{} was not an integer", name)))
+        }
+        Ok(())
+    }
+
+    fn validate_timeouts(value: &Json) -> WebDriverResult<()> {
+        let obj = try_opt!(value.as_object(),
+                           ErrorStatus::InvalidArgument,
+                           "timeouts capability was not an object");
+        for (key, value) in obj.iter() {
+            match &**key {
+                x @ "script" |
+                x @ "pageLoad" |
+                x @ "implicit" => {
+                    let timeout = try_opt!(value.as_i64(),
+                                           ErrorStatus::InvalidArgument,
+                                           format!("{} timeouts value was not an integer", x));
+                    if timeout < 0 {
+                        return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                       format!("{} timeouts value was negative", x)))
+                    }
+                },
+                x => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                    format!("{} was not a valid timeouts capability", x)))
+            }
+        }
+        Ok(())
+    }
+
+    fn validate_unhandled_prompt_behaviour(value: &Json) -> WebDriverResult<()> {
+        let behaviour = try_opt!(value.as_string(),
+                                 ErrorStatus::InvalidArgument,
+                                 "unhandledPromptBehaviour capability was not a string");
+        match behaviour {
+            "dismiss" |
+            "accept" => {},
+            x => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                format!("{} was not a valid unhandledPromptBehaviour value", x)))        }
+        Ok(())
+    }
+}
+
+impl Parameters for SpecNewSessionParameters {
+    fn from_json(body: &Json) -> WebDriverResult<SpecNewSessionParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::UnknownError,
+                            "Message body was not an object");
+
+        let capabilities = try_opt!(
+            try_opt!(data.get("capabilities"),
+                     ErrorStatus::InvalidArgument,
+                     "Missing 'capabilities' parameter").as_object(),
+            ErrorStatus::InvalidArgument,
+                     "'capabilities' parameter is not an object");
+
+        let default_always_match = Json::Object(Capabilities::new());
+        let always_match = try_opt!(capabilities.get("alwaysMatch")
+                                   .unwrap_or(&default_always_match)
+                                   .as_object(),
+                                   ErrorStatus::InvalidArgument,
+                                   "'alwaysMatch' parameter is not an object");
+        let default_first_matches = Json::Array(vec![]);
+        let first_matches = try!(
+            try_opt!(capabilities.get("firstMatch")
+                     .unwrap_or(&default_first_matches)
+                     .as_array(),
+                     ErrorStatus::InvalidArgument,
+                     "'firstMatch' parameter is not an array")
+                .iter()
+                .map(|x| x.as_object()
+                     .map(|x| x.clone())
+                     .ok_or(WebDriverError::new(ErrorStatus::InvalidArgument,
+                                                "'firstMatch' entry is not an object")))
+                .collect::<WebDriverResult<Vec<Capabilities>>>());
+
+        return Ok(SpecNewSessionParameters {
+            alwaysMatch: always_match.clone(),
+            firstMatch: first_matches
+        });
+    }
+}
+
+impl ToJson for SpecNewSessionParameters {
+    fn to_json(&self) -> Json {
+        let mut body = BTreeMap::new();
+        let mut capabilities = BTreeMap::new();
+        capabilities.insert("alwaysMatch".into(), self.alwaysMatch.to_json());
+        capabilities.insert("firstMatch".into(), self.firstMatch.to_json());
+        body.insert("capabilities".into(), capabilities.to_json());
+        Json::Object(body)
+    }
+}
+
+
+impl CapabilitiesMatching for SpecNewSessionParameters {
+    fn match_browser<T: BrowserCapabilities>(&self, browser_capabilities: &mut T)
+                                             -> WebDriverResult<Option<Capabilities>> {
+        let default = vec![BTreeMap::new()];
+        let capabilities_list = if self.firstMatch.len() > 0 {
+            &self.firstMatch
+        } else {
+            &default
+        };
+
+        let merged_capabilities = try!(capabilities_list
+            .iter()
+            .map(|first_match_entry| {
+                if first_match_entry.keys().any(|k| {
+                    self.alwaysMatch.contains_key(k)
+                }) {
+                    return Err(WebDriverError::new(
+                        ErrorStatus::InvalidArgument,
+                        "'firstMatch' key shadowed a value in 'alwaysMatch'"));
+                }
+                let mut merged = self.alwaysMatch.clone();
+                merged.append(&mut first_match_entry.clone());
+                Ok(merged)
+            })
+            .map(|merged| merged.and_then(|x| self.validate(x, browser_capabilities)))
+            .collect::<WebDriverResult<Vec<Capabilities>>>());
+
+        let selected = merged_capabilities
+            .iter()
+            .filter_map(|merged| {
+                browser_capabilities.init(merged);
+
+                for (key, value) in merged.iter() {
+                    match &**key {
+                        "browserName" => {
+                            let browserValue = browser_capabilities
+                                .browser_name(merged)
+                                .ok()
+                                .and_then(|x| x);
+
+                            if value.as_string() != browserValue.as_ref().map(|x| &**x) {
+                                    return None;
+                            }
+                        },
+                        "browserVersion" => {
+                            let browserValue = browser_capabilities
+                                .browser_version(merged)
+                                .ok()
+                                .and_then(|x| x);
+                            // We already validated this was a string
+                            let version_cond = value.as_string().unwrap_or("");
+                            if let Some(version) = browserValue {
+                                if !browser_capabilities
+                                    .compare_browser_version(&*version, version_cond)
+                                    .unwrap_or(false) {
+                                        return None;
+                                    }
+                            } else {
+                                return None
+                            }
+                        },
+                        "platformName" => {
+                            let browserValue = browser_capabilities
+                                .platform_name(merged)
+                                .ok()
+                                .and_then(|x| x);
+                            if value.as_string() != browserValue.as_ref().map(|x| &**x) {
+                                return None;
+                            }
+                        }
+                        "acceptInsecureCerts" => {
+                            if value.as_boolean().unwrap_or(false) &&
+                                !browser_capabilities
+                                .accept_insecure_certs(merged)
+                                .unwrap_or(false) {
+                                return None;
+                            }
+                        },
+                        "proxy" => {
+                            let default = BTreeMap::new();
+                            let proxy = value.as_object().unwrap_or(&default);
+                            if !browser_capabilities.accept_proxy(&proxy,
+                                                                  merged)
+                                .unwrap_or(false) {
+                                return None
+                            }
+                        },
+                        name => {
+                            if name.contains(":") {
+                                if !browser_capabilities
+                                    .accept_custom(name, value, merged)
+                                    .unwrap_or(false) {
+                                        return None
+                                    }
+                            } else {
+                                // Accept the capability
+                            }
+                        }
+                    }
+                }
+
+                return Some(merged)
+            })
+            .next()
+            .map(|x| x.clone());
+            Ok(selected)
+    }
+}
+
+#[derive(PartialEq)]
+pub struct LegacyNewSessionParameters {
+    pub desired: Capabilities,
+    pub required: Capabilities,
+}
+
+impl CapabilitiesMatching for LegacyNewSessionParameters {
+    fn match_browser<T: BrowserCapabilities>(&self, browser_capabilities: &mut T)
+                                             -> WebDriverResult<Option<Capabilities>> {
+        /* For now don't do anything much, just merge the
+        desired and required and return the merged list. */
+
+        let mut capabilities: Capabilities = BTreeMap::new();
+        self.required.iter()
+            .chain(self.desired.iter())
+            .fold(&mut capabilities,
+                  |mut caps, (key, value)| {
+                      if !caps.contains_key(key) {
+                          caps.insert(key.clone(), value.clone());
+                      }
+                      caps});
+        browser_capabilities.init(&capabilities);
+        Ok(Some(capabilities))
+    }
+}
+
+impl Parameters for LegacyNewSessionParameters {
+    fn from_json(body: &Json) -> WebDriverResult<LegacyNewSessionParameters> {
+        let data = try_opt!(body.as_object(),
+                            ErrorStatus::UnknownError,
+                            "Message body was not an object");
+
+        let desired_capabilities =
+            if let Some(capabilities) = data.get("desiredCapabilities") {
+                try_opt!(capabilities.as_object(),
+                         ErrorStatus::InvalidArgument,
+                         "'desiredCapabilities' parameter is not an object").clone()
+            } else {
+                BTreeMap::new()
+            };
+
+        let required_capabilities =
+            if let Some(capabilities) = data.get("requiredCapabilities") {
+                try_opt!(capabilities.as_object(),
+                         ErrorStatus::InvalidArgument,
+                         "'requiredCapabilities' parameter is not an object").clone()
+            } else {
+                BTreeMap::new()
+            };
+
+        Ok(LegacyNewSessionParameters {
+            desired: desired_capabilities,
+            required: required_capabilities
+        })
+    }
+}
+
+impl ToJson for LegacyNewSessionParameters {
+    fn to_json(&self) -> Json {
+        let mut data = BTreeMap::new();
+        data.insert("desiredCapabilities".to_owned(), self.desired.to_json());
+        data.insert("requiredCapabilities".to_owned(), self.required.to_json());
+        Json::Object(data)
+    }
+}
+
+#[cfg(test)]
+mod tests {
+    use rustc_serialize::json::Json;
+    use std::collections::BTreeMap;
+    use super::{WebDriverResult, SpecNewSessionParameters};
+
+    fn parse(data: &str) -> BTreeMap<String, Json> {
+        Json::from_str(&*data).unwrap().as_object().unwrap().clone()
+    }
+
+    fn validate_host(name: &str, scheme: &str, caps: &str, value: &str) -> WebDriverResult<()> {
+        SpecNewSessionParameters::validate_host_domain(name,
+                                                       scheme,
+                                                       &parse(caps),
+                                                       &Json::String(value.into()))
+    }
+
+    #[test]
+    fn test_validate_host_domain() {
+        validate_host("ftpProxy", "ftp", "{}", "example.org").unwrap();
+        assert!(validate_host("ftpProxy", "ftp", "{}", "ftp://example.org").is_err());
+        assert!(validate_host("ftpProxy", "ftp", "{}", "example.org/foo").is_err());
+        assert!(validate_host("ftpProxy", "ftp", "{}", "example.org#bar").is_err());
+        assert!(validate_host("ftpProxy", "ftp", "{}", "example.org?bar=baz").is_err());
+        assert!(validate_host("ftpProxy", "ftp", "{}", "foo:bar@example.org").is_err());
+        assert!(validate_host("ftpProxy", "ftp", "{}", "foo@example.org").is_err());
+        validate_host("httpProxy", "http", "{}", "example.org:8000").unwrap();
+        validate_host("httpProxy", "http", "{}", "example.org:8000").unwrap();
+        validate_host("httpProxy", "http", "{\"ftpProxyPort\": \"1234\"}", "example.org:8000").unwrap();
+        assert!(validate_host("httpProxy", "http", "{\"httpProxyPort\": \"1234\"}", "example.org:8000").is_err())
+    }
+}
--- a/testing/webdriver/src/command.rs
+++ b/testing/webdriver/src/command.rs
@@ -1,8 +1,10 @@
+use capabilities::{SpecNewSessionParameters, LegacyNewSessionParameters,
+                   CapabilitiesMatching, BrowserCapabilities, Capabilities};
 use common::{Date, Nullable, WebElement, FrameId, LocatorStrategy};
 use error::{WebDriverResult, WebDriverError, ErrorStatus};
 use httpapi::{Route, WebDriverExtensionRoute, VoidWebDriverExtensionRoute};
 use regex::Captures;
 use rustc_serialize::json::{ToJson, Json};
 use std::collections::BTreeMap;
 use std::default::Default;
 
@@ -423,75 +425,57 @@ impl <U:WebDriverExtensionRoute> ToJson 
         Json::Object(data)
     }
 }
 
 pub trait Parameters: Sized {
     fn from_json(body: &Json) -> WebDriverResult<Self>;
 }
 
+/// Wrapper around the two supported variants of new session paramters
+///
+/// The Spec variant is used for storing spec-compliant parameters whereas
+/// the legacy variant is used to store desiredCapabilities/requiredCapabilities
+/// parameters, and is intended to minimise breakage as we transition users to
+/// the spec design.
 #[derive(PartialEq)]
-pub struct NewSessionParameters {
-    pub desired: BTreeMap<String, Json>,
-    pub required: BTreeMap<String, Json>,
-}
-
-impl NewSessionParameters {
-    pub fn get(&self, name: &str) -> Option<&Json> {
-        self.required.get(name).or_else(|| self.desired.get(name))
-    }
-
-    pub fn consume(&mut self, name: &str) -> Option<Json> {
-        let required = self.required.remove(name);
-        let desired = self.desired.remove(name);
-        if required.is_some() {
-            required
-        } else {
-            desired
-        }
-    }
+pub enum NewSessionParameters {
+    Spec(SpecNewSessionParameters),
+    Legacy(LegacyNewSessionParameters)
 }
 
 impl Parameters for NewSessionParameters {
     fn from_json(body: &Json) -> WebDriverResult<NewSessionParameters> {
         let data = try_opt!(body.as_object(),
                             ErrorStatus::UnknownError,
                             "Message body was not an object");
-
-        let desired_capabilities =
-            if let Some(capabilities) = data.get("desiredCapabilities") {
-                try_opt!(capabilities.as_object(),
-                         ErrorStatus::InvalidArgument,
-                         "'desiredCapabilities' parameter is not an object").clone()
-            } else {
-                BTreeMap::new()
-            };
-
-        let required_capabilities =
-            if let Some(capabilities) = data.get("requiredCapabilities") {
-                try_opt!(capabilities.as_object(),
-                         ErrorStatus::InvalidArgument,
-                         "'requiredCapabilities' parameter is not an object").clone()
-            } else {
-                BTreeMap::new()
-            };
-
-        Ok(NewSessionParameters {
-            desired: desired_capabilities,
-            required: required_capabilities
-        })
+        if data.get("capabilities").is_some() {
+            Ok(NewSessionParameters::Spec(try!(SpecNewSessionParameters::from_json(body))))
+        } else {
+            Ok(NewSessionParameters::Legacy(try!(LegacyNewSessionParameters::from_json(body))))
+        }
     }
 }
 
 impl ToJson for NewSessionParameters {
     fn to_json(&self) -> Json {
-        let mut data = BTreeMap::new();
-        data.insert("desiredCapabilities".to_owned(), self.desired.to_json());
-        data.insert("requiredCapabilities".to_owned(), self.required.to_json());
-        Json::Object(data)
+        match self {
+            &NewSessionParameters::Spec(ref x) => x.to_json(),
+            &NewSessionParameters::Legacy(ref x) => x.to_json()
+        }
+    }
+}
+
+impl CapabilitiesMatching for NewSessionParameters {
+    fn match_browser<T: BrowserCapabilities>(&self, browser_capabilities: &mut T)
+                                             -> WebDriverResult<Option<Capabilities>> {
+        match self {
+            &NewSessionParameters::Spec(ref x) => x.match_browser(browser_capabilities),
+            &NewSessionParameters::Legacy(ref x) => x.match_browser(browser_capabilities)
+        }
     }
 }
 
 
 #[derive(PartialEq)]
 pub struct GetParameters {
     pub url: String
 }
--- a/testing/webdriver/src/lib.rs
+++ b/testing/webdriver/src/lib.rs
@@ -3,19 +3,21 @@
 extern crate backtrace;
 #[macro_use]
 extern crate log;
 extern crate rustc_serialize;
 extern crate hyper;
 extern crate regex;
 extern crate cookie;
 extern crate time;
+extern crate url;
 
 #[macro_use] pub mod macros;
 pub mod httpapi;
+pub mod capabilities;
 pub mod command;
 pub mod common;
 pub mod error;
 pub mod server;
 pub mod response;
 
 #[cfg(test)]
 mod nullable_tests {
--- a/testing/webdriver/src/response.rs
+++ b/testing/webdriver/src/response.rs
@@ -65,23 +65,23 @@ impl ToJson for CloseWindowResponse {
                     .map(|x| Json::String(x.clone()))
                     .collect::<Vec<Json>>())
     }
 }
 
 #[derive(RustcEncodable, Debug)]
 pub struct NewSessionResponse {
     pub sessionId: String,
-    pub value: json::Json
+    pub capabilities: json::Json
 }
 
 impl NewSessionResponse {
-    pub fn new(session_id: String, value: json::Json) -> NewSessionResponse {
+    pub fn new(session_id: String, capabilities: json::Json) -> NewSessionResponse {
         NewSessionResponse {
-            value: value,
+            capabilities: capabilities,
             sessionId: session_id
         }
     }
 }
 
 #[derive(RustcEncodable, Debug)]
 pub struct TimeoutsResponse {
     pub script: u64,
@@ -278,17 +278,17 @@ mod tests {
         test(resp, expected);
     }
 
     #[test]
     fn test_new_session() {
         let resp = WebDriverResponse::NewSession(
             NewSessionResponse::new("test".into(),
                                     Json::Object(BTreeMap::new())));
-        let expected = r#"{"value": {"sessionId": "test", "value": {}}}"#;
+        let expected = r#"{"value": {"sessionId": "test", "capabilities": {}}}"#;
         test(resp, expected);
     }
 
     #[test]
     fn test_timeouts() {
          let resp = WebDriverResponse::Timeouts(TimeoutsResponse::new(
             1, 2, 3));
         let expected = r#"{"value": {"script": 1, "pageLoad": 2, "implicit": 3}}"#;