#![allow(dead_code)]
#[macro_use]
extern crate lazy_static;
use std::fmt::Display;
use chrono::Timelike;
use std::fmt;
use chrono::Utc;
use chrono::{DateTime, Datelike};
use std::str::FromStr;
use std::error::Error;
use std::time::{Duration, SystemTime};
use std::ops::Add;
use std::collections::HashMap;
use std::cmp::{PartialEq, Eq};
use std::hash::{Hash, Hasher};
mod rfc_1123;
pub use rfc_1123::parse_rfc_1123_date;
mod rfc_850;
pub use rfc_850::parse_rfc_850_date;
mod asct;
pub use asct::parse_asct_date;
pub(crate) const COOKIE: &str = "cookie";
pub(crate) const COOKIE_EXPIRES: &str = "expires";
pub(crate) const COOKIE_MAX_AGE: &str = "max-age";
pub(crate) const COOKIE_DOMAIN: &str = "domain";
pub(crate) const COOKIE_PATH: &str = "path";
pub(crate) const COOKIE_SAME_SITE: &str = "samesite";
pub(crate) const COOKIE_SAME_SITE_STRICT: &str = "strict";
pub(crate) const COOKIE_SAME_SITE_LAX: &str = "lax";
pub(crate) const COOKIE_SAME_SITE_NONE: &str = "none";
pub(crate) const COOKIE_SECURE: &str = "secure";
pub(crate) const COOKIE_HTTP_ONLY: &str = "httponly";
#[derive(Debug)]
pub struct ParseError {
details: String
}
impl ParseError {
fn new<S>(msg: S) -> ParseError
where S: Into<String> {
ParseError{details: msg.into()}
}
}
impl fmt::Display for ParseError {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
write!(f,"{}",self.details)
}
}
impl Error for ParseError {
fn description(&self) -> &str {
&self.details
}
}
#[derive(Debug, PartialEq,Eq)]
pub struct Cookie {
pub name: String,
pub value: String
}
impl Cookie {
pub fn new<S>(name:S, value: S) -> Cookie
where S: Into<String> {
Cookie {
name: name.into(),
value: value.into()
}
}
}
impl Display for Cookie {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{}={}", &self.name, &self.value)
}
}
impl Hash for Cookie {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
}
}
impl FromStr for Cookie {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let (key, value) = parse_cookie_value(s)?;
Ok(Cookie::new(key, value))
}
}
#[derive(Debug,Copy,Clone,PartialEq,Eq)]
pub enum SameSiteValue {Strict, Lax, None}
impl FromStr for SameSiteValue {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
return match s {
COOKIE_SAME_SITE_STRICT => Ok(SameSiteValue::Strict),
COOKIE_SAME_SITE_LAX => Ok(SameSiteValue::Lax),
COOKIE_SAME_SITE_NONE => Ok(SameSiteValue::None),
_ => Err(
ParseError::new(format!("Invalid SameSite cookie directive value: {}", s)))
}
}
}
#[derive(Debug,Clone)]
pub struct SetCookie {
pub name: String,
pub value: String,
pub domain: Option<String>,
pub path: Option<String>,
pub expires: Option<DateTime<Utc>>,
pub max_age: Option<Duration>,
pub(crate) created: SystemTime,
pub same_site: SameSiteValue,
pub secure: bool,
pub http_only: bool,
pub extensions: HashMap<String, Option<String>>
}
impl SetCookie {
pub fn new<S>(name: S, value: S) -> SetCookie
where S : Into<String> {
SetCookie {
name: name.into(),
value: value.into(),
domain: None,
path: None,
expires: None,
max_age: None,
created: SystemTime::now(),
same_site: SameSiteValue::Lax,
secure: false,
http_only: false,
extensions: HashMap::new()
}
}
pub fn to_cookie (& self) -> Cookie {
Cookie {
name: self.name.clone(),
value: self.value.clone()
}
}
pub fn path_or_default(&self) -> &str {
self.path.as_deref().unwrap_or("/")
}
pub fn expire_time(&self) -> Option<SystemTime> {
if let Some(duration) = self.max_age {
return Some(self.created.add(duration));
}
if let Some(date) = self.expires {
let time = date.timestamp();
if let Ok(utime) = u64::try_from(time) {
return Some(SystemTime::UNIX_EPOCH.add(Duration::from_secs(utime)));
} else { return Some(self.created.clone())
}
}
return None
}
pub fn expired(&self) -> bool {
if let Some(expires) = self.expire_time() {
let now = SystemTime::now();
return expires < now;
}
return false;
}
pub fn use_in_request_path(&self, path: &str) -> bool {
let cookie_path = self.path_or_default();
let cookie_path_len = cookie_path.len();
let request_path_len = path.len();
if !path.starts_with(cookie_path) {
return false;
}
return
request_path_len == cookie_path_len
|| cookie_path.chars().nth(cookie_path_len - 1).unwrap() == '/'
|| path.chars().nth(cookie_path_len).unwrap() == '/';
}
pub fn use_in_request_domain(&self, request_domain: &str) -> bool {
if self.domain.is_none() {
return false;
}
let cookie_domain = self.domain.as_deref().unwrap();
if let Some(index) = request_domain.rfind(cookie_domain) {
if index == 0 { return true;
}
return request_domain.chars().nth(index-1).unwrap() == '.';
}
return false;
}
pub fn use_in_request(&self, request_domain: &str, request_path: &str, secure: bool) -> bool {
if self.domain.is_none() {
return false;
}
if self.secure && !secure {
return false;
}
if self.same_site == SameSiteValue::Strict && self.domain.as_deref().unwrap() != request_domain {
return false;
}
if self.same_site == SameSiteValue::Lax && !self.use_in_request_domain(request_domain) {
return false;
}
if self.same_site == SameSiteValue::None && ! self.secure {
return false;
}
return self.use_in_request_path(request_path);
}
}
impl PartialEq for SetCookie {
fn eq(&self, other: &Self) -> bool {
return self.name == other.name &&
self.value == other.value &&
self.domain == other.domain &&
self.path == other.path
}
}
impl Eq for SetCookie{}
impl FromStr for SetCookie {
type Err = ParseError;
fn from_str(s: &str) -> Result<Self, Self::Err> {
let mut components = s.split(';');
return if let Some(slice) = components.next() {
let (key, value) = parse_cookie_value(slice)?;
let mut cookie = SetCookie::new(key, value);
while let Some(param) = components.next() {
let directive = CookieDirective::from_str(param)?;
match directive {
CookieDirective::Expires(date) =>
cookie.expires = Some(date),
CookieDirective::MaxAge(seconds) =>
cookie.max_age = Some(seconds),
CookieDirective::Domain(url) => cookie.domain = Some(if let Some(stripped) = url.as_str().strip_prefix(".") {
String::from(stripped)
} else {
url
}),
CookieDirective::Path(path) => cookie.path = Some(path),
CookieDirective::SameSite(val) => cookie.same_site = val,
CookieDirective::Secure => cookie.secure = true,
CookieDirective::HttpOnly => cookie.http_only = true,
CookieDirective::Extension(name, value) => {
let _res = cookie.extensions.insert(name, value);
}
}
}
Ok(cookie)
} else {
if CookieDirective::from_str(s).is_ok() {
return Err(ParseError::new("Cookie has not got name/value"));
};
let (key, value) = parse_cookie_value(s)?;
Ok(SetCookie::new(key, value))
}
}
}
impl Hash for SetCookie {
fn hash<H: Hasher>(&self, state: &mut H) {
self.name.hash(state);
self.domain.hash(state);
}
}
const MONTH_NAME: [&'static str; 12] = ["Jan" , "Feb", "Mar", "Apr", "May", "Jun", "Jul", "Aug", "Sep", "Oct", "Nov", "Dec"];
impl fmt::Display for SetCookie {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::result::Result<(), std::fmt::Error> {
write!(f, "{}={}", self.name, self.value)?;
if let Some(ref domain) = self.domain {
write!(f, ", Domain={}", domain)?;
}
if let Some(ref path) = self.path {
write!(f, ", Path={}", path)?;
}
if let Some(duration) = self.max_age {
write!(f, ", Max-Age={}", duration.as_secs())?;
}else if let Some(ref date) = self.expires {
write!(f, ", Expires={}, {:02}-{}-{} {:02}:{:02}:{:02} GMT",
date.weekday(), date.day(), MONTH_NAME[(date.month()-1) as usize], date.year(),
date.hour(), date.minute(), date.second())?;
}
match self.same_site {
SameSiteValue::None => write!(f, ", SameSite=None")?,
SameSiteValue::Strict => write!(f, ", SameSite=Strict")?,
_ => {}
};
if self.secure {
write!(f, ", Secure")?;
}
if self.http_only {
write!(f, ", HttpOnly")?;
}
for (key, value) in &self.extensions {
if let Some(val) = value {
write!(f, ", {}={}", key, val)?;
} else {
write!(f, ", {}", key)?;
}
}
return Ok(());
}
}
pub(crate) fn parse_cookie_value(cookie: &str) -> Result<(String, String), ParseError>{
if let Some(index) = cookie.find('=') {
let key = String::from(cookie[0..index].trim());
let value = String::from(cookie[index + 1..].trim());
if value.len() == 0 {
return Err(ParseError::new("Cookie value must not be empty"));
}
return Ok((key, value));
} else {
return Err(ParseError::new(format!("Malformed HTTP cookie: {}", cookie)));
}
}
enum CookieDirective {
Expires(DateTime<Utc>),
MaxAge(Duration),
Domain(String),
Path(String),
SameSite(SameSiteValue),
Secure,
HttpOnly,
Extension(String, Option<String>)
}
impl FromStr for CookieDirective {
type Err = ParseError;
fn from_str(s: &str) -> Result<CookieDirective, ParseError> {
if let Some(index) = s.find('=') { let key = s[0..index].trim().to_ascii_lowercase();
let value = s[index + 1..].trim();
if value.len() == 0 {
return Err(ParseError::new(format!("Directive {} value must not be empty", key)));
}
return match key.as_str() {
COOKIE_EXPIRES => {
let expires = parse_rfc_1123_date(value)
.or_else(|_| parse_rfc_850_date(value))
.or_else(|_| parse_asct_date(value))?;
Ok(CookieDirective::Expires(expires))
},
COOKIE_MAX_AGE => { let digit = u64::from_str(value)
.or_else(|_| {
Err(ParseError::new("Cannot parse Max-age"))
})?;
Ok(CookieDirective::MaxAge(Duration::from_secs(digit)))
},
COOKIE_DOMAIN => {
Ok(CookieDirective::Domain(String::from(value)))
},
COOKIE_PATH => {
Ok(CookieDirective::Path(String::from(value)))
}
COOKIE_SAME_SITE => {
let lower_case = value.to_ascii_lowercase();
match SameSiteValue::from_str(lower_case.as_str()) {
Ok(site_value) => Ok(CookieDirective::SameSite(site_value)),
Err(e) => Err(e)
}
},
_ => Ok(CookieDirective::Extension(key, Some(value.to_string())))
}
} else {
let directive = s.trim().to_ascii_lowercase();
match directive.as_str() {
COOKIE_SECURE => Ok(CookieDirective::Secure),
COOKIE_HTTP_ONLY => Ok(CookieDirective::HttpOnly),
COOKIE_DOMAIN | COOKIE_EXPIRES | COOKIE_MAX_AGE | COOKIE_PATH |COOKIE_SAME_SITE => Err(ParseError::new(format!("Directive {} needs a value", directive))),
_ => Ok(CookieDirective::Extension(directive, None))
}
}
}
}
#[cfg(test)]
mod test;