use std::borrow::Cow;
use std::fmt;
use std::sync::{Mutex, MutexGuard};
use cookie_store::CookieStore;
use http::Uri;
use crate::http;
use crate::util::UriExt;
use crate::Error;
#[cfg(feature = "json")]
use std::io;
#[derive(Debug)]
pub(crate) struct SharedCookieJar {
inner: Mutex<CookieStore>,
}
pub struct CookieJar<'a>(MutexGuard<'a, CookieStore>);
pub struct Cookie<'a>(CookieInner<'a>);
#[allow(clippy::large_enum_variant)]
enum CookieInner<'a> {
Borrowed(&'a cookie_store::Cookie<'a>),
Owned(cookie_store::Cookie<'a>),
}
impl<'a> CookieInner<'a> {
fn into_static(self) -> cookie_store::Cookie<'static> {
match self {
CookieInner::Borrowed(v) => v.clone().into_owned(),
CookieInner::Owned(v) => v.into_owned(),
}
}
}
impl<'a> Cookie<'a> {
pub fn parse<S>(cookie_str: S, uri: &Uri) -> Result<Cookie<'a>, Error>
where
S: Into<Cow<'a, str>>,
{
let cookie = cookie_store::Cookie::parse(cookie_str, &uri.try_into_url()?)?;
Ok(Cookie(CookieInner::Owned(cookie)))
}
pub fn name(&self) -> &str {
match &self.0 {
CookieInner::Borrowed(v) => v.name(),
CookieInner::Owned(v) => v.name(),
}
}
pub fn value(&self) -> &str {
match &self.0 {
CookieInner::Borrowed(v) => v.value(),
CookieInner::Owned(v) => v.value(),
}
}
#[cfg(test)]
fn as_cookie_store(&self) -> &cookie_store::Cookie<'a> {
match &self.0 {
CookieInner::Borrowed(v) => v,
CookieInner::Owned(v) => v,
}
}
}
impl Cookie<'static> {
fn into_owned(self) -> cookie_store::Cookie<'static> {
match self.0 {
CookieInner::Owned(v) => v,
_ => unreachable!(),
}
}
}
impl<'a> CookieJar<'a> {
pub fn get(&self, domain: &str, path: &str, name: &str) -> Option<Cookie<'_>> {
self.0
.get(domain, path, name)
.map(|c| Cookie(CookieInner::Borrowed(c)))
}
pub fn remove(&mut self, domain: &str, path: &str, name: &str) -> Option<Cookie<'static>> {
self.0
.remove(domain, path, name)
.map(|c| Cookie(CookieInner::Owned(c)))
}
pub fn insert(&mut self, cookie: Cookie<'static>, uri: &Uri) -> Result<(), Error> {
let url = uri.try_into_url()?;
self.0.insert(cookie.into_owned(), &url)?;
Ok(())
}
pub fn clear(&mut self) {
self.0.clear()
}
pub fn iter(&self) -> impl Iterator<Item = Cookie<'_>> {
self.0
.iter_unexpired()
.map(|c| Cookie(CookieInner::Borrowed(c)))
}
#[cfg(feature = "json")]
pub fn save_json<W: io::Write>(&self, writer: &mut W) -> Result<(), Error> {
Ok(cookie_store::serde::json::save(&self.0, writer)?)
}
#[cfg(feature = "json")]
pub fn load_json<R: io::BufRead>(&mut self, reader: R) -> Result<(), Error> {
let store = cookie_store::serde::json::load(reader)?;
*self.0 = store;
Ok(())
}
pub(crate) fn store_response_cookies<'b>(
&mut self,
iter: impl Iterator<Item = Cookie<'b>>,
uri: &Uri,
) {
let url = uri.try_into_url().expect("uri to be a url");
let raw_cookies = iter.map(|c| c.0.into_static().into());
self.0.store_response_cookies(raw_cookies, &url);
}
pub fn release(self) {}
}
impl SharedCookieJar {
pub(crate) fn new() -> Self {
SharedCookieJar {
inner: Mutex::new(CookieStore::new()),
}
}
pub(crate) fn lock(&self) -> CookieJar<'_> {
let lock = self.inner.lock().unwrap();
CookieJar(lock)
}
pub(crate) fn get_request_cookies(&self, uri: &Uri) -> String {
let mut cookies = String::new();
let url = match uri.try_into_url() {
Ok(v) => v,
Err(e) => {
debug!("Bad url for cookie: {:?}", e);
return cookies;
}
};
let store = self.inner.lock().unwrap();
for c in store.matches(&url) {
if !is_cookie_rfc_compliant(c) {
debug!("Do not send non compliant cookie: {:?}", c.name());
continue;
}
if !cookies.is_empty() {
cookies.push(';');
}
cookies.push_str(&c.to_string());
}
cookies
}
}
fn is_cookie_rfc_compliant(cookie: &cookie_store::Cookie) -> bool {
fn is_valid_name(b: &u8) -> bool {
is_tchar(b)
}
fn is_valid_value(b: &u8) -> bool {
b.is_ascii()
&& !b.is_ascii_control()
&& !b.is_ascii_whitespace()
&& *b != b'"'
&& *b != b','
&& *b != b';'
&& *b != b'\\'
}
let name = cookie.name().as_bytes();
let valid_name = name.iter().all(is_valid_name);
if !valid_name {
log::trace!("cookie name is not valid: {:?}", cookie.name());
return false;
}
let value = cookie.value().as_bytes();
let valid_value = value.iter().all(is_valid_value);
if !valid_value {
log::trace!("cookie value is not valid: {:?}", cookie.name());
return false;
}
true
}
#[inline]
pub(crate) fn is_tchar(b: &u8) -> bool {
match b {
b'!' | b'#' | b'$' | b'%' | b'&' => true,
b'\'' | b'*' | b'+' | b'-' | b'.' => true,
b'^' | b'_' | b'`' | b'|' | b'~' => true,
b if b.is_ascii_alphanumeric() => true,
_ => false,
}
}
impl fmt::Display for Cookie<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match &self.0 {
CookieInner::Borrowed(v) => v.fmt(f),
CookieInner::Owned(v) => v.fmt(f),
}
}
}
#[cfg(test)]
mod test {
use std::convert::TryFrom;
use super::*;
fn uri() -> Uri {
Uri::try_from("https://example.test").unwrap()
}
#[test]
fn illegal_cookie_name() {
let cookie = Cookie::parse("borked/=value", &uri()).unwrap();
assert!(!is_cookie_rfc_compliant(cookie.as_cookie_store()));
}
#[test]
fn illegal_cookie_value() {
let cookie = Cookie::parse("name=borked,", &uri()).unwrap();
assert!(!is_cookie_rfc_compliant(cookie.as_cookie_store()));
}
#[test]
fn legal_cookie_name_value() {
let cookie = Cookie::parse("name=value", &uri()).unwrap();
assert!(is_cookie_rfc_compliant(cookie.as_cookie_store()));
}
}