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[start..].find(&close)? + start;
2452 Some(xml[start..end].to_string())
2453}
2454
2455pub(crate) fn parse_complete_multipart_xml(xml: &str) -> Vec<(u32, String)> {
2457 let mut parts = Vec::new();
2458 let mut remaining = xml;
2459 while let Some(part_start) = remaining.find("<Part>") {
2460 let after = &remaining[part_start + 6..];
2461 if let Some(part_end) = after.find("</Part>") {
2462 let part_body = &after[..part_end];
2463 let part_num =
2464 extract_xml_value(part_body, "PartNumber").and_then(|s| s.parse::<u32>().ok());
2465 let etag = extract_xml_value(part_body, "ETag")
2466 .map(|s| s.replace(""", "").replace('"', ""));
2467 if let (Some(num), Some(e)) = (part_num, etag) {
2468 parts.push((num, e));
2469 }
2470 remaining = &after[part_end + 7..];
2471 } else {
2472 break;
2473 }
2474 }
2475 parts
2476}
2477
2478pub(crate) fn parse_url_encoded_tags(s: &str) -> Vec<(String, String)> {
2479 let mut tags = Vec::new();
2480 for pair in s.split('&') {
2481 if pair.is_empty() {
2482 continue;
2483 }
2484 let (key, value) = match pair.find('=') {
2485 Some(pos) => (&pair[..pos], &pair[pos + 1..]),
2486 None => (pair, ""),
2487 };
2488 tags.push((
2489 percent_encoding::percent_decode_str(key)
2490 .decode_utf8_lossy()
2491 .to_string(),
2492 percent_encoding::percent_decode_str(value)
2493 .decode_utf8_lossy()
2494 .to_string(),
2495 ));
2496 }
2497 tags
2498}
2499
2500pub(crate) fn validate_lifecycle_xml(xml: &str) -> Result<(), AwsServiceError> {
2502 let malformed = || {
2503 AwsServiceError::aws_error(
2504 StatusCode::BAD_REQUEST,
2505 "MalformedXML",
2506 "The XML you provided was not well-formed or did not validate against our published schema",
2507 )
2508 };
2509
2510 let mut remaining = xml;
2511 while let Some(rule_start) = remaining.find("<Rule>") {
2512 let after = &remaining[rule_start + 6..];
2513 if let Some(rule_end) = after.find("</Rule>") {
2514 let rule_body = &after[..rule_end];
2515
2516 let has_filter = rule_body.contains("<Filter>")
2518 || rule_body.contains("<Filter/>")
2519 || rule_body.contains("<Filter />");
2520
2521 let has_prefix_outside_filter = {
2523 if !rule_body.contains("<Prefix") {
2524 false
2525 } else if !has_filter {
2526 true } else {
2528 let mut stripped = rule_body.to_string();
2530 if let Some(fs) = stripped.find("<Filter") {
2532 if let Some(fe) = stripped.find("</Filter>") {
2533 stripped = format!("{}{}", &stripped[..fs], &stripped[fe + 9..]);
2534 }
2535 }
2536 stripped.contains("<Prefix")
2537 }
2538 };
2539
2540 if !has_filter && !has_prefix_outside_filter {
2541 return Err(malformed());
2542 }
2543 if has_filter && has_prefix_outside_filter {
2545 return Err(malformed());
2546 }
2547
2548 if let Some(exp_start) = rule_body.find("<Expiration>") {
2551 if let Some(exp_end) = rule_body[exp_start..].find("</Expiration>") {
2552 let exp_body = &rule_body[exp_start..exp_start + exp_end];
2553 if exp_body.contains("<ExpiredObjectDeleteMarker>")
2554 && (exp_body.contains("<Days>") || exp_body.contains("<Date>"))
2555 {
2556 return Err(malformed());
2557 }
2558 }
2559 }
2560
2561 if has_filter {
2563 if let Some(fs) = rule_body.find("<Filter>") {
2564 if let Some(fe) = rule_body.find("</Filter>") {
2565 let filter_body = &rule_body[fs + 8..fe];
2566 let has_prefix_in_filter = filter_body.contains("<Prefix");
2567 let has_tag_in_filter = filter_body.contains("<Tag>");
2568 let has_and_in_filter = filter_body.contains("<And>");
2569 if has_prefix_in_filter && has_tag_in_filter && !has_and_in_filter {
2571 return Err(malformed());
2572 }
2573 if has_tag_in_filter && has_and_in_filter {
2575 let and_start = filter_body.find("<And>").unwrap_or(0);
2577 let tag_pos = filter_body.find("<Tag>").unwrap_or(0);
2578 if tag_pos < and_start {
2579 return Err(malformed());
2580 }
2581 }
2582 }
2583 }
2584 }
2585
2586 if rule_body.contains("<NoncurrentVersionTransition>") {
2588 let mut nvt_remaining = rule_body;
2589 while let Some(nvt_start) = nvt_remaining.find("<NoncurrentVersionTransition>") {
2590 let nvt_after = &nvt_remaining[nvt_start + 29..];
2591 if let Some(nvt_end) = nvt_after.find("</NoncurrentVersionTransition>") {
2592 let nvt_body = &nvt_after[..nvt_end];
2593 if !nvt_body.contains("<NoncurrentDays>") {
2594 return Err(malformed());
2595 }
2596 if !nvt_body.contains("<StorageClass>") {
2597 return Err(malformed());
2598 }
2599 nvt_remaining = &nvt_after[nvt_end + 30..];
2600 } else {
2601 break;
2602 }
2603 }
2604 }
2605
2606 remaining = &after[rule_end + 7..];
2607 } else {
2608 break;
2609 }
2610 }
2611
2612 Ok(())
2613}
2614
2615pub(crate) struct CorsRule {
2617 allowed_origins: Vec<String>,
2618 allowed_methods: Vec<String>,
2619 allowed_headers: Vec<String>,
2620 expose_headers: Vec<String>,
2621 max_age_seconds: Option<u32>,
2622}
2623
2624pub(crate) fn parse_cors_config(xml: &str) -> Vec<CorsRule> {
2626 let mut rules = Vec::new();
2627 let mut remaining = xml;
2628 while let Some(start) = remaining.find("<CORSRule>") {
2629 let after = &remaining[start + 10..];
2630 if let Some(end) = after.find("</CORSRule>") {
2631 let block = &after[..end];
2632 let allowed_origins = extract_all_xml_values(block, "AllowedOrigin");
2633 let allowed_methods = extract_all_xml_values(block, "AllowedMethod");
2634 let allowed_headers = extract_all_xml_values(block, "AllowedHeader");
2635 let expose_headers = extract_all_xml_values(block, "ExposeHeader");
2636 let max_age_seconds =
2637 extract_xml_value(block, "MaxAgeSeconds").and_then(|s| s.parse().ok());
2638 rules.push(CorsRule {
2639 allowed_origins,
2640 allowed_methods,
2641 allowed_headers,
2642 expose_headers,
2643 max_age_seconds,
2644 });
2645 remaining = &after[end + 11..];
2646 } else {
2647 break;
2648 }
2649 }
2650 rules
2651}
2652
2653pub(crate) fn origin_matches(origin: &str, pattern: &str) -> bool {
2655 if pattern == "*" {
2656 return true;
2657 }
2658 if let Some(suffix) = pattern.strip_prefix('*') {
2660 return origin.ends_with(suffix);
2661 }
2662 origin == pattern
2663}
2664
2665pub(crate) fn find_cors_rule<'a>(
2667 rules: &'a [CorsRule],
2668 origin: &str,
2669 method: Option<&str>,
2670) -> Option<&'a CorsRule> {
2671 rules.iter().find(|rule| {
2672 let origin_ok = rule
2673 .allowed_origins
2674 .iter()
2675 .any(|o| origin_matches(origin, o));
2676 let method_ok = match method {
2677 Some(m) => rule.allowed_methods.iter().any(|am| am == m),
2678 None => true,
2679 };
2680 origin_ok && method_ok
2681 })
2682}
2683
2684pub(crate) fn check_object_lock_for_overwrite(
2687 obj: &S3Object,
2688 req: &AwsRequest,
2689) -> Option<&'static str> {
2690 if obj.lock_legal_hold.as_deref() == Some("ON") {
2692 return Some("AccessDenied");
2693 }
2694 if let (Some(mode), Some(until)) = (&obj.lock_mode, &obj.lock_retain_until) {
2696 if *until > Utc::now() {
2697 if mode == "COMPLIANCE" {
2698 return Some("AccessDenied");
2699 }
2700 if mode == "GOVERNANCE" {
2701 let bypass = req
2702 .headers
2703 .get("x-amz-bypass-governance-retention")
2704 .and_then(|v| v.to_str().ok())
2705 .map(|s| s.eq_ignore_ascii_case("true"))
2706 .unwrap_or(false);
2707 if !bypass {
2708 return Some("AccessDenied");
2709 }
2710 }
2711 }
2712 }
2713 None
2714}
2715
2716#[cfg(test)]
2717mod tests;
2718
2719#[cfg(test)]
2720mod extract_xml_value_tests {
2721 use super::extract_xml_value;
2722
2723 #[test]
2724 fn returns_inner_value() {
2725 assert_eq!(
2726 extract_xml_value("<Root><Key>value</Key></Root>", "Key"),
2727 Some("value".to_string())
2728 );
2729 }
2730
2731 #[test]
2732 fn missing_tag_is_none() {
2733 assert_eq!(extract_xml_value("<Root></Root>", "Key"), None);
2734 }
2735
2736 #[test]
2739 fn close_before_open_does_not_panic() {
2740 assert_eq!(extract_xml_value("</Key>oops<Key>value", "Key"), None);
2741 }
2742
2743 #[test]
2744 fn open_without_close_is_none() {
2745 assert_eq!(extract_xml_value("<Key>value", "Key"), None);
2746 }
2747}