Skip to content

Commit

Permalink
fixed upload size limit?
Browse files Browse the repository at this point in the history
  • Loading branch information
worldofjoni committed Nov 3, 2023
1 parent 05818b2 commit d470b43
Show file tree
Hide file tree
Showing 5 changed files with 81 additions and 72 deletions.
3 changes: 3 additions & 0 deletions backend/Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

8 changes: 7 additions & 1 deletion backend/Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -20,7 +20,12 @@ async-trait = "0.1.68"
chrono = "0.4.26"
thiserror = "1.0.40"
uuid = "1.4.0"
axum = { version = "0.6.18", features = ["http2", "macros", "multipart", "tracing"] }
axum = { version = "0.6.18", features = [
"http2",
"macros",
"multipart",
"tracing",
] }
async-graphql = { version = "6.0.0", features = [
"chrono",
"uuid",
Expand Down Expand Up @@ -61,6 +66,7 @@ google-jwt-auth = "0.0.2"
hyper = { version = "0.14" }
mime = "0.3.17"
hmac = "0.12.1"
multer = { version = "2.1.0", features = ["tokio-io"] }

[dev-dependencies]
serial_test = "2.0.0"
Expand Down
138 changes: 68 additions & 70 deletions backend/src/layer/trigger/api/auth.rs
Original file line number Diff line number Diff line change
@@ -1,36 +1,29 @@
//! Module responsible for authenticating graphql requests.
//! For more details, see <https://github.com/kronos-et-al/MensaApp/blob/main/doc/ApiAuth.md>.

use std::fmt::Display;
use std::{error::Error, fmt::Display, io::Cursor};

use axum::{
extract::{self, multipart::MultipartError, FromRequest},
extract,
headers::{authorization::Credentials, Authorization, ContentType},
http::{
request::{self, Parts},
Request,
},
http::{request::Parts, Request},
middleware::Next,
response::IntoResponse,
TypedHeader,
RequestExt, TypedHeader,
};
use base64::{
engine::general_purpose::{self, STANDARD},
Engine,
};
use hmac::{Hmac, Mac};
use hyper::{
body::{Bytes, HttpBody},
StatusCode,
};
use hyper::{body::Bytes, StatusCode};
use mime::Mime;
use multer::{parse_boundary, Multipart};
use sha2::Sha512;
use thiserror::Error;

use crate::{interface::persistent_data::model::ApiKey, util::Uuid};

use super::server::MAX_BODY_SIZE;

pub(super) type AuthResult<T> = Result<T, AuthError>;

const AUTH_DOC_URL: &str = "https://github.com/kronos-et-al/MensaApp/blob/main/doc/ApiAuth.md";
Expand Down Expand Up @@ -123,14 +116,10 @@ impl Credentials for MensaAuthHeader {

#[derive(Error, Debug)]
pub(super) enum AuthMiddlewareError {
#[error("Could not read body: {0}")]
UnableToReadBody(#[from] hyper::Error),
#[error("`Content-Length` larger than {max} or not set.")]
BodyTooLarge { max: u64 },
#[error("error while inspecting multipart request: {0:?}")]
MultipartError(#[from] MultipartError),
#[error("error while inspecting multipart request (generic): {0}")]
GenericMultipartError(String),
#[error("could not read body: {0}")]
UnableToReadBody(Box<dyn Error>),
#[error("error inspecting multipart request: {0}")]
MultipartError(#[from] multer::Error),
#[error("multipart request needs `operations` part")]
MissingOperationsPart,
}
Expand All @@ -149,18 +138,14 @@ pub(super) async fn auth_middleware(
next: Next<axum::body::Body>,
) -> Result<impl IntoResponse, AuthMiddlewareError> {
let auth_header = auth.map(|a| a.0 .0);
let (parts, body) = req.into_parts();

if !body.size_hint().upper().is_some_and(|u| u <= MAX_BODY_SIZE) {
return Err(AuthMiddlewareError::BodyTooLarge { max: MAX_BODY_SIZE });
}

let body_bytes = hyper::body::to_bytes(body).await?;
let (parts, body_bytes) = read_bytes_from_request(req).await?;

let bytes_to_hash = if content_type.is_some_and(|c| is_multipart(c.0)) {
extract_operation_bytes_from_multipart(&parts, &body_bytes).await?
} else {
body_bytes.clone()
let bytes_to_hash = match content_type {
Some(TypedHeader(content_type)) if is_multipart(content_type.clone()) => {
extract_operation_bytes_from_multipart(&content_type, &body_bytes).await?
}
_ => body_bytes.clone(),
};

let auth = AuthInfo {
Expand All @@ -184,34 +169,42 @@ pub(super) async fn auth_middleware(
Ok(next.run(req).await)
}

async fn read_bytes_from_request(
req: Request<hyper::Body>,
) -> Result<(Parts, Bytes), AuthMiddlewareError> {
Ok(match req.with_limited_body() {
Ok(r) => {
let (p, b) = r.into_parts();
(
p,
hyper::body::to_bytes(b)
.await
.map_err(|e| AuthMiddlewareError::UnableToReadBody(e))?,
)
}
Err(r) => {
let (p, b) = r.into_parts();
(
p,
hyper::body::to_bytes(b)
.await
.map_err(|e| AuthMiddlewareError::UnableToReadBody(Box::new(e)))?,
)
}
})
}

fn is_multipart(content_type: ContentType) -> bool {
Mime::from(content_type).essence_str() == mime::MULTIPART_FORM_DATA.essence_str()
}

async fn extract_operation_bytes_from_multipart(
parts: &Parts,
content_type: &ContentType,
body_bytes: &Bytes,
) -> Result<Bytes, AuthMiddlewareError> {
// copy parts
let mut parts_builder = request::Builder::new()
.method(parts.method.clone())
.uri(parts.uri.clone())
.version(parts.version);
parts_builder
.headers_mut()
.ok_or(AuthMiddlewareError::GenericMultipartError(String::new()))?
.extend(parts.headers.clone());
let parts = parts_builder
.body(())
.map_err(|e| AuthMiddlewareError::GenericMultipartError(e.to_string()))?
.into_parts()
.0;

// inspect copy of multipart request
let req = Request::from_parts(parts, hyper::Body::from(body_bytes.clone()));
let mut multipart = axum::extract::Multipart::from_request(req, &())
.await
.map_err(|e| AuthMiddlewareError::GenericMultipartError(e.to_string()))?;
let boundary = parse_boundary(content_type.to_string())?;

let mut multipart = Multipart::with_reader(Cursor::new(body_bytes), boundary);

let mut operations_bytes = None;

Expand Down Expand Up @@ -291,11 +284,8 @@ fn read_auth_from_header(header: &str) -> Option<MensaAuthHeader> {
mod tests {
#![allow(clippy::unwrap_used)]

use std::str::FromStr;

use hyper::Version;

use super::*;
use std::str::FromStr;

#[test]
fn test_auth_info_parsing() {
Expand Down Expand Up @@ -358,28 +348,36 @@ mod tests {

#[tokio::test]
async fn test_extract_operation_bytes() {
let body = b"--boundary\r\nContent-Disposition: form-data; name=\"operations\"\r\n\r\n{\"operationName\":null,\"variables\":{\"mealId\":\"bd3c88f9-5dc8-4773-85dc-53305930e7b6\",\"image\":null},\"query\":\"mutation LinkImage($mealId: UUID!, $image: Upload!) {\\n __typename\\n addImage(mealId: $mealId, image: $image)\\n}\"}\r\n--boundary\r\nContent-Disposition: form-data; name=\"map\"\r\n\r\n{\"0\":[\"variables.image\"]}\r\n--boundary\r\ncontent-type: text/plain; charset=utf-8\r\ncontent-disposition: form-data; name=\"0\"; filename=\"a\"\r\n\r\nimage\r\n--boundary--";
let body = b"--boundary\r\nContent-Disposition: form-data; name=\"operations\"\r\n\r\n{\"operationName\":null,\"variables\":{\"mealId\":\"bd3c88f9-5dc8-4773-85dc-53305930e7b6\",\"image\":null},\"query\":\"mutation LinkImage($mealId: UUID!, $image: Upload!) {\\n __typename\\n addImage(mealId: $mealId, image: $image)\\n}\"}\r\n--boundary\r\nContent-Disposition: form-data; name=\"map\"\r\n\r\n{\"0\":[\"variables.image\"]}\r\n--boundary\r\ncontent-type: text/plain; charset=utf-8\r\ncontent-disposition: form-data; name=\"0\"; filename=\"a\"\r\n\r\nimage\r\n--boundary--".as_ref();

let operations = Bytes::from(
r#"{"operationName":null,"variables":{"mealId":"bd3c88f9-5dc8-4773-85dc-53305930e7b6","image":null},"query":"mutation LinkImage($mealId: UUID!, $image: Upload!) {\n __typename\n addImage(mealId: $mealId, image: $image)\n}"}"#,
);

let request = Request::builder()
.method("POST")
.uri("/")
.version(Version::HTTP_11)
.header("Content-Type", "multipart/form-data; boundary=boundary")
.header(
"Authorization",
"Mensa ZTk5N2MyZTMtNjhlMS00YjZkLWIzMjgtNGFkY2Q1NzNjODM0Ojo=",
)
.header("Content-Length", 504)
.body(Bytes::from(body.as_slice()))
let content_type = ContentType::from_str("multipart/form-data; boundary=boundary").unwrap();
let bytes = Bytes::from(body);

let extracted = extract_operation_bytes_from_multipart(&content_type, &bytes)
.await
.unwrap();

let (parts, body_bytes) = request.into_parts();
assert_eq!(operations, extracted);
}

#[tokio::test]
async fn test_extract_operation_bytes_large() {
let body = [b"--boundary\r\nContent-Disposition: form-data; name=\"operations\"\r\n\r\n{\"operationName\":null,\"variables\":{\"mealId\":\"bd3c88f9-5dc8-4773-85dc-53305930e7b6\",\"image\":null},\"query\":\"mutation LinkImage($mealId: UUID!, $image: Upload!) {\\n __typename\\n addImage(mealId: $mealId, image: $image)\\n}\"}\r\n--boundary\r\nContent-Disposition: form-data; name=\"map\"\r\n\r\n{\"0\":[\"variables.image\"]}\r\n--boundary\r\ncontent-type: image/jpeg\r\ncontent-disposition: form-data; name=\"0\"; filename=\"a\"\r\n\r\n".as_ref(),
include_bytes!("test_data/test_real.jpg").as_ref(),
b"\r\n--boundary--".as_ref()].concat();

let operations = Bytes::from(
r#"{"operationName":null,"variables":{"mealId":"bd3c88f9-5dc8-4773-85dc-53305930e7b6","image":null},"query":"mutation LinkImage($mealId: UUID!, $image: Upload!) {\n __typename\n addImage(mealId: $mealId, image: $image)\n}"}"#,
);

let content_type = ContentType::from_str("multipart/form-data; boundary=boundary").unwrap();
let bytes = Bytes::from(body);

let extracted = extract_operation_bytes_from_multipart(&parts, &body_bytes)
let extracted = extract_operation_bytes_from_multipart(&content_type, &bytes)
.await
.unwrap();

Expand Down
4 changes: 3 additions & 1 deletion backend/src/layer/trigger/api/server.rs
Original file line number Diff line number Diff line change
Expand Up @@ -79,7 +79,7 @@ impl Display for State {
}
}

pub(super) const MAX_BODY_SIZE: u64 = 100 << 20; // 100 MiB
pub(super) const MAX_BODY_SIZE: u64 = 100 << 20; // 100 MiB // todo make env config

/// Class witch controls the webserver for API requests.
pub struct ApiServer {
Expand Down Expand Up @@ -429,4 +429,6 @@ mod tests {

reader.decode().expect("Should decode response to image")
}

// Todo test large image upload
}
Loading
Sorry, something went wrong. Reload?
Sorry, we cannot display this file.
Sorry, this file is invalid so it cannot be displayed.

0 comments on commit d470b43

Please sign in to comment.