hyaenidae/src/middleware.rs
2021-04-02 12:07:19 -05:00

287 lines
8.1 KiB
Rust

use crate::{
error::{Error, OptionExt},
State,
};
use actix_session::{Session, UserSession};
use actix_web::{
dev::{Payload, Service, ServiceRequest, ServiceResponse, Transform},
http::StatusCode,
web::{Data, Path},
FromRequest, HttpMessage, HttpRequest, HttpResponse, ResponseError,
};
use event_listener::Event;
use futures::future::LocalBoxFuture;
use hyaenidae_accounts::Authenticated;
use hyaenidae_profiles::store::{Profile, Submission};
use std::{
cell::{Cell, RefCell},
future::{ready, Ready},
rc::Rc,
};
use uuid::Uuid;
pub(crate) struct Referer(pub(crate) String);
impl FromRequest for Referer {
type Config = ();
type Error = actix_web::Error;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let optionable = || {
let value = req.headers().get("Referer")?;
let s = value.to_str().ok()?;
Some(Referer(s.to_owned()))
};
ready((optionable)().ok_or_else(|| Error::Required.into()))
}
}
pub struct CurrentSubmission(pub Submission);
#[derive(Clone, Debug, serde::Deserialize)]
struct SubmissionPath {
submission_id: Uuid,
}
impl FromRequest for CurrentSubmission {
type Config = ();
type Error = actix_web::Error;
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let state_fut = Data::<State>::extract(&req);
let submission_id_fut = Path::<SubmissionPath>::extract(&req);
Box::pin(async move {
let submission_id = submission_id_fut.await?.submission_id;
let state = state_fut.await?;
let store = state.profiles.clone();
let submission =
actix_web::web::block(move || store.store.submissions.by_id(submission_id)?.req())
.await??;
Ok(CurrentSubmission(submission))
})
}
}
pub(crate) struct UserProfile(pub Profile);
impl UserProfile {
pub fn into_inner(self) -> Profile {
self.0
}
}
impl FromRequest for UserProfile {
type Config = ();
type Error = actix_web::Error;
type Future = LocalBoxFuture<'static, Result<Self, Self::Error>>;
fn from_request(req: &HttpRequest, _: &mut Payload) -> Self::Future {
let opt = req
.extensions()
.get::<ProfileExtractor>()
.map(|extractor| (extractor.clone(), extractor.2.listen()));
let user_fut = Authenticated::extract(&req);
let state_fut = Data::<State>::extract(&req);
Box::pin(async move {
let (extractor, listen_fut) = match opt {
Some(tuple) => tuple,
None => return Err(ServerError("/404".to_owned()).into()),
};
if let Some(profile) = extractor.0.borrow().as_ref() {
return Ok(UserProfile(profile.clone()));
}
if !extractor.1.get() {
listen_fut.await;
if let Some(profile) = extractor.0.borrow().as_ref() {
return Ok(UserProfile(profile.clone()));
}
}
let state = state_fut.await?;
let user_id = user_fut.await?.user().id();
let has_profiles = state
.profiles
.store
.profiles
.for_local(user_id)
.next()
.is_some();
let redirect = if has_profiles {
"/profiles/change"
} else {
"/profiles/create/handle"
};
Err(ServerError(redirect.to_owned()).into())
})
}
}
#[derive(Clone, Debug, serde::Deserialize, serde::Serialize)]
pub(super) struct ProfileData {
id: Uuid,
}
impl ProfileData {
pub(super) fn set_data(id: Uuid, session: &Session) -> Result<(), SessionError> {
session
.insert("profile-data", ProfileData { id })
.map_err(|_| SessionError::Set)
}
fn data(session: &Session) -> Result<Option<Self>, actix_web::Error> {
Ok(session.get("profile-data")?)
}
}
#[derive(Debug, thiserror::Error)]
pub(super) enum SessionError {
#[error("Error setting key")]
Set,
}
#[derive(Debug, thiserror::Error)]
#[error("Redirecting to {0}")]
struct ServerError(String);
impl ResponseError for ServerError {
fn status_code(&self) -> StatusCode {
StatusCode::SEE_OTHER
}
fn error_response(&self) -> HttpResponse {
HttpResponse::SeeOther()
.insert_header(("Location", self.0.clone()))
.finish()
}
}
pub(crate) struct CurrentProfile(pub State);
pub(crate) struct ProfileMiddleware<S>(State, S);
#[derive(Clone)]
struct ProfileExtractor(Rc<RefCell<Option<Profile>>>, Rc<Cell<bool>>, Rc<Event>);
struct ProfileDropGuard(Rc<Event>, Rc<Cell<bool>>);
fn profile_extractor() -> (ProfileExtractor, ProfileDropGuard) {
let event = Rc::new(Event::new());
let state = Rc::new(RefCell::new(None));
let flag = Rc::new(Cell::new(false));
(
ProfileExtractor(state, Rc::clone(&flag), Rc::clone(&event)),
ProfileDropGuard(event, flag),
)
}
impl Drop for ProfileDropGuard {
fn drop(&mut self) {
self.1.set(true);
self.0.notify(usize::MAX);
}
}
impl<S, B> Transform<S, ServiceRequest> for CurrentProfile
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error>,
S::Future: 'static,
{
type Response = S::Response;
type Error = S::Error;
type InitError = ();
type Transform = ProfileMiddleware<S>;
type Future = Ready<Result<Self::Transform, Self::InitError>>;
fn new_transform(&self, service: S) -> Self::Future {
ready(Ok(ProfileMiddleware(self.0.clone(), service)))
}
}
impl<S, B> Service<ServiceRequest> for ProfileMiddleware<S>
where
S: Service<ServiceRequest, Response = ServiceResponse<B>, Error = actix_web::Error>,
S::Future: 'static,
{
type Response = S::Response;
type Error = S::Error;
type Future = LocalBoxFuture<'static, Result<Self::Response, Self::Error>>;
fn poll_ready(
&self,
cx: &mut std::task::Context<'_>,
) -> std::task::Poll<Result<(), Self::Error>> {
self.1.poll_ready(cx)
}
fn call(&self, req: ServiceRequest) -> Self::Future {
let session = req.get_session();
let (extractor, drop_guard) = profile_extractor();
req.extensions_mut().insert(extractor.clone());
let (req, pl) = req.into_parts();
let user_fut = Authenticated::extract(&req);
let req = ServiceRequest::from_parts(req, pl);
let fut = self.1.call(req);
let state = self.0.clone();
Box::pin(async move {
let user_id = if let Ok(auth) = user_fut.await {
auth.user().id()
} else {
drop(drop_guard);
return fut.await;
};
if let Some(ProfileData { id: profile_id }) = ProfileData::data(&session)? {
match actix_web::web::block(move || {
Ok(state.profiles.store.profiles.by_id(profile_id)?)
as Result<Option<Profile>, Error>
})
.await
{
Ok(Ok(Some(profile))) => {
if profile
.local_owner()
.map(|id| id == user_id)
.unwrap_or(false)
{
*extractor.0.borrow_mut() = Some(profile);
}
}
Ok(Ok(None)) => {
log::warn!("No profile found for {}", profile_id);
}
Ok(Err(e)) => {
log::warn!("Error fetching profile {}: {}", profile_id, e);
}
Err(_) => {
log::warn!("Panic while fetching profile {}", profile_id);
}
}
}
drop(drop_guard);
fut.await
})
}
}