From 9bda05b93ca4afed007effef7d425223a7f0ac51 Mon Sep 17 00:00:00 2001 From: Dotan Nahum Date: Thu, 10 Oct 2024 14:56:47 +0300 Subject: [PATCH] reshape middleware configuration and loading --- docs-site/content/docs/the-app/controller.md | 162 ++++++++++++++++- examples/demo/Cargo.lock | 2 +- examples/demo/config/development.yaml | 53 +----- src/boot.rs | 28 ++- src/cli.rs | 20 ++- src/config.rs | 1 + src/controller/middleware/catch_panic.rs | 7 +- src/controller/middleware/compression.rs | 12 +- src/controller/middleware/cors.rs | 56 ++++-- src/controller/middleware/etag.rs | 23 ++- src/controller/middleware/fallback.rs | 10 +- src/controller/middleware/limit_payload.rs | 10 +- src/controller/middleware/logger.rs | 42 ++--- src/controller/middleware/mod.rs | 168 +++++++++++++----- src/controller/middleware/remote_ip.rs | 5 +- src/controller/middleware/request_id.rs | 9 +- src/controller/middleware/secure_headers.rs | 58 +++--- src/controller/middleware/static_assets.rs | 34 +++- src/controller/middleware/timeout.rs | 43 +++-- src/gen/mod.rs | 17 +- .../config/development.yaml | 41 ----- starters/lightweight-service/config/test.yaml | 22 --- starters/rest-api/config/development.yaml | 54 ------ starters/rest-api/config/test.yaml | 35 ---- starters/saas/config/development.yaml | 63 ------- starters/saas/config/test.yaml | 33 ---- tests/controller/middlewares.rs | 45 ++--- ...rs_[none]_overrides[none]@middlewares.snap | 4 +- 28 files changed, 547 insertions(+), 510 deletions(-) diff --git a/docs-site/content/docs/the-app/controller.md b/docs-site/content/docs/the-app/controller.md index fd12c42cd..d2d70d6c0 100644 --- a/docs-site/content/docs/the-app/controller.md +++ b/docs-site/content/docs/the-app/controller.md @@ -261,6 +261,8 @@ impl Hooks for App { Loco comes with a set of built-in middleware out of the box. Some are enabled by default, while others need to be configured. Middleware registration is flexible and can be managed either through the `*.yaml` environment configuration or directly in the code. +## The default stack + You get all the enabled middlewares run the following command ```sh @@ -268,6 +270,139 @@ cargo loco middleware --config ``` +This is the stack in `development` mode: + +```sh +$ cargo loco middleware --config + +limit_payload {"enable":true,"body_limit":2000000} +cors {"enable":true,"allow_origins":["any"],"allow_headers":["*"],"allow_methods":["*"],"max_age":null,"vary":["origin","access-control-request-method","access-control-request-headers"]} +catch_panic {"enable":true} +etag {"enable":true} +logger {"config":{"enable":true},"environment":"development"} +request_id {"enable":true} +fallback {"enable":true,"code":200,"file":null,"not_found":null} +powered_by {"ident":"loco.rs"} + + +remote_ip (disabled) +compression (disabled) +timeout (disabled) +static_assets (disabled) +secure_headers (disabled) +``` + +### Example: disable all middleware + +Take what ever is enabled, and use `enable: false` with the relevant field. If `middlewares:` section in `server` is missing, add it. + +```yaml +server: + middlewares: + limit_payload: + enable: false + cors: + enable: false + catch_panic: + enable: false + etag: + enable: false + logger: + enable: false + request_id: + enable: false + fallback: + enable: false +``` + +The result: + +```sh +$ cargo loco middleware --config +powered_by {"ident":"loco.rs"} + + +limit_payload (disabled) +cors (disabled) +catch_panic (disabled) +etag (disabled) +remote_ip (disabled) +compression (disabled) +timeout_request (disabled) +static (disabled) +secure_headers (disabled) +logger (disabled) +request_id (disabled) +fallback (disabled) +``` + +You can control the `powered_by` middleware by changing the value for `server.ident`: + +```yaml +server: + ident: my-server #(or empty string to disable) +``` + +### Example: add a non-default middleware + +Lets add the _Remote IP_ middleware to the stack. This is done just by configuration: + +```yaml +server: + middlewares: + remote_ip: + enable: true +``` + +The result: + +```sh +$ cargo loco middleware --config + +limit_payload {"enable":true,"body_limit":2000000} +cors {"enable":true,"allow_origins":["any"],"allow_headers":["*"],"allow_methods":["*"],"max_age":null,"vary":["origin","access-control-request-method","access-control-request-headers"]} +catch_panic {"enable":true} +etag {"enable":true} +remote_ip {"enable":true,"trusted_proxies":null} +logger {"config":{"enable":true},"environment":"development"} +request_id {"enable":true} +fallback {"enable":true,"code":200,"file":null,"not_found":null} +powered_by {"ident":"loco.rs"} +``` + +### Example: change a configuration for an enabled middleware + +Let's change the request body limit to `5mb`. When overriding a middleware configuration, rememeber to keep an `enable: true`: + +```yaml + middlewares: + limit_payload: + enable: true + body_limit: 5mb +``` + +The result: + +```sh +$ cargo loco middleware --config + +limit_payload {"enable":true,"body_limit":5000000} +cors {"enable":true,"allow_origins":["any"],"allow_headers":["*"],"allow_methods":["*"],"max_age":null,"vary":["origin","access-control-request-method","access-control-request-headers"]} +catch_panic {"enable":true} +etag {"enable":true} +logger {"config":{"enable":true},"environment":"development"} +request_id {"enable":true} +fallback {"enable":true,"code":200,"file":null,"not_found":null} +powered_by {"ident":"loco.rs"} + + +remote_ip (disabled) +compression (disabled) +timeout_request (disabled) +static (disabled) +secure_headers (disabled) +``` + ### Authentication In the `Loco` framework, middleware plays a crucial role in authentication. `Loco` supports various authentication methods, including JSON Web Token (JWT) and API Key authentication. This section outlines how to configure and use authentication middleware in your application. @@ -523,6 +658,16 @@ server: foo: bar ``` +To support `htmx`, You can add the following override, to allow some inline running of scripts: + +```yaml +secure_headers: + preset: github + overrides: + # this allows you to use HTMX, and has unsafe-inline. Remove or consider in production + "Content-Security-Policy": "default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src 'unsafe-inline' 'self' https:; style-src 'self' https: 'unsafe-inline'" +``` + ## Compression `Loco` leverages [CompressionLayer](https://docs.rs/tower-http/0.5.0/tower_http/compression/index.html) to enable a `one click` solution. @@ -554,14 +699,7 @@ middlewares: precompressed: true ``` -## Handler and Route based middleware - -`Loco` also allow us to apply [layers](https://docs.rs/tower/latest/tower/trait.Layer.html) to specific handlers or -routes. -For more information on handler and route based middleware, refer to the [middleware](/docs/the-app/middlewares) -documentation. - -## Cors +## CORS This middleware enables Cross-Origin Resource Sharing (CORS) by allowing configurable origins, methods, and headers in HTTP requests. It can be tailored to fit various application requirements, supporting permissive CORS or specific rules as defined in the middleware configuration. @@ -585,6 +723,14 @@ middlewares: ``` +## Handler and Route based middleware + +`Loco` also allow us to apply [layers](https://docs.rs/tower/latest/tower/trait.Layer.html) to specific handlers or +routes. +For more information on handler and route based middleware, refer to the [middleware](/docs/the-app/middlewares) +documentation. + + ### Handler based middleware: Apply a layer to a specific handler using `layer` method. diff --git a/examples/demo/Cargo.lock b/examples/demo/Cargo.lock index 7e691e0d2..f5803b146 100644 --- a/examples/demo/Cargo.lock +++ b/examples/demo/Cargo.lock @@ -3015,7 +3015,7 @@ dependencies = [ [[package]] name = "loco-rs" -version = "0.9.0" +version = "0.10.0" dependencies = [ "argon2", "async-trait", diff --git a/examples/demo/config/development.yaml b/examples/demo/config/development.yaml index a73d85950..ac3c78131 100644 --- a/examples/demo/config/development.yaml +++ b/examples/demo/config/development.yaml @@ -30,58 +30,7 @@ server: port: {{ get_env(name="NODE_PORT", default=5150) }} # The UI hostname or IP address that mailers will point to. host: http://localhost - # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block - # - middlewares: - # Allows to limit the payload size request. payload that bigger than this file will blocked the request. - limit_payload: - # Enable/Disable the middleware. - enable: true - # the limit size. can be b,kb,kib,mb,mib,gb,gib - body_limit: 5mb - # set secure headers - secure_headers: - preset: github - overrides: - # this allows you to use HTMX, and has unsafe-inline. Remove or consider in production - "Content-Security-Policy": "default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src 'unsafe-inline' 'self' https:; style-src 'self' https: 'unsafe-inline'" - # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. - logger: - # Enable/Disable the middleware. - enable: true - # when your code is panicked, the request still returns 500 status code. - catch_panic: - # Enable/Disable the middleware. - enable: true - # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. - timeout_request: - # Enable/Disable the middleware. - enable: true - # Duration time in milliseconds. - timeout: 5000 - compression: - # Enable/Disable the middleware. - enable: true - static_assets: - enable: true - must_exist: true - precompressed: true - folder: - path: assets - fallback: index.html - cors: - enable: true - # Set the value of the [`Access-Control-Allow-Origin`][mdn] header - # allow_origins: - # - https://loco.rs - # Set the value of the [`Access-Control-Allow-Headers`][mdn] header - # allow_headers: - # - Content-Type - # Set the value of the [`Access-Control-Allow-Methods`][mdn] header - # allow_methods: - # - POST - # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds - # max_age: 3600 +# # Worker Configuration workers: diff --git a/src/boot.rs b/src/boot.rs index 3f0e80f56..236f38043 100644 --- a/src/boot.rs +++ b/src/boot.rs @@ -25,7 +25,6 @@ use crate::{ task::{self, Tasks}, Result, }; -use colored::Colorize; /// Represents the application startup mode. pub enum StartMode { @@ -381,23 +380,22 @@ pub fn list_endpoints(ctx: &AppContext) -> Vec { H::routes(ctx).collect() } +pub struct MiddlewareInfo { + pub id: String, + pub enabled: bool, + pub detail: String, +} + #[must_use] -pub fn list_middlewares(ctx: &AppContext, with_config: bool) -> Vec { - H::routes(ctx) - .middlewares::(ctx) +pub fn list_middlewares(ctx: &AppContext) -> Vec { + H::middlewares(ctx) .iter() - .map(|m| { - let text = heck::AsSnakeCase(m.name()).to_string().bold(); - if with_config { - format!( - "{text:<22} {}", - serde_json::to_string(&m.config().unwrap_or_default()).unwrap_or_default() - ) - } else { - format!("{text}") - } + .map(|m| MiddlewareInfo { + id: m.name().to_string(), + enabled: m.is_enabled(), + detail: m.config().unwrap_or_default().to_string(), }) - .collect::>() + .collect::>() } /// Initializes an [`EmailSender`] based on the mailer configuration settings diff --git a/src/cli.rs b/src/cli.rs index 27f962472..3541400cf 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -423,6 +423,8 @@ pub async fn playground() -> crate::Result { #[allow(clippy::too_many_lines)] #[allow(clippy::cognitive_complexity)] pub async fn main() -> crate::Result<()> { + use colored::Colorize; + let cli: Cli = Cli::parse(); let environment: Environment = cli.environment.unwrap_or_else(resolve_from_env).into(); @@ -473,9 +475,21 @@ pub async fn main() -> crate::Result<()> { } Commands::Middleware { config } => { let app_context = create_context::(&environment).await?; - let middlewares = list_middlewares::(&app_context, config); - for middleware in middlewares { - println!("{middleware}"); + let middlewares = list_middlewares::(&app_context); + for middleware in middlewares.iter().filter(|m| m.enabled) { + println!( + "{:<22} {}", + middleware.id.bold(), + if config { + middleware.detail.as_str() + } else { + "" + } + ); + } + println!("\n"); + for middleware in middlewares.iter().filter(|m| !m.enabled) { + println!("{:<22} (disabled)", middleware.id.bold().dimmed(),); } } Commands::Task { name, params } => { diff --git a/src/config.rs b/src/config.rs index 110f20f70..77d781120 100644 --- a/src/config.rs +++ b/src/config.rs @@ -373,6 +373,7 @@ pub struct Server { pub ident: Option, /// Middleware configurations for the server, including payload limits, /// logging, and error handling. + #[serde(default)] pub middlewares: middleware::Config, } diff --git a/src/controller/middleware/catch_panic.rs b/src/controller/middleware/catch_panic.rs index 3e42e8a58..c56bc879d 100644 --- a/src/controller/middleware/catch_panic.rs +++ b/src/controller/middleware/catch_panic.rs @@ -17,15 +17,10 @@ use crate::{ #[derive(Debug, Clone, Deserialize, Serialize)] pub struct CatchPanic { + #[serde(default)] pub enable: bool, } -impl Default for CatchPanic { - fn default() -> Self { - Self { enable: true } - } -} - /// Handler function for the [`CatchPanicLayer`] middleware. /// /// This function processes panics by extracting error messages, logging them, diff --git a/src/controller/middleware/compression.rs b/src/controller/middleware/compression.rs index 1b301192e..42a2de54a 100644 --- a/src/controller/middleware/compression.rs +++ b/src/controller/middleware/compression.rs @@ -5,20 +5,16 @@ //! times and reducing bandwidth usage. The middleware configuration allows for //! enabling or disabling compression based on the application settings. -use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; use axum::Router as AXRouter; use serde::{Deserialize, Serialize}; use tower_http::compression::CompressionLayer; +use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Compression { - enable: bool, -} - -impl Default for Compression { - fn default() -> Self { - Self { enable: true } - } + #[serde(default)] + pub enable: bool, } impl MiddlewareLayer for Compression { diff --git a/src/controller/middleware/cors.rs b/src/controller/middleware/cors.rs index 00fa3d439..4ed7de71b 100644 --- a/src/controller/middleware/cors.rs +++ b/src/controller/middleware/cors.rs @@ -1,19 +1,23 @@ //! Configurable and Flexible CORS Middleware //! //! This middleware enables Cross-Origin Resource Sharing (CORS) by allowing -//! configurable origins, methods, and headers in HTTP requests. It can be tailored -//! to fit various application requirements, supporting permissive CORS or -//! specific rules as defined in the middleware configuration. +//! configurable origins, methods, and headers in HTTP requests. It can be +//! tailored to fit various application requirements, supporting permissive CORS +//! or specific rules as defined in the middleware configuration. + +use std::time::Duration; -use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; use axum::Router as AXRouter; use serde::{Deserialize, Serialize}; -use std::time::Duration; +use serde_json::json; use tower_http::cors; +use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; + /// CORS middleware configuration -#[derive(Default, Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Cors { + #[serde(default)] pub enable: bool, /// Allow origins #[serde(default = "default_allow_origins")] @@ -31,6 +35,12 @@ pub struct Cors { pub vary: Vec, } +impl Default for Cors { + fn default() -> Self { + serde_json::from_value(json!({})).unwrap() + } +} + fn default_allow_origins() -> Vec { vec!["any".to_string()] } @@ -52,21 +62,32 @@ fn default_vary_headers() -> Vec { } impl Cors { + #[must_use] + pub fn empty() -> Self { + Self { + enable: true, + allow_headers: vec![], + allow_methods: vec![], + allow_origins: vec![], + max_age: None, + vary: vec![], + } + } /// Creates cors layer /// /// # Errors /// /// This function returns an error in the following cases: /// - /// - If any of the provided origins in `allow_origins` cannot be parsed as a valid URI, - /// the function will return a parsing error. - /// - If any of the provided headers in `allow_headers` cannot be parsed as valid HTTP headers, - /// the function will return a parsing error. - /// - If any of the provided methods in `allow_methods` cannot be parsed as valid HTTP methods, - /// the function will return a parsing error. + /// - If any of the provided origins in `allow_origins` cannot be parsed as + /// a valid URI, the function will return a parsing error. + /// - If any of the provided headers in `allow_headers` cannot be parsed as + /// valid HTTP headers, the function will return a parsing error. + /// - If any of the provided methods in `allow_methods` cannot be parsed as + /// valid HTTP methods, the function will return a parsing error. /// - /// In all of these cases, the error returned will be the result of the `parse` method - /// of the corresponding type. + /// In all of these cases, the error returned will be the result of the + /// `parse` method of the corresponding type. pub fn cors(&self) -> Result { let mut cors: cors::CorsLayer = cors::CorsLayer::permissive(); @@ -139,8 +160,6 @@ impl MiddlewareLayer for Cors { #[cfg(test)] mod tests { - use super::*; - use crate::tests_cfg; use axum::{ body::Body, http::{Method, Request}, @@ -151,6 +170,9 @@ mod tests { use rstest::rstest; use tower::ServiceExt; + use super::*; + use crate::tests_cfg; + #[rstest] #[case("default", None, None, None)] #[case("with_allow_headers", Some(vec!["token".to_string(), "user".to_string()]), None, None)] @@ -164,7 +186,7 @@ mod tests { #[case] allow_methods: Option>, #[case] max_age: Option, ) { - let mut middleware = Cors::default(); + let mut middleware = Cors::empty(); if let Some(allow_headers) = allow_headers { middleware.allow_headers = allow_headers; } diff --git a/src/controller/middleware/etag.rs b/src/controller/middleware/etag.rs index f2b54227d..87029fd81 100644 --- a/src/controller/middleware/etag.rs +++ b/src/controller/middleware/etag.rs @@ -1,31 +1,29 @@ //! `ETag` Middleware for Caching Requests //! //! This middleware implements the [ETag](https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/ETag) -//! HTTP header for caching responses in Axum. `ETags` are used to validate cache entries by comparing -//! a client's stored `ETag` with the one generated by the server. If the `ETags` match, a `304 Not Modified` -//! response is sent, avoiding the need to resend the full content. +//! HTTP header for caching responses in Axum. `ETags` are used to validate +//! cache entries by comparing a client's stored `ETag` with the one generated +//! by the server. If the `ETags` match, a `304 Not Modified` response is sent, +//! avoiding the need to resend the full content. + +use std::task::{Context, Poll}; -use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; use axum::{ body::Body, extract::Request, http::StatusCode, response::Response, Router as AXRouter, }; use futures_util::future::BoxFuture; use hyper::header::{ETAG, IF_NONE_MATCH}; use serde::{Deserialize, Serialize}; -use std::task::{Context, Poll}; use tower::{Layer, Service}; +use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; + #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Etag { + #[serde(default)] pub enable: bool, } -impl Default for Etag { - fn default() -> Self { - Self { enable: true } - } -} - impl MiddlewareLayer for Etag { /// Returns the name of the middleware fn name(&self) -> &'static str { @@ -47,7 +45,8 @@ impl MiddlewareLayer for Etag { } } -/// [`EtagLayer`] struct for adding `ETag` functionality as a Tower service layer. +/// [`EtagLayer`] struct for adding `ETag` functionality as a Tower service +/// layer. #[derive(Default, Clone)] struct EtagLayer; diff --git a/src/controller/middleware/fallback.rs b/src/controller/middleware/fallback.rs index 4c4ebdcf5..0ef7faf19 100644 --- a/src/controller/middleware/fallback.rs +++ b/src/controller/middleware/fallback.rs @@ -6,16 +6,18 @@ use axum::{http::StatusCode, response::Html, Router as AXRouter}; use serde::{Deserialize, Deserializer, Serialize, Serializer}; +use serde_json::json; use tower_http::services::ServeFile; use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; pub struct StatusCodeWrapper(pub StatusCode); -#[derive(Default, Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct Fallback { /// By default when enabled, returns a prebaked 404 not found page optimized /// for development. For production set something else (see fields below) + #[serde(default)] pub enable: bool, /// For the unlikely reason to return something different than `404`, you /// can set it here @@ -37,6 +39,12 @@ fn default_status_code() -> StatusCode { StatusCode::OK } +impl Default for Fallback { + fn default() -> Self { + serde_json::from_value(json!({})).unwrap() + } +} + fn deserialize_status_code<'de, D>(de: D) -> Result where D: Deserializer<'de>, diff --git a/src/controller/middleware/limit_payload.rs b/src/controller/middleware/limit_payload.rs index 5dd6b5184..313a93183 100644 --- a/src/controller/middleware/limit_payload.rs +++ b/src/controller/middleware/limit_payload.rs @@ -18,20 +18,26 @@ use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct LimitPayload { + #[serde(default)] pub enable: bool, #[serde(deserialize_with = "deserialize_body_limit")] + #[serde(default = "default_body_limit")] pub body_limit: usize, } impl Default for LimitPayload { fn default() -> Self { Self { - enable: true, - body_limit: 2_000_000, + enable: false, + body_limit: default_body_limit(), } } } +fn default_body_limit() -> usize { + 2_000_000 +} + fn deserialize_body_limit<'de, D>(deserializer: D) -> Result where D: Deserializer<'de>, diff --git a/src/controller/middleware/logger.rs b/src/controller/middleware/logger.rs index 98146e658..6f30b16b6 100644 --- a/src/controller/middleware/logger.rs +++ b/src/controller/middleware/logger.rs @@ -1,9 +1,15 @@ //! Logger Middleware //! -//! This middleware provides logging functionality for HTTP requests. It uses `TraceLayer` to -//! log detailed information about each request, such as the HTTP method, URI, version, user agent, -//! and an associated request ID. Additionally, it integrates the application's runtime environment -//! into the log context, allowing environment-specific logging (e.g., "development", "production"). +//! This middleware provides logging functionality for HTTP requests. It uses +//! `TraceLayer` to log detailed information about each request, such as the +//! HTTP method, URI, version, user agent, and an associated request ID. +//! Additionally, it integrates the application's runtime environment +//! into the log context, allowing environment-specific logging (e.g., +//! "development", "production"). + +use axum::{http, Router as AXRouter}; +use serde::{Deserialize, Serialize}; +use tower_http::{add_extension::AddExtensionLayer, trace::TraceLayer}; use crate::{ app::AppContext, @@ -11,19 +17,11 @@ use crate::{ environment::Environment, Result, }; -use axum::{http, Router as AXRouter}; -use serde::{Deserialize, Serialize}; -use tower_http::{add_extension::AddExtensionLayer, trace::TraceLayer}; #[derive(Debug, Clone, Deserialize, Serialize)] pub struct Config { - enable: bool, -} - -impl Default for Config { - fn default() -> Self { - Self { enable: true } - } + #[serde(default)] + pub enable: bool, } /// [`Middleware`] struct responsible for logging HTTP requests. @@ -33,7 +31,8 @@ pub struct Middleware { environment: Environment, } -/// Creates a new instance of [`Middleware`] by cloning the [`Config`] configuration. +/// Creates a new instance of [`Middleware`] by cloning the [`Config`] +/// configuration. #[must_use] pub fn new(config: &Config, environment: &Environment) -> Middleware { Middleware { @@ -57,15 +56,16 @@ impl MiddlewareLayer for Middleware { serde_json::to_value(self) } - /// Applies the logger middleware to the application router by adding layers for: + /// Applies the logger middleware to the application router by adding layers + /// for: /// /// - `TraceLayer`: Logs detailed information about each HTTP request. - /// - `AddExtensionLayer`: Adds the current environment to the request extensions, making it - /// accessible to the `TraceLayer` for logging. - /// - /// The `TraceLayer` is customized with `make_span_with` to extract request-specific details - /// like method, URI, version, user agent, and request ID, then create a tracing span for the request. + /// - `AddExtensionLayer`: Adds the current environment to the request + /// extensions, making it accessible to the `TraceLayer` for logging. /// + /// The `TraceLayer` is customized with `make_span_with` to extract + /// request-specific details like method, URI, version, user agent, and + /// request ID, then create a tracing span for the request. fn apply(&self, app: AXRouter) -> Result> { Ok(app .layer( diff --git a/src/controller/middleware/mod.rs b/src/controller/middleware/mod.rs index 5675443db..4dc341675 100644 --- a/src/controller/middleware/mod.rs +++ b/src/controller/middleware/mod.rs @@ -24,16 +24,31 @@ pub mod static_assets; pub mod timeout; use axum::Router as AXRouter; +use limit_payload::LimitPayload; use serde::{Deserialize, Serialize}; -use crate::{app::AppContext, Result}; +use crate::{app::AppContext, environment::Environment, Result}; /// Trait representing the behavior of middleware components in the application. +/// When implementing a new middleware, make sure to go over this checklist: +/// * The name of the middleware should be an ID that is similar to the field +/// name in configuration (look at how `serde` calls it) +/// * Default value implementation should be paired with `serde` default +/// handlers and default serialization implementation. Which means deriving +/// `Default` will _not_ work. You can use `serde_json` and serialize a new +/// config from an empty value, which will cause `serde` default value +/// handlers to kick in. +/// * If you need completely blank values for configuration (for example for +/// testing), implement an `::empty() -> Self` call ad-hoc. pub trait MiddlewareLayer { /// Returns the name of the middleware. + /// This should match the name of the property in the containing + /// `middleware` section in configuration (as named by `serde`) fn name(&self) -> &'static str; /// Returns whether the middleware is enabled or not. + /// If the middleware is switchable, take this value from a configuration + /// value fn is_enabled(&self) -> bool { true } @@ -53,30 +68,109 @@ pub trait MiddlewareLayer { fn apply(&self, app: AXRouter) -> Result>; } -/// Constructs a default stack of middleware for the Axum application based on -/// the provided context. -/// -/// This function initializes and returns a vector of middleware components that -/// are commonly used in the application. Each middleware is created using its -/// respective `new` function and +#[allow(clippy::unnecessary_lazy_evaluations)] #[must_use] pub fn default_middleware_stack(ctx: &AppContext) -> Vec> { + // Shortened reference to middlewares + let middlewares = &ctx.config.server.middlewares; + vec![ - Box::new(ctx.config.server.middlewares.limit_payload.clone()), - Box::new(ctx.config.server.middlewares.cors.clone()), - Box::new(ctx.config.server.middlewares.catch_panic.clone()), - Box::new(ctx.config.server.middlewares.etag.clone()), - Box::new(ctx.config.server.middlewares.remote_ip.clone()), - Box::new(ctx.config.server.middlewares.compression.clone()), - Box::new(ctx.config.server.middlewares.timeout_request.clone()), - Box::new(ctx.config.server.middlewares.static_assets.clone()), - Box::new(ctx.config.server.middlewares.secure_headers.clone()), + // Limit Payload middleware with a default if none + Box::new( + middlewares + .limit_payload + .clone() + .unwrap_or_else(|| LimitPayload { + enable: true, + ..Default::default() + }), + ), + // CORS middleware with a default if none + Box::new(middlewares.cors.clone().unwrap_or_else(|| cors::Cors { + enable: true, + ..Default::default() + })), + // Catch Panic middleware with a default if none + Box::new( + middlewares + .catch_panic + .clone() + .unwrap_or_else(|| catch_panic::CatchPanic { enable: true }), + ), + // Etag middleware with a default if none + Box::new( + middlewares + .etag + .clone() + .unwrap_or_else(|| etag::Etag { enable: true }), + ), + // Remote IP middleware with a default if none + Box::new( + middlewares + .remote_ip + .clone() + .unwrap_or_else(|| remote_ip::RemoteIpMiddleware { + enable: false, + ..Default::default() + }), + ), + // Compression middleware with a default if none + Box::new( + middlewares + .compression + .clone() + .unwrap_or_else(|| compression::Compression { enable: false }), + ), + // Timeout Request middleware with a default if none + Box::new( + middlewares + .timeout_request + .clone() + .unwrap_or_else(|| timeout::TimeOut { + enable: false, + ..Default::default() + }), + ), + // Static Assets middleware with a default if none + Box::new(middlewares.static_assets.clone().unwrap_or_else(|| { + static_assets::StaticAssets { + enable: false, + ..Default::default() + } + })), + // Secure Headers middleware with a default if none + Box::new(middlewares.secure_headers.clone().unwrap_or_else(|| { + secure_headers::SecureHeader { + enable: false, + ..Default::default() + } + })), + // Logger middleware with default logger configuration Box::new(logger::new( - &ctx.config.server.middlewares.logger, + &middlewares + .logger + .clone() + .unwrap_or_else(|| logger::Config { enable: true }), &ctx.environment, )), - Box::new(ctx.config.server.middlewares.request_id.clone()), - Box::new(ctx.config.server.middlewares.fallback.clone()), + // Request ID middleware with a default if none + Box::new( + middlewares + .request_id + .clone() + .unwrap_or_else(|| request_id::RequestId { enable: true }), + ), + // Fallback middleware with a default if none + Box::new( + middlewares + .fallback + .clone() + .unwrap_or_else(|| fallback::Fallback { + enable: ctx.environment != Environment::Production, + ..Default::default() + }), + ), + // Powered by middleware with a default identifier Box::new(powered_by::new(ctx.config.server.ident.as_deref())), ] } @@ -85,51 +179,39 @@ pub fn default_middleware_stack(ctx: &AppContext) -> Vec, /// Etag cache headers. - #[serde(default)] - pub etag: etag::Etag, + pub etag: Option, /// Limit the payload request. - #[serde(default)] - pub limit_payload: limit_payload::LimitPayload, + pub limit_payload: Option, /// Logger and augmenting trace id with request data - #[serde(default)] - pub logger: logger::Config, + pub logger: Option, /// Catch any code panic and log the error. - #[serde(default)] - pub catch_panic: catch_panic::CatchPanic, + pub catch_panic: Option, /// Setting a global timeout for requests - #[serde(default)] - pub timeout_request: timeout::TimeOut, + pub timeout_request: Option, /// CORS configuration - #[serde(default)] - pub cors: cors::Cors, + pub cors: Option, /// Serving static assets #[serde(rename = "static")] - #[serde(default)] - pub static_assets: static_assets::StaticAssets, + pub static_assets: Option, /// Sets a set of secure headers - #[serde(default)] - pub secure_headers: secure_headers::SecureHeader, + pub secure_headers: Option, /// Calculates a remote IP based on `X-Forwarded-For` when behind a proxy - #[serde(default)] - pub remote_ip: remote_ip::RemoteIpMiddleware, + pub remote_ip: Option, /// Configure fallback behavior when hitting a missing URL - #[serde(default)] - pub fallback: fallback::Fallback, + pub fallback: Option, /// Request ID - #[serde(default)] - pub request_id: request_id::RequestId, + pub request_id: Option, } diff --git a/src/controller/middleware/remote_ip.rs b/src/controller/middleware/remote_ip.rs index 7723e1c01..b0fbf18d8 100644 --- a/src/controller/middleware/remote_ip.rs +++ b/src/controller/middleware/remote_ip.rs @@ -93,6 +93,7 @@ const X_FORWARDED_FOR: &str = "X-Forwarded-For"; /// "Trusted proxy list" #[derive(Default, Serialize, Deserialize, Debug, Clone)] pub struct RemoteIpMiddleware { + #[serde(default)] pub enable: bool, /// A list of alternative proxy list IP ranges and/or network range (will /// replace built-in proxy list) @@ -107,7 +108,9 @@ impl MiddlewareLayer for RemoteIpMiddleware { /// Returns whether the middleware is enabled or not fn is_enabled(&self) -> bool { - self.enable && self.trusted_proxies.as_ref().is_some_and(|t| !t.is_empty()) + self.enable + && (self.trusted_proxies.is_none() + || self.trusted_proxies.as_ref().is_some_and(|t| !t.is_empty())) } fn config(&self) -> serde_json::Result { diff --git a/src/controller/middleware/request_id.rs b/src/controller/middleware/request_id.rs index b22a95f41..aab60f54a 100644 --- a/src/controller/middleware/request_id.rs +++ b/src/controller/middleware/request_id.rs @@ -24,13 +24,8 @@ lazy_static! { #[derive(Debug, Clone, Deserialize, Serialize)] pub struct RequestId { - enable: bool, -} - -impl Default for RequestId { - fn default() -> Self { - Self { enable: true } - } + #[serde(default)] + pub enable: bool, } impl MiddlewareLayer for RequestId { diff --git a/src/controller/middleware/secure_headers.rs b/src/controller/middleware/secure_headers.rs index cbe35a608..bbdbe26c7 100644 --- a/src/controller/middleware/secure_headers.rs +++ b/src/controller/middleware/secure_headers.rs @@ -16,7 +16,7 @@ use axum::{ use futures_util::future::BoxFuture; use lazy_static::lazy_static; use serde::{Deserialize, Serialize}; -use serde_json; +use serde_json::{self, json}; use tower::{Layer, Service}; use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Error, Result}; @@ -61,35 +61,42 @@ lazy_static! { /// one: two /// ``` /// +/// To support `htmx`, You can add the following override, to allow some inline +/// running of scripts: +/// +/// ```yaml +/// secure_headers: +/// preset: github +/// overrides: +/// # this allows you to use HTMX, and has unsafe-inline. Remove or consider in production +/// "Content-Security-Policy": "default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src 'unsafe-inline' 'self' https:; style-src 'self' https: 'unsafe-inline'" +/// ``` +/// /// For the list of presets and their content look at [secure_headers.json](https://github.com/loco-rs/loco/blob/master/src/controller/middleware/secure_headers.rs) #[derive(Serialize, Deserialize, Debug, Clone)] pub struct SecureHeader { - #[serde(default = "default_true")] + #[serde(default)] pub enable: bool, - pub preset: Option, + #[serde(default = "default_preset")] + pub preset: String, + #[serde(default)] pub overrides: Option>, } -fn default_true() -> bool { - true -} - impl Default for SecureHeader { - /// Provides a default secure header configuration, using the `github` - /// preset. fn default() -> Self { - Self { - enable: true, - preset: Some("github".to_string()), - overrides: None, - } + serde_json::from_value(json!({})).unwrap() } } +fn default_preset() -> String { + "github".to_string() +} + impl MiddlewareLayer for SecureHeader { /// Returns the name of the middleware fn name(&self) -> &'static str { - "secure headers" + "secure_headers" } /// Returns whether the middleware is enabled or not @@ -113,14 +120,15 @@ impl SecureHeader { /// Applies the preset headers and any custom overrides. fn as_headers(&self) -> Result> { let mut headers = vec![]; - if let Some(preset) = &self.preset { - let p = PRESETS.get(preset).ok_or_else(|| { - Error::Message(format!( - "secure_headers: a preset named `{preset}` does not exist" - )) - })?; - Self::push_headers(&mut headers, p)?; - } + + let preset = &self.preset; + let p = PRESETS.get(preset).ok_or_else(|| { + Error::Message(format!( + "secure_headers: a preset named `{preset}` does not exist" + )) + })?; + + Self::push_headers(&mut headers, p)?; if let Some(overrides) = &self.overrides { Self::push_headers(&mut headers, overrides)?; } @@ -227,7 +235,7 @@ mod tests { async fn can_set_headers() { let config = SecureHeader { enable: true, - preset: Some("github".to_string()), + preset: "github".to_string(), overrides: None, }; let app = Router::new() @@ -251,7 +259,7 @@ mod tests { let config = SecureHeader { enable: true, - preset: Some("github".to_string()), + preset: "github".to_string(), overrides: Some(overrides), }; let app = Router::new() diff --git a/src/controller/middleware/static_assets.rs b/src/controller/middleware/static_assets.rs index 06e95e2f4..20aea4f45 100644 --- a/src/controller/middleware/static_assets.rs +++ b/src/controller/middleware/static_assets.rs @@ -13,25 +13,55 @@ use std::path::PathBuf; use axum::Router as AXRouter; use serde::{Deserialize, Serialize}; +use serde_json::json; use tower_http::services::{ServeDir, ServeFile}; use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Error, Result}; /// Static asset middleware configuration -#[derive(Default, Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct StaticAssets { + #[serde(default)] pub enable: bool, /// Check that assets must exist on disk + #[serde(default = "default_must_exist")] pub must_exist: bool, /// Assets location + #[serde(default = "default_folder_config")] pub folder: FolderConfig, /// Fallback page for a case when no asset exists (404). Useful for SPA /// (single page app) where routes are virtual. + #[serde(default = "default_fallback")] pub fallback: String, /// Enable `precompressed_gzip` + #[serde(default = "default_precompressed")] pub precompressed: bool, } +impl Default for StaticAssets { + fn default() -> Self { + serde_json::from_value(json!({})).unwrap() + } +} + +fn default_must_exist() -> bool { + true +} + +fn default_precompressed() -> bool { + false +} + +fn default_fallback() -> String { + "assets/static/404.html".to_string() +} + +fn default_folder_config() -> FolderConfig { + FolderConfig { + uri: "/static".to_string(), + path: "assets/static".to_string(), + } +} #[derive(Default, Debug, Clone, Deserialize, Serialize)] pub struct FolderConfig { /// Uri for the assets @@ -44,7 +74,7 @@ pub struct FolderConfig { impl MiddlewareLayer for StaticAssets { /// Returns the name of the middleware. fn name(&self) -> &'static str { - "static_assets" + "static" } /// Checks if the static assets middleware is enabled. diff --git a/src/controller/middleware/timeout.rs b/src/controller/middleware/timeout.rs index 1488d204f..fc0a5c16e 100644 --- a/src/controller/middleware/timeout.rs +++ b/src/controller/middleware/timeout.rs @@ -1,32 +1,47 @@ //! Timeout Request Middleware. //! //! This middleware applies a timeout to requests processed by the application. -//! The timeout duration is configurable and defined via the [`TimeoutRequestMiddleware`] -//! configuration. The middleware ensures that requests do not run beyond the specified -//! timeout period, improving the overall performance and responsiveness of the application. +//! The timeout duration is configurable and defined via the +//! [`TimeoutRequestMiddleware`] configuration. The middleware ensures that +//! requests do not run beyond the specified timeout period, improving the +//! overall performance and responsiveness of the application. //! -//! If a request exceeds the specified timeout duration, the middleware will return -//! a `408 Request Timeout` status code to the client, indicating that the request -//! took too long to process. -//! -use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; +//! If a request exceeds the specified timeout duration, the middleware will +//! return a `408 Request Timeout` status code to the client, indicating that +//! the request took too long to process. +use std::time::Duration; + use axum::Router as AXRouter; use serde::{Deserialize, Serialize}; -use std::time::Duration; +use serde_json::json; use tower_http::timeout::TimeoutLayer; +use crate::{app::AppContext, controller::middleware::MiddlewareLayer, Result}; + /// Timeout middleware configuration -#[derive(Default, Debug, Clone, Deserialize, Serialize)] +#[derive(Debug, Clone, Deserialize, Serialize)] pub struct TimeOut { + #[serde(default)] pub enable: bool, // Timeout request in milliseconds + #[serde(default = "default_timeout")] pub timeout: u64, } +impl Default for TimeOut { + fn default() -> Self { + serde_json::from_value(json!({})).unwrap() + } +} + +fn default_timeout() -> u64 { + 5_000 +} + impl MiddlewareLayer for TimeOut { /// Returns the name of the middleware. fn name(&self) -> &'static str { - "timeout" + "timeout_request" } /// Checks if the timeout middleware is enabled. @@ -40,9 +55,9 @@ impl MiddlewareLayer for TimeOut { /// Applies the timeout middleware to the application router. /// - /// This method wraps the provided [`AXRouter`] in a [`TimeoutLayer`], ensuring - /// that requests exceeding the specified timeout duration will be interrupted. - /// + /// This method wraps the provided [`AXRouter`] in a [`TimeoutLayer`], + /// ensuring that requests exceeding the specified timeout duration will + /// be interrupted. fn apply(&self, app: AXRouter) -> Result> { Ok(app.layer(TimeoutLayer::new(Duration::from_millis(self.timeout)))) } diff --git a/src/gen/mod.rs b/src/gen/mod.rs index a8c629b9a..7640b74d6 100644 --- a/src/gen/mod.rs +++ b/src/gen/mod.rs @@ -246,10 +246,21 @@ pub fn generate(component: Component, config: &Config) -> Result<()> { match deployment_kind { DeploymentKind::Docker => { - let copy_asset_folder = - &config.server.middlewares.static_assets.folder.path.clone(); + let copy_asset_folder = &config + .server + .middlewares + .static_assets + .clone() + .map(|a| a.folder.path) + .unwrap_or_default(); - let fallback_file = &config.server.middlewares.static_assets.fallback.clone(); + let fallback_file = &config + .server + .middlewares + .static_assets + .clone() + .map(|a| a.fallback) + .unwrap_or_default(); let vars = json!({ "pkg_name": H::app_name(), diff --git a/starters/lightweight-service/config/development.yaml b/starters/lightweight-service/config/development.yaml index 3996c8145..cb7330818 100644 --- a/starters/lightweight-service/config/development.yaml +++ b/starters/lightweight-service/config/development.yaml @@ -18,44 +18,3 @@ server: port: 5150 # The UI hostname or IP address that mailers will point to. host: http://localhost - # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block - middlewares: - # Enable Etag cache header middleware - etag: - enable: true - # Allows to limit the payload size request. payload that bigger than this file will blocked the request. - limit_payload: - # Enable/Disable the middleware. - enable: true - # the limit size. can be b,kb,kib,mb,mib,gb,gib - body_limit: 5mb - # set secure headers - secure_headers: - preset: github - # calculate remote IP based on `X-Forwarded-For` when behind a proxy or load balancer - # use RemoteIP(..) extractor to get the remote IP. - # without this middleware, you'll get the proxy IP instead. - # For more: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/remote_ip.rb - # - # NOTE! only enable when under a proxy, otherwise this can lead to IP spoofing vulnerabilities - # trust me, you'll know if you need this middleware. - remote_ip: - enable: false - # # replace the default trusted proxies: - # trusted_proxies: - # - ip range 1 - # - ip range 2 .. - # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. - logger: - # Enable/Disable the middleware. - enable: true - # when your code is panicked, the request still returns 500 status code. - catch_panic: - # Enable/Disable the middleware. - enable: true - # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. - timeout_request: - # Enable/Disable the middleware. - enable: false - # Duration time in milliseconds. - timeout: 5000 diff --git a/starters/lightweight-service/config/test.yaml b/starters/lightweight-service/config/test.yaml index 35eb0fe0f..701d146b6 100644 --- a/starters/lightweight-service/config/test.yaml +++ b/starters/lightweight-service/config/test.yaml @@ -18,25 +18,3 @@ server: port: 5150 # The UI hostname or IP address that mailers will point to. host: http://localhost - # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block - middlewares: - # Allows to limit the payload size request. payload that bigger than this file will blocked the request. - limit_payload: - # Enable/Disable the middleware. - enable: true - # the limit size. can be b,kb,kib,mb,mib,gb,gib - body_limit: 5mb - # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. - logger: - # Enable/Disable the middleware. - enable: true - # when your code is panicked, the request still returns 500 status code. - catch_panic: - # Enable/Disable the middleware. - enable: true - # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. - timeout_request: - # Enable/Disable the middleware. - enable: false - # Duration time in milliseconds. - timeout: 5000 diff --git a/starters/rest-api/config/development.yaml b/starters/rest-api/config/development.yaml index 0223329e6..bdcde4409 100644 --- a/starters/rest-api/config/development.yaml +++ b/starters/rest-api/config/development.yaml @@ -20,60 +20,6 @@ server: port: 5150 # The UI hostname or IP address that mailers will point to. host: http://localhost - # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block - middlewares: - # Enable Etag cache header middleware - etag: - enable: true - # Allows to limit the payload size request. payload that bigger than this file will blocked the request. - limit_payload: - # Enable/Disable the middleware. - enable: true - # the limit size. can be b,kb,kib,mb,mib,gb,gib - body_limit: 5mb - # set secure headers - secure_headers: - preset: github - # calculate remote IP based on `X-Forwarded-For` when behind a proxy or load balancer - # use RemoteIP(..) extractor to get the remote IP. - # without this middleware, you'll get the proxy IP instead. - # For more: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/remote_ip.rb - # - # NOTE! only enable when under a proxy, otherwise this can lead to IP spoofing vulnerabilities - # trust me, you'll know if you need this middleware. - remote_ip: - enable: false - # # replace the default trusted proxies: - # trusted_proxies: - # - ip range 1 - # - ip range 2 .. - # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. - logger: - # Enable/Disable the middleware. - enable: true - # when your code is panicked, the request still returns 500 status code. - catch_panic: - # Enable/Disable the middleware. - enable: true - # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. - timeout_request: - # Enable/Disable the middleware. - enable: false - # Duration time in milliseconds. - timeout: 5000 - cors: - enable: true - # Set the value of the [`Access-Control-Allow-Origin`][mdn] header - # allow_origins: - # - https://loco.rs - # Set the value of the [`Access-Control-Allow-Headers`][mdn] header - # allow_headers: - # - Content-Type - # Set the value of the [`Access-Control-Allow-Methods`][mdn] header - # allow_methods: - # - POST - # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds - # max_age: 3600 # Worker Configuration workers: diff --git a/starters/rest-api/config/test.yaml b/starters/rest-api/config/test.yaml index 24f30abeb..f54d0b610 100644 --- a/starters/rest-api/config/test.yaml +++ b/starters/rest-api/config/test.yaml @@ -18,41 +18,6 @@ server: port: 5150 # The UI hostname or IP address that mailers will point to. host: http://localhost - # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block - middlewares: - # Allows to limit the payload size request. payload that bigger than this file will blocked the request. - limit_payload: - # Enable/Disable the middleware. - enable: true - # the limit size. can be b,kb,kib,mb,mib,gb,gib - body_limit: 5mb - # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. - logger: - # Enable/Disable the middleware. - enable: true - # when your code is panicked, the request still returns 500 status code. - catch_panic: - # Enable/Disable the middleware. - enable: true - # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. - timeout_request: - # Enable/Disable the middleware. - enable: false - # Duration time in milliseconds. - timeout: 5000 - cors: - enable: true - # Set the value of the [`Access-Control-Allow-Origin`][mdn] header - # allow_origins: - # - https://loco.rs - # Set the value of the [`Access-Control-Allow-Headers`][mdn] header - # allow_headers: - # - Content-Type - # Set the value of the [`Access-Control-Allow-Methods`][mdn] header - # allow_methods: - # - POST - # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds - # max_age: 3600 # Worker Configuration workers: diff --git a/starters/saas/config/development.yaml b/starters/saas/config/development.yaml index bc8870114..c20c113f9 100644 --- a/starters/saas/config/development.yaml +++ b/starters/saas/config/development.yaml @@ -22,69 +22,6 @@ server: host: http://localhost # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block middlewares: - # Fallback file for not found (404) routes - # disable this for production or use your own file - fallback: - enable: true - # use a file if you want a different 404 page - # file: path/to/file.html - - # Enable Etag cache header middleware - etag: - enable: true - # Allows to limit the payload size request. payload that bigger than this file will blocked the request. - limit_payload: - # Enable/Disable the middleware. - enable: true - # the limit size. can be b,kb,kib,mb,mib,gb,gib - body_limit: 5mb - # set secure headers - secure_headers: - preset: github - overrides: - # this allows you to use HTMX, and has unsafe-inline. Remove or consider in production - "Content-Security-Policy": "default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src 'unsafe-inline' 'self' https:; style-src 'self' https: 'unsafe-inline'" - # calculate remote IP based on `X-Forwarded-For` when behind a proxy or load balancer - # use RemoteIP(..) extractor to get the remote IP. - # without this middleware, you'll get the proxy IP instead. - # For more: https://github.com/rails/rails/blob/main/actionpack/lib/action_dispatch/middleware/remote_ip.rb - # - # NOTE! only enable when under a proxy, otherwise this can lead to IP spoofing vulnerabilities - # trust me, you'll know if you need this middleware. - remote_ip: - enable: false - # # replace the default trusted proxies: - # trusted_proxies: - # - ip range 1 - # - ip range 2 .. - # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. - logger: - # Enable/Disable the middleware. - enable: true - # when your code is panicked, the request still returns 500 status code. - catch_panic: - # Enable/Disable the middleware. - enable: true - # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. - timeout_request: - # Enable/Disable the middleware. - enable: false - # Duration time in milliseconds. - timeout: 5000 - cors: - enable: true - # Set the value of the [`Access-Control-Allow-Origin`][mdn] header - # allow_origins: - # - https://loco.rs - # Set the value of the [`Access-Control-Allow-Headers`][mdn] header - # allow_headers: - # - Content-Type - # Set the value of the [`Access-Control-Allow-Methods`][mdn] header - # allow_methods: - # - POST - # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds - # max_age: 3600 - # ############################################# # Full stack SaaS asset serving # ############################################# diff --git a/starters/saas/config/test.yaml b/starters/saas/config/test.yaml index b0dbb8d34..f872bff2b 100644 --- a/starters/saas/config/test.yaml +++ b/starters/saas/config/test.yaml @@ -20,39 +20,6 @@ server: host: http://localhost # Out of the box middleware configuration. to disable middleware you can changed the `enable` field to `false` of comment the middleware block middlewares: - # Allows to limit the payload size request. payload that bigger than this file will blocked the request. - limit_payload: - # Enable/Disable the middleware. - enable: true - # the limit size. can be b,kb,kib,mb,mib,gb,gib - body_limit: 5mb - # Generating a unique request ID and enhancing logging with additional information such as the start and completion of request processing, latency, status code, and other request details. - logger: - # Enable/Disable the middleware. - enable: true - # when your code is panicked, the request still returns 500 status code. - catch_panic: - # Enable/Disable the middleware. - enable: true - # Timeout for incoming requests middleware. requests that take more time from the configuration will cute and 408 status code will returned. - timeout_request: - # Enable/Disable the middleware. - enable: false - # Duration time in milliseconds. - timeout: 5000 - cors: - enable: true - # Set the value of the [`Access-Control-Allow-Origin`][mdn] header - # allow_origins: - # - https://loco.rs - # Set the value of the [`Access-Control-Allow-Headers`][mdn] header - # allow_headers: - # - Content-Type - # Set the value of the [`Access-Control-Allow-Methods`][mdn] header - # allow_methods: - # - POST - # Set the value of the [`Access-Control-Max-Age`][mdn] header in seconds - # max_age: 3600 static: enable: true precompressed: false diff --git a/tests/controller/middlewares.rs b/tests/controller/middlewares.rs index 81553913e..e68ded28a 100644 --- a/tests/controller/middlewares.rs +++ b/tests/controller/middlewares.rs @@ -1,10 +1,12 @@ -use crate::infra_cfg; +use std::{collections::BTreeMap, path::PathBuf}; + use axum::http::StatusCode; use insta::assert_debug_snapshot; use loco_rs::{controller::middleware, prelude::*, tests_cfg}; use rstest::rstest; use serial_test::serial; -use std::{collections::BTreeMap, path::PathBuf}; + +use crate::infra_cfg; macro_rules! configure_insta { ($($expr:expr),*) => { @@ -29,7 +31,8 @@ async fn panic(#[case] enable: bool) { } let mut ctx: AppContext = tests_cfg::app::get_app_context().await; - ctx.config.server.middlewares.catch_panic = middleware::catch_panic::CatchPanic { enable }; + ctx.config.server.middlewares.catch_panic = + Some(middleware::catch_panic::CatchPanic { enable }); let handle = infra_cfg::server::start_with_route(ctx, "/", get(action)).await; let res = reqwest::get(infra_cfg::server::get_base_url()).await; @@ -59,7 +62,7 @@ async fn etag(#[case] enable: bool) { let mut ctx: AppContext = tests_cfg::app::get_app_context().await; - ctx.config.server.middlewares.etag = middleware::etag::Etag { enable }; + ctx.config.server.middlewares.etag = Some(middleware::etag::Etag { enable }); let handle = infra_cfg::server::start_with_route(ctx, "/", get(action)).await; @@ -92,10 +95,10 @@ async fn remote_ip(#[case] enable: bool, #[case] expected: &str) { let mut ctx: AppContext = tests_cfg::app::get_app_context().await; - ctx.config.server.middlewares.remote_ip = middleware::remote_ip::RemoteIpMiddleware { + ctx.config.server.middlewares.remote_ip = Some(middleware::remote_ip::RemoteIpMiddleware { enable, trusted_proxies: Some(vec!["192.1.1.1/8".to_string()]), - }; + }); let handle = infra_cfg::server::start_with_route(ctx, "/", get(action)).await; @@ -129,7 +132,7 @@ async fn timeout(#[case] enable: bool) { let mut ctx: AppContext = tests_cfg::app::get_app_context().await; ctx.config.server.middlewares.timeout_request = - middleware::timeout::TimeOut { enable, timeout: 2 }; + Some(middleware::timeout::TimeOut { enable, timeout: 2 }); let handle = infra_cfg::server::start_with_route(ctx, "/", get(action)).await; @@ -161,14 +164,15 @@ async fn cors( #[case] allow_methods: Option>, #[case] max_age: Option, ) { + use loco_rs::controller::middleware::cors::Cors; + configure_insta!(); let mut ctx: AppContext = tests_cfg::app::get_app_context().await; - let mut middleware = loco_rs::controller::middleware::cors::Cors { - enable, - ..Default::default() - }; + let mut middleware = Cors::empty(); + middleware.enable = enable; + if let Some(allow_headers) = allow_headers { middleware.allow_headers = allow_headers; } @@ -177,7 +181,7 @@ async fn cors( } middleware.max_age = max_age; - ctx.config.server.middlewares.cors = middleware; + ctx.config.server.middlewares.cors = Some(middleware); let handle = infra_cfg::server::start_from_ctx(ctx).await; @@ -220,10 +224,10 @@ async fn limit_payload(#[case] enable: bool) { let mut ctx: AppContext = tests_cfg::app::get_app_context().await; - ctx.config.server.middlewares.limit_payload = middleware::limit_payload::LimitPayload { + ctx.config.server.middlewares.limit_payload = Some(middleware::limit_payload::LimitPayload { enable, body_limit: 0x1B, - }; + }); let handle = infra_cfg::server::start_from_ctx(ctx).await; @@ -263,7 +267,7 @@ async fn static_assets() { let mut ctx: AppContext = tests_cfg::app::get_app_context().await; let base_static_path = static_asset_path.join(base_static_assets_path); - ctx.config.server.middlewares.static_assets = middleware::static_assets::StaticAssets { + ctx.config.server.middlewares.static_assets = Some(middleware::static_assets::StaticAssets { enable: true, must_exist: true, folder: middleware::static_assets::FolderConfig { @@ -272,7 +276,7 @@ async fn static_assets() { }, fallback: base_static_path.join("404.html").display().to_string(), precompressed: false, - }; + }); let handle = infra_cfg::server::start_from_ctx(ctx).await; @@ -314,12 +318,13 @@ async fn secure_headers( let mut ctx: AppContext = tests_cfg::app::get_app_context().await; - ctx.config.server.middlewares.secure_headers = + ctx.config.server.middlewares.secure_headers = Some( loco_rs::controller::middleware::secure_headers::SecureHeader { enable: true, - preset: preset.clone(), + preset: preset.clone().unwrap_or_else(|| "github".to_string()), overrides: overrides.clone(), - }; + }, + ); let handle = infra_cfg::server::start_from_ctx(ctx).await; @@ -388,7 +393,7 @@ async fn fallback( fallback_config.code = code; }; - ctx.config.server.middlewares.fallback = fallback_config; + ctx.config.server.middlewares.fallback = Some(fallback_config); let handle = infra_cfg::server::start_from_ctx(ctx).await; diff --git a/tests/controller/snapshots/secure_headers_[none]_overrides[none]@middlewares.snap b/tests/controller/snapshots/secure_headers_[none]_overrides[none]@middlewares.snap index 13d3d8a2c..7a5a84027 100644 --- a/tests/controller/snapshots/secure_headers_[none]_overrides[none]@middlewares.snap +++ b/tests/controller/snapshots/secure_headers_[none]_overrides[none]@middlewares.snap @@ -2,4 +2,6 @@ source: tests/controller/middlewares.rs expression: policy --- -None +Some( + "default-src 'self' https:; font-src 'self' https: data:; img-src 'self' https: data:; object-src 'none'; script-src https:; style-src 'self' https: 'unsafe-inline'", +)