From 920b7a21fe859ca7337bba1380c26e7bba35f867 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Thu, 25 Apr 2024 09:15:09 +0200 Subject: [PATCH 1/5] Expose socket info in `new_session` Fixes: https://github.com/wiktor-k/ssh-agent-lib/issues/55 Signed-off-by: Wiktor Kwapisiewicz --- examples/key_storage.rs | 7 +++++-- src/agent.rs | 11 ++++++++--- 2 files changed, 13 insertions(+), 5 deletions(-) diff --git a/examples/key_storage.rs b/examples/key_storage.rs index 9cfb356..6c9bd10 100644 --- a/examples/key_storage.rs +++ b/examples/key_storage.rs @@ -9,7 +9,7 @@ use rsa::BigUint; use sha1::Sha1; #[cfg(windows)] use ssh_agent_lib::agent::NamedPipeListener as Listener; -use ssh_agent_lib::agent::Session; +use ssh_agent_lib::agent::{ListeningSocket, Session}; use ssh_agent_lib::error::AgentError; use ssh_agent_lib::proto::extension::{QueryResponse, RestrictDestination, SessionBind}; use ssh_agent_lib::proto::{ @@ -238,7 +238,10 @@ impl KeyStorageAgent { } impl Agent for KeyStorageAgent { - fn new_session(&mut self) -> impl Session { + fn new_session(&mut self, _socket: &S::Stream) -> impl Session + where + S: ListeningSocket + std::fmt::Debug + Send, + { KeyStorage { identities: Arc::clone(&self.identities), } diff --git a/src/agent.rs b/src/agent.rs index 537e4c6..15f36d7 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -253,7 +253,9 @@ where #[async_trait] pub trait Agent: 'static + Sync + Send + Sized { /// Create new session object when a new socket is accepted. - fn new_session(&mut self) -> impl Session; + fn new_session(&mut self, socket: &S::Stream) -> impl Session + where + S: ListeningSocket + fmt::Debug + Send; /// Listen on a socket waiting for client connections. async fn listen(mut self, mut socket: S) -> Result<(), AgentError> @@ -264,7 +266,7 @@ pub trait Agent: 'static + Sync + Send + Sized { loop { match socket.accept().await { Ok(socket) => { - let session = self.new_session(); + let session = self.new_session::(&socket); tokio::spawn(async move { let adapter = Framed::new(socket, Codec::::default()); if let Err(e) = handle_socket::(session, adapter).await { @@ -306,7 +308,10 @@ impl Agent for T where T: Default + Session, { - fn new_session(&mut self) -> impl Session { + fn new_session(&mut self, _socket: &S::Stream) -> impl Session + where + S: ListeningSocket + fmt::Debug + Send, + { Self::default() } } From 3af10b9bc56306f48cc16e28250e5bc0c2063df7 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Mon, 29 Apr 2024 11:36:44 +0200 Subject: [PATCH 2/5] Extract `listen` from the `Agent` Signed-off-by: Wiktor Kwapisiewicz --- README.md | 4 +- examples/key_storage.rs | 27 ++++--- examples/openpgp-card-agent.rs | 37 +++++++-- src/agent.rs | 144 ++++++++++++++++++++------------- src/lib.rs | 3 - 5 files changed, 141 insertions(+), 74 deletions(-) diff --git a/README.md b/README.md index 10bdd84..fcc26cc 100644 --- a/README.md +++ b/README.md @@ -18,7 +18,7 @@ use tokio::net::UnixListener as Listener; #[cfg(windows)] use ssh_agent_lib::agent::NamedPipeListener as Listener; use ssh_agent_lib::error::AgentError; -use ssh_agent_lib::agent::{Session, Agent}; +use ssh_agent_lib::agent::{Session, listen}; use ssh_agent_lib::proto::{Identity, SignRequest}; use ssh_key::{Algorithm, Signature}; @@ -50,7 +50,7 @@ async fn main() -> Result<(), Box> { let _ = std::fs::remove_file(socket); // remove the socket if exists - MyAgent.listen(Listener::bind(socket)?).await?; + listen(Listener::bind(socket)?, MyAgent::default()).await?; Ok(()) } ``` diff --git a/examples/key_storage.rs b/examples/key_storage.rs index 6c9bd10..3aebfcb 100644 --- a/examples/key_storage.rs +++ b/examples/key_storage.rs @@ -9,14 +9,13 @@ use rsa::BigUint; use sha1::Sha1; #[cfg(windows)] use ssh_agent_lib::agent::NamedPipeListener as Listener; -use ssh_agent_lib::agent::{ListeningSocket, Session}; +use ssh_agent_lib::agent::{listen, Agent, Session}; use ssh_agent_lib::error::AgentError; use ssh_agent_lib::proto::extension::{QueryResponse, RestrictDestination, SessionBind}; use ssh_agent_lib::proto::{ message, signature, AddIdentity, AddIdentityConstrained, AddSmartcardKeyConstrained, Credential, Extension, KeyConstraint, RemoveIdentity, SignRequest, SmartcardKey, }; -use ssh_agent_lib::Agent; use ssh_key::{ private::{KeypairData, PrivateKey}, public::PublicKey, @@ -237,11 +236,21 @@ impl KeyStorageAgent { } } -impl Agent for KeyStorageAgent { - fn new_session(&mut self, _socket: &S::Stream) -> impl Session - where - S: ListeningSocket + std::fmt::Debug + Send, - { +#[cfg(unix)] +impl Agent for KeyStorageAgent { + fn new_session(&mut self, _socket: &tokio::net::UnixStream) -> impl Session { + KeyStorage { + identities: Arc::clone(&self.identities), + } + } +} + +#[cfg(windows)] +impl Agent for KeyStorageAgent { + fn new_session( + &mut self, + _socket: &tokio::net::windows::named_pipe::NamedPipeServer, + ) -> impl Session { KeyStorage { identities: Arc::clone(&self.identities), } @@ -263,8 +272,6 @@ async fn main() -> Result<(), AgentError> { #[cfg(windows)] std::fs::File::create("server-started")?; - KeyStorageAgent::new() - .listen(Listener::bind(socket)?) - .await?; + listen(Listener::bind(socket)?, KeyStorageAgent::new()).await?; Ok(()) } diff --git a/examples/openpgp-card-agent.rs b/examples/openpgp-card-agent.rs index 3e5f992..599f4f2 100644 --- a/examples/openpgp-card-agent.rs +++ b/examples/openpgp-card-agent.rs @@ -16,6 +16,11 @@ use std::{sync::Arc, time::Duration}; +#[cfg(windows)] +use ssh_agent_lib::agent::NamedPipeListener as Listener; + +#[cfg(not(windows))] +use tokio::net::UnixListener as Listener; use card_backend_pcsc::PcscBackend; use clap::Parser; use openpgp_card::{ @@ -27,16 +32,16 @@ use retainer::{Cache, CacheExpiration}; use secrecy::{ExposeSecret, SecretString}; use service_binding::Binding; use ssh_agent_lib::{ - agent::Session, + agent::{bind, Session, Agent}, error::AgentError, proto::{AddSmartcardKeyConstrained, Identity, KeyConstraint, SignRequest, SmartcardKey}, - Agent, }; use ssh_key::{ public::{Ed25519PublicKey, KeyData}, Algorithm, Signature, }; use testresult::TestResult; +use tokio::net::TcpListener; struct CardAgent { pwds: Arc>, @@ -51,8 +56,30 @@ impl CardAgent { } } -impl Agent for CardAgent { - fn new_session(&mut self) -> impl Session { +#[cfg(unix)] +impl Agent for CardAgent { + fn new_session(&mut self, _socket: &tokio::net::UnixStream) -> impl Session { + CardSession { + pwds: Arc::clone(&self.pwds), + } + } +} + +#[cfg(unix)] +impl Agent for CardAgent { + fn new_session(&mut self, _socket: &tokio::net::TcpStream) -> impl Session { + CardSession { + pwds: Arc::clone(&self.pwds), + } + } +} + +#[cfg(windows)] +impl Agent for CardAgent { + fn new_session( + &mut self, + _socket: &tokio::net::windows::named_pipe::NamedPipeServer, + ) -> impl Session { CardSession { pwds: Arc::clone(&self.pwds), } @@ -201,6 +228,6 @@ async fn main() -> TestResult { env_logger::init(); let args = Args::parse(); - CardAgent::new().bind(args.host.try_into()?).await?; + bind(args.host.try_into()?, CardAgent::new()).await?; Ok(()) } diff --git a/src/agent.rs b/src/agent.rs index 15f36d7..61a5ecd 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -249,69 +249,105 @@ where } } -/// Type representing an agent listening for incoming connections. -#[async_trait] -pub trait Agent: 'static + Sync + Send + Sized { - /// Create new session object when a new socket is accepted. - fn new_session(&mut self, socket: &S::Stream) -> impl Session - where - S: ListeningSocket + fmt::Debug + Send; - - /// Listen on a socket waiting for client connections. - async fn listen(mut self, mut socket: S) -> Result<(), AgentError> - where - S: ListeningSocket + fmt::Debug + Send, - { - log::info!("Listening; socket = {:?}", socket); - loop { - match socket.accept().await { - Ok(socket) => { - let session = self.new_session::(&socket); - tokio::spawn(async move { - let adapter = Framed::new(socket, Codec::::default()); - if let Err(e) = handle_socket::(session, adapter).await { - log::error!("Agent protocol error: {:?}", e); - } - }); - } - Err(e) => { - log::error!("Failed to accept socket: {:?}", e); - return Err(AgentError::IO(e)); - } - } - } - } +/// Factory of sessions for the given type of sockets. +pub trait Agent: 'static + Send + Sync +where + S: ListeningSocket + fmt::Debug + Send, +{ + /// Create a [`Session`] object for a given `socket`. + fn new_session(&mut self, socket: &S::Stream) -> impl Session; +} - /// Bind to a service binding listener. - async fn bind(mut self, listener: service_binding::Listener) -> Result<(), AgentError> { - match listener { - #[cfg(unix)] - service_binding::Listener::Unix(listener) => { - self.listen(UnixListener::from_std(listener)?).await - } - service_binding::Listener::Tcp(listener) => { - self.listen(TcpListener::from_std(listener)?).await +/// Listen for connections on a given socket and use session factory +/// to create new session for each accepted socket. +pub async fn listen(mut socket: S, mut sf: impl Agent) -> 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); + tokio::spawn(async move { + let adapter = Framed::new(socket, Codec::::default()); + if let Err(e) = handle_socket::(session, adapter).await { + log::error!("Agent protocol error: {:?}", e); + } + }); } - #[cfg(windows)] - service_binding::Listener::NamedPipe(pipe) => { - self.listen(NamedPipeListener::bind(pipe)?).await + Err(e) => { + log::error!("Failed to accept socket: {:?}", e); + return Err(AgentError::IO(e)); } - #[cfg(not(windows))] - service_binding::Listener::NamedPipe(_) => Err(AgentError::IO(std::io::Error::other( - "Named pipes supported on Windows only", - ))), } } } -impl Agent for T +#[cfg(unix)] +impl Agent for T +where + T: Default + Send + Sync + Session, +{ + fn new_session(&mut self, _socket: &tokio::net::UnixStream) -> impl Session { + Self::default() + } +} + +impl Agent for T where - T: Default + Session, + T: Default + Send + Sync + Session, { - fn new_session(&mut self, _socket: &S::Stream) -> impl Session - where - S: ListeningSocket + fmt::Debug + Send, - { + fn new_session(&mut self, _socket: &tokio::net::TcpStream) -> impl Session { Self::default() } } + +#[cfg(windows)] +impl Agent for T +where + T: Default + Send + Sync + Session, +{ + fn new_session( + &mut self, + _socket: &tokio::net::windows::named_pipe::NamedPipeServer, + ) -> impl Session { + Self::default() + } +} + +/// Bind to a service binding listener. +#[cfg(unix)] +pub async fn bind(listener: service_binding::Listener, sf: SF) -> Result<(), AgentError> +where + SF: Agent + Agent, +{ + match listener { + #[cfg(unix)] + service_binding::Listener::Unix(listener) => { + listen(UnixListener::from_std(listener)?, sf).await + } + service_binding::Listener::Tcp(listener) => { + listen(TcpListener::from_std(listener)?, sf).await + } + _ => Err(AgentError::IO(std::io::Error::other( + "Unsupported type of a listener.", + ))), + } +} + +/// Bind to a service binding listener. +#[cfg(windows)] +pub async fn bind(listener: service_binding::Listener, sf: SF) -> Result<(), AgentError> +where + SF: Agent + Agent, +{ + 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 + } + } +} diff --git a/src/lib.rs b/src/lib.rs index aae5ca4..b6023f0 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -15,6 +15,3 @@ pub mod error; #[cfg(feature = "agent")] pub use async_trait::async_trait; - -#[cfg(feature = "agent")] -pub use self::agent::Agent; From e795e076c7c8a47b9db2726d5cbc924b6aa05684 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Mon, 6 May 2024 11:44:51 +0200 Subject: [PATCH 3/5] Fix `openpgp-card-agent` example Signed-off-by: Wiktor Kwapisiewicz --- examples/openpgp-card-agent.rs | 11 +++++------ 1 file changed, 5 insertions(+), 6 deletions(-) diff --git a/examples/openpgp-card-agent.rs b/examples/openpgp-card-agent.rs index 599f4f2..ba27283 100644 --- a/examples/openpgp-card-agent.rs +++ b/examples/openpgp-card-agent.rs @@ -16,11 +16,6 @@ use std::{sync::Arc, time::Duration}; -#[cfg(windows)] -use ssh_agent_lib::agent::NamedPipeListener as Listener; - -#[cfg(not(windows))] -use tokio::net::UnixListener as Listener; use card_backend_pcsc::PcscBackend; use clap::Parser; use openpgp_card::{ @@ -31,8 +26,10 @@ use openpgp_card::{ use retainer::{Cache, CacheExpiration}; use secrecy::{ExposeSecret, SecretString}; use service_binding::Binding; +#[cfg(windows)] +use ssh_agent_lib::agent::NamedPipeListener as Listener; use ssh_agent_lib::{ - agent::{bind, Session, Agent}, + agent::{bind, Agent, Session}, error::AgentError, proto::{AddSmartcardKeyConstrained, Identity, KeyConstraint, SignRequest, SmartcardKey}, }; @@ -42,6 +39,8 @@ use ssh_key::{ }; use testresult::TestResult; use tokio::net::TcpListener; +#[cfg(not(windows))] +use tokio::net::UnixListener as Listener; struct CardAgent { pwds: Arc>, From 42fa2a4344f2a3e1054bed733574ee3a60e29bb7 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Mon, 6 May 2024 12:03:47 +0200 Subject: [PATCH 4/5] Use `Clone` trait instead of `Default` Signed-off-by: Wiktor Kwapisiewicz --- README.md | 2 +- examples/key_storage.rs | 38 ++------------------------ examples/openpgp-card-agent.rs | 50 ++++------------------------------ src/agent.rs | 12 ++++---- 4 files changed, 15 insertions(+), 87 deletions(-) diff --git a/README.md b/README.md index fcc26cc..0d618e1 100644 --- a/README.md +++ b/README.md @@ -22,7 +22,7 @@ use ssh_agent_lib::agent::{Session, listen}; use ssh_agent_lib::proto::{Identity, SignRequest}; use ssh_key::{Algorithm, Signature}; -#[derive(Default)] +#[derive(Default, Clone)] struct MyAgent; #[ssh_agent_lib::async_trait] diff --git a/examples/key_storage.rs b/examples/key_storage.rs index 3aebfcb..44b7650 100644 --- a/examples/key_storage.rs +++ b/examples/key_storage.rs @@ -9,7 +9,7 @@ use rsa::BigUint; use sha1::Sha1; #[cfg(windows)] use ssh_agent_lib::agent::NamedPipeListener as Listener; -use ssh_agent_lib::agent::{listen, Agent, Session}; +use ssh_agent_lib::agent::{listen, Session}; use ssh_agent_lib::error::AgentError; use ssh_agent_lib::proto::extension::{QueryResponse, RestrictDestination, SessionBind}; use ssh_agent_lib::proto::{ @@ -31,6 +31,7 @@ struct Identity { comment: String, } +#[derive(Default, Clone)] struct KeyStorage { identities: Arc>>, } @@ -224,39 +225,6 @@ impl Session for KeyStorage { } } -struct KeyStorageAgent { - identities: Arc>>, -} - -impl KeyStorageAgent { - fn new() -> Self { - Self { - identities: Arc::new(Mutex::new(vec![])), - } - } -} - -#[cfg(unix)] -impl Agent for KeyStorageAgent { - fn new_session(&mut self, _socket: &tokio::net::UnixStream) -> impl Session { - KeyStorage { - identities: Arc::clone(&self.identities), - } - } -} - -#[cfg(windows)] -impl Agent for KeyStorageAgent { - fn new_session( - &mut self, - _socket: &tokio::net::windows::named_pipe::NamedPipeServer, - ) -> impl Session { - KeyStorage { - identities: Arc::clone(&self.identities), - } - } -} - #[tokio::main] async fn main() -> Result<(), AgentError> { env_logger::init(); @@ -272,6 +240,6 @@ async fn main() -> Result<(), AgentError> { #[cfg(windows)] std::fs::File::create("server-started")?; - listen(Listener::bind(socket)?, KeyStorageAgent::new()).await?; + listen(Listener::bind(socket)?, KeyStorage::default()).await?; Ok(()) } diff --git a/examples/openpgp-card-agent.rs b/examples/openpgp-card-agent.rs index ba27283..f84d305 100644 --- a/examples/openpgp-card-agent.rs +++ b/examples/openpgp-card-agent.rs @@ -26,10 +26,8 @@ use openpgp_card::{ use retainer::{Cache, CacheExpiration}; use secrecy::{ExposeSecret, SecretString}; use service_binding::Binding; -#[cfg(windows)] -use ssh_agent_lib::agent::NamedPipeListener as Listener; use ssh_agent_lib::{ - agent::{bind, Agent, Session}, + agent::{bind, Session}, error::AgentError, proto::{AddSmartcardKeyConstrained, Identity, KeyConstraint, SignRequest, SmartcardKey}, }; @@ -38,58 +36,20 @@ use ssh_key::{ Algorithm, Signature, }; use testresult::TestResult; -use tokio::net::TcpListener; -#[cfg(not(windows))] -use tokio::net::UnixListener as Listener; -struct CardAgent { +#[derive(Clone)] +struct CardSession { pwds: Arc>, } -impl CardAgent { +impl CardSession { pub fn new() -> Self { let pwds: Arc> = Arc::new(Default::default()); let clone = Arc::clone(&pwds); tokio::spawn(async move { clone.monitor(4, 0.25, Duration::from_secs(3)).await }); Self { pwds } } -} - -#[cfg(unix)] -impl Agent for CardAgent { - fn new_session(&mut self, _socket: &tokio::net::UnixStream) -> impl Session { - CardSession { - pwds: Arc::clone(&self.pwds), - } - } -} - -#[cfg(unix)] -impl Agent for CardAgent { - fn new_session(&mut self, _socket: &tokio::net::TcpStream) -> impl Session { - CardSession { - pwds: Arc::clone(&self.pwds), - } - } -} -#[cfg(windows)] -impl Agent for CardAgent { - fn new_session( - &mut self, - _socket: &tokio::net::windows::named_pipe::NamedPipeServer, - ) -> impl Session { - CardSession { - pwds: Arc::clone(&self.pwds), - } - } -} - -struct CardSession { - pwds: Arc>, -} - -impl CardSession { async fn handle_sign( &self, request: SignRequest, @@ -227,6 +187,6 @@ async fn main() -> TestResult { env_logger::init(); let args = Args::parse(); - bind(args.host.try_into()?, CardAgent::new()).await?; + bind(args.host.try_into()?, CardSession::new()).await?; Ok(()) } diff --git a/src/agent.rs b/src/agent.rs index 61a5ecd..6e2261a 100644 --- a/src/agent.rs +++ b/src/agent.rs @@ -287,32 +287,32 @@ where #[cfg(unix)] impl Agent for T where - T: Default + Send + Sync + Session, + T: Clone + Send + Sync + Session, { fn new_session(&mut self, _socket: &tokio::net::UnixStream) -> impl Session { - Self::default() + Self::clone(self) } } impl Agent for T where - T: Default + Send + Sync + Session, + T: Clone + Send + Sync + Session, { fn new_session(&mut self, _socket: &tokio::net::TcpStream) -> impl Session { - Self::default() + Self::clone(self) } } #[cfg(windows)] impl Agent for T where - T: Default + Send + Sync + Session, + T: Clone + Send + Sync + Session, { fn new_session( &mut self, _socket: &tokio::net::windows::named_pipe::NamedPipeServer, ) -> impl Session { - Self::default() + Self::clone(self) } } From 8c3fb5b00a7fdf7996fb64a3427b5bf007371af2 Mon Sep 17 00:00:00 2001 From: Wiktor Kwapisiewicz Date: Mon, 6 May 2024 12:38:16 +0200 Subject: [PATCH 5/5] Add example illustrating retrieving underlying socket info Signed-off-by: Wiktor Kwapisiewicz --- examples/agent-socket-info.rs | 82 +++++++++++++++++++++++++++++++++++ 1 file changed, 82 insertions(+) create mode 100644 examples/agent-socket-info.rs diff --git a/examples/agent-socket-info.rs b/examples/agent-socket-info.rs new file mode 100644 index 0000000..f819b6c --- /dev/null +++ b/examples/agent-socket-info.rs @@ -0,0 +1,82 @@ +//! This example shows how to access the underlying socket info. +//! The socket info can be used to implement fine-grained access controls based on UID/GID. +//! +//! Run the example with: `cargo run --example agent-socket-info -- -H unix:///tmp/sock` +//! Then inspect the socket info with: `SSH_AUTH_SOCK=/tmp/sock ssh-add -L` which should display +//! something like this: +//! +//! ssh-ed25519 AAAAC3NzaC1lZDI1NTE5AAAAIAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA unix: addr: (unnamed) cred: UCred { pid: Some(68463), uid: 1000, gid: 1000 } + +use clap::Parser; +use service_binding::Binding; +use ssh_agent_lib::{ + agent::{bind, Agent, Session}, + error::AgentError, + proto::Identity, +}; +use ssh_key::public::KeyData; +use testresult::TestResult; + +#[derive(Debug, Default)] +struct AgentSocketInfo { + comment: String, +} + +#[ssh_agent_lib::async_trait] +impl Session for AgentSocketInfo { + async fn request_identities(&mut self) -> Result, AgentError> { + Ok(vec![Identity { + // this is just a dummy key, the comment is important + pubkey: KeyData::Ed25519(ssh_key::public::Ed25519PublicKey([0; 32])), + comment: self.comment.clone(), + }]) + } +} + +#[cfg(unix)] +impl Agent for AgentSocketInfo { + fn new_session(&mut self, socket: &tokio::net::UnixStream) -> impl Session { + Self { + comment: format!( + "unix: addr: {:?} cred: {:?}", + socket.peer_addr().unwrap(), + socket.peer_cred().unwrap() + ), + } + } +} + +impl Agent for AgentSocketInfo { + fn new_session(&mut self, _socket: &tokio::net::TcpStream) -> impl Session { + Self { + comment: "tcp".into(), + } + } +} + +#[cfg(windows)] +impl Agent for AgentSocketInfo { + fn new_session( + &mut self, + _socket: &tokio::net::windows::named_pipe::NamedPipeServer, + ) -> impl Session { + Self { + comment: "pipe".into(), + } + } +} + +#[derive(Debug, Parser)] +struct Args { + #[clap(short = 'H', long)] + host: Binding, +} + +#[tokio::main] +async fn main() -> TestResult { + env_logger::init(); + + let args = Args::parse(); + bind(args.host.try_into()?, AgentSocketInfo::default()).await?; + Ok(()) +}