1
0
mirror of https://github.com/actix/actix-extras.git synced 2025-02-12 22:55:32 +01:00

Release 0.3.0

This commit is contained in:
svartalf 2019-06-05 18:52:47 +03:00
parent aad60744ed
commit 0ebfd790fc
8 changed files with 167 additions and 113 deletions

View File

@ -1,6 +1,6 @@
[package] [package]
name = "actix-web-httpauth" name = "actix-web-httpauth"
version = "0.3.0-alpha.2" version = "0.3.0"
authors = ["svartalf <self@svartalf.info>"] authors = ["svartalf <self@svartalf.info>"]
description = "HTTP authentication schemes for actix-web" description = "HTTP authentication schemes for actix-web"
readme = "README.md" readme = "README.md"
@ -14,7 +14,7 @@ exclude = [".travis.yml", ".gitignore"]
edition = "2018" edition = "2018"
[dependencies] [dependencies]
actix-web = { version = "1.0.0-beta.5", default_features = false } actix-web = { version = "^1.0", default_features = false }
futures = "0.1" futures = "0.1"
actix-service = "0.4.0" actix-service = "0.4.0"
bytes = "0.4" bytes = "0.4"

View File

@ -2,13 +2,19 @@
[![Latest Version](https://img.shields.io/crates/v/actix-web-httpauth.svg)](https://crates.io/crates/actix-web-httpauth) [![Latest Version](https://img.shields.io/crates/v/actix-web-httpauth.svg)](https://crates.io/crates/actix-web-httpauth)
[![Latest Version](https://docs.rs/actix-web-httpauth/badge.svg)](https://docs.rs/actix-web-httpauth) [![Latest Version](https://docs.rs/actix-web-httpauth/badge.svg)](https://docs.rs/actix-web-httpauth)
[![dependency status](https://deps.rs/crate/actix-web-httpauth/0.3.0/status.svg)](https://deps.rs/crate/actix-web-httpauth/0.3.0)
![Build Status](https://travis-ci.org/svartalf/actix-web-httpauth.svg?branch=master) ![Build Status](https://travis-ci.org/svartalf/actix-web-httpauth.svg?branch=master)
![Apache 2.0 OR MIT licensed](https://img.shields.io/badge/license-Apache2.0%2FMIT-blue.svg) ![Apache 2.0 OR MIT licensed](https://img.shields.io/badge/license-Apache2.0%2FMIT-blue.svg)
HTTP authentication schemes for [actix-web](https://github.com/actix/actix-web) framework. HTTP authentication schemes for [actix-web](https://github.com/actix/actix-web) framework.
All supported schemas are actix [Extractors](https://docs.rs/actix-web/0.6.7/actix_web/trait.FromRequest.html), Provides:
and can be used both in middlewares and request handlers, check the `examples/` folder. * typed [Authorization] and [WWW-Authenticate] headers
* [extractors] for an [Authorization] header
* [middleware] for easier authorization checking
All supported schemas are actix [Extractors](https://docs.rs/actix-web/1.0.0/actix_web/trait.FromRequest.html),
and can be used both in the middlewares and request handlers.
## Supported schemes ## Supported schemes

View File

@ -10,7 +10,6 @@ use bytes::{BufMut, BytesMut};
use crate::headers::authorization::errors::ParseError; use crate::headers::authorization::errors::ParseError;
use crate::headers::authorization::Scheme; use crate::headers::authorization::Scheme;
use crate::utils;
/// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617) /// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617)
#[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)]
@ -107,13 +106,14 @@ impl IntoHeaderValue for Basic {
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> { fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
let mut credentials = BytesMut::with_capacity( let mut credentials = BytesMut::with_capacity(
self.user_id.len() self.user_id.len()
+ 1 + 1 // ':'
+ self.password.as_ref().map_or(0, |pwd| pwd.len()), + self.password.as_ref().map_or(0, |pwd| pwd.len()),
); );
utils::put_cow(&mut credentials, &self.user_id);
credentials.extend_from_slice(self.user_id.as_bytes());
credentials.put_u8(b':'); credentials.put_u8(b':');
if let Some(ref password) = self.password { if let Some(ref password) = self.password {
utils::put_cow(&mut credentials, password); credentials.extend_from_slice(password.as_bytes());
} }
// TODO: It would be nice not to allocate new `String` here but write // TODO: It would be nice not to allocate new `String` here but write

View File

@ -8,7 +8,6 @@ use bytes::{BufMut, BytesMut};
use crate::headers::authorization::errors::ParseError; use crate::headers::authorization::errors::ParseError;
use crate::headers::authorization::scheme::Scheme; use crate::headers::authorization::scheme::Scheme;
use crate::utils;
/// Credentials for `Bearer` authentication scheme, defined in [RFC6750](https://tools.ietf.org/html/rfc6750) /// Credentials for `Bearer` authentication scheme, defined in [RFC6750](https://tools.ietf.org/html/rfc6750)
/// ///
@ -82,7 +81,7 @@ impl IntoHeaderValue for Bearer {
fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> { fn try_into(self) -> Result<HeaderValue, <Self as IntoHeaderValue>::Error> {
let mut buffer = BytesMut::with_capacity(7 + self.token.len()); let mut buffer = BytesMut::with_capacity(7 + self.token.len());
buffer.put("Bearer "); buffer.put("Bearer ");
utils::put_cow(&mut buffer, &self.token); buffer.extend_from_slice(self.token.as_bytes());
HeaderValue::from_shared(buffer.freeze()) HeaderValue::from_shared(buffer.freeze())
} }

View File

@ -85,7 +85,7 @@ impl Challenge for Basic {
buffer.put("Basic"); buffer.put("Basic");
if let Some(ref realm) = self.realm { if let Some(ref realm) = self.realm {
buffer.put(" realm=\""); buffer.put(" realm=\"");
utils::put_cow(&mut buffer, realm); utils::put_quoted(&mut buffer, realm);
buffer.put("\""); buffer.put("\"");
} }

View File

@ -82,13 +82,13 @@ impl Challenge for Bearer {
if let Some(ref realm) = self.realm { if let Some(ref realm) = self.realm {
buffer.put(" realm=\""); buffer.put(" realm=\"");
utils::put_cow(&mut buffer, realm); utils::put_quoted(&mut buffer, realm);
buffer.put("\""); buffer.put("\"");
} }
if let Some(ref scope) = self.scope { if let Some(ref scope) = self.scope {
buffer.put(" scope=\""); buffer.put(" scope=\"");
utils::put_cow(&mut buffer, scope); utils::put_quoted(&mut buffer, scope);
buffer.put("\""); buffer.put("\"");
} }
@ -100,19 +100,19 @@ impl Challenge for Bearer {
buffer.reserve(required); buffer.reserve(required);
} }
buffer.put(" error=\""); buffer.put(" error=\"");
buffer.put(error_repr); utils::put_quoted(&mut buffer, error_repr);
buffer.put("\"") buffer.put("\"")
} }
if let Some(ref error_description) = self.error_description { if let Some(ref error_description) = self.error_description {
buffer.put(" error_description=\""); buffer.put(" error_description=\"");
utils::put_cow(&mut buffer, error_description); utils::put_quoted(&mut buffer, error_description);
buffer.put("\""); buffer.put("\"");
} }
if let Some(ref error_uri) = self.error_uri { if let Some(ref error_uri) = self.error_uri {
buffer.put(" error_uri=\""); buffer.put(" error_uri=\"");
utils::put_cow(&mut buffer, error_uri); utils::put_quoted(&mut buffer, error_uri);
buffer.put("\""); buffer.put("\"");
} }

View File

@ -16,27 +16,31 @@ use crate::extractors::{basic, bearer, AuthExtractor};
/// If there is no `Authorization` header in the request, /// If there is no `Authorization` header in the request,
/// this middleware returns an error immediately, /// this middleware returns an error immediately,
/// without calling the `F` callback. /// without calling the `F` callback.
/// Otherwise, it will pass parsed credentials into it. ///
/// 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.
pub struct HttpAuthentication<T, F> pub struct HttpAuthentication<T, F>
where where
T: AuthExtractor, T: AuthExtractor,
{ {
validator_fn: Rc<F>, process_fn: Rc<F>,
_extractor: PhantomData<T>, _extractor: PhantomData<T>,
} }
impl<T, F, O> HttpAuthentication<T, F> impl<T, F, O> HttpAuthentication<T, F>
where where
T: AuthExtractor, T: AuthExtractor,
F: FnMut(&mut ServiceRequest, T) -> O, F: FnMut(ServiceRequest, T) -> O,
O: IntoFuture<Item = (), Error = Error>, O: IntoFuture<Item = ServiceRequest, Error = Error>,
{ {
/// Construct `HttpAuthentication` middleware /// Construct `HttpAuthentication` middleware
/// with the provided auth extractor `T` and /// with the provided auth extractor `T` and
/// validation callback `F`. /// validation callback `F`.
pub fn with_fn(validator_fn: F) -> HttpAuthentication<T, F> { pub fn with_fn(process_fn: F) -> HttpAuthentication<T, F> {
HttpAuthentication { HttpAuthentication {
validator_fn: Rc::new(validator_fn), process_fn: Rc::new(process_fn),
_extractor: PhantomData, _extractor: PhantomData,
} }
} }
@ -44,15 +48,15 @@ where
impl<F, O> HttpAuthentication<basic::BasicAuth, F> impl<F, O> HttpAuthentication<basic::BasicAuth, F>
where where
F: FnMut(&mut ServiceRequest, basic::BasicAuth) -> O, F: FnMut(ServiceRequest, basic::BasicAuth) -> O,
O: IntoFuture<Item = (), Error = Error>, O: IntoFuture<Item = ServiceRequest, Error = Error>,
{ {
/// Construct `HttpAuthentication` middleware for the HTTP "Basic" /// Construct `HttpAuthentication` middleware for the HTTP "Basic"
/// authentication scheme. /// authentication scheme.
/// ///
/// ## Example /// ## Example
/// ///
/// ``` /// ```rust
/// # use actix_web::Error; /// # use actix_web::Error;
/// # use actix_web::dev::ServiceRequest; /// # use actix_web::dev::ServiceRequest;
/// # use futures::future::{self, FutureResult}; /// # use futures::future::{self, FutureResult};
@ -64,39 +68,40 @@ where
/// // it can be extended to query database /// // it can be extended to query database
/// // or to do something else in a async manner. /// // or to do something else in a async manner.
/// fn validator( /// fn validator(
/// req: &mut ServiceRequest, /// req: ServiceRequest,
/// credentials: BasicAuth, /// credentials: BasicAuth,
/// ) -> FutureResult<(), Error> { /// ) -> FutureResult<ServiceRequest, Error> {
/// // All users are great and more than welcome! /// // All users are great and more than welcome!
/// future::ok(()) /// future::ok(req)
/// } /// }
/// ///
/// let middleware = HttpAuthentication::basic(validator); /// let middleware = HttpAuthentication::basic(validator);
/// ``` /// ```
pub fn basic(validator_fn: F) -> Self { pub fn basic(process_fn: F) -> Self {
Self::with_fn(validator_fn) Self::with_fn(process_fn)
} }
} }
impl<F, O> HttpAuthentication<bearer::BearerAuth, F> impl<F, O> HttpAuthentication<bearer::BearerAuth, F>
where where
F: FnMut(&mut ServiceRequest, bearer::BearerAuth) -> O, F: FnMut(ServiceRequest, bearer::BearerAuth) -> O,
O: IntoFuture<Item = (), Error = Error>, O: IntoFuture<Item = ServiceRequest, Error = Error>,
{ {
/// Construct `HttpAuthentication` middleware for the HTTP "Bearer" /// Construct `HttpAuthentication` middleware for the HTTP "Bearer"
/// authentication scheme. /// authentication scheme.
///
/// ## Example /// ## Example
/// ///
/// ``` /// ```rust
/// # use actix_web::Error; /// # use actix_web::Error;
/// # use actix_web::dev::ServiceRequest; /// # use actix_web::dev::ServiceRequest;
/// # use futures::future::{self, FutureResult}; /// # use futures::future::{self, FutureResult};
/// # use actix_web_httpauth::middleware::HttpAuthentication; /// # use actix_web_httpauth::middleware::HttpAuthentication;
/// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth}; /// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth};
/// # use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractorConfig}; /// # use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractorConfig};
/// fn validator(req: &mut ServiceRequest, credentials: BearerAuth) -> FutureResult<(), Error> { /// fn validator(req: ServiceRequest, credentials: BearerAuth) -> FutureResult<ServiceRequest, Error> {
/// if credentials.token() == "mF_9.B5f-4.1JqM" { /// if credentials.token() == "mF_9.B5f-4.1JqM" {
/// future::ok(()) /// future::ok(req)
/// } else { /// } else {
/// let config = req.app_data::<Config>() /// let config = req.app_data::<Config>()
/// .map(|data| data.get_ref().clone()) /// .map(|data| data.get_ref().clone())
@ -109,8 +114,8 @@ where
/// ///
/// let middleware = HttpAuthentication::bearer(validator); /// let middleware = HttpAuthentication::bearer(validator);
/// ``` /// ```
pub fn bearer(validator_fn: F) -> Self { pub fn bearer(process_fn: F) -> Self {
Self::with_fn(validator_fn) Self::with_fn(process_fn)
} }
} }
@ -122,8 +127,8 @@ where
Error = Error, Error = Error,
> + 'static, > + 'static,
S::Future: 'static, S::Future: 'static,
F: Fn(&mut ServiceRequest, T) -> O + 'static, F: Fn(ServiceRequest, T) -> O + 'static,
O: IntoFuture<Item = (), Error = Error> + 'static, O: IntoFuture<Item = ServiceRequest, Error = Error> + 'static,
T: AuthExtractor + 'static, T: AuthExtractor + 'static,
{ {
type Request = ServiceRequest; type Request = ServiceRequest;
@ -136,7 +141,7 @@ where
fn new_transform(&self, service: S) -> Self::Future { fn new_transform(&self, service: S) -> Self::Future {
future::ok(AuthenticationMiddleware { future::ok(AuthenticationMiddleware {
service: Some(service), service: Some(service),
validator_fn: self.validator_fn.clone(), process_fn: self.process_fn.clone(),
_extractor: PhantomData, _extractor: PhantomData,
}) })
} }
@ -148,7 +153,7 @@ where
T: AuthExtractor, T: AuthExtractor,
{ {
service: Option<S>, service: Option<S>,
validator_fn: Rc<F>, process_fn: Rc<F>,
_extractor: PhantomData<T>, _extractor: PhantomData<T>,
} }
@ -160,8 +165,8 @@ where
Error = Error, Error = Error,
> + 'static, > + 'static,
S::Future: 'static, S::Future: 'static,
F: Fn(&mut ServiceRequest, T) -> O + 'static, F: Fn(ServiceRequest, T) -> O + 'static,
O: IntoFuture<Item = (), Error = Error> + 'static, O: IntoFuture<Item = ServiceRequest, Error = Error> + 'static,
T: AuthExtractor + 'static, T: AuthExtractor + 'static,
{ {
type Request = ServiceRequest; type Request = ServiceRequest;
@ -177,7 +182,7 @@ where
} }
fn call(&mut self, req: Self::Request) -> Self::Future { fn call(&mut self, req: Self::Request) -> Self::Future {
let validator_fn = self.validator_fn.clone(); let process_fn = self.process_fn.clone();
let mut service = self let mut service = self
.service .service
.take() .take()
@ -185,7 +190,7 @@ where
let f = Extract::new(req) let f = Extract::new(req)
.and_then(move |(req, credentials)| { .and_then(move |(req, credentials)| {
Validate::new(req, validator_fn, credentials) (process_fn)(req, credentials)
}) })
.and_then(move |req| service.call(req)); .and_then(move |req| service.call(req));
@ -238,60 +243,3 @@ where
Ok(Async::Ready((req, credentials))) Ok(Async::Ready((req, credentials)))
} }
} }
struct Validate<F, T> {
req: Option<ServiceRequest>,
validation_f: Option<Box<dyn Future<Item = (), Error = Error>>>,
validator_fn: Rc<F>,
credentials: Option<T>,
}
impl<F, T> Validate<F, T> {
pub fn new(
req: ServiceRequest,
validator_fn: Rc<F>,
credentials: T,
) -> Self {
Validate {
req: Some(req),
credentials: Some(credentials),
validator_fn,
validation_f: None,
}
}
}
impl<F, T, O> Future for Validate<F, T>
where
F: Fn(&mut ServiceRequest, T) -> O,
O: IntoFuture<Item = (), Error = Error> + 'static,
{
type Item = ServiceRequest;
type Error = Error;
fn poll(&mut self) -> Poll<Self::Item, Self::Error> {
if self.validation_f.is_none() {
let req = self
.req
.as_mut()
.expect("Unable to get the mutable access to the request");
let credentials = self
.credentials
.take()
.expect("Validate future was polled in some weird manner");
let f = (self.validator_fn)(req, credentials).into_future();
self.validation_f = Some(Box::new(f));
}
let f = self
.validation_f
.as_mut()
.expect("Validation future should exist at this moment");
// We do not care about returned `Ok(())`
futures::try_ready!(f.poll());
let req = self.req.take().expect("Validate future was polled already");
Ok(Async::Ready(req))
}
}

View File

@ -1,14 +1,115 @@
use std::borrow::Cow; use std::str;
use bytes::{BufMut, BytesMut}; use bytes::BytesMut;
// `bytes::Buf` is not implemented for `Cow<'static, str>`, implementing it by enum State {
// ourselves. YieldStr,
#[inline] YieldQuote,
#[allow(clippy::ptr_arg)] // Totally okay to accept the reference to Cow here }
pub fn put_cow(buf: &mut BytesMut, value: &Cow<'static, str>) {
match value { struct Quoted<'a> {
Cow::Borrowed(str) => buf.put(str), inner: ::std::iter::Peekable<str::Split<'a, char>>,
Cow::Owned(ref string) => buf.put(string), 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");
} }
} }