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