diff --git a/Cargo.toml b/Cargo.toml index 9ff9574..1c64b21 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -8,6 +8,7 @@ edition = "2018" futures = "0.3" log = "0.4" pretty_env_logger = "0.4" +sessions = { version = "0.0.2", features = ["fs-store", "nanoid", "tokio"] } tokio = { version = "0.2", features = ["macros"] } warp = "0.2" webrtc-unreliable = "0.4" diff --git a/src/main.rs b/src/main.rs index 5d52486..14e16ef 100644 --- a/src/main.rs +++ b/src/main.rs @@ -1,44 +1,82 @@ use std::collections::HashMap; -use log::info; +use log::{info, trace}; use warp::{Filter, Rejection, Reply}; -use warp::http::StatusCode; +use warp::http::{StatusCode, Uri}; + +const SESSION_HEADER: &'static str = "Session"; #[tokio::main] async fn main() { pretty_env_logger::init(); + let mut store = sessions::FilesystemStore::new("data/sessions.json".into()); // GET / let root = warp::path::end() .and(warp::get()) - .and(warp::fs::file("./static/index.html")); + .and(with_session(&mut store)) + .map(|session| format!("Hello user #{}", session)); - // POST /name name={name} - let namechange = warp::path("name") - .and(warp::path::end()) - .and(warp::post()) - .and(warp::body::content_length_limit(1024 * 16)) - .and(warp::body::form()) - .and_then(|form: HashMap| async move { - let name = match form.get("name") { - None => return Err(warp::reject::custom(BadName)), - Some(name) if name.is_empty() || - name.chars().count() > 32 => return Err(warp::reject::custom(BadName)), - Some(name) => name - }; - info!("POST /name as \"{}\"", name); - Ok(warp::reply::with_header(StatusCode::SEE_OTHER, - warp::http::header::LOCATION, - "/")) - }) - .recover(handle_reject); + // GET/POST /user name={name} + let namechange = warp::path!("user") + .and(warp::get() + .and(with_session(&mut store)) + .and(warp::fs::file("./static/index.html")) + .map(|_session, file| file) + // .recover(|err| todo!("prevent redir loop")) // reply::with_status(..) + .or(warp::post() + .and(warp::body::content_length_limit(1024 * 16)) + .and(warp::body::form()) + .and_then(|form: HashMap| async move { + let name = match form.get("name") { + None => return Err(warp::reject::custom(BadName)), + Some(name) if name.is_empty() || + name.chars().count() > 32 => return Err(warp::reject::custom(BadName)), + Some(name) => name + }; + info!("POST /user as \"{}\"", name); + Ok(warp::reply::with_header(StatusCode::SEE_OTHER, + warp::http::header::LOCATION, + "/")) + }) + .recover(handle_reject) + ) + ); let routes = root - .or(namechange); + .or(namechange) + .recover(handle_no_session); warp::serve(routes).run(([127, 0, 0, 1], 8060)).await; } +// TODO: change `T` to `!` when/if `never` stabilizes? +async fn clarify_error(err: Rejection) -> Result +where From: 'static, + To: warp::reject::Reject + Default +{ + Err(match err.find::() { + Some(_) => { trace!("no session"); warp::reject::custom(To::default()) }, + _ => { + trace!("no clarify to {}: {} ≠ {:?}", std::any::type_name::(), + std::any::type_name::(), + err); + err + } + }) +} + +fn with_session(_store: &mut impl sessions::Storable) -> impl Filter + Clone { + warp::cookie::cookie(SESSION_HEADER) + .map(|session| { + info!("Found session: {}", session); + session + }) + .or_else(clarify_error::<_, warp::reject::MissingCookie, NoSession>) // Has cookies, but not session cookie + .or_else(clarify_error::<_, warp::reject::InvalidHeader, NoSession>) // No cookies at all +} + #[derive(Debug)] struct BadName; impl warp::reject::Reject for BadName {} @@ -49,3 +87,16 @@ async fn handle_reject(err: Rejection) -> Result { _ => Err(err) } } + +#[derive(Debug, Default)] +struct NoSession; +impl warp::reject::Reject for NoSession {} + +async fn handle_no_session(err: Rejection) -> Result { + match err.find() { + Some(NoSession) => Ok(warp::reply::with_header(warp::redirect::temporary(Uri::from_static("/user")), + "Set-Cookie", + format!("{}={}; Max-Age=31536000; SameSite=Lax", SESSION_HEADER, 5))), + _ => Err(err) + } +}