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