aboutsummaryrefslogtreecommitdiff
path: root/src/remote/saved.rs
diff options
context:
space:
mode:
Diffstat (limited to 'src/remote/saved.rs')
-rw-r--r--src/remote/saved.rs136
1 files changed, 136 insertions, 0 deletions
diff --git a/src/remote/saved.rs b/src/remote/saved.rs
new file mode 100644
index 00000000..47684758
--- /dev/null
+++ b/src/remote/saved.rs
@@ -0,0 +1,136 @@
+use std::io;
+use std::path::PathBuf;
+
+use super::attach::{find_installed_remote_herdr, RemoteSsh, SshStdioBridge};
+
+pub(crate) struct SavedSshBridge {
+ _bridge: SshStdioBridge,
+}
+
+pub(crate) struct SavedSshStream {
+ pub(crate) stream: crate::ipc::LocalStream,
+ pub(crate) bridge: SavedSshBridge,
+}
+
+pub(crate) fn connect_saved_ssh(
+ profile_id: &str,
+ target: &str,
+ session: &str,
+) -> io::Result<SavedSshStream> {
+ validate_profile_path_id(profile_id)?;
+ crate::session::validate_name(session)
+ .map_err(|error| io::Error::new(io::ErrorKind::InvalidInput, error))?;
+ let ssh = RemoteSsh::new_noninteractive(target.to_owned());
+ let remote_herdr = find_installed_remote_herdr(&ssh)?;
+ let path = saved_bridge_path(profile_id);
+ let bridge = SshStdioBridge::start(
+ target.to_owned(),
+ remote_herdr,
+ path.clone(),
+ session.to_owned(),
+ ssh.options(),
+ true,
+ )?;
+ let stream = crate::ipc::connect_local_stream(&path)?;
+ Ok(SavedSshStream {
+ stream,
+ bridge: SavedSshBridge { _bridge: bridge },
+ })
+}
+
+pub(crate) fn saved_ssh_bootstrap_command(target: &str, session: &str) -> String {
+ format!(
+ "herdr --remote {} --session {} --handoff",
+ super::shell_quote(target),
+ super::shell_quote(session)
+ )
+}
+
+pub(crate) fn saved_ssh_failure_needs_attention(error: &io::Error) -> bool {
+ if matches!(
+ error.kind(),
+ io::ErrorKind::InvalidInput
+ | io::ErrorKind::InvalidData
+ | io::ErrorKind::NotFound
+ | io::ErrorKind::PermissionDenied
+ | io::ErrorKind::Unsupported
+ ) {
+ return true;
+ }
+ let message = error.to_string().to_ascii_lowercase();
+ [
+ "permission denied",
+ "host key verification failed",
+ "remote host identification has changed",
+ "no matching host key",
+ "unsupported remote platform",
+ "not ready",
+ "install or update",
+ "protocol",
+ "handshake",
+ ]
+ .iter()
+ .any(|needle| message.contains(needle))
+}
+
+fn saved_bridge_path(profile_id: &str) -> PathBuf {
+ let pid = std::process::id();
+ let readable = format!("herdr-ssh-{pid}-{profile_id}.sock");
+ let short = format!("herdr-s-{pid}-{}.sock", &profile_id[..16]);
+ crate::platform::remote_bridge_endpoint_path(&readable, &short)
+}
+
+fn validate_profile_path_id(profile_id: &str) -> io::Result<()> {
+ if profile_id.len() == 32
+ && profile_id
+ .bytes()
+ .all(|byte| byte.is_ascii_digit() || (b'a'..=b'f').contains(&byte))
+ {
+ Ok(())
+ } else {
+ Err(io::Error::new(
+ io::ErrorKind::InvalidInput,
+ "invalid SSH endpoint profile id",
+ ))
+ }
+}
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ #[test]
+ fn bridge_paths_use_profile_identity_not_target_or_session() {
+ let first = saved_bridge_path("0123456789abcdef0123456789abcdef");
+ let second = saved_bridge_path("fedcba9876543210fedcba9876543210");
+ assert_ne!(first, second);
+ assert!(!first.to_string_lossy().contains("example.com"));
+ assert!(!first.to_string_lossy().contains("default"));
+ }
+
+ #[test]
+ fn bootstrap_command_preserves_the_explicit_remote_session() {
+ assert_eq!(
+ saved_ssh_bootstrap_command("build host", "agent work"),
+ "herdr --remote 'build host' --session 'agent work' --handoff"
+ );
+ }
+
+ #[test]
+ fn prompt_and_compatibility_failures_require_attention() {
+ for message in [
+ "Permission denied (publickey)",
+ "Host key verification failed",
+ "matching Herdr is not ready; install or update",
+ "handshake rejected",
+ ] {
+ assert!(saved_ssh_failure_needs_attention(&io::Error::other(
+ message
+ )));
+ }
+ assert!(!saved_ssh_failure_needs_attention(&io::Error::new(
+ io::ErrorKind::TimedOut,
+ "network timed out"
+ )));
+ }
+}