diff options
Diffstat (limited to 'src')
-rw-r--r-- | src/android.rs | 6 | ||||
-rw-r--r-- | src/browser.rs | 186 | ||||
-rw-r--r-- | src/capabilities.rs | 229 | ||||
-rw-r--r-- | src/logging.rs | 13 | ||||
-rw-r--r-- | src/main.rs | 278 | ||||
-rw-r--r-- | src/marionette.rs | 158 | ||||
-rw-r--r-- | src/prefs.rs | 15 |
7 files changed, 672 insertions, 213 deletions
diff --git a/src/android.rs b/src/android.rs index 8969fb1..3cc87b0 100644 --- a/src/android.rs +++ b/src/android.rs @@ -235,7 +235,7 @@ impl AndroidHandler { .split_terminator('\n') .filter(|line| line.starts_with("package:")) .map(|line| line.rsplit(':').next().expect("Package name found")); - if packages.find(|x| x == &options.package.as_str()).is_none() { + if !packages.any(|x| x == options.package.as_str()) { return Err(AndroidError::PackageNotFound(options.package.clone())); } @@ -498,7 +498,7 @@ mod test { }; assert_eq!(handler.test_root, test_root); - let mut profile = test_root.clone(); + let mut profile = test_root; profile.push(format!("{}-geckodriver-profile", &package)); assert_eq!(handler.profile, profile); } @@ -507,7 +507,7 @@ mod test { #[ignore] fn android_handler_storage_as_app() { let package = "org.mozilla.geckoview_example"; - run_handler_storage_test(&package, AndroidStorageInput::App); + run_handler_storage_test(package, AndroidStorageInput::App); } #[test] diff --git a/src/browser.rs b/src/browser.rs index b777a0a..306bfe1 100644 --- a/src/browser.rs +++ b/src/browser.rs @@ -3,16 +3,16 @@ * file, You can obtain one at http://mozilla.org/MPL/2.0/. */ use crate::android::AndroidHandler; -use crate::capabilities::FirefoxOptions; +use crate::capabilities::{FirefoxOptions, ProfileType}; use crate::logging; use crate::prefs; use mozprofile::preferences::Pref; use mozprofile::profile::{PrefFile, Profile}; use mozrunner::runner::{FirefoxProcess, FirefoxRunner, Runner, RunnerProcess}; use std::fs; -use std::path::PathBuf; +use std::io::Read; +use std::path::{Path, PathBuf}; use std::time; - use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult}; /// A running Gecko instance. @@ -22,7 +22,7 @@ pub(crate) enum Browser { Remote(RemoteBrowser), /// An existing browser instance not controlled by GeckoDriver - Existing, + Existing(u16), } impl Browser { @@ -30,7 +30,29 @@ impl Browser { match self { Browser::Local(x) => x.close(wait_for_shutdown), Browser::Remote(x) => x.close(), - Browser::Existing => Ok(()), + Browser::Existing(_) => Ok(()), + } + } + + pub(crate) fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> { + match self { + Browser::Local(x) => x.marionette_port(), + Browser::Remote(x) => x.marionette_port(), + Browser::Existing(x) => Ok(Some(*x)), + } + } + + pub(crate) fn update_marionette_port(&mut self, port: u16) { + match self { + Browser::Local(x) => x.update_marionette_port(port), + Browser::Remote(x) => x.update_marionette_port(port), + Browser::Existing(x) => { + if port != *x { + error!( + "Cannot re-assign Marionette port when connected to an existing browser" + ); + } + } } } } @@ -38,8 +60,10 @@ impl Browser { #[derive(Debug)] /// A local Firefox process, running on this (host) device. pub(crate) struct LocalBrowser { - process: FirefoxProcess, + marionette_port: u16, prefs_backup: Option<PrefsBackup>, + process: FirefoxProcess, + profile_path: Option<PathBuf>, } impl LocalBrowser { @@ -58,26 +82,34 @@ impl LocalBrowser { ) })?; - let is_custom_profile = options.profile.is_some(); + let is_custom_profile = matches!(options.profile, ProfileType::Path(_)); let mut profile = match options.profile { - Some(x) => x, - None => Profile::new()?, + ProfileType::Named => None, + ProfileType::Path(x) => Some(x), + ProfileType::Temporary => Some(Profile::new()?), }; - let prefs_backup = set_prefs( - marionette_port, - &mut profile, - is_custom_profile, - options.prefs, - jsdebugger, - ) - .map_err(|e| { - WebDriverError::new( - ErrorStatus::SessionNotCreated, - format!("Failed to set preferences: {}", e), + let (profile_path, prefs_backup) = if let Some(ref mut profile) = profile { + let profile_path = profile.path.clone(); + let prefs_backup = set_prefs( + marionette_port, + profile, + is_custom_profile, + options.prefs, + jsdebugger, ) - })?; + .map_err(|e| { + WebDriverError::new( + ErrorStatus::SessionNotCreated, + format!("Failed to set preferences: {}", e), + ) + })?; + (Some(profile_path), prefs_backup) + } else { + warn!("Unable to set geckodriver prefs when using a named profile"); + (None, None) + }; let mut runner = FirefoxRunner::new(&binary, profile); @@ -109,8 +141,10 @@ impl LocalBrowser { }; Ok(LocalBrowser { - process, + marionette_port, prefs_backup, + process, + profile_path, }) } @@ -132,6 +166,26 @@ impl LocalBrowser { Ok(()) } + fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> { + if self.marionette_port != 0 { + return Ok(Some(self.marionette_port)); + } + + if let Some(profile_path) = self.profile_path.as_ref() { + return Ok(read_marionette_port(profile_path)); + } + + // This should be impossible, but it isn't enforced + Err(WebDriverError::new( + ErrorStatus::SessionNotCreated, + "Port not known when using named profile", + )) + } + + fn update_marionette_port(&mut self, port: u16) { + self.marionette_port = port; + } + pub(crate) fn check_status(&mut self) -> Option<String> { match self.process.try_wait() { Ok(Some(status)) => Some( @@ -146,10 +200,33 @@ impl LocalBrowser { } } +fn read_marionette_port(profile_path: &Path) -> Option<u16> { + let port_file = profile_path.join("MarionetteActivePort"); + let mut port_str = String::with_capacity(6); + let mut file = match fs::File::open(&port_file) { + Ok(file) => file, + Err(_) => { + trace!("Failed to open {}", &port_file.to_string_lossy()); + return None; + } + }; + if let Err(e) = file.read_to_string(&mut port_str) { + trace!("Failed to read {}: {}", &port_file.to_string_lossy(), e); + return None; + }; + println!("Read port: {}", port_str); + let port = port_str.parse::<u16>().ok(); + if port.is_none() { + warn!("Failed fo convert {} to u16", &port_str); + } + port +} + #[derive(Debug)] /// A remote instance, running on a (target) Android device. pub(crate) struct RemoteBrowser { handler: AndroidHandler, + marionette_port: u16, } impl RemoteBrowser { @@ -163,9 +240,16 @@ impl RemoteBrowser { let handler = AndroidHandler::new(&android_options, marionette_port, websocket_port)?; // Profile management. - let is_custom_profile = options.profile.is_some(); - - let mut profile = options.profile.unwrap_or(Profile::new()?); + let (mut profile, is_custom_profile) = match options.profile { + ProfileType::Named => { + return Err(WebDriverError::new( + ErrorStatus::SessionNotCreated, + "Cannot use a named profile on Android", + )); + } + ProfileType::Path(x) => (x, true), + ProfileType::Temporary => (Profile::new()?, false), + }; set_prefs( handler.marionette_target_port, @@ -185,13 +269,24 @@ impl RemoteBrowser { handler.launch()?; - Ok(RemoteBrowser { handler }) + Ok(RemoteBrowser { + handler, + marionette_port, + }) } fn close(self) -> WebDriverResult<()> { self.handler.force_stop()?; Ok(()) } + + fn marionette_port(&mut self) -> WebDriverResult<Option<u16>> { + Ok(Some(self.marionette_port)) + } + + fn update_marionette_port(&mut self, port: u16) { + self.marionette_port = port; + } } fn set_prefs( @@ -209,14 +304,14 @@ fn set_prefs( })?; let backup_prefs = if custom_profile && prefs.path.exists() { - Some(PrefsBackup::new(&prefs)?) + Some(PrefsBackup::new(prefs)?) } else { None }; - for &(ref name, ref value) in prefs::DEFAULT.iter() { + for &(name, ref value) in prefs::DEFAULT.iter() { if !custom_profile || !prefs.contains_key(name) { - prefs.insert((*name).to_string(), (*value).clone()); + prefs.insert(name.to_string(), (*value).clone()); } } @@ -232,9 +327,6 @@ fn set_prefs( prefs.insert("marionette.port", Pref::new(port)); prefs.insert("remote.log.level", logging::max_level().into()); - // Deprecated since Firefox 91. - prefs.insert("marionette.log.level", logging::max_level().into()); - prefs.write().map_err(|e| { WebDriverError::new( ErrorStatus::UnknownError, @@ -284,12 +376,15 @@ impl PrefsBackup { #[cfg(test)] mod tests { use super::set_prefs; - use crate::capabilities::FirefoxOptions; + use crate::browser::read_marionette_port; + use crate::capabilities::{FirefoxOptions, ProfileType}; use mozprofile::preferences::{Pref, PrefValue}; use mozprofile::profile::Profile; use serde_json::{Map, Value}; use std::fs::File; use std::io::{Read, Write}; + use std::path::Path; + use tempfile::tempdir; fn example_profile() -> Value { let mut profile_data = Vec::with_capacity(1024); @@ -342,7 +437,10 @@ mod tests { let opts = FirefoxOptions::from_capabilities(None, &marionette_settings, &mut caps) .expect("Valid profile and prefs"); - let mut profile = opts.profile.expect("valid firefox profile"); + let mut profile = match opts.profile { + ProfileType::Path(profile) => profile, + _ => panic!("Expected ProfileType::Path"), + }; set_prefs(2828, &mut profile, true, opts.prefs, false).expect("set preferences"); @@ -420,4 +518,24 @@ mod tests { .unwrap(); assert_eq!(final_prefs_data, initial_prefs_data); } + + #[test] + fn test_local_read_marionette_port() { + fn create_port_file(profile_path: &Path, data: &[u8]) { + let port_path = profile_path.join("MarionetteActivePort"); + let mut file = File::create(&port_path).unwrap(); + file.write_all(data).unwrap(); + } + + let profile_dir = tempdir().unwrap(); + let profile_path = profile_dir.path(); + assert_eq!(read_marionette_port(profile_path), None); + assert_eq!(read_marionette_port(profile_path), None); + create_port_file(profile_path, b""); + assert_eq!(read_marionette_port(profile_path), None); + create_port_file(profile_path, b"1234"); + assert_eq!(read_marionette_port(profile_path), Some(1234)); + create_port_file(profile_path, b"1234abc"); + assert_eq!(read_marionette_port(profile_path), None); + } } diff --git a/src/capabilities.rs b/src/capabilities.rs index 278530e..97e1e28 100644 --- a/src/capabilities.rs +++ b/src/capabilities.rs @@ -178,10 +178,8 @@ impl<'a> BrowserCapabilities for FirefoxCapabilities<'a> { Ok(true) } - fn web_socket_url(&mut self, caps: &Capabilities) -> WebDriverResult<bool> { - self.browser_version(caps)? - .map(|v| self.compare_browser_version(&v, ">=90")) - .unwrap_or(Ok(false)) + fn web_socket_url(&mut self, _: &Capabilities) -> WebDriverResult<bool> { + Ok(true) } fn validate_custom(&mut self, name: &str, value: &Value) -> WebDriverResult<()> { @@ -369,6 +367,19 @@ impl AndroidOptions { } } +#[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 @@ -378,7 +389,7 @@ impl AndroidOptions { #[derive(Default, Debug)] pub struct FirefoxOptions { pub binary: Option<PathBuf>, - pub profile: Option<Profile>, + pub profile: ProfileType, pub args: Option<Vec<String>>, pub env: Option<Vec<(String, String)>>, pub log: LogOptions, @@ -409,33 +420,71 @@ impl FirefoxOptions { ) })?; - 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)?; - rv.profile = FirefoxOptions::load_profile(&options)?; + 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(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 rv.profile.is_some() { + 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 = Some(Profile::new_from_path(&path_buf)?); + rv.profile = ProfileType::Path(Profile::new_from_path(&path_buf)?); } - if get_arg_value(os_args.iter(), Arg::NamedProfile).is_some() && rv.profile.is_some() { + 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, - "Can't provide both a -P argument and a profile", + format!("Argument {} can't be set via capabilities", arg), )); - } + }; } let has_web_socket_url = matched @@ -459,6 +508,32 @@ impl FirefoxOptions { 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 { @@ -506,11 +581,7 @@ impl FirefoxOptions { 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", - ) + WebDriverError::new(ErrorStatus::InvalidArgument, "Arguments were not an array") })?; let args = args_array .iter() @@ -522,6 +593,7 @@ impl FirefoxOptions { "Arguments entries were not all strings", ) })?; + Ok(Some(args)) } else { Ok(None) @@ -792,7 +864,7 @@ mod tests { 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 { @@ -863,6 +935,23 @@ mod tests { } #[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(); @@ -905,12 +994,56 @@ mod tests { if let Some(args) = opts.args { let mut iter = args.iter(); - assert!(iter - .find(|&arg| arg == &"--remote-debugging-port".to_owned()) - .is_some()); + assert!(iter.any(|arg| arg == &"--remote-debugging-port".to_owned())); assert_eq!(iter.next(), Some(&"1234".to_owned())); } else { - assert!(false, "CLI arguments for Firefox not found"); + 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"); } } @@ -951,12 +1084,10 @@ mod tests { if let Some(args) = opts.args { let mut iter = args.iter(); - assert!(iter - .find(|&arg| arg == &"--remote-debugging-port".to_owned()) - .is_some()); + assert!(iter.any(|arg| arg == &"--remote-debugging-port".to_owned())); assert_eq!(iter.next(), Some(&"1234".to_owned())); } else { - assert!(false, "CLI arguments for Firefox not found"); + panic!("CLI arguments for Firefox not found"); } assert!(opts @@ -976,6 +1107,16 @@ mod tests { } #[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")); @@ -1199,7 +1340,7 @@ mod tests { let env = Value::Number(1.into()); let mut firefox_opts = Capabilities::new(); - firefox_opts.insert("env".into(), env.into()); + firefox_opts.insert("env".into(), env); make_options(firefox_opts, None).expect_err("invalid firefox options"); } @@ -1222,7 +1363,10 @@ mod tests { firefox_opts.insert("profile".into(), encoded_profile); let opts = make_options(firefox_opts, None).expect("valid firefox options"); - let mut profile = opts.profile.expect("valid firefox profile"); + 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); @@ -1238,7 +1382,26 @@ mod tests { let mut firefox_opts = Capabilities::new(); firefox_opts.insert("args".into(), json!(["--profile", "foo"])); - make_options(firefox_opts, None).expect("Valid args"); + 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] diff --git a/src/logging.rs b/src/logging.rs index de477dc..73ce062 100644 --- a/src/logging.rs +++ b/src/logging.rs @@ -219,8 +219,6 @@ mod tests { use std::str::FromStr; use std::sync::Mutex; - use chrono; - use log; use mozprofile::preferences::{Pref, PrefValue}; lazy_static! { @@ -239,17 +237,6 @@ mod tests { } #[test] - fn test_level_eq() { - assert_eq!(Level::Fatal, Level::Fatal); - assert_eq!(Level::Error, Level::Error); - assert_eq!(Level::Warn, Level::Warn); - assert_eq!(Level::Info, Level::Info); - assert_eq!(Level::Config, Level::Config); - assert_eq!(Level::Debug, Level::Debug); - assert_eq!(Level::Trace, Level::Trace); - } - - #[test] fn test_level_from_log() { assert_eq!(Level::from(log::Level::Error), Level::Error); assert_eq!(Level::from(log::Level::Warn), Level::Warn); diff --git a/src/main.rs b/src/main.rs index c6c5503..115d650 100644 --- a/src/main.rs +++ b/src/main.rs @@ -17,6 +17,7 @@ extern crate serde; extern crate serde_derive; extern crate serde_json; extern crate serde_yaml; +extern crate url; extern crate uuid; extern crate webdriver; extern crate zip; @@ -27,12 +28,12 @@ extern crate log; use std::env; use std::fmt; use std::io; -use std::net::{IpAddr, SocketAddr}; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; use std::path::PathBuf; use std::result; use std::str::FromStr; -use clap::{App, Arg}; +use clap::{App, AppSettings, Arg}; macro_rules! try_opt { ($expr:expr, $err_type:expr, $err_msg:expr) => {{ @@ -59,6 +60,7 @@ use crate::command::extension_routes; use crate::logging::Level; use crate::marionette::{MarionetteHandler, MarionetteSettings}; use mozdevice::AndroidStorageInput; +use url::{Host, Url}; const EXIT_SUCCESS: i32 = 0; const EXIT_USAGE: i32 = 64; @@ -103,7 +105,7 @@ impl fmt::Display for FatalError { let s = match *self { Parsing(ref err) => err.to_string(), Usage(ref s) => format!("error: {}", s), - Server(ref err) => format!("error: {}", err.to_string()), + Server(ref err) => format!("error: {}", err), }; write!(f, "{}", s) } @@ -111,67 +113,162 @@ impl fmt::Display for FatalError { macro_rules! usage { ($msg:expr) => { - return Err(FatalError::Usage($msg.to_string())); + return Err(FatalError::Usage($msg.to_string())) }; ($fmt:expr, $($arg:tt)+) => { - return Err(FatalError::Usage(format!($fmt, $($arg)+))); + return Err(FatalError::Usage(format!($fmt, $($arg)+))) }; } type ProgramResult<T> = result::Result<T, FatalError>; +#[allow(clippy::large_enum_variant)] enum Operation { Help, Version, Server { log_level: Option<Level>, - host: String, address: SocketAddr, + allow_hosts: Vec<Host>, + allow_origins: Vec<Url>, settings: MarionetteSettings, deprecated_storage_arg: bool, }, } +/// Get a socket address from the provided host and port +/// +/// # Arguments +/// * `webdriver_host` - The hostname on which the server will listen +/// * `webdriver_port` - The port on which the server will listen +/// +/// When the host and port resolve to multiple addresses, prefer +/// IPv4 addresses vs IPv6. +fn server_address(webdriver_host: &str, webdriver_port: u16) -> ProgramResult<SocketAddr> { + let mut socket_addrs = match format!("{}:{}", webdriver_host, webdriver_port).to_socket_addrs() + { + Ok(addrs) => addrs.collect::<Vec<_>>(), + Err(e) => usage!("{}: {}:{}", e, webdriver_host, webdriver_port), + }; + if socket_addrs.is_empty() { + usage!( + "Unable to resolve host: {}:{}", + webdriver_host, + webdriver_port + ) + } + // Prefer ipv4 address + socket_addrs.sort_by(|a, b| { + let a_val = if a.ip().is_ipv4() { 0 } else { 1 }; + let b_val = if b.ip().is_ipv4() { 0 } else { 1 }; + a_val.partial_cmp(&b_val).expect("Comparison failed") + }); + Ok(socket_addrs.remove(0)) +} + +/// Parse a given string into a Host +fn parse_hostname(webdriver_host: &str) -> Result<Host, url::ParseError> { + let host_str = if let Ok(ip_addr) = IpAddr::from_str(webdriver_host) { + // In this case we have an IP address as the host + if ip_addr.is_ipv6() { + // Convert to quoted form + format!("[{}]", &webdriver_host) + } else { + webdriver_host.into() + } + } else { + webdriver_host.into() + }; + + Host::parse(&host_str) +} + +/// Get a list of default hostnames to allow +/// +/// This only covers domain names, not IP addresses, since IP adresses +/// are always accepted. +fn get_default_allowed_hosts(ip: IpAddr) -> Vec<Result<Host, url::ParseError>> { + let localhost_is_loopback = ("localhost".to_string(), 80) + .to_socket_addrs() + .map(|addr_iter| { + addr_iter + .map(|addr| addr.ip()) + .filter(|ip| ip.is_loopback()) + }) + .iter() + .len() + > 0; + if ip.is_loopback() && localhost_is_loopback { + vec![Host::parse("localhost")] + } else { + vec![] + } +} + +fn get_allowed_hosts( + host: Host, + allow_hosts: Option<clap::Values>, +) -> Result<Vec<Host>, url::ParseError> { + allow_hosts + .map(|hosts| hosts.map(Host::parse).collect::<Vec<_>>()) + .unwrap_or_else(|| match host { + Host::Domain(_) => { + vec![Ok(host.clone())] + } + Host::Ipv4(ip) => get_default_allowed_hosts(IpAddr::V4(ip)), + Host::Ipv6(ip) => get_default_allowed_hosts(IpAddr::V6(ip)), + }) + .into_iter() + .collect::<Result<Vec<Host>, url::ParseError>>() +} + +fn get_allowed_origins(allow_origins: Option<clap::Values>) -> Result<Vec<Url>, url::ParseError> { + allow_origins + .map(|origins| { + origins + .map(Url::parse) + .collect::<Result<Vec<Url>, url::ParseError>>() + }) + .unwrap_or_else(|| Ok(vec![])) +} + fn parse_args(app: &mut App) -> ProgramResult<Operation> { - let matches = app.get_matches_from_safe_borrow(env::args())?; + let args = app.try_get_matches_from_mut(env::args())?; - let log_level = if matches.is_present("log_level") { - Level::from_str(matches.value_of("log_level").unwrap()).ok() + if args.is_present("help") { + return Ok(Operation::Help); + } else if args.is_present("version") { + return Ok(Operation::Version); + } + + let log_level = if args.is_present("log_level") { + Level::from_str(args.value_of("log_level").unwrap()).ok() } else { - Some(match matches.occurrences_of("verbosity") { + Some(match args.occurrences_of("verbosity") { 0 => Level::Info, 1 => Level::Debug, _ => Level::Trace, }) }; - let host = matches.value_of("webdriver_host").unwrap(); - let port = { - let s = matches.value_of("webdriver_port").unwrap(); + let webdriver_host = args.value_of("webdriver_host").unwrap(); + let webdriver_port = { + let s = args.value_of("webdriver_port").unwrap(); match u16::from_str(s) { Ok(n) => n, Err(e) => usage!("invalid --port: {}: {}", e, s), } }; - let address = match IpAddr::from_str(host) { - Ok(addr) => SocketAddr::new(addr, port), - Err(e) => usage!("{}: {}:{}", e, host, port), - }; - if !address.ip().is_loopback() { - usage!( - "invalid --host: {}. Must be a local loopback interface", - host - ) - } - let android_storage = value_t!(matches, "android_storage", AndroidStorageInput) + let android_storage = args + .value_of_t::<AndroidStorageInput>("android_storage") .unwrap_or(AndroidStorageInput::Auto); - let binary = matches.value_of("binary").map(PathBuf::from); + let binary = args.value_of("binary").map(PathBuf::from); - let marionette_host = matches.value_of("marionette_host").unwrap(); - let marionette_port = match matches.value_of("marionette_port") { + let marionette_host = args.value_of("marionette_host").unwrap(); + let marionette_port = match args.value_of("marionette_port") { Some(s) => match u16::from_str(s) { Ok(n) => Some(n), Err(e) => usage!("invalid --marionette-port: {}", e), @@ -181,7 +278,7 @@ fn parse_args(app: &mut App) -> ProgramResult<Operation> { // For Android the port on the device must be the same as the one on the // host. For now default to 9222, which is the default for --remote-debugging-port. - let websocket_port = match matches.value_of("websocket_port") { + let websocket_port = match args.value_of("websocket_port") { Some(s) => match u16::from_str(s) { Ok(n) => n, Err(e) => usage!("invalid --websocket-port: {}", e), @@ -189,30 +286,42 @@ fn parse_args(app: &mut App) -> ProgramResult<Operation> { None => 9222, }; - let op = if matches.is_present("help") { - Operation::Help - } else if matches.is_present("version") { - Operation::Version - } else { - let settings = MarionetteSettings { - binary, - connect_existing: matches.is_present("connect_existing"), - host: marionette_host.to_string(), - port: marionette_port, - websocket_port, - jsdebugger: matches.is_present("jsdebugger"), - android_storage, - }; - Operation::Server { - log_level, - host: host.into(), - address, - settings, - deprecated_storage_arg: matches.is_present("android_storage"), - } + let host = match parse_hostname(webdriver_host) { + Ok(name) => name, + Err(e) => usage!("invalid --host {}: {}", webdriver_host, e), + }; + + let allow_hosts = match get_allowed_hosts(host, args.values_of("allow_hosts")) { + Ok(hosts) => hosts, + Err(e) => usage!("invalid --allow-hosts {}", e), }; - Ok(op) + let allow_origins = match get_allowed_origins(args.values_of("allow_origins")) { + Ok(origins) => origins, + Err(e) => usage!("invalid --allow-origins {}", e), + }; + + let address = server_address(webdriver_host, webdriver_port)?; + + let settings = MarionetteSettings { + binary, + connect_existing: args.is_present("connect_existing"), + host: marionette_host.into(), + port: marionette_port, + websocket_port, + allow_hosts: allow_hosts.clone(), + allow_origins: allow_origins.clone(), + jsdebugger: args.is_present("jsdebugger"), + android_storage, + }; + Ok(Operation::Server { + log_level, + allow_hosts, + allow_origins, + address, + settings, + deprecated_storage_arg: args.is_present("android_storage"), + }) } fn inner_main(app: &mut App) -> ProgramResult<()> { @@ -222,8 +331,9 @@ fn inner_main(app: &mut App) -> ProgramResult<()> { Operation::Server { log_level, - host, address, + allow_hosts, + allow_origins, settings, deprecated_storage_arg, } => { @@ -238,7 +348,13 @@ fn inner_main(app: &mut App) -> ProgramResult<()> { }; let handler = MarionetteHandler::new(settings); - let listening = webdriver::server::start(host, address, handler, extension_routes())?; + let listening = webdriver::server::start( + address, + allow_hosts, + allow_origins, + handler, + extension_routes(), + )?; info!("Listening on {}", listening.socket); } } @@ -266,11 +382,13 @@ fn main() { }); } -fn make_app<'a, 'b>() -> App<'a, 'b> { +fn make_app<'a>() -> App<'a> { App::new(format!("geckodriver {}", build::build_info())) + .setting(AppSettings::NoAutoHelp) + .setting(AppSettings::NoAutoVersion) .about("WebDriver implementation for Firefox") .arg( - Arg::with_name("webdriver_host") + Arg::new("webdriver_host") .long("host") .takes_value(true) .value_name("HOST") @@ -278,8 +396,8 @@ fn make_app<'a, 'b>() -> App<'a, 'b> { .help("Host IP to use for WebDriver server"), ) .arg( - Arg::with_name("webdriver_port") - .short("p") + Arg::new("webdriver_port") + .short('p') .long("port") .takes_value(true) .value_name("PORT") @@ -287,15 +405,15 @@ fn make_app<'a, 'b>() -> App<'a, 'b> { .help("Port to use for WebDriver server"), ) .arg( - Arg::with_name("binary") - .short("b") + Arg::new("binary") + .short('b') .long("binary") .takes_value(true) .value_name("BINARY") .help("Path to the Firefox binary"), ) .arg( - Arg::with_name("marionette_host") + Arg::new("marionette_host") .long("marionette-host") .takes_value(true) .value_name("HOST") @@ -303,14 +421,14 @@ fn make_app<'a, 'b>() -> App<'a, 'b> { .help("Host to use to connect to Gecko"), ) .arg( - Arg::with_name("marionette_port") + Arg::new("marionette_port") .long("marionette-port") .takes_value(true) .value_name("PORT") .help("Port to use to connect to Gecko [default: system-allocated port]"), ) .arg( - Arg::with_name("websocket_port") + Arg::new("websocket_port") .long("websocket-port") .takes_value(true) .value_name("PORT") @@ -318,25 +436,25 @@ fn make_app<'a, 'b>() -> App<'a, 'b> { .help("Port to use to connect to WebDriver BiDi [default: 9222]"), ) .arg( - Arg::with_name("connect_existing") + Arg::new("connect_existing") .long("connect-existing") .requires("marionette_port") .help("Connect to an existing Firefox instance"), ) .arg( - Arg::with_name("jsdebugger") + Arg::new("jsdebugger") .long("jsdebugger") .help("Attach browser toolbox debugger for Firefox"), ) .arg( - Arg::with_name("verbosity") - .multiple(true) + Arg::new("verbosity") + .multiple_occurrences(true) .conflicts_with("log_level") - .short("v") + .short('v') .help("Log level verbosity (-v for debug and -vv for trace level)"), ) .arg( - Arg::with_name("log_level") + Arg::new("log_level") .long("log") .takes_value(true) .value_name("LEVEL") @@ -344,24 +462,40 @@ fn make_app<'a, 'b>() -> App<'a, 'b> { .help("Set Gecko log level"), ) .arg( - Arg::with_name("help") - .short("h") + Arg::new("help") + .short('h') .long("help") .help("Prints this message"), ) .arg( - Arg::with_name("version") - .short("V") + Arg::new("version") + .short('V') .long("version") .help("Prints version and copying information"), ) .arg( - Arg::with_name("android_storage") + Arg::new("android_storage") .long("android-storage") .possible_values(&["auto", "app", "internal", "sdcard"]) .value_name("ANDROID_STORAGE") .help("Selects storage location to be used for test data (deprecated)."), ) + .arg( + Arg::new("allow_hosts") + .long("allow-hosts") + .takes_value(true) + .multiple_values(true) + .value_name("ALLOW_HOSTS") + .help("List of hostnames to allow. By default the value of --host is allowed, and in addition if that's a well known local address, other variations on well known local addresses are allowed. If --allow-hosts is provided only exactly those hosts are allowed."), + ) + .arg( + Arg::new("allow_origins") + .long("allow-origins") + .takes_value(true) + .multiple_values(true) + .value_name("ALLOW_ORIGINS") + .help("List of request origins to allow. These must be formatted as scheme://host:port. By default any request with an origin header is rejected. If --allow-origins is provided then only exactly those origins are allowed."), + ) } fn get_program_name() -> String { diff --git a/src/marionette.rs b/src/marionette.rs index cf3f40e..325cc9a 100644 --- a/src/marionette.rs +++ b/src/marionette.rs @@ -4,7 +4,7 @@ use crate::browser::{Browser, LocalBrowser, RemoteBrowser}; use crate::build; -use crate::capabilities::{FirefoxCapabilities, FirefoxOptions}; +use crate::capabilities::{FirefoxCapabilities, FirefoxOptions, ProfileType}; use crate::command::{ AddonInstallParameters, AddonUninstallParameters, GeckoContextParameters, GeckoExtensionCommand, GeckoExtensionRoute, CHROME_ELEMENT_KEY, @@ -37,17 +37,19 @@ use std::path::PathBuf; use std::sync::Mutex; use std::thread; use std::time; +use url::{Host, Url}; +use webdriver::capabilities::BrowserCapabilities; use webdriver::command::WebDriverCommand::{ AcceptAlert, AddCookie, CloseWindow, DeleteCookie, DeleteCookies, DeleteSession, DismissAlert, ElementClear, ElementClick, ElementSendKeys, ExecuteAsyncScript, ExecuteScript, Extension, FindElement, FindElementElement, FindElementElements, FindElements, FullscreenWindow, Get, GetActiveElement, GetAlertText, GetCSSValue, GetCookies, GetCurrentUrl, GetElementAttribute, GetElementProperty, GetElementRect, GetElementTagName, GetElementText, GetNamedCookie, - GetPageSource, GetTimeouts, GetTitle, GetWindowHandle, GetWindowHandles, GetWindowRect, GoBack, - GoForward, IsDisplayed, IsEnabled, IsSelected, MaximizeWindow, MinimizeWindow, NewSession, - NewWindow, PerformActions, Print, Refresh, ReleaseActions, SendAlertText, SetTimeouts, - SetWindowRect, Status, SwitchToFrame, SwitchToParentFrame, SwitchToWindow, - TakeElementScreenshot, TakeScreenshot, + GetPageSource, GetShadowRoot, GetTimeouts, GetTitle, GetWindowHandle, GetWindowHandles, + GetWindowRect, GoBack, GoForward, IsDisplayed, IsEnabled, IsSelected, MaximizeWindow, + MinimizeWindow, NewSession, NewWindow, PerformActions, Print, Refresh, ReleaseActions, + SendAlertText, SetTimeouts, SetWindowRect, Status, SwitchToFrame, SwitchToParentFrame, + SwitchToWindow, TakeElementScreenshot, TakeScreenshot, }; use webdriver::command::{ ActionsParameters, AddCookieParameters, GetNamedCookieParameters, GetParameters, @@ -57,7 +59,8 @@ use webdriver::command::{ }; use webdriver::command::{WebDriverCommand, WebDriverMessage}; use webdriver::common::{ - Cookie, Date, FrameId, LocatorStrategy, WebElement, ELEMENT_KEY, FRAME_KEY, WINDOW_KEY, + Cookie, Date, FrameId, LocatorStrategy, ShadowRoot, WebElement, ELEMENT_KEY, FRAME_KEY, + SHADOW_KEY, WINDOW_KEY, }; use webdriver::error::{ErrorStatus, WebDriverError, WebDriverResult}; use webdriver::response::{ @@ -82,6 +85,8 @@ pub(crate) struct MarionetteSettings { pub(crate) host: String, pub(crate) port: Option<u16>, pub(crate) websocket_port: u16, + pub(crate) allow_hosts: Vec<Host>, + pub(crate) allow_origins: Vec<Url>, /// Brings up the Browser Toolbox when starting Firefox, /// letting you debug internals. @@ -109,8 +114,8 @@ impl MarionetteHandler { session_id: Option<String>, new_session_parameters: &NewSessionParameters, ) -> WebDriverResult<MarionetteConnection> { + let mut fx_capabilities = FirefoxCapabilities::new(self.settings.binary.as_ref()); let (capabilities, options) = { - let mut fx_capabilities = FirefoxCapabilities::new(self.settings.binary.as_ref()); let mut capabilities = new_session_parameters .match_browser(&mut fx_capabilities)? .ok_or_else(|| { @@ -121,7 +126,7 @@ impl MarionetteHandler { })?; let options = FirefoxOptions::from_capabilities( - fx_capabilities.chosen_binary, + fx_capabilities.chosen_binary.clone(), &self.settings, &mut capabilities, )?; @@ -133,14 +138,39 @@ impl MarionetteHandler { } let marionette_host = self.settings.host.to_owned(); - let marionette_port = self - .settings - .port - .unwrap_or(get_free_port(&marionette_host)?); - - let websocket_port = match options.use_websocket { - true => Some(self.settings.websocket_port), - false => None, + let marionette_port = match self.settings.port { + Some(port) => port, + None => { + // If we're launching Firefox Desktop version 95 or later, and there's no port + // specified, we can pass 0 as the port and later read it back from + // the profile. + let can_use_profile: bool = options.android.is_none() + && options.profile != ProfileType::Named + && !self.settings.connect_existing + && fx_capabilities + .browser_version(&capabilities) + .map(|opt_v| { + opt_v + .map(|v| { + fx_capabilities + .compare_browser_version(&v, ">=95") + .unwrap_or(false) + }) + .unwrap_or(false) + }) + .unwrap_or(false); + if can_use_profile { + 0 + } else { + get_free_port(&marionette_host)? + } + } + }; + + let websocket_port = if options.use_websocket { + Some(self.settings.websocket_port) + } else { + None }; let browser = if options.android.is_some() { @@ -166,10 +196,10 @@ impl MarionetteHandler { self.settings.jsdebugger, )?) } else { - Browser::Existing + Browser::Existing(marionette_port) }; let session = MarionetteSession::new(session_id, capabilities); - MarionetteConnection::new(marionette_host, marionette_port, browser, session) + MarionetteConnection::new(marionette_host, browser, session) } fn close_connection(&mut self, wait_for_shutdown: bool) { @@ -214,7 +244,7 @@ impl WebDriverHandler<GeckoExtensionRoute> for MarionetteHandler { Ok(mut connection) => { if connection.is_none() { if let NewSession(ref capabilities) = msg.command { - let conn = self.create_connection(msg.session_id.clone(), &capabilities)?; + let conn = self.create_connection(msg.session_id.clone(), capabilities)?; *connection = Some(conn); } else { return Err(WebDriverError::new( @@ -322,6 +352,30 @@ impl MarionetteSession { Ok(WebElement(id)) } + /// Converts a Marionette JSON response into a `ShadowRoot`. + fn to_shadow_root(&self, json_data: &Value) -> WebDriverResult<ShadowRoot> { + let data = try_opt!( + json_data.as_object(), + ErrorStatus::UnknownError, + "Failed to convert data to an object" + ); + + let shadow_root = data.get(SHADOW_KEY); + + let value = try_opt!( + shadow_root, + ErrorStatus::UnknownError, + "Failed to extract shadow root from Marionette response" + ); + let id = try_opt!( + value.as_str(), + ErrorStatus::UnknownError, + "Failed to convert shadow root reference value to string" + ) + .to_string(); + Ok(ShadowRoot(id)) + } + fn next_command_id(&mut self) -> MessageId { self.command_id += 1; self.command_id @@ -405,13 +459,9 @@ impl MarionetteSession { "Failed to interpret script timeout duration as u64" ), }; - // Check for the spec-compliant "pageLoad", but also for "page load", - // which was sent by Firefox 52 and earlier. let page_load = try_opt!( try_opt!( - resp.result - .get("pageLoad") - .or_else(|| resp.result.get("page load")), + resp.result.get("pageLoad"), ErrorStatus::UnknownError, "Missing field: pageLoad" ) @@ -631,6 +681,14 @@ impl MarionetteSession { .collect(), ))) } + GetShadowRoot(_) => { + let shadow_root = self.to_shadow_root(try_opt!( + resp.result.get("value"), + ErrorStatus::UnknownError, + "Failed to find value field" + ))?; + WebDriverResponse::Generic(ValueResponse(serde_json::to_value(shadow_root)?)) + } GetActiveElement => { let element = self.to_web_element(try_opt!( resp.result.get("value"), @@ -707,7 +765,7 @@ fn try_convert_to_marionette_message( flags: vec![AppStatus::eForceQuit], }, )), - Browser::Existing => Some(Command::WebDriver( + Browser::Existing(_) => Some(Command::WebDriver( MarionetteWebDriverCommand::DeleteSession, )), }, @@ -806,6 +864,11 @@ fn try_convert_to_marionette_message( GetPageSource => Some(Command::WebDriver( MarionetteWebDriverCommand::GetPageSource, )), + GetShadowRoot(ref e) => Some(Command::WebDriver( + MarionetteWebDriverCommand::GetShadowRoot { + id: e.clone().to_string(), + }, + )), GetTitle => Some(Command::WebDriver(MarionetteWebDriverCommand::GetTitle)), GetWindowHandle => Some(Command::WebDriver( MarionetteWebDriverCommand::GetWindowHandle, @@ -1063,11 +1126,10 @@ struct MarionetteConnection { impl MarionetteConnection { fn new( host: String, - port: u16, mut browser: Browser, session: MarionetteSession, ) -> WebDriverResult<MarionetteConnection> { - let stream = match MarionetteConnection::connect(&host, port, &mut browser) { + let stream = match MarionetteConnection::connect(&host, &mut browser) { Ok(stream) => stream, Err(e) => { if let Err(e) = browser.close(true) { @@ -1083,16 +1145,15 @@ impl MarionetteConnection { }) } - fn connect(host: &str, port: u16, browser: &mut Browser) -> WebDriverResult<TcpStream> { + fn connect(host: &str, browser: &mut Browser) -> WebDriverResult<TcpStream> { let timeout = time::Duration::from_secs(60); let poll_interval = time::Duration::from_millis(100); let now = time::Instant::now(); debug!( - "Waiting {}s to connect to browser on {}:{}", + "Waiting {}s to connect to browser on {}", timeout.as_secs(), host, - port ); loop { @@ -1106,19 +1167,31 @@ impl MarionetteConnection { } } - match MarionetteConnection::try_connect(host, port) { - Ok(stream) => { - debug!("Connection to Marionette established on {}:{}.", host, port); - return Ok(stream); - } - Err(e) => { - if now.elapsed() < timeout { - trace!("{}. Retrying in {:?}", e.to_string(), poll_interval); - thread::sleep(poll_interval); - } else { - return Err(WebDriverError::new(ErrorStatus::Timeout, e.to_string())); + let last_err; + + if let Some(port) = browser.marionette_port()? { + match MarionetteConnection::try_connect(host, port) { + Ok(stream) => { + debug!("Connection to Marionette established on {}:{}.", host, port); + browser.update_marionette_port(port); + return Ok(stream); + } + Err(e) => { + let err_str = e.to_string(); + last_err = Some(err_str); } } + } else { + last_err = Some("Failed to read marionette port".into()); + } + if now.elapsed() < timeout { + trace!("Retrying in {:?}", poll_interval); + thread::sleep(poll_interval); + } else { + return Err(WebDriverError::new( + ErrorStatus::Timeout, + last_err.unwrap_or_else(|| "Unknown error".into()), + )); } } } @@ -1469,7 +1542,6 @@ impl ToMarionette<MarionetteFrame> for SwitchToFrameParameters { impl ToMarionette<Window> for SwitchToWindowParameters { fn to_marionette(&self) -> WebDriverResult<Window> { Ok(Window { - name: self.handle.clone(), handle: self.handle.clone(), }) } diff --git a/src/prefs.rs b/src/prefs.rs index e9a4032..b9b1785 100644 --- a/src/prefs.rs +++ b/src/prefs.rs @@ -29,9 +29,6 @@ lazy_static! { // Note: Possible update tests could reset or flip the value to allow // updates to be downloaded and applied. ("app.update.disabledForTesting", Pref::new(true)), - // !!! For backward compatibility up to Firefox 64. Only remove - // when this Firefox version is no longer supported by geckodriver !!! - ("app.update.auto", Pref::new(false)), // Enable the dump function, which sends messages to the system // console @@ -58,14 +55,6 @@ lazy_static! { // Start with a blank page (about:blank) ("browser.startup.page", Pref::new(0)), - // Do not close the window when the last tab gets closed - // TODO: Remove once minimum supported Firefox release is 61. - ("browser.tabs.closeWindowWithLastTab", Pref::new(false)), - - // Do not warn when closing all open tabs - // TODO: Remove once minimum supported Firefox release is 61. - ("browser.tabs.warnOnClose", Pref::new(false)), - // Disable the UI tour ("browser.uitour.enabled", Pref::new(false)), @@ -126,10 +115,6 @@ lazy_static! { // Disable the GFX sanity window ("media.sanity-test.disabled", Pref::new(true)), - // Do not prompt with long usernames or passwords in URLs - // TODO: Remove once minimum supported Firefox release is 61. - ("network.http.phishy-userpass-length", Pref::new(255)), - // Do not automatically switch between offline and online ("network.manage-offline-status", Pref::new(false)), |