use crate::storage::interface::{FlashMessageStore, LoadError, StoreError};
use crate::FlashMessage;
use actix_web::cookie::{Cookie, SameSite};
use actix_web::cookie::{CookieJar, Key};
use actix_web::dev::ResponseHead;
use actix_web::http::header;
use actix_web::http::header::HeaderValue;
use actix_web::HttpRequest;
use anyhow::Context;
use percent_encoding::{percent_encode, AsciiSet};
pub struct CookieMessageStore {
cookie_name: String,
signing_key: Key,
bytes_size_limit: u32,
same_site: SameSite,
path: String,
domain: Option<String>,
}
pub struct CookieMessageStoreBuilder {
cookie_name: Option<String>,
signing_key: Key,
bytes_size_limit: Option<u32>,
same_site: Option<SameSite>,
path: Option<String>,
domain: Option<String>,
}
impl CookieMessageStore {
pub fn builder(signing_key: Key) -> CookieMessageStoreBuilder {
CookieMessageStoreBuilder {
cookie_name: None,
signing_key,
bytes_size_limit: None,
same_site: None,
path: None,
domain: None,
}
}
fn encode(&self, messages: &[FlashMessage]) -> Result<Cookie<'_>, StoreError> {
let serialised = serde_json::to_string(messages)
.context("Failed to serialise flash messages to JSON.")
.map_err(StoreError::SerializationError)?;
let mut cookie_jar = CookieJar::new();
cookie_jar.signed_mut(&self.signing_key).add(
Cookie::build(self.cookie_name.to_owned(), serialised)
.same_site(self.same_site)
.finish(),
);
let signed_cookie = cookie_jar.get(&self.cookie_name).unwrap();
let encoded_value =
percent_encode(signed_cookie.value().as_bytes(), USERINFO_ENCODE_SET).to_string();
if encoded_value.len() > self.bytes_size_limit as usize {
Err(StoreError::SizeLimitExceeded(anyhow::anyhow!(
"The configured maximum cookie size, in bytes, is {}. The serialised and signed outgoing flash messages are {} bytes long.",
self.bytes_size_limit,
encoded_value.len()
)))
} else {
let mut signed_cookie = Cookie::build(&self.cookie_name, encoded_value)
.secure(true)
.http_only(true)
.same_site(self.same_site)
.path(&self.path)
.finish();
if let Some(domain) = &self.domain {
signed_cookie.set_domain(domain);
}
Ok(signed_cookie)
}
}
fn decode(&self, cookie: Cookie<'static>) -> Result<Vec<FlashMessage>, LoadError> {
let mut cookie_jar = CookieJar::new();
cookie_jar.add_original(cookie);
if let Some(cookie) = cookie_jar.signed(&self.signing_key).get(&self.cookie_name) {
let messages = serde_json::from_str(cookie.value()).context(
"Failed to deserialise the URL-decoded flash messages according to the JSON format",
).map_err(LoadError::DeserializationError)?;
Ok(messages)
} else {
Err(LoadError::IntegrityCheckFailed(anyhow::anyhow!(
"Signature validation failed for the cookie storing incoming flash messages"
)))
}
}
}
impl CookieMessageStoreBuilder {
pub fn cookie_name(mut self, name: String) -> Self {
self.cookie_name = Some(name);
self
}
pub fn bytes_size_limit(mut self, bytes_size_limit: u32) -> Self {
self.bytes_size_limit = Some(bytes_size_limit);
self
}
pub fn path(mut self, path: String) -> Self {
self.path = Some(path);
self
}
pub fn domain(mut self, domain: String) -> Self {
self.domain = Some(domain);
self
}
pub fn same_site(mut self, same_site: SameSite) -> Self {
self.same_site = Some(same_site);
self
}
pub fn build(self) -> CookieMessageStore {
CookieMessageStore {
cookie_name: self.cookie_name.unwrap_or_else(|| "_flash".to_string()),
signing_key: self.signing_key,
bytes_size_limit: self.bytes_size_limit.unwrap_or(2048),
same_site: self.same_site.unwrap_or(SameSite::Lax),
path: self.path.unwrap_or_else(|| "/".to_string()),
domain: self.domain,
}
}
}
impl FlashMessageStore for CookieMessageStore {
fn load(&self, request: &HttpRequest) -> Result<Vec<FlashMessage>, LoadError> {
if let Some(cookie) = request.cookie(&self.cookie_name) {
Ok(self.decode(cookie)?)
} else {
Ok(vec![])
}
}
fn store(
&self,
messages: &[FlashMessage],
_request: HttpRequest,
response_head: &mut ResponseHead,
) -> Result<(), StoreError> {
if !messages.is_empty() {
let cookie = self.encode(messages)?;
response_head
.add_cookie(&cookie)
.context("Failed to add the flash message cookie to the response")
.map_err(StoreError::GenericError)?;
} else {
let removal_cookie = Cookie::build(self.cookie_name.clone(), "")
.same_site(self.same_site)
.max_age(time::Duration::seconds(0))
.path("/")
.finish();
response_head
.add_cookie(&removal_cookie)
.context("Failed to add 'removal cookie' for flash message storage to the response")
.map_err(StoreError::GenericError)?;
}
Ok(())
}
}
const FRAGMENT_ENCODE_SET: &AsciiSet = &percent_encoding::CONTROLS
.add(b' ')
.add(b'"')
.add(b'<')
.add(b'>')
.add(b'`');
const PATH_ENCODE_SET: &AsciiSet = &FRAGMENT_ENCODE_SET.add(b'#').add(b'?').add(b'{').add(b'}');
const USERINFO_ENCODE_SET: &AsciiSet = &PATH_ENCODE_SET
.add(b'/')
.add(b':')
.add(b';')
.add(b'=')
.add(b'@')
.add(b'[')
.add(b'\\')
.add(b']')
.add(b'^')
.add(b'|')
.add(b'%');
trait ResponseHeadExt {
fn add_cookie(&mut self, cookie: &Cookie) -> Result<(), anyhow::Error>;
}
impl ResponseHeadExt for ResponseHead {
fn add_cookie(&mut self, cookie: &Cookie) -> Result<(), anyhow::Error> {
HeaderValue::from_str(&cookie.to_string())
.map(|c| {
self.headers_mut().append(header::SET_COOKIE, c);
})
.map_err(|e| e.into())
}
}