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, 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
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: "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
228impl 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#[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}