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