summaryrefslogtreecommitdiffstats
path: root/testing/geckodriver/src/main.rs
diff options
context:
space:
mode:
Diffstat (limited to 'testing/geckodriver/src/main.rs')
-rw-r--r--testing/geckodriver/src/main.rs496
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/.");
+}