Skip to main content

ssh_browser/sftp/
transport.rs

1//! Transport is `ssh <host> -s sftp`.
2//!
3//! OpenSSH owns ssh_config, so ProxyJump, non-standard ports, agent keys and
4//! certificates work without reimplementation. rclone's sftp backend cannot read
5//! ssh_config (rclone#6987), which is why it fails on any host behind a jump box;
6//! borrowing the system ssh removes that whole class of bug.
7
8use std::process::Stdio;
9
10use anyhow::{Context, Result};
11use tokio::io::{BufReader, BufWriter};
12use tokio::process::{Child, ChildStdin, ChildStdout, Command};
13
14/// Large enough that a whole batch of requests lands in a single write.
15const WRITE_BUF: usize = 256 * 1024;
16
17/// Holds the ssh process. Dropping it kills the child, which closes both pipes and
18/// makes every pending caller fail rather than hang.
19pub struct SshChild(#[allow(dead_code)] Child);
20
21type Halves = (SshChild, BufWriter<ChildStdin>, BufReader<ChildStdout>);
22
23pub fn open(host: &str) -> Result<Halves> {
24    let mut child = Command::new("ssh")
25        .arg("-o")
26        .arg("BatchMode=yes")
27        .arg(host)
28        .arg("-s")
29        .arg("sftp")
30        .stdin(Stdio::piped())
31        .stdout(Stdio::piped())
32        .stderr(Stdio::inherit())
33        .kill_on_drop(true)
34        .spawn()
35        .context("spawn ssh: is the OpenSSH client on PATH?")?;
36
37    let stdin = child.stdin.take().context("ssh stdin was not piped")?;
38    let stdout = child.stdout.take().context("ssh stdout was not piped")?;
39
40    Ok((
41        SshChild(child),
42        BufWriter::with_capacity(WRITE_BUF, stdin),
43        BufReader::new(stdout),
44    ))
45}