Skip to main content

origin_mcp_http/
http.rs

1//! Just enough HTTP/1.1 to carry JSON-RPC over loopback.
2//!
3//! Deliberately minimal: one request per connection, `Content-Length` only (no
4//! chunked encoding), and hard caps on header and body size so a local process cannot
5//! exhaust memory. Anything more belongs in a general-purpose HTTP stack, which this
6//! adapter deliberately does not pull in.
7
8use origin_domain::{AppError, Result};
9
10/// Largest request line + headers we will buffer.
11const MAX_HEADER_BYTES: usize = 64 * 1024;
12
13/// Largest body we will read. MCP tool arguments are small; a megabyte is generous.
14const MAX_BODY_BYTES: usize = 1024 * 1024;
15
16const HEADER_TERMINATOR: &[u8] = b"\r\n\r\n";
17
18#[derive(Debug, Clone, PartialEq, Eq)]
19pub struct HttpRequest {
20    pub method: String,
21    pub path: String,
22    /// Header names lowercased for case-insensitive lookup.
23    pub headers: Vec<(String, String)>,
24    pub body: Vec<u8>,
25}
26
27impl HttpRequest {
28    pub fn header(&self, name: &str) -> Option<&str> {
29        let name = name.to_ascii_lowercase();
30        self.headers
31            .iter()
32            .find(|(key, _)| *key == name)
33            .map(|(_, value)| value.as_str())
34    }
35
36    /// The bearer token from `Authorization: Bearer <token>`, if present.
37    pub fn bearer_token(&self) -> Option<&str> {
38        self.header("authorization")?
39            .strip_prefix("Bearer ")
40            .map(str::trim)
41    }
42}
43
44#[derive(Debug, Clone, PartialEq, Eq)]
45pub struct HttpResponse {
46    pub status: u16,
47    pub body: Vec<u8>,
48}
49
50impl HttpResponse {
51    pub fn json(status: u16, value: &serde_json::Value) -> Self {
52        let body = serde_json::to_vec(value).unwrap_or_else(|_| b"null".to_vec());
53        Self { status, body }
54    }
55
56    pub fn text(status: u16, message: impl Into<String>) -> Self {
57        Self {
58            status,
59            body: message.into().into_bytes(),
60        }
61    }
62
63    fn reason(&self) -> &'static str {
64        match self.status {
65            200 => "OK",
66            400 => "Bad Request",
67            401 => "Unauthorized",
68            404 => "Not Found",
69            405 => "Method Not Allowed",
70            413 => "Payload Too Large",
71            500 => "Internal Server Error",
72            _ => "Error",
73        }
74    }
75
76    /// Encode as an HTTP/1.1 response, always closing the connection.
77    pub fn encode(&self) -> Vec<u8> {
78        let head = format!(
79            "HTTP/1.1 {} {}\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n",
80            self.status,
81            self.reason(),
82            self.body.len()
83        );
84        let mut out = head.into_bytes();
85        out.extend_from_slice(&self.body);
86        out
87    }
88}
89
90/// Parse one request from `buffer`.
91///
92/// Returns `Ok(None)` when more bytes are needed; `Ok(Some((request, consumed)))` once
93/// the full request (headers + `Content-Length` body) is present.
94pub fn parse_request(buffer: &[u8]) -> Result<Option<(HttpRequest, usize)>> {
95    let Some(header_end) = find_terminator(buffer) else {
96        if buffer.len() > MAX_HEADER_BYTES {
97            return Err(AppError::validation("http headers exceed the size limit"));
98        }
99        return Ok(None);
100    };
101
102    let head = std::str::from_utf8(&buffer[..header_end])
103        .map_err(|_| AppError::validation("http headers are not utf-8"))?;
104
105    let mut lines = head.split("\r\n");
106    let request_line = lines
107        .next()
108        .ok_or_else(|| AppError::validation("empty http request"))?;
109
110    let mut parts = request_line.split_whitespace();
111    let method = parts
112        .next()
113        .ok_or_else(|| AppError::validation("missing http method"))?
114        .to_owned();
115    let path = parts
116        .next()
117        .ok_or_else(|| AppError::validation("missing http path"))?
118        .to_owned();
119
120    let mut headers = Vec::new();
121    for line in lines {
122        if line.is_empty() {
123            continue;
124        }
125        let (key, value) = line
126            .split_once(':')
127            .ok_or_else(|| AppError::validation("malformed http header"))?;
128        headers.push((key.trim().to_ascii_lowercase(), value.trim().to_owned()));
129    }
130
131    let content_length = headers
132        .iter()
133        .find(|(key, _)| key == "content-length")
134        .map(|(_, value)| {
135            value
136                .parse::<usize>()
137                .map_err(|_| AppError::validation("invalid content-length"))
138        })
139        .transpose()?
140        .unwrap_or(0);
141
142    if content_length > MAX_BODY_BYTES {
143        return Err(AppError::validation("http body exceeds the size limit"));
144    }
145
146    let body_start = header_end + HEADER_TERMINATOR.len();
147    let body_end = body_start + content_length;
148    if buffer.len() < body_end {
149        return Ok(None);
150    }
151
152    Ok(Some((
153        HttpRequest {
154            method,
155            path,
156            headers,
157            body: buffer[body_start..body_end].to_vec(),
158        },
159        body_end,
160    )))
161}
162
163fn find_terminator(buffer: &[u8]) -> Option<usize> {
164    buffer
165        .windows(HEADER_TERMINATOR.len())
166        .position(|window| window == HEADER_TERMINATOR)
167}
168
169#[cfg(test)]
170mod tests {
171    use super::*;
172
173    #[test]
174    fn a_complete_post_is_parsed() {
175        let mut raw = Vec::new();
176        raw.extend_from_slice(b"POST /mcp HTTP/1.1");
177        raw.extend_from_slice(b"\r\n");
178        raw.extend_from_slice(b"Host: 127.0.0.1");
179        raw.extend_from_slice(b"\r\n");
180        raw.extend_from_slice(b"Authorization: Bearer abc");
181        raw.extend_from_slice(b"\r\n");
182        raw.extend_from_slice(b"Content-Length: 7");
183        raw.extend_from_slice(b"\r\n\r\n");
184        raw.extend_from_slice(b"{\"a\":1}");
185
186        let (request, consumed) = parse_request(&raw).unwrap().expect("complete request");
187
188        assert_eq!(request.method, "POST");
189        assert_eq!(request.path, "/mcp");
190        assert_eq!(request.body, b"{\"a\":1}");
191        assert_eq!(request.bearer_token(), Some("abc"));
192        assert_eq!(consumed, raw.len());
193    }
194
195    #[test]
196    fn an_incomplete_request_asks_for_more() {
197        let raw = b"POST /mcp HTTP/1.1\r\nContent-Length: 20\r\n\r\n{\"a\"";
198        assert!(parse_request(raw).unwrap().is_none());
199    }
200
201    #[test]
202    fn a_request_without_a_body_parses() {
203        let raw = b"GET /mcp HTTP/1.1\r\nHost: x\r\n\r\n";
204        let (request, _) = parse_request(raw).unwrap().expect("complete request");
205        assert_eq!(request.method, "GET");
206        assert!(request.body.is_empty());
207    }
208
209    #[test]
210    fn a_missing_bearer_prefix_yields_no_token() {
211        let raw = b"POST /mcp HTTP/1.1\r\nAuthorization: Basic xyz\r\n\r\n";
212        let (request, _) = parse_request(raw).unwrap().unwrap();
213        assert_eq!(request.bearer_token(), None);
214    }
215
216    #[test]
217    fn a_response_encodes_a_content_length_and_closes() {
218        let response = HttpResponse::text(401, "nope");
219        let encoded = String::from_utf8(response.encode()).unwrap();
220
221        assert!(encoded.starts_with("HTTP/1.1 401 Unauthorized\r\n"));
222        assert!(encoded.contains("Content-Length: 4\r\n"));
223        assert!(encoded.ends_with("\r\n\r\nnope"));
224    }
225
226    #[test]
227    fn an_oversized_body_is_rejected() {
228        let raw = format!(
229            "POST /mcp HTTP/1.1\r\nContent-Length: {}\r\n\r\n",
230            MAX_BODY_BYTES + 1
231        );
232        assert!(parse_request(raw.as_bytes()).is_err());
233    }
234}