#![cfg(unix)]
pub mod actions;
use anyhow::{Context, Result};
use bytes::{BufMut, Bytes, BytesMut};
use std::collections::HashMap;
use std::path::PathBuf;
use std::sync::Arc;
use std::time::Duration;
use tokio::io::{AsyncReadExt, AsyncWriteExt};
use tokio::net::UnixStream;
use tokio::sync::{mpsc, Mutex};
use tracing::{debug, error, info, trace};
use super::connection::ConnectionId;
use crate::llm::action_helper::call_llm;
use crate::llm::ollama_client::OllamaClient;
use crate::llm::ActionResult;
use crate::protocol::Event;
use crate::server::SshAgentProtocol;
use crate::state::app_state::AppState;
use actions::*;
const SSH_AGENTC_REQUEST_IDENTITIES: u8 = 11;
const SSH_AGENTC_SIGN_REQUEST: u8 = 13;
const SSH_AGENTC_ADD_IDENTITY: u8 = 17;
const SSH_AGENTC_REMOVE_IDENTITY: u8 = 18;
const SSH_AGENTC_REMOVE_ALL_IDENTITIES: u8 = 19;
const SSH_AGENTC_ADD_ID_CONSTRAINED: u8 = 25;
const SSH_AGENTC_LOCK: u8 = 22;
const SSH_AGENTC_UNLOCK: u8 = 23;
const SSH_AGENT_FAILURE: u8 = 5;
const SSH_AGENT_SUCCESS: u8 = 6;
const SSH_AGENT_IDENTITIES_ANSWER: u8 = 12;
const SSH_AGENT_SIGN_RESPONSE: u8 = 14;
pub const MAX_AGENT_MESSAGE_LEN: usize = 256 * 1024;
fn take_framed_message(pending: &mut Vec<u8>) -> Result<Option<Vec<u8>>> {
if pending.len() < 4 {
return Ok(None);
}
let length = u32::from_be_bytes([pending[0], pending[1], pending[2], pending[3]]) as usize;
if length == 0 {
anyhow::bail!("SSH Agent message declared zero length");
}
if length > MAX_AGENT_MESSAGE_LEN {
anyhow::bail!(
"SSH Agent message length {length} exceeds the {MAX_AGENT_MESSAGE_LEN} byte limit"
);
}
if pending.len() < 4 + length {
return Ok(None);
}
let frame = pending[..4 + length].to_vec();
pending.drain(..4 + length);
Ok(Some(frame))
}
const FIRST_BYTE_READ_TIMEOUT: Duration = Duration::from_secs(300);
const IDLE_BETWEEN_REQUESTS_TIMEOUT: Duration = Duration::from_secs(900);
const MAX_CONNECTIONS: usize = crate::server::accept_bounded::DEFAULT_MAX_CONNECTIONS;
const CONNECTION_CAP_REFUSAL: &[u8] = b"";
#[derive(Debug, Clone, PartialEq)]
enum ConnectionState {
Idle,
Processing,
Accumulating,
}
struct ConnectionData {
state: ConnectionState,
queued_data: Vec<u8>,
memory: String,
write_half: Arc<Mutex<tokio::io::WriteHalf<UnixStream>>>,
}
pub struct SshAgentServer;
impl SshAgentServer {
pub async fn spawn_with_llm_actions(
socket_path: PathBuf,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
server_id: crate::state::ServerId,
first_byte_timeout_secs: Option<u64>,
idle_timeout_secs: Option<u64>,
) -> Result<PathBuf> {
let first_byte_timeout = first_byte_timeout_secs
.map(Duration::from_secs)
.unwrap_or(FIRST_BYTE_READ_TIMEOUT);
let idle_timeout = idle_timeout_secs
.map(Duration::from_secs)
.unwrap_or(IDLE_BETWEEN_REQUESTS_TIMEOUT);
if let Ok(metadata) = std::fs::symlink_metadata(&socket_path) {
use std::os::unix::fs::FileTypeExt;
if !metadata.file_type().is_socket() {
anyhow::bail!(
"Refusing to start SSH Agent: {:?} already exists and is not a Unix socket. \
Choose a different socket_path.",
socket_path
);
}
std::fs::remove_file(&socket_path).with_context(|| {
format!("Failed to remove existing socket file: {:?}", socket_path)
})?;
}
let listener = tokio::net::UnixListener::bind(&socket_path)
.with_context(|| format!("Failed to bind to socket path: {:?}", socket_path))?;
info!("SSH Agent server listening on {:?}", socket_path);
let _ = status_tx.send(format!("SSH Agent server listening on {:?}", socket_path));
let connections = Arc::new(Mutex::new(HashMap::new()));
let protocol = Arc::new(SshAgentProtocol::new());
let socket_path_clone = socket_path.clone();
let task_registrar = app_state.clone();
let limiter = crate::server::accept_bounded::ConnectionLimiter::new(MAX_CONNECTIONS);
let accept_handle = tokio::spawn(async move {
loop {
match crate::server::accept_bounded::accept_bounded_unix(
&listener,
&limiter,
CONNECTION_CAP_REFUSAL,
"SSH Agent",
Some(&status_tx),
)
.await
{
Ok((stream, permit)) => {
let connection_id =
ConnectionId::new(app_state.get_next_unified_id().await);
info!("Accepted SSH Agent connection {}", connection_id);
let _ = status_tx
.send(format!("✓ SSH Agent connection {} opened", connection_id));
let (read_half, write_half) = tokio::io::split(stream);
let write_half_arc = Arc::new(Mutex::new(write_half));
use crate::state::server::{
ConnectionState as ServerConnectionState, ConnectionStatus,
ProtocolConnectionInfo,
};
let now = crate::utils::clock::Instant::now();
let dummy_addr = "127.0.0.1:0".parse().unwrap();
let conn_state = ServerConnectionState {
id: connection_id,
remote_addr: dummy_addr,
local_addr: dummy_addr,
bytes_sent: 0,
bytes_received: 0,
packets_sent: 0,
packets_received: 0,
last_activity: now,
status: ConnectionStatus::Active,
status_changed_at: now,
protocol_info: ProtocolConnectionInfo::new(serde_json::json!({
"state": "Idle",
"socket_path": socket_path_clone.to_string_lossy()
})),
};
app_state
.add_connection_to_server(server_id, conn_state)
.await;
let _ = status_tx.send("__UPDATE_UI__".to_string());
connections.lock().await.insert(
connection_id,
ConnectionData {
state: ConnectionState::Idle,
queued_data: Vec::new(),
memory: String::new(),
write_half: write_half_arc.clone(),
},
);
let llm_client_clone = llm_client.clone();
let app_state_clone = app_state.clone();
let status_tx_clone = status_tx.clone();
let connections_clone = connections.clone();
let protocol_clone = protocol.clone();
let task_owner = app_state.clone();
task_owner
.spawn_server_task(server_id, async move {
Self::raise_connection_opened(
connection_id,
server_id,
llm_client_clone,
app_state_clone,
status_tx_clone,
connections_clone,
protocol_clone,
)
.await;
})
.await;
let llm_client_clone = llm_client.clone();
let app_state_clone = app_state.clone();
let status_tx_clone = status_tx.clone();
let connections_clone = connections.clone();
let protocol_clone = protocol.clone();
let task_owner = app_state.clone();
task_owner.spawn_server_task(server_id, async move {
let _permit = permit;
let mut buffer = vec![0u8; 8192];
let mut read_half = read_half;
let mut pending: Vec<u8> = Vec::new();
let mut seen_bytes = false;
loop {
let bound = if seen_bytes {
idle_timeout
} else {
first_byte_timeout
};
let read = match tokio::time::timeout(
bound,
read_half.read(&mut buffer),
)
.await
{
Ok(read) => read,
Err(_) => {
connections_clone.lock().await.remove(&connection_id);
app_state_clone
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx_clone.send(format!(
"SSH Agent connection {} sent nothing for {}s; \
closing idle connection",
connection_id,
bound.as_secs()
));
let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
break;
}
};
match read {
Ok(0) => {
connections_clone.lock().await.remove(&connection_id);
app_state_clone
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx_clone.send(format!(
"✗ SSH Agent connection {} closed",
connection_id
));
let _ = status_tx_clone.send("__UPDATE_UI__".to_string());
break;
}
Ok(n) => {
seen_bytes = true;
trace!(
"SSH Agent received {} bytes on connection {}",
n,
connection_id
);
pending.extend_from_slice(&buffer[..n]);
let mut fatal = false;
loop {
match take_framed_message(&mut pending) {
Ok(Some(frame)) => {
Self::handle_data_with_actions(
connection_id,
server_id,
Bytes::from(frame),
llm_client_clone.clone(),
app_state_clone.clone(),
status_tx_clone.clone(),
connections_clone.clone(),
protocol_clone.clone(),
)
.await;
}
Ok(None) => break,
Err(e) => {
error!(
"Malformed framing on SSH Agent connection {}: {}",
connection_id, e
);
let _ = status_tx_clone.send(format!(
"[ERROR] SSH Agent framing error on connection {}: {}",
connection_id, e
));
fatal = true;
break;
}
}
}
if fatal {
connections_clone.lock().await.remove(&connection_id);
app_state_clone
.close_connection_on_server(
server_id,
connection_id,
)
.await;
let _ =
status_tx_clone.send("__UPDATE_UI__".to_string());
break;
}
}
Err(e) => {
error!(
"Read error on SSH Agent connection {}: {}",
connection_id, e
);
connections_clone.lock().await.remove(&connection_id);
break;
}
}
}
}).await;
}
Err(e) => {
error!("SSH Agent accept failed, listener stopped: {}", e);
let _ = status_tx.send(format!(
"[ERROR] SSH Agent accept failed, listener stopped: {}",
e
));
break;
}
}
}
});
task_registrar
.register_server_task(server_id, accept_handle)
.await;
Ok(socket_path)
}
async fn raise_connection_opened(
connection_id: ConnectionId,
server_id: crate::state::ServerId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
connections: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
protocol: Arc<SshAgentProtocol>,
) {
let event = Event::new(
&SSH_AGENT_CONNECTION_OPENED_EVENT,
serde_json::json!({
"connection_id": connection_id.to_string(),
}),
);
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution_result) => {
for msg in execution_result.messages {
let _ = status_tx.send(msg);
}
for action in execution_result.protocol_results {
if let Err(e) = Self::execute_action_result(
action,
connection_id,
server_id,
&connections,
&app_state,
&status_tx,
)
.await
{
error!("Failed to execute action: {}", e);
}
}
}
Err(e) => {
error!("LLM error on connection opened: {}", e);
}
}
}
async fn handle_data_with_actions(
connection_id: ConnectionId,
server_id: crate::state::ServerId,
data: Bytes,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
connections: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
protocol: Arc<SshAgentProtocol>,
) {
let mut conns = connections.lock().await;
let conn_data = match conns.get_mut(&connection_id) {
Some(cd) => cd,
None => return,
};
match conn_data.state {
ConnectionState::Idle => {
conn_data.state = ConnectionState::Processing;
let current_memory = conn_data.memory.clone();
drop(conns);
Box::pin(Self::process_message_with_llm(
connection_id,
server_id,
data.to_vec(),
llm_client,
app_state,
status_tx,
connections.clone(),
protocol,
current_memory,
))
.await;
}
ConnectionState::Processing => {
conn_data.queued_data.extend_from_slice(&data);
conn_data.state = ConnectionState::Accumulating;
debug!(
"SSH Agent: Queuing {} bytes (total queued: {})",
data.len(),
conn_data.queued_data.len()
);
}
ConnectionState::Accumulating => {
conn_data.queued_data.extend_from_slice(&data);
debug!(
"SSH Agent: Queuing {} bytes (total queued: {})",
data.len(),
conn_data.queued_data.len()
);
}
}
}
async fn process_message_with_llm(
connection_id: ConnectionId,
server_id: crate::state::ServerId,
data: Vec<u8>,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
connections: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
protocol: Arc<SshAgentProtocol>,
_current_memory: String,
) {
let event = match Self::parse_message(&data) {
Ok(Some(event)) => event,
Ok(None) => {
Self::send_failure(connection_id, &connections).await;
Self::check_queued_data(
connection_id,
server_id,
llm_client,
app_state,
status_tx,
connections,
protocol,
)
.await;
return;
}
Err(e) => {
error!("Failed to parse SSH Agent message: {}", e);
Self::send_failure(connection_id, &connections).await;
Self::check_queued_data(
connection_id,
server_id,
llm_client,
app_state,
status_tx,
connections,
protocol,
)
.await;
return;
}
};
match call_llm(
&llm_client,
&app_state,
server_id,
Some(connection_id),
&event,
protocol.as_ref(),
)
.await
{
Ok(execution_result) => {
for msg in execution_result.messages {
let _ = status_tx.send(msg);
}
let mut answered = false;
let mut execution_failed = false;
for action in execution_result.protocol_results {
match Self::execute_action_result(
action,
connection_id,
server_id,
&connections,
&app_state,
&status_tx,
)
.await
{
Ok(true) => answered = true,
Ok(false) => {}
Err(e) => {
error!("Failed to execute action: {}", e);
execution_failed = true;
}
}
}
if !answered {
let decision = if execution_failed {
"fail_closed_action_error"
} else {
"fail_closed_no_action"
};
let message = format!(
"SSH Agent request on connection {}: decision={} (denied because no \
usable answer was produced)",
connection_id, decision
);
error!("{}", message);
let _ = status_tx.send(message);
Self::send_failure(connection_id, &connections).await;
}
}
Err(e) => {
let category = crate::utils::WireFailure::classify(&e);
error!(
"SSH Agent request on connection {}: decision=fail_closed_llm_error \
category={:?}: {}",
connection_id, category, e
);
let _ = status_tx.send(format!(
"SSH Agent request on connection {}: decision=fail_closed_llm_error \
category={:?}",
connection_id, category
));
Self::send_failure(connection_id, &connections).await;
}
}
Self::check_queued_data(
connection_id,
server_id,
llm_client,
app_state,
status_tx,
connections,
protocol,
)
.await;
}
fn parse_message(data: &[u8]) -> Result<Option<Event>> {
if data.len() < 5 {
return Ok(None);
}
let _length = u32::from_be_bytes([data[0], data[1], data[2], data[3]]);
let msg_type = data[4];
let mut cursor = &data[5..];
debug!(
"SSH Agent: parsing message type {} from {} bytes",
msg_type,
data.len()
);
match msg_type {
SSH_AGENTC_REQUEST_IDENTITIES => Ok(Some(Event::new(
&SSH_AGENT_REQUEST_IDENTITIES_EVENT,
serde_json::json!({}),
))),
SSH_AGENTC_SIGN_REQUEST => {
let public_key_blob = Self::read_string(&mut cursor)?;
let data_to_sign = Self::read_string(&mut cursor)?;
let flags = Self::read_uint32(&mut cursor)?;
Ok(Some(Event::new(
&SSH_AGENT_SIGN_REQUEST_EVENT,
serde_json::json!({
"key_type": Self::key_type_of(public_key_blob),
"public_key_blob_hex": hex::encode(public_key_blob),
"data_hex": hex::encode(data_to_sign),
"flags": flags,
}),
)))
}
SSH_AGENTC_ADD_IDENTITY | SSH_AGENTC_ADD_ID_CONSTRAINED => {
let key_type = Self::read_string(&mut cursor)?;
let key_type_str = String::from_utf8_lossy(key_type);
let public_key_blob = Self::read_string(&mut cursor)?;
let _private_key_blob = Self::read_string(&mut cursor)?; let comment = Self::read_string(&mut cursor)?;
Ok(Some(Event::new(
&SSH_AGENT_ADD_IDENTITY_EVENT,
serde_json::json!({
"key_type": key_type_str,
"public_key_blob_hex": hex::encode(public_key_blob),
"comment": String::from_utf8_lossy(comment),
"constrained": msg_type == SSH_AGENTC_ADD_ID_CONSTRAINED,
}),
)))
}
SSH_AGENTC_REMOVE_IDENTITY => {
let public_key_blob = Self::read_string(&mut cursor)?;
Ok(Some(Event::new(
&SSH_AGENT_REMOVE_IDENTITY_EVENT,
serde_json::json!({
"public_key_blob_hex": hex::encode(public_key_blob),
}),
)))
}
SSH_AGENTC_REMOVE_ALL_IDENTITIES => Ok(Some(Event::new(
&SSH_AGENT_REMOVE_ALL_IDENTITIES_EVENT,
serde_json::json!({}),
))),
SSH_AGENTC_LOCK => {
let passphrase = Self::read_string(&mut cursor)?;
Ok(Some(Event::new(
&SSH_AGENT_LOCK_EVENT,
serde_json::json!({
"passphrase": String::from_utf8_lossy(passphrase),
}),
)))
}
SSH_AGENTC_UNLOCK => {
let passphrase = Self::read_string(&mut cursor)?;
Ok(Some(Event::new(
&SSH_AGENT_UNLOCK_EVENT,
serde_json::json!({
"passphrase": String::from_utf8_lossy(passphrase),
}),
)))
}
_ => {
debug!("Unknown SSH Agent message type: {}", msg_type);
Ok(None)
}
}
}
fn key_type_of(blob: &[u8]) -> String {
let mut cursor = blob;
match Self::read_string(&mut cursor) {
Ok(name) => String::from_utf8_lossy(name).to_string(),
Err(_) => String::new(),
}
}
fn read_string<'a>(cursor: &mut &'a [u8]) -> Result<&'a [u8]> {
if cursor.len() < 4 {
anyhow::bail!("Not enough data to read string length");
}
let len = u32::from_be_bytes([cursor[0], cursor[1], cursor[2], cursor[3]]) as usize;
*cursor = &cursor[4..];
if cursor.len() < len {
anyhow::bail!("Not enough data to read string of length {}", len);
}
let result = &cursor[..len];
*cursor = &cursor[len..];
Ok(result)
}
fn read_uint32(cursor: &mut &[u8]) -> Result<u32> {
if cursor.len() < 4 {
anyhow::bail!("Not enough data to read uint32");
}
let result = u32::from_be_bytes([cursor[0], cursor[1], cursor[2], cursor[3]]);
*cursor = &cursor[4..];
Ok(result)
}
async fn execute_action_result(
action: ActionResult,
connection_id: ConnectionId,
server_id: crate::state::ServerId,
connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
app_state: &Arc<AppState>,
status_tx: &mpsc::UnboundedSender<String>,
) -> Result<bool> {
match action {
ActionResult::Custom { name, data } => match name.as_str() {
"send_identities_list" => {
let identities = data["identities"]
.as_array()
.context("Missing 'identities' field")?;
Self::send_identities_list(connection_id, identities, connections).await;
Ok(true)
}
"send_sign_response" => {
let signature_hex = data["signature_hex"]
.as_str()
.context("Missing 'signature_hex' field")?;
Self::send_sign_response(connection_id, signature_hex, connections).await;
Ok(true)
}
"send_success" => {
Self::send_success(connection_id, connections).await;
Ok(true)
}
"send_failure" => {
Self::send_failure(connection_id, connections).await;
Ok(true)
}
other => {
debug!("Unknown custom action: {}", other);
Ok(false)
}
},
ActionResult::CloseConnection => {
connections.lock().await.remove(&connection_id);
app_state
.close_connection_on_server(server_id, connection_id)
.await;
let _ = status_tx.send(format!("✗ SSH Agent connection {} closed", connection_id));
let _ = status_tx.send("__UPDATE_UI__".to_string());
Ok(true)
}
ActionResult::WaitForMore => Ok(true),
_ => Ok(false),
}
}
async fn send_identities_list(
connection_id: ConnectionId,
identities: &Vec<serde_json::Value>,
connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
) {
let mut decoded: Vec<(Vec<u8>, &str)> = Vec::with_capacity(identities.len());
for identity in identities {
let key_blob_hex = identity["public_key_blob_hex"].as_str().unwrap_or("");
let comment = identity["comment"].as_str().unwrap_or("");
match hex::decode(key_blob_hex) {
Ok(blob) if !blob.is_empty() => decoded.push((blob, comment)),
Ok(_) => {
error!(
"SSH Agent: identity {:?} has an empty public_key_blob_hex; failing the request",
comment
);
Self::send_failure(connection_id, connections).await;
return;
}
Err(e) => {
error!(
"SSH Agent: identity {:?} has invalid hex in public_key_blob_hex: {}",
comment, e
);
Self::send_failure(connection_id, connections).await;
return;
}
}
}
let mut response = BytesMut::new();
response.put_u8(SSH_AGENT_IDENTITIES_ANSWER);
response.put_u32(decoded.len() as u32);
for (key_blob, comment) in &decoded {
response.put_u32(key_blob.len() as u32);
response.put_slice(key_blob);
response.put_u32(comment.len() as u32);
response.put_slice(comment.as_bytes());
}
Self::send_response(connection_id, response.freeze(), connections).await;
}
async fn send_sign_response(
connection_id: ConnectionId,
signature_hex: &str,
connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
) {
let signature_blob = match hex::decode(signature_hex) {
Ok(blob) if !blob.is_empty() => blob,
Ok(_) => {
error!("SSH Agent: signature_hex decoded to zero bytes; failing the sign request");
Self::send_failure(connection_id, connections).await;
return;
}
Err(e) => {
error!("SSH Agent: invalid hex in signature_hex: {}", e);
Self::send_failure(connection_id, connections).await;
return;
}
};
let mut response = BytesMut::new();
response.put_u8(SSH_AGENT_SIGN_RESPONSE);
response.put_u32(signature_blob.len() as u32);
response.put_slice(&signature_blob);
Self::send_response(connection_id, response.freeze(), connections).await;
}
async fn send_success(
connection_id: ConnectionId,
connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
) {
let mut response = BytesMut::new();
response.put_u8(SSH_AGENT_SUCCESS);
Self::send_response(connection_id, response.freeze(), connections).await;
}
async fn send_failure(
connection_id: ConnectionId,
connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
) {
let mut response = BytesMut::new();
response.put_u8(SSH_AGENT_FAILURE);
Self::send_response(connection_id, response.freeze(), connections).await;
}
async fn send_response(
connection_id: ConnectionId,
data: Bytes,
connections: &Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
) {
let conns = connections.lock().await;
if let Some(conn_data) = conns.get(&connection_id) {
let mut message = BytesMut::new();
message.put_u32(data.len() as u32);
message.put_slice(&data);
let mut write_half = conn_data.write_half.lock().await;
if let Err(e) = write_half.write_all(&message).await {
error!("Failed to write SSH Agent response: {}", e);
}
} else {
debug!(
"SSH Agent: dropping response for closed connection {}",
connection_id
);
}
}
async fn check_queued_data(
connection_id: ConnectionId,
server_id: crate::state::ServerId,
llm_client: OllamaClient,
app_state: Arc<AppState>,
status_tx: mpsc::UnboundedSender<String>,
connections: Arc<Mutex<HashMap<ConnectionId, ConnectionData>>>,
protocol: Arc<SshAgentProtocol>,
) {
let mut conns = connections.lock().await;
if let Some(conn_data) = conns.get_mut(&connection_id) {
if conn_data.state == ConnectionState::Accumulating {
let queued = std::mem::take(&mut conn_data.queued_data);
conn_data.state = ConnectionState::Processing;
let current_memory = conn_data.memory.clone();
drop(conns);
Box::pin(Self::process_message_with_llm(
connection_id,
server_id,
queued,
llm_client,
app_state,
status_tx,
connections,
protocol,
current_memory,
))
.await;
} else {
conn_data.state = ConnectionState::Idle;
}
}
}
}