use crate::security::zap::{ZapMechanism, ZapRequest, ZapResponse, ZapStatus};
use crate::{DealerSocket, inproc_stream::InprocStream};
use bytes::Bytes;
use monocoque_core::options::SocketOptions;
use std::io;
use std::time::Duration;
pub struct ZapClient {
socket: DealerSocket<InprocStream>,
timeout: Duration,
}
impl ZapClient {
pub fn new(timeout: Duration) -> io::Result<Self> {
let socket =
DealerSocket::connect_inproc("inproc://zeromq.zap.01", SocketOptions::default())?;
Ok(Self { socket, timeout })
}
fn denial_response(request_id: impl Into<String>, reason: &str) -> ZapResponse {
ZapResponse {
version: crate::security::zap::ZAP_VERSION.to_string(),
request_id: request_id.into(),
status_code: ZapStatus::Failure,
status_text: reason.to_string(),
user_id: String::new(),
metadata: std::collections::HashMap::new(),
}
}
pub async fn authenticate(&mut self, request: &ZapRequest) -> io::Result<ZapResponse> {
let frames = request.encode();
if let Err(e) = self.socket.send(frames).await {
if e.kind() == io::ErrorKind::NotFound {
return Ok(Self::denial_response(
&request.request_id,
"No ZAP handler registered - connection denied by default",
));
}
return Err(e);
}
let recv_future = self.socket.recv();
let response_frames = match monocoque_core::rt::timeout(self.timeout, recv_future).await {
Ok(Ok(Some(frames))) => frames,
Ok(Ok(None)) => {
return Err(io::Error::new(
io::ErrorKind::ConnectionReset,
"ZAP handler disconnected",
));
}
Ok(Err(e)) if e.kind() == io::ErrorKind::NotFound => {
return Ok(Self::denial_response(
&request.request_id,
"ZAP handler unavailable - connection denied by default",
));
}
Ok(Err(e)) => return Err(e),
Err(_) => {
return Err(io::Error::new(
io::ErrorKind::TimedOut,
"ZAP request timed out",
));
}
};
let response = ZapResponse::decode(&response_frames).map_err(|e| {
io::Error::new(
io::ErrorKind::InvalidData,
format!("Failed to decode ZAP response: {}", e),
)
})?;
if response.version != crate::security::zap::ZAP_VERSION {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"ZAP response version mismatch: expected {}, got {}",
crate::security::zap::ZAP_VERSION,
response.version
),
));
}
if response.request_id != request.request_id {
return Err(io::Error::new(
io::ErrorKind::InvalidData,
format!(
"ZAP response request_id mismatch: expected {:?}, got {:?}",
request.request_id, response.request_id
),
));
}
Ok(response)
}
pub async fn authenticate_plain(
&mut self,
username: &str,
password: &str,
domain: &str,
address: &str,
) -> io::Result<ZapResponse> {
let request = ZapRequest::new_with_unique_id(
domain,
address,
Bytes::new(),
ZapMechanism::Plain,
vec![
Bytes::from(username.as_bytes().to_vec()),
Bytes::from(password.as_bytes().to_vec()),
],
);
self.authenticate(&request).await
}
pub async fn authenticate_curve(
&mut self,
client_key: &[u8; 32],
domain: &str,
address: &str,
) -> io::Result<ZapResponse> {
let request = ZapRequest::new_with_unique_id(
domain,
address,
Bytes::new(),
ZapMechanism::Curve,
vec![Bytes::from(client_key.to_vec())],
);
self.authenticate(&request).await
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::security::zap::next_request_id;
#[test]
fn test_zap_client_creation() {
let result = ZapClient::new(Duration::from_secs(1));
assert!(result.is_ok() || result.is_err());
}
#[test]
fn test_unique_request_ids_are_monotonic() {
let id1 = next_request_id();
let id2 = next_request_id();
let id3 = next_request_id();
let n1: u64 = id1.parse().expect("request ID must be a decimal number");
let n2: u64 = id2.parse().expect("request ID must be a decimal number");
let n3: u64 = id3.parse().expect("request ID must be a decimal number");
assert!(n2 > n1, "request IDs must be strictly increasing");
assert!(n3 > n2, "request IDs must be strictly increasing");
}
#[test]
fn test_new_with_unique_id_produces_distinct_ids() {
let r1 = ZapRequest::new_with_unique_id(
"test",
"127.0.0.1",
Bytes::new(),
ZapMechanism::Null,
vec![],
);
let r2 = ZapRequest::new_with_unique_id(
"test",
"127.0.0.1",
Bytes::new(),
ZapMechanism::Null,
vec![],
);
assert_ne!(
r1.request_id, r2.request_id,
"each request must have a unique ID"
);
}
#[test]
fn test_denial_response_is_failure() {
let resp = ZapClient::denial_response(
"42",
"No ZAP handler registered - connection denied by default",
);
assert_eq!(
resp.status_code,
ZapStatus::Failure,
"missing ZAP handler must produce a Failure (400) response"
);
assert!(
resp.user_id.is_empty(),
"denied response must have empty user_id"
);
assert_eq!(resp.request_id, "42");
}
}