diff --git a/Cargo.lock b/Cargo.lock index 7a0e2e0a12..f64584eb99 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4732,11 +4732,17 @@ version = "0.1.0" dependencies = [ "axum 0.7.5", "bytes", + "colored", + "futures", "mime", "serde", "serde_json", "serde_yaml", + "tokio", + "tower", + "tracing", "utoipa", + "zeroize", ] [[package]] diff --git a/common/http-api-common/Cargo.toml b/common/http-api-common/Cargo.toml index 0bd7767a05..de5e5193ff 100644 --- a/common/http-api-common/Cargo.toml +++ b/common/http-api-common/Cargo.toml @@ -12,9 +12,15 @@ license.workspace = true [dependencies] axum.workspace = true -bytes = { workspace = true } -mime = { workspace = true } +bytes.workspace = true +colored.workspace = true +futures.workspace = true +mime.workspace = true serde = { workspace = true, features = ["derive"] } serde_json.workspace = true -serde_yaml = { workspace = true } -utoipa = { workspace = true } +serde_yaml.workspace = true +tokio.workspace = true +tower.workspace = true +tracing.workspace = true +utoipa.workspace = true +zeroize.workspace = true diff --git a/common/http-api-common/src/lib.rs b/common/http-api-common/src/lib.rs index 83b9c685d8..d54c7c2a28 100644 --- a/common/http-api-common/src/lib.rs +++ b/common/http-api-common/src/lib.rs @@ -8,6 +8,8 @@ use bytes::{BufMut, BytesMut}; use serde::{Deserialize, Serialize}; use utoipa::{IntoParams, ToSchema}; +pub mod middleware; + #[derive(Debug, Clone, ToSchema)] pub enum FormattedResponse { Json(Json), diff --git a/common/http-api-common/src/middleware/auth/bearer.rs b/common/http-api-common/src/middleware/auth/bearer.rs new file mode 100644 index 0000000000..937b85669c --- /dev/null +++ b/common/http-api-common/src/middleware/auth/bearer.rs @@ -0,0 +1,115 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +use axum::http::{header, HeaderValue, StatusCode}; +use axum::response::IntoResponse; +use axum::{extract::Request, response::Response}; +use futures::future::BoxFuture; +use std::sync::Arc; +use std::task::{Context, Poll}; +use tower::{Layer, Service}; +use tracing::{debug, instrument, trace}; +use zeroize::Zeroizing; + +#[derive(Debug, Clone)] +pub struct BearerAuthLayer { + bearer_token: Arc>, +} + +impl BearerAuthLayer { + pub fn new(bearer_token: Arc>) -> Self { + BearerAuthLayer { bearer_token } + } +} + +impl Layer for BearerAuthLayer { + type Service = RequireBearerAuth; + + fn layer(&self, inner: S) -> Self::Service { + RequireBearerAuth::new(inner, self.bearer_token.clone()) + } +} + +#[derive(Debug, Clone)] +pub struct RequireBearerAuth { + inner: S, + bearer_token: Arc>, +} + +impl RequireBearerAuth { + pub fn new(inner: S, bearer_token: Arc>) -> Self { + RequireBearerAuth { + inner, + bearer_token, + } + } + + fn check_auth_header(&self, header: Option<&HeaderValue>) -> Result<(), &'static str> { + let Some(token) = header else { + trace!("missing header"); + return Err("`Authorization` header is missing"); + }; + + let Ok(authorization) = token.to_str() else { + trace!("invalid header"); + return Err("`Authorization` header contains invalid characters"); + }; + + debug!("header value: '{authorization}'"); + + let split = authorization.split_once(' '); + let bearer_token = match split { + // Found proper bearer + Some(("Bearer", contents)) => contents, + // Found empty bearer; + _ if authorization == "Bearer" => "", + // Found nothing + _ => return Err("`Authorization` header must be a bearer token"), + }; + + debug!("parsed token: '{bearer_token}'"); + + if self.bearer_token.is_empty() && bearer_token.is_empty() { + return Ok(()); + } + if bearer_token.is_empty() { + return Err("`Authorization` header must contain non-empty `Bearer` token"); + } + + if self.bearer_token.as_str() != bearer_token { + return Err("`Authorization` header does not contain the correct `Bearer` token"); + } + + Ok(()) + } +} + +impl Service for RequireBearerAuth +where + S: Service + Send + 'static, + S: Send + Sync + 'static, + S::Future: Send + 'static, +{ + type Response = S::Response; + type Error = S::Error; + type Future = BoxFuture<'static, Result>; + + #[inline] + fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll> { + self.inner.poll_ready(cx) + } + + #[instrument(skip_all, fields(uri = %req.uri()))] + fn call(&mut self, req: Request) -> Self::Future { + debug!("checking the auth"); + + let auth_header = req.headers().get(header::AUTHORIZATION); + + match self.check_auth_header(auth_header) { + Ok(_authorised) => Box::pin(self.inner.call(req)), + Err(err) => { + Box::pin(async move { Ok((StatusCode::UNAUTHORIZED, err).into_response()) }) + } + } + } +} diff --git a/common/http-api-common/src/middleware/auth/mod.rs b/common/http-api-common/src/middleware/auth/mod.rs new file mode 100644 index 0000000000..56620b539b --- /dev/null +++ b/common/http-api-common/src/middleware/auth/mod.rs @@ -0,0 +1,4 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod bearer; diff --git a/nym-node/nym-node-http-api/src/middleware/logging.rs b/common/http-api-common/src/middleware/logging.rs similarity index 64% rename from nym-node/nym-node-http-api/src/middleware/logging.rs rename to common/http-api-common/src/middleware/logging.rs index ae3c5f1d4e..825104ec5b 100644 --- a/nym-node/nym-node-http-api/src/middleware/logging.rs +++ b/common/http-api-common/src/middleware/logging.rs @@ -1,4 +1,4 @@ -// Copyright 2023-2024 - Nym Technologies SA +// Copyright 2024 - Nym Technologies SA // SPDX-License-Identifier: GPL-3.0-only use axum::{ @@ -12,6 +12,7 @@ use axum::{ }; use colored::*; use std::net::SocketAddr; +use tokio::time::Instant; use tracing::info; /// Simple logger for requests @@ -29,7 +30,9 @@ pub async fn logger( let host = header_map(req.headers().get(HOST), "Unknown Host".to_string()); + let start = Instant::now(); let res = next.run(req).await; + let time_taken = start.elapsed(); let status = res.status(); let print_status = if status.is_client_error() || status.is_server_error() { status.to_string().red() @@ -39,7 +42,17 @@ pub async fn logger( status.to_string().yellow() }; - info!(target: "incoming request", "[{addr} -> {host}] {method} '{uri}': {print_status} / agent: {agent}"); + let taken = "time taken".bold(); + + let time_taken = match time_taken.as_millis() { + ms if ms > 500 => format!("{taken}: {}", format!("{ms}ms").red()), + ms if ms > 200 => format!("{taken}: {}", format!("{ms}ms").yellow()), + ms if ms > 50 => format!("{taken}: {}", format!("{ms}ms").bright_yellow()), + ms => format!("{taken}: {ms}ms"), + }; + + let agent_str = "agent".bold(); + info!("[{addr} -> {host}] {method} '{uri}': {print_status} {time_taken} {agent_str}: {agent}"); res } diff --git a/common/http-api-common/src/middleware/mod.rs b/common/http-api-common/src/middleware/mod.rs new file mode 100644 index 0000000000..eb9b81da11 --- /dev/null +++ b/common/http-api-common/src/middleware/mod.rs @@ -0,0 +1,8 @@ +// Copyright 2024 - Nym Technologies SA +// SPDX-License-Identifier: Apache-2.0 + +pub mod auth; +pub mod logging; + +pub use auth::bearer::{BearerAuthLayer, RequireBearerAuth}; +pub use logging::logger; diff --git a/nym-api/Cargo.toml b/nym-api/Cargo.toml index 0146af7d70..0ec34da786 100644 --- a/nym-api/Cargo.toml +++ b/nym-api/Cargo.toml @@ -32,8 +32,6 @@ pin-project = { workspace = true } rand = "0.8.5" rand-07 = { package = "rand", version = "0.7.3" } # required for compatibility reqwest = { workspace = true, features = ["json"] } -rocket = { workspace = true, features = ["json"] } -rocket_cors = { workspace = true } serde = { workspace = true } serde_json = { workspace = true } tap = { workspace = true } @@ -55,11 +53,14 @@ sqlx = { workspace = true, features = [ "migrate", ] } -okapi = { workspace = true, features = ["impl_json_schema"] } -rocket_okapi = { workspace = true, features = ["swagger"] } schemars = { workspace = true, features = ["preserve_order"] } zeroize = { workspace = true } +rocket = { workspace = true, features = ["json"] } +rocket_cors = { workspace = true } +rocket_okapi = { workspace = true, features = ["swagger"] } +okapi = { workspace = true, features = ["impl_json_schema"] } + ## ephemera-specific #actix-web = "4" #array-bytes = "6.0.0" @@ -68,12 +69,9 @@ zeroize = { workspace = true } #serde_derive = "1.0.149" #uuid = { version = "1.3.0", features = ["serde", "v4"] } -## internal -#ephemera = { path = "../ephemera" } nym-bandwidth-controller = { path = "../common/bandwidth-controller" } nym-coconut-bandwidth-contract-common = { path = "../common/cosmwasm-smart-contracts/coconut-bandwidth-contract" } nym-coconut-dkg-common = { path = "../common/cosmwasm-smart-contracts/coconut-dkg" } -#nym-ephemera-common = { path = "../common/cosmwasm-smart-contracts/ephemera" } nym-config = { path = "../common/config" } cosmwasm-std = { workspace = true } nym-credential-storage = { path = "../common/credential-storage" } diff --git a/nym-node/nym-node-http-api/src/lib.rs b/nym-node/nym-node-http-api/src/lib.rs index 2621a30eda..3363151b0c 100644 --- a/nym-node/nym-node-http-api/src/lib.rs +++ b/nym-node/nym-node-http-api/src/lib.rs @@ -11,7 +11,6 @@ use std::net::SocketAddr; use tracing::{debug, error}; pub mod error; -pub mod middleware; pub mod router; pub mod state; diff --git a/nym-node/nym-node-http-api/src/middleware/mod.rs b/nym-node/nym-node-http-api/src/middleware/mod.rs deleted file mode 100644 index 54a67e0147..0000000000 --- a/nym-node/nym-node-http-api/src/middleware/mod.rs +++ /dev/null @@ -1,4 +0,0 @@ -// Copyright 2023 - Nym Technologies SA -// SPDX-License-Identifier: GPL-3.0-only - -pub mod logging; diff --git a/nym-node/nym-node-http-api/src/mod.rs b/nym-node/nym-node-http-api/src/mod.rs index 77cb66ba14..0a078bcd3f 100644 --- a/nym-node/nym-node-http-api/src/mod.rs +++ b/nym-node/nym-node-http-api/src/mod.rs @@ -9,7 +9,6 @@ use nym_task::TaskClient; use std::net::SocketAddr; use tracing::{debug, error, info}; -pub mod middleware; pub mod router; pub mod state; diff --git a/nym-node/nym-node-http-api/src/router/mod.rs b/nym-node/nym-node-http-api/src/router/mod.rs index aba74606f5..f61eafad77 100644 --- a/nym-node/nym-node-http-api/src/router/mod.rs +++ b/nym-node/nym-node-http-api/src/router/mod.rs @@ -3,12 +3,12 @@ pub use crate::api::v1::gateway::client_interfaces::wireguard::WireguardAppState; use crate::error::NymNodeHttpError; -use crate::middleware::logging; use crate::state::AppState; use crate::NymNodeHTTPServer; use axum::response::Redirect; use axum::routing::get; use axum::Router; +use nym_http_api_common::middleware::logger; use nym_node_requests::api::v1::gateway::models::{Gateway, Wireguard}; use nym_node_requests::api::v1::ip_packet_router::models::IpPacketRouter; use nym_node_requests::api::v1::mixnode::models::Mixnode; @@ -193,7 +193,7 @@ impl NymNodeRouter { routes::API, api::routes(config.api, initial_wg_state.unwrap_or_default()), ) - .layer(axum::middleware::from_fn(logging::logger)) + .layer(axum::middleware::from_fn(logger)) .with_state(state), } }