Skip to content

Commit

Permalink
Add SSH agent client with an example
Browse files Browse the repository at this point in the history
Fixes: #32
Signed-off-by: Wiktor Kwapisiewicz <[email protected]>
  • Loading branch information
wiktor-k committed Apr 17, 2024
1 parent b519c44 commit 62244c9
Show file tree
Hide file tree
Showing 5 changed files with 215 additions and 1 deletion.
2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -45,7 +45,7 @@ required-features = ["agent"]
env_logger = "0.11.0"
rand = "0.8.5"
rsa = { version = "0.9.6", features = ["sha2", "sha1"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
tokio = { version = "1", features = ["macros", "rt-multi-thread", "time"] }
sha1 = { version = "0.10.5", default-features = false, features = ["oid"] }
testresult = "0.4.0"
hex-literal = "0.4.1"
Expand Down
40 changes: 40 additions & 0 deletions examples/ssh-agent-client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,40 @@
use ssh_agent_lib::agent::Session;
use ssh_agent_lib::client::Client;
#[cfg(windows)]
use tokio::net::windows::named_pipe::ClientOptions;
#[cfg(unix)]
use tokio::net::UnixStream;

#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
#[cfg(unix)]
let mut client = {
let stream = UnixStream::connect(std::env::var("SSH_AUTH_SOCK")?).await?;
Client::new(stream)
};
#[cfg(windows)]
let mut client = {
let stream = loop {
// https://docs.rs/windows-sys/latest/windows_sys/Win32/Foundation/constant.ERROR_PIPE_BUSY.html
const ERROR_PIPE_BUSY: u32 = 231u32;

// correct way to do it taken from
// https://docs.rs/tokio/latest/tokio/net/windows/named_pipe/struct.NamedPipeClient.html
match ClientOptions::new().open(std::env::var("SSH_AUTH_SOCK")?) {
Ok(client) => break client,
Err(e) if e.raw_os_error() == Some(ERROR_PIPE_BUSY as i32) => (),
Err(e) => Err(e)?,
}

tokio::time::sleep(std::time::Duration::from_millis(50)).await;
};
Client::new(stream)
};

eprintln!(
"Identities that this agent knows of: {:#?}",
client.request_identities().await?
);

Ok(())
}
170 changes: 170 additions & 0 deletions src/client.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,170 @@
use std::fmt;

use futures::{SinkExt, TryStreamExt};
use ssh_key::Signature;
use tokio::io::{AsyncRead, AsyncWrite};
use tokio_util::codec::Framed;

use crate::{
codec::Codec,
proto::{
AddIdentity, AddIdentityConstrained, AddSmartcardKeyConstrained, Extension, Identity,
ProtoError, RemoveIdentity, Request, Response, SignRequest, SmartcardKey,
},
};

#[derive(Debug)]
pub struct Client<Stream>
where
Stream: fmt::Debug + AsyncRead + AsyncWrite + Send + Unpin + 'static,
{
adapter: Framed<Stream, Codec<Response, Request>>,
}

impl<Stream> Client<Stream>
where
Stream: fmt::Debug + AsyncRead + AsyncWrite + Send + Unpin + 'static,
{
pub fn new(socket: Stream) -> Self {
let adapter = Framed::new(socket, Codec::default());
Self { adapter }
}
}

#[async_trait::async_trait]
impl<Stream> crate::agent::Session for Client<Stream>
where
Stream: fmt::Debug + AsyncRead + AsyncWrite + Send + Sync + Unpin + 'static,
{
async fn request_identities(&mut self) -> Result<Vec<Identity>, Box<dyn std::error::Error>> {
if let Response::IdentitiesAnswer(identities) =
self.handle(Request::RequestIdentities).await?
{
Ok(identities)
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

async fn sign(
&mut self,
request: SignRequest,
) -> Result<Signature, Box<dyn std::error::Error>> {
if let Response::SignResponse(response) = self.handle(Request::SignRequest(request)).await?
{
Ok(response)
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

async fn add_identity(
&mut self,
identity: AddIdentity,
) -> Result<(), Box<dyn std::error::Error>> {
if let Response::Success = self.handle(Request::AddIdentity(identity)).await? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

async fn add_identity_constrained(
&mut self,
identity: AddIdentityConstrained,
) -> Result<(), Box<dyn std::error::Error>> {
if let Response::Success = self.handle(Request::AddIdConstrained(identity)).await? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

async fn remove_identity(
&mut self,
identity: RemoveIdentity,
) -> Result<(), Box<dyn std::error::Error>> {
if let Response::Success = self.handle(Request::RemoveIdentity(identity)).await? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

async fn remove_all_identities(&mut self) -> Result<(), Box<dyn std::error::Error>> {
if let Response::Success = self.handle(Request::RemoveAllIdentities).await? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

async fn add_smartcard_key(
&mut self,
key: SmartcardKey,
) -> Result<(), Box<dyn std::error::Error>> {
if let Response::Success = self.handle(Request::AddSmartcardKey(key)).await? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

async fn add_smartcard_key_constrained(
&mut self,
key: AddSmartcardKeyConstrained,
) -> Result<(), Box<dyn std::error::Error>> {
if let Response::Success = self
.handle(Request::AddSmartcardKeyConstrained(key))
.await?
{
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

async fn remove_smartcard_key(
&mut self,
key: SmartcardKey,
) -> Result<(), Box<dyn std::error::Error>> {
if let Response::Success = self.handle(Request::RemoveSmartcardKey(key)).await? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

async fn lock(&mut self, key: String) -> Result<(), Box<dyn std::error::Error>> {
if let Response::Success = self.handle(Request::Lock(key)).await? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

async fn unlock(&mut self, key: String) -> Result<(), Box<dyn std::error::Error>> {
if let Response::Success = self.handle(Request::Unlock(key)).await? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

async fn extension(&mut self, extension: Extension) -> Result<(), Box<dyn std::error::Error>> {
if let Response::Success = self.handle(Request::Extension(extension)).await? {
Ok(())
} else {
Err(ProtoError::UnexpectedResponse.into())
}
}

async fn handle(&mut self, message: Request) -> Result<Response, Box<dyn std::error::Error>> {
self.adapter.send(message).await?;
if let Some(response) = self.adapter.try_next().await? {
Ok(response)
} else {
Err(ProtoError::IO(std::io::Error::other("server disconnected")).into())
}
}
}
2 changes: 2 additions & 0 deletions src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -6,6 +6,8 @@ pub mod proto;

#[cfg(feature = "agent")]
pub mod agent;
#[cfg(feature = "agent")]
pub mod client;
#[cfg(feature = "codec")]
pub mod codec;
pub mod error;
Expand Down
2 changes: 2 additions & 0 deletions src/proto/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -14,6 +14,8 @@ pub enum ProtoError {
SshKey(#[from] ssh_key::Error),
#[error("Command not supported ({command})")]
UnsupportedCommand { command: u8 },
#[error("Unexpected response received")]
UnexpectedResponse,
}

pub type ProtoResult<T> = Result<T, ProtoError>;

0 comments on commit 62244c9

Please sign in to comment.