diff --git a/Cargo.toml b/Cargo.toml index 68b78922e..0e9ee8a07 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "actix-web-httpauth" -version = "0.3.0-alpha.2" +version = "0.3.0" authors = ["svartalf "] description = "HTTP authentication schemes for actix-web" readme = "README.md" @@ -14,7 +14,7 @@ exclude = [".travis.yml", ".gitignore"] edition = "2018" [dependencies] -actix-web = { version = "1.0.0-beta.5", default_features = false } +actix-web = { version = "^1.0", default_features = false } futures = "0.1" actix-service = "0.4.0" bytes = "0.4" diff --git a/README.md b/README.md index 7f78ccf13..51a82e39d 100644 --- a/README.md +++ b/README.md @@ -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://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) ![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. -All supported schemas are actix [Extractors](https://docs.rs/actix-web/0.6.7/actix_web/trait.FromRequest.html), -and can be used both in middlewares and request handlers, check the `examples/` folder. +Provides: + * 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 diff --git a/src/headers/authorization/scheme/basic.rs b/src/headers/authorization/scheme/basic.rs index 6c91d6009..0889044b3 100644 --- a/src/headers/authorization/scheme/basic.rs +++ b/src/headers/authorization/scheme/basic.rs @@ -10,7 +10,6 @@ use bytes::{BufMut, BytesMut}; use crate::headers::authorization::errors::ParseError; use crate::headers::authorization::Scheme; -use crate::utils; /// Credentials for `Basic` authentication scheme, defined in [RFC 7617](https://tools.ietf.org/html/rfc7617) #[derive(Clone, Eq, Ord, PartialEq, PartialOrd)] @@ -107,13 +106,14 @@ impl IntoHeaderValue for Basic { fn try_into(self) -> Result::Error> { let mut credentials = BytesMut::with_capacity( self.user_id.len() - + 1 + + 1 // ':' + 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':'); 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 diff --git a/src/headers/authorization/scheme/bearer.rs b/src/headers/authorization/scheme/bearer.rs index 78b532a58..d9eac636d 100644 --- a/src/headers/authorization/scheme/bearer.rs +++ b/src/headers/authorization/scheme/bearer.rs @@ -8,7 +8,6 @@ use bytes::{BufMut, BytesMut}; use crate::headers::authorization::errors::ParseError; use crate::headers::authorization::scheme::Scheme; -use crate::utils; /// 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::Error> { let mut buffer = BytesMut::with_capacity(7 + self.token.len()); buffer.put("Bearer "); - utils::put_cow(&mut buffer, &self.token); + buffer.extend_from_slice(self.token.as_bytes()); HeaderValue::from_shared(buffer.freeze()) } diff --git a/src/headers/www_authenticate/challenge/basic.rs b/src/headers/www_authenticate/challenge/basic.rs index 86c637c98..d5699e01a 100644 --- a/src/headers/www_authenticate/challenge/basic.rs +++ b/src/headers/www_authenticate/challenge/basic.rs @@ -85,7 +85,7 @@ impl Challenge for Basic { buffer.put("Basic"); if let Some(ref realm) = self.realm { buffer.put(" realm=\""); - utils::put_cow(&mut buffer, realm); + utils::put_quoted(&mut buffer, realm); buffer.put("\""); } diff --git a/src/headers/www_authenticate/challenge/bearer/challenge.rs b/src/headers/www_authenticate/challenge/bearer/challenge.rs index 025b0e585..92f977dda 100644 --- a/src/headers/www_authenticate/challenge/bearer/challenge.rs +++ b/src/headers/www_authenticate/challenge/bearer/challenge.rs @@ -82,13 +82,13 @@ impl Challenge for Bearer { if let Some(ref realm) = self.realm { buffer.put(" realm=\""); - utils::put_cow(&mut buffer, realm); + utils::put_quoted(&mut buffer, realm); buffer.put("\""); } if let Some(ref scope) = self.scope { buffer.put(" scope=\""); - utils::put_cow(&mut buffer, scope); + utils::put_quoted(&mut buffer, scope); buffer.put("\""); } @@ -100,19 +100,19 @@ impl Challenge for Bearer { buffer.reserve(required); } buffer.put(" error=\""); - buffer.put(error_repr); + utils::put_quoted(&mut buffer, error_repr); buffer.put("\"") } if let Some(ref error_description) = self.error_description { buffer.put(" error_description=\""); - utils::put_cow(&mut buffer, error_description); + utils::put_quoted(&mut buffer, error_description); buffer.put("\""); } if let Some(ref error_uri) = self.error_uri { buffer.put(" error_uri=\""); - utils::put_cow(&mut buffer, error_uri); + utils::put_quoted(&mut buffer, error_uri); buffer.put("\""); } diff --git a/src/middleware.rs b/src/middleware.rs index c2eceb12d..e35efdb30 100644 --- a/src/middleware.rs +++ b/src/middleware.rs @@ -16,27 +16,31 @@ use crate::extractors::{basic, bearer, AuthExtractor}; /// If there is no `Authorization` header in the request, /// this middleware returns an error immediately, /// 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 where T: AuthExtractor, { - validator_fn: Rc, + process_fn: Rc, _extractor: PhantomData, } impl HttpAuthentication where T: AuthExtractor, - F: FnMut(&mut ServiceRequest, T) -> O, - O: IntoFuture, + F: FnMut(ServiceRequest, T) -> O, + O: IntoFuture, { /// Construct `HttpAuthentication` middleware /// with the provided auth extractor `T` and /// validation callback `F`. - pub fn with_fn(validator_fn: F) -> HttpAuthentication { + pub fn with_fn(process_fn: F) -> HttpAuthentication { HttpAuthentication { - validator_fn: Rc::new(validator_fn), + process_fn: Rc::new(process_fn), _extractor: PhantomData, } } @@ -44,15 +48,15 @@ where impl HttpAuthentication where - F: FnMut(&mut ServiceRequest, basic::BasicAuth) -> O, - O: IntoFuture, + F: FnMut(ServiceRequest, basic::BasicAuth) -> O, + O: IntoFuture, { /// Construct `HttpAuthentication` middleware for the HTTP "Basic" /// authentication scheme. /// /// ## Example /// - /// ``` + /// ```rust /// # use actix_web::Error; /// # use actix_web::dev::ServiceRequest; /// # use futures::future::{self, FutureResult}; @@ -64,39 +68,40 @@ where /// // it can be extended to query database /// // or to do something else in a async manner. /// fn validator( - /// req: &mut ServiceRequest, - /// credentials: BasicAuth, - /// ) -> FutureResult<(), Error> { + /// req: ServiceRequest, + /// credentials: BasicAuth, + /// ) -> FutureResult { /// // All users are great and more than welcome! - /// future::ok(()) + /// future::ok(req) /// } /// /// let middleware = HttpAuthentication::basic(validator); /// ``` - pub fn basic(validator_fn: F) -> Self { - Self::with_fn(validator_fn) + pub fn basic(process_fn: F) -> Self { + Self::with_fn(process_fn) } } impl HttpAuthentication where - F: FnMut(&mut ServiceRequest, bearer::BearerAuth) -> O, - O: IntoFuture, + F: FnMut(ServiceRequest, bearer::BearerAuth) -> O, + O: IntoFuture, { /// Construct `HttpAuthentication` middleware for the HTTP "Bearer" /// authentication scheme. + /// /// ## Example /// - /// ``` + /// ```rust /// # use actix_web::Error; /// # use actix_web::dev::ServiceRequest; /// # use futures::future::{self, FutureResult}; /// # use actix_web_httpauth::middleware::HttpAuthentication; /// # use actix_web_httpauth::extractors::bearer::{Config, BearerAuth}; /// # use actix_web_httpauth::extractors::{AuthenticationError, AuthExtractorConfig}; - /// fn validator(req: &mut ServiceRequest, credentials: BearerAuth) -> FutureResult<(), Error> { + /// fn validator(req: ServiceRequest, credentials: BearerAuth) -> FutureResult { /// if credentials.token() == "mF_9.B5f-4.1JqM" { - /// future::ok(()) + /// future::ok(req) /// } else { /// let config = req.app_data::() /// .map(|data| data.get_ref().clone()) @@ -109,8 +114,8 @@ where /// /// let middleware = HttpAuthentication::bearer(validator); /// ``` - pub fn bearer(validator_fn: F) -> Self { - Self::with_fn(validator_fn) + pub fn bearer(process_fn: F) -> Self { + Self::with_fn(process_fn) } } @@ -122,8 +127,8 @@ where Error = Error, > + 'static, S::Future: 'static, - F: Fn(&mut ServiceRequest, T) -> O + 'static, - O: IntoFuture + 'static, + F: Fn(ServiceRequest, T) -> O + 'static, + O: IntoFuture + 'static, T: AuthExtractor + 'static, { type Request = ServiceRequest; @@ -136,7 +141,7 @@ where fn new_transform(&self, service: S) -> Self::Future { future::ok(AuthenticationMiddleware { service: Some(service), - validator_fn: self.validator_fn.clone(), + process_fn: self.process_fn.clone(), _extractor: PhantomData, }) } @@ -148,7 +153,7 @@ where T: AuthExtractor, { service: Option, - validator_fn: Rc, + process_fn: Rc, _extractor: PhantomData, } @@ -160,8 +165,8 @@ where Error = Error, > + 'static, S::Future: 'static, - F: Fn(&mut ServiceRequest, T) -> O + 'static, - O: IntoFuture + 'static, + F: Fn(ServiceRequest, T) -> O + 'static, + O: IntoFuture + 'static, T: AuthExtractor + 'static, { type Request = ServiceRequest; @@ -177,7 +182,7 @@ where } 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 .service .take() @@ -185,7 +190,7 @@ where let f = Extract::new(req) .and_then(move |(req, credentials)| { - Validate::new(req, validator_fn, credentials) + (process_fn)(req, credentials) }) .and_then(move |req| service.call(req)); @@ -238,60 +243,3 @@ where Ok(Async::Ready((req, credentials))) } } - -struct Validate { - req: Option, - validation_f: Option>>, - validator_fn: Rc, - credentials: Option, -} - -impl Validate { - pub fn new( - req: ServiceRequest, - validator_fn: Rc, - credentials: T, - ) -> Self { - Validate { - req: Some(req), - credentials: Some(credentials), - validator_fn, - validation_f: None, - } - } -} - -impl Future for Validate -where - F: Fn(&mut ServiceRequest, T) -> O, - O: IntoFuture + 'static, -{ - type Item = ServiceRequest; - type Error = Error; - - fn poll(&mut self) -> Poll { - 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)) - } -} diff --git a/src/utils.rs b/src/utils.rs index e30faedf3..4312663e1 100644 --- a/src/utils.rs +++ b/src/utils.rs @@ -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 -// ourselves. -#[inline] -#[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 { - Cow::Borrowed(str) => buf.put(str), - Cow::Owned(ref string) => buf.put(string), +enum State { + YieldStr, + YieldQuote, +} + +struct Quoted<'a> { + inner: ::std::iter::Peekable>, + 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 { + 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"); } }