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    async fn http_start(&self) {
114        let Some(addr) = self.http_addr else {
115            return;
116        };
117
118        let listener = match TcpListener::bind(addr).await {
119            Ok(l) => l,
120            Err(err) => {
121                log::error!("failed to bind HTTP listener at {}: {}", addr, err);
122                return;
123            }
124        };
125
126        log::info!(
127            "Greetings from apimock-rs (API Mock) !!\nListening on {} ...\n",
128            style(format!("http://{}", addr)).cyan()
129        );
130
131        let app_state = Arc::new(Mutex::new(self.app_state.clone()));
132        loop {
133            let (stream, _) = match listener.accept().await {
134                Ok(pair) => pair,
135                Err(err) => {
136                    log::error!("HTTP accept failed: {}", err);
137                    continue;
138                }
139            };
140            let io = TokioIo::new(stream);
141
142            let app_state = app_state.clone();
143            tokio::task::spawn(async move {
144                if let Err(err) = Builder::new(TokioExecutor::new())
145                    .serve_connection(
146                        io,
147                        service_fn(move |request: hyper::Request<body::Incoming>| {
148                            service(request, app_state.clone())
149                        }),
150                    )
151                    .await
152                {
153                    log::error!("{} to build connection: {:?}", style("failed").red(), err);
154                }
155            });
156        }
157    }
158
159    async fn https_start(&self) {
160        let Some(addr) = self.https_addr else {
161            return;
162        };
163
164        let tls = match self
165            .app_state
166            .config
167            .listener
168            .as_ref()
169            .and_then(|l| l.tls.as_ref())
170        {
171            Some(t) => t.clone(),
172            None => {
173                log::error!("internal: HTTPS listener scheduled without TLS config");
174                return;
175            }
176        };
177
178        let certs = match load_certs(tls.cert.as_str()) {
179            Ok(c) => c,
180            Err(err) => {
181                log::error!("{}", err);
182                return;
183            }
184        };
185        let key = match load_private_key(tls.key.as_str()) {
186            Ok(k) => k,
187            Err(err) => {
188                log::error!("{}", err);
189                return;
190            }
191        };
192
193        // RFC 020: use a reloadable resolver so TlsCertFile / TlsKeyFile
194        // changes are SoftReload (no listener rebind needed).
195        let (tls_config, resolver) = match build_server_config_reloadable(certs, key) {
196            Ok(pair) => pair,
197            Err(err) => {
198                log::error!("failed to build TLS config: {}", err);
199                return;
200            }
201        };
202        let acceptor = TlsAcceptor::from(Arc::new(tls_config));
203        drop(resolver); // Server holds the resolver via the config; expose via ServerHandle if needed
204
205        let listener = match TcpListener::bind(addr).await {
206            Ok(l) => l,
207            Err(err) => {
208                log::error!("failed to bind HTTPS listener at {}: {}", addr, err);
209                return;
210            }
211        };
212
213        log::info!(
214            "Greetings from apimock-rs (API Mock) !!\nListening on {} ...\n",
215            style(format!("https://{}", addr)).cyan()
216        );
217
218        let app_state = Arc::new(Mutex::new(self.app_state.clone()));
219        loop {
220            let (stream, _) = match listener.accept().await {
221                Ok(pair) => pair,
222                Err(err) => {
223                    log::error!("HTTPS accept failed: {}", err);
224                    continue;
225                }
226            };
227            let acceptor = acceptor.clone();
228            let app_state = app_state.clone();
229
230            tokio::spawn(async move {
231                let tls_stream = match acceptor.accept(stream).await {
232                    Ok(s) => s,
233                    Err(e) => {
234                        log::error!("TLS handshake failed: {:?}", e);
235                        return;
236                    }
237                };
238                let io = TokioIo::new(tls_stream);
239                let app_state = app_state.clone();
240                tokio::task::spawn(async move {
241                    if let Err(err) = Builder::new(TokioExecutor::new())
242                        .serve_connection(
243                            io,
244                            service_fn(move |request: hyper::Request<body::Incoming>| {
245                                service(request, app_state.clone())
246                            }),
247                        )
248                        .await
249                    {
250                        log::error!("{} to build connection: {:?}", style("failed").red(), err);
251                    }
252                });
253            });
254        }
255    }
256}
257
258/// Resolve an `ip:port` string into a single `SocketAddr`.
259// clippy: ServerError is a public error type (RFC 030 §6 escalation
260// trigger); boxing its large variant would change that type's shape.
261// See ESCALATION-002 in the RFC 030 review-request package.
262#[allow(clippy::result_large_err)]
263fn resolve_listener(addr_str: Option<&str>) -> ServerResult<Option<SocketAddr>> {
264    let Some(addr_str) = addr_str else {
265        return Ok(None);
266    };
267
268    let mut addrs = addr_str
269        .to_socket_addrs()
270        .map_err(|e| ServerError::ListenerAddress {
271            addr: addr_str.to_owned(),
272            reason: e.to_string(),
273        })?;
274
275    addrs
276        .next()
277        .map(Some)
278        .ok_or_else(|| ServerError::ListenerAddress {
279            addr: addr_str.to_owned(),
280            reason: "address resolved to no socket addresses".to_owned(),
281        })
282}
283
284/// Entry point for each HTTP request.
285///
286/// # Routing order
287///
288/// OPTIONS → middleware → rule sets → dyn_route (fallback). See
289/// `respond_response` and `dyn_route_content` for each step's details.
290pub async fn service(
291    request: hyper::Request<body::Incoming>,
292    app_state: Arc<Mutex<AppState>>,
293) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
294    let request_headers = request.headers().clone();
295
296    if request.method() == hyper::Method::OPTIONS {
297        return handle_options(&request_headers);
298    }
299
300    let parsed_request = match parsed_request_from(request).await {
301        Ok(x) => x,
302        Err(err) => return internal_server_error_response(err.as_str(), &request_headers),
303    };
304
305    let shared_app_state = { app_state.lock().await.clone() };
306
307    let config = shared_app_state.config;
308    let middlewares = shared_app_state.middlewares;
309    let tracer = shared_app_state.tracer;
310
311    let received_at_ms = std::time::SystemTime::now()
312        .duration_since(std::time::UNIX_EPOCH)
313        .unwrap_or_default()
314        .as_millis() as u64;
315    let start = std::time::Instant::now();
316
317    capture_in_log(
318        &parsed_request,
319        config.log.clone().unwrap_or_default().verbose,
320    );
321
322    if let Some(response) = middleware_response(&middlewares, &parsed_request).await {
323        return response;
324    }
325
326    if let Some(response) = rule_set_response(&config, &parsed_request).await {
327        // Emit trace event on match.
328        if tracer.has_subscribers() {
329            let mut summary = RequestSummary {
330                method: parsed_request.component_parts.method.to_string(),
331                url_path: parsed_request.url_path.clone(),
332                headers: parsed_request
333                    .component_parts
334                    .headers
335                    .iter()
336                    .filter_map(|(k, v)| v.to_str().ok().map(|v| (k.to_string(), v.to_owned())))
337                    .collect(),
338                body_json: None,
339                body_truncated: false,
340            };
341            tracer.enrich_with_body(&mut summary, parsed_request.body_json.as_ref());
342            tracer.emit(
343                received_at_ms,
344                start.elapsed().as_millis() as u32,
345                summary,
346                Outcome::Miss { status: 0 }, // coarse-grained; fine-grained tracing is a future pass
347            );
348        }
349        return response;
350    }
351
352    dyn_route_content(
353        parsed_request.url_path.as_str(),
354        config.service.fallback_respond_dir.as_str(),
355        &request_headers,
356    )
357    .await
358}
359
360/// Dispatch the request through every loaded middleware in order.
361async fn middleware_response(
362    middlewares: &LoadedMiddlewares,
363    parsed_request: &ParsedRequest,
364) -> Option<Result<hyper::Response<BoxBody>, hyper::http::Error>> {
365    for handler in middlewares.iter() {
366        match handler
367            .handle(
368                parsed_request.url_path.as_str(),
369                parsed_request.body_json.as_ref(),
370                &parsed_request.component_parts.headers,
371            )
372            .await
373        {
374            Some(x) => return Some(x),
375            None => continue,
376        }
377    }
378    None
379}
380
381/// Dispatch through the configured rule sets.
382async fn rule_set_response(
383    config: &Config,
384    parsed_request: &ParsedRequest,
385) -> Option<Result<hyper::Response<BoxBody>, hyper::http::Error>> {
386    for (rule_set_idx, rule_set) in config.service.rule_sets.iter().enumerate() {
387        if let Some(respond) = rule_set.find_matched(
388            parsed_request,
389            config.service.strategy.as_ref(),
390            rule_set_idx,
391        ) {
392            let dir_prefix = rule_set.dir_prefix();
393            return Some(respond_response(&respond, dir_prefix.as_str(), parsed_request).await);
394        }
395    }
396    None
397}
398
399/// OPTIONS request handler (CORS preflight).
400fn handle_options(
401    request_headers: &HeaderMap,
402) -> Result<hyper::Response<BoxBody>, hyper::http::Error> {
403    let mut response = Response::new(Empty::new().boxed());
404    *response.status_mut() = hyper::StatusCode::NO_CONTENT;
405    response
406        .headers_mut()
407        .insert(CONTENT_LENGTH, HeaderValue::from_static("0"));
408
409    for (header_key, header_value) in default_response_headers(request_headers).into_iter() {
410        if let Some(header_key) = header_key {
411            response.headers_mut().insert(header_key, header_value);
412        }
413    }
414
415    Ok(response)
416}