pub mod response;
pub use response::{
decode_response, encode_response_fragment, pack_response, split_response, unpack_response,
FragmentOutcome, ResponseDecoder, ResponseHeader, ResponseSymbols, RpcResponse,
MAX_RESPONSE_BODY, MAX_RESPONSE_FRAGMENT, MAX_RESPONSE_FRAGMENTS, MAX_RESPONSE_HEADER_BLOCK,
RESPONSE_HEADERS_FLAG, RPC_REPAIR_SYMBOLS, RPC_RESPONSE_OVERHEAD_BASE,
};
use crate::aux_mux::AUX_MAX_PAYLOAD;
use crate::node_status::MAX_DEVICE_ID;
pub const RPC_REQUEST_FRAGMENT_OVERHEAD: usize = 10;
pub const MAX_REQUEST_CHUNK: usize =
AUX_MAX_PAYLOAD - RPC_REQUEST_FRAGMENT_OVERHEAD - MAX_DEVICE_ID;
pub const MAX_REQUEST_BODY: usize = 1024 * 1024;
pub const MAX_REQUEST_TICKET: usize = 1024;
pub const MAX_REQUEST_OBJECT: usize =
MAX_REQUEST_BODY + 2 + u16::MAX as usize + 1 + u8::MAX as usize + 2 + MAX_REQUEST_TICKET;
pub const MAX_REQUEST_FRAGMENTS: usize = MAX_REQUEST_OBJECT.div_ceil(MAX_REQUEST_CHUNK);
pub const REQUEST_REASSEMBLY_TIMEOUT: std::time::Duration = std::time::Duration::from_secs(15);
pub const MAX_PARTIAL_REQUESTS: usize = 8;
pub const MAX_PARTIAL_REQUEST_BYTES: usize = 4 * MAX_REQUEST_OBJECT;
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
#[repr(u8)]
pub enum RpcMethod {
Get = 1,
Post = 2,
Put = 3,
Delete = 4,
Patch = 5,
}
impl RpcMethod {
pub fn from_u8(v: u8) -> Option<Self> {
match v {
1 => Some(Self::Get),
2 => Some(Self::Post),
3 => Some(Self::Put),
4 => Some(Self::Delete),
5 => Some(Self::Patch),
_ => None,
}
}
pub fn from_http_method(method: &str) -> Option<Self> {
match method {
"GET" => Some(Self::Get),
"POST" => Some(Self::Post),
"PUT" => Some(Self::Put),
"DELETE" => Some(Self::Delete),
"PATCH" => Some(Self::Patch),
_ => None,
}
}
pub fn as_http_method(&self) -> &'static str {
match self {
Self::Get => "GET",
Self::Post => "POST",
Self::Put => "PUT",
Self::Delete => "DELETE",
Self::Patch => "PATCH",
}
}
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RpcRequest {
pub id: u32,
pub method: RpcMethod,
pub target: Vec<u8>,
pub path: Vec<u8>,
pub content_type: Vec<u8>,
pub ticket: Vec<u8>,
pub body: Vec<u8>,
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub struct RequestFragment<'a> {
pub id: u32,
pub method: RpcMethod,
pub target: &'a [u8],
pub seq: u16,
pub total: u16,
pub chunk: &'a [u8],
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RpcCodecError {
TooShort,
BadMethod(u8),
LengthMismatch { declared: usize, actual: usize },
BadFragmentIndex { index: u16, total: u16 },
BadSenderLen(u8),
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum RequestEncodeError {
BodyTooLarge,
FieldTooLong,
}
pub fn encode_request(
method: RpcMethod,
id: u32,
target: &[u8],
request: &RequestParts<'_>,
) -> Result<Vec<Vec<u8>>, RequestEncodeError> {
if request.body.len() > MAX_REQUEST_BODY {
return Err(RequestEncodeError::BodyTooLarge);
}
if target.len() > MAX_DEVICE_ID
|| request.path.len() > u16::MAX as usize
|| request.content_type.len() > u8::MAX as usize
|| request.ticket.len() > MAX_REQUEST_TICKET
{
return Err(RequestEncodeError::FieldTooLong);
}
let mut object = Vec::with_capacity(
5 + request.path.len()
+ request.content_type.len()
+ request.ticket.len()
+ request.body.len(),
);
object.extend_from_slice(&(request.path.len() as u16).to_be_bytes());
object.extend_from_slice(request.path);
object.push(request.content_type.len() as u8);
object.extend_from_slice(request.content_type);
object.extend_from_slice(&(request.ticket.len() as u16).to_be_bytes());
object.extend_from_slice(request.ticket);
object.extend_from_slice(request.body);
let chunks: Vec<&[u8]> = object.chunks(MAX_REQUEST_CHUNK).collect();
let total = chunks.len() as u16;
Ok(chunks
.iter()
.enumerate()
.map(|(seq, chunk)| {
let mut out =
Vec::with_capacity(RPC_REQUEST_FRAGMENT_OVERHEAD + target.len() + chunk.len());
out.push(method as u8);
out.extend_from_slice(&id.to_be_bytes());
out.push(target.len() as u8);
out.extend_from_slice(target);
out.extend_from_slice(&(seq as u16).to_be_bytes());
out.extend_from_slice(&total.to_be_bytes());
out.extend_from_slice(chunk);
out
})
.collect())
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq)]
pub struct RequestParts<'a> {
pub path: &'a [u8],
pub content_type: &'a [u8],
pub ticket: &'a [u8],
pub body: &'a [u8],
}
pub fn decode_request_fragment(payload: &[u8]) -> Result<RequestFragment<'_>, RpcCodecError> {
if payload.len() < RPC_REQUEST_FRAGMENT_OVERHEAD {
return Err(RpcCodecError::TooShort);
}
let method = RpcMethod::from_u8(payload[0]).ok_or(RpcCodecError::BadMethod(payload[0]))?;
let id = u32::from_be_bytes([payload[1], payload[2], payload[3], payload[4]]);
let target_len = payload[5] as usize;
let seq_offset = 6 + target_len;
if seq_offset + 4 > payload.len() {
return Err(RpcCodecError::LengthMismatch {
declared: seq_offset + 4,
actual: payload.len(),
});
}
let target = &payload[6..seq_offset];
let seq = u16::from_be_bytes([payload[seq_offset], payload[seq_offset + 1]]);
let total = u16::from_be_bytes([payload[seq_offset + 2], payload[seq_offset + 3]]);
if total == 0 || seq >= total || total as usize > MAX_REQUEST_FRAGMENTS {
return Err(RpcCodecError::BadFragmentIndex { index: seq, total });
}
Ok(RequestFragment {
id,
method,
target,
seq,
total,
chunk: &payload[seq_offset + 4..],
})
}
fn decode_request_object(
id: u32,
method: RpcMethod,
target: &[u8],
object: &[u8],
) -> Result<RpcRequest, RpcCodecError> {
let mut at = 0usize;
let take = |at: &mut usize, n: usize| -> Result<std::ops::Range<usize>, RpcCodecError> {
let end = at.checked_add(n).filter(|end| *end <= object.len()).ok_or(
RpcCodecError::LengthMismatch {
declared: at.saturating_add(n),
actual: object.len(),
},
)?;
let range = *at..end;
*at = end;
Ok(range)
};
let r = take(&mut at, 2)?;
let path_len = u16::from_be_bytes([object[r.start], object[r.start + 1]]) as usize;
let path = take(&mut at, path_len)?;
let r = take(&mut at, 1)?;
let ct_len = object[r.start] as usize;
let content_type = take(&mut at, ct_len)?;
let r = take(&mut at, 2)?;
let ticket_len = u16::from_be_bytes([object[r.start], object[r.start + 1]]) as usize;
if ticket_len > MAX_REQUEST_TICKET {
return Err(RpcCodecError::LengthMismatch {
declared: ticket_len,
actual: MAX_REQUEST_TICKET,
});
}
let ticket = take(&mut at, ticket_len)?;
let body = &object[at..];
if body.len() > MAX_REQUEST_BODY {
return Err(RpcCodecError::LengthMismatch {
declared: body.len(),
actual: MAX_REQUEST_BODY,
});
}
Ok(RpcRequest {
id,
method,
target: target.to_vec(),
path: object[path].to_vec(),
content_type: object[content_type].to_vec(),
ticket: object[ticket].to_vec(),
body: body.to_vec(),
})
}
#[derive(Debug, Clone, PartialEq, Eq)]
pub enum ReassemblyOutcome {
Complete(RpcRequest),
Pending,
Duplicate,
Rejected,
}
struct PartialRequest {
method: RpcMethod,
target: Vec<u8>,
total: u16,
chunks: Vec<Option<Vec<u8>>>,
received: u16,
bytes: usize,
started: std::time::Instant,
}
#[derive(Default)]
pub struct RequestReassembler {
partials: std::collections::HashMap<u32, PartialRequest>,
buffered: usize,
}
impl RequestReassembler {
pub fn new() -> Self {
Self::default()
}
pub fn push(
&mut self,
fragment: &RequestFragment<'_>,
now: std::time::Instant,
) -> ReassemblyOutcome {
self.expire(now);
if fragment.total == 1 {
return match decode_request_object(
fragment.id,
fragment.method,
fragment.target,
fragment.chunk,
) {
Ok(request) => ReassemblyOutcome::Complete(request),
Err(_) => ReassemblyOutcome::Rejected,
};
}
if fragment.chunk.len() > MAX_REQUEST_CHUNK {
return ReassemblyOutcome::Rejected;
}
let conflicting = self.partials.get(&fragment.id).is_some_and(|p| {
p.method != fragment.method || p.total != fragment.total || p.target != fragment.target
});
if conflicting {
self.drop_partial(fragment.id);
}
if !self.partials.contains_key(&fragment.id) {
if self.partials.len() >= MAX_PARTIAL_REQUESTS {
return ReassemblyOutcome::Rejected;
}
self.partials.insert(
fragment.id,
PartialRequest {
method: fragment.method,
target: fragment.target.to_vec(),
total: fragment.total,
chunks: vec![None; fragment.total as usize],
received: 0,
bytes: 0,
started: now,
},
);
}
let buffered = self.buffered;
let Some(partial) = self.partials.get_mut(&fragment.id) else {
return ReassemblyOutcome::Rejected;
};
if partial.chunks[fragment.seq as usize].is_some() {
return ReassemblyOutcome::Duplicate;
}
if buffered + fragment.chunk.len() > MAX_PARTIAL_REQUEST_BYTES
|| partial.bytes + fragment.chunk.len() > MAX_REQUEST_OBJECT
{
self.drop_partial(fragment.id);
return ReassemblyOutcome::Rejected;
}
partial.chunks[fragment.seq as usize] = Some(fragment.chunk.to_vec());
partial.received += 1;
partial.bytes += fragment.chunk.len();
self.buffered += fragment.chunk.len();
if partial.received < partial.total {
return ReassemblyOutcome::Pending;
}
let Some(done) = self.partials.remove(&fragment.id) else {
return ReassemblyOutcome::Rejected;
};
self.buffered = self.buffered.saturating_sub(done.bytes);
let mut object = Vec::with_capacity(done.bytes);
for chunk in done.chunks.into_iter().flatten() {
object.extend_from_slice(&chunk);
}
match decode_request_object(fragment.id, done.method, &done.target, &object) {
Ok(request) => ReassemblyOutcome::Complete(request),
Err(_) => ReassemblyOutcome::Rejected,
}
}
pub fn partial_count(&self) -> usize {
self.partials.len()
}
fn drop_partial(&mut self, id: u32) {
if let Some(p) = self.partials.remove(&id) {
self.buffered = self.buffered.saturating_sub(p.bytes);
}
}
fn expire(&mut self, now: std::time::Instant) {
let stale: Vec<u32> = self
.partials
.iter()
.filter(|(_, p)| now.saturating_duration_since(p.started) > REQUEST_REASSEMBLY_TIMEOUT)
.map(|(id, _)| *id)
.collect();
for id in stale {
self.drop_partial(id);
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::time::{Duration, Instant};
fn parts<'a>(path: &'a [u8], body: &'a [u8]) -> RequestParts<'a> {
RequestParts {
path,
content_type: b"",
ticket: b"",
body,
}
}
fn round_trip(method: RpcMethod, id: u32, target: &[u8], req: &RequestParts<'_>) -> RpcRequest {
let frags = encode_request(method, id, target, req).unwrap();
let mut r = RequestReassembler::new();
let now = Instant::now();
let mut done = None;
for f in &frags {
if let ReassemblyOutcome::Complete(req) =
r.push(&decode_request_fragment(f).unwrap(), now)
{
done = Some(req);
}
}
done.expect("the request completes")
}
#[test]
fn a_small_request_is_one_fragment_and_round_trips() {
let req = RequestParts {
path: b"/api/config",
content_type: b"application/json",
ticket: b"v2|tok",
body: br#"{"key":"agent.name","value":"example-drone"}"#,
};
let frags = encode_request(RpcMethod::Put, 7, b"0a1b2c3d4e5f", &req).unwrap();
assert_eq!(frags.len(), 1);
assert!(frags[0].len() <= AUX_MAX_PAYLOAD);
let got = round_trip(RpcMethod::Put, 7, b"0a1b2c3d4e5f", &req);
assert_eq!(got.id, 7);
assert_eq!(got.method, RpcMethod::Put);
assert_eq!(got.target, b"0a1b2c3d4e5f");
assert_eq!(got.path, req.path);
assert_eq!(got.content_type, req.content_type);
assert_eq!(got.ticket, req.ticket);
assert_eq!(got.body, req.body);
}
#[test]
fn a_body_far_past_one_frame_fragments_and_reassembles_out_of_order() {
let body: Vec<u8> = (0..200_000u32).map(|i| (i % 251) as u8).collect();
let req = RequestParts {
path: b"/api/plugins/com.example.tool/x/upload",
content_type: b"application/octet-stream",
ticket: b"",
body: &body,
};
let frags = encode_request(RpcMethod::Patch, 9, b"d1", &req).unwrap();
assert!(frags.len() > 100);
assert!(frags.iter().all(|f| f.len() <= AUX_MAX_PAYLOAD));
let mut r = RequestReassembler::new();
let now = Instant::now();
let mut order: Vec<usize> = (0..frags.len()).rev().collect();
order.insert(5, order[3]);
let mut done = None;
for i in order {
match r.push(&decode_request_fragment(&frags[i]).unwrap(), now) {
ReassemblyOutcome::Complete(req) => done = Some(req),
ReassemblyOutcome::Pending | ReassemblyOutcome::Duplicate => {}
ReassemblyOutcome::Rejected => panic!("rejected"),
}
}
let got = done.expect("complete");
assert_eq!(got.method, RpcMethod::Patch);
assert_eq!(got.body, body);
assert_eq!(got.content_type, b"application/octet-stream");
assert_eq!(r.partial_count(), 0);
}
#[test]
fn a_body_over_one_mebibyte_is_refused_at_encode() {
let body = vec![0u8; MAX_REQUEST_BODY + 1];
assert_eq!(
encode_request(RpcMethod::Post, 1, b"d1", &parts(b"/api/x", &body)),
Err(RequestEncodeError::BodyTooLarge)
);
let body = vec![0u8; MAX_REQUEST_BODY];
assert!(encode_request(RpcMethod::Post, 1, b"d1", &parts(b"/api/x", &body)).is_ok());
}
#[test]
fn a_partial_request_is_dropped_after_the_reassembly_window() {
let body = vec![1u8; 5_000];
let frags = encode_request(RpcMethod::Post, 3, b"d1", &parts(b"/api/x", &body)).unwrap();
let mut r = RequestReassembler::new();
let t0 = Instant::now();
assert_eq!(
r.push(&decode_request_fragment(&frags[0]).unwrap(), t0),
ReassemblyOutcome::Pending
);
let later = t0 + REQUEST_REASSEMBLY_TIMEOUT + Duration::from_secs(1);
for f in &frags[1..] {
assert_eq!(
r.push(&decode_request_fragment(f).unwrap(), later),
ReassemblyOutcome::Pending,
"the stale first fragment is gone, so this cannot complete"
);
}
}
#[test]
fn concurrent_partials_are_bounded() {
let body = vec![1u8; 3_000];
let mut r = RequestReassembler::new();
let now = Instant::now();
for id in 0..MAX_PARTIAL_REQUESTS as u32 {
let frags =
encode_request(RpcMethod::Post, id, b"d1", &parts(b"/api/x", &body)).unwrap();
assert_eq!(
r.push(&decode_request_fragment(&frags[0]).unwrap(), now),
ReassemblyOutcome::Pending
);
}
let frags = encode_request(RpcMethod::Post, 999, b"d1", &parts(b"/api/x", &body)).unwrap();
assert_eq!(
r.push(&decode_request_fragment(&frags[0]).unwrap(), now),
ReassemblyOutcome::Rejected
);
}
#[test]
fn rejects_a_target_longer_than_a_device_id() {
let too_long = vec![b'a'; MAX_DEVICE_ID + 1];
assert_eq!(
encode_request(RpcMethod::Get, 1, &too_long, &parts(b"/api/x", b"")),
Err(RequestEncodeError::FieldTooLong)
);
let fits = vec![b'a'; MAX_DEVICE_ID];
assert_eq!(
round_trip(RpcMethod::Get, 1, &fits, &parts(b"/api/x", b"")).target,
fits
);
}
#[test]
fn rejects_a_truncated_fragment() {
let frags =
encode_request(RpcMethod::Post, 9, b"abc", &parts(b"/api/x", b"payload")).unwrap();
assert_eq!(
decode_request_fragment(&frags[0][..5]).unwrap_err(),
RpcCodecError::TooShort
);
}
#[test]
fn an_impossible_sequence_number_is_refused() {
let mut frag =
encode_request(RpcMethod::Get, 1, b"", &parts(b"/api/x", b"")).unwrap()[0].clone();
frag[6..8].copy_from_slice(&3u16.to_be_bytes());
assert_eq!(
decode_request_fragment(&frag).unwrap_err(),
RpcCodecError::BadFragmentIndex { index: 3, total: 1 }
);
}
#[test]
fn an_object_whose_lengths_lie_is_rejected() {
let mut frag =
encode_request(RpcMethod::Get, 1, b"", &parts(b"/api/x", b"")).unwrap()[0].clone();
frag[10..12].copy_from_slice(&500u16.to_be_bytes());
let mut r = RequestReassembler::new();
assert_eq!(
r.push(&decode_request_fragment(&frag).unwrap(), Instant::now()),
ReassemblyOutcome::Rejected
);
}
#[test]
fn rejects_an_unknown_method_byte() {
let mut frag =
encode_request(RpcMethod::Get, 1, b"", &parts(b"/api/x", b"")).unwrap()[0].clone();
frag[0] = 0xFF;
assert_eq!(
decode_request_fragment(&frag).unwrap_err(),
RpcCodecError::BadMethod(0xFF)
);
}
#[test]
fn method_numbers_are_pinned() {
assert_eq!(RpcMethod::Get as u8, 1);
assert_eq!(RpcMethod::Post as u8, 2);
assert_eq!(RpcMethod::Put as u8, 3);
assert_eq!(RpcMethod::Delete as u8, 4);
assert_eq!(RpcMethod::Patch as u8, 5);
assert_eq!(RpcMethod::from_u8(5), Some(RpcMethod::Patch));
assert_eq!(RpcMethod::from_u8(0), None);
for m in [
RpcMethod::Get,
RpcMethod::Post,
RpcMethod::Put,
RpcMethod::Delete,
RpcMethod::Patch,
] {
assert_eq!(RpcMethod::from_http_method(m.as_http_method()), Some(m));
}
assert_eq!(RpcMethod::from_http_method("OPTIONS"), None);
}
}