use std::path::{Path, PathBuf};
use std::time::Duration;
use serde::Serialize;
use serde::de::DeserializeOwned;
use tokio::io::{AsyncBufReadExt, AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt, BufReader};
use tokio::net::UnixStream;
use super::{UdsSecurityError, connect_hardened};
pub const MAX_FRAME_BYTES: u64 = 8 * 1024 * 1024;
#[derive(Debug, thiserror::Error)]
#[non_exhaustive]
pub enum UdsRpcError {
#[error("dial {path}: {source}")]
Dial {
path: PathBuf,
#[source]
source: UdsSecurityError,
},
#[error("serialize request frame for {path}: {source}")]
Encode {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("write request frame to {path}: {source}")]
Write {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("read response frame from {path}: {source}")]
Read {
path: PathBuf,
#[source]
source: std::io::Error,
},
#[error("{path} closed the connection without sending a response frame")]
NoResponse {
path: PathBuf,
},
#[error("response frame from {path} exceeded {limit} bytes without a newline")]
FrameTooLarge {
path: PathBuf,
limit: u64,
},
#[error("decode response frame from {path}: {source}")]
Decode {
path: PathBuf,
#[source]
source: serde_json::Error,
},
#[error("{path} did not complete the exchange within {timeout:?}")]
Timeout {
path: PathBuf,
timeout: Duration,
},
#[error("{path} ended the stream with {error}")]
Stream {
path: PathBuf,
error: crate::uds::server::RpcError,
},
#[error("{path} answered with a single response frame rather than a stream")]
NotAStream {
path: PathBuf,
response: Box<crate::uds::server::RpcResponse>,
},
}
pub async fn send_framed_request<Req, Resp>(
path: &Path,
request: &Req,
timeout: Duration,
) -> Result<Resp, UdsRpcError>
where
Req: Serialize + ?Sized,
Resp: DeserializeOwned,
{
send_framed_request_capped(path, request, timeout, MAX_FRAME_BYTES).await
}
pub async fn send_framed_request_capped<Req, Resp>(
path: &Path,
request: &Req,
timeout: Duration,
max_frame_bytes: u64,
) -> Result<Resp, UdsRpcError>
where
Req: Serialize + ?Sized,
Resp: DeserializeOwned,
{
match tokio::time::timeout(
timeout,
exchange::<Req, Resp>(path, request, max_frame_bytes),
)
.await
{
Ok(result) => result,
Err(_) => Err(UdsRpcError::Timeout {
path: path.to_path_buf(),
timeout,
}),
}
}
pub async fn send_framed_notification<Req>(
path: &Path,
request: &Req,
timeout: Duration,
) -> Result<(), UdsRpcError>
where
Req: Serialize + ?Sized,
{
match tokio::time::timeout(timeout, dial_and_send(path, request)).await {
Ok(result) => result.map(|_stream| ()),
Err(_) => Err(UdsRpcError::Timeout {
path: path.to_path_buf(),
timeout,
}),
}
}
pub fn encode_frame<T>(value: &T) -> serde_json::Result<Vec<u8>>
where
T: Serialize + ?Sized,
{
let mut frame = serde_json::to_vec(value)?;
frame.push(b'\n');
Ok(frame)
}
pub async fn write_frame<W, T>(writer: &mut W, value: &T) -> std::io::Result<()>
where
W: AsyncWrite + Unpin,
T: Serialize + ?Sized,
{
let frame =
encode_frame(value).map_err(|e| std::io::Error::new(std::io::ErrorKind::InvalidData, e))?;
writer.write_all(&frame).await?;
writer.flush().await
}
pub(super) async fn dial_and_send<Req>(
path: &Path,
request: &Req,
) -> Result<UnixStream, UdsRpcError>
where
Req: Serialize + ?Sized,
{
let frame = encode_frame(request).map_err(|source| UdsRpcError::Encode {
path: path.to_path_buf(),
source,
})?;
let mut stream = connect_hardened(path)
.await
.map_err(|source| UdsRpcError::Dial {
path: path.to_path_buf(),
source,
})?;
let write = async {
stream.write_all(&frame).await?;
stream.flush().await?;
stream.shutdown().await
};
write.await.map_err(|source| UdsRpcError::Write {
path: path.to_path_buf(),
source,
})?;
Ok(stream)
}
async fn exchange<Req, Resp>(
path: &Path,
request: &Req,
max_frame_bytes: u64,
) -> Result<Resp, UdsRpcError>
where
Req: Serialize + ?Sized,
Resp: DeserializeOwned,
{
let stream = dial_and_send(path, request).await?;
read_one_frame(stream, path, max_frame_bytes).await
}
async fn read_one_frame<R, Resp>(
source: R,
path: &Path,
max_frame_bytes: u64,
) -> Result<Resp, UdsRpcError>
where
R: AsyncRead + Unpin,
Resp: DeserializeOwned,
{
let mut reader = BufReader::new(source.take(max_frame_bytes));
let mut line: Vec<u8> = Vec::new();
let read = match reader.read_until(b'\n', &mut line).await {
Ok(read) => read,
Err(source) => return Err(classify_read_failure(path, source, line.is_empty())),
};
if read == 0 && line.is_empty() {
return Err(UdsRpcError::NoResponse {
path: path.to_path_buf(),
});
}
if !line.ends_with(b"\n") && line.len() as u64 >= max_frame_bytes {
return Err(UdsRpcError::FrameTooLarge {
path: path.to_path_buf(),
limit: max_frame_bytes,
});
}
serde_json::from_slice(&line).map_err(|source| UdsRpcError::Decode {
path: path.to_path_buf(),
source,
})
}
pub(super) fn classify_read_failure(
path: &Path,
source: std::io::Error,
nothing_buffered: bool,
) -> UdsRpcError {
let hung_up = matches!(
source.kind(),
std::io::ErrorKind::ConnectionReset | std::io::ErrorKind::ConnectionAborted
);
if hung_up && nothing_buffered {
return UdsRpcError::NoResponse {
path: path.to_path_buf(),
};
}
UdsRpcError::Read {
path: path.to_path_buf(),
source,
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::uds::bind_hardened;
use serde::Deserialize;
use std::path::PathBuf;
use tokio::net::UnixListener;
#[derive(Debug, Serialize)]
struct Ping {
method: &'static str,
n: u32,
}
#[derive(Debug, Deserialize, PartialEq, Eq)]
struct Pong {
echoed: u32,
}
enum StubReply {
Bytes(Vec<u8>),
HangUp,
Silence,
}
fn spawn_stub(dir: &Path, replies: Vec<StubReply>) -> PathBuf {
let sock = dir.join("sockets").join("stub.sock");
let listener: UnixListener = bind_hardened(&sock).expect("bind stub socket");
tokio::spawn(async move {
for reply in replies {
let Ok((mut conn, _)) = listener.accept().await else {
return;
};
let mut sink = Vec::new();
let _ = conn.read_to_end(&mut sink).await;
match reply {
StubReply::Bytes(bytes) => {
let _ = conn.write_all(&bytes).await;
let _ = conn.flush().await;
}
StubReply::HangUp => {}
StubReply::Silence => {
tokio::time::sleep(Duration::from_secs(300)).await;
}
}
}
});
sock
}
#[tokio::test]
async fn send_framed_request_round_trips_a_typed_value() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = spawn_stub(
tmp.path(),
vec![StubReply::Bytes(b"{\"echoed\":41}\n".to_vec())],
);
let got: Pong = send_framed_request(
&sock,
&Ping {
method: "ping",
n: 41,
},
Duration::from_secs(5),
)
.await
.expect("round trip");
assert_eq!(got, Pong { echoed: 41 });
}
#[tokio::test]
async fn send_framed_request_accepts_a_frame_without_a_trailing_newline() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = spawn_stub(
tmp.path(),
vec![StubReply::Bytes(b"{\"echoed\":7}".to_vec())],
);
let got: Pong = send_framed_request(
&sock,
&Ping {
method: "ping",
n: 7,
},
Duration::from_secs(5),
)
.await
.expect("round trip");
assert_eq!(got, Pong { echoed: 7 });
}
#[tokio::test]
async fn send_framed_request_reports_no_response_when_peer_hangs_up() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = spawn_stub(tmp.path(), vec![StubReply::HangUp]);
let err = send_framed_request::<_, Pong>(
&sock,
&Ping {
method: "ping",
n: 1,
},
Duration::from_secs(5),
)
.await
.expect_err("a silent hang-up is not a response");
assert!(
matches!(err, UdsRpcError::NoResponse { .. }),
"expected NoResponse, got {err:?}"
);
}
#[tokio::test]
async fn send_framed_request_rejects_an_over_long_frame() {
let tmp = tempfile::tempdir().expect("tempdir");
let flood = vec![b'x'; (MAX_FRAME_BYTES + 1) as usize];
let sock = spawn_stub(tmp.path(), vec![StubReply::Bytes(flood)]);
let err = send_framed_request::<_, Pong>(
&sock,
&Ping {
method: "ping",
n: 1,
},
Duration::from_secs(30),
)
.await
.expect_err("an unterminated flood must not be buffered without bound");
assert!(
matches!(err, UdsRpcError::FrameTooLarge { limit, .. } if limit == MAX_FRAME_BYTES),
"expected FrameTooLarge, got {err:?}"
);
}
#[tokio::test]
async fn send_framed_request_reports_a_decode_failure() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = spawn_stub(tmp.path(), vec![StubReply::Bytes(b"not json\n".to_vec())]);
let err = send_framed_request::<_, Pong>(
&sock,
&Ping {
method: "ping",
n: 1,
},
Duration::from_secs(5),
)
.await
.expect_err("garbage is not a response");
assert!(
matches!(err, UdsRpcError::Decode { .. }),
"expected Decode, got {err:?}"
);
}
#[tokio::test]
async fn send_framed_request_reports_dial_failure_for_a_missing_socket() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("absent.sock");
let err = send_framed_request::<_, Pong>(
&sock,
&Ping {
method: "ping",
n: 1,
},
Duration::from_secs(5),
)
.await
.expect_err("no listener means no delivery");
assert!(
matches!(err, UdsRpcError::Dial { .. }),
"expected Dial, got {err:?}"
);
}
#[tokio::test]
async fn send_framed_request_capped_honours_a_caller_supplied_budget() {
let tmp = tempfile::tempdir().expect("tempdir");
let flood = vec![b'x'; 4096];
let sock = spawn_stub(tmp.path(), vec![StubReply::Bytes(flood)]);
let err = send_framed_request_capped::<_, Pong>(
&sock,
&Ping {
method: "ping",
n: 1,
},
Duration::from_secs(5),
1024,
)
.await
.expect_err("an unterminated flood past the caller's budget must be refused");
assert!(
matches!(err, UdsRpcError::FrameTooLarge { limit, .. } if limit == 1024),
"expected FrameTooLarge at the caller's 1024-byte budget, got {err:?}"
);
}
#[tokio::test]
async fn send_framed_notification_delivers_exactly_one_frame() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("notify.sock");
let listener: UnixListener = bind_hardened(&sock).expect("bind");
let served = tokio::spawn(async move {
let (mut conn, _) = listener.accept().await.expect("accept");
let mut got = Vec::new();
conn.read_to_end(&mut got).await.expect("drain");
got
});
send_framed_notification(
&sock,
&Ping {
method: "ping",
n: 9,
},
Duration::from_secs(5),
)
.await
.expect("a peer that never replies is still a successful delivery");
let bytes = served.await.expect("join");
let text = String::from_utf8(bytes).expect("utf8");
assert_eq!(
text, "{\"method\":\"ping\",\"n\":9}\n",
"one frame, one trailing newline, nothing else"
);
}
#[tokio::test]
async fn send_framed_notification_reports_dial_failure_for_a_missing_socket() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = tmp.path().join("sockets").join("absent.sock");
let err = send_framed_notification(
&sock,
&Ping {
method: "ping",
n: 1,
},
Duration::from_secs(5),
)
.await
.expect_err("no listener means no delivery");
assert!(
matches!(err, UdsRpcError::Dial { .. }),
"expected Dial, got {err:?}"
);
}
#[test]
fn encode_frame_appends_exactly_one_newline() {
let frame = encode_frame(&Ping {
method: "ping",
n: 3,
})
.expect("encode");
assert_eq!(frame, b"{\"method\":\"ping\",\"n\":3}\n");
assert_eq!(
frame.iter().filter(|b| **b == b'\n').count(),
1,
"a frame carries exactly one newline, and it is the terminator"
);
}
#[tokio::test]
async fn write_frame_terminates_each_value_with_one_newline() {
let mut buf: Vec<u8> = Vec::new();
write_frame(&mut buf, &Ping { method: "a", n: 1 })
.await
.expect("first frame");
write_frame(&mut buf, &Ping { method: "b", n: 2 })
.await
.expect("second frame");
assert_eq!(
String::from_utf8(buf).expect("utf8"),
"{\"method\":\"a\",\"n\":1}\n{\"method\":\"b\",\"n\":2}\n"
);
}
#[tokio::test]
async fn send_framed_request_times_out_on_a_silent_peer() {
let tmp = tempfile::tempdir().expect("tempdir");
let sock = spawn_stub(tmp.path(), vec![StubReply::Silence]);
let err = send_framed_request::<_, Pong>(
&sock,
&Ping {
method: "ping",
n: 1,
},
Duration::from_millis(150),
)
.await
.expect_err("a peer that never answers must not hold the caller open");
assert!(
matches!(err, UdsRpcError::Timeout { .. }),
"expected Timeout, got {err:?}"
);
}
#[test]
fn read_failure_from_an_abortive_close_reads_as_a_hang_up() {
for kind in [
std::io::ErrorKind::ConnectionReset,
std::io::ErrorKind::ConnectionAborted,
] {
let err = classify_read_failure(
Path::new("/tmp/relay.sock"),
std::io::Error::new(kind, "peer went away"),
true,
);
assert!(
matches!(err, UdsRpcError::NoResponse { .. }),
"expected NoResponse for {kind:?}, got {err:?}"
);
}
}
#[test]
fn read_failure_after_partial_bytes_stays_a_read_error() {
let err = classify_read_failure(
Path::new("/tmp/relay.sock"),
std::io::Error::new(std::io::ErrorKind::ConnectionReset, "peer went away"),
false,
);
assert!(
matches!(err, UdsRpcError::Read { .. }),
"expected Read, got {err:?}"
);
}
#[test]
fn read_failure_from_an_unrelated_errno_stays_a_read_error() {
let err = classify_read_failure(
Path::new("/tmp/relay.sock"),
std::io::Error::from(std::io::ErrorKind::PermissionDenied),
true,
);
assert!(
matches!(err, UdsRpcError::Read { .. }),
"expected Read, got {err:?}"
);
}
}