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};
#[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 {
pub fn get_signature_key(&self) -> &Vec<u8> {
&self.raw_key
}
fn build_from_req(&self, req: &Request) -> Result<Option<String>, Error> {
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();
if value.len() < 44 {
return Err(Error::custom("length of cookie cannot contain even the hmac value"));
} else {
let signature = value.get(0..44).unwrap();
let content = value.get(44..value.len()).unwrap();
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)
}
}