1use crate::rpc::{Incoming, Request};
28use crate::server::{Handler, PeerOrigin, ServeStream, SharedWriter, SubRegistry};
29use crate::wire::method;
30use serde_json::{Value, json};
31use std::io::{self, BufRead, BufReader, Read, Write};
32use std::net::{TcpListener, TcpStream};
33use std::sync::atomic::{AtomicU64, Ordering};
34use std::sync::{Arc, Mutex};
35use std::thread;
36use std::time::Duration;
37
38const SSE_KEEPALIVE: Duration = Duration::from_secs(15);
41
42const MAX_BODY: usize = 8 * 1024 * 1024;
44
45const MAX_HEAD_BYTES: usize = 64 * 1024;
51
52const MAX_HEADERS: usize = 100;
57
58#[derive(Default, Clone)]
62pub struct PeerId {
63 pub cert: bool,
65 pub subject: Option<String>,
67 pub sans: Vec<String>,
70}
71
72pub struct RequestParts<'a> {
74 pub headers: &'a [(String, String)],
76 pub peer_cert: bool,
78 pub peer_subject: Option<&'a str>,
80 pub peer_sans: &'a [String],
82}
83
84impl RequestParts<'_> {
85 pub fn header(&self, name: &str) -> Option<&str> {
87 self.headers
88 .iter()
89 .find(|(k, _)| k == name)
90 .map(|(_, v)| v.as_str())
91 }
92}
93
94pub trait HttpAuth: Send + Sync + 'static {
98 fn authenticate(&self, parts: &RequestParts) -> Option<PeerOrigin>;
99}
100
101pub struct AllowAll;
105impl HttpAuth for AllowAll {
106 fn authenticate(&self, _parts: &RequestParts) -> Option<PeerOrigin> {
107 Some(PeerOrigin::Management)
108 }
109}
110
111#[derive(Default, Clone)]
113pub struct ServeOptions {
114 pub extra_origins: Vec<String>,
121}
122
123pub enum HttpAcceptor {
128 Plain,
130 #[cfg(feature = "tls")]
132 Tls(net::tls::TlsAcceptor),
133}
134
135pub fn bind_tcp(addr: &str) -> io::Result<TcpListener> {
139 TcpListener::bind(addr)
140}
141
142#[allow(clippy::too_many_arguments)]
146pub fn spawn_accept_http(
147 listener: TcpListener,
148 acceptor: Arc<HttpAcceptor>,
149 handler: Arc<dyn Handler>,
150 auth: Arc<dyn HttpAuth>,
151 subs: SubRegistry,
152 conn_counter: Arc<AtomicU64>,
153 write_timeout: Duration,
154) -> io::Result<()> {
155 spawn_accept_http_opts(
156 listener,
157 acceptor,
158 handler,
159 auth,
160 subs,
161 conn_counter,
162 write_timeout,
163 ServeOptions::default(),
164 )
165}
166
167#[allow(clippy::too_many_arguments)]
169pub fn spawn_accept_http_opts(
170 listener: TcpListener,
171 acceptor: Arc<HttpAcceptor>,
172 handler: Arc<dyn Handler>,
173 auth: Arc<dyn HttpAuth>,
174 subs: SubRegistry,
175 conn_counter: Arc<AtomicU64>,
176 write_timeout: Duration,
177 opts: ServeOptions,
178) -> io::Result<()> {
179 let opts = Arc::new(opts);
180 thread::Builder::new()
181 .name("serve-http".into())
182 .spawn(move || {
183 for tcp in listener.incoming().flatten() {
184 let acceptor = Arc::clone(&acceptor);
185 let handler = Arc::clone(&handler);
186 let auth = Arc::clone(&auth);
187 let subs = Arc::clone(&subs);
188 let conn_counter = Arc::clone(&conn_counter);
189 let opts = Arc::clone(&opts);
190 thread::Builder::new()
191 .name("serve-http-conn".into())
192 .spawn(move || {
193 accept_and_serve(
194 tcp,
195 &acceptor,
196 &handler,
197 &auth,
198 &subs,
199 &conn_counter,
200 write_timeout,
201 &opts,
202 );
203 })
204 .ok();
205 }
206 })
207 .map(|_| ())
208}
209
210#[allow(clippy::too_many_arguments)]
211fn accept_and_serve(
212 tcp: TcpStream,
213 acceptor: &HttpAcceptor,
214 handler: &Arc<dyn Handler>,
215 auth: &Arc<dyn HttpAuth>,
216 subs: &SubRegistry,
217 conn_counter: &AtomicU64,
218 write_timeout: Duration,
219 opts: &ServeOptions,
220) {
221 let _ = tcp.set_write_timeout(Some(write_timeout));
222 let _ = tcp.set_read_timeout(Some(write_timeout));
223 match acceptor {
224 HttpAcceptor::Plain => {
225 serve_conn(
226 tcp,
227 PeerId::default(),
228 handler,
229 auth,
230 subs,
231 conn_counter,
232 opts,
233 );
234 }
235 #[cfg(feature = "tls")]
237 HttpAcceptor::Tls(tls) => {
238 if let Ok(stream) = tls.accept(tcp) {
239 let peer = peer_id(&stream);
240 serve_conn(stream, peer, handler, auth, subs, conn_counter, opts);
241 }
242 }
243 }
244}
245
246#[cfg(feature = "tls")]
249fn peer_id(stream: &net::tls::ServerTlsStream) -> PeerId {
250 match net::tls::peer_identity(stream) {
251 Some(id) => PeerId {
252 cert: true,
253 subject: id.subject_cn,
254 sans: id.sans,
255 },
256 None => PeerId::default(),
257 }
258}
259
260fn serve_conn<S: Read + Write + Send + 'static>(
263 stream: S,
264 peer: PeerId,
265 handler: &Arc<dyn Handler>,
266 auth: &Arc<dyn HttpAuth>,
267 subs: &SubRegistry,
268 conn_counter: &AtomicU64,
269 opts: &ServeOptions,
270) {
271 let mut reader = BufReader::new(stream);
272 let req = match read_request(&mut reader) {
273 Ok(req) => req,
274 Err(ReadError::HeadTooLarge) => {
278 let _ = write_simple(
279 reader.get_mut(),
280 431,
281 "Request Header Fields Too Large",
282 b"request head exceeds the header size/count limits",
283 None,
284 );
285 return;
286 }
287 Err(ReadError::Incomplete) => return, };
289
290 let cors = match check_origin(&req.headers, &opts.extra_origins) {
300 OriginCheck::NoBrowser => None,
301 OriginCheck::Allowed(o) => Some(o),
302 OriginCheck::Denied => {
303 let _ = write_simple(
304 reader.get_mut(),
305 403,
306 "Forbidden",
307 b"cross-origin request rejected",
308 None,
309 );
310 return;
311 }
312 };
313 if req.method.eq_ignore_ascii_case("OPTIONS") {
317 let _ = write_preflight(reader.get_mut(), cors.as_deref());
318 return;
319 }
320
321 let origin = {
323 let parts = RequestParts {
324 headers: &req.headers,
325 peer_cert: peer.cert,
326 peer_subject: peer.subject.as_deref(),
327 peer_sans: &peer.sans,
328 };
329 auth.authenticate(&parts)
330 };
331 let Some(origin) = origin else {
332 let _ = write_simple(reader.get_mut(), 401, "Unauthorized", b"", cors.as_deref());
333 return;
334 };
335
336 if !req.method.eq_ignore_ascii_case("POST") {
339 let _ = write_simple(
340 reader.get_mut(),
341 405,
342 "Method Not Allowed",
343 b"POST a JSON-RPC request, or POST subscriptions/listen for the SSE stream",
344 cors.as_deref(),
345 );
346 return;
347 }
348
349 let conn = conn_counter.fetch_add(1, Ordering::Relaxed);
350 handler.on_connect(origin, conn);
351
352 let incoming: Result<Incoming, _> = serde_json::from_slice(&req.body);
353 match incoming {
354 Ok(Incoming::Request(rpc_req)) if rpc_req.method == method::SUBSCRIPTIONS_LISTEN => {
355 serve_listen(
356 reader,
357 rpc_req,
358 origin,
359 conn,
360 handler,
361 subs,
362 cors.as_deref(),
363 );
364 }
365 Ok(Incoming::Request(rpc_req)) if handler.streams(&rpc_req.method) => {
368 serve_stream(reader, rpc_req, origin, conn, handler, cors.as_deref());
369 remove_and_disconnect(subs, conn, origin, handler);
370 }
371 Ok(Incoming::Request(rpc_req)) => {
372 serve_unary(
373 reader.get_mut(),
374 rpc_req,
375 origin,
376 conn,
377 handler,
378 cors.as_deref(),
379 );
380 remove_and_disconnect(subs, conn, origin, handler);
381 }
382 Ok(Incoming::Notification(_)) | Ok(Incoming::Response(_)) => {
384 let _ = write_simple(reader.get_mut(), 202, "Accepted", b"", cors.as_deref());
385 remove_and_disconnect(subs, conn, origin, handler);
386 }
387 Err(_) => {
388 let _ = write_simple(
389 reader.get_mut(),
390 400,
391 "Bad Request",
392 b"invalid JSON-RPC frame",
393 cors.as_deref(),
394 );
395 remove_and_disconnect(subs, conn, origin, handler);
396 }
397 }
398}
399
400fn serve_stream<S: Read + Write + Send + 'static>(
406 reader: BufReader<S>,
407 req: Request,
408 origin: PeerOrigin,
409 conn: u64,
410 handler: &Arc<dyn Handler>,
411 cors: Option<&str>,
412) {
413 let mut stream = reader.into_inner();
414 let head = format!(
415 "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-store\r\n{}Connection: close\r\n\r\n",
416 cors_headers(cors)
417 );
418 if stream
419 .write_all(head.as_bytes())
420 .and_then(|_| stream.flush())
421 .is_err()
422 {
423 return;
424 }
425 let writer: SharedWriter = Arc::new(Mutex::new(ServeStream::Http(Box::new(stream))));
426
427 let stop = Arc::new(std::sync::atomic::AtomicBool::new(false));
430 let ka = {
431 let writer = Arc::clone(&writer);
432 let stop = Arc::clone(&stop);
433 thread::spawn(move || {
434 while !stop.load(Ordering::Relaxed) {
435 thread::sleep(SSE_KEEPALIVE);
436 if stop.load(Ordering::Relaxed) {
437 break;
438 }
439 let alive = writer
440 .lock()
441 .map(|mut w| {
442 w.write_all(b": keep-alive\n\n")
443 .and_then(|_| w.flush())
444 .is_ok()
445 })
446 .unwrap_or(false);
447 if !alive {
448 break;
449 }
450 }
451 })
452 };
453
454 let resp = handler.dispatch(req, origin, &writer, conn);
455 stop.store(true, Ordering::Relaxed);
456 if let Ok(mut w) = writer.lock() {
457 let _ = w.write_response(&resp);
458 }
459 let _ = ka.join();
460}
461
462fn serve_unary<S: Write>(
465 stream: &mut S,
466 req: Request,
467 origin: PeerOrigin,
468 conn: u64,
469 handler: &Arc<dyn Handler>,
470 cors: Option<&str>,
471) {
472 let sink: SharedWriter = Arc::new(Mutex::new(ServeStream::Http(Box::new(io::sink()))));
475 let is_initialize = req.method == method::INITIALIZE;
476 let resp = handler.dispatch(req, origin, &sink, conn);
477 let body = serde_json::to_vec(&resp).unwrap_or_default();
478 let session = if is_initialize {
479 format!("Mcp-Session-Id: {}\r\n", next_session_id())
480 } else {
481 String::new()
482 };
483 let head = format!(
484 "HTTP/1.1 200 OK\r\nContent-Type: application/json\r\n{session}{}Content-Length: {}\r\nConnection: close\r\n\r\n",
485 cors_headers(cors),
486 body.len()
487 );
488 let _ = stream.write_all(head.as_bytes());
489 let _ = stream.write_all(&body);
490 let _ = stream.flush();
491}
492
493fn serve_listen<S: Read + Write + Send + 'static>(
499 reader: BufReader<S>,
500 req: Request,
501 origin: PeerOrigin,
502 conn: u64,
503 handler: &Arc<dyn Handler>,
504 subs: &SubRegistry,
505 cors: Option<&str>,
506) {
507 let uris = listen_uris(&req);
508 let mut stream = reader.into_inner();
509 let head = format!(
511 "HTTP/1.1 200 OK\r\nContent-Type: text/event-stream\r\nCache-Control: no-store\r\n{}Connection: close\r\n\r\n",
512 cors_headers(cors)
513 );
514 if stream
515 .write_all(head.as_bytes())
516 .and_then(|_| stream.flush())
517 .is_err()
518 {
519 remove_and_disconnect(subs, conn, origin, handler);
520 return;
521 }
522
523 let writer: SharedWriter = Arc::new(Mutex::new(ServeStream::Http(Box::new(stream))));
527 for uri in &uris {
528 let sub_req = Request::new(0, method::RESOURCES_SUBSCRIBE, Some(json!({ "uri": uri })));
529 let _ = handler.dispatch(sub_req, origin, &writer, conn);
530 }
531
532 loop {
534 thread::sleep(SSE_KEEPALIVE);
535 let alive = writer
536 .lock()
537 .map(|mut w| {
538 w.write_all(b": keep-alive\n\n")
539 .and_then(|_| w.flush())
540 .is_ok()
541 })
542 .unwrap_or(false);
543 if !alive {
544 break;
545 }
546 }
547 remove_and_disconnect(subs, conn, origin, handler);
548}
549
550fn listen_uris(req: &Request) -> Vec<String> {
553 req.params
554 .as_ref()
555 .and_then(|p| p.get("notifications"))
556 .and_then(|n| n.get("resourceSubscriptions"))
557 .and_then(Value::as_array)
558 .map(|a| {
559 a.iter()
560 .filter_map(Value::as_str)
561 .map(str::to_string)
562 .collect()
563 })
564 .unwrap_or_default()
565}
566
567fn remove_and_disconnect(
568 subs: &SubRegistry,
569 conn: u64,
570 origin: PeerOrigin,
571 handler: &Arc<dyn Handler>,
572) {
573 crate::server::remove_conn_subscriptions(subs, conn);
574 handler.on_disconnect(origin, conn);
575}
576
577fn write_simple<S: Write>(
579 stream: &mut S,
580 code: u16,
581 reason: &str,
582 body: &[u8],
583 cors: Option<&str>,
584) -> io::Result<()> {
585 let head = format!(
586 "HTTP/1.1 {code} {reason}\r\nContent-Type: text/plain\r\n{}Content-Length: {}\r\nConnection: close\r\n\r\n",
587 cors_headers(cors),
588 body.len()
589 );
590 stream.write_all(head.as_bytes())?;
591 stream.write_all(body)?;
592 stream.flush()
593}
594
595fn cors_headers(origin: Option<&str>) -> String {
599 match origin {
600 Some(o) => format!(
601 "Access-Control-Allow-Origin: {o}\r\nVary: Origin\r\nAccess-Control-Expose-Headers: Mcp-Session-Id\r\n"
602 ),
603 None => String::new(),
604 }
605}
606
607fn write_preflight<S: Write>(stream: &mut S, origin: Option<&str>) -> io::Result<()> {
610 let grant = match origin {
611 Some(o) => format!(
612 "Access-Control-Allow-Origin: {o}\r\nVary: Origin\r\nAccess-Control-Allow-Methods: POST, OPTIONS\r\nAccess-Control-Allow-Headers: content-type, authorization, last-event-id, mcp-session-id\r\nAccess-Control-Max-Age: 600\r\n"
613 ),
614 None => String::new(),
615 };
616 let head =
617 format!("HTTP/1.1 204 No Content\r\n{grant}Content-Length: 0\r\nConnection: close\r\n\r\n");
618 stream.write_all(head.as_bytes())?;
619 stream.flush()
620}
621
622pub struct RawRequest {
631 pub method: String,
632 pub target: String,
633 pub headers: Vec<(String, String)>,
634 pub body: Vec<u8>,
635 pub peer_cert: bool,
637 pub peer_subject: Option<String>,
639 pub peer_sans: Vec<String>,
641}
642
643impl RawRequest {
644 pub fn header(&self, name: &str) -> Option<&str> {
646 self.headers
647 .iter()
648 .find(|(k, _)| k == name)
649 .map(|(_, v)| v.as_str())
650 }
651 pub fn path(&self) -> &str {
653 self.target.split('?').next().unwrap_or(&self.target)
654 }
655}
656
657pub struct RawResponse {
659 pub status: u16,
660 pub reason: &'static str,
661 pub content_type: &'static str,
662 pub body: Vec<u8>,
663 pub headers: Vec<(&'static str, String)>,
665}
666
667impl RawResponse {
668 pub fn json(status: u16, reason: &'static str, body: impl Into<Vec<u8>>) -> RawResponse {
670 RawResponse {
671 status,
672 reason,
673 content_type: "application/json",
674 body: body.into(),
675 headers: Vec::new(),
676 }
677 }
678 pub fn text(status: u16, reason: &'static str, body: impl Into<Vec<u8>>) -> RawResponse {
680 RawResponse {
681 status,
682 reason,
683 content_type: "text/plain",
684 body: body.into(),
685 headers: Vec::new(),
686 }
687 }
688}
689
690pub trait RawHandler: Send + Sync + 'static {
693 fn handle(&self, req: &RawRequest) -> RawResponse;
694}
695
696pub fn spawn_accept_raw(
699 listener: TcpListener,
700 acceptor: Arc<HttpAcceptor>,
701 handler: Arc<dyn RawHandler>,
702 write_timeout: Duration,
703) -> io::Result<()> {
704 thread::Builder::new()
705 .name("serve-webhook".into())
706 .spawn(move || {
707 for tcp in listener.incoming().flatten() {
708 let acceptor = Arc::clone(&acceptor);
709 let handler = Arc::clone(&handler);
710 thread::Builder::new()
711 .name("webhook-conn".into())
712 .spawn(move || {
713 let _ = tcp.set_write_timeout(Some(write_timeout));
714 let _ = tcp.set_read_timeout(Some(write_timeout));
715 match &*acceptor {
716 HttpAcceptor::Plain => serve_conn_raw(tcp, PeerId::default(), &handler),
717 #[cfg(feature = "tls")]
718 HttpAcceptor::Tls(tls) => {
719 if let Ok(stream) = tls.accept(tcp) {
720 let peer = peer_id(&stream);
721 serve_conn_raw(stream, peer, &handler);
722 }
723 }
724 }
725 })
726 .ok();
727 }
728 })
729 .map(|_| ())
730}
731
732fn serve_conn_raw<S: Read + Write + Send + 'static>(
733 stream: S,
734 peer: PeerId,
735 handler: &Arc<dyn RawHandler>,
736) {
737 let mut reader = BufReader::new(stream);
738 let req = match read_request(&mut reader) {
739 Ok(req) => req,
740 Err(ReadError::HeadTooLarge) => {
741 let _ = write_simple(
742 reader.get_mut(),
743 431,
744 "Request Header Fields Too Large",
745 b"request head exceeds the header size/count limits",
746 None,
747 );
748 return;
749 }
750 Err(ReadError::Incomplete) => return,
751 };
752 if matches!(check_origin(&req.headers, &[]), OriginCheck::Denied) {
754 let _ = write_simple(
755 reader.get_mut(),
756 403,
757 "Forbidden",
758 b"cross-origin request rejected",
759 None,
760 );
761 return;
762 }
763 let raw = RawRequest {
764 method: req.method,
765 target: req.target,
766 headers: req.headers,
767 body: req.body,
768 peer_cert: peer.cert,
769 peer_subject: peer.subject,
770 peer_sans: peer.sans,
771 };
772 let resp = handler.handle(&raw);
773 let _ = write_raw(reader.get_mut(), &resp);
774}
775
776fn write_raw<S: Write>(stream: &mut S, resp: &RawResponse) -> io::Result<()> {
777 let mut head = format!(
778 "HTTP/1.1 {} {}\r\nContent-Type: {}\r\nContent-Length: {}\r\nConnection: close\r\n",
779 resp.status,
780 resp.reason,
781 resp.content_type,
782 resp.body.len()
783 );
784 for (name, value) in &resp.headers {
785 head.push_str(name);
786 head.push_str(": ");
787 head.push_str(value);
788 head.push_str("\r\n");
789 }
790 head.push_str("\r\n");
791 stream.write_all(head.as_bytes())?;
792 stream.write_all(&resp.body)?;
793 stream.flush()
794}
795
796struct HttpRequest {
798 method: String,
799 #[allow(dead_code)]
800 target: String,
801 headers: Vec<(String, String)>,
802 body: Vec<u8>,
803}
804
805enum OriginCheck {
807 NoBrowser,
809 Allowed(String),
811 Denied,
813}
814
815fn check_origin(headers: &[(String, String)], extra: &[String]) -> OriginCheck {
819 match headers.iter().find(|(k, _)| k == "origin") {
820 None => OriginCheck::NoBrowser,
821 Some((_, origin)) => {
822 if origin_is_loopback(origin) || extra.iter().any(|e| e == origin) {
823 OriginCheck::Allowed(origin.clone())
824 } else {
825 OriginCheck::Denied
826 }
827 }
828 }
829}
830
831fn origin_is_loopback(origin: &str) -> bool {
834 let after_scheme = origin.split_once("://").map(|(_, r)| r).unwrap_or(origin);
835 let authority = after_scheme.split('/').next().unwrap_or(after_scheme);
836 let host = if let Some(v6) = authority.strip_prefix('[') {
838 v6.split(']').next().unwrap_or(v6)
839 } else {
840 authority.split(':').next().unwrap_or(authority)
841 };
842 host == "localhost" || host == "::1" || host.starts_with("127.")
843}
844
845fn next_session_id() -> String {
851 static SEQ: AtomicU64 = AtomicU64::new(0);
852 let n = SEQ.fetch_add(1, Ordering::Relaxed);
853 let millis = std::time::SystemTime::now()
854 .duration_since(std::time::UNIX_EPOCH)
855 .map(|d| d.as_millis())
856 .unwrap_or(0);
857 format!("s-{millis:x}-{n:x}")
858}
859
860enum ReadError {
862 Incomplete,
865 HeadTooLarge,
868}
869
870fn read_head_line<S: Read>(
876 reader: &mut BufReader<S>,
877 budget: &mut usize,
878 line: &mut String,
879) -> Result<usize, ReadError> {
880 let n = Read::take(&mut *reader, *budget as u64 + 1)
883 .read_line(line)
884 .map_err(|_| ReadError::Incomplete)?;
885 if n > *budget {
886 return Err(ReadError::HeadTooLarge);
887 }
888 *budget -= n;
889 Ok(n)
890}
891
892fn read_request<S: Read>(reader: &mut BufReader<S>) -> Result<HttpRequest, ReadError> {
895 let mut budget = MAX_HEAD_BYTES;
896 let mut request_line = String::new();
897 if read_head_line(reader, &mut budget, &mut request_line)? == 0 {
898 return Err(ReadError::Incomplete);
899 }
900 let mut parts = request_line.split_whitespace();
901 let method = parts.next().ok_or(ReadError::Incomplete)?.to_string();
902 let target = parts.next().ok_or(ReadError::Incomplete)?.to_string();
903
904 let mut headers = Vec::new();
905 let mut content_length = 0usize;
906 loop {
907 let mut line = String::new();
908 if read_head_line(reader, &mut budget, &mut line)? == 0 {
909 break;
910 }
911 let line = line.trim_end();
912 if line.is_empty() {
913 break; }
915 if let Some((k, v)) = line.split_once(':') {
916 if headers.len() >= MAX_HEADERS {
917 return Err(ReadError::HeadTooLarge);
918 }
919 let name = k.trim().to_ascii_lowercase();
920 let value = v.trim().to_string();
921 if name == "content-length" {
922 content_length = value.parse().unwrap_or(0);
923 }
924 headers.push((name, value));
925 }
926 }
927 if content_length > MAX_BODY {
928 return Err(ReadError::Incomplete);
929 }
930 let mut body = vec![0u8; content_length];
931 if content_length > 0 {
932 reader
933 .read_exact(&mut body)
934 .map_err(|_| ReadError::Incomplete)?;
935 }
936 Ok(HttpRequest {
937 method,
938 target,
939 headers,
940 body,
941 })
942}
943
944#[cfg(test)]
945mod tests {
946 use super::*;
947 use crate::rpc::{self, Response};
948 use crate::server::{notify_resource_updated_keep, register_subscriber};
949 use std::io::BufRead;
950
951 struct TestHandler {
955 subs: SubRegistry,
956 }
957 impl Handler for TestHandler {
958 fn dispatch(
959 &self,
960 req: Request,
961 _origin: PeerOrigin,
962 writer: &SharedWriter,
963 conn: u64,
964 ) -> Response {
965 if let Some(resp) = crate::server::lifecycle_response(
966 &req,
967 &json!({"name": "test", "version": "1"}),
968 &json!({"tools": {}, "resources": {"subscribe": true}}),
969 ) {
970 return resp;
971 }
972 match req.method.as_str() {
973 "tools/call" => Response::ok(req.id, json!({"ok": true})),
974 "resources/subscribe" => {
975 let uri = req
976 .params
977 .as_ref()
978 .and_then(|p| p["uri"].as_str())
979 .unwrap_or("");
980 if uri == "res://ok" {
982 register_subscriber(&self.subs, uri, conn, writer);
983 Response::ok(req.id, json!({}))
984 } else {
985 Response::err(req.id, rpc::RESOURCE_NOT_FOUND, "no")
986 }
987 }
988 _ => Response::err(req.id, rpc::METHOD_NOT_FOUND, "unknown"),
989 }
990 }
991 }
992
993 fn http_post(addr: &str, body: &str) -> (Vec<(String, String)>, String) {
994 let mut s = TcpStream::connect(addr).unwrap();
995 let req = format!(
996 "POST /mcp HTTP/1.1\r\nHost: x\r\nContent-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
997 body.len()
998 );
999 s.write_all(req.as_bytes()).unwrap();
1000 s.set_read_timeout(Some(Duration::from_secs(5))).ok();
1001 let mut reader = BufReader::new(s);
1002 let mut status = String::new();
1003 reader.read_line(&mut status).unwrap();
1004 let mut headers = Vec::new();
1005 loop {
1006 let mut l = String::new();
1007 reader.read_line(&mut l).unwrap();
1008 if l.trim().is_empty() {
1009 break;
1010 }
1011 if let Some((k, v)) = l.split_once(':') {
1012 headers.push((k.trim().to_ascii_lowercase(), v.trim().to_string()));
1013 }
1014 }
1015 let mut body = String::new();
1016 reader.read_to_string(&mut body).unwrap();
1017 (headers, body)
1018 }
1019
1020 fn spawn_server() -> (String, SubRegistry) {
1021 let subs: SubRegistry = Arc::new(Mutex::new(std::collections::HashMap::new()));
1022 let listener = bind_tcp("127.0.0.1:0").unwrap();
1023 let addr = listener.local_addr().unwrap().to_string();
1024 spawn_accept_http(
1025 listener,
1026 Arc::new(HttpAcceptor::Plain),
1027 Arc::new(TestHandler {
1028 subs: Arc::clone(&subs),
1029 }),
1030 Arc::new(AllowAll),
1031 Arc::clone(&subs),
1032 Arc::new(AtomicU64::new(0)),
1033 Duration::from_secs(5),
1034 )
1035 .unwrap();
1036 (addr, subs)
1037 }
1038
1039 #[test]
1040 fn unary_post_returns_application_json() {
1041 let (addr, _subs) = spawn_server();
1042 let (headers, body) = http_post(
1043 &addr,
1044 r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"x"}}"#,
1045 );
1046 assert!(
1047 headers
1048 .iter()
1049 .any(|(k, v)| k == "content-type" && v.contains("application/json")),
1050 "headers: {headers:?}"
1051 );
1052 let v: Value = serde_json::from_str(&body).unwrap();
1053 assert_eq!(v["result"]["ok"], true);
1054 }
1055
1056 fn http_post_origin(addr: &str, origin: Option<&str>, body: &str) -> u16 {
1058 let mut s = TcpStream::connect(addr).unwrap();
1059 let origin_line = origin
1060 .map(|o| format!("Origin: {o}\r\n"))
1061 .unwrap_or_default();
1062 let req = format!(
1063 "POST /mcp HTTP/1.1\r\nHost: x\r\n{origin_line}Content-Type: application/json\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1064 body.len()
1065 );
1066 s.write_all(req.as_bytes()).unwrap();
1067 s.set_read_timeout(Some(Duration::from_secs(5))).ok();
1068 let mut status = String::new();
1069 BufReader::new(s).read_line(&mut status).unwrap();
1070 status
1071 .split_whitespace()
1072 .nth(1)
1073 .and_then(|c| c.parse().ok())
1074 .unwrap_or(0)
1075 }
1076
1077 #[test]
1078 fn initialize_stamps_a_unique_session_header() {
1079 let (addr, _subs) = spawn_server();
1080 let init = r#"{"jsonrpc":"2.0","id":1,"method":"initialize","params":{"protocolVersion":"2025-11-25"}}"#;
1081 let sid = |addr: &str| {
1082 let (headers, _) = http_post(addr, init);
1083 headers
1084 .into_iter()
1085 .find(|(k, _)| k == "mcp-session-id")
1086 .map(|(_, v)| v)
1087 };
1088 let a = sid(&addr).expect("initialize stamps a session id");
1089 let b = sid(&addr).expect("second initialize stamps a session id");
1090 assert_ne!(a, "srv", "the session id is not the old constant");
1091 assert_ne!(a, b, "each initialize mints a distinct session id");
1092 }
1093
1094 #[test]
1095 fn a_cross_origin_request_is_rejected_403() {
1096 let (addr, _subs) = spawn_server();
1097 let call = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"x"}}"#;
1098 assert_eq!(
1100 http_post_origin(&addr, Some("https://evil.example"), call),
1101 403
1102 );
1103 assert_eq!(http_post_origin(&addr, None, call), 200);
1105 assert_eq!(
1107 http_post_origin(&addr, Some("http://localhost:3000"), call),
1108 200
1109 );
1110 assert_eq!(http_post_origin(&addr, Some("http://127.0.0.1"), call), 200);
1111 }
1112
1113 fn spawn_server_with_origins(extra: &[&str]) -> String {
1115 let subs: SubRegistry = Arc::new(Mutex::new(std::collections::HashMap::new()));
1116 let listener = bind_tcp("127.0.0.1:0").unwrap();
1117 let addr = listener.local_addr().unwrap().to_string();
1118 spawn_accept_http_opts(
1119 listener,
1120 Arc::new(HttpAcceptor::Plain),
1121 Arc::new(TestHandler {
1122 subs: Arc::clone(&subs),
1123 }),
1124 Arc::new(AllowAll),
1125 Arc::clone(&subs),
1126 Arc::new(AtomicU64::new(0)),
1127 Duration::from_secs(5),
1128 ServeOptions {
1129 extra_origins: extra.iter().map(|s| s.to_string()).collect(),
1130 },
1131 )
1132 .unwrap();
1133 addr
1134 }
1135
1136 fn http_raw(addr: &str, req: &str) -> (u16, Vec<(String, String)>) {
1138 let mut s = TcpStream::connect(addr).unwrap();
1139 s.write_all(req.as_bytes()).unwrap();
1140 s.set_read_timeout(Some(Duration::from_secs(5))).ok();
1141 let mut reader = BufReader::new(s);
1142 let mut status = String::new();
1143 reader.read_line(&mut status).unwrap();
1144 let code = status
1145 .split_whitespace()
1146 .nth(1)
1147 .and_then(|c| c.parse().ok())
1148 .unwrap_or(0);
1149 let mut headers = Vec::new();
1150 loop {
1151 let mut l = String::new();
1152 if reader.read_line(&mut l).unwrap_or(0) == 0 {
1153 break;
1154 }
1155 if l.trim().is_empty() {
1156 break;
1157 }
1158 if let Some((k, v)) = l.split_once(':') {
1159 headers.push((k.trim().to_ascii_lowercase(), v.trim().to_string()));
1160 }
1161 }
1162 (code, headers)
1163 }
1164
1165 #[test]
1166 fn a_configured_extra_origin_is_served_with_cors_headers() {
1167 let addr = spawn_server_with_origins(&["https://ui.example"]);
1168 let call = r#"{"jsonrpc":"2.0","id":1,"method":"tools/call","params":{"name":"x"}}"#;
1169 let req = format!(
1171 "POST / HTTP/1.1\r\nHost: x\r\nOrigin: https://ui.example\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{call}",
1172 call.len()
1173 );
1174 let (code, headers) = http_raw(&addr, &req);
1175 assert_eq!(code, 200);
1176 assert!(
1177 headers
1178 .iter()
1179 .any(|(k, v)| { k == "access-control-allow-origin" && v == "https://ui.example" }),
1180 "CORS echo missing: {headers:?}"
1181 );
1182 let bad = format!(
1184 "POST / HTTP/1.1\r\nHost: x\r\nOrigin: https://evil.example\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{call}",
1185 call.len()
1186 );
1187 assert_eq!(http_raw(&addr, &bad).0, 403);
1188 let local = format!(
1190 "POST / HTTP/1.1\r\nHost: x\r\nOrigin: http://127.0.0.1:5173\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{call}",
1191 call.len()
1192 );
1193 let (code, headers) = http_raw(&addr, &local);
1194 assert_eq!(code, 200);
1195 assert!(
1196 headers
1197 .iter()
1198 .any(|(k, v)| k == "access-control-allow-origin" && v == "http://127.0.0.1:5173")
1199 );
1200 }
1201
1202 #[test]
1203 fn a_cors_preflight_options_is_answered_before_auth() {
1204 let addr = spawn_server_with_origins(&["https://ui.example"]);
1205 let req = "OPTIONS / HTTP/1.1\r\nHost: x\r\nOrigin: https://ui.example\r\nAccess-Control-Request-Method: POST\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
1206 let (code, headers) = http_raw(&addr, req);
1207 assert_eq!(code, 204);
1208 assert!(
1209 headers
1210 .iter()
1211 .any(|(k, v)| k == "access-control-allow-origin" && v == "https://ui.example")
1212 );
1213 assert!(
1214 headers
1215 .iter()
1216 .any(|(k, v)| k == "access-control-allow-headers" && v.contains("authorization"))
1217 );
1218 let bad = "OPTIONS / HTTP/1.1\r\nHost: x\r\nOrigin: https://evil.example\r\nContent-Length: 0\r\nConnection: close\r\n\r\n";
1220 assert_eq!(http_raw(&addr, bad).0, 403);
1221 }
1222
1223 #[test]
1224 fn origin_loopback_classification() {
1225 assert!(origin_is_loopback("http://localhost"));
1226 assert!(origin_is_loopback("http://localhost:8080"));
1227 assert!(origin_is_loopback("https://127.0.0.1:443"));
1228 assert!(origin_is_loopback("http://[::1]:9000"));
1229 assert!(!origin_is_loopback("https://evil.example"));
1230 assert!(!origin_is_loopback("http://169.254.1.1")); assert!(!origin_is_loopback("null")); }
1233
1234 #[test]
1235 fn subscriptions_listen_streams_a_pushed_update_as_sse() {
1236 let (addr, subs) = spawn_server();
1237 let addr2 = addr.clone();
1239 let got = Arc::new(Mutex::new(String::new()));
1240 let got2 = Arc::clone(&got);
1241 thread::spawn(move || {
1242 let mut s = TcpStream::connect(&addr2).unwrap();
1243 let body = r#"{"jsonrpc":"2.0","id":1,"method":"subscriptions/listen","params":{"notifications":{"resourceSubscriptions":["res://ok"]}}}"#;
1244 let req = format!(
1245 "POST /mcp HTTP/1.1\r\nHost: x\r\nAccept: text/event-stream\r\nContent-Length: {}\r\nConnection: close\r\n\r\n{body}",
1246 body.len()
1247 );
1248 s.write_all(req.as_bytes()).unwrap();
1249 s.set_read_timeout(Some(Duration::from_secs(5))).ok();
1250 let mut reader = BufReader::new(s);
1251 let mut line = String::new();
1252 for _ in 0..50 {
1254 line.clear();
1255 if reader.read_line(&mut line).unwrap_or(0) == 0 {
1256 break;
1257 }
1258 if line.starts_with("data:") {
1259 *got2.lock().unwrap() = line.clone();
1260 break;
1261 }
1262 }
1263 });
1264
1265 let deadline = std::time::Instant::now() + Duration::from_secs(3);
1267 while std::time::Instant::now() < deadline {
1268 if subs.lock().unwrap().contains_key("res://ok") {
1269 break;
1270 }
1271 thread::sleep(Duration::from_millis(20));
1272 }
1273 notify_resource_updated_keep(&subs, "res://ok");
1274
1275 let deadline = std::time::Instant::now() + Duration::from_secs(3);
1276 loop {
1277 if got.lock().unwrap().starts_with("data:") {
1278 break;
1279 }
1280 assert!(std::time::Instant::now() < deadline, "no SSE push observed");
1281 thread::sleep(Duration::from_millis(20));
1282 }
1283 let data = got.lock().unwrap().clone();
1284 assert!(data.contains("notifications/resources/updated"), "{data}");
1285 assert!(data.contains("res://ok"), "{data}");
1286 }
1287}