digest-headers 0.2.1

A simple library to hash a request's body in the headers
/* This file is part of Digest Headers
 *
 * Digest Headers is free software: you can redistribute it and/or modify
 * it under the terms of the GNU General Public License as published by
 * the Free Software Foundation, either version 3 of the License, or
 * (at your option) any later version.
 *
 * Digest Headers is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 *
 * You should have received a copy of the GNU General Public License
 * along with Digest Headers  If not, see <http://www.gnu.org/licenses/>.
 */

//! The `use_hyper` module provides useful Types and Traits for interacting with `Hyper` with
//! `Digest`s.

use actix_web::{
    client::{ClientRequest, ClientRequestBuilder, ClientResponse},
    dev::HttpResponseBuilder,
    http::header::HeaderValue,
    Binary, Error, FromRequest, HttpMessage, HttpRequest, HttpResponse,
};
use bytes::Bytes;
use failure::Fail;
use futures::Future;

use crate::{Digest, ShaSize};

#[derive(Debug, Fail)]
#[fail(display = "Failed to verify digest")]
pub struct VerifyError;

pub struct VerifiedDigest;

impl<T> FromRequest<T> for VerifiedDigest
where
    T: 'static,
{
    type Config = ();
    type Result = Box<dyn Future<Item = VerifiedDigest, Error = Error> + 'static>;

    fn from_request(req: &HttpRequest<T>, _: &Self::Config) -> Self::Result {
        let maybe_digest = req.headers().get("Digest").cloned();

        let fut = req
            .body()
            .from_err()
            .and_then(|bytes| verify(bytes, maybe_digest));

        Box::new(fut)
    }
}

pub trait VerifyDigest {
    fn verify_digest(&self) -> Box<dyn Future<Item = VerifiedDigest, Error = Error>>;
}

impl VerifyDigest for ClientResponse {
    fn verify_digest(&self) -> Box<dyn Future<Item = VerifiedDigest, Error = Error>> {
        let maybe_digest = self.headers().get("Digest").cloned();

        let fut = self
            .body()
            .from_err()
            .and_then(|bytes| verify(bytes, maybe_digest));

        Box::new(fut)
    }
}

pub trait WithDigest<T> {
    fn with_digest<B: Into<Binary>>(&mut self, body: B, sha_size: ShaSize) -> T;
}

impl WithDigest<HttpResponse> for HttpResponseBuilder {
    fn with_digest<B: Into<Binary>>(&mut self, body: B, sha_size: ShaSize) -> HttpResponse {
        let mut body: Binary = body.into();

        let bytes = body.take();
        let digest = Digest::new(&bytes, sha_size);

        self.header("Digest", digest.as_string())
            .content_length(bytes.len() as u64)
            .body(bytes)
    }
}

impl WithDigest<Result<ClientRequest, Error>> for ClientRequestBuilder {
    fn with_digest<B: Into<Binary>>(
        &mut self,
        body: B,
        sha_size: ShaSize,
    ) -> Result<ClientRequest, Error> {
        let mut body: Binary = body.into();

        let bytes = body.take();
        let digest = Digest::new(&bytes, sha_size);

        self.header("Digest", digest.as_string())
            .content_length(bytes.len() as u64)
            .body(bytes)
    }
}

fn verify(bytes: Bytes, maybe_digest: Option<HeaderValue>) -> Result<VerifiedDigest, Error> {
    if let Some(digest) = maybe_digest {
        digest
            .to_str()
            .map_err(failure::Error::from)
            .map_err(Error::from)
            .and_then(|s| {
                s.parse()
                    .map_err(failure::Error::from)
                    .map_err(Error::from)
                    .and_then(|parsed: Digest| {
                        if parsed.verify(&bytes).is_ok() {
                            Ok(VerifiedDigest)
                        } else {
                            Err(failure::Error::from(VerifyError).into())
                        }
                    })
            })
    } else {
        Err(failure::Error::from(VerifyError).into())
    }
}

#[cfg(test)]
mod tests {
    use actix_web::{client::post, Binary, Body, HttpResponse};

    use crate::{prelude::*, Digest, ShaSize};

    #[test]
    fn add_digest_to_request_256() {
        add_digest_to_request(ShaSize::TwoFiftySix);
    }

    #[test]
    fn add_digest_to_request_384() {
        add_digest_to_request(ShaSize::ThreeEightyFour);
    }

    #[test]
    fn add_digest_to_request_512() {
        add_digest_to_request(ShaSize::FiveTwelve);
    }

    #[test]
    fn add_digest_to_response_256() {
        add_digest_to_response(ShaSize::TwoFiftySix);
    }

    #[test]
    fn add_digest_to_response_384() {
        add_digest_to_response(ShaSize::ThreeEightyFour);
    }

    #[test]
    fn add_digest_to_response_512() {
        add_digest_to_response(ShaSize::FiveTwelve);
    }

    fn add_digest_to_response(sha_size: ShaSize) {
        let json = r#"{"Library":"Hyper"}"#;

        let mut res = HttpResponse::Ok()
            .content_type("application/json")
            .with_digest(json, sha_size);

        let digest_header = res.headers_mut().remove("Digest");

        assert!(digest_header.is_some());

        let digest_header: Digest = digest_header.unwrap().to_str().unwrap().parse().unwrap();

        match res.replace_body("") {
            Body::Binary(mut body) => {
                assert!(digest_header.verify(&body.take()).is_ok());
            }
            _ => {
                panic!("Body isn't binary");
            }
        }
    }

    fn add_digest_to_request(sha_size: ShaSize) {
        let uri = "http://example.com";
        let json = r#"{"Library":"Hyper"}"#;

        let mut req = post(uri)
            .content_type("application/json")
            .with_digest(json, sha_size)
            .unwrap();

        let digest_header = req.headers_mut().remove("Digest");

        assert!(digest_header.is_some());

        let digest_header: Digest = digest_header.unwrap().to_str().unwrap().parse().unwrap();

        let is_ok = match *req.body() {
            Body::Binary(ref body) => match body {
                Binary::Bytes(ref bytes) => digest_header.verify(bytes).is_ok(),
                Binary::Slice(slice) => digest_header.verify(slice).is_ok(),
                Binary::SharedVec(vec) => digest_header.verify(&vec).is_ok(),
                Binary::SharedString(string) => digest_header.verify(string.as_bytes()).is_ok(),
            },
            _ => false,
        };

        assert!(is_ok);
    }
}