pub mod routing { pub use super::{delete, get, patch, post, put}; } use std::convert::Infallible; use std::future::{Future, Ready}; use std::marker::PhantomData; use std::ops::{Deref, DerefMut}; use std::pin::Pin; use actix_web::body::BoxBody; use actix_web::dev::{Payload, ServiceRequest, ServiceResponse}; use actix_web::http::{Method, StatusCode}; use actix_web::web::{self, Data}; use actix_web::{ Error, FromRequest, HttpMessage, HttpRequest, HttpResponse, Resource, Responder, Scope, }; use serde::de::DeserializeOwned; use crate::rest::AppResponseData; pub type Request = HttpRequest; pub type Response = HttpResponse; pub use actix_web::http::header; pub use actix_web::web::{Bytes, Form, Json}; #[derive(Clone, Default)] pub struct HeaderMap(actix_web::http::header::HeaderMap); impl Deref for HeaderMap { type Target = actix_web::http::header::HeaderMap; fn deref(&self) -> &Self::Target { &self.0 } } impl DerefMut for HeaderMap { fn deref_mut(&mut self) -> &mut Self::Target { &mut self.0 } } #[derive(Debug)] pub struct ExtractError { status: StatusCode, message: &'static str, detail: Option, } impl ExtractError { pub fn bad_request(message: &'static str, detail: impl ToString) -> Self { Self { status: StatusCode::BAD_REQUEST, message, detail: Some(detail.to_string()), } } fn internal(message: &'static str) -> Self { Self { status: StatusCode::INTERNAL_SERVER_ERROR, message, detail: None, } } } impl std::fmt::Display for ExtractError { fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { write!(f, "{}", self.message) } } impl actix_web::ResponseError for ExtractError { fn status_code(&self) -> StatusCode { self.status } fn error_response(&self) -> HttpResponse { let mut envelope = AppResponseData::<()>::err(self.status.as_u16() as i32, self.message); if let Some(detail) = &self.detail { envelope = envelope.with_error(detail.clone()); } let envelope = envelope .with_request_id(crate::middleware::request_id().unwrap_or_default()) .with_log_id(crate::middleware::log_id().unwrap_or_default()); HttpResponse::build(self.status).json(envelope) } } pub struct Path(pub T); impl FromRequest for Path { type Error = ExtractError; type Future = Pin>>>; fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future { let request = request.clone(); Box::pin(async move { actix_web::web::Path::::from_request(&request, &mut Payload::None) .await .map(|value| Path(value.into_inner())) .map_err(|error| ExtractError::bad_request("invalid request path", error)) }) } } pub struct Query(pub T); impl FromRequest for Query { type Error = ExtractError; type Future = Pin>>>; fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future { let request = request.clone(); Box::pin(async move { actix_web::web::Query::::from_request(&request, &mut Payload::None) .await .map(|value| Query(value.into_inner())) .map_err(|error| ExtractError::bad_request("invalid query parameters", error)) }) } } pub struct Html(pub T); impl> Responder for Html { type Body = BoxBody; fn respond_to(self, _request: &HttpRequest) -> HttpResponse { HttpResponse::Ok() .content_type("text/html; charset=utf-8") .body(self.0.into()) } } pub struct Redirect(HttpResponse); impl Redirect { pub fn to(location: &str) -> Self { Self( HttpResponse::SeeOther() .insert_header((header::LOCATION, location.to_string())) .finish(), ) } pub fn into_response(self) -> HttpResponse { self.0 } } pub struct State(pub T); impl FromRequest for State { type Error = ExtractError; type Future = Ready>; fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future { std::future::ready( request .app_data::>() .map(|data| State(data.get_ref().clone())) .ok_or_else(|| ExtractError::internal("application state is not registered")), ) } } pub struct Extension(pub T); impl FromRequest for Extension { type Error = ExtractError; type Future = Ready>; fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future { let value = request .app_data::>() .map(|value| value.get_ref().clone()) .or_else(|| request.extensions().get::().cloned()); std::future::ready( value .map(Extension) .ok_or_else(|| ExtractError::internal("request extension is not registered")), ) } } impl FromRequest for HeaderMap { type Error = Infallible; type Future = Ready>; fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future { std::future::ready(Ok(HeaderMap(request.headers().clone()))) } } pub fn json_error(error: actix_web::error::JsonPayloadError, _request: &HttpRequest) -> Error { ExtractError::bad_request("invalid request body", error).into() } pub struct MethodRouter { entries: Vec Resource>>, payload_limit: Option, } impl Default for MethodRouter { fn default() -> Self { Self { entries: Vec::new(), payload_limit: None, } } } impl MethodRouter { pub fn new() -> Self { Self::default() } pub fn payload_limit(mut self, limit: usize) -> Self { self.payload_limit = Some(limit); self } fn add(mut self, method: Method, handler: H) -> Self where H: actix_web::Handler + 'static, Args: FromRequest + 'static, H::Output: Responder + 'static, { self.entries.push(Box::new(move |resource| { resource.route(web::route().method(method).to(handler)) })); self } pub fn get(mut self, handler: H) -> Self where H: actix_web::Handler + 'static, Args: FromRequest + 'static, H::Output: Responder + 'static, { let head = handler.clone(); self = self.add(Method::GET, handler); self.add(Method::HEAD, head) } pub fn post(self, handler: H) -> Self where H: actix_web::Handler + 'static, Args: FromRequest + 'static, H::Output: Responder + 'static, { self.add(Method::POST, handler) } pub fn put(self, handler: H) -> Self where H: actix_web::Handler + 'static, Args: FromRequest + 'static, H::Output: Responder + 'static, { self.add(Method::PUT, handler) } pub fn patch(self, handler: H) -> Self where H: actix_web::Handler + 'static, Args: FromRequest + 'static, H::Output: Responder + 'static, { self.add(Method::PATCH, handler) } pub fn delete(self, handler: H) -> Self where H: actix_web::Handler + 'static, Args: FromRequest + 'static, H::Output: Responder + 'static, { self.add(Method::DELETE, handler) } fn apply(self, resource: Resource) -> Resource { let mut resource = if let Some(limit) = self.payload_limit { resource.app_data(web::PayloadConfig::new(limit)).app_data( web::JsonConfig::default() .limit(limit) .error_handler(json_error), ) } else { resource }; for entry in self.entries { resource = entry(resource); } resource } } pub fn get(handler: H) -> MethodRouter where H: actix_web::Handler + 'static, Args: FromRequest + 'static, H::Output: Responder + 'static, { MethodRouter::new().get(handler) } pub fn post(handler: H) -> MethodRouter where H: actix_web::Handler + 'static, Args: FromRequest + 'static, H::Output: Responder + 'static, { MethodRouter::new().post(handler) } pub fn put(handler: H) -> MethodRouter where H: actix_web::Handler + 'static, Args: FromRequest + 'static, H::Output: Responder + 'static, { MethodRouter::new().put(handler) } pub fn patch(handler: H) -> MethodRouter where H: actix_web::Handler + 'static, Args: FromRequest + 'static, H::Output: Responder + 'static, { MethodRouter::new().patch(handler) } pub fn delete(handler: H) -> MethodRouter where H: actix_web::Handler + 'static, Args: FromRequest + 'static, H::Output: Responder + 'static, { MethodRouter::new().delete(handler) } #[derive(Clone, Copy, PartialEq, Eq, Debug)] pub enum Guard { Session, ServiceKey, } struct RouteEntry { path: String, methods: MethodRouter, guards: Vec, } pub struct Router { routes: Vec, nests: Vec<(String, Router<()>)>, setups: Vec Scope>>, pending_guards: Vec, _marker: PhantomData S>, } impl Default for Router { fn default() -> Self { Self { routes: Vec::new(), nests: Vec::new(), setups: Vec::new(), pending_guards: Vec::new(), _marker: PhantomData, } } } impl Router { pub fn new() -> Self { Self::default() } pub fn route(mut self, path: &str, methods: MethodRouter) -> Self { self.routes.push(RouteEntry { path: path.to_string(), methods, guards: self.pending_guards.clone(), }); self } pub fn nest(mut self, prefix: &str, router: Router) -> Self { self.nests.push((prefix.to_string(), router.erase())); self } pub fn merge(mut self, other: Router) -> Self { self.routes.extend(other.routes); self.nests.extend(other.nests); self.setups.extend(other.setups); self } pub fn with_state(mut self, state: S2) -> Router<()> { self.setups .push(Box::new(move |scope| scope.app_data(Data::new(state)))); self.erase() } pub fn setup(mut self, setup: impl FnOnce(Scope) -> Scope + 'static) -> Self { self.setups.push(Box::new(setup)); self } pub fn wrap(mut self, guard: Guard) -> Self { for route in &mut self.routes { route.guards.push(guard); } self.pending_guards.push(guard); self } fn erase(self) -> Router<()> { Router { routes: self.routes, nests: self.nests, setups: self.setups, pending_guards: self.pending_guards, _marker: PhantomData, } } pub fn into_scope(self, scope: Scope) -> Scope { let mut scope = scope; for setup in self.setups { scope = setup(scope); } let mut grouped: Vec<(String, Vec, MethodRouter)> = Vec::new(); for entry in self.routes { if let Some((_, _, methods)) = grouped .iter_mut() .find(|(path, guards, _)| *path == entry.path && *guards == entry.guards) { methods.entries.extend(entry.methods.entries); if entry.methods.payload_limit.is_some() { methods.payload_limit = entry.methods.payload_limit; } } else { grouped.push((entry.path, entry.guards, entry.methods)); } } grouped.sort_by(|left, right| compare_specificity(&left.0, &right.0)); for (path, guards, methods) in grouped { let resource = methods.apply(web::resource(path)); scope = register_resource(scope, resource, &guards); } for (prefix, child) in self.nests { scope = scope.service(child.into_scope(web::scope(&prefix))); } scope } } fn compare_specificity(left: &str, right: &str) -> std::cmp::Ordering { let left_segments: Vec = left.split('/').map(|seg| seg.starts_with('{')).collect(); let right_segments: Vec = right.split('/').map(|seg| seg.starts_with('{')).collect(); use std::cmp::Ordering; for (left_dynamic, right_dynamic) in left_segments.iter().zip(right_segments.iter()) { match (left_dynamic, right_dynamic) { (false, true) => return Ordering::Less, (true, false) => return Ordering::Greater, _ => {} } } right_segments.len().cmp(&left_segments.len()) } fn register_resource(scope: Scope, resource: Resource, guards: &[Guard]) -> Scope { let Some(guard) = guards.first().copied() else { return scope.service(resource); }; let middleware = guard_middleware(guard); scope.service(resource.wrap(actix_web::middleware::from_fn(middleware))) } fn guard_middleware( guard: Guard, ) -> impl Fn( ServiceRequest, actix_web::middleware::Next, ) -> Pin, Error>>>> { move |request, next| { Box::pin(async move { match guard { Guard::Session => session::share::guard::session_guard(request, next).await, Guard::ServiceKey => require_service_key(request, next).await, } }) } } pub async fn require_service_key( request: ServiceRequest, next: actix_web::middleware::Next, ) -> Result, Error> { let expected = request .app_data::>() .map(|value| value.get_ref().clone()) .unwrap_or_default(); let provided = request .headers() .get("x-gitdata-service-key") .and_then(|value| value.to_str().ok()) .unwrap_or_default(); if expected.is_empty() || !constant_time_eq(provided.as_bytes(), expected.as_bytes()) { return Ok(request.into_response(HttpResponse::Unauthorized().body("invalid service key"))); } Ok(next.call(request).await?.map_into_boxed_body()) } fn constant_time_eq(left: &[u8], right: &[u8]) -> bool { if left.len() != right.len() { return false; } let mut diff = 0u8; for (a, b) in left.iter().zip(right.iter()) { diff |= a ^ b; } diff == 0 }