LCOV - code coverage report
Current view: top level - antares-api/src - router.rs (source / functions) Coverage Total Hit
Test: merged.info Lines: 95.9 % 415 398
Test Date: 2026-11-02 13:21:27 Functions: 75.8 % 124 94

            Line data    Source code
       1              : // SPDX-License-Identifier: EUPL-1.2
       2              : //! Router assembly: the NGSI-LD route table, the ops-only router and the request layers (tenant, CORS, metrics, trailing slash, 405/501/OPTIONS).
       3              : 
       4              : use super::*;
       5              : use tower::layer::util::Stack;
       6              : use tower_http::compression::predicate::{And, NotForContentType, Predicate, SizeAbove};
       7              : use tower_http::compression::{CompressionLayer, CompressionLevel};
       8              : 
       9              : /// 4.3.5 NGSI-LD API structure: Core API mandatory; Distributed API mandatory for
      10              : /// distributed/federated deployments; Temporal API and Registry API integrated
      11              : /// locally here (Table 4.3.5-2 row "integrated temporal + integrated Context
      12              : /// Registry"); JSONLDContext API implemented; optional Snapshot API offered
      13              : /// (5.16, resources 6.36-6.38, NGSILD-Snapshot scoping 6.3.22).
      14              : /// Ops-only router: what a pod WITHOUT the api role serves —
      15              : /// health, readiness and metrics, nothing else. The shipped antares-worker
      16              : /// Deployment used to answer the full read/write NGSI-LD API on its pod IP
      17              : /// (and a subscription created there was never KV-synced, because the sync
      18              : /// hooks are wired `if roles.api`); a worker now 404s the API surface.
      19           12 : pub fn ops_router(state: AppState) -> Router {
      20              :     // Admin only, deliberately: a worker widening its surface to whatever a
      21              :     // deployment registered is the same class of accident this router exists
      22              :     // to prevent.
      23           12 :     Router::new()
      24           12 :         .nest(Admin.prefix(), Admin.router(state.clone()))
      25           12 :         .with_state(state)
      26           12 : }
      27              : 
      28              : /// The NGSI-LD API, path by path: `router()` mounts exactly these under the
      29              : /// API root, and the policy seam's own test walks the same list, asking the
      30              : /// router (OPTIONS, the Allow set) which methods each path serves.
      31        18846 : pub fn api_routes() -> Vec<(&'static str, MethodRouter<AppState>)> {
      32        18846 :     vec![
      33              :         // entities (6.4/6.5)
      34        18846 :         (
      35        18846 :             "/entities",
      36        18846 :             post(entities::create_entity)
      37        18846 :                 .get(entities::query_entities)
      38        18846 :                 .delete(entities::purge_entities)
      39        18846 :                 .patch(missing_entity_id)
      40        18846 :                 .put(missing_entity_id),
      41        18846 :         ),
      42        18846 :         (
      43        18846 :             "/entities/{id}",
      44        18846 :             get(entities::retrieve_entity)
      45        18846 :                 .patch(entities::merge_entity)
      46        18846 :                 .put(entities::replace_entity)
      47        18846 :                 .delete(entities::delete_entity),
      48        18846 :         ),
      49              :         // attrs (6.6/6.7)
      50        18846 :         (
      51        18846 :             "/entities/{id}/attrs",
      52        18846 :             post(attrs::append_attrs).patch(attrs::update_attrs),
      53        18846 :         ),
      54        18846 :         (
      55        18846 :             "/entities/{id}/attrs/{attr}",
      56        18846 :             // GET is the 2.0 #14 pre-adoption
      57        18846 :             get(entities::retrieve_entity_attr)
      58        18846 :                 .patch(attrs::partial_update_attr)
      59        18846 :                 .put(attrs::replace_attr)
      60        18846 :                 .delete(attrs::delete_attr),
      61        18846 :         ),
      62              :         // 2.0 #15 pre-adoption: the bare attribute value, and PUT is Set
      63              :         // Attribute Value (TS 104 176 7.6.3.2)
      64        18846 :         (
      65        18846 :             "/entities/{id}/attrs/{attr}/value",
      66        18846 :             get(entities::retrieve_entity_attr_value).put(attrs::set_attr_value),
      67        18846 :         ),
      68              :         // batch (6.14–6.17, 6.23, 6.31)
      69        18846 :         ("/entityOperations/create", post(batch::batch_create)),
      70        18846 :         ("/entityOperations/upsert", post(batch::batch_upsert)),
      71        18846 :         ("/entityOperations/update", post(batch::batch_update)),
      72        18846 :         ("/entityOperations/delete", post(batch::batch_delete)),
      73        18846 :         ("/entityOperations/merge", post(batch::batch_merge)),
      74        18846 :         ("/entityOperations/query", post(batch::batch_query)),
      75              :         // EntityMaps (5.14; 6.32/6.34/6.35)
      76        18846 :         (
      77        18846 :             "/entityMaps",
      78        18846 :             get(entity_maps::create_entity_map).post(entity_maps::create_entity_map_post),
      79        18846 :         ),
      80        18846 :         (
      81        18846 :             "/entityMaps/{id}",
      82        18846 :             get(entity_maps::retrieve_entity_map)
      83        18846 :                 .patch(entity_maps::update_entity_map)
      84        18846 :                 .delete(entity_maps::delete_entity_map),
      85        18846 :         ),
      86        18846 :         (
      87        18846 :             "/temporal/entityMaps",
      88        18846 :             get(entity_maps::create_temporal_entity_map)
      89        18846 :                 .post(entity_maps::create_temporal_entity_map_post),
      90        18846 :         ),
      91              :         // subscriptions (6.10/6.11)
      92        18846 :         (
      93        18846 :             "/subscriptions",
      94        18846 :             post(subscriptions::create_subscription).get(subscriptions::query_subscriptions),
      95        18846 :         ),
      96        18846 :         (
      97        18846 :             "/subscriptions/{id}",
      98        18846 :             get(subscriptions::retrieve_subscription)
      99        18846 :                 .patch(subscriptions::update_subscription)
     100        18846 :                 .delete(subscriptions::delete_subscription),
     101        18846 :         ),
     102              :         // csourceRegistrations (6.8/6.9)
     103        18846 :         (
     104        18846 :             "/csourceRegistrations",
     105        18846 :             post(csource::create_registration).get(csource::query_registrations),
     106        18846 :         ),
     107        18846 :         (
     108        18846 :             "/csourceRegistrations/{id}",
     109        18846 :             get(csource::retrieve_registration)
     110        18846 :                 .patch(csource::update_registration)
     111        18846 :                 .delete(csource::delete_registration),
     112        18846 :         ),
     113              :         // csourceSubscriptions (6.12/6.13)
     114        18846 :         (
     115        18846 :             "/csourceSubscriptions",
     116        18846 :             post(subscriptions::create_csource_subscription)
     117        18846 :                 .get(subscriptions::query_csource_subscriptions),
     118        18846 :         ),
     119        18846 :         (
     120        18846 :             "/csourceSubscriptions/{id}",
     121        18846 :             get(subscriptions::retrieve_csource_subscription)
     122        18846 :                 .patch(subscriptions::update_csource_subscription)
     123        18846 :                 .delete(subscriptions::delete_csource_subscription),
     124        18846 :         ),
     125              :         // temporal (6.18–6.22, 6.24)
     126        18846 :         (
     127        18846 :             "/temporal/entities",
     128        18846 :             post(temporal::upsert_temporal).get(temporal::query_temporal),
     129        18846 :         ),
     130        18846 :         (
     131        18846 :             "/temporal/entities/{id}",
     132        18846 :             get(temporal::retrieve_temporal).delete(temporal::delete_temporal),
     133        18846 :         ),
     134        18846 :         (
     135        18846 :             "/temporal/entities/{id}/attrs",
     136        18846 :             post(temporal::add_temporal_attrs),
     137        18846 :         ),
     138        18846 :         (
     139        18846 :             "/temporal/entities/{id}/attrs/{attr}",
     140        18846 :             delete(temporal::delete_temporal_attr),
     141        18846 :         ),
     142        18846 :         (
     143        18846 :             "/temporal/entities/{id}/attrs/{attr}/{instance}",
     144        18846 :             patch(temporal::modify_temporal_instance).delete(temporal::delete_temporal_instance),
     145        18846 :         ),
     146        18846 :         (
     147        18846 :             "/temporal/entityOperations/query",
     148        18846 :             post(temporal::batch_temporal_query),
     149        18846 :         ),
     150              :         // discovery (6.25–6.28)
     151        18846 :         ("/types", get(types_attrs::entity_types)),
     152        18846 :         ("/types/{type}", get(types_attrs::entity_type_info)),
     153        18846 :         ("/attributes", get(types_attrs::attributes)),
     154        18846 :         ("/attributes/{attr}", get(types_attrs::attribute_info)),
     155              :         // jsonldContexts (6.29/6.30)
     156        18846 :         (
     157        18846 :             "/jsonldContexts",
     158        18846 :             post(contexts::add_context).get(contexts::list_contexts),
     159        18846 :         ),
     160        18846 :         (
     161        18846 :             "/jsonldContexts/{id}",
     162        18846 :             get(contexts::serve_context).delete(contexts::delete_context),
     163        18846 :         ),
     164              :         // snapshots (5.16; 6.36-6.38, optional API group — offered)
     165        18846 :         (
     166        18846 :             "/snapshots",
     167        18846 :             post(snapshots::create_snapshot).delete(snapshots::purge_snapshots),
     168        18846 :         ),
     169        18846 :         (
     170        18846 :             "/snapshots/{id}",
     171        18846 :             get(snapshots::retrieve_snapshot)
     172        18846 :                 .patch(snapshots::update_snapshot)
     173        18846 :                 .delete(snapshots::delete_snapshot),
     174        18846 :         ),
     175        18846 :         ("/snapshots/{id}/clone", post(snapshots::clone_snapshot)),
     176              :         // info (6.33)
     177        18846 :         ("/info/sourceIdentity", get(source_identity)),
     178              :     ]
     179        18846 : }
     180              : 
     181              : /// The broker's whole HTTP surface, under trailing-slash normalization: every
     182              : /// host of it (the binary, the browser build, an in-process forward, a test)
     183              : /// serves `/entities/{id}/attrs/` as `/entities/{id}/attrs`, the form 5.6.2
     184              : /// and 5.6.3 forwards are sent in.
     185        18840 : pub fn router(state: AppState) -> Router {
     186        18840 :     let served = routes(state);
     187        18840 :     Router::new().fallback_service(tower::util::MapRequest::new(
     188        18840 :         served,
     189              :         trim_trailing_slash::<axum::body::Body>,
     190              :     ))
     191        18840 : }
     192              : 
     193              : /// gzip or br for a client whose Accept-Encoding asks for it, laid
     194              : /// over [`router()`] by the binary that serves the network and by nothing else:
     195              : /// an in-process call (`AppState::call`, a forward) parses the body it gets
     196              : /// back and must read plain JSON, and each nested frame would grow the stack
     197              : /// a façade chain spends. A body that already has a Content-Encoding or a
     198              : /// Content-Range is never touched, and `Vary: Accept-Encoding` is appended
     199              : /// once to whatever Vary the handler set. Request bodies stay as sent: no
     200              : /// decompression, so the body bound holds.
     201              : ///
     202              : /// Levels (the best size per CPU on real answers): brotli quality 2
     203              : /// and gzip level 1, each its own layer because tower-http gives one layer one
     204              : /// quality. The br layer sits inside: it packs whenever the client accepts br,
     205              : /// and the gzip layer outside leaves an encoded answer alone, so gzip serves
     206              : /// only a client without br. [`CompressWhen`] leaves a body of known size
     207              : /// under 1 KiB plain, and text/event-stream, images and gRPC.
     208           86 : pub fn compression() -> Stack<CompressionLayer<CompressWhen>, CompressionLayer<CompressWhen>> {
     209           86 :     let br = CompressionLayer::new()
     210           86 :         .gzip(false)
     211           86 :         .quality(CompressionLevel::Precise(2))
     212           86 :         .compress_when(compress_when());
     213           86 :     let gzip = CompressionLayer::new()
     214           86 :         .br(false)
     215           86 :         .quality(CompressionLevel::Precise(1))
     216           86 :         .compress_when(compress_when());
     217           86 :     Stack::new(br, gzip)
     218           86 : }
     219              : 
     220              : /// The predicate of [`compression()`]: tower-http's default with its 32-byte
     221              : /// minimum raised to 1 KiB, since a smaller body saves under 700 bytes.
     222              : pub type CompressWhen =
     223              :     And<And<And<SizeAbove, NotForContentType>, NotForContentType>, NotForContentType>;
     224              : 
     225          172 : fn compress_when() -> CompressWhen {
     226          172 :     SizeAbove::new(1024)
     227          172 :         .and(NotForContentType::GRPC)
     228          172 :         .and(NotForContentType::IMAGES)
     229          172 :         .and(NotForContentType::SSE)
     230          172 : }
     231              : 
     232              : /// tower-http's `NormalizePath::trim_trailing_slash`: trailing slashes and a
     233              : /// leading `//` collapse to one path, `/` alone for a path of slashes, and
     234              : /// the query is kept.
     235        79212 : pub fn trim_trailing_slash<B>(mut req: axum::http::Request<B>) -> axum::http::Request<B> {
     236        79212 :     let path = req.uri().path();
     237        79212 :     if !path.ends_with('/') && !path.starts_with("//") {
     238        73164 :         return req;
     239         6048 :     }
     240         6048 :     let trimmed = format!("/{}", path.trim_matches('/'));
     241         6048 :     let path_and_query = match req.uri().query() {
     242         1386 :         Some(query) => format!("{trimmed}?{query}"),
     243         4662 :         None => trimmed,
     244              :     };
     245         6048 :     let mut parts = req.uri().clone().into_parts();
     246              :     // A path cut from a valid one stays valid; were it ever not, the request
     247              :     // goes on as sent rather than failing here.
     248         6048 :     if let Ok(pq) = path_and_query.parse() {
     249         6048 :         parts.path_and_query = Some(pq);
     250         6048 :         if let Ok(uri) = axum::http::Uri::from_parts(parts) {
     251         6048 :             *req.uri_mut() = uri;
     252         6048 :         }
     253            0 :     }
     254         6048 :     req
     255        79212 : }
     256              : 
     257        18840 : pub(super) fn routes(state: AppState) -> Router {
     258        18840 :     let api = api_routes()
     259        18840 :         .into_iter()
     260       678240 :         .fold(Router::new(), |api, (path, methods)| {
     261       678240 :             api.route(path, methods)
     262       678240 :         });
     263              : 
     264              :     // 5.8.1.4 consumer half: where forwarded subscription copies point their
     265              :     // notifications; remapped to the original subscriber. CIM 009 defines no
     266              :     // path for it — 5.2.15 makes a notification endpoint any URI, and 6.2
     267              :     // standardizes only what hangs under the API root — so it lives outside
     268              :     // the `/ngsi-ld` prefix ETSI owns, under this broker's own versioned
     269              :     // peer-facing root (ADR-0019). Being outside the API nest, it carries the
     270              :     // bounds wall and the body limit itself: a peer-facing write path must
     271              :     // not be the one route where the documented caps do not apply.
     272        18840 :     let remote_notify = Router::new()
     273        18840 :         .route("/ex/v1/remote-notify", post(distsub::remote_notify))
     274        18840 :         .layer(axum::extract::DefaultBodyLimit::max(
     275        18840 :             *bounds::MAX_BODY_BYTES,
     276              :         ))
     277        18840 :         .layer(axum::middleware::from_fn_with_state(
     278        18840 :             state.clone(),
     279              :             bounds::bounds_layer,
     280              :         ));
     281              : 
     282              :     // Every registered surface, each under its own reserved prefix — the
     283              :     // admin one by default. `with_surface` already refused a prefix outside
     284              :     // /q and /x and any overlap, so the merge here cannot shadow a route.
     285              :     // The caps are the broker's, not the API nest's: these routes carry the
     286              :     // bounds wall and the body limit for the same reason the peer-facing
     287              :     // notification endpoint above does — /q deletes Tenants and replays dead
     288              :     // letters, and a deployment's own /x routes are no less a way in.
     289        18840 :     let mut surfaces = Router::new();
     290        18964 :     for s in state.surfaces.iter() {
     291        18964 :         surfaces = surfaces.nest(s.prefix(), s.router(state.clone()));
     292        18964 :     }
     293        18840 :     let surfaces = surfaces
     294        18840 :         .layer(axum::extract::DefaultBodyLimit::max(
     295        18840 :             *bounds::MAX_BODY_BYTES,
     296              :         ))
     297        18840 :         .layer(axum::middleware::from_fn_with_state(
     298        18840 :             state.clone(),
     299              :             bounds::bounds_layer,
     300              :         ));
     301              : 
     302        18840 :     surfaces
     303        18840 :         .merge(remote_notify)
     304              :         // 6.3.6/6.3.21: Prefer: ngsi-ld=<version> → 4.3.6.8 amendment +
     305              :         // Preference-Applied (+203 when altered) on every API response.
     306              :         // OPTIONS (2.0 #59 pre-adoption): axum's MethodRouter already
     307              :         // computes the exact per-route Allow set for its 405s — the layer
     308              :         // turns an OPTIONS 405 into 204 + that same Allow. HEAD (#58) needs
     309              :         // nothing: axum's get() serves HEAD natively.
     310        18840 :         .nest(
     311        18840 :             API_ROOT,
     312        18840 :             api.layer(axum::middleware::from_fn(conformance::prefer_version_layer))
     313              :                 // The temporal seam's drain: every write's history events
     314              :                 // land in one driver call once the handler is done.
     315        18840 :                 .layer(axum::middleware::from_fn_with_state(
     316        18840 :                     state.clone(),
     317              :                     history::layer,
     318              :                 ))
     319              :                 // 6.3.22 / 5.5.15: NGSILD-Snapshot resolves to the
     320              :                 // snapshot's synthetic tenant, so the handlers below serve
     321              :                 // the frozen copy.
     322        18840 :                 .layer(axum::middleware::from_fn_with_state(
     323        18840 :                     state.clone(),
     324              :                     snapshots::snapshot_layer,
     325              :                 ))
     326              :                 // 5.5.10: non-create operations targeting a non-existing
     327              :                 // Tenant answer NonexistentTenant 404; create operations
     328              :                 // implicitly create the Tenant. Outside the snapshot layer,
     329              :                 // so a client naming one of the broker's own internal
     330              :                 // tenants is refused before that layer legitimately sets
     331              :                 // one.
     332        18840 :                 .layer(axum::middleware::from_fn_with_state(
     333        18840 :                     state.clone(),
     334              :                     tenant_exists_layer,
     335              :                 ))
     336              :                 // a request without a subject needs the admin
     337              :                 // bearer, before the tenant lookup so a 404 tells such a
     338              :                 // caller nothing about which Tenants exist.
     339        18840 :                 .layer(axum::middleware::from_fn_with_state(
     340        18840 :                     state.clone(),
     341              :                     crate::admin_auth::subject_or_admin,
     342              :                 ))
     343              :                 // Bounds wall: URI length, body size, JSON depth — checked
     344              :                 // before any parse (size-check-before-parse), and outside the
     345              :                 // tenant and snapshot lookups so 6.3.4's bare 411/414 is not
     346              :                 // spent on a store round-trip or masked by its 404.
     347        18840 :                 .layer(axum::middleware::from_fn_with_state(
     348        18840 :                     state.clone(),
     349              :                     bounds::bounds_layer,
     350              :                 ))
     351              :                 // axum's built-in extractor limit defaults to 2 MiB and fires
     352              :                 // BEFORE the bounds wall's documented cap — a 3 MiB body was
     353              :                 // 413'd although /q/health advertises maxBodyBytes = 4 MiB
     354              :                 // (found by mutation-testing the 413 wall). One
     355              :                 // number governs both walls.
     356        18840 :                 .layer(axum::extract::DefaultBodyLimit::max(
     357        18840 :                     *bounds::MAX_BODY_BYTES,
     358              :                 ))
     359              :                 // 6.3.14, last so it sees every exit: outside the snapshot
     360              :                 // layer, which rewrites the request header to the snapshot's
     361              :                 // internal tenant, and outside the bounds wall, whose bare
     362              :                 // 411/414 leave through no handler at all.
     363        18840 :                 .layer(axum::middleware::from_fn(echo_tenant_layer)),
     364              :         )
     365        18840 :         .fallback(not_found)
     366              :         // a panicking handler answers 500 instead of unwinding the
     367              :         // connection task; inside the metrics layer so the 500 is counted.
     368        18840 :         .layer(tower_http::catch_panic::CatchPanicLayer::custom(panic_500))
     369              :         // outermost on purpose: axum attaches the 405 Allow header in the
     370              :         // Router itself, above any nested layer — only a layer wrapping the
     371              :         // WHOLE router sees it.
     372        18840 :         .layer(axum::middleware::from_fn(options_204))
     373              :         // TS 104 176 6.3.2: an unrecognised verb is 501 before any routing,
     374              :         // so the method router's 405 stays the answer for a recognised one.
     375        18840 :         .layer(axum::middleware::from_fn(unrecognised_method_501))
     376              :         // Outermost so the duration covers the full stack.
     377        18840 :         .layer(axum::middleware::from_fn(http_metrics_layer))
     378              :         // absent knob = no layer at all: a bare OPTIONS keeps its 204 + Allow
     379        18840 :         .layer(tower::util::option_layer(cors_layer()))
     380        18840 :         .with_state(state)
     381        18840 : }
     382              : 
     383              : /// A handler that panicked: 500 InternalError, and the broker goes on
     384              : /// serving. The panic message stays in the log (the default hook prints it),
     385              : /// never in the body, where it could carry a stored value.
     386            4 : pub(super) fn panic_500(_: Box<dyn std::any::Any + Send + 'static>) -> Response {
     387            4 :     ApiError::from(NgsiError::InternalError("the request failed".into())).into_response()
     388            4 : }
     389              : 
     390              : /// Browser access is off unless ANTARES_CORS_ORIGINS names the origins
     391              : /// (comma-separated, or `*` for any). NGSI-LD says nothing about CORS; the
     392              : /// Link header and the NGSILD-Tenant / NGSILD-Results-Count headers are
     393              : /// exposed so a browser client can read them.
     394        18840 : pub(super) fn cors_layer() -> Option<tower_http::cors::CorsLayer> {
     395              :     use tower_http::cors::{AllowOrigin, CorsLayer};
     396        18840 :     let spec = std::env::var("ANTARES_CORS_ORIGINS")
     397        18840 :         .ok()
     398        18840 :         .filter(|s| !s.trim().is_empty())?;
     399            0 :     let origin = if spec.trim() == "*" {
     400            0 :         AllowOrigin::any()
     401              :     } else {
     402            0 :         AllowOrigin::list(
     403            0 :             spec.split(',')
     404            0 :                 .filter_map(|o| o.trim().parse::<axum::http::HeaderValue>().ok()),
     405              :         )
     406              :     };
     407            0 :     let layer = CorsLayer::new()
     408            0 :         .allow_origin(origin)
     409            0 :         .allow_methods(tower_http::cors::Any)
     410            0 :         .allow_headers(tower_http::cors::Any)
     411            0 :         .expose_headers([
     412            0 :             axum::http::header::LINK,
     413            0 :             axum::http::HeaderName::from_static("ngsild-tenant"),
     414            0 :             axum::http::HeaderName::from_static("ngsild-results-count"),
     415            0 :         ]);
     416            0 :     Some(layer)
     417        18840 : }
     418              : 
     419              : /// Tenants the broker mints for its own bookkeeping: the snapshot module's
     420              : /// internal and per-snapshot tenants share a prefix, the distributed
     421              : /// subscription inbound index is a single fixed name. A client-supplied
     422              : /// tenant may not name any of them — it would put request-shaped writes in
     423              : /// the same keyspace the broker keeps its own state in. The names live on
     424              : /// `TenantId`, where the constructor a client's name goes through refuses
     425              : /// them; the admin surfaces below ask by name, before any parse.
     426         2742 : pub(super) fn reserved_tenant(raw: &str) -> bool {
     427         2742 :     TenantId::is_reserved_str(raw)
     428         2742 : }
     429              : 
     430              : /// 5.5.10 Multi-Tenant Behaviour: Tenants are created implicitly by create
     431              : /// operations (Create Entity 5.6.1, Batch Create/Upsert 5.6.7/5.6.8, Create
     432              : /// Temporal 5.6.11, Create Subscription 5.8.1, Register Context Source
     433              : /// 5.9.2, Create CSource Subscription 5.11.2); "all other NGSI-LD
     434              : /// operations … that target a non-existing Tenant should raise an error of
     435              : /// type NonexistentTenant". Malformed tenant headers pass through — the
     436              : /// handler's own parse answers 400.
     437        55158 : pub(super) async fn tenant_exists_layer(
     438        55158 :     axum::extract::State(st): axum::extract::State<AppState>,
     439        55158 :     req: axum::http::Request<axum::body::Body>,
     440        55158 :     next: axum::middleware::Next,
     441        55158 : ) -> Response {
     442              :     // 6.3.14: the tenant namespace the broker mints for itself is not a
     443              :     // legal client tenant. Snapshots run on internal tenants named
     444              :     // "snap-index" (the synthetic-tenant reverse index) and "snap-<uuid>"
     445              :     // (one per snapshot); a client naming one would read and delete another
     446              :     // tenant's snapshot bookkeeping. Rejected here, ahead of the snapshot
     447              :     // layer that legitimately rewrites the header to such a tenant.
     448              :     // Read once, and refuse a request that names two Tenants (or one in a
     449              :     // form no header can carry): the guard below and the handler further in
     450              :     // have to be deciding about the same value, or the wall inspects one
     451              :     // name while the operation runs on another.
     452        55158 :     let named = match crate::negotiate::single_header(req.headers(), "NGSILD-Tenant") {
     453        55154 :         Ok(named) => named,
     454            4 :         Err(e) => return e.into_response(),
     455              :     };
     456        55154 :     if let Some(raw) = &named {
     457         2530 :         if reserved_tenant(raw) {
     458           48 :             return crate::negotiate::ApiError::from(NgsiError::BadRequestData(format!(
     459           48 :                 "invalid NGSILD-Tenant value: {raw:?}"
     460           48 :             )))
     461           48 :             .into_response();
     462         2482 :         }
     463        52624 :     }
     464        55106 :     let path = req.uri().path().trim_start_matches(API_ROOT);
     465              :     // 7.8.2 lists the creating operations; Batch Upsert creates Entities too
     466              :     // and joins them unless ANTARES_IMPLICIT_TENANTS=listed.
     467        55106 :     let implicit_create = req.method() == axum::http::Method::POST
     468        18600 :         && (matches!(
     469        20752 :             path,
     470        20752 :             "/entities"
     471         8070 :                 | "/entityOperations/create"
     472         7860 :                 | "/temporal/entities"
     473         6574 :                 | "/subscriptions"
     474         5342 :                 | "/csourceRegistrations"
     475         2414 :                 | "/csourceSubscriptions"
     476         2152 :         ) || (path == "/entityOperations/upsert" && !st.profile.implicit_tenants_listed_only));
     477              :     // tenant-independent resources: broker identity (6.33) answers for any
     478              :     // tenant (the per-tenant alias exists before any data does), and the
     479              :     // @context resources are keyed by local id across tenants (5.13) — a
     480              :     // Cached row belongs to no tenant, so the existence of the requesting
     481              :     // one decides nothing here. Ownership of a Hosted row is still enforced,
     482              :     // by `contexts::row_visible` on every serve, list and delete. A POST
     483              :     // stores a Hosted row its Tenant owns, so that Tenant must exist:
     484              :     // a row of a Tenant that never existed could not be
     485              :     // purged with it. DELETE stays free so such a row can still go.
     486        55106 :     let tenant_free = path.starts_with("/info/")
     487        55044 :         || (path.starts_with("/jsonldContexts") && req.method() != axum::http::Method::POST);
     488        55106 :     if !implicit_create && !tenant_free {
     489        35254 :         if let Some(t) = named
     490        35254 :             .as_deref()
     491        35254 :             .and_then(|s| antares_model::TenantId::new(s).ok())
     492              :         {
     493              :             // a Tenant the store said exists a moment ago is not
     494              :             // asked about again; only "exists" is remembered.
     495          756 :             let known = st.known_tenants.holds(&t);
     496          756 :             let exists = if known {
     497          502 :                 Ok(true)
     498              :             } else {
     499          254 :                 st.store.tenant_exists(&t).await
     500              :             };
     501          700 :             match exists {
     502              :                 Ok(false) => {
     503           56 :                     let mut resp = crate::negotiate::ApiError::from(NgsiError::NonexistentTenant(
     504           56 :                         format!("tenant {} does not exist", t.as_str()),
     505           56 :                     ))
     506           56 :                     .into_response();
     507              :                     // 6.3.14: a request-supplied NGSILD-Tenant SHALL be
     508              :                     // present in the response — error responses included.
     509           56 :                     crate::negotiate::echo_tenant(&t, &mut resp);
     510           56 :                     return resp;
     511              :                 }
     512            0 :                 Err(e) => return crate::negotiate::ApiError::from(e).into_response(),
     513          198 :                 Ok(true) if !known => st.known_tenants.remember(&t),
     514          502 :                 Ok(true) => {}
     515              :             }
     516        34498 :         }
     517        19852 :     }
     518        55050 :     next.run(req).await
     519        55158 : }
     520              : 
     521              : /// 6.3.14: "If the HTTP header `NGSILD-Tenant` is present in the HTTP
     522              : /// request, it shall also be present in HTTP response." The sentence admits
     523              : /// no exception, and the responses that most need it are the ones no handler
     524              : /// shapes: a rejection from the bounds wall, a 405, a body that never parsed.
     525              : /// An echo the handlers own is an echo some handler forgets, so it is read
     526              : /// once here, from the request the CALLER sent — the snapshot layer below
     527              : /// rewrites that header to the snapshot's internal tenant, which must never
     528              : /// reach the caller.
     529        55230 : pub(super) async fn echo_tenant_layer(
     530        55230 :     req: axum::http::Request<axum::body::Body>,
     531        55230 :     next: axum::middleware::Next,
     532        55230 : ) -> Response {
     533              :     // A repeated header names no Tenant, so there is none to echo; the
     534              :     // BadRequestData the wall answers says so on its own.
     535        55230 :     let tenant = crate::negotiate::single_header(req.headers(), "NGSILD-Tenant")
     536        55230 :         .ok()
     537        55230 :         .flatten()
     538        55230 :         .and_then(|s| TenantId::new(&s).ok());
     539        55230 :     let mut resp = next.run(req).await;
     540        55230 :     if let Some(t) = tenant {
     541         2498 :         if !resp.headers().contains_key("NGSILD-Tenant") {
     542          118 :             crate::negotiate::echo_tenant(&t, &mut resp);
     543         2380 :         }
     544        52732 :     }
     545        55230 :     resp
     546        55230 : }
     547              : 
     548              : /// Bounded method label for the request metrics: standard HTTP methods
     549              : /// pass through, anything else (extension methods are minted by the
     550              : /// client) collapses to "OTHER" so the label's cardinality stays bounded.
     551       112016 : pub(super) fn metric_method(m: &str) -> &'static str {
     552       112016 :     match m {
     553       112016 :         "GET" => "GET",
     554        92692 :         "POST" => "POST",
     555        50820 :         "PATCH" => "PATCH",
     556        21944 :         "PUT" => "PUT",
     557        21232 :         "DELETE" => "DELETE",
     558          828 :         "OPTIONS" => "OPTIONS",
     559          380 :         "HEAD" => "HEAD",
     560           48 :         _ => "OTHER",
     561              :     }
     562       112016 : }
     563              : 
     564              : /// Request counter + duration histogram, labelled by method and status
     565              : /// class only (bounded cardinality).
     566        56000 : pub(super) async fn http_metrics_layer(
     567        56000 :     req: axum::http::Request<axum::body::Body>,
     568        56000 :     next: axum::middleware::Next,
     569        56000 : ) -> Response {
     570        56000 :     let method = metric_method(req.method().as_str());
     571              :     // Clock rule: std Instant panics on wasm32.
     572              :     #[cfg(not(target_arch = "wasm32"))]
     573        56000 :     let start = std::time::Instant::now();
     574              :     #[cfg(target_arch = "wasm32")]
     575              :     let start = web_time::Instant::now();
     576        56000 :     let resp = next.run(req).await;
     577        56000 :     let class = match resp.status().as_u16() {
     578        56000 :         100..=199 => "1xx",
     579        56000 :         200..=299 => "2xx",
     580         8432 :         300..=399 => "3xx",
     581         8432 :         400..=499 => "4xx",
     582          214 :         _ => "5xx",
     583              :     };
     584        56000 :     metrics::counter!("antares_http_requests_total", "method" => method, "status" => class)
     585        56000 :         .increment(1);
     586        56000 :     metrics::histogram!("antares_http_request_duration_seconds", "method" => method)
     587        56000 :         .record(start.elapsed().as_secs_f64());
     588        56000 :     resp
     589        56000 : }
     590              : 
     591              : /// TS 104 176 6.3.2: "Not Implemented (501) ... when a client invokes an
     592              : /// HTTP verb that is unrecognized or not implemented". The recognised set is
     593              : /// the one [`metric_method`] names; CONNECT, TRACE and every extension
     594              : /// method are 501. A bare status like 6.3.4's 411 and 415: the error table
     595              : /// names no error type for it.
     596        56000 : pub(super) async fn unrecognised_method_501(
     597        56000 :     req: axum::http::Request<axum::body::Body>,
     598        56000 :     next: axum::middleware::Next,
     599        56000 : ) -> Response {
     600        56000 :     if metric_method(req.method().as_str()) == "OTHER" {
     601           20 :         return StatusCode::NOT_IMPLEMENTED.into_response();
     602        55980 :     }
     603        55980 :     next.run(req).await
     604        56000 : }
     605              : 
     606              : /// 2.0 #59 pre-adoption: OPTIONS → 204 No Content + the Allow set the
     607              : /// method router computed. Non-OPTIONS traffic passes through untouched.
     608        55980 : pub(super) async fn options_204(
     609        55980 :     req: axum::http::Request<axum::body::Body>,
     610        55980 :     next: axum::middleware::Next,
     611        55980 : ) -> Response {
     612        55980 :     let is_options = req.method() == axum::http::Method::OPTIONS;
     613        55980 :     let resp = next.run(req).await;
     614        55980 :     if is_options && resp.status() == axum::http::StatusCode::METHOD_NOT_ALLOWED {
     615              :         // Keep the response PARTS: axum carries the computed Allow set as a
     616              :         // response extension and turns it into the header above all layers —
     617              :         // preserving the extensions preserves the Allow header.
     618          224 :         let (mut parts, _) = resp.into_parts();
     619          224 :         parts.status = axum::http::StatusCode::NO_CONTENT;
     620          224 :         return Response::from_parts(parts, axum::body::Body::empty());
     621        55756 :     }
     622        55756 :     resp
     623        55980 : }
     624              : 
     625              : /// PATCH/PUT on the entities collection: the entity id is missing — 400.
     626           16 : pub(super) async fn missing_entity_id(headers: HeaderMap) -> Response {
     627           16 :     let tenant = tenant_from(&headers).unwrap_or_default();
     628           16 :     let mut resp = ApiError::from(NgsiError::BadRequestData(
     629           16 :         "entity id is required in the request path".into(),
     630           16 :     ))
     631           16 :     .into_response();
     632           16 :     echo_tenant(&tenant, &mut resp);
     633           16 :     resp
     634           16 : }
     635              : 
     636           32 : pub(super) async fn not_found(headers: HeaderMap, uri: axum::http::Uri) -> Response {
     637           32 :     let path = uri.path().to_owned();
     638           32 :     let tenant = tenant_from(&headers).unwrap_or_default();
     639              :     // A path with an empty segment (…/attrs//{x}) names a resource whose
     640              :     // methods don't apply — 405 per the suite (016_02_04/06).
     641           32 :     if path.starts_with(API_ROOT) && path.contains("//") {
     642            8 :         let mut resp = axum::http::StatusCode::METHOD_NOT_ALLOWED.into_response();
     643            8 :         echo_tenant(&tenant, &mut resp);
     644            8 :         return resp;
     645           24 :     }
     646           24 :     let mut resp =
     647           24 :         ApiError::from(NgsiError::ResourceNotFound(format!("unknown path {path}"))).into_response();
     648           24 :     echo_tenant(&tenant, &mut resp);
     649           24 :     resp
     650           32 : }
     651              : 
     652              : #[cfg(test)]
     653              : mod trim_trailing_slash_tests {
     654              :     use super::trim_trailing_slash;
     655              : 
     656           28 :     fn trimmed(uri: &str) -> String {
     657           28 :         let req = axum::http::Request::builder()
     658           28 :             .uri(uri)
     659           28 :             .body(())
     660           28 :             .unwrap_or_default();
     661           28 :         trim_trailing_slash(req).uri().to_string()
     662           28 :     }
     663              : 
     664              :     /// The NGSI-LD API and every other surface: tower-http's trim, the query
     665              :     /// kept.
     666              :     #[test]
     667            4 :     fn trims_every_path() {
     668           28 :         for (sent, served) in [
     669            4 :             ("/ngsi-ld/v1/entities/", "/ngsi-ld/v1/entities"),
     670            4 :             (
     671            4 :                 "/ngsi-ld/v1/entities//?type=Car",
     672            4 :                 "/ngsi-ld/v1/entities?type=Car",
     673            4 :             ),
     674            4 :             ("//ngsi-ld/v1/entities", "/ngsi-ld/v1/entities"),
     675            4 :             ("/", "/"),
     676            4 :             ("///", "/"),
     677            4 :             ("/x/example/", "/x/example"),
     678            4 :             ("/q/tenants/", "/q/tenants"),
     679            4 :         ] {
     680           28 :             assert_eq!(trimmed(sent), served, "{sent}");
     681              :         }
     682            4 :     }
     683              : }
        

Generated by: LCOV version 2.0-1