Add persistant sessions

This commit is contained in:
2020-03-31 17:26:25 -07:00
parent 18ae2e0f9a
commit 10aac891cc
2 changed files with 82 additions and 31 deletions

View File

@@ -8,8 +8,9 @@ edition = "2018"
cookie = "0.13" cookie = "0.13"
futures = "0.3" futures = "0.3"
log = "0.4" log = "0.4"
nanoid = "0.3"
pretty_env_logger = "0.4" pretty_env_logger = "0.4"
sessions = { version = "0.0.2", features = ["fs-store", "nanoid", "tokio"] } sessions = { version = "0.0.2", features = ["fs-store", "tokio"] }
time = "0.2" time = "0.2"
tokio = { version = "0.2", features = ["macros"] } tokio = { version = "0.2", features = ["macros"] }
warp = "0.2" warp = "0.2"

View File

@@ -1,7 +1,10 @@
use std::collections::HashMap; use std::collections::HashMap;
use std::sync::Arc;
use cookie::{Cookie, SameSite}; use cookie::{Cookie, SameSite};
use log::{info, trace}; use futures::TryFutureExt;
use log::{debug, error, info, trace};
use sessions::{Session, SessionStatus, Storable};
use warp::{Filter, Rejection, Reply}; use warp::{Filter, Rejection, Reply};
use warp::http::{StatusCode, Uri}; use warp::http::{StatusCode, Uri};
@@ -10,25 +13,30 @@ const SESSION_HEADER: &'static str = "Spectrum-Session";
#[tokio::main] #[tokio::main]
async fn main() { async fn main() {
pretty_env_logger::init(); pretty_env_logger::init();
let mut store = sessions::FilesystemStore::new("data/sessions.json".into()); let store = Arc::new(sessions::FilesystemStore::new("data".into()));
// GET / // GET /
let root = warp::path::end() let root = warp::path::end()
.and(warp::get()) .and(warp::get())
.and(with_session(&mut store)) .map(|| info!("GET /")).untuple_one()
.map(|session| format!("Hello user #{}", session)); .and(with_session(store.clone(), SessionPolicy::Existing))
.map(|session: Session| format!("Hello, {} (user #{})!",
session.get::<String>(SESSION_NAME).unwrap().unwrap(),
session.id().unwrap()));
// GET/POST /user name={name} // GET/POST /user name={name}
let namechange = warp::path!("user") let namechange = warp::path!("user")
.and(warp::get() .and(warp::get()
.and(with_session(&mut store)) .map(|| info!("GET /user")).untuple_one()
.and(with_session(store.clone(), SessionPolicy::AllowNew))
// .recover(|err| todo!("prevent redir loop")) // reply::with_status(..)
.and(warp::fs::file("./static/index.html")) .and(warp::fs::file("./static/index.html"))
.map(|_session, file| file) .map(|_session, file| file)
// .recover(|err| todo!("prevent redir loop")) // reply::with_status(..)
.or(warp::post() .or(warp::post()
.and(with_session(store.clone(), SessionPolicy::AllowNew))
.and(warp::body::content_length_limit(1024 * 16)) .and(warp::body::content_length_limit(1024 * 16))
.and(warp::body::form()) .and(warp::body::form())
.and_then(|form: HashMap<String, String>| async move { .and_then(|session: Session, form: HashMap<String, String>| async move {
let name = match form.get("name") { let name = match form.get("name") {
None => return Err(warp::reject::custom(BadName)), None => return Err(warp::reject::custom(BadName)),
Some(name) if name.is_empty() || Some(name) if name.is_empty() ||
@@ -36,6 +44,9 @@ async fn main() {
Some(name) => name Some(name) => name
}; };
info!("POST /user as \"{}\"", name); info!("POST /user as \"{}\"", name);
let _old = session.set(SESSION_NAME, name.clone());
session.save().await.map_err(|e| warp::reject::custom(ServerError(Box::new(e))))?;
Ok(warp::reply::with_header(StatusCode::SEE_OTHER, Ok(warp::reply::with_header(StatusCode::SEE_OTHER,
warp::http::header::LOCATION, warp::http::header::LOCATION,
"/")) "/"))
@@ -67,42 +78,81 @@ where From: 'static,
}) })
} }
fn with_session(_store: &mut impl sessions::Storable) -> impl Filter<Extract = (String,), #[derive(Debug, Clone, Copy)]
Error = Rejection> + Clone { enum SessionPolicy {
warp::cookie::cookie(SESSION_HEADER) Existing,
.map(|session| { AllowNew
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)] const SESSION_NAME: &str = "name";
fn with_session(store: Arc<impl Storable>,
policy: SessionPolicy) -> impl Filter<Extract = (Session,),
Error = Rejection> + Clone {
warp::cookie::cookie(SESSION_HEADER)
.and_then(move |session_id: String| {
trace!("Looking up session: {}", session_id);
let store = store.clone(); // pending async reqs allowed to outlive returned Filter
async move {
let session = store.get(&session_id).await?;
session.set_id(session_id)?;
Ok(session)
}
}.map_err(|err: std::io::Error| warp::reject::custom(ServerError(Box::new(err)))))
.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
.and_then(move |session: Session| async move {
match (session.status(), policy) {
(Ok(SessionStatus::Existed), _ )
// Session exists, but name might not be set yet
if session.get::<String>(SESSION_NAME)
.map_or(false, |name| name.filter(|name| !name.is_empty()).is_some()) => Ok(session),
(Ok(SessionStatus::Existed), _ ) => Err(warp::reject::custom(BadName)),
(Ok(SessionStatus::Created), SessionPolicy::AllowNew) => Ok(session),
(Ok(SessionStatus::Created), _) => Err(warp::reject::custom(NoSession)),
(Err(e), _) => Err(warp::reject::custom(ServerError(Box::new(e)))),
_ => Err(warp::reject::custom(NoSession))
}
})
}
#[derive(Debug, Default)]
struct BadName; struct BadName;
impl warp::reject::Reject for BadName {} impl warp::reject::Reject for BadName {}
async fn handle_reject(err: Rejection) -> Result<impl Reply, Rejection> { async fn handle_reject(err: Rejection) -> Result<Box<dyn Reply>, Rejection> {
match err.find() { if let Some(BadName) = err.find() {
Some(BadName) => Ok(warp::reply::with_status(warp::reply(), StatusCode::BAD_REQUEST)), Ok(Box::new(warp::reply::with_header(StatusCode::SEE_OTHER,
_ => Err(err) warp::http::header::LOCATION,
} "/user")))
} else if let Some(ServerError(e)) = err.find() {
error!("Server Error: {:?}", e);
Ok(Box::new(warp::reply::with_status("jrd done messed up X-(", StatusCode::INTERNAL_SERVER_ERROR)))
} else { Err(err) }
} }
#[derive(Debug)]
struct ServerError(Box<dyn std::error::Error + Send + Sync>);
impl warp::reject::Reject for ServerError {}
#[derive(Debug, Default)] #[derive(Debug, Default)]
struct NoSession; struct NoSession;
impl warp::reject::Reject for NoSession {} impl warp::reject::Reject for NoSession {}
async fn handle_no_session(err: Rejection) -> Result<impl Reply, Rejection> { async fn handle_no_session(err: Rejection) -> Result<impl Reply, Rejection> {
match err.find() { match err.find() {
Some(NoSession) => Ok(warp::reply::with_header(warp::redirect::temporary(Uri::from_static("/user")), Some(NoSession) => {
"Set-Cookie", debug!("No Session");
Cookie::build(SESSION_HEADER, 5.to_string()) let id = nanoid::nanoid!(32);
.max_age(time::Duration::seconds(60 * 60 * 24 * 365)) Ok(warp::reply::with_header(warp::redirect::temporary(Uri::from_static("/user")),
.max_age(time::Duration::seconds(10)) "Set-Cookie",
.same_site(SameSite::Lax) Cookie::build(SESSION_HEADER, id)
.finish() .max_age(time::Duration::seconds(60 * 60 * 24 * 365))
.to_string())), .max_age(time::Duration::seconds(10))
.same_site(SameSite::Lax)
.finish()
.to_string()))
}
_ => Err(err) _ => Err(err)
} }
} }