main.rs (15863B)
1 #![forbid(unsafe_code)] 2 3 extern crate chrono; 4 #[macro_use] 5 extern crate clap; 6 #[macro_use] 7 extern crate lazy_static; 8 extern crate hyper; 9 extern crate marionette as marionette_rs; 10 extern crate mozdevice; 11 extern crate mozprofile; 12 extern crate mozrunner; 13 extern crate mozversion; 14 extern crate regex; 15 extern crate serde; 16 #[macro_use] 17 extern crate serde_derive; 18 extern crate serde_json; 19 extern crate tempfile; 20 extern crate url; 21 extern crate uuid; 22 extern crate webdriver; 23 extern crate yaml_rust; 24 extern crate zip; 25 26 #[macro_use] 27 extern crate log; 28 29 use std::env; 30 use std::net::{IpAddr, SocketAddr, ToSocketAddrs}; 31 use std::path::PathBuf; 32 use std::process::ExitCode; 33 34 use std::str::FromStr; 35 36 use clap::{Arg, ArgAction, Command}; 37 38 macro_rules! try_opt { 39 ($expr:expr, $err_type:expr, $err_msg:expr) => {{ 40 match $expr { 41 Some(x) => x, 42 None => return Err(WebDriverError::new($err_type, $err_msg)), 43 } 44 }}; 45 } 46 47 mod android; 48 mod browser; 49 mod build; 50 mod capabilities; 51 mod command; 52 mod logging; 53 mod marionette; 54 mod prefs; 55 56 #[cfg(test)] 57 pub mod test; 58 59 use crate::command::extension_routes; 60 use crate::logging::Level; 61 use crate::marionette::{MarionetteHandler, MarionetteSettings}; 62 use anyhow::{bail, Result as ProgramResult}; 63 use clap::ArgMatches; 64 use mozdevice::AndroidStorageInput; 65 use url::{Host, Url}; 66 67 const EXIT_USAGE: u8 = 64; 68 const EXIT_UNAVAILABLE: u8 = 69; 69 70 #[allow(clippy::large_enum_variant)] 71 enum Operation { 72 Help, 73 Version, 74 Server { 75 log_level: Option<Level>, 76 log_truncate: bool, 77 address: SocketAddr, 78 allow_hosts: Vec<Host>, 79 allow_origins: Vec<Url>, 80 settings: MarionetteSettings, 81 deprecated_storage_arg: bool, 82 }, 83 } 84 85 /// Get a socket address from the provided host and port 86 /// 87 /// # Arguments 88 /// * `webdriver_host` - The hostname on which the server will listen 89 /// * `webdriver_port` - The port on which the server will listen 90 /// 91 /// When the host and port resolve to multiple addresses, prefer 92 /// IPv4 addresses vs IPv6. 93 fn server_address(webdriver_host: &str, webdriver_port: u16) -> ProgramResult<SocketAddr> { 94 let mut socket_addrs = match format!("{}:{}", webdriver_host, webdriver_port).to_socket_addrs() 95 { 96 Ok(addrs) => addrs.collect::<Vec<_>>(), 97 Err(e) => bail!("{}: {}:{}", e, webdriver_host, webdriver_port), 98 }; 99 if socket_addrs.is_empty() { 100 bail!( 101 "Unable to resolve host: {}:{}", 102 webdriver_host, 103 webdriver_port 104 ) 105 } 106 // Prefer ipv4 address 107 socket_addrs.sort_by(|a, b| { 108 let a_val = i32::from(!a.ip().is_ipv4()); 109 let b_val = i32::from(!b.ip().is_ipv4()); 110 a_val.partial_cmp(&b_val).expect("Comparison failed") 111 }); 112 Ok(socket_addrs.remove(0)) 113 } 114 115 /// Parse a given string into a Host 116 fn parse_hostname(webdriver_host: &str) -> Result<Host, url::ParseError> { 117 let host_str = if let Ok(ip_addr) = IpAddr::from_str(webdriver_host) { 118 // In this case we have an IP address as the host 119 if ip_addr.is_ipv6() { 120 // Convert to quoted form 121 format!("[{}]", &webdriver_host) 122 } else { 123 webdriver_host.into() 124 } 125 } else { 126 webdriver_host.into() 127 }; 128 129 Host::parse(&host_str) 130 } 131 132 /// Get a list of default hostnames to allow 133 /// 134 /// This only covers domain names, not IP addresses, since IP adresses 135 /// are always accepted. 136 fn get_default_allowed_hosts(ip: IpAddr) -> Vec<Host> { 137 let localhost_is_loopback = ("localhost".to_string(), 80) 138 .to_socket_addrs() 139 .map(|addr_iter| { 140 addr_iter 141 .map(|addr| addr.ip()) 142 .filter(|ip| ip.is_loopback()) 143 }) 144 .iter() 145 .len() 146 > 0; 147 if ip.is_loopback() && localhost_is_loopback { 148 vec![Host::parse("localhost").unwrap()] 149 } else { 150 vec![] 151 } 152 } 153 154 fn get_allowed_hosts(host: Host, allow_hosts: Option<clap::parser::ValuesRef<Host>>) -> Vec<Host> { 155 allow_hosts 156 .map(|hosts| hosts.cloned().collect()) 157 .unwrap_or_else(|| match host { 158 Host::Domain(_) => { 159 vec![host.clone()] 160 } 161 Host::Ipv4(ip) => get_default_allowed_hosts(IpAddr::V4(ip)), 162 Host::Ipv6(ip) => get_default_allowed_hosts(IpAddr::V6(ip)), 163 }) 164 } 165 166 fn get_allowed_origins(allow_origins: Option<clap::parser::ValuesRef<Url>>) -> Vec<Url> { 167 allow_origins.into_iter().flatten().cloned().collect() 168 } 169 170 fn parse_args(args: &ArgMatches) -> ProgramResult<Operation> { 171 if args.get_flag("help") { 172 return Ok(Operation::Help); 173 } else if args.get_flag("version") { 174 return Ok(Operation::Version); 175 } 176 177 let log_level = if let Some(log_level) = args.get_one::<String>("log_level") { 178 Level::from_str(log_level).ok() 179 } else { 180 Some(match args.get_count("verbosity") { 181 0 => Level::Info, 182 1 => Level::Debug, 183 _ => Level::Trace, 184 }) 185 }; 186 187 let webdriver_host = args.get_one::<String>("webdriver_host").unwrap(); 188 let webdriver_port = { 189 let s = args.get_one::<String>("webdriver_port").unwrap(); 190 match u16::from_str(s) { 191 Ok(n) => n, 192 Err(e) => bail!("invalid --port: {}: {}", e, s), 193 } 194 }; 195 196 let android_storage = args 197 .get_one::<String>("android_storage") 198 .and_then(|arg| AndroidStorageInput::from_str(arg).ok()) 199 .unwrap_or(AndroidStorageInput::Auto); 200 201 let binary = args.get_one::<String>("binary").map(PathBuf::from); 202 203 let profile_root = args.get_one::<String>("profile_root").map(PathBuf::from); 204 205 // Try to create a temporary directory on startup to check that the directory exists and is writable 206 { 207 let tmp_dir = if let Some(ref tmp_root) = profile_root { 208 tempfile::tempdir_in(tmp_root) 209 } else { 210 tempfile::tempdir() 211 }; 212 if tmp_dir.is_err() { 213 bail!("Unable to write to temporary directory; consider --profile-root with a writeable directory") 214 } 215 } 216 217 let marionette_host = args.get_one::<String>("marionette_host").unwrap(); 218 let marionette_port = match args.get_one::<String>("marionette_port") { 219 Some(s) => match u16::from_str(s) { 220 Ok(n) => Some(n), 221 Err(e) => bail!("invalid --marionette-port: {}", e), 222 }, 223 None => None, 224 }; 225 226 // For Android the port on the device must be the same as the one on the 227 // host. For now default to 9222, which is the default for --remote-debugging-port. 228 let websocket_port = match args.get_one::<String>("websocket_port") { 229 Some(s) => match u16::from_str(s) { 230 Ok(n) => n, 231 Err(e) => bail!("invalid --websocket-port: {}", e), 232 }, 233 None => 9222, 234 }; 235 236 let host = match parse_hostname(webdriver_host) { 237 Ok(name) => name, 238 Err(e) => bail!("invalid --host {}: {}", webdriver_host, e), 239 }; 240 241 let allow_hosts = get_allowed_hosts(host, args.get_many("allow_hosts")); 242 243 let allow_origins = get_allowed_origins(args.get_many("allow_origins")); 244 245 let address = server_address(webdriver_host, webdriver_port)?; 246 247 let settings = MarionetteSettings { 248 binary, 249 profile_root, 250 connect_existing: args.get_flag("connect_existing"), 251 host: marionette_host.into(), 252 port: marionette_port, 253 websocket_port, 254 allow_hosts: allow_hosts.clone(), 255 allow_origins: allow_origins.clone(), 256 jsdebugger: args.get_flag("jsdebugger"), 257 android_storage, 258 system_access: args.get_flag("allow_system_access"), 259 }; 260 Ok(Operation::Server { 261 log_level, 262 log_truncate: !args.get_flag("log_no_truncate"), 263 allow_hosts, 264 allow_origins, 265 address, 266 settings, 267 deprecated_storage_arg: args.contains_id("android_storage"), 268 }) 269 } 270 271 fn inner_main(operation: Operation, cmd: &mut Command) -> ProgramResult<()> { 272 match operation { 273 Operation::Help => print_help(cmd), 274 Operation::Version => print_version(), 275 276 Operation::Server { 277 log_level, 278 log_truncate, 279 address, 280 allow_hosts, 281 allow_origins, 282 settings, 283 deprecated_storage_arg, 284 } => { 285 if let Some(ref level) = log_level { 286 logging::init_with_level(*level, log_truncate).unwrap(); 287 } else { 288 logging::init(log_truncate).unwrap(); 289 } 290 291 if deprecated_storage_arg { 292 warn!("--android-storage argument is deprecated and will be removed soon."); 293 }; 294 295 let handler = MarionetteHandler::new(settings); 296 let listening = webdriver::server::start( 297 address, 298 allow_hosts, 299 allow_origins, 300 handler, 301 extension_routes(), 302 )?; 303 info!("Listening on {}", listening.socket); 304 } 305 } 306 307 Ok(()) 308 } 309 310 fn main() -> ExitCode { 311 let mut cmd = make_command(); 312 313 let args = match cmd.try_get_matches_from_mut(env::args()) { 314 Ok(args) => args, 315 Err(e) => { 316 // Clap already says "error:" and don't repeat help. 317 eprintln!("{}: {}", get_program_name(), e); 318 return ExitCode::from(EXIT_USAGE); 319 } 320 }; 321 322 let operation = match parse_args(&args) { 323 Ok(op) => op, 324 Err(e) => { 325 eprintln!("{}: error: {}", get_program_name(), e); 326 print_help(&mut cmd); 327 return ExitCode::from(EXIT_USAGE); 328 } 329 }; 330 331 if let Err(e) = inner_main(operation, &mut cmd) { 332 eprintln!("{}: error: {}", get_program_name(), e); 333 print_help(&mut cmd); 334 return ExitCode::from(EXIT_UNAVAILABLE); 335 } 336 337 ExitCode::SUCCESS 338 } 339 340 fn make_command() -> Command { 341 Command::new(format!("geckodriver {}", build::build_info())) 342 .disable_help_flag(true) 343 .disable_version_flag(true) 344 .about("WebDriver implementation for Firefox") 345 .arg( 346 Arg::new("allow_hosts") 347 .long("allow-hosts") 348 .num_args(1..) 349 .value_parser(clap::builder::ValueParser::new(Host::parse)) 350 .value_name("ALLOW_HOSTS") 351 .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."), 352 ) 353 .arg( 354 Arg::new("allow_origins") 355 .long("allow-origins") 356 .num_args(1..) 357 .value_parser(clap::builder::ValueParser::new(Url::parse)) 358 .value_name("ALLOW_ORIGINS") 359 .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."), 360 ) 361 .arg( 362 Arg::new("allow_system_access") 363 .long("allow-system-access") 364 .action(ArgAction::SetTrue) 365 .help("Enable privileged access to the application's parent process"), 366 ) 367 .arg( 368 Arg::new("android_storage") 369 .long("android-storage") 370 .value_parser(["auto", "app", "internal", "sdcard"]) 371 .value_name("ANDROID_STORAGE") 372 .help("Selects storage location to be used for test data (deprecated)."), 373 ) 374 .arg( 375 Arg::new("binary") 376 .short('b') 377 .long("binary") 378 .num_args(1) 379 .value_name("BINARY") 380 .help("Path to the Firefox binary"), 381 ) 382 .arg( 383 Arg::new("connect_existing") 384 .long("connect-existing") 385 .requires("marionette_port") 386 .action(ArgAction::SetTrue) 387 .help("Connect to an existing Firefox instance"), 388 ) 389 .arg( 390 Arg::new("help") 391 .short('h') 392 .long("help") 393 .action(ArgAction::SetTrue) 394 .help("Prints this message"), 395 ) 396 .arg( 397 Arg::new("webdriver_host") 398 .long("host") 399 .num_args(1) 400 .value_name("HOST") 401 .default_value("127.0.0.1") 402 .help("Host IP to use for WebDriver server"), 403 ) 404 .arg( 405 Arg::new("jsdebugger") 406 .long("jsdebugger") 407 .action(ArgAction::SetTrue) 408 .help("Attach browser toolbox debugger for Firefox"), 409 ) 410 .arg( 411 Arg::new("log_level") 412 .long("log") 413 .num_args(1) 414 .value_name("LEVEL") 415 .value_parser(["fatal", "error", "warn", "info", "config", "debug", "trace"]) 416 .help("Set Gecko log level"), 417 ) 418 .arg( 419 Arg::new("log_no_truncate") 420 .long("log-no-truncate") 421 .action(ArgAction::SetTrue) 422 .help("Disable truncation of long log lines"), 423 ) 424 .arg( 425 Arg::new("marionette_host") 426 .long("marionette-host") 427 .num_args(1) 428 .value_name("HOST") 429 .default_value("127.0.0.1") 430 .help("Host to use to connect to Gecko"), 431 ) 432 .arg( 433 Arg::new("marionette_port") 434 .long("marionette-port") 435 .num_args(1) 436 .value_name("PORT") 437 .help("Port to use to connect to Gecko [default: system-allocated port]"), 438 ) 439 .arg( 440 Arg::new("webdriver_port") 441 .short('p') 442 .long("port") 443 .num_args(1) 444 .value_name("PORT") 445 .default_value("4444") 446 .help("Port to use for WebDriver server"), 447 ) 448 .arg( 449 Arg::new("profile_root") 450 .long("profile-root") 451 .num_args(1) 452 .value_name("PROFILE_ROOT") 453 .help("Directory in which to create profiles. Defaults to the system temporary directory."), 454 ) 455 .arg( 456 Arg::new("verbosity") 457 .conflicts_with("log_level") 458 .short('v') 459 .action(ArgAction::Count) 460 .help("Log level verbosity (-v for debug and -vv for trace level)"), 461 ) 462 .arg( 463 Arg::new("version") 464 .short('V') 465 .long("version") 466 .action(ArgAction::SetTrue) 467 .help("Prints version and copying information"), 468 ) 469 .arg( 470 Arg::new("websocket_port") 471 .long("websocket-port") 472 .num_args(1) 473 .value_name("PORT") 474 .conflicts_with("connect_existing") 475 .help("Port to use to connect to WebDriver BiDi [default: 9222]"), 476 ) 477 } 478 479 fn get_program_name() -> String { 480 env::args().next().unwrap() 481 } 482 483 fn print_help(cmd: &mut Command) { 484 cmd.print_help().ok(); 485 println!(); 486 } 487 488 fn print_version() { 489 println!("geckodriver {}", build::build_info()); 490 println!(); 491 println!("The source code of this program is available from"); 492 println!("testing/geckodriver in https://hg.mozilla.org/mozilla-central."); 493 println!(); 494 println!("This program is subject to the terms of the Mozilla Public License 2.0."); 495 println!("You can obtain a copy of the license at https://mozilla.org/MPL/2.0/."); 496 }