- Package renamed from @push.rocks/smarts3 to @push.rocks/smartstorage - Class: Smarts3 → SmartStorage, Interface: ISmarts3Config → ISmartStorageConfig - Method: getS3Descriptor → getStorageDescriptor - Rust binary: rusts3 → ruststorage - Rust types: S3Error→StorageError, S3Action→StorageAction, S3Config→SmartStorageConfig, S3Server→StorageServer - On-disk file extension: ._S3_object → ._storage_object - Default credentials: S3RVER → STORAGE - All internal S3 branding removed; AWS S3 protocol compatibility fully maintained
173 lines
6.2 KiB
Rust
173 lines
6.2 KiB
Rust
use hyper::body::Incoming;
|
|
use hyper::{Method, Request};
|
|
use std::collections::HashMap;
|
|
|
|
/// Storage actions that map to IAM permission strings.
|
|
#[derive(Debug, Clone, PartialEq, Eq)]
|
|
pub enum StorageAction {
|
|
ListAllMyBuckets,
|
|
CreateBucket,
|
|
DeleteBucket,
|
|
HeadBucket,
|
|
ListBucket,
|
|
GetObject,
|
|
HeadObject,
|
|
PutObject,
|
|
DeleteObject,
|
|
CopyObject,
|
|
ListBucketMultipartUploads,
|
|
AbortMultipartUpload,
|
|
InitiateMultipartUpload,
|
|
UploadPart,
|
|
CompleteMultipartUpload,
|
|
GetBucketPolicy,
|
|
PutBucketPolicy,
|
|
DeleteBucketPolicy,
|
|
}
|
|
|
|
impl StorageAction {
|
|
/// Return the IAM-style action string (e.g. "s3:GetObject").
|
|
pub fn iam_action(&self) -> &'static str {
|
|
match self {
|
|
StorageAction::ListAllMyBuckets => "s3:ListAllMyBuckets",
|
|
StorageAction::CreateBucket => "s3:CreateBucket",
|
|
StorageAction::DeleteBucket => "s3:DeleteBucket",
|
|
StorageAction::HeadBucket => "s3:ListBucket",
|
|
StorageAction::ListBucket => "s3:ListBucket",
|
|
StorageAction::GetObject => "s3:GetObject",
|
|
StorageAction::HeadObject => "s3:GetObject",
|
|
StorageAction::PutObject => "s3:PutObject",
|
|
StorageAction::DeleteObject => "s3:DeleteObject",
|
|
StorageAction::CopyObject => "s3:PutObject",
|
|
StorageAction::ListBucketMultipartUploads => "s3:ListBucketMultipartUploads",
|
|
StorageAction::AbortMultipartUpload => "s3:AbortMultipartUpload",
|
|
StorageAction::InitiateMultipartUpload => "s3:PutObject",
|
|
StorageAction::UploadPart => "s3:PutObject",
|
|
StorageAction::CompleteMultipartUpload => "s3:PutObject",
|
|
StorageAction::GetBucketPolicy => "s3:GetBucketPolicy",
|
|
StorageAction::PutBucketPolicy => "s3:PutBucketPolicy",
|
|
StorageAction::DeleteBucketPolicy => "s3:DeleteBucketPolicy",
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Context extracted from a request, used for policy evaluation.
|
|
#[derive(Debug, Clone)]
|
|
pub struct RequestContext {
|
|
pub action: StorageAction,
|
|
pub bucket: Option<String>,
|
|
pub key: Option<String>,
|
|
}
|
|
|
|
impl RequestContext {
|
|
/// Build the ARN for this request's resource.
|
|
pub fn resource_arn(&self) -> String {
|
|
match (&self.bucket, &self.key) {
|
|
(Some(bucket), Some(key)) => format!("arn:aws:s3:::{}/{}", bucket, key),
|
|
(Some(bucket), None) => format!("arn:aws:s3:::{}", bucket),
|
|
_ => "arn:aws:s3:::*".to_string(),
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Resolve the storage action from an incoming HTTP request.
|
|
pub fn resolve_action(req: &Request<Incoming>) -> RequestContext {
|
|
let method = req.method().clone();
|
|
let path = req.uri().path().to_string();
|
|
let query_string = req.uri().query().unwrap_or("").to_string();
|
|
let query = parse_query_simple(&query_string);
|
|
|
|
let segments: Vec<&str> = path
|
|
.trim_start_matches('/')
|
|
.splitn(2, '/')
|
|
.filter(|s| !s.is_empty())
|
|
.collect();
|
|
|
|
match segments.len() {
|
|
0 => {
|
|
// Root: GET / -> ListBuckets
|
|
RequestContext {
|
|
action: StorageAction::ListAllMyBuckets,
|
|
bucket: None,
|
|
key: None,
|
|
}
|
|
}
|
|
1 => {
|
|
let bucket = percent_decode(segments[0]);
|
|
let has_policy = query.contains_key("policy");
|
|
let has_uploads = query.contains_key("uploads");
|
|
|
|
let action = match (&method, has_policy, has_uploads) {
|
|
(&Method::GET, true, _) => StorageAction::GetBucketPolicy,
|
|
(&Method::PUT, true, _) => StorageAction::PutBucketPolicy,
|
|
(&Method::DELETE, true, _) => StorageAction::DeleteBucketPolicy,
|
|
(&Method::GET, _, true) => StorageAction::ListBucketMultipartUploads,
|
|
(&Method::GET, _, _) => StorageAction::ListBucket,
|
|
(&Method::PUT, _, _) => StorageAction::CreateBucket,
|
|
(&Method::DELETE, _, _) => StorageAction::DeleteBucket,
|
|
(&Method::HEAD, _, _) => StorageAction::HeadBucket,
|
|
_ => StorageAction::ListBucket,
|
|
};
|
|
|
|
RequestContext {
|
|
action,
|
|
bucket: Some(bucket),
|
|
key: None,
|
|
}
|
|
}
|
|
2 => {
|
|
let bucket = percent_decode(segments[0]);
|
|
let key = percent_decode(segments[1]);
|
|
|
|
let has_copy_source = req.headers().contains_key("x-amz-copy-source");
|
|
let has_part_number = query.contains_key("partNumber");
|
|
let has_upload_id = query.contains_key("uploadId");
|
|
let has_uploads = query.contains_key("uploads");
|
|
|
|
let action = match &method {
|
|
&Method::PUT if has_part_number && has_upload_id => StorageAction::UploadPart,
|
|
&Method::PUT if has_copy_source => StorageAction::CopyObject,
|
|
&Method::PUT => StorageAction::PutObject,
|
|
&Method::GET => StorageAction::GetObject,
|
|
&Method::HEAD => StorageAction::HeadObject,
|
|
&Method::DELETE if has_upload_id => StorageAction::AbortMultipartUpload,
|
|
&Method::DELETE => StorageAction::DeleteObject,
|
|
&Method::POST if has_uploads => StorageAction::InitiateMultipartUpload,
|
|
&Method::POST if has_upload_id => StorageAction::CompleteMultipartUpload,
|
|
_ => StorageAction::GetObject,
|
|
};
|
|
|
|
RequestContext {
|
|
action,
|
|
bucket: Some(bucket),
|
|
key: Some(key),
|
|
}
|
|
}
|
|
_ => RequestContext {
|
|
action: StorageAction::ListAllMyBuckets,
|
|
bucket: None,
|
|
key: None,
|
|
},
|
|
}
|
|
}
|
|
|
|
fn parse_query_simple(query_string: &str) -> HashMap<String, String> {
|
|
let mut map = HashMap::new();
|
|
if query_string.is_empty() {
|
|
return map;
|
|
}
|
|
for pair in query_string.split('&') {
|
|
let mut parts = pair.splitn(2, '=');
|
|
let key = parts.next().unwrap_or("");
|
|
let value = parts.next().unwrap_or("");
|
|
map.insert(key.to_string(), value.to_string());
|
|
}
|
|
map
|
|
}
|
|
|
|
fn percent_decode(s: &str) -> String {
|
|
percent_encoding::percent_decode_str(s)
|
|
.decode_utf8_lossy()
|
|
.to_string()
|
|
}
|