1use crate::ICAP_VERSION;
53use crate::error::{Error, IcapResult};
54#[cfg(test)]
55use crate::error::{ProtocolError, ProtocolField};
56use crate::protocol::{
57 find_double_crlf, parse_header_lines, parse_http_request_start_line,
58 parse_http_response_start_line,
59};
60use http::{HeaderMap, HeaderName, HeaderValue, Request as HttpRequest, Response as HttpResponse};
61use memchr::memmem;
62use std::fmt;
63use std::future::Future;
64use std::str::FromStr;
65use tracing::trace;
66
67use std::io::Write as _;
68use std::pin::Pin;
69use tokio::io::AsyncRead;
70
71#[derive(Debug)]
87#[must_use]
88pub struct Request<R = Vec<u8>, D: DirectionMeta = Outbound> {
89 pub(crate) method: Method,
91 pub(crate) service: String,
93 pub(crate) icap_headers: HeaderMap,
95 pub(crate) embedded: Option<EmbeddedHttp<R>>,
97 pub(crate) preview_size: Option<usize>,
99 pub(crate) allow_204: bool,
101 pub(crate) allow_206: bool,
103 pub(crate) preview_ieof: bool,
105 pub(crate) meta: D::Meta,
110}
111
112pub type OutboundRequest<R = Vec<u8>> = Request<R, Outbound>;
114
115pub type IncomingRequest<R = Vec<u8>> = Request<R, Incoming>;
121
122#[derive(Debug, Clone, Copy, Eq, PartialEq)]
124pub enum Outbound {}
125
126#[derive(Debug, Clone, Copy, Eq, PartialEq)]
128pub enum Incoming {}
129
130mod private {
135 pub trait Sealed {}
136 impl Sealed for super::Outbound {}
137 impl Sealed for super::Incoming {}
138}
139
140pub trait DirectionMeta: private::Sealed {
154 #[doc(hidden)]
159 type Meta: Default + fmt::Debug + Clone;
160}
161
162#[doc(hidden)]
166#[derive(Debug, Clone, Default)]
167pub struct OutboundMeta;
168
169#[doc(hidden)]
175#[derive(Debug, Clone, Default)]
176pub struct IncomingMeta {
177 pub(crate) istag: Option<String>,
178 pub(crate) chunk_trailers: HeaderMap,
179}
180
181impl DirectionMeta for Outbound {
182 type Meta = OutboundMeta;
183}
184
185impl DirectionMeta for Incoming {
186 type Meta = IncomingMeta;
187}
188
189#[derive(Debug, Clone, Copy, Eq, Hash, PartialEq, Ord, PartialOrd)]
207pub enum Method {
208 ReqMod,
210 RespMod,
212 Options,
214}
215
216impl Method {
217 #[inline]
221 pub const fn as_str(&self) -> &'static str {
222 match self {
223 Self::ReqMod => "REQMOD",
224 Self::RespMod => "RESPMOD",
225 Self::Options => "OPTIONS",
226 }
227 }
228
229 pub fn parse_token(token: &str) -> IcapResult<Self> {
233 token.parse().map_err(|_| {
234 Error::invalid_method(format!("Unknown ICAP method string: {}", token.trim()))
235 })
236 }
237}
238
239impl fmt::Display for Method {
240 #[inline]
241 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
242 f.write_str(self.as_str())
243 }
244}
245
246impl FromStr for Method {
247 type Err = &'static str;
248
249 #[inline]
250 fn from_str(s: &str) -> Result<Self, Self::Err> {
251 let t = s.trim();
252 if t.eq_ignore_ascii_case("REQMOD") {
253 Ok(Self::ReqMod)
254 } else if t.eq_ignore_ascii_case("RESPMOD") {
255 Ok(Self::RespMod)
256 } else if t.eq_ignore_ascii_case("OPTIONS") {
257 Ok(Self::Options)
258 } else {
259 Err("Unknown ICAP method string")
260 }
261 }
262}
263
264impl From<&str> for Method {
265 #[inline]
266 fn from(s: &str) -> Self {
267 s.parse()
268 .unwrap_or_else(|_| panic!("Unknown ICAP method string: {s}"))
269 }
270}
271
272impl From<String> for Method {
273 #[inline]
274 fn from(s: String) -> Self {
275 s.as_str().into()
276 }
277}
278
279type BoxedUnitFut = Pin<Box<dyn Future<Output = IcapResult<()>> + Send>>;
280
281pub struct ContinueHandle {
284 send: Option<Box<dyn FnOnce() -> BoxedUnitFut + Send + Sync>>,
285}
286
287impl ContinueHandle {
288 pub fn new<F, Fut>(f: F) -> Self
289 where
290 F: FnOnce() -> Fut + Send + Sync + 'static,
291 Fut: Future<Output = IcapResult<()>> + Send + 'static,
292 {
293 Self {
294 send: Some(Box::new(move || Box::pin(f()))),
295 }
296 }
297
298 pub async fn send_100_continue(&mut self) -> IcapResult<()> {
299 if let Some(f) = self.send.take() {
300 f().await
301 } else {
302 Ok(())
303 }
304 }
305}
306
307pub struct Remainder<R> {
312 reader: R,
313 cont: Option<ContinueHandle>,
314}
315
316impl<R> Remainder<R> {
317 pub const fn new(reader: R, cont: Option<ContinueHandle>) -> Self {
318 Self { reader, cont }
319 }
320 pub async fn continue_if_needed(&mut self) -> IcapResult<()> {
321 if let Some(mut c) = self.cont.take() {
322 c.send_100_continue().await
323 } else {
324 Ok(())
325 }
326 }
327 pub fn take_reader(self) -> R {
328 self.reader
329 }
330}
331
332impl<R> fmt::Debug for Remainder<R> {
333 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
334 f.debug_struct("Remainder")
335 .field("cont_present", &self.cont.is_some())
336 .finish_non_exhaustive()
337 }
338}
339
340pub enum Body<R> {
352 Empty,
353 Preview {
354 bytes: Vec<u8>,
355 ieof: bool,
356 remainder: Remainder<R>,
357 },
358 Full {
359 reader: R,
360 },
361}
362
363impl<R> fmt::Debug for Body<R> {
364 fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
365 match self {
366 Self::Empty => f.write_str("Body::Empty"),
367 Self::Full { .. } => f.write_str("Body::Full"),
368 Self::Preview {
369 bytes,
370 ieof,
371 remainder,
372 } => f
373 .debug_struct("Body::Preview")
374 .field("bytes_len", &bytes.len())
375 .field("ieof", ieof)
376 .field("remainder", remainder)
377 .finish(),
378 }
379 }
380}
381
382pub type BodyRead = Box<dyn AsyncRead + Unpin + Send>;
384
385struct CursorReader<T>(std::io::Cursor<T>);
387
388impl<T: AsRef<[u8]> + Unpin> AsyncRead for CursorReader<T> {
389 fn poll_read(
390 mut self: Pin<&mut Self>,
391 _cx: &mut std::task::Context<'_>,
392 buf: &mut tokio::io::ReadBuf<'_>,
393 ) -> std::task::Poll<std::io::Result<()>> {
394 let src = self.0.get_ref().as_ref();
397 let pos = usize::try_from(self.0.position()).map_or(src.len(), |pos| pos.min(src.len()));
398 let remaining = src.len().saturating_sub(pos);
399 if remaining > 0 {
400 let to_copy = remaining.min(buf.remaining());
401 buf.put_slice(&src[pos..pos + to_copy]);
402 self.0.set_position((pos + to_copy) as u64);
403 }
404 std::task::Poll::Ready(Ok(()))
405 }
406}
407impl Body<BodyRead> {
408 pub async fn ensure_full(&mut self) -> IcapResult<&mut (dyn AsyncRead + Unpin + Send)> {
415 struct Concat<A, B>(Option<A>, B);
416
417 impl<A: AsyncRead + Unpin, B: AsyncRead + Unpin> AsyncRead for Concat<A, B> {
418 fn poll_read(
419 mut self: Pin<&mut Self>,
420 cx: &mut std::task::Context<'_>,
421 buf: &mut tokio::io::ReadBuf<'_>,
422 ) -> std::task::Poll<std::io::Result<()>> {
423 if let Some(a) = self.0.as_mut() {
424 let mut tmp = [0u8; 8192];
425 let want = buf.remaining().min(tmp.len());
426 let mut sub = tokio::io::ReadBuf::new(&mut tmp[..want]);
427
428 match std::pin::Pin::new(a).poll_read(cx, &mut sub) {
429 std::task::Poll::Pending => return std::task::Poll::Pending,
430 std::task::Poll::Ready(Err(e)) => return std::task::Poll::Ready(Err(e)),
431 std::task::Poll::Ready(Ok(())) => {
432 let n = sub.filled().len();
433 if n > 0 {
434 buf.put_slice(sub.filled());
435 return std::task::Poll::Ready(Ok(()));
436 }
437 self.0 = None;
438 }
439 }
440 }
441
442 std::pin::Pin::new(&mut self.1).poll_read(cx, buf)
443 }
444 }
445
446 match self {
447 Body::Empty => Err(Error::body("no body")),
448 Body::Full { reader } => Ok(reader.as_mut()),
449 Body::Preview {
450 bytes,
451 ieof,
452 remainder,
453 } => {
454 if !*ieof {
455 remainder.continue_if_needed().await?;
456 }
457
458 let preview_reader: BodyRead =
459 Box::new(CursorReader(std::io::Cursor::new(std::mem::take(bytes))));
460
461 let rem = std::mem::replace(
462 remainder,
463 Remainder::new(Box::new(tokio::io::empty()), None),
464 );
465 let tail = rem.take_reader();
466
467 let concat: BodyRead = Box::new(Concat(Some(preview_reader), tail));
468 *self = Body::Full { reader: concat };
469
470 match self {
471 Body::Full { reader } => Ok(reader.as_mut()),
472 _ => unreachable!(),
473 }
474 }
475 }
476 }
477}
478
479#[derive(Debug)]
483pub enum EmbeddedHttp<R> {
484 Req {
486 head: HttpRequest<()>,
487 body: Body<R>,
488 },
489 Resp {
496 req_head: Option<HttpRequest<()>>,
499 head: HttpResponse<()>,
500 body: Body<R>,
501 },
502}
503
504#[derive(Debug, Clone, Copy, Eq, PartialEq)]
506#[must_use]
507pub enum EmbeddedHttpKind {
508 Request,
510 Response,
512}
513
514impl<R> EmbeddedHttp<R> {
515 pub const fn kind(&self) -> EmbeddedHttpKind {
517 match self {
518 Self::Req { .. } => EmbeddedHttpKind::Request,
519 Self::Resp { .. } => EmbeddedHttpKind::Response,
520 }
521 }
522
523 pub const fn respmod_request_head(&self) -> Option<&HttpRequest<()>> {
531 match self {
532 Self::Resp { req_head, .. } => req_head.as_ref(),
533 Self::Req { .. } => None,
534 }
535 }
536}
537
538pub(crate) fn serialize_embedded_http(
550 e: &EmbeddedHttp<Vec<u8>>,
551 body_limit: Option<usize>,
552) -> (Vec<u8>, Option<Vec<u8>>, usize) {
553 #[inline]
554 fn copy_body(reader: &[u8], limit: Option<usize>) -> (Option<Vec<u8>>, usize) {
555 if reader.is_empty() {
556 return (None, 0);
557 }
558 let original_len = reader.len();
559 let end = limit.map_or(original_len, |lim| original_len.min(lim));
560 (Some(reader[..end].to_vec()), original_len)
561 }
562
563 match e {
564 EmbeddedHttp::Req { head, body } => {
565 let head_bytes = serialize_http_request_head(head);
566 let (body_bytes, original_len) = match body {
567 Body::Full { reader } => copy_body(reader, body_limit),
568 Body::Empty | Body::Preview { .. } => (None, 0),
569 };
570 (head_bytes, body_bytes, original_len)
571 }
572 EmbeddedHttp::Resp { head, body, .. } => {
573 let head_bytes = serialize_http_response_head(head);
574 let (body_bytes, original_len) = match body {
575 Body::Full { reader } => copy_body(reader, body_limit),
576 Body::Empty | Body::Preview { .. } => (None, 0),
577 };
578 (head_bytes, body_bytes, original_len)
579 }
580 }
581}
582
583fn serialize_http_request_head(head: &HttpRequest<()>) -> Vec<u8> {
584 let mut out = Vec::with_capacity(256 + head.headers().len() * 32);
585 write!(
586 &mut out,
587 "{} {} {}\r\n",
588 head.method(),
589 head.uri(),
590 crate::protocol::http_version_str(head.version())
591 )
592 .expect("write request line");
593 for (name, value) in head.headers() {
594 out.extend_from_slice(name.as_str().as_bytes());
595 out.extend_from_slice(b": ");
596 out.extend_from_slice(value.as_bytes());
597 out.extend_from_slice(b"\r\n");
598 }
599 out.extend_from_slice(b"\r\n");
600 out
601}
602
603fn serialize_http_response_head(head: &HttpResponse<()>) -> Vec<u8> {
604 let mut out = Vec::with_capacity(256 + head.headers().len() * 32);
605 write!(
606 &mut out,
607 "{} {} {}\r\n",
608 crate::protocol::http_version_str(head.version()),
609 head.status().as_u16(),
610 head.status().canonical_reason().unwrap_or("")
611 )
612 .expect("write status line");
613 for (name, value) in head.headers() {
614 out.extend_from_slice(name.as_str().as_bytes());
615 out.extend_from_slice(b": ");
616 out.extend_from_slice(value.as_bytes());
617 out.extend_from_slice(b"\r\n");
618 }
619 out.extend_from_slice(b"\r\n");
620 out
621}
622
623pub(crate) struct IncomingRequestParts<R> {
624 method: Method,
625 service: String,
626 icap_headers: HeaderMap,
627 embedded: Option<EmbeddedHttp<R>>,
628 preview_size: Option<usize>,
629 allow_204: bool,
630 allow_206: bool,
631 preview_ieof: bool,
632}
633
634impl<R, D: DirectionMeta> Request<R, D> {
635 #[inline]
637 pub const fn method(&self) -> Method {
638 self.method
639 }
640
641 #[inline]
643 pub fn service(&self) -> &str {
644 &self.service
645 }
646
647 #[inline]
649 pub const fn icap_headers(&self) -> &HeaderMap {
650 &self.icap_headers
651 }
652
653 #[inline]
655 pub const fn embedded(&self) -> Option<&EmbeddedHttp<R>> {
656 self.embedded.as_ref()
657 }
658
659 #[inline]
661 pub const fn embedded_mut(&mut self) -> Option<&mut EmbeddedHttp<R>> {
662 self.embedded.as_mut()
663 }
664
665 #[inline]
667 pub fn into_embedded(self) -> Option<EmbeddedHttp<R>> {
668 self.embedded
669 }
670
671 #[inline]
673 pub const fn preview_size(&self) -> Option<usize> {
674 self.preview_size
675 }
676
677 #[inline]
679 pub const fn is_preview_ieof(&self) -> bool {
680 self.preview_ieof
681 }
682
683 #[inline]
685 pub const fn allows_204(&self) -> bool {
686 self.allow_204
687 }
688
689 #[inline]
691 pub const fn allows_206(&self) -> bool {
692 self.allow_206
693 }
694}
695
696impl<R> Request<R, Outbound> {
697 pub fn new(method: Method, service: impl Into<String>) -> Self {
699 Self {
700 method,
701 service: service.into(),
702 icap_headers: HeaderMap::new(),
703 embedded: None,
704 preview_size: None,
705 allow_204: false,
706 allow_206: false,
707 preview_ieof: false,
708 meta: OutboundMeta,
709 }
710 }
711
712 pub fn try_new(method: &str, service: impl Into<String>) -> IcapResult<Self> {
714 Ok(Self::new(Method::parse_token(method)?, service))
715 }
716
717 pub fn options(service: impl Into<String>) -> Self {
719 Self::new(Method::Options, service)
720 }
721
722 pub fn reqmod(service: impl Into<String>) -> Self {
724 Self::new(Method::ReqMod, service)
725 }
726
727 pub fn respmod(service: impl Into<String>) -> Self {
729 Self::new(Method::RespMod, service)
730 }
731
732 pub fn try_icap_header(mut self, name: &str, value: &str) -> IcapResult<Self> {
734 let n: HeaderName = name.parse()?;
735 let v: HeaderValue = HeaderValue::from_str(value)?;
736 self.icap_headers.insert(n, v);
737 Ok(self)
738 }
739
740 pub fn icap_header(self, name: &str, value: &str) -> Self {
747 self.try_icap_header(name, value)
748 .expect("invalid ICAP header name or value")
749 }
750
751 pub const fn preview(mut self, n: usize) -> Self {
756 self.preview_size = Some(n);
757 self
758 }
759
760 pub const fn preview_ieof(mut self) -> Self {
762 self.preview_ieof = true;
763 self
764 }
765
766 pub const fn allow_204(mut self) -> Self {
768 self.allow_204 = true;
769 self
770 }
771
772 pub const fn allow_206(mut self) -> Self {
777 self.allow_206 = true;
778 self
779 }
780
781 #[inline]
783 pub const fn is_mod(&self) -> bool {
784 matches!(self.method, Method::ReqMod | Method::RespMod)
785 }
786
787 #[inline]
789 pub const fn is_options(&self) -> bool {
790 matches!(self.method, Method::Options)
791 }
792
793 #[inline]
795 pub const fn embedded_kind(&self) -> Option<EmbeddedHttpKind> {
796 match &self.embedded {
797 Some(embedded) => Some(embedded.kind()),
798 None => None,
799 }
800 }
801
802 pub fn validate_for_send(&self) -> IcapResult<()> {
807 match (self.method, self.embedded_kind()) {
808 (Method::Options, Some(_)) => {
809 return Err(Error::serialization(
810 "OPTIONS requests must not carry embedded HTTP",
811 ));
812 }
813 (Method::ReqMod, Some(EmbeddedHttpKind::Response)) => {
814 return Err(Error::serialization(
815 "REQMOD requests must carry an embedded HTTP request",
816 ));
817 }
818 (Method::RespMod, Some(EmbeddedHttpKind::Request)) => {
819 return Err(Error::serialization(
820 "RESPMOD requests must carry an embedded HTTP response",
821 ));
822 }
823 _ => {}
824 }
825
826 if self.preview_ieof && self.preview_size != Some(0) {
827 return Err(Error::serialization(
828 "preview_ieof is only valid together with Preview: 0",
829 ));
830 }
831
832 Ok(())
833 }
834}
835
836impl<R> Request<R, Incoming> {
837 #[allow(clippy::missing_const_for_fn)]
838 pub(crate) fn incoming(parts: IncomingRequestParts<R>) -> Self {
839 Self {
840 method: parts.method,
841 service: parts.service,
842 icap_headers: parts.icap_headers,
843 embedded: parts.embedded,
844 preview_size: parts.preview_size,
845 allow_204: parts.allow_204,
846 allow_206: parts.allow_206,
847 preview_ieof: parts.preview_ieof,
848 meta: IncomingMeta::default(),
849 }
850 }
851
852 #[inline]
863 pub const fn chunk_trailers(&self) -> &HeaderMap {
864 &self.meta.chunk_trailers
865 }
866
867 #[inline]
879 pub fn istag(&self) -> Option<&str> {
880 self.meta.istag.as_deref()
881 }
882}
883
884impl Request<Vec<u8>, Outbound> {
886 pub fn with_http_request_head(mut self, head: HttpRequest<()>) -> IcapResult<Self> {
890 if self.method != Method::ReqMod {
891 return Err(Error::serialization(
892 "HTTP request heads can only be attached to REQMOD requests",
893 ));
894 }
895 self.embedded = Some(EmbeddedHttp::Req {
896 head,
897 body: Body::Empty,
898 });
899 Ok(self)
900 }
901
902 pub fn with_http_response_head(mut self, head: HttpResponse<()>) -> IcapResult<Self> {
906 if self.method != Method::RespMod {
907 return Err(Error::serialization(
908 "HTTP response heads can only be attached to RESPMOD requests",
909 ));
910 }
911 self.embedded = Some(EmbeddedHttp::Resp {
912 req_head: None,
913 head,
914 body: Body::Empty,
915 });
916 Ok(self)
917 }
918
919 pub fn with_http_request(mut self, req: HttpRequest<Vec<u8>>) -> IcapResult<Self> {
925 if self.method != Method::ReqMod {
926 return Err(Error::serialization(
927 "HTTP requests can only be attached to REQMOD requests",
928 ));
929 }
930 let (parts, body) = req.into_parts();
931 let head = HttpRequest::from_parts(parts, ());
932 self.embedded = Some(EmbeddedHttp::Req {
933 head,
934 body: Body::Full { reader: body },
935 });
936 Ok(self)
937 }
938
939 pub fn with_http_response(mut self, resp: HttpResponse<Vec<u8>>) -> IcapResult<Self> {
945 if self.method != Method::RespMod {
946 return Err(Error::serialization(
947 "HTTP responses can only be attached to RESPMOD requests",
948 ));
949 }
950 let (parts, body) = resp.into_parts();
951 let head = HttpResponse::from_parts(parts, ());
952 self.embedded = Some(EmbeddedHttp::Resp {
953 req_head: None,
954 head,
955 body: Body::Full { reader: body },
956 });
957 Ok(self)
958 }
959
960 pub fn with_http_response_and_request_context(
967 mut self,
968 resp: HttpResponse<Vec<u8>>,
969 orig_req: HttpRequest<()>,
970 ) -> IcapResult<Self> {
971 if self.method != Method::RespMod {
972 return Err(Error::serialization(
973 "HTTP responses can only be attached to RESPMOD requests",
974 ));
975 }
976 let (parts, body) = resp.into_parts();
977 let head = HttpResponse::from_parts(parts, ());
978 self.embedded = Some(EmbeddedHttp::Resp {
979 req_head: Some(orig_req),
980 head,
981 body: Body::Full { reader: body },
982 });
983 Ok(self)
984 }
985}
986
987#[must_use]
999pub(crate) fn normalize_service_path(raw: &str) -> String {
1000 let s = raw.trim();
1001
1002 if s.is_empty() || s == "*" {
1004 return "/".to_string();
1005 }
1006
1007 let path = strip_icap_scheme(s).map_or(s, |authority_and_path| {
1009 authority_and_path
1010 .find('/')
1011 .map_or("/", |idx| &authority_and_path[idx..])
1012 });
1013
1014 let trimmed = path.trim_matches('/');
1015 if trimmed.is_empty() {
1016 "/".to_string()
1017 } else {
1018 format!("/{trimmed}")
1019 }
1020}
1021
1022fn strip_icap_scheme(s: &str) -> Option<&str> {
1025 for scheme in ["icaps://", "icap://"] {
1026 if let Some(prefix) = s.get(..scheme.len())
1027 && prefix.eq_ignore_ascii_case(scheme)
1028 {
1029 return Some(&s[scheme.len()..]);
1030 }
1031 }
1032 None
1033}
1034
1035pub(crate) fn parse_icap_request(data: &[u8]) -> IcapResult<IncomingRequest<Vec<u8>>> {
1040 parse_icap_request_with_mode(data, RequestParserMode::Strict)
1041}
1042
1043#[derive(Debug, Default, Clone, Copy, Eq, PartialEq)]
1044pub(crate) enum RequestParserMode {
1045 Compatibility,
1046 #[default]
1047 Strict,
1048}
1049
1050pub(crate) fn parse_icap_request_with_mode(
1051 data: &[u8],
1052 mode: RequestParserMode,
1053) -> IcapResult<IncomingRequest<Vec<u8>>> {
1054 trace!("parse_icap_request: len={}", data.len());
1055
1056 let hdr_end =
1057 find_double_crlf(data).ok_or_else(|| Error::parse("ICAP request headers not complete"))?;
1058 let head = &data[..hdr_end];
1059 let head_str = std::str::from_utf8(head)?;
1060
1061 let mut lines = head_str.split("\r\n");
1062 let request_line = lines.next().ok_or_else(|| Error::parse("Empty request"))?;
1063 let mut parts = request_line.split_whitespace();
1064
1065 let method_str = parts
1066 .next()
1067 .ok_or_else(|| Error::parse("Invalid request line"))?;
1068 let method = match method_str.trim().to_ascii_uppercase().as_str() {
1069 "REQMOD" => Method::ReqMod,
1070 "RESPMOD" => Method::RespMod,
1071 "OPTIONS" => Method::Options,
1072 other => {
1073 return Err(Error::invalid_method(format!(
1074 "Unknown ICAP method: {other}"
1075 )));
1076 }
1077 };
1078
1079 let icap_uri = parts
1080 .next()
1081 .ok_or_else(|| Error::parse("Invalid request line"))?;
1082 let version = parts
1083 .next()
1084 .ok_or_else(|| Error::parse("Invalid request line"))?;
1085
1086 if !version.eq_ignore_ascii_case(ICAP_VERSION) {
1087 return Err(Error::invalid_version(version.to_string()));
1088 }
1089
1090 let icap_headers = parse_header_lines(lines)?;
1091
1092 if !icap_headers.contains_key("Host") {
1093 return Err(Error::missing_header("Host"));
1094 }
1095
1096 if (matches!(method, Method::ReqMod | Method::RespMod)
1097 || (mode == RequestParserMode::Strict && method == Method::Options))
1098 && !icap_headers.contains_key("Encapsulated")
1099 {
1100 return Err(Error::missing_header("Encapsulated"));
1101 }
1102
1103 let service = normalize_service_path(icap_uri);
1104
1105 let allow_204 = allow_contains_token(&icap_headers, "204");
1106 let allow_206 = allow_contains_token(&icap_headers, "206");
1107
1108 let preview_size = match icap_headers.get("Preview") {
1111 None => None,
1112 Some(v) => {
1113 let s = v
1114 .to_str()
1115 .map_err(|_| Error::parse("Preview header has non-ASCII value"))?;
1116 let trimmed = s.trim();
1117 let n = trimmed.parse::<usize>().map_err(|_| {
1118 Error::parse(format!("Preview header has invalid integer '{trimmed}'"))
1119 })?;
1120 Some(n)
1121 }
1122 };
1123
1124 let enc_area = &data[hdr_end..];
1126
1127 let enc = match icap_headers.get("Encapsulated") {
1128 Some(v) => {
1129 let raw = v.to_str()?;
1130 crate::protocol::parse_encapsulated_value(raw)?
1131 }
1132 None => crate::protocol::Encapsulated::default(),
1133 };
1134 validate_encapsulated_for_method(method, &enc)?;
1135
1136 let http_hdr_off = match method {
1137 Method::ReqMod => enc.req_hdr,
1138 Method::RespMod => enc.res_hdr,
1139 Method::Options => None,
1140 };
1141
1142 let embedded = if let Some(hdr_off) = http_hdr_off {
1143 let hdr_end_off = next_offset_after(&enc, hdr_off);
1144 let http_region = slice_encapsulated(enc_area, hdr_off, hdr_end_off)?;
1145 if http_region.is_empty() {
1146 return Ok(IncomingRequest::incoming(IncomingRequestParts {
1147 method,
1148 service,
1149 icap_headers,
1150 embedded: None,
1151 preview_size,
1152 allow_204,
1153 allow_206,
1154 preview_ieof: false,
1155 }));
1156 }
1157
1158 let http_hdr_len = find_double_crlf(http_region)
1159 .ok_or_else(|| Error::http_parse("embedded HTTP headers not complete".to_string()))?;
1160 let http_head_bytes = &http_region[..http_hdr_len];
1161 let inline_body = &http_region[http_hdr_len..];
1162
1163 let first_line_end =
1164 memmem::find(http_head_bytes, b"\r\n").unwrap_or(http_head_bytes.len());
1165 let start_bytes = &http_head_bytes[..first_line_end];
1166 let start = std::str::from_utf8(start_bytes)?;
1167
1168 let http_head_str = std::str::from_utf8(http_head_bytes)?;
1169 let mut hlines = http_head_str.split("\r\n");
1170 let _ = hlines.next();
1171
1172 let http_headers = parse_header_lines(hlines)?;
1173
1174 let body_off = match method {
1175 Method::ReqMod => enc.req_body,
1176 Method::RespMod => enc.res_body,
1177 Method::Options => None,
1178 };
1179
1180 let no_body = body_off.is_none() && enc.null_body.is_some();
1181
1182 let body_bytes: Vec<u8> = if no_body {
1183 Vec::new()
1184 } else if let Some(boff) = body_off {
1185 let bend = next_offset_after(&enc, boff);
1187 let body_slice = slice_encapsulated(enc_area, boff, bend)?;
1188
1189 if boff < hdr_off {
1190 return Err(Error::header(
1191 "Encapsulated offsets invalid (body before headers)",
1192 ));
1193 }
1194
1195 body_slice.to_vec()
1196 } else {
1197 inline_body.to_vec()
1198 };
1199
1200 if method == Method::RespMod {
1201 let (version, status) = parse_http_response_start_line(start)?;
1202
1203 let mut builder = HttpResponse::builder().status(status).version(version);
1204
1205 {
1206 let headers_mut = builder
1207 .headers_mut()
1208 .ok_or_else(|| Error::unexpected("response builder: headers_mut is None"))?;
1209 headers_mut.extend(http_headers);
1210 }
1211
1212 let head = builder
1213 .body(())
1214 .map_err(|e| Error::http_parse(format!("build http::Response head: {e}")))?;
1215
1216 let req_head = if let Some(req_hdr_off) = enc.req_hdr {
1219 let req_end = next_offset_after(&enc, req_hdr_off);
1220 let req_region = slice_encapsulated(enc_area, req_hdr_off, req_end)?;
1221 parse_http_request_head_only(req_region)?
1222 } else {
1223 None
1224 };
1225
1226 Some(EmbeddedHttp::Resp {
1227 req_head,
1228 head,
1229 body: Body::Full { reader: body_bytes },
1230 })
1231 } else {
1232 let (http_method, uri, version) = parse_http_request_start_line(start)?;
1233
1234 let mut builder = HttpRequest::builder()
1235 .method(http_method)
1236 .uri(uri)
1237 .version(version);
1238
1239 {
1240 let headers_mut = builder
1241 .headers_mut()
1242 .ok_or_else(|| Error::unexpected("request builder: headers_mut is None"))?;
1243 headers_mut.extend(http_headers);
1244 }
1245
1246 let head = builder
1247 .body(())
1248 .map_err(|e| Error::http_parse(format!("build http::Request head: {e}")))?;
1249
1250 Some(EmbeddedHttp::Req {
1251 head,
1252 body: Body::Full { reader: body_bytes },
1253 })
1254 }
1255 } else {
1256 None
1257 };
1258
1259 Ok(IncomingRequest::incoming(IncomingRequestParts {
1260 method,
1261 service,
1262 icap_headers,
1263 embedded,
1264 preview_size,
1265 allow_204,
1266 allow_206,
1267 preview_ieof: false,
1268 }))
1269}
1270
1271fn parse_http_request_head_only(region: &[u8]) -> IcapResult<Option<HttpRequest<()>>> {
1276 if region.is_empty() {
1277 return Ok(None);
1278 }
1279
1280 let hdr_len = find_double_crlf(region)
1281 .ok_or_else(|| Error::http_parse("req-hdr section: HTTP request headers not complete"))?;
1282 let head_bytes = ®ion[..hdr_len];
1283
1284 let first_line_end = memmem::find(head_bytes, b"\r\n").unwrap_or(head_bytes.len());
1285 let start_str = std::str::from_utf8(&head_bytes[..first_line_end])?;
1286
1287 let head_str = std::str::from_utf8(head_bytes)?;
1288 let http_headers = parse_header_lines(head_str.split("\r\n").skip(1))?;
1289
1290 let (http_method, uri, version) = parse_http_request_start_line(start_str)?;
1291 let mut builder = HttpRequest::builder()
1292 .method(http_method)
1293 .uri(uri)
1294 .version(version);
1295 if let Some(h) = builder.headers_mut() {
1296 h.extend(http_headers);
1297 }
1298 let head = builder
1299 .body(())
1300 .map_err(|e| Error::http_parse(format!("build req-hdr HTTP request head: {e}")))?;
1301 Ok(Some(head))
1302}
1303
1304fn validate_encapsulated_for_method(
1305 method: Method,
1306 enc: &crate::protocol::Encapsulated,
1307) -> IcapResult<()> {
1308 if enc.null_body.is_some()
1309 && (enc.req_body.is_some() || enc.res_body.is_some() || enc.opt_body.is_some())
1310 {
1311 return Err(Error::header(
1312 "Encapsulated null-body must not be combined with body tokens",
1313 ));
1314 }
1315
1316 match method {
1317 Method::ReqMod => {
1318 if enc.res_hdr.is_some() || enc.res_body.is_some() || enc.opt_body.is_some() {
1319 return Err(Error::header(
1320 "REQMOD Encapsulated must not contain response or opt-body parts",
1321 ));
1322 }
1323 if enc.req_body.is_some() && enc.req_hdr.is_none() {
1324 return Err(Error::header(
1325 "REQMOD Encapsulated req-body requires req-hdr",
1326 ));
1327 }
1328 }
1329 Method::RespMod => {
1330 if enc.req_body.is_some() || enc.opt_body.is_some() {
1331 return Err(Error::header(
1332 "RESPMOD Encapsulated must not contain req-body or opt-body parts",
1333 ));
1334 }
1335 if enc.res_body.is_some() && enc.res_hdr.is_none() {
1336 return Err(Error::header(
1337 "RESPMOD Encapsulated res-body requires res-hdr",
1338 ));
1339 }
1340 }
1341 Method::Options => {
1342 if enc.req_hdr.is_some()
1343 || enc.res_hdr.is_some()
1344 || enc.req_body.is_some()
1345 || enc.res_body.is_some()
1346 || enc.opt_body.is_some()
1347 {
1348 return Err(Error::header(
1349 "OPTIONS request Encapsulated must be absent or null-body",
1350 ));
1351 }
1352 }
1353 }
1354
1355 Ok(())
1356}
1357
1358fn next_offset_after(enc: &crate::protocol::Encapsulated, start: usize) -> Option<usize> {
1359 let mut min: Option<usize> = None;
1360
1361 let mut consider = |v: Option<usize>| {
1362 if let Some(o) = v
1363 && o > start
1364 {
1365 min = Some(min.map_or(o, |m| m.min(o)));
1366 }
1367 };
1368
1369 consider(enc.req_hdr);
1370 consider(enc.res_hdr);
1371 consider(enc.req_body);
1372 consider(enc.res_body);
1373 consider(enc.opt_body);
1374 consider(enc.null_body);
1375
1376 min
1377}
1378
1379fn slice_encapsulated(enc_area: &[u8], start: usize, end: Option<usize>) -> IcapResult<&[u8]> {
1380 if start > enc_area.len() {
1381 return Err(Error::header("Encapsulated offset out of bounds"));
1382 }
1383 let end = end.unwrap_or(enc_area.len());
1384 if end > enc_area.len() {
1385 return Err(Error::header("Encapsulated end offset out of bounds"));
1386 }
1387 if end < start {
1388 return Err(Error::header("Encapsulated offsets invalid (end < start)"));
1389 }
1390 Ok(&enc_area[start..end])
1391}
1392
1393#[inline]
1394fn allow_contains_token(headers: &HeaderMap, token: &str) -> bool {
1395 headers
1396 .get("Allow")
1397 .and_then(|v| v.to_str().ok())
1398 .is_some_and(|s| s.split(',').any(|p| p.trim().eq_ignore_ascii_case(token)))
1399}
1400
1401#[cfg(test)]
1402mod tests {
1403 use super::*;
1404 use http::{
1405 HeaderValue, Method as HttpMethod, Request as HttpRequest, Response as HttpResponse,
1406 StatusCode as HttpStatus, Version,
1407 };
1408 use rstest::rstest;
1409
1410 #[inline]
1411 fn icap_bytes(s: &str) -> Vec<u8> {
1412 s.as_bytes().to_vec()
1413 }
1414
1415 #[rstest]
1416 #[case("reqmod", Method::ReqMod)]
1417 #[case("RESPMOD", Method::RespMod)]
1418 #[case(" Options ", Method::Options)]
1419 fn method_from_str_is_case_insensitive(#[case] s: &str, #[case] expected: Method) {
1420 assert_eq!(Method::from(s), expected);
1421 }
1422
1423 #[test]
1424 #[should_panic(expected = "Unknown ICAP method string")]
1425 fn method_from_str_unknown_panics() {
1426 let _ = Method::from("PATCH");
1427 }
1428
1429 #[test]
1430 fn builder_creates_basic_requests() {
1431 let o: Request = Request::options("icap/test");
1432 assert_eq!(o.method, Method::Options);
1433 assert_eq!(o.service, "icap/test");
1434 assert!(!o.is_mod());
1435
1436 let r: Request = Request::reqmod("svc");
1437 assert_eq!(r.method, Method::ReqMod);
1438 assert!(r.is_mod());
1439
1440 let s: Request = Request::respmod("svc");
1441 assert_eq!(s.method, Method::RespMod);
1442 assert!(s.is_mod());
1443 }
1444
1445 #[test]
1446 fn builder_flags_preview_allow() {
1447 let req: Request = Request::reqmod("icap/test")
1448 .allow_204()
1449 .allow_206()
1450 .preview(16)
1451 .preview_ieof();
1452
1453 assert!(req.allow_204);
1454 assert!(req.allow_206);
1455 assert_eq!(req.preview_size, Some(16));
1456 assert!(req.preview_ieof);
1457 }
1458
1459 #[test]
1460 fn preview_zero_does_not_imply_ieof() {
1461 let req: Request = Request::reqmod("svc").preview(0);
1462 assert_eq!(req.preview_size, Some(0));
1463 assert!(!req.preview_ieof);
1464
1465 let req: Request = Request::reqmod("svc").preview(0).preview_ieof();
1466 assert_eq!(req.preview_size, Some(0));
1467 assert!(req.preview_ieof);
1468 }
1469
1470 #[test]
1471 fn builder_sets_and_overrides_headers() {
1472 let req: Request = Request::options("icap/test")
1473 .icap_header("Host", "icap.example.org")
1474 .icap_header("Host", "icap2.example.org");
1475
1476 assert_eq!(
1477 req.icap_headers.get("Host").unwrap(),
1478 &HeaderValue::from_static("icap2.example.org")
1479 );
1480 }
1481
1482 #[test]
1483 fn try_icap_header_rejects_invalid_header_input() {
1484 let err = Request::<Vec<u8>>::options("icap/test")
1485 .try_icap_header("Bad Header", "value")
1486 .expect_err("invalid header name should be rejected");
1487
1488 assert!(matches!(err, Error::Protocol(ProtocolError::HeaderName(_))));
1489 }
1490
1491 #[test]
1492 fn try_new_rejects_unknown_method_without_panic() {
1493 let err = Request::<Vec<u8>>::try_new("PATCH", "svc")
1494 .expect_err("unknown method should be rejected");
1495
1496 assert!(matches!(
1497 err,
1498 Error::Protocol(ProtocolError::InvalidField {
1499 field: ProtocolField::Method,
1500 ..
1501 })
1502 ));
1503 }
1504
1505 #[test]
1506 fn parse_reqmod_body_uses_req_body_offset() {
1507 let http = b"GET / HTTP/1.1\r\nHost: ex\r\n\r\n";
1508 let body = b"12345678";
1509
1510 let req_body_off = http.len(); let raw = format!(
1513 "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1514Host: icap.example.org\r\n\
1515Encapsulated: req-hdr=0, req-body={req_body_off}\r\n\
1516\r\n",
1517 )
1518 .into_bytes();
1519
1520 let mut bytes = raw;
1521 bytes.extend_from_slice(http);
1522 bytes.extend_from_slice(body);
1523
1524 let r = parse_icap_request(&bytes).expect("parse");
1525 match r.embedded {
1526 Some(EmbeddedHttp::Req {
1527 body: Body::Full { reader },
1528 ..
1529 }) => {
1530 assert_eq!(reader, body);
1531 }
1532 _ => panic!("expected embedded req with full body"),
1533 }
1534 }
1535
1536 #[test]
1537 fn builder_embeds_http_req_and_resp() {
1538 let http_req = HttpRequest::builder()
1539 .method("POST")
1540 .uri("/x")
1541 .version(Version::HTTP_11)
1542 .body(Vec::<u8>::new())
1543 .unwrap();
1544
1545 let req: Request = Request::reqmod("svc").with_http_request(http_req).unwrap();
1546 assert!(matches!(req.embedded, Some(EmbeddedHttp::Req { .. })));
1547
1548 let http_resp = HttpResponse::builder()
1549 .status(HttpStatus::OK)
1550 .version(Version::HTTP_11)
1551 .body(Vec::<u8>::new())
1552 .unwrap();
1553
1554 let req2: Request = Request::respmod("svc")
1555 .with_http_response(http_resp)
1556 .unwrap();
1557 assert!(matches!(req2.embedded, Some(EmbeddedHttp::Resp { .. })));
1558 }
1559
1560 #[test]
1561 fn builder_embeds_http_heads_without_buffered_body() {
1562 let req_head = HttpRequest::builder()
1563 .method("POST")
1564 .uri("/x")
1565 .version(Version::HTTP_11)
1566 .body(())
1567 .unwrap();
1568 let req: Request = Request::reqmod("svc")
1569 .with_http_request_head(req_head)
1570 .unwrap();
1571 assert!(matches!(
1572 req.embedded,
1573 Some(EmbeddedHttp::Req {
1574 body: Body::Empty,
1575 ..
1576 })
1577 ));
1578
1579 let resp_head = HttpResponse::builder()
1580 .status(HttpStatus::OK)
1581 .version(Version::HTTP_11)
1582 .body(())
1583 .unwrap();
1584 let req2: Request = Request::respmod("svc")
1585 .with_http_response_head(resp_head)
1586 .unwrap();
1587 assert!(matches!(
1588 req2.embedded,
1589 Some(EmbeddedHttp::Resp {
1590 body: Body::Empty,
1591 ..
1592 })
1593 ));
1594 }
1595
1596 #[test]
1597 fn validated_embedded_builders_reject_method_mismatch() {
1598 let http_req = HttpRequest::builder()
1599 .method("GET")
1600 .uri("/x")
1601 .version(Version::HTTP_11)
1602 .body(Vec::<u8>::new())
1603 .unwrap();
1604
1605 let err = Request::respmod("svc")
1606 .with_http_request(http_req)
1607 .expect_err("RESPMOD must reject embedded HTTP requests");
1608
1609 assert!(matches!(
1610 err,
1611 Error::Protocol(ProtocolError::Serialization(_))
1612 ));
1613
1614 let http_resp = HttpResponse::builder()
1615 .status(HttpStatus::OK)
1616 .version(Version::HTTP_11)
1617 .body(Vec::<u8>::new())
1618 .unwrap();
1619
1620 let err = Request::reqmod("svc")
1621 .with_http_response(http_resp)
1622 .expect_err("REQMOD must reject embedded HTTP responses");
1623
1624 assert!(matches!(
1625 err,
1626 Error::Protocol(ProtocolError::Serialization(_))
1627 ));
1628 }
1629
1630 #[test]
1631 fn validate_for_send_rejects_incoherent_request_shapes() {
1632 let req: Request = Request::reqmod("svc").preview(16).preview_ieof();
1633 let err = req
1634 .validate_for_send()
1635 .expect_err("ieof is only valid with Preview: 0");
1636 assert!(matches!(
1637 err,
1638 Error::Protocol(ProtocolError::Serialization(_))
1639 ));
1640 }
1641
1642 #[test]
1645 fn version_must_be_icap_1_0_in_request() {
1646 let raw = b"REQMOD icap://h/s ICAP/2.0\r\n\
1647 Host: h\r\n\
1648 Encapsulated: req-hdr=0\r\n\
1649 \r\n";
1650 let err = parse_icap_request(raw).unwrap_err();
1651 assert!(
1652 matches!(err, Error::Protocol(ProtocolError::InvalidField { field: ProtocolField::Version, value: v, .. }) if v == "ICAP/2.0")
1653 );
1654 }
1655
1656 #[rstest]
1659 #[case(
1660 "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\nHost: icap.example.org\r\nEncapsulated: req-hdr=0\r\n\r\n",
1661 "/icap/test"
1662 )]
1663 #[case(
1664 "RESPMOD icap://icap.example.org/respmod ICAP/1.0\r\nHost: icap.example.org\r\nEncapsulated: res-hdr=0\r\n\r\n",
1665 "/respmod"
1666 )]
1667 #[case(
1668 "REQMOD icap://icap.example.org/v1/scan/ ICAP/1.0\r\nHost: icap.example.org\r\nEncapsulated: req-hdr=0\r\n\r\n",
1669 "/v1/scan"
1670 )]
1671 fn service_is_full_path_of_icap_uri(#[case] wire: &str, #[case] expected_service: &str) {
1672 let r = parse_icap_request(&icap_bytes(wire)).expect("parse");
1673 assert_eq!(r.service, expected_service);
1674 }
1675
1676 #[rstest]
1678 #[case("scan", "/scan")]
1679 #[case("/scan", "/scan")]
1680 #[case("/v1/scan", "/v1/scan")]
1681 #[case("v1/scan", "/v1/scan")]
1682 #[case("/v1/scan/", "/v1/scan")]
1683 #[case("icap://host:1344/v1/scan", "/v1/scan")]
1684 #[case("ICAP://host:1344/v1/scan", "/v1/scan")]
1685 #[case("icaps://host:1344/secure/scan", "/secure/scan")]
1686 #[case("icap://host:1344/", "/")]
1687 #[case("icap://host:1344", "/")]
1688 #[case("*", "/")]
1689 #[case("", "/")]
1690 #[case("/", "/")]
1691 fn normalize_service_path_cases(#[case] raw: &str, #[case] expected: &str) {
1692 assert_eq!(normalize_service_path(raw), expected);
1693 }
1694
1695 #[test]
1696 fn headers_are_case_insensitive_allow_parsed_with_whitespace() {
1697 let raw = icap_bytes(
1698 "REQMOD icap://h/s ICAP/1.0\r\n\
1699 host: icap.example.org\r\n\
1700 aLlOw: 206, 204 \r\n\
1701 Encapsulated: req-hdr=0\r\n\
1702 \r\n",
1703 );
1704 let r = parse_icap_request(&raw).expect("parse");
1705 assert!(r.allow_204);
1706 assert!(r.allow_206);
1707 assert_eq!(
1708 r.icap_headers.get("Host").unwrap(),
1709 &HeaderValue::from_static("icap.example.org")
1710 );
1711 }
1712
1713 #[test]
1714 fn parse_ignores_malformed_header_line_without_colon() {
1715 let raw = icap_bytes(
1716 "OPTIONS icap://icap.example.org/icap/test ICAP/1.0\r\n\
1717 Host: icap.example.org\r\n\
1718 ThisIsBadHeader\r\n\
1719 Encapsulated: null-body=0\r\n\
1720 \r\n",
1721 );
1722 let r = parse_icap_request(&raw).expect("parse");
1723 assert_eq!(
1724 r.icap_headers.get("Host").unwrap(),
1725 &HeaderValue::from_static("icap.example.org")
1726 );
1727 }
1728
1729 #[test]
1730 fn host_header_is_required() {
1731 let raw = b"REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1732 Encapsulated: req-hdr=0\r\n\
1733 \r\n";
1734 let err = parse_icap_request(raw).unwrap_err();
1735 let m = err.to_string().to_lowercase();
1736 assert!(m.contains("host"), "expected missing Host error; got: {m}");
1737 }
1738
1739 #[test]
1740 fn encapsulated_is_required_for_request() {
1741 let raw_req = b"REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1742 Host: icap.example.org\r\n\
1743 \r\n";
1744 let err1 = parse_icap_request(raw_req).unwrap_err();
1745 assert!(
1746 matches!(err1, Error::Protocol(ProtocolError::MissingHeader(h)) if h == "Encapsulated"),
1747 "expected MissingHeader(Encapsulated); got: {err1}"
1748 );
1749
1750 let raw_resp = b"RESPMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1751 Host: icap.example.org\r\n\
1752 \r\n";
1753 let err2 = parse_icap_request(raw_resp).unwrap_err();
1754 assert!(
1755 matches!(err2, Error::Protocol(ProtocolError::MissingHeader(h)) if h == "Encapsulated"),
1756 "expected MissingHeader(Encapsulated); got: {err2}"
1757 );
1758 }
1759
1760 #[test]
1761 fn invalid_encapsulated_token_is_rejected() {
1762 let raw = icap_bytes(
1763 "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1764 Host: icap.example.org\r\n\
1765 Encapsulated: req-hdr=0, bad-token=10\r\n\
1766 \r\n",
1767 );
1768 let err = parse_icap_request(&raw).unwrap_err();
1769 let m = err.to_string().to_lowercase();
1770 assert!(
1771 m.contains("encapsulated") || m.contains("invalid"),
1772 "expected Encapsulated parse error, got: {m}"
1773 );
1774 }
1775
1776 #[test]
1777 fn duplicate_encapsulated_part_is_rejected() {
1778 let raw = icap_bytes(
1779 "RESPMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1780 Host: icap.example.org\r\n\
1781 Encapsulated: res-hdr=0, res-hdr=10\r\n\
1782 \r\n",
1783 );
1784 let err = parse_icap_request(&raw).unwrap_err();
1785 let m = err.to_string().to_lowercase();
1786 assert!(
1787 m.contains("duplicate") && m.contains("encapsulated"),
1788 "expected duplicate Encapsulated part error, got: {m}"
1789 );
1790 }
1791
1792 #[test]
1793 fn reqmod_rejects_response_oriented_encapsulated_parts() {
1794 let raw = icap_bytes(
1795 "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1796 Host: icap.example.org\r\n\
1797 Encapsulated: res-hdr=0\r\n\
1798 \r\n\
1799 HTTP/1.1 200 OK\r\n\
1800 Content-Length: 0\r\n\
1801 \r\n",
1802 );
1803 let err = parse_icap_request(&raw).unwrap_err();
1804 let m = err.to_string().to_lowercase();
1805 assert!(
1806 m.contains("reqmod") && m.contains("encapsulated"),
1807 "expected REQMOD Encapsulated validation error, got: {m}"
1808 );
1809 }
1810
1811 #[test]
1812 fn respmod_rejects_request_body_encapsulated_part() {
1813 let raw = icap_bytes(
1814 "RESPMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1815 Host: icap.example.org\r\n\
1816 Encapsulated: req-hdr=0, req-body=30\r\n\
1817 \r\n\
1818 GET / HTTP/1.1\r\n\
1819 Host: example.com\r\n\
1820 \r\n",
1821 );
1822 let err = parse_icap_request(&raw).unwrap_err();
1823 let m = err.to_string().to_lowercase();
1824 assert!(
1825 m.contains("respmod") && m.contains("encapsulated"),
1826 "expected RESPMOD Encapsulated validation error, got: {m}"
1827 );
1828 }
1829
1830 #[test]
1831 fn null_body_must_not_be_combined_with_body_tokens() {
1832 let raw = icap_bytes(
1833 "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1834 Host: icap.example.org\r\n\
1835 Encapsulated: req-hdr=0, req-body=30, null-body=34\r\n\
1836 \r\n\
1837 GET / HTTP/1.1\r\n\
1838 Host: example.com\r\n\
1839 \r\n\
1840 body",
1841 );
1842 let err = parse_icap_request(&raw).unwrap_err();
1843 let m = err.to_string().to_lowercase();
1844 assert!(
1845 m.contains("null-body") && m.contains("body"),
1846 "expected null-body/body validation error, got: {m}"
1847 );
1848 }
1849
1850 #[test]
1851 fn options_request_rejects_embedded_http_parts() {
1852 let raw = icap_bytes(
1853 "OPTIONS icap://icap.example.org/icap/test ICAP/1.0\r\n\
1854 Host: icap.example.org\r\n\
1855 Encapsulated: req-hdr=0\r\n\
1856 \r\n",
1857 );
1858 let err = parse_icap_request(&raw).unwrap_err();
1859 let m = err.to_string().to_lowercase();
1860 assert!(
1861 m.contains("options") && m.contains("encapsulated"),
1862 "expected OPTIONS Encapsulated validation error, got: {m}"
1863 );
1864 }
1865
1866 #[test]
1867 fn compatibility_mode_accepts_options_without_encapsulated() {
1868 let raw = icap_bytes(
1869 "OPTIONS icap://icap.example.org/icap/test ICAP/1.0\r\n\
1870 Host: icap.example.org\r\n\
1871 \r\n",
1872 );
1873 let r = parse_icap_request_with_mode(&raw, RequestParserMode::Compatibility)
1874 .expect("compatibility mode keeps OPTIONS lenient");
1875 assert_eq!(r.method, Method::Options);
1876 }
1877
1878 #[test]
1879 fn default_parser_requires_encapsulated_for_options() {
1880 let raw = icap_bytes(
1881 "OPTIONS icap://icap.example.org/icap/test ICAP/1.0\r\n\
1882 Host: icap.example.org\r\n\
1883 \r\n",
1884 );
1885 let err = parse_icap_request(&raw).unwrap_err();
1886 assert!(
1887 matches!(err, Error::Protocol(ProtocolError::MissingHeader(h)) if h == "Encapsulated"),
1888 "expected strict MissingHeader(Encapsulated), got: {err}"
1889 );
1890 }
1891
1892 #[test]
1893 fn default_parser_accepts_options_with_null_body() {
1894 let raw = icap_bytes(
1895 "OPTIONS icap://icap.example.org/icap/test ICAP/1.0\r\n\
1896 Host: icap.example.org\r\n\
1897 Encapsulated: null-body=0\r\n\
1898 \r\n",
1899 );
1900 let r = parse_icap_request(&raw).expect("strict parse");
1901 assert_eq!(r.method, Method::Options);
1902 }
1903
1904 #[test]
1905 fn parse_reqmod_with_allow_and_preview() {
1906 let raw = icap_bytes(
1907 "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1908 Host: icap.example.org\r\n\
1909 Encapsulated: req-hdr=0\r\n\
1910 Allow: 204, 206\r\n\
1911 Preview: 128\r\n\
1912 \r\n",
1913 );
1914 let r = parse_icap_request(&raw).expect("parse");
1915 assert_eq!(r.method, Method::ReqMod);
1916 assert!(r.allow_204);
1917 assert!(r.allow_206);
1918 assert_eq!(r.preview_size, Some(128));
1919 }
1920
1921 #[test]
1922 fn parse_preview_not_a_number_is_rejected() {
1923 let raw = icap_bytes(
1926 "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1927 Host: icap.example.org\r\n\
1928 Preview: notanumber\r\n\
1929 Encapsulated: req-hdr=0\r\n\
1930 \r\n",
1931 );
1932 let err = parse_icap_request(&raw).expect_err("malformed Preview must error");
1933 let msg = err.to_string();
1934 assert!(
1935 msg.contains("Preview"),
1936 "error should mention Preview, got: {msg}"
1937 );
1938 }
1939
1940 #[test]
1943 fn parse_reqmod_with_embedded_http_request_and_body() {
1944 let raw = icap_bytes(
1945 "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1946 Host: icap.example.org\r\n\
1947 Encapsulated: req-hdr=0\r\n\
1948 \r\n\
1949 GET / HTTP/1.1\r\n\
1950 Host: example.com\r\n\
1951 \r\n\
1952 body...",
1953 );
1954 let r = parse_icap_request(&raw).expect("parse");
1955 match r.embedded {
1956 Some(EmbeddedHttp::Req { ref head, ref body }) => {
1957 assert_eq!(head.method(), &HttpMethod::GET);
1958 assert_eq!(head.uri(), "/");
1959 assert_eq!(
1960 head.headers().get("Host").unwrap(),
1961 &HeaderValue::from_static("example.com")
1962 );
1963 match body {
1964 Body::Full { reader } => assert_eq!(reader, b"body..."),
1965 _ => panic!("expected Full body"),
1966 }
1967 }
1968 _ => panic!("expected embedded HTTP request"),
1969 }
1970 }
1971
1972 #[test]
1973 fn rejects_incomplete_embedded_http_headers() {
1974 let raw = icap_bytes(
1975 "REQMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1976 Host: icap.example.org\r\n\
1977 Encapsulated: req-hdr=0\r\n\
1978 \r\n\
1979 GET / HTTP/1.1\r\n\
1980 Host: example.com\r\n",
1981 );
1982
1983 let err = parse_icap_request(&raw).unwrap_err();
1984 assert!(
1985 matches!(err, Error::Protocol(ProtocolError::HttpParse(_))),
1986 "expected embedded HTTP parse error, got: {err}"
1987 );
1988 }
1989
1990 #[test]
1991 fn rejects_respmod_with_http_request_start_line() {
1992 let raw = icap_bytes(
1993 "RESPMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
1994 Host: icap.example.org\r\n\
1995 Encapsulated: res-hdr=0\r\n\
1996 \r\n\
1997 GET / HTTP/1.1\r\n\
1998 Host: example.com\r\n\
1999 \r\n",
2000 );
2001
2002 let err = parse_icap_request(&raw).unwrap_err();
2003 assert!(
2004 matches!(err, Error::Protocol(ProtocolError::HttpParse(_))),
2005 "expected embedded HTTP status-line parse error, got: {err}"
2006 );
2007 }
2008
2009 #[test]
2010 fn rejects_respmod_with_invalid_status_code() {
2011 let raw = icap_bytes(
2012 "RESPMOD icap://icap.example.org/icap/test ICAP/1.0\r\n\
2013 Host: icap.example.org\r\n\
2014 Encapsulated: res-hdr=0\r\n\
2015 \r\n\
2016 HTTP/1.1 nope OK\r\n\
2017 Content-Length: 0\r\n\
2018 \r\n",
2019 );
2020
2021 let err = parse_icap_request(&raw).unwrap_err();
2022 assert!(
2023 matches!(err, Error::Protocol(ProtocolError::HttpParse(_))),
2024 "expected embedded HTTP status code parse error, got: {err}"
2025 );
2026 }
2027
2028 #[test]
2029 fn parse_minimal_options_with_null_body() {
2030 let raw = icap_bytes(
2031 "OPTIONS icap://icap.example.org/icap/test ICAP/1.0\r\n\
2032 Host: icap.example.org\r\n\
2033 Encapsulated: null-body=0\r\n\
2034 \r\n",
2035 );
2036 let r = parse_icap_request(&raw).expect("parse");
2037 assert_eq!(r.method, Method::Options);
2038 assert_eq!(r.service, "/icap/test");
2039 assert_eq!(
2040 r.icap_headers.get("Host").unwrap(),
2041 &HeaderValue::from_static("icap.example.org")
2042 );
2043 assert!(r.embedded.is_none());
2044 assert!(!r.allow_204);
2045 assert!(!r.allow_206);
2046 assert_eq!(r.preview_size, None);
2047 }
2048}