soph_server/request/
mod.rsuse crate::{async_trait, traits::RequestTrait};
#[cfg(feature = "request-validate")]
pub use validate::Validate;
#[cfg(feature = "request-validate")]
mod validate;
#[async_trait]
impl RequestTrait for axum::extract::Request {
fn content_type(&self) -> mime::Mime {
if has_content_type(self.headers(), &mime::APPLICATION_WWW_FORM_URLENCODED) {
return mime::APPLICATION_WWW_FORM_URLENCODED;
}
if has_content_type(self.headers(), &mime::APPLICATION_JSON) {
return mime::APPLICATION_JSON;
}
if has_content_type(self.headers(), &mime::MULTIPART_FORM_DATA) {
return mime::MULTIPART_FORM_DATA;
}
mime::TEXT_PLAIN
}
fn header<K>(&self, key: K) -> Option<&str>
where
K: axum::http::header::AsHeaderName,
{
self.headers().get(key).and_then(|value| value.to_str().ok())
}
fn user_agent(&self) -> &str {
self.headers()
.get(axum::http::header::USER_AGENT)
.map_or("", |h| h.to_str().unwrap_or(""))
}
async fn path<T: serde::de::DeserializeOwned + Send + 'static>(
&mut self,
) -> Result<T, axum::extract::rejection::PathRejection> {
use axum::{extract::Path, RequestExt};
let Path(path) = self.extract_parts::<Path<T>>().await?;
Ok(path)
}
#[cfg(feature = "request-query")]
async fn query<T: serde::de::DeserializeOwned + 'static>(
&mut self,
) -> crate::ServerResult<T, axum::extract::rejection::QueryRejection> {
use axum::{extract::Query, RequestExt};
let Query(query) = self.extract_parts::<Query<T>>().await?;
Ok(query)
}
#[cfg(feature = "request-form")]
async fn form<T: serde::de::DeserializeOwned>(
self,
) -> crate::ServerResult<T, axum::extract::rejection::FormRejection> {
use axum::{extract::FromRequest, Form};
let Form(payload) = Form::<T>::from_request(self, &()).await?;
Ok(payload)
}
#[cfg(feature = "request-json")]
async fn json<T: serde::de::DeserializeOwned>(
self,
) -> crate::ServerResult<T, axum::extract::rejection::JsonRejection> {
use axum::{extract::FromRequest, Json};
let Json(payload) = Json::<T>::from_request(self, &()).await?;
Ok(payload)
}
#[cfg(feature = "request-multipart")]
async fn multipart<T: serde::de::DeserializeOwned>(self) -> crate::ServerResult<T> {
use axum::extract::{FromRequest, Multipart};
let mut multipart = Multipart::from_request(self, &()).await?;
let mut data = serde_json::Map::new();
while let Some(field) = multipart.next_field().await? {
if let Some(name) = field.name() {
if let Some(file_name) = field.file_name() {
data.insert(name.to_owned(), file_name.into());
} else {
data.insert(name.to_owned(), field.text().await?.into());
}
}
}
let payload = serde_json::from_value::<T>(data.into())?;
Ok(payload)
}
#[cfg(feature = "request-multipart")]
async fn file(self, key: &str) -> crate::ServerResult<Option<axum::body::Bytes>> {
use axum::extract::{FromRequest, Multipart};
let mut multipart = Multipart::from_request(self, &()).await?;
while let Some(field) = multipart.next_field().await? {
if let Some(name) = field.name() {
if field.file_name().is_some() && name == key {
return Ok(Some(field.bytes().await?));
}
}
}
Ok(None)
}
#[cfg(feature = "request-validate")]
async fn validate<T: serde::de::DeserializeOwned + validator::Validate>(self) -> crate::ServerResult<T> {
let payload: T = if self.content_type() == mime::APPLICATION_JSON {
self.json::<T>().await?
} else if self.content_type() == mime::APPLICATION_WWW_FORM_URLENCODED {
self.form::<T>().await?
} else if self.content_type() == mime::MULTIPART_FORM_DATA {
self.multipart::<T>().await?
} else {
return Err(crate::error::Error::UnsupportedMediaType);
};
payload.validate()?;
Ok(payload)
}
#[cfg(feature = "request-id")]
fn id(&self) -> Option<&str> {
self.header(crate::config::X_REQUEST_ID)
}
#[cfg(feature = "request-auth")]
fn user(&self) -> Option<soph_auth::support::UserClaims> {
self.extensions().get::<soph_auth::support::UserClaims>().cloned()
}
#[cfg(feature = "request-auth")]
fn token(&self) -> crate::ServerResult<String> {
use crate::{error::Error, traits::ErrorTrait};
use soph_config::support::config;
let config = config().parse::<soph_auth::config::Auth>().map_err(Error::wrap)?;
match config
.jwt
.location
.as_ref()
.unwrap_or(&soph_auth::config::JwtLocation::Bearer)
{
soph_auth::config::JwtLocation::Query { name } => Ok(extract_token_from_query(name, self.uri())?),
soph_auth::config::JwtLocation::Cookie { name } => Ok(extract_token_from_cookie(
name,
&axum_extra::extract::cookie::CookieJar::from_headers(self.headers()),
)?),
soph_auth::config::JwtLocation::Bearer => Ok(extract_token_from_header(self.headers())
.map_err(|e| soph_auth::error::Error::Unauthorized(e.to_string()))?),
}
}
}
fn has_content_type(headers: &axum::http::header::HeaderMap, expected_content_type: &mime::Mime) -> bool {
let content_type = if let Some(content_type) = headers.get(axum::http::header::CONTENT_TYPE) {
content_type
} else {
return false;
};
let content_type = if let Ok(content_type) = content_type.to_str() {
content_type
} else {
return false;
};
content_type.starts_with(expected_content_type.as_ref())
}
#[cfg(feature = "request-auth")]
fn extract_token_from_header(headers: &axum::http::header::HeaderMap) -> crate::ServerResult<String> {
use axum::http::header::AUTHORIZATION;
use soph_auth::error::Error;
Ok(headers
.get(AUTHORIZATION)
.ok_or(Error::Unauthorized(format!("header {} token not found", AUTHORIZATION)))?
.to_str()
.map_err(|err| Error::Unauthorized(err.to_string()))?
.strip_prefix("Bearer ")
.ok_or(Error::Unauthorized(format!("error strip {} value", AUTHORIZATION)))?
.to_string())
}
#[cfg(feature = "request-auth")]
fn extract_token_from_cookie(name: &str, jar: &axum_extra::extract::cookie::CookieJar) -> crate::ServerResult<String> {
use soph_auth::error::Error;
Ok(jar
.get(name)
.ok_or(Error::Unauthorized("token is not found".to_string()))?
.to_string()
.strip_prefix(&format!("{name}="))
.ok_or(Error::Unauthorized("error strip value".to_string()))?
.to_string())
}
#[cfg(feature = "request-auth")]
fn extract_token_from_query(name: &str, uri: &axum::http::Uri) -> crate::ServerResult<String> {
use axum::extract::Query;
use soph_auth::error::Error;
use std::collections::HashMap;
let parameters: Query<HashMap<String, String>> =
Query::try_from_uri(uri).map_err(|err| Error::Unauthorized(err.to_string()))?;
Ok(parameters
.get(name)
.cloned()
.ok_or(Error::Unauthorized(format!("`{name}` query parameter not found")))?)
}