use command::Parameters;
use error::{ErrorStatus, WebDriverError, WebDriverResult};
use rustc_serialize::json::{Json, ToJson};
use std::collections::BTreeMap;
use url::Url;
pub type Capabilities = BTreeMap<String, Json>;
pub trait BrowserCapabilities {
fn init(&mut self, &Capabilities);
fn browser_name(&mut self, &Capabilities) -> WebDriverResult<Option<String>>;
fn browser_version(&mut self, &Capabilities) -> WebDriverResult<Option<String>>;
fn compare_browser_version(&mut self, version: &str, comparison: &str) -> WebDriverResult<bool>;
fn platform_name(&mut self, &Capabilities) -> WebDriverResult<Option<String>>;
fn accept_insecure_certs(&mut self, &Capabilities) -> WebDriverResult<bool>;
fn accept_proxy(&mut self, proxy_settings: &BTreeMap<String, Json>, &Capabilities) -> WebDriverResult<bool>;
fn validate_custom(&self, name: &str, value: &Json) -> WebDriverResult<()>;
fn accept_custom(&mut self, name: &str, value: &Json, merged: &Capabilities) -> WebDriverResult<bool>;
}
pub trait CapabilitiesMatching {
fn match_browser<T: BrowserCapabilities>(&self, browser_capabilities: &mut T)
-> WebDriverResult<Option<Capabilities>>;
}
#[derive(Debug, 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> {
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))
},
"unhandledPromptBehavior" => {
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("direct") |
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(value)),
"httpProxy" => try!(SpecNewSessionParameters::validate_host(value)),
"noProxy" => try!(SpecNewSessionParameters::validate_no_proxy(value)),
"sslProxy" => try!(SpecNewSessionParameters::validate_host(value)),
"socksProxy" => try!(SpecNewSessionParameters::validate_host(value)),
"socksVersion" => if !value.is_number() {
return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
"socksVersion was not a number"))
},
"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(())
}
fn validate_no_proxy(value: &Json) -> WebDriverResult<()> {
match value.as_array() {
Some(hosts) => {
for host in hosts {
match host.as_string() {
Some(_) => {},
None => return Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
format!("{} was not a string", host)
))
}
}
},
None => return Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
format!("{} was not an array", value)
))
}
Ok(())
}
fn validate_host(value: &Json) -> WebDriverResult<()> {
match value.as_string() {
Some(host) => {
if host.contains("://") {
return Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
format!("{} contains a scheme", host)));
}
let s = String::from(format!("http://{}", host));
let url = try!(Url::parse(s.as_str()).or(Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
format!("{} is not a valid host", host)))));
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]", host)));
}
},
None => return Err(WebDriverError::new(
ErrorStatus::InvalidArgument,
format!("{} was not a string", value)
))
}
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,
"unhandledPromptBehavior capability was not a string");
match behaviour {
"dismiss" |
"accept" => {},
x => return Err(WebDriverError::new(ErrorStatus::InvalidArgument,
format!("{} was not a valid unhandledPromptBehavior 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);
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 {
}
}
}
}
return Some(merged)
})
.next()
.map(|x| x.clone());
Ok(selected)
}
}
#[derive(Debug, 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>> {
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 super::{WebDriverResult, SpecNewSessionParameters};
fn validate_proxy(value: &str) -> WebDriverResult<()> {
let data = Json::from_str(value).unwrap();
SpecNewSessionParameters::validate_proxy(&data)
}
#[test]
fn test_validate_proxy() {
validate_proxy("{\"httpProxy\": \"127.0.0.1\"}").unwrap();
validate_proxy("{\"httpProxy\": \"127.0.0.1:\"}").unwrap();
validate_proxy("{\"httpProxy\": \"127.0.0.1:3128\"}").unwrap();
validate_proxy("{\"httpProxy\": \"localhost\"}").unwrap();
validate_proxy("{\"httpProxy\": \"localhost:3128\"}").unwrap();
validate_proxy("{\"httpProxy\": \"[2001:db8::1]\"}").unwrap();
validate_proxy("{\"httpProxy\": \"[2001:db8::1]:3128\"}").unwrap();
validate_proxy("{\"httpProxy\": \"example.org\"}").unwrap();
validate_proxy("{\"httpProxy\": \"example.org:3128\"}").unwrap();
assert!(validate_proxy("{\"httpProxy\": \"http://example.org\"}").is_err());
assert!(validate_proxy("{\"httpProxy\": \"example.org:-1\"}").is_err());
assert!(validate_proxy("{\"httpProxy\": \"2001:db8::1\"}").is_err());
validate_proxy("{\"noProxy\": [\"foo\"]}").unwrap();
assert!(validate_proxy("{\"noProxy\": \"foo\"}").is_err());
assert!(validate_proxy("{\"noProxy\": [42]}").is_err());
}
}