diff options
Diffstat (limited to 'testing/geckodriver/src/main.rs')
-rw-r--r-- | testing/geckodriver/src/main.rs | 496 |
1 files changed, 496 insertions, 0 deletions
diff --git a/testing/geckodriver/src/main.rs b/testing/geckodriver/src/main.rs new file mode 100644 index 0000000000..3d7cdf10b2 --- /dev/null +++ b/testing/geckodriver/src/main.rs @@ -0,0 +1,496 @@ +#![forbid(unsafe_code)] + +extern crate chrono; +#[macro_use] +extern crate clap; +#[macro_use] +extern crate lazy_static; +extern crate hyper; +extern crate marionette as marionette_rs; +extern crate mozdevice; +extern crate mozprofile; +extern crate mozrunner; +extern crate mozversion; +extern crate regex; +extern crate serde; +#[macro_use] +extern crate serde_derive; +extern crate serde_json; +extern crate serde_yaml; +extern crate tempfile; +extern crate url; +extern crate uuid; +extern crate webdriver; +extern crate zip; + +#[macro_use] +extern crate log; + +use std::env; +use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; +use std::path::PathBuf; +use std::process::ExitCode; + +use std::str::FromStr; + +use clap::{Arg, ArgAction, Command}; + +macro_rules! try_opt { + ($expr:expr, $err_type:expr, $err_msg:expr) => {{ + match $expr { + Some(x) => x, + None => return Err(WebDriverError::new($err_type, $err_msg)), + } + }}; +} + +mod android; +mod browser; +mod build; +mod capabilities; +mod command; +mod logging; +mod marionette; +mod prefs; + +#[cfg(test)] +pub mod test; + +use crate::command::extension_routes; +use crate::logging::Level; +use crate::marionette::{MarionetteHandler, MarionetteSettings}; +use anyhow::{bail, Result as ProgramResult}; +use clap::ArgMatches; +use mozdevice::AndroidStorageInput; +use url::{Host, Url}; + +const EXIT_USAGE: u8 = 64; +const EXIT_UNAVAILABLE: u8 = 69; + +#[allow(clippy::large_enum_variant)] +enum Operation { + Help, + Version, + Server { + log_level: Option<Level>, + log_truncate: bool, + 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) => bail!("{}: {}:{}", e, webdriver_host, webdriver_port), + }; + if socket_addrs.is_empty() { + bail!( + "Unable to resolve host: {}:{}", + webdriver_host, + webdriver_port + ) + } + // Prefer ipv4 address + socket_addrs.sort_by(|a, b| { + let a_val = i32::from(!a.ip().is_ipv4()); + let b_val = i32::from(!b.ip().is_ipv4()); + 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<Host> { + 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").unwrap()] + } else { + vec![] + } +} + +fn get_allowed_hosts(host: Host, allow_hosts: Option<clap::parser::ValuesRef<Host>>) -> Vec<Host> { + allow_hosts + .map(|hosts| hosts.cloned().collect()) + .unwrap_or_else(|| match host { + Host::Domain(_) => { + vec![host.clone()] + } + Host::Ipv4(ip) => get_default_allowed_hosts(IpAddr::V4(ip)), + Host::Ipv6(ip) => get_default_allowed_hosts(IpAddr::V6(ip)), + }) +} + +fn get_allowed_origins(allow_origins: Option<clap::parser::ValuesRef<Url>>) -> Vec<Url> { + allow_origins.into_iter().flatten().cloned().collect() +} + +fn parse_args(args: &ArgMatches) -> ProgramResult<Operation> { + if args.get_flag("help") { + return Ok(Operation::Help); + } else if args.get_flag("version") { + return Ok(Operation::Version); + } + + let log_level = if let Some(log_level) = args.get_one::<String>("log_level") { + Level::from_str(log_level).ok() + } else { + Some(match args.get_count("verbosity") { + 0 => Level::Info, + 1 => Level::Debug, + _ => Level::Trace, + }) + }; + + let webdriver_host = args.get_one::<String>("webdriver_host").unwrap(); + let webdriver_port = { + let s = args.get_one::<String>("webdriver_port").unwrap(); + match u16::from_str(s) { + Ok(n) => n, + Err(e) => bail!("invalid --port: {}: {}", e, s), + } + }; + + let android_storage = args + .get_one::<String>("android_storage") + .and_then(|arg| AndroidStorageInput::from_str(arg).ok()) + .unwrap_or(AndroidStorageInput::Auto); + + let binary = args.get_one::<String>("binary").map(PathBuf::from); + + let profile_root = args.get_one::<String>("profile_root").map(PathBuf::from); + + // Try to create a temporary directory on startup to check that the directory exists and is writable + { + let tmp_dir = if let Some(ref tmp_root) = profile_root { + tempfile::tempdir_in(tmp_root) + } else { + tempfile::tempdir() + }; + if tmp_dir.is_err() { + bail!("Unable to write to temporary directory; consider --profile-root with a writeable directory") + } + } + + let marionette_host = args.get_one::<String>("marionette_host").unwrap(); + let marionette_port = match args.get_one::<String>("marionette_port") { + Some(s) => match u16::from_str(s) { + Ok(n) => Some(n), + Err(e) => bail!("invalid --marionette-port: {}", e), + }, + None => None, + }; + + // 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 args.get_one::<String>("websocket_port") { + Some(s) => match u16::from_str(s) { + Ok(n) => n, + Err(e) => bail!("invalid --websocket-port: {}", e), + }, + None => 9222, + }; + + let host = match parse_hostname(webdriver_host) { + Ok(name) => name, + Err(e) => bail!("invalid --host {}: {}", webdriver_host, e), + }; + + let allow_hosts = get_allowed_hosts(host, args.get_many("allow_hosts")); + + let allow_origins = get_allowed_origins(args.get_many("allow_origins")); + + let address = server_address(webdriver_host, webdriver_port)?; + + let settings = MarionetteSettings { + binary, + profile_root, + connect_existing: args.get_flag("connect_existing"), + host: marionette_host.into(), + port: marionette_port, + websocket_port, + allow_hosts: allow_hosts.clone(), + allow_origins: allow_origins.clone(), + jsdebugger: args.get_flag("jsdebugger"), + enable_crash_reporter: args.get_flag("enable_crash_reporter"), + android_storage, + }; + Ok(Operation::Server { + log_level, + log_truncate: !args.get_flag("log_no_truncate"), + allow_hosts, + allow_origins, + address, + settings, + deprecated_storage_arg: args.contains_id("android_storage"), + }) +} + +fn inner_main(operation: Operation, cmd: &mut Command) -> ProgramResult<()> { + match operation { + Operation::Help => print_help(cmd), + Operation::Version => print_version(), + + Operation::Server { + log_level, + log_truncate, + address, + allow_hosts, + allow_origins, + settings, + deprecated_storage_arg, + } => { + if let Some(ref level) = log_level { + logging::init_with_level(*level, log_truncate).unwrap(); + } else { + logging::init(log_truncate).unwrap(); + } + + if deprecated_storage_arg { + warn!("--android-storage argument is deprecated and will be removed soon."); + }; + + let handler = MarionetteHandler::new(settings); + let listening = webdriver::server::start( + address, + allow_hosts, + allow_origins, + handler, + extension_routes(), + )?; + info!("Listening on {}", listening.socket); + } + } + + Ok(()) +} + +fn main() -> ExitCode { + let mut cmd = make_command(); + + let args = match cmd.try_get_matches_from_mut(env::args()) { + Ok(args) => args, + Err(e) => { + // Clap already says "error:" and don't repeat help. + eprintln!("{}: {}", get_program_name(), e); + return ExitCode::from(EXIT_USAGE); + } + }; + + let operation = match parse_args(&args) { + Ok(op) => op, + Err(e) => { + eprintln!("{}: error: {}", get_program_name(), e); + print_help(&mut cmd); + return ExitCode::from(EXIT_USAGE); + } + }; + + if let Err(e) = inner_main(operation, &mut cmd) { + eprintln!("{}: error: {}", get_program_name(), e); + print_help(&mut cmd); + return ExitCode::from(EXIT_UNAVAILABLE); + } + + ExitCode::SUCCESS +} + +fn make_command() -> Command { + Command::new(format!("geckodriver {}", build::build_info())) + .disable_help_flag(true) + .disable_version_flag(true) + .about("WebDriver implementation for Firefox") + .arg( + Arg::new("allow_hosts") + .long("allow-hosts") + .num_args(1..) + .value_parser(clap::builder::ValueParser::new(Host::parse)) + .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") + .num_args(1..) + .value_parser(clap::builder::ValueParser::new(Url::parse)) + .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."), + ) + .arg( + Arg::new("android_storage") + .long("android-storage") + .value_parser(["auto", "app", "internal", "sdcard"]) + .value_name("ANDROID_STORAGE") + .help("Selects storage location to be used for test data (deprecated)."), + ) + .arg( + Arg::new("binary") + .short('b') + .long("binary") + .num_args(1) + .value_name("BINARY") + .help("Path to the Firefox binary"), + ) + .arg( + Arg::new("connect_existing") + .long("connect-existing") + .requires("marionette_port") + .action(ArgAction::SetTrue) + .help("Connect to an existing Firefox instance"), + ) + .arg( + Arg::new("enable_crash_reporter") + .long("enable-crash-reporter") + .action(ArgAction::SetTrue) + .help("Enable the Firefox crash reporter for diagnostic purposes"), + ) + .arg( + Arg::new("help") + .short('h') + .long("help") + .action(ArgAction::SetTrue) + .help("Prints this message"), + ) + .arg( + Arg::new("webdriver_host") + .long("host") + .num_args(1) + .value_name("HOST") + .default_value("127.0.0.1") + .help("Host IP to use for WebDriver server"), + ) + .arg( + Arg::new("jsdebugger") + .long("jsdebugger") + .action(ArgAction::SetTrue) + .help("Attach browser toolbox debugger for Firefox"), + ) + .arg( + Arg::new("log_level") + .long("log") + .num_args(1) + .value_name("LEVEL") + .value_parser(["fatal", "error", "warn", "info", "config", "debug", "trace"]) + .help("Set Gecko log level"), + ) + .arg( + Arg::new("log_no_truncate") + .long("log-no-truncate") + .action(ArgAction::SetTrue) + .help("Disable truncation of long log lines"), + ) + .arg( + Arg::new("marionette_host") + .long("marionette-host") + .num_args(1) + .value_name("HOST") + .default_value("127.0.0.1") + .help("Host to use to connect to Gecko"), + ) + .arg( + Arg::new("marionette_port") + .long("marionette-port") + .num_args(1) + .value_name("PORT") + .help("Port to use to connect to Gecko [default: system-allocated port]"), + ) + .arg( + Arg::new("webdriver_port") + .short('p') + .long("port") + .num_args(1) + .value_name("PORT") + .default_value("4444") + .help("Port to use for WebDriver server"), + ) + .arg( + Arg::new("profile_root") + .long("profile-root") + .num_args(1) + .value_name("PROFILE_ROOT") + .help("Directory in which to create profiles. Defaults to the system temporary directory."), + ) + .arg( + Arg::new("verbosity") + .conflicts_with("log_level") + .short('v') + .action(ArgAction::Count) + .help("Log level verbosity (-v for debug and -vv for trace level)"), + ) + .arg( + Arg::new("version") + .short('V') + .long("version") + .action(ArgAction::SetTrue) + .help("Prints version and copying information"), + ) + .arg( + Arg::new("websocket_port") + .long("websocket-port") + .num_args(1) + .value_name("PORT") + .conflicts_with("connect_existing") + .help("Port to use to connect to WebDriver BiDi [default: 9222]"), + ) +} + +fn get_program_name() -> String { + env::args().next().unwrap() +} + +fn print_help(cmd: &mut Command) { + cmd.print_help().ok(); + println!(); +} + +fn print_version() { + println!("geckodriver {}", build::build_info()); + println!(); + println!("The source code of this program is available from"); + println!("testing/geckodriver in https://hg.mozilla.org/mozilla-central."); + println!(); + println!("This program is subject to the terms of the Mozilla Public License 2.0."); + println!("You can obtain a copy of the license at https://mozilla.org/MPL/2.0/."); +} |