mirror of
https://github.com/tower-rs/tower-http.git
synced 2026-03-07 05:39:10 +00:00
Add RequireAuthorization middleware (#70)
This commit is contained in:
parent
526368ca31
commit
dee4286c2e
@ -128,7 +128,7 @@ that the example is correct and provides additional test coverage.
|
||||
The trick to documentation tests is striking a balance between being succinct
|
||||
for a reader to understand and actually testing the API.
|
||||
|
||||
All middlewares should have at least one example that demonstrates how to use
|
||||
All middleware should have at least one example that demonstrates how to use
|
||||
them. The example should highlight the most common use case that we expect most
|
||||
users to have. If the middleware can be configured there should be at least two
|
||||
examples: One that uses the default configuration (as that is probably the most
|
||||
|
||||
12
README.md
12
README.md
@ -5,7 +5,7 @@ initial churn is over, Tower HTTP still isn't released on crates.io. We are
|
||||
actively working on that and you can follow the progress towards 0.1.0
|
||||
[here][milestone].
|
||||
|
||||
Tower middlewares and utilities for HTTP clients and servers.
|
||||
Tower middleware and utilities for HTTP clients and servers.
|
||||
|
||||
[](https://github.com/tower-rs/tower-http/actions)
|
||||
[](https://crates.io/crates/tower-http)
|
||||
@ -14,9 +14,9 @@ Tower middlewares and utilities for HTTP clients and servers.
|
||||
|
||||
More information about this crate can be found in the [crate documentation][docs].
|
||||
|
||||
## Middlewares
|
||||
## Middleware
|
||||
|
||||
Tower HTTP contains lots of middlewares that are generally useful when building
|
||||
Tower HTTP contains lots of middleware that are generally useful when building
|
||||
HTTP servers and clients. Some of the highlights are:
|
||||
|
||||
- `Trace` adds high level logging of requests and responses. Supports both
|
||||
@ -24,12 +24,12 @@ HTTP servers and clients. Some of the highlights are:
|
||||
- `Compression` and `Decompression` to compress/decompress response bodies.
|
||||
- `FollowRedirect` to automatically follow redirection responses.
|
||||
|
||||
See the [docs] for the complete list of middlewares.
|
||||
See the [docs] for the complete list of middleware.
|
||||
|
||||
Middlewares uses the [`http`] crate as the HTTP interface so they're compatible
|
||||
Middleware uses the [`http`] crate as the HTTP interface so they're compatible
|
||||
with any library or framework that also uses [`http`]. For example [hyper].
|
||||
|
||||
The middlewares were originally extracted from one of [@EmbarkStudios] internal
|
||||
The middleware were originally extracted from one of [@EmbarkStudios] internal
|
||||
projects.
|
||||
|
||||
## Examples
|
||||
|
||||
@ -161,7 +161,7 @@ async fn serve_forever(listener: TcpListener) -> Result<(), Box<dyn std::error::
|
||||
// Build our tonic `Service`
|
||||
let service = key_value_store_server::KeyValueStoreServer::new(ServerImpl { db, tx });
|
||||
|
||||
// Apply middlewares to our service
|
||||
// Apply middleware to our service
|
||||
let service = ServiceBuilder::new()
|
||||
// Set a timeout
|
||||
.timeout(Duration::from_secs(10))
|
||||
@ -182,7 +182,7 @@ async fn serve_forever(listener: TcpListener) -> Result<(), Box<dyn std::error::
|
||||
tracing::info!("Listening on {}", addr);
|
||||
|
||||
// We cannot use `tonic::transport::Server` directly as it requires services to implement
|
||||
// `tonic::transport::NamedService` which tower-http middlewares don't
|
||||
// `tonic::transport::NamedService` which tower-http middleware don't
|
||||
Server::from_tcp(listener)?
|
||||
// Required for gRPC
|
||||
.http2_only(true)
|
||||
@ -250,7 +250,7 @@ impl key_value_store_server::KeyValueStore for ServerImpl {
|
||||
}
|
||||
}
|
||||
|
||||
// Build a client with a few middlewares applied and connect to the server
|
||||
// Build a client with a few middleware applied and connect to the server
|
||||
async fn make_client(
|
||||
addr: SocketAddr,
|
||||
) -> Result<
|
||||
@ -273,10 +273,10 @@ async fn make_client(
|
||||
.unwrap();
|
||||
|
||||
// We have to use a `tonic::transport::Channel` as it implementes `Service` so we can apply
|
||||
// middlewares to it
|
||||
// middleware to it
|
||||
let channel = Channel::builder(uri).connect().await?;
|
||||
|
||||
// Apply middlewares to our client
|
||||
// Apply middleware to our client
|
||||
let channel = ServiceBuilder::new()
|
||||
// Decompress response bodies
|
||||
.layer(DecompressionLayer::new())
|
||||
|
||||
@ -66,7 +66,7 @@ async fn serve_forever(listener: TcpListener) -> Result<(), hyper::Error> {
|
||||
// Convert our `Filter` into a `Service`
|
||||
let warp_service = warp::service(filter);
|
||||
|
||||
// Apply middlewares to our service.
|
||||
// Apply middleware to our service.
|
||||
let service = ServiceBuilder::new()
|
||||
// Add high level tracing/logging to all requests
|
||||
.layer(
|
||||
|
||||
@ -1,6 +1,6 @@
|
||||
[package]
|
||||
name = "tower-http"
|
||||
description = "Tower middlewares and utilities for HTTP clients and servers"
|
||||
description = "Tower middleware and utilities for HTTP clients and servers"
|
||||
version = "0.1.0"
|
||||
authors = ["Tower Maintainers <team@tower-rs.com>"]
|
||||
edition = "2018"
|
||||
@ -24,6 +24,7 @@ tower-service = "0.3"
|
||||
|
||||
# optional dependencies
|
||||
async-compression = { version = "0.3", optional = true, features = ["tokio"] }
|
||||
base64 = { version = "0.13", optional = true }
|
||||
hyper = { version = "0.14", optional = true, default_features = false, features = ["stream"] }
|
||||
iri-string = { version = "0.3", optional = true }
|
||||
mime = { version = "0.3", optional = true, default_features = false }
|
||||
@ -47,6 +48,7 @@ tracing-subscriber = "0.2"
|
||||
default = []
|
||||
full = [
|
||||
"add-extension",
|
||||
"auth",
|
||||
"compression",
|
||||
"compression-full",
|
||||
"decompression-full",
|
||||
@ -62,6 +64,7 @@ full = [
|
||||
]
|
||||
|
||||
add-extension = []
|
||||
auth = ["base64"]
|
||||
follow-redirect = ["iri-string", "tower/util"]
|
||||
fs = ["tokio/fs", "tokio-util/io", "mime_guess", "mime"]
|
||||
map-request-body = []
|
||||
|
||||
8
tower-http/src/auth/mod.rs
Normal file
8
tower-http/src/auth/mod.rs
Normal file
@ -0,0 +1,8 @@
|
||||
//! Authorization related middleware.
|
||||
|
||||
pub mod require_authorization;
|
||||
|
||||
#[doc(inline)]
|
||||
pub use self::require_authorization::{
|
||||
AuthorizeRequest, RequireAuthorization, RequireAuthorizationLayer,
|
||||
};
|
||||
640
tower-http/src/auth/require_authorization.rs
Normal file
640
tower-http/src/auth/require_authorization.rs
Normal file
@ -0,0 +1,640 @@
|
||||
//! Authorize requests using the [`Authorization`] header.
|
||||
//!
|
||||
//! [`Authorization`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
|
||||
//!
|
||||
//! # Example
|
||||
//!
|
||||
//! ```
|
||||
//! use tower_http::auth::RequireAuthorizationLayer;
|
||||
//! use hyper::{Request, Response, Body, Error};
|
||||
//! use http::{StatusCode, header::AUTHORIZATION};
|
||||
//! use tower::{Service, ServiceExt, ServiceBuilder, service_fn};
|
||||
//!
|
||||
//! async fn handle(request: Request<Body>) -> Result<Response<Body>, Error> {
|
||||
//! Ok(Response::new(Body::empty()))
|
||||
//! }
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let mut service = ServiceBuilder::new()
|
||||
//! // Require the `Authorization` header to be `Bearer passwordlol`
|
||||
//! .layer(RequireAuthorizationLayer::bearer("passwordlol"))
|
||||
//! .service_fn(handle);
|
||||
//!
|
||||
//! // Requests with the correct token are allowed through
|
||||
//! let request = Request::builder()
|
||||
//! .header(AUTHORIZATION, "Bearer passwordlol")
|
||||
//! .body(Body::empty())
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! let response = service
|
||||
//! .ready_and()
|
||||
//! .await?
|
||||
//! .call(request)
|
||||
//! .await?;
|
||||
//!
|
||||
//! assert_eq!(StatusCode::OK, response.status());
|
||||
//!
|
||||
//! // Requests with an invalid token get a `401 Unauthorized` response
|
||||
//! let request = Request::builder()
|
||||
//! .body(Body::empty())
|
||||
//! .unwrap();
|
||||
//!
|
||||
//! let response = service
|
||||
//! .ready_and()
|
||||
//! .await?
|
||||
//! .call(request)
|
||||
//! .await?;
|
||||
//!
|
||||
//! assert_eq!(StatusCode::UNAUTHORIZED, response.status());
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
//!
|
||||
//! Custom authorization schemes can be made by implementing [`AuthorizeRequest`]:
|
||||
//!
|
||||
//! ```
|
||||
//! use tower_http::auth::{RequireAuthorizationLayer, AuthorizeRequest};
|
||||
//! use hyper::{Request, Response, Body, Error};
|
||||
//! use http::{StatusCode, header::AUTHORIZATION};
|
||||
//! use tower::{Service, ServiceExt, ServiceBuilder, service_fn};
|
||||
//!
|
||||
//! #[derive(Clone, Copy)]
|
||||
//! struct MyAuth;
|
||||
//!
|
||||
//! impl AuthorizeRequest for MyAuth {
|
||||
//! type Output = UserId;
|
||||
//! type ResponseBody = Body;
|
||||
//!
|
||||
//! fn authorize<B>(&mut self, request: &Request<B>) -> Option<UserId> {
|
||||
//! // ...
|
||||
//! # None
|
||||
//! }
|
||||
//!
|
||||
//! fn on_authorized<B>(&mut self, request: &mut Request<B>, user_id: UserId) {
|
||||
//! // Set `user_id` as a request extension so it can be accessed by other
|
||||
//! // services down the stack.
|
||||
//! request.extensions_mut().insert(user_id);
|
||||
//! }
|
||||
//!
|
||||
//! fn unauthorized_response<B>(&mut self, request: &Request<B>) -> Response<Body> {
|
||||
//! Response::builder()
|
||||
//! .status(StatusCode::UNAUTHORIZED)
|
||||
//! .body(Body::empty())
|
||||
//! .unwrap()
|
||||
//! }
|
||||
//! }
|
||||
//!
|
||||
//! #[derive(Debug)]
|
||||
//! struct UserId(String);
|
||||
//!
|
||||
//! async fn handle(request: Request<Body>) -> Result<Response<Body>, Error> {
|
||||
//! // Access the `UserId` that was set in `on_authorized`. If `handle` gets called the
|
||||
//! // request was authorized and `UserId` will be present.
|
||||
//! let user_id = request
|
||||
//! .extensions()
|
||||
//! .get::<UserId>()
|
||||
//! .expect("UserId will be there if request was authorized");
|
||||
//!
|
||||
//! println!("request from {:?}", user_id);
|
||||
//!
|
||||
//! Ok(Response::new(Body::empty()))
|
||||
//! }
|
||||
//!
|
||||
//! # #[tokio::main]
|
||||
//! # async fn main() -> Result<(), Box<dyn std::error::Error>> {
|
||||
//! let service = ServiceBuilder::new()
|
||||
//! // Authorize requests using `MyAuth`
|
||||
//! .layer(RequireAuthorizationLayer::custom(MyAuth))
|
||||
//! .service_fn(handle);
|
||||
//! # Ok(())
|
||||
//! # }
|
||||
//! ```
|
||||
|
||||
use http::{
|
||||
header::{self, HeaderValue},
|
||||
Request, Response, StatusCode,
|
||||
};
|
||||
use http_body::Body;
|
||||
use pin_project::pin_project;
|
||||
use std::{
|
||||
fmt,
|
||||
future::Future,
|
||||
marker::PhantomData,
|
||||
pin::Pin,
|
||||
task::{Context, Poll},
|
||||
};
|
||||
use tower_layer::Layer;
|
||||
use tower_service::Service;
|
||||
|
||||
/// Layer that applies [`RequireAuthorization`] which authorizes all requests using the
|
||||
/// [`Authorization`] header.
|
||||
///
|
||||
/// See the [module docs](crate::auth::require_authorization) for an example.
|
||||
///
|
||||
/// [`Authorization`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct RequireAuthorizationLayer<T> {
|
||||
auth: T,
|
||||
}
|
||||
|
||||
impl<ResBody> RequireAuthorizationLayer<Bearer<ResBody>> {
|
||||
/// Authorize requests using a "bearer token". Commonly used for OAuth 2.
|
||||
///
|
||||
/// The `Authorization` header is required to be `Bearer {token}`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the token is not a valid [`HeaderValue`](http::header::HeaderValue).
|
||||
pub fn bearer(token: &str) -> Self
|
||||
where
|
||||
ResBody: Body + Default,
|
||||
{
|
||||
Self::custom(Bearer::new(token))
|
||||
}
|
||||
}
|
||||
|
||||
impl<ResBody> RequireAuthorizationLayer<Basic<ResBody>> {
|
||||
/// Authorize requests using a username and password pair.
|
||||
///
|
||||
/// The `Authorization` header is required to be `Basic {credentials}` where `credentials` is
|
||||
/// `base64_encode("{username}:{password}")`.
|
||||
///
|
||||
/// Since the username and password is sent in clear text it is recommended to use HTTPS/TLS
|
||||
/// with this method. However use of HTTPS/TLS is not enforced by this middleware.
|
||||
pub fn basic(username: &str, password: &str) -> Self
|
||||
where
|
||||
ResBody: Body + Default,
|
||||
{
|
||||
Self::custom(Basic::new(username, password))
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> RequireAuthorizationLayer<T>
|
||||
where
|
||||
T: AuthorizeRequest,
|
||||
{
|
||||
/// Authorize requests using a custom scheme.
|
||||
pub fn custom(auth: T) -> RequireAuthorizationLayer<T> {
|
||||
Self { auth }
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> Layer<S> for RequireAuthorizationLayer<T>
|
||||
where
|
||||
T: Clone,
|
||||
{
|
||||
type Service = RequireAuthorization<S, T>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
RequireAuthorization::new(inner, self.auth.clone())
|
||||
}
|
||||
}
|
||||
|
||||
/// Middleware that authorizes all requests using the [`Authorization`] header.
|
||||
///
|
||||
/// See the [module docs](crate::auth::require_authorization) for an example.
|
||||
///
|
||||
/// [`Authorization`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
|
||||
#[derive(Clone, Debug)]
|
||||
pub struct RequireAuthorization<S, T> {
|
||||
inner: S,
|
||||
auth: T,
|
||||
}
|
||||
|
||||
impl<S, T> RequireAuthorization<S, T> {
|
||||
fn new(inner: S, auth: T) -> Self {
|
||||
Self { inner, auth }
|
||||
}
|
||||
|
||||
define_inner_service_accessors!();
|
||||
}
|
||||
|
||||
impl<S, ResBody> RequireAuthorization<S, Bearer<ResBody>> {
|
||||
/// Authorize requests using a "bearer token". Commonly used for OAuth 2.
|
||||
///
|
||||
/// The `Authorization` header is required to be `Bearer {token}`.
|
||||
///
|
||||
/// # Panics
|
||||
///
|
||||
/// Panics if the token is not a valid [`HeaderValue`](http::header::HeaderValue).
|
||||
pub fn bearer(inner: S, token: &str) -> Self
|
||||
where
|
||||
ResBody: Body + Default,
|
||||
{
|
||||
Self::custom(inner, Bearer::new(token))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, ResBody> RequireAuthorization<S, Basic<ResBody>> {
|
||||
/// Authorize requests using a username and password pair.
|
||||
///
|
||||
/// The `Authorization` header is required to be `Basic {credentials}` where `credentials` is
|
||||
/// `base64_encode("{username}:{password}")`.
|
||||
///
|
||||
/// Since the username and password is sent in clear text it is recommended to use HTTPS/TLS
|
||||
/// with this method. However use of HTTPS/TLS is not enforced by this middleware.
|
||||
pub fn basic(inner: S, username: &str, password: &str) -> Self
|
||||
where
|
||||
ResBody: Body + Default,
|
||||
{
|
||||
Self::custom(inner, Basic::new(username, password))
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, T> RequireAuthorization<S, T>
|
||||
where
|
||||
T: AuthorizeRequest,
|
||||
{
|
||||
/// Authorize requests using a custom scheme.
|
||||
///
|
||||
/// The `Authorization` header is required to have the value provided.
|
||||
pub fn custom(inner: S, auth: T) -> RequireAuthorization<S, T> {
|
||||
Self { inner, auth }
|
||||
}
|
||||
}
|
||||
|
||||
impl<ReqBody, ResBody, S, T> Service<Request<ReqBody>> for RequireAuthorization<S, T>
|
||||
where
|
||||
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
|
||||
ResBody: Default,
|
||||
T: AuthorizeRequest<ResponseBody = ResBody>,
|
||||
{
|
||||
type Response = Response<ResBody>;
|
||||
type Error = S::Error;
|
||||
type Future = ResponseFuture<S::Future, ResBody>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, mut req: Request<ReqBody>) -> Self::Future {
|
||||
if let Some(output) = self.auth.authorize(&req) {
|
||||
self.auth.on_authorized(&mut req, output);
|
||||
ResponseFuture::future(self.inner.call(req))
|
||||
} else {
|
||||
let res = self.auth.unauthorized_response(&req);
|
||||
ResponseFuture::invalid_auth(res)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Response future for [`RequireAuthorization`].
|
||||
#[pin_project]
|
||||
pub struct ResponseFuture<F, B> {
|
||||
#[pin]
|
||||
kind: Kind<F, B>,
|
||||
}
|
||||
|
||||
impl<F, B> ResponseFuture<F, B> {
|
||||
fn future(future: F) -> Self {
|
||||
Self {
|
||||
kind: Kind::Future(future),
|
||||
}
|
||||
}
|
||||
|
||||
fn invalid_auth(res: Response<B>) -> Self {
|
||||
Self {
|
||||
kind: Kind::Error(Some(res)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[pin_project(project = KindProj)]
|
||||
enum Kind<F, B> {
|
||||
Future(#[pin] F),
|
||||
Error(Option<Response<B>>),
|
||||
}
|
||||
|
||||
impl<F, B, E> Future for ResponseFuture<F, B>
|
||||
where
|
||||
F: Future<Output = Result<Response<B>, E>>,
|
||||
{
|
||||
type Output = F::Output;
|
||||
|
||||
fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Self::Output> {
|
||||
match self.project().kind.project() {
|
||||
KindProj::Future(future) => future.poll(cx),
|
||||
KindProj::Error(response) => {
|
||||
let response = response.take().unwrap();
|
||||
Poll::Ready(Ok(response))
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for authorizing requests.
|
||||
pub trait AuthorizeRequest {
|
||||
/// The output type of doing the authorization.
|
||||
///
|
||||
/// Use `()` if authorization doesn't produce any meaningful output.
|
||||
type Output;
|
||||
|
||||
/// The body type used for responses to unauthorized requests.
|
||||
type ResponseBody: Body;
|
||||
|
||||
/// Authorize the request.
|
||||
///
|
||||
/// If `Some(_)` is returned then the request is allowed through, otherwise not.
|
||||
fn authorize<B>(&mut self, request: &Request<B>) -> Option<Self::Output>;
|
||||
|
||||
/// Callback for when a request has been successfully authorized.
|
||||
///
|
||||
/// For example this allows you to save `Self::Output` in a [request extension][] to make it
|
||||
/// available to services further down the stack. This could for example be the "claims" for a
|
||||
/// valid [JWT].
|
||||
///
|
||||
/// Defaults to doing nothing.
|
||||
///
|
||||
/// See the [module docs](crate::auth::require_authorization) for an example.
|
||||
///
|
||||
/// [request extension]: https://docs.rs/http/latest/http/struct.Extensions.html
|
||||
/// [JWT]: https://jwt.io
|
||||
#[inline]
|
||||
fn on_authorized<B>(&mut self, _request: &mut Request<B>, _output: Self::Output) {}
|
||||
|
||||
/// Create the response for an unauthorized request.
|
||||
fn unauthorized_response<B>(&mut self, request: &Request<B>) -> Response<Self::ResponseBody>;
|
||||
}
|
||||
|
||||
/// Type that performs "bearer token" authorization.
|
||||
///
|
||||
/// See [`RequireAuthorization::bearer`] for more details.
|
||||
pub struct Bearer<ResBody> {
|
||||
header_value: HeaderValue,
|
||||
_ty: PhantomData<fn() -> ResBody>,
|
||||
}
|
||||
|
||||
impl<ResBody> Bearer<ResBody> {
|
||||
fn new(token: &str) -> Self
|
||||
where
|
||||
ResBody: Body + Default,
|
||||
{
|
||||
Self {
|
||||
header_value: format!("Bearer {}", token)
|
||||
.parse()
|
||||
.expect("token is not a valid header value"),
|
||||
_ty: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ResBody> Clone for Bearer<ResBody> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
header_value: self.header_value.clone(),
|
||||
_ty: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ResBody> fmt::Debug for Bearer<ResBody> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Bearer")
|
||||
.field("header_value", &self.header_value)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<ResBody> AuthorizeRequest for Bearer<ResBody>
|
||||
where
|
||||
ResBody: Body + Default,
|
||||
{
|
||||
type Output = ();
|
||||
type ResponseBody = ResBody;
|
||||
|
||||
fn authorize<B>(&mut self, request: &Request<B>) -> Option<Self::Output> {
|
||||
if let Some(actual) = request.headers().get(header::AUTHORIZATION) {
|
||||
(actual == self.header_value).then(|| ())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn unauthorized_response<B>(&mut self, _request: &Request<B>) -> Response<Self::ResponseBody> {
|
||||
let body = ResBody::default();
|
||||
let mut res = Response::new(body);
|
||||
*res.status_mut() = StatusCode::UNAUTHORIZED;
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
/// Type that performs basic authorization.
|
||||
///
|
||||
/// See [`RequireAuthorization::basic`] for more details.
|
||||
pub struct Basic<ResBody> {
|
||||
header_value: HeaderValue,
|
||||
_ty: PhantomData<fn() -> ResBody>,
|
||||
}
|
||||
|
||||
impl<ResBody> Basic<ResBody> {
|
||||
fn new(username: &str, password: &str) -> Self
|
||||
where
|
||||
ResBody: Body + Default,
|
||||
{
|
||||
let encoded = base64::encode(format!("{}:{}", username, password));
|
||||
let header_value = format!("Basic {}", encoded).parse().unwrap();
|
||||
Self {
|
||||
header_value,
|
||||
_ty: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ResBody> Clone for Basic<ResBody> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
header_value: self.header_value.clone(),
|
||||
_ty: PhantomData,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<ResBody> fmt::Debug for Basic<ResBody> {
|
||||
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
|
||||
f.debug_struct("Basic")
|
||||
.field("header_value", &self.header_value)
|
||||
.finish()
|
||||
}
|
||||
}
|
||||
|
||||
impl<ResBody> AuthorizeRequest for Basic<ResBody>
|
||||
where
|
||||
ResBody: Body + Default,
|
||||
{
|
||||
type Output = ();
|
||||
type ResponseBody = ResBody;
|
||||
|
||||
fn authorize<B>(&mut self, request: &Request<B>) -> Option<Self::Output> {
|
||||
if let Some(actual) = request.headers().get(header::AUTHORIZATION) {
|
||||
(actual == self.header_value).then(|| ())
|
||||
} else {
|
||||
None
|
||||
}
|
||||
}
|
||||
|
||||
fn unauthorized_response<B>(&mut self, _request: &Request<B>) -> Response<Self::ResponseBody> {
|
||||
let body = ResBody::default();
|
||||
let mut res = Response::new(body);
|
||||
*res.status_mut() = StatusCode::UNAUTHORIZED;
|
||||
res.headers_mut()
|
||||
.insert(header::WWW_AUTHENTICATE, "Basic".parse().unwrap());
|
||||
res
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
#[allow(unused_imports)]
|
||||
use super::*;
|
||||
use http::header;
|
||||
use hyper::Body;
|
||||
use tower::{BoxError, ServiceBuilder, ServiceExt};
|
||||
|
||||
#[tokio::test]
|
||||
async fn valid_basic_token() {
|
||||
let mut service = ServiceBuilder::new()
|
||||
.layer(RequireAuthorizationLayer::basic("foo", "bar"))
|
||||
.service_fn(echo);
|
||||
|
||||
let request = Request::get("/")
|
||||
.header(
|
||||
header::AUTHORIZATION,
|
||||
format!("Basic {}", base64::encode("foo:bar")),
|
||||
)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let res = service.ready().await.unwrap().call(request).await.unwrap();
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_basic_token() {
|
||||
let mut service = ServiceBuilder::new()
|
||||
.layer(RequireAuthorizationLayer::basic("foo", "bar"))
|
||||
.service_fn(echo);
|
||||
|
||||
let request = Request::get("/")
|
||||
.header(
|
||||
header::AUTHORIZATION,
|
||||
format!("Basic {}", base64::encode("wrong:credentials")),
|
||||
)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let res = service.ready().await.unwrap().call(request).await.unwrap();
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
|
||||
let www_authenticate = res.headers().get(header::WWW_AUTHENTICATE).unwrap();
|
||||
assert_eq!(www_authenticate, "Basic");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn valid_bearer_token() {
|
||||
let mut service = ServiceBuilder::new()
|
||||
.layer(RequireAuthorizationLayer::bearer("foobar"))
|
||||
.service_fn(echo);
|
||||
|
||||
let request = Request::get("/")
|
||||
.header(header::AUTHORIZATION, "Bearer foobar")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let res = service.ready().await.unwrap().call(request).await.unwrap();
|
||||
|
||||
assert_eq!(res.status(), StatusCode::OK);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_auth_is_case_sensitive_in_prefix() {
|
||||
let mut service = ServiceBuilder::new()
|
||||
.layer(RequireAuthorizationLayer::basic("foo", "bar"))
|
||||
.service_fn(echo);
|
||||
|
||||
let request = Request::get("/")
|
||||
.header(
|
||||
header::AUTHORIZATION,
|
||||
format!("basic {}", base64::encode("foo:bar")),
|
||||
)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let res = service.ready().await.unwrap().call(request).await.unwrap();
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn basic_auth_is_case_sensitive_in_value() {
|
||||
let mut service = ServiceBuilder::new()
|
||||
.layer(RequireAuthorizationLayer::basic("foo", "bar"))
|
||||
.service_fn(echo);
|
||||
|
||||
let request = Request::get("/")
|
||||
.header(
|
||||
header::AUTHORIZATION,
|
||||
format!("Basic {}", base64::encode("Foo:bar")),
|
||||
)
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let res = service.ready().await.unwrap().call(request).await.unwrap();
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn invalid_bearer_token() {
|
||||
let mut service = ServiceBuilder::new()
|
||||
.layer(RequireAuthorizationLayer::bearer("foobar"))
|
||||
.service_fn(echo);
|
||||
|
||||
let request = Request::get("/")
|
||||
.header(header::AUTHORIZATION, "Bearer wat")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let res = service.ready().await.unwrap().call(request).await.unwrap();
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bearer_token_is_case_sensitive_in_prefix() {
|
||||
let mut service = ServiceBuilder::new()
|
||||
.layer(RequireAuthorizationLayer::bearer("foobar"))
|
||||
.service_fn(echo);
|
||||
|
||||
let request = Request::get("/")
|
||||
.header(header::AUTHORIZATION, "bearer foobar")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let res = service.ready().await.unwrap().call(request).await.unwrap();
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn bearer_token_is_case_sensitive_in_token() {
|
||||
let mut service = ServiceBuilder::new()
|
||||
.layer(RequireAuthorizationLayer::bearer("foobar"))
|
||||
.service_fn(echo);
|
||||
|
||||
let request = Request::get("/")
|
||||
.header(header::AUTHORIZATION, "Bearer Foobar")
|
||||
.body(Body::empty())
|
||||
.unwrap();
|
||||
|
||||
let res = service.ready().await.unwrap().call(request).await.unwrap();
|
||||
|
||||
assert_eq!(res.status(), StatusCode::UNAUTHORIZED);
|
||||
}
|
||||
|
||||
async fn echo(req: Request<Body>) -> Result<Response<Body>, BoxError> {
|
||||
Ok(Response::new(req.into_body()))
|
||||
}
|
||||
}
|
||||
@ -1,4 +1,4 @@
|
||||
//! Types used by compression and decompression middlewares.
|
||||
//! Types used by compression and decompression middleware.
|
||||
|
||||
use bytes::{Bytes, BytesMut};
|
||||
use futures_core::Stream;
|
||||
|
||||
@ -7,7 +7,7 @@
|
||||
//!
|
||||
//! The middleware tries to clone the original [`Request`] when making a redirected request.
|
||||
//! However, since [`Extensions`][http::Extensions] are `!Clone`, any extensions set by outer
|
||||
//! middlewares will be discarded. Also, the request body cannot always be cloned. When the
|
||||
//! middleware will be discarded. Also, the request body cannot always be cloned. When the
|
||||
//! original body is known to be empty by [`Body::size_hint`], the middleware uses `Default`
|
||||
//! implementation of the body type to create a new request body. If you know that the body can be
|
||||
//! cloned in some way, you can tell the middleware to clone it by configuring a [`policy`].
|
||||
|
||||
@ -2,16 +2,16 @@
|
||||
//!
|
||||
//! # Overview
|
||||
//!
|
||||
//! `tower-http` is a library that provides HTTP-specific middlewares and utilities built on top of
|
||||
//! `tower-http` is a library that provides HTTP-specific middleware and utilities built on top of
|
||||
//! [`tower`].
|
||||
//!
|
||||
//! All middlewares uses the [`http`] and [`http-body`] crates as the HTTP abstractions. That means
|
||||
//! All middleware uses the [`http`] and [`http-body`] crates as the HTTP abstractions. That means
|
||||
//! they're compatible with any library or framework that also uses those crates, such as
|
||||
//! [`hyper`].
|
||||
//!
|
||||
//! # Example server
|
||||
//!
|
||||
//! This example shows how to apply middlewares from `tower-http` to a [`Service`] and then run
|
||||
//! This example shows how to apply middleware from `tower-http` to a [`Service`] and then run
|
||||
//! that service using [`hyper`].
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
@ -19,6 +19,7 @@
|
||||
//! add_extension::AddExtensionLayer,
|
||||
//! compression::CompressionLayer,
|
||||
//! propagate_header::PropagateHeaderLayer,
|
||||
//! auth::RequireAuthorizationLayer,
|
||||
//! sensitive_header::SetSensitiveRequestHeaderLayer,
|
||||
//! set_header::SetResponseHeaderLayer,
|
||||
//! trace::TraceLayer,
|
||||
@ -67,6 +68,8 @@
|
||||
//! .layer(PropagateHeaderLayer::new(HeaderName::from_static("x-request-id")))
|
||||
//! // If the response has a known size set the `Content-Length` header
|
||||
//! .layer(SetResponseHeaderLayer::overriding(CONTENT_TYPE, content_length_from_response))
|
||||
//! // Authorize requests using a token
|
||||
//! .layer(RequireAuthorizationLayer::bearer("passwordlol"))
|
||||
//! // Wrap a `Service` in our middleware stack
|
||||
//! .service_fn(handler);
|
||||
//!
|
||||
@ -84,7 +87,7 @@
|
||||
//!
|
||||
//! # Example client
|
||||
//!
|
||||
//! `tower-http` middlewares can also be applied to HTTP clients:
|
||||
//! `tower-http` middleware can also be applied to HTTP clients:
|
||||
//!
|
||||
//! ```rust,no_run
|
||||
//! use tower_http::{
|
||||
@ -157,6 +160,7 @@
|
||||
//! [`Service`]: https://docs.rs/tower/latest/tower/trait.Service.html
|
||||
//! [chat]: https://discord.gg/tokio
|
||||
//! [issue]: https://github.com/tower-rs/tower-http/issues/new
|
||||
//! [`Trace`]: crate::trace::Trace
|
||||
|
||||
#![doc(html_root_url = "https://docs.rs/tower-http/0.1.0")]
|
||||
#![warn(
|
||||
@ -207,6 +211,10 @@
|
||||
#[macro_use]
|
||||
pub(crate) mod macros;
|
||||
|
||||
#[cfg(feature = "auth")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "auth")))]
|
||||
pub mod auth;
|
||||
|
||||
#[cfg(feature = "set-header")]
|
||||
#[cfg_attr(docsrs, doc(cfg(feature = "set-header")))]
|
||||
pub mod set_header;
|
||||
@ -299,7 +307,7 @@ where
|
||||
}
|
||||
}
|
||||
|
||||
/// The latency unit used to report latencies by middlewares.
|
||||
/// The latency unit used to report latencies by middleware.
|
||||
#[non_exhaustive]
|
||||
#[derive(Copy, Clone, Debug)]
|
||||
pub enum LatencyUnit {
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
//! Middlewares that mark headers as [sensitive].
|
||||
//! Middleware that mark headers as [sensitive].
|
||||
//!
|
||||
//! [sensitive]: https://docs.rs/http/latest/http/header/struct.HeaderValue.html#method.set_sensitive
|
||||
//!
|
||||
|
||||
@ -1,4 +1,4 @@
|
||||
//! Middlewares for setting headers on requests and responses.
|
||||
//! Middleware for setting headers on requests and responses.
|
||||
//!
|
||||
//! See [request] and [response] for more details.
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user