use std::pin::Pin; use std::task::{Context, Poll}; use actix_web::body::Body; use actix_web::dev::{Service, ServiceRequest, ServiceResponse, Transform}; use actix_web::web; use actix_web::ResponseError as _; use futures::future::{ok, Future, Ready}; use futures::ready; use pin_project::pin_project; use crate::error::{Error, ResponseError}; use crate::Data; #[derive(Clone, Copy)] pub enum Authentication { Public, Private, Admin, } impl Transform for Authentication where S: Service, Error = actix_web::Error>, { type Response = ServiceResponse; type Error = actix_web::Error; type InitError = (); type Transform = LoggingMiddleware; type Future = Ready>; fn new_transform(&self, service: S) -> Self::Future { ok(LoggingMiddleware { acl: *self, service, }) } } pub struct LoggingMiddleware { acl: Authentication, service: S, } #[allow(clippy::type_complexity)] impl Service for LoggingMiddleware where S: Service, Error = actix_web::Error>, { type Response = ServiceResponse; type Error = actix_web::Error; type Future = AuthenticationFuture; fn poll_ready(&self, cx: &mut Context) -> Poll> { self.service.poll_ready(cx) } fn call(&self, req: ServiceRequest) -> Self::Future { let data = req.app_data::>().unwrap(); if data.api_keys().master.is_none() { return AuthenticationFuture::Authenticated(self.service.call(req)); } let auth_header = match req.headers().get("X-Meili-API-Key") { Some(auth) => match auth.to_str() { Ok(auth) => auth, Err(_) => return AuthenticationFuture::NoHeader(Some(req)), }, None => return AuthenticationFuture::NoHeader(Some(req)), }; let authenticated = match self.acl { Authentication::Admin => data.api_keys().master.as_deref() == Some(auth_header), Authentication::Private => { data.api_keys().master.as_deref() == Some(auth_header) || data.api_keys().private.as_deref() == Some(auth_header) } Authentication::Public => { data.api_keys().master.as_deref() == Some(auth_header) || data.api_keys().private.as_deref() == Some(auth_header) || data.api_keys().public.as_deref() == Some(auth_header) } }; if authenticated { AuthenticationFuture::Authenticated(self.service.call(req)) } else { AuthenticationFuture::Refused(Some(req)) } } } #[pin_project(project = AuthProj)] pub enum AuthenticationFuture where S: Service, { Authenticated(#[pin] S::Future), NoHeader(Option), Refused(Option), } impl Future for AuthenticationFuture where S: Service, Error = actix_web::Error>, { type Output = Result, actix_web::Error>; fn poll(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll { let this = self.project(); match this { AuthProj::Authenticated(fut) => match ready!(fut.poll(cx)) { Ok(resp) => Poll::Ready(Ok(resp)), Err(e) => Poll::Ready(Err(e)), }, AuthProj::NoHeader(req) => { match req.take() { Some(req) => { let response = ResponseError::from(Error::MissingAuthorizationHeader); let response = response.error_response(); let response = req.into_response(response); Poll::Ready(Ok(response)) } // https://doc.rust-lang.org/nightly/std/future/trait.Future.html#panics None => unreachable!("poll called again on ready future"), } } AuthProj::Refused(req) => { match req.take() { Some(req) => { let bad_token = req .headers() .get("X-Meili-API-Key") .map(|h| h.to_str().map(String::from).unwrap_or_default()) .unwrap_or_default(); let response = ResponseError::from(Error::InvalidToken(bad_token)); let response = response.error_response(); let response = req.into_response(response); Poll::Ready(Ok(response)) } // https://doc.rust-lang.org/nightly/std/future/trait.Future.html#panics None => unreachable!("poll called again on ready future"), } } } } }