Skip to main content

icap_rs/
server.rs

1//! # ICAP server implementation in Rust.
2//!
3//! ICAP server with per-service routing and **one handler that can serve multiple
4//! ICAP methods** (`REQMOD`, `RESPMOD`). The server:
5//!
6//! - Supports `OPTIONS`, `REQMOD`, `RESPMOD`;
7//! - Lets you register services with **one handler** and a **list of allowed methods** via
8//!   [`ServerBuilder::route`];
9//! - **Supports internal rerouting** via [`ServerBuilder::alias`] and [`ServerBuilder::default_service`]:
10//!   map one service name to another (e.g. `/` → `scan`) and choose the default service for empty or `/` path;
11//! - Automatically answers `OPTIONS` per service using the allowed methods;
12//! - Returns `404` for unknown services and `405` for unsupported methods;
13//! - Reads encapsulated **chunked bodies to completion** before parsing (avoids premature close
14//!   observed by clients like `c-icap-client`);
15//! - Can limit concurrent connections via a semaphore.
16//! ## Quick example
17//!
18//! ```rust,no_run
19//! use icap_rs::{IcapResult, IncomingRequest, Method, Response, Server, ServiceOptions, StatusCode};
20//!
21//! const ISTAG: &str = "scan-1.0";
22//!
23//! #[tokio::main]
24//! async fn main() -> IcapResult<()> {
25//!     let server = Server::builder()
26//!         .bind("127.0.0.1:1344")
27//!         // One handler for REQMOD and RESPMOD of the "scan" service.
28//!         .route(
29//!             "scan",
30//!             [Method::ReqMod, Method::RespMod],
31//!             |req: IncomingRequest| async move {
32//!                 match req.method() {
33//!                     Method::ReqMod => Ok(Response::no_content_with_istag(ISTAG)?),
34//!                     Method::RespMod => Ok(Response::no_content_with_istag(ISTAG)?),
35//!                     Method::Options => unreachable!("OPTIONS is handled automatically by the server"),
36//!                 }
37//!             },
38//!             Some(ServiceOptions::new().with_static_istag(ISTAG)
39//!                 .with_service("Scan Service")
40//!                 .allow_204()
41//!                 .with_preview(2048))
42//!         )
43//!         // If a client uses `icap://host/`, internally route it to the "scan" service.
44//!         .default_service("scan")
45//!         .alias("/", "scan")
46//!         .with_max_connections(128)
47//!         .build()
48//!         .await?;
49//!
50//!     server.run().await
51//! }
52//! ```
53//!
54//! When a client sends `OPTIONS icap://host/scan`, the server responds with
55//! `Methods: REQMOD, RESPMOD` based on the registration above. If a method not in
56//! the list is used, `405 Method Not Allowed` is returned; unknown services yield `404`.
57
58mod builder;
59mod connection;
60mod errors;
61pub mod handler;
62mod no_modification;
63pub mod options;
64mod preview;
65mod router;
66pub mod timeouts;
67pub use builder::ServerBuilder;
68pub use handler::{BoxError, HandlerError, HandlerResult};
69pub use preview::PreviewDecision;
70pub use router::RouteOutput;
71pub use timeouts::ServerTimeouts;
72
73use std::collections::HashMap;
74use std::future::Future;
75use std::sync::Arc;
76use std::time::Duration;
77use tokio::io::AsyncWriteExt;
78use tokio::net::TcpListener;
79use tokio::sync::{Semaphore, watch};
80use tokio::task::JoinSet;
81use tokio::time::{sleep, timeout};
82use tokio_util::task::TaskTracker;
83use tracing::{error, trace, warn};
84
85use crate::error::IcapResult;
86use crate::request::RequestParserMode;
87pub use crate::server::options::{IsTagHandle, ServiceOptions, TransferBehavior};
88use crate::{Response, StatusCode};
89use router::RouteEntry;
90#[cfg(feature = "tls-rustls")]
91use tokio_rustls::TlsAcceptor;
92
93/// An event emitted during graceful shutdown.
94///
95/// Register a handler with [`ServerBuilder::on_shutdown_event`] to receive these.
96/// When no handler is registered, the server logs via [`tracing::warn`] by default.
97///
98/// # Example
99///
100/// ```rust,no_run
101/// use icap_rs::{IcapResult, Server, ShutdownEvent};
102///
103/// #[tokio::main]
104/// async fn main() -> IcapResult<()> {
105///     let server = Server::builder()
106///         .bind("127.0.0.1:1344")
107///         .on_shutdown_event(|event| match event {
108///             ShutdownEvent::Draining { active_connections, drain_timeout } => {
109///                 eprintln!("shutting down: {active_connections} connections in flight");
110///                 if let Some(d) = drain_timeout {
111///                     eprintln!("force-close in {d:.1?}");
112///                 }
113///             }
114///             ShutdownEvent::DrainTimedOut { remaining_connections } => {
115///                 eprintln!("drain timed out, cancelling {remaining_connections} connections");
116///             }
117///             _ => {}
118///         })
119///         .build()
120///         .await?;
121///
122///     server.run_until(async { tokio::signal::ctrl_c().await.ok(); }).await
123/// }
124/// ```
125#[derive(Debug, Clone, Copy)]
126#[non_exhaustive]
127pub enum ShutdownEvent {
128    /// Shutdown signal received; drain phase starting.
129    ///
130    /// New connections are refused with `503 Service Unavailable`.
131    /// `active_connections` are still processing requests.
132    /// If `drain_timeout` is set, remaining connections will be force-cancelled after it expires.
133    Draining {
134        /// Number of connections still in flight.
135        active_connections: usize,
136        /// How long until in-flight connections are force-cancelled, if configured.
137        drain_timeout: Option<Duration>,
138    },
139    /// Drain deadline expired; remaining connections are being cancelled.
140    DrainTimedOut {
141        /// Number of connections that are being cancelled.
142        remaining_connections: usize,
143    },
144}
145
146fn default_shutdown_handler(event: ShutdownEvent) {
147    match event {
148        ShutdownEvent::Draining {
149            active_connections,
150            drain_timeout: Some(d),
151        } => warn!(
152            connections = %active_connections,
153            "shutting down; draining {active_connections} active connection(s); \
154             new connections will be refused; force-close in {d:.1?}",
155        ),
156        ShutdownEvent::Draining {
157            active_connections,
158            drain_timeout: None,
159        } => warn!(
160            connections = %active_connections,
161            "shutting down; draining {active_connections} active connection(s); \
162             new connections will be refused",
163        ),
164        ShutdownEvent::DrainTimedOut {
165            remaining_connections,
166        } => warn!(
167            connections = %remaining_connections,
168            "shutdown drain timeout expired; \
169             cancelling {remaining_connections} remaining connection(s)",
170        ),
171    }
172}
173
174/// ICAP server.
175///
176/// Use [`Server::builder`] to construct and run an instance.
177///
178/// # Example
179///
180/// ```rust,no_run
181/// use icap_rs::{IcapResult, IncomingRequest, Method, Response, Server, ServiceOptions};
182///
183/// const ISTAG: &str = "scan-1.0";
184///
185/// #[tokio::main]
186/// async fn main() -> IcapResult<()> {
187///     let server = Server::builder()
188///         .bind("127.0.0.1:1344")
189///         .route(
190///             "scan",
191///             [Method::ReqMod],
192///             |_req: IncomingRequest| async move {
193///                 Ok(Response::no_content_with_istag(ISTAG)?)
194///             },
195///             Some(ServiceOptions::new().with_static_istag(ISTAG).with_preview(1024)),
196///         )
197///         .build()
198///         .await?;
199///
200///     server.run().await
201/// }
202/// ```
203pub struct Server {
204    listener: TcpListener,
205    routes: Arc<HashMap<String, RouteEntry>>,
206    conn_limit: Option<Arc<Semaphore>>,
207    advertised_max_conn: Option<usize>,
208    aliases: Arc<HashMap<String, String>>,
209    default_service: Option<String>,
210    request_parser_mode: RequestParserMode,
211    timeouts: ServerTimeouts,
212    max_request_header_bytes: usize,
213    shutdown_handler: Arc<dyn Fn(ShutdownEvent) + Send + Sync>,
214    task_tracker: Option<TaskTracker>,
215    #[cfg(feature = "tls-rustls")]
216    tls: Option<(TlsAcceptor, Duration)>,
217}
218impl Server {
219    /// Create a new [`ServerBuilder`].
220    pub fn builder() -> ServerBuilder {
221        ServerBuilder::default()
222    }
223
224    /// Local socket address the server is bound to.
225    ///
226    /// Useful after binding to an ephemeral port (`127.0.0.1:0`).
227    pub fn local_addr(&self) -> std::io::Result<std::net::SocketAddr> {
228        self.listener.local_addr()
229    }
230
231    /// Run the accept loop until the process is killed.
232    ///
233    /// This is a convenience wrapper around [`run_until`](Self::run_until) with a
234    /// `pending()` shutdown future, meaning the server runs indefinitely.
235    /// Use [`run_until`](Self::run_until) when you need graceful shutdown.
236    pub async fn run(self) -> IcapResult<()> {
237        self.run_until(std::future::pending::<()>()).await
238    }
239
240    /// Run the accept loop until `shutdown` resolves, then drain active connections.
241    ///
242    /// When `shutdown` completes the server stops accepting new connections and
243    /// signals all active keep-alive connections to close after their current
244    /// in-flight request completes. Idle connections (waiting for the next
245    /// request) are closed immediately. The method returns only after every
246    /// active connection handler has finished.
247    ///
248    /// # Example
249    ///
250    /// ```rust,no_run
251    /// use icap_rs::{IcapResult, Server};
252    ///
253    /// #[tokio::main]
254    /// async fn main() -> IcapResult<()> {
255    ///     let server = Server::builder()
256    ///         .bind("127.0.0.1:1344")
257    ///         .build()
258    ///         .await?;
259    ///
260    ///     // Shut down cleanly on Ctrl-C.
261    ///     server.run_until(async { tokio::signal::ctrl_c().await.ok(); }).await
262    /// }
263    /// ```
264    pub async fn run_until<F>(self, shutdown: F) -> IcapResult<()>
265    where
266        F: Future<Output = ()>,
267    {
268        let local_addr = self.listener.local_addr()?;
269        trace!(addr=%local_addr, "ICAP server started");
270
271        let (shutdown_tx, shutdown_rx) = watch::channel(false);
272        let mut tasks: JoinSet<()> = JoinSet::new();
273        let mut shutting_down = false;
274        let mut drain_timer_armed = false;
275        let mut drain_deadline: Option<tokio::time::Instant> = None;
276
277        // Sentinel timer far in the future; reset to a real deadline when drain begins.
278        let drain_timer = sleep(Duration::from_hours(8760));
279        tokio::pin!(shutdown, drain_timer);
280
281        loop {
282            // Break as soon as the shutdown is complete and all connections have finished.
283            if shutting_down && tasks.is_empty() {
284                break;
285            }
286
287            tokio::select! {
288                biased;
289
290                () = &mut shutdown, if !shutting_down => {
291                    let _ = shutdown_tx.send(true);
292                    shutting_down = true;
293                    let active = tasks.len();
294                    if active == 0 {
295                        break;
296                    }
297                    (self.shutdown_handler)(ShutdownEvent::Draining {
298                        active_connections: active,
299                        drain_timeout: self.timeouts.shutdown_drain,
300                    });
301                    if let Some(d) = self.timeouts.shutdown_drain {
302                        let deadline = tokio::time::Instant::now() + d;
303                        drain_timer.as_mut().reset(deadline);
304                        drain_deadline = Some(deadline);
305                        drain_timer_armed = true;
306                    }
307                }
308
309                () = &mut drain_timer, if drain_timer_armed => {
310                    let remaining = tasks.len();
311                    (self.shutdown_handler)(ShutdownEvent::DrainTimedOut {
312                        remaining_connections: remaining,
313                    });
314                    tasks.abort_all();
315                    while tasks.join_next().await.is_some() {}
316                    trace!(addr=%local_addr, "ICAP server stopped");
317                    return Ok(());
318                }
319
320                // Poll finished tasks continuously so tasks.len() stays accurate and
321                // we detect when the last in-flight connection completes during drain.
322                Some(_) = tasks.join_next(), if !tasks.is_empty() => {}
323
324                accept_result = self.listener.accept() => {
325                    let (socket, addr) = accept_result?;
326
327                    if shutting_down {
328                        // Refuse new connections during drain with 503.
329                        // For TLS, close the raw TCP socket — no handshake before rejecting.
330                        trace!(client=%addr, "refusing connection: server is shutting down");
331                        #[cfg(feature = "tls-rustls")]
332                        if self.tls.is_some() {
333                            tokio::spawn(async move {
334                                let mut s = socket;
335                                let _ = s.shutdown().await;
336                            });
337                            continue;
338                        }
339                        tokio::spawn(async move {
340                            let mut s = socket;
341                            let _ = Self::write_wire_error_response(
342                                &mut s,
343                                StatusCode::SERVICE_UNAVAILABLE,
344                                "Service Shutting Down",
345                            )
346                            .await;
347                        });
348                        continue;
349                    }
350
351                    trace!(client=%addr, "new connection");
352
353                    let (permit_opt, over_limit) = self.conn_limit.as_ref().map_or_else(
354                        || (None, false),
355                        |sem| {
356                            sem.clone()
357                                .try_acquire_owned()
358                                .map_or_else(|_| (None, true), |p| (Some(p), false))
359                        },
360                    );
361
362                    let routes = Arc::clone(&self.routes);
363                    let aliases = Arc::clone(&self.aliases);
364                    let default_service = self.default_service.clone();
365                    let advertised_max = self.advertised_max_conn;
366                    let request_parser_mode = self.request_parser_mode;
367                    let timeouts = self.timeouts.clone();
368                    let max_request_header_bytes = self.max_request_header_bytes;
369                    let conn_shutdown = shutdown_rx.clone();
370
371                    #[cfg(feature = "tls-rustls")]
372                    let tls = self.tls.clone();
373
374                    tasks.spawn(async move {
375                        let _permit = permit_opt;
376
377                        if over_limit {
378                            // Reject before TLS handshake: avoids spending CPU on handshakes
379                            // just to reject. For TLS, close the raw TCP socket instead.
380                            #[cfg(feature = "tls-rustls")]
381                            if tls.is_some() {
382                                let mut sock = socket;
383                                let _ = sock.shutdown().await;
384                                return;
385                            }
386
387                            let resp =
388                                Response::new(StatusCode::SERVICE_UNAVAILABLE, "Service Unavailable")
389                                    .add_header("Connection", "close");
390                            match resp.to_raw() {
391                                Ok(bytes) => {
392                                    let mut sock = socket;
393                                    if let Err(e) = sock.write_all(&bytes).await {
394                                        warn!(client=%addr, error=%e, "failed to send 503");
395                                    } else {
396                                        let _ = sock.shutdown().await;
397                                    }
398                                }
399                                Err(e) => warn!(client=%addr, error=%e, "failed to serialize 503"),
400                            }
401                            return;
402                        }
403
404                        #[cfg(feature = "tls-rustls")]
405                        if let Some((acceptor, hs_timeout)) = tls {
406                            match timeout(hs_timeout, acceptor.accept(socket)).await {
407                                Ok(Ok(stream)) => {
408                                    if let Err(e) = Box::pin(Self::handle_connection(
409                                        stream,
410                                        routes,
411                                        aliases,
412                                        default_service,
413                                        advertised_max,
414                                        request_parser_mode,
415                                        timeouts,
416                                        max_request_header_bytes,
417                                        conn_shutdown,
418                                        addr,
419                                    ))
420                                    .await
421                                    {
422                                        error!(client=%addr, error=%e, "error handling TLS connection");
423                                    }
424                                }
425                                Ok(Err(e)) => {
426                                    warn!(client=%addr, error=%e, "TLS handshake failed");
427                                }
428                                Err(_) => {
429                                    warn!(
430                                        client=%addr,
431                                        timeout=?hs_timeout,
432                                        "TLS handshake timed out",
433                                    );
434                                }
435                            }
436                            return;
437                        }
438
439                        // Plain TCP
440                        if let Err(e) = Box::pin(Self::handle_connection(
441                            socket,
442                            routes,
443                            aliases,
444                            default_service,
445                            advertised_max,
446                            request_parser_mode,
447                            timeouts,
448                            max_request_header_bytes,
449                            conn_shutdown,
450                            addr,
451                        ))
452                        .await
453                        {
454                            error!(client=%addr, error=%e, "error handling connection");
455                        }
456                    });
457                }
458            }
459        }
460
461        // After all connections drain, wait for any user-registered background tasks.
462        if let Some(ref tracker) = self.task_tracker {
463            tracker.close();
464            match drain_deadline {
465                Some(deadline) => {
466                    let remaining = deadline.saturating_duration_since(tokio::time::Instant::now());
467                    if !remaining.is_zero() {
468                        let _ = timeout(remaining, tracker.wait()).await;
469                    }
470                }
471                None => tracker.wait().await,
472            }
473        }
474
475        trace!(addr=%local_addr, "ICAP server stopped");
476        Ok(())
477    }
478}
479
480#[cfg(test)]
481mod tests {
482    use super::*;
483    use crate::{IncomingRequest, Method};
484    use rstest::rstest;
485    use std::panic::{AssertUnwindSafe, catch_unwind};
486
487    async fn handler_ok(_: IncomingRequest) -> crate::HandlerResult<Response> {
488        Ok(Response::new(StatusCode::OK, "OK")
489            .add_header("Encapsulated", "null-body=0")
490            .add_header("Content-Length", "0"))
491    }
492
493    fn panic_str(res: Result<(), Box<dyn std::any::Any + Send>>) -> String {
494        match res {
495            Ok(()) => String::new(),
496            Err(e) => e.downcast_ref::<&str>().map_or_else(
497                || {
498                    e.downcast_ref::<String>()
499                        .map_or_else(|| "<non-string panic>".to_string(), Clone::clone)
500                },
501                |s| (*s).to_string(),
502            ),
503        }
504    }
505
506    fn assert_panics_with<F>(f: F, needles: &[&str])
507    where
508        F: FnOnce() + std::panic::UnwindSafe,
509    {
510        let res = catch_unwind(AssertUnwindSafe(f));
511        assert!(res.is_err(), "expected panic, but code did not panic");
512        let msg = panic_str(res);
513        for n in needles {
514            assert!(
515                msg.contains(n),
516                "expected panic message to contain {n:?}, got: {msg}",
517            );
518        }
519    }
520
521    #[test]
522    fn route_allows_different_methods_same_service() {
523        let h1 = handler_ok;
524        let h2 = handler_ok;
525
526        let _builder = Server::builder()
527            .route("/spool", [Method::ReqMod], h1, None)
528            .route("/spool", [Method::RespMod], h2, None);
529    }
530
531    #[rstest]
532    #[case("/spool")]
533    #[case("/svc")]
534    fn route_panics_on_duplicate_method_same_service(#[case] path: &str) {
535        assert_panics_with(
536            || {
537                let h1 = handler_ok;
538                let h2 = handler_ok;
539
540                let builder = Server::builder().route(path, [Method::ReqMod], h1, None);
541                let _ = builder.route(path, [Method::ReqMod], h2, None);
542            },
543            &["Overlapping method route", "REQMOD", path],
544        );
545    }
546
547    #[rstest]
548    #[case(Method::RespMod, "RESPMOD")]
549    #[case(Method::ReqMod, "REQMOD")]
550    fn panics_when_overlapping_multiple_methods(
551        #[case] overlap: Method,
552        #[case] overlap_str: &str,
553    ) {
554        let path = "/svc";
555        assert_panics_with(
556            || {
557                let h = handler_ok;
558                let h2 = handler_ok;
559
560                let builder =
561                    Server::builder().route(path, [Method::ReqMod, Method::RespMod], h, None);
562                let _ = builder.route(path, [overlap], h2, None);
563            },
564            &["Overlapping method route", overlap_str, path],
565        );
566    }
567
568    #[rstest]
569    #[case("reqmod", "RESPMOD")]
570    #[case("REQMOD", "respmod")]
571    #[case("  ReqMod  ", " ReSpMoD ")]
572    fn route_accepts_methods_case_insensitive(#[case] a: &str, #[case] b: &str) {
573        let h = handler_ok;
574        let _b = Server::builder().route("/svc", [a, b], h, None);
575    }
576
577    #[test]
578    fn route_accepts_mixed_enum_and_string() {
579        let h = handler_ok;
580        let _b = Server::builder().route("/svc", vec![Method::ReqMod, "respmod".into()], h, None);
581    }
582
583    #[rstest]
584    #[case("FOO")]
585    #[case("BAR")]
586    fn route_panics_on_unknown_method(#[case] bad: &str) {
587        assert_panics_with(
588            || {
589                let h = handler_ok;
590                let _ = Server::builder().route("/svc", [bad], h, None);
591            },
592            &["Unknown ICAP method string"],
593        );
594    }
595
596    #[rstest]
597    #[case("OPTIONS")]
598    #[case("options")]
599    #[case("  Options  ")]
600    fn route_panics_on_options(#[case] opt: &str) {
601        assert_panics_with(
602            || {
603                let h = handler_ok;
604                let _ = Server::builder().route("/svc", [opt], h, None);
605            },
606            &["OPTIONS"],
607        );
608    }
609
610    #[tokio::test]
611    async fn build_errors_when_default_service_is_unknown() {
612        let result = Server::builder().default_service("missing").build().await;
613        let err = result
614            .err()
615            .expect("unknown default service should fail at build time");
616
617        let msg = err.to_string();
618        assert!(msg.contains("default service"), "unexpected error: {msg}");
619        assert!(msg.contains("missing"), "unexpected error: {msg}");
620    }
621
622    #[tokio::test]
623    async fn build_errors_when_alias_target_is_unknown() {
624        let options = ServiceOptions::new().with_static_istag("svc-1.0");
625        let result = Server::builder()
626            .route_reqmod("svc", handler_ok, Some(options))
627            .alias("alt", "missing")
628            .build()
629            .await;
630        let err = result
631            .err()
632            .expect("unknown alias target should fail at build time");
633
634        let msg = err.to_string();
635        assert!(msg.contains("alias"), "unexpected error: {msg}");
636        assert!(msg.contains("missing"), "unexpected error: {msg}");
637    }
638
639    #[tokio::test]
640    async fn build_errors_when_service_options_are_invalid() {
641        let options = ServiceOptions::new()
642            .with_static_istag("svc-1.0")
643            .add_transfer_rule("exe", TransferBehavior::Preview);
644        let result = Server::builder()
645            .route_reqmod("svc", handler_ok, Some(options))
646            .build()
647            .await;
648        let err = result
649            .err()
650            .expect("invalid service options should fail at build time");
651
652        let msg = err.to_string();
653        assert!(msg.contains("invalid options"), "unexpected error: {msg}");
654        assert!(
655            msg.contains("Default transfer behavior"),
656            "unexpected error: {msg}"
657        );
658    }
659
660    #[tokio::test]
661    async fn build_errors_when_service_options_are_missing() {
662        let result = Server::builder()
663            .route_reqmod("svc", handler_ok, None)
664            .build()
665            .await;
666        let err = result
667            .err()
668            .expect("missing service options should fail at build time");
669
670        let msg = err.to_string();
671        assert!(msg.contains("ServiceOptions"), "unexpected error: {msg}");
672        assert!(msg.contains("ISTag"), "unexpected error: {msg}");
673    }
674}