5f84089
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<String>,
}
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<T>(pub T);
impl<T: DeserializeOwned> FromRequest for Path<T> {
type Error = ExtractError;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let request = request.clone();
Box::pin(async move {
actix_web::web::Path::<T>::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<T>(pub T);
impl<T: DeserializeOwned> FromRequest for Query<T> {
type Error = ExtractError;
type Future = Pin<Box<dyn Future<Output = Result<Self, Self::Error>>>>;
fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let request = request.clone();
Box::pin(async move {
actix_web::web::Query::<T>::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<T>(pub T);
impl<T: Into<String>> Responder for Html<T> {
type Body = BoxBody;
fn respond_to(self, _request: &HttpRequest) -> HttpResponse<Self::Body> {
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<T>(pub T);
impl<T: Clone + 'static> FromRequest for State<T> {
type Error = ExtractError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future {
std::future::ready(
request
.app_data::<Data<T>>()
.map(|data| State(data.get_ref().clone()))
.ok_or_else(|| ExtractError::internal("application state is not registered")),
)
}
}
pub struct Extension<T>(pub T);
impl<T: Clone + 'static> FromRequest for Extension<T> {
type Error = ExtractError;
type Future = Ready<Result<Self, Self::Error>>;
fn from_request(request: &HttpRequest, _payload: &mut Payload) -> Self::Future {
let value = request
.app_data::<Data<T>>()
.map(|value| value.get_ref().clone())
.or_else(|| request.extensions().get::<T>().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<Result<Self, Self::Error>>;
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<Box<dyn FnOnce(Resource) -> Resource>>,
payload_limit: Option<usize>,
}
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<H, Args>(mut self, method: Method, handler: H) -> Self
where
H: actix_web::Handler<Args> + '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<H, Args>(mut self, handler: H) -> Self
where
H: actix_web::Handler<Args> + '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<H, Args>(self, handler: H) -> Self
where
H: actix_web::Handler<Args> + 'static,
Args: FromRequest + 'static,
H::Output: Responder + 'static,
{
self.add(Method::POST, handler)
}
pub fn put<H, Args>(self, handler: H) -> Self
where
H: actix_web::Handler<Args> + 'static,
Args: FromRequest + 'static,
H::Output: Responder + 'static,
{
self.add(Method::PUT, handler)
}
pub fn patch<H, Args>(self, handler: H) -> Self
where
H: actix_web::Handler<Args> + 'static,
Args: FromRequest + 'static,
H::Output: Responder + 'static,
{
self.add(Method::PATCH, handler)
}
pub fn delete<H, Args>(self, handler: H) -> Self
where
H: actix_web::Handler<Args> + '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<H, Args>(handler: H) -> MethodRouter
where
H: actix_web::Handler<Args> + 'static,
Args: FromRequest + 'static,
H::Output: Responder + 'static,
{
MethodRouter::new().get(handler)
}
pub fn post<H, Args>(handler: H) -> MethodRouter
where
H: actix_web::Handler<Args> + 'static,
Args: FromRequest + 'static,
H::Output: Responder + 'static,
{
MethodRouter::new().post(handler)
}
pub fn put<H, Args>(handler: H) -> MethodRouter
where
H: actix_web::Handler<Args> + 'static,
Args: FromRequest + 'static,
H::Output: Responder + 'static,
{
MethodRouter::new().put(handler)
}
pub fn patch<H, Args>(handler: H) -> MethodRouter
where
H: actix_web::Handler<Args> + 'static,
Args: FromRequest + 'static,
H::Output: Responder + 'static,
{
MethodRouter::new().patch(handler)
}
pub fn delete<H, Args>(handler: H) -> MethodRouter
where
H: actix_web::Handler<Args> + '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<Guard>,
}
pub struct Router<S = ()> {
routes: Vec<RouteEntry>,
nests: Vec<(String, Router<()>)>,
setups: Vec<Box<dyn FnOnce(Scope) -> Scope>>,
pending_guards: Vec<Guard>,
_marker: PhantomData<fn() -> S>,
}
impl<S> Default for Router<S> {
fn default() -> Self {
Self {
routes: Vec::new(),
nests: Vec::new(),
setups: Vec::new(),
pending_guards: Vec::new(),
_marker: PhantomData,
}
}
}
impl<S> Router<S> {
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<S>) -> Self {
self.nests.push((prefix.to_string(), router.erase()));
self
}
pub fn merge<S2>(mut self, other: Router<S2>) -> Self {
self.routes.extend(other.routes);
self.nests.extend(other.nests);
self.setups.extend(other.setups);
self
}
pub fn with_state<S2: 'static>(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<Guard>, 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<bool> = left.split('/').map(|seg| seg.starts_with('{')).collect();
let right_segments: Vec<bool> = 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<BoxBody>,
) -> Pin<Box<dyn Future<Output = Result<ServiceResponse<BoxBody>, 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<BoxBody>,
) -> Result<ServiceResponse<BoxBody>, Error> {
let expected = request
.app_data::<Data<String>>()
.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
}