diff options
| author | Markus Pettersson <markus.pettersson@mullvad.net> | 2024-08-07 11:00:15 +0200 |
|---|---|---|
| committer | Markus Pettersson <markus.pettersson@mullvad.net> | 2024-08-12 15:25:00 +0200 |
| commit | e593ca40447eda3045089eb92fb4264ff6773120 (patch) | |
| tree | d826bfa9fc3568f6e5a8c529b28a8ec2c7499dcc /test | |
| parent | 042f2f04d5b0a6dc172610cf81276b9bb28e9456 (diff) | |
| download | mullvadvpn-e593ca40447eda3045089eb92fb4264ff6773120.tar.xz mullvadvpn-e593ca40447eda3045089eb92fb4264ff6773120.zip | |
Replace OpenVPN CA certificate using CLI flag
Diffstat (limited to 'test')
| -rw-r--r-- | test/assets/openvpn.ca.crt (renamed from test/openvpn.ca.crt) | 0 | ||||
| -rwxr-xr-x | test/scripts/build-runner-image.sh | 1 | ||||
| -rw-r--r-- | test/scripts/ssh-setup.sh | 2 | ||||
| -rw-r--r-- | test/test-manager/src/config.rs | 12 | ||||
| -rw-r--r-- | test/test-manager/src/main.rs | 45 | ||||
| -rw-r--r-- | test/test-manager/src/tests/config.rs | 88 | ||||
| -rw-r--r-- | test/test-manager/src/tests/install.rs | 37 | ||||
| -rw-r--r-- | test/test-manager/src/tests/mod.rs | 12 | ||||
| -rw-r--r-- | test/test-manager/src/vm/mod.rs | 8 | ||||
| -rw-r--r-- | test/test-manager/src/vm/network/linux.rs | 2 | ||||
| -rw-r--r-- | test/test-manager/src/vm/network/macos.rs | 2 | ||||
| -rw-r--r-- | test/test-manager/src/vm/network/mod.rs | 10 | ||||
| -rw-r--r-- | test/test-manager/src/vm/provision.rs | 103 |
13 files changed, 214 insertions, 108 deletions
diff --git a/test/openvpn.ca.crt b/test/assets/openvpn.ca.crt index 4e04d2cb1a..4e04d2cb1a 100644 --- a/test/openvpn.ca.crt +++ b/test/assets/openvpn.ca.crt diff --git a/test/scripts/build-runner-image.sh b/test/scripts/build-runner-image.sh index 4aec7b0439..512dc93a4c 100755 --- a/test/scripts/build-runner-image.sh +++ b/test/scripts/build-runner-image.sh @@ -35,7 +35,6 @@ case $TARGET in "${SCRIPT_DIR}/../target/$TARGET/release/test-runner.exe" \ "${SCRIPT_DIR}/../target/$TARGET/release/connection-checker.exe" \ "${PACKAGE_DIR}/"*.exe \ - "${SCRIPT_DIR}/../openvpn.ca.crt" \ "::" mdir -i "${TEST_RUNNER_IMAGE_PATH}" ;; diff --git a/test/scripts/ssh-setup.sh b/test/scripts/ssh-setup.sh index 7eb8ab56d3..41131169e0 100644 --- a/test/scripts/ssh-setup.sh +++ b/test/scripts/ssh-setup.sh @@ -16,7 +16,7 @@ echo "Copying test-runner to $RUNNER_DIR" mkdir -p "$RUNNER_DIR" -for file in test-runner connection-checker $APP_PACKAGE $PREVIOUS_APP $UI_RUNNER openvpn.ca.crt; do +for file in test-runner connection-checker $APP_PACKAGE $PREVIOUS_APP $UI_RUNNER; do echo "Moving $file to $RUNNER_DIR" cp -f "$SCRIPT_DIR/$file" "$RUNNER_DIR" done diff --git a/test/test-manager/src/config.rs b/test/test-manager/src/config.rs index 585e6c75df..7b74c48dcf 100644 --- a/test/test-manager/src/config.rs +++ b/test/test-manager/src/config.rs @@ -8,6 +8,8 @@ use std::{ path::{Path, PathBuf}, }; +use crate::tests::config::DEFAULT_MULLVAD_HOST; + #[derive(thiserror::Error, Debug)] pub enum Error { #[error("Could not find config dir")] @@ -67,6 +69,16 @@ impl Config { pub fn get_vm(&self, name: &str) -> Option<&VmConfig> { self.vms.get(name) } + + /// Get the Mullvad host to use. + /// + /// Defaults to [`DEFAULT_MULLVAD_HOST`] if the host was not provided in the [`ConfigFile`]. + pub fn get_host(&self) -> String { + self.mullvad_host.clone().unwrap_or_else(|| { + log::debug!("No Mullvad host has been set explicitly. Falling back to default host"); + DEFAULT_MULLVAD_HOST.to_owned() + }) + } } pub struct ConfigFile { diff --git a/test/test-manager/src/main.rs b/test/test-manager/src/main.rs index daad04983b..b26fa02fe9 100644 --- a/test/test-manager/src/main.rs +++ b/test/test-manager/src/main.rs @@ -9,12 +9,12 @@ mod summary; mod tests; mod vm; -use std::path::PathBuf; +use std::{net::SocketAddr, path::PathBuf}; use anyhow::{Context, Result}; use clap::Parser; -use std::net::SocketAddr; -use tests::config::DEFAULT_MULLVAD_HOST; + +use crate::tests::config::OpenVPNCertificate; /// Test manager for Mullvad VPN app #[derive(Parser, Debug)] @@ -110,6 +110,12 @@ enum Commands { #[arg(long, value_name = "DIR")] package_dir: Option<PathBuf>, + /// OpenVPN CA certificate to use with the app under test. The expected argument is a path + /// (absolut or relative) to the desired CA certificate. The default certificate is + /// `assets/openvpn.ca.crt`. + #[arg(long)] + openvpn_certificate: Option<PathBuf>, + /// Only run tests matching substrings test_filters: Vec<String>, @@ -228,6 +234,7 @@ async fn main() -> Result<()> { app_package_to_upgrade_from, gui_package, package_dir, + openvpn_certificate, test_filters, verbose, test_report, @@ -240,10 +247,7 @@ async fn main() -> Result<()> { (true, true) => unreachable!("invalid combination"), }; - let mullvad_host = config - .mullvad_host - .clone() - .unwrap_or(DEFAULT_MULLVAD_HOST.to_owned()); + let mullvad_host = config.get_host(); log::debug!("Mullvad host: {mullvad_host}"); let vm_config = vm::get_vm_config(&config, &vm).context("Cannot get VM config")?; @@ -270,6 +274,13 @@ async fn main() -> Result<()> { ) .context("Could not find the specified app packages")?; + // Load a new OpenVPN CA certificate if the user provided a path. + let openvpn_certificate = openvpn_certificate + .map(OpenVPNCertificate::from_file) + .transpose() + .context("Could not find OpenVPN CA certificate")? + .unwrap_or_default(); + let mut instance = vm::run(&config, &vm).await.context("Failed to start VM")?; let artifacts_dir = vm::provision(&config, &vm, &*instance, &manifest) .await @@ -285,28 +296,26 @@ async fn main() -> Result<()> { let skip_wait = vm_config.provisioner != config::Provisioner::Noop; let result = run_tests::run( - tests::config::TestConfig { - account_number: account, + tests::config::TestConfig::new( + account, artifacts_dir, - app_package_filename: manifest + manifest .app_package_path .file_name() .unwrap() .to_string_lossy() .into_owned(), - app_package_to_upgrade_from_filename: manifest + manifest .app_package_to_upgrade_from_path .map(|path| path.file_name().unwrap().to_string_lossy().into_owned()), - ui_e2e_tests_filename: manifest + manifest .gui_package_path .map(|path| path.file_name().unwrap().to_string_lossy().into_owned()), mullvad_host, - #[cfg(target_os = "macos")] - host_bridge_name: crate::vm::network::macos::find_vm_bridge()?, - #[cfg(not(target_os = "macos"))] - host_bridge_name: crate::vm::network::linux::BRIDGE_NAME.to_owned(), - os: test_rpc::meta::Os::from(vm_config.os_type), - }, + vm::network::bridge()?, + test_rpc::meta::Os::from(vm_config.os_type), + openvpn_certificate, + ), &*instance, &test_filters, skip_wait, diff --git a/test/test-manager/src/tests/config.rs b/test/test-manager/src/tests/config.rs index 58ebc4fa01..ae2f434698 100644 --- a/test/test-manager/src/tests/config.rs +++ b/test/test-manager/src/tests/config.rs @@ -1,9 +1,21 @@ use once_cell::sync::OnceCell; -use std::ops::Deref; +use std::{ops::Deref, path::Path}; use test_rpc::meta::Os; -// Default `mullvad_host`. This should match the production env. +/// Default `mullvad_host`. This should match the production env. pub const DEFAULT_MULLVAD_HOST: &str = "mullvad.net"; +/// Bundled OpenVPN CA certificate use with the installed Mullvad app. +const OPENVPN_CA_CERTIFICATE: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../assets/", + "openvpn.ca.crt" +)); +/// Script for bootstrapping the test-runner after the test-manager has successfully logged in. +pub const BOOTSTRAP_SCRIPT: &[u8] = include_bytes!(concat!( + env!("CARGO_MANIFEST_DIR"), + "/../scripts/", + "ssh-setup.sh" +)); /// Constants that are accessible from each test via `TEST_CONFIG`. /// The constants must be initialized before running any tests using `TEST_CONFIG.init()`. @@ -23,6 +35,78 @@ pub struct TestConfig { pub host_bridge_name: String, pub os: Os, + /// The OpenVPN CA certificate to use with the the installed Mullvad App. + pub openvpn_certificate: OpenVPNCertificate, +} + +impl TestConfig { + #[allow(clippy::too_many_arguments)] + // TODO: This argument list is very long, we should strive to shorten it if possible. + pub const fn new( + account_number: String, + artifacts_dir: String, + app_package_filename: String, + app_package_to_upgrade_from_filename: Option<String>, + ui_e2e_tests_filename: Option<String>, + mullvad_host: String, + host_bridge_name: String, + os: Os, + openvpn_certificate: OpenVPNCertificate, + ) -> Self { + Self { + account_number, + artifacts_dir, + app_package_filename, + app_package_to_upgrade_from_filename, + ui_e2e_tests_filename, + mullvad_host, + host_bridge_name, + os, + openvpn_certificate, + } + } +} + +/// The OpenVPN CA certificate to use with the installed Mullvad App. +#[derive(Clone, Debug)] +pub struct OpenVPNCertificate(Vec<u8>); + +impl OpenVPNCertificate { + pub fn from_file(path: impl AsRef<Path>) -> std::io::Result<Self> { + Ok(Self(std::fs::read(path)?)) + } +} + +impl Deref for OpenVPNCertificate { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Default for OpenVPNCertificate { + fn default() -> Self { + Self(Vec::from(OPENVPN_CA_CERTIFICATE)) + } +} + +/// A script which should be run *in* the test runner before the test run begins. +#[derive(Clone, Debug)] +pub struct BootstrapScript(Vec<u8>); + +impl Deref for BootstrapScript { + type Target = [u8]; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl Default for BootstrapScript { + fn default() -> Self { + Self(Vec::from(BOOTSTRAP_SCRIPT)) + } } #[derive(Debug, Clone)] diff --git a/test/test-manager/src/tests/install.rs b/test/test-manager/src/tests/install.rs index 0c9e2b82fd..b92f68b413 100644 --- a/test/test-manager/src/tests/install.rs +++ b/test/test-manager/src/tests/install.rs @@ -1,5 +1,5 @@ use anyhow::{bail, Context}; -use std::time::Duration; +use std::{path::Path, time::Duration}; use mullvad_management_interface::MullvadProxyClient; use mullvad_types::{constraints::Constraint, relay_constraints}; @@ -29,14 +29,14 @@ pub async fn test_install_previous_app(_: TestContext, rpc: ServiceClient) -> an .context("Missing previous app version")?, )?) .await?; + log::debug!("Replacing the OpenVPN CA certificate"); + replace_openvpn_certificate(&rpc).await?; // verify that daemon is running if rpc.mullvad_daemon_get_status().await? != ServiceStatus::Running { bail!(Error::DaemonNotRunning); } - replace_openvpn_cert(&rpc).await?; - // Override env vars rpc.set_daemon_environment(get_app_env().await?).await?; @@ -270,7 +270,7 @@ pub async fn test_install_new_app(_: TestContext, rpc: ServiceClient) -> anyhow: rpc.set_daemon_log_level(test_rpc::mullvad_daemon::Verbosity::Trace) .await?; - replace_openvpn_cert(&rpc).await?; + replace_openvpn_certificate(&rpc).await?; // Override env vars rpc.set_daemon_environment(get_app_env().await?).await?; @@ -348,10 +348,9 @@ pub async fn test_installation_idempotency( Ok(()) } -async fn replace_openvpn_cert(rpc: &ServiceClient) -> Result<(), Error> { - use std::path::Path; - - const SOURCE_CERT_FILENAME: &str = "openvpn.ca.crt"; +/// Replace the OpenVPN CA certificate which is currently used by the installed Mullvad App. +/// This needs to be invoked after reach (re)installation to use the custom OpenVPN certificate. +async fn replace_openvpn_certificate(rpc: &ServiceClient) -> Result<(), Error> { const DEST_CERT_FILENAME: &str = "ca.crt"; let dest_dir = match TEST_CONFIG.os { @@ -360,18 +359,12 @@ async fn replace_openvpn_cert(rpc: &ServiceClient) -> Result<(), Error> { Os::Macos => "/Applications/Mullvad VPN.app/Contents/Resources", }; - rpc.copy_file( - Path::new(&TEST_CONFIG.artifacts_dir) - .join(SOURCE_CERT_FILENAME) - .as_os_str() - .to_string_lossy() - .into_owned(), - Path::new(dest_dir) - .join(DEST_CERT_FILENAME) - .as_os_str() - .to_string_lossy() - .into_owned(), - ) - .await - .map_err(Error::Rpc) + let dest = Path::new(dest_dir) + .join(DEST_CERT_FILENAME) + .as_os_str() + .to_string_lossy() + .into_owned(); + rpc.write_file(dest, TEST_CONFIG.openvpn_certificate.to_vec()) + .await + .map_err(Error::Rpc) } diff --git a/test/test-manager/src/tests/mod.rs b/test/test-manager/src/tests/mod.rs index 0a9a6913df..312e8ae2b3 100644 --- a/test/test-manager/src/tests/mod.rs +++ b/test/test-manager/src/tests/mod.rs @@ -14,18 +14,18 @@ mod tunnel; mod tunnel_state; mod ui; +pub use test_metadata::TestMetadata; + +use anyhow::Context; +use futures::future::BoxFuture; +use std::time::Duration; + use crate::{ mullvad_daemon::{MullvadClientArgument, RpcClientProvider}, tests::helpers::get_app_env, }; -use anyhow::Context; -pub use test_metadata::TestMetadata; use test_rpc::ServiceClient; -use futures::future::BoxFuture; - -use std::time::Duration; - const WAIT_FOR_TUNNEL_STATE_TIMEOUT: Duration = Duration::from_secs(40); #[derive(Clone)] diff --git a/test/test-manager/src/vm/mod.rs b/test/test-manager/src/vm/mod.rs index a5c794a58a..6cb18c9c5f 100644 --- a/test/test-manager/src/vm/mod.rs +++ b/test/test-manager/src/vm/mod.rs @@ -1,13 +1,14 @@ +use anyhow::{Context, Result}; +use std::net::IpAddr; + use crate::{ config::{Config, ConfigFile, VmConfig, VmType}, package, }; -use anyhow::{Context, Result}; -use std::net::IpAddr; mod logging; pub mod network; -mod provision; +pub mod provision; mod qemu; mod ssh; #[cfg(target_os = "macos")] @@ -62,6 +63,7 @@ pub async fn run(config: &Config, name: &str) -> Result<Box<dyn VmInstance>> { Ok(instance) } +/// Returns the directory in the test runner where the test-runner binary is installed. pub async fn provision( config: &Config, name: &str, diff --git a/test/test-manager/src/vm/network/linux.rs b/test/test-manager/src/vm/network/linux.rs index 12095138f6..34e8438b92 100644 --- a/test/test-manager/src/vm/network/linux.rs +++ b/test/test-manager/src/vm/network/linux.rs @@ -21,7 +21,7 @@ pub const TEST_SUBNET_DHCP_FIRST: Ipv4Addr = Ipv4Addr::new(172, 29, 1, 2); pub const TEST_SUBNET_DHCP_LAST: Ipv4Addr = Ipv4Addr::new(172, 29, 1, 128); /// Bridge interface on the host -pub const BRIDGE_NAME: &str = "br-mullvadtest"; +pub(crate) const BRIDGE_NAME: &str = "br-mullvadtest"; /// TAP interface used by the guest pub const TAP_NAME: &str = "tap-mullvadtest"; diff --git a/test/test-manager/src/vm/network/macos.rs b/test/test-manager/src/vm/network/macos.rs index 78653df41c..35467144ec 100644 --- a/test/test-manager/src/vm/network/macos.rs +++ b/test/test-manager/src/vm/network/macos.rs @@ -53,7 +53,7 @@ pub async fn setup_test_network() -> Result<()> { /// A hack to find the Tart bridge interface using `NON_TUN_GATEWAY`. /// It should be possible to retrieve this using the virtualization framework instead, /// but that requires an entitlement. -pub fn find_vm_bridge() -> Result<String> { +pub(crate) fn find_vm_bridge() -> Result<String> { for addr in nix::ifaddrs::getifaddrs().unwrap() { if !addr.interface_name.starts_with("bridge") { continue; diff --git a/test/test-manager/src/vm/network/mod.rs b/test/test-manager/src/vm/network/mod.rs index de055376b0..a06227027b 100644 --- a/test/test-manager/src/vm/network/mod.rs +++ b/test/test-manager/src/vm/network/mod.rs @@ -17,3 +17,13 @@ pub use platform::{ /// Port on NON_TUN_GATEWAY that hosts a SOCKS5 server pub const SOCKS5_PORT: u16 = 54321; + +/// Get the name of the bridge interface between the test-manager and the test-runner. +pub fn bridge() -> anyhow::Result<String> { + #[cfg(target_os = "macos")] + { + crate::vm::network::macos::find_vm_bridge() + } + #[cfg(not(target_os = "macos"))] + Ok(platform::BRIDGE_NAME.to_owned()) +} diff --git a/test/test-manager/src/vm/provision.rs b/test/test-manager/src/vm/provision.rs index 0b7b88e766..067a4dd900 100644 --- a/test/test-manager/src/vm/provision.rs +++ b/test/test-manager/src/vm/provision.rs @@ -1,16 +1,17 @@ use crate::{ config::{OsType, Provisioner, VmConfig}, package, + tests::config::BOOTSTRAP_SCRIPT, }; use anyhow::{bail, Context, Result}; use ssh2::Session; use std::{ - fs::File, io::{self, Read}, net::{IpAddr, SocketAddr, TcpStream}, - path::Path, + path::{Path, PathBuf}, }; +/// Returns the directory in the test runner where the test-runner binary is installed. pub async fn provision( config: &VmConfig, instance: &dyn super::VmInstance, @@ -21,7 +22,7 @@ pub async fn provision( log::debug!("SSH provisioning"); let (user, password) = config.get_ssh_options().context("missing SSH config")?; - ssh( + provision_ssh( instance, config.os_type, &config.get_runner_dir(), @@ -42,7 +43,8 @@ pub async fn provision( } } -async fn ssh( +/// Returns the directory in the test runner where the test-runner binary is installed. +async fn provision_ssh( instance: &dyn super::VmInstance, os_type: OsType, local_runner_dir: &Path, @@ -89,8 +91,6 @@ fn blocking_ssh( ) -> Result<()> { // Directory that receives the payload. Any directory that the SSH user has access to. const REMOTE_TEMP_DIR: &str = "/tmp/"; - const SCRIPT_PAYLOAD: &[u8] = include_bytes!("../../../scripts/ssh-setup.sh"); - const OPENVPN_CERT: &[u8] = include_bytes!("../../../openvpn.ca.crt"); let temp_dir = Path::new(REMOTE_TEMP_DIR); @@ -106,55 +106,36 @@ fn blocking_ssh( // Transfer a test runner let source = local_runner_dir.join("test-runner"); - ssh_send_file_path(&session, &source, temp_dir) - .context("Failed to send test runner to remote")?; + ssh_send_file(&session, &source, temp_dir).context("Failed to send test runner to remote")?; // Transfer connection-checker let source = local_runner_dir.join("connection-checker"); - ssh_send_file_path(&session, &source, temp_dir) + ssh_send_file(&session, &source, temp_dir) .context("Failed to send connection-checker to remote")?; // Transfer app packages - ssh_send_file_path(&session, &local_app_manifest.app_package_path, temp_dir) + ssh_send_file(&session, &local_app_manifest.app_package_path, temp_dir) .context("Failed to send current app package to remote")?; if let Some(app_package_to_upgrade_from_path) = &local_app_manifest.app_package_to_upgrade_from_path { - ssh_send_file_path(&session, app_package_to_upgrade_from_path, temp_dir) + ssh_send_file(&session, app_package_to_upgrade_from_path, temp_dir) .context("Failed to send previous app package to remote")?; } else { log::warn!("No previous app to send to remote") } if let Some(gui_package_path) = &local_app_manifest.gui_package_path { - ssh_send_file_path(&session, gui_package_path, temp_dir) + ssh_send_file(&session, gui_package_path, temp_dir) .context("Failed to send gui_package_path to remote")?; } else { log::warn!("No UI e2e test to send to remote") } - // Transfer openvpn cert - let dest: std::path::PathBuf = temp_dir.join("openvpn.ca.crt"); - log::debug!("Copying remote openvpn.ca.crt -> {}", dest.display()); - #[allow(const_item_mutation)] - ssh_send_file( - &session, - &mut OPENVPN_CERT, - u64::try_from(OPENVPN_CERT.len()).expect("cert too long"), - &dest, - ) - .context("failed to send openvpn crt to remote")?; - // Transfer setup script - let dest = temp_dir.join("ssh-setup.sh"); - log::debug!("Copying remote setup script -> {}", dest.display()); - #[allow(const_item_mutation)] - ssh_send_file( - &session, - &mut SCRIPT_PAYLOAD, - u64::try_from(SCRIPT_PAYLOAD.len()).expect("script too long"), - &dest, - ) - .context("failed to send bootstrap script to remote")?; + // TODO: Move this name to a constant somewhere? + let bootstrap_script_dest = temp_dir.join("ssh-setup.sh"); + ssh_write(&session, &bootstrap_script_dest, BOOTSTRAP_SCRIPT) + .context("failed to send bootstrap script to remote")?; // Run setup script let app_package_path = local_app_manifest @@ -171,9 +152,10 @@ fn blocking_ssh( .map(|path| path.file_name().unwrap().to_string_lossy().into_owned()) .unwrap_or_default(); + // Run the setup script in the test runner let cmd = format!( - "sudo {} {remote_dir} \"{app_package_path}\" \"{app_package_to_upgrade_from_path}\" \"{gui_package_path}\"", - dest.display() + r#"sudo {} {remote_dir} "{app_package_path}" "{app_package_to_upgrade_from_path}" "{gui_package_path}""#, + bootstrap_script_dest.display(), ); log::debug!("Running setup script on remote, cmd: {cmd}"); ssh_exec(&session, &cmd) @@ -181,36 +163,51 @@ fn blocking_ssh( .context("Failed to run setup script") } -fn ssh_send_file_path(session: &Session, source: &Path, dest_dir: &Path) -> Result<()> { - let dest = dest_dir.join(source.file_name().context("Missing source file name")?); +/// Copy a `source` file to `dest_dir` in the test runner. +/// +/// Returns the aboslute path in the test runner where the file is stored. +fn ssh_send_file<P: AsRef<Path> + Copy>( + session: &Session, + source: P, + dest_dir: &Path, +) -> Result<PathBuf> { + let dest = dest_dir.join( + source + .as_ref() + .file_name() + .context("Missing source file name")?, + ); log::debug!( "Copying file to remote: {} -> {}", - source.display(), + source.as_ref().display(), dest.display(), ); - let mut file = - File::open(source).with_context(|| format!("Failed to open file at {source:?}"))?; - let file_len = file - .metadata() - .with_context(|| format!("Failed to get file size of {source:?}"))? - .len(); - ssh_send_file(session, &mut file, file_len, &dest) + let source = std::fs::read(source) + .with_context(|| format!("Failed to open file at {}", source.as_ref().display()))?; + + ssh_write(session, &dest, &source[..])?; + + Ok(dest) } -fn ssh_send_file<R: Read>( - session: &Session, - source: &mut R, - source_len: u64, - dest: &Path, -) -> Result<()> { - let mut remote_file = session.scp_send(dest, 0o744, source_len, None)?; +/// Analogues to [`std::fs::write`], but over ssh! +fn ssh_write<P: AsRef<Path>, C: AsRef<[u8]>>(session: &Session, dest: P, source: C) -> Result<()> { + let bytes = source.as_ref(); + + let source = &mut &bytes[..]; + let source_len = u64::try_from(bytes.len()).context("File too large, did not fit in a u64")?; + + let mut remote_file = session.scp_send(dest.as_ref(), 0o744, source_len, None)?; + io::copy(source, &mut remote_file).context("failed to write file")?; + remote_file.send_eof()?; remote_file.wait_eof()?; remote_file.close()?; remote_file.wait_close()?; + Ok(()) } |
