Skip to main content

clawft_kernel/infer_proxy/
wire.rs

1//! Mesh wire format of a forwarded inference request (frame types
2//! `InferRequest` 0x10 and `InferResponse` 0x11).
3//!
4//! ```text
5//! InferRequest   [ver u8][head_len u32 BE][head JSON][body bytes]
6//! InferResponse  [kind u8][data]     kind 0 head (JSON {status, content_type})
7//!                                    kind 1 chunk (raw bytes)
8//!                                    kind 2 end
9//!                                    kind 3 error (UTF-8 text)
10//! ```
11//!
12//! The request carries no `Authorization`: the credentials of the client on
13//! one node are not forwarded to another. Everything decoded from the wire
14//! is revalidated as if it came from a client, because it came from a peer.
15
16use serde::{Deserialize, Serialize};
17
18use super::http::{safe_header_value, validate_path};
19use super::types::{Method, ProxyError, ProxyLimits, ProxyRequest};
20use crate::workload_pkg::manifest::valid_token;
21
22/// Wire version of `InferRequest`.
23pub const WIRE_VERSION: u8 = 1;
24/// Largest body chunk put in one response frame.
25pub const CHUNK_BYTES: usize = 64 * 1024;
26/// Longest role name.
27pub const MAX_ROLE: usize = 64;
28
29#[derive(Serialize, Deserialize)]
30#[serde(deny_unknown_fields)]
31struct RequestHead {
32    role: String,
33    method: String,
34    path: String,
35    #[serde(default)]
36    content_type: Option<String>,
37    #[serde(default)]
38    accept: Option<String>,
39}
40
41/// Encode a request for the mesh (without its `Authorization`).
42pub fn encode_request(req: &ProxyRequest) -> Result<Vec<u8>, ProxyError> {
43    let head = serde_json::to_vec(&RequestHead {
44        role: req.role.clone(),
45        method: req.method.as_str().to_string(),
46        path: req.path.clone(),
47        content_type: req.content_type.clone(),
48        accept: req.accept.clone(),
49    })
50    .map_err(|e| ProxyError::Mesh(e.to_string()))?;
51    let mut out = Vec::with_capacity(5 + head.len() + req.body.len());
52    out.push(WIRE_VERSION);
53    out.extend_from_slice(&(head.len() as u32).to_be_bytes());
54    out.extend_from_slice(&head);
55    out.extend_from_slice(&req.body);
56    Ok(out)
57}
58
59/// Decode and revalidate a request received from a peer.
60pub fn decode_request(payload: &[u8], limits: &ProxyLimits) -> Result<ProxyRequest, ProxyError> {
61    let bad = |m: &str| ProxyError::BadRequest(m.into());
62    if payload.len() < 5 || payload[0] != WIRE_VERSION {
63        return Err(bad("unsupported infer request version"));
64    }
65    let head_len = u32::from_be_bytes([payload[1], payload[2], payload[3], payload[4]]) as usize;
66    if head_len > limits.max_head_bytes || payload.len() < 5 + head_len {
67        return Err(bad("bad infer request head length"));
68    }
69    let head: RequestHead =
70        serde_json::from_slice(&payload[5..5 + head_len]).map_err(|_| bad("bad infer request head"))?;
71    let body = &payload[5 + head_len..];
72    if body.len() > limits.max_request_body {
73        return Err(ProxyError::TooLarge("request body".into()));
74    }
75    if !valid_token(&head.role, MAX_ROLE) {
76        return Err(bad("bad role"));
77    }
78    let method = match head.method.as_str() {
79        "GET" => Method::Get,
80        "POST" => Method::Post,
81        _ => return Err(ProxyError::MethodNotAllowed),
82    };
83    validate_path(&head.path)?;
84    let clean = |v: Option<String>| -> Result<Option<String>, ProxyError> {
85        match v {
86            None => Ok(None),
87            Some(s) if safe_header_value(&s).is_some() => Ok(Some(s)),
88            Some(_) => Err(bad("bad header value")),
89        }
90    };
91    Ok(ProxyRequest {
92        role: head.role,
93        method,
94        path: head.path,
95        content_type: clean(head.content_type)?,
96        accept: clean(head.accept)?,
97        authorization: None,
98        body: body.to_vec(),
99    })
100}
101
102/// One response frame.
103#[derive(Debug, Clone, PartialEq, Eq)]
104pub enum Resp {
105    /// Status and content type.
106    Head {
107        /// HTTP status.
108        status: u16,
109        /// Content type.
110        content_type: Option<String>,
111    },
112    /// Body bytes.
113    Chunk(Vec<u8>),
114    /// Complete.
115    End,
116    /// Failed; no more frames.
117    Error(String),
118}
119
120#[derive(Serialize, Deserialize)]
121struct HeadJson {
122    status: u16,
123    #[serde(default)]
124    content_type: Option<String>,
125}
126
127/// Encode a response frame payload.
128pub fn encode_resp(r: &Resp) -> Vec<u8> {
129    match r {
130        Resp::Head {
131            status,
132            content_type,
133        } => {
134            let mut v = vec![0];
135            v.extend(
136                serde_json::to_vec(&HeadJson {
137                    status: *status,
138                    content_type: content_type.clone(),
139                })
140                .unwrap_or_default(),
141            );
142            v
143        }
144        Resp::Chunk(d) => {
145            let mut v = Vec::with_capacity(1 + d.len());
146            v.push(1);
147            v.extend_from_slice(d);
148            v
149        }
150        Resp::End => vec![2],
151        Resp::Error(m) => {
152            let mut v = vec![3];
153            v.extend_from_slice(m.as_bytes());
154            v
155        }
156    }
157}
158
159/// Decode a response frame payload received from a peer.
160pub fn decode_resp(p: &[u8]) -> Result<Resp, ProxyError> {
161    let bad = |m: &str| ProxyError::Mesh(m.into());
162    let (kind, data) = p.split_first().ok_or_else(|| bad("empty infer response"))?;
163    match kind {
164        0 => {
165            let h: HeadJson = serde_json::from_slice(data).map_err(|_| bad("bad response head"))?;
166            if !(200..=599).contains(&h.status) {
167                return Err(bad("bad response status"));
168            }
169            Ok(Resp::Head {
170                status: h.status,
171                content_type: h
172                    .content_type
173                    .filter(|c| safe_header_value(c).is_some()),
174            })
175        }
176        1 => Ok(Resp::Chunk(data.to_vec())),
177        2 => Ok(Resp::End),
178        3 => Ok(Resp::Error(
179            String::from_utf8_lossy(&data[..data.len().min(512)]).into_owned(),
180        )),
181        _ => Err(bad("unknown infer response kind")),
182    }
183}