use crate::utils::IntoUrl;
use cookie::Cookie as RawCookie;
use cookie_store::{Cookie, CookieStore};
use std::io::{BufRead, Write};
use url::{ParseError as ParseUrlError, Url};
pub trait SessionRequest {
fn add_cookies(self, _: Vec<&RawCookie<'static>>) -> Self;
}
pub trait SessionResponse {
fn parse_set_cookie(&self) -> Option<Vec<RawCookie<'static>>>;
fn final_url(&self) -> Option<&Url>;
}
macro_rules! define_with_fn {
($with_fn: ident, $request_fn: ident) => {
pub fn $with_fn<U, P>(
&mut self,
url: U,
prepare: P,
) -> ::std::result::Result<<C as SessionClient>::Response, <C as SessionClient>::SendError>
where
P: FnOnce(<C as SessionClient>::Request) -> <C as SessionClient>::Request,
U: IntoUrl
{
let url = url.into_url()?;
let request = self.client.$request_fn(&url);
self.run_request(request, &url, prepare)
}
}
}
macro_rules! define_send_fn {
($send_fn: ident, $request_fn: ident) => {
pub fn $send_fn<U>(
&mut self,
url: U,
) -> ::std::result::Result<<C as SessionClient>::Response, <C as SessionClient>::SendError>
where
U: IntoUrl
{
let url = url.into_url()?;
let request = self.client.$request_fn(&url);
self.run_request(request, &url, |req| req)
}
}
}
pub trait SessionClient {
type Request: SessionRequest;
type Response: SessionResponse;
type SendError: From<ParseUrlError>;
fn get_request(&self, url: &Url) -> Self::Request;
fn put_request(&self, url: &Url) -> Self::Request;
fn head_request(&self, url: &Url) -> Self::Request;
fn delete_request(&self, url: &Url) -> Self::Request;
fn post_request(&self, url: &Url) -> Self::Request;
fn send(&self, request: Self::Request) -> Result<Self::Response, Self::SendError>;
}
pub struct Session<C: SessionClient> {
pub client: C,
pub store: CookieStore,
}
impl<C: SessionClient> Session<C> {
pub fn new(client: C) -> Self {
Session {
client,
store: CookieStore::default(),
}
}
pub fn load<R, E, F>(
client: C,
reader: R,
cookie_from_str: F,
) -> Result<Session<C>, failure::Error>
where
R: BufRead,
F: Fn(&str) -> ::std::result::Result<Cookie<'static>, E>,
failure::Error: From<E>,
{
let store = CookieStore::load(reader, cookie_from_str)?;
Ok(Session { client, store })
}
pub fn load_json<R: BufRead>(client: C, reader: R) -> Result<Session<C>, failure::Error> {
let store = CookieStore::load_json(reader)?;
Ok(Session { client, store })
}
pub fn save<W, E, F>(&self, writer: &mut W, cookie_to_string: F) -> Result<(), failure::Error>
where
W: Write,
F: Fn(&Cookie<'_>) -> ::std::result::Result<String, E>,
failure::Error: From<E>,
{
self.store.save(writer, cookie_to_string)
}
pub fn save_json<W: Write>(&self, writer: &mut W) -> Result<(), failure::Error> {
self.store.save_json(writer)
}
define_with_fn!(get_with, get_request);
define_with_fn!(put_with, put_request);
define_with_fn!(head_with, head_request);
define_with_fn!(delete_with, delete_request);
define_with_fn!(post_with, post_request);
define_send_fn!(get, get_request);
define_send_fn!(put, put_request);
define_send_fn!(head, head_request);
define_send_fn!(delete, delete_request);
define_send_fn!(post, post_request);
fn run_request<P>(
&mut self,
request: <C as SessionClient>::Request,
url: &Url,
prepare: P,
) -> ::std::result::Result<<C as SessionClient>::Response, <C as SessionClient>::SendError>
where
P: FnOnce(
<C as SessionClient>::Request,
) -> <C as SessionClient>::Request,
{
let Session {ref client, ref mut store} = self;
let response = {
let cookies = store.get_request_cookies(url).collect();
let request = request.add_cookies(cookies);
let request = prepare(request);
client.send(request)?
};
if let Some(cookies) = response.parse_set_cookie() {
let final_url: &Url = response.final_url().unwrap_or(url);
store.store_response_cookies(cookies.into_iter(), final_url);
}
Ok(response)
}
}
#[cfg(test)]
mod tests {
use super::{Session, SessionClient, SessionRequest, SessionResponse};
use cookie::Cookie as RawCookie;
use std::io::{self, Read};
use url::ParseError as ParseUrlError;
use url::Url;
pub enum Body<'b> {
ChunkedBody(&'b mut (dyn Read + 'b)),
BufBody(&'b [u8], usize),
}
impl<'b> Read for Body<'b> {
#[inline]
fn read(&mut self, buf: &mut [u8]) -> io::Result<usize> {
match *self {
Body::ChunkedBody(ref mut r) => r.read(buf),
Body::BufBody(ref mut r, _) => Read::read(r, buf),
}
}
}
impl<'b> Into<Body<'b>> for &'b [u8] {
#[inline]
fn into(self) -> Body<'b> {
Body::BufBody(self, self.len())
}
}
impl<'b> Into<Body<'b>> for &'b str {
#[inline]
fn into(self) -> Body<'b> {
self.as_bytes().into()
}
}
impl<'b> Into<Body<'b>> for &'b String {
#[inline]
fn into(self) -> Body<'b> {
self.as_bytes().into()
}
}
impl<'b, R: Read> From<&'b mut R> for Body<'b> {
#[inline]
fn from(r: &'b mut R) -> Body<'b> {
Body::ChunkedBody(r)
}
}
impl<'b> SessionRequest for TestClientRequest<'b> {
fn add_cookies(mut self, cookies: Vec<&RawCookie<'static>>) -> Self {
for cookie in cookies.into_iter() {
self.cookies.push(cookie.clone());
}
self
}
}
struct TestClientRequest<'b> {
cookies: Vec<RawCookie<'static>>,
outgoing: Vec<RawCookie<'static>>,
body: Option<Body<'b>>,
}
impl<'b> TestClientRequest<'b> {
fn set_body<B: Into<Body<'b>>>(&mut self, body: B) {
self.body = Some(body.into());
}
fn set_outgoing(&mut self, cookies: Vec<RawCookie<'static>>) {
self.outgoing = cookies;
}
fn send(self) -> Result<TestClientResponse, TestError> {
Ok(TestClientResponse(
match self.body {
Some(mut body) => {
let mut b = String::new();
body.read_to_string(&mut b).unwrap();
format!("body was: '{}'", b)
}
None => "no body sent".to_string(),
},
self.outgoing,
))
}
}
struct TestClientResponse(String, Vec<RawCookie<'static>>);
impl SessionResponse for TestClientResponse {
fn parse_set_cookie(&self) -> Option<Vec<RawCookie<'static>>> {
Some(self.1.clone())
}
fn final_url(&self) -> Option<&Url> {
None
}
}
impl TestClientResponse {
pub fn body(self) -> String {
self.0
}
}
struct TestClient;
impl TestClient {
fn request(&self, _: &Url) -> TestClientRequest<'_> {
TestClientRequest {
cookies: vec![],
outgoing: vec![],
body: None,
}
}
}
impl<'b> SessionClient for &'b TestClient {
type Request = TestClientRequest<'b>;
type Response = TestClientResponse;
type SendError = TestError;
fn get_request(&self, url: &Url) -> Self::Request {
self.request(url)
}
fn put_request(&self, url: &Url) -> Self::Request {
self.request(url)
}
fn head_request(&self, url: &Url) -> Self::Request {
self.request(url)
}
fn delete_request(&self, url: &Url) -> Self::Request {
self.request(url)
}
fn post_request(&self, url: &Url) -> Self::Request {
self.request(url)
}
fn send(&self, request: Self::Request) -> Result<Self::Response, Self::SendError> {
request.send()
}
}
type TestSession<'c> = Session<&'c TestClient>;
#[derive(Debug, Clone, PartialEq)]
struct TestError;
use std::error;
impl error::Error for TestError {
fn description(&self) -> &str {
"TestError"
}
fn cause(&self) -> Option<&dyn error::Error> {
None
}
}
use std::fmt;
impl fmt::Display for TestError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "test error")
}
}
impl From<ParseUrlError> for TestError {
fn from(_: ParseUrlError) -> TestError {
TestError
}
}
#[allow(unused_macros)]
macro_rules! dump {
($e: expr, $i: ident) => {{
use serde_json;
use time::now_utc;
println!("");
println!("==== {}: {} ====", $e, now_utc().rfc3339());
for c in $i.iter_any() {
println!(
"{} {}",
if c.is_expired() {
"XXXXX"
} else if c.is_persistent() {
"PPPPP"
} else {
" "
},
serde_json::to_string(c).unwrap()
);
println!("----------------");
}
println!("================");
}};
}
macro_rules! is_in_vec {
($i: ident, $e: expr) => {
assert!($i.iter().any(|c| c.name() == $e));
};
}
macro_rules! value_in_vec {
($i: ident, $e: expr, $v: expr) => {
assert!($i.iter().find(|c| c.name() == $e).unwrap().value() == $v);
};
}
macro_rules! not_in_vec {
($i: ident, $e: expr) => {
assert!(!$i.iter().any(|c| c.name() == $e));
};
}
macro_rules! has_sess {
($store: ident, $d: expr, $p: expr, $n: expr) => {
assert!(!$store.store.get($d, $p, $n).unwrap().is_persistent());
};
}
macro_rules! has_pers {
($store: ident, $d: expr, $p: expr, $n: expr) => {
assert!($store.store.get($d, $p, $n).unwrap().is_persistent());
};
}
macro_rules! has_expired {
($store: ident, $d: expr, $p: expr, $n: expr) => {
assert!($store.store.contains_any($d, $p, $n) && !$store.store.contains($d, $p, $n));
};
}
macro_rules! has_value {
($store: ident, $d: expr, $p: expr, $n: expr, $v: expr) => {
assert_eq!($store.store.get($d, $p, $n).unwrap().value(), $v);
};
}
macro_rules! not_has {
($store: ident, $n: expr) => {
assert_eq!($store.store.iter_any().filter(|c| c.name() == $n).count(), 0);
};
}
macro_rules! load_session {
($s: ident, $c: expr, $sd: ident) => {
let mut $s = Session::load_json($c, &$sd[..]).unwrap();
};
}
macro_rules! save_session {
($s: ident) => {{
let mut output = vec![];
$s.save_json(&mut output).unwrap();
output
}};
}
#[test]
fn client() {
let session1 = {
let mut s = TestSession::new(&TestClient);
let url = Url::parse("http://www.example.com").unwrap();
s.store.parse("0=_", &url).unwrap();
s.store.parse("1=a; Max-Age=120", &url).unwrap();
s.store.parse("2=b; Max-Age=120", &url).unwrap();
s.store.parse("secure=zz; Max-Age=120; Secure", &url).unwrap();
s.store.parse(
"foo_domain=zzz",
&Url::parse("http://foo.example.com").unwrap(),
)
.unwrap();
s.store.parse(
"foo_domain_pers=zzz; Max-Age=120",
&Url::parse("http://foo.example.com").unwrap(),
)
.unwrap();
has_sess!(s, "www.example.com", "/", "0");
has_pers!(s, "www.example.com", "/", "1");
has_pers!(s, "www.example.com", "/", "2");
has_pers!(s, "www.example.com", "/", "secure");
has_sess!(s, "foo.example.com", "/", "foo_domain");
has_pers!(s, "foo.example.com", "/", "foo_domain_pers");
let body = "this is the body".to_string();
let resp = s
.get_with("http://www.example.com", |mut r| {
let incoming = r.cookies.clone();
is_in_vec!(incoming, "0");
is_in_vec!(incoming, "1");
is_in_vec!(incoming, "2");
not_in_vec!(incoming, "3");
not_in_vec!(incoming, "secure");
not_in_vec!(incoming, "foo_domain");
not_in_vec!(incoming, "foo_domain_pers");
r.set_body(&body);
r.set_outgoing(vec![
RawCookie::parse("0=hi").unwrap(),
RawCookie::parse("1=sess1; Max-Age=120").unwrap(),
RawCookie::parse("2=c; Max-Age=0").unwrap(),
RawCookie::parse("3=c").unwrap(),
RawCookie::parse("4=d; Max-Age=0").unwrap(),
RawCookie::parse("5=e; Domain=invalid.com").unwrap(),
RawCookie::parse("6=f; Domain=example.com").unwrap(),
RawCookie::parse("7=g; Max-Age=300").unwrap(),
]);
r
})
.unwrap();
assert_eq!("body was: 'this is the body'", resp.body());
has_sess!(s, "www.example.com", "/", "0");
has_value!(s, "www.example.com", "/", "0", "hi");
has_pers!(s, "www.example.com", "/", "1");
has_value!(s, "www.example.com", "/", "1", "sess1");
has_expired!(s, "www.example.com", "/", "2");
has_sess!(s, "www.example.com", "/", "3");
has_value!(s, "www.example.com", "/", "3", "c");
not_has!(s, "4");
not_has!(s, "5");
has_sess!(s, "example.com", "/", "6");
has_pers!(s, "www.example.com", "/", "7");
has_pers!(s, "www.example.com", "/", "secure");
has_sess!(s, "foo.example.com", "/", "foo_domain");
has_pers!(s, "foo.example.com", "/", "foo_domain_pers");
save_session!(s)
};
let session2 = {
load_session!(s, &TestClient, session1);
not_has!(s, "0");
has_pers!(s, "www.example.com", "/", "1");
has_value!(s, "www.example.com", "/", "1", "sess1");
not_has!(s, "2");
not_has!(s, "3");
not_has!(s, "4");
not_has!(s, "5");
not_has!(s, "6");
has_pers!(s, "www.example.com", "/", "7");
has_pers!(s, "www.example.com", "/", "secure");
not_has!(s, "foo_domain");
has_pers!(s, "foo.example.com", "/", "foo_domain_pers");
let resp = s
.get_with("https://www.example.com", |mut r| {
let incoming = r.cookies.clone();
not_in_vec!(incoming, "0");
is_in_vec!(incoming, "1");
not_in_vec!(incoming, "2");
not_in_vec!(incoming, "3");
not_in_vec!(incoming, "4");
not_in_vec!(incoming, "5");
not_in_vec!(incoming, "6");
is_in_vec!(incoming, "7");
is_in_vec!(incoming, "secure");
not_in_vec!(incoming, "foo_domain");
not_in_vec!(incoming, "foo_domain_pers");
r.set_body("this is the second body");
r.set_outgoing(vec![
RawCookie::parse("1=sess2; Max-Age=120").unwrap(),
RawCookie::parse("secure=ZZ; Max-Age=120").unwrap(),
RawCookie::parse("2=B; Max-Age=120; Path=/foo").unwrap(),
RawCookie::parse("8=h; Domain=example.com").unwrap(),
]);
r
})
.unwrap();
assert_eq!("body was: 'this is the second body'", resp.body());
not_has!(s, "0");
has_pers!(s, "www.example.com", "/", "1");
has_value!(s, "www.example.com", "/", "1", "sess2");
has_value!(s, "www.example.com", "/foo", "2", "B");
not_has!(s, "3");
not_has!(s, "4");
not_has!(s, "5");
not_has!(s, "6");
has_pers!(s, "www.example.com", "/", "7");
has_sess!(s, "example.com", "/", "8");
has_value!(s, "www.example.com", "/", "secure", "ZZ");
not_has!(s, "foo_domain");
has_pers!(s, "foo.example.com", "/", "foo_domain_pers");
save_session!(s)
};
let session3 = {
load_session!(s, &TestClient, session2);
not_has!(s, "0");
has_pers!(s, "www.example.com", "/", "1");
has_value!(s, "www.example.com", "/", "1", "sess2");
has_value!(s, "www.example.com", "/foo", "2", "B");
not_has!(s, "3");
not_has!(s, "4");
not_has!(s, "5");
not_has!(s, "6");
has_pers!(s, "www.example.com", "/", "7");
not_has!(s, "8");
has_value!(s, "www.example.com", "/", "secure", "ZZ");
not_has!(s, "foo_domain");
has_pers!(s, "foo.example.com", "/", "foo_domain_pers");
let resp = s
.get_with("http://foo.example.com", |mut r| {
let incoming = r.cookies.clone();
not_in_vec!(incoming, "0");
not_in_vec!(incoming, "1");
not_in_vec!(incoming, "2");
not_in_vec!(incoming, "3");
not_in_vec!(incoming, "4");
not_in_vec!(incoming, "5");
not_in_vec!(incoming, "6");
not_in_vec!(incoming, "7");
not_in_vec!(incoming, "8");
not_in_vec!(incoming, "secure");
not_in_vec!(incoming, "foo_domain");
is_in_vec!(incoming, "foo_domain_pers");
r.set_outgoing(vec![
RawCookie::parse("1=sess3; Max-Age=120").unwrap(),
RawCookie::parse("secure=YY; Max-Age=120; Secure").unwrap(),
RawCookie::parse("9=v; Domain=example.com; Path=/foo; Max-Age=120; Secure")
.unwrap(),
]);
r
})
.unwrap();
assert_eq!("no body sent", resp.body());
not_has!(s, "0");
has_pers!(s, "www.example.com", "/", "1");
has_value!(s, "www.example.com", "/", "1", "sess2");
has_pers!(s, "foo.example.com", "/", "1");
has_value!(s, "foo.example.com", "/", "1", "sess3");
has_value!(s, "www.example.com", "/foo", "2", "B");
not_has!(s, "3");
not_has!(s, "4");
not_has!(s, "5");
not_has!(s, "6");
has_pers!(s, "www.example.com", "/", "7");
not_has!(s, "8");
has_value!(s, "www.example.com", "/", "secure", "ZZ");
has_value!(s, "foo.example.com", "/", "secure", "YY");
not_has!(s, "foo_domain");
has_pers!(s, "foo.example.com", "/", "foo_domain_pers");
save_session!(s)
};
let session4 = {
load_session!(s, &TestClient, session3);
not_has!(s, "0");
has_pers!(s, "www.example.com", "/", "1");
has_value!(s, "www.example.com", "/", "1", "sess2");
has_pers!(s, "foo.example.com", "/", "1");
has_value!(s, "foo.example.com", "/", "1", "sess3");
has_value!(s, "www.example.com", "/foo", "2", "B");
not_has!(s, "3");
not_has!(s, "4");
not_has!(s, "5");
not_has!(s, "6");
has_pers!(s, "www.example.com", "/", "7");
not_has!(s, "8");
has_pers!(s, "example.com", "/foo", "9");
has_value!(s, "www.example.com", "/", "secure", "ZZ");
has_value!(s, "foo.example.com", "/", "secure", "YY");
not_has!(s, "foo_domain");
has_pers!(s, "foo.example.com", "/", "foo_domain_pers");
s.get_with("https://www.example.com/foo", |r| {
let incoming = r.cookies.clone();
not_in_vec!(incoming, "0");
is_in_vec!(incoming, "1");
is_in_vec!(incoming, "2");
not_in_vec!(incoming, "3");
not_in_vec!(incoming, "4");
not_in_vec!(incoming, "5");
not_in_vec!(incoming, "6");
is_in_vec!(incoming, "7");
not_in_vec!(incoming, "8");
is_in_vec!(incoming, "9");
value_in_vec!(incoming, "secure", "ZZ");
not_in_vec!(incoming, "foo_domain");
not_in_vec!(incoming, "foo_domain_pers");
r
})
.unwrap();
save_session!(s)
};
let session5 = {
load_session!(s, &TestClient, session4);
not_has!(s, "0");
has_pers!(s, "www.example.com", "/", "1");
has_value!(s, "www.example.com", "/", "1", "sess2");
has_pers!(s, "foo.example.com", "/", "1");
has_value!(s, "foo.example.com", "/", "1", "sess3");
has_value!(s, "www.example.com", "/foo", "2", "B");
not_has!(s, "3");
not_has!(s, "4");
not_has!(s, "5");
not_has!(s, "6");
has_pers!(s, "www.example.com", "/", "7");
not_has!(s, "8");
has_pers!(s, "example.com", "/foo", "9");
has_pers!(s, "example.com", "/foo", "9");
has_value!(s, "www.example.com", "/", "secure", "ZZ");
has_value!(s, "foo.example.com", "/", "secure", "YY");
not_has!(s, "foo_domain");
has_pers!(s, "foo.example.com", "/", "foo_domain_pers");
s.get_with("https://www.example.com/foo/bar", |r| {
let incoming = r.cookies.clone();
not_in_vec!(incoming, "0");
is_in_vec!(incoming, "1");
is_in_vec!(incoming, "2");
not_in_vec!(incoming, "3");
not_in_vec!(incoming, "4");
not_in_vec!(incoming, "5");
not_in_vec!(incoming, "6");
is_in_vec!(incoming, "7");
not_in_vec!(incoming, "8");
is_in_vec!(incoming, "9");
value_in_vec!(incoming, "secure", "ZZ");
not_in_vec!(incoming, "foo_domain");
not_in_vec!(incoming, "foo_domain_pers");
r
})
.unwrap();
save_session!(s)
};
load_session!(s, &TestClient, session5);
s.get_with("https://www.example.com/", |r| {
let incoming = r.cookies.clone();
not_in_vec!(incoming, "9");
r
})
.unwrap();
s.get_with("https://www.example.com/bar", |r| {
let incoming = r.cookies.clone();
not_in_vec!(incoming, "9");
r
})
.unwrap();
}
}