1use std::sync::Arc;
2
3use async_trait::async_trait;
4use bytes::Bytes;
5use chrono::{DateTime, Timelike, Utc};
6use http::{HeaderMap, Method, StatusCode};
7use md5::{Digest, Md5};
8
9use fakecloud_aws::arn::Arn;
10use fakecloud_core::delivery::DeliveryBus;
11use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError};
12use fakecloud_kms::SharedKmsState;
13use fakecloud_persistence::{MemoryS3Store, S3Store, StoreError};
14
15use base64::engine::general_purpose::STANDARD as BASE64;
16use base64::Engine as _;
17
18use crate::logging;
19use crate::state::{AclGrant, S3Bucket, S3Object, SharedS3State};
20
21mod access_points;
22mod acl;
23mod buckets;
24mod config;
25mod lock;
26mod multipart;
27mod notifications;
28mod objects;
29mod tags;
30
31#[cfg(test)]
33use notifications::replicate_object;
34pub(super) use notifications::{
35 deliver_notifications, normalize_notification_ids, normalize_replication_xml,
36 replicate_through_store,
37};
38
39use notifications::extract_all_xml_values;
41
42#[cfg(test)]
44use notifications::{
45 event_matches, key_matches_filters, parse_notification_config, parse_replication_rules,
46 NotificationTargetType,
47};
48
49pub struct S3Service {
50 state: SharedS3State,
51 delivery: Arc<DeliveryBus>,
52 kms_state: Option<SharedKmsState>,
53 pub(crate) kms_hook: Option<Arc<dyn fakecloud_core::delivery::KmsHook>>,
54 store: Arc<dyn S3Store>,
55}
56
57pub(crate) fn persistence_error(err: StoreError) -> AwsServiceError {
62 AwsServiceError::aws_error(
63 StatusCode::INTERNAL_SERVER_ERROR,
64 "InternalError",
65 format!("persistence store error: {err}"),
66 )
67}
68
69pub(crate) fn io_to_aws(err: std::io::Error) -> AwsServiceError {
72 AwsServiceError::aws_error(
73 StatusCode::INTERNAL_SERVER_ERROR,
74 "InternalError",
75 format!("failed to read object body from disk: {err}"),
76 )
77}
78
79impl S3Service {
80 pub fn new(state: SharedS3State, delivery: Arc<DeliveryBus>) -> Self {
81 Self::with_store(state, delivery, Arc::new(MemoryS3Store::new()))
82 }
83
84 pub fn with_store(
85 state: SharedS3State,
86 delivery: Arc<DeliveryBus>,
87 store: Arc<dyn S3Store>,
88 ) -> Self {
89 Self {
90 state,
91 delivery,
92 kms_state: None,
93 kms_hook: None,
94 store,
95 }
96 }
97
98 pub fn with_kms(mut self, kms_state: SharedKmsState) -> Self {
99 self.kms_state = Some(kms_state);
100 self
101 }
102
103 pub fn with_kms_hook(mut self, hook: Arc<dyn fakecloud_core::delivery::KmsHook>) -> Self {
104 self.kms_hook = Some(hook);
105 self
106 }
107
108 pub(crate) fn encrypt_object_body(
119 &self,
120 account_id: &str,
121 region: &str,
122 bucket: &str,
123 plaintext: &[u8],
124 kms_key_id: Option<&str>,
125 ) -> Result<bytes::Bytes, AwsServiceError> {
126 let Some(hook) = &self.kms_hook else {
127 return Ok(bytes::Bytes::copy_from_slice(plaintext));
128 };
129 let key = kms_key_id.filter(|k| !k.is_empty()).unwrap_or("aws/s3");
130 let bucket_arn = Arn::s3(bucket).to_string();
131 let mut ctx = std::collections::HashMap::new();
132 ctx.insert("aws:s3:arn".to_string(), bucket_arn);
133 match hook.encrypt(account_id, region, key, plaintext, "s3.amazonaws.com", ctx) {
134 Ok(envelope) => Ok(bytes::Bytes::from(envelope.into_bytes())),
135 Err(err) => {
136 tracing::warn!(bucket = %bucket, error = %err, "SSE-KMS encrypt failed");
137 Err(AwsServiceError::aws_error(
138 StatusCode::INTERNAL_SERVER_ERROR,
139 "KMS.InternalFailureException",
140 format!("Failed to encrypt object via KMS: {err}"),
141 ))
142 }
143 }
144 }
145
146 pub(crate) fn decrypt_object_body(
156 &self,
157 account_id: &str,
158 bucket: &str,
159 ciphertext: &[u8],
160 ) -> Result<bytes::Bytes, AwsServiceError> {
161 let Some(hook) = &self.kms_hook else {
162 return Ok(bytes::Bytes::copy_from_slice(ciphertext));
163 };
164 let envelope = match std::str::from_utf8(ciphertext) {
167 Ok(s) => s,
168 Err(_) => return Ok(bytes::Bytes::copy_from_slice(ciphertext)),
169 };
170 let bucket_arn = Arn::s3(bucket).to_string();
171 let mut ctx = std::collections::HashMap::new();
172 ctx.insert("aws:s3:arn".to_string(), bucket_arn);
173 match hook.decrypt(account_id, envelope, "s3.amazonaws.com", ctx) {
174 Ok(bytes) => Ok(bytes::Bytes::from(bytes)),
175 Err(err) => {
176 tracing::warn!(bucket = %bucket, error = %err, "SSE-KMS decrypt failed");
177 Err(AwsServiceError::aws_error(
178 StatusCode::INTERNAL_SERVER_ERROR,
179 "KMS.InternalFailureException",
180 format!("Failed to decrypt object via KMS: {err}"),
181 ))
182 }
183 }
184 }
185}
186
187#[async_trait]
188impl AwsService for S3Service {
189 fn service_name(&self) -> &str {
190 "s3"
191 }
192
193 async fn handle(&self, mut req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
194 let is_put_to_key = req.method == Method::PUT
204 && req.path_segments.len() >= 2
205 && req
206 .path_segments
207 .first()
208 .map(|s| !s.is_empty())
209 .unwrap_or(false);
210 let q = &req.query_params;
211 let is_put_object = is_put_to_key
212 && !q.contains_key("tagging")
213 && !q.contains_key("acl")
214 && !q.contains_key("retention")
215 && !q.contains_key("legal-hold")
216 && !q.contains_key("renameObject")
217 && !q.contains_key("encryption")
218 && !req.headers.contains_key("x-amz-copy-source");
219 let is_upload_part =
224 is_put_to_key && q.contains_key("partNumber") && q.contains_key("uploadId");
225 if !is_put_object && !is_upload_part {
226 if let Some(stream) = req.take_body_stream() {
227 req.body = fakecloud_core::service::drain_request_stream(stream).await?;
228 }
229 }
230
231 access_points::resolve_access_point(self, &mut req)?;
233
234 let account_id = req.account_id.as_str();
235
236 let host = req
238 .headers
239 .get("host")
240 .and_then(|v| v.to_str().ok())
241 .unwrap_or("");
242 let is_control = host.to_ascii_lowercase().contains("s3-control");
243 if is_control {
244 let v1 = req.path_segments.first().map(|s| s.as_str());
245 let v2 = req.path_segments.get(1).map(|s| s.as_str());
246 let v3 = req.path_segments.get(2).map(|s| s.as_str());
247 if v1 == Some("v20180820") && v2 == Some("accesspoint") {
248 if let Some(name) = v3 {
249 match req.method {
250 Method::PUT => return self.create_access_point(account_id, &req, name),
251 Method::GET => return self.get_access_point(account_id, &req, name),
252 Method::DELETE => return self.delete_access_point(account_id, &req, name),
253 _ => {}
254 }
255 } else if req.method == Method::GET {
256 return self.list_access_points(account_id, &req);
257 }
258 }
259 }
260
261 if req.raw_path.starts_with("//") {
270 return Err(AwsServiceError::aws_error(
271 StatusCode::BAD_REQUEST,
272 "InvalidBucketName",
273 "The specified bucket is not valid: bucket name cannot be empty",
274 ));
275 }
276 let bucket = req.path_segments.first().map(|s| s.as_str());
277 let key = if let Some(b) = bucket {
280 let prefix = format!("/{b}/");
281 if req.raw_path.starts_with(&prefix) && req.raw_path.len() > prefix.len() {
282 let raw_key = &req.raw_path[prefix.len()..];
283 Some(
284 percent_encoding::percent_decode_str(raw_key)
285 .decode_utf8_lossy()
286 .into_owned(),
287 )
288 } else if req.path_segments.len() > 1 {
289 let raw = req.path_segments[1..].join("/");
290 Some(
291 percent_encoding::percent_decode_str(&raw)
292 .decode_utf8_lossy()
293 .into_owned(),
294 )
295 } else {
296 None
297 }
298 } else {
299 None
300 };
301
302 if let Some(b) = bucket {
304 if req.method == Method::POST
306 && key.is_some()
307 && req.query_params.contains_key("uploads")
308 {
309 return self.create_multipart_upload(account_id, &req, b, key.as_deref().unwrap());
310 }
311
312 if req.method == Method::POST
314 && key.is_some()
315 && req.query_params.contains_key("restore")
316 {
317 return self.restore_object(account_id, &req, b, key.as_deref().unwrap());
318 }
319
320 if req.method == Method::POST && key.is_some() {
322 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
323 return self.complete_multipart_upload(
324 account_id,
325 &req,
326 b,
327 key.as_deref().unwrap(),
328 &upload_id,
329 );
330 }
331 }
332
333 if req.method == Method::PUT && key.is_some() {
335 if let (Some(part_num_str), Some(upload_id)) = (
336 req.query_params.get("partNumber").cloned(),
337 req.query_params.get("uploadId").cloned(),
338 ) {
339 if let Ok(part_number) = part_num_str.parse::<i64>() {
340 if req.headers.contains_key("x-amz-copy-source") {
341 return self.upload_part_copy(
342 account_id,
343 &req,
344 b,
345 key.as_deref().unwrap(),
346 &upload_id,
347 part_number,
348 );
349 }
350 return self
351 .upload_part(
352 account_id,
353 &req,
354 b,
355 key.as_deref().unwrap(),
356 &upload_id,
357 part_number,
358 )
359 .await;
360 }
361 }
362 }
363
364 if req.method == Method::DELETE && key.is_some() {
366 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
367 return self.abort_multipart_upload(
368 account_id,
369 b,
370 key.as_deref().unwrap(),
371 &upload_id,
372 );
373 }
374 }
375
376 if req.method == Method::GET
378 && key.is_none()
379 && req.query_params.contains_key("uploads")
380 {
381 return self.list_multipart_uploads(account_id, b);
382 }
383
384 if req.method == Method::GET && key.is_some() {
386 if let Some(upload_id) = req.query_params.get("uploadId").cloned() {
387 return self.list_parts(
388 account_id,
389 &req,
390 b,
391 key.as_deref().unwrap(),
392 &upload_id,
393 );
394 }
395 }
396 }
397
398 if req.method == Method::OPTIONS {
400 if let Some(b_name) = bucket {
401 let cors_config = {
402 let accounts = self.state.read();
403 let _empty_s3 = crate::state::S3State::new(&req.account_id, &req.region);
404 let state = accounts.get(&req.account_id).unwrap_or(&_empty_s3);
405 state
406 .buckets
407 .get(b_name)
408 .and_then(|b| b.cors_config.clone())
409 };
410 if let Some(ref config) = cors_config {
411 let origin = req
412 .headers
413 .get("origin")
414 .and_then(|v| v.to_str().ok())
415 .unwrap_or("");
416 let request_method = req
417 .headers
418 .get("access-control-request-method")
419 .and_then(|v| v.to_str().ok())
420 .unwrap_or("");
421 let rules = parse_cors_config(config);
422 if let Some(rule) = find_cors_rule(&rules, origin, Some(request_method)) {
423 let mut headers = HeaderMap::new();
424 let matched_origin = if rule.allowed_origins.contains(&"*".to_string()) {
425 "*"
426 } else {
427 origin
428 };
429 headers.insert(
430 "access-control-allow-origin",
431 matched_origin
432 .parse()
433 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
434 );
435 headers.insert(
436 "access-control-allow-methods",
437 rule.allowed_methods
438 .join(", ")
439 .parse()
440 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
441 );
442 if !rule.allowed_headers.is_empty() {
443 let ah = if rule.allowed_headers.contains(&"*".to_string()) {
444 req.headers
445 .get("access-control-request-headers")
446 .and_then(|v| v.to_str().ok())
447 .unwrap_or("*")
448 .to_string()
449 } else {
450 rule.allowed_headers.join(", ")
451 };
452 headers.insert(
453 "access-control-allow-headers",
454 ah.parse()
455 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
456 );
457 }
458 if let Some(max_age) = rule.max_age_seconds {
459 headers.insert(
460 "access-control-max-age",
461 max_age
462 .to_string()
463 .parse()
464 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
465 );
466 }
467 return Ok(AwsResponse {
468 status: StatusCode::OK,
469 content_type: String::new(),
470 body: Bytes::new().into(),
471 headers,
472 });
473 }
474 }
475 return Err(AwsServiceError::aws_error(
476 StatusCode::FORBIDDEN,
477 "CORSResponse",
478 "CORS is not enabled for this bucket",
479 ));
480 }
481 }
482
483 let origin_header = req
485 .headers
486 .get("origin")
487 .and_then(|v| v.to_str().ok())
488 .map(|s| s.to_string());
489
490 let bucket_subresources: &[&str] = &[
497 "tagging",
498 "acl",
499 "versioning",
500 "cors",
501 "notification",
502 "website",
503 "accelerate",
504 "publicAccessBlock",
505 "encryption",
506 "lifecycle",
507 "logging",
508 "policy",
509 "policyStatus",
510 "replication",
511 "ownershipControls",
512 "inventory",
513 "analytics",
514 "intelligent-tiering",
515 "metrics",
516 "requestPayment",
517 "abac",
518 "metadataConfiguration",
519 "metadataTable",
520 "metadataInventoryTable",
521 "metadataJournalTable",
522 "object-lock",
523 "versions",
524 "session",
525 "retention",
526 "legal-hold",
527 "attributes",
528 "torrent",
529 "renameObject",
530 "uploads",
531 "restore",
532 "select",
533 "location",
540 "delete",
541 "list-type",
542 ];
543 if bucket.is_none()
544 && bucket_subresources
545 .iter()
546 .any(|q| req.query_params.contains_key(*q))
547 {
548 return Err(AwsServiceError::aws_error(
549 StatusCode::BAD_REQUEST,
550 "InvalidBucketName",
551 "The specified bucket is not valid: bucket name is required for this operation",
552 ));
553 }
554
555 let mut result = match (&req.method, bucket, key.as_deref()) {
556 (&Method::GET, None, None) => {
558 if req.query_params.get("x-id").map(|s| s.as_str()) == Some("ListDirectoryBuckets")
559 {
560 self.list_directory_buckets(account_id, &req)
561 } else {
562 self.list_buckets(account_id, &req)
563 }
564 }
565
566 (&Method::PUT, Some(b), None) => {
568 if req.query_params.contains_key("tagging") {
569 self.put_bucket_tagging(account_id, &req, b)
570 } else if req.query_params.contains_key("acl") {
571 self.put_bucket_acl(account_id, &req, b)
572 } else if req.query_params.contains_key("versioning") {
573 self.put_bucket_versioning(account_id, &req, b)
574 } else if req.query_params.contains_key("cors") {
575 self.put_bucket_cors(account_id, &req, b)
576 } else if req.query_params.contains_key("notification") {
577 self.put_bucket_notification(account_id, &req, b)
578 } else if req.query_params.contains_key("website") {
579 self.put_bucket_website(account_id, &req, b)
580 } else if req.query_params.contains_key("accelerate") {
581 self.put_bucket_accelerate(account_id, &req, b)
582 } else if req.query_params.contains_key("publicAccessBlock") {
583 self.put_public_access_block(account_id, &req, b)
584 } else if req.query_params.contains_key("encryption") {
585 self.put_bucket_encryption(account_id, &req, b)
586 } else if req.query_params.contains_key("lifecycle") {
587 self.put_bucket_lifecycle(account_id, &req, b)
588 } else if req.query_params.contains_key("logging") {
589 self.put_bucket_logging(account_id, &req, b)
590 } else if req.query_params.contains_key("policy") {
591 self.put_bucket_policy(account_id, &req, b)
592 } else if req.query_params.contains_key("object-lock") {
593 self.put_object_lock_config(account_id, &req, b)
594 } else if req.query_params.contains_key("replication") {
595 self.put_bucket_replication(account_id, &req, b)
596 } else if req.query_params.contains_key("ownershipControls") {
597 self.put_bucket_ownership_controls(account_id, &req, b)
598 } else if req.query_params.contains_key("inventory") {
599 self.put_bucket_inventory(account_id, &req, b)
600 } else if req.query_params.contains_key("analytics") {
601 self.put_bucket_analytics_config(account_id, &req, b)
602 } else if req.query_params.contains_key("intelligent-tiering") {
603 self.put_bucket_intelligent_tiering_config(account_id, &req, b)
604 } else if req.query_params.contains_key("metrics") {
605 self.put_bucket_metrics_config(account_id, &req, b)
606 } else if req.query_params.contains_key("requestPayment") {
607 self.put_bucket_request_payment(account_id, &req, b)
608 } else if req.query_params.contains_key("abac") {
609 self.put_bucket_abac(account_id, &req, b)
610 } else if req.query_params.contains_key("metadataInventoryTable") {
611 self.update_bucket_metadata_inventory_table(account_id, &req, b)
612 } else if req.query_params.contains_key("metadataJournalTable") {
613 self.update_bucket_metadata_journal_table(account_id, &req, b)
614 } else {
615 self.create_bucket(account_id, &req, b)
616 }
617 }
618 (&Method::DELETE, Some(b), None) => {
619 if req.query_params.contains_key("tagging") {
620 self.delete_bucket_tagging(account_id, &req, b)
621 } else if req.query_params.contains_key("cors") {
622 self.delete_bucket_cors(account_id, b)
623 } else if req.query_params.contains_key("website") {
624 self.delete_bucket_website(account_id, b)
625 } else if req.query_params.contains_key("publicAccessBlock") {
626 self.delete_public_access_block(account_id, b)
627 } else if req.query_params.contains_key("encryption") {
628 self.delete_bucket_encryption(account_id, b)
629 } else if req.query_params.contains_key("lifecycle") {
630 self.delete_bucket_lifecycle(account_id, b)
631 } else if req.query_params.contains_key("policy") {
632 self.delete_bucket_policy(account_id, b)
633 } else if req.query_params.contains_key("replication") {
634 self.delete_bucket_replication(account_id, b)
635 } else if req.query_params.contains_key("ownershipControls") {
636 self.delete_bucket_ownership_controls(account_id, b)
637 } else if req.query_params.contains_key("inventory") {
638 self.delete_bucket_inventory(account_id, &req, b)
639 } else if req.query_params.contains_key("analytics") {
640 self.delete_bucket_analytics_config(account_id, &req, b)
641 } else if req.query_params.contains_key("intelligent-tiering") {
642 self.delete_bucket_intelligent_tiering_config(account_id, &req, b)
643 } else if req.query_params.contains_key("metrics") {
644 self.delete_bucket_metrics_config(account_id, &req, b)
645 } else if req.query_params.contains_key("metadataConfiguration") {
646 self.delete_bucket_metadata_config(account_id, b)
647 } else if req.query_params.contains_key("metadataTable") {
648 self.delete_bucket_metadata_table_config(account_id, b)
649 } else {
650 self.delete_bucket(account_id, &req, b)
651 }
652 }
653 (&Method::HEAD, Some(b), None) => self.head_bucket(account_id, b),
654 (&Method::GET, Some(b), None) => {
655 if req.query_params.contains_key("tagging") {
656 self.get_bucket_tagging(account_id, &req, b)
657 } else if req.query_params.contains_key("location") {
658 self.get_bucket_location(account_id, b)
659 } else if req.query_params.contains_key("acl") {
660 self.get_bucket_acl(account_id, &req, b)
661 } else if req.query_params.contains_key("versioning") {
662 self.get_bucket_versioning(account_id, b)
663 } else if req.query_params.contains_key("versions") {
664 self.list_object_versions(account_id, &req, b)
665 } else if req.query_params.contains_key("object-lock") {
666 self.get_object_lock_configuration(account_id, b)
667 } else if req.query_params.contains_key("cors") {
668 self.get_bucket_cors(account_id, b)
669 } else if req.query_params.contains_key("notification") {
670 self.get_bucket_notification(account_id, b)
671 } else if req.query_params.contains_key("website") {
672 self.get_bucket_website(account_id, b)
673 } else if req.query_params.contains_key("accelerate") {
674 self.get_bucket_accelerate(account_id, b)
675 } else if req.query_params.contains_key("publicAccessBlock") {
676 self.get_public_access_block(account_id, b)
677 } else if req.query_params.contains_key("encryption") {
678 self.get_bucket_encryption(account_id, b)
679 } else if req.query_params.contains_key("lifecycle") {
680 self.get_bucket_lifecycle(account_id, b)
681 } else if req.query_params.contains_key("logging") {
682 self.get_bucket_logging(account_id, b)
683 } else if req.query_params.contains_key("policy") {
684 self.get_bucket_policy(account_id, b)
685 } else if req.query_params.contains_key("replication") {
686 self.get_bucket_replication(account_id, b)
687 } else if req.query_params.contains_key("ownershipControls") {
688 self.get_bucket_ownership_controls(account_id, b)
689 } else if req.query_params.contains_key("inventory") {
690 if req.query_params.contains_key("id") {
691 self.get_bucket_inventory(account_id, &req, b)
692 } else {
693 self.list_bucket_inventory_configurations(account_id, b)
694 }
695 } else if req.query_params.contains_key("analytics") {
696 if req.query_params.contains_key("id") {
697 self.get_bucket_analytics_config(account_id, &req, b)
698 } else {
699 self.list_bucket_analytics_configurations(account_id, b)
700 }
701 } else if req.query_params.contains_key("intelligent-tiering") {
702 if req.query_params.contains_key("id") {
703 self.get_bucket_intelligent_tiering_config(account_id, &req, b)
704 } else {
705 self.list_bucket_intelligent_tiering_configurations(account_id, b)
706 }
707 } else if req.query_params.contains_key("metrics") {
708 if req.query_params.contains_key("id") {
709 self.get_bucket_metrics_config(account_id, &req, b)
710 } else {
711 self.list_bucket_metrics_configurations(account_id, b)
712 }
713 } else if req.query_params.contains_key("requestPayment") {
714 self.get_bucket_request_payment(account_id, b)
715 } else if req.query_params.contains_key("abac") {
716 self.get_bucket_abac(account_id, b)
717 } else if req.query_params.contains_key("policyStatus") {
718 self.get_bucket_policy_status(account_id, b)
719 } else if req.query_params.contains_key("metadataConfiguration") {
720 self.get_bucket_metadata_config(account_id, b)
721 } else if req.query_params.contains_key("metadataTable") {
722 self.get_bucket_metadata_table_config(account_id, b)
723 } else if req.query_params.contains_key("session") {
724 self.create_session(account_id, &req, b)
725 } else if req.query_params.get("list-type").map(|s| s.as_str()) == Some("2") {
726 self.list_objects_v2(account_id, &req, b)
727 } else if req.query_params.is_empty() {
728 let website_config = {
730 let accounts = self.state.read();
731 let _empty_s3 = crate::state::S3State::new(&req.account_id, &req.region);
732 let state = accounts.get(&req.account_id).unwrap_or(&_empty_s3);
733 state
734 .buckets
735 .get(b)
736 .and_then(|bkt| bkt.website_config.clone())
737 };
738 if let Some(ref config) = website_config {
739 if let Some(index_doc) = extract_xml_value(config, "Suffix").or_else(|| {
740 extract_xml_value(config, "IndexDocument").and_then(|inner| {
741 let open = "<Suffix>";
742 let close = "</Suffix>";
743 let s = inner.find(open)? + open.len();
744 let e = inner.find(close)?;
745 Some(inner[s..e].trim().to_string())
746 })
747 }) {
748 self.serve_website_object(account_id, &req, b, &index_doc, config)
749 } else {
750 self.list_objects_v1(account_id, &req, b)
751 }
752 } else {
753 self.list_objects_v1(account_id, &req, b)
754 }
755 } else {
756 self.list_objects_v1(account_id, &req, b)
757 }
758 }
759
760 (&Method::PUT, Some(b), Some(k)) => {
762 if req.query_params.contains_key("tagging") {
763 self.put_object_tagging(account_id, &req, b, k)
764 } else if req.query_params.contains_key("acl") {
765 self.put_object_acl(account_id, &req, b, k)
766 } else if req.query_params.contains_key("retention") {
767 self.put_object_retention(account_id, &req, b, k)
768 } else if req.query_params.contains_key("legal-hold") {
769 self.put_object_legal_hold(account_id, &req, b, k)
770 } else if req.query_params.contains_key("renameObject") {
771 self.rename_object(account_id, &req, b, k)
772 } else if req.query_params.contains_key("encryption") {
773 self.update_object_encryption(account_id, &req, b, k)
774 } else if req.headers.contains_key("x-amz-copy-source") {
775 self.copy_object(account_id, &req, b, k)
776 } else {
777 self.put_object(account_id, &req, b, k).await
778 }
779 }
780 (&Method::GET, Some(b), Some(k)) => {
781 if req.query_params.contains_key("tagging") {
782 self.get_object_tagging(account_id, &req, b, k)
783 } else if req.query_params.contains_key("acl") {
784 self.get_object_acl(account_id, &req, b, k)
785 } else if req.query_params.contains_key("retention") {
786 self.get_object_retention(account_id, &req, b, k)
787 } else if req.query_params.contains_key("legal-hold") {
788 self.get_object_legal_hold(account_id, &req, b, k)
789 } else if req.query_params.contains_key("attributes") {
790 self.get_object_attributes(account_id, &req, b, k)
791 } else if req.query_params.contains_key("torrent") {
792 self.get_object_torrent(account_id, &req, b, k)
793 } else {
794 let result = self.get_object(account_id, &req, b, k);
795 let is_not_found = matches!(
797 &result,
798 Err(e) if e.code() == "NoSuchKey"
799 );
800 if is_not_found {
801 let website_config = {
802 let accounts = self.state.read();
803 let _empty_s3 =
804 crate::state::S3State::new(&req.account_id, &req.region);
805 let state = accounts.get(&req.account_id).unwrap_or(&_empty_s3);
806 state
807 .buckets
808 .get(b)
809 .and_then(|bkt| bkt.website_config.clone())
810 };
811 if let Some(ref config) = website_config {
812 if let Some(error_key) = extract_xml_value(config, "ErrorDocument")
813 .and_then(|inner| {
814 let open = "<Key>";
815 let close = "</Key>";
816 let s = inner.find(open)? + open.len();
817 let e = inner.find(close)?;
818 Some(inner[s..e].trim().to_string())
819 })
820 .or_else(|| extract_xml_value(config, "Key"))
821 {
822 return self.serve_website_error(account_id, &req, b, &error_key);
823 }
824 }
825 }
826 result
827 }
828 }
829 (&Method::DELETE, Some(b), Some(k)) => {
830 if req.query_params.contains_key("tagging") {
831 self.delete_object_tagging(account_id, b, k)
832 } else {
833 self.delete_object(account_id, &req, b, k)
834 }
835 }
836 (&Method::HEAD, Some(b), Some(k)) => self.head_object(account_id, &req, b, k),
837
838 (&Method::POST, Some(b), None) if req.query_params.contains_key("delete") => {
840 self.delete_objects(account_id, &req, b)
841 }
842 (&Method::POST, Some(b), None)
843 if req.query_params.contains_key("metadataConfiguration") =>
844 {
845 self.create_bucket_metadata_config(account_id, &req, b)
846 }
847 (&Method::POST, Some(b), None) if req.query_params.contains_key("metadataTable") => {
848 self.create_bucket_metadata_table_config(account_id, &req, b)
849 }
850 (&Method::POST, Some(b), Some(k))
851 if req.query_params.get("select-type").map(|s| s.as_str()) == Some("2") =>
852 {
853 self.select_object_content(account_id, &req, b, k)
854 }
855 (&Method::POST, Some("WriteGetObjectResponse"), None) => {
856 self.write_get_object_response(account_id, &req)
857 }
858
859 _ => Err(AwsServiceError::aws_error(
860 StatusCode::METHOD_NOT_ALLOWED,
861 "MethodNotAllowed",
862 "The specified method is not allowed against this resource",
863 )),
864 };
865
866 if let (Some(ref origin), Some(b_name)) = (&origin_header, bucket) {
868 let cors_config = {
869 let accounts = self.state.read();
870 let _empty_s3 = crate::state::S3State::new(&req.account_id, &req.region);
871 let state = accounts.get(&req.account_id).unwrap_or(&_empty_s3);
872 state
873 .buckets
874 .get(b_name)
875 .and_then(|b| b.cors_config.clone())
876 };
877 if let Some(ref config) = cors_config {
878 let rules = parse_cors_config(config);
879 if let Some(rule) = find_cors_rule(&rules, origin, None) {
880 if let Ok(ref mut resp) = result {
881 let matched_origin = if rule.allowed_origins.contains(&"*".to_string()) {
882 "*"
883 } else {
884 origin
885 };
886 resp.headers.insert(
887 "access-control-allow-origin",
888 matched_origin
889 .parse()
890 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
891 );
892 if !rule.expose_headers.is_empty() {
893 resp.headers.insert(
894 "access-control-expose-headers",
895 rule.expose_headers
896 .join(", ")
897 .parse()
898 .unwrap_or_else(|_| http::HeaderValue::from_static("")),
899 );
900 }
901 }
902 }
903 }
904 }
905
906 if let Some(b_name) = bucket {
908 let status_code = match &result {
909 Ok(resp) => resp.status.as_u16(),
910 Err(e) => e.status().as_u16(),
911 };
912 let op = logging::operation_name(&req.method, key.as_deref());
913 logging::maybe_write_access_log(
914 &self.state,
915 &self.store,
916 b_name,
917 &logging::AccessLogRequest {
918 operation: op,
919 key: key.as_deref(),
920 status: status_code,
921 request_id: &req.request_id,
922 method: req.method.as_str(),
923 path: &req.raw_path,
924 },
925 );
926 }
927
928 result
929 }
930
931 fn supported_actions(&self) -> &[&str] {
932 &[
933 "ListBuckets",
935 "CreateBucket",
936 "DeleteBucket",
937 "HeadBucket",
938 "GetBucketLocation",
939 "PutObject",
941 "GetObject",
942 "DeleteObject",
943 "HeadObject",
944 "CopyObject",
945 "DeleteObjects",
946 "ListObjectsV2",
947 "ListObjects",
948 "ListObjectVersions",
949 "GetObjectAttributes",
950 "RestoreObject",
951 "PutObjectTagging",
953 "GetObjectTagging",
954 "DeleteObjectTagging",
955 "PutObjectAcl",
956 "GetObjectAcl",
957 "PutObjectRetention",
958 "GetObjectRetention",
959 "PutObjectLegalHold",
960 "GetObjectLegalHold",
961 "PutBucketTagging",
963 "GetBucketTagging",
964 "DeleteBucketTagging",
965 "PutBucketAcl",
966 "GetBucketAcl",
967 "PutBucketVersioning",
968 "GetBucketVersioning",
969 "PutBucketCors",
970 "GetBucketCors",
971 "DeleteBucketCors",
972 "PutBucketNotificationConfiguration",
973 "GetBucketNotificationConfiguration",
974 "PutBucketWebsite",
975 "GetBucketWebsite",
976 "DeleteBucketWebsite",
977 "PutBucketAccelerateConfiguration",
978 "GetBucketAccelerateConfiguration",
979 "PutPublicAccessBlock",
980 "GetPublicAccessBlock",
981 "DeletePublicAccessBlock",
982 "PutBucketEncryption",
983 "GetBucketEncryption",
984 "DeleteBucketEncryption",
985 "PutBucketLifecycleConfiguration",
986 "GetBucketLifecycleConfiguration",
987 "DeleteBucketLifecycle",
988 "PutBucketLogging",
989 "GetBucketLogging",
990 "PutBucketPolicy",
991 "GetBucketPolicy",
992 "DeleteBucketPolicy",
993 "PutObjectLockConfiguration",
994 "GetObjectLockConfiguration",
995 "PutBucketReplication",
996 "GetBucketReplication",
997 "DeleteBucketReplication",
998 "PutBucketOwnershipControls",
999 "GetBucketOwnershipControls",
1000 "DeleteBucketOwnershipControls",
1001 "PutBucketInventoryConfiguration",
1002 "GetBucketInventoryConfiguration",
1003 "DeleteBucketInventoryConfiguration",
1004 "ListBucketInventoryConfigurations",
1005 "PutBucketAnalyticsConfiguration",
1006 "GetBucketAnalyticsConfiguration",
1007 "DeleteBucketAnalyticsConfiguration",
1008 "ListBucketAnalyticsConfigurations",
1009 "PutBucketIntelligentTieringConfiguration",
1010 "GetBucketIntelligentTieringConfiguration",
1011 "DeleteBucketIntelligentTieringConfiguration",
1012 "ListBucketIntelligentTieringConfigurations",
1013 "PutBucketMetricsConfiguration",
1014 "GetBucketMetricsConfiguration",
1015 "DeleteBucketMetricsConfiguration",
1016 "ListBucketMetricsConfigurations",
1017 "PutBucketRequestPayment",
1018 "GetBucketRequestPayment",
1019 "PutBucketAbac",
1020 "GetBucketAbac",
1021 "GetBucketPolicyStatus",
1022 "CreateBucketMetadataConfiguration",
1023 "GetBucketMetadataConfiguration",
1024 "DeleteBucketMetadataConfiguration",
1025 "CreateBucketMetadataTableConfiguration",
1026 "GetBucketMetadataTableConfiguration",
1027 "DeleteBucketMetadataTableConfiguration",
1028 "UpdateBucketMetadataInventoryTableConfiguration",
1029 "UpdateBucketMetadataJournalTableConfiguration",
1030 "GetObjectTorrent",
1031 "RenameObject",
1032 "SelectObjectContent",
1033 "UpdateObjectEncryption",
1034 "WriteGetObjectResponse",
1035 "ListDirectoryBuckets",
1036 "CreateSession",
1037 "CreateMultipartUpload",
1039 "UploadPart",
1040 "UploadPartCopy",
1041 "CompleteMultipartUpload",
1042 "AbortMultipartUpload",
1043 "ListParts",
1044 "ListMultipartUploads",
1045 ]
1046 }
1047
1048 fn iam_enforceable(&self) -> bool {
1049 true
1050 }
1051
1052 fn iam_action_for(&self, request: &AwsRequest) -> Option<fakecloud_core::auth::IamAction> {
1062 let bucket = request.path_segments.first().map(|s| s.as_str());
1067 let key = if request.path_segments.len() > 1 {
1068 Some(request.path_segments[1..].join("/"))
1069 } else {
1070 None
1071 };
1072 let action = s3_detect_action(
1073 request.method.as_str(),
1074 bucket,
1075 key.as_deref(),
1076 &request.query_params,
1077 )?;
1078 let resource = s3_resource_for(action, bucket, key.as_deref());
1079 Some(fakecloud_core::auth::IamAction {
1080 service: "s3",
1081 action,
1082 resource,
1083 })
1084 }
1085
1086 fn iam_condition_keys_for(
1087 &self,
1088 request: &AwsRequest,
1089 action: &fakecloud_core::auth::IamAction,
1090 ) -> std::collections::BTreeMap<String, Vec<String>> {
1091 s3_condition_keys(action.action, &request.query_params)
1092 }
1093
1094 fn resource_tags_for(
1095 &self,
1096 resource_arn: &str,
1097 ) -> Option<std::collections::HashMap<String, String>> {
1098 s3_resource_tags(&self.state, resource_arn)
1099 .map(|m| m.into_iter().collect::<std::collections::HashMap<_, _>>())
1100 }
1101
1102 fn request_tags_from(
1103 &self,
1104 request: &AwsRequest,
1105 action: &str,
1106 ) -> Option<std::collections::HashMap<String, String>> {
1107 s3_request_tags(request, action)
1108 .map(|m| m.into_iter().collect::<std::collections::HashMap<_, _>>())
1109 }
1110}
1111
1112fn s3_condition_keys(
1119 action: &str,
1120 query: &std::collections::HashMap<String, String>,
1121) -> std::collections::BTreeMap<String, Vec<String>> {
1122 let mut out = std::collections::BTreeMap::new();
1123 if matches!(action, "ListObjects" | "ListObjectsV2") {
1124 if let Some(prefix) = query.get("prefix") {
1126 out.insert("s3:prefix".to_string(), vec![prefix.clone()]);
1127 }
1128 if let Some(delimiter) = query.get("delimiter") {
1129 out.insert("s3:delimiter".to_string(), vec![delimiter.clone()]);
1130 }
1131 if let Some(max_keys) = query.get("max-keys") {
1132 out.insert("s3:max-keys".to_string(), vec![max_keys.clone()]);
1133 }
1134 }
1135 out
1136}
1137
1138fn s3_resource_tags(
1144 state: &SharedS3State,
1145 resource_arn: &str,
1146) -> Option<std::collections::BTreeMap<String, String>> {
1147 if resource_arn == "*" {
1148 return Some(std::collections::BTreeMap::new());
1149 }
1150 let after_prefix = resource_arn.strip_prefix("arn:aws:s3:::")?;
1152 let mas = state.read();
1153 let bucket_name = after_prefix.split('/').next().unwrap_or(after_prefix);
1155 let state = mas
1156 .find_account(|s| s.buckets.contains_key(bucket_name))
1157 .and_then(|id| mas.get(id))
1158 .or_else(|| Some(mas.default_ref()))?;
1159 if let Some(slash_pos) = after_prefix.find('/') {
1160 let bucket_name = &after_prefix[..slash_pos];
1162 let key = &after_prefix[slash_pos + 1..];
1163 let bucket = state.buckets.get(bucket_name)?;
1164 if let Some(obj) = bucket.objects.get(key) {
1166 Some(obj.tags.clone())
1167 } else if let Some(versions) = bucket.object_versions.get(key) {
1168 versions.last().map(|v| v.tags.clone())
1169 } else {
1170 Some(std::collections::BTreeMap::new())
1172 }
1173 } else {
1174 let bucket = state.buckets.get(after_prefix)?;
1176 Some(bucket.tags.clone())
1177 }
1178}
1179
1180fn s3_request_tags(
1186 request: &AwsRequest,
1187 action: &str,
1188) -> Option<std::collections::BTreeMap<String, String>> {
1189 match action {
1190 "PutObject" | "CopyObject" | "CreateMultipartUpload" => {
1191 if let Some(tagging) = request.headers.get("x-amz-tagging") {
1193 let tags = parse_url_encoded_tags(tagging.to_str().unwrap_or(""));
1194 Some(tags.into_iter().collect())
1195 } else {
1196 Some(std::collections::BTreeMap::new())
1197 }
1198 }
1199 "PutBucketTagging" | "PutObjectTagging" => {
1200 let body = std::str::from_utf8(&request.body).unwrap_or("");
1202 let tags = parse_tagging_xml(body);
1203 Some(tags.into_iter().collect())
1204 }
1205 _ => Some(std::collections::BTreeMap::new()),
1206 }
1207}
1208
1209fn s3_detect_action(
1222 method: &str,
1223 bucket: Option<&str>,
1224 key: Option<&str>,
1225 query: &std::collections::HashMap<String, String>,
1226) -> Option<&'static str> {
1227 let has = |q: &str| query.contains_key(q);
1228 let is_get = method == "GET";
1229 let is_put = method == "PUT";
1230 let is_post = method == "POST";
1231 let is_delete = method == "DELETE";
1232
1233 if bucket.is_none() {
1235 return match method {
1236 "GET" => Some("ListBuckets"),
1237 _ => None,
1238 };
1239 }
1240 let has_key = key.is_some();
1241
1242 if has_key && is_post && has("uploads") {
1244 return Some("CreateMultipartUpload");
1245 }
1246 if has_key && is_post && has("uploadId") {
1247 return Some("CompleteMultipartUpload");
1248 }
1249 if has_key && is_put && has("partNumber") && has("uploadId") {
1250 return Some("UploadPart");
1251 }
1252 if has_key && is_delete && has("uploadId") {
1253 return Some("AbortMultipartUpload");
1254 }
1255 if has_key && is_get && has("uploadId") {
1256 return Some("ListParts");
1257 }
1258 if !has_key && is_get && has("uploads") {
1259 return Some("ListMultipartUploads");
1260 }
1261
1262 if has_key {
1266 if has("tagging") {
1267 return Some(match method {
1268 "GET" => "GetObjectTagging",
1269 "PUT" => "PutObjectTagging",
1270 "DELETE" => "DeleteObjectTagging",
1271 _ => return None,
1272 });
1273 }
1274 if has("acl") {
1275 return Some(match method {
1276 "GET" => "GetObjectAcl",
1277 "PUT" => "PutObjectAcl",
1278 _ => return None,
1279 });
1280 }
1281 if has("retention") {
1282 return Some(match method {
1283 "GET" => "GetObjectRetention",
1284 "PUT" => "PutObjectRetention",
1285 _ => return None,
1286 });
1287 }
1288 if has("legal-hold") {
1289 return Some(match method {
1290 "GET" => "GetObjectLegalHold",
1291 "PUT" => "PutObjectLegalHold",
1292 _ => return None,
1293 });
1294 }
1295 if has("attributes") && is_get {
1300 return Some("GetObjectAttributes");
1301 }
1302 if has("restore") && is_post {
1303 return Some("RestoreObject");
1304 }
1305 }
1306
1307 if !has_key {
1309 if has("tagging") {
1310 return Some(match method {
1311 "GET" => "GetBucketTagging",
1312 "PUT" => "PutBucketTagging",
1313 "DELETE" => "DeleteBucketTagging",
1314 _ => return None,
1315 });
1316 }
1317 if has("acl") {
1318 return Some(match method {
1319 "GET" => "GetBucketAcl",
1320 "PUT" => "PutBucketAcl",
1321 _ => return None,
1322 });
1323 }
1324 if has("versioning") {
1325 return Some(match method {
1326 "GET" => "GetBucketVersioning",
1327 "PUT" => "PutBucketVersioning",
1328 _ => return None,
1329 });
1330 }
1331 if has("cors") {
1332 return Some(match method {
1333 "GET" => "GetBucketCors",
1334 "PUT" => "PutBucketCors",
1335 "DELETE" => "DeleteBucketCors",
1336 _ => return None,
1337 });
1338 }
1339 if has("policy") {
1340 return Some(match method {
1341 "GET" => "GetBucketPolicy",
1342 "PUT" => "PutBucketPolicy",
1343 "DELETE" => "DeleteBucketPolicy",
1344 _ => return None,
1345 });
1346 }
1347 if has("website") {
1348 return Some(match method {
1349 "GET" => "GetBucketWebsite",
1350 "PUT" => "PutBucketWebsite",
1351 "DELETE" => "DeleteBucketWebsite",
1352 _ => return None,
1353 });
1354 }
1355 if has("lifecycle") {
1356 return Some(match method {
1357 "GET" => "GetBucketLifecycleConfiguration",
1358 "PUT" => "PutBucketLifecycleConfiguration",
1359 "DELETE" => "DeleteBucketLifecycle",
1360 _ => return None,
1361 });
1362 }
1363 if has("encryption") {
1364 return Some(match method {
1365 "GET" => "GetBucketEncryption",
1366 "PUT" => "PutBucketEncryption",
1367 "DELETE" => "DeleteBucketEncryption",
1368 _ => return None,
1369 });
1370 }
1371 if has("logging") {
1372 return Some(match method {
1373 "GET" => "GetBucketLogging",
1374 "PUT" => "PutBucketLogging",
1375 _ => return None,
1376 });
1377 }
1378 if has("notification") {
1379 return Some(match method {
1380 "GET" => "GetBucketNotificationConfiguration",
1381 "PUT" => "PutBucketNotificationConfiguration",
1382 _ => return None,
1383 });
1384 }
1385 if has("replication") {
1386 return Some(match method {
1387 "GET" => "GetBucketReplication",
1388 "PUT" => "PutBucketReplication",
1389 "DELETE" => "DeleteBucketReplication",
1390 _ => return None,
1391 });
1392 }
1393 if has("ownershipControls") {
1394 return Some(match method {
1395 "GET" => "GetBucketOwnershipControls",
1396 "PUT" => "PutBucketOwnershipControls",
1397 "DELETE" => "DeleteBucketOwnershipControls",
1398 _ => return None,
1399 });
1400 }
1401 if has("publicAccessBlock") {
1402 return Some(match method {
1403 "GET" => "GetPublicAccessBlock",
1404 "PUT" => "PutPublicAccessBlock",
1405 "DELETE" => "DeletePublicAccessBlock",
1406 _ => return None,
1407 });
1408 }
1409 if has("accelerate") {
1410 return Some(match method {
1411 "GET" => "GetBucketAccelerateConfiguration",
1412 "PUT" => "PutBucketAccelerateConfiguration",
1413 _ => return None,
1414 });
1415 }
1416 if has("inventory") {
1417 return Some(match method {
1418 "GET" => "GetBucketInventoryConfiguration",
1419 "PUT" => "PutBucketInventoryConfiguration",
1420 "DELETE" => "DeleteBucketInventoryConfiguration",
1421 _ => return None,
1422 });
1423 }
1424 if has("analytics") {
1425 return Some(match method {
1426 "GET" if has("id") => "GetBucketAnalyticsConfiguration",
1427 "GET" => "ListBucketAnalyticsConfigurations",
1428 "PUT" => "PutBucketAnalyticsConfiguration",
1429 "DELETE" => "DeleteBucketAnalyticsConfiguration",
1430 _ => return None,
1431 });
1432 }
1433 if has("intelligent-tiering") {
1434 return Some(match method {
1435 "GET" if has("id") => "GetBucketIntelligentTieringConfiguration",
1436 "GET" => "ListBucketIntelligentTieringConfigurations",
1437 "PUT" => "PutBucketIntelligentTieringConfiguration",
1438 "DELETE" => "DeleteBucketIntelligentTieringConfiguration",
1439 _ => return None,
1440 });
1441 }
1442 if has("metrics") {
1443 return Some(match method {
1444 "GET" if has("id") => "GetBucketMetricsConfiguration",
1445 "GET" => "ListBucketMetricsConfigurations",
1446 "PUT" => "PutBucketMetricsConfiguration",
1447 "DELETE" => "DeleteBucketMetricsConfiguration",
1448 _ => return None,
1449 });
1450 }
1451 if has("requestPayment") {
1452 return Some(match method {
1453 "GET" => "GetBucketRequestPayment",
1454 "PUT" => "PutBucketRequestPayment",
1455 _ => return None,
1456 });
1457 }
1458 if has("policyStatus") && is_get {
1459 return Some("GetBucketPolicyStatus");
1460 }
1461 if has("metadataConfiguration") {
1462 return Some(match method {
1463 "GET" => "GetBucketMetadataConfiguration",
1464 "POST" => "CreateBucketMetadataConfiguration",
1465 "DELETE" => "DeleteBucketMetadataConfiguration",
1466 _ => return None,
1467 });
1468 }
1469 if has("metadataTable") {
1470 return Some(match method {
1471 "GET" => "GetBucketMetadataTableConfiguration",
1472 "POST" => "CreateBucketMetadataTableConfiguration",
1473 "DELETE" => "DeleteBucketMetadataTableConfiguration",
1474 _ => return None,
1475 });
1476 }
1477 if has("metadataInventoryTable") && is_put {
1478 return Some("UpdateBucketMetadataInventoryTableConfiguration");
1479 }
1480 if has("metadataJournalTable") && is_put {
1481 return Some("UpdateBucketMetadataJournalTableConfiguration");
1482 }
1483 if has("abac") && is_put {
1484 return Some("PutBucketAbacConfiguration");
1485 }
1486 if has("renameObject") && is_put {
1487 return Some("RenameObject");
1488 }
1489 if has("object-lock") {
1490 return Some(match method {
1491 "GET" => "GetObjectLockConfiguration",
1492 "PUT" => "PutObjectLockConfiguration",
1493 _ => return None,
1494 });
1495 }
1496 if has("location") {
1497 return Some("GetBucketLocation");
1498 }
1499 if is_post && has("delete") {
1500 return Some("DeleteObjects");
1501 }
1502 if is_get && has("versions") {
1503 return Some("ListObjectVersions");
1504 }
1505 }
1506
1507 match (method, has_key) {
1509 ("GET", true) => Some("GetObject"),
1510 ("PUT", true) => {
1511 Some("PutObject")
1517 }
1518 ("DELETE", true) => Some("DeleteObject"),
1519 ("HEAD", true) => Some("HeadObject"),
1520 ("GET", false) => {
1521 if query.contains_key("list-type") {
1522 Some("ListObjectsV2")
1523 } else {
1524 Some("ListObjects")
1525 }
1526 }
1527 ("PUT", false) => Some("CreateBucket"),
1528 ("DELETE", false) => Some("DeleteBucket"),
1529 ("HEAD", false) => Some("HeadBucket"),
1530 _ => None,
1531 }
1532}
1533
1534fn s3_resource_for(action: &'static str, bucket: Option<&str>, key: Option<&str>) -> String {
1538 const OBJECT_ACTIONS: &[&str] = &[
1540 "PutObject",
1541 "GetObject",
1542 "DeleteObject",
1543 "HeadObject",
1544 "CopyObject",
1545 "GetObjectAttributes",
1546 "RestoreObject",
1547 "PutObjectTagging",
1548 "GetObjectTagging",
1549 "DeleteObjectTagging",
1550 "PutObjectAcl",
1551 "GetObjectAcl",
1552 "PutObjectRetention",
1553 "GetObjectRetention",
1554 "PutObjectLegalHold",
1555 "GetObjectLegalHold",
1556 "CreateMultipartUpload",
1557 "UploadPart",
1558 "UploadPartCopy",
1559 "CompleteMultipartUpload",
1560 "AbortMultipartUpload",
1561 "ListParts",
1562 ];
1563 if action == "ListBuckets" {
1564 return "*".to_string();
1565 }
1566 let Some(bucket) = bucket else {
1567 return "*".to_string();
1568 };
1569 if OBJECT_ACTIONS.contains(&action) {
1570 match key {
1571 Some(k) if !k.is_empty() => Arn::s3(&format!("{bucket}/{k}")).to_string(),
1572 _ => Arn::s3(&format!("{bucket}/*")).to_string(),
1573 }
1574 } else {
1575 Arn::s3(bucket).to_string()
1577 }
1578}
1579
1580pub(crate) fn truncate_to_seconds(dt: DateTime<Utc>) -> DateTime<Utc> {
1584 dt.with_nanosecond(0).unwrap_or(dt)
1585}
1586
1587pub(crate) fn check_get_conditionals(
1588 req: &AwsRequest,
1589 obj: &S3Object,
1590) -> Result<(), AwsServiceError> {
1591 let obj_etag = format!("\"{}\"", obj.etag);
1592 let obj_time = truncate_to_seconds(obj.last_modified);
1593
1594 if let Some(if_match) = req.headers.get("if-match").and_then(|v| v.to_str().ok()) {
1596 if !etag_matches(if_match, &obj_etag) {
1597 return Err(precondition_failed("If-Match"));
1598 }
1599 }
1600
1601 if let Some(if_none_match) = req
1603 .headers
1604 .get("if-none-match")
1605 .and_then(|v| v.to_str().ok())
1606 {
1607 if etag_matches(if_none_match, &obj_etag) {
1608 return Err(not_modified_with_etag(&obj_etag));
1609 }
1610 }
1611
1612 if let Some(since) = req
1614 .headers
1615 .get("if-unmodified-since")
1616 .and_then(|v| v.to_str().ok())
1617 {
1618 if let Some(dt) = parse_http_date(since) {
1619 if obj_time > dt {
1620 return Err(precondition_failed("If-Unmodified-Since"));
1621 }
1622 }
1623 }
1624
1625 if let Some(since) = req
1627 .headers
1628 .get("if-modified-since")
1629 .and_then(|v| v.to_str().ok())
1630 {
1631 if let Some(dt) = parse_http_date(since) {
1632 if obj_time <= dt {
1633 return Err(not_modified());
1634 }
1635 }
1636 }
1637
1638 Ok(())
1639}
1640
1641pub(crate) fn check_head_conditionals(
1642 req: &AwsRequest,
1643 obj: &S3Object,
1644) -> Result<(), AwsServiceError> {
1645 let obj_etag = format!("\"{}\"", obj.etag);
1646 let obj_time = truncate_to_seconds(obj.last_modified);
1647
1648 if let Some(if_match) = req.headers.get("if-match").and_then(|v| v.to_str().ok()) {
1650 if !etag_matches(if_match, &obj_etag) {
1651 return Err(AwsServiceError::aws_error(
1652 StatusCode::PRECONDITION_FAILED,
1653 "412",
1654 "Precondition Failed",
1655 ));
1656 }
1657 }
1658
1659 if let Some(if_none_match) = req
1661 .headers
1662 .get("if-none-match")
1663 .and_then(|v| v.to_str().ok())
1664 {
1665 if etag_matches(if_none_match, &obj_etag) {
1666 return Err(not_modified_with_etag(&obj_etag));
1667 }
1668 }
1669
1670 if let Some(since) = req
1672 .headers
1673 .get("if-unmodified-since")
1674 .and_then(|v| v.to_str().ok())
1675 {
1676 if let Some(dt) = parse_http_date(since) {
1677 if obj_time > dt {
1678 return Err(AwsServiceError::aws_error(
1679 StatusCode::PRECONDITION_FAILED,
1680 "412",
1681 "Precondition Failed",
1682 ));
1683 }
1684 }
1685 }
1686
1687 if let Some(since) = req
1689 .headers
1690 .get("if-modified-since")
1691 .and_then(|v| v.to_str().ok())
1692 {
1693 if let Some(dt) = parse_http_date(since) {
1694 if obj_time <= dt {
1695 return Err(not_modified());
1696 }
1697 }
1698 }
1699
1700 Ok(())
1701}
1702
1703pub(crate) fn etag_matches(condition: &str, obj_etag: &str) -> bool {
1704 let condition = condition.trim();
1705 if condition == "*" {
1706 return true;
1707 }
1708 let clean_etag = obj_etag.replace('"', "");
1709 for part in condition.split(',') {
1711 let part = part.trim().replace('"', "");
1712 if part == clean_etag {
1713 return true;
1714 }
1715 }
1716 false
1717}
1718
1719pub(crate) fn parse_http_date(s: &str) -> Option<DateTime<Utc>> {
1720 if let Ok(dt) = DateTime::parse_from_rfc2822(s) {
1722 return Some(dt.with_timezone(&Utc));
1723 }
1724 if let Ok(dt) = DateTime::parse_from_rfc3339(s) {
1726 return Some(dt.with_timezone(&Utc));
1727 }
1728 if let Ok(dt) =
1730 chrono::NaiveDateTime::parse_from_str(s.trim_end_matches(" GMT"), "%a, %d %b %Y %H:%M:%S")
1731 {
1732 return Some(dt.and_utc());
1733 }
1734 if let Ok(dt) = s.parse::<DateTime<Utc>>() {
1736 return Some(dt);
1737 }
1738 None
1739}
1740
1741pub(crate) fn not_modified() -> AwsServiceError {
1742 AwsServiceError::aws_error(StatusCode::NOT_MODIFIED, "304", "Not Modified")
1743}
1744
1745pub(crate) fn not_modified_with_etag(etag: &str) -> AwsServiceError {
1746 AwsServiceError::aws_error_with_headers(
1747 StatusCode::NOT_MODIFIED,
1748 "304",
1749 "Not Modified",
1750 vec![("etag".to_string(), etag.to_string())],
1751 )
1752}
1753
1754pub(crate) fn precondition_failed(condition: &str) -> AwsServiceError {
1755 AwsServiceError::aws_error_with_fields(
1756 StatusCode::PRECONDITION_FAILED,
1757 "PreconditionFailed",
1758 "At least one of the pre-conditions you specified did not hold",
1759 vec![("Condition".to_string(), condition.to_string())],
1760 )
1761}
1762
1763pub(crate) fn build_acl_xml(owner_id: &str, grants: &[AclGrant], _account_id: &str) -> String {
1766 let mut grants_xml = String::new();
1767 for g in grants {
1768 let grantee_xml = if g.grantee_type == "Group" {
1769 let uri = g.grantee_uri.as_deref().unwrap_or("");
1770 format!(
1771 "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"Group\">\
1772 <URI>{}</URI></Grantee>",
1773 xml_escape(uri),
1774 )
1775 } else {
1776 let id = g.grantee_id.as_deref().unwrap_or("");
1777 format!(
1778 "<Grantee xmlns:xsi=\"http://www.w3.org/2001/XMLSchema-instance\" xsi:type=\"CanonicalUser\">\
1779 <ID>{}</ID></Grantee>",
1780 xml_escape(id),
1781 )
1782 };
1783 grants_xml.push_str(&format!(
1784 "<Grant>{grantee_xml}<Permission>{}</Permission></Grant>",
1785 xml_escape(&g.permission),
1786 ));
1787 }
1788
1789 format!(
1790 "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\
1791 <AccessControlPolicy xmlns=\"http://s3.amazonaws.com/doc/2006-03-01/\">\
1792 <Owner><ID>{owner_id}</ID><DisplayName>{owner_id}</DisplayName></Owner>\
1793 <AccessControlList>{grants_xml}</AccessControlList>\
1794 </AccessControlPolicy>",
1795 owner_id = xml_escape(owner_id),
1796 )
1797}
1798
1799pub(crate) fn canned_acl_grants(acl: &str, owner_id: &str) -> Vec<AclGrant> {
1800 let owner_grant = AclGrant {
1801 grantee_type: "CanonicalUser".to_string(),
1802 grantee_id: Some(owner_id.to_string()),
1803 grantee_display_name: Some(owner_id.to_string()),
1804 grantee_uri: None,
1805 permission: "FULL_CONTROL".to_string(),
1806 };
1807 match acl {
1808 "private" => vec![owner_grant],
1809 "public-read" => vec![
1810 owner_grant,
1811 AclGrant {
1812 grantee_type: "Group".to_string(),
1813 grantee_id: None,
1814 grantee_display_name: None,
1815 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
1816 permission: "READ".to_string(),
1817 },
1818 ],
1819 "public-read-write" => vec![
1820 owner_grant,
1821 AclGrant {
1822 grantee_type: "Group".to_string(),
1823 grantee_id: None,
1824 grantee_display_name: None,
1825 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
1826 permission: "READ".to_string(),
1827 },
1828 AclGrant {
1829 grantee_type: "Group".to_string(),
1830 grantee_id: None,
1831 grantee_display_name: None,
1832 grantee_uri: Some("http://acs.amazonaws.com/groups/global/AllUsers".to_string()),
1833 permission: "WRITE".to_string(),
1834 },
1835 ],
1836 "authenticated-read" => vec![
1837 owner_grant,
1838 AclGrant {
1839 grantee_type: "Group".to_string(),
1840 grantee_id: None,
1841 grantee_display_name: None,
1842 grantee_uri: Some(
1843 "http://acs.amazonaws.com/groups/global/AuthenticatedUsers".to_string(),
1844 ),
1845 permission: "READ".to_string(),
1846 },
1847 ],
1848 "bucket-owner-full-control" => vec![owner_grant],
1849 _ => vec![owner_grant],
1850 }
1851}
1852
1853pub(crate) fn canned_acl_grants_for_object(acl: &str, owner_id: &str) -> Vec<AclGrant> {
1854 canned_acl_grants(acl, owner_id)
1856}
1857
1858pub(crate) fn parse_grant_headers(headers: &HeaderMap) -> Vec<AclGrant> {
1859 let mut grants = Vec::new();
1860 let header_permission_map = [
1861 ("x-amz-grant-read", "READ"),
1862 ("x-amz-grant-write", "WRITE"),
1863 ("x-amz-grant-read-acp", "READ_ACP"),
1864 ("x-amz-grant-write-acp", "WRITE_ACP"),
1865 ("x-amz-grant-full-control", "FULL_CONTROL"),
1866 ];
1867
1868 for (header, permission) in &header_permission_map {
1869 if let Some(value) = headers.get(*header).and_then(|v| v.to_str().ok()) {
1870 for part in value.split(',') {
1872 let part = part.trim();
1873 if let Some((key, val)) = part.split_once('=') {
1874 let val = val.trim().trim_matches('"');
1875 let key = key.trim().to_lowercase();
1876 match key.as_str() {
1877 "id" => {
1878 grants.push(AclGrant {
1879 grantee_type: "CanonicalUser".to_string(),
1880 grantee_id: Some(val.to_string()),
1881 grantee_display_name: Some(val.to_string()),
1882 grantee_uri: None,
1883 permission: permission.to_string(),
1884 });
1885 }
1886 "uri" | "url" => {
1887 grants.push(AclGrant {
1888 grantee_type: "Group".to_string(),
1889 grantee_id: None,
1890 grantee_display_name: None,
1891 grantee_uri: Some(val.to_string()),
1892 permission: permission.to_string(),
1893 });
1894 }
1895 _ => {}
1896 }
1897 }
1898 }
1899 }
1900 }
1901 grants
1902}
1903
1904pub(crate) fn parse_acl_xml(xml: &str) -> Result<Vec<AclGrant>, AwsServiceError> {
1905 if xml.contains("<AccessControlPolicy") && !xml.contains("<Owner>") {
1907 return Err(AwsServiceError::aws_error(
1908 StatusCode::BAD_REQUEST,
1909 "MalformedACLError",
1910 "The XML you provided was not well-formed or did not validate against our published schema",
1911 ));
1912 }
1913
1914 let valid_permissions = ["READ", "WRITE", "READ_ACP", "WRITE_ACP", "FULL_CONTROL"];
1915
1916 let mut grants = Vec::new();
1917 let mut remaining = xml;
1918 while let Some(start) = remaining.find("<Grant>") {
1919 let after = &remaining[start + 7..];
1920 if let Some(end) = after.find("</Grant>") {
1921 let grant_body = &after[..end];
1922
1923 let permission = extract_xml_value(grant_body, "Permission").unwrap_or_default();
1925 if !valid_permissions.contains(&permission.as_str()) {
1926 return Err(AwsServiceError::aws_error(
1927 StatusCode::BAD_REQUEST,
1928 "MalformedACLError",
1929 "The XML you provided was not well-formed or did not validate against our published schema",
1930 ));
1931 }
1932
1933 if grant_body.contains("xsi:type=\"Group\"") || grant_body.contains("<URI>") {
1935 let uri = extract_xml_value(grant_body, "URI").unwrap_or_default();
1936 grants.push(AclGrant {
1937 grantee_type: "Group".to_string(),
1938 grantee_id: None,
1939 grantee_display_name: None,
1940 grantee_uri: Some(uri),
1941 permission,
1942 });
1943 } else {
1944 let id = extract_xml_value(grant_body, "ID").unwrap_or_default();
1945 let display =
1946 extract_xml_value(grant_body, "DisplayName").unwrap_or_else(|| id.clone());
1947 grants.push(AclGrant {
1948 grantee_type: "CanonicalUser".to_string(),
1949 grantee_id: Some(id),
1950 grantee_display_name: Some(display),
1951 grantee_uri: None,
1952 permission,
1953 });
1954 }
1955
1956 remaining = &after[end + 8..];
1957 } else {
1958 break;
1959 }
1960 }
1961 Ok(grants)
1962}
1963
1964pub(crate) enum RangeResult {
1967 Satisfiable { start: usize, end: usize },
1968 NotSatisfiable,
1969 Ignored,
1970}
1971
1972pub(crate) fn parse_range_header(range_str: &str, total_size: usize) -> Option<RangeResult> {
1973 let range_str = range_str.strip_prefix("bytes=")?;
1974 let (start_str, end_str) = range_str.split_once('-')?;
1975 if start_str.is_empty() {
1976 let suffix_len: usize = end_str.parse().ok()?;
1977 if suffix_len == 0 || total_size == 0 {
1978 return Some(RangeResult::NotSatisfiable);
1979 }
1980 let start = total_size.saturating_sub(suffix_len);
1981 Some(RangeResult::Satisfiable {
1982 start,
1983 end: total_size - 1,
1984 })
1985 } else {
1986 let start: usize = start_str.parse().ok()?;
1987 if start >= total_size {
1988 return Some(RangeResult::NotSatisfiable);
1989 }
1990 let end = if end_str.is_empty() {
1991 total_size - 1
1992 } else {
1993 let e: usize = end_str.parse().ok()?;
1994 if e < start {
1995 return Some(RangeResult::Ignored);
1996 }
1997 std::cmp::min(e, total_size - 1)
1998 };
1999 Some(RangeResult::Satisfiable { start, end })
2000 }
2001}
2002
2003pub(crate) fn s3_xml(status: StatusCode, body: impl Into<Bytes>) -> AwsResponse {
2007 AwsResponse {
2008 status,
2009 content_type: "application/xml".to_string(),
2010 body: body.into().into(),
2011 headers: HeaderMap::new(),
2012 }
2013}
2014
2015pub(crate) fn empty_response(status: StatusCode) -> AwsResponse {
2016 AwsResponse {
2017 status,
2018 content_type: "application/xml".to_string(),
2019 body: Bytes::new().into(),
2020 headers: HeaderMap::new(),
2021 }
2022}
2023
2024pub(crate) fn is_frozen(obj: &S3Object) -> bool {
2027 matches!(obj.storage_class.as_str(), "GLACIER" | "DEEP_ARCHIVE")
2028 && obj.restore_ongoing != Some(false)
2029}
2030
2031pub(crate) fn no_such_bucket(bucket: &str) -> AwsServiceError {
2032 AwsServiceError::aws_error_with_fields(
2033 StatusCode::NOT_FOUND,
2034 "NoSuchBucket",
2035 "The specified bucket does not exist",
2036 vec![("BucketName".to_string(), bucket.to_string())],
2037 )
2038}
2039
2040pub(crate) fn no_such_key(key: &str) -> AwsServiceError {
2041 AwsServiceError::aws_error_with_fields(
2042 StatusCode::NOT_FOUND,
2043 "NoSuchKey",
2044 "The specified key does not exist.",
2045 vec![("Key".to_string(), key.to_string())],
2046 )
2047}
2048
2049pub(crate) fn no_such_upload(upload_id: &str) -> AwsServiceError {
2050 AwsServiceError::aws_error_with_fields(
2051 StatusCode::NOT_FOUND,
2052 "NoSuchUpload",
2053 "The specified upload does not exist. The upload ID may be invalid, \
2054 or the upload may have been aborted or completed.",
2055 vec![("UploadId".to_string(), upload_id.to_string())],
2056 )
2057}
2058
2059pub(crate) fn no_such_key_with_detail(key: &str) -> AwsServiceError {
2060 AwsServiceError::aws_error_with_fields(
2061 StatusCode::NOT_FOUND,
2062 "NoSuchKey",
2063 "The specified key does not exist.",
2064 vec![("Key".to_string(), key.to_string())],
2065 )
2066}
2067
2068pub(crate) fn compute_md5(data: &[u8]) -> String {
2069 let digest = Md5::digest(data);
2070 format!("{:x}", digest)
2071}
2072
2073pub(crate) fn compute_checksum(algorithm: &str, data: &[u8]) -> String {
2074 match algorithm {
2075 "CRC32" => {
2076 let crc = crc32fast::hash(data);
2077 BASE64.encode(crc.to_be_bytes())
2078 }
2079 "SHA1" => {
2080 use sha1::Digest as _;
2081 let hash = sha1::Sha1::digest(data);
2082 BASE64.encode(hash)
2083 }
2084 "SHA256" => {
2085 use sha2::Digest as _;
2086 let hash = sha2::Sha256::digest(data);
2087 BASE64.encode(hash)
2088 }
2089 _ => String::new(),
2090 }
2091}
2092
2093pub(crate) async fn compute_checksum_streaming(
2099 algorithm: &str,
2100 path: &std::path::Path,
2101) -> Result<String, std::io::Error> {
2102 use tokio::io::AsyncReadExt;
2103 let mut file = tokio::fs::File::open(path).await?;
2104 let mut buf = vec![0u8; 1024 * 1024];
2105 match algorithm {
2106 "CRC32" => {
2107 let mut hasher = crc32fast::Hasher::new();
2108 loop {
2109 let n = file.read(&mut buf).await?;
2110 if n == 0 {
2111 break;
2112 }
2113 hasher.update(&buf[..n]);
2114 }
2115 Ok(BASE64.encode(hasher.finalize().to_be_bytes()))
2116 }
2117 "CRC32C" => {
2118 let mut crc: u32 = 0;
2119 loop {
2120 let n = file.read(&mut buf).await?;
2121 if n == 0 {
2122 break;
2123 }
2124 crc = crc32c::crc32c_append(crc, &buf[..n]);
2125 }
2126 Ok(BASE64.encode(crc.to_be_bytes()))
2127 }
2128 "CRC64NVME" => {
2129 let mut hasher = crc64fast_nvme::Digest::new();
2130 loop {
2131 let n = file.read(&mut buf).await?;
2132 if n == 0 {
2133 break;
2134 }
2135 hasher.write(&buf[..n]);
2136 }
2137 Ok(BASE64.encode(hasher.sum64().to_be_bytes()))
2138 }
2139 "SHA1" => {
2140 use sha1::Digest as _;
2141 let mut hasher = sha1::Sha1::new();
2142 loop {
2143 let n = file.read(&mut buf).await?;
2144 if n == 0 {
2145 break;
2146 }
2147 hasher.update(&buf[..n]);
2148 }
2149 Ok(BASE64.encode(hasher.finalize()))
2150 }
2151 "SHA256" => {
2152 use sha2::Digest as _;
2153 let mut hasher = sha2::Sha256::new();
2154 loop {
2155 let n = file.read(&mut buf).await?;
2156 if n == 0 {
2157 break;
2158 }
2159 hasher.update(&buf[..n]);
2160 }
2161 Ok(BASE64.encode(hasher.finalize()))
2162 }
2163 _ => Ok(String::new()),
2164 }
2165}
2166
2167pub(crate) fn url_encode_s3_key(s: &str) -> String {
2168 let mut out = String::with_capacity(s.len() * 2);
2169 for byte in s.bytes() {
2170 match byte {
2171 b'A'..=b'Z' | b'a'..=b'z' | b'0'..=b'9' | b'-' | b'_' | b'.' | b'~' | b'/' => {
2172 out.push(byte as char);
2173 }
2174 _ => {
2175 out.push_str(&format!("%{:02X}", byte));
2176 }
2177 }
2178 }
2179 out
2180}
2181
2182pub(crate) use fakecloud_aws::xml::xml_escape;
2183
2184pub(crate) fn extract_user_metadata(
2185 headers: &HeaderMap,
2186) -> std::collections::BTreeMap<String, String> {
2187 let mut meta = std::collections::BTreeMap::new();
2188 for (name, value) in headers {
2189 if let Some(key) = name.as_str().strip_prefix("x-amz-meta-") {
2190 if let Ok(v) = value.to_str() {
2191 meta.insert(key.to_string(), v.to_string());
2192 }
2193 }
2194 }
2195 meta
2196}
2197
2198pub(crate) fn is_valid_storage_class(class: &str) -> bool {
2199 matches!(
2200 class,
2201 "STANDARD"
2202 | "REDUCED_REDUNDANCY"
2203 | "STANDARD_IA"
2204 | "ONEZONE_IA"
2205 | "INTELLIGENT_TIERING"
2206 | "GLACIER"
2207 | "DEEP_ARCHIVE"
2208 | "GLACIER_IR"
2209 | "OUTPOSTS"
2210 | "SNOW"
2211 | "EXPRESS_ONEZONE"
2212 )
2213}
2214
2215pub(crate) fn is_valid_bucket_name(name: &str) -> bool {
2216 if name.len() < 3 || name.len() > 63 {
2217 return false;
2218 }
2219 let bytes = name.as_bytes();
2221 if !bytes[0].is_ascii_alphanumeric() || !bytes[bytes.len() - 1].is_ascii_alphanumeric() {
2222 return false;
2223 }
2224 name.chars()
2226 .all(|c| c.is_ascii_lowercase() || c.is_ascii_digit() || c == '-' || c == '.' || c == '_')
2227}
2228
2229pub(crate) fn is_valid_region(region: &str) -> bool {
2230 let valid_regions = [
2232 "us-east-1",
2233 "us-east-2",
2234 "us-west-1",
2235 "us-west-2",
2236 "af-south-1",
2237 "ap-east-1",
2238 "ap-south-1",
2239 "ap-south-2",
2240 "ap-southeast-1",
2241 "ap-southeast-2",
2242 "ap-southeast-3",
2243 "ap-southeast-4",
2244 "ap-northeast-1",
2245 "ap-northeast-2",
2246 "ap-northeast-3",
2247 "ca-central-1",
2248 "ca-west-1",
2249 "eu-central-1",
2250 "eu-central-2",
2251 "eu-west-1",
2252 "eu-west-2",
2253 "eu-west-3",
2254 "eu-south-1",
2255 "eu-south-2",
2256 "eu-north-1",
2257 "il-central-1",
2258 "me-south-1",
2259 "me-central-1",
2260 "sa-east-1",
2261 "cn-north-1",
2262 "cn-northwest-1",
2263 "us-gov-east-1",
2264 "us-gov-east-2",
2265 "us-gov-west-1",
2266 "us-iso-east-1",
2267 "us-iso-west-1",
2268 "us-isob-east-1",
2269 "us-isof-south-1",
2270 ];
2271 valid_regions.contains(®ion)
2272}
2273
2274pub(crate) fn resolve_object<'a>(
2275 b: &'a S3Bucket,
2276 key: &str,
2277 version_id: Option<&String>,
2278) -> Result<&'a S3Object, AwsServiceError> {
2279 if let Some(vid) = version_id {
2280 if vid == "null" {
2282 if let Some(versions) = b.object_versions.get(key) {
2284 if let Some(obj) = versions
2285 .iter()
2286 .find(|o| o.version_id.is_none() || o.version_id.as_deref() == Some("null"))
2287 {
2288 return Ok(obj);
2289 }
2290 }
2291 if let Some(obj) = b.objects.get(key) {
2293 if obj.version_id.is_none() || obj.version_id.as_deref() == Some("null") {
2294 return Ok(obj);
2295 }
2296 }
2297 } else {
2298 if let Some(versions) = b.object_versions.get(key) {
2300 if let Some(obj) = versions
2301 .iter()
2302 .find(|o| o.version_id.as_deref() == Some(vid.as_str()))
2303 {
2304 return Ok(obj);
2305 }
2306 }
2307 if let Some(obj) = b.objects.get(key) {
2309 if obj.version_id.as_deref() == Some(vid.as_str()) {
2310 return Ok(obj);
2311 }
2312 }
2313 }
2314 if b.versioning.is_some() {
2316 Err(AwsServiceError::aws_error_with_fields(
2317 StatusCode::NOT_FOUND,
2318 "NoSuchVersion",
2319 "The specified version does not exist.",
2320 vec![
2321 ("Key".to_string(), key.to_string()),
2322 ("VersionId".to_string(), vid.to_string()),
2323 ],
2324 ))
2325 } else {
2326 Err(AwsServiceError::aws_error(
2327 StatusCode::BAD_REQUEST,
2328 "InvalidArgument",
2329 "Invalid version id specified",
2330 ))
2331 }
2332 } else {
2333 b.objects.get(key).ok_or_else(|| no_such_key(key))
2334 }
2335}
2336
2337pub(crate) fn make_delete_marker(key: &str, dm_id: &str) -> S3Object {
2338 S3Object {
2339 key: key.to_string(),
2340 last_modified: Utc::now(),
2341 storage_class: "STANDARD".to_string(),
2342 version_id: Some(dm_id.to_string()),
2343 is_delete_marker: true,
2344 ..Default::default()
2345 }
2346}
2347
2348pub(crate) struct DeleteObjectEntry {
2350 key: String,
2351 version_id: Option<String>,
2352}
2353
2354pub(crate) fn parse_delete_objects_xml(xml: &str) -> Vec<DeleteObjectEntry> {
2355 let mut entries = Vec::new();
2356 let mut remaining = xml;
2357 while let Some(obj_start) = remaining.find("<Object>") {
2358 let after = &remaining[obj_start + 8..];
2359 if let Some(obj_end) = after.find("</Object>") {
2360 let obj_body = &after[..obj_end];
2361 let key = extract_xml_value(obj_body, "Key");
2362 let version_id = extract_xml_value(obj_body, "VersionId");
2363 if let Some(k) = key {
2364 entries.push(DeleteObjectEntry { key: k, version_id });
2365 }
2366 remaining = &after[obj_end + 9..];
2367 } else {
2368 break;
2369 }
2370 }
2371 entries
2372}
2373
2374pub(crate) fn parse_delete_objects_quiet(xml: &str) -> bool {
2377 extract_xml_value(xml, "Quiet")
2378 .map(|v| v.eq_ignore_ascii_case("true"))
2379 .unwrap_or(false)
2380}
2381
2382pub(crate) fn parse_tagging_xml(xml: &str) -> Vec<(String, String)> {
2385 let mut tags = Vec::new();
2386 let mut remaining = xml;
2387 while let Some(tag_start) = remaining.find("<Tag>") {
2388 let after = &remaining[tag_start + 5..];
2389 if let Some(tag_end) = after.find("</Tag>") {
2390 let tag_body = &after[..tag_end];
2391 let key = extract_xml_value(tag_body, "Key");
2392 let value = extract_xml_value(tag_body, "Value");
2393 if let (Some(k), Some(v)) = (key, value) {
2394 tags.push((k, v));
2395 }
2396 remaining = &after[tag_end + 6..];
2397 } else {
2398 break;
2399 }
2400 }
2401 tags
2402}
2403
2404pub(crate) fn validate_tags(tags: &[(String, String)]) -> Result<(), AwsServiceError> {
2405 let mut seen = std::collections::HashSet::new();
2407 for (k, _) in tags {
2408 if !seen.insert(k.as_str()) {
2409 return Err(AwsServiceError::aws_error(
2410 StatusCode::BAD_REQUEST,
2411 "InvalidTag",
2412 "Cannot provide multiple Tags with the same key",
2413 ));
2414 }
2415 if k.starts_with("aws:") {
2417 return Err(AwsServiceError::aws_error(
2418 StatusCode::BAD_REQUEST,
2419 "InvalidTag",
2420 "System tags cannot be added/updated by requester",
2421 ));
2422 }
2423 }
2424 Ok(())
2425}
2426
2427pub(crate) fn extract_xml_value(xml: &str, tag: &str) -> Option<String> {
2428 let self_closing1 = format!("<{tag} />");
2430 let self_closing2 = format!("<{tag}/>");
2431 if xml.contains(&self_closing1) || xml.contains(&self_closing2) {
2432 let self_pos = xml
2434 .find(&self_closing1)
2435 .or_else(|| xml.find(&self_closing2));
2436 let open = format!("<{tag}>");
2437 let open_pos = xml.find(&open);
2438 match (self_pos, open_pos) {
2439 (Some(sp), Some(op)) if sp < op => return Some(String::new()),
2440 (Some(_), None) => return Some(String::new()),
2441 _ => {}
2442 }
2443 }
2444
2445 let open = format!("<{tag}>");
2446 let close = format!("</{tag}>");
2447 let start = xml.find(&open)? + open.len();
2448 let end = xml.find(&close)?;
2449 Some(xml[start..end].to_string())
2450}
2451
2452pub(crate) fn parse_complete_multipart_xml(xml: &str) -> Vec<(u32, String)> {
2454 let mut parts = Vec::new();
2455 let mut remaining = xml;
2456 while let Some(part_start) = remaining.find("<Part>") {
2457 let after = &remaining[part_start + 6..];
2458 if let Some(part_end) = after.find("</Part>") {
2459 let part_body = &after[..part_end];
2460 let part_num =
2461 extract_xml_value(part_body, "PartNumber").and_then(|s| s.parse::<u32>().ok());
2462 let etag = extract_xml_value(part_body, "ETag")
2463 .map(|s| s.replace(""", "").replace('"', ""));
2464 if let (Some(num), Some(e)) = (part_num, etag) {
2465 parts.push((num, e));
2466 }
2467 remaining = &after[part_end + 7..];
2468 } else {
2469 break;
2470 }
2471 }
2472 parts
2473}
2474
2475pub(crate) fn parse_url_encoded_tags(s: &str) -> Vec<(String, String)> {
2476 let mut tags = Vec::new();
2477 for pair in s.split('&') {
2478 if pair.is_empty() {
2479 continue;
2480 }
2481 let (key, value) = match pair.find('=') {
2482 Some(pos) => (&pair[..pos], &pair[pos + 1..]),
2483 None => (pair, ""),
2484 };
2485 tags.push((
2486 percent_encoding::percent_decode_str(key)
2487 .decode_utf8_lossy()
2488 .to_string(),
2489 percent_encoding::percent_decode_str(value)
2490 .decode_utf8_lossy()
2491 .to_string(),
2492 ));
2493 }
2494 tags
2495}
2496
2497pub(crate) fn validate_lifecycle_xml(xml: &str) -> Result<(), AwsServiceError> {
2499 let malformed = || {
2500 AwsServiceError::aws_error(
2501 StatusCode::BAD_REQUEST,
2502 "MalformedXML",
2503 "The XML you provided was not well-formed or did not validate against our published schema",
2504 )
2505 };
2506
2507 let mut remaining = xml;
2508 while let Some(rule_start) = remaining.find("<Rule>") {
2509 let after = &remaining[rule_start + 6..];
2510 if let Some(rule_end) = after.find("</Rule>") {
2511 let rule_body = &after[..rule_end];
2512
2513 let has_filter = rule_body.contains("<Filter>")
2515 || rule_body.contains("<Filter/>")
2516 || rule_body.contains("<Filter />");
2517
2518 let has_prefix_outside_filter = {
2520 if !rule_body.contains("<Prefix") {
2521 false
2522 } else if !has_filter {
2523 true } else {
2525 let mut stripped = rule_body.to_string();
2527 if let Some(fs) = stripped.find("<Filter") {
2529 if let Some(fe) = stripped.find("</Filter>") {
2530 stripped = format!("{}{}", &stripped[..fs], &stripped[fe + 9..]);
2531 }
2532 }
2533 stripped.contains("<Prefix")
2534 }
2535 };
2536
2537 if !has_filter && !has_prefix_outside_filter {
2538 return Err(malformed());
2539 }
2540 if has_filter && has_prefix_outside_filter {
2542 return Err(malformed());
2543 }
2544
2545 if let Some(exp_start) = rule_body.find("<Expiration>") {
2548 if let Some(exp_end) = rule_body[exp_start..].find("</Expiration>") {
2549 let exp_body = &rule_body[exp_start..exp_start + exp_end];
2550 if exp_body.contains("<ExpiredObjectDeleteMarker>")
2551 && (exp_body.contains("<Days>") || exp_body.contains("<Date>"))
2552 {
2553 return Err(malformed());
2554 }
2555 }
2556 }
2557
2558 if has_filter {
2560 if let Some(fs) = rule_body.find("<Filter>") {
2561 if let Some(fe) = rule_body.find("</Filter>") {
2562 let filter_body = &rule_body[fs + 8..fe];
2563 let has_prefix_in_filter = filter_body.contains("<Prefix");
2564 let has_tag_in_filter = filter_body.contains("<Tag>");
2565 let has_and_in_filter = filter_body.contains("<And>");
2566 if has_prefix_in_filter && has_tag_in_filter && !has_and_in_filter {
2568 return Err(malformed());
2569 }
2570 if has_tag_in_filter && has_and_in_filter {
2572 let and_start = filter_body.find("<And>").unwrap_or(0);
2574 let tag_pos = filter_body.find("<Tag>").unwrap_or(0);
2575 if tag_pos < and_start {
2576 return Err(malformed());
2577 }
2578 }
2579 }
2580 }
2581 }
2582
2583 if rule_body.contains("<NoncurrentVersionTransition>") {
2585 let mut nvt_remaining = rule_body;
2586 while let Some(nvt_start) = nvt_remaining.find("<NoncurrentVersionTransition>") {
2587 let nvt_after = &nvt_remaining[nvt_start + 29..];
2588 if let Some(nvt_end) = nvt_after.find("</NoncurrentVersionTransition>") {
2589 let nvt_body = &nvt_after[..nvt_end];
2590 if !nvt_body.contains("<NoncurrentDays>") {
2591 return Err(malformed());
2592 }
2593 if !nvt_body.contains("<StorageClass>") {
2594 return Err(malformed());
2595 }
2596 nvt_remaining = &nvt_after[nvt_end + 30..];
2597 } else {
2598 break;
2599 }
2600 }
2601 }
2602
2603 remaining = &after[rule_end + 7..];
2604 } else {
2605 break;
2606 }
2607 }
2608
2609 Ok(())
2610}
2611
2612pub(crate) struct CorsRule {
2614 allowed_origins: Vec<String>,
2615 allowed_methods: Vec<String>,
2616 allowed_headers: Vec<String>,
2617 expose_headers: Vec<String>,
2618 max_age_seconds: Option<u32>,
2619}
2620
2621pub(crate) fn parse_cors_config(xml: &str) -> Vec<CorsRule> {
2623 let mut rules = Vec::new();
2624 let mut remaining = xml;
2625 while let Some(start) = remaining.find("<CORSRule>") {
2626 let after = &remaining[start + 10..];
2627 if let Some(end) = after.find("</CORSRule>") {
2628 let block = &after[..end];
2629 let allowed_origins = extract_all_xml_values(block, "AllowedOrigin");
2630 let allowed_methods = extract_all_xml_values(block, "AllowedMethod");
2631 let allowed_headers = extract_all_xml_values(block, "AllowedHeader");
2632 let expose_headers = extract_all_xml_values(block, "ExposeHeader");
2633 let max_age_seconds =
2634 extract_xml_value(block, "MaxAgeSeconds").and_then(|s| s.parse().ok());
2635 rules.push(CorsRule {
2636 allowed_origins,
2637 allowed_methods,
2638 allowed_headers,
2639 expose_headers,
2640 max_age_seconds,
2641 });
2642 remaining = &after[end + 11..];
2643 } else {
2644 break;
2645 }
2646 }
2647 rules
2648}
2649
2650pub(crate) fn origin_matches(origin: &str, pattern: &str) -> bool {
2652 if pattern == "*" {
2653 return true;
2654 }
2655 if let Some(suffix) = pattern.strip_prefix('*') {
2657 return origin.ends_with(suffix);
2658 }
2659 origin == pattern
2660}
2661
2662pub(crate) fn find_cors_rule<'a>(
2664 rules: &'a [CorsRule],
2665 origin: &str,
2666 method: Option<&str>,
2667) -> Option<&'a CorsRule> {
2668 rules.iter().find(|rule| {
2669 let origin_ok = rule
2670 .allowed_origins
2671 .iter()
2672 .any(|o| origin_matches(origin, o));
2673 let method_ok = match method {
2674 Some(m) => rule.allowed_methods.iter().any(|am| am == m),
2675 None => true,
2676 };
2677 origin_ok && method_ok
2678 })
2679}
2680
2681pub(crate) fn check_object_lock_for_overwrite(
2684 obj: &S3Object,
2685 req: &AwsRequest,
2686) -> Option<&'static str> {
2687 if obj.lock_legal_hold.as_deref() == Some("ON") {
2689 return Some("AccessDenied");
2690 }
2691 if let (Some(mode), Some(until)) = (&obj.lock_mode, &obj.lock_retain_until) {
2693 if *until > Utc::now() {
2694 if mode == "COMPLIANCE" {
2695 return Some("AccessDenied");
2696 }
2697 if mode == "GOVERNANCE" {
2698 let bypass = req
2699 .headers
2700 .get("x-amz-bypass-governance-retention")
2701 .and_then(|v| v.to_str().ok())
2702 .map(|s| s.eq_ignore_ascii_case("true"))
2703 .unwrap_or(false);
2704 if !bypass {
2705 return Some("AccessDenied");
2706 }
2707 }
2708 }
2709 }
2710 None
2711}
2712
2713#[cfg(test)]
2714mod tests;