1- //! Traits for implementing custom SSH agents
1+ //! Traits for implementing custom SSH agents.
2+ //!
3+ //! Agents which store no state or their state is minimal should
4+ //! implement the [`Session`] trait. If a more elaborate state is
5+ //! needed, especially one which depends on the socket making the
6+ //! connection then it is advisable to implement the [`Agent`] trait.
27
38use std:: fmt;
49use std:: io;
@@ -29,6 +34,39 @@ use crate::proto::SignRequest;
2934use crate :: proto:: SmartcardKey ;
3035
3136/// Type representing a socket that asynchronously returns a list of streams.
37+ ///
38+ /// This trait is implemented for [TCP sockets](TcpListener) on all
39+ /// platforms, Unix sockets on Unix platforms (e.g. Linux, macOS) and
40+ /// Named Pipes on Windows.
41+ ///
42+ /// Objects implementing this trait are passed to the [`listen`]
43+ /// function.
44+ ///
45+ /// # Examples
46+ ///
47+ /// The following example starts listening for connections and
48+ /// processes them with the `MyAgent` struct.
49+ ///
50+ /// ```no_run
51+ /// # async fn main_() -> testresult::TestResult {
52+ /// use ssh_agent_lib::agent::{listen, Session};
53+ /// use tokio::net::TcpListener;
54+ ///
55+ /// #[derive(Default, Clone)]
56+ /// struct MyAgent;
57+ ///
58+ /// impl Session for MyAgent {
59+ /// // implement your agent logic here
60+ /// }
61+ ///
62+ /// listen(
63+ /// TcpListener::bind("127.0.0.1:8080").await?,
64+ /// MyAgent::default(),
65+ /// )
66+ /// .await?;
67+ /// # Ok(()) }
68+ /// ```
69+
3270#[ async_trait]
3371pub trait ListeningSocket {
3472 /// Stream type that represents an accepted socket.
@@ -91,6 +129,43 @@ impl ListeningSocket for NamedPipeListener {
91129///
92130/// This type is implemented by agents that want to handle incoming SSH agent
93131/// connections.
132+ ///
133+ /// # Examples
134+ ///
135+ /// The following examples shows the most minimal [`Session`]
136+ /// implementation: one that returns a list of public keys that it
137+ /// manages and signs all incoming signing requests.
138+ ///
139+ /// Note that the `MyAgent` struct is cloned for all new sessions
140+ /// (incoming connections). If the cloning needs special behavior
141+ /// implementing [`Clone`] manually is a viable approach. If the newly
142+ /// created sessions require information from the underlying socket it
143+ /// is advisable to implement the [`Agent`] trait.
144+ ///
145+ /// ```
146+ /// use ssh_agent_lib::{agent::Session, error::AgentError};
147+ /// use ssh_agent_lib::proto::{Identity, SignRequest};
148+ /// use ssh_key::{Algorithm, Signature};
149+ ///
150+ /// #[derive(Default, Clone)]
151+ /// struct MyAgent;
152+ ///
153+ /// #[ssh_agent_lib::async_trait]
154+ /// impl Session for MyAgent {
155+ /// async fn request_identities(&mut self) -> Result<Vec<Identity>, AgentError> {
156+ /// Ok(vec![ /* public keys that this agent knows of */ ])
157+ /// }
158+ ///
159+ /// async fn sign(&mut self, request: SignRequest) -> Result<Signature, AgentError> {
160+ /// // get the signature by signing `request.data`
161+ /// let signature = vec![];
162+ /// Ok(Signature::new(
163+ /// Algorithm::new("algorithm").map_err(AgentError::other)?,
164+ /// signature,
165+ /// ).map_err(AgentError::other)?)
166+ /// }
167+ /// }
168+ /// ```
94169#[ async_trait]
95170pub trait Session : ' static + Sync + Send + Unpin {
96171 /// Request a list of keys managed by this session.
@@ -251,6 +326,37 @@ where
251326}
252327
253328/// Factory of sessions for the given type of sockets.
329+ ///
330+ /// An agent implementation is automatically created for types which
331+ /// implement [`Session`] and [`Clone`]: new sessions are created by
332+ /// cloning the agent object. This is usually sufficient for the
333+ /// majority of use cases. In case the information about the
334+ /// underlying socket (connection source) is needed the [`Agent`] can
335+ /// be implemented manually.
336+ ///
337+ /// # Examples
338+ ///
339+ /// This example shows how to retrieve the connecting process ID on Unix:
340+ ///
341+ /// ```
342+ /// use ssh_agent_lib::agent::{Agent, Session};
343+ ///
344+ /// #[derive(Debug, Default)]
345+ /// struct AgentSocketInfo;
346+ ///
347+ /// #[cfg(unix)]
348+ /// impl Agent<tokio::net::UnixListener> for AgentSocketInfo {
349+ /// fn new_session(&mut self, socket: &tokio::net::UnixStream) -> impl Session {
350+ /// let _socket_info = format!(
351+ /// "unix: addr: {:?} cred: {:?}",
352+ /// socket.peer_addr().unwrap(),
353+ /// socket.peer_cred().unwrap()
354+ /// );
355+ /// Self
356+ /// }
357+ /// }
358+ /// # impl Session for AgentSocketInfo { }
359+ /// ```
254360pub trait Agent < S > : ' static + Send + Sync
255361where
256362 S : ListeningSocket + fmt:: Debug + Send ,
@@ -261,15 +367,40 @@ where
261367
262368/// Listen for connections on a given socket and use session factory
263369/// to create new session for each accepted socket.
264- pub async fn listen < S > ( mut socket : S , mut sf : impl Agent < S > ) -> Result < ( ) , AgentError >
370+ ///
371+ /// # Examples
372+ ///
373+ /// The following example starts listening for connections and
374+ /// processes them with the `MyAgent` struct.
375+ ///
376+ /// ```no_run
377+ /// # async fn main_() -> testresult::TestResult {
378+ /// use ssh_agent_lib::agent::{listen, Session};
379+ /// use tokio::net::TcpListener;
380+ ///
381+ /// #[derive(Default, Clone)]
382+ /// struct MyAgent;
383+ ///
384+ /// impl Session for MyAgent {
385+ /// // implement your agent logic here
386+ /// }
387+ ///
388+ /// listen(
389+ /// TcpListener::bind("127.0.0.1:8080").await?,
390+ /// MyAgent::default(),
391+ /// )
392+ /// .await?;
393+ /// # Ok(()) }
394+ /// ```
395+ pub async fn listen < S > ( mut socket : S , mut agent : impl Agent < S > ) -> Result < ( ) , AgentError >
265396where
266397 S : ListeningSocket + fmt:: Debug + Send ,
267398{
268399 log:: info!( "Listening; socket = {:?}" , socket) ;
269400 loop {
270401 match socket. accept ( ) . await {
271402 Ok ( socket) => {
272- let session = sf . new_session ( & socket) ;
403+ let session = agent . new_session ( & socket) ;
273404 tokio:: spawn ( async move {
274405 let adapter = Framed :: new ( socket, Codec :: < Request , Response > :: default ( ) ) ;
275406 if let Err ( e) = handle_socket :: < S > ( session, adapter) . await {
@@ -317,38 +448,65 @@ where
317448 }
318449}
319450
320- /// Bind to a service binding listener.
321451#[ cfg( unix) ]
322- pub async fn bind < SF > ( listener : service_binding:: Listener , sf : SF ) -> Result < ( ) , AgentError >
452+ type PlatformSpecificListener = tokio:: net:: UnixListener ;
453+
454+ #[ cfg( windows) ]
455+ type PlatformSpecificListener = NamedPipeListener ;
456+
457+ /// Bind to a service binding listener.
458+ ///
459+ /// # Examples
460+ ///
461+ /// The following example uses `clap` to parse the host socket data
462+ /// thus allowing the user to choose at runtime whether they want to
463+ /// use TCP sockets, Unix domain sockets (including systemd socket
464+ /// activation) or Named Pipes (under Windows).
465+ ///
466+ /// ```no_run
467+ /// use clap::Parser;
468+ /// use service_binding::Binding;
469+ /// use ssh_agent_lib::agent::{bind, Session};
470+ ///
471+ /// #[derive(Debug, Parser)]
472+ /// struct Args {
473+ /// #[clap(long, short = 'H', default_value = "unix:///tmp/ssh.sock")]
474+ /// host: Binding,
475+ /// }
476+ ///
477+ /// #[derive(Default, Clone)]
478+ /// struct MyAgent;
479+ ///
480+ /// impl Session for MyAgent {}
481+ ///
482+ /// #[tokio::main]
483+ /// async fn main() -> Result<(), Box<dyn std::error::Error>> {
484+ /// let args = Args::parse();
485+ ///
486+ /// bind(args.host.try_into()?, MyAgent::default()).await?;
487+ ///
488+ /// Ok(())
489+ /// }
490+ /// ```
491+ pub async fn bind < A > ( listener : service_binding:: Listener , agent : A ) -> Result < ( ) , AgentError >
323492where
324- SF : Agent < tokio :: net :: UnixListener > + Agent < tokio:: net:: TcpListener > ,
493+ A : Agent < PlatformSpecificListener > + Agent < tokio:: net:: TcpListener > ,
325494{
326495 match listener {
327496 #[ cfg( unix) ]
328497 service_binding:: Listener :: Unix ( listener) => {
329- listen ( UnixListener :: from_std ( listener) ?, sf ) . await
498+ listen ( UnixListener :: from_std ( listener) ?, agent ) . await
330499 }
331500 service_binding:: Listener :: Tcp ( listener) => {
332- listen ( TcpListener :: from_std ( listener) ?, sf ) . await
501+ listen ( TcpListener :: from_std ( listener) ?, agent ) . await
333502 }
503+ #[ cfg( windows) ]
504+ service_binding:: Listener :: NamedPipe ( pipe) => {
505+ listen ( NamedPipeListener :: bind ( pipe) ?, agent) . await
506+ }
507+ #[ allow( unreachable_patterns) ]
334508 _ => Err ( AgentError :: IO ( std:: io:: Error :: other (
335509 "Unsupported type of a listener." ,
336510 ) ) ) ,
337511 }
338512}
339-
340- /// Bind to a service binding listener.
341- #[ cfg( windows) ]
342- pub async fn bind < SF > ( listener : service_binding:: Listener , sf : SF ) -> Result < ( ) , AgentError >
343- where
344- SF : Agent < NamedPipeListener > + Agent < tokio:: net:: TcpListener > ,
345- {
346- match listener {
347- service_binding:: Listener :: Tcp ( listener) => {
348- listen ( TcpListener :: from_std ( listener) ?, sf) . await
349- }
350- service_binding:: Listener :: NamedPipe ( pipe) => {
351- listen ( NamedPipeListener :: bind ( pipe) ?, sf) . await
352- }
353- }
354- }
0 commit comments