summaryrefslogtreecommitdiffstats
path: root/testing/geckodriver/src/capabilities.rs
diff options
context:
space:
mode:
Diffstat (limited to '')
-rw-r--r--testing/geckodriver/src/capabilities.rs1417
1 files changed, 1417 insertions, 0 deletions
diff --git a/testing/geckodriver/src/capabilities.rs b/testing/geckodriver/src/capabilities.rs
new file mode 100644
index 0000000000..eda75ad62b
--- /dev/null
+++ b/testing/geckodriver/src/capabilities.rs
@@ -0,0 +1,1417 @@
+/* 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 crate::command::LogOptions;
+use crate::logging::Level;
+use crate::marionette::MarionetteSettings;
+use base64::prelude::BASE64_STANDARD;
+use base64::Engine;
+use mozdevice::AndroidStorageInput;
+use mozprofile::preferences::Pref;
+use mozprofile::profile::Profile;
+use mozrunner::firefox_args::{get_arg_value, parse_args, Arg};
+use mozrunner::runner::platform::firefox_default_path;
+use mozversion::{self, firefox_binary_version, firefox_version, Version};
+use regex::bytes::Regex;
+use serde_json::{Map, Value};
+use std::collections::BTreeMap;
+use std::default::Default;
+use std::ffi::OsString;
+use std::fmt::{self, Display};
+use std::fs;
+use std::io;
+use std::io::BufWriter;
+use std::io::Cursor;
+use std::path::{Path, PathBuf};
+use std::str::{self, FromStr};
+use webdriver::capabilities::{BrowserCapabilities, Capabilities};
+use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult};
+
+#[derive(Clone, Debug)]
+enum VersionError {
+ VersionError(mozversion::Error),
+ MissingBinary,
+}
+
+impl From<mozversion::Error> for VersionError {
+ fn from(err: mozversion::Error) -> VersionError {
+ VersionError::VersionError(err)
+ }
+}
+
+impl Display for VersionError {
+ fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
+ match *self {
+ VersionError::VersionError(ref x) => x.fmt(f),
+ VersionError::MissingBinary => "No binary provided".fmt(f),
+ }
+ }
+}
+
+impl From<VersionError> for WebDriverError {
+ fn from(err: VersionError) -> WebDriverError {
+ WebDriverError::new(ErrorStatus::SessionNotCreated, err.to_string())
+ }
+}
+
+/// Provides matching of `moz:firefoxOptions` and resolutionnized of which Firefox
+/// binary to use.
+///
+/// `FirefoxCapabilities` is constructed with the fallback binary, should
+/// `moz:firefoxOptions` not contain a binary entry. This may either be the
+/// system Firefox installation or an override, for example given to the
+/// `--binary` flag of geckodriver.
+pub struct FirefoxCapabilities<'a> {
+ pub chosen_binary: Option<PathBuf>,
+ fallback_binary: Option<&'a PathBuf>,
+ version_cache: BTreeMap<PathBuf, Result<Version, VersionError>>,
+}
+
+impl<'a> FirefoxCapabilities<'a> {
+ pub fn new(fallback_binary: Option<&'a PathBuf>) -> FirefoxCapabilities<'a> {
+ FirefoxCapabilities {
+ chosen_binary: None,
+ fallback_binary,
+ version_cache: BTreeMap::new(),
+ }
+ }
+
+ fn set_binary(&mut self, capabilities: &Map<String, Value>) {
+ self.chosen_binary = capabilities
+ .get("moz:firefoxOptions")
+ .and_then(|x| x.get("binary"))
+ .and_then(|x| x.as_str())
+ .map(PathBuf::from)
+ .or_else(|| self.fallback_binary.cloned())
+ .or_else(firefox_default_path);
+ }
+
+ fn version(&mut self, binary: Option<&Path>) -> Result<Version, VersionError> {
+ if let Some(binary) = binary {
+ if let Some(cache_value) = self.version_cache.get(binary) {
+ return cache_value.clone();
+ }
+ let rv = self
+ .version_from_ini(binary)
+ .or_else(|_| self.version_from_binary(binary));
+ if let Ok(ref version) = rv {
+ debug!("Found version {}", version);
+ } else {
+ debug!("Failed to get binary version");
+ }
+ self.version_cache.insert(binary.to_path_buf(), rv.clone());
+ rv
+ } else {
+ Err(VersionError::MissingBinary)
+ }
+ }
+
+ fn version_from_ini(&self, binary: &Path) -> Result<Version, VersionError> {
+ debug!("Trying to read firefox version from ini files");
+ let version = firefox_version(binary)?;
+ if let Some(version_string) = version.version_string {
+ Version::from_str(&version_string).map_err(|err| err.into())
+ } else {
+ Err(VersionError::VersionError(
+ mozversion::Error::MetadataError("Missing version string".into()),
+ ))
+ }
+ }
+
+ fn version_from_binary(&self, binary: &Path) -> Result<Version, VersionError> {
+ debug!("Trying to read firefox version from binary");
+ Ok(firefox_binary_version(binary)?)
+ }
+}
+
+impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> {
+ fn init(&mut self, capabilities: &Capabilities) {
+ self.set_binary(capabilities);
+ }
+
+ fn browser_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
+ Ok(Some("firefox".into()))
+ }
+
+ fn browser_version(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
+ let binary = self.chosen_binary.clone();
+ self.version(binary.as_ref().map(|x| x.as_ref()))
+ .map_err(|err| err.into())
+ .map(|x| Some(x.to_string()))
+ }
+
+ fn platform_name(&mut self, _: &Capabilities) -> WebDriverResult<Option<String>> {
+ Ok(if cfg!(target_os = "windows") {
+ Some("windows".into())
+ } else if cfg!(target_os = "macos") {
+ Some("mac".into())
+ } else if cfg!(target_os = "linux") {
+ Some("linux".into())
+ } else {
+ None
+ })
+ }
+
+ fn accept_insecure_certs(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn accept_proxy(&mut self, _: &Capabilities, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn set_window_rect(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn compare_browser_version(
+ &mut self,
+ version: &str,
+ comparison: &str,
+ ) -> WebDriverResult<bool> {
+ Version::from_str(version)
+ .map_err(VersionError::from)?
+ .matches(comparison)
+ .map_err(|err| VersionError::from(err).into())
+ }
+
+ fn strict_file_interactability(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn web_socket_url(&mut self, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+
+ fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()> {
+ if !name.starts_with("moz:") {
+ return Ok(());
+ }
+ match name {
+ "moz:firefoxOptions" => {
+ let data = try_opt!(
+ value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "moz:firefoxOptions is not an object"
+ );
+ for (key, value) in data.iter() {
+ match &**key {
+ "androidActivity"
+ | "androidDeviceSerial"
+ | "androidPackage"
+ | "profile" => {
+ if !value.is_string() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} is not a string", &**key),
+ ));
+ }
+ }
+ "androidIntentArguments" | "args" => {
+ if !try_opt!(
+ value.as_array(),
+ ErrorStatus::InvalidArgument,
+ format!("{} is not an array", &**key)
+ )
+ .iter()
+ .all(|value| value.is_string())
+ {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} entry is not a string", &**key),
+ ));
+ }
+ }
+ "binary" => {
+ if let Some(binary) = value.as_str() {
+ if !data.contains_key("androidPackage")
+ && self.version(Some(Path::new(binary))).is_err()
+ {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} is not a Firefox executable", &**key),
+ ));
+ }
+ } else {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("{} is not a string", &**key),
+ ));
+ }
+ }
+ "env" => {
+ let env_data = try_opt!(
+ value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "env value is not an object"
+ );
+ if !env_data.values().all(Value::is_string) {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Environment values were not all strings",
+ ));
+ }
+ }
+ "log" => {
+ let log_data = try_opt!(
+ value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "log value is not an object"
+ );
+ for (log_key, log_value) in log_data.iter() {
+ match &**log_key {
+ "level" => {
+ let level = try_opt!(
+ log_value.as_str(),
+ ErrorStatus::InvalidArgument,
+ "log level is not a string"
+ );
+ if Level::from_str(level).is_err() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Not a valid log level: {}", level),
+ ));
+ }
+ }
+ x => {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Invalid log field {}", x),
+ ))
+ }
+ }
+ }
+ }
+ "prefs" => {
+ let prefs_data = try_opt!(
+ value.as_object(),
+ ErrorStatus::InvalidArgument,
+ "prefs value is not an object"
+ );
+ let is_pref_value_type = |x: &Value| {
+ x.is_string() || x.is_i64() || x.is_u64() || x.is_boolean()
+ };
+ if !prefs_data.values().all(is_pref_value_type) {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Preference values not all string or integer or boolean",
+ ));
+ }
+ }
+ x => {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Invalid moz:firefoxOptions field {}", x),
+ ))
+ }
+ }
+ }
+ }
+ "moz:useNonSpecCompliantPointerOrigin" => {
+ warn!("You are using the deprecated vendor specific capability 'moz:useNonSpecCompliantPointerOrigin', which will be removed in Firefox 116.");
+ if !value.is_boolean() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "moz:useNonSpecCompliantPointerOrigin is not a boolean",
+ ));
+ }
+ }
+ "moz:webdriverClick" => {
+ if !value.is_boolean() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "moz:webdriverClick is not a boolean",
+ ));
+ }
+ }
+ "moz:debuggerAddress" => {
+ if !value.is_boolean() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "moz:debuggerAddress is not a boolean",
+ ));
+ }
+ }
+ _ => {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Unrecognised option {}", name),
+ ))
+ }
+ }
+ Ok(())
+ }
+
+ fn accept_custom(&mut self, _: &str, _: &Value, _: &Capabilities) -> WebDriverResult<bool> {
+ Ok(true)
+ }
+}
+
+/// Android-specific options in the `moz:firefoxOptions` struct.
+/// These map to "androidCamelCase", following [chromedriver's Android-specific
+/// Capabilities](http://chromedriver.chromium.org/getting-started/getting-started---android).
+#[derive(Default, Clone, Debug, PartialEq)]
+pub struct AndroidOptions {
+ pub activity: Option<String>,
+ pub device_serial: Option<String>,
+ pub intent_arguments: Option<Vec<String>>,
+ pub package: String,
+ pub storage: AndroidStorageInput,
+}
+
+impl AndroidOptions {
+ pub fn new(package: String, storage: AndroidStorageInput) -> AndroidOptions {
+ AndroidOptions {
+ package,
+ storage,
+ ..Default::default()
+ }
+ }
+}
+
+#[derive(Debug, PartialEq)]
+pub enum ProfileType {
+ Path(Profile),
+ Named,
+ Temporary,
+}
+
+impl Default for ProfileType {
+ fn default() -> Self {
+ ProfileType::Temporary
+ }
+}
+
+/// Rust representation of `moz:firefoxOptions`.
+///
+/// Calling `FirefoxOptions::from_capabilities(binary, capabilities)` causes
+/// the encoded profile, the binary arguments, log settings, and additional
+/// preferences to be checked and unmarshaled from the `moz:firefoxOptions`
+/// JSON Object into a Rust representation.
+#[derive(Default, Debug)]
+pub struct FirefoxOptions {
+ pub binary: Option<PathBuf>,
+ pub profile: ProfileType,
+ pub args: Option<Vec<String>>,
+ pub env: Option<Vec<(String, String)>>,
+ pub log: LogOptions,
+ pub prefs: Vec<(String, Pref)>,
+ pub android: Option<AndroidOptions>,
+ pub use_websocket: bool,
+}
+
+impl FirefoxOptions {
+ pub fn new() -> FirefoxOptions {
+ Default::default()
+ }
+
+ pub(crate) fn from_capabilities(
+ binary_path: Option<PathBuf>,
+ settings: &MarionetteSettings,
+ matched: &mut Capabilities,
+ ) -> WebDriverResult<FirefoxOptions> {
+ let mut rv = FirefoxOptions::new();
+ rv.binary = binary_path;
+
+ if let Some(json) = matched.remove("moz:firefoxOptions") {
+ let options = json.as_object().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "'moz:firefoxOptions' \
+ capability is not an object",
+ )
+ })?;
+
+ if options.get("androidPackage").is_some() && options.get("binary").is_some() {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidPackage and binary are mutual exclusive",
+ ));
+ }
+
+ rv.android = FirefoxOptions::load_android(settings.android_storage, options)?;
+ rv.args = FirefoxOptions::load_args(options)?;
+ rv.env = FirefoxOptions::load_env(options)?;
+ rv.log = FirefoxOptions::load_log(options)?;
+ rv.prefs = FirefoxOptions::load_prefs(options)?;
+ if let Some(profile) =
+ FirefoxOptions::load_profile(settings.profile_root.as_deref(), options)?
+ {
+ rv.profile = ProfileType::Path(profile);
+ }
+ }
+
+ if let Some(args) = rv.args.as_ref() {
+ let os_args = parse_args(args.iter().map(OsString::from).collect::<Vec<_>>().iter());
+
+ if let Some(path) = get_arg_value(os_args.iter(), Arg::Profile) {
+ if let ProfileType::Path(_) = rv.profile {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Can't provide both a --profile argument and a profile",
+ ));
+ }
+ let path_buf = PathBuf::from(path);
+ rv.profile = ProfileType::Path(Profile::new_from_path(&path_buf)?);
+ }
+
+ if get_arg_value(os_args.iter(), Arg::NamedProfile).is_some() {
+ if let ProfileType::Path(_) = rv.profile {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Can't provide both a -P argument and a profile",
+ ));
+ }
+ // See bug 1757720
+ warn!("Firefox was configured to use a named profile (`-P <name>`). \
+ Support for named profiles will be removed in a future geckodriver release. \
+ Please instead use the `--profile <path>` Firefox argument to start with an existing profile");
+ rv.profile = ProfileType::Named;
+ }
+
+ // Block these Firefox command line arguments that should not be settable
+ // via session capabilities.
+ if let Some(arg) = os_args
+ .iter()
+ .filter_map(|(opt_arg, _)| opt_arg.as_ref())
+ .find(|arg| {
+ matches!(
+ arg,
+ Arg::Marionette
+ | Arg::RemoteAllowHosts
+ | Arg::RemoteAllowOrigins
+ | Arg::RemoteDebuggingPort
+ )
+ })
+ {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ format!("Argument {} can't be set via capabilities", arg),
+ ));
+ };
+ }
+
+ let has_web_socket_url = matched
+ .get("webSocketUrl")
+ .and_then(|x| x.as_bool())
+ .unwrap_or(false);
+
+ let has_debugger_address = matched
+ .remove("moz:debuggerAddress")
+ .and_then(|x| x.as_bool())
+ .unwrap_or(false);
+
+ // Set a command line provided port for the Remote Agent for now.
+ // It needs to be the same on the host and the Android device.
+ if has_web_socket_url || has_debugger_address {
+ rv.use_websocket = true;
+
+ // Bug 1722863: Setting of command line arguments would be
+ // better suited in the individual Browser implementations.
+ let mut remote_args = Vec::new();
+ remote_args.push("--remote-debugging-port".to_owned());
+ remote_args.push(settings.websocket_port.to_string());
+
+ // Handle additional hosts for WebDriver BiDi WebSocket connections
+ if !settings.allow_hosts.is_empty() {
+ remote_args.push("--remote-allow-hosts".to_owned());
+ remote_args.push(
+ settings
+ .allow_hosts
+ .iter()
+ .map(|host| host.to_string())
+ .collect::<Vec<String>>()
+ .join(","),
+ );
+ }
+
+ // Handle additional origins for WebDriver BiDi WebSocket connections
+ if !settings.allow_origins.is_empty() {
+ remote_args.push("--remote-allow-origins".to_owned());
+ remote_args.push(
+ settings
+ .allow_origins
+ .iter()
+ .map(|origin| origin.to_string())
+ .collect::<Vec<String>>()
+ .join(","),
+ );
+ }
+
+ if let Some(ref mut args) = rv.args {
+ args.append(&mut remote_args);
+ } else {
+ rv.args = Some(remote_args);
+ }
+ }
+
+ Ok(rv)
+ }
+
+ fn load_profile(
+ profile_root: Option<&Path>,
+ options: &Capabilities,
+ ) -> WebDriverResult<Option<Profile>> {
+ if let Some(profile_json) = options.get("profile") {
+ let profile_base64 = profile_json.as_str().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Profile is not a string")
+ })?;
+ let profile_zip = &*BASE64_STANDARD.decode(profile_base64)?;
+
+ // Create an emtpy profile directory
+ let profile = Profile::new(profile_root)?;
+ unzip_buffer(
+ profile_zip,
+ profile
+ .temp_dir
+ .as_ref()
+ .expect("Profile doesn't have a path")
+ .path(),
+ )?;
+
+ Ok(Some(profile))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn load_args(options: &Capabilities) -> WebDriverResult<Option<Vec<String>>> {
+ if let Some(args_json) = options.get("args") {
+ let args_array = args_json.as_array().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Arguments were not an array")
+ })?;
+ let args = args_array
+ .iter()
+ .map(|x| x.as_str().map(|x| x.to_owned()))
+ .collect::<Option<Vec<String>>>()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Arguments entries were not all strings",
+ )
+ })?;
+
+ Ok(Some(args))
+ } else {
+ Ok(None)
+ }
+ }
+
+ pub fn load_env(options: &Capabilities) -> WebDriverResult<Option<Vec<(String, String)>>> {
+ if let Some(env_data) = options.get("env") {
+ let env = env_data.as_object().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Env was not an object")
+ })?;
+ let mut rv = Vec::with_capacity(env.len());
+ for (key, value) in env.iter() {
+ rv.push((
+ key.clone(),
+ value
+ .as_str()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Env value is not a string",
+ )
+ })?
+ .to_string(),
+ ));
+ }
+ Ok(Some(rv))
+ } else {
+ Ok(None)
+ }
+ }
+
+ fn load_log(options: &Capabilities) -> WebDriverResult<LogOptions> {
+ if let Some(json) = options.get("log") {
+ let log = json.as_object().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Log section is not an object")
+ })?;
+
+ let level = match log.get("level") {
+ Some(json) => {
+ let s = json.as_str().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Log level is not a string",
+ )
+ })?;
+ Some(Level::from_str(s).ok().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Log level is unknown")
+ })?)
+ }
+ None => None,
+ };
+
+ Ok(LogOptions { level })
+ } else {
+ Ok(Default::default())
+ }
+ }
+
+ pub fn load_prefs(options: &Capabilities) -> WebDriverResult<Vec<(String, Pref)>> {
+ if let Some(prefs_data) = options.get("prefs") {
+ let prefs = prefs_data.as_object().ok_or_else(|| {
+ WebDriverError::new(ErrorStatus::InvalidArgument, "Prefs were not an object")
+ })?;
+ let mut rv = Vec::with_capacity(prefs.len());
+ for (key, value) in prefs.iter() {
+ rv.push((key.clone(), pref_from_json(value)?));
+ }
+ Ok(rv)
+ } else {
+ Ok(vec![])
+ }
+ }
+
+ pub fn load_android(
+ storage: AndroidStorageInput,
+ options: &Capabilities,
+ ) -> WebDriverResult<Option<AndroidOptions>> {
+ if let Some(package_json) = options.get("androidPackage") {
+ let package = package_json
+ .as_str()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidPackage is not a string",
+ )
+ })?
+ .to_owned();
+
+ // https://developer.android.com/studio/build/application-id
+ let package_regexp =
+ Regex::new(r#"^([a-zA-Z][a-zA-Z0-9_]*\.){1,}([a-zA-Z][a-zA-Z0-9_]*)$"#).unwrap();
+ if !package_regexp.is_match(package.as_bytes()) {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "Not a valid androidPackage name",
+ ));
+ }
+
+ let mut android = AndroidOptions::new(package.clone(), storage);
+
+ android.activity = match options.get("androidActivity") {
+ Some(json) => {
+ let activity = json
+ .as_str()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidActivity is not a string",
+ )
+ })?
+ .to_owned();
+
+ if activity.contains('/') {
+ return Err(WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidActivity should not contain '/",
+ ));
+ }
+
+ Some(activity)
+ }
+ None => {
+ match package.as_str() {
+ "org.mozilla.firefox"
+ | "org.mozilla.firefox_beta"
+ | "org.mozilla.fenix"
+ | "org.mozilla.fenix.debug"
+ | "org.mozilla.reference.browser" => {
+ Some("org.mozilla.fenix.IntentReceiverActivity".to_string())
+ }
+ "org.mozilla.focus"
+ | "org.mozilla.focus.debug"
+ | "org.mozilla.klar"
+ | "org.mozilla.klar.debug" => {
+ Some("org.mozilla.focus.activity.IntentReceiverActivity".to_string())
+ }
+ // For all other applications fallback to auto-detection.
+ _ => None,
+ }
+ }
+ };
+
+ android.device_serial = match options.get("androidDeviceSerial") {
+ Some(json) => Some(
+ json.as_str()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidDeviceSerial is not a string",
+ )
+ })?
+ .to_owned(),
+ ),
+ None => None,
+ };
+
+ android.intent_arguments = match options.get("androidIntentArguments") {
+ Some(json) => {
+ let args_array = json.as_array().ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidIntentArguments is not an array",
+ )
+ })?;
+ let args = args_array
+ .iter()
+ .map(|x| x.as_str().map(|x| x.to_owned()))
+ .collect::<Option<Vec<String>>>()
+ .ok_or_else(|| {
+ WebDriverError::new(
+ ErrorStatus::InvalidArgument,
+ "androidIntentArguments entries are not all strings",
+ )
+ })?;
+
+ Some(args)
+ }
+ None => {
+ // All GeckoView based applications support this view,
+ // and allow to open a blank page in a Gecko window.
+ Some(vec![
+ "-a".to_string(),
+ "android.intent.action.VIEW".to_string(),
+ "-d".to_string(),
+ "about:blank".to_string(),
+ ])
+ }
+ };
+
+ Ok(Some(android))
+ } else {
+ Ok(None)
+ }
+ }
+}
+
+fn pref_from_json(value: &Value) -> WebDriverResult<Pref> {
+ match *value {
+ Value::String(ref x) => Ok(Pref::new(x.clone())),
+ Value::Number(ref x) => Ok(Pref::new(x.as_i64().unwrap())),
+ Value::Bool(x) => Ok(Pref::new(x)),
+ _ => Err(WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Could not convert pref value to string, boolean, or integer",
+ )),
+ }
+}
+
+fn unzip_buffer(buf: &[u8], dest_dir: &Path) -> WebDriverResult<()> {
+ let reader = Cursor::new(buf);
+ let mut zip = zip::ZipArchive::new(reader)
+ .map_err(|_| WebDriverError::new(ErrorStatus::UnknownError, "Failed to unzip profile"))?;
+
+ for i in 0..zip.len() {
+ let mut file = zip.by_index(i).map_err(|_| {
+ WebDriverError::new(
+ ErrorStatus::UnknownError,
+ "Processing profile zip file failed",
+ )
+ })?;
+ let unzip_path = {
+ let name = file.name();
+ let is_dir = name.ends_with('/');
+ let rel_path = Path::new(name);
+ let dest_path = dest_dir.join(rel_path);
+
+ {
+ let create_dir = if is_dir {
+ Some(dest_path.as_path())
+ } else {
+ dest_path.parent()
+ };
+ if let Some(dir) = create_dir {
+ if !dir.exists() {
+ debug!("Creating profile directory tree {}", dir.to_string_lossy());
+ fs::create_dir_all(dir)?;
+ }
+ }
+ }
+
+ if is_dir {
+ None
+ } else {
+ Some(dest_path)
+ }
+ };
+
+ if let Some(unzip_path) = unzip_path {
+ debug!("Extracting profile to {}", unzip_path.to_string_lossy());
+ let dest = fs::File::create(unzip_path)?;
+ if file.size() > 0 {
+ let mut writer = BufWriter::new(dest);
+ io::copy(&mut file, &mut writer)?;
+ }
+ }
+ }
+
+ Ok(())
+}
+
+#[cfg(test)]
+mod tests {
+ extern crate mozprofile;
+
+ use self::mozprofile::preferences::Pref;
+ use super::*;
+ use serde_json::{json, Map, Value};
+ use std::fs::File;
+ use std::io::Read;
+ use url::{Host, Url};
+ use webdriver::capabilities::Capabilities;
+
+ fn example_profile() -> Value {
+ let mut profile_data = Vec::with_capacity(1024);
+ let mut profile = File::open("src/tests/profile.zip").unwrap();
+ profile.read_to_end(&mut profile_data).unwrap();
+ Value::String(BASE64_STANDARD.encode(&profile_data))
+ }
+
+ fn make_options(
+ firefox_opts: Capabilities,
+ marionette_settings: Option<MarionetteSettings>,
+ ) -> WebDriverResult<FirefoxOptions> {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:firefoxOptions".into(), Value::Object(firefox_opts));
+
+ FirefoxOptions::from_capabilities(None, &marionette_settings.unwrap_or_default(), &mut caps)
+ }
+
+ #[test]
+ fn fx_options_default() {
+ let opts: FirefoxOptions = Default::default();
+ assert_eq!(opts.android, None);
+ assert_eq!(opts.args, None);
+ assert_eq!(opts.binary, None);
+ assert_eq!(opts.log, LogOptions { level: None });
+ assert_eq!(opts.prefs, vec![]);
+ // Profile doesn't support PartialEq
+ // assert_eq!(opts.profile, None);
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_no_binary_and_empty_caps() {
+ let mut caps = Capabilities::new();
+
+ let marionette_settings = Default::default();
+ let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect("valid firefox options");
+ assert_eq!(opts.android, None);
+ assert_eq!(opts.args, None);
+ assert_eq!(opts.binary, None);
+ assert_eq!(opts.log, LogOptions { level: None });
+ assert_eq!(opts.prefs, vec![]);
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_binary_and_caps() {
+ let mut caps = Capabilities::new();
+ caps.insert(
+ "moz:firefoxOptions".into(),
+ Value::Object(Capabilities::new()),
+ );
+
+ let binary = PathBuf::from("foo");
+ let marionette_settings = Default::default();
+
+ let opts = FirefoxOptions::from_capabilities(
+ Some(binary.clone()),
+ &marionette_settings,
+ &mut caps,
+ )
+ .expect("valid firefox options");
+ assert_eq!(opts.android, None);
+ assert_eq!(opts.args, None);
+ assert_eq!(opts.binary, Some(binary));
+ assert_eq!(opts.log, LogOptions { level: None });
+ assert_eq!(opts.prefs, vec![]);
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_blocked_firefox_arguments() {
+ let blocked_args = vec![
+ "--marionette",
+ "--remote-allow-hosts",
+ "--remote-allow-origins",
+ "--remote-debugging-port",
+ ];
+
+ for arg in blocked_args {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("args".into(), json!([arg]));
+
+ make_options(firefox_opts, None).expect_err("invalid firefox options");
+ }
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_websocket_url_not_set() {
+ let mut caps = Capabilities::new();
+
+ let marionette_settings = Default::default();
+ let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect("Valid Firefox options");
+
+ assert!(
+ opts.args.is_none(),
+ "CLI arguments for Firefox unexpectedly found"
+ );
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_websocket_url_false() {
+ let mut caps = Capabilities::new();
+ caps.insert("webSocketUrl".into(), json!(false));
+
+ let marionette_settings = Default::default();
+ let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect("Valid Firefox options");
+
+ assert!(
+ opts.args.is_none(),
+ "CLI arguments for Firefox unexpectedly found"
+ );
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_websocket_url_true() {
+ let mut caps = Capabilities::new();
+ caps.insert("webSocketUrl".into(), json!(true));
+
+ let settings = MarionetteSettings {
+ websocket_port: 1234,
+ ..Default::default()
+ };
+ let opts = FirefoxOptions::from_capabilities(None, &settings, &mut caps)
+ .expect("Valid Firefox options");
+
+ if let Some(args) = opts.args {
+ let mut iter = args.iter();
+ assert!(iter.any(|arg| arg == &"--remote-debugging-port".to_owned()));
+ assert_eq!(iter.next(), Some(&"1234".to_owned()));
+ } else {
+ panic!("CLI arguments for Firefox not found");
+ }
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_websocket_and_allow_hosts() {
+ let mut caps = Capabilities::new();
+ caps.insert("webSocketUrl".into(), json!(true));
+
+ let mut marionette_settings: MarionetteSettings = Default::default();
+ marionette_settings.allow_hosts = vec![
+ Host::parse("foo").expect("host"),
+ Host::parse("bar").expect("host"),
+ ];
+ let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect("Valid Firefox options");
+
+ if let Some(args) = opts.args {
+ let mut iter = args.iter();
+ assert!(iter.any(|arg| arg == &"--remote-allow-hosts".to_owned()));
+ assert_eq!(iter.next(), Some(&"foo,bar".to_owned()));
+ assert!(!iter.any(|arg| arg == &"--remote-allow-origins".to_owned()));
+ } else {
+ panic!("CLI arguments for Firefox not found");
+ }
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_websocket_and_allow_origins() {
+ let mut caps = Capabilities::new();
+ caps.insert("webSocketUrl".into(), json!(true));
+
+ let mut marionette_settings: MarionetteSettings = Default::default();
+ marionette_settings.allow_origins = vec![
+ Url::parse("http://foo/").expect("url"),
+ Url::parse("http://bar/").expect("url"),
+ ];
+ let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect("Valid Firefox options");
+
+ if let Some(args) = opts.args {
+ let mut iter = args.iter();
+ assert!(iter.any(|arg| arg == &"--remote-allow-origins".to_owned()));
+ assert_eq!(iter.next(), Some(&"http://foo/,http://bar/".to_owned()));
+ assert!(!iter.any(|arg| arg == &"--remote-allow-hosts".to_owned()));
+ } else {
+ panic!("CLI arguments for Firefox not found");
+ }
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_debugger_address_not_set() {
+ let caps = Capabilities::new();
+
+ let opts = make_options(caps, None).expect("valid firefox options");
+ assert!(
+ opts.args.is_none(),
+ "CLI arguments for Firefox unexpectedly found"
+ );
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_debugger_address_false() {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:debuggerAddress".into(), json!(false));
+
+ let opts = make_options(caps, None).expect("valid firefox options");
+ assert!(
+ opts.args.is_none(),
+ "CLI arguments for Firefox unexpectedly found"
+ );
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_debugger_address_true() {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:debuggerAddress".into(), json!(true));
+
+ let settings = MarionetteSettings {
+ websocket_port: 1234,
+ ..Default::default()
+ };
+ let opts = FirefoxOptions::from_capabilities(None, &settings, &mut caps)
+ .expect("Valid Firefox options");
+
+ if let Some(args) = opts.args {
+ let mut iter = args.iter();
+ assert!(iter.any(|arg| arg == &"--remote-debugging-port".to_owned()));
+ assert_eq!(iter.next(), Some(&"1234".to_owned()));
+ } else {
+ panic!("CLI arguments for Firefox not found");
+ }
+ }
+
+ #[test]
+ fn fx_options_from_capabilities_with_invalid_caps() {
+ let mut caps = Capabilities::new();
+ caps.insert("moz:firefoxOptions".into(), json!(42));
+
+ let marionette_settings = Default::default();
+ FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps)
+ .expect_err("Firefox options need to be of type object");
+ }
+
+ #[test]
+ fn fx_options_android_package_and_binary() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo"));
+ firefox_opts.insert("binary".into(), json!("bar"));
+
+ make_options(firefox_opts, None)
+ .expect_err("androidPackage and binary are mutual exclusive");
+ }
+
+ #[test]
+ fn fx_options_android_no_package() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidAvtivity".into(), json!("foo"));
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert_eq!(opts.android, None);
+ }
+
+ #[test]
+ fn fx_options_android_package_valid_value() {
+ for value in ["foo.bar", "foo.bar.cheese.is.good", "Foo.Bar_9"].iter() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(value));
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert_eq!(opts.android.unwrap().package, value.to_string());
+ }
+ }
+
+ #[test]
+ fn fx_options_android_package_invalid_type() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(42));
+
+ make_options(firefox_opts, None).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_package_invalid_value() {
+ for value in ["../foo", "\\foo\n", "foo", "_foo", "0foo"].iter() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(value));
+ make_options(firefox_opts, None).expect_err("invalid firefox options");
+ }
+ }
+
+ #[test]
+ fn fx_options_android_activity_default_known_apps() {
+ let packages = vec![
+ "org.mozilla.firefox",
+ "org.mozilla.firefox_beta",
+ "org.mozilla.fenix",
+ "org.mozilla.fenix.debug",
+ "org.mozilla.focus",
+ "org.mozilla.focus.debug",
+ "org.mozilla.klar",
+ "org.mozilla.klar.debug",
+ "org.mozilla.reference.browser",
+ ];
+
+ for package in packages {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(package));
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert!(opts
+ .android
+ .unwrap()
+ .activity
+ .unwrap()
+ .contains("IntentReceiverActivity"));
+ }
+ }
+
+ #[test]
+ fn fx_options_android_activity_default_unknown_apps() {
+ let packages = vec!["org.mozilla.geckoview_example", "com.some.other.app"];
+
+ for package in packages {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(package));
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert_eq!(opts.android.unwrap().activity, None);
+ }
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert(
+ "androidPackage".into(),
+ json!("org.mozilla.geckoview_example"),
+ );
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert_eq!(opts.android.unwrap().activity, None);
+ }
+
+ #[test]
+ fn fx_options_android_activity_override() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidActivity".into(), json!("foo"));
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert_eq!(opts.android.unwrap().activity, Some("foo".to_string()));
+ }
+
+ #[test]
+ fn fx_options_android_activity_invalid_type() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidActivity".into(), json!(42));
+
+ make_options(firefox_opts, None).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_activity_invalid_value() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidActivity".into(), json!("foo.bar/cheese"));
+
+ make_options(firefox_opts, None).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_device_serial() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidDeviceSerial".into(), json!("cheese"));
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert_eq!(
+ opts.android.unwrap().device_serial,
+ Some("cheese".to_string())
+ );
+ }
+
+ #[test]
+ fn fx_options_android_device_serial_invalid() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidDeviceSerial".into(), json!(42));
+
+ make_options(firefox_opts, None).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_intent_arguments_defaults() {
+ let packages = vec![
+ "org.mozilla.firefox",
+ "org.mozilla.firefox_beta",
+ "org.mozilla.fenix",
+ "org.mozilla.fenix.debug",
+ "org.mozilla.geckoview_example",
+ "org.mozilla.reference.browser",
+ "com.some.other.app",
+ ];
+
+ for package in packages {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!(package));
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert_eq!(
+ opts.android.unwrap().intent_arguments,
+ Some(vec![
+ "-a".to_string(),
+ "android.intent.action.VIEW".to_string(),
+ "-d".to_string(),
+ "about:blank".to_string(),
+ ])
+ );
+ }
+ }
+
+ #[test]
+ fn fx_options_android_intent_arguments_override() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", "ipsum"]));
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ assert_eq!(
+ opts.android.unwrap().intent_arguments,
+ Some(vec!["lorem".to_string(), "ipsum".to_string()])
+ );
+ }
+
+ #[test]
+ fn fx_options_android_intent_arguments_no_array() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidIntentArguments".into(), json!(42));
+
+ make_options(firefox_opts, None).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_android_intent_arguments_invalid_value() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("androidPackage".into(), json!("foo.bar"));
+ firefox_opts.insert("androidIntentArguments".into(), json!(["lorem", 42]));
+
+ make_options(firefox_opts, None).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_env() {
+ let mut env: Map<String, Value> = Map::new();
+ env.insert("TEST_KEY_A".into(), Value::String("test_value_a".into()));
+ env.insert("TEST_KEY_B".into(), Value::String("test_value_b".into()));
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("env".into(), env.into());
+
+ let mut opts = make_options(firefox_opts, None).expect("valid firefox options");
+ for sorted in opts.env.iter_mut() {
+ sorted.sort()
+ }
+ assert_eq!(
+ opts.env,
+ Some(vec![
+ ("TEST_KEY_A".into(), "test_value_a".into()),
+ ("TEST_KEY_B".into(), "test_value_b".into()),
+ ])
+ );
+ }
+
+ #[test]
+ fn fx_options_env_invalid_container() {
+ let env = Value::Number(1.into());
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("env".into(), env);
+
+ make_options(firefox_opts, None).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn fx_options_env_invalid_value() {
+ let mut env: Map<String, Value> = Map::new();
+ env.insert("TEST_KEY".into(), Value::Number(1.into()));
+
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("env".into(), env.into());
+
+ make_options(firefox_opts, None).expect_err("invalid firefox options");
+ }
+
+ #[test]
+ fn test_profile() {
+ let encoded_profile = example_profile();
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("profile".into(), encoded_profile);
+
+ let opts = make_options(firefox_opts, None).expect("valid firefox options");
+ let mut profile = match opts.profile {
+ ProfileType::Path(profile) => profile,
+ _ => panic!("Expected ProfileType::Path"),
+ };
+ let prefs = profile.user_prefs().expect("valid preferences");
+
+ println!("{:#?}", prefs.prefs);
+
+ assert_eq!(
+ prefs.get("startup.homepage_welcome_url"),
+ Some(&Pref::new("data:text/html,PASS"))
+ );
+ }
+
+ #[test]
+ fn fx_options_args_profile() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("args".into(), json!(["--profile", "foo"]));
+
+ let options = make_options(firefox_opts, None).expect("Valid args");
+ assert!(matches!(options.profile, ProfileType::Path(_)));
+ }
+
+ #[test]
+ fn fx_options_args_named_profile() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("args".into(), json!(["-P", "foo"]));
+
+ let options = make_options(firefox_opts, None).expect("Valid args");
+ assert!(matches!(options.profile, ProfileType::Named));
+ }
+
+ #[test]
+ fn fx_options_args_no_profile() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("args".into(), json!(["--headless"]));
+
+ let options = make_options(firefox_opts, None).expect("Valid args");
+ assert!(matches!(options.profile, ProfileType::Temporary));
+ }
+
+ #[test]
+ fn fx_options_args_profile_and_profile() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("args".into(), json!(["--profile", "foo"]));
+ firefox_opts.insert("profile".into(), json!("foo"));
+
+ make_options(firefox_opts, None).expect_err("Invalid args");
+ }
+
+ #[test]
+ fn fx_options_args_p_and_profile() {
+ let mut firefox_opts = Capabilities::new();
+ firefox_opts.insert("args".into(), json!(["-P"]));
+ firefox_opts.insert("profile".into(), json!("foo"));
+
+ make_options(firefox_opts, None).expect_err("Invalid args");
+ }
+}