Skip to main content

fakecloud_cloudfront/
extras2_service.rs

1//! Handlers for CloudFront Batch 6b: Connection Groups, Domain ops,
2//! Managed Certificate Details, UpdateDistributionWithStagingConfig.
3
4use 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, generate_id_with_prefix, invalid_argument, xml_response, CloudFrontService,
18    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
25// ─── Connection Group ─────────────────────────────────────────────────
26
27impl 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        // UpdateConnectionGroup re-propagates to the edge; mirror the
163        // Distribution lifecycle by flipping back to InProgress.
164        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
233// ─── Domain ops + cert + staging ──────────────────────────────────────
234
235impl CloudFrontService {
236    pub(crate) fn list_domain_conflicts(
237        &self,
238        _req: &AwsRequest,
239    ) -> Result<AwsResponse, AwsServiceError> {
240        let mut body = String::with_capacity(256);
241        body.push_str(XML_DECL);
242        body.push_str(&format!("<ListDomainConflictsResult xmlns=\"{NS}\">"));
243        body.push_str("<DomainConflicts/>");
244        body.push_str("</ListDomainConflictsResult>");
245        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
246    }
247
248    pub(crate) fn update_domain_association(
249        &self,
250        req: &AwsRequest,
251    ) -> Result<AwsResponse, AwsServiceError> {
252        let parsed: UpdateDomainAssociationBody =
253            xml_io::from_xml_root(&req.body).map_err(|e| {
254                invalid_argument(format!("invalid UpdateDomainAssociationRequest XML: {e}"))
255            })?;
256        if parsed.domain.is_empty() {
257            return Err(invalid_argument("Domain is required"));
258        }
259        let target = parsed
260            .target_resource
261            .as_ref()
262            .and_then(|t| {
263                t.distribution_id
264                    .clone()
265                    .or_else(|| t.distribution_tenant_id.clone())
266            })
267            .unwrap_or_default();
268        if target.is_empty() {
269            return Err(invalid_argument(
270                "TargetResource must specify DistributionId or DistributionTenantId",
271            ));
272        }
273        let etag = generate_id_with_prefix("E");
274        let mut body = String::with_capacity(256);
275        body.push_str(XML_DECL);
276        body.push_str(&format!("<UpdateDomainAssociationResult xmlns=\"{NS}\">"));
277        body.push_str(&format!("<Domain>{}</Domain>", esc(&parsed.domain)));
278        body.push_str(&format!("<ResourceId>{}</ResourceId>", esc(&target)));
279        body.push_str("</UpdateDomainAssociationResult>");
280        Ok(xml_with_etag(StatusCode::OK, body, &etag, None))
281    }
282
283    pub(crate) fn verify_dns_configuration(
284        &self,
285        req: &AwsRequest,
286    ) -> Result<AwsResponse, AwsServiceError> {
287        let parsed: VerifyDnsConfigurationBody = xml_io::from_xml_root(&req.body).map_err(|e| {
288            invalid_argument(format!("invalid VerifyDnsConfigurationRequest XML: {e}"))
289        })?;
290        if parsed.identifier.is_empty() {
291            return Err(invalid_argument("Identifier is required"));
292        }
293        let mut body = String::with_capacity(256);
294        body.push_str(XML_DECL);
295        body.push_str(&format!("<VerifyDnsConfigurationResult xmlns=\"{NS}\">"));
296        body.push_str("<DnsConfigurationList>");
297        if let Some(d) = &parsed.domain {
298            body.push_str("<DnsConfiguration>");
299            body.push_str(&format!("<Domain>{}</Domain>", esc(d)));
300            body.push_str("<Reason>fakecloud</Reason>");
301            body.push_str("<Status>valid-configuration</Status>");
302            body.push_str("</DnsConfiguration>");
303        }
304        body.push_str("</DnsConfigurationList>");
305        body.push_str("</VerifyDnsConfigurationResult>");
306        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
307    }
308
309    pub(crate) fn get_managed_certificate_details(
310        &self,
311        route: &Route,
312    ) -> Result<AwsResponse, AwsServiceError> {
313        let id = route_id(route, "ManagedCertificate")?;
314        let mut body = String::with_capacity(256);
315        body.push_str(XML_DECL);
316        body.push_str(&format!("<ManagedCertificateDetails xmlns=\"{NS}\">"));
317        body.push_str(&format!(
318            "<CertificateArn>{}</CertificateArn>",
319            esc(&format!(
320                "arn:aws:acm:us-east-1:{}:certificate/{}",
321                DEFAULT_ACCOUNT, id
322            ))
323        ));
324        body.push_str("<CertificateStatus>issued</CertificateStatus>");
325        body.push_str("<ValidationTokenHost>cloudfront</ValidationTokenHost>");
326        body.push_str("</ManagedCertificateDetails>");
327        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
328    }
329
330    pub(crate) fn update_distribution_with_staging_config(
331        &self,
332        req: &AwsRequest,
333        route: &Route,
334    ) -> Result<AwsResponse, AwsServiceError> {
335        let id = route_id(route, "Distribution")?;
336        let if_match = require_if_match(req)?;
337        let staging_id = req
338            .query_params
339            .get("StagingDistributionId")
340            .cloned()
341            .ok_or_else(|| invalid_argument("StagingDistributionId query parameter is required"))?;
342        let mut state = self.state.write();
343        let account = state
344            .accounts
345            .get_mut(DEFAULT_ACCOUNT)
346            .ok_or_else(|| not_found("Distribution", &id))?;
347        if !account.distributions.contains_key(&staging_id) {
348            return Err(not_found("Distribution", &staging_id));
349        }
350        let dist = account
351            .distributions
352            .get_mut(&id)
353            .ok_or_else(|| not_found("Distribution", &id))?;
354        if dist.etag != if_match {
355            return Err(precondition_failed());
356        }
357        dist.etag = generate_id_with_prefix("E");
358        dist.last_modified_time = Utc::now();
359        let snap = dist.clone();
360        drop(state);
361        let body = crate::service::build_distribution_xml(&snap);
362        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
363    }
364}
365
366// ─── Helpers ──────────────────────────────────────────────────────────
367
368#[derive(Debug, serde::Deserialize, Default)]
369#[serde(rename_all = "PascalCase")]
370struct UpdateDomainAssociationBody {
371    pub domain: String,
372    #[serde(default)]
373    pub target_resource: Option<DistributionResourceId>,
374}
375
376#[derive(Debug, serde::Deserialize, Default)]
377#[serde(rename_all = "PascalCase")]
378struct DistributionResourceId {
379    #[serde(default)]
380    pub distribution_id: Option<String>,
381    #[serde(default)]
382    pub distribution_tenant_id: Option<String>,
383}
384
385#[derive(Debug, serde::Deserialize, Default)]
386#[serde(rename_all = "PascalCase")]
387struct VerifyDnsConfigurationBody {
388    pub identifier: String,
389    #[serde(default)]
390    pub domain: Option<String>,
391}
392
393fn render_connection_group(g: &StoredConnectionGroup) -> String {
394    let mut out = String::with_capacity(512);
395    out.push_str(XML_DECL);
396    out.push_str(&format!("<ConnectionGroup xmlns=\"{NS}\">"));
397    push_connection_group_inner(&mut out, g);
398    out.push_str("</ConnectionGroup>");
399    out
400}
401
402fn push_connection_group_inner(out: &mut String, g: &StoredConnectionGroup) {
403    out.push_str(&format!("<Id>{}</Id>", esc(&g.id)));
404    out.push_str(&format!("<Name>{}</Name>", esc(&g.name)));
405    out.push_str(&format!("<Arn>{}</Arn>", esc(&g.arn)));
406    out.push_str(&format!(
407        "<RoutingEndpoint>{}</RoutingEndpoint>",
408        esc(&g.routing_endpoint)
409    ));
410    out.push_str(&format!(
411        "<CreatedTime>{}</CreatedTime>",
412        rfc3339(&g.created_time)
413    ));
414    out.push_str(&format!(
415        "<LastModifiedTime>{}</LastModifiedTime>",
416        rfc3339(&g.last_modified_time)
417    ));
418    out.push_str(&format!("<Ipv6Enabled>{}</Ipv6Enabled>", g.ipv6_enabled));
419    if let Some(a) = &g.anycast_ip_list_id {
420        out.push_str(&format!("<AnycastIpListId>{}</AnycastIpListId>", esc(a)));
421    }
422    out.push_str(&format!("<Status>{}</Status>", esc(&g.status)));
423    out.push_str(&format!("<Enabled>{}</Enabled>", g.enabled));
424    out.push_str(&format!("<IsDefault>{}</IsDefault>", g.is_default));
425}