Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add doc examples #78

Merged
merged 9 commits into from
Jun 28, 2024
206 changes: 182 additions & 24 deletions src/agent.rs
Original file line number Diff line number Diff line change
@@ -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;
Expand Down Expand Up @@ -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.
Expand Down Expand Up @@ -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<Vec<Identity>, AgentError> {
/// Ok(vec![ /* public keys that this agent knows of */ ])
/// }
///
/// async fn sign(&mut self, request: SignRequest) -> Result<Signature, AgentError> {
/// // 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.
Expand Down Expand Up @@ -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<tokio::net::UnixListener> 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<S>: 'static + Send + Sync
where
S: ListeningSocket + fmt::Debug + Send,
Expand All @@ -261,15 +367,40 @@ where

/// Listen for connections on a given socket and use session factory
/// to create new session for each accepted socket.
pub async fn listen<S>(mut socket: S, mut sf: impl Agent<S>) -> 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<S>(mut socket: S, mut agent: impl Agent<S>) -> Result<(), AgentError>
where
S: ListeningSocket + fmt::Debug + Send,
{
log::info!("Listening; socket = {:?}", socket);
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::<Request, Response>::default());
if let Err(e) = handle_socket::<S>(session, adapter).await {
Expand Down Expand Up @@ -317,38 +448,65 @@ where
}
}

/// Bind to a service binding listener.
#[cfg(unix)]
pub async fn bind<SF>(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<dyn std::error::Error>> {
/// let args = Args::parse();
///
/// bind(args.host.try_into()?, MyAgent::default()).await?;
///
/// Ok(())
/// }
/// ```
pub async fn bind<A>(listener: service_binding::Listener, agent: A) -> Result<(), AgentError>
where
SF: Agent<tokio::net::UnixListener> + Agent<tokio::net::TcpListener>,
A: Agent<PlatformSpecificListener> + Agent<tokio::net::TcpListener>,
{
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<SF>(listener: service_binding::Listener, sf: SF) -> Result<(), AgentError>
where
SF: Agent<NamedPipeListener> + Agent<tokio::net::TcpListener>,
{
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
}
}
}
2 changes: 1 addition & 1 deletion src/codec.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! SSH agent protocol framing codec
//! SSH agent protocol framing codec.

use std::marker::PhantomData;
use std::mem::size_of;
Expand Down
2 changes: 1 addition & 1 deletion src/error.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! SSH agent errors
//! SSH agent errors.

use std::io;

Expand Down
2 changes: 1 addition & 1 deletion src/proto.rs
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
//! SSH agent protocol structures
//! SSH agent protocol structures.

pub mod error;
pub mod extension;
Expand Down
Loading