1use chrono::Utc;
5use http::{HeaderMap, StatusCode};
6
7use fakecloud_core::service::{AwsRequest, AwsResponse, AwsServiceError};
8
9use crate::extras2::{
10 CreateConnectionGroupRequest, StoredConnectionGroup, UpdateConnectionGroupRequest,
11};
12use crate::policies::{
13 not_found, precondition_failed, require_if_match, rfc3339, route_id, xml_with_etag,
14};
15use crate::router::Route;
16use crate::service::{
17 aws_error, esc, extract_body_field, generate_id_with_prefix, invalid_argument, xml_response,
18 CloudFrontService, DEFAULT_ACCOUNT,
19};
20use crate::xml_io;
21
22const NS: &str = crate::NAMESPACE;
23const XML_DECL: &str = r#"<?xml version="1.0" encoding="UTF-8"?>"#;
24
25impl CloudFrontService {
28 pub(crate) fn create_connection_group(
29 &self,
30 req: &AwsRequest,
31 ) -> Result<AwsResponse, AwsServiceError> {
32 let cfg: CreateConnectionGroupRequest = xml_io::from_xml_root(&req.body).map_err(|e| {
33 invalid_argument(format!("invalid CreateConnectionGroupRequest XML: {e}"))
34 })?;
35 if cfg.name.is_empty() {
36 return Err(invalid_argument("Name is required"));
37 }
38 let mut state = self.state.write();
39 let account = state
40 .accounts
41 .entry(DEFAULT_ACCOUNT.to_string())
42 .or_default();
43 if account
44 .connection_groups
45 .values()
46 .any(|g| g.name == cfg.name)
47 {
48 return Err(aws_error(
49 StatusCode::CONFLICT,
50 "EntityAlreadyExists",
51 format!("ConnectionGroup {} already exists", cfg.name),
52 ));
53 }
54 let id = generate_id_with_prefix("CG");
55 let arn = format!(
56 "arn:aws:cloudfront::{}:connection-group/{}",
57 DEFAULT_ACCOUNT, id
58 );
59 let routing_endpoint = format!("{}.cloudfront.net", id.to_lowercase());
60 let etag = generate_id_with_prefix("E");
61 let now = Utc::now();
62 let stored = StoredConnectionGroup {
63 id: id.clone(),
64 name: cfg.name,
65 arn,
66 routing_endpoint,
67 status: "InProgress".to_string(),
68 etag: etag.clone(),
69 created_time: now,
70 last_modified_time: now,
71 ipv6_enabled: cfg.ipv6_enabled.unwrap_or(true),
72 anycast_ip_list_id: cfg.anycast_ip_list_id,
73 enabled: cfg.enabled.unwrap_or(true),
74 is_default: false,
75 };
76 account.connection_groups.insert(id.clone(), stored.clone());
77 drop(state);
78 self.schedule_connection_group_deploy(id.clone());
79 let body = render_connection_group(&stored);
80 Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
81 }
82
83 pub(crate) fn get_connection_group(
84 &self,
85 route: &Route,
86 ) -> Result<AwsResponse, AwsServiceError> {
87 let id = route_id(route, "ConnectionGroup")?;
88 let state = self.state.read();
89 let g = state
90 .accounts
91 .get(DEFAULT_ACCOUNT)
92 .and_then(|a| {
93 a.connection_groups
94 .get(&id)
95 .cloned()
96 .or_else(|| a.connection_groups.values().find(|g| g.name == id).cloned())
97 })
98 .ok_or_else(|| not_found("ConnectionGroup", &id))?;
99 drop(state);
100 let body = render_connection_group(&g);
101 Ok(xml_with_etag(StatusCode::OK, body, &g.etag, None))
102 }
103
104 pub(crate) fn get_connection_group_by_routing_endpoint(
105 &self,
106 req: &AwsRequest,
107 ) -> Result<AwsResponse, AwsServiceError> {
108 let routing_endpoint = req
109 .query_params
110 .get("RoutingEndpoint")
111 .cloned()
112 .ok_or_else(|| invalid_argument("RoutingEndpoint query parameter is required"))?;
113 let state = self.state.read();
114 let g = state
115 .accounts
116 .get(DEFAULT_ACCOUNT)
117 .and_then(|a| {
118 a.connection_groups
119 .values()
120 .find(|g| g.routing_endpoint == routing_endpoint)
121 .cloned()
122 })
123 .ok_or_else(|| not_found("ConnectionGroup", &routing_endpoint))?;
124 drop(state);
125 let body = render_connection_group(&g);
126 Ok(xml_with_etag(StatusCode::OK, body, &g.etag, None))
127 }
128
129 pub(crate) fn update_connection_group(
130 &self,
131 req: &AwsRequest,
132 route: &Route,
133 ) -> Result<AwsResponse, AwsServiceError> {
134 let id = route_id(route, "ConnectionGroup")?;
135 let if_match = require_if_match(req)?;
136 let cfg: UpdateConnectionGroupRequest = xml_io::from_xml_root(&req.body).map_err(|e| {
137 invalid_argument(format!("invalid UpdateConnectionGroupRequest XML: {e}"))
138 })?;
139 let mut state = self.state.write();
140 let account = state
141 .accounts
142 .get_mut(DEFAULT_ACCOUNT)
143 .ok_or_else(|| not_found("ConnectionGroup", &id))?;
144 let g = account
145 .connection_groups
146 .get_mut(&id)
147 .ok_or_else(|| not_found("ConnectionGroup", &id))?;
148 if g.etag != if_match {
149 return Err(precondition_failed());
150 }
151 if let Some(v) = cfg.ipv6_enabled {
152 g.ipv6_enabled = v;
153 }
154 if let Some(v) = cfg.anycast_ip_list_id {
155 g.anycast_ip_list_id = Some(v);
156 }
157 if let Some(v) = cfg.enabled {
158 g.enabled = v;
159 }
160 g.etag = generate_id_with_prefix("E");
161 g.last_modified_time = Utc::now();
162 g.status = "InProgress".to_string();
165 let snap = g.clone();
166 drop(state);
167 self.schedule_connection_group_deploy(id.clone());
168 let body = render_connection_group(&snap);
169 Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
170 }
171
172 pub(crate) fn delete_connection_group(
173 &self,
174 req: &AwsRequest,
175 route: &Route,
176 ) -> Result<AwsResponse, AwsServiceError> {
177 let id = route_id(route, "ConnectionGroup")?;
178 let if_match = require_if_match(req)?;
179 let mut state = self.state.write();
180 let account = state
181 .accounts
182 .get_mut(DEFAULT_ACCOUNT)
183 .ok_or_else(|| not_found("ConnectionGroup", &id))?;
184 let g = account
185 .connection_groups
186 .get(&id)
187 .ok_or_else(|| not_found("ConnectionGroup", &id))?;
188 if g.etag != if_match {
189 return Err(precondition_failed());
190 }
191 if g.enabled {
192 return Err(aws_error(
193 StatusCode::PRECONDITION_FAILED,
194 "ResourceInUse",
195 "ConnectionGroup must be disabled before delete",
196 ));
197 }
198 let arn = g.arn.clone();
199 account.connection_groups.remove(&id);
200 account.tags.remove(&arn);
201 drop(state);
202 Ok(crate::policies::empty(StatusCode::NO_CONTENT))
203 }
204
205 pub(crate) fn list_connection_groups(
206 &self,
207 _req: &AwsRequest,
208 ) -> Result<AwsResponse, AwsServiceError> {
209 let state = self.state.read();
210 let mut items: Vec<StoredConnectionGroup> = state
211 .accounts
212 .get(DEFAULT_ACCOUNT)
213 .map(|a| a.connection_groups.values().cloned().collect())
214 .unwrap_or_default();
215 drop(state);
216 items.sort_by(|a, b| a.id.cmp(&b.id));
217
218 let mut body = String::with_capacity(512);
219 body.push_str(XML_DECL);
220 body.push_str(&format!("<ListConnectionGroupsResult xmlns=\"{NS}\">"));
221 body.push_str("<ConnectionGroups>");
222 for g in &items {
223 body.push_str("<ConnectionGroupSummary>");
224 push_connection_group_inner(&mut body, g);
225 body.push_str("</ConnectionGroupSummary>");
226 }
227 body.push_str("</ConnectionGroups>");
228 body.push_str("</ListConnectionGroupsResult>");
229 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
230 }
231}
232
233impl CloudFrontService {
236 pub(crate) fn list_domain_conflicts(
237 &self,
238 req: &AwsRequest,
239 ) -> Result<AwsResponse, AwsServiceError> {
240 let domain = extract_body_field(&req.body, "Domain");
244 if domain.as_deref().unwrap_or("").is_empty() {
245 return Err(invalid_argument("Domain is required"));
246 }
247 let dcv = extract_body_field(&req.body, "DomainControlValidationResource");
248 if dcv.is_none() {
249 return Err(invalid_argument(
250 "DomainControlValidationResource is required",
251 ));
252 }
253 let mut body = String::with_capacity(256);
254 body.push_str(XML_DECL);
255 body.push_str(&format!("<ListDomainConflictsResult xmlns=\"{NS}\">"));
256 body.push_str("<DomainConflicts/>");
257 body.push_str("</ListDomainConflictsResult>");
258 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
259 }
260
261 pub(crate) fn update_domain_association(
262 &self,
263 req: &AwsRequest,
264 ) -> Result<AwsResponse, AwsServiceError> {
265 let parsed: UpdateDomainAssociationBody =
266 xml_io::from_xml_root(&req.body).map_err(|e| {
267 invalid_argument(format!("invalid UpdateDomainAssociationRequest XML: {e}"))
268 })?;
269 if parsed.domain.is_empty() {
270 return Err(invalid_argument("Domain is required"));
271 }
272 let target = parsed
273 .target_resource
274 .as_ref()
275 .and_then(|t| {
276 t.distribution_id
277 .clone()
278 .or_else(|| t.distribution_tenant_id.clone())
279 })
280 .unwrap_or_default();
281 if target.is_empty() {
282 return Err(invalid_argument(
283 "TargetResource must specify DistributionId or DistributionTenantId",
284 ));
285 }
286 let etag = generate_id_with_prefix("E");
287 let mut body = String::with_capacity(256);
288 body.push_str(XML_DECL);
289 body.push_str(&format!("<UpdateDomainAssociationResult xmlns=\"{NS}\">"));
290 body.push_str(&format!("<Domain>{}</Domain>", esc(&parsed.domain)));
291 body.push_str(&format!("<ResourceId>{}</ResourceId>", esc(&target)));
292 body.push_str("</UpdateDomainAssociationResult>");
293 Ok(xml_with_etag(StatusCode::OK, body, &etag, None))
294 }
295
296 pub(crate) fn verify_dns_configuration(
297 &self,
298 req: &AwsRequest,
299 ) -> Result<AwsResponse, AwsServiceError> {
300 let parsed: VerifyDnsConfigurationBody = xml_io::from_xml_root(&req.body).map_err(|e| {
301 invalid_argument(format!("invalid VerifyDnsConfigurationRequest XML: {e}"))
302 })?;
303 if parsed.identifier.is_empty() {
304 return Err(invalid_argument("Identifier is required"));
305 }
306 let mut body = String::with_capacity(256);
307 body.push_str(XML_DECL);
308 body.push_str(&format!("<VerifyDnsConfigurationResult xmlns=\"{NS}\">"));
309 body.push_str("<DnsConfigurationList>");
310 if let Some(d) = &parsed.domain {
311 body.push_str("<DnsConfiguration>");
312 body.push_str(&format!("<Domain>{}</Domain>", esc(d)));
313 body.push_str("<Reason>fakecloud</Reason>");
314 body.push_str("<Status>valid-configuration</Status>");
315 body.push_str("</DnsConfiguration>");
316 }
317 body.push_str("</DnsConfigurationList>");
318 body.push_str("</VerifyDnsConfigurationResult>");
319 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
320 }
321
322 pub(crate) fn get_managed_certificate_details(
323 &self,
324 route: &Route,
325 ) -> Result<AwsResponse, AwsServiceError> {
326 let id = route_id(route, "ManagedCertificate")?;
327 if crate::service::is_placeholder_label(&id) {
333 return Err(aws_error(
334 StatusCode::NOT_FOUND,
335 "EntityNotFound",
336 format!("ManagedCertificate not found: {id}"),
337 ));
338 }
339 let mut body = String::with_capacity(256);
340 body.push_str(XML_DECL);
341 body.push_str(&format!("<ManagedCertificateDetails xmlns=\"{NS}\">"));
342 body.push_str(&format!(
343 "<CertificateArn>{}</CertificateArn>",
344 esc(&format!(
345 "arn:aws:acm:us-east-1:{}:certificate/{}",
346 DEFAULT_ACCOUNT, id
347 ))
348 ));
349 body.push_str("<CertificateStatus>issued</CertificateStatus>");
350 body.push_str("<ValidationTokenHost>cloudfront</ValidationTokenHost>");
351 body.push_str("</ManagedCertificateDetails>");
352 Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
353 }
354
355 pub(crate) fn update_distribution_with_staging_config(
356 &self,
357 req: &AwsRequest,
358 route: &Route,
359 ) -> Result<AwsResponse, AwsServiceError> {
360 let id = route_id(route, "Distribution")?;
361 let if_match = require_if_match(req)?;
362 let staging_id = req
363 .query_params
364 .get("StagingDistributionId")
365 .cloned()
366 .ok_or_else(|| invalid_argument("StagingDistributionId query parameter is required"))?;
367 let mut state = self.state.write();
368 let account = state
369 .accounts
370 .get_mut(DEFAULT_ACCOUNT)
371 .ok_or_else(|| not_found("Distribution", &id))?;
372 if !account.distributions.contains_key(&staging_id) {
373 return Err(not_found("Distribution", &staging_id));
374 }
375 let dist = account
376 .distributions
377 .get_mut(&id)
378 .ok_or_else(|| not_found("Distribution", &id))?;
379 if dist.etag != if_match {
380 return Err(precondition_failed());
381 }
382 dist.etag = generate_id_with_prefix("E");
383 dist.last_modified_time = Utc::now();
384 let snap = dist.clone();
385 drop(state);
386 let body = crate::service::build_distribution_xml(&snap);
387 Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
388 }
389}
390
391#[derive(Debug, serde::Deserialize, Default)]
394#[serde(rename_all = "PascalCase")]
395struct UpdateDomainAssociationBody {
396 pub domain: String,
397 #[serde(default)]
398 pub target_resource: Option<DistributionResourceId>,
399}
400
401#[derive(Debug, serde::Deserialize, Default)]
402#[serde(rename_all = "PascalCase")]
403struct DistributionResourceId {
404 #[serde(default)]
405 pub distribution_id: Option<String>,
406 #[serde(default)]
407 pub distribution_tenant_id: Option<String>,
408}
409
410#[derive(Debug, serde::Deserialize, Default)]
411#[serde(rename_all = "PascalCase")]
412struct VerifyDnsConfigurationBody {
413 pub identifier: String,
414 #[serde(default)]
415 pub domain: Option<String>,
416}
417
418fn render_connection_group(g: &StoredConnectionGroup) -> String {
419 let mut out = String::with_capacity(512);
420 out.push_str(XML_DECL);
421 out.push_str(&format!("<ConnectionGroup xmlns=\"{NS}\">"));
422 push_connection_group_inner(&mut out, g);
423 out.push_str("</ConnectionGroup>");
424 out
425}
426
427fn push_connection_group_inner(out: &mut String, g: &StoredConnectionGroup) {
428 out.push_str(&format!("<Id>{}</Id>", esc(&g.id)));
429 out.push_str(&format!("<Name>{}</Name>", esc(&g.name)));
430 out.push_str(&format!("<Arn>{}</Arn>", esc(&g.arn)));
431 out.push_str(&format!(
432 "<RoutingEndpoint>{}</RoutingEndpoint>",
433 esc(&g.routing_endpoint)
434 ));
435 out.push_str(&format!(
436 "<CreatedTime>{}</CreatedTime>",
437 rfc3339(&g.created_time)
438 ));
439 out.push_str(&format!(
440 "<LastModifiedTime>{}</LastModifiedTime>",
441 rfc3339(&g.last_modified_time)
442 ));
443 out.push_str(&format!("<Ipv6Enabled>{}</Ipv6Enabled>", g.ipv6_enabled));
444 if let Some(a) = &g.anycast_ip_list_id {
445 out.push_str(&format!("<AnycastIpListId>{}</AnycastIpListId>", esc(a)));
446 }
447 out.push_str(&format!("<Status>{}</Status>", esc(&g.status)));
448 out.push_str(&format!("<Enabled>{}</Enabled>", g.enabled));
449 out.push_str(&format!("<IsDefault>{}</IsDefault>", g.is_default));
450}