Bug 1370510 - Update WebDriver Crate dependency; r=jgraham
[gecko.git] / third_party / rust / webdriver / src / capabilities.rs
blobeb159417f8792a7ec683378b7b565825d3610459
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;
6 use url::Url;
8 pub type Capabilities = BTreeMap<String, Json>;
10 /// Trait for objects that can be used to inspect browser capabilities
11 ///
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
20     ///
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
29     ///
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
41     ///
42     /// Check that custom properties containing ":" have the correct data types.
43     /// Properties that are unrecognised must be ignored i.e. return without
44     /// error.
45     fn validate_custom(&self, name: &str, value: &Json) -> WebDriverResult<()>;
46     /// Check if custom properties are accepted capabilites
47     ///
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
54 ///
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
59     ///
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
62     /// capabilities.
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
79             .iter()
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);
85         }
87         for (key, value) in capabilities.iter() {
88             match &**key {
89                 "acceptInsecureCerts" => if !value.is_boolean() {
90                         return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
91                                                        "acceptInsecureCerts was not a boolean"))
92                     },
93                 x @ "browserName" |
94                 x @ "browserVersion" |
95                 x @ "platformName" => if !value.is_string() {
96                         return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
97                                                        format!("{} was not a boolean", x)))
98                     },
99                 "pageLoadStrategy" => {
100                     try!(SpecNewSessionParameters::validate_page_load_strategy(value))
101                 }
102                 "proxy" => {
103                     try!(SpecNewSessionParameters::validate_proxy(value))
104                 },
105                 "timeouts" => {
106                     try!(SpecNewSessionParameters::validate_timeouts(value))
107                 },
108                 "unhandledPromptBehavior" => {
109                     try!(SpecNewSessionParameters::validate_unhandled_prompt_behaviour(value))
110                 }
111                 x => {
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)))
115                     } else {
116                         try!(browser_capabilities.validate_custom(x, value));
117                     }
118                 }
119             }
120         }
121         Ok(capabilities)
122     }
124     fn validate_page_load_strategy(value: &Json) -> WebDriverResult<()> {
125         match value {
126             &Json::String(ref x) => {
127                 match &**x {
128                     "normal" |
129                     "eager" |
130                     "none" => {},
131                     x => {
132                         return Err(WebDriverError::new(
133                             ErrorStatus::InvalidArgument,
134                             format!("\"{}\" not a valid page load strategy", x)))
135                     }
136                 }
137             }
138             _ => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
139                                                 "pageLoadStrategy was not a string"))
140         }
141         Ok(())
142     }
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() {
149             match &**key {
150                 "proxyType" => match value.as_string() {
151                     Some("pac") |
152                     Some("noproxy") |
153                     Some("autodetect") |
154                     Some("system") |
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")),
162                 },
163                 "proxyAutoconfigUrl" => match value.as_string() {
164                     Some(x) => {
165                         try!(Url::parse(x).or(Err(WebDriverError::new(
166                             ErrorStatus::InvalidArgument,
167                             "proxyAutoconfigUrl was not a valid url"))));
168                     },
169                     None => return Err(WebDriverError::new(
170                         ErrorStatus::InvalidArgument,
171                         "proxyAutoconfigUrl was not a string"
172                     ))
173                 },
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"))
185                 },
186                 "socksPassword" => if !value.is_string() {
187                     return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
188                                                    "socksPassword was not a string"))
189                 },
190                 x => return Err(WebDriverError::new(
191                     ErrorStatus::InvalidArgument,
192                     format!("{} was not a valid proxy configuration capability", x)))
193             }
194         }
195         Ok(())
196     }
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,
201                             scheme: &str,
202                             obj: &Capabilities,
203                             value: &Json) -> WebDriverResult<()> {
204         match value.as_string() {
205             Some(x) => {
206                 if x.contains("::/") {
207                     return Err(WebDriverError::new(
208                         ErrorStatus::InvalidArgument,
209                         format!("{} contains a scheme", name)));
210                 }
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(),
216                 };
218                 let mut s = String::with_capacity(scheme.len() + host.len() + 3);
219                 s.push_str(scheme);
220                 s.push_str("://");
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 ||
228                     url.path() != "/" ||
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)));
234                     }
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 {}",
243                                             name, port_key)));
244                     }
245             },
246             None => return Err(WebDriverError::new(
247                 ErrorStatus::InvalidArgument,
248                 format!("{} was not a string", name)
249             ))
250         }
251         Ok(())
252     }
254     fn validate_port(name: &str, value: &Json) -> WebDriverResult<()> {
255         match value.as_i64() {
256             Some(x) => {
257                 if x < 0 || x > 2i64.pow(16) - 1 {
258                     return Err(WebDriverError::new(
259                         ErrorStatus::InvalidArgument,
260                         format!("{} is out of range", name)))
261                 }
262             }
263             _ => return Err(WebDriverError::new(
264                 ErrorStatus::InvalidArgument,
265                 format!("{} was not an integer", name)))
266         }
267         Ok(())
268     }
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() {
275             match &**key {
276                 x @ "script" |
277                 x @ "pageLoad" |
278                 x @ "implicit" => {
279                     let timeout = try_opt!(value.as_i64(),
280                                            ErrorStatus::InvalidArgument,
281                                            format!("{} timeouts value was not an integer", x));
282                     if timeout < 0 {
283                         return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
284                                                        format!("{} timeouts value was negative", x)))
285                     }
286                 },
287                 x => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
288                                                     format!("{} was not a valid timeouts capability", x)))
289             }
290         }
291         Ok(())
292     }
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");
298         match behaviour {
299             "dismiss" |
300             "accept" => {},
301             x => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
302                                                 format!("{} was not a valid unhandledPromptBehavior value", x)))        }
303         Ok(())
304     }
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)
323                                    .as_object(),
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)
330                      .as_array(),
331                      ErrorStatus::InvalidArgument,
332                      "'firstMatch' parameter is not an array")
333                 .iter()
334                 .map(|x| x.as_object()
335                      .map(|x| x.clone())
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
343         });
344     }
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());
354         Json::Object(body)
355     }
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 {
363             &self.firstMatch
364         } else {
365             &default
366         };
368         let merged_capabilities = try!(capabilities_list
369             .iter()
370             .map(|first_match_entry| {
371                 if first_match_entry.keys().any(|k| {
372                     self.alwaysMatch.contains_key(k)
373                 }) {
374                     return Err(WebDriverError::new(
375                         ErrorStatus::InvalidArgument,
376                         "'firstMatch' key shadowed a value in 'alwaysMatch'"));
377                 }
378                 let mut merged = self.alwaysMatch.clone();
379                 merged.append(&mut first_match_entry.clone());
380                 Ok(merged)
381             })
382             .map(|merged| merged.and_then(|x| self.validate(x, browser_capabilities)))
383             .collect::<WebDriverResult<Vec<Capabilities>>>());
385         let selected = merged_capabilities
386             .iter()
387             .filter_map(|merged| {
388                 browser_capabilities.init(merged);
390                 for (key, value) in merged.iter() {
391                     match &**key {
392                         "browserName" => {
393                             let browserValue = browser_capabilities
394                                 .browser_name(merged)
395                                 .ok()
396                                 .and_then(|x| x);
398                             if value.as_string() != browserValue.as_ref().map(|x| &**x) {
399                                     return None;
400                             }
401                         },
402                         "browserVersion" => {
403                             let browserValue = browser_capabilities
404                                 .browser_version(merged)
405                                 .ok()
406                                 .and_then(|x| x);
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)
412                                     .unwrap_or(false) {
413                                         return None;
414                                     }
415                             } else {
416                                 return None
417                             }
418                         },
419                         "platformName" => {
420                             let browserValue = browser_capabilities
421                                 .platform_name(merged)
422                                 .ok()
423                                 .and_then(|x| x);
424                             if value.as_string() != browserValue.as_ref().map(|x| &**x) {
425                                 return None;
426                             }
427                         }
428                         "acceptInsecureCerts" => {
429                             if value.as_boolean().unwrap_or(false) &&
430                                 !browser_capabilities
431                                 .accept_insecure_certs(merged)
432                                 .unwrap_or(false) {
433                                 return None;
434                             }
435                         },
436                         "proxy" => {
437                             let default = BTreeMap::new();
438                             let proxy = value.as_object().unwrap_or(&default);
439                             if !browser_capabilities.accept_proxy(&proxy,
440                                                                   merged)
441                                 .unwrap_or(false) {
442                                 return None
443                             }
444                         },
445                         name => {
446                             if name.contains(":") {
447                                 if !browser_capabilities
448                                     .accept_custom(name, value, merged)
449                                     .unwrap_or(false) {
450                                         return None
451                                     }
452                             } else {
453                                 // Accept the capability
454                             }
455                         }
456                     }
457                 }
459                 return Some(merged)
460             })
461             .next()
462             .map(|x| x.clone());
463             Ok(selected)
464     }
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();
480         self.required.iter()
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());
486                       }
487                       caps});
488         browser_capabilities.init(&capabilities);
489         Ok(Some(capabilities))
490     }
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()
504             } else {
505                 BTreeMap::new()
506             };
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()
513             } else {
514                 BTreeMap::new()
515             };
517         Ok(LegacyNewSessionParameters {
518             desired: desired_capabilities,
519             required: required_capabilities
520         })
521     }
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());
529         Json::Object(data)
530     }
533 #[cfg(test)]
534 mod tests {
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()
541     }
543     fn validate_host(name: &str, scheme: &str, caps: &str, value: &str) -> WebDriverResult<()> {
544         SpecNewSessionParameters::validate_host_domain(name,
545                                                        scheme,
546                                                        &parse(caps),
547                                                        &Json::String(value.into()))
548     }
550     #[test]
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());
567     }