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: "Deployed".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        let body = render_connection_group(&stored);
79        Ok(xml_with_etag(StatusCode::CREATED, body, &etag, Some(&id)))
80    }
81
82    pub(crate) fn get_connection_group(
83        &self,
84        route: &Route,
85    ) -> Result<AwsResponse, AwsServiceError> {
86        let id = route_id(route, "ConnectionGroup")?;
87        let state = self.state.read();
88        let g = state
89            .accounts
90            .get(DEFAULT_ACCOUNT)
91            .and_then(|a| {
92                a.connection_groups
93                    .get(&id)
94                    .cloned()
95                    .or_else(|| a.connection_groups.values().find(|g| g.name == id).cloned())
96            })
97            .ok_or_else(|| not_found("ConnectionGroup", &id))?;
98        drop(state);
99        let body = render_connection_group(&g);
100        Ok(xml_with_etag(StatusCode::OK, body, &g.etag, None))
101    }
102
103    pub(crate) fn get_connection_group_by_routing_endpoint(
104        &self,
105        req: &AwsRequest,
106    ) -> Result<AwsResponse, AwsServiceError> {
107        let routing_endpoint = req
108            .query_params
109            .get("RoutingEndpoint")
110            .cloned()
111            .ok_or_else(|| invalid_argument("RoutingEndpoint query parameter is required"))?;
112        let state = self.state.read();
113        let g = state
114            .accounts
115            .get(DEFAULT_ACCOUNT)
116            .and_then(|a| {
117                a.connection_groups
118                    .values()
119                    .find(|g| g.routing_endpoint == routing_endpoint)
120                    .cloned()
121            })
122            .ok_or_else(|| not_found("ConnectionGroup", &routing_endpoint))?;
123        drop(state);
124        let body = render_connection_group(&g);
125        Ok(xml_with_etag(StatusCode::OK, body, &g.etag, None))
126    }
127
128    pub(crate) fn update_connection_group(
129        &self,
130        req: &AwsRequest,
131        route: &Route,
132    ) -> Result<AwsResponse, AwsServiceError> {
133        let id = route_id(route, "ConnectionGroup")?;
134        let if_match = require_if_match(req)?;
135        let cfg: UpdateConnectionGroupRequest = xml_io::from_xml_root(&req.body).map_err(|e| {
136            invalid_argument(format!("invalid UpdateConnectionGroupRequest XML: {e}"))
137        })?;
138        let mut state = self.state.write();
139        let account = state
140            .accounts
141            .get_mut(DEFAULT_ACCOUNT)
142            .ok_or_else(|| not_found("ConnectionGroup", &id))?;
143        let g = account
144            .connection_groups
145            .get_mut(&id)
146            .ok_or_else(|| not_found("ConnectionGroup", &id))?;
147        if g.etag != if_match {
148            return Err(precondition_failed());
149        }
150        if let Some(v) = cfg.ipv6_enabled {
151            g.ipv6_enabled = v;
152        }
153        if let Some(v) = cfg.anycast_ip_list_id {
154            g.anycast_ip_list_id = Some(v);
155        }
156        if let Some(v) = cfg.enabled {
157            g.enabled = v;
158        }
159        g.etag = generate_id_with_prefix("E");
160        g.last_modified_time = Utc::now();
161        let snap = g.clone();
162        drop(state);
163        let body = render_connection_group(&snap);
164        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
165    }
166
167    pub(crate) fn delete_connection_group(
168        &self,
169        req: &AwsRequest,
170        route: &Route,
171    ) -> Result<AwsResponse, AwsServiceError> {
172        let id = route_id(route, "ConnectionGroup")?;
173        let if_match = require_if_match(req)?;
174        let mut state = self.state.write();
175        let account = state
176            .accounts
177            .get_mut(DEFAULT_ACCOUNT)
178            .ok_or_else(|| not_found("ConnectionGroup", &id))?;
179        let g = account
180            .connection_groups
181            .get(&id)
182            .ok_or_else(|| not_found("ConnectionGroup", &id))?;
183        if g.etag != if_match {
184            return Err(precondition_failed());
185        }
186        if g.enabled {
187            return Err(aws_error(
188                StatusCode::PRECONDITION_FAILED,
189                "ResourceInUse",
190                "ConnectionGroup must be disabled before delete",
191            ));
192        }
193        let arn = g.arn.clone();
194        account.connection_groups.remove(&id);
195        account.tags.remove(&arn);
196        drop(state);
197        Ok(crate::policies::empty(StatusCode::NO_CONTENT))
198    }
199
200    pub(crate) fn list_connection_groups(
201        &self,
202        _req: &AwsRequest,
203    ) -> Result<AwsResponse, AwsServiceError> {
204        let state = self.state.read();
205        let mut items: Vec<StoredConnectionGroup> = state
206            .accounts
207            .get(DEFAULT_ACCOUNT)
208            .map(|a| a.connection_groups.values().cloned().collect())
209            .unwrap_or_default();
210        drop(state);
211        items.sort_by(|a, b| a.id.cmp(&b.id));
212
213        let mut body = String::with_capacity(512);
214        body.push_str(XML_DECL);
215        body.push_str(&format!("<ListConnectionGroupsResult xmlns=\"{NS}\">"));
216        body.push_str("<ConnectionGroups>");
217        for g in &items {
218            body.push_str("<ConnectionGroupSummary>");
219            push_connection_group_inner(&mut body, g);
220            body.push_str("</ConnectionGroupSummary>");
221        }
222        body.push_str("</ConnectionGroups>");
223        body.push_str("</ListConnectionGroupsResult>");
224        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
225    }
226}
227
228// ─── Domain ops + cert + staging ──────────────────────────────────────
229
230impl CloudFrontService {
231    pub(crate) fn list_domain_conflicts(
232        &self,
233        _req: &AwsRequest,
234    ) -> Result<AwsResponse, AwsServiceError> {
235        let mut body = String::with_capacity(256);
236        body.push_str(XML_DECL);
237        body.push_str(&format!("<ListDomainConflictsResult xmlns=\"{NS}\">"));
238        body.push_str("<DomainConflicts/>");
239        body.push_str("</ListDomainConflictsResult>");
240        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
241    }
242
243    pub(crate) fn update_domain_association(
244        &self,
245        req: &AwsRequest,
246    ) -> Result<AwsResponse, AwsServiceError> {
247        let parsed: UpdateDomainAssociationBody =
248            xml_io::from_xml_root(&req.body).map_err(|e| {
249                invalid_argument(format!("invalid UpdateDomainAssociationRequest XML: {e}"))
250            })?;
251        if parsed.domain.is_empty() {
252            return Err(invalid_argument("Domain is required"));
253        }
254        let target = parsed
255            .target_resource
256            .as_ref()
257            .and_then(|t| {
258                t.distribution_id
259                    .clone()
260                    .or_else(|| t.distribution_tenant_id.clone())
261            })
262            .unwrap_or_default();
263        if target.is_empty() {
264            return Err(invalid_argument(
265                "TargetResource must specify DistributionId or DistributionTenantId",
266            ));
267        }
268        let etag = generate_id_with_prefix("E");
269        let mut body = String::with_capacity(256);
270        body.push_str(XML_DECL);
271        body.push_str(&format!("<UpdateDomainAssociationResult xmlns=\"{NS}\">"));
272        body.push_str(&format!("<Domain>{}</Domain>", esc(&parsed.domain)));
273        body.push_str(&format!("<ResourceId>{}</ResourceId>", esc(&target)));
274        body.push_str("</UpdateDomainAssociationResult>");
275        Ok(xml_with_etag(StatusCode::OK, body, &etag, None))
276    }
277
278    pub(crate) fn verify_dns_configuration(
279        &self,
280        req: &AwsRequest,
281    ) -> Result<AwsResponse, AwsServiceError> {
282        let parsed: VerifyDnsConfigurationBody = xml_io::from_xml_root(&req.body).map_err(|e| {
283            invalid_argument(format!("invalid VerifyDnsConfigurationRequest XML: {e}"))
284        })?;
285        if parsed.identifier.is_empty() {
286            return Err(invalid_argument("Identifier is required"));
287        }
288        let mut body = String::with_capacity(256);
289        body.push_str(XML_DECL);
290        body.push_str(&format!("<VerifyDnsConfigurationResult xmlns=\"{NS}\">"));
291        body.push_str("<DnsConfigurationList>");
292        if let Some(d) = &parsed.domain {
293            body.push_str("<DnsConfiguration>");
294            body.push_str(&format!("<Domain>{}</Domain>", esc(d)));
295            body.push_str("<Reason>fakecloud</Reason>");
296            body.push_str("<Status>valid-configuration</Status>");
297            body.push_str("</DnsConfiguration>");
298        }
299        body.push_str("</DnsConfigurationList>");
300        body.push_str("</VerifyDnsConfigurationResult>");
301        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
302    }
303
304    pub(crate) fn get_managed_certificate_details(
305        &self,
306        route: &Route,
307    ) -> Result<AwsResponse, AwsServiceError> {
308        let id = route_id(route, "ManagedCertificate")?;
309        let mut body = String::with_capacity(256);
310        body.push_str(XML_DECL);
311        body.push_str(&format!("<ManagedCertificateDetails xmlns=\"{NS}\">"));
312        body.push_str(&format!(
313            "<CertificateArn>{}</CertificateArn>",
314            esc(&format!(
315                "arn:aws:acm:us-east-1:{}:certificate/{}",
316                DEFAULT_ACCOUNT, id
317            ))
318        ));
319        body.push_str("<CertificateStatus>issued</CertificateStatus>");
320        body.push_str("<ValidationTokenHost>cloudfront</ValidationTokenHost>");
321        body.push_str("</ManagedCertificateDetails>");
322        Ok(xml_response(StatusCode::OK, body, HeaderMap::new()))
323    }
324
325    pub(crate) fn update_distribution_with_staging_config(
326        &self,
327        req: &AwsRequest,
328        route: &Route,
329    ) -> Result<AwsResponse, AwsServiceError> {
330        let id = route_id(route, "Distribution")?;
331        let if_match = require_if_match(req)?;
332        let staging_id = req
333            .query_params
334            .get("StagingDistributionId")
335            .cloned()
336            .ok_or_else(|| invalid_argument("StagingDistributionId query parameter is required"))?;
337        let mut state = self.state.write();
338        let account = state
339            .accounts
340            .get_mut(DEFAULT_ACCOUNT)
341            .ok_or_else(|| not_found("Distribution", &id))?;
342        if !account.distributions.contains_key(&staging_id) {
343            return Err(not_found("Distribution", &staging_id));
344        }
345        let dist = account
346            .distributions
347            .get_mut(&id)
348            .ok_or_else(|| not_found("Distribution", &id))?;
349        if dist.etag != if_match {
350            return Err(precondition_failed());
351        }
352        dist.etag = generate_id_with_prefix("E");
353        dist.last_modified_time = Utc::now();
354        let snap = dist.clone();
355        drop(state);
356        let body = crate::service::build_distribution_xml(&snap);
357        Ok(xml_with_etag(StatusCode::OK, body, &snap.etag, None))
358    }
359}
360
361// ─── Helpers ──────────────────────────────────────────────────────────
362
363#[derive(Debug, serde::Deserialize, Default)]
364#[serde(rename_all = "PascalCase")]
365struct UpdateDomainAssociationBody {
366    pub domain: String,
367    #[serde(default)]
368    pub target_resource: Option<DistributionResourceId>,
369}
370
371#[derive(Debug, serde::Deserialize, Default)]
372#[serde(rename_all = "PascalCase")]
373struct DistributionResourceId {
374    #[serde(default)]
375    pub distribution_id: Option<String>,
376    #[serde(default)]
377    pub distribution_tenant_id: Option<String>,
378}
379
380#[derive(Debug, serde::Deserialize, Default)]
381#[serde(rename_all = "PascalCase")]
382struct VerifyDnsConfigurationBody {
383    pub identifier: String,
384    #[serde(default)]
385    pub domain: Option<String>,
386}
387
388fn render_connection_group(g: &StoredConnectionGroup) -> String {
389    let mut out = String::with_capacity(512);
390    out.push_str(XML_DECL);
391    out.push_str(&format!("<ConnectionGroup xmlns=\"{NS}\">"));
392    push_connection_group_inner(&mut out, g);
393    out.push_str("</ConnectionGroup>");
394    out
395}
396
397fn push_connection_group_inner(out: &mut String, g: &StoredConnectionGroup) {
398    out.push_str(&format!("<Id>{}</Id>", esc(&g.id)));
399    out.push_str(&format!("<Name>{}</Name>", esc(&g.name)));
400    out.push_str(&format!("<Arn>{}</Arn>", esc(&g.arn)));
401    out.push_str(&format!(
402        "<RoutingEndpoint>{}</RoutingEndpoint>",
403        esc(&g.routing_endpoint)
404    ));
405    out.push_str(&format!(
406        "<CreatedTime>{}</CreatedTime>",
407        rfc3339(&g.created_time)
408    ));
409    out.push_str(&format!(
410        "<LastModifiedTime>{}</LastModifiedTime>",
411        rfc3339(&g.last_modified_time)
412    ));
413    out.push_str(&format!("<Ipv6Enabled>{}</Ipv6Enabled>", g.ipv6_enabled));
414    if let Some(a) = &g.anycast_ip_list_id {
415        out.push_str(&format!("<AnycastIpListId>{}</AnycastIpListId>", esc(a)));
416    }
417    out.push_str(&format!("<Status>{}</Status>", esc(&g.status)));
418    out.push_str(&format!("<Enabled>{}</Enabled>", g.enabled));
419    out.push_str(&format!("<IsDefault>{}</IsDefault>", g.is_default));
420}