1use std::fmt;
7use std::time::{Duration, SystemTime, UNIX_EPOCH};
8
9use lean_ctx_protocol::runtime_exchange::{
10 RuntimeExchangeProfile, RuntimeRequestV1, RuntimeResponseV1,
11};
12use lean_ctx_protocol::runtime_frame::{
13 RUNTIME_FRAME_HEADER_BYTES, RUNTIME_FRAME_NONCE_BYTES, RUNTIME_FRAME_TAG_BYTES,
14 RuntimeFrameDirection, decode_runtime_frame_in, encode_runtime_frame_in,
15 runtime_payload_length_in,
16};
17use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
18use tokio::time::{Instant, timeout_at};
19
20use super::DaemonAddr;
21
22#[derive(Debug, Clone, Copy, PartialEq, Eq)]
24pub enum RuntimeExchangeError {
25 InvalidRequest,
26 InvalidEndpoint,
27 EntropyUnavailable,
28 DeadlineExceeded,
29 ConnectionUnavailable,
30 Io,
31 InvalidFrame,
32 InvalidResponse,
33 ClockUnavailable,
34}
35
36impl fmt::Display for RuntimeExchangeError {
37 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
38 formatter.write_str(match self {
39 Self::InvalidRequest => "invalid local runtime request",
40 Self::InvalidEndpoint => "invalid local runtime endpoint",
41 Self::EntropyUnavailable => "local runtime randomness unavailable",
42 Self::DeadlineExceeded => "local runtime deadline exceeded",
43 Self::ConnectionUnavailable => "local runtime connection unavailable",
44 Self::Io => "local runtime I/O failed",
45 Self::InvalidFrame => "invalid authenticated runtime frame",
46 Self::InvalidResponse => "invalid local runtime response",
47 Self::ClockUnavailable => "local runtime clock unavailable",
48 })
49 }
50}
51
52impl std::error::Error for RuntimeExchangeError {}
53
54pub async fn exchange(
64 address: &DaemonAddr,
65 key: &[u8; 32],
66 request: &RuntimeRequestV1,
67) -> Result<RuntimeResponseV1, RuntimeExchangeError> {
68 exchange_in(address, key, request, RuntimeExchangeProfile::ORDINARY).await
69}
70
71pub(crate) async fn exchange_in(
75 address: &DaemonAddr,
76 key: &[u8; 32],
77 request: &RuntimeRequestV1,
78 profile: RuntimeExchangeProfile,
79) -> Result<RuntimeResponseV1, RuntimeExchangeError> {
80 validate_local_address(address)?;
81 let (deadline, frame) = prepare_exchange(key, request, profile)?;
82 let response = timeout_at(deadline, async {
83 let mut stream = super::connect(address)
84 .await
85 .map_err(|_| RuntimeExchangeError::ConnectionUnavailable)?;
86 exchange_connected(&mut stream, key, request, &frame, profile).await
87 })
88 .await
89 .map_err(|_| RuntimeExchangeError::DeadlineExceeded)?;
90 finish_exchange(deadline, response)
91}
92
93pub(crate) async fn exchange_inherited<S: AsyncRead + AsyncWrite + Unpin>(
98 stream: &mut S,
99 key: &[u8; 32],
100 request: &RuntimeRequestV1,
101 profile: RuntimeExchangeProfile,
102) -> Result<RuntimeResponseV1, RuntimeExchangeError> {
103 let (deadline, frame) = prepare_exchange(key, request, profile)?;
104 let response = timeout_at(
105 deadline,
106 exchange_connected(stream, key, request, &frame, profile),
107 )
108 .await
109 .map_err(|_| RuntimeExchangeError::DeadlineExceeded)?;
110 finish_exchange(deadline, response)
111}
112
113fn prepare_exchange(
114 key: &[u8; 32],
115 request: &RuntimeRequestV1,
116 profile: RuntimeExchangeProfile,
117) -> Result<(Instant, Vec<u8>), RuntimeExchangeError> {
118 let started = Instant::now();
119 let now = unix_millis()?;
120 request
121 .validate_at_in(now, profile)
122 .map_err(|_| RuntimeExchangeError::InvalidRequest)?;
123 let deadline = started + Duration::from_millis(request.deadline_unix_ms - now);
124 let payload = request
125 .to_bytes_in(profile)
126 .map_err(|_| RuntimeExchangeError::InvalidRequest)?;
127 let mut nonce = [0; RUNTIME_FRAME_NONCE_BYTES];
128 getrandom::fill(&mut nonce).map_err(|_| RuntimeExchangeError::EntropyUnavailable)?;
129 let frame = encode_runtime_frame_in(
130 key,
131 RuntimeFrameDirection::Request,
132 &nonce,
133 &payload,
134 profile,
135 )
136 .map_err(|_| RuntimeExchangeError::InvalidFrame)?;
137 ensure_remaining(deadline)?;
138 Ok((deadline, frame))
139}
140
141fn finish_exchange(
142 deadline: Instant,
143 response: Result<RuntimeResponseV1, RuntimeExchangeError>,
144) -> Result<RuntimeResponseV1, RuntimeExchangeError> {
145 ensure_remaining(deadline)?;
147 response
148}
149
150fn validate_local_address(address: &DaemonAddr) -> Result<(), RuntimeExchangeError> {
151 let valid = match address {
152 #[cfg(unix)]
153 DaemonAddr::Unix(path) => path.is_absolute(),
154 #[cfg(windows)]
155 DaemonAddr::NamedPipe(name) => name.strip_prefix(r"\\.\pipe\").is_some_and(|suffix| {
156 !suffix.is_empty()
157 && suffix.len() <= 200
158 && !suffix
159 .chars()
160 .any(|ch| ch.is_control() || ch == '\\' || ch == '/')
161 }),
162 };
163 if !valid {
164 return Err(RuntimeExchangeError::InvalidEndpoint);
165 }
166 Ok(())
167}
168
169async fn exchange_connected<S: AsyncRead + AsyncWrite + Unpin>(
170 stream: &mut S,
171 key: &[u8; 32],
172 request: &RuntimeRequestV1,
173 frame: &[u8],
174 profile: RuntimeExchangeProfile,
175) -> Result<RuntimeResponseV1, RuntimeExchangeError> {
176 stream
177 .write_all(frame)
178 .await
179 .map_err(|_| RuntimeExchangeError::Io)?;
180 stream.flush().await.map_err(|_| RuntimeExchangeError::Io)?;
181 let response_frame = read_frame(stream, profile).await?;
182 let payload = decode_runtime_frame_in(
183 key,
184 RuntimeFrameDirection::Response,
185 &response_frame,
186 profile,
187 )
188 .map_err(|_| RuntimeExchangeError::InvalidFrame)?;
189 RuntimeResponseV1::from_bytes_in(&payload, request, unix_millis()?, profile)
190 .map_err(|_| RuntimeExchangeError::InvalidResponse)
191}
192
193async fn read_frame<S: AsyncRead + Unpin>(
194 stream: &mut S,
195 profile: RuntimeExchangeProfile,
196) -> Result<Vec<u8>, RuntimeExchangeError> {
197 let mut header = [0_u8; RUNTIME_FRAME_HEADER_BYTES];
198 stream
199 .read_exact(&mut header)
200 .await
201 .map_err(|_| RuntimeExchangeError::Io)?;
202 let length = runtime_payload_length_in(&header, RuntimeFrameDirection::Response, profile)
204 .map_err(|_| RuntimeExchangeError::InvalidFrame)?;
205 let mut frame = vec![
206 0;
207 RUNTIME_FRAME_HEADER_BYTES
208 + RUNTIME_FRAME_NONCE_BYTES
209 + RUNTIME_FRAME_TAG_BYTES
210 + length
211 ];
212 frame[..RUNTIME_FRAME_HEADER_BYTES].copy_from_slice(&header);
213 stream
214 .read_exact(&mut frame[RUNTIME_FRAME_HEADER_BYTES..])
215 .await
216 .map_err(|_| RuntimeExchangeError::Io)?;
217 Ok(frame)
218}
219
220fn ensure_remaining(deadline: Instant) -> Result<(), RuntimeExchangeError> {
221 if Instant::now() >= deadline {
222 return Err(RuntimeExchangeError::DeadlineExceeded);
223 }
224 Ok(())
225}
226
227fn unix_millis() -> Result<u64, RuntimeExchangeError> {
228 SystemTime::now()
229 .duration_since(UNIX_EPOCH)
230 .ok()
231 .and_then(|duration| u64::try_from(duration.as_millis()).ok())
232 .ok_or(RuntimeExchangeError::ClockUnavailable)
233}
234
235#[cfg(test)]
236mod tests;