From 8a19834ea37181eeff42a5e44534f5f9447a5e06 Mon Sep 17 00:00:00 2001 From: glendc Date: Tue, 7 Nov 2023 14:25:02 +0100 Subject: [PATCH] add header config tower support useful for http services to turn request headers into typed extension config values, e.g. proxy config, tls config, http config, emulation config, etc... --- rama/Cargo.toml | 2 +- .../net/http/{headers.rs => header_value.rs} | 2 +- rama/src/net/http/mod.rs | 9 +- rama/src/server/http/header.rs | 1 - rama/src/server/http/layer/header_config.rs | 179 ++++++++++++++++++ rama/src/server/http/layer/mod.rs | 3 +- rama/src/server/http/mod.rs | 1 - rama/src/server/http/service/router.rs | 2 +- rama/src/state.rs | 4 +- 9 files changed, 193 insertions(+), 10 deletions(-) rename rama/src/net/http/{headers.rs => header_value.rs} (97%) delete mode 100644 rama/src/server/http/header.rs create mode 100644 rama/src/server/http/layer/header_config.rs diff --git a/rama/Cargo.toml b/rama/Cargo.toml index 00b89c30..77a5d5b1 100644 --- a/rama/Cargo.toml +++ b/rama/Cargo.toml @@ -15,7 +15,7 @@ http = "0.2.9" matchit = "0.7.3" pin-project-lite = "0.2.13" rustls = "0.22.0-alpha.3" -serde = "1.0.192" +serde = { version = "1.0", features = ["derive"] } serde_urlencoded = "0.7.1" tokio = { version = "1.33.0", features = ["net", "io-util"] } tokio-graceful = "0.1.5" diff --git a/rama/src/net/http/headers.rs b/rama/src/net/http/header_value.rs similarity index 97% rename from rama/src/net/http/headers.rs rename to rama/src/net/http/header_value.rs index 00e590c0..dd3b344d 100644 --- a/rama/src/net/http/headers.rs +++ b/rama/src/net/http/header_value.rs @@ -1,4 +1,4 @@ -use http::{ +use crate::net::http::{ header::{AsHeaderName, GetAll}, HeaderValue, Request, Response, }; diff --git a/rama/src/net/http/mod.rs b/rama/src/net/http/mod.rs index 0a80dbac..2317b5c8 100644 --- a/rama/src/net/http/mod.rs +++ b/rama/src/net/http/mod.rs @@ -1,2 +1,7 @@ -mod headers; -pub use headers::HeaderValueGetter; +mod header_value; +pub use header_value::HeaderValueGetter; + +pub use http::{ + header, request, response, HeaderMap, HeaderName, HeaderValue, Method, Request, Response, + StatusCode, +}; diff --git a/rama/src/server/http/header.rs b/rama/src/server/http/header.rs deleted file mode 100644 index 8b137891..00000000 --- a/rama/src/server/http/header.rs +++ /dev/null @@ -1 +0,0 @@ - diff --git a/rama/src/server/http/layer/header_config.rs b/rama/src/server/http/layer/header_config.rs new file mode 100644 index 00000000..69996bf8 --- /dev/null +++ b/rama/src/server/http/layer/header_config.rs @@ -0,0 +1,179 @@ +use std::marker::PhantomData; + +use serde::de::DeserializeOwned; + +use crate::{ + net::http::{HeaderValueGetter, Request}, + service::{BoxError, Layer, Service}, +}; + +#[derive(Debug)] +pub struct HeaderConfigService { + inner: S, + key: String, + _marker: PhantomData, +} + +impl HeaderConfigService { + pub fn new(inner: S, key: String) -> Self { + Self { + inner, + key, + _marker: PhantomData, + } + } +} + +impl Clone for HeaderConfigService +where + S: Clone, +{ + fn clone(&self) -> Self { + Self { + inner: self.inner.clone(), + key: self.key.clone(), + _marker: PhantomData, + } + } +} + +impl Service> for HeaderConfigService +where + S: Service, Error = E>, + T: DeserializeOwned + Send + Sync + 'static, + E: Into, +{ + type Response = S::Response; + type Error = BoxError; + + async fn call(&mut self, mut request: Request) -> Result { + let value = request + .header_value(&self.key) + .ok_or(HeaderMissingErr(self.key.clone()))? + .to_str()?; + let config = serde_urlencoded::from_str::(value)?; + request.extensions_mut().insert(config); + self.inner.call(request).await.map_err(Into::into) + } +} + +#[derive(Debug)] +struct HeaderMissingErr(String); + +impl std::fmt::Display for HeaderMissingErr { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!(f, "`{}` header is missing", self.0) + } +} + +impl std::error::Error for HeaderMissingErr {} + +pub struct HeaderConfigLayer { + key: String, + _marker: PhantomData, +} + +impl HeaderConfigLayer { + pub fn new(key: String) -> Self { + Self { + key, + _marker: PhantomData, + } + } +} + +impl Layer for HeaderConfigLayer +where + S: Service>, +{ + type Service = HeaderConfigService; + + fn layer(&self, inner: S) -> Self::Service { + HeaderConfigService { + inner, + key: self.key.clone(), + _marker: PhantomData, + } + } +} + +#[cfg(test)] +mod test { + use serde::Deserialize; + + use crate::net::http::Method; + + use super::*; + + #[tokio::test] + async fn test_header_config_happy_path() { + let request = Request::builder() + .method(Method::GET) + .uri("https://www.example.com") + .header("x-proxy-config", "s=E%26G&n=1&b=true") + .body(()) + .unwrap(); + + let inner_service = crate::service::service_fn(|req: Request<()>| async move { + let cfg: &Config = req.extensions().get().unwrap(); + assert_eq!(cfg.s, "E&G"); + assert_eq!(cfg.n, 1); + assert!(cfg.m.is_none()); + assert!(cfg.b); + + Ok::<_, std::convert::Infallible>(()) + }); + + let mut service = + HeaderConfigService::::new(inner_service, "x-proxy-config".to_string()); + + service.call(request).await.unwrap(); + } + + #[tokio::test] + async fn test_header_config_missing_header() { + let request = Request::builder() + .method(Method::GET) + .uri("https://www.example.com") + .body(()) + .unwrap(); + + let inner_service = crate::service::service_fn(|_: Request<()>| async move { + Ok::<_, std::convert::Infallible>(()) + }); + + let mut service = + HeaderConfigService::::new(inner_service, "x-proxy-config".to_string()); + + let result = service.call(request).await; + assert!(result.is_err()); + } + + #[tokio::test] + async fn test_header_config_invalid_config() { + let request = Request::builder() + .method(Method::GET) + .uri("https://www.example.com") + .header("x-proxy-config", "s=bar&n=1&b=invalid") + .body(()) + .unwrap(); + + let inner_service = crate::service::service_fn(|_: Request<()>| async move { + Ok::<_, std::convert::Infallible>(()) + }); + + let mut service = + HeaderConfigService::::new(inner_service, "x-proxy-config".to_string()); + + let result = service.call(request).await; + assert!(result.is_err()); + } + + #[derive(Debug, Deserialize)] + struct Config { + s: String, + n: i32, + m: Option, + b: bool, + } +} diff --git a/rama/src/server/http/layer/mod.rs b/rama/src/server/http/layer/mod.rs index 8b137891..de78e38b 100644 --- a/rama/src/server/http/layer/mod.rs +++ b/rama/src/server/http/layer/mod.rs @@ -1 +1,2 @@ - +mod header_config; +pub use header_config::{HeaderConfigLayer, HeaderConfigService}; diff --git a/rama/src/server/http/mod.rs b/rama/src/server/http/mod.rs index 1f587155..568de303 100644 --- a/rama/src/server/http/mod.rs +++ b/rama/src/server/http/mod.rs @@ -1,4 +1,3 @@ -pub mod header; pub mod layer; pub mod service; diff --git a/rama/src/server/http/service/router.rs b/rama/src/server/http/service/router.rs index 1d629541..e627b53f 100644 --- a/rama/src/server/http/service/router.rs +++ b/rama/src/server/http/service/router.rs @@ -1,6 +1,6 @@ // use std::future::Future; -// use http::{Method, Request, Response}; +// use crate::net::http::{Method, Request, Response}; // use matchit::Router as PathRouter; // use crate::Service; diff --git a/rama/src/state.rs b/rama/src/state.rs index 4fcd8221..30f75306 100644 --- a/rama/src/state.rs +++ b/rama/src/state.rs @@ -15,7 +15,7 @@ impl Extendable for Extensions { } } -impl Extendable for http::Request { +impl Extendable for crate::net::http::Request { fn extensions(&self) -> &Extensions { self.extensions() } @@ -25,7 +25,7 @@ impl Extendable for http::Request { } } -impl Extendable for http::Response { +impl Extendable for crate::net::http::Response { fn extensions(&self) -> &Extensions { self.extensions() }