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_routing::ParsedRequest;
18use console::style;
19use http_body_util::{BodyExt, Empty};
20use hyper::{
21    HeaderMap, Response, body,
22    header::{CONTENT_LENGTH, HeaderValue},
23    service::service_fn,
24};
25use hyper_util::{
26    rt::{TokioExecutor, TokioIo},
27    server::conn::auto::Builder,
28};
29use tokio::net::TcpListener;
30use tokio::sync::Mutex;
31use tokio_rustls::TlsAcceptor;
32
33use std::net::{SocketAddr, ToSocketAddrs};
34use std::path::PathBuf;
35use std::sync::Arc;
36
37use crate::{
38    dyn_route::dyn_route_content,
39    error::{ServerError, ServerResult},
40    middleware::LoadedMiddlewares,
41    parsed_request::{capture_in_log_with_trace_config, parsed_request_from},
42    respond_response::respond_response,
43    response::{confine::canonical_dir, error_response::internal_server_error_response},
44    response_handler::default_response_headers,
45    tls::{build_server_config_reloadable, load_certs, load_private_key},
46    types::BoxBody,
47};
48
49pub use crate::control::{ReloadHint, ServerControl, ServerHandle, ServerState};
50use crate::trace::{Outcome, RequestSummary, TraceEmitter};
51
52/// Shared state cloned into each per-request task.
53#[derive(Clone)]
54#[non_exhaustive]
55pub struct AppState {
56    pub config: Config,
57    pub middlewares: LoadedMiddlewares,
58    /// Live match-trace channel. Shared across all request handler tasks.
59    pub tracer: TraceEmitter,
60    /// `config.service.fallback_respond_dir`, canonicalised once here
61    /// rather than per request — only the per-request candidate needs
62    /// fresh canonicalisation. `None` if the directory doesn't exist.
63    canonical_fallback_respond_dir: Option<PathBuf>,
64    /// Parallel to `config.service.rule_sets`, same reasoning.
65    canonical_rule_set_respond_dirs: Vec<Option<PathBuf>>,
66}
67
68impl AppState {
69    pub fn new(config: Config, middlewares: LoadedMiddlewares, tracer: TraceEmitter) -> Self {
70        let canonical_fallback_respond_dir =
71            canonical_dir(config.service.fallback_respond_dir.as_str());
72        let canonical_rule_set_respond_dirs = config
73            .service
74            .rule_sets
75            .iter()
76            .map(|rule_set| canonical_dir(rule_set.dir_prefix().as_str()))
77            .collect();
78        Self {
79            config,
80            middlewares,
81            tracer,
82            canonical_fallback_respond_dir,
83            canonical_rule_set_respond_dirs,
84        }
85    }
86}
87
88/// HTTP(S) server.
89#[non_exhaustive]
90pub struct Server {
91    pub app_state: AppState,
92    pub http_addr: Option<SocketAddr>,
93    pub https_addr: Option<SocketAddr>,
94}
95
96impl Server {
97    /// Resolve listener addresses and build the server shell.
98    ///
99    /// Also compiles Rhai middlewares listed in
100    /// `config.service.middlewares_file_paths`. Compilation happens here
101    /// (not in the config crate) because the compiled artefact is a
102    /// runtime object — see the server-level module docstring.
103    pub async fn new(config: Config) -> ServerResult<Self> {
104        let http_addr = resolve_listener(config.listener_http_addr().as_deref())?;
105        let https_addr = resolve_listener(config.listener_https_addr().as_deref())?;
106
107        // Resolve middleware paths against the config file's dir
108        let relative_dir_path = config
109            .current_dir_to_parent_dir_relative_path()
110            .map_err(ServerError::Config)?;
111
112        let middlewares = LoadedMiddlewares::compile(
113            config
114                .service
115                .middlewares_file_paths
116                .as_deref()
117                .unwrap_or(&[]),
118            relative_dir_path.as_str(),
119        )?;
120        if !middlewares.is_empty() {
121            log::info!("middleware is activated: {} file(s)", middlewares.len());
122        }
123
124        Ok(Server {
125            http_addr,
126            https_addr,
127            app_state: AppState::new(config, middlewares, TraceEmitter::new()),
128        })
129    }
130
131    /// Start both listeners (whichever are configured) and block.
132    pub async fn start(&self) {
133        let http = self.http_start();
134        let https = self.https_start();
135        tokio::join!(http, https);
136    }
137
138    /// Bind the HTTP listener without accepting connections yet.
139    ///
140    /// Returns `Ok(None)` if no HTTP listener is configured, `Ok(Some(_))`
141    /// on a successful bind, or `Err` if the bind itself failed — this is
142    /// the piece `http_start` used to swallow via `log::error!` + early
143    /// return, with no way for a caller to observe it.
144    ///
145    /// Splitting bind from serve exists for callers (namely the
146    /// integration-test harness) that need the two to be separate steps:
147    /// bind, read back the real port via `local_addr()` (useful when
148    /// `[listener].port` is `0` and the OS assigns one), *then* hand the
149    /// same listener to [`Server::serve_http`]. Because it's the same
150    /// listener throughout, there is no window between "port known" and
151    /// "port held" for another process to take it.
152    pub async fn bind_http(&self) -> ServerResult<Option<TcpListener>> {
153        let Some(addr) = self.http_addr else {
154            return Ok(None);
155        };
156
157        let listener =
158            TcpListener::bind(addr)
159                .await
160                .map_err(|err| ServerError::ListenerAddress {
161                    addr: addr.to_string(),
162                    reason: err.to_string(),
163                })?;
164
165        Ok(Some(listener))
166    }
167
168    /// Accept connections forever on an already-bound HTTP listener.
169    pub async fn serve_http(&self, listener: TcpListener) {
170        if let Ok(addr) = listener.local_addr() {
171            log::info!(
172                "Greetings from apimock-rs (API Mock) !!\nListening on {} ...\n",
173                style(format!("http://{}", addr)).cyan()
174            );
175        }
176
177        let app_state = Arc::new(Mutex::new(self.app_state.clone()));
178        loop {
179            let (stream, _) = match listener.accept().await {
180                Ok(pair) => pair,
181                Err(err) => {
182                    log::error!("HTTP accept failed: {}", err);
183                    continue;
184                }
185            };
186            let io = TokioIo::new(stream);
187
188            let app_state = app_state.clone();
189            tokio::task::spawn(async move {
190                if let Err(err) = Builder::new(TokioExecutor::new())
191                    .serve_connection(
192                        io,
193                        service_fn(move |request: hyper::Request<body::Incoming>| {
194                            service(request, app_state.clone())
195                        }),
196                    )
197                    .await
198                {
199                    log::error!("{} to build connection: {:?}", style("failed").red(), err);
200                }
201            });
202        }
203    }
204
205    async fn http_start(&self) {
206        match self.bind_http().await {
207            Ok(Some(listener)) => self.serve_http(listener).await,
208            Ok(None) => (),
209            Err(err) => log::error!("{}", err),
210        }
211    }
212
213    /// Bind the HTTPS listener (including loading TLS material) without
214    /// accepting connections yet. See [`Server::bind_http`] for why this
215    /// is split from serving.
216    ///
217    /// Every failure this used to swallow via `log::error!` + early
218    /// return - missing TLS config, unreadable cert/key, a TLS config
219    /// that fails to build, or the bind itself - now surfaces as `Err`.
220    pub async fn bind_https(&self) -> ServerResult<Option<(TcpListener, TlsAcceptor)>> {
221        let Some(addr) = self.https_addr else {
222            return Ok(None);
223        };
224
225        let tls = self
226            .app_state
227            .config
228            .listener
229            .as_ref()
230            .and_then(|l| l.tls.as_ref())
231            .cloned()
232            .ok_or_else(|| ServerError::ListenerAddress {
233                addr: addr.to_string(),
234                reason: "internal: HTTPS listener scheduled without TLS config".to_owned(),
235            })?;
236
237        let certs = load_certs(tls.cert.as_str())?;
238        let key = load_private_key(tls.key.as_str())?;
239
240        // RFC 020: use a reloadable resolver so TlsCertFile / TlsKeyFile
241        // changes are SoftReload (no listener rebind needed).
242        let (tls_config, resolver) = build_server_config_reloadable(certs, key).map_err(|err| {
243            ServerError::ListenerAddress {
244                addr: addr.to_string(),
245                reason: format!("failed to build TLS config: {}", err),
246            }
247        })?;
248        let acceptor = TlsAcceptor::from(Arc::new(tls_config));
249        drop(resolver); // Server holds the resolver via the config; expose via ServerHandle if needed
250
251        let listener =
252            TcpListener::bind(addr)
253                .await
254                .map_err(|err| ServerError::ListenerAddress {
255                    addr: addr.to_string(),
256                    reason: err.to_string(),
257                })?;
258
259        Ok(Some((listener, acceptor)))
260    }
261
262    /// Accept connections forever on an already-bound HTTPS listener.
263    pub async fn serve_https(&self, listener: TcpListener, acceptor: TlsAcceptor) {
264        if let Ok(addr) = listener.local_addr() {
265            log::info!(
266                "Greetings from apimock-rs (API Mock) !!\nListening on {} ...\n",
267                style(format!("https://{}", addr)).cyan()
268            );
269        }
270
271        let app_state = Arc::new(Mutex::new(self.app_state.clone()));
272        loop {
273            let (stream, _) = match listener.accept().await {
274                Ok(pair) => pair,
275                Err(err) => {
276                    log::error!("HTTPS accept failed: {}", err);
277                    continue;
278                }
279            };
280            let acceptor = acceptor.clone();
281            let app_state = app_state.clone();
282
283            tokio::spawn(async move {
284                let tls_stream = match acceptor.accept(stream).await {
285                    Ok(s) => s,
286                    Err(e) => {
287                        log::error!("TLS handshake failed: {:?}", e);
288                        return;
289                    }
290                };
291                let io = TokioIo::new(tls_stream);
292                let app_state = app_state.clone();
293                tokio::task::spawn(async move {
294                    if let Err(err) = Builder::new(TokioExecutor::new())
295                        .serve_connection(
296                            io,
297                            service_fn(move |request: hyper::Request<body::Incoming>| {
298                                service(request, app_state.clone())
299                            }),
300                        )
301                        .await
302                    {
303                        log::error!("{} to build connection: {:?}", style("failed").red(), err);
304                    }
305                });
306            });
307        }
308    }
309
310    async fn https_start(&self) {
311        match self.bind_https().await {
312            Ok(Some((listener, acceptor))) => self.serve_https(listener, acceptor).await,
313            Ok(None) => (),
314            Err(err) => log::error!("{}", err),
315        }
316    }
317}
318
319/// Resolve an `ip:port` string into a single `SocketAddr`.
320fn resolve_listener(addr_str: Option<&str>) -> ServerResult<Option<SocketAddr>> {
321    let Some(addr_str) = addr_str else {
322        return Ok(None);
323    };
324
325    let mut addrs = addr_str
326        .to_socket_addrs()
327        .map_err(|e| ServerError::ListenerAddress {
328            addr: addr_str.to_owned(),
329            reason: e.to_string(),
330        })?;
331
332    addrs
333        .next()
334        .map(Some)
335        .ok_or_else(|| ServerError::ListenerAddress {
336            addr: addr_str.to_owned(),
337            reason: "address resolved to no socket addresses".to_owned(),
338        })
339}
340
341/// Entry point for each HTTP request.
342///
343/// # Routing order
344///
345/// OPTIONS → middleware → rule sets → dyn_route (fallback). See
346/// `respond_response` and `dyn_route_content` for each step's details.
347pub async fn service(
348    request: hyper::Request<body::Incoming>,
349    app_state: Arc<Mutex<AppState>>,
350) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
351    let request_headers = request.headers().clone();
352
353    if request.method() == hyper::Method::OPTIONS {
354        return handle_options(&request_headers);
355    }
356
357    let parsed_request = match parsed_request_from(request).await {
358        Ok(x) => x,
359        Err(err) => return internal_server_error_response(err.as_str(), &request_headers),
360    };
361
362    let shared_app_state = { app_state.lock().await.clone() };
363
364    let config = shared_app_state.config;
365    let middlewares = shared_app_state.middlewares;
366    let tracer = shared_app_state.tracer;
367    let canonical_fallback_respond_dir = shared_app_state.canonical_fallback_respond_dir;
368    let canonical_rule_set_respond_dirs = shared_app_state.canonical_rule_set_respond_dirs;
369
370    let received_at_ms = std::time::SystemTime::now()
371        .duration_since(std::time::UNIX_EPOCH)
372        .unwrap_or_default()
373        .as_millis() as u64;
374    let start = std::time::Instant::now();
375
376    capture_in_log_with_trace_config(
377        &parsed_request,
378        config.log.clone().unwrap_or_default().verbose,
379        &tracer.config,
380    );
381
382    if let Some(response) = middleware_response(&middlewares, &parsed_request).await {
383        return response;
384    }
385
386    if let Some(response) = rule_set_response(
387        &config,
388        &parsed_request,
389        canonical_rule_set_respond_dirs.as_slice(),
390    )
391    .await
392    {
393        // Emit trace event on match.
394        if tracer.has_subscribers() {
395            let headers = parsed_request
396                .component_parts
397                .headers
398                .iter()
399                .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_owned())))
400                .collect();
401            let mut summary = RequestSummary::new(
402                parsed_request.component_parts.method.to_string(),
403                parsed_request.url_path.clone(),
404                headers,
405                parsed_request.body_len,
406                &tracer.config,
407            );
408            tracer.enrich_with_body(&mut summary, parsed_request.body_json.as_ref());
409            tracer.emit(
410                received_at_ms,
411                start.elapsed().as_millis() as u32,
412                summary,
413                Outcome::Miss { status: 0 }, // coarse-grained; fine-grained tracing is a future pass
414            );
415        }
416        return response;
417    }
418
419    dyn_route_content(
420        parsed_request.url_path.as_str(),
421        config.service.fallback_respond_dir.as_str(),
422        &request_headers,
423        canonical_fallback_respond_dir.as_deref(),
424    )
425    .await
426}
427
428/// Dispatch the request through every loaded middleware in order.
429async fn middleware_response(
430    middlewares: &LoadedMiddlewares,
431    parsed_request: &ParsedRequest,
432) -> Option<Result<hyper::Response<BoxBody>, hyper::http::Error>> {
433    for handler in middlewares.iter() {
434        match handler
435            .handle(
436                parsed_request.url_path.as_str(),
437                parsed_request.body_json.as_ref(),
438                &parsed_request.component_parts.headers,
439            )
440            .await
441        {
442            Some(x) => return Some(x),
443            None => continue,
444        }
445    }
446    None
447}
448
449/// Dispatch through the configured rule sets.
450///
451/// `canonical_rule_set_respond_dirs` is parallel to
452/// `config.service.rule_sets` — see `AppState::new`.
453async fn rule_set_response(
454    config: &Config,
455    parsed_request: &ParsedRequest,
456    canonical_rule_set_respond_dirs: &[Option<PathBuf>],
457) -> Option<Result<hyper::Response<BoxBody>, hyper::http::Error>> {
458    for (rule_set_idx, rule_set) in config.service.rule_sets.iter().enumerate() {
459        if let Some((_rule_idx, respond)) = rule_set.find_matched(
460            parsed_request,
461            config.service.strategy.as_ref(),
462            rule_set_idx,
463        ) {
464            let dir_prefix = rule_set.dir_prefix();
465            let rule_set_default_delay_ms = rule_set
466                .default
467                .as_ref()
468                .and_then(|default| default.delay_response_milliseconds);
469            let confine_to = canonical_rule_set_respond_dirs
470                .get(rule_set_idx)
471                .and_then(|dir| dir.as_deref());
472            return Some(
473                respond_response(
474                    &respond,
475                    dir_prefix.as_str(),
476                    parsed_request,
477                    rule_set_default_delay_ms,
478                    confine_to,
479                )
480                .await,
481            );
482        }
483    }
484    None
485}
486
487/// OPTIONS request handler (CORS preflight). `pub` so `apimock get`
488/// (RFC 055) can answer for an `OPTIONS` request through the exact same
489/// function `service` calls, rather than reimplementing it.
490pub fn handle_options(
491    request_headers: &HeaderMap,
492) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
493    let mut response = Response::new(Empty::new().boxed());
494    *response.status_mut() = hyper::StatusCode::NO_CONTENT;
495    response
496        .headers_mut()
497        .insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
498
499    for (header_key, header_value) in default_response_headers(request_headers).into_iter() {
500        if let Some(header_key) = header_key {
501            response.headers_mut().insert(header_key, header_value);
502        }
503    }
504
505    Ok(response)
506}