1use std::sync::Arc;
4
5use async_trait::async_trait;
6use bytes::Bytes;
7use chrono::Utc;
8use http::header::{HeaderName, HeaderValue, ETAG, IF_MATCH, LOCATION};
9use http::{HeaderMap, StatusCode};
10use parking_lot::RwLock;
11use uuid::Uuid;
12
13use fakecloud_core::service::{AwsRequest, AwsResponse, AwsService, AwsServiceError, ResponseBody};
14
15use crate::model::{
16 DistributionConfig, DistributionConfigWithTags, InvalidationBatch, TagKeys, Tags as ModelTags,
17};
18use crate::router::{route, Route};
19use crate::state::{
20 CloudFrontAccounts, SharedCloudFrontState, StoredDistribution, StoredInvalidation, Tag,
21};
22use crate::xml_io;
23
24pub(crate) const DEFAULT_ACCOUNT: &str = "000000000000";
25
26const SUPPORTED_ACTIONS: &[&str] = &[
27 "CreateDistribution",
28 "CreateDistributionWithTags",
29 "GetDistribution",
30 "GetDistributionConfig",
31 "UpdateDistribution",
32 "DeleteDistribution",
33 "ListDistributions",
34 "CopyDistribution",
35 "CreateInvalidation",
36 "GetInvalidation",
37 "ListInvalidations",
38 "TagResource",
39 "UntagResource",
40 "ListTagsForResource",
41 "AssociateAlias",
42 "ListConflictingAliases",
43 "ListDistributionsByCachePolicyId",
44 "ListDistributionsByOriginRequestPolicyId",
45 "ListDistributionsByResponseHeadersPolicyId",
46 "ListDistributionsByKeyGroup",
47 "ListDistributionsByWebACLId",
48 "ListDistributionsByVpcOriginId",
49 "ListDistributionsByAnycastIpListId",
50 "ListDistributionsByConnectionMode",
51 "ListDistributionsByConnectionFunction",
52 "ListDistributionsByOwnedResource",
53 "ListDistributionsByTrustStore",
54 "ListDistributionsByRealtimeLogConfig",
55 "AssociateDistributionWebACL",
56 "DisassociateDistributionWebACL",
57 "CreateOriginAccessControl",
58 "GetOriginAccessControl",
59 "GetOriginAccessControlConfig",
60 "UpdateOriginAccessControl",
61 "DeleteOriginAccessControl",
62 "ListOriginAccessControls",
63 "CreateCachePolicy",
64 "GetCachePolicy",
65 "GetCachePolicyConfig",
66 "UpdateCachePolicy",
67 "DeleteCachePolicy",
68 "ListCachePolicies",
69 "CreateOriginRequestPolicy",
70 "GetOriginRequestPolicy",
71 "GetOriginRequestPolicyConfig",
72 "UpdateOriginRequestPolicy",
73 "DeleteOriginRequestPolicy",
74 "ListOriginRequestPolicies",
75 "CreateResponseHeadersPolicy",
76 "GetResponseHeadersPolicy",
77 "GetResponseHeadersPolicyConfig",
78 "UpdateResponseHeadersPolicy",
79 "DeleteResponseHeadersPolicy",
80 "ListResponseHeadersPolicies",
81 "CreateContinuousDeploymentPolicy",
82 "GetContinuousDeploymentPolicy",
83 "GetContinuousDeploymentPolicyConfig",
84 "UpdateContinuousDeploymentPolicy",
85 "DeleteContinuousDeploymentPolicy",
86 "ListContinuousDeploymentPolicies",
87 "CreateFunction",
88 "DescribeFunction",
89 "GetFunction",
90 "UpdateFunction",
91 "DeleteFunction",
92 "ListFunctions",
93 "PublishFunction",
94 "TestFunction",
95 "CreatePublicKey",
96 "GetPublicKey",
97 "GetPublicKeyConfig",
98 "UpdatePublicKey",
99 "DeletePublicKey",
100 "ListPublicKeys",
101 "CreateKeyGroup",
102 "GetKeyGroup",
103 "GetKeyGroupConfig",
104 "UpdateKeyGroup",
105 "DeleteKeyGroup",
106 "ListKeyGroups",
107 "CreateKeyValueStore",
108 "DescribeKeyValueStore",
109 "UpdateKeyValueStore",
110 "DeleteKeyValueStore",
111 "ListKeyValueStores",
112 "CreateCloudFrontOriginAccessIdentity",
113 "GetCloudFrontOriginAccessIdentity",
114 "GetCloudFrontOriginAccessIdentityConfig",
115 "UpdateCloudFrontOriginAccessIdentity",
116 "DeleteCloudFrontOriginAccessIdentity",
117 "ListCloudFrontOriginAccessIdentities",
118 "CreateMonitoringSubscription",
119 "GetMonitoringSubscription",
120 "DeleteMonitoringSubscription",
121 "CreateStreamingDistribution",
122 "CreateStreamingDistributionWithTags",
123 "GetStreamingDistribution",
124 "GetStreamingDistributionConfig",
125 "UpdateStreamingDistribution",
126 "DeleteStreamingDistribution",
127 "ListStreamingDistributions",
128 "CreateFieldLevelEncryptionConfig",
129 "GetFieldLevelEncryption",
130 "GetFieldLevelEncryptionConfig",
131 "UpdateFieldLevelEncryptionConfig",
132 "DeleteFieldLevelEncryptionConfig",
133 "ListFieldLevelEncryptionConfigs",
134 "CreateFieldLevelEncryptionProfile",
135 "GetFieldLevelEncryptionProfile",
136 "GetFieldLevelEncryptionProfileConfig",
137 "UpdateFieldLevelEncryptionProfile",
138 "DeleteFieldLevelEncryptionProfile",
139 "ListFieldLevelEncryptionProfiles",
140 "CreateRealtimeLogConfig",
141 "GetRealtimeLogConfig",
142 "UpdateRealtimeLogConfig",
143 "DeleteRealtimeLogConfig",
144 "ListRealtimeLogConfigs",
145 "CreateVpcOrigin",
146 "GetVpcOrigin",
147 "UpdateVpcOrigin",
148 "DeleteVpcOrigin",
149 "ListVpcOrigins",
150 "CreateAnycastIpList",
151 "GetAnycastIpList",
152 "UpdateAnycastIpList",
153 "DeleteAnycastIpList",
154 "ListAnycastIpLists",
155 "CreateTrustStore",
156 "GetTrustStore",
157 "UpdateTrustStore",
158 "DeleteTrustStore",
159 "ListTrustStores",
160 "GetResourcePolicy",
161 "PutResourcePolicy",
162 "DeleteResourcePolicy",
163 "CreateConnectionGroup",
164 "GetConnectionGroup",
165 "GetConnectionGroupByRoutingEndpoint",
166 "UpdateConnectionGroup",
167 "DeleteConnectionGroup",
168 "ListConnectionGroups",
169 "ListDomainConflicts",
170 "UpdateDomainAssociation",
171 "VerifyDnsConfiguration",
172 "GetManagedCertificateDetails",
173 "UpdateDistributionWithStagingConfig",
174];
175
176pub struct CloudFrontService {
177 pub(crate) state: SharedCloudFrontState,
178}
179
180impl CloudFrontService {
181 pub fn new(state: SharedCloudFrontState) -> Self {
182 Self { state }
183 }
184
185 pub fn shared_state(&self) -> SharedCloudFrontState {
186 Arc::clone(&self.state)
187 }
188}
189
190impl Default for CloudFrontService {
191 fn default() -> Self {
192 Self::new(Arc::new(RwLock::new(CloudFrontAccounts::new())))
193 }
194}
195
196#[async_trait]
197impl AwsService for CloudFrontService {
198 fn service_name(&self) -> &str {
199 "cloudfront"
200 }
201
202 fn supported_actions(&self) -> &[&str] {
203 SUPPORTED_ACTIONS
204 }
205
206 async fn handle(&self, req: AwsRequest) -> Result<AwsResponse, AwsServiceError> {
207 let resolved = match route(&req.method, &req.raw_path, &req.raw_query) {
208 Some(r) => r,
209 None => {
210 return Err(aws_error(
211 StatusCode::NOT_FOUND,
212 "InvalidArgument",
213 format!("Unknown CloudFront route: {} {}", req.method, req.raw_path),
214 ));
215 }
216 };
217
218 match resolved.action {
219 "CreateDistribution" => self.create_distribution(&req, false),
220 "CreateDistributionWithTags" => self.create_distribution(&req, true),
221 "GetDistribution" => self.get_distribution(&resolved),
222 "GetDistributionConfig" => self.get_distribution_config(&resolved),
223 "UpdateDistribution" => self.update_distribution(&req, &resolved),
224 "DeleteDistribution" => self.delete_distribution(&req, &resolved),
225 "ListDistributions" => self.list_distributions(&req),
226 "CopyDistribution" => self.copy_distribution(&req, &resolved),
227 "CreateInvalidation" => self.create_invalidation(&req, &resolved),
228 "GetInvalidation" => self.get_invalidation(&resolved),
229 "ListInvalidations" => self.list_invalidations(&resolved),
230 "TagResource" => self.tag_resource(&req),
231 "UntagResource" => self.untag_resource(&req),
232 "ListTagsForResource" => self.list_tags_for_resource(&req),
233 "AssociateAlias" => self.associate_alias(&req, &resolved),
234 "ListConflictingAliases" => self.list_conflicting_aliases(&req),
235 "AssociateDistributionWebACL" => self.associate_web_acl(&req, &resolved),
236 "DisassociateDistributionWebACL" => self.disassociate_web_acl(&req, &resolved),
237 "ListDistributionsByCachePolicyId"
238 | "ListDistributionsByOriginRequestPolicyId"
239 | "ListDistributionsByResponseHeadersPolicyId"
240 | "ListDistributionsByKeyGroup"
241 | "ListDistributionsByWebACLId"
242 | "ListDistributionsByVpcOriginId"
243 | "ListDistributionsByAnycastIpListId"
244 | "ListDistributionsByConnectionMode"
245 | "ListDistributionsByConnectionFunction"
246 | "ListDistributionsByOwnedResource"
247 | "ListDistributionsByTrustStore"
248 | "ListDistributionsByRealtimeLogConfig" => self.list_distributions_by(resolved.action),
249 "CreateOriginAccessControl" => self.create_origin_access_control(&req),
250 "GetOriginAccessControl" => self.get_origin_access_control(&resolved),
251 "GetOriginAccessControlConfig" => self.get_origin_access_control_config(&resolved),
252 "UpdateOriginAccessControl" => self.update_origin_access_control(&req, &resolved),
253 "DeleteOriginAccessControl" => self.delete_origin_access_control(&req, &resolved),
254 "ListOriginAccessControls" => self.list_origin_access_controls(&req),
255 "CreateCachePolicy" => self.create_cache_policy(&req),
256 "GetCachePolicy" => self.get_cache_policy(&resolved),
257 "GetCachePolicyConfig" => self.get_cache_policy_config(&resolved),
258 "UpdateCachePolicy" => self.update_cache_policy(&req, &resolved),
259 "DeleteCachePolicy" => self.delete_cache_policy(&req, &resolved),
260 "ListCachePolicies" => self.list_cache_policies(&req),
261 "CreateOriginRequestPolicy" => self.create_origin_request_policy(&req),
262 "GetOriginRequestPolicy" => self.get_origin_request_policy(&resolved),
263 "GetOriginRequestPolicyConfig" => self.get_origin_request_policy_config(&resolved),
264 "UpdateOriginRequestPolicy" => self.update_origin_request_policy(&req, &resolved),
265 "DeleteOriginRequestPolicy" => self.delete_origin_request_policy(&req, &resolved),
266 "ListOriginRequestPolicies" => self.list_origin_request_policies(&req),
267 "CreateResponseHeadersPolicy" => self.create_response_headers_policy(&req),
268 "GetResponseHeadersPolicy" => self.get_response_headers_policy(&resolved),
269 "GetResponseHeadersPolicyConfig" => self.get_response_headers_policy_config(&resolved),
270 "UpdateResponseHeadersPolicy" => self.update_response_headers_policy(&req, &resolved),
271 "DeleteResponseHeadersPolicy" => self.delete_response_headers_policy(&req, &resolved),
272 "ListResponseHeadersPolicies" => self.list_response_headers_policies(&req),
273 "CreateContinuousDeploymentPolicy" => self.create_continuous_deployment_policy(&req),
274 "GetContinuousDeploymentPolicy" => self.get_continuous_deployment_policy(&resolved),
275 "GetContinuousDeploymentPolicyConfig" => {
276 self.get_continuous_deployment_policy_config(&resolved)
277 }
278 "UpdateContinuousDeploymentPolicy" => {
279 self.update_continuous_deployment_policy(&req, &resolved)
280 }
281 "DeleteContinuousDeploymentPolicy" => {
282 self.delete_continuous_deployment_policy(&req, &resolved)
283 }
284 "ListContinuousDeploymentPolicies" => self.list_continuous_deployment_policies(&req),
285 "CreateFunction" => self.create_function(&req),
286 "DescribeFunction" => self.describe_function(&req, &resolved),
287 "GetFunction" => self.get_function(&req, &resolved),
288 "UpdateFunction" => self.update_function(&req, &resolved),
289 "DeleteFunction" => self.delete_function(&req, &resolved),
290 "ListFunctions" => self.list_functions(&req),
291 "PublishFunction" => self.publish_function(&req, &resolved),
292 "TestFunction" => self.test_function(&req, &resolved),
293 "CreatePublicKey" => self.create_public_key(&req),
294 "GetPublicKey" => self.get_public_key(&resolved),
295 "GetPublicKeyConfig" => self.get_public_key_config(&resolved),
296 "UpdatePublicKey" => self.update_public_key(&req, &resolved),
297 "DeletePublicKey" => self.delete_public_key(&req, &resolved),
298 "ListPublicKeys" => self.list_public_keys(&req),
299 "CreateKeyGroup" => self.create_key_group(&req),
300 "GetKeyGroup" => self.get_key_group(&resolved),
301 "GetKeyGroupConfig" => self.get_key_group_config(&resolved),
302 "UpdateKeyGroup" => self.update_key_group(&req, &resolved),
303 "DeleteKeyGroup" => self.delete_key_group(&req, &resolved),
304 "ListKeyGroups" => self.list_key_groups(&req),
305 "CreateKeyValueStore" => self.create_key_value_store(&req),
306 "DescribeKeyValueStore" => self.describe_key_value_store(&resolved),
307 "UpdateKeyValueStore" => self.update_key_value_store(&req, &resolved),
308 "DeleteKeyValueStore" => self.delete_key_value_store(&req, &resolved),
309 "ListKeyValueStores" => self.list_key_value_stores(&req),
310 "CreateCloudFrontOriginAccessIdentity" => self.create_oai(&req),
311 "GetCloudFrontOriginAccessIdentity" => self.get_oai(&resolved),
312 "GetCloudFrontOriginAccessIdentityConfig" => self.get_oai_config(&resolved),
313 "UpdateCloudFrontOriginAccessIdentity" => self.update_oai(&req, &resolved),
314 "DeleteCloudFrontOriginAccessIdentity" => self.delete_oai(&req, &resolved),
315 "ListCloudFrontOriginAccessIdentities" => self.list_oai(&req),
316 "CreateMonitoringSubscription" => self.create_monitoring_subscription(&req, &resolved),
317 "GetMonitoringSubscription" => self.get_monitoring_subscription(&resolved),
318 "DeleteMonitoringSubscription" => self.delete_monitoring_subscription(&resolved),
319 "CreateStreamingDistribution" => self.create_streaming_distribution(&req, false),
320 "CreateStreamingDistributionWithTags" => self.create_streaming_distribution(&req, true),
321 "GetStreamingDistribution" => self.get_streaming_distribution(&resolved),
322 "GetStreamingDistributionConfig" => self.get_streaming_distribution_config(&resolved),
323 "UpdateStreamingDistribution" => self.update_streaming_distribution(&req, &resolved),
324 "DeleteStreamingDistribution" => self.delete_streaming_distribution(&req, &resolved),
325 "ListStreamingDistributions" => self.list_streaming_distributions(&req),
326 "CreateFieldLevelEncryptionConfig" => self.create_field_level_encryption_config(&req),
327 "GetFieldLevelEncryption" => self.get_field_level_encryption(&resolved),
328 "GetFieldLevelEncryptionConfig" => self.get_field_level_encryption_config(&resolved),
329 "UpdateFieldLevelEncryptionConfig" => {
330 self.update_field_level_encryption_config(&req, &resolved)
331 }
332 "DeleteFieldLevelEncryptionConfig" => {
333 self.delete_field_level_encryption_config(&req, &resolved)
334 }
335 "ListFieldLevelEncryptionConfigs" => self.list_field_level_encryption_configs(&req),
336 "CreateFieldLevelEncryptionProfile" => self.create_field_level_encryption_profile(&req),
337 "GetFieldLevelEncryptionProfile" => self.get_field_level_encryption_profile(&resolved),
338 "GetFieldLevelEncryptionProfileConfig" => {
339 self.get_field_level_encryption_profile_config(&resolved)
340 }
341 "UpdateFieldLevelEncryptionProfile" => {
342 self.update_field_level_encryption_profile(&req, &resolved)
343 }
344 "DeleteFieldLevelEncryptionProfile" => {
345 self.delete_field_level_encryption_profile(&req, &resolved)
346 }
347 "ListFieldLevelEncryptionProfiles" => self.list_field_level_encryption_profiles(&req),
348 "CreateRealtimeLogConfig" => self.create_realtime_log_config(&req),
349 "GetRealtimeLogConfig" => self.get_realtime_log_config(&req),
350 "UpdateRealtimeLogConfig" => self.update_realtime_log_config(&req),
351 "DeleteRealtimeLogConfig" => self.delete_realtime_log_config(&req),
352 "ListRealtimeLogConfigs" => self.list_realtime_log_configs(&req),
353 "CreateVpcOrigin" => self.create_vpc_origin(&req),
354 "GetVpcOrigin" => self.get_vpc_origin(&resolved),
355 "UpdateVpcOrigin" => self.update_vpc_origin(&req, &resolved),
356 "DeleteVpcOrigin" => self.delete_vpc_origin(&req, &resolved),
357 "ListVpcOrigins" => self.list_vpc_origins(&req),
358 "CreateAnycastIpList" => self.create_anycast_ip_list(&req),
359 "GetAnycastIpList" => self.get_anycast_ip_list(&resolved),
360 "UpdateAnycastIpList" => self.update_anycast_ip_list(&req, &resolved),
361 "DeleteAnycastIpList" => self.delete_anycast_ip_list(&req, &resolved),
362 "ListAnycastIpLists" => self.list_anycast_ip_lists(&req),
363 "CreateTrustStore" => self.create_trust_store(&req),
364 "GetTrustStore" => self.get_trust_store(&resolved),
365 "UpdateTrustStore" => self.update_trust_store(&req, &resolved),
366 "DeleteTrustStore" => self.delete_trust_store(&req, &resolved),
367 "ListTrustStores" => self.list_trust_stores(&req),
368 "GetResourcePolicy" => self.get_resource_policy(&req),
369 "PutResourcePolicy" => self.put_resource_policy(&req),
370 "DeleteResourcePolicy" => self.delete_resource_policy(&req),
371 "CreateConnectionGroup" => self.create_connection_group(&req),
372 "GetConnectionGroup" => self.get_connection_group(&resolved),
373 "GetConnectionGroupByRoutingEndpoint" => {
374 self.get_connection_group_by_routing_endpoint(&req)
375 }
376 "UpdateConnectionGroup" => self.update_connection_group(&req, &resolved),
377 "DeleteConnectionGroup" => self.delete_connection_group(&req, &resolved),
378 "ListConnectionGroups" => self.list_connection_groups(&req),
379 "ListDomainConflicts" => self.list_domain_conflicts(&req),
380 "UpdateDomainAssociation" => self.update_domain_association(&req),
381 "VerifyDnsConfiguration" => self.verify_dns_configuration(&req),
382 "GetManagedCertificateDetails" => self.get_managed_certificate_details(&resolved),
383 "UpdateDistributionWithStagingConfig" => {
384 self.update_distribution_with_staging_config(&req, &resolved)
385 }
386 other => Err(aws_error(
387 StatusCode::NOT_IMPLEMENTED,
388 "InvalidAction",
389 format!("CloudFront action {other} is not implemented yet"),
390 )),
391 }
392 }
393}
394
395impl CloudFrontService {
398 fn create_distribution(
399 &self,
400 req: &AwsRequest,
401 with_tags: bool,
402 ) -> Result<AwsResponse, AwsServiceError> {
403 let (config, tags) = if with_tags {
404 let parsed: DistributionConfigWithTags = xml_io::from_xml_root(&req.body)
405 .map_err(|e| invalid_argument(format!("invalid request XML: {e}")))?;
406 let tags = parsed
407 .tags
408 .items
409 .map(|i| {
410 i.tag
411 .into_iter()
412 .map(|t| Tag {
413 key: t.key,
414 value: t.value,
415 })
416 .collect()
417 })
418 .unwrap_or_default();
419 (parsed.distribution_config, tags)
420 } else {
421 let parsed: DistributionConfig = xml_io::from_xml_root(&req.body)
422 .map_err(|e| invalid_argument(format!("invalid request XML: {e}")))?;
423 (parsed, Vec::new())
424 };
425
426 validate_caller_reference(&config.caller_reference)?;
427 validate_origins(&config)?;
428
429 let mut state = self.state.write();
430 let account = state.entry(account_id(req));
431
432 if let Some(existing) = account
433 .distributions
434 .values()
435 .find(|d| d.config.caller_reference == config.caller_reference)
436 {
437 return Err(aws_error(
438 StatusCode::CONFLICT,
439 "DistributionAlreadyExists",
440 format!(
441 "Distribution with the same CallerReference exists: {}",
442 existing.id
443 ),
444 ));
445 }
446
447 let id = generate_distribution_id();
448 let now = Utc::now();
449 let etag = generate_etag();
450 let domain = format!("{}.cloudfront.net", id.to_lowercase());
451 let arn = format!(
452 "arn:aws:cloudfront::{}:distribution/{}",
453 account_id(req),
454 id
455 );
456
457 let stored = StoredDistribution {
458 id: id.clone(),
459 arn: arn.clone(),
460 status: "Deployed".to_string(),
461 last_modified_time: now,
462 domain_name: domain,
463 in_progress_invalidation_batches: 0,
464 etag: etag.clone(),
465 config,
466 };
467 account.distributions.insert(id.clone(), stored.clone());
468 if !tags.is_empty() {
469 account.tags.insert(arn.clone(), tags);
470 }
471 drop(state);
472
473 let body = build_distribution_xml(&stored);
474 let mut headers = HeaderMap::new();
475 set_header(&mut headers, ETAG, &etag);
476 set_header(&mut headers, LOCATION, &stored.arn);
477 Ok(xml_response(StatusCode::CREATED, body, headers))
478 }
479
480 fn get_distribution(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
481 let id = route
482 .id
483 .as_deref()
484 .ok_or_else(|| invalid_argument("missing distribution id"))?;
485 let state = self.state.read();
486 let account = state
487 .accounts
488 .get(DEFAULT_ACCOUNT)
489 .ok_or_else(|| no_such_distribution(id))?;
490 let dist = account
491 .distributions
492 .get(id)
493 .ok_or_else(|| no_such_distribution(id))?
494 .clone();
495 drop(state);
496 let body = build_distribution_xml(&dist);
497 let mut headers = HeaderMap::new();
498 set_header(&mut headers, ETAG, &dist.etag);
499 Ok(xml_response(StatusCode::OK, body, headers))
500 }
501
502 fn get_distribution_config(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
503 let id = route
504 .id
505 .as_deref()
506 .ok_or_else(|| invalid_argument("missing distribution id"))?;
507 let state = self.state.read();
508 let account = state
509 .accounts
510 .get(DEFAULT_ACCOUNT)
511 .ok_or_else(|| no_such_distribution(id))?;
512 let dist = account
513 .distributions
514 .get(id)
515 .ok_or_else(|| no_such_distribution(id))?
516 .clone();
517 drop(state);
518 let body = xml_io::to_xml_root("DistributionConfig", &dist.config)
519 .map_err(|e| internal_error(format!("xml encode failed: {e}")))?;
520 let mut headers = HeaderMap::new();
521 set_header(&mut headers, ETAG, &dist.etag);
522 Ok(xml_response(StatusCode::OK, body, headers))
523 }
524
525 fn update_distribution(
526 &self,
527 req: &AwsRequest,
528 route: &Route,
529 ) -> Result<AwsResponse, AwsServiceError> {
530 let id = route
531 .id
532 .as_deref()
533 .ok_or_else(|| invalid_argument("missing distribution id"))?;
534 let if_match = req
535 .headers
536 .get(IF_MATCH)
537 .and_then(|v| v.to_str().ok())
538 .ok_or_else(|| {
539 aws_error(
540 StatusCode::BAD_REQUEST,
541 "InvalidIfMatchVersion",
542 "Missing If-Match header for UpdateDistribution",
543 )
544 })?
545 .to_string();
546 let new_config: DistributionConfig = xml_io::from_xml_root(&req.body)
547 .map_err(|e| invalid_argument(format!("invalid DistributionConfig XML: {e}")))?;
548 validate_caller_reference(&new_config.caller_reference)?;
549 validate_origins(&new_config)?;
550
551 let mut state = self.state.write();
552 let account = state
553 .accounts
554 .get_mut(DEFAULT_ACCOUNT)
555 .ok_or_else(|| no_such_distribution(id))?;
556 let dist = account
557 .distributions
558 .get_mut(id)
559 .ok_or_else(|| no_such_distribution(id))?;
560 if dist.etag != if_match {
561 return Err(aws_error(
562 StatusCode::PRECONDITION_FAILED,
563 "PreconditionFailed",
564 "If-Match header does not match the current ETag",
565 ));
566 }
567 dist.config = new_config;
568 dist.etag = generate_etag();
569 dist.last_modified_time = Utc::now();
570 let snapshot = dist.clone();
571 drop(state);
572
573 let body = build_distribution_xml(&snapshot);
574 let mut headers = HeaderMap::new();
575 set_header(&mut headers, ETAG, &snapshot.etag);
576 Ok(xml_response(StatusCode::OK, body, headers))
577 }
578
579 fn delete_distribution(
580 &self,
581 req: &AwsRequest,
582 route: &Route,
583 ) -> Result<AwsResponse, AwsServiceError> {
584 let id = route
585 .id
586 .as_deref()
587 .ok_or_else(|| invalid_argument("missing distribution id"))?;
588 let if_match = req
589 .headers
590 .get(IF_MATCH)
591 .and_then(|v| v.to_str().ok())
592 .ok_or_else(|| {
593 aws_error(
594 StatusCode::BAD_REQUEST,
595 "InvalidIfMatchVersion",
596 "Missing If-Match header for DeleteDistribution",
597 )
598 })?
599 .to_string();
600 let mut state = self.state.write();
601 let account = state
602 .accounts
603 .get_mut(DEFAULT_ACCOUNT)
604 .ok_or_else(|| no_such_distribution(id))?;
605 {
606 let dist = account
607 .distributions
608 .get(id)
609 .ok_or_else(|| no_such_distribution(id))?;
610 if dist.etag != if_match {
611 return Err(aws_error(
612 StatusCode::PRECONDITION_FAILED,
613 "PreconditionFailed",
614 "If-Match header does not match the current ETag",
615 ));
616 }
617 if dist.config.enabled {
618 return Err(aws_error(
619 StatusCode::PRECONDITION_FAILED,
620 "DistributionNotDisabled",
621 "Distribution must be disabled before delete",
622 ));
623 }
624 }
625 let removed = account.distributions.remove(id).unwrap();
626 account.tags.remove(&removed.arn);
627 Ok(empty_response(StatusCode::NO_CONTENT))
628 }
629
630 fn list_distributions(&self, _req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
631 let state = self.state.read();
632 let mut dists: Vec<StoredDistribution> = state
633 .accounts
634 .values()
635 .flat_map(|a| a.distributions.values().cloned())
636 .collect();
637 dists.sort_by_key(|a| a.last_modified_time);
638 drop(state);
639 let body = build_distribution_list_xml(&dists, "DistributionList");
640 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
641 }
642
643 fn list_distributions_by(&self, action: &str) -> Result<AwsResponse, AwsServiceError> {
644 let root = match action {
649 "ListDistributionsByCachePolicyId"
650 | "ListDistributionsByOriginRequestPolicyId"
651 | "ListDistributionsByResponseHeadersPolicyId"
652 | "ListDistributionsByKeyGroup"
653 | "ListDistributionsByVpcOriginId" => "DistributionIdList",
654 "ListDistributionsByOwnedResource" => "DistributionIdOwnerList",
655 _ => "DistributionList",
656 };
657 let body = build_empty_distribution_id_list(root);
658 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
659 }
660
661 fn copy_distribution(
662 &self,
663 req: &AwsRequest,
664 route: &Route,
665 ) -> Result<AwsResponse, AwsServiceError> {
666 let primary_id = route
667 .id
668 .as_deref()
669 .ok_or_else(|| invalid_argument("missing primary distribution id"))?;
670 let if_match = req
671 .headers
672 .get(IF_MATCH)
673 .and_then(|v| v.to_str().ok())
674 .ok_or_else(|| {
675 aws_error(
676 StatusCode::BAD_REQUEST,
677 "InvalidIfMatchVersion",
678 "Missing If-Match header for CopyDistribution",
679 )
680 })?
681 .to_string();
682 let parsed: CopyDistributionRequest = xml_io::from_xml_root(&req.body)
683 .map_err(|e| invalid_argument(format!("invalid request XML: {e}")))?;
684 validate_caller_reference(&parsed.caller_reference)?;
685 let mut state = self.state.write();
686 let account = state
687 .accounts
688 .get_mut(DEFAULT_ACCOUNT)
689 .ok_or_else(|| no_such_distribution(primary_id))?;
690 let primary = account
691 .distributions
692 .get(primary_id)
693 .ok_or_else(|| no_such_distribution(primary_id))?
694 .clone();
695 if primary.etag != if_match {
696 return Err(aws_error(
697 StatusCode::PRECONDITION_FAILED,
698 "PreconditionFailed",
699 "If-Match header does not match the current ETag",
700 ));
701 }
702 if account
703 .distributions
704 .values()
705 .any(|d| d.config.caller_reference == parsed.caller_reference)
706 {
707 return Err(aws_error(
708 StatusCode::CONFLICT,
709 "DistributionAlreadyExists",
710 "Distribution with the same CallerReference exists",
711 ));
712 }
713 let new_id = generate_distribution_id();
714 let mut config = primary.config.clone();
715 config.caller_reference = parsed.caller_reference;
716 config.enabled = parsed.enabled.unwrap_or(false);
717 config.staging = parsed.staging;
718 let now = Utc::now();
719 let etag = generate_etag();
720 let arn = format!(
721 "arn:aws:cloudfront::{}:distribution/{}",
722 account_id(req),
723 new_id
724 );
725 let stored = StoredDistribution {
726 id: new_id.clone(),
727 arn: arn.clone(),
728 status: "Deployed".into(),
729 last_modified_time: now,
730 domain_name: format!("{}.cloudfront.net", new_id.to_lowercase()),
731 in_progress_invalidation_batches: 0,
732 etag: etag.clone(),
733 config,
734 };
735 account.distributions.insert(new_id.clone(), stored.clone());
736 drop(state);
737 let body = build_distribution_xml(&stored);
738 let mut headers = HeaderMap::new();
739 set_header(&mut headers, ETAG, &etag);
740 set_header(&mut headers, LOCATION, &stored.arn);
741 Ok(xml_response(StatusCode::CREATED, body, headers))
742 }
743}
744
745#[derive(Debug, serde::Deserialize, Default)]
746#[serde(rename_all = "PascalCase")]
747struct CopyDistributionRequest {
748 caller_reference: String,
749 #[serde(default)]
750 enabled: Option<bool>,
751 #[serde(default)]
752 staging: Option<bool>,
753}
754
755impl CloudFrontService {
758 fn create_invalidation(
759 &self,
760 req: &AwsRequest,
761 route: &Route,
762 ) -> Result<AwsResponse, AwsServiceError> {
763 let dist_id = route
764 .id
765 .as_deref()
766 .ok_or_else(|| invalid_argument("missing distribution id"))?;
767 let batch: InvalidationBatch = xml_io::from_xml_root(&req.body)
768 .map_err(|e| invalid_argument(format!("invalid InvalidationBatch XML: {e}")))?;
769 if batch.caller_reference.is_empty() {
770 return Err(invalid_argument("CallerReference is required"));
771 }
772 if batch.paths.quantity < 1 {
773 return Err(invalid_argument(
774 "InvalidationBatch.Paths must be non-empty",
775 ));
776 }
777 let mut state = self.state.write();
778 let account = state.entry(DEFAULT_ACCOUNT);
779 if !account.distributions.contains_key(dist_id) {
780 return Err(no_such_distribution(dist_id));
781 }
782 let id = generate_invalidation_id();
783 let stored = StoredInvalidation {
784 id: id.clone(),
785 distribution_id: dist_id.to_string(),
786 status: "Completed".to_string(),
787 create_time: Utc::now(),
788 batch: batch.clone(),
789 };
790 account.invalidations.insert(id.clone(), stored.clone());
791 drop(state);
792 let body = build_invalidation_xml(&stored);
793 let mut headers = HeaderMap::new();
794 set_header(
795 &mut headers,
796 LOCATION,
797 &format!(
798 "/2020-05-31/distribution/{dist_id}/invalidation/{}",
799 stored.id
800 ),
801 );
802 Ok(xml_response(StatusCode::CREATED, body, headers))
803 }
804
805 fn get_invalidation(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
806 let dist_id = route
807 .id
808 .as_deref()
809 .ok_or_else(|| invalid_argument("missing distribution id"))?;
810 let inv_id = route
811 .second_id
812 .as_deref()
813 .ok_or_else(|| invalid_argument("missing invalidation id"))?;
814 let state = self.state.read();
815 let account = state
816 .accounts
817 .get(DEFAULT_ACCOUNT)
818 .ok_or_else(|| no_such_invalidation(inv_id))?;
819 if !account.distributions.contains_key(dist_id) {
820 return Err(no_such_distribution(dist_id));
821 }
822 let inv = account
823 .invalidations
824 .get(inv_id)
825 .filter(|i| i.distribution_id == dist_id)
826 .ok_or_else(|| no_such_invalidation(inv_id))?
827 .clone();
828 drop(state);
829 let body = build_invalidation_xml(&inv);
830 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
831 }
832
833 fn list_invalidations(&self, route: &Route) -> Result<AwsResponse, AwsServiceError> {
834 let dist_id = route
835 .id
836 .as_deref()
837 .ok_or_else(|| invalid_argument("missing distribution id"))?;
838 let state = self.state.read();
839 let account = state
840 .accounts
841 .get(DEFAULT_ACCOUNT)
842 .ok_or_else(|| no_such_distribution(dist_id))?;
843 if !account.distributions.contains_key(dist_id) {
844 return Err(no_such_distribution(dist_id));
845 }
846 let mut items: Vec<&StoredInvalidation> = account
847 .invalidations
848 .values()
849 .filter(|i| i.distribution_id == dist_id)
850 .collect();
851 items.sort_by_key(|a| a.create_time);
852 let body = build_invalidation_list_xml(&items);
853 drop(state);
854 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
855 }
856}
857
858impl CloudFrontService {
861 fn parse_arn_query(query: &str) -> Option<String> {
862 for pair in query.split('&').filter(|p| !p.is_empty()) {
863 if let Some(rest) = pair.strip_prefix("Resource=") {
864 return Some(percent_decode(rest));
865 }
866 }
867 None
868 }
869
870 fn tag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
871 let arn = Self::parse_arn_query(&req.raw_query)
872 .ok_or_else(|| invalid_argument("Resource query parameter is required"))?;
873 let parsed: ModelTags = xml_io::from_xml_root(&req.body)
874 .map_err(|e| invalid_argument(format!("invalid Tags XML: {e}")))?;
875 let new_tags: Vec<Tag> = parsed
876 .items
877 .map(|i| {
878 i.tag
879 .into_iter()
880 .map(|t| Tag {
881 key: t.key,
882 value: t.value,
883 })
884 .collect()
885 })
886 .unwrap_or_default();
887 let mut state = self.state.write();
888 let account = state.entry(DEFAULT_ACCOUNT);
889 let entry = account.tags.entry(arn).or_default();
890 for tag in new_tags {
891 if let Some(existing) = entry.iter_mut().find(|t| t.key == tag.key) {
892 existing.value = tag.value;
893 } else {
894 entry.push(tag);
895 }
896 }
897 Ok(empty_response(StatusCode::NO_CONTENT))
898 }
899
900 fn untag_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
901 let arn = Self::parse_arn_query(&req.raw_query)
902 .ok_or_else(|| invalid_argument("Resource query parameter is required"))?;
903 let parsed: TagKeys = xml_io::from_xml_root(&req.body)
904 .map_err(|e| invalid_argument(format!("invalid TagKeys XML: {e}")))?;
905 let keys: Vec<String> = parsed.items.map(|k| k.key).unwrap_or_default();
906 let mut state = self.state.write();
907 let account = state.entry(DEFAULT_ACCOUNT);
908 if let Some(existing) = account.tags.get_mut(&arn) {
909 existing.retain(|t| !keys.contains(&t.key));
910 }
911 Ok(empty_response(StatusCode::NO_CONTENT))
912 }
913
914 fn list_tags_for_resource(&self, req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
915 let arn = Self::parse_arn_query(&req.raw_query)
916 .ok_or_else(|| invalid_argument("Resource query parameter is required"))?;
917 let state = self.state.read();
918 let tags = state
919 .accounts
920 .get(DEFAULT_ACCOUNT)
921 .and_then(|a| a.tags.get(&arn))
922 .cloned()
923 .unwrap_or_default();
924 drop(state);
925 let body = build_tags_xml(&tags);
926 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
927 }
928}
929
930impl CloudFrontService {
933 fn associate_alias(
934 &self,
935 req: &AwsRequest,
936 route: &Route,
937 ) -> Result<AwsResponse, AwsServiceError> {
938 let id = route
939 .id
940 .as_deref()
941 .ok_or_else(|| invalid_argument("missing distribution id"))?;
942 let alias = parse_query_value(&req.raw_query, "Alias")
943 .ok_or_else(|| invalid_argument("Alias query parameter is required"))?;
944 let mut state = self.state.write();
945 let account = state
946 .accounts
947 .get_mut(DEFAULT_ACCOUNT)
948 .ok_or_else(|| no_such_distribution(id))?;
949 if let Some(other) = account.distributions.values().find(|d| {
951 d.id != id
952 && d.config
953 .aliases
954 .as_ref()
955 .and_then(|a| a.items.as_ref())
956 .is_some_and(|i| i.cname.iter().any(|c| c == &alias))
957 }) {
958 return Err(aws_error(
959 StatusCode::CONFLICT,
960 "CNAMEAlreadyExists",
961 format!(
962 "Alias {alias} is already associated with distribution {}",
963 other.id
964 ),
965 ));
966 }
967 let dist = account
968 .distributions
969 .get_mut(id)
970 .ok_or_else(|| no_such_distribution(id))?;
971 let aliases = dist.config.aliases.get_or_insert_with(Default::default);
972 let items = aliases
973 .items
974 .get_or_insert_with(crate::model::AliasItems::default);
975 if !items.cname.iter().any(|c| c == &alias) {
976 items.cname.push(alias.clone());
977 aliases.quantity = items.cname.len() as i32;
978 }
979 dist.etag = generate_etag();
980 dist.last_modified_time = Utc::now();
981 Ok(empty_response(StatusCode::OK))
982 }
983
984 fn list_conflicting_aliases(&self, _req: &AwsRequest) -> Result<AwsResponse, AwsServiceError> {
985 let body = format!(
988 "{XML_DECL}<ConflictingAliasesList xmlns=\"{NS}\"><Quantity>0</Quantity></ConflictingAliasesList>",
989 NS = crate::NAMESPACE,
990 XML_DECL = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
991 );
992 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
993 }
994
995 fn associate_web_acl(
996 &self,
997 req: &AwsRequest,
998 route: &Route,
999 ) -> Result<AwsResponse, AwsServiceError> {
1000 let id = route
1001 .id
1002 .as_deref()
1003 .ok_or_else(|| invalid_argument("missing distribution id"))?;
1004 let parsed: AssociateAliasRequest = xml_io::from_xml_root(&req.body)
1005 .map_err(|e| invalid_argument(format!("invalid request XML: {e}")))?;
1006 let mut state = self.state.write();
1007 let account = state
1008 .accounts
1009 .get_mut(DEFAULT_ACCOUNT)
1010 .ok_or_else(|| no_such_distribution(id))?;
1011 let dist = account
1012 .distributions
1013 .get_mut(id)
1014 .ok_or_else(|| no_such_distribution(id))?;
1015 dist.config.web_acl_id = Some(parsed.web_acl_arn.clone());
1016 dist.etag = generate_etag();
1017 dist.last_modified_time = Utc::now();
1018 let body = format!(
1019 "{XML_DECL}<AssociateDistributionWebACLResult xmlns=\"{NS}\"><Id>{}</Id><WebACLArn>{}</WebACLArn></AssociateDistributionWebACLResult>",
1020 esc(id), esc(&parsed.web_acl_arn),
1021 NS = crate::NAMESPACE,
1022 XML_DECL = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1023 );
1024 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
1025 }
1026
1027 fn disassociate_web_acl(
1028 &self,
1029 _req: &AwsRequest,
1030 route: &Route,
1031 ) -> Result<AwsResponse, AwsServiceError> {
1032 let id = route
1033 .id
1034 .as_deref()
1035 .ok_or_else(|| invalid_argument("missing distribution id"))?;
1036 let mut state = self.state.write();
1037 let account = state
1038 .accounts
1039 .get_mut(DEFAULT_ACCOUNT)
1040 .ok_or_else(|| no_such_distribution(id))?;
1041 let dist = account
1042 .distributions
1043 .get_mut(id)
1044 .ok_or_else(|| no_such_distribution(id))?;
1045 dist.config.web_acl_id = None;
1046 dist.etag = generate_etag();
1047 dist.last_modified_time = Utc::now();
1048 let body = format!(
1049 "{XML_DECL}<DisassociateDistributionWebACLResult xmlns=\"{NS}\"><Id>{}</Id></DisassociateDistributionWebACLResult>",
1050 esc(id),
1051 NS = crate::NAMESPACE,
1052 XML_DECL = "<?xml version=\"1.0\" encoding=\"UTF-8\"?>"
1053 );
1054 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
1055 }
1056}
1057
1058#[derive(serde::Deserialize, Default, Debug)]
1059#[serde(rename_all = "PascalCase")]
1060struct AssociateAliasRequest {
1061 #[serde(rename = "WebACLArn", default)]
1062 web_acl_arn: String,
1063}
1064
1065pub(crate) fn esc(s: &str) -> String {
1073 let mut out = String::with_capacity(s.len());
1074 for c in s.chars() {
1075 match c {
1076 '&' => out.push_str("&"),
1077 '<' => out.push_str("<"),
1078 '>' => out.push_str(">"),
1079 '"' => out.push_str("""),
1080 '\'' => out.push_str("'"),
1081 _ => out.push(c),
1082 }
1083 }
1084 out
1085}
1086
1087pub(crate) fn build_distribution_xml(dist: &StoredDistribution) -> String {
1088 let mut out = String::with_capacity(2048);
1089 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
1090 out.push_str(&format!(
1091 "<Distribution xmlns=\"{ns}\">",
1092 ns = crate::NAMESPACE
1093 ));
1094 out.push_str(&format!("<Id>{}</Id>", esc(&dist.id)));
1095 out.push_str(&format!("<ARN>{}</ARN>", esc(&dist.arn)));
1096 out.push_str(&format!("<Status>{}</Status>", esc(&dist.status)));
1097 out.push_str(&format!(
1098 "<LastModifiedTime>{}</LastModifiedTime>",
1099 rfc3339(&dist.last_modified_time)
1100 ));
1101 out.push_str(&format!(
1102 "<InProgressInvalidationBatches>{}</InProgressInvalidationBatches>",
1103 dist.in_progress_invalidation_batches
1104 ));
1105 out.push_str(&format!(
1106 "<DomainName>{}</DomainName>",
1107 esc(&dist.domain_name)
1108 ));
1109 out.push_str("<ActiveTrustedSigners><Enabled>false</Enabled><Quantity>0</Quantity></ActiveTrustedSigners>");
1110 out.push_str("<ActiveTrustedKeyGroups><Enabled>false</Enabled><Quantity>0</Quantity></ActiveTrustedKeyGroups>");
1111 let inner = quick_xml::se::to_string_with_root("DistributionConfig", &dist.config)
1112 .unwrap_or_else(|_| String::new());
1113 out.push_str(&inner);
1114 out.push_str("</Distribution>");
1115 out
1116}
1117
1118fn build_distribution_list_xml(dists: &[StoredDistribution], root: &str) -> String {
1119 let mut out = String::with_capacity(2048);
1120 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
1121 out.push_str(&format!("<{root} xmlns=\"{ns}\">", ns = crate::NAMESPACE));
1122 out.push_str("<Marker></Marker>");
1123 out.push_str(&format!("<MaxItems>{}</MaxItems>", dists.len().max(100)));
1124 out.push_str("<IsTruncated>false</IsTruncated>");
1125 out.push_str(&format!("<Quantity>{}</Quantity>", dists.len()));
1126 if dists.is_empty() {
1127 out.push_str(&format!("</{root}>"));
1128 return out;
1129 }
1130 out.push_str("<Items>");
1131 for d in dists {
1132 out.push_str("<DistributionSummary>");
1133 out.push_str(&format!("<Id>{}</Id>", esc(&d.id)));
1134 out.push_str(&format!("<ARN>{}</ARN>", esc(&d.arn)));
1135 out.push_str(&format!("<Status>{}</Status>", esc(&d.status)));
1136 out.push_str(&format!(
1137 "<LastModifiedTime>{}</LastModifiedTime>",
1138 rfc3339(&d.last_modified_time)
1139 ));
1140 out.push_str(&format!("<DomainName>{}</DomainName>", esc(&d.domain_name)));
1141 let aliases = d.config.aliases.clone().unwrap_or_default();
1142 out.push_str(&render_inline("Aliases", &aliases));
1143 let origins = d.config.origins.clone();
1144 out.push_str(&render_inline("Origins", &origins));
1145 let dcb = d.config.default_cache_behavior.clone();
1146 out.push_str(&render_inline("DefaultCacheBehavior", &dcb));
1147 let cb = d.config.cache_behaviors.clone().unwrap_or_default();
1148 out.push_str(&render_inline("CacheBehaviors", &cb));
1149 let cer = d.config.custom_error_responses.clone().unwrap_or_default();
1150 out.push_str(&render_inline("CustomErrorResponses", &cer));
1151 out.push_str(&format!("<Comment>{}</Comment>", esc(&d.config.comment)));
1152 out.push_str(&format!(
1153 "<PriceClass>{}</PriceClass>",
1154 esc(&d
1155 .config
1156 .price_class
1157 .clone()
1158 .unwrap_or_else(|| "PriceClass_All".to_string()))
1159 ));
1160 out.push_str(&format!("<Enabled>{}</Enabled>", d.config.enabled));
1161 out.push_str(&render_inline(
1162 "ViewerCertificate",
1163 &d.config.viewer_certificate.clone().unwrap_or_default(),
1164 ));
1165 out.push_str(&render_inline(
1166 "Restrictions",
1167 &d.config.restrictions.clone().unwrap_or_default(),
1168 ));
1169 out.push_str(&format!(
1170 "<WebACLId>{}</WebACLId>",
1171 esc(&d.config.web_acl_id.clone().unwrap_or_default())
1172 ));
1173 out.push_str(&format!(
1174 "<HttpVersion>{}</HttpVersion>",
1175 esc(&d
1176 .config
1177 .http_version
1178 .clone()
1179 .unwrap_or_else(|| "http2".to_string()))
1180 ));
1181 out.push_str(&format!(
1182 "<IsIPV6Enabled>{}</IsIPV6Enabled>",
1183 d.config.is_ipv6_enabled.unwrap_or(true)
1184 ));
1185 out.push_str("<Staging>false</Staging>");
1186 out.push_str("</DistributionSummary>");
1187 }
1188 out.push_str("</Items>");
1189 out.push_str(&format!("</{root}>"));
1190 out
1191}
1192
1193fn build_empty_distribution_id_list(root: &str) -> String {
1194 let mut out = String::with_capacity(256);
1195 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
1196 out.push_str(&format!("<{root} xmlns=\"{ns}\">", ns = crate::NAMESPACE));
1197 out.push_str("<Marker></Marker>");
1198 out.push_str("<MaxItems>100</MaxItems>");
1199 out.push_str("<IsTruncated>false</IsTruncated>");
1200 out.push_str("<Quantity>0</Quantity>");
1201 out.push_str(&format!("</{root}>"));
1202 out
1203}
1204
1205fn build_invalidation_xml(inv: &StoredInvalidation) -> String {
1206 let mut out = String::with_capacity(512);
1207 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
1208 out.push_str(&format!(
1209 "<Invalidation xmlns=\"{ns}\">",
1210 ns = crate::NAMESPACE
1211 ));
1212 out.push_str(&format!("<Id>{}</Id>", esc(&inv.id)));
1213 out.push_str(&format!("<Status>{}</Status>", esc(&inv.status)));
1214 out.push_str(&format!(
1215 "<CreateTime>{}</CreateTime>",
1216 rfc3339(&inv.create_time)
1217 ));
1218 out.push_str(&render_inline("InvalidationBatch", &inv.batch));
1219 out.push_str("</Invalidation>");
1220 out
1221}
1222
1223fn build_invalidation_list_xml(items: &[&StoredInvalidation]) -> String {
1224 let mut out = String::with_capacity(1024);
1225 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
1226 out.push_str(&format!(
1227 "<InvalidationList xmlns=\"{ns}\">",
1228 ns = crate::NAMESPACE
1229 ));
1230 out.push_str("<Marker></Marker>");
1231 out.push_str("<MaxItems>100</MaxItems>");
1232 out.push_str("<IsTruncated>false</IsTruncated>");
1233 out.push_str(&format!("<Quantity>{}</Quantity>", items.len()));
1234 if !items.is_empty() {
1235 out.push_str("<Items>");
1236 for inv in items {
1237 out.push_str("<InvalidationSummary>");
1238 out.push_str(&format!("<Id>{}</Id>", esc(&inv.id)));
1239 out.push_str(&format!(
1240 "<CreateTime>{}</CreateTime>",
1241 rfc3339(&inv.create_time)
1242 ));
1243 out.push_str(&format!("<Status>{}</Status>", esc(&inv.status)));
1244 out.push_str("</InvalidationSummary>");
1245 }
1246 out.push_str("</Items>");
1247 }
1248 out.push_str("</InvalidationList>");
1249 out
1250}
1251
1252fn build_tags_xml(tags: &[Tag]) -> String {
1253 let mut out = String::with_capacity(256);
1254 out.push_str("<?xml version=\"1.0\" encoding=\"UTF-8\"?>");
1255 out.push_str(&format!("<Tags xmlns=\"{ns}\">", ns = crate::NAMESPACE));
1256 out.push_str("<Items>");
1257 for t in tags {
1258 out.push_str("<Tag>");
1259 out.push_str(&format!("<Key>{}</Key>", esc(&t.key)));
1260 if let Some(v) = &t.value {
1261 out.push_str(&format!("<Value>{}</Value>", esc(v)));
1262 }
1263 out.push_str("</Tag>");
1264 }
1265 out.push_str("</Items>");
1266 out.push_str("</Tags>");
1267 out
1268}
1269
1270fn render_inline<T: serde::Serialize>(root: &str, value: &T) -> String {
1271 quick_xml::se::to_string_with_root(root, value).unwrap_or_default()
1272}
1273
1274fn validate_caller_reference(s: &str) -> Result<(), AwsServiceError> {
1277 if s.is_empty() {
1278 return Err(invalid_argument("CallerReference is required"));
1279 }
1280 Ok(())
1281}
1282
1283fn validate_origins(config: &DistributionConfig) -> Result<(), AwsServiceError> {
1284 if config.origins.quantity < 1 {
1285 return Err(invalid_argument(
1286 "DistributionConfig.Origins must contain at least one origin",
1287 ));
1288 }
1289 Ok(())
1290}
1291
1292fn account_id(_req: &AwsRequest) -> &'static str {
1293 DEFAULT_ACCOUNT
1298}
1299
1300fn generate_distribution_id() -> String {
1301 let raw = Uuid::new_v4().simple().to_string().to_uppercase();
1303 format!("E{}", &raw[..13])
1304}
1305
1306fn generate_invalidation_id() -> String {
1307 let raw = Uuid::new_v4().simple().to_string().to_uppercase();
1308 format!("I{}", &raw[..13])
1309}
1310
1311pub(crate) fn generate_etag() -> String {
1312 let raw = Uuid::new_v4().simple().to_string().to_uppercase();
1313 format!("E{}", &raw[..13])
1314}
1315
1316pub(crate) fn generate_id_with_prefix(prefix: &str) -> String {
1321 let raw = Uuid::new_v4().simple().to_string().to_uppercase();
1322 let suffix_len = 14usize.saturating_sub(prefix.len()).min(raw.len());
1323 format!("{prefix}{}", &raw[..suffix_len])
1324}
1325
1326fn rfc3339(t: &chrono::DateTime<chrono::Utc>) -> String {
1327 t.to_rfc3339_opts(chrono::SecondsFormat::Millis, true)
1328}
1329
1330pub(crate) fn invalid_argument(msg: impl Into<String>) -> AwsServiceError {
1331 aws_error(StatusCode::BAD_REQUEST, "InvalidArgument", msg)
1332}
1333
1334fn no_such_distribution(id: &str) -> AwsServiceError {
1335 aws_error(
1336 StatusCode::NOT_FOUND,
1337 "NoSuchDistribution",
1338 format!("The specified distribution does not exist: {id}"),
1339 )
1340}
1341
1342fn no_such_invalidation(id: &str) -> AwsServiceError {
1343 aws_error(
1344 StatusCode::NOT_FOUND,
1345 "NoSuchInvalidation",
1346 format!("The specified invalidation does not exist: {id}"),
1347 )
1348}
1349
1350fn internal_error(msg: impl Into<String>) -> AwsServiceError {
1351 aws_error(StatusCode::INTERNAL_SERVER_ERROR, "InternalError", msg)
1352}
1353
1354pub(crate) fn aws_error(
1355 status: StatusCode,
1356 code: impl Into<String>,
1357 msg: impl Into<String>,
1358) -> AwsServiceError {
1359 AwsServiceError::aws_error(status, code.into(), msg)
1360}
1361
1362fn set_header(headers: &mut HeaderMap, name: HeaderName, value: &str) {
1363 if let Ok(v) = HeaderValue::from_str(value) {
1364 headers.insert(name, v);
1365 }
1366}
1367
1368pub(crate) fn xml_response(status: StatusCode, body: String, headers: HeaderMap) -> AwsResponse {
1369 AwsResponse {
1370 status,
1371 content_type: "text/xml".to_string(),
1372 body: ResponseBody::Bytes(Bytes::from(body)),
1373 headers,
1374 }
1375}
1376
1377fn empty_response(status: StatusCode) -> AwsResponse {
1378 AwsResponse {
1379 status,
1380 content_type: "text/xml".to_string(),
1381 body: ResponseBody::Bytes(Bytes::new()),
1382 headers: HeaderMap::new(),
1383 }
1384}
1385
1386fn percent_decode(input: &str) -> String {
1387 let mut out = String::with_capacity(input.len());
1388 let bytes = input.as_bytes();
1389 let mut i = 0;
1390 while i < bytes.len() {
1391 let b = bytes[i];
1392 if b == b'%' && i + 2 < bytes.len() {
1393 if let (Some(a), Some(c)) = (hex_digit(bytes[i + 1]), hex_digit(bytes[i + 2])) {
1394 out.push(((a << 4) | c) as char);
1395 i += 3;
1396 continue;
1397 }
1398 }
1399 if b == b'+' {
1400 out.push(' ');
1401 } else {
1402 out.push(b as char);
1403 }
1404 i += 1;
1405 }
1406 out
1407}
1408
1409fn hex_digit(b: u8) -> Option<u8> {
1410 match b {
1411 b'0'..=b'9' => Some(b - b'0'),
1412 b'a'..=b'f' => Some(b - b'a' + 10),
1413 b'A'..=b'F' => Some(b - b'A' + 10),
1414 _ => None,
1415 }
1416}
1417
1418fn parse_query_value(query: &str, key: &str) -> Option<String> {
1419 let prefix = format!("{key}=");
1420 for pair in query.split('&').filter(|p| !p.is_empty()) {
1421 if let Some(rest) = pair.strip_prefix(&prefix) {
1422 return Some(percent_decode(rest));
1423 }
1424 }
1425 None
1426}
1427
1428#[cfg(test)]
1429mod tests {
1430 use super::*;
1431
1432 fn make_state() -> SharedCloudFrontState {
1433 Arc::new(RwLock::new(CloudFrontAccounts::new()))
1434 }
1435
1436 fn make_request(method: http::Method, path: &str, query: &str, body: &str) -> AwsRequest {
1437 AwsRequest {
1438 service: "cloudfront".into(),
1439 action: String::new(),
1440 region: "us-east-1".into(),
1441 account_id: DEFAULT_ACCOUNT.into(),
1442 request_id: Uuid::new_v4().to_string(),
1443 headers: HeaderMap::new(),
1444 query_params: std::collections::HashMap::new(),
1445 body_stream: parking_lot::Mutex::new(None),
1446 body: Bytes::from(body.to_string()),
1447 path_segments: path
1448 .split('/')
1449 .filter(|s| !s.is_empty())
1450 .map(String::from)
1451 .collect(),
1452 raw_path: path.into(),
1453 raw_query: query.into(),
1454 method,
1455 is_query_protocol: false,
1456 access_key_id: None,
1457 principal: None,
1458 }
1459 }
1460
1461 fn minimal_dist_config_xml(caller_ref: &str) -> String {
1462 format!(
1463 r#"<?xml version="1.0" encoding="UTF-8"?>
1464<DistributionConfig xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
1465 <CallerReference>{caller_ref}</CallerReference>
1466 <Origins>
1467 <Quantity>1</Quantity>
1468 <Items>
1469 <Origin>
1470 <Id>primary</Id>
1471 <DomainName>example.com</DomainName>
1472 </Origin>
1473 </Items>
1474 </Origins>
1475 <DefaultCacheBehavior>
1476 <TargetOriginId>primary</TargetOriginId>
1477 <ViewerProtocolPolicy>allow-all</ViewerProtocolPolicy>
1478 </DefaultCacheBehavior>
1479 <Comment></Comment>
1480 <Enabled>true</Enabled>
1481</DistributionConfig>"#
1482 )
1483 }
1484
1485 #[tokio::test]
1486 async fn create_then_get_then_delete_distribution() {
1487 let svc = CloudFrontService::new(make_state());
1488 let body = minimal_dist_config_xml("ref-1");
1489 let create = svc
1490 .handle(make_request(
1491 http::Method::POST,
1492 "/2020-05-31/distribution",
1493 "",
1494 &body,
1495 ))
1496 .await
1497 .unwrap();
1498 assert_eq!(create.status, StatusCode::CREATED);
1499 let etag = create
1500 .headers
1501 .get(ETAG)
1502 .unwrap()
1503 .to_str()
1504 .unwrap()
1505 .to_string();
1506 let xml = std::str::from_utf8(create.body.expect_bytes()).unwrap();
1507 let id = xml
1508 .split("<Id>")
1509 .nth(1)
1510 .unwrap()
1511 .split("</Id>")
1512 .next()
1513 .unwrap()
1514 .to_string();
1515
1516 let get = svc
1517 .handle(make_request(
1518 http::Method::GET,
1519 &format!("/2020-05-31/distribution/{id}"),
1520 "",
1521 "",
1522 ))
1523 .await
1524 .unwrap();
1525 assert_eq!(get.status, StatusCode::OK);
1526
1527 let disable_body = body.replace("<Enabled>true</Enabled>", "<Enabled>false</Enabled>");
1529 let mut update_req = make_request(
1530 http::Method::PUT,
1531 &format!("/2020-05-31/distribution/{id}/config"),
1532 "",
1533 &disable_body,
1534 );
1535 update_req.headers.insert(IF_MATCH, etag.parse().unwrap());
1536 let updated = svc.handle(update_req).await.unwrap();
1537 assert_eq!(updated.status, StatusCode::OK);
1538 let new_etag = updated
1539 .headers
1540 .get(ETAG)
1541 .unwrap()
1542 .to_str()
1543 .unwrap()
1544 .to_string();
1545
1546 let mut del_req = make_request(
1547 http::Method::DELETE,
1548 &format!("/2020-05-31/distribution/{id}"),
1549 "",
1550 "",
1551 );
1552 del_req.headers.insert(IF_MATCH, new_etag.parse().unwrap());
1553 let del = svc.handle(del_req).await.unwrap();
1554 assert_eq!(del.status, StatusCode::NO_CONTENT);
1555 }
1556
1557 #[tokio::test]
1558 async fn duplicate_caller_reference_is_rejected() {
1559 let svc = CloudFrontService::new(make_state());
1560 let body = minimal_dist_config_xml("dup-ref");
1561 svc.handle(make_request(
1562 http::Method::POST,
1563 "/2020-05-31/distribution",
1564 "",
1565 &body,
1566 ))
1567 .await
1568 .unwrap();
1569 let result = svc
1570 .handle(make_request(
1571 http::Method::POST,
1572 "/2020-05-31/distribution",
1573 "",
1574 &body,
1575 ))
1576 .await;
1577 let err = match result {
1578 Ok(_) => panic!("expected duplicate caller-reference to fail"),
1579 Err(e) => e,
1580 };
1581 assert_eq!(err.code(), "DistributionAlreadyExists");
1582 assert_eq!(err.status(), StatusCode::CONFLICT);
1583 }
1584
1585 #[tokio::test]
1586 async fn invalidation_lifecycle() {
1587 let svc = CloudFrontService::new(make_state());
1588 let body = minimal_dist_config_xml("inv-ref");
1589 let create = svc
1590 .handle(make_request(
1591 http::Method::POST,
1592 "/2020-05-31/distribution",
1593 "",
1594 &body,
1595 ))
1596 .await
1597 .unwrap();
1598 let xml = std::str::from_utf8(create.body.expect_bytes()).unwrap();
1599 let dist_id = xml
1600 .split("<Id>")
1601 .nth(1)
1602 .unwrap()
1603 .split("</Id>")
1604 .next()
1605 .unwrap();
1606 let inv_body = r#"<?xml version="1.0" encoding="UTF-8"?>
1607<InvalidationBatch xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
1608 <Paths><Quantity>1</Quantity><Items><Path>/*</Path></Items></Paths>
1609 <CallerReference>inv-1</CallerReference>
1610</InvalidationBatch>"#;
1611 let inv_resp = svc
1612 .handle(make_request(
1613 http::Method::POST,
1614 &format!("/2020-05-31/distribution/{dist_id}/invalidation"),
1615 "",
1616 inv_body,
1617 ))
1618 .await
1619 .unwrap();
1620 assert_eq!(inv_resp.status, StatusCode::CREATED);
1621 let inv_xml = std::str::from_utf8(inv_resp.body.expect_bytes()).unwrap();
1622 let inv_id = inv_xml
1623 .split("<Id>")
1624 .nth(1)
1625 .unwrap()
1626 .split("</Id>")
1627 .next()
1628 .unwrap();
1629 let get = svc
1630 .handle(make_request(
1631 http::Method::GET,
1632 &format!("/2020-05-31/distribution/{dist_id}/invalidation/{inv_id}"),
1633 "",
1634 "",
1635 ))
1636 .await
1637 .unwrap();
1638 assert_eq!(get.status, StatusCode::OK);
1639 let list = svc
1640 .handle(make_request(
1641 http::Method::GET,
1642 &format!("/2020-05-31/distribution/{dist_id}/invalidation"),
1643 "",
1644 "",
1645 ))
1646 .await
1647 .unwrap();
1648 let xml = std::str::from_utf8(list.body.expect_bytes()).unwrap();
1649 assert!(xml.contains("<Quantity>1</Quantity>"));
1650 }
1651
1652 #[tokio::test]
1653 async fn tags_roundtrip() {
1654 let svc = CloudFrontService::new(make_state());
1655 let body = minimal_dist_config_xml("tag-ref");
1656 let create = svc
1657 .handle(make_request(
1658 http::Method::POST,
1659 "/2020-05-31/distribution",
1660 "",
1661 &body,
1662 ))
1663 .await
1664 .unwrap();
1665 let xml = std::str::from_utf8(create.body.expect_bytes()).unwrap();
1666 let arn = xml
1667 .split("<ARN>")
1668 .nth(1)
1669 .unwrap()
1670 .split("</ARN>")
1671 .next()
1672 .unwrap();
1673 let tag_body = r#"<?xml version="1.0" encoding="UTF-8"?>
1674<Tags xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
1675 <Items><Tag><Key>env</Key><Value>prod</Value></Tag></Items>
1676</Tags>"#;
1677 let arn_q = format!("Operation=Tag&Resource={}", arn);
1678 let resp = svc
1679 .handle(make_request(
1680 http::Method::POST,
1681 "/2020-05-31/tagging",
1682 &arn_q,
1683 tag_body,
1684 ))
1685 .await
1686 .unwrap();
1687 assert_eq!(resp.status, StatusCode::NO_CONTENT);
1688 let list = svc
1689 .handle(make_request(
1690 http::Method::GET,
1691 "/2020-05-31/tagging",
1692 &format!("Resource={}", arn),
1693 "",
1694 ))
1695 .await
1696 .unwrap();
1697 let xml = std::str::from_utf8(list.body.expect_bytes()).unwrap();
1698 assert!(xml.contains("<Key>env</Key>"));
1699 assert!(xml.contains("<Value>prod</Value>"));
1700 }
1701
1702 #[tokio::test]
1703 async fn xml_metacharacters_in_user_input_are_escaped() {
1704 let svc = CloudFrontService::new(make_state());
1705 let body = minimal_dist_config_xml("escape-ref").replace(
1706 "<Comment></Comment>",
1707 "<Comment><![CDATA[a&b<c>d]]></Comment>",
1708 );
1709 let create = svc
1710 .handle(make_request(
1711 http::Method::POST,
1712 "/2020-05-31/distribution",
1713 "",
1714 &body,
1715 ))
1716 .await
1717 .unwrap();
1718 let xml = std::str::from_utf8(create.body.expect_bytes()).unwrap();
1719 let dist_id = xml
1720 .split("<Id>")
1721 .nth(1)
1722 .unwrap()
1723 .split("</Id>")
1724 .next()
1725 .unwrap();
1726 let arn = xml
1727 .split("<ARN>")
1728 .nth(1)
1729 .unwrap()
1730 .split("</ARN>")
1731 .next()
1732 .unwrap();
1733
1734 let tag_body = r#"<?xml version="1.0" encoding="UTF-8"?>
1735<Tags xmlns="http://cloudfront.amazonaws.com/doc/2020-05-31/">
1736 <Items><Tag><Key>env</Key><Value>a&b<c>d</Value></Tag></Items>
1737</Tags>"#;
1738 let arn_q = format!("Operation=Tag&Resource={}", arn);
1739 svc.handle(make_request(
1740 http::Method::POST,
1741 "/2020-05-31/tagging",
1742 &arn_q,
1743 tag_body,
1744 ))
1745 .await
1746 .unwrap();
1747
1748 let list = svc
1749 .handle(make_request(
1750 http::Method::GET,
1751 "/2020-05-31/tagging",
1752 &format!("Resource={}", arn),
1753 "",
1754 ))
1755 .await
1756 .unwrap();
1757 let xml = std::str::from_utf8(list.body.expect_bytes()).unwrap();
1758 assert!(xml.contains("<Value>a&b<c>d</Value>"));
1759 assert!(!xml.contains("<Value>a&b<c>d</Value>"));
1760
1761 let list_resp = svc
1764 .handle(make_request(
1765 http::Method::GET,
1766 "/2020-05-31/distribution",
1767 "",
1768 "",
1769 ))
1770 .await
1771 .unwrap();
1772 let xml = std::str::from_utf8(list_resp.body.expect_bytes()).unwrap();
1773 assert!(!xml.contains("<Comment>a&b<c>d"));
1776 assert!(xml.contains(dist_id));
1777 }
1778}