tor-browser

The Tor Browser
git clone https://git.dasho.dev/tor-browser.git
Log | Files | Refs | README | LICENSE

android.rs (20843B)


      1 use crate::capabilities::AndroidOptions;
      2 use mozdevice::{AndroidStorage, Device, Host, RemoteMetadata, UnixPathBuf};
      3 use mozprofile::profile::Profile;
      4 use std::fs::File;
      5 use std::io;
      6 use std::path::PathBuf;
      7 use std::time;
      8 use thiserror::Error;
      9 use webdriver::error::{ErrorStatus, WebDriverError};
     10 use yaml_rust::yaml::{Hash, Yaml};
     11 
     12 // TODO: avoid port clashes across GeckoView-vehicles.
     13 // For now, we always use target port 2829, leading to issues like bug 1533704.
     14 const MARIONETTE_TARGET_PORT: u16 = 2829;
     15 
     16 const CONFIG_FILE_HEADING: &str = r#"## GeckoView configuration YAML
     17 ##
     18 ## Auto-generated by geckodriver.
     19 ## See https://mozilla.github.io/geckoview/consumer/docs/automation.
     20 "#;
     21 
     22 pub type Result<T> = std::result::Result<T, AndroidError>;
     23 
     24 #[derive(Debug, Error)]
     25 pub enum AndroidError {
     26    #[error("Activity for package '{0}' not found")]
     27    ActivityNotFound(String),
     28 
     29    #[error(transparent)]
     30    Device(#[from] mozdevice::DeviceError),
     31 
     32    #[error(transparent)]
     33    IO(#[from] io::Error),
     34 
     35    #[error("Package '{0}' not found")]
     36    PackageNotFound(String),
     37 
     38    #[error(transparent)]
     39    Serde(#[from] yaml_rust::EmitError),
     40 }
     41 
     42 impl From<AndroidError> for WebDriverError {
     43    fn from(value: AndroidError) -> WebDriverError {
     44        WebDriverError::new(ErrorStatus::UnknownError, value.to_string())
     45    }
     46 }
     47 
     48 /// A remote Gecko instance.
     49 ///
     50 /// Host refers to the device running `geckodriver`.  Target refers to the
     51 /// Android device running Gecko in a GeckoView-based vehicle.
     52 #[derive(Debug)]
     53 pub struct AndroidProcess {
     54    pub device: Device,
     55    pub package: String,
     56    pub activity: String,
     57 }
     58 
     59 impl AndroidProcess {
     60    pub fn new(
     61        device: Device,
     62        package: String,
     63        activity: String,
     64    ) -> mozdevice::Result<AndroidProcess> {
     65        Ok(AndroidProcess {
     66            device,
     67            package,
     68            activity,
     69        })
     70    }
     71 }
     72 
     73 #[derive(Debug)]
     74 pub struct AndroidHandler {
     75    pub config: UnixPathBuf,
     76    pub options: AndroidOptions,
     77    pub process: AndroidProcess,
     78    pub profile: UnixPathBuf,
     79    pub test_root: UnixPathBuf,
     80 
     81    // Port forwarding for Marionette: host => target
     82    pub marionette_host_port: u16,
     83    pub marionette_target_port: u16,
     84 
     85    pub system_access: bool,
     86 
     87    // Port forwarding for WebSocket connections (WebDriver BiDi and CDP)
     88    pub websocket_port: Option<u16>,
     89 }
     90 
     91 impl Drop for AndroidHandler {
     92    fn drop(&mut self) {
     93        // Try to clean up various settings
     94        let clear_command = format!("am clear-debug-app {}", self.process.package);
     95        match self
     96            .process
     97            .device
     98            .execute_host_shell_command(&clear_command)
     99        {
    100            Ok(_) => debug!("Disabled reading from configuration file"),
    101            Err(e) => error!("Failed disabling from configuration file: {}", e),
    102        }
    103 
    104        match self.process.device.remove(&self.config) {
    105            Ok(_) => debug!("Deleted GeckoView configuration file"),
    106            Err(e) => error!("Failed deleting GeckoView configuration file: {}", e),
    107        }
    108 
    109        match self.process.device.remove(&self.test_root) {
    110            Ok(_) => debug!("Deleted test root folder: {}", &self.test_root.display()),
    111            Err(e) => error!("Failed deleting test root folder: {}", e),
    112        }
    113 
    114        debug!(
    115            "Stop forwarding Marionette port ({} -> {})",
    116            &self.marionette_host_port, &self.marionette_target_port
    117        );
    118        match self
    119            .process
    120            .device
    121            .kill_forward_port(self.marionette_host_port)
    122        {
    123            Ok(_) => {}
    124            Err(e) => error!(
    125                "Failed to stop forwarding Marionette port ({} -> {}): {}",
    126                &self.marionette_host_port, &self.marionette_target_port, e
    127            ),
    128        }
    129 
    130        if let Some(port) = self.websocket_port {
    131            debug!(
    132                "Stop forwarding WebSocket port ({} -> {})",
    133                &self.marionette_host_port, &self.marionette_target_port
    134            );
    135            match self.process.device.kill_forward_port(port) {
    136                Ok(_) => {}
    137                Err(e) => error!(
    138                    "Failed to stop forwarding WebSocket port ({0} -> {0}): {1}",
    139                    &port, e
    140                ),
    141            }
    142        }
    143    }
    144 }
    145 
    146 impl AndroidHandler {
    147    pub fn new(
    148        options: &AndroidOptions,
    149        marionette_host_port: u16,
    150        system_access: bool,
    151        websocket_port: Option<u16>,
    152    ) -> Result<AndroidHandler> {
    153        // We need to push profile.pathbuf to a safe space on the device.
    154        // Make it per-Android package to avoid clashes and confusion.
    155        // This naming scheme follows GeckoView's configuration file naming scheme,
    156        // see bug 1533385.
    157 
    158        let host = Host {
    159            host: None,
    160            port: None,
    161            read_timeout: Some(time::Duration::from_millis(5000)),
    162            write_timeout: Some(time::Duration::from_millis(5000)),
    163        };
    164 
    165        let mut device = host.device_or_default(options.device_serial.as_ref(), options.storage)?;
    166 
    167        // Set up port forwarding for Marionette.
    168        debug!(
    169            "Start forwarding Marionette port ({} -> {})",
    170            marionette_host_port, MARIONETTE_TARGET_PORT
    171        );
    172        device.forward_port(marionette_host_port, MARIONETTE_TARGET_PORT)?;
    173 
    174        if let Some(port) = websocket_port {
    175            // Set up port forwarding for WebSocket connections (WebDriver BiDi, and CDP).
    176            debug!("Start forwarding WebSocket port ({} -> {})", port, port);
    177            device.forward_port(port, port)?;
    178        }
    179 
    180        let test_root = match device.storage {
    181            AndroidStorage::App => {
    182                device.run_as_package = Some(options.package.to_owned());
    183                let mut buf = UnixPathBuf::from("/data/data");
    184                buf.push(&options.package);
    185                buf.push("test_root");
    186                buf
    187            }
    188            AndroidStorage::Internal => UnixPathBuf::from("/data/local/tmp/test_root"),
    189            AndroidStorage::Sdcard => {
    190                // We need to push the profile to a location on the device that can also
    191                // be read and write by the application, and works for unrooted devices.
    192                // The only location that meets this criteria is under:
    193                //     $EXTERNAL_STORAGE/Android/data/%options.package%/files
    194                let response = device.execute_host_shell_command("echo $EXTERNAL_STORAGE")?;
    195                let mut buf = UnixPathBuf::from(response.trim_end_matches('\n'));
    196                buf.push("Android/data");
    197                buf.push(&options.package);
    198                buf.push("files/test_root");
    199                buf
    200            }
    201        };
    202 
    203        debug!(
    204            "Connecting: options={:?}, storage={:?}) test_root={}, run_as_package={:?}",
    205            options,
    206            device.storage,
    207            test_root.display(),
    208            device.run_as_package
    209        );
    210 
    211        let mut profile = test_root.clone();
    212        profile.push(format!("{}-geckodriver-profile", &options.package));
    213 
    214        // Check if the specified package is installed
    215        let response =
    216            device.execute_host_shell_command(&format!("pm list packages {}", &options.package))?;
    217        let mut packages = response
    218            .trim()
    219            .split_terminator('\n')
    220            .filter(|line| line.starts_with("package:"))
    221            .map(|line| line.rsplit(':').next().expect("Package name found"));
    222        if !packages.any(|x| x == options.package.as_str()) {
    223            return Err(AndroidError::PackageNotFound(options.package.clone()));
    224        }
    225 
    226        let config = UnixPathBuf::from(format!(
    227            "/data/local/tmp/{}-geckoview-config.yaml",
    228            &options.package
    229        ));
    230 
    231        // If activity hasn't been specified default to the main activity of the package
    232        let activity = match options.activity {
    233            Some(ref activity) => activity.clone(),
    234            None => {
    235                let response = device.execute_host_shell_command(&format!(
    236                    "cmd package resolve-activity --brief {}",
    237                    &options.package
    238                ))?;
    239                let activities = response
    240                    .split_terminator('\n')
    241                    .filter(|line| line.starts_with(&options.package))
    242                    .map(|line| line.rsplit('/').next().unwrap())
    243                    .collect::<Vec<&str>>();
    244                if activities.is_empty() {
    245                    return Err(AndroidError::ActivityNotFound(options.package.clone()));
    246                }
    247 
    248                activities[0].to_owned()
    249            }
    250        };
    251 
    252        let process = AndroidProcess::new(device, options.package.clone(), activity)?;
    253 
    254        Ok(AndroidHandler {
    255            config,
    256            process,
    257            profile,
    258            test_root,
    259            marionette_host_port,
    260            marionette_target_port: MARIONETTE_TARGET_PORT,
    261            options: options.clone(),
    262            system_access,
    263            websocket_port,
    264        })
    265    }
    266 
    267    pub fn copy_minidumps_files(&self, save_path: &str) -> Result<()> {
    268        let minidumps_path = self.profile.join("minidumps");
    269 
    270        match self.process.device.list_dir(&minidumps_path) {
    271            Ok(entries) => {
    272                for entry in entries {
    273                    if let RemoteMetadata::RemoteFile(_) = entry.metadata {
    274                        let file_path = minidumps_path.join(&entry.name);
    275 
    276                        let extension = file_path
    277                            .extension()
    278                            .and_then(|ext| ext.to_str())
    279                            .map(|ext| ext.to_lowercase())
    280                            .unwrap_or(String::from(""));
    281 
    282                        if extension == "dmp" || extension == "extra" {
    283                            let mut dest_path = PathBuf::from(save_path);
    284                            dest_path.push(&entry.name);
    285 
    286                            self.process
    287                                .device
    288                                .pull(&file_path, &mut File::create(dest_path.as_path())?)?;
    289 
    290                            debug!(
    291                                "Copied minidump file {:?} from the device to the local path {:?}.",
    292                                entry.name, save_path
    293                            );
    294                        }
    295                    }
    296                }
    297            }
    298            Err(_) => {
    299                warn!(
    300                    "Couldn't read files from minidumps folder '{}'",
    301                    minidumps_path.display(),
    302                );
    303 
    304                return Ok(());
    305            }
    306        }
    307 
    308        Ok(())
    309    }
    310 
    311    pub fn generate_config_file<I, K, V>(
    312        &self,
    313        args: Option<Vec<String>>,
    314        envs: I,
    315    ) -> Result<String>
    316    where
    317        I: IntoIterator<Item = (K, V)>,
    318        K: ToString,
    319        V: ToString,
    320    {
    321        // To configure GeckoView, we use the automation techniques documented at
    322        // https://mozilla.github.io/geckoview/consumer/docs/automation.
    323 
    324        let args = {
    325            let mut args_yaml = Vec::from([
    326                "--marionette".into(),
    327                "--profile".into(),
    328                self.profile.display().to_string(),
    329            ]);
    330 
    331            if self.system_access {
    332                args_yaml.push("--remote-allow-system-access".into());
    333            }
    334            args_yaml.append(&mut args.unwrap_or_default());
    335            args_yaml.into_iter().map(Yaml::String).collect()
    336        };
    337 
    338        let mut env = Hash::new();
    339 
    340        for (key, value) in envs {
    341            env.insert(
    342                Yaml::String(key.to_string()),
    343                Yaml::String(value.to_string()),
    344            );
    345        }
    346 
    347        env.insert(
    348            Yaml::String("MOZ_CRASHREPORTER".to_owned()),
    349            Yaml::String("1".to_owned()),
    350        );
    351        env.insert(
    352            Yaml::String("MOZ_CRASHREPORTER_NO_REPORT".to_owned()),
    353            Yaml::String("1".to_owned()),
    354        );
    355        env.insert(
    356            Yaml::String("MOZ_CRASHREPORTER_SHUTDOWN".to_owned()),
    357            Yaml::String("1".to_owned()),
    358        );
    359 
    360        let config_yaml = {
    361            let mut config = Hash::new();
    362            config.insert(Yaml::String("env".into()), Yaml::Hash(env));
    363            config.insert(Yaml::String("args".into()), Yaml::Array(args));
    364 
    365            let mut yaml = String::new();
    366            let mut emitter = yaml_rust::YamlEmitter::new(&mut yaml);
    367            emitter.dump(&Yaml::Hash(config))?;
    368            yaml
    369        };
    370 
    371        Ok([CONFIG_FILE_HEADING, &*config_yaml].concat())
    372    }
    373 
    374    pub fn prepare<I, K, V>(
    375        &self,
    376        profile: &Profile,
    377        args: Option<Vec<String>>,
    378        env: I,
    379    ) -> Result<()>
    380    where
    381        I: IntoIterator<Item = (K, V)>,
    382        K: ToString,
    383        V: ToString,
    384    {
    385        self.process.device.clear_app_data(&self.process.package)?;
    386 
    387        // These permissions, at least, are required to read profiles in /mnt/sdcard.
    388        for perm in &["READ_EXTERNAL_STORAGE", "WRITE_EXTERNAL_STORAGE"] {
    389            self.process.device.execute_host_shell_command(&format!(
    390                "pm grant {} android.permission.{}",
    391                &self.process.package, perm
    392            ))?;
    393        }
    394 
    395        // Make sure to create the test root.
    396        self.process.device.create_dir(&self.test_root)?;
    397        self.process.device.chmod(&self.test_root, "777", true)?;
    398 
    399        // Replace the profile
    400        self.process.device.remove(&self.profile)?;
    401        self.process
    402            .device
    403            .push_dir(&profile.path, &self.profile, 0o777)?;
    404 
    405        let contents = self.generate_config_file(args, env)?;
    406        debug!("Content of generated GeckoView config file:\n{}", contents);
    407        let reader = &mut io::BufReader::new(contents.as_bytes());
    408 
    409        debug!(
    410            "Pushing GeckoView configuration file to {}",
    411            self.config.display()
    412        );
    413        self.process.device.push(reader, &self.config, 0o777)?;
    414 
    415        // Tell GeckoView to read configuration even when `android:debuggable="false"`.
    416        self.process.device.execute_host_shell_command(&format!(
    417            "am set-debug-app --persistent {}",
    418            self.process.package
    419        ))?;
    420 
    421        Ok(())
    422    }
    423 
    424    pub fn launch(&self) -> Result<()> {
    425        // TODO: Remove the usage of intent arguments once Fennec is no longer
    426        // supported. Packages which are using GeckoView always read the arguments
    427        // via the YAML configuration file.
    428        let mut intent_arguments = self
    429            .options
    430            .intent_arguments
    431            .clone()
    432            .unwrap_or_else(|| Vec::with_capacity(3));
    433        intent_arguments.push("--es".to_owned());
    434        intent_arguments.push("args".to_owned());
    435        intent_arguments.push(format!("--marionette --profile {}", self.profile.display()));
    436 
    437        debug!(
    438            "Launching {}/{}",
    439            self.process.package, self.process.activity
    440        );
    441 
    442        // A counter to how many times to try launching the package.
    443        let max_start_attempts = 2;
    444        let mut n = 0;
    445 
    446        loop {
    447            match self.process.device.launch(
    448                &self.process.package,
    449                &self.process.activity,
    450                &intent_arguments,
    451            ) {
    452                Ok(_) => break,
    453                Err(e) => {
    454                    n += 1;
    455                    if n < max_start_attempts
    456                        && e.to_string().contains("Resource temporarily unavailable")
    457                    {
    458                        debug!(
    459                            "Failed the {} attempt to launch Android {}/{}: {}, wait for 2 seconds and try starting again",
    460                            n, self.process.package, self.process.activity, e
    461                        );
    462 
    463                        std::thread::sleep(std::time::Duration::from_secs(2));
    464 
    465                        continue;
    466                    } else {
    467                        let message = format!(
    468                            "Could not launch Android {}/{}: {}",
    469                            self.process.package, self.process.activity, e
    470                        );
    471                        return Err(AndroidError::from(mozdevice::DeviceError::Adb(message)));
    472                    }
    473                }
    474            }
    475        }
    476 
    477        Ok(())
    478    }
    479 
    480    pub fn push_as_file(&self, content: &[u8], path: &str) -> Result<String> {
    481        let mut dest = self.test_root.clone();
    482        dest.push(path);
    483 
    484        let buffer = &mut io::Cursor::new(content);
    485        self.process.device.push(buffer, &dest, 0o777)?;
    486 
    487        Ok(dest.display().to_string())
    488    }
    489 
    490    pub fn force_stop(&self) -> Result<()> {
    491        debug!(
    492            "Force stopping the Android package: {}",
    493            &self.process.package
    494        );
    495        self.process.device.force_stop(&self.process.package)?;
    496 
    497        Ok(())
    498    }
    499 }
    500 
    501 #[cfg(test)]
    502 mod test {
    503    // To successfully run those tests the geckoview_example package needs to
    504    // be installed on the device or emulator. After setting up the build
    505    // environment (https://mzl.la/3muLv5M), the following mach commands have to
    506    // be executed:
    507    //
    508    //     $ ./mach build && ./mach install
    509    //
    510    // Currently the mozdevice API is not safe for multiple requests at the same
    511    // time. It is recommended to run each of the unit tests on its own. Also adb
    512    // specific tests cannot be run in CI yet. To check those locally, also run
    513    // the ignored tests.
    514    //
    515    // Use the following command to accomplish that:
    516    //
    517    //     $ cargo test -- --ignored --test-threads=1
    518 
    519    use crate::android::AndroidHandler;
    520    use crate::capabilities::AndroidOptions;
    521    use mozdevice::{AndroidStorage, AndroidStorageInput, UnixPathBuf};
    522 
    523    fn run_handler_storage_test(package: &str, storage: AndroidStorageInput) {
    524        let options = AndroidOptions::new(package.to_owned(), storage);
    525        let handler =
    526            AndroidHandler::new(&options, 4242, true, None).expect("has valid Android handler");
    527 
    528        assert_eq!(handler.options, options);
    529        assert_eq!(handler.marionette_host_port, 4242);
    530        assert_eq!(handler.process.package, package);
    531        assert_eq!(handler.system_access, true);
    532        assert_eq!(handler.websocket_port, None);
    533 
    534        let expected_config_path = UnixPathBuf::from(format!(
    535            "/data/local/tmp/{}-geckoview-config.yaml",
    536            &package
    537        ));
    538        assert_eq!(handler.config, expected_config_path);
    539 
    540        if handler.process.device.storage == AndroidStorage::App {
    541            assert_eq!(
    542                handler.process.device.run_as_package,
    543                Some(package.to_owned())
    544            );
    545        } else {
    546            assert_eq!(handler.process.device.run_as_package, None);
    547        }
    548 
    549        let test_root = match handler.process.device.storage {
    550            AndroidStorage::App => {
    551                let mut buf = UnixPathBuf::from("/data/data");
    552                buf.push(package);
    553                buf.push("test_root");
    554                buf
    555            }
    556            AndroidStorage::Internal => UnixPathBuf::from("/data/local/tmp/test_root"),
    557            AndroidStorage::Sdcard => {
    558                let response = handler
    559                    .process
    560                    .device
    561                    .execute_host_shell_command("echo $EXTERNAL_STORAGE")
    562                    .unwrap();
    563 
    564                let mut buf = UnixPathBuf::from(response.trim_end_matches('\n'));
    565                buf.push("Android/data/");
    566                buf.push(package);
    567                buf.push("files/test_root");
    568                buf
    569            }
    570        };
    571        assert_eq!(handler.test_root, test_root);
    572 
    573        let mut profile = test_root;
    574        profile.push(format!("{}-geckodriver-profile", &package));
    575        assert_eq!(handler.profile, profile);
    576    }
    577 
    578    #[test]
    579    #[ignore]
    580    fn android_handler_storage_as_app() {
    581        let package = "org.mozilla.geckoview_example";
    582        run_handler_storage_test(package, AndroidStorageInput::App);
    583    }
    584 
    585    #[test]
    586    #[ignore]
    587    fn android_handler_storage_as_auto() {
    588        let package = "org.mozilla.geckoview_example";
    589        run_handler_storage_test(package, AndroidStorageInput::Auto);
    590    }
    591 
    592    #[test]
    593    #[ignore]
    594    fn android_handler_storage_as_internal() {
    595        let package = "org.mozilla.geckoview_example";
    596        run_handler_storage_test(package, AndroidStorageInput::Internal);
    597    }
    598 
    599    #[test]
    600    #[ignore]
    601    fn android_handler_storage_as_sdcard() {
    602        let package = "org.mozilla.geckoview_example";
    603        run_handler_storage_test(package, AndroidStorageInput::Sdcard);
    604    }
    605 }