From c55b189fe2f266c8f8b5159a26a306b6c3d52864 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Wed, 5 Jun 2024 16:47:52 +0200 Subject: [PATCH 1/9] Add example to `agent::listen` Signed-off-by: Wiktor Kwapisiewicz --- src/agent.rs | 25 +++++++++++++++++++++++++ 1 file changed, 25 insertions(+) diff --git a/src/agent.rs b/src/agent.rs index 452104f..e3b61ec 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -261,6 +261,31 @@ where /// Listen for connections on a given socket and use session factory /// to create new session for each accepted socket. +/// +/// # 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 sf: impl Agent) -> Result<(), AgentError> where S: ListeningSocket + fmt::Debug + Send, From 0ac8b270679c0efe05e3371eac0bdcbd1bab3915 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Wed, 5 Jun 2024 17:10:51 +0200 Subject: [PATCH 2/9] Add doc example to `agent::Agent` Signed-off-by: Wiktor Kwapisiewicz --- src/agent.rs | 31 +++++++++++++++++++++++++++++++ 1 file changed, 31 insertions(+) diff --git a/src/agent.rs b/src/agent.rs index e3b61ec..d1390d6 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -251,6 +251,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, From 16ba495eab45e03889d16038e96628212c8f5dad Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Wed, 5 Jun 2024 17:21:11 +0200 Subject: [PATCH 3/9] Add missing dots for consistency with std docs Signed-off-by: Wiktor Kwapisiewicz --- src/agent.rs | 2 +- src/codec.rs | 2 +- src/error.rs | 2 +- src/proto.rs | 2 +- 4 files changed, 4 insertions(+), 4 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index d1390d6..83e4312 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -1,4 +1,4 @@ -//! Traits for implementing custom SSH agents +//! Traits for implementing custom SSH agents. use std::fmt; use std::io; 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; From 0aefece7bc770911dc3e7783daf0424a301ed958 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Wed, 26 Jun 2024 14:20:22 +0200 Subject: [PATCH 4/9] Add doc example to `agent::Session` Signed-off-by: Wiktor Kwapisiewicz --- src/agent.rs | 37 +++++++++++++++++++++++++++++++++++++ 1 file changed, 37 insertions(+) diff --git a/src/agent.rs b/src/agent.rs index 83e4312..7507a97 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -91,6 +91,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. From 2b8aa88d37f1e056503f868ee35db79edcb89764 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Wed, 26 Jun 2024 14:23:49 +0200 Subject: [PATCH 5/9] Add more documentation to the `agent` module Signed-off-by: Wiktor Kwapisiewicz --- src/agent.rs | 5 +++++ 1 file changed, 5 insertions(+) diff --git a/src/agent.rs b/src/agent.rs index 7507a97..bf04003 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -1,4 +1,9 @@ //! 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; From 4783f5d689e738ecf028a25aa7861121362b955e Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Wed, 26 Jun 2024 14:25:36 +0200 Subject: [PATCH 6/9] Rename arguments to `bind`/`listen` to `agent` Signed-off-by: Wiktor Kwapisiewicz --- src/agent.rs | 20 ++++++++++---------- 1 file changed, 10 insertions(+), 10 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index bf04003..5ca12d5 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -359,7 +359,7 @@ where /// .await?; /// # Ok(()) } /// ``` -pub async fn listen(mut socket: S, mut sf: impl Agent) -> Result<(), AgentError> +pub async fn listen(mut socket: S, mut agent: impl Agent) -> Result<(), AgentError> where S: ListeningSocket + fmt::Debug + Send, { @@ -367,7 +367,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 { @@ -417,17 +417,17 @@ where /// Bind to a service binding listener. #[cfg(unix)] -pub async fn bind(listener: service_binding::Listener, sf: SF) -> Result<(), AgentError> +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 } _ => Err(AgentError::IO(std::io::Error::other( "Unsupported type of a listener.", @@ -437,16 +437,16 @@ where /// Bind to a service binding listener. #[cfg(windows)] -pub async fn bind(listener: service_binding::Listener, sf: SF) -> Result<(), AgentError> +pub async fn bind(listener: service_binding::Listener, agent: A) -> Result<(), AgentError> where - SF: Agent + Agent, + A: Agent + Agent, { match listener { service_binding::Listener::Tcp(listener) => { - listen(TcpListener::from_std(listener)?, sf).await + listen(TcpListener::from_std(listener)?, agent).await } service_binding::Listener::NamedPipe(pipe) => { - listen(NamedPipeListener::bind(pipe)?, sf).await + listen(NamedPipeListener::bind(pipe)?, agent).await } } } From 13042248646df10d238a9193d7b23156b529f456 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Wed, 26 Jun 2024 14:34:19 +0200 Subject: [PATCH 7/9] Add doc examples to `agent::bind` Signed-off-by: Wiktor Kwapisiewicz --- src/agent.rs | 66 ++++++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 66 insertions(+) diff --git a/src/agent.rs b/src/agent.rs index 5ca12d5..d48a407 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -416,6 +416,39 @@ where } /// 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(()) +/// } +/// ``` #[cfg(unix)] pub async fn bind(listener: service_binding::Listener, agent: A) -> Result<(), AgentError> where @@ -436,6 +469,39 @@ where } /// 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(()) +/// } +/// ``` #[cfg(windows)] pub async fn bind(listener: service_binding::Listener, agent: A) -> Result<(), AgentError> where From 577d5fb1cb4f391833f2d7a0d89ccdb93d7ddb00 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Wed, 26 Jun 2024 14:39:57 +0200 Subject: [PATCH 8/9] Add doc examples to the `agent::ListeningSocket` trait Signed-off-by: Wiktor Kwapisiewicz --- src/agent.rs | 33 +++++++++++++++++++++++++++++++++ 1 file changed, 33 insertions(+) diff --git a/src/agent.rs b/src/agent.rs index d48a407..3a47a95 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -34,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. From 9536f133a594c0ede05a47e8ba1806be1edb26f2 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Fri, 28 Jun 2024 11:58:14 +0200 Subject: [PATCH 9/9] Simplify `agent::bind` Signed-off-by: Wiktor Kwapisiewicz --- src/agent.rs | 63 ++++++++++------------------------------------------ 1 file changed, 12 insertions(+), 51 deletions(-) diff --git a/src/agent.rs b/src/agent.rs index 3a47a95..ef4160a 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -448,6 +448,12 @@ where } } +#[cfg(unix)] +type PlatformSpecificListener = tokio::net::UnixListener; + +#[cfg(windows)] +type PlatformSpecificListener = NamedPipeListener; + /// Bind to a service binding listener. /// /// # Examples @@ -482,10 +488,9 @@ where /// Ok(()) /// } /// ``` -#[cfg(unix)] pub async fn bind(listener: service_binding::Listener, agent: A) -> Result<(), AgentError> where - A: Agent + Agent, + A: Agent + Agent, { match listener { #[cfg(unix)] @@ -495,57 +500,13 @@ where service_binding::Listener::Tcp(listener) => { listen(TcpListener::from_std(listener)?, agent).await } - _ => Err(AgentError::IO(std::io::Error::other( - "Unsupported type of a listener.", - ))), - } -} - -/// 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(()) -/// } -/// ``` -#[cfg(windows)] -pub async fn bind(listener: service_binding::Listener, agent: A) -> Result<(), AgentError> -where - A: Agent + Agent, -{ - match listener { - service_binding::Listener::Tcp(listener) => { - 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.", + ))), } }