1
0
mirror of https://github.com/actix/actix-extras.git synced 2025-06-26 18:37:41 +02:00

move files into module

This commit is contained in:
Rob Ede
2020-01-29 11:33:00 +00:00
parent c4b3ef64c2
commit b4267818de
37 changed files with 0 additions and 0 deletions

View File

@ -0,0 +1,152 @@
//! Extractor for the "Basic" HTTP Authentication Scheme
use std::borrow::Cow;
use actix_web::dev::{Payload, ServiceRequest};
use actix_web::http::header::Header;
use actix_web::{FromRequest, HttpRequest};
use futures::future;
use super::config::AuthExtractorConfig;
use super::errors::AuthenticationError;
use super::AuthExtractor;
use crate::headers::authorization::{Authorization, Basic};
use crate::headers::www_authenticate::basic::Basic as Challenge;
/// [`BasicAuth`] extractor configuration,
/// used for [`WWW-Authenticate`] header later.
///
/// [`BasicAuth`]: ./struct.BasicAuth.html
/// [`WWW-Authenticate`]:
/// ../../headers/www_authenticate/struct.WwwAuthenticate.html
#[derive(Debug, Clone, Default)]
pub struct Config(Challenge);
impl Config {
/// Set challenge `realm` attribute.
///
/// The "realm" attribute indicates the scope of protection in the manner
/// described in HTTP/1.1 [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2).
pub fn realm<T>(mut self, value: T) -> Config
where
T: Into<Cow<'static, str>>,
{
self.0.realm = Some(value.into());
self
}
}
impl AsRef<Challenge> for Config {
fn as_ref(&self) -> &Challenge {
&self.0
}
}
impl AuthExtractorConfig for Config {
type Inner = Challenge;
fn into_inner(self) -> Self::Inner {
self.0
}
}
// Needs `fn main` to display complete example.
#[allow(clippy::needless_doctest_main)]
/// Extractor for HTTP Basic auth.
///
/// # Example
///
/// ```
/// use actix_web::Result;
/// use actix_web_httpauth::extractors::basic::BasicAuth;
///
/// async fn index(auth: BasicAuth) -> String {
/// format!("Hello, {}!", auth.user_id())
/// }
/// ```
///
/// If authentication fails, this extractor fetches the [`Config`] instance
/// from the [app data] in order to properly form the `WWW-Authenticate`
/// response header.
///
/// ## Example
///
/// ```
/// use actix_web::{web, App};
/// use actix_web_httpauth::extractors::basic::{BasicAuth, Config};
///
/// async fn index(auth: BasicAuth) -> String {
/// format!("Hello, {}!", auth.user_id())
/// }
///
/// fn main() {
/// let app = App::new()
/// .data(Config::default().realm("Restricted area"))
/// .service(web::resource("/index.html").route(web::get().to(index)));
/// }
/// ```
///
/// [`Config`]: ./struct.Config.html
/// [app data]: https://docs.rs/actix-web/1.0.0-beta.5/actix_web/struct.App.html#method.data
#[derive(Debug, Clone)]
pub struct BasicAuth(Basic);
impl BasicAuth {
/// Returns client's user-ID.
pub fn user_id(&self) -> &Cow<'static, str> {
&self.0.user_id()
}
/// Returns client's password.
pub fn password(&self) -> Option<&Cow<'static, str>> {
self.0.password()
}
}
impl FromRequest for BasicAuth {
type Future = future::Ready<Result<Self, Self::Error>>;
type Config = Config;
type Error = AuthenticationError<Challenge>;
fn from_request(
req: &HttpRequest,
_: &mut Payload,
) -> <Self as FromRequest>::Future {
future::ready(
Authorization::<Basic>::parse(req)
.map(|auth| BasicAuth(auth.into_scheme()))
.map_err(|_| {
// TODO: debug! the original error
let challenge = req
.app_data::<Self::Config>()
.map(|config| config.0.clone())
// TODO: Add trace! about `Default::default` call
.unwrap_or_else(Default::default);
AuthenticationError::new(challenge)
}),
)
}
}
impl AuthExtractor for BasicAuth {
type Error = AuthenticationError<Challenge>;
type Future = future::Ready<Result<Self, Self::Error>>;
fn from_service_request(req: &ServiceRequest) -> Self::Future {
future::ready(
Authorization::<Basic>::parse(req)
.map(|auth| BasicAuth(auth.into_scheme()))
.map_err(|_| {
// TODO: debug! the original error
let challenge = req
.app_data::<Config>()
.map(|config| config.0.clone())
// TODO: Add trace! about `Default::default` call
.unwrap_or_else(Default::default);
AuthenticationError::new(challenge)
}),
)
}
}

View File

@ -0,0 +1,180 @@
//! Extractor for the "Bearer" HTTP Authentication Scheme
use std::borrow::Cow;
use std::default::Default;
use actix_web::dev::{Payload, ServiceRequest};
use actix_web::http::header::Header;
use actix_web::{FromRequest, HttpRequest};
use futures::future;
use super::config::AuthExtractorConfig;
use super::errors::AuthenticationError;
use super::AuthExtractor;
use crate::headers::authorization;
use crate::headers::www_authenticate::bearer;
pub use crate::headers::www_authenticate::bearer::Error;
/// [BearerAuth](./struct/BearerAuth.html) extractor configuration.
#[derive(Debug, Clone, Default)]
pub struct Config(bearer::Bearer);
impl Config {
/// Set challenge `scope` attribute.
///
/// The `"scope"` attribute is a space-delimited list of case-sensitive
/// scope values indicating the required scope of the access token for
/// accessing the requested resource.
pub fn scope<T: Into<Cow<'static, str>>>(mut self, value: T) -> Config {
self.0.scope = Some(value.into());
self
}
/// Set challenge `realm` attribute.
///
/// The "realm" attribute indicates the scope of protection in the manner
/// described in HTTP/1.1 [RFC2617](https://tools.ietf.org/html/rfc2617#section-1.2).
pub fn realm<T: Into<Cow<'static, str>>>(mut self, value: T) -> Config {
self.0.realm = Some(value.into());
self
}
}
impl AsRef<bearer::Bearer> for Config {
fn as_ref(&self) -> &bearer::Bearer {
&self.0
}
}
impl AuthExtractorConfig for Config {
type Inner = bearer::Bearer;
fn into_inner(self) -> Self::Inner {
self.0
}
}
// Needs `fn main` to display complete example.
#[allow(clippy::needless_doctest_main)]
/// Extractor for HTTP Bearer auth
///
/// # Example
///
/// ```
/// use actix_web_httpauth::extractors::bearer::BearerAuth;
///
/// async fn index(auth: BearerAuth) -> String {
/// format!("Hello, user with token {}!", auth.token())
/// }
/// ```
///
/// If authentication fails, this extractor fetches the [`Config`] instance
/// from the [app data] in order to properly form the `WWW-Authenticate`
/// response header.
///
/// ## Example
///
/// ```
/// use actix_web::{web, App};
/// use actix_web_httpauth::extractors::bearer::{BearerAuth, Config};
///
/// async fn index(auth: BearerAuth) -> String {
/// format!("Hello, {}!", auth.token())
/// }
///
/// fn main() {
/// let app = App::new()
/// .data(
/// Config::default()
/// .realm("Restricted area")
/// .scope("email photo"),
/// )
/// .service(web::resource("/index.html").route(web::get().to(index)));
/// }
/// ```
#[derive(Debug, Clone)]
pub struct BearerAuth(authorization::Bearer);
impl BearerAuth {
/// Returns bearer token provided by client.
pub fn token(&self) -> &str {
self.0.token()
}
}
impl FromRequest for BearerAuth {
type Config = Config;
type Future = future::Ready<Result<Self, Self::Error>>;
type Error = AuthenticationError<bearer::Bearer>;
fn from_request(
req: &HttpRequest,
_payload: &mut Payload,
) -> <Self as FromRequest>::Future {
future::ready(
authorization::Authorization::<authorization::Bearer>::parse(req)
.map(|auth| BearerAuth(auth.into_scheme()))
.map_err(|_| {
let bearer = req
.app_data::<Self::Config>()
.map(|config| config.0.clone())
.unwrap_or_else(Default::default);
AuthenticationError::new(bearer)
}),
)
}
}
impl AuthExtractor for BearerAuth {
type Future = future::Ready<Result<Self, Self::Error>>;
type Error = AuthenticationError<bearer::Bearer>;
fn from_service_request(req: &ServiceRequest) -> Self::Future {
future::ready(
authorization::Authorization::<authorization::Bearer>::parse(req)
.map(|auth| BearerAuth(auth.into_scheme()))
.map_err(|_| {
let bearer = req
.app_data::<Config>()
.map(|config| config.0.clone())
.unwrap_or_else(Default::default);
AuthenticationError::new(bearer)
}),
)
}
}
/// Extended error customization for HTTP `Bearer` auth.
impl AuthenticationError<bearer::Bearer> {
/// Attach `Error` to the current Authentication error.
///
/// Error status code will be changed to the one provided by the `kind`
/// Error.
pub fn with_error(mut self, kind: Error) -> Self {
*self.status_code_mut() = kind.status_code();
self.challenge_mut().error = Some(kind);
self
}
/// Attach error description to the current Authentication error.
pub fn with_error_description<T>(mut self, desc: T) -> Self
where
T: Into<Cow<'static, str>>,
{
self.challenge_mut().error_description = Some(desc.into());
self
}
/// Attach error URI to the current Authentication error.
///
/// It is up to implementor to provide properly formed absolute URI.
pub fn with_error_uri<T>(mut self, uri: T) -> Self
where
T: Into<Cow<'static, str>>,
{
self.challenge_mut().error_uri = Some(uri.into());
self
}
}

View File

@ -0,0 +1,23 @@
use super::AuthenticationError;
use crate::headers::www_authenticate::Challenge;
/// Trait implemented for types that provides configuration
/// for the authentication [extractors].
///
/// [extractors]: ./trait.AuthExtractor.html
pub trait AuthExtractorConfig {
/// Associated challenge type.
type Inner: Challenge;
/// Convert the config instance into a HTTP challenge.
fn into_inner(self) -> Self::Inner;
}
impl<T> From<T> for AuthenticationError<<T as AuthExtractorConfig>::Inner>
where
T: AuthExtractorConfig,
{
fn from(config: T) -> Self {
AuthenticationError::new(config.into_inner())
}
}

View File

@ -0,0 +1,60 @@
use std::error::Error;
use std::fmt;
use actix_web::http::StatusCode;
use actix_web::{HttpResponse, ResponseError};
use crate::headers::www_authenticate::Challenge;
use crate::headers::www_authenticate::WwwAuthenticate;
/// Authentication error returned by authentication extractors.
///
/// Different extractors may extend `AuthenticationError` implementation
/// in order to provide access to inner challenge fields.
#[derive(Debug)]
pub struct AuthenticationError<C: Challenge> {
challenge: C,
status_code: StatusCode,
}
impl<C: Challenge> AuthenticationError<C> {
/// Creates new authentication error from the provided `challenge`.
///
/// By default returned error will resolve into the `HTTP 401` status code.
pub fn new(challenge: C) -> AuthenticationError<C> {
AuthenticationError {
challenge,
status_code: StatusCode::UNAUTHORIZED,
}
}
/// Returns mutable reference to the inner challenge instance.
pub fn challenge_mut(&mut self) -> &mut C {
&mut self.challenge
}
/// Returns mutable reference to the inner status code.
///
/// Can be used to override returned status code, but by default
/// this lib tries to stick to the RFC, so it might be unreasonable.
pub fn status_code_mut(&mut self) -> &mut StatusCode {
&mut self.status_code
}
}
impl<C: Challenge> fmt::Display for AuthenticationError<C> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.status_code, f)
}
}
impl<C: 'static + Challenge> Error for AuthenticationError<C> {}
impl<C: 'static + Challenge> ResponseError for AuthenticationError<C> {
fn error_response(&self) -> HttpResponse {
HttpResponse::build(self.status_code)
// TODO: Get rid of the `.clone()`
.set(WwwAuthenticate(self.challenge.clone()))
.finish()
}
}

View File

@ -0,0 +1,33 @@
//! Type-safe authentication information extractors
use actix_web::dev::ServiceRequest;
use actix_web::Error;
use futures::future::Future;
pub mod basic;
pub mod bearer;
mod config;
mod errors;
pub use self::config::AuthExtractorConfig;
pub use self::errors::AuthenticationError;
/// Trait implemented by types that can extract
/// HTTP authentication scheme credentials from the request.
///
/// It is very similar to actix' `FromRequest` trait,
/// except it operates with a `ServiceRequest` struct instead,
/// therefore it can be used in the middlewares.
///
/// You will not need it unless you want to implement your own
/// authentication scheme.
pub trait AuthExtractor: Sized {
/// The associated error which can be returned.
type Error: Into<Error>;
/// Future that resolves into extracted credentials type.
type Future: Future<Output = Result<Self, Self::Error>>;
/// Parse the authentication credentials from the actix' `ServiceRequest`.
fn from_service_request(req: &ServiceRequest) -> Self::Future;
}

View File

@ -0,0 +1,71 @@
use std::convert::From;
use std::error::Error;
use std::fmt;
use std::str;
use actix_web::http::header;
/// Possible errors while parsing `Authorization` header.
///
/// Should not be used directly unless you are implementing
/// your own [authentication scheme](./trait.Scheme.html).
#[derive(Debug)]
pub enum ParseError {
/// Header value is malformed
Invalid,
/// Authentication scheme is missing
MissingScheme,
/// Required authentication field is missing
MissingField(&'static str),
/// Unable to convert header into the str
ToStrError(header::ToStrError),
/// Malformed base64 string
Base64DecodeError(base64::DecodeError),
/// Malformed UTF-8 string
Utf8Error(str::Utf8Error),
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let display = match self {
ParseError::Invalid => "Invalid header value".to_string(),
ParseError::MissingScheme => {
"Missing authorization scheme".to_string()
}
ParseError::MissingField(_) => "Missing header field".to_string(),
ParseError::ToStrError(e) => e.to_string(),
ParseError::Base64DecodeError(e) => e.to_string(),
ParseError::Utf8Error(e) => e.to_string(),
};
f.write_str(&display)
}
}
impl Error for ParseError {
fn source(&self) -> Option<&(dyn Error + 'static)> {
match self {
ParseError::Invalid => None,
ParseError::MissingScheme => None,
ParseError::MissingField(_) => None,
ParseError::ToStrError(e) => Some(e),
ParseError::Base64DecodeError(e) => Some(e),
ParseError::Utf8Error(e) => Some(e),
}
}
}
impl From<header::ToStrError> for ParseError {
fn from(e: header::ToStrError) -> Self {
ParseError::ToStrError(e)
}
}
impl From<base64::DecodeError> for ParseError {
fn from(e: base64::DecodeError) -> Self {
ParseError::Base64DecodeError(e)
}
}
impl From<str::Utf8Error> for ParseError {
fn from(e: str::Utf8Error) -> Self {
ParseError::Utf8Error(e)
}
}

View File

@ -0,0 +1,104 @@
use std::fmt;
use actix_web::error::ParseError;
use actix_web::http::header::{
Header, HeaderName, HeaderValue, IntoHeaderValue, AUTHORIZATION,
};
use actix_web::HttpMessage;
use crate::headers::authorization::scheme::Scheme;
/// `Authorization` header, defined in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.2)
///
/// The "Authorization" header field allows a user agent to authenticate
/// itself with an origin server -- usually, but not necessarily, after
/// receiving a 401 (Unauthorized) response. Its value consists of
/// credentials containing the authentication information of the user
/// agent for the realm of the resource being requested.
///
/// `Authorization` header is generic over [authentication
/// scheme](./trait.Scheme.html).
///
/// # Example
///
/// ```
/// # use actix_web::http::header::Header;
/// # use actix_web::{HttpRequest, Result};
/// # use actix_web_httpauth::headers::authorization::{Authorization, Basic};
/// fn handler(req: HttpRequest) -> Result<String> {
/// let auth = Authorization::<Basic>::parse(&req)?;
///
/// Ok(format!("Hello, {}!", auth.as_ref().user_id()))
/// }
/// ```
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
pub struct Authorization<S: Scheme>(S);
impl<S> Authorization<S>
where
S: Scheme,
{
/// Consumes `Authorization` header and returns inner [`Scheme`]
/// implementation.
///
/// [`Scheme`]: ./trait.Scheme.html
pub fn into_scheme(self) -> S {
self.0
}
}
impl<S> From<S> for Authorization<S>
where
S: Scheme,
{
fn from(scheme: S) -> Authorization<S> {
Authorization(scheme)
}
}
impl<S> AsRef<S> for Authorization<S>
where
S: Scheme,
{
fn as_ref(&self) -> &S {
&self.0
}
}
impl<S> AsMut<S> for Authorization<S>
where
S: Scheme,
{
fn as_mut(&mut self) -> &mut S {
&mut self.0
}
}
impl<S: Scheme> Header for Authorization<S> {
#[inline]
fn name() -> HeaderName {
AUTHORIZATION
}
fn parse<T: HttpMessage>(msg: &T) -> Result<Self, ParseError> {
let header =
msg.headers().get(AUTHORIZATION).ok_or(ParseError::Header)?;
let scheme = S::parse(header).map_err(|_| ParseError::Header)?;
Ok(Authorization(scheme))
}
}
impl<S: Scheme> IntoHeaderValue for Authorization<S> {
type Error = <S as IntoHeaderValue>::Error;
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
self.0.try_into()
}
}
impl<S: Scheme> fmt::Display for Authorization<S> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
fmt::Display::fmt(&self.0, f)
}
}

View File

@ -0,0 +1,11 @@
//! `Authorization` header and various auth schemes
mod errors;
mod header;
mod scheme;
pub use self::errors::ParseError;
pub use self::header::Authorization;
pub use self::scheme::basic::Basic;
pub use self::scheme::bearer::Bearer;
pub use self::scheme::Scheme;

View File

@ -0,0 +1,204 @@
use std::borrow::Cow;
use std::fmt;
use std::str;
use actix_web::http::header::{
HeaderValue, IntoHeaderValue, InvalidHeaderValue,
};
use base64;
use bytes::{BufMut, BytesMut};
use crate::headers::authorization::errors::ParseError;
use crate::headers::authorization::Scheme;
/// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617)
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct Basic {
user_id: Cow<'static, str>,
password: Option<Cow<'static, str>>,
}
impl Basic {
/// Creates `Basic` credentials with provided `user_id` and optional
/// `password`.
///
/// ## Example
///
/// ```
/// # use actix_web_httpauth::headers::authorization::Basic;
/// let credentials = Basic::new("Alladin", Some("open sesame"));
/// ```
pub fn new<U, P>(user_id: U, password: Option<P>) -> Basic
where
U: Into<Cow<'static, str>>,
P: Into<Cow<'static, str>>,
{
Basic {
user_id: user_id.into(),
password: password.map(Into::into),
}
}
/// Returns client's user-ID.
pub fn user_id(&self) -> &Cow<'static, str> {
&self.user_id
}
/// Returns client's password if provided.
pub fn password(&self) -> Option<&Cow<'static, str>> {
self.password.as_ref()
}
}
impl Scheme for Basic {
fn parse(header: &HeaderValue) -> Result<Self, ParseError> {
// "Basic *" length
if header.len() < 7 {
return Err(ParseError::Invalid);
}
let mut parts = header.to_str()?.splitn(2, ' ');
match parts.next() {
Some(scheme) if scheme == "Basic" => (),
_ => return Err(ParseError::MissingScheme),
}
let decoded = base64::decode(parts.next().ok_or(ParseError::Invalid)?)?;
let mut credentials = str::from_utf8(&decoded)?.splitn(2, ':');
let user_id = credentials
.next()
.ok_or(ParseError::MissingField("user_id"))
.map(|user_id| user_id.to_string().into())?;
let password = credentials
.next()
.ok_or(ParseError::MissingField("password"))
.map(|password| {
if password.is_empty() {
None
} else {
Some(password.to_string().into())
}
})?;
Ok(Basic {
user_id,
password,
})
}
}
impl fmt::Debug for Basic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!("Basic {}:******", self.user_id))
}
}
impl fmt::Display for Basic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!("Basic {}:******", self.user_id))
}
}
impl IntoHeaderValue for Basic {
type Error = InvalidHeaderValue;
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
let mut credentials = BytesMut::with_capacity(
self.user_id.len()
+ 1 // ':'
+ self.password.as_ref().map_or(0, |pwd| pwd.len()),
);
credentials.extend_from_slice(self.user_id.as_bytes());
credentials.put_u8(b':');
if let Some(ref password) = self.password {
credentials.extend_from_slice(password.as_bytes());
}
// TODO: It would be nice not to allocate new `String` here but write
// directly to `value`
let encoded = base64::encode(&credentials);
let mut value = BytesMut::with_capacity(6 + encoded.len());
value.put(&b"Basic "[..]);
value.put(&encoded.as_bytes()[..]);
HeaderValue::from_maybe_shared(value.freeze())
}
}
#[cfg(test)]
mod tests {
use super::{Basic, Scheme};
use actix_web::http::header::{HeaderValue, IntoHeaderValue};
#[test]
fn test_parse_header() {
let value =
HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==");
let scheme = Basic::parse(&value);
assert!(scheme.is_ok());
let scheme = scheme.unwrap();
assert_eq!(scheme.user_id, "Aladdin");
assert_eq!(scheme.password, Some("open sesame".into()));
}
#[test]
fn test_empty_password() {
let value = HeaderValue::from_static("Basic QWxhZGRpbjo=");
let scheme = Basic::parse(&value);
assert!(scheme.is_ok());
let scheme = scheme.unwrap();
assert_eq!(scheme.user_id, "Aladdin");
assert_eq!(scheme.password, None);
}
#[test]
fn test_empty_header() {
let value = HeaderValue::from_static("");
let scheme = Basic::parse(&value);
assert!(scheme.is_err());
}
#[test]
fn test_wrong_scheme() {
let value = HeaderValue::from_static("THOUSHALLNOTPASS please?");
let scheme = Basic::parse(&value);
assert!(scheme.is_err());
}
#[test]
fn test_missing_credentials() {
let value = HeaderValue::from_static("Basic ");
let scheme = Basic::parse(&value);
assert!(scheme.is_err());
}
#[test]
fn test_missing_credentials_colon() {
let value = HeaderValue::from_static("Basic QWxsYWRpbg==");
let scheme = Basic::parse(&value);
assert!(scheme.is_err());
}
#[test]
fn test_into_header_value() {
let basic = Basic {
user_id: "Aladdin".into(),
password: Some("open sesame".into()),
};
let result = basic.try_into();
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
HeaderValue::from_static("Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==")
);
}
}

View File

@ -0,0 +1,140 @@
use std::borrow::Cow;
use std::fmt;
use actix_web::http::header::{
HeaderValue, IntoHeaderValue, InvalidHeaderValue,
};
use bytes::{BufMut, BytesMut};
use crate::headers::authorization::errors::ParseError;
use crate::headers::authorization::scheme::Scheme;
/// Credentials for `Bearer` authentication scheme, defined in [RFC6750](https://tools.ietf.org/html/rfc6750)
///
/// Should be used in combination with
/// [`Authorization`](./struct.Authorization.html) header.
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
pub struct Bearer {
token: Cow<'static, str>,
}
impl Bearer {
/// Creates new `Bearer` credentials with the token provided.
///
/// ## Example
///
/// ```
/// # use actix_web_httpauth::headers::authorization::Bearer;
/// let credentials = Bearer::new("mF_9.B5f-4.1JqM");
/// ```
pub fn new<T>(token: T) -> Bearer
where
T: Into<Cow<'static, str>>,
{
Bearer {
token: token.into(),
}
}
/// Gets reference to the credentials token.
pub fn token(&self) -> &Cow<'static, str> {
&self.token
}
}
impl Scheme for Bearer {
fn parse(header: &HeaderValue) -> Result<Self, ParseError> {
// "Bearer *" length
if header.len() < 8 {
return Err(ParseError::Invalid);
}
let mut parts = header.to_str()?.splitn(2, ' ');
match parts.next() {
Some(scheme) if scheme == "Bearer" => (),
_ => return Err(ParseError::MissingScheme),
}
let token = parts.next().ok_or(ParseError::Invalid)?;
Ok(Bearer {
token: token.to_string().into(),
})
}
}
impl fmt::Debug for Bearer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!("Bearer ******"))
}
}
impl fmt::Display for Bearer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_fmt(format_args!("Bearer {}", self.token))
}
}
impl IntoHeaderValue for Bearer {
type Error = InvalidHeaderValue;
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
let mut buffer = BytesMut::with_capacity(7 + self.token.len());
buffer.put(&b"Bearer "[..]);
buffer.extend_from_slice(self.token.as_bytes());
HeaderValue::from_maybe_shared(buffer.freeze())
}
}
#[cfg(test)]
mod tests {
use super::{Bearer, Scheme};
use actix_web::http::header::{HeaderValue, IntoHeaderValue};
#[test]
fn test_parse_header() {
let value = HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM");
let scheme = Bearer::parse(&value);
assert!(scheme.is_ok());
let scheme = scheme.unwrap();
assert_eq!(scheme.token, "mF_9.B5f-4.1JqM");
}
#[test]
fn test_empty_header() {
let value = HeaderValue::from_static("");
let scheme = Bearer::parse(&value);
assert!(scheme.is_err());
}
#[test]
fn test_wrong_scheme() {
let value = HeaderValue::from_static("OAuthToken foo");
let scheme = Bearer::parse(&value);
assert!(scheme.is_err());
}
#[test]
fn test_missing_token() {
let value = HeaderValue::from_static("Bearer ");
let scheme = Bearer::parse(&value);
assert!(scheme.is_err());
}
#[test]
fn test_into_header_value() {
let bearer = Bearer::new("mF_9.B5f-4.1JqM");
let result = bearer.try_into();
assert!(result.is_ok());
assert_eq!(
result.unwrap(),
HeaderValue::from_static("Bearer mF_9.B5f-4.1JqM")
);
}
}

View File

@ -0,0 +1,17 @@
use std::fmt::{Debug, Display};
use actix_web::http::header::{HeaderValue, IntoHeaderValue};
pub mod basic;
pub mod bearer;
use crate::headers::authorization::errors::ParseError;
/// Authentication scheme for [`Authorization`](./struct.Authorization.html)
/// header.
pub trait Scheme:
IntoHeaderValue + Debug + Display + Clone + Send + Sync
{
/// Try to parse the authentication scheme from the `Authorization` header.
fn parse(header: &HeaderValue) -> Result<Self, ParseError>;
}

View File

@ -0,0 +1,4 @@
//! Typed HTTP headers
pub mod authorization;
pub mod www_authenticate;

View File

@ -0,0 +1,144 @@
//! Challenge for the "Basic" HTTP Authentication Scheme
use std::borrow::Cow;
use std::default::Default;
use std::fmt;
use std::str;
use actix_web::http::header::{
HeaderValue, IntoHeaderValue, InvalidHeaderValue,
};
use bytes::{BufMut, Bytes, BytesMut};
use super::Challenge;
use crate::utils;
/// Challenge for [`WWW-Authenticate`] header with HTTP Basic auth scheme,
/// described in [RFC 7617](https://tools.ietf.org/html/rfc7617)
///
/// ## Example
///
/// ```
/// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
/// use actix_web_httpauth::headers::www_authenticate::basic::Basic;
/// use actix_web_httpauth::headers::www_authenticate::WwwAuthenticate;
///
/// fn index(_req: HttpRequest) -> HttpResponse {
/// let challenge = Basic::with_realm("Restricted area");
///
/// HttpResponse::Unauthorized()
/// .set(WwwAuthenticate(challenge))
/// .finish()
/// }
/// ```
///
/// [`WWW-Authenticate`]: ../struct.WwwAuthenticate.html
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
pub struct Basic {
// "realm" parameter is optional now: https://tools.ietf.org/html/rfc7235#appendix-A
pub(crate) realm: Option<Cow<'static, str>>,
}
impl Basic {
/// Creates new `Basic` challenge with an empty `realm` field.
///
/// ## Example
///
/// ```
/// # use actix_web_httpauth::headers::www_authenticate::basic::Basic;
/// let challenge = Basic::new();
/// ```
pub fn new() -> Basic {
Default::default()
}
/// Creates new `Basic` challenge from the provided `realm` field value.
///
/// ## Examples
///
/// ```
/// # use actix_web_httpauth::headers::www_authenticate::basic::Basic;
/// let challenge = Basic::with_realm("Restricted area");
/// ```
///
/// ```
/// # use actix_web_httpauth::headers::www_authenticate::basic::Basic;
/// let my_realm = "Earth realm".to_string();
/// let challenge = Basic::with_realm(my_realm);
/// ```
pub fn with_realm<T>(value: T) -> Basic
where
T: Into<Cow<'static, str>>,
{
Basic {
realm: Some(value.into()),
}
}
}
#[doc(hidden)]
impl Challenge for Basic {
fn to_bytes(&self) -> Bytes {
// 5 is for `"Basic"`, 9 is for `"realm=\"\""`
let length = 5 + self.realm.as_ref().map_or(0, |realm| realm.len() + 9);
let mut buffer = BytesMut::with_capacity(length);
buffer.put(&b"Basic"[..]);
if let Some(ref realm) = self.realm {
buffer.put(&b" realm=\""[..]);
utils::put_quoted(&mut buffer, realm);
buffer.put_u8(b'"');
}
buffer.freeze()
}
}
impl fmt::Display for Basic {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let bytes = self.to_bytes();
let repr = str::from_utf8(&bytes)
// Should not happen since challenges are crafted manually
// from `&'static str`'s and Strings
.map_err(|_| fmt::Error)?;
f.write_str(repr)
}
}
impl IntoHeaderValue for Basic {
type Error = InvalidHeaderValue;
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
HeaderValue::from_maybe_shared(self.to_bytes())
}
}
#[cfg(test)]
mod tests {
use super::Basic;
use actix_web::http::header::IntoHeaderValue;
#[test]
fn test_plain_into_header_value() {
let challenge = Basic {
realm: None,
};
let value = challenge.try_into();
assert!(value.is_ok());
let value = value.unwrap();
assert_eq!(value, "Basic");
}
#[test]
fn test_with_realm_into_header_value() {
let challenge = Basic {
realm: Some("Restricted area".into()),
};
let value = challenge.try_into();
assert!(value.is_ok());
let value = value.unwrap();
assert_eq!(value, "Basic realm=\"Restricted area\"");
}
}

View File

@ -0,0 +1,63 @@
use std::borrow::Cow;
use super::{Bearer, Error};
/// Builder for the [`Bearer`] challenge.
///
/// It is up to implementor to fill all required fields,
/// neither this `Builder` or [`Bearer`] does not provide any validation.
///
/// [`Bearer`]: struct.Bearer.html
#[derive(Debug, Default)]
pub struct BearerBuilder(Bearer);
impl BearerBuilder {
/// Provides the `scope` attribute, as defined in [RFC6749, Section 3.3](https://tools.ietf.org/html/rfc6749#section-3.3)
pub fn scope<T>(mut self, value: T) -> Self
where
T: Into<Cow<'static, str>>,
{
self.0.scope = Some(value.into());
self
}
/// Provides the `realm` attribute, as defined in [RFC2617](https://tools.ietf.org/html/rfc2617)
pub fn realm<T>(mut self, value: T) -> Self
where
T: Into<Cow<'static, str>>,
{
self.0.realm = Some(value.into());
self
}
/// Provides the `error` attribute, as defined in [RFC6750, Section 3.1](https://tools.ietf.org/html/rfc6750#section-3.1)
pub fn error(mut self, value: Error) -> Self {
self.0.error = Some(value);
self
}
/// Provides the `error_description` attribute, as defined in [RFC6750, Section 3](https://tools.ietf.org/html/rfc6750#section-3)
pub fn error_description<T>(mut self, value: T) -> Self
where
T: Into<Cow<'static, str>>,
{
self.0.error_description = Some(value.into());
self
}
/// Provides the `error_uri` attribute, as defined in [RFC6750, Section 3](https://tools.ietf.org/html/rfc6750#section-3)
///
/// It is up to implementor to provide properly-formed absolute URI.
pub fn error_uri<T>(mut self, value: T) -> Self
where
T: Into<Cow<'static, str>>,
{
self.0.error_uri = Some(value.into());
self
}
/// Consumes the builder and returns built `Bearer` instance.
pub fn finish(self) -> Bearer {
self.0
}
}

View File

@ -0,0 +1,141 @@
use std::borrow::Cow;
use std::fmt;
use std::str;
use actix_web::http::header::{
HeaderValue, IntoHeaderValue, InvalidHeaderValue,
};
use bytes::{BufMut, Bytes, BytesMut};
use super::super::Challenge;
use super::{BearerBuilder, Error};
use crate::utils;
/// Challenge for [`WWW-Authenticate`] header with HTTP Bearer auth scheme,
/// described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3)
///
/// ## Example
///
/// ```
/// # use actix_web::{web, App, HttpRequest, HttpResponse, HttpServer};
/// use actix_web_httpauth::headers::www_authenticate::bearer::{
/// Bearer, Error,
/// };
/// use actix_web_httpauth::headers::www_authenticate::WwwAuthenticate;
///
/// fn index(_req: HttpRequest) -> HttpResponse {
/// let challenge = Bearer::build()
/// .realm("example")
/// .scope("openid profile email")
/// .error(Error::InvalidToken)
/// .error_description("The access token expired")
/// .error_uri("http://example.org")
/// .finish();
///
/// HttpResponse::Unauthorized()
/// .set(WwwAuthenticate(challenge))
/// .finish()
/// }
/// ```
///
/// [`WWW-Authenticate`]: ../struct.WwwAuthenticate.html
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
pub struct Bearer {
pub(crate) scope: Option<Cow<'static, str>>,
pub(crate) realm: Option<Cow<'static, str>>,
pub(crate) error: Option<Error>,
pub(crate) error_description: Option<Cow<'static, str>>,
pub(crate) error_uri: Option<Cow<'static, str>>,
}
impl Bearer {
/// Creates the builder for `Bearer` challenge.
///
/// ## Example
///
/// ```
/// # use actix_web_httpauth::headers::www_authenticate::bearer::{Bearer};
/// let challenge = Bearer::build()
/// .realm("Restricted area")
/// .scope("openid profile email")
/// .finish();
/// ```
pub fn build() -> BearerBuilder {
BearerBuilder::default()
}
}
#[doc(hidden)]
impl Challenge for Bearer {
fn to_bytes(&self) -> Bytes {
let desc_uri_required = self
.error_description
.as_ref()
.map_or(0, |desc| desc.len() + 20)
+ self.error_uri.as_ref().map_or(0, |url| url.len() + 12);
let capacity = 6
+ self.realm.as_ref().map_or(0, |realm| realm.len() + 9)
+ self.scope.as_ref().map_or(0, |scope| scope.len() + 9)
+ desc_uri_required;
let mut buffer = BytesMut::with_capacity(capacity);
buffer.put(&b"Bearer"[..]);
if let Some(ref realm) = self.realm {
buffer.put(&b" realm=\""[..]);
utils::put_quoted(&mut buffer, realm);
buffer.put_u8(b'"');
}
if let Some(ref scope) = self.scope {
buffer.put(&b" scope=\""[..]);
utils::put_quoted(&mut buffer, scope);
buffer.put_u8(b'"');
}
if let Some(ref error) = self.error {
let error_repr = error.as_str();
let remaining = buffer.remaining_mut();
let required = desc_uri_required + error_repr.len() + 9; // 9 is for `" error=\"\""`
if remaining < required {
buffer.reserve(required);
}
buffer.put(&b" error=\""[..]);
utils::put_quoted(&mut buffer, error_repr);
buffer.put_u8(b'"')
}
if let Some(ref error_description) = self.error_description {
buffer.put(&b" error_description=\""[..]);
utils::put_quoted(&mut buffer, error_description);
buffer.put_u8(b'"');
}
if let Some(ref error_uri) = self.error_uri {
buffer.put(&b" error_uri=\""[..]);
utils::put_quoted(&mut buffer, error_uri);
buffer.put_u8(b'"');
}
buffer.freeze()
}
}
impl fmt::Display for Bearer {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> Result<(), fmt::Error> {
let bytes = self.to_bytes();
let repr = str::from_utf8(&bytes)
// Should not happen since challenges are crafted manually
// from `&'static str`'s and Strings
.map_err(|_| fmt::Error)?;
f.write_str(repr)
}
}
impl IntoHeaderValue for Bearer {
type Error = InvalidHeaderValue;
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
HeaderValue::from_maybe_shared(self.to_bytes())
}
}

View File

@ -0,0 +1,51 @@
use std::fmt;
use actix_web::http::StatusCode;
/// Bearer authorization error types, described in [RFC 6750](https://tools.ietf.org/html/rfc6750#section-3.1)
#[derive(Copy, Clone, Eq, PartialEq, Ord, PartialOrd, Hash, Debug)]
pub enum Error {
/// The request is missing a required parameter, includes an unsupported
/// parameter or parameter value, repeats the same parameter, uses more
/// than one method for including an access token, or is otherwise
/// malformed.
InvalidRequest,
/// The access token provided is expired, revoked, malformed, or invalid
/// for other reasons.
InvalidToken,
/// The request requires higher privileges than provided by the access
/// token.
InsufficientScope,
}
impl Error {
/// Returns [HTTP status code] suitable for current error type.
///
/// [HTTP status code]: `actix_web::http::StatusCode`
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn status_code(&self) -> StatusCode {
match self {
Error::InvalidRequest => StatusCode::BAD_REQUEST,
Error::InvalidToken => StatusCode::UNAUTHORIZED,
Error::InsufficientScope => StatusCode::FORBIDDEN,
}
}
#[doc(hidden)]
#[allow(clippy::trivially_copy_pass_by_ref)]
pub fn as_str(&self) -> &str {
match self {
Error::InvalidRequest => "invalid_request",
Error::InvalidToken => "invalid_token",
Error::InsufficientScope => "insufficient_scope",
}
}
}
impl fmt::Display for Error {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(self.as_str())
}
}

View File

@ -0,0 +1,12 @@
//! Challenge for the "Bearer" HTTP Authentication Scheme
mod builder;
mod challenge;
mod errors;
pub use self::builder::BearerBuilder;
pub use self::challenge::Bearer;
pub use self::errors::Error;
#[cfg(test)]
mod tests;

View File

@ -0,0 +1,16 @@
use super::*;
#[test]
fn to_bytes() {
let b = Bearer::build()
.error(Error::InvalidToken)
.error_description(
"Subject 8740827c-2e0a-447b-9716-d73042e4039d not found",
)
.finish();
assert_eq!(
"Bearer error=\"invalid_token\" error_description=\"Subject 8740827c-2e0a-447b-9716-d73042e4039d not found\"",
format!("{}", b)
);
}

View File

@ -0,0 +1,15 @@
use std::fmt::{Debug, Display};
use actix_web::http::header::IntoHeaderValue;
use bytes::Bytes;
pub mod basic;
pub mod bearer;
/// Authentication challenge for `WWW-Authenticate` header.
pub trait Challenge:
IntoHeaderValue + Debug + Display + Clone + Send + Sync
{
/// Converts the challenge into a bytes suitable for HTTP transmission.
fn to_bytes(&self) -> Bytes;
}

View File

@ -0,0 +1,33 @@
use actix_web::error::ParseError;
use actix_web::http::header::{
Header, HeaderName, HeaderValue, IntoHeaderValue, WWW_AUTHENTICATE,
};
use actix_web::HttpMessage;
use super::Challenge;
/// `WWW-Authenticate` header, described in [RFC 7235](https://tools.ietf.org/html/rfc7235#section-4.1)
///
/// This header is generic over [Challenge](./trait.Challenge.html) trait,
/// see [Basic](./basic/struct.Basic.html) and
/// [Bearer](./bearer/struct.Bearer.html) challenges for details.
#[derive(Eq, PartialEq, Ord, PartialOrd, Hash, Debug, Default, Clone)]
pub struct WwwAuthenticate<C: Challenge>(pub C);
impl<C: Challenge> Header for WwwAuthenticate<C> {
fn name() -> HeaderName {
WWW_AUTHENTICATE
}
fn parse<T: HttpMessage>(_msg: &T) -> Result<Self, ParseError> {
unimplemented!()
}
}
impl<C: Challenge> IntoHeaderValue for WwwAuthenticate<C> {
type Error = <C as IntoHeaderValue>::Error;
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
self.0.try_into()
}
}

View File

@ -0,0 +1,9 @@
//! `WWW-Authenticate` header and various auth challenges
mod challenge;
mod header;
pub use self::challenge::basic;
pub use self::challenge::bearer;
pub use self::challenge::Challenge;
pub use self::header::WwwAuthenticate;

View File

@ -0,0 +1,29 @@
//! HTTP Authorization support for [actix-web](https://actix.rs) framework.
//!
//! Provides:
//! * typed [Authorization] and [WWW-Authenticate] headers
//! * [extractors] for an [Authorization] header
//! * [middleware] for easier authorization checking
//!
//! ## Supported schemes
//!
//! * `Basic`, as defined in [RFC7617](https://tools.ietf.org/html/rfc7617)
//! * `Bearer`, as defined in [RFC6750](https://tools.ietf.org/html/rfc6750)
//!
//! [Authorization]: `crate::headers::authorization::Authorization`
//! [WWW-Authenticate]: `crate::headers::www_authenticate::WwwAuthenticate`
//! [extractors]: https://actix.rs/docs/extractors/
//! [middleware]: ./middleware/
#![deny(bare_trait_objects)]
#![deny(missing_docs)]
#![deny(nonstandard_style)]
#![deny(rust_2018_idioms)]
#![deny(unused)]
#![deny(clippy::all)]
#![cfg_attr(feature = "nightly", feature(test))]
pub mod extractors;
pub mod headers;
pub mod middleware;
mod utils;

View File

@ -0,0 +1,247 @@
//! HTTP Authentication middleware.
use std::marker::PhantomData;
use std::pin::Pin;
use std::sync::Arc;
use actix_service::{Service, Transform};
use actix_web::dev::{ServiceRequest, ServiceResponse};
use actix_web::Error;
use futures::future::{self, Future, FutureExt, LocalBoxFuture, TryFutureExt};
use futures::lock::Mutex;
use futures::task::{Context, Poll};
use crate::extractors::{basic, bearer, AuthExtractor};
/// Middleware for checking HTTP authentication.
///
/// If there is no `Authorization` header in the request,
/// this middleware returns an error immediately,
/// without calling the `F` callback.
///
/// Otherwise, it will pass both the request and
/// the parsed credentials into it.
/// In case of successful validation `F` callback
/// is required to return the `ServiceRequest` back.
#[derive(Debug, Clone)]
pub struct HttpAuthentication<T, F>
where
T: AuthExtractor,
{
process_fn: Arc<F>,
_extractor: PhantomData<T>,
}
impl<T, F, O> HttpAuthentication<T, F>
where
T: AuthExtractor,
F: Fn(ServiceRequest, T) -> O,
O: Future<Output = Result<ServiceRequest, Error>>,
{
/// Construct `HttpAuthentication` middleware
/// with the provided auth extractor `T` and
/// validation callback `F`.
pub fn with_fn(process_fn: F) -> HttpAuthentication<T, F> {
HttpAuthentication {
process_fn: Arc::new(process_fn),
_extractor: PhantomData,
}
}
}
impl<F, O> HttpAuthentication<basic::BasicAuth, F>
where
F: Fn(ServiceRequest, basic::BasicAuth) -> O,
O: Future<Output = Result<ServiceRequest, Error>>,
{
/// Construct `HttpAuthentication` middleware for the HTTP "Basic"
/// authentication scheme.
///
/// ## Example
///
/// ```
/// # use actix_web::Error;
/// # use actix_web::dev::ServiceRequest;
/// # use actix_web_httpauth::middleware::HttpAuthentication;
/// # use actix_web_httpauth::extractors::basic::BasicAuth;
/// // In this example validator returns immediately,
/// // but since it is required to return anything
/// // that implements `IntoFuture` trait,
/// // it can be extended to query database
/// // or to do something else in a async manner.
/// async fn validator(
/// req: ServiceRequest,
/// credentials: BasicAuth,
/// ) -> Result<ServiceRequest, Error> {
/// // All users are great and more than welcome!
/// Ok(req)
/// }
///
/// let middleware = HttpAuthentication::basic(validator);
/// ```
pub fn basic(process_fn: F) -> Self {
Self::with_fn(process_fn)
}
}
impl<F, O> HttpAuthentication<bearer::BearerAuth, F>
where
F: Fn(ServiceRequest, bearer::BearerAuth) -> O,
O: Future<Output = Result<ServiceRequest, Error>>,
{
/// Construct `HttpAuthentication` middleware for the HTTP "Bearer"
/// authentication scheme.
///
/// ## Example
///
/// ```
/// # use actix_web::Error;
/// # use actix_web::dev::ServiceRequest;
/// # use actix_web_httpauth::middleware::HttpAuthentication;
/// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth};
/// # use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractorConfig};
/// async fn validator(req: ServiceRequest, credentials: BearerAuth) -> Result<ServiceRequest, Error> {
/// if credentials.token() == "mF_9.B5f-4.1JqM" {
/// Ok(req)
/// } else {
/// let config = req.app_data::<Config>()
/// .map(|data| data.get_ref().clone())
/// .unwrap_or_else(Default::default)
/// .scope("urn:example:channel=HBO&urn:example:rating=G,PG-13");
///
/// Err(AuthenticationError::from(config).into())
/// }
/// }
///
/// let middleware = HttpAuthentication::bearer(validator);
/// ```
pub fn bearer(process_fn: F) -> Self {
Self::with_fn(process_fn)
}
}
impl<S, B, T, F, O> Transform<S> for HttpAuthentication<T, F>
where
S: Service<
Request = ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
> + 'static,
S::Future: 'static,
F: Fn(ServiceRequest, T) -> O + 'static,
O: Future<Output = Result<ServiceRequest, Error>> + 'static,
T: AuthExtractor + 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = Error;
type Transform = AuthenticationMiddleware<S, F, T>;
type InitError = ();
type Future = future::Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
future::ok(AuthenticationMiddleware {
service: Arc::new(Mutex::new(service)),
process_fn: self.process_fn.clone(),
_extractor: PhantomData,
})
}
}
#[doc(hidden)]
pub struct AuthenticationMiddleware<S, F, T>
where
T: AuthExtractor,
{
service: Arc<Mutex<S>>,
process_fn: Arc<F>,
_extractor: PhantomData<T>,
}
impl<S, B, F, T, O> Service for AuthenticationMiddleware<S, F, T>
where
S: Service<
Request = ServiceRequest,
Response = ServiceResponse<B>,
Error = Error,
> + 'static,
S::Future: 'static,
F: Fn(ServiceRequest, T) -> O + 'static,
O: Future<Output = Result<ServiceRequest, Error>> + 'static,
T: AuthExtractor + 'static,
{
type Request = ServiceRequest;
type Response = ServiceResponse<B>;
type Error = S::Error;
type Future = LocalBoxFuture<'static, Result<ServiceResponse<B>, Error>>;
fn poll_ready(
&mut self,
ctx: &mut Context<'_>,
) -> Poll<Result<(), Self::Error>> {
self.service
.try_lock()
.expect("AuthenticationMiddleware was called already")
.poll_ready(ctx)
}
fn call(&mut self, req: Self::Request) -> Self::Future {
let process_fn = self.process_fn.clone();
// Note: cloning the mutex, not the service itself
let inner = self.service.clone();
async move {
let (req, credentials) = Extract::<T>::new(req).await?;
let req = process_fn(req, credentials).await?;
let mut service = inner.lock().await;
service.call(req).await
}
.boxed_local()
}
}
struct Extract<T> {
req: Option<ServiceRequest>,
f: Option<LocalBoxFuture<'static, Result<T, Error>>>,
_extractor: PhantomData<fn() -> T>,
}
impl<T> Extract<T> {
pub fn new(req: ServiceRequest) -> Self {
Extract {
req: Some(req),
f: None,
_extractor: PhantomData,
}
}
}
impl<T> Future for Extract<T>
where
T: AuthExtractor,
T::Future: 'static,
T::Error: 'static,
{
type Output = Result<(ServiceRequest, T), Error>;
fn poll(
mut self: Pin<&mut Self>,
ctx: &mut Context<'_>,
) -> Poll<Self::Output> {
if self.f.is_none() {
let req =
self.req.as_ref().expect("Extract future was polled twice!");
let f = T::from_service_request(req).map_err(Into::into);
self.f = Some(f.boxed_local());
}
let f = self
.f
.as_mut()
.expect("Extraction future should be initialized at this point");
let credentials = futures::ready!(Future::poll(f.as_mut(), ctx))?;
let req = self.req.take().expect("Extract future was polled twice!");
Poll::Ready(Ok((req, credentials)))
}
}

View File

@ -0,0 +1,111 @@
use std::str;
use bytes::BytesMut;
enum State {
YieldStr,
YieldQuote,
}
struct Quoted<'a> {
inner: ::std::iter::Peekable<str::Split<'a, char>>,
state: State,
}
impl<'a> Quoted<'a> {
pub fn new(s: &'a str) -> Quoted<'_> {
Quoted {
inner: s.split('"').peekable(),
state: State::YieldStr,
}
}
}
impl<'a> Iterator for Quoted<'a> {
type Item = &'a str;
fn next(&mut self) -> Option<Self::Item> {
match self.state {
State::YieldStr => match self.inner.next() {
Some(s) => {
self.state = State::YieldQuote;
Some(s)
}
None => None,
},
State::YieldQuote => match self.inner.peek() {
Some(_) => {
self.state = State::YieldStr;
Some("\\\"")
}
None => None,
},
}
}
}
/// Tries to quote the quotes in the passed `value`
pub fn put_quoted(buf: &mut BytesMut, value: &str) {
for part in Quoted::new(value) {
buf.extend_from_slice(part.as_bytes());
}
}
#[cfg(test)]
mod tests {
use std::str;
use bytes::BytesMut;
use super::put_quoted;
#[test]
fn test_quote_str() {
let input = "a \"quoted\" string";
let mut output = BytesMut::new();
put_quoted(&mut output, input);
let result = str::from_utf8(&output).unwrap();
assert_eq!(result, "a \\\"quoted\\\" string");
}
#[test]
fn test_without_quotes() {
let input = "non-quoted string";
let mut output = BytesMut::new();
put_quoted(&mut output, input);
let result = str::from_utf8(&output).unwrap();
assert_eq!(result, "non-quoted string");
}
#[test]
fn test_starts_with_quote() {
let input = "\"first-quoted string";
let mut output = BytesMut::new();
put_quoted(&mut output, input);
let result = str::from_utf8(&output).unwrap();
assert_eq!(result, "\\\"first-quoted string");
}
#[test]
fn test_ends_with_quote() {
let input = "last-quoted string\"";
let mut output = BytesMut::new();
put_quoted(&mut output, input);
let result = str::from_utf8(&output).unwrap();
assert_eq!(result, "last-quoted string\\\"");
}
#[test]
fn test_double_quote() {
let input = "quote\"\"string";
let mut output = BytesMut::new();
put_quoted(&mut output, input);
let result = str::from_utf8(&output).unwrap();
assert_eq!(result, "quote\\\"\\\"string");
}
}