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.
2
7
3
8
use std:: fmt;
4
9
use std:: io;
@@ -29,6 +34,39 @@ use crate::proto::SignRequest;
29
34
use crate :: proto:: SmartcardKey ;
30
35
31
36
/// 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
+
32
70
#[ async_trait]
33
71
pub trait ListeningSocket {
34
72
/// Stream type that represents an accepted socket.
@@ -91,6 +129,43 @@ impl ListeningSocket for NamedPipeListener {
91
129
///
92
130
/// This type is implemented by agents that want to handle incoming SSH agent
93
131
/// 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
+ /// ```
94
169
#[ async_trait]
95
170
pub trait Session : ' static + Sync + Send + Unpin {
96
171
/// Request a list of keys managed by this session.
@@ -251,6 +326,37 @@ where
251
326
}
252
327
253
328
/// 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
+ /// ```
254
360
pub trait Agent < S > : ' static + Send + Sync
255
361
where
256
362
S : ListeningSocket + fmt:: Debug + Send ,
@@ -261,15 +367,40 @@ where
261
367
262
368
/// Listen for connections on a given socket and use session factory
263
369
/// 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 >
265
396
where
266
397
S : ListeningSocket + fmt:: Debug + Send ,
267
398
{
268
399
log:: info!( "Listening; socket = {:?}" , socket) ;
269
400
loop {
270
401
match socket. accept ( ) . await {
271
402
Ok ( socket) => {
272
- let session = sf . new_session ( & socket) ;
403
+ let session = agent . new_session ( & socket) ;
273
404
tokio:: spawn ( async move {
274
405
let adapter = Framed :: new ( socket, Codec :: < Request , Response > :: default ( ) ) ;
275
406
if let Err ( e) = handle_socket :: < S > ( session, adapter) . await {
@@ -317,38 +448,65 @@ where
317
448
}
318
449
}
319
450
320
- /// Bind to a service binding listener.
321
451
#[ 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 >
323
492
where
324
- SF : Agent < tokio :: net :: UnixListener > + Agent < tokio:: net:: TcpListener > ,
493
+ A : Agent < PlatformSpecificListener > + Agent < tokio:: net:: TcpListener > ,
325
494
{
326
495
match listener {
327
496
#[ cfg( unix) ]
328
497
service_binding:: Listener :: Unix ( listener) => {
329
- listen ( UnixListener :: from_std ( listener) ?, sf ) . await
498
+ listen ( UnixListener :: from_std ( listener) ?, agent ) . await
330
499
}
331
500
service_binding:: Listener :: Tcp ( listener) => {
332
- listen ( TcpListener :: from_std ( listener) ?, sf ) . await
501
+ listen ( TcpListener :: from_std ( listener) ?, agent ) . await
333
502
}
503
+ #[ cfg( windows) ]
504
+ service_binding:: Listener :: NamedPipe ( pipe) => {
505
+ listen ( NamedPipeListener :: bind ( pipe) ?, agent) . await
506
+ }
507
+ #[ allow( unreachable_patterns) ]
334
508
_ => Err ( AgentError :: IO ( std:: io:: Error :: other (
335
509
"Unsupported type of a listener." ,
336
510
) ) ) ,
337
511
}
338
512
}
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