#![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, log_truncate: bool, address: SocketAddr, allow_hosts: Vec, allow_origins: Vec, 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 { let mut socket_addrs = match format!("{}:{}", webdriver_host, webdriver_port).to_socket_addrs() { Ok(addrs) => addrs.collect::>(), 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 { 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 { 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>) -> Vec { 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>) -> Vec { allow_origins.into_iter().flatten().cloned().collect() } fn parse_args(args: &ArgMatches) -> ProgramResult { 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::("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::("webdriver_host").unwrap(); let webdriver_port = { let s = args.get_one::("webdriver_port").unwrap(); match u16::from_str(s) { Ok(n) => n, Err(e) => bail!("invalid --port: {}: {}", e, s), } }; let android_storage = args .get_one::("android_storage") .and_then(|arg| AndroidStorageInput::from_str(arg).ok()) .unwrap_or(AndroidStorageInput::Auto); let binary = args.get_one::("binary").map(PathBuf::from); let profile_root = args.get_one::("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::("marionette_host").unwrap(); let marionette_port = match args.get_one::("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::("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/."); }