1use std::io::{self, BufRead, BufReader, Read, Write};
22use std::net::TcpStream;
23use std::time::Duration;
24
25pub const MAX_RESPONSE: usize = 8 * 1024 * 1024;
28
29pub trait Stream: Read + Write {}
34impl<T: Read + Write> Stream for T {}
35
36#[derive(Debug, Clone, PartialEq, Eq)]
38pub struct Url {
39 pub scheme: String,
40 pub host: String,
41 pub port: u16,
42 pub path: String,
44}
45
46impl Url {
47 pub fn parse(s: &str) -> Result<Url, String> {
50 let (scheme, rest) = s
51 .split_once("://")
52 .ok_or_else(|| format!("not an absolute URL: {s}"))?;
53 let scheme = scheme.to_ascii_lowercase();
54 let default_port = match scheme.as_str() {
55 "http" => 80,
56 "https" => 443,
57 other => return Err(format!("unsupported scheme: {other}")),
58 };
59 let (authority, path) = match rest.find('/') {
60 Some(i) => (&rest[..i], &rest[i..]),
61 None => (rest, "/"),
62 };
63 if authority.is_empty() {
64 return Err(format!("missing host in URL: {s}"));
65 }
66 let (host, port) = match authority.rsplit_once(':') {
67 Some((h, p)) if p.chars().all(|c| c.is_ascii_digit()) && !p.is_empty() => (
70 h.to_string(),
71 p.parse().map_err(|_| format!("bad port in {s}"))?,
72 ),
73 _ => (authority.to_string(), default_port),
74 };
75 Ok(Url {
76 scheme,
77 host,
78 port,
79 path: path.to_string(),
80 })
81 }
82
83 pub fn is_tls(&self) -> bool {
84 self.scheme == "https"
85 }
86
87 pub fn host_header(&self) -> String {
89 let default = if self.is_tls() { 443 } else { 80 };
90 if self.port == default {
91 self.host.clone()
92 } else {
93 format!("{}:{}", self.host, self.port)
94 }
95 }
96}
97
98#[derive(Debug, Clone)]
100pub struct Response {
101 pub status: u16,
102 pub headers: Vec<(String, String)>,
103 pub body: Vec<u8>,
104}
105
106impl Response {
107 pub fn header(&self, name: &str) -> Option<&str> {
109 let name = name.to_ascii_lowercase();
110 self.headers
111 .iter()
112 .find(|(k, _)| *k == name)
113 .map(|(_, v)| v.as_str())
114 }
115
116 pub fn is_success(&self) -> bool {
117 (200..300).contains(&self.status)
118 }
119
120 pub fn body_str(&self) -> std::borrow::Cow<'_, str> {
121 String::from_utf8_lossy(&self.body)
122 }
123}
124
125pub fn is_loopback_host(host: &str) -> bool {
131 let h = host.trim_start_matches('[').trim_end_matches(']');
132 if h.eq_ignore_ascii_case("localhost") {
133 return true;
134 }
135 h.parse::<std::net::IpAddr>()
136 .map(|ip| ip.is_loopback())
137 .unwrap_or(false)
138}
139
140pub fn connect_tcp(host: &str, port: u16, timeout: Duration) -> io::Result<TcpStream> {
144 use std::net::ToSocketAddrs;
145 let addr = (host, port).to_socket_addrs()?.next().ok_or_else(|| {
146 io::Error::new(
147 io::ErrorKind::NotFound,
148 format!("cannot resolve {host}:{port}"),
149 )
150 })?;
151 let stream = TcpStream::connect_timeout(&addr, timeout)?;
152 stream.set_read_timeout(Some(timeout))?;
153 stream.set_write_timeout(Some(timeout))?;
154 stream.set_nodelay(true).ok();
155 Ok(stream)
156}
157
158pub fn send<S: Read + Write + ?Sized>(
162 stream: &mut S,
163 host_header: &str,
164 method: &str,
165 path: &str,
166 headers: &[(&str, &str)],
167 body: &[u8],
168) -> io::Result<Response> {
169 let mut req: Vec<u8> = Vec::with_capacity(256 + body.len());
170 write!(req, "{method} {path} HTTP/1.1\r\n")?;
171 write!(req, "Host: {host_header}\r\n")?;
172 req.extend_from_slice(b"Connection: close\r\n");
173 for (k, v) in headers {
174 if k.contains(['\r', '\n']) || v.contains(['\r', '\n']) {
176 return Err(io::Error::new(
177 io::ErrorKind::InvalidInput,
178 "CR/LF in header",
179 ));
180 }
181 write!(req, "{k}: {v}\r\n")?;
182 }
183 write!(req, "Content-Length: {}\r\n\r\n", body.len())?;
184 req.extend_from_slice(body);
185 stream.write_all(&req)?;
186 stream.flush()?;
187
188 let mut reader = BufReader::new(stream);
189 read_response(&mut reader)
190}
191
192fn read_head<R: BufRead>(r: &mut R) -> io::Result<(u16, Vec<(String, String)>)> {
195 let mut status_line = String::new();
196 r.read_line(&mut status_line)?;
197 let status = parse_status(&status_line)?;
198
199 let mut headers = Vec::new();
200 loop {
201 let mut line = String::new();
202 if r.read_line(&mut line)? == 0 {
203 break;
204 }
205 let line = line.trim_end_matches(['\r', '\n']);
206 if line.is_empty() {
207 break;
208 }
209 if let Some((k, v)) = line.split_once(':') {
210 headers.push((k.trim().to_ascii_lowercase(), v.trim().to_string()));
211 }
212 }
213 Ok((status, headers))
214}
215
216fn read_response<R: BufRead>(r: &mut R) -> io::Result<Response> {
217 let (status, headers) = read_head(r)?;
218
219 let content_length = headers
220 .iter()
221 .find(|(k, _)| k == "content-length")
222 .and_then(|(_, v)| v.parse::<usize>().ok());
223 let chunked = headers
224 .iter()
225 .any(|(k, v)| k == "transfer-encoding" && v.to_ascii_lowercase().contains("chunked"));
226
227 let body = if chunked {
228 read_chunked(r)?
229 } else if let Some(n) = content_length {
230 read_exact_capped(r, n)?
231 } else {
232 read_to_end_capped(r)?
234 };
235
236 Ok(Response {
237 status,
238 headers,
239 body,
240 })
241}
242
243fn parse_status(line: &str) -> io::Result<u16> {
244 line.split_whitespace()
246 .nth(1)
247 .and_then(|s| s.parse().ok())
248 .ok_or_else(|| {
249 io::Error::new(
250 io::ErrorKind::InvalidData,
251 format!("bad status line: {line:?}"),
252 )
253 })
254}
255
256fn read_exact_capped<R: Read>(r: &mut R, n: usize) -> io::Result<Vec<u8>> {
257 if n > MAX_RESPONSE {
258 return Err(io::Error::new(
259 io::ErrorKind::InvalidData,
260 "response exceeds cap",
261 ));
262 }
263 let mut buf = vec![0u8; n];
264 r.read_exact(&mut buf)?;
265 Ok(buf)
266}
267
268fn read_to_end_capped<R: Read>(r: &mut R) -> io::Result<Vec<u8>> {
269 let mut buf = Vec::new();
270 r.take(MAX_RESPONSE as u64 + 1).read_to_end(&mut buf)?;
271 if buf.len() > MAX_RESPONSE {
272 return Err(io::Error::new(
273 io::ErrorKind::InvalidData,
274 "response exceeds cap",
275 ));
276 }
277 Ok(buf)
278}
279
280fn read_chunked<R: BufRead>(r: &mut R) -> io::Result<Vec<u8>> {
281 let mut body = Vec::new();
282 loop {
283 let mut size_line = String::new();
284 r.read_line(&mut size_line)?;
285 let size_hex = size_line.trim_end_matches(['\r', '\n']);
286 let size_hex = size_hex.split(';').next().unwrap_or("").trim();
288 let size = usize::from_str_radix(size_hex, 16)
289 .map_err(|_| io::Error::new(io::ErrorKind::InvalidData, "bad chunk size"))?;
290 if size == 0 {
291 loop {
293 let mut t = String::new();
294 if r.read_line(&mut t)? == 0 || t.trim_end_matches(['\r', '\n']).is_empty() {
295 break;
296 }
297 }
298 break;
299 }
300 if body.len() + size > MAX_RESPONSE {
301 return Err(io::Error::new(
302 io::ErrorKind::InvalidData,
303 "chunked response exceeds cap",
304 ));
305 }
306 let mut chunk = vec![0u8; size];
307 r.read_exact(&mut chunk)?;
308 body.extend_from_slice(&chunk);
309 let mut crlf = [0u8; 2];
311 r.read_exact(&mut crlf)?;
312 }
313 Ok(body)
314}
315
316pub struct StreamingResponse<S: Read + Write> {
321 pub status: u16,
322 pub headers: Vec<(String, String)>,
323 reader: BufReader<S>,
324}
325
326impl<S: Read + Write> StreamingResponse<S> {
327 pub fn header(&self, name: &str) -> Option<&str> {
328 let name = name.to_ascii_lowercase();
329 self.headers
330 .iter()
331 .find(|(k, _)| *k == name)
332 .map(|(_, v)| v.as_str())
333 }
334 pub fn is_success(&self) -> bool {
335 (200..300).contains(&self.status)
336 }
337 pub fn content_type(&self) -> Option<&str> {
339 self.header("content-type")
340 .map(|v| v.split(';').next().unwrap_or(v).trim())
341 }
342 pub fn is_event_stream(&self) -> bool {
344 self.content_type() == Some("text/event-stream")
345 }
346 pub fn into_body(mut self) -> io::Result<Vec<u8>> {
348 let content_length = self
349 .headers
350 .iter()
351 .find(|(k, _)| k == "content-length")
352 .and_then(|(_, v)| v.parse::<usize>().ok());
353 let chunked = self
354 .headers
355 .iter()
356 .any(|(k, v)| k == "transfer-encoding" && v.to_ascii_lowercase().contains("chunked"));
357 if chunked {
358 read_chunked(&mut self.reader)
359 } else if let Some(n) = content_length {
360 read_exact_capped(&mut self.reader, n)
361 } else {
362 read_to_end_capped(&mut self.reader)
363 }
364 }
365 pub fn sse(self) -> SseReader<BufReader<S>> {
367 SseReader::new(self.reader)
368 }
369
370 pub fn into_reader(self) -> BufReader<S> {
373 self.reader
374 }
375}
376
377pub fn send_streaming<S: Read + Write>(
382 mut stream: S,
383 host_header: &str,
384 method: &str,
385 path: &str,
386 headers: &[(&str, &str)],
387 body: &[u8],
388) -> io::Result<StreamingResponse<S>> {
389 let mut req: Vec<u8> = Vec::with_capacity(256 + body.len());
390 write!(req, "{method} {path} HTTP/1.1\r\n")?;
391 write!(req, "Host: {host_header}\r\n")?;
392 req.extend_from_slice(b"Connection: close\r\n");
393 for (k, v) in headers {
394 if k.contains(['\r', '\n']) || v.contains(['\r', '\n']) {
395 return Err(io::Error::new(
396 io::ErrorKind::InvalidInput,
397 "CR/LF in header",
398 ));
399 }
400 write!(req, "{k}: {v}\r\n")?;
401 }
402 write!(req, "Content-Length: {}\r\n\r\n", body.len())?;
403 req.extend_from_slice(body);
404 stream.write_all(&req)?;
405 stream.flush()?;
406
407 let mut reader = BufReader::new(stream);
408 let (status, headers) = read_head(&mut reader)?;
409 Ok(StreamingResponse {
410 status,
411 headers,
412 reader,
413 })
414}
415
416#[derive(Debug, Clone, Default, PartialEq, Eq)]
419pub struct SseEvent {
420 pub event: Option<String>,
421 pub data: String,
422 pub id: Option<String>,
423}
424
425pub struct SseReader<R: BufRead> {
430 r: R,
431}
432
433impl<R: BufRead> SseReader<R> {
434 pub fn new(r: R) -> SseReader<R> {
435 SseReader { r }
436 }
437
438 pub fn next_event(&mut self) -> io::Result<Option<SseEvent>> {
441 let mut ev = SseEvent::default();
442 let mut saw_field = false;
443 let mut total = 0usize;
444 loop {
445 let mut line = String::new();
446 let n = self.r.read_line(&mut line)?;
447 if n == 0 {
448 return Ok(if saw_field { Some(ev) } else { None });
450 }
451 total += n;
452 if total > MAX_RESPONSE {
453 return Err(io::Error::new(
454 io::ErrorKind::InvalidData,
455 "SSE event exceeds cap",
456 ));
457 }
458 let line = line.trim_end_matches(['\r', '\n']);
459 if line.is_empty() {
460 if saw_field {
462 return Ok(Some(ev));
463 }
464 continue; }
466 if line.starts_with(':') {
467 continue; }
469 let (field, value) = match line.split_once(':') {
470 Some((f, v)) => (f, v.strip_prefix(' ').unwrap_or(v)),
471 None => (line, ""), };
473 saw_field = true;
474 match field {
475 "event" => ev.event = Some(value.to_string()),
476 "id" => ev.id = Some(value.to_string()),
477 "data" => {
478 if !ev.data.is_empty() {
479 ev.data.push('\n');
480 }
481 ev.data.push_str(value);
482 }
483 _ => {} }
485 }
486 }
487}
488
489#[cfg(test)]
490mod tests {
491 use super::*;
492 use std::io::Cursor;
493
494 #[test]
495 fn url_parse_https_default_port() {
496 let u = Url::parse("https://api.openai.com/v1/chat/completions").unwrap();
497 assert_eq!(u.scheme, "https");
498 assert_eq!(u.host, "api.openai.com");
499 assert_eq!(u.port, 443);
500 assert_eq!(u.path, "/v1/chat/completions");
501 assert_eq!(u.host_header(), "api.openai.com");
502 assert!(u.is_tls());
503 }
504
505 #[test]
506 fn url_parse_http_with_port_and_no_path() {
507 let u = Url::parse("http://localhost:8080").unwrap();
508 assert_eq!(u.port, 8080);
509 assert_eq!(u.path, "/");
510 assert_eq!(u.host_header(), "localhost:8080");
511 assert!(!u.is_tls());
512 }
513
514 #[test]
515 fn url_rejects_bad_scheme() {
516 assert!(Url::parse("ftp://x/").is_err());
517 assert!(Url::parse("no-scheme").is_err());
518 }
519
520 #[test]
521 fn response_content_length() {
522 let raw = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nContent-Length: 13\r\n\r\n{\"ok\":true}!!";
523 let mut cur = Cursor::new(raw.as_bytes().to_vec());
524 let resp = read_response(&mut cur).unwrap();
525 assert_eq!(resp.status, 200);
526 assert_eq!(resp.header("content-type"), Some("application/json"));
527 assert_eq!(resp.body, b"{\"ok\":true}!!");
528 assert!(resp.is_success());
529 }
530
531 #[test]
532 fn response_chunked() {
533 let raw = "HTTP/1.1 200 OK\r\nTransfer-Encoding: chunked\r\n\r\n5\r\nhello\r\n6\r\n world\r\n0\r\n\r\n";
534 let mut cur = Cursor::new(raw.as_bytes().to_vec());
535 let resp = read_response(&mut cur).unwrap();
536 assert_eq!(resp.body, b"hello world");
537 }
538
539 #[test]
540 fn cr_lf_header_injection_rejected() {
541 let mut sink: Vec<u8> = Vec::new();
542 let mut stream = Cursor::new(Vec::new());
544 let _ = &mut sink;
545 let err = send(&mut stream, "h", "POST", "/", &[("X", "a\r\nEvil: 1")], b"").unwrap_err();
546 assert_eq!(err.kind(), io::ErrorKind::InvalidInput);
547 }
548
549 struct FakeStream {
552 resp: Cursor<Vec<u8>>,
553 sink: Vec<u8>,
554 }
555 impl Read for FakeStream {
556 fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
557 self.resp.read(buf)
558 }
559 }
560 impl Write for FakeStream {
561 fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
562 self.sink.extend_from_slice(buf);
563 Ok(buf.len())
564 }
565 fn flush(&mut self) -> io::Result<()> {
566 Ok(())
567 }
568 }
569
570 fn sse_events(body: &str) -> Vec<SseEvent> {
571 let mut r = SseReader::new(BufReader::new(Cursor::new(body.as_bytes().to_vec())));
572 let mut out = Vec::new();
573 while let Some(e) = r.next_event().unwrap() {
574 out.push(e);
575 }
576 out
577 }
578
579 #[test]
580 fn sse_parses_events_with_event_id_and_data() {
581 let body = "event: message\nid: 7\ndata: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{}}\n\n";
582 let evs = sse_events(body);
583 assert_eq!(evs.len(), 1);
584 assert_eq!(evs[0].event.as_deref(), Some("message"));
585 assert_eq!(evs[0].id.as_deref(), Some("7"));
586 assert_eq!(evs[0].data, "{\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{}}");
587 }
588
589 #[test]
590 fn sse_joins_multi_data_lines_and_ignores_comments() {
591 let body = ": keep-alive\ndata: line1\ndata: line2\n\ndata: second\n\n";
593 let evs = sse_events(body);
594 assert_eq!(evs.len(), 2);
595 assert_eq!(evs[0].data, "line1\nline2");
596 assert_eq!(evs[1].data, "second");
597 }
598
599 #[test]
600 fn sse_flushes_trailing_event_without_final_blank_line() {
601 let evs = sse_events("data: only\n");
602 assert_eq!(evs.len(), 1);
603 assert_eq!(evs[0].data, "only");
604 }
605
606 #[test]
607 fn send_streaming_reads_head_then_buffers_json_body() {
608 let raw = "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\nMcp-Session-Id: abc123\r\nContent-Length: 11\r\n\r\n{\"ok\":true}".to_string();
609 let stream = FakeStream {
610 resp: Cursor::new(raw.into_bytes()),
611 sink: Vec::new(),
612 };
613 let resp = send_streaming(
614 stream,
615 "h",
616 "POST",
617 "/mcp",
618 &[("Accept", "application/json")],
619 b"{}",
620 )
621 .unwrap();
622 assert_eq!(resp.status, 200);
623 assert!(resp.is_success());
624 assert_eq!(resp.content_type(), Some("application/json"));
625 assert!(!resp.is_event_stream());
626 assert_eq!(resp.header("mcp-session-id"), Some("abc123"));
627 assert_eq!(resp.into_body().unwrap(), b"{\"ok\":true}");
628 }
629
630 #[test]
631 fn send_streaming_detects_event_stream_and_pumps_sse() {
632 let raw = "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\n\r\ndata: {\"jsonrpc\":\"2.0\",\"id\":1,\"result\":{\"x\":1}}\n\n".to_string();
633 let stream = FakeStream {
634 resp: Cursor::new(raw.into_bytes()),
635 sink: Vec::new(),
636 };
637 let resp = send_streaming(stream, "h", "POST", "/mcp", &[], b"{}").unwrap();
638 assert!(resp.is_event_stream());
639 let mut sse = resp.sse();
640 let ev = sse.next_event().unwrap().expect("one event");
641 assert!(ev.data.contains("\"id\":1"));
642 assert!(sse.next_event().unwrap().is_none());
643 }
644}