1 use command::Parameters;
2 use error::{ErrorStatus, WebDriverError, WebDriverResult};
3 use rustc_serialize::json::{Json, ToJson};
4 use std::collections::BTreeMap;
5 use std::net::Ipv6Addr;
8 pub type Capabilities = BTreeMap<String, Json>;
10 /// Trait for objects that can be used to inspect browser capabilities
12 /// The main methods in this trait are called with a Capabilites object
13 /// resulting from a full set of potential capabilites for the session.
14 /// Given those Capabilities they return a property of the browser instance
15 /// that would be initiated. In many cases this will be independent of the
16 /// input, but in the case of e.g. browser version, it might depend on a
17 /// path to the binary provided as a capability.
18 pub trait BrowserCapabilities {
19 /// Set up the Capabilites object
21 /// Typically used to create any internal caches
22 fn init(&mut self, &Capabilities);
24 /// Name of the browser
25 fn browser_name(&mut self, &Capabilities) -> WebDriverResult<Option<String>>;
26 /// Version number of the browser
27 fn browser_version(&mut self, &Capabilities) -> WebDriverResult<Option<String>>;
28 /// Compare actual browser version to that provided in a version specifier
30 /// Parameters are the actual browser version and the comparison string,
31 /// respectively. The format of the comparison string is implementation-defined.
32 fn compare_browser_version(&mut self, version: &str, comparison: &str) -> WebDriverResult<bool>;
33 /// Name of the platform/OS
34 fn platform_name(&mut self, &Capabilities) -> WebDriverResult<Option<String>>;
35 /// Whether insecure certificates are supported
36 fn accept_insecure_certs(&mut self, &Capabilities) -> WebDriverResult<bool>;
38 fn accept_proxy(&mut self, proxy_settings: &BTreeMap<String, Json>, &Capabilities) -> WebDriverResult<bool>;
40 /// Type check custom properties
42 /// Check that custom properties containing ":" have the correct data types.
43 /// Properties that are unrecognised must be ignored i.e. return without
45 fn validate_custom(&self, name: &str, value: &Json) -> WebDriverResult<()>;
46 /// Check if custom properties are accepted capabilites
48 /// Check that custom properties containing ":" are compatible with
49 /// the implementation.
50 fn accept_custom(&mut self, name: &str, value: &Json, merged: &Capabilities) -> WebDriverResult<bool>;
53 /// Trait to abstract over various version of the new session parameters
55 /// This trait is expected to be implemented on objects holding the capabilities
56 /// from a new session command.
57 pub trait CapabilitiesMatching {
58 /// Match the BrowserCapabilities against some candidate capabilites
60 /// Takes a BrowserCapabilites object and returns a set of capabilites that
61 /// are valid for that browser, if any, or None if there are no matching
63 fn match_browser<T: BrowserCapabilities>(&self, browser_capabilities: &mut T)
64 -> WebDriverResult<Option<Capabilities>>;
67 #[derive(Debug, PartialEq)]
68 pub struct SpecNewSessionParameters {
69 pub alwaysMatch: Capabilities,
70 pub firstMatch: Vec<Capabilities>,
73 impl SpecNewSessionParameters {
74 fn validate<T: BrowserCapabilities>(&self,
75 mut capabilities: Capabilities,
76 browser_capabilities: &T) -> WebDriverResult<Capabilities> {
77 // Filter out entries with the value `null`
78 let null_entries = capabilities
80 .filter(|&(_, ref value)| **value == Json::Null)
81 .map(|(k, _)| k.clone())
82 .collect::<Vec<String>>();
83 for key in null_entries {
84 capabilities.remove(&key);
87 for (key, value) in capabilities.iter() {
89 "acceptInsecureCerts" => if !value.is_boolean() {
90 return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
91 "acceptInsecureCerts was not a boolean"))
94 x @ "browserVersion" |
95 x @ "platformName" => if !value.is_string() {
96 return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
97 format!("{} was not a boolean", x)))
99 "pageLoadStrategy" => {
100 try!(SpecNewSessionParameters::validate_page_load_strategy(value))
103 try!(SpecNewSessionParameters::validate_proxy(value))
106 try!(SpecNewSessionParameters::validate_timeouts(value))
108 "unhandledPromptBehavior" => {
109 try!(SpecNewSessionParameters::validate_unhandled_prompt_behaviour(value))
112 if !x.contains(":") {
113 return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
114 format!("{} was not a the name of a known capability or a valid extension capability", x)))
116 try!(browser_capabilities.validate_custom(x, value));
124 fn validate_page_load_strategy(value: &Json) -> WebDriverResult<()> {
126 &Json::String(ref x) => {
132 return Err(WebDriverError::new(
133 ErrorStatus::InvalidArgument,
134 format!("\"{}\" not a valid page load strategy", x)))
138 _ => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
139 "pageLoadStrategy was not a string"))
144 fn validate_proxy(proxy_value: &Json) -> WebDriverResult<()> {
145 let obj = try_opt!(proxy_value.as_object(),
146 ErrorStatus::InvalidArgument,
147 "proxy was not an object");
148 for (key, value) in obj.iter() {
150 "proxyType" => match value.as_string() {
155 Some("manual") => {},
156 Some(x) => return Err(WebDriverError::new(
157 ErrorStatus::InvalidArgument,
158 format!("{} was not a valid proxyType value", x))),
159 None => return Err(WebDriverError::new(
160 ErrorStatus::InvalidArgument,
161 "proxyType value was not a string")),
163 "proxyAutoconfigUrl" => match value.as_string() {
165 try!(Url::parse(x).or(Err(WebDriverError::new(
166 ErrorStatus::InvalidArgument,
167 "proxyAutoconfigUrl was not a valid url"))));
169 None => return Err(WebDriverError::new(
170 ErrorStatus::InvalidArgument,
171 "proxyAutoconfigUrl was not a string"
174 "ftpProxy" => try!(SpecNewSessionParameters::validate_host_domain("ftpProxy", "ftp", obj, value)),
175 "ftpProxyPort" => try!(SpecNewSessionParameters::validate_port("ftpProxyPort", value)),
176 "httpProxy" => try!(SpecNewSessionParameters::validate_host_domain("httpProxy", "http", obj, value)),
177 "httpProxyPort" => try!(SpecNewSessionParameters::validate_port("httpProxyPort", value)),
178 "sslProxy" => try!(SpecNewSessionParameters::validate_host_domain("sslProxy", "http", obj, value)),
179 "sslProxyPort" => try!(SpecNewSessionParameters::validate_port("sslProxyPort", value)),
180 "socksProxy" => try!(SpecNewSessionParameters::validate_host_domain("socksProxy", "ssh", obj, value)),
181 "socksProxyPort" => try!(SpecNewSessionParameters::validate_port("socksProxyPort", value)),
182 "socksUsername" => if !value.is_string() {
183 return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
184 "socksUsername was not a string"))
186 "socksPassword" => if !value.is_string() {
187 return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
188 "socksPassword was not a string"))
190 x => return Err(WebDriverError::new(
191 ErrorStatus::InvalidArgument,
192 format!("{} was not a valid proxy configuration capability", x)))
198 /// Validate whether a named capability is JSON value is a string containing a host
199 /// and possible port
200 fn validate_host_domain(name: &str,
203 value: &Json) -> WebDriverResult<()> {
204 match value.as_string() {
206 if x.contains("::/") {
207 return Err(WebDriverError::new(
208 ErrorStatus::InvalidArgument,
209 format!("{} contains a scheme", name)));
212 // IPv6 hosts must be enclosed with "[" and "]" in URLs
213 let host = match x.parse::<Ipv6Addr>() {
214 Ok(ip) => format!("[{}]", ip),
215 Err(_) => x.to_owned(),
218 let mut s = String::with_capacity(scheme.len() + host.len() + 3);
221 s.push_str(host.as_str());
223 let url = try!(Url::parse(&*s).or(Err(WebDriverError::new(
224 ErrorStatus::InvalidArgument,
225 format!("{} was not a valid url", name)))));
226 if url.username() != "" ||
227 url.password() != None ||
229 url.query() != None ||
230 url.fragment() != None {
231 return Err(WebDriverError::new(
232 ErrorStatus::InvalidArgument,
233 format!("{} was not of the form host[:port]", name)));
235 let mut port_key = String::with_capacity(name.len() + 4);
236 port_key.push_str(name);
237 port_key.push_str("Port");
238 if url.port() != None &&
239 obj.contains_key(&*port_key) {
240 return Err(WebDriverError::new(
241 ErrorStatus::InvalidArgument,
242 format!("{} supplied with a port as well as {}",
246 None => return Err(WebDriverError::new(
247 ErrorStatus::InvalidArgument,
248 format!("{} was not a string", name)
254 fn validate_port(name: &str, value: &Json) -> WebDriverResult<()> {
255 match value.as_i64() {
257 if x < 0 || x > 2i64.pow(16) - 1 {
258 return Err(WebDriverError::new(
259 ErrorStatus::InvalidArgument,
260 format!("{} is out of range", name)))
263 _ => return Err(WebDriverError::new(
264 ErrorStatus::InvalidArgument,
265 format!("{} was not an integer", name)))
270 fn validate_timeouts(value: &Json) -> WebDriverResult<()> {
271 let obj = try_opt!(value.as_object(),
272 ErrorStatus::InvalidArgument,
273 "timeouts capability was not an object");
274 for (key, value) in obj.iter() {
279 let timeout = try_opt!(value.as_i64(),
280 ErrorStatus::InvalidArgument,
281 format!("{} timeouts value was not an integer", x));
283 return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
284 format!("{} timeouts value was negative", x)))
287 x => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
288 format!("{} was not a valid timeouts capability", x)))
294 fn validate_unhandled_prompt_behaviour(value: &Json) -> WebDriverResult<()> {
295 let behaviour = try_opt!(value.as_string(),
296 ErrorStatus::InvalidArgument,
297 "unhandledPromptBehavior capability was not a string");
301 x => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
302 format!("{} was not a valid unhandledPromptBehavior value", x))) }
307 impl Parameters for SpecNewSessionParameters {
308 fn from_json(body: &Json) -> WebDriverResult<SpecNewSessionParameters> {
309 let data = try_opt!(body.as_object(),
310 ErrorStatus::UnknownError,
311 "Message body was not an object");
313 let capabilities = try_opt!(
314 try_opt!(data.get("capabilities"),
315 ErrorStatus::InvalidArgument,
316 "Missing 'capabilities' parameter").as_object(),
317 ErrorStatus::InvalidArgument,
318 "'capabilities' parameter is not an object");
320 let default_always_match = Json::Object(Capabilities::new());
321 let always_match = try_opt!(capabilities.get("alwaysMatch")
322 .unwrap_or(&default_always_match)
324 ErrorStatus::InvalidArgument,
325 "'alwaysMatch' parameter is not an object");
326 let default_first_matches = Json::Array(vec![]);
327 let first_matches = try!(
328 try_opt!(capabilities.get("firstMatch")
329 .unwrap_or(&default_first_matches)
331 ErrorStatus::InvalidArgument,
332 "'firstMatch' parameter is not an array")
334 .map(|x| x.as_object()
336 .ok_or(WebDriverError::new(ErrorStatus::InvalidArgument,
337 "'firstMatch' entry is not an object")))
338 .collect::<WebDriverResult<Vec<Capabilities>>>());
340 return Ok(SpecNewSessionParameters {
341 alwaysMatch: always_match.clone(),
342 firstMatch: first_matches
347 impl ToJson for SpecNewSessionParameters {
348 fn to_json(&self) -> Json {
349 let mut body = BTreeMap::new();
350 let mut capabilities = BTreeMap::new();
351 capabilities.insert("alwaysMatch".into(), self.alwaysMatch.to_json());
352 capabilities.insert("firstMatch".into(), self.firstMatch.to_json());
353 body.insert("capabilities".into(), capabilities.to_json());
358 impl CapabilitiesMatching for SpecNewSessionParameters {
359 fn match_browser<T: BrowserCapabilities>(&self, browser_capabilities: &mut T)
360 -> WebDriverResult<Option<Capabilities>> {
361 let default = vec![BTreeMap::new()];
362 let capabilities_list = if self.firstMatch.len() > 0 {
368 let merged_capabilities = try!(capabilities_list
370 .map(|first_match_entry| {
371 if first_match_entry.keys().any(|k| {
372 self.alwaysMatch.contains_key(k)
374 return Err(WebDriverError::new(
375 ErrorStatus::InvalidArgument,
376 "'firstMatch' key shadowed a value in 'alwaysMatch'"));
378 let mut merged = self.alwaysMatch.clone();
379 merged.append(&mut first_match_entry.clone());
382 .map(|merged| merged.and_then(|x| self.validate(x, browser_capabilities)))
383 .collect::<WebDriverResult<Vec<Capabilities>>>());
385 let selected = merged_capabilities
387 .filter_map(|merged| {
388 browser_capabilities.init(merged);
390 for (key, value) in merged.iter() {
393 let browserValue = browser_capabilities
394 .browser_name(merged)
398 if value.as_string() != browserValue.as_ref().map(|x| &**x) {
402 "browserVersion" => {
403 let browserValue = browser_capabilities
404 .browser_version(merged)
407 // We already validated this was a string
408 let version_cond = value.as_string().unwrap_or("");
409 if let Some(version) = browserValue {
410 if !browser_capabilities
411 .compare_browser_version(&*version, version_cond)
420 let browserValue = browser_capabilities
421 .platform_name(merged)
424 if value.as_string() != browserValue.as_ref().map(|x| &**x) {
428 "acceptInsecureCerts" => {
429 if value.as_boolean().unwrap_or(false) &&
430 !browser_capabilities
431 .accept_insecure_certs(merged)
437 let default = BTreeMap::new();
438 let proxy = value.as_object().unwrap_or(&default);
439 if !browser_capabilities.accept_proxy(&proxy,
446 if name.contains(":") {
447 if !browser_capabilities
448 .accept_custom(name, value, merged)
453 // Accept the capability
467 #[derive(Debug, PartialEq)]
468 pub struct LegacyNewSessionParameters {
469 pub desired: Capabilities,
470 pub required: Capabilities,
473 impl CapabilitiesMatching for LegacyNewSessionParameters {
474 fn match_browser<T: BrowserCapabilities>(&self, browser_capabilities: &mut T)
475 -> WebDriverResult<Option<Capabilities>> {
476 /* For now don't do anything much, just merge the
477 desired and required and return the merged list. */
479 let mut capabilities: Capabilities = BTreeMap::new();
481 .chain(self.desired.iter())
482 .fold(&mut capabilities,
483 |mut caps, (key, value)| {
484 if !caps.contains_key(key) {
485 caps.insert(key.clone(), value.clone());
488 browser_capabilities.init(&capabilities);
489 Ok(Some(capabilities))
493 impl Parameters for LegacyNewSessionParameters {
494 fn from_json(body: &Json) -> WebDriverResult<LegacyNewSessionParameters> {
495 let data = try_opt!(body.as_object(),
496 ErrorStatus::UnknownError,
497 "Message body was not an object");
499 let desired_capabilities =
500 if let Some(capabilities) = data.get("desiredCapabilities") {
501 try_opt!(capabilities.as_object(),
502 ErrorStatus::InvalidArgument,
503 "'desiredCapabilities' parameter is not an object").clone()
508 let required_capabilities =
509 if let Some(capabilities) = data.get("requiredCapabilities") {
510 try_opt!(capabilities.as_object(),
511 ErrorStatus::InvalidArgument,
512 "'requiredCapabilities' parameter is not an object").clone()
517 Ok(LegacyNewSessionParameters {
518 desired: desired_capabilities,
519 required: required_capabilities
524 impl ToJson for LegacyNewSessionParameters {
525 fn to_json(&self) -> Json {
526 let mut data = BTreeMap::new();
527 data.insert("desiredCapabilities".to_owned(), self.desired.to_json());
528 data.insert("requiredCapabilities".to_owned(), self.required.to_json());
535 use rustc_serialize::json::Json;
536 use std::collections::BTreeMap;
537 use super::{WebDriverResult, SpecNewSessionParameters};
539 fn parse(data: &str) -> BTreeMap<String, Json> {
540 Json::from_str(&*data).unwrap().as_object().unwrap().clone()
543 fn validate_host(name: &str, scheme: &str, caps: &str, value: &str) -> WebDriverResult<()> {
544 SpecNewSessionParameters::validate_host_domain(name,
547 &Json::String(value.into()))
551 fn test_validate_host_domain() {
552 validate_host("ftpProxy", "ftp", "{}", "example.org").unwrap();
553 validate_host("ftpProxy", "ftp", "{}", "::1").unwrap();
554 assert!(validate_host("ftpProxy", "ftp", "{}", "ftp://example.org").is_err());
555 assert!(validate_host("ftpProxy", "ftp", "{}", "example.org/foo").is_err());
556 assert!(validate_host("ftpProxy", "ftp", "{}", "example.org#bar").is_err());
557 assert!(validate_host("ftpProxy", "ftp", "{}", "example.org?bar=baz").is_err());
558 assert!(validate_host("ftpProxy", "ftp", "{}", "foo:bar@example.org").is_err());
559 assert!(validate_host("ftpProxy", "ftp", "{}", "foo@example.org").is_err());
560 validate_host("httpProxy", "http", "{}", "example.org:8000").unwrap();
561 validate_host("httpProxy", "http", "{}", "::1:8000").unwrap();
562 validate_host("httpProxy", "http", "{\"ftpProxyPort\": \"1234\"}", "example.org:8000").unwrap();
563 assert!(validate_host("httpProxy", "http", "{\"httpProxyPort\": \"1234\"}", "example.org:8000").is_err());
564 validate_host("sslProxy", "http", "{}", "example.org:8000").unwrap();
565 validate_host("sslProxy", "http", "{\"ftpProxyPort\": \"1234\"}", "example.org:8000").unwrap();
566 assert!(validate_host("sslProxy", "http", "{\"sslProxyPort\": \"1234\"}", "example.org:8000").is_err());