diff --git a/src/agent.rs b/src/agent.rs index 452104f..ef4160a 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -1,4 +1,9 @@ -//! Traits for implementing custom SSH agents +//! Traits for implementing custom SSH agents. +//! +//! Agents which store no state or their state is minimal should +//! implement the [`Session`] trait. If a more elaborate state is +//! needed, especially one which depends on the socket making the +//! connection then it is advisable to implement the [`Agent`] trait. use std::fmt; use std::io; @@ -29,6 +34,39 @@ use crate::proto::SignRequest; use crate::proto::SmartcardKey; /// Type representing a socket that asynchronously returns a list of streams. +/// +/// This trait is implemented for [TCP sockets](TcpListener) on all +/// platforms, Unix sockets on Unix platforms (e.g. Linux, macOS) and +/// Named Pipes on Windows. +/// +/// Objects implementing this trait are passed to the [`listen`] +/// function. +/// +/// # Examples +/// +/// The following example starts listening for connections and +/// processes them with the `MyAgent` struct. +/// +/// ```no_run +/// # async fn main_() -> testresult::TestResult { +/// use ssh_agent_lib::agent::{listen, Session}; +/// use tokio::net::TcpListener; +/// +/// #[derive(Default, Clone)] +/// struct MyAgent; +/// +/// impl Session for MyAgent { +/// // implement your agent logic here +/// } +/// +/// listen( +/// TcpListener::bind("127.0.0.1:8080").await?, +/// MyAgent::default(), +/// ) +/// .await?; +/// # Ok(()) } +/// ``` + #[async_trait] pub trait ListeningSocket { /// Stream type that represents an accepted socket. @@ -91,6 +129,43 @@ impl ListeningSocket for NamedPipeListener { /// /// This type is implemented by agents that want to handle incoming SSH agent /// connections. +/// +/// # Examples +/// +/// The following examples shows the most minimal [`Session`] +/// implementation: one that returns a list of public keys that it +/// manages and signs all incoming signing requests. +/// +/// Note that the `MyAgent` struct is cloned for all new sessions +/// (incoming connections). If the cloning needs special behavior +/// implementing [`Clone`] manually is a viable approach. If the newly +/// created sessions require information from the underlying socket it +/// is advisable to implement the [`Agent`] trait. +/// +/// ``` +/// use ssh_agent_lib::{agent::Session, error::AgentError}; +/// use ssh_agent_lib::proto::{Identity, SignRequest}; +/// use ssh_key::{Algorithm, Signature}; +/// +/// #[derive(Default, Clone)] +/// struct MyAgent; +/// +/// #[ssh_agent_lib::async_trait] +/// impl Session for MyAgent { +/// async fn request_identities(&mut self) -> Result, AgentError> { +/// Ok(vec![ /* public keys that this agent knows of */ ]) +/// } +/// +/// async fn sign(&mut self, request: SignRequest) -> Result { +/// // get the signature by signing `request.data` +/// let signature = vec![]; +/// Ok(Signature::new( +/// Algorithm::new("algorithm").map_err(AgentError::other)?, +/// signature, +/// ).map_err(AgentError::other)?) +/// } +/// } +/// ``` #[async_trait] pub trait Session: 'static + Sync + Send + Unpin { /// Request a list of keys managed by this session. @@ -251,6 +326,37 @@ where } /// Factory of sessions for the given type of sockets. +/// +/// An agent implementation is automatically created for types which +/// implement [`Session`] and [`Clone`]: new sessions are created by +/// cloning the agent object. This is usually sufficient for the +/// majority of use cases. In case the information about the +/// underlying socket (connection source) is needed the [`Agent`] can +/// be implemented manually. +/// +/// # Examples +/// +/// This example shows how to retrieve the connecting process ID on Unix: +/// +/// ``` +/// use ssh_agent_lib::agent::{Agent, Session}; +/// +/// #[derive(Debug, Default)] +/// struct AgentSocketInfo; +/// +/// #[cfg(unix)] +/// impl Agent for AgentSocketInfo { +/// fn new_session(&mut self, socket: &tokio::net::UnixStream) -> impl Session { +/// let _socket_info = format!( +/// "unix: addr: {:?} cred: {:?}", +/// socket.peer_addr().unwrap(), +/// socket.peer_cred().unwrap() +/// ); +/// Self +/// } +/// } +/// # impl Session for AgentSocketInfo { } +/// ``` pub trait Agent: 'static + Send + Sync where S: ListeningSocket + fmt::Debug + Send, @@ -261,7 +367,32 @@ where /// Listen for connections on a given socket and use session factory /// to create new session for each accepted socket. -pub async fn listen(mut socket: S, mut sf: impl Agent) -> Result<(), AgentError> +/// +/// # Examples +/// +/// The following example starts listening for connections and +/// processes them with the `MyAgent` struct. +/// +/// ```no_run +/// # async fn main_() -> testresult::TestResult { +/// use ssh_agent_lib::agent::{listen, Session}; +/// use tokio::net::TcpListener; +/// +/// #[derive(Default, Clone)] +/// struct MyAgent; +/// +/// impl Session for MyAgent { +/// // implement your agent logic here +/// } +/// +/// listen( +/// TcpListener::bind("127.0.0.1:8080").await?, +/// MyAgent::default(), +/// ) +/// .await?; +/// # Ok(()) } +/// ``` +pub async fn listen(mut socket: S, mut agent: impl Agent) -> Result<(), AgentError> where S: ListeningSocket + fmt::Debug + Send, { @@ -269,7 +400,7 @@ where loop { match socket.accept().await { Ok(socket) => { - let session = sf.new_session(&socket); + let session = agent.new_session(&socket); tokio::spawn(async move { let adapter = Framed::new(socket, Codec::::default()); if let Err(e) = handle_socket::(session, adapter).await { @@ -317,38 +448,65 @@ where } } -/// Bind to a service binding listener. #[cfg(unix)] -pub async fn bind(listener: service_binding::Listener, sf: SF) -> Result<(), AgentError> +type PlatformSpecificListener = tokio::net::UnixListener; + +#[cfg(windows)] +type PlatformSpecificListener = NamedPipeListener; + +/// Bind to a service binding listener. +/// +/// # Examples +/// +/// The following example uses `clap` to parse the host socket data +/// thus allowing the user to choose at runtime whether they want to +/// use TCP sockets, Unix domain sockets (including systemd socket +/// activation) or Named Pipes (under Windows). +/// +/// ```no_run +/// use clap::Parser; +/// use service_binding::Binding; +/// use ssh_agent_lib::agent::{bind, Session}; +/// +/// #[derive(Debug, Parser)] +/// struct Args { +/// #[clap(long, short = 'H', default_value = "unix:///tmp/ssh.sock")] +/// host: Binding, +/// } +/// +/// #[derive(Default, Clone)] +/// struct MyAgent; +/// +/// impl Session for MyAgent {} +/// +/// #[tokio::main] +/// async fn main() -> Result<(), Box> { +/// let args = Args::parse(); +/// +/// bind(args.host.try_into()?, MyAgent::default()).await?; +/// +/// Ok(()) +/// } +/// ``` +pub async fn bind(listener: service_binding::Listener, agent: A) -> Result<(), AgentError> where - SF: Agent + Agent, + A: Agent + Agent, { match listener { #[cfg(unix)] service_binding::Listener::Unix(listener) => { - listen(UnixListener::from_std(listener)?, sf).await + listen(UnixListener::from_std(listener)?, agent).await } service_binding::Listener::Tcp(listener) => { - listen(TcpListener::from_std(listener)?, sf).await + listen(TcpListener::from_std(listener)?, agent).await } + #[cfg(windows)] + service_binding::Listener::NamedPipe(pipe) => { + listen(NamedPipeListener::bind(pipe)?, agent).await + } + #[allow(unreachable_patterns)] _ => Err(AgentError::IO(std::io::Error::other( "Unsupported type of a listener.", ))), } } - -/// Bind to a service binding listener. -#[cfg(windows)] -pub async fn bind(listener: service_binding::Listener, sf: SF) -> Result<(), AgentError> -where - SF: Agent + Agent, -{ - match listener { - service_binding::Listener::Tcp(listener) => { - listen(TcpListener::from_std(listener)?, sf).await - } - service_binding::Listener::NamedPipe(pipe) => { - listen(NamedPipeListener::bind(pipe)?, sf).await - } - } -} diff --git a/src/codec.rs b/src/codec.rs index 2766479..fcf902c 100644 --- a/src/codec.rs +++ b/src/codec.rs @@ -1,4 +1,4 @@ -//! SSH agent protocol framing codec +//! SSH agent protocol framing codec. use std::marker::PhantomData; use std::mem::size_of; diff --git a/src/error.rs b/src/error.rs index 1369b2b..8b4db7b 100644 --- a/src/error.rs +++ b/src/error.rs @@ -1,4 +1,4 @@ -//! SSH agent errors +//! SSH agent errors. use std::io; diff --git a/src/proto.rs b/src/proto.rs index 185b653..4f12607 100644 --- a/src/proto.rs +++ b/src/proto.rs @@ -1,4 +1,4 @@ -//! SSH agent protocol structures +//! SSH agent protocol structures. pub mod error; pub mod extension;