1/*
  2 * SPDX-FileCopyrightText: 2025 Copyright (c) Contributors to the Eclipse Foundation
  3 *
  4 * See the NOTICE file(s) distributed with this work for additional
  5 * information regarding copyright ownership.
  6 *
  7 * This program and the accompanying materials are made available under the
  8 * terms of the Apache License Version 2.0 which is available at
  9 * https://www.apache.org/licenses/LICENSE-2.0
 10 *
 11 * SPDX-License-Identifier: Apache-2.0
 12 */
 13
 14use std::{
 15    future::Future,
 16    sync::{Arc, atomic::AtomicBool},
 17};
 18
 19use aide::{axum::routing, swagger::Swagger};
 20use axum::{
 21    Json,
 22    http::{self, Request},
 23};
 24use cda_interfaces::{
 25    DoipGatewaySetupError, FunctionalDescriptionConfig, HashMap, SchemaProvider, UdsEcu,
 26    datatypes::ComponentsConfig, dlt_ctx, file_manager::FileManager,
 27};
 28use cda_plugin_security::SecurityPluginLoader;
 29use dynamic_router::DynamicRouter;
 30pub use dynamic_router::{RouteGroupNotFound, RouteHandle};
 31pub use http::Method;
 32use tokio::net::TcpListener;
 33use tower::{Layer, ServiceExt as TowerServiceExt};
 34use tower_http::{normalize_path::NormalizePathLayer, trace::TraceLayer};
 35
 36/// Public API surface re-exported from the crate-internal `sovd` module.
 37pub use crate::sovd::{
 38    EcuExecutionRegistry, SovdLockStateProvider,
 39    apps::sovd2uds::bulk_data::runtimefiles::RuntimeUpdateRouteState,
 40    error::VendorErrorCode,
 41    locks::Locks,
 42    static_data::add_static_data_endpoint,
 43    update_guard::{ExemptRoute, UpdateGuardLayer, UpdateGuardState},
 44};
 45pub mod dynamic_router;
 46mod openapi;
 47pub(crate) mod sovd;
 48
 49// Consts for HTTP
 50pub const SWAGGER_UI_ROUTE: &str = "/swagger-ui";
 51pub const OPENAPI_JSON_ROUTE: &str = "/openapi.json";
 52#[derive(Clone)]
 53pub struct WebServerConfig {
 54    pub host: String,
 55    pub port: u16,
 56}
 57
 58/// Static configuration for vehicle SOVD routes.
 59pub struct VehicleConfig {
 60    pub flash_files_path: String,
 61    pub functional_group_config: FunctionalDescriptionConfig,
 62    pub components_config: ComponentsConfig,
 63}
 64
 65/// Runtime resources (handles, shared state) for vehicle SOVD routes.
 66pub struct VehicleResources<T, M> {
 67    pub ecu_uds: T,
 68    pub file_manager: HashMap<String, M>,
 69    pub locks: Arc<Locks>,
 70    pub update_in_progress: Arc<AtomicBool>,
 71}
 72
[docs] 73/// [[ dimpl~sovd-api-http-server, Starts HTTP Server ]]
 74///
 75/// Launches the http(s) webserver with deferred initialization
 76///
 77/// The server starts immediately with static endpoints. SOVD routes and other functionality
 78/// can be added later by calling methods on the returned `DynamicRouter`.
 79///
 80/// # Errors
 81/// Will return `Err` in case that the webserver couldn't be launched.
 82/// This can be caused due to invalid config, ports or addresses already being in use.
 83///
 84#[tracing::instrument(
 85    skip(config, shutdown_signal),
 86    fields(
 87        host = %config.host,
 88        port = %config.port,
 89    )
 90)]
 91pub async fn launch_webserver<F>(
 92    config: WebServerConfig,
 93    shutdown_signal: F,
 94) -> Result<(DynamicRouter, tokio::task::JoinHandle<()>), DoipGatewaySetupError>
 95where
 96    F: Future<Output = ()> + Clone + Send + 'static,
 97{
 98    let dynamic_router = DynamicRouter::new();
 99    let listen_address = format!("{}:{}", config.host, config.port);
100    let listener = TcpListener::bind(&listen_address).await.map_err(|e| {
101        DoipGatewaySetupError::ServerError(format!("Failed to bind to {listen_address}: {e}"))
102    })?;
103
104    let dynamic_router_for_service = dynamic_router.clone();
105    let webserver_task = cda_interfaces::spawn_named!("webserver", async move {
106        let service = tower::service_fn(move |request: Request<axum::body::Body>| {
107            let dr = dynamic_router_for_service.clone();
108            async move {
109                let router = dr.get_router().await;
110                TowerServiceExt::oneshot(router, request).await
111            }
112        });
113
114        let middleware = tower::util::MapRequestLayer::new(rewrite_request_uri);
115        let trim_trailing_slash_middleware = NormalizePathLayer::trim_trailing_slash();
116        let service_with_middleware =
117            middleware.layer(trim_trailing_slash_middleware.layer(service));
118
119        let _ = axum::serve(listener, tower::make::Shared::new(service_with_middleware))
120            .with_graceful_shutdown(shutdown_signal)
121            .await;
122    });
123
124    Ok((dynamic_router, webserver_task))
125}
126
127/// Add vehicle routes to the dynamic router
128///
129/// This function should be called after the database is loaded to add all vehicle routes
130///
131/// # Errors
132/// Returns `Err` if routes cannot be added to the dynamic router.
133#[allow(
134    clippy::implicit_hasher,
135    reason = "Type alias doesn't allow specifying hasher"
136)]
137#[tracing::instrument(
138    skip(dynamic_router, config, resources),
139    fields(
140        flash_files_path = %config.flash_files_path
141    )
142)]
143pub async fn add_vehicle_routes<T, M, S>(
144    dynamic_router: &DynamicRouter,
145    config: VehicleConfig,
146    resources: VehicleResources<T, M>,
147) -> Result<(EcuExecutionRegistry, RouteHandle), DoipGatewaySetupError>
148where
149    T: UdsEcu + SchemaProvider + Clone + Send + Sync + 'static,
150    M: FileManager + Send + Sync + 'static,
151    S: SecurityPluginLoader,
152{
153    let (vehicle_router, registry) = build_vehicle_routes::<T, M, S>(config, resources).await;
154
155    let handle = dynamic_router.add_routes(vehicle_router).await;
156
157    tracing::info!("Vehicle routes added to webserver");
158    Ok((registry, handle))
159}
160
161#[allow(
162    clippy::implicit_hasher,
163    reason = "Type alias doesn't allow specifying hasher"
164)]
165pub async fn build_vehicle_routes<T, M, S>(
166    config: VehicleConfig,
167    resources: VehicleResources<T, M>,
168) -> (aide::axum::ApiRouter, EcuExecutionRegistry)
169where
170    T: UdsEcu + SchemaProvider + Clone + Send + Sync + 'static,
171    M: FileManager + Send + Sync + 'static,
172    S: SecurityPluginLoader,
173{
174    let (router, registry) = sovd::route::<T, M, S>(
175        config.functional_group_config,
176        config.components_config,
177        &resources.ecu_uds,
178        config.flash_files_path,
179        resources.file_manager,
180        resources.locks,
181        resources.update_in_progress,
182    )
183    .await;
184    (router, registry)
185}
186
187/// Mounts the runtime-update HTTP routes onto the dynamic router and returns a handle to them.
188///
189/// Adds the runtime-file update endpoints to the router, registers exempt routes on the
190/// [`UpdateGuardState`], and logs when the routes become active.
191pub async fn add_runtime_update_routes<S, P, L>(
192    dynamic_router: &DynamicRouter,
193    plugin: Arc<P>,
194    lock_state: Arc<L>,
195    update_guard: &UpdateGuardState,
196    upload_limit: usize,
197    retry_after_seconds: u64,
198) -> RouteHandle
199where
200    S: SecurityPluginLoader,
201    P: cda_interfaces::runtime_update_api::RuntimeFilesUpdatePlugin,
202    L: cda_interfaces::runtime_update_api::LockStateProvider,
203{
204    update_guard
205        .extend_exempt(sovd::apps::sovd2uds::bulk_data::runtimefiles::update_exempt_routes())
206        .await;
207
208    let route_state = RuntimeUpdateRouteState {
209        plugin,
210        vehicle_lock_states: lock_state,
211        retry_after_seconds,
212    };
213    let router =
214        sovd::apps::sovd2uds::bulk_data::runtimefiles::routes::<S, P, L>(route_state, upload_limit);
215    let handle = dynamic_router.add_routes(router.into()).await;
216    tracing::info!("Runtime update routes added to webserver");
217    handle
218}
219
220/// `OpenAPI` spec regenerates on every recomposition, reflecting current routes.
221pub async fn add_openapi_routes(
222    dynamic_router: &DynamicRouter,
223    _update_guard: &UpdateGuardState,
224    web_server_config: &WebServerConfig,
225) {
226    let server_url = format!(
227        "http://{}:{}",
228        web_server_config.host, web_server_config.port
229    );
230    let dr = dynamic_router.clone();
231    dynamic_router
232        .add_finalizer(Arc::new(move |router: axum::Router| -> axum::Router {
233            let server_url = server_url.clone();
234            let dr = dr.clone();
235            let swagger_route: axum::routing::MethodRouter =
236                Swagger::new(OPENAPI_JSON_ROUTE).axum_route().into();
237            let openapi_route: axum::routing::MethodRouter = routing::get(move || async move {
238                let mut api = (*dr.get_openapi().await).clone();
239                let _ =
240                    openapi::api_docs(aide::transform::TransformOpenApi::new(&mut api), server_url);
241                Json(api)
242            })
243            .into();
244            router
245                .route(SWAGGER_UI_ROUTE, swagger_route)
246                .route(OPENAPI_JSON_ROUTE, openapi_route)
247        }))
248        .await;
249}
250
251pub async fn install_update_guard(dynamic_router: &DynamicRouter, update_guard: UpdateGuardState) {
252    let layer = UpdateGuardLayer::new(update_guard);
253    dynamic_router
254        .add_finalizer(Arc::new(move |router: axum::Router| -> axum::Router {
255            router.layer(layer.clone())
256        }))
257        .await;
258}
259
260fn rewrite_request_uri<B>(mut req: Request<B>) -> Request<B> {
261    let uri = req.uri();
262    // Decode URI here, so we can use query params later without
263    // needing to decode them later on.
264    let decoded = percent_encoding::percent_decode_str(
265        uri.path_and_query()
266            .map(http::uri::PathAndQuery::as_str)
267            .unwrap_or_default(),
268    )
269    .decode_utf8()
270    .unwrap_or_else(|_| uri.to_string().into());
271
272    let new_uri = match decoded.to_lowercase().parse() {
273        Ok(uri) => uri,
274        Err(e) => {
275            tracing::warn!(error = %e, "Failed to parse URI, using original");
276            uri.clone()
277        }
278    };
279    *req.uri_mut() = new_uri;
280    req
281}
282
283fn create_trace_layer<S>(route: axum::Router<S>) -> axum::Router<S>
284where
285    S: Clone + Send + Sync + 'static,
286{
287    route.layer(
288        TraceLayer::new_for_http()
289            .make_span_with(|request: &axum::http::Request<_>| {
290                tracing::info_span!(
291                        "request",
292                    method = ?request.method(),
293                        path = request.uri().to_string(),
294                        status_code = tracing::field::Empty,
295                        latency = tracing::field::Empty,
296                        error = tracing::field::Empty,
297                        dlt_context = dlt_ctx!("SOVD"),
298                )
299            })
300            .on_request(|request: &axum::http::Request<_>, _span: &tracing::Span| {
301                tracing::debug!(
302                    method = %request.method(),
303                    path = %request.uri(),
304                    "Request received"
305                );
306            })
307            .on_response(
308                |response: &axum::http::Response<_>,
309                 latency: std::time::Duration,
310                 span: &tracing::Span| {
311                    span.record("status_code", response.status().as_u16());
312                    span.record("latency", format!("{latency:?}"));
313                },
314            )
315            .on_failure(
316                |error: tower_http::classify::ServerErrorsFailureClass,
317                 latency: std::time::Duration,
318                 span: &tracing::Span| {
319                    span.record("latency", format!("{latency:?}"));
320                    if let tower_http::classify::ServerErrorsFailureClass::StatusCode(status) =
321                        error
322                    {
323                        span.record("status_code", status.as_u16());
324                        if status == http::StatusCode::BAD_GATEWAY {
325                            return; // Ignore 502 errors
326                        }
327                    }
328                    span.record("error", error.to_string());
329                    tracing::error!("HTTP request failed");
330                },
331            ),
332    )
333}
334
335#[cfg(test)]
336pub(crate) mod test_utils {
337    use serde::de::DeserializeOwned;
338
339    pub(crate) async fn axum_response_into<T: DeserializeOwned>(
340        response: axum::response::Response,
341    ) -> Result<T, serde_json::Error> {
342        let body = axum::body::to_bytes(response.into_body(), usize::MAX)
343            .await
344            .unwrap();
345        serde_json::from_slice::<T>(body.as_ref())
346    }
347}