2022-02-05 23:09:40 +00:00
|
|
|
use std::collections::HashMap;
|
|
|
|
use std::convert::TryInto;
|
2022-02-01 22:41:02 +00:00
|
|
|
use std::sync::Arc;
|
|
|
|
|
2022-02-05 23:09:40 +00:00
|
|
|
use futures::StreamExt;
|
|
|
|
use hyper::header::{self, HeaderMap, HeaderName, HeaderValue};
|
|
|
|
use hyper::{Body, Request, Response, StatusCode};
|
2022-02-05 12:38:39 +00:00
|
|
|
use multer::{Constraints, Multipart, SizeLimit};
|
|
|
|
use serde::Deserialize;
|
2022-02-01 22:41:02 +00:00
|
|
|
|
|
|
|
use garage_model::garage::Garage;
|
|
|
|
|
2022-02-05 12:38:39 +00:00
|
|
|
use crate::api_server::resolve_bucket;
|
|
|
|
use crate::error::*;
|
2022-02-05 23:09:40 +00:00
|
|
|
use crate::s3_put::{get_headers, save_stream};
|
2022-02-05 19:36:03 +00:00
|
|
|
use crate::signature::payload::{parse_date, verify_v4};
|
2022-02-01 22:41:02 +00:00
|
|
|
|
|
|
|
pub async fn handle_post_object(
|
|
|
|
garage: Arc<Garage>,
|
|
|
|
req: Request<Body>,
|
|
|
|
bucket: String,
|
|
|
|
) -> Result<Response<Body>, Error> {
|
|
|
|
let boundary = req
|
|
|
|
.headers()
|
|
|
|
.get(header::CONTENT_TYPE)
|
|
|
|
.and_then(|ct| ct.to_str().ok())
|
|
|
|
.and_then(|ct| multer::parse_boundary(ct).ok())
|
2022-02-05 12:38:39 +00:00
|
|
|
.ok_or_bad_request("Counld not get multipart boundary")?;
|
2022-02-01 22:41:02 +00:00
|
|
|
|
2022-02-05 23:09:40 +00:00
|
|
|
// 16k seems plenty for a header. 5G is the max size of a single part, so it seemrs reasonable
|
|
|
|
// for a PostObject
|
2022-02-01 22:41:02 +00:00
|
|
|
let constraints = Constraints::new().size_limit(
|
|
|
|
SizeLimit::new()
|
2022-02-05 23:09:40 +00:00
|
|
|
.per_field(16 * 1024)
|
2022-02-01 22:41:02 +00:00
|
|
|
.for_field("file", 5 * 1024 * 1024 * 1024),
|
|
|
|
);
|
|
|
|
|
|
|
|
let mut multipart = Multipart::with_constraints(req.into_body(), boundary, constraints);
|
|
|
|
|
2022-02-05 23:09:40 +00:00
|
|
|
let mut params = HeaderMap::new();
|
2022-02-05 12:38:39 +00:00
|
|
|
while let Some(field) = multipart.next_field().await? {
|
2022-02-05 23:09:40 +00:00
|
|
|
let name: HeaderName = if let Some(Ok(name)) = field.name().map(TryInto::try_into) {
|
|
|
|
name
|
2022-02-01 22:41:02 +00:00
|
|
|
} else {
|
|
|
|
continue;
|
|
|
|
};
|
|
|
|
if name != "file" {
|
2022-02-05 23:09:40 +00:00
|
|
|
if let Ok(content) = HeaderValue::from_str(&field.text().await?) {
|
|
|
|
match name.as_str() {
|
|
|
|
"tag" => (/* tag need to be reencoded, but we don't support them yet anyway */),
|
|
|
|
"acl" => {
|
|
|
|
params.append("x-amz-acl", content);
|
|
|
|
}
|
|
|
|
_ => {
|
|
|
|
params.append(name, content);
|
|
|
|
}
|
2022-02-01 22:41:02 +00:00
|
|
|
}
|
|
|
|
}
|
|
|
|
continue;
|
|
|
|
}
|
|
|
|
|
2022-02-05 12:38:39 +00:00
|
|
|
// Current part is file. Do some checks before handling to PutObject code
|
2022-02-05 23:09:40 +00:00
|
|
|
let key = params
|
|
|
|
.get("key")
|
|
|
|
.ok_or_bad_request("No key was provided")?
|
|
|
|
.to_str()?;
|
|
|
|
let credential = params
|
|
|
|
.get("x-amz-credential")
|
|
|
|
.ok_or_else(|| {
|
|
|
|
Error::Forbidden("Garage does not support anonymous access yet".to_string())
|
|
|
|
})?
|
|
|
|
.to_str()?;
|
|
|
|
let policy = params
|
|
|
|
.get("policy")
|
|
|
|
.ok_or_bad_request("No policy was provided")?
|
|
|
|
.to_str()?;
|
|
|
|
let signature = params
|
|
|
|
.get("x-amz-signature")
|
|
|
|
.ok_or_bad_request("No signature was provided")?
|
|
|
|
.to_str()?;
|
|
|
|
let date = params
|
|
|
|
.get("x-amz-date")
|
|
|
|
.ok_or_bad_request("No date was provided")?
|
|
|
|
.to_str()?;
|
2022-02-05 12:38:39 +00:00
|
|
|
|
|
|
|
let key = if key.contains("${filename}") {
|
|
|
|
let filename = field.file_name();
|
|
|
|
// is this correct? Maybe we should error instead of default?
|
2022-02-05 12:51:15 +00:00
|
|
|
key.replace("${filename}", filename.unwrap_or_default())
|
2022-02-05 12:38:39 +00:00
|
|
|
} else {
|
2022-02-05 23:09:40 +00:00
|
|
|
key.to_owned()
|
2022-02-05 12:38:39 +00:00
|
|
|
};
|
|
|
|
|
2022-02-05 23:09:40 +00:00
|
|
|
let date = parse_date(date)?;
|
|
|
|
let api_key = verify_v4(&garage, credential, &date, signature, policy.as_bytes()).await?;
|
2022-02-05 12:38:39 +00:00
|
|
|
|
|
|
|
let bucket_id = resolve_bucket(&garage, &bucket, &api_key).await?;
|
|
|
|
|
|
|
|
if !api_key.allow_write(&bucket_id) {
|
|
|
|
return Err(Error::Forbidden(
|
|
|
|
"Operation is not allowed for this key.".to_string(),
|
|
|
|
));
|
|
|
|
}
|
|
|
|
|
|
|
|
let decoded_policy = base64::decode(&policy)?;
|
|
|
|
let _decoded_policy: Policy = serde_json::from_slice(&decoded_policy).unwrap();
|
2022-02-05 23:09:40 +00:00
|
|
|
|
2022-02-05 12:38:39 +00:00
|
|
|
// TODO validate policy against request
|
|
|
|
// unsafe to merge until implemented
|
|
|
|
|
|
|
|
let content_type = field
|
|
|
|
.content_type()
|
2022-02-05 23:09:40 +00:00
|
|
|
.map(AsRef::as_ref)
|
|
|
|
.map(HeaderValue::from_str)
|
|
|
|
.transpose()
|
|
|
|
.ok_or_bad_request("Invalid content type")?
|
|
|
|
.unwrap_or_else(|| HeaderValue::from_static("blob"));
|
|
|
|
|
|
|
|
params.append(header::CONTENT_TYPE, content_type);
|
|
|
|
let headers = get_headers(¶ms)?;
|
2022-02-05 12:38:39 +00:00
|
|
|
|
|
|
|
let res = save_stream(
|
|
|
|
garage,
|
|
|
|
headers,
|
|
|
|
field.map(|r| r.map_err(Into::into)),
|
|
|
|
bucket_id,
|
|
|
|
&key,
|
|
|
|
None,
|
|
|
|
None,
|
|
|
|
)
|
|
|
|
.await?;
|
2022-02-01 22:41:02 +00:00
|
|
|
|
2022-02-05 23:09:40 +00:00
|
|
|
let resp = if let Some(target) = params
|
|
|
|
.get("success_action_redirect")
|
|
|
|
.and_then(|h| h.to_str().ok())
|
|
|
|
{
|
|
|
|
// TODO should validate it's a valid url
|
|
|
|
let target = target.to_owned();
|
|
|
|
Response::builder()
|
2022-02-05 12:38:39 +00:00
|
|
|
.status(StatusCode::SEE_OTHER)
|
2022-02-05 23:09:40 +00:00
|
|
|
.header(header::LOCATION, target.clone())
|
|
|
|
.body(target.into())?
|
|
|
|
} else {
|
|
|
|
let action = params
|
|
|
|
.get("success_action_status")
|
|
|
|
.and_then(|h| h.to_str().ok())
|
|
|
|
.unwrap_or("204");
|
|
|
|
match action {
|
|
|
|
"200" => Response::builder()
|
|
|
|
.status(StatusCode::OK)
|
|
|
|
.body(Body::empty())?,
|
|
|
|
"201" => {
|
|
|
|
// TODO body should be an XML document, not sure which yet
|
|
|
|
Response::builder()
|
|
|
|
.status(StatusCode::CREATED)
|
|
|
|
.body(res.into_body())?
|
|
|
|
}
|
|
|
|
_ => Response::builder()
|
|
|
|
.status(StatusCode::NO_CONTENT)
|
|
|
|
.body(Body::empty())?,
|
|
|
|
}
|
2022-02-01 22:41:02 +00:00
|
|
|
};
|
|
|
|
|
|
|
|
return Ok(resp);
|
|
|
|
}
|
|
|
|
|
2022-02-05 12:51:15 +00:00
|
|
|
Err(Error::BadRequest(
|
2022-02-01 22:41:02 +00:00
|
|
|
"Request did not contain a file".to_owned(),
|
2022-02-05 12:51:15 +00:00
|
|
|
))
|
2022-02-01 22:41:02 +00:00
|
|
|
}
|
2022-02-05 12:38:39 +00:00
|
|
|
|
|
|
|
// TODO remove allow(dead_code) when policy is verified
|
|
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
struct Policy {
|
|
|
|
expiration: String,
|
|
|
|
conditions: Vec<PolicyCondition>,
|
|
|
|
}
|
|
|
|
|
|
|
|
/// A single condition from a policy
|
|
|
|
#[derive(Deserialize)]
|
|
|
|
#[serde(untagged)]
|
|
|
|
enum PolicyCondition {
|
|
|
|
// will contain a single key-value pair
|
|
|
|
Equal(HashMap<String, String>),
|
|
|
|
OtherOp([String; 3]),
|
|
|
|
SizeRange(String, u64, u64),
|
|
|
|
}
|
|
|
|
|
|
|
|
#[allow(dead_code)]
|
|
|
|
#[derive(PartialEq, Eq)]
|
|
|
|
enum Operation {
|
|
|
|
Equal,
|
|
|
|
StartsWith,
|
|
|
|
StartsWithCT,
|
|
|
|
SizeRange,
|
|
|
|
}
|