224 lines
6.5 KiB
Rust
224 lines
6.5 KiB
Rust
#[cfg(unix)]
|
|
mod unix;
|
|
|
|
#[cfg(unix)]
|
|
pub(crate) use unix::*;
|
|
|
|
#[cfg(windows)]
|
|
pub(crate) const REATTACH_COMMAND_ENV_VAR: &str = "HERDR_REATTACH_COMMAND";
|
|
#[cfg(windows)]
|
|
pub(crate) const REMOTE_KEYBINDINGS_ENV_VAR: &str = "HERDR_REMOTE_KEYBINDINGS";
|
|
|
|
#[cfg(windows)]
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub(crate) enum RemoteKeybindings {
|
|
Local,
|
|
Server,
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
impl RemoteKeybindings {
|
|
fn parse(value: &str) -> Result<Self, String> {
|
|
match value {
|
|
"local" => Ok(Self::Local),
|
|
"server" => Ok(Self::Server),
|
|
_ => Err("--remote-keybindings must be 'local' or 'server'".to_string()),
|
|
}
|
|
}
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub(crate) struct RemoteLaunch {
|
|
pub(crate) target: String,
|
|
pub(crate) keybindings: RemoteKeybindings,
|
|
pub(crate) live_handoff: bool,
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub(crate) fn extract_remote_args(
|
|
args: &[String],
|
|
) -> Result<(Vec<String>, Option<RemoteLaunch>), String> {
|
|
let mut cleaned = Vec::with_capacity(args.len());
|
|
if let Some(program) = args.first() {
|
|
cleaned.push(program.clone());
|
|
}
|
|
|
|
let mut remote_target = None;
|
|
let mut keybindings = RemoteKeybindings::Local;
|
|
let mut keybindings_seen = false;
|
|
let mut live_handoff = false;
|
|
let mut index = 1;
|
|
while index < args.len() {
|
|
let arg = &args[index];
|
|
if arg == "--" {
|
|
cleaned.extend_from_slice(&args[index..]);
|
|
break;
|
|
}
|
|
if arg == "--handoff" {
|
|
live_handoff = true;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if arg == "--remote" {
|
|
if remote_target.is_some() {
|
|
return Err("--remote can only be specified once".to_string());
|
|
}
|
|
let Some(value) = args.get(index + 1) else {
|
|
return Err("missing value for --remote".to_string());
|
|
};
|
|
remote_target = Some(validate_remote_target(value)?.to_owned());
|
|
index += 2;
|
|
continue;
|
|
}
|
|
if let Some(value) = arg.strip_prefix("--remote=") {
|
|
if remote_target.is_some() {
|
|
return Err("--remote can only be specified once".to_string());
|
|
}
|
|
remote_target = Some(validate_remote_target(value)?.to_owned());
|
|
index += 1;
|
|
continue;
|
|
}
|
|
if arg == "--remote-keybindings" {
|
|
if keybindings_seen {
|
|
return Err("--remote-keybindings can only be specified once".to_string());
|
|
}
|
|
let Some(value) = args.get(index + 1) else {
|
|
return Err("missing value for --remote-keybindings".to_string());
|
|
};
|
|
keybindings = RemoteKeybindings::parse(value)?;
|
|
keybindings_seen = true;
|
|
index += 2;
|
|
continue;
|
|
}
|
|
if let Some(value) = arg.strip_prefix("--remote-keybindings=") {
|
|
if keybindings_seen {
|
|
return Err("--remote-keybindings can only be specified once".to_string());
|
|
}
|
|
keybindings = RemoteKeybindings::parse(value)?;
|
|
keybindings_seen = true;
|
|
index += 1;
|
|
continue;
|
|
}
|
|
|
|
cleaned.push(arg.clone());
|
|
index += 1;
|
|
}
|
|
|
|
let remote = remote_target.map(|target| RemoteLaunch {
|
|
target,
|
|
keybindings,
|
|
live_handoff,
|
|
});
|
|
if remote.is_none() && keybindings_seen {
|
|
return Err("--remote-keybindings requires --remote".to_string());
|
|
}
|
|
if remote.is_none() && live_handoff {
|
|
cleaned.push("--handoff".to_string());
|
|
}
|
|
|
|
Ok((cleaned, remote))
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
fn validate_remote_target(target: &str) -> Result<&str, String> {
|
|
if target.is_empty() {
|
|
return Err("missing value for --remote".to_string());
|
|
}
|
|
if target.starts_with('-') {
|
|
return Err("--remote target must not start with '-'".to_string());
|
|
}
|
|
Ok(target)
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub(crate) fn run_remote(_remote: RemoteLaunch) -> std::io::Result<()> {
|
|
debug_assert!(!crate::platform::capabilities().remote_attach);
|
|
Err(std::io::Error::other(
|
|
"remote mode is not supported on Windows yet",
|
|
))
|
|
}
|
|
|
|
#[cfg(windows)]
|
|
pub(crate) fn run_remote_client_bridge() -> std::io::Result<()> {
|
|
debug_assert!(!crate::platform::capabilities().remote_attach);
|
|
Err(std::io::Error::other(
|
|
"remote client bridge is not supported on Windows yet",
|
|
))
|
|
}
|
|
|
|
pub(crate) fn print_remote_error_hint(err: &std::io::Error, target: &str) {
|
|
if is_remote_auth_error(err) {
|
|
eprintln!(
|
|
"hint: verify SSH access first with `{}`.",
|
|
ssh_check_command(target)
|
|
);
|
|
eprintln!(
|
|
"hint: if your SSH key has a passphrase, load it into ssh-agent with `ssh-add` before running `herdr --remote`."
|
|
);
|
|
}
|
|
}
|
|
|
|
fn is_remote_auth_error(err: &std::io::Error) -> bool {
|
|
let message = err.to_string();
|
|
message.contains("Permission denied")
|
|
&& (message.contains("(publickey")
|
|
|| message.contains("(keyboard-interactive")
|
|
|| message.contains("(password"))
|
|
}
|
|
|
|
fn ssh_check_command(target: &str) -> String {
|
|
format!("ssh {}", shell_quote(target))
|
|
}
|
|
|
|
fn shell_quote(value: &str) -> String {
|
|
if !value.is_empty()
|
|
&& value.chars().all(|ch| {
|
|
ch.is_ascii_alphanumeric()
|
|
|| matches!(
|
|
ch,
|
|
'@' | '%' | '_' | '+' | '=' | ':' | ',' | '.' | '/' | '-'
|
|
)
|
|
})
|
|
{
|
|
return value.to_string();
|
|
}
|
|
|
|
format!("'{}'", value.replace('\'', "'\\''"))
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn remote_auth_error_matches_ssh_auth_denied() {
|
|
let err = std::io::Error::other(
|
|
"remote platform detection failed: user@host: Permission denied (publickey).",
|
|
);
|
|
|
|
assert!(is_remote_auth_error(&err));
|
|
}
|
|
|
|
#[test]
|
|
fn remote_auth_error_matches_keyboard_interactive_denied() {
|
|
let err = std::io::Error::other(
|
|
"remote server status failed: user@host: Permission denied (keyboard-interactive).",
|
|
);
|
|
|
|
assert!(is_remote_auth_error(&err));
|
|
}
|
|
|
|
#[test]
|
|
fn remote_auth_error_ignores_non_auth_errors() {
|
|
let err = std::io::Error::other("remote platform detection failed: unsupported platform");
|
|
|
|
assert!(!is_remote_auth_error(&err));
|
|
}
|
|
|
|
#[test]
|
|
fn ssh_check_command_quotes_remote_target() {
|
|
assert_eq!(ssh_check_command("host name"), "ssh 'host name'");
|
|
}
|
|
}
|