Skip to main content

apimock_server/
server.rs

1//! HTTP(S) server runtime.
2//!
3//! # 5.0 layout
4//!
5//! The [`Server`] struct holds the listener addresses and the shared
6//! application state. [`AppState`] in turn holds a `Config` (editable
7//! declarative data from `apimock-config`) alongside [`LoadedMiddlewares`]
8//! (compiled Rhai — runtime only, server-owned).
9//!
10//! Dispatch methods (`middleware_response`, `rule_set_response`) used
11//! to hang off `ServiceConfig` but were moved here in 5.0 because they
12//! build `hyper::Response` values, which a config crate must not do.
13//! They are now free functions in this module that take borrowed config
14//! + loaded state and produce an `hyper::Response`.
15
16use apimock_config::Config;
17use apimock_config::config::constant::{
18    SERVICE_DEFAULT_MAX_REQUEST_BODY_BYTES, SERVICE_DEFAULT_MIDDLEWARE_MAX_OPERATIONS,
19    TLS_DEFAULT_HANDSHAKE_TIMEOUT_SECONDS, TLS_DEFAULT_MAX_CONNECTIONS,
20};
21use apimock_routing::ParsedRequest;
22use console::style;
23use http_body_util::{BodyExt, Empty};
24use hyper::{
25    HeaderMap, Response, body,
26    header::{CONTENT_LENGTH, HeaderValue},
27    service::service_fn,
28};
29use hyper_util::{
30    rt::{TokioExecutor, TokioIo},
31    server::conn::auto::Builder,
32};
33use tokio::net::TcpListener;
34use tokio::sync::Semaphore;
35use tokio_rustls::TlsAcceptor;
36
37use std::net::{SocketAddr, ToSocketAddrs};
38use std::path::PathBuf;
39use std::sync::Arc;
40
41use crate::{
42    dyn_route::dyn_route_content,
43    error::{ServerError, ServerResult},
44    middleware::LoadedMiddlewares,
45    parsed_request::{ParsedRequestError, capture_in_log_with_trace_config, parsed_request_from},
46    respond_response::respond_response,
47    response::{
48        confine::canonical_dir,
49        error_response::{internal_server_error_response, payload_too_large_response},
50    },
51    response_handler::default_response_headers,
52    tls::{build_server_config_reloadable, load_certs, load_private_key},
53    types::BoxBody,
54};
55
56pub use crate::control::{ReloadHint, ServerControl, ServerHandle, ServerState};
57use crate::trace::{Outcome, RequestSummary, TraceEmitter};
58
59/// Shared state, held once and read by reference from each per-request
60/// task via `Arc<AppState>` — see [`Server::app_state`].
61///
62/// # RFC 071: no `Clone`, deliberately
63///
64/// Before RFC 071, `Server::app_state` was a plain `AppState` behind
65/// `Arc<Mutex<_>>`, and `service()` called `.lock().await.clone()` once
66/// per request — an `O(config size)` deep clone (rule sets included) on
67/// every request, serialised through the lock. Neither half is needed:
68/// nothing mutates `AppState` after startup (see the interior-mutability
69/// note on `Server::app_state`), so a plain `Arc` shares it, and removing
70/// `Clone` here closes the door on silently reintroducing the per-request
71/// deep clone this RFC exists to remove.
72#[non_exhaustive]
73pub struct AppState {
74    pub config: Config,
75    pub middlewares: LoadedMiddlewares,
76    /// Live match-trace channel. Shared across all request handler tasks.
77    pub tracer: TraceEmitter,
78    /// `config.service.fallback_respond_dir`, canonicalised once here
79    /// rather than per request — only the per-request candidate needs
80    /// fresh canonicalisation. `None` if the directory doesn't exist.
81    canonical_fallback_respond_dir: Option<PathBuf>,
82    /// Parallel to `config.service.rule_sets`, same reasoning.
83    canonical_rule_set_respond_dirs: Vec<Option<PathBuf>>,
84}
85
86impl AppState {
87    pub fn new(config: Config, middlewares: LoadedMiddlewares, tracer: TraceEmitter) -> Self {
88        let canonical_fallback_respond_dir =
89            canonical_dir(config.service.fallback_respond_dir.as_str());
90        let canonical_rule_set_respond_dirs = config
91            .service
92            .rule_sets
93            .iter()
94            .map(|rule_set| canonical_dir(rule_set.dir_prefix().as_str()))
95            .collect();
96        Self {
97            config,
98            middlewares,
99            tracer,
100            canonical_fallback_respond_dir,
101            canonical_rule_set_respond_dirs,
102        }
103    }
104}
105
106/// HTTP(S) server.
107#[non_exhaustive]
108pub struct Server {
109    /// RFC 071: shared via `Arc`, not cloned per request. HTTP and HTTPS
110    /// listeners each hold a clone of this `Arc` (a pointer bump, not a
111    /// clone of `AppState`'s contents) rather than each keeping its own
112    /// independent copy of the state as before.
113    pub app_state: Arc<AppState>,
114    pub http_addr: Option<SocketAddr>,
115    pub https_addr: Option<SocketAddr>,
116    /// TLS material, loaded and built once at construction — see
117    /// `Server::new`'s doc comment for why this isn't built lazily
118    /// inside `bind_https` any more (RFC 074 S-08). `None` iff
119    /// `https_addr` is `None`.
120    https_tls: Option<HttpsTls>,
121}
122
123/// Everything `bind_https`/`serve_https` need once TLS material has
124/// been loaded and validated (RFC 074 S-08, S-07).
125#[derive(Clone)]
126struct HttpsTls {
127    acceptor: TlsAcceptor,
128    handshake_timeout: std::time::Duration,
129    max_connections: usize,
130}
131
132impl Server {
133    /// Resolve listener addresses and build the server shell.
134    ///
135    /// Also compiles Rhai middlewares listed in
136    /// `config.service.middlewares_file_paths`. Compilation happens here
137    /// (not in the config crate) because the compiled artefact is a
138    /// runtime object — see the server-level module docstring.
139    ///
140    /// # TLS material is loaded here, eagerly (RFC 074 S-08)
141    ///
142    /// If `[listener.tls]` is present, its cert/key are loaded and the
143    /// TLS config built as part of this call — not lazily, the first
144    /// time `bind_https` runs. A malformed PEM (the file exists —
145    /// `apimock_config::Config::new` already rejected a missing one —
146    /// but doesn't parse) now fails `Server::new` itself, which
147    /// `App::new` propagates with `?`, which `main` propagates with
148    /// `?`: the process exits before `main` ever reaches
149    /// `app.server.start().await`, so *no* listener binds, HTTP
150    /// included. Building this lazily inside `bind_https` — the
151    /// previous behaviour — let `https_start` log the error and return
152    /// while any separately-configured HTTP listener kept serving,
153    /// which is exactly the silent HTTP-only degradation this RFC
154    /// exists to close.
155    pub async fn new(config: Config) -> ServerResult<Self> {
156        let http_addr = resolve_listener(config.listener_http_addr().as_deref())?;
157        let https_addr = resolve_listener(config.listener_https_addr().as_deref())?;
158
159        let https_tls = match https_addr {
160            Some(addr) => Some(build_https_tls(&config, addr)?),
161            None => None,
162        };
163
164        // Resolve middleware paths against the config file's dir
165        let relative_dir_path = config
166            .current_dir_to_parent_dir_relative_path()
167            .map_err(ServerError::Config)?;
168
169        let middleware_max_operations = config
170            .service
171            .middleware_max_operations
172            .unwrap_or(SERVICE_DEFAULT_MIDDLEWARE_MAX_OPERATIONS);
173        let middlewares = LoadedMiddlewares::compile(
174            config
175                .service
176                .middlewares_file_paths
177                .as_deref()
178                .unwrap_or(&[]),
179            relative_dir_path.as_str(),
180            middleware_max_operations,
181        )?;
182        if !middlewares.is_empty() {
183            log::info!("middleware is activated: {} file(s)", middlewares.len());
184        }
185
186        Ok(Server {
187            http_addr,
188            https_addr,
189            https_tls,
190            app_state: Arc::new(AppState::new(config, middlewares, TraceEmitter::new())),
191        })
192    }
193
194    /// Start both listeners (whichever are configured) and block.
195    pub async fn start(&self) {
196        let http = self.http_start();
197        let https = self.https_start();
198        tokio::join!(http, https);
199    }
200
201    /// Bind the HTTP listener without accepting connections yet.
202    ///
203    /// Returns `Ok(None)` if no HTTP listener is configured, `Ok(Some(_))`
204    /// on a successful bind, or `Err` if the bind itself failed — this is
205    /// the piece `http_start` used to swallow via `log::error!` + early
206    /// return, with no way for a caller to observe it.
207    ///
208    /// Splitting bind from serve exists for callers (namely the
209    /// integration-test harness) that need the two to be separate steps:
210    /// bind, read back the real port via `local_addr()` (useful when
211    /// `[listener].port` is `0` and the OS assigns one), *then* hand the
212    /// same listener to [`Server::serve_http`]. Because it's the same
213    /// listener throughout, there is no window between "port known" and
214    /// "port held" for another process to take it.
215    pub async fn bind_http(&self) -> ServerResult<Option<TcpListener>> {
216        let Some(addr) = self.http_addr else {
217            return Ok(None);
218        };
219
220        let listener =
221            TcpListener::bind(addr)
222                .await
223                .map_err(|err| ServerError::ListenerAddress {
224                    addr: addr.to_string(),
225                    reason: err.to_string(),
226                })?;
227
228        Ok(Some(listener))
229    }
230
231    /// Accept connections forever on an already-bound HTTP listener.
232    pub async fn serve_http(&self, listener: TcpListener) {
233        if let Ok(addr) = listener.local_addr() {
234            log::info!(
235                "Greetings from apimock-rs (API Mock) !!\nListening on {} ...\n",
236                style(format!("http://{}", addr)).cyan()
237            );
238        }
239
240        let app_state = Arc::clone(&self.app_state);
241        loop {
242            let (stream, _) = match listener.accept().await {
243                Ok(pair) => pair,
244                Err(err) => {
245                    log::error!("HTTP accept failed: {}", err);
246                    continue;
247                }
248            };
249            let io = TokioIo::new(stream);
250
251            let app_state = Arc::clone(&app_state);
252            tokio::task::spawn(async move {
253                if let Err(err) = Builder::new(TokioExecutor::new())
254                    .serve_connection(
255                        io,
256                        service_fn(move |request: hyper::Request<body::Incoming>| {
257                            service(request, app_state.clone())
258                        }),
259                    )
260                    .await
261                {
262                    log::error!("{} to build connection: {:?}", style("failed").red(), err);
263                }
264            });
265        }
266    }
267
268    async fn http_start(&self) {
269        match self.bind_http().await {
270            Ok(Some(listener)) => self.serve_http(listener).await,
271            Ok(None) => (),
272            Err(err) => log::error!("{}", err),
273        }
274    }
275
276    /// Bind the HTTPS listener without accepting connections yet. See
277    /// [`Server::bind_http`] for why this is split from serving.
278    ///
279    /// TLS material is already loaded and validated by this point —
280    /// see [`Server::new`]'s doc comment — so the only failure left
281    /// here is the socket bind itself (e.g. the port is in use).
282    pub async fn bind_https(&self) -> ServerResult<Option<(TcpListener, TlsAcceptor)>> {
283        let (Some(addr), Some(https_tls)) = (self.https_addr, self.https_tls.as_ref()) else {
284            return Ok(None);
285        };
286
287        let listener =
288            TcpListener::bind(addr)
289                .await
290                .map_err(|err| ServerError::ListenerAddress {
291                    addr: addr.to_string(),
292                    reason: err.to_string(),
293                })?;
294
295        Ok(Some((listener, https_tls.acceptor.clone())))
296    }
297
298    /// Accept connections forever on an already-bound HTTPS listener.
299    ///
300    /// # RFC 074 S-07: handshake timeout and connection cap
301    ///
302    /// A connection that opens and never completes its TLS handshake is
303    /// dropped after `handshake_timeout` — previously nothing bounded
304    /// this, so such a connection held its task (and the OS socket)
305    /// forever. Concurrency is bounded by a `Semaphore` sized to
306    /// `max_connections`: a permit is acquired *before* spawning the
307    /// per-connection task, so once `max_connections` connections are
308    /// in flight, `listener.accept()` keeps accepting into the kernel
309    /// backlog but this loop stops handing new connections to the TLS
310    /// handshake until a permit frees — the server recovers as soon as
311    /// existing connections close, rather than needing a restart.
312    pub async fn serve_https(&self, listener: TcpListener, acceptor: TlsAcceptor) {
313        if let Ok(addr) = listener.local_addr() {
314            log::info!(
315                "Greetings from apimock-rs (API Mock) !!\nListening on {} ...\n",
316                style(format!("https://{}", addr)).cyan()
317            );
318        }
319
320        let (handshake_timeout, max_connections) = self
321            .https_tls
322            .as_ref()
323            .map(|t| (t.handshake_timeout, t.max_connections))
324            .unwrap_or((
325                std::time::Duration::from_secs(TLS_DEFAULT_HANDSHAKE_TIMEOUT_SECONDS),
326                TLS_DEFAULT_MAX_CONNECTIONS,
327            ));
328        let connection_slots = Arc::new(Semaphore::new(max_connections));
329
330        let app_state = Arc::clone(&self.app_state);
331        loop {
332            let (stream, _) = match listener.accept().await {
333                Ok(pair) => pair,
334                Err(err) => {
335                    log::error!("HTTPS accept failed: {}", err);
336                    continue;
337                }
338            };
339            let Ok(permit) = Arc::clone(&connection_slots).acquire_owned().await else {
340                // Semaphore only closes if `close()` is called, which
341                // nothing here does — unreachable in practice, but the
342                // accept loop should not panic on it.
343                continue;
344            };
345            let acceptor = acceptor.clone();
346            let app_state = Arc::clone(&app_state);
347
348            tokio::spawn(async move {
349                let _permit = permit; // held for the connection's lifetime
350                let tls_stream =
351                    match tokio::time::timeout(handshake_timeout, acceptor.accept(stream)).await {
352                        Ok(Ok(s)) => s,
353                        Ok(Err(e)) => {
354                            log::error!("TLS handshake failed: {:?}", e);
355                            return;
356                        }
357                        Err(_elapsed) => {
358                            log::error!(
359                                "TLS handshake timed out after {:?}; dropping connection",
360                                handshake_timeout
361                            );
362                            return;
363                        }
364                    };
365                let io = TokioIo::new(tls_stream);
366                let app_state = app_state.clone();
367                tokio::task::spawn(async move {
368                    if let Err(err) = Builder::new(TokioExecutor::new())
369                        .serve_connection(
370                            io,
371                            service_fn(move |request: hyper::Request<body::Incoming>| {
372                                service(request, app_state.clone())
373                            }),
374                        )
375                        .await
376                    {
377                        log::error!("{} to build connection: {:?}", style("failed").red(), err);
378                    }
379                });
380            });
381        }
382    }
383
384    async fn https_start(&self) {
385        match self.bind_https().await {
386            Ok(Some((listener, acceptor))) => self.serve_https(listener, acceptor).await,
387            Ok(None) => (),
388            Err(err) => log::error!("{}", err),
389        }
390    }
391}
392
393/// Load TLS material and build the acceptor + S-07 settings for the
394/// HTTPS listener. Called once, eagerly, from [`Server::new`] — see
395/// its doc comment for why this doesn't happen lazily in `bind_https`
396/// any more.
397fn build_https_tls(config: &Config, addr: SocketAddr) -> ServerResult<HttpsTls> {
398    let tls = config
399        .listener
400        .as_ref()
401        .and_then(|l| l.tls.as_ref())
402        .cloned()
403        .ok_or_else(|| ServerError::ListenerAddress {
404            addr: addr.to_string(),
405            reason: "internal: HTTPS listener scheduled without TLS config".to_owned(),
406        })?;
407
408    let certs = load_certs(tls.cert.as_str())?;
409    let key = load_private_key(tls.key.as_str())?;
410
411    // RFC 020: use a reloadable resolver so TlsCertFile / TlsKeyFile
412    // changes are SoftReload (no listener rebind needed).
413    let (tls_config, resolver) =
414        build_server_config_reloadable(certs, key).map_err(|err| ServerError::ListenerAddress {
415            addr: addr.to_string(),
416            reason: format!("failed to build TLS config: {}", err),
417        })?;
418    let acceptor = TlsAcceptor::from(Arc::new(tls_config));
419    drop(resolver); // Server holds the resolver via the config; expose via ServerHandle if needed
420
421    let handshake_timeout = std::time::Duration::from_secs(
422        tls.handshake_timeout_seconds
423            .unwrap_or(TLS_DEFAULT_HANDSHAKE_TIMEOUT_SECONDS),
424    );
425    let max_connections = tls.max_connections.unwrap_or(TLS_DEFAULT_MAX_CONNECTIONS);
426
427    Ok(HttpsTls {
428        acceptor,
429        handshake_timeout,
430        max_connections,
431    })
432}
433
434/// Resolve an `ip:port` string into a single `SocketAddr`.
435fn resolve_listener(addr_str: Option<&str>) -> ServerResult<Option<SocketAddr>> {
436    let Some(addr_str) = addr_str else {
437        return Ok(None);
438    };
439
440    let mut addrs = addr_str
441        .to_socket_addrs()
442        .map_err(|e| ServerError::ListenerAddress {
443            addr: addr_str.to_owned(),
444            reason: e.to_string(),
445        })?;
446
447    addrs
448        .next()
449        .map(Some)
450        .ok_or_else(|| ServerError::ListenerAddress {
451            addr: addr_str.to_owned(),
452            reason: "address resolved to no socket addresses".to_owned(),
453        })
454}
455
456/// Entry point for each HTTP request.
457///
458/// # Routing order
459///
460/// OPTIONS → middleware → rule sets → dyn_route (fallback). See
461/// `respond_response` and `dyn_route_content` for each step's details.
462pub async fn service(
463    request: hyper::Request<body::Incoming>,
464    app_state: Arc<AppState>,
465) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
466    let request_headers = request.headers().clone();
467
468    // RFC 071: fields read through the shared `Arc` rather than cloned
469    // out of a lock-guarded owned copy — see `AppState`'s doc comment.
470    // `cors_allow_credentials_origins` still needs an owned `Vec<String>`
471    // below (moved twice — the OPTIONS early return, and payload/ISE
472    // error responses) so it's cloned once here as before; everything
473    // else is used by reference for the rest of this function.
474    let config = &app_state.config;
475    let middlewares = &app_state.middlewares;
476    let tracer = &app_state.tracer;
477
478    let cors_allow_credentials_origins = config
479        .service
480        .cors_allow_credentials_origins
481        .clone()
482        .unwrap_or_default();
483
484    if request.method() == hyper::Method::OPTIONS {
485        return handle_options(&request_headers, &cors_allow_credentials_origins);
486    }
487
488    let max_request_body_bytes = config
489        .service
490        .max_request_body_bytes
491        .unwrap_or(SERVICE_DEFAULT_MAX_REQUEST_BODY_BYTES);
492    let max_request_body_bytes = usize::try_from(max_request_body_bytes).unwrap_or(usize::MAX);
493
494    let parsed_request = match parsed_request_from(request, max_request_body_bytes).await {
495        Ok(x) => x,
496        Err(ParsedRequestError::BodyTooLarge) => {
497            return payload_too_large_response(
498                &format!(
499                    "request body exceeds the configured limit ({} bytes)",
500                    max_request_body_bytes
501                ),
502                &request_headers,
503                &cors_allow_credentials_origins,
504            );
505        }
506        Err(ParsedRequestError::Other(err)) => {
507            return internal_server_error_response(
508                err.as_str(),
509                &request_headers,
510                &cors_allow_credentials_origins,
511            );
512        }
513    };
514
515    let received_at_ms = std::time::SystemTime::now()
516        .duration_since(std::time::UNIX_EPOCH)
517        .unwrap_or_default()
518        .as_millis() as u64;
519    let start = std::time::Instant::now();
520
521    capture_in_log_with_trace_config(
522        &parsed_request,
523        config.log.clone().unwrap_or_default().verbose,
524        &tracer.config,
525    );
526
527    if let Some(response) = middleware_response(
528        middlewares,
529        &parsed_request,
530        &cors_allow_credentials_origins,
531    )
532    .await
533    {
534        return response;
535    }
536
537    if let Some(response) = rule_set_response(
538        config,
539        &parsed_request,
540        app_state.canonical_rule_set_respond_dirs.as_slice(),
541        &cors_allow_credentials_origins,
542    )
543    .await
544    {
545        // Emit trace event on match.
546        if tracer.has_subscribers() {
547            let headers = parsed_request
548                .component_parts
549                .headers
550                .iter()
551                .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_owned())))
552                .collect();
553            let mut summary = RequestSummary::new(
554                parsed_request.component_parts.method.to_string(),
555                parsed_request.url_path.clone(),
556                headers,
557                parsed_request.body_len,
558                &tracer.config,
559            );
560            tracer.enrich_with_body(&mut summary, parsed_request.body_json.as_ref());
561            tracer.emit(
562                received_at_ms,
563                start.elapsed().as_millis() as u32,
564                summary,
565                Outcome::Miss { status: 0 }, // coarse-grained; fine-grained tracing is a future pass
566            );
567        }
568        return response;
569    }
570
571    dyn_route_content(
572        parsed_request.url_path.as_str(),
573        config.service.fallback_respond_dir.as_str(),
574        &request_headers,
575        app_state.canonical_fallback_respond_dir.as_deref(),
576        &cors_allow_credentials_origins,
577    )
578    .await
579}
580
581/// Dispatch the request through every loaded middleware in order.
582async fn middleware_response(
583    middlewares: &LoadedMiddlewares,
584    parsed_request: &ParsedRequest,
585    cors_allow_credentials_origins: &[String],
586) -> Option<Result<hyper::Response<BoxBody>, hyper::http::Error>> {
587    for handler in middlewares.iter() {
588        match handler
589            .handle(
590                parsed_request.url_path.as_str(),
591                parsed_request.body_json.as_ref(),
592                &parsed_request.component_parts.headers,
593                cors_allow_credentials_origins,
594            )
595            .await
596        {
597            Some(x) => return Some(x),
598            None => continue,
599        }
600    }
601    None
602}
603
604/// Dispatch through the configured rule sets.
605///
606/// `canonical_rule_set_respond_dirs` is parallel to
607/// `config.service.rule_sets` — see `AppState::new`.
608async fn rule_set_response(
609    config: &Config,
610    parsed_request: &ParsedRequest,
611    canonical_rule_set_respond_dirs: &[Option<PathBuf>],
612    cors_allow_credentials_origins: &[String],
613) -> Option<Result<hyper::Response<BoxBody>, hyper::http::Error>> {
614    for (rule_set_idx, rule_set) in config.service.rule_sets.iter().enumerate() {
615        if let Some((_rule_idx, respond)) = rule_set.find_matched(
616            parsed_request,
617            config.service.strategy.as_ref(),
618            rule_set_idx,
619        ) {
620            let dir_prefix = rule_set.dir_prefix();
621            let rule_set_default_delay_ms = rule_set
622                .default
623                .as_ref()
624                .and_then(|default| default.delay_response_milliseconds);
625            let confine_to = canonical_rule_set_respond_dirs
626                .get(rule_set_idx)
627                .and_then(|dir| dir.as_deref());
628            return Some(
629                respond_response(
630                    &respond,
631                    dir_prefix.as_str(),
632                    parsed_request,
633                    rule_set_default_delay_ms,
634                    confine_to,
635                    cors_allow_credentials_origins,
636                )
637                .await,
638            );
639        }
640    }
641    None
642}
643
644/// OPTIONS request handler (CORS preflight). `pub` so `apimock get`
645/// (RFC 055) can answer for an `OPTIONS` request through the exact same
646/// function `service` calls, rather than reimplementing it.
647pub fn handle_options(
648    request_headers: &HeaderMap,
649    cors_allow_credentials_origins: &[String],
650) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
651    let mut response = Response::new(Empty::new().boxed());
652    *response.status_mut() = hyper::StatusCode::NO_CONTENT;
653    response
654        .headers_mut()
655        .insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
656
657    for (header_key, header_value) in
658        default_response_headers(request_headers, cors_allow_credentials_origins).into_iter()
659    {
660        if let Some(header_key) = header_key {
661            response.headers_mut().insert(header_key, header_value);
662        }
663    }
664
665    Ok(response)
666}