Source code
Revision control
Copy as Markdown
Other Tools
#![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!();
println!("This program is subject to the terms of the Mozilla Public License 2.0.");
}