cataclysm 0.5.5

A simple http framework
Documentation
use openssl::{
    pkey::{PKey, Private},
    sign::Signer,
    hash::MessageDigest,
    memcmp
};

use crate::{
    Error,
    http::{Request, Response},
    session::{SessionCreator}
};
use super::SameSite;
use std::time::Duration;
use cookie::Cookie;
use chrono::{DateTime, Utc};
use base64::{Engine, engine::general_purpose};

/// Session implementation, based on signed cookies.
///
/// This structure needs to be created through [CookieSessionBuilder](super::CookieSessionBuilder).
#[derive(Clone)]
pub struct SignedCookieSession {
    pub(crate) raw_key: Vec<u8>,
    pub(crate) key: PKey<Private>,
    pub(crate) cookie_name: String,
    pub(crate) path: Option<String>,
    pub(crate) domain: Option<String>,
    pub(crate) expires: Option<DateTime<Utc>>,
    pub(crate) max_age: Option<Duration>,
    pub(crate) secure: Option<bool>,
    pub(crate) http_only: Option<bool>,
    pub(crate) same_site: Option<SameSite>,
    pub(crate) force_failure: bool
}

impl SignedCookieSession {
    /// Hands in the inner signature key.
    ///
    /// Should only be used for debugging purposes. Printing or logging the encryption key could compromise the secrecy that ensures safety.
    pub fn get_signature_key(&self) -> &Vec<u8> {
        &self.raw_key
    }

    /// Helper function to extract a session from a cookie
    fn build_from_req(&self, req: &Request) -> Result<Option<String>, Error> {
        // we can have multiple cookies, so we try for each one
        let empty = Vec::new();
        let cookie_headers = vec![
            req.header.headers.get("Cookie").unwrap_or_else(|| &empty),
            req.header.headers.get("cookie").unwrap_or_else(|| &empty)
        ].into_iter().flatten();
        for cookie_header in cookie_headers {
            for single_cookie in cookie_header.split("; ") {
                let cookie = Cookie::parse_encoded(single_cookie).map_err(|e| Error::custom(format!("{}", e)))?;
                if cookie.name() == self.cookie_name {
                    let value = cookie.value();
                    // The hmac value is at least 44 bytes
                    if value.len() < 44 {
                        return Err(Error::custom("length of cookie cannot contain even the hmac value"));
                    } else {
                        // I know these unwraps look unsafe, but trust me, they are, TODO FIX with let Some
                        let signature = value.get(0..44).unwrap();
                        let content = value.get(44..value.len()).unwrap();
                        // Decodificamos la firma
                        let decoded_signature = general_purpose::STANDARD.decode(signature).map_err(|e| Error::custom(format!("{}", e)))?;
                        let mut signer = Signer::new(MessageDigest::sha256(), &self.key)?;
                        let real_signature = signer.sign_oneshot_to_vec(content.as_bytes())?;
                        if memcmp::eq(&real_signature, &decoded_signature) {
                            return Ok(Some(content.to_string()))
                        }
                    }
                }
            }
        }
        Ok(None)
    }
}

impl SessionCreator for SignedCookieSession {
    fn parse(&self, req: &Request) -> Result<Option<String>, Error> {
        match self.build_from_req(req) {
            Ok(Some(content)) => Ok(Some(content)),
            Ok(None) => {
                #[cfg(feature = "full_log")]
                log::debug!("cookie not found among request headers");
                Ok(None)
            },
            Err(e) => {
                if self.force_failure {
                    Err(e)
                } else {
                    #[cfg(feature = "full_log")]
                    log::debug!("error while creating session: {}", e);
                    return Ok(None)
                }
            }
        }
    }

    fn apply(&self, content: String, mut res: Response) -> Result<Response, Error> {
        let mut signer = Signer::new(MessageDigest::sha256(), &self.key)?;
        let signature = general_purpose::STANDARD.encode(signer.sign_oneshot_to_vec(content.as_bytes())?);

        let cookie_builder = Cookie::build((&self.cookie_name, format!("{}{}", signature, content)));

        let cookie_builder = if let Some(path) = &self.path {
            cookie_builder.path(path)
        } else {
            cookie_builder
        };

        let cookie_builder = if let Some(domain) = &self.domain {
            cookie_builder.domain(domain)
        } else {
            cookie_builder
        };

        let cookie_builder = if let Some(expires) = &self.expires {
            match cookie::time::OffsetDateTime::from_unix_timestamp(expires.timestamp()) {
                Ok(v) => cookie_builder.expires(v),
                Err(e) => {
                    return Err(Error::Session(format!("failed to set expiration to cookie, {}", e)));
                }
            }
        } else {
            cookie_builder
        };

        let cookie_builder = if let Some(max_age) = &self.max_age {
            let max_age = match max_age.clone().try_into() {
                Ok(v) => v,
                Err(e) => {
                    return Err(Error::Session(format!("failed to set max-age to cookie, {}", e)));
                }
            };
            cookie_builder.max_age(max_age)
        } else {
            cookie_builder
        };

        let cookie_builder = if let Some(secure) = &self.secure {
            cookie_builder.secure(*secure)
        } else {
            cookie_builder
        };

        let cookie_builder = if let Some(http_only) = &self.http_only {
            cookie_builder.http_only(*http_only)
        } else {
            cookie_builder
        };

        let cookie_builder = if let Some(same_site) = &self.same_site {
            cookie_builder.same_site(same_site.to_cookie_same_site())
        } else {
            cookie_builder
        };

        let cookie = cookie_builder.build();

        res = res.header("Set-Cookie", format!("{}", cookie.encoded()));
        Ok(res)
    }
}