diff --git a/examples/http_connect_proxy.rs b/examples/http_connect_proxy.rs index be935cbe8..83ae813f1 100644 --- a/examples/http_connect_proxy.rs +++ b/examples/http_connect_proxy.rs @@ -12,7 +12,7 @@ //! //! ```sh //! curl -v -x http://127.0.0.1:8080 --proxy-user 'john:secret' http://www.example.com/ -//! curl -v -x http://127.0.0.1:8080 --proxy-user 'john-cc-us:secret' http://www.example.com/ +//! curl -v -x http://127.0.0.1:8080 --proxy-user 'john-red-blue:secret' http://www.example.com/ //! curl -v -x http://127.0.0.1:8080 --proxy-user 'john:secret' https://www.example.com/ //! curl -v -x http://127.0.0.1:8080 --proxy-user 'john:secret' http://echo.example/foo/bar //! curl -v -x http://127.0.0.1:8080 --proxy-user 'john:secret' -XPOST http://echo.example/lucky/7 @@ -21,7 +21,7 @@ //! //! ```sh //! curl -v -x http://127.0.0.1:8080 --proxy-user 'john:secret' http://echo.example/foo/bar -//! curl -v -x http://127.0.0.1:8080 --proxy-user 'john-cc-us:secret' http://echo.example/foo/bar +//! curl -v -x http://127.0.0.1:8080 --proxy-user 'john-red-blue:secret' http://echo.example/foo/bar //! ``` //! //! You should see in all the above examples the responses from the server. @@ -57,7 +57,7 @@ use rama::{ http::{ client::HttpClient, layer::{ - proxy_auth::ProxyAuthLayer, + proxy_auth::{ProxyAuthLayer, ProxyUsernameLabels}, trace::TraceLayer, upgrade::{UpgradeLayer, Upgraded}, }, @@ -70,14 +70,13 @@ use rama::{ }, Body, IntoResponse, Request, Response, StatusCode, }, - proxy::ProxyFilter, rt::Executor, service::{layer::HijackLayer, service_fn, Context, Service, ServiceBuilder}, tcp::utils::is_connection_error, }; use serde::Deserialize; use serde_json::json; -use std::{convert::Infallible, sync::Arc, time::Duration}; +use std::{convert::Infallible, ops::Deref, sync::Arc, time::Duration}; use tracing::level_filters::LevelFilter; use tracing_subscriber::{fmt, layer::SubscriberExt, util::SubscriberInitExt, EnvFilter}; @@ -110,11 +109,9 @@ async fn main() { "127.0.0.1:8080", ServiceBuilder::new() .layer(TraceLayer::new_for_http()) - // - specify it as `with_proxy_filter_labels::<'_'>()` - // in case you want to define a different separator, such as '_'. - // - specify `.with_labels::()` in case you want to use a custom labels extractor. - // - `ProxyAuthLayer::new` can be used for a custom Credentials type. - .layer(ProxyAuthLayer::basic(("john", "secret")).with_proxy_filter_labels_default()) + // See [`ProxyAuthLayer::with_labels`] for more information, + // e.g. can also be used to extract upstream proxy filters + .layer(ProxyAuthLayer::basic(("john", "secret")).with_labels::()) // example of how one might insert an API layer into their proxy .layer(HijackLayer::new( DomainMatcher::new("echo.example"), @@ -128,7 +125,7 @@ async fn main() { Json(json!({ "method": req.method().as_str(), "path": req.uri().path(), - "filter": ctx.get::().map(|f| format!("{:?}", f)), + "username_labels": ctx.get::().map(|labels| labels.deref()), })) }, _ => StatusCode::NOT_FOUND, diff --git a/src/http/layer/proxy_auth/auth.rs b/src/http/layer/proxy_auth/auth.rs index af350a5e8..cda6a9475 100644 --- a/src/http/layer/proxy_auth/auth.rs +++ b/src/http/layer/proxy_auth/auth.rs @@ -1,12 +1,16 @@ use crate::{ + error::BoxError, http::headers::{ authorization::{Basic, Credentials}, Authorization, }, - proxy::UsernameConfig, + proxy::{username::UsernameConfigError, ProxyFilter, UsernameConfig}, service::context::Extensions, }; -use std::future::Future; +use std::{ + future::Future, + ops::{Deref, DerefMut}, +}; /// The `ProxyAuthority` trait is used to determine if a set of [`Credential`]s are authorized. /// @@ -28,6 +32,25 @@ pub trait ProxyAuthoritySync: Send + Sync + 'static { fn authorized(&self, ext: &mut Extensions, credentials: &C) -> bool; } +/// A trait to convert a username into a tuple of username and meta info attached to the username. +/// +/// This trait is to be implemented in case you want to define your own metadata extractor from the username. +/// +/// See [`UsernameConfig`] for an example of why one might want to use this. +pub trait FromUsername { + /// The output type of the username metadata extraction, + /// and which will be added to the [`Request`]'s [`Extensions`]. + /// + /// [`Request`]: crate::http::Request + type Output: Clone + Send + Sync + 'static; + + /// The error type that can be returned when parsing the username went wrong. + type Error; + + /// Parse the username and return the username and the metadata. + fn from_username(username: &str) -> Result<(String, Option), Self::Error>; +} + impl ProxyAuthority for A where A: ProxyAuthoritySync, @@ -55,7 +78,7 @@ impl ProxyAuthoritySync for Basic { } } -impl ProxyAuthoritySync> for Basic { +impl ProxyAuthoritySync for Basic { fn authorized(&self, ext: &mut Extensions, credentials: &Basic) -> bool { let username = credentials.username(); let password = credentials.password(); @@ -64,8 +87,8 @@ impl ProxyAuthoritySync> for Basic { return false; } - let (username, mut filter) = match username.parse::>() { - Ok(t) => t.into_parts(), + let (username, mut metadata) = match T::from_username(username) { + Ok(t) => t, Err(_) => { return if self == credentials { ext.insert(self.clone()); @@ -80,8 +103,8 @@ impl ProxyAuthoritySync> for Basic { return false; } - if let Some(filter) = filter.take() { - ext.insert(filter); + if let Some(metadata) = metadata.take() { + ext.insert(metadata); } ext.insert(Authorization::basic(username.as_str(), password).0); true @@ -99,7 +122,7 @@ impl ProxyAuthoritySync for (&'static str, &'static str) { } } -impl ProxyAuthoritySync> for (&'static str, &'static str) { +impl ProxyAuthoritySync for (&'static str, &'static str) { fn authorized(&self, ext: &mut Extensions, credentials: &Basic) -> bool { let username = credentials.username(); let password = credentials.password(); @@ -108,8 +131,8 @@ impl ProxyAuthoritySync> for (&'static s return false; } - let (username, mut filter) = match username.parse::>() { - Ok(t) => t.into_parts(), + let (username, mut metadata) = match T::from_username(username) { + Ok(t) => t, Err(_) => { return if self.0 == credentials.username() && self.1 == credentials.password() { ext.insert(Authorization::basic(self.0, self.1).0); @@ -124,8 +147,8 @@ impl ProxyAuthoritySync> for (&'static s return false; } - if let Some(filter) = filter.take() { - ext.insert(filter); + if let Some(metadata) = metadata.take() { + ext.insert(metadata); } ext.insert(Authorization::basic(username.as_str(), password).0); true @@ -143,7 +166,7 @@ impl ProxyAuthoritySync for (String, String) { } } -impl ProxyAuthoritySync> for (String, String) { +impl ProxyAuthoritySync for (String, String) { fn authorized(&self, ext: &mut Extensions, credentials: &Basic) -> bool { let username = credentials.username(); let password = credentials.password(); @@ -152,8 +175,8 @@ impl ProxyAuthoritySync> for (String, St return false; } - let (username, mut filter) = match username.parse::>() { - Ok(t) => t.into_parts(), + let (username, mut metadata) = match T::from_username(username) { + Ok(t) => t, Err(_) => { return if self.0 == credentials.username() && self.1 == credentials.password() { ext.insert(Authorization::basic(self.0.as_str(), self.1.as_str()).0); @@ -168,37 +191,24 @@ impl ProxyAuthoritySync> for (String, St return false; } - if let Some(filter) = filter.take() { - ext.insert(filter); + if let Some(metadata) = metadata.take() { + ext.insert(metadata); } ext.insert(Authorization::basic(username.as_str(), password).0); true } } -macro_rules! impl_proxy_auth_sync_tuple { - ($($T:ident),+ $(,)?) => { - #[allow(unused_parens)] - #[allow(non_snake_case)] - impl ProxyAuthoritySync for ($($T),+,) - where C: Credentials + Send + 'static, - $( - $T: ProxyAuthoritySync, - )+ - - { - fn authorized(&self, ext: &mut Extensions, credentials: &C) -> bool { - let ($($T),+,) = self; - $( - ProxyAuthoritySync::authorized($T, ext, &credentials) - )||+ - } - } - }; +impl ProxyAuthoritySync for [T; N] +where + C: Credentials + Send + 'static, + T: ProxyAuthoritySync, +{ + fn authorized(&self, ext: &mut Extensions, credentials: &C) -> bool { + self.iter().any(|t| t.authorized(ext, credentials)) + } } -all_the_tuples_no_last_special_case!(impl_proxy_auth_sync_tuple); - impl ProxyAuthoritySync for Vec where C: Credentials + Send + 'static, @@ -209,11 +219,58 @@ where } } +/// A wrapper type to extract username labels and store them as-is in the [`Extensions`]. +#[derive(Debug, Clone)] +pub struct ProxyUsernameLabels(pub Vec); + +impl Deref for ProxyUsernameLabels { + type Target = Vec; + + fn deref(&self) -> &Self::Target { + &self.0 + } +} + +impl DerefMut for ProxyUsernameLabels { + fn deref_mut(&mut self) -> &mut Self::Target { + &mut self.0 + } +} + +impl FromUsername for ProxyUsernameLabels { + type Output = Self; + type Error = BoxError; + + fn from_username(username: &str) -> Result<(String, Option), Self::Error> { + let mut it = username.split(C); + let username = match it.next() { + Some(username) => username.to_owned(), + None => return Err("no username found".into()), + }; + let labels: Vec<_> = it.map(str::to_owned).collect(); + if labels.is_empty() { + Ok((username, None)) + } else { + Ok((username, Some(Self(labels)))) + } + } +} + +impl FromUsername for UsernameConfig { + type Output = ProxyFilter; + type Error = UsernameConfigError; + + fn from_username(username: &str) -> Result<(String, Option), Self::Error> { + let username_cfg: Self = username.parse()?; + let (username, filter) = username_cfg.into_parts(); + Ok((username, filter)) + } +} + #[cfg(test)] mod test { + use super::*; use crate::proxy::{ProxyFilter, UsernameConfig}; - - use super::ProxyAuthority; use headers::{authorization::Basic, Authorization}; #[tokio::test] @@ -248,6 +305,27 @@ mod test { assert_eq!(filter.country, Some("us".to_owned())); } + #[tokio::test] + async fn basic_authorization_with_labels_found() { + let Authorization(auth) = Authorization::basic("john", "secret"); + let auths = vec![ + Authorization::basic("foo", "bar").0, + Authorization::basic("john", "secret").0, + ]; + + let ext = ProxyAuthority::<_, ProxyUsernameLabels>::authorized( + &auths, + Authorization::basic("john-green-red", "secret").0, + ) + .await + .unwrap(); + let c: &Basic = ext.get().unwrap(); + assert_eq!(&auth, c); + + let labels: &ProxyUsernameLabels = ext.get().unwrap(); + assert_eq!(labels.deref(), &vec!["green".to_owned(), "red".to_owned()]); + } + #[tokio::test] async fn basic_authorization_with_filter_not_found() { let Authorization(auth) = Authorization::basic("john", "secret"); @@ -265,6 +343,23 @@ mod test { assert!(ext.get::().is_none()); } + #[tokio::test] + async fn basic_authorization_with_labels_not_found() { + let Authorization(auth) = Authorization::basic("john", "secret"); + let auths = vec![ + Authorization::basic("foo", "bar").0, + Authorization::basic("john", "secret").0, + ]; + + let ext = ProxyAuthority::<_, ProxyUsernameLabels>::authorized(&auths, auth.clone()) + .await + .unwrap(); + let c: &Basic = ext.get().unwrap(); + assert_eq!(&auth, c); + + assert!(ext.get::().is_none()); + } + #[tokio::test] async fn basic_authorization_tuple() { let auths = vec![("foo", "bar"), ("Aladdin", "open sesame"), ("baz", "qux")]; diff --git a/src/http/layer/proxy_auth/mod.rs b/src/http/layer/proxy_auth/mod.rs index 333c117a4..2d7f24adb 100644 --- a/src/http/layer/proxy_auth/mod.rs +++ b/src/http/layer/proxy_auth/mod.rs @@ -2,20 +2,17 @@ //! //! If the request is not authorized a `407 Proxy Authentication Required` response will be sent. +use crate::http::headers::{ + authorization::{Basic, Credentials}, + HeaderMapExt, ProxyAuthorization, +}; use crate::http::{Request, Response, StatusCode}; use crate::service::{Context, Layer, Service}; -use crate::{ - http::headers::{ - authorization::{Basic, Credentials}, - HeaderMapExt, ProxyAuthorization, - }, - proxy::UsernameConfig, -}; use std::marker::PhantomData; mod auth; #[doc(inline)] -pub use auth::{ProxyAuthority, ProxyAuthoritySync}; +pub use auth::{FromUsername, ProxyAuthority, ProxyAuthoritySync, ProxyUsernameLabels}; /// Layer that applies the [`ProxyAuthService`] middleware which apply a timeout to requests. /// @@ -40,36 +37,12 @@ impl ProxyAuthLayer { /// Overwrite the Labels extract type /// /// This is used if the username contains labels that you need to extract out. - /// E.g. by using the [`UsernameConfig`]. + /// Example implementations are [`ProxyUsernameLabels`] and [`UsernameConfig`]. /// - /// [`UsernameConfig`]: crate::proxy::UsernameConfig - pub fn with_labels(self) -> ProxyAuthLayer { - ProxyAuthLayer { - proxy_auth: self.proxy_auth, - _phantom: PhantomData, - } - } - - /// Overwrite the Labels extract type with a [`UsernameConfig`] extractor, - /// to optionally extract [`ProxyFilter`] from the username. + /// You can provide your own extractor by implementing the [`FromUsername`] trait. /// /// [`UsernameConfig`]: crate::proxy::UsernameConfig - /// [`ProxyFilter`]: crate::proxy::ProxyFilter - pub fn with_proxy_filter_labels( - self, - ) -> ProxyAuthLayer> { - ProxyAuthLayer { - proxy_auth: self.proxy_auth, - _phantom: PhantomData, - } - } - - /// Overwrite the Labels extract type with a [`UsernameConfig`] extractor, - /// to optionally extract [`ProxyFilter`] from the username. - /// - /// [`UsernameConfig`]: crate::proxy::UsernameConfig - /// [`ProxyFilter`]: crate::proxy::ProxyFilter - pub fn with_proxy_filter_labels_default(self) -> ProxyAuthLayer { + pub fn with_labels(self) -> ProxyAuthLayer { ProxyAuthLayer { proxy_auth: self.proxy_auth, _phantom: PhantomData,