1pub mod builder;
19pub mod options_cache;
20pub mod timeouts;
21
22#[cfg(test)]
23use crate::error::ProtocolError;
24use crate::error::{Error, IcapResult, TimeoutError, TimeoutKind};
25use crate::protocol::{
26 canon_icap_header, find_double_crlf, parse_encapsulated_header, read_chunked_to_end,
27 write_chunk, write_chunk_into,
28};
29use crate::request::{Request, normalize_service_path, serialize_embedded_http};
30use crate::response::{ParsedResponse, parse_icap_response};
31
32use crate::Method;
33#[cfg(feature = "tls-rustls")]
34use crate::tls::client::ClientTlsConnector;
35
36use http::HeaderMap;
37use std::collections::HashSet;
38use std::io::Write as _;
39use std::path::Path;
40use std::sync::Arc;
41use std::time::Duration;
42
43use crate::client::builder::{ClientBuilder, ConnectionPolicy, ProxyAuth};
44use crate::client::options_cache::{CachedOptions, OptionsCache, TransferAction};
45use crate::client::timeouts::ClientTimeouts;
46use crate::net::Conn;
47use tokio::fs::File as TokioFile;
48use tokio::io::{AsyncRead, AsyncReadExt, AsyncWrite, AsyncWriteExt};
49use tokio::net::TcpStream;
50use tokio::sync::Mutex;
51use tokio::time::timeout;
52use tracing::trace;
53
54#[derive(Debug, Clone)]
61#[must_use]
62pub struct Client {
63 inner: Arc<ClientRef>,
64}
65
66#[derive(Debug)]
67struct ClientRef {
68 host: String,
69 port: u16,
70 host_override: Option<String>,
71 default_headers: HeaderMap,
72 connection_policy: ConnectionPolicy,
73 timeouts: ClientTimeouts,
74 max_response_header_bytes: usize,
75 #[cfg(feature = "tls-rustls")]
76 tls: Option<ClientTlsConnector>,
77 idle_conn: Mutex<Option<Conn>>,
78 options_cache: Option<OptionsCache>,
79 proxy_auth: Option<ProxyAuth>,
80}
81
82impl Client {
83 pub fn builder() -> ClientBuilder {
84 ClientBuilder::default()
85 }
86
87 pub fn get_request(&self, req: &Request) -> IcapResult<Vec<u8>> {
92 let built = self.build_icap_request_bytes(
93 req,
94 EffectivePreview::Inherit,
95 None,
96 false,
97 req.preview_ieof,
98 true,
99 )?;
100 Ok(built.bytes)
101 }
102
103 pub async fn send(&self, req: &Request) -> IcapResult<ParsedResponse> {
110 if self.inner.options_cache.is_some() && req.is_mod() {
111 self.ensure_options_cached(req).await;
112 }
113
114 let effective_preview = self.resolve_effective_preview(req).await;
116 if matches!(effective_preview, EffectivePreview::Skip) {
117 return synthetic_204();
119 }
120
121 let response = Box::pin(Self::with_timeout_as(
122 self.inner.timeouts.operation,
123 self.send_inner(req, effective_preview, None),
124 Error::client_total_timeout,
125 ))
126 .await?;
127
128 let response = if response.status_code() == http::StatusCode::PROXY_AUTHENTICATION_REQUIRED
130 {
131 if let Some(auth) = &self.inner.proxy_auth {
132 let auth_value = basic_auth_value(&auth.username, &auth.password);
133 Box::pin(Self::with_timeout_as(
134 self.inner.timeouts.operation,
135 self.send_inner(req, effective_preview, Some(auth_value.as_str())),
136 Error::client_total_timeout,
137 ))
138 .await?
139 } else {
140 response
141 }
142 } else {
143 response
144 };
145
146 if let Some(cache) = &self.inner.options_cache
147 && req.is_mod()
148 {
149 let path = normalize_service_path(&req.service);
150 let observed = response.get_header("ISTag").and_then(|v| v.to_str().ok());
151 cache
152 .reconcile_istag(&self.inner.host, self.inner.port, &path, observed)
153 .await;
154 }
155
156 Ok(response)
157 }
158
159 pub async fn invalidate_options_cache(&self) {
165 if let Some(cache) = &self.inner.options_cache {
166 cache.clear().await;
167 }
168 }
169
170 async fn ensure_options_cached(&self, req: &Request) {
176 let Some(cache) = &self.inner.options_cache else {
177 return;
178 };
179 let path = normalize_service_path(&req.service);
180 if cache
181 .has_fresh(&self.inner.host, self.inner.port, &path)
182 .await
183 {
184 return;
185 }
186 let options_req = Request::options(path.as_str());
187 let response = match Box::pin(self.send(&options_req)).await {
188 Ok(r) => r,
189 Err(err) => {
190 tracing::warn!(
193 host = %self.inner.host,
194 port = self.inner.port,
195 path = %path,
196 error = %err,
197 "OPTIONS fetch failed; Transfer-* policy will not apply for this request"
198 );
199 return;
200 }
201 };
202 if !response.is_success() {
203 tracing::warn!(
204 host = %self.inner.host,
205 port = self.inner.port,
206 path = %path,
207 status = %response.status_code(),
208 "OPTIONS returned non-2xx; Transfer-* policy will not apply for this request"
209 );
210 return;
211 }
212 if let Some(entry) = CachedOptions::from_response(&response, cache.config()) {
213 cache
214 .store(&self.inner.host, self.inner.port, &path, entry)
215 .await;
216 }
217 }
218
219 async fn resolve_effective_preview(&self, req: &Request) -> EffectivePreview {
227 let Some(cache) = self.inner.options_cache.as_ref() else {
228 return EffectivePreview::Inherit;
229 };
230 if !req.is_mod() {
231 return EffectivePreview::Inherit;
232 }
233 let path = normalize_service_path(&req.service);
234 let ext = file_ext_from_request(req);
235 match cache
236 .resolve_transfer(&self.inner.host, self.inner.port, &path, &ext)
237 .await
238 {
239 Some(TransferAction::Skip) => EffectivePreview::Skip,
240 Some(TransferAction::Full) => EffectivePreview::FullBody,
241 Some(TransferAction::Preview(n)) => EffectivePreview::Preview(n),
242 None => EffectivePreview::Inherit,
243 }
244 }
245
246 async fn send_inner(
253 &self,
254 req: &Request,
255 effective_preview: EffectivePreview,
256 proxy_auth_value: Option<&str>,
257 ) -> IcapResult<ParsedResponse> {
258 trace!(
259 "client.send: method={}, service={}",
260 req.method, req.service
261 );
262
263 let (mut stream, early) = self.acquire_conn().await?;
264 if let Some(resp) = early {
265 return Ok(resp);
266 }
267
268 let built = self.build_icap_request_bytes(
269 req,
270 effective_preview,
271 proxy_auth_value,
272 false,
273 req.preview_ieof,
274 false,
275 )?;
276 if let Err(write_err) = self.write_all(&mut stream, &built.bytes).await {
277 if matches!(
278 write_err,
279 Error::Timeout(TimeoutError {
280 kind: TimeoutKind::ClientWrite,
281 ..
282 })
283 ) {
284 return Err(write_err);
285 }
286 if let Ok((_code, hdr_buf)) =
287 read_icap_headers(&mut stream, self.inner.max_response_header_bytes).await
288 && let Ok(response_buf) = self
289 .read_response_buffer_with_headers(&mut stream, hdr_buf)
290 .await
291 {
292 return self.finalize_response(stream, response_buf).await;
293 }
294 return Err(write_err);
295 }
296
297 if let Err(flush_err) = self.flush(&mut stream).await {
298 if matches!(
299 flush_err,
300 Error::Timeout(TimeoutError {
301 kind: TimeoutKind::ClientWrite,
302 ..
303 })
304 ) {
305 return Err(flush_err);
306 }
307 if let Ok((_code, hdr_buf)) =
308 read_icap_headers(&mut stream, self.inner.max_response_header_bytes).await
309 && let Ok(response_buf) = self
310 .read_response_buffer_with_headers(&mut stream, hdr_buf)
311 .await
312 {
313 return self.finalize_response(stream, response_buf).await;
314 }
315 return Err(flush_err);
316 }
317
318 if req.method == Method::Options {
320 let buf = self.read_response_buffer(&mut stream).await?;
321 return self.finalize_response(stream, buf).await;
322 }
323
324 if built.expect_continue {
326 let (code, hdr_buf) = Self::with_timeout_as(
327 self.continue_timeout(),
328 read_icap_headers(&mut stream, self.inner.max_response_header_bytes),
329 Error::client_continue_timeout,
330 )
331 .await?;
332
333 if code == 100 {
334 if let Some(rest) = built.remaining_body
336 && !rest.is_empty()
337 {
338 self.write_chunk(&mut stream, &rest).await?;
339 }
340 self.write_all(&mut stream, b"0\r\n\r\n").await?;
341 self.flush(&mut stream).await?;
342
343 let response_buf = self.read_response_buffer(&mut stream).await?;
345 return self.finalize_response(stream, response_buf).await;
346 }
347 let response_buf = self
349 .read_response_buffer_with_headers(&mut stream, hdr_buf)
350 .await?;
351 return self.finalize_response(stream, response_buf).await;
352 }
353
354 let response_buf = self.read_response_buffer(&mut stream).await?;
356 self.finalize_response(stream, response_buf).await
357 }
358
359 pub async fn send_streaming<P: AsRef<Path>>(
361 &self,
362 req: &Request,
363 file_path: P,
364 ) -> IcapResult<ParsedResponse> {
365 let file = TokioFile::open(file_path).await?;
366 Box::pin(self.send_streaming_reader(req, file)).await
367 }
368
369 pub async fn send_raw(&self, raw: &[u8]) -> IcapResult<ParsedResponse> {
416 let fut = async {
417 let (mut stream, early) = self.acquire_conn().await?;
418 if let Some(resp) = early {
419 return Ok(resp);
420 }
421 self.write_all(&mut stream, raw).await?;
422 self.flush(&mut stream).await?;
423 let response_buf = self.read_response_buffer(&mut stream).await?;
424 self.finalize_response(stream, response_buf).await
425 };
426 Box::pin(Self::with_timeout_as(
427 self.inner.timeouts.operation,
428 fut,
429 Error::client_total_timeout,
430 ))
431 .await
432 }
433
434 pub async fn send_raw_str(&self, raw: &str) -> IcapResult<ParsedResponse> {
438 self.send_raw(raw.as_bytes()).await
439 }
440
441 pub async fn send_streaming_reader<R>(
447 &self,
448 req: &Request,
449 mut reader: R,
450 ) -> IcapResult<ParsedResponse>
451 where
452 R: AsyncRead + Unpin + Send,
453 {
454 Box::pin(Self::with_timeout_as(
455 self.inner.timeouts.operation,
456 self.send_streaming_reader_inner(req, &mut reader),
457 Error::client_total_timeout,
458 ))
459 .await
460 }
461
462 async fn send_streaming_reader_inner<R>(
463 &self,
464 req: &Request,
465 reader: &mut R,
466 ) -> IcapResult<ParsedResponse>
467 where
468 R: AsyncRead + Unpin + Send,
469 {
470 trace!(
471 "client.send_streaming_reader: method={} service={}",
472 req.method, req.service
473 );
474
475 let (mut stream, early) = self.acquire_conn().await?;
476 if let Some(resp) = early {
477 return Ok(resp);
478 }
479
480 let built = self.build_icap_request_bytes(
481 req,
482 EffectivePreview::Inherit,
483 None,
484 true,
485 false,
486 false,
487 )?;
488 self.write_all(&mut stream, &built.bytes).await?;
489
490 match req.preview_size {
491 None => {
492 write_reader_chunks(&mut stream, reader, self.inner.timeouts.write).await?;
493 self.write_all(&mut stream, b"0\r\n\r\n").await?;
494 self.flush(&mut stream).await?;
495
496 let response_buf = self.read_response_buffer(&mut stream).await?;
497 return self.finalize_response(stream, response_buf).await;
498 }
499 Some(0) => {
500 if req.preview_ieof {
501 self.write_all(&mut stream, b"0; ieof\r\n\r\n").await?;
502 } else {
503 self.write_all(&mut stream, b"0\r\n\r\n").await?;
504 }
505 }
506 Some(preview_size) => {
507 let (preview, eof) = read_preview_bytes(reader, preview_size).await?;
508 if !preview.is_empty() {
509 self.write_chunk(&mut stream, &preview).await?;
510 }
511 if eof {
512 self.write_all(&mut stream, b"0; ieof\r\n\r\n").await?;
513 } else {
514 self.write_all(&mut stream, b"0\r\n\r\n").await?;
515 }
516 }
517 }
518 self.flush(&mut stream).await?;
519
520 let (code, hdr_buf) = Self::with_timeout_as(
522 self.continue_timeout(),
523 read_icap_headers(&mut stream, self.inner.max_response_header_bytes),
524 Error::client_continue_timeout,
525 )
526 .await?;
527
528 if code == 100 {
529 write_reader_chunks(&mut stream, reader, self.inner.timeouts.write).await?;
530 self.write_all(&mut stream, b"0\r\n\r\n").await?;
531 self.flush(&mut stream).await?;
532
533 let response_buf = self.read_response_buffer(&mut stream).await?;
535 self.finalize_response(stream, response_buf).await
536 } else {
537 let response_buf = self
539 .read_response_buffer_with_headers(&mut stream, hdr_buf)
540 .await?;
541 self.finalize_response(stream, response_buf).await
542 }
543 }
544
545 pub fn get_request_wire(&self, req: &Request, streaming: bool) -> IcapResult<Vec<u8>> {
551 let built = self.build_icap_request_bytes(
552 req,
553 EffectivePreview::Inherit,
554 None,
555 streaming || matches!(req.preview_size, Some(0)),
556 req.preview_ieof,
557 true,
558 )?;
559 let mut out = built.bytes;
560 if req.is_mod()
561 && matches!(req.preview_size, Some(0))
562 && !request_has_non_empty_buffered_body(req)
563 {
564 if req.preview_ieof {
565 out.extend_from_slice(b"0; ieof\r\n\r\n");
566 } else {
567 out.extend_from_slice(b"0\r\n\r\n");
568 }
569 }
570 Ok(out)
571 }
572
573 async fn with_timeout_as<T, F>(
574 dur: Option<Duration>,
575 fut: F,
576 timeout_error: fn(Duration) -> Error,
577 ) -> Result<T, Error>
578 where
579 F: std::future::Future<Output = Result<T, Error>>,
580 {
581 if let Some(timeout_duration) = dur {
582 timeout(timeout_duration, fut)
583 .await
584 .unwrap_or_else(|_| Err(timeout_error(timeout_duration)))
585 } else {
586 fut.await
587 }
588 }
589
590 async fn with_io_timeout<T, F>(
591 dur: Option<Duration>,
592 fut: F,
593 timeout_error: fn(Duration) -> Error,
594 ) -> IcapResult<T>
595 where
596 F: std::future::Future<Output = std::io::Result<T>>,
597 {
598 Self::with_timeout_as(dur, async { fut.await.map_err(Error::Io) }, timeout_error).await
599 }
600
601 async fn write_all(&self, stream: &mut Conn, bytes: &[u8]) -> IcapResult<()> {
602 Self::with_io_timeout(
603 self.inner.timeouts.write,
604 AsyncWriteExt::write_all(stream, bytes),
605 Error::client_write_timeout,
606 )
607 .await
608 }
609
610 async fn flush(&self, stream: &mut Conn) -> IcapResult<()> {
611 Self::with_io_timeout(
612 self.inner.timeouts.write,
613 AsyncWriteExt::flush(stream),
614 Error::client_write_timeout,
615 )
616 .await
617 }
618
619 async fn write_chunk(&self, stream: &mut Conn, bytes: &[u8]) -> IcapResult<()> {
620 Self::with_timeout_as(
621 self.inner.timeouts.write,
622 write_chunk(stream, bytes),
623 Error::client_write_timeout,
624 )
625 .await
626 }
627
628 fn continue_timeout(&self) -> Option<Duration> {
629 self.inner.timeouts.continue_after_preview
630 }
631
632 async fn read_response_buffer(&self, stream: &mut Conn) -> IcapResult<Vec<u8>> {
633 let (_code, mut response_buf) =
634 read_icap_headers(stream, self.inner.max_response_header_bytes).await?;
635 read_icap_body_if_any(stream, &mut response_buf).await?;
636 Ok(response_buf)
637 }
638
639 async fn read_response_buffer_with_headers(
640 &self,
641 stream: &mut Conn,
642 mut response_buf: Vec<u8>,
643 ) -> IcapResult<Vec<u8>> {
644 read_icap_body_if_any(stream, &mut response_buf).await?;
645 Ok(response_buf)
646 }
647
648 async fn finalize_response(
649 &self,
650 stream: Conn,
651 response_buf: Vec<u8>,
652 ) -> IcapResult<ParsedResponse> {
653 let resp = parse_icap_response(&response_buf)?;
654 let can_reuse = !response_wants_close(&resp);
655 maybe_put_back(
656 self.inner.connection_policy,
657 &self.inner.idle_conn,
658 stream,
659 can_reuse,
660 )
661 .await;
662 Ok(resp)
663 }
664
665 fn build_icap_request_bytes(
666 &self,
667 req: &Request,
668 effective_preview: EffectivePreview,
669 proxy_auth_value: Option<&str>,
670 force_has_body: bool,
671 preview0_ieof: bool,
672 limit_body_to_preview: bool,
673 ) -> IcapResult<BuiltIcap> {
674 req.validate_for_send()?;
675
676 trace!(
677 "build_icap_request_bytes: method={} service={} preview={:?} allow_204={} allow_206={} force_has_body={} preview0_ieof={}",
678 req.method,
679 req.service,
680 req.preview_size,
681 req.allow_204,
682 req.allow_206,
683 force_has_body,
684 preview0_ieof
685 );
686
687 let mut out = Vec::with_capacity(512);
688
689 let service_path = normalize_service_path(&req.service);
692 write!(
693 &mut out,
694 "{} icap://{}:{}{} ICAP/1.0\r\n",
695 req.method, self.inner.host, self.inner.port, service_path
696 )
697 .map_err(Error::Io)?;
698
699 let host_value = self
701 .inner
702 .host_override
703 .clone()
704 .unwrap_or_else(|| self.inner.host.clone());
705
706 let body_limit = limit_body_to_preview.then_some(req.preview_size).flatten();
711
712 let (http_headers_bytes, http_body_bytes, enc_head_key, enc_body_key, original_body_len) =
713 if req.is_mod() {
714 req.embedded.as_ref().map_or_else(
715 || (Vec::new(), None, None, None, 0usize),
716 |emb| {
717 let (hdrs, body_from_emb, orig_len) =
718 serialize_embedded_http(emb, body_limit);
719 let hdr_len = hdrs.len();
720 let (hdr_key, body_key) = match req.method.as_str() {
721 "REQMOD" => ("req-hdr", "req-body"),
722 _ => ("res-hdr", "res-body"),
723 };
724 let will_send_body = force_has_body || body_from_emb.is_some();
725 if will_send_body && !hdrs.is_empty() {
726 (
727 hdrs,
728 body_from_emb,
729 Some((hdr_key, hdr_len)),
730 Some(body_key),
731 orig_len,
732 )
733 } else if !hdrs.is_empty() {
734 (hdrs, None, Some((hdr_key, hdr_len)), None, orig_len)
735 } else {
736 (hdrs, None, None, None, orig_len)
737 }
738 },
739 )
740 } else {
741 (Vec::new(), None, None, None, 0usize)
742 };
743
744 let preview_for_wire = match effective_preview {
745 EffectivePreview::Inherit => req.preview_size,
746 EffectivePreview::FullBody => None,
747 EffectivePreview::Preview(n) => Some(n),
748 EffectivePreview::Skip => unreachable!("Skip must be handled before send_inner"),
749 };
750
751 write_icap_headers(
753 &mut out,
754 &self.inner.default_headers,
755 &req.icap_headers,
756 &host_value,
757 req.allow_204,
758 req.allow_206,
759 preview_for_wire,
760 matches!(self.inner.connection_policy, ConnectionPolicy::Close),
761 );
762 if let Some(auth) = proxy_auth_value {
764 write!(&mut out, "Proxy-Authorization: {auth}\r\n").map_err(Error::Io)?;
765 }
766 if let Some((hdr_key, hdr_len)) = enc_head_key {
768 if let Some(body_key) = enc_body_key {
769 write!(
770 &mut out,
771 "Encapsulated: {hdr_key}=0, {body_key}={hdr_len}\r\n"
772 )
773 .map_err(Error::Io)?;
774 } else {
775 write!(
776 &mut out,
777 "Encapsulated: {hdr_key}=0, null-body={hdr_len}\r\n"
778 )
779 .map_err(Error::Io)?;
780 }
781 } else {
782 out.extend_from_slice(b"Encapsulated: null-body=0\r\n");
783 }
784 out.extend_from_slice(b"\r\n");
785
786 if !http_headers_bytes.is_empty() {
788 out.extend_from_slice(&http_headers_bytes);
789 }
790
791 if req.is_mod()
793 && let Some(body_now) = http_body_bytes
794 {
795 let (bytes, expect_continue, remaining) = build_preview_and_chunks(
796 preview_for_wire,
797 body_now,
798 preview0_ieof,
799 original_body_len,
800 );
801 out.extend_from_slice(&bytes);
802 return Ok(BuiltIcap {
803 bytes: out,
804 expect_continue,
805 remaining_body: remaining,
806 });
807 }
808
809 Ok(BuiltIcap {
810 bytes: out,
811 expect_continue: false,
812 remaining_body: None,
813 })
814 }
815
816 async fn acquire_conn(&self) -> IcapResult<(Conn, Option<ParsedResponse>)> {
823 let mut stream = match self.inner.connection_policy {
824 ConnectionPolicy::KeepAlive => {
825 let idle = self.inner.idle_conn.lock().await.take();
826 if let Some(s) = idle {
827 s
828 } else {
829 self.inner.connect().await?
830 }
831 }
832 ConnectionPolicy::Close => self.inner.connect().await?,
833 };
834
835 if let Some(inner) = stream.plain_mut()
838 && let Some(resp) =
839 Self::try_read_early_response_now(inner, self.inner.max_response_header_bytes)
840 .await?
841 {
842 return Ok((stream, Some(resp)));
843 }
844
845 Ok((stream, None))
846 }
847
848 async fn try_read_early_response_now(
863 stream: &mut TcpStream,
864 max_response_header_bytes: usize,
865 ) -> IcapResult<Option<ParsedResponse>> {
866 let mut buf = Vec::new();
867 let mut tmp = [0u8; 4096];
868
869 loop {
870 match stream.try_read(&mut tmp) {
871 Ok(0) => {
872 return Ok(None);
873 }
874 Ok(n) => {
875 buf.extend_from_slice(&tmp[..n]);
876 if let Some(header_len) = find_double_crlf(&buf) {
877 check_icap_response_header_limit(header_len, max_response_header_bytes)?;
878 let _ = read_icap_body_if_any(stream, &mut buf).await;
879 return parse_icap_response(&buf).map(Some);
880 }
881 check_icap_response_header_limit(buf.len(), max_response_header_bytes)?;
882 }
883 Err(ref e) if e.kind() == std::io::ErrorKind::WouldBlock => {
884 return Ok(None);
885 }
886 Err(e) => return Err(e.into()),
887 }
888 }
889 }
890}
891
892const fn request_has_non_empty_buffered_body(req: &Request) -> bool {
893 match &req.embedded {
894 Some(
895 crate::request::EmbeddedHttp::Req {
896 body: crate::request::Body::Full { reader },
897 ..
898 }
899 | crate::request::EmbeddedHttp::Resp {
900 body: crate::request::Body::Full { reader },
901 ..
902 },
903 ) => !reader.is_empty(),
904 _ => false,
905 }
906}
907
908impl ClientRef {
909 async fn connect(&self) -> IcapResult<Conn> {
910 let tcp = Client::with_timeout_as(
911 self.timeouts.connect,
912 async {
913 TcpStream::connect((&*self.host, self.port))
914 .await
915 .map_err(Error::Io)
916 },
917 Error::client_connect_timeout,
918 )
919 .await?;
920
921 #[cfg(feature = "tls-rustls")]
922 if let Some(tls) = &self.tls {
923 let fallback = self.host_override.as_deref().unwrap_or(&self.host);
925 let sni = tls.resolve_sni(fallback).to_string();
926 let stream = tls.connect(tcp, &sni).await?;
927 return Ok(Conn::Rustls { inner: stream });
928 }
929
930 Ok(Conn::Plain { inner: tcp })
931 }
932}
933
934async fn maybe_put_back(
935 policy: ConnectionPolicy,
936 slot: &Mutex<Option<Conn>>,
937 stream: Conn,
938 can_reuse: bool,
939) {
940 if matches!(policy, ConnectionPolicy::KeepAlive) && can_reuse {
941 *slot.lock().await = Some(stream);
942 }
943}
944
945#[derive(Debug, Clone)]
946struct BuiltIcap {
947 bytes: Vec<u8>,
948 expect_continue: bool,
949 remaining_body: Option<Vec<u8>>,
950}
951
952#[derive(Debug, Clone, Copy, PartialEq, Eq)]
953enum EffectivePreview {
954 Inherit,
955 Skip,
956 FullBody,
957 Preview(usize),
958}
959
960async fn read_until_len<S: AsyncRead + AsyncWrite + Unpin>(
961 stream: &mut S,
962 buf: &mut Vec<u8>,
963 need: usize,
964) -> IcapResult<()> {
965 let mut tmp = [0u8; 4096];
966 while buf.len() < need {
967 let n = AsyncReadExt::read(stream, &mut tmp).await?;
968 if n == 0 {
969 return Err(Error::body("Unexpected EOF while reading response body"));
970 }
971 buf.extend_from_slice(&tmp[..n]);
972 }
973 Ok(())
974}
975
976async fn read_until_double_crlf_from<S: AsyncRead + AsyncWrite + Unpin>(
977 stream: &mut S,
978 buf: &mut Vec<u8>,
979 start: usize,
980) -> IcapResult<usize> {
981 let mut tmp = [0u8; 4096];
982 loop {
983 if start <= buf.len()
984 && let Some(pos) = memchr::memmem::find(&buf[start..], b"\r\n\r\n")
985 {
986 return Ok(start + pos + 4);
987 }
988
989 let n = AsyncReadExt::read(stream, &mut tmp).await?;
990 if n == 0 {
991 return Err(Error::body(
992 "Unexpected EOF while reading encapsulated HTTP headers",
993 ));
994 }
995 buf.extend_from_slice(&tmp[..n]);
996 }
997}
998
999async fn write_reader_chunks<S, R>(
1000 stream: &mut S,
1001 reader: &mut R,
1002 write_timeout: Option<Duration>,
1003) -> IcapResult<()>
1004where
1005 S: AsyncWrite + Unpin,
1006 R: AsyncRead + Unpin,
1007{
1008 let mut buf = vec![0u8; 64 * 1024];
1009 loop {
1010 let n = reader.read(&mut buf).await?;
1011 if n == 0 {
1012 return Ok(());
1013 }
1014 Client::with_timeout_as(
1015 write_timeout,
1016 write_chunk(stream, &buf[..n]),
1017 Error::client_write_timeout,
1018 )
1019 .await?;
1020 }
1021}
1022
1023async fn read_preview_bytes<R>(reader: &mut R, preview_size: usize) -> IcapResult<(Vec<u8>, bool)>
1024where
1025 R: AsyncRead + Unpin,
1026{
1027 let mut preview = vec![0u8; preview_size];
1028 let mut filled = 0;
1029 while filled < preview_size {
1030 let n = reader.read(&mut preview[filled..]).await?;
1031 if n == 0 {
1032 preview.truncate(filled);
1033 return Ok((preview, true));
1034 }
1035 filled += n;
1036 }
1037 Ok((preview, false))
1038}
1039
1040async fn read_icap_body_if_any<S>(stream: &mut S, buf: &mut Vec<u8>) -> IcapResult<()>
1043where
1044 S: AsyncRead + AsyncWrite + Unpin,
1045{
1046 let Some(h_end) = find_double_crlf(buf) else {
1047 return Err(Error::parse("Corrupted ICAP headers"));
1048 };
1049
1050 let hdr_text = std::str::from_utf8(&buf[..h_end])
1051 .map_err(|_| Error::http_parse("Invalid headers utf8"))?;
1052 let enc = parse_encapsulated_header(hdr_text)?;
1053
1054 if let Some(body_rel) = enc.req_body.or(enc.res_body).or(enc.opt_body) {
1055 let body_abs = h_end + body_rel;
1056 if buf.len() < body_abs {
1057 read_until_len(stream, buf, body_abs).await?;
1058 }
1059 let _ = read_chunked_to_end(stream, buf, body_abs).await?;
1060 } else if let Some(hdr_rel) = enc.req_hdr.or(enc.res_hdr) {
1061 let hdr_abs = h_end + hdr_rel;
1062 if buf.len() < hdr_abs {
1063 read_until_len(stream, buf, hdr_abs).await?;
1064 }
1065 let _ = read_until_double_crlf_from(stream, buf, hdr_abs).await?;
1066 }
1067
1068 Ok(())
1069}
1070
1071fn file_ext_from_request(req: &Request) -> String {
1086 match req.embedded() {
1087 Some(crate::request::EmbeddedHttp::Req { head, .. }) => {
1088 let path = head.uri().path();
1089 path.rsplit('.')
1090 .next()
1091 .filter(|e| !e.is_empty() && !e.contains('/'))
1092 .map(str::to_lowercase)
1093 .unwrap_or_default()
1094 }
1095 Some(crate::request::EmbeddedHttp::Resp { head, .. }) => {
1096 let content_type = head
1097 .headers()
1098 .get(http::header::CONTENT_TYPE)
1099 .and_then(|v| v.to_str().ok())
1100 .unwrap_or("");
1101 ext_from_content_type(content_type).to_string()
1102 }
1103 None => String::new(),
1104 }
1105}
1106
1107fn ext_from_content_type(content_type: &str) -> &'static str {
1113 let base = content_type.split(';').next().unwrap_or("").trim();
1114 match base {
1115 "text/html" => "html",
1116 "text/plain" => "txt",
1117 "text/css" => "css",
1118 "text/javascript" | "application/javascript" => "js",
1119 "application/json" => "json",
1120 "application/xml" | "text/xml" => "xml",
1121 "application/pdf" => "pdf",
1122 "application/zip" | "application/x-zip-compressed" => "zip",
1123 "application/gzip" | "application/x-gzip" => "gz",
1124 "application/x-tar" => "tar",
1125 "application/x-rar-compressed" | "application/vnd.rar" => "rar",
1126 "application/x-7z-compressed" => "7z",
1127 "application/vnd.ms-excel"
1128 | "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" => "xlsx",
1129 "application/msword"
1130 | "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => "docx",
1131 "image/jpeg" => "jpg",
1132 "image/png" => "png",
1133 "image/gif" => "gif",
1134 "image/webp" => "webp",
1135 "image/svg+xml" => "svg",
1136 "audio/mpeg" => "mp3",
1137 "audio/ogg" => "ogg",
1138 "video/mp4" => "mp4",
1139 "video/webm" => "webm",
1140 _ => "",
1141 }
1142}
1143
1144fn synthetic_204() -> IcapResult<ParsedResponse> {
1149 const BYTES: &[u8] =
1150 b"ICAP/1.0 204 No Content\r\nISTag: \"bypass\"\r\nEncapsulated: null-body=0\r\n\r\n";
1151 ParsedResponse::from_raw(BYTES)
1152 .map_err(|_| crate::error::Error::unexpected("failed to build synthetic 204"))
1153}
1154
1155fn basic_auth_value(username: &str, password: &str) -> String {
1161 let credentials = format!("{username}:{password}");
1162 format!("Basic {}", base64_encode(credentials.as_bytes()))
1163}
1164
1165fn base64_encode(input: &[u8]) -> String {
1167 const TABLE: &[u8; 64] = b"ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789+/";
1168 let mut out = Vec::with_capacity(input.len().div_ceil(3) * 4);
1169 for chunk in input.chunks(3) {
1170 let b = [
1171 chunk[0],
1172 chunk.get(1).copied().unwrap_or(0),
1173 chunk.get(2).copied().unwrap_or(0),
1174 ];
1175 let n = (u32::from(b[0]) << 16) | (u32::from(b[1]) << 8) | u32::from(b[2]);
1176 out.push(TABLE[(n >> 18) as usize]);
1177 out.push(TABLE[((n >> 12) & 0x3f) as usize]);
1178 out.push(if chunk.len() > 1 {
1179 TABLE[((n >> 6) & 0x3f) as usize]
1180 } else {
1181 b'='
1182 });
1183 out.push(if chunk.len() > 2 {
1184 TABLE[(n & 0x3f) as usize]
1185 } else {
1186 b'='
1187 });
1188 }
1189 String::from_utf8(out).unwrap_or_default()
1191}
1192
1193fn parse_authority_with_scheme(uri: &str) -> IcapResult<(String, u16, bool)> {
1196 let s = uri.trim();
1197 let (tls, rest) = if let Some(r) = s.strip_prefix("icaps://") {
1198 (true, r)
1199 } else if let Some(r) = s.strip_prefix("icap://") {
1200 (false, r)
1201 } else {
1202 return Err(Error::invalid_uri(
1203 "URI must start with icap:// or icaps://",
1204 ));
1205 };
1206
1207 let authority = rest.split('/').next().unwrap_or(rest);
1208 let (host, port) = if let Some(i) = authority.rfind(':') {
1209 let h = &authority[..i];
1210 let p: u16 = authority[i + 1..]
1211 .parse()
1212 .map_err(|_| Error::invalid_uri("Invalid port"))?;
1213 (h.to_string(), p)
1214 } else {
1215 (authority.to_string(), if tls { 11344 } else { 1344 })
1216 };
1217
1218 if host.is_empty() {
1219 return Err(Error::invalid_uri("Empty host in authority"));
1220 }
1221 Ok((host, port, tls))
1222}
1223
1224#[cfg(test)]
1225fn append_to_allow(headers: &mut HeaderMap, code: &str) {
1226 use http::{HeaderName, HeaderValue};
1227
1228 let name = HeaderName::from_static("allow");
1229 match headers.get_mut(&name) {
1230 Some(v) => {
1231 let mut s = v.to_str().unwrap_or("").to_string();
1232 if !s.split(',').any(|p| p.trim() == code) {
1233 if !s.is_empty() {
1234 s.push_str(", ");
1235 }
1236 s.push_str(code);
1237 *v = HeaderValue::from_str(&s).unwrap();
1238 }
1239 }
1240 None => {
1241 headers.insert(name, HeaderValue::from_str(code).unwrap());
1242 }
1243 }
1244}
1245
1246#[inline]
1247const fn is_virtual_icap_header(name: &str) -> bool {
1248 name.eq_ignore_ascii_case("host")
1249 || name.eq_ignore_ascii_case("encapsulated")
1250 || name.eq_ignore_ascii_case("allow")
1251 || name.eq_ignore_ascii_case("preview")
1252 || name.eq_ignore_ascii_case("connection")
1253}
1254
1255#[allow(clippy::too_many_arguments)]
1256fn write_icap_headers(
1257 out: &mut Vec<u8>,
1258 default_headers: &HeaderMap,
1259 req_headers: &HeaderMap,
1260 host_value: &str,
1261 allow_204: bool,
1262 allow_206: bool,
1263 preview_size: Option<usize>,
1264 connection_close: bool,
1265) {
1266 out.extend_from_slice(canon_icap_header("host").as_bytes());
1268 out.extend_from_slice(b": ");
1269 if let Some(v) = req_headers.get("Host") {
1270 out.extend_from_slice(v.as_bytes());
1271 } else {
1272 out.extend_from_slice(host_value.as_bytes());
1273 }
1274 out.extend_from_slice(b"\r\n");
1275
1276 if connection_close {
1278 out.extend_from_slice(b"Connection: close\r\n");
1279 }
1280
1281 let req_names: HashSet<&str> = req_headers.keys().map(http::HeaderName::as_str).collect();
1282
1283 for (name, value) in default_headers {
1285 let key = name.as_str();
1286 if is_virtual_icap_header(key) || req_names.contains(key) {
1287 continue;
1288 }
1289 let cname = canon_icap_header(key);
1290 out.extend_from_slice(cname.as_bytes());
1291 out.extend_from_slice(b": ");
1292 out.extend_from_slice(value.as_bytes());
1293 out.extend_from_slice(b"\r\n");
1294 }
1295
1296 for (name, value) in req_headers {
1298 let key = name.as_str();
1299 if is_virtual_icap_header(key) {
1300 continue;
1301 }
1302 let cname = canon_icap_header(key);
1303 out.extend_from_slice(cname.as_bytes());
1304 out.extend_from_slice(b": ");
1305 out.extend_from_slice(value.as_bytes());
1306 out.extend_from_slice(b"\r\n");
1307 }
1308
1309 if let Some(mut allow) = req_headers
1311 .get("Allow")
1312 .or_else(|| default_headers.get("Allow"))
1313 .and_then(|v| v.to_str().ok())
1314 .map(|s| s.trim().to_string())
1315 {
1316 if allow_204
1317 && !allow
1318 .split(',')
1319 .any(|p| p.trim().eq_ignore_ascii_case("204"))
1320 {
1321 if !allow.is_empty() {
1322 allow.push_str(", ");
1323 }
1324 allow.push_str("204");
1325 }
1326 if allow_206
1327 && !allow
1328 .split(',')
1329 .any(|p| p.trim().eq_ignore_ascii_case("206"))
1330 {
1331 if !allow.is_empty() {
1332 allow.push_str(", ");
1333 }
1334 allow.push_str("206");
1335 }
1336 if !allow.is_empty() {
1337 out.extend_from_slice(canon_icap_header("allow").as_bytes());
1338 out.extend_from_slice(b": ");
1339 out.extend_from_slice(allow.as_bytes());
1340 out.extend_from_slice(b"\r\n");
1341 }
1342 } else if allow_204 || allow_206 {
1343 let allow = match (allow_204, allow_206) {
1344 (true, true) => "204, 206",
1345 (true, false) => "204",
1346 (false, true) => "206",
1347 (false, false) => "",
1348 };
1349 if !allow.is_empty() {
1350 out.extend_from_slice(canon_icap_header("allow").as_bytes());
1351 out.extend_from_slice(b": ");
1352 out.extend_from_slice(allow.as_bytes());
1353 out.extend_from_slice(b"\r\n");
1354 }
1355 }
1356
1357 if let Some(ps) = preview_size {
1358 out.extend_from_slice(canon_icap_header("preview").as_bytes());
1359 out.extend_from_slice(b": ");
1360 out.extend_from_slice(ps.to_string().as_bytes());
1361 out.extend_from_slice(b"\r\n");
1362 } else if let Some(v) = req_headers
1363 .get("Preview")
1364 .or_else(|| default_headers.get("Preview"))
1365 {
1366 out.extend_from_slice(canon_icap_header("preview").as_bytes());
1367 out.extend_from_slice(b": ");
1368 out.extend_from_slice(v.as_bytes());
1369 out.extend_from_slice(b"\r\n");
1370 }
1371}
1372
1373fn response_wants_close(resp: &ParsedResponse) -> bool {
1374 let Some(v) = resp.headers().get(http::header::CONNECTION) else {
1375 return false;
1376 };
1377
1378 let Ok(s) = v.to_str() else {
1379 return true;
1380 };
1381
1382 s.split(',').any(|t| t.trim().eq_ignore_ascii_case("close"))
1383}
1384
1385const CRLFCRLF: u32 = 0x0D0A_0D0A;
1386
1387fn parse_status_code_from_status_line(line: &[u8]) -> Option<u16> {
1388 let sp1 = line.iter().position(|&b| b == b' ')?;
1389 let mut i = sp1;
1390 while i < line.len() && line[i] == b' ' {
1391 i += 1;
1392 }
1393 if i + 3 > line.len() {
1394 return None;
1395 }
1396 let d0 = line[i];
1397 let d1 = line[i + 1];
1398 let d2 = line[i + 2];
1399 if !d0.is_ascii_digit() || !d1.is_ascii_digit() || !d2.is_ascii_digit() {
1400 return None;
1401 }
1402 Some(u16::from(d0 - b'0') * 100 + u16::from(d1 - b'0') * 10 + u16::from(d2 - b'0'))
1403}
1404
1405fn check_icap_response_header_limit(size: usize, max: usize) -> IcapResult<()> {
1406 if size > max {
1407 return Err(Error::header(format!(
1408 "ICAP response headers too large: {size} bytes (max {max})"
1409 )));
1410 }
1411 Ok(())
1412}
1413
1414async fn read_icap_headers<S>(stream: &mut S, max_header_bytes: usize) -> IcapResult<(u16, Vec<u8>)>
1415where
1416 S: AsyncRead + AsyncWrite + Unpin,
1417{
1418 let mut buf = Vec::new();
1419 let mut tmp = [0u8; 4096];
1420
1421 let mut status_line_end: Option<usize> = None;
1422 let mut hdr_end: Option<usize> = None;
1423
1424 let mut code: Option<u16> = None;
1425
1426 let mut win: u32 = 0;
1427 let mut prev: Option<u8> = None;
1428
1429 loop {
1430 let n = AsyncReadExt::read(stream, &mut tmp)
1431 .await
1432 .map_err(Error::Io)?;
1433
1434 if n == 0 {
1435 if buf.is_empty() {
1437 return Err(Error::Protocol(crate::error::ProtocolError::EarlyClose));
1438 }
1439
1440 if let Some(c) = code
1441 && (400..=599).contains(&c)
1442 && hdr_end.is_none()
1443 {
1444 if !buf.ends_with(b"\r\n\r\n") {
1445 if buf.ends_with(b"\r\n") {
1446 buf.extend_from_slice(b"\r\n");
1447 } else {
1448 buf.extend_from_slice(b"\r\n\r\n");
1449 }
1450 }
1451 return Ok((c, buf));
1452 }
1453
1454 if hdr_end.is_none() && status_line_end.is_some() {
1455 if !buf.ends_with(b"\r\n\r\n") {
1456 if buf.ends_with(b"\r\n") {
1457 buf.extend_from_slice(b"\r\n");
1458 } else {
1459 buf.extend_from_slice(b"\r\n\r\n");
1460 }
1461 }
1462 return Err(Error::Protocol(crate::error::ProtocolError::EarlyClose));
1463 }
1464 } else {
1465 let old_len = buf.len();
1466 buf.extend_from_slice(&tmp[..n]);
1467
1468 for (j, &b) in tmp[..n].iter().enumerate() {
1469 let i = old_len + j;
1470
1471 if status_line_end.is_none() && prev == Some(b'\r') && b == b'\n' {
1472 status_line_end = Some(i - 1);
1473 let line = &buf[..(i - 1)];
1474 if let Some(c) = parse_status_code_from_status_line(line) {
1475 code = Some(c);
1476 }
1477 }
1478
1479 win = (win << 8) | u32::from(b);
1480 if hdr_end.is_none() && win == CRLFCRLF {
1481 hdr_end = Some(i + 1);
1482 }
1483
1484 prev = Some(b);
1485 }
1486 }
1487
1488 if code.is_none()
1489 && let Some(end) = status_line_end
1490 {
1491 let line = &buf[..end];
1492 if let Some(c) = parse_status_code_from_status_line(line) {
1493 code = Some(c);
1494 }
1495 }
1496
1497 if let Some(header_len) = hdr_end {
1522 check_icap_response_header_limit(header_len, max_header_bytes)?;
1523 let c = code.ok_or_else(|| Error::parse("missing/bad status code"))?;
1524 return Ok((c, buf));
1525 }
1526
1527 check_icap_response_header_limit(buf.len(), max_header_bytes)?;
1528 }
1529}
1530
1531fn build_preview_and_chunks(
1540 preview_size: Option<usize>,
1541 body: Vec<u8>,
1542 preview0_ieof: bool,
1543 original_body_len: usize,
1544) -> (Vec<u8>, bool, Option<Vec<u8>>) {
1545 let mut out = Vec::new();
1546 match preview_size {
1547 None => {
1548 if !body.is_empty() {
1549 write_chunk_into(&mut out, &body);
1550 }
1551 out.extend_from_slice(b"0\r\n\r\n");
1552 (out, false, None)
1553 }
1554 Some(0) => {
1555 let has_body = original_body_len > 0;
1558 if has_body {
1559 out.extend_from_slice(b"0\r\n\r\n");
1560 (out, true, Some(body))
1561 } else if preview0_ieof {
1562 out.extend_from_slice(b"0; ieof\r\n\r\n");
1563 (out, false, None)
1564 } else {
1565 out.extend_from_slice(b"0\r\n\r\n");
1566 (out, true, Some(Vec::new()))
1567 }
1568 }
1569 Some(ps) => {
1570 let send_n = original_body_len.min(ps);
1572 let actual_send = body.len().min(send_n);
1574 if actual_send > 0 {
1575 write_chunk_into(&mut out, &body[..actual_send]);
1576 }
1577 let rest = original_body_len.saturating_sub(send_n);
1579 if rest == 0 {
1580 out.extend_from_slice(b"0; ieof\r\n\r\n");
1581 (out, false, None)
1582 } else {
1583 out.extend_from_slice(b"0\r\n\r\n");
1584 (out, true, Some(body[actual_send..].to_vec()))
1587 }
1588 }
1589 }
1590}
1591
1592#[cfg(test)]
1593mod tests {
1594 use super::*;
1595 use crate::protocol::find_double_crlf;
1596 use http::{Request as HttpReq, Version, header};
1597 use rstest::{fixture, rstest};
1598 use std::future;
1599 use tokio::net::{TcpListener, TcpStream};
1600 use tokio::time::{Duration, timeout};
1601
1602 fn bytes_to_string_prefix(v: &[u8], n: usize) -> String {
1603 String::from_utf8_lossy(&v[..v.len().min(n)]).to_string()
1604 }
1605
1606 fn extract_headers_text(wire: &[u8]) -> String {
1607 let end = find_double_crlf(wire).expect("headers terminator not found");
1608 String::from_utf8_lossy(&wire[..end]).to_string()
1609 }
1610
1611 fn find_header_line(hdrs: &str, name_ci: &str) -> Option<String> {
1612 let needle = format!("{}:", name_ci.to_ascii_lowercase());
1613 hdrs.lines()
1614 .find(|l| l.to_ascii_lowercase().starts_with(&needle))
1615 .map(std::string::ToString::to_string)
1616 }
1617
1618 #[fixture]
1619 fn client() -> Client {
1620 Client::builder()
1621 .host("icap.example")
1622 .port(1344)
1623 .default_header("x-trace-id", "test-123")
1624 .unwrap()
1625 .keep_alive(true)
1626 .build()
1627 }
1628
1629 async fn connect_pair() -> (TcpStream, TcpStream) {
1630 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1631 let addr = listener.local_addr().unwrap();
1632
1633 let client = TcpStream::connect(addr).await.unwrap();
1634 let (server, _) = listener.accept().await.unwrap();
1635 (client, server)
1636 }
1637
1638 async fn read_until_contains(stream: &mut TcpStream, needle: &[u8]) -> Vec<u8> {
1639 let mut buf = Vec::new();
1640 let mut tmp = [0u8; 1024];
1641 loop {
1642 if memchr::memmem::find(&buf, needle).is_some() {
1643 return buf;
1644 }
1645 let n = stream.read(&mut tmp).await.unwrap();
1646 assert!(n > 0, "connection closed before expected bytes arrived");
1647 buf.extend_from_slice(&tmp[..n]);
1648 }
1649 }
1650
1651 fn streaming_req(preview: Option<usize>) -> Request {
1652 let http = HttpReq::builder()
1653 .method("POST")
1654 .uri("/scan")
1655 .version(Version::HTTP_11)
1656 .header(header::HOST, "app")
1657 .header(header::CONTENT_LENGTH, "7")
1658 .body(())
1659 .unwrap();
1660
1661 let req = Request::reqmod("scan")
1662 .with_http_request_head(http)
1663 .unwrap();
1664 if let Some(preview_size) = preview {
1665 req.preview(preview_size)
1666 } else {
1667 req
1668 }
1669 }
1670
1671 async fn spawn_raw_icap_server<F, Fut>(handler: F) -> u16
1672 where
1673 F: FnOnce(TcpStream) -> Fut + Send + 'static,
1674 Fut: std::future::Future<Output = ()> + Send + 'static,
1675 {
1676 let listener = TcpListener::bind("127.0.0.1:0").await.unwrap();
1677 let port = listener.local_addr().unwrap().port();
1678 tokio::spawn(async move {
1679 let (stream, _) = listener.accept().await.unwrap();
1680 handler(stream).await;
1681 });
1682 port
1683 }
1684
1685 async fn server_write(server: TcpStream, bytes: &[u8], keep_open: bool) {
1686 use tokio::io::AsyncWriteExt;
1687 let mut s = server;
1688 if !bytes.is_empty() {
1689 s.write_all(bytes).await.unwrap();
1690 let _ = s.flush().await;
1691 }
1692 if keep_open {
1693 let () = future::pending::<()>().await;
1694 }
1695 }
1696
1697 #[rstest]
1698 #[case("icap://proxy.local/service", Ok(("proxy.local".to_string(), 1344, false)))]
1699 #[case("icap://proxy.local:1345/respmod", Ok(("proxy.local".to_string(), 1345, false)))]
1700 #[case("icaps://proxy.local/service",
1701 Ok(("proxy.local".to_string(), 11344, true))
1702 )] #[case("icaps://proxy.local:2346/svc", Ok(("proxy.local".to_string(), 2346, true)))]
1704 #[case("http://wrong", Err(()))]
1705 #[case("icap://:1344/", Err(()))]
1706 #[case("icap://proxy:bad/", Err(()))]
1707 fn parse_authority_cases(
1708 #[case] input: &str,
1709 #[case] expected: Result<(String, u16, bool), ()>,
1710 ) {
1711 match (parse_authority_with_scheme(input), expected) {
1712 (Ok((h, p, t)), Ok((eh, ep, et))) => assert_eq!((h, p, t), (eh, ep, et)),
1713 (Err(_), Err(())) => {}
1714 other => panic!("mismatch: {other:?}"),
1715 }
1716 }
1717
1718 #[test]
1719 fn append_to_allow_no_duplicates() {
1720 let mut h = HeaderMap::new();
1721 append_to_allow(&mut h, "204");
1722 append_to_allow(&mut h, "206");
1723 append_to_allow(&mut h, "204");
1724 let s = h.get("allow").unwrap().to_str().unwrap().to_string();
1725 assert!(s.contains("204"));
1726 assert!(s.contains("206"));
1727 assert_eq!(s.matches("204").count(), 1);
1728 }
1729
1730 #[test]
1731 fn try_user_agent_rejects_invalid_header_value() {
1732 let err = Client::builder()
1733 .try_user_agent("bad\r\nvalue")
1734 .expect_err("invalid header value should be rejected");
1735
1736 assert!(matches!(
1737 err,
1738 Error::Protocol(ProtocolError::HeaderValue(_))
1739 ));
1740 }
1741
1742 #[tokio::test]
1743 async fn timeout_caps_whole_send_operation() {
1744 let port = spawn_raw_icap_server(|mut stream| async move {
1745 let _request = read_until_contains(&mut stream, b"\r\n\r\n").await;
1746 let () = future::pending::<()>().await;
1747 })
1748 .await;
1749
1750 let client = Client::builder()
1751 .host("127.0.0.1")
1752 .port(port)
1753 .timeout(Some(Duration::from_millis(50)))
1754 .build();
1755
1756 let err = client
1757 .send(&Request::options("scan"))
1758 .await
1759 .expect_err("operation timeout should fire");
1760
1761 assert!(
1762 matches!(err, Error::Timeout(TimeoutError { kind: TimeoutKind::ClientTotal, duration: d }) if d == Duration::from_millis(50))
1763 );
1764 }
1765
1766 #[tokio::test]
1767 async fn continue_timeout_applies_to_preview_decision() {
1768 let port = spawn_raw_icap_server(|mut stream| async move {
1769 let _preview = read_until_contains(&mut stream, b"0\r\n\r\n").await;
1770 let () = future::pending::<()>().await;
1771 })
1772 .await;
1773
1774 let client = Client::builder()
1775 .host("127.0.0.1")
1776 .port(port)
1777 .continue_timeout(Some(Duration::from_millis(50)))
1778 .build();
1779
1780 let err = client
1781 .send_streaming_reader(&streaming_req(Some(0)), &b"ABCDEFG"[..])
1782 .await
1783 .expect_err("continue timeout should fire");
1784
1785 assert!(
1786 matches!(err, Error::Timeout(TimeoutError { kind: TimeoutKind::ClientContinue, duration: d }) if d == Duration::from_millis(50))
1787 );
1788 }
1789
1790 #[tokio::test]
1791 async fn write_timeout_applies_to_streaming_body_chunks() {
1792 let (mut client, _server) = tokio::io::duplex(1);
1793 let mut reader = tokio::io::repeat(0x61).take(1024 * 1024);
1794
1795 let err = write_reader_chunks(&mut client, &mut reader, Some(Duration::from_millis(50)))
1796 .await
1797 .expect_err("write timeout should fire");
1798
1799 assert!(
1800 matches!(err, Error::Timeout(TimeoutError { kind: TimeoutKind::ClientWrite, duration: d }) if d == Duration::from_millis(50))
1801 );
1802 }
1803
1804 #[rstest]
1805 #[case(None, b"" as &[u8], false, b"0\r\n\r\n".as_ref(), false, None)]
1806 #[case(None, b"abcd".as_ref(), false, b"4\r\nabcd\r\n0\r\n\r\n".as_ref(), false, None)]
1807 #[case(Some(0), b"".as_ref(), true, b"0; ieof\r\n\r\n".as_ref(), false, None)]
1808 #[case(Some(0), b"DATA".as_ref(), false, b"0\r\n\r\n".as_ref(), true, Some(b"DATA".as_ref()))]
1809 #[case(Some(4), b"ABCDEFG".as_ref(), false, b"4\r\nABCD\r\n0\r\n\r\n".as_ref(), true, Some(b"EFG".as_ref())
1810 )]
1811 #[case(Some(8), b"ABC".as_ref(), false, b"3\r\nABC\r\n0; ieof\r\n\r\n".as_ref(), false, None)]
1812 fn build_preview(
1813 #[case] preview: Option<usize>,
1814 #[case] body: &[u8],
1815 #[case] ieof: bool,
1816 #[case] expected_prefix: &[u8],
1817 #[case] expect_continue: bool,
1818 #[case] rest: Option<&[u8]>,
1819 ) {
1820 let body_vec = body.to_vec();
1821 let orig_len = body_vec.len();
1822 let (bytes, got_expect_continue, rest_opt) =
1823 build_preview_and_chunks(preview, body_vec, ieof, orig_len);
1824
1825 assert!(bytes.starts_with(expected_prefix));
1826 assert_eq!(got_expect_continue, expect_continue);
1827
1828 match (rest_opt.as_deref(), rest) {
1829 (None, None) => {}
1830 (Some(a), Some(b)) => assert_eq!(a, b),
1831 other => panic!("rest mismatch: {other:?}"),
1832 }
1833 }
1834
1835 #[tokio::test]
1836 async fn send_raw_delivers_verbatim_bytes_and_parses_response() {
1837 let port = spawn_raw_icap_server(|mut stream| async move {
1838 let wire = read_until_contains(&mut stream, b"\r\n\r\n").await;
1839 let text = String::from_utf8_lossy(&wire);
1840 assert!(text.starts_with("OPTIONS icap://127.0.0.1:"));
1841 assert!(text.contains("X-Custom: yes"));
1842
1843 stream
1844 .write_all(
1845 b"ICAP/1.0 200 OK\r\n\
1846 ISTag: \"raw-test\"\r\n\
1847 Methods: REQMOD\r\n\
1848 Encapsulated: null-body=0\r\n\r\n",
1849 )
1850 .await
1851 .unwrap();
1852 })
1853 .await;
1854
1855 let client = Client::builder().host("127.0.0.1").port(port).build();
1856
1857 let raw = format!(
1858 "OPTIONS icap://127.0.0.1:{port}/echo ICAP/1.0\r\n\
1859 Host: 127.0.0.1:{port}\r\n\
1860 X-Custom: yes\r\n\
1861 Encapsulated: null-body=0\r\n\r\n"
1862 );
1863
1864 let resp = timeout(Duration::from_millis(500), client.send_raw(raw.as_bytes()))
1865 .await
1866 .expect("send_raw timed out")
1867 .expect("send_raw returned error");
1868
1869 assert_eq!(resp.status_code, http::StatusCode::OK);
1870 assert_eq!(
1871 resp.headers().get("istag").map(|v| v.to_str().unwrap()),
1872 Some("\"raw-test\"")
1873 );
1874 }
1875
1876 #[tokio::test]
1877 async fn send_raw_str_is_equivalent_to_send_raw() {
1878 let port = spawn_raw_icap_server(|mut stream| async move {
1879 let _ = read_until_contains(&mut stream, b"\r\n\r\n").await;
1880 stream
1881 .write_all(
1882 b"ICAP/1.0 200 OK\r\n\
1883 ISTag: \"str-test\"\r\n\
1884 Methods: REQMOD\r\n\
1885 Encapsulated: null-body=0\r\n\r\n",
1886 )
1887 .await
1888 .unwrap();
1889 })
1890 .await;
1891
1892 let client = Client::builder().host("127.0.0.1").port(port).build();
1893
1894 let raw = format!(
1895 "OPTIONS icap://127.0.0.1:{port}/echo ICAP/1.0\r\n\
1896 Host: 127.0.0.1:{port}\r\n\
1897 Encapsulated: null-body=0\r\n\r\n"
1898 );
1899
1900 let resp = timeout(Duration::from_millis(500), client.send_raw_str(&raw))
1901 .await
1902 .expect("send_raw_str timed out")
1903 .expect("send_raw_str returned error");
1904
1905 assert_eq!(resp.status_code, http::StatusCode::OK);
1906 }
1907
1908 #[tokio::test]
1909 async fn rfc_streaming_without_preview_sends_body_before_reading_response() {
1910 let port = spawn_raw_icap_server(|mut stream| async move {
1911 let wire = read_until_contains(&mut stream, b"5\r\nHELLO\r\n0\r\n\r\n").await;
1912 let text = String::from_utf8_lossy(&wire);
1913 assert!(text.contains("Encapsulated: req-hdr=0, req-body="));
1914 assert!(!text.contains("\r\nPreview:"));
1915
1916 stream
1917 .write_all(
1918 b"ICAP/1.0 204 No Content\r\n\
1919 ISTag: \"stream-test\"\r\n\
1920 Encapsulated: null-body=0\r\n\r\n",
1921 )
1922 .await
1923 .unwrap();
1924 })
1925 .await;
1926
1927 let client = Client::builder().host("127.0.0.1").port(port).build();
1928 let resp = timeout(
1929 Duration::from_millis(500),
1930 client.send_streaming_reader(&streaming_req(None), &b"HELLO"[..]),
1931 )
1932 .await
1933 .expect("client waited for response before streaming body")
1934 .unwrap();
1935
1936 assert_eq!(resp.status_code, http::StatusCode::NO_CONTENT);
1937 }
1938
1939 #[tokio::test]
1940 async fn rfc_streaming_preview_sends_remainder_after_100_continue() {
1941 let port = spawn_raw_icap_server(|mut stream| async move {
1942 let preview = read_until_contains(&mut stream, b"4\r\nABCD\r\n0\r\n\r\n").await;
1943 let text = String::from_utf8_lossy(&preview);
1944 assert!(text.contains("\r\nPreview: 4\r\n"));
1945
1946 stream
1947 .write_all(b"ICAP/1.0 100 Continue\r\n\r\n")
1948 .await
1949 .unwrap();
1950
1951 let remainder = read_until_contains(&mut stream, b"3\r\nEFG\r\n0\r\n\r\n").await;
1952 assert!(
1953 String::from_utf8_lossy(&remainder).contains("3\r\nEFG\r\n0\r\n\r\n"),
1954 "missing chunked remainder"
1955 );
1956
1957 stream
1958 .write_all(
1959 b"ICAP/1.0 204 No Content\r\n\
1960 ISTag: \"stream-test\"\r\n\
1961 Encapsulated: null-body=0\r\n\r\n",
1962 )
1963 .await
1964 .unwrap();
1965 })
1966 .await;
1967
1968 let client = Client::builder().host("127.0.0.1").port(port).build();
1969 let resp = client
1970 .send_streaming_reader(&streaming_req(Some(4)), &b"ABCDEFG"[..])
1971 .await
1972 .unwrap();
1973
1974 assert_eq!(resp.status_code, http::StatusCode::NO_CONTENT);
1975 }
1976
1977 #[tokio::test]
1978 async fn rfc_streaming_preview_marks_ieof_when_reader_ends_inside_preview() {
1979 let port = spawn_raw_icap_server(|mut stream| async move {
1980 let preview = read_until_contains(&mut stream, b"3\r\nABC\r\n0; ieof\r\n\r\n").await;
1981 let text = String::from_utf8_lossy(&preview);
1982 assert!(text.contains("\r\nPreview: 8\r\n"));
1983
1984 stream
1985 .write_all(
1986 b"ICAP/1.0 204 No Content\r\n\
1987 ISTag: \"stream-test\"\r\n\
1988 Encapsulated: null-body=0\r\n\r\n",
1989 )
1990 .await
1991 .unwrap();
1992 })
1993 .await;
1994
1995 let client = Client::builder().host("127.0.0.1").port(port).build();
1996 let resp = client
1997 .send_streaming_reader(&streaming_req(Some(8)), &b"ABC"[..])
1998 .await
1999 .unwrap();
2000
2001 assert_eq!(resp.status_code, http::StatusCode::NO_CONTENT);
2002 }
2003
2004 #[tokio::test]
2005 async fn rfc_streaming_preview0_sends_body_after_100_continue() {
2006 let port = spawn_raw_icap_server(|mut stream| async move {
2007 let preview = read_until_contains(&mut stream, b"0\r\n\r\n").await;
2008 let text = String::from_utf8_lossy(&preview);
2009 assert!(text.contains("\r\nPreview: 0\r\n"));
2010 assert!(!text.contains("5\r\nHELLO\r\n"));
2011
2012 stream
2013 .write_all(b"ICAP/1.0 100 Continue\r\n\r\n")
2014 .await
2015 .unwrap();
2016
2017 let body = read_until_contains(&mut stream, b"5\r\nHELLO\r\n0\r\n\r\n").await;
2018 assert!(
2019 String::from_utf8_lossy(&body).contains("5\r\nHELLO\r\n0\r\n\r\n"),
2020 "missing full body after 100 Continue"
2021 );
2022
2023 stream
2024 .write_all(
2025 b"ICAP/1.0 204 No Content\r\n\
2026 ISTag: \"stream-test\"\r\n\
2027 Encapsulated: null-body=0\r\n\r\n",
2028 )
2029 .await
2030 .unwrap();
2031 })
2032 .await;
2033
2034 let client = Client::builder().host("127.0.0.1").port(port).build();
2035 let resp = client
2036 .send_streaming_reader(&streaming_req(Some(0)), &b"HELLO"[..])
2037 .await
2038 .unwrap();
2039
2040 assert_eq!(resp.status_code, http::StatusCode::NO_CONTENT);
2041 }
2042
2043 #[tokio::test]
2044 async fn rfc_streaming_preview0_final_response_skips_body_upload() {
2045 let port = spawn_raw_icap_server(|mut stream| async move {
2046 let preview = read_until_contains(&mut stream, b"0\r\n\r\n").await;
2047 let text = String::from_utf8_lossy(&preview);
2048 assert!(text.contains("\r\nPreview: 0\r\n"));
2049 assert!(!text.contains("5\r\nHELLO\r\n"));
2050
2051 stream
2052 .write_all(
2053 b"ICAP/1.0 204 No Content\r\n\
2054 ISTag: \"stream-test\"\r\n\
2055 Encapsulated: null-body=0\r\n\r\n",
2056 )
2057 .await
2058 .unwrap();
2059 })
2060 .await;
2061
2062 let client = Client::builder().host("127.0.0.1").port(port).build();
2063 let resp = client
2064 .send_streaming_reader(&streaming_req(Some(0)), &b"HELLO"[..])
2065 .await
2066 .unwrap();
2067
2068 assert_eq!(resp.status_code, http::StatusCode::NO_CONTENT);
2069 }
2070
2071 #[tokio::test]
2072 async fn rfc_streaming_preview0_ieof_sends_ieof_and_reads_final_response() {
2073 let port = spawn_raw_icap_server(|mut stream| async move {
2074 let preview = read_until_contains(&mut stream, b"0; ieof\r\n\r\n").await;
2075 let text = String::from_utf8_lossy(&preview);
2076 assert!(text.contains("\r\nPreview: 0\r\n"));
2077
2078 stream
2079 .write_all(
2080 b"ICAP/1.0 204 No Content\r\n\
2081 ISTag: \"stream-test\"\r\n\
2082 Encapsulated: null-body=0\r\n\r\n",
2083 )
2084 .await
2085 .unwrap();
2086 })
2087 .await;
2088
2089 let client = Client::builder().host("127.0.0.1").port(port).build();
2090 let req = streaming_req(Some(0)).preview_ieof();
2091 let resp = client
2092 .send_streaming_reader(&req, tokio::io::empty())
2093 .await
2094 .unwrap();
2095
2096 assert_eq!(resp.status_code, http::StatusCode::NO_CONTENT);
2097 }
2098
2099 #[tokio::test]
2100 async fn rfc_streaming_preview_final_response_skips_remainder_upload() {
2101 let port = spawn_raw_icap_server(|mut stream| async move {
2102 let preview = read_until_contains(&mut stream, b"4\r\nABCD\r\n0\r\n\r\n").await;
2103 let text = String::from_utf8_lossy(&preview);
2104 assert!(text.contains("\r\nPreview: 4\r\n"));
2105 assert!(!text.contains("3\r\nEFG\r\n"));
2106
2107 stream
2108 .write_all(
2109 b"ICAP/1.0 204 No Content\r\n\
2110 ISTag: \"stream-test\"\r\n\
2111 Encapsulated: null-body=0\r\n\r\n",
2112 )
2113 .await
2114 .unwrap();
2115 })
2116 .await;
2117
2118 let client = Client::builder().host("127.0.0.1").port(port).build();
2119 let resp = client
2120 .send_streaming_reader(&streaming_req(Some(4)), &b"ABCDEFG"[..])
2121 .await
2122 .unwrap();
2123
2124 assert_eq!(resp.status_code, http::StatusCode::NO_CONTENT);
2125 }
2126
2127 #[tokio::test]
2128 async fn rfc_streaming_preview_empty_reader_sends_ieof() {
2129 let port = spawn_raw_icap_server(|mut stream| async move {
2130 let preview = read_until_contains(&mut stream, b"0; ieof\r\n\r\n").await;
2131 let text = String::from_utf8_lossy(&preview);
2132 assert!(text.contains("\r\nPreview: 8\r\n"));
2133
2134 stream
2135 .write_all(
2136 b"ICAP/1.0 204 No Content\r\n\
2137 ISTag: \"stream-test\"\r\n\
2138 Encapsulated: null-body=0\r\n\r\n",
2139 )
2140 .await
2141 .unwrap();
2142 })
2143 .await;
2144
2145 let client = Client::builder().host("127.0.0.1").port(port).build();
2146 let resp = client
2147 .send_streaming_reader(&streaming_req(Some(8)), tokio::io::empty())
2148 .await
2149 .unwrap();
2150
2151 assert_eq!(resp.status_code, http::StatusCode::NO_CONTENT);
2152 }
2153
2154 #[test]
2155 fn reqmod_with_embedded_and_preview_offsets() {
2156 let c = client();
2157 let http = HttpReq::builder()
2158 .method("POST")
2159 .uri("/scan")
2160 .version(Version::HTTP_11)
2161 .header(header::HOST, "app")
2162 .header(header::CONTENT_LENGTH, "7")
2163 .body(b"PAYLOAD".to_vec())
2164 .unwrap();
2165
2166 let req = Request::reqmod("icap/test")
2167 .preview(4)
2168 .allow_204()
2169 .icap_header("x-foo", "bar")
2170 .with_http_request(http)
2171 .unwrap();
2172
2173 let wire = c.get_request_wire(&req, false).unwrap();
2174 let head = extract_headers_text(&wire);
2175 let enc_line = find_header_line(&head, "Encapsulated").unwrap();
2176 assert!(enc_line.contains("req-hdr=0"));
2177 let off = enc_line.split('=').next_back().unwrap().trim();
2178 let off_num: usize = off.parse().unwrap();
2179
2180 let icap_headers_end = head.len();
2181 let http_start = icap_headers_end;
2182 assert_eq!(
2183 &wire[http_start + off_num..http_start + off_num + 2],
2184 b"4\r"
2185 );
2186 let tail_str = bytes_to_string_prefix(&wire[http_start + off_num..], 64);
2187 assert!(tail_str.contains("\r\n0\r\n\r\n"));
2188 }
2189
2190 #[test]
2191 fn reqmod_header_only_uses_null_body_offset() {
2192 let http = HttpReq::builder()
2193 .method("GET")
2194 .uri("http://origin.example/")
2195 .header(header::HOST, "origin.example")
2196 .body(Vec::<u8>::new())
2197 .unwrap();
2198
2199 let req = Request::reqmod("echo").with_http_request(http).unwrap();
2200 let wire = client().get_request_wire(&req, false).unwrap();
2201 let head = extract_headers_text(&wire);
2202 let enc = find_header_line(&head, "Encapsulated").unwrap();
2203
2204 assert!(enc.contains("req-hdr=0"));
2205 assert!(
2206 enc.contains("null-body="),
2207 "header-only REQMOD must delimit the embedded HTTP head for strict servers: {enc}"
2208 );
2209 }
2210
2211 #[rstest]
2212 #[case::preview0(false, false, "\r\n\r\n0\r\n\r\n")]
2213 #[case::preview0_ieof(true, false, "\r\n\r\n0; ieof\r\n\r\n")]
2214 fn reqmod_wire_variants(
2216 client: Client,
2217 #[case] ieof: bool,
2218 #[case] streaming: bool,
2219 #[case] must_contain: &'static str,
2220 ) {
2221 let http = HttpReq::builder()
2222 .method("POST")
2223 .uri("/scan")
2224 .header(header::HOST, "x")
2225 .body(Vec::<u8>::new())
2226 .unwrap();
2227
2228 let mut req = Request::reqmod("icap/test")
2229 .preview(0)
2230 .with_http_request(http)
2231 .unwrap();
2232 if ieof {
2233 req = req.preview_ieof();
2234 }
2235
2236 let wire = client.get_request_wire(&req, streaming).unwrap();
2237 let all = std::str::from_utf8(&wire).unwrap();
2238 assert!(all.contains(must_contain));
2239
2240 let head = extract_headers_text(&wire);
2241 let enc = find_header_line(&head, "Encapsulated").unwrap();
2242 assert!(enc.to_ascii_lowercase().contains("req-hdr=0"));
2243 if streaming {
2244 assert!(enc.to_ascii_lowercase().contains("req-body="));
2245 }
2246 }
2247
2248 #[test]
2249 fn preview_zero_wire_for_buffered_body_has_single_preview_marker() {
2250 let http = HttpReq::builder()
2251 .method("POST")
2252 .uri("/scan")
2253 .header(header::HOST, "x")
2254 .body(b"PAYLOAD".to_vec())
2255 .unwrap();
2256
2257 let req = Request::reqmod("icap/test")
2258 .preview(0)
2259 .with_http_request(http)
2260 .unwrap();
2261
2262 let wire = client().get_request_wire(&req, false).unwrap();
2263 let marker_count = wire
2264 .windows(b"0\r\n\r\n".len())
2265 .filter(|w| *w == b"0\r\n\r\n")
2266 .count();
2267
2268 assert_eq!(marker_count, 1);
2269 }
2270
2271 #[test]
2272 fn mismatched_embedded_message_is_rejected_before_serialization() {
2273 let http = http::Response::builder()
2274 .status(http::StatusCode::OK)
2275 .body(Vec::<u8>::new())
2276 .unwrap();
2277
2278 let err = Request::reqmod("icap/test")
2279 .with_http_response(http)
2280 .expect_err("REQMOD must reject embedded HTTP responses");
2281
2282 assert!(matches!(
2283 err,
2284 Error::Protocol(ProtocolError::Serialization(_))
2285 ));
2286 }
2287
2288 #[rstest]
2289 #[case("icap.example", None, "icap://icap.example:1344/options")]
2290 #[case(
2291 "icap.internal",
2292 Some("icap.external.name"),
2293 "icap://icap.internal:1344/options"
2294 )]
2295 fn options_and_host_header(
2296 #[case] client_host: &'static str,
2297 #[case] host_override: Option<&'static str>,
2298 #[case] uri_prefix: &'static str,
2299 ) {
2300 let mut b = Client::builder().host(client_host).port(1344);
2301 if let Some(ho) = host_override {
2302 b = b.host_override(ho);
2303 }
2304 let c = b.build();
2305
2306 let req = Request::options("options");
2307 let wire = c.get_request_wire(&req, false).unwrap();
2308 let head = extract_headers_text(&wire);
2309
2310 assert!(head.starts_with(&format!("OPTIONS {uri_prefix} ICAP/1.0\r\n")));
2311 let host_line = find_header_line(&head, "Host").unwrap();
2312 let expected_host = host_override.unwrap_or(client_host);
2313 assert!(host_line.contains(expected_host));
2314 }
2315
2316 #[tokio::test]
2319 async fn error_404_single_crlf_kept_open_times_out() {
2320 let (mut client, server) = connect_pair().await;
2321
2322 tokio::spawn(server_write(
2323 server,
2324 b"ICAP/1.0 404 ICAP Service not found\r\n",
2325 true,
2326 ));
2327
2328 let res = timeout(
2329 Duration::from_millis(50),
2330 read_icap_headers(&mut client, crate::DEFAULT_ICAP_HEADER_BYTES),
2331 )
2332 .await;
2333 assert!(res.is_err());
2334 }
2335
2336 #[tokio::test]
2338 async fn error_404_with_headers_and_double_crlf() {
2339 let (mut client, server) = connect_pair().await;
2340
2341 let wire = b"ICAP/1.0 404 ICAP Service not found\r\nISTag: x\r\nDate: Thu, 21 Aug 2025 17:00:00 GMT\r\n\r\n";
2342 tokio::spawn(server_write(server, wire, false));
2343
2344 let (code, buf) = timeout(
2345 Duration::from_millis(300),
2346 read_icap_headers(&mut client, crate::DEFAULT_ICAP_HEADER_BYTES),
2347 )
2348 .await
2349 .expect("client hung on proper 404")
2350 .expect("read_icap_headers failed");
2351
2352 assert_eq!(code, 404);
2353 assert!(buf.ends_with(b"\r\n\r\n"));
2354 let text = String::from_utf8(buf).unwrap();
2355 assert!(text.contains("ISTag: x"));
2356 assert!(text.contains("Date: "));
2357 }
2358
2359 #[tokio::test]
2360 async fn error_404_split_headers_are_preserved() {
2361 let (mut client, mut server) = connect_pair().await;
2362
2363 tokio::spawn(async move {
2364 use tokio::io::AsyncWriteExt;
2365
2366 server
2367 .write_all(b"ICAP/1.0 404 ICAP Service not found\r\n")
2368 .await
2369 .unwrap();
2370 server.flush().await.unwrap();
2371 tokio::time::sleep(Duration::from_millis(25)).await;
2372 server
2373 .write_all(b"ISTag: split-test\r\nX-Late: yes\r\n\r\n")
2374 .await
2375 .unwrap();
2376 server.flush().await.unwrap();
2377 });
2378
2379 let (code, buf) = timeout(
2380 Duration::from_millis(300),
2381 read_icap_headers(&mut client, crate::DEFAULT_ICAP_HEADER_BYTES),
2382 )
2383 .await
2384 .expect("client hung on split 404")
2385 .expect("read_icap_headers failed");
2386
2387 assert_eq!(code, 404);
2388 let text = String::from_utf8(buf).unwrap();
2389 assert!(text.contains("ISTag: split-test"));
2390 assert!(text.contains("X-Late: yes"));
2391 }
2392
2393 #[tokio::test]
2395 async fn error_404_headers_then_eof_before_double_crlf() {
2396 let (mut client, server) = connect_pair().await;
2397
2398 let wire = b"ICAP/1.0 404 ICAP Service not found\r\nISTag: y\r\n";
2400 tokio::spawn(server_write(server, wire, false));
2401
2402 let (code, buf) = timeout(
2403 Duration::from_millis(300),
2404 read_icap_headers(&mut client, crate::DEFAULT_ICAP_HEADER_BYTES),
2405 )
2406 .await
2407 .expect("client hung on 404 with EOF")
2408 .expect("read_icap_headers failed");
2409
2410 assert_eq!(code, 404);
2411 assert!(buf.ends_with(b"\r\n\r\n"), "normalized to CRLFCRLF on EOF");
2412 let text = String::from_utf8(buf).unwrap();
2413 assert!(
2414 text.contains("ISTag: y"),
2415 "header bytes should be preserved"
2416 );
2417 }
2418
2419 #[tokio::test]
2422 async fn non_error_200_single_crlf_kept_open_times_out() {
2423 let (mut client, server) = connect_pair().await;
2424
2425 tokio::spawn(server_write(server, b"ICAP/1.0 200 OK\r\n", true));
2426 let res = timeout(
2427 Duration::from_millis(50),
2428 read_icap_headers(&mut client, crate::DEFAULT_ICAP_HEADER_BYTES),
2429 )
2430 .await;
2431 assert!(res.is_err());
2432 }
2433
2434 #[rstest]
2439 #[case("text/html", "html")]
2440 #[case("text/html; charset=utf-8", "html")]
2441 #[case("application/pdf", "pdf")]
2442 #[case("application/zip", "zip")]
2443 #[case("application/x-zip-compressed", "zip")]
2444 #[case("image/jpeg", "jpg")]
2445 #[case("image/png", "png")]
2446 #[case("image/gif", "gif")]
2447 #[case("application/json", "json")]
2448 #[case("application/xml", "xml")]
2449 #[case("text/xml", "xml")]
2450 #[case("application/gzip", "gz")]
2451 #[case("video/mp4", "mp4")]
2452 #[case("application/octet-stream", "")]
2453 #[case("", "")]
2454 #[case("totally/unknown", "")]
2455 fn content_type_to_extension(#[case] ct: &str, #[case] expected: &str) {
2456 assert_eq!(ext_from_content_type(ct), expected);
2457 }
2458}