Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Ported Cookies implementation from archive-split #380

Merged
merged 12 commits into from
Jan 30, 2020
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -41,6 +41,7 @@ serde_qs = "0.5.0"
async-std = { version = "1.0.1", features = ["unstable"] }
pin-project-lite = "0.1.0"
mime = "0.3.14"
cookie = { version="0.12.0", features = ["percent-encode"]}

[dev-dependencies]
async-std = { version = "1.0.1", features = ["unstable", "attributes"] }
Expand Down
26 changes: 0 additions & 26 deletions backup/examples/cookies.rs

This file was deleted.

34 changes: 34 additions & 0 deletions examples/cookies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
use async_std::task;
use cookie::Cookie;
use tide::{Request, Response};

/// Tide will use the the `Cookies`'s `Extract` implementation to build this parameter.
///
async fn retrieve_cookie(cx: Request<()>) -> String {
format!("hello cookies: {:?}", cx.cookie("hello").unwrap())
}

async fn set_cookie(_req: Request<()>) -> Response {
let mut res = tide::Response::new(200);
res.set_cookie(Cookie::new("hello", "world"));
res
}

async fn remove_cookie(_req: Request<()>) -> Response {
let mut res = tide::Response::new(200);
res.remove_cookie(Cookie::named("hello"));
res
}

fn main() -> Result<(), std::io::Error> {
task::block_on(async {
let mut app = tide::new();

app.at("/").get(retrieve_cookie);
app.at("/set").get(set_cookie);
app.at("/remove").get(remove_cookie);
app.listen("127.0.0.1:8080").await?;

Ok(())
})
}
14 changes: 14 additions & 0 deletions src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -35,6 +35,20 @@ impl From<StatusCode> for Error {
}
}

/// A simple error type that wraps a String
#[derive(Debug)]
pub struct StringError(pub String);
impl std::error::Error for StringError {}

impl std::fmt::Display for StringError {
fn fmt(
&self,
formatter: &mut std::fmt::Formatter<'_>,
) -> std::result::Result<(), std::fmt::Error> {
self.0.fmt(formatter)
}
}

/// Extension methods for `Result`.
pub trait ResultExt<T>: Sized {
/// Convert to an `Result`, treating the `Err` case as a client
Expand Down
105 changes: 105 additions & 0 deletions src/middleware/cookies.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,105 @@
use crate::{
middleware::{Middleware, Next},
response::CookieEvent,
Request, Response,
};
use cookie::{Cookie, CookieJar, ParseError};
use futures::future::BoxFuture;

use http::HeaderMap;
use std::sync::{Arc, RwLock};

/// A middleware for making cookie data available in requests.
///
/// # Examples
///
/// ```rust
///
/// let mut app = tide::Server::new();
/// app.at("/get").get(|cx: tide::Request<()>| async move { cx.cookie("testCookie").unwrap().unwrap().value().to_string() });
/// app.at("/set").get(|_| async {
/// let mut res = tide::Response::new(200);
/// res.set_cookie(cookie::Cookie::new("testCookie", "NewCookieValue"));
/// res
/// });
///
/// ```
#[derive(Debug, Clone, Default)]
pub(crate) struct CookiesMiddleware;

impl CookiesMiddleware {
/// Creates a new CookiesMiddleware.
pub fn new() -> Self {
nhellwig marked this conversation as resolved.
Show resolved Hide resolved
Self::default()
}
}

impl<State: Send + Sync + 'static> Middleware<State> for CookiesMiddleware {
fn handle<'a>(
&'a self,
mut ctx: Request<State>,
next: Next<'a, State>,
) -> BoxFuture<'a, Response> {
Box::pin(async move {
let cookie_jar = if let Some(cookie_data) = ctx.local::<CookieData>() {
cookie_data.content.clone()
} else {
// no cookie data in local context, so we need to create it
let cookie_data = CookieData::from_headers(ctx.headers());
let content = cookie_data.content.clone();
ctx = ctx.set_local(cookie_data);
content
};

let mut res = next.run(ctx).await;

// add modifications from response to original
for cookie in res.cookie_events.drain(..) {
match cookie {
CookieEvent::Added(cookie) => cookie_jar.write().unwrap().add(cookie.clone()),
CookieEvent::Removed(cookie) => {
cookie_jar.write().unwrap().remove(cookie.clone())
}
}
}

// iterate over added and removed cookies
for cookie in cookie_jar.read().unwrap().delta() {
let set_cookie_header = http::header::SET_COOKIE.as_ref();
let encoded_cookie = cookie.encoded().to_string();
res = res.append_header(set_cookie_header, encoded_cookie);
}
res
})
}
}

#[derive(Debug, Clone)]
pub(crate) struct CookieData {
pub(crate) content: Arc<RwLock<CookieJar>>,
}

impl CookieData {
pub(crate) fn from_headers(headers: &HeaderMap) -> Self {
let cookie_header = headers.get(http::header::COOKIE);
let cookie_jar = cookie_header.and_then(|raw| {
let mut jar = CookieJar::new();

// as long as we have an ascii string this will start parsing the cookie
if let Some(raw_str) = raw.to_str().ok() {
raw_str
.split(';')
.try_for_each(|s| -> Result<_, ParseError> {
jar.add_original(Cookie::parse(s.trim().to_owned())?);
Ok(())
})
.ok()?;
}

Some(jar)
});
let content = Arc::new(RwLock::new(cookie_jar.unwrap_or_default()));

CookieData { content }
}
}
nhellwig marked this conversation as resolved.
Show resolved Hide resolved
3 changes: 1 addition & 2 deletions src/middleware/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -10,13 +10,12 @@ use crate::utils::BoxFuture;
use crate::{Request, Response};

// mod compression;
// mod cookies;
pub(crate) mod cookies;
mod cors;
// mod default_headers;
mod logger;

// pub use compression::{Compression, Decompression};
// pub use cookies::CookiesMiddleware;
pub use cors::{Cors, Origin};
// pub use default_headers::DefaultHeaders;
pub use logger::RequestLogger;
Expand Down
14 changes: 14 additions & 0 deletions src/request.rs
Original file line number Diff line number Diff line change
@@ -1,3 +1,4 @@
use cookie::Cookie;
use http::{HeaderMap, Method, Uri, Version};
use http_service::Body;
use route_recognizer::Params;
Expand All @@ -9,6 +10,9 @@ use async_std::task::{Context, Poll};
use std::pin::Pin;
use std::{str::FromStr, sync::Arc};

use crate::error::Error;
use crate::middleware::cookies::CookieData;

pin_project_lite::pin_project! {
/// An HTTP request.
///
Expand Down Expand Up @@ -293,6 +297,16 @@ impl<State> Request<State> {
})?;
Ok(res)
}

/// returns a `Cookie` by name of the cookie.
pub fn cookie(&self, name: &str) -> Result<Option<Cookie<'static>>, Error> {
let cookie_data = self
.local::<CookieData>()
.expect("should always be set by the cookies middleware");

let locked_jar = cookie_data.content.read().unwrap();
Ok(locked_jar.get(name).cloned())
}
}

impl<State> Read for Request<State> {
Expand Down
42 changes: 39 additions & 3 deletions src/response/mod.rs
Original file line number Diff line number Diff line change
@@ -1,5 +1,6 @@
use async_std::io::prelude::*;

use cookie::Cookie;
use http::StatusCode;
use http_service::Body;
use mime::Mime;
Expand All @@ -9,10 +10,18 @@ pub use into_response::IntoResponse;

mod into_response;

#[derive(Debug)]
pub(crate) enum CookieEvent {
Added(Cookie<'static>),
Removed(Cookie<'static>),
}

/// An HTTP response
#[derive(Debug)]
pub struct Response {
res: http_service::Response,
// tracking here
pub(crate) cookie_events: Vec<CookieEvent>,
}

impl Response {
Expand All @@ -23,7 +32,10 @@ impl Response {
.status(status)
.body(Body::empty())
.unwrap();
Self { res }
Self {
res,
cookie_events: vec![],
}
}

/// Create a new instance from a reader.
Expand All @@ -36,7 +48,10 @@ impl Response {
.status(status)
.body(Box::pin(reader).into())
.unwrap();
Self { res }
Self {
res,
cookie_events: vec![],
}
}

/// Returns the statuscode.
Expand All @@ -57,6 +72,13 @@ impl Response {
self
}

/// Append an HTTP header.
pub fn append_header(mut self, key: &'static str, value: impl AsRef<str>) -> Self {
let value = value.as_ref().to_owned();
self.res.headers_mut().append(key, value.parse().unwrap());
self
}

nhellwig marked this conversation as resolved.
Show resolved Hide resolved
/// Set the request MIME.
///
/// [Read more on MDN](https://developer.mozilla.org/en-US/docs/Web/HTTP/Basics_of_HTTP/MIME_types)
Expand Down Expand Up @@ -131,6 +153,17 @@ impl Response {
// Ok(Multipart::with_body(Cursor::new(body), boundary))
// })
// }

/// Add cookie to the cookie jar.
pub fn set_cookie(&mut self, cookie: Cookie<'static>) {
self.cookie_events.push(CookieEvent::Added(cookie));
}

/// Removes the cookie. This instructs the `CookiesMiddleware` to send a cookie with empty value
/// in the response.
pub fn remove_cookie(&mut self, cookie: Cookie<'static>) {
self.cookie_events.push(CookieEvent::Removed(cookie));
}
}

#[doc(hidden)]
Expand All @@ -143,6 +176,9 @@ impl Into<http_service::Response> for Response {
#[doc(hidden)]
impl From<http_service::Response> for Response {
fn from(res: http_service::Response) -> Self {
Self { res }
Self {
res,
cookie_events: vec![],
}
}
}
4 changes: 3 additions & 1 deletion src/server/mod.rs
Original file line number Diff line number Diff line change
Expand Up @@ -203,7 +203,9 @@ impl<State: Send + Sync + 'static> Server<State> {
pub fn with_state(state: State) -> Server<State> {
Server {
router: Router::new(),
middleware: Vec::new(),
middleware: vec![Arc::new(
crate::middleware::cookies::CookiesMiddleware::new(),
)],
state,
}
}
Expand Down
Loading