1use std::collections::BTreeMap;
42use std::sync::{Arc, Mutex};
43use std::time::Duration;
44
45use time::OffsetDateTime;
46
47use crate::route53::{
48 CredentialFetch, ImdsError, InstanceCredentials, Response, RetryPolicy, Route53Credentials,
49 Route53Dns, TimedCredentials, Transport,
50};
51
52pub struct FakeImds {
65 state: Mutex<ImdsState>,
66 lifetime: time::Duration,
67}
68
69struct ImdsState {
70 issued: Vec<(String, OffsetDateTime)>,
72 reads: u64,
73 fail_next: u32,
75}
76
77impl FakeImds {
78 #[must_use]
84 pub fn new() -> Arc<Self> {
85 Self::with_lifetime(time::Duration::hours(6))
86 }
87
88 #[must_use]
90 pub fn with_lifetime(lifetime: time::Duration) -> Arc<Self> {
91 Arc::new(Self {
92 state: Mutex::new(ImdsState {
93 issued: Vec::new(),
94 reads: 0,
95 fail_next: 0,
96 }),
97 lifetime,
98 })
99 }
100
101 pub fn credentials(self: &Arc<Self>) -> Result<Arc<InstanceCredentials>, ImdsError> {
110 InstanceCredentials::with_fetch(Box::new(Arc::clone(self))).map(Arc::new)
111 }
112
113 #[must_use]
115 pub fn reads(&self) -> u64 {
116 self.guard().reads
117 }
118
119 #[must_use]
121 pub fn issued(&self) -> Vec<String> {
122 self.guard()
123 .issued
124 .iter()
125 .map(|(id, _)| id.clone())
126 .collect()
127 }
128
129 pub fn fail_next(&self, count: u32) {
131 self.guard().fail_next = count;
132 }
133
134 pub fn expire_current(&self) {
142 let mut state = self.guard();
143 if let Some((_, expires_at)) = state.issued.last_mut() {
144 *expires_at = OffsetDateTime::now_utc() - time::Duration::seconds(1);
145 }
146 }
147
148 #[must_use]
150 pub fn is_live(&self, access_key_id: &str) -> bool {
151 let now = OffsetDateTime::now_utc();
152 self.guard()
153 .issued
154 .iter()
155 .any(|(id, expires_at)| id == access_key_id && *expires_at > now)
156 }
157
158 fn guard(&self) -> std::sync::MutexGuard<'_, ImdsState> {
159 self.state
160 .lock()
161 .unwrap_or_else(|poisoned| poisoned.into_inner())
162 }
163}
164
165impl CredentialFetch for Arc<FakeImds> {
166 fn fetch(&self) -> Result<TimedCredentials, ImdsError> {
167 let mut state = self.guard();
168 state.reads += 1;
169 if state.fail_next > 0 {
170 state.fail_next -= 1;
171 return Err(ImdsError::Response(
172 "instance metadata service is unreachable".to_string(),
173 ));
174 }
175 let serial = state.issued.len();
176 let access_key_id = format!("ASIAFAKEKEY{serial:04}");
177 let expires_at = OffsetDateTime::now_utc() + self.lifetime;
178 state.issued.push((access_key_id.clone(), expires_at));
179 Ok(TimedCredentials {
180 credentials: Route53Credentials {
181 access_key_id,
182 secret_access_key: format!("fake-secret-{serial:04}"),
183 session_token: Some(format!("fake-session-token-{serial:04}")),
184 },
185 expires_at: Some(expires_at),
186 })
187 }
188}
189
190#[derive(Clone, Debug)]
196struct RecordSet {
197 ttl: u32,
198 values: Vec<String>,
199 visible: bool,
203 deleting: bool,
206}
207
208type Key = (String, String);
211
212struct PendingChange {
214 polls_left: u32,
215 keys: Vec<Key>,
217}
218
219struct ZoneState {
220 sets: BTreeMap<Key, RecordSet>,
221 changes: BTreeMap<String, PendingChange>,
222 next_change: u64,
223 calls: Vec<(String, String)>,
224 call_count: u64,
225 change_batches: u64,
226 throttle_next: u32,
227 fail_next: u32,
228 server_error_next: u32,
229 pending_polls: u32,
230 max_record_sets: u64,
231 page_size: usize,
232 expire_credential_after: Option<u64>,
233}
234
235pub struct FakeAws {
243 hosted_zone_id: String,
244 state: Mutex<ZoneState>,
245 credentials: Mutex<Option<(Arc<InstanceCredentials>, Arc<FakeImds>)>>,
246}
247
248impl FakeAws {
249 #[must_use]
253 pub fn new(hosted_zone_id: impl Into<String>) -> Arc<Self> {
254 Arc::new(Self {
255 hosted_zone_id: hosted_zone_id.into(),
256 state: Mutex::new(ZoneState {
257 sets: BTreeMap::new(),
258 changes: BTreeMap::new(),
259 next_change: 0,
260 calls: Vec::new(),
261 call_count: 0,
262 change_batches: 0,
263 throttle_next: 0,
264 fail_next: 0,
265 server_error_next: 0,
266 pending_polls: 0,
267 max_record_sets: 10_000,
268 page_size: 100,
269 expire_credential_after: None,
270 }),
271 credentials: Mutex::new(None),
272 })
273 }
274
275 #[must_use]
277 pub fn hosted_zone_id(&self) -> &str {
278 &self.hosted_zone_id
279 }
280
281 pub fn require_credentials(&self, credentials: Arc<InstanceCredentials>, imds: Arc<FakeImds>) {
285 *self
286 .credentials
287 .lock()
288 .unwrap_or_else(|poisoned| poisoned.into_inner()) = Some((credentials, imds));
289 }
290
291 pub fn expire_credential_after(&self, calls: u64) {
295 self.guard().expire_credential_after = Some(calls);
296 }
297
298 pub fn expire_credential(&self) {
303 let held = self
304 .credentials
305 .lock()
306 .unwrap_or_else(|poisoned| poisoned.into_inner());
307 if let Some((credentials, imds)) = held.as_ref() {
308 credentials.expire_held();
309 imds.expire_current();
310 }
311 }
312
313 pub fn set_pending_polls(&self, polls: u32) {
317 self.guard().pending_polls = polls;
318 }
319
320 pub fn set_max_record_sets(&self, max: u64) {
323 self.guard().max_record_sets = max;
324 }
325
326 pub fn set_page_size(&self, size: usize) {
328 self.guard().page_size = size.max(1);
329 }
330
331 pub fn throttle_next(&self, count: u32) {
333 self.guard().throttle_next = count;
334 }
335
336 pub fn server_error_next(&self, count: u32) {
338 self.guard().server_error_next = count;
339 }
340
341 pub fn fail_next(&self, count: u32) {
343 self.guard().fail_next = count;
344 }
345
346 pub fn seed(&self, name: &str, rtype: &str, ttl: u32, values: &[&str]) {
350 let mut state = self.guard();
351 state.sets.insert(
352 (normalize(name), rtype.to_ascii_uppercase()),
353 RecordSet {
354 ttl,
355 values: values.iter().map(|v| (*v).to_string()).collect(),
356 visible: true,
357 deleting: false,
358 },
359 );
360 }
361
362 #[must_use]
365 pub fn record_sets(&self) -> Vec<(String, String, Vec<String>)> {
366 self.guard()
367 .sets
368 .iter()
369 .filter(|(_, set)| set.visible)
370 .map(|((name, rtype), set)| (name.clone(), rtype.clone(), set.values.clone()))
371 .collect()
372 }
373
374 #[must_use]
376 pub fn holds(&self, name: &str, rtype: &str) -> bool {
377 self.guard()
378 .sets
379 .get(&(normalize(name), rtype.to_ascii_uppercase()))
380 .is_some_and(|set| set.visible)
381 }
382
383 #[must_use]
386 pub fn record_set_count(&self) -> usize {
387 self.guard().sets.len()
388 }
389
390 #[must_use]
392 pub fn calls(&self) -> Vec<(String, String)> {
393 self.guard().calls.clone()
394 }
395
396 #[must_use]
400 pub fn change_batches(&self) -> u64 {
401 self.guard().change_batches
402 }
403
404 fn guard(&self) -> std::sync::MutexGuard<'_, ZoneState> {
405 self.state
406 .lock()
407 .unwrap_or_else(|poisoned| poisoned.into_inner())
408 }
409
410 fn check_credentials(&self, call_count: u64, expire_after: Option<u64>) -> Option<Response> {
412 let held = self
413 .credentials
414 .lock()
415 .unwrap_or_else(|poisoned| poisoned.into_inner());
416 let (credentials, imds) = held.as_ref()?;
417 if expire_after.is_some_and(|after| call_count > after) {
418 credentials.expire_held();
419 imds.expire_current();
420 }
421 match credentials.current() {
422 Ok(current) if imds.is_live(¤t.access_key_id) => None,
423 Ok(_) => Some(Response {
424 status: 403,
425 body: error_body(
426 "ExpiredTokenException",
427 "The security token included in the request is expired",
428 ),
429 }),
430 Err(err) => Some(Response {
431 status: 403,
432 body: error_body("InvalidClientTokenId", &err.to_string()),
433 }),
434 }
435 }
436}
437
438impl Transport for Arc<FakeAws> {
439 fn call(
440 &self,
441 method: &str,
442 path: &str,
443 query: &[(String, String)],
444 body: &str,
445 ) -> Result<Response, String> {
446 let (call_count, expire_after) = {
447 let mut state = self.guard();
448 state.calls.push((method.to_string(), path.to_string()));
449 state.call_count += 1;
450 if method == "POST" && path.ends_with("/rrset") {
451 state.change_batches += 1;
452 }
453 (state.call_count, state.expire_credential_after)
454 };
455
456 if let Some(refusal) = self.check_credentials(call_count, expire_after) {
457 return Ok(refusal);
458 }
459
460 let mut state = self.guard();
461 if state.fail_next > 0 {
462 state.fail_next -= 1;
463 return Err("connection reset by peer".to_string());
464 }
465 if state.throttle_next > 0 {
466 state.throttle_next -= 1;
467 return Ok(Response {
468 status: 400,
469 body: error_body("Throttling", "Rate exceeded"),
470 });
471 }
472 if state.server_error_next > 0 {
473 state.server_error_next -= 1;
474 return Ok(Response {
475 status: 500,
476 body: error_body("InternalError", "We encountered an internal error"),
477 });
478 }
479
480 if path.ends_with("/rrset") {
481 match method {
482 "POST" => return Ok(state.apply_change_batch(body)),
483 "GET" => return Ok(state.list_record_sets(query)),
484 _ => {}
485 }
486 }
487 if method == "GET" {
488 if let Some((_, change_id)) = path.rsplit_once("/change/") {
489 return Ok(state.get_change(change_id));
490 }
491 }
492 Ok(Response {
493 status: 404,
494 body: error_body("NoSuchElement", "no such route on the fake zone"),
495 })
496 }
497}
498
499impl ZoneState {
500 fn apply_change_batch(&mut self, body: &str) -> Response {
501 let changes = match parse_changes(body) {
502 Ok(changes) => changes,
503 Err(message) => {
504 return Response {
505 status: 400,
506 body: error_body("InvalidChangeBatch", &message),
507 }
508 }
509 };
510
511 let mut created = 0u64;
514 for change in &changes {
515 let key = (normalize(&change.name), change.rtype.clone());
516 let live = self.sets.get(&key).filter(|set| !set.deleting);
517 match change.action.as_str() {
518 "CREATE" => {
519 if live.is_some() {
520 return Response {
521 status: 400,
522 body: error_body(
523 "InvalidChangeBatch",
524 &format!(
525 "Tried to create resource record set [name='{}.', type='{}'] but it already exists",
526 key.0, key.1
527 ),
528 ),
529 };
530 }
531 created += 1;
532 }
533 "DELETE" => match live {
534 None => {
535 return Response {
536 status: 400,
537 body: error_body(
538 "InvalidChangeBatch",
539 &format!(
540 "Tried to delete resource record set [name='{}.', type='{}'] but it was not found",
541 key.0, key.1
542 ),
543 ),
544 }
545 }
546 Some(set) if set.values != change.values => {
547 return Response {
548 status: 400,
549 body: error_body(
550 "InvalidChangeBatch",
551 &format!(
552 "Tried to delete resource record set [name='{}.', type='{}'] but the values provided do not match the current values",
553 key.0, key.1
554 ),
555 ),
556 }
557 }
558 Some(_) => {}
559 },
560 "UPSERT" => {
561 if live.is_none() {
562 created += 1;
563 }
564 }
565 other => {
566 return Response {
567 status: 400,
568 body: error_body(
569 "InvalidChangeBatch",
570 &format!("unsupported action {other}"),
571 ),
572 }
573 }
574 }
575 }
576
577 if self.sets.len() as u64 + created > self.max_record_sets {
578 return Response {
579 status: 400,
580 body: error_body(
581 "LimitsExceeded",
582 &format!(
583 "the hosted zone has reached its limit of {} resource record sets",
584 self.max_record_sets
585 ),
586 ),
587 };
588 }
589
590 self.next_change += 1;
591 let change_id = format!("C{:08}", self.next_change);
592 let polls = self.pending_polls;
593 let mut keys = Vec::with_capacity(changes.len());
594 for change in changes {
595 let key = (normalize(&change.name), change.rtype.clone());
596 keys.push(key.clone());
597 match change.action.as_str() {
598 "DELETE" => {
599 if polls == 0 {
600 self.sets.remove(&key);
601 } else if let Some(set) = self.sets.get_mut(&key) {
602 set.deleting = true;
603 }
604 }
605 _ => {
606 self.sets.insert(
607 key,
608 RecordSet {
609 ttl: change.ttl,
610 values: change.values,
611 visible: polls == 0,
612 deleting: false,
613 },
614 );
615 }
616 }
617 }
618 if polls > 0 {
619 self.changes.insert(
620 change_id.clone(),
621 PendingChange {
622 polls_left: polls,
623 keys,
624 },
625 );
626 }
627
628 let status = if polls == 0 { "INSYNC" } else { "PENDING" };
629 Response {
630 status: 200,
631 body: format!(
632 "<ChangeResourceRecordSetsResponse><ChangeInfo><Id>/change/{change_id}</Id>\
633 <Status>{status}</Status></ChangeInfo></ChangeResourceRecordSetsResponse>"
634 ),
635 }
636 }
637
638 fn get_change(&mut self, change_id: &str) -> Response {
639 let status = match self.changes.get_mut(change_id) {
640 None => "INSYNC",
641 Some(change) if change.polls_left > 0 => {
642 change.polls_left -= 1;
643 "PENDING"
644 }
645 Some(_) => {
646 let change = self
647 .changes
648 .remove(change_id)
649 .expect("matched immediately above");
650 for key in change.keys {
651 match self.sets.get_mut(&key) {
652 Some(set) if set.deleting => {
653 self.sets.remove(&key);
654 }
655 Some(set) => set.visible = true,
656 None => {}
657 }
658 }
659 "INSYNC"
660 }
661 };
662 Response {
663 status: 200,
664 body: format!(
665 "<GetChangeResponse><ChangeInfo><Id>/change/{change_id}</Id>\
666 <Status>{status}</Status></ChangeInfo></GetChangeResponse>"
667 ),
668 }
669 }
670
671 fn list_record_sets(&self, query: &[(String, String)]) -> Response {
672 let field = |name: &str| {
673 query
674 .iter()
675 .find(|(key, _)| key == name)
676 .map(|(_, value)| value.clone())
677 };
678 let start = field("name").map(|name| (normalize(&name), field("type").unwrap_or_default()));
679
680 let listed: Vec<(&Key, &RecordSet)> = self
681 .sets
682 .iter()
683 .filter(|(_, set)| set.visible)
684 .filter(|(key, _)| match &start {
685 None => true,
686 Some(start) => *key >= start,
687 })
688 .collect();
689
690 let page = &listed[..listed.len().min(self.page_size)];
691 let mut body = String::from("<ListResourceRecordSetsResponse><ResourceRecordSets>");
692 for ((name, rtype), set) in page {
693 body.push_str(&format!(
694 "<ResourceRecordSet><Name>{name}.</Name><Type>{rtype}</Type><TTL>{}</TTL><ResourceRecords>",
695 set.ttl
696 ));
697 for value in &set.values {
698 body.push_str(&format!(
699 "<ResourceRecord><Value>{}</Value></ResourceRecord>",
700 escape(value)
701 ));
702 }
703 body.push_str("</ResourceRecords></ResourceRecordSet>");
704 }
705 body.push_str("</ResourceRecordSets>");
706 match listed.get(self.page_size) {
707 Some(((name, rtype), _)) => body.push_str(&format!(
708 "<IsTruncated>true</IsTruncated><NextRecordName>{name}.</NextRecordName>\
709 <NextRecordType>{rtype}</NextRecordType>"
710 )),
711 None => body.push_str("<IsTruncated>false</IsTruncated>"),
712 }
713 body.push_str("<MaxItems>100</MaxItems></ListResourceRecordSetsResponse>");
714 Response { status: 200, body }
715 }
716}
717
718#[must_use]
725pub fn route53_provider(zone: &str, aws: &Arc<FakeAws>) -> Route53Dns {
726 Route53Dns::with_transport(zone, aws.hosted_zone_id(), Box::new(Arc::clone(aws)))
727 .with_retry_policy(RetryPolicy {
728 max_attempts: 5,
729 base_delay: Duration::from_millis(1),
730 })
731 .with_poll_interval(Duration::from_millis(1))
732 .with_propagation_timeout(Duration::from_secs(10))
733}
734
735struct WireChange {
741 action: String,
742 name: String,
743 rtype: String,
744 ttl: u32,
745 values: Vec<String>,
746}
747
748fn normalize(name: &str) -> String {
750 name.trim_end_matches('.').to_ascii_lowercase()
751}
752
753fn error_body(code: &str, message: &str) -> String {
754 format!(
755 "<ErrorResponse><Error><Type>Sender</Type><Code>{code}</Code>\
756 <Message>{}</Message></Error><RequestId>fake</RequestId></ErrorResponse>",
757 escape(message)
758 )
759}
760
761fn escape(value: &str) -> String {
762 value
763 .replace('&', "&")
764 .replace('<', "<")
765 .replace('>', ">")
766}
767
768fn unescape(value: &str) -> String {
769 value
770 .replace("<", "<")
771 .replace(">", ">")
772 .replace(""", "\"")
773 .replace("'", "'")
774 .replace("&", "&")
775}
776
777fn tag(xml: &str, from: usize, name: &str) -> Option<(String, usize)> {
779 let open = format!("<{name}>");
780 let close = format!("</{name}>");
781 let start = xml[from..].find(&open)? + from + open.len();
782 let end = xml[start..].find(&close)? + start;
783 Some((unescape(&xml[start..end]), end + close.len()))
784}
785
786fn parse_changes(body: &str) -> Result<Vec<WireChange>, String> {
787 let mut changes = Vec::new();
788 let mut pos = 0;
789 while let Some(offset) = body[pos..].find("<Change>") {
790 let start = pos + offset;
791 let end = body[start..]
792 .find("</Change>")
793 .ok_or_else(|| "unterminated Change".to_string())?
794 + start;
795 let block = &body[start..end];
796 let (action, _) =
797 tag(block, 0, "Action").ok_or_else(|| "Change without Action".to_string())?;
798 let (name, _) = tag(block, 0, "Name").ok_or_else(|| "Change without Name".to_string())?;
799 let (rtype, _) = tag(block, 0, "Type").ok_or_else(|| "Change without Type".to_string())?;
800 let ttl = tag(block, 0, "TTL")
801 .and_then(|(value, _)| value.parse().ok())
802 .unwrap_or(300);
803 let mut values = Vec::new();
804 let mut vpos = 0;
805 while let Some((value, next)) = tag(block, vpos, "Value") {
806 values.push(value);
807 vpos = next;
808 }
809 changes.push(WireChange {
810 action: action.to_ascii_uppercase(),
811 name,
812 rtype: rtype.to_ascii_uppercase(),
813 ttl,
814 values,
815 });
816 pos = end + "</Change>".len();
817 }
818 if changes.is_empty() {
819 return Err("change batch with no changes".to_string());
820 }
821 Ok(changes)
822}