mod allowed_hosts;
mod daemon;
mod lock;
use std::fmt::{self, Display, Formatter};
use std::path::PathBuf;
use anyhow::Result;
use clap::{Args as ClapArgs, Subcommand};
use serde::{Deserialize, Serialize};
pub use daemon::is_default_running;
use crate::auth::AuthOptions;
use crate::outcome::Outcome;
use crate::socket::SocketMode;
use crate::ssh::registry::SshKeyName;
#[derive(Debug, ClapArgs)]
#[command(
about = "Manage a background SSH agent over a Unix socket.",
long_about = None
)]
pub struct Args {
#[command(subcommand)]
command: Command,
}
pub async fn run(args: Args, options: &AuthOptions) -> Result<Outcome> {
match args.command {
Command::Start(args) => daemon::start(args, options).await,
Command::Stop(args) => daemon::stop(args).await,
Command::Status(args) => daemon::status(args).await,
Command::InternalRun(args) => daemon::internal_run(args, options.clone()).await,
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
pub struct AgentRunning {
pub pid: u32,
pub socket: String,
pub socket_mode: SocketMode,
pub keys: Vec<String>,
#[serde(flatten)]
pub constraints: Option<DestinationConstraints>,
}
#[derive(Clone, Deserialize, Serialize)]
#[cfg_attr(test, derive(Debug, PartialEq))]
#[serde(rename_all = "camelCase")]
pub struct DestinationConstraints {
pub allowed_hosts: Vec<String>,
pub allowed_namespaces: Vec<String>,
}
#[cfg(test)]
impl Default for AgentRunning {
fn default() -> Self {
Self {
pid: 0,
socket: String::new(),
socket_mode: "600".parse().unwrap(),
keys: Vec::new(),
constraints: None,
}
}
}
impl Display for AgentRunning {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
write!(
f,
"ssh-agent running with pid {} on {}",
self.pid, self.socket
)?;
if let Some(DestinationConstraints {
allowed_hosts,
allowed_namespaces,
}) = &self.constraints
{
write!(f, "\nallowed hosts: {}", allowed_hosts.join(", "))?;
if !allowed_namespaces.is_empty() {
write!(f, "\nallowed namespaces: {}", allowed_namespaces.join(", "))?;
}
}
for key in &self.keys {
write!(f, "\n{key}")?;
}
Ok(())
}
}
#[derive(Serialize)]
#[cfg_attr(test, derive(Default))]
#[serde(rename_all = "camelCase")]
pub struct AgentStopped {}
impl Display for AgentStopped {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("ssh-agent stopped")
}
}
#[derive(Serialize)]
#[cfg_attr(test, derive(Default))]
#[serde(rename_all = "camelCase")]
pub struct AgentNotRunning {}
impl Display for AgentNotRunning {
fn fmt(&self, f: &mut Formatter<'_>) -> fmt::Result {
f.write_str("ssh-agent was not running")
}
}
#[derive(Debug, Subcommand)]
enum Command {
Start(StartArgs),
Stop(AgentPathArgs),
Status(AgentPathArgs),
#[command(hide = true)]
InternalRun(InternalRunArgs),
}
#[derive(Debug, ClapArgs)]
struct StartArgs {
#[command(flatten)]
serving: ServingArgs,
#[arg(long, value_name = "PATH")]
socket: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pid_file: Option<PathBuf>,
#[arg(long, default_value = "600")]
socket_mode: SocketMode,
}
#[derive(Debug, ClapArgs)]
struct ServingArgs {
#[arg(long, value_name = "KEY")]
key: Vec<SshKeyName>,
#[arg(long, value_name = "PATH")]
allowed_hosts_file: Option<PathBuf>,
#[arg(long, value_name = "NAMESPACE", requires = "allowed_hosts_file")]
allow_namespace: Vec<String>,
}
#[derive(Debug, ClapArgs)]
struct AgentPathArgs {
#[arg(long, value_name = "PATH")]
socket: Option<PathBuf>,
#[arg(long, value_name = "PATH")]
pid_file: Option<PathBuf>,
}
#[derive(Debug, ClapArgs)]
struct InternalRunArgs {
#[command(flatten)]
serving: ServingArgs,
#[arg(long, value_name = "PATH")]
socket: PathBuf,
#[arg(long, value_name = "PATH", hide = true)]
pid_file: PathBuf,
#[arg(long)]
socket_mode: SocketMode,
}
#[cfg(test)]
mod tests {
use clap::Parser;
use clap::error::ErrorKind;
use super::*;
#[derive(Debug, Parser)]
struct AgentParser {
#[command(flatten)]
args: Args,
}
#[test]
fn start_rejects_invalid_socket_modes_during_cli_parsing() {
for value in ["999", "abc"] {
let error = AgentParser::try_parse_from(["agent", "start", "--socket-mode", value])
.unwrap_err();
assert_eq!(error.kind(), ErrorKind::ValueValidation, "{value}");
}
}
}