1use bytes::Bytes;
2use http::HeaderMap;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum AwsProtocol {
8 Query,
11 Ec2Query,
21 Json,
24 Rest,
27 RestJson,
30}
31
32fn query_protocol_for(service: &str) -> AwsProtocol {
36 if service == "ec2" {
37 AwsProtocol::Ec2Query
38 } else {
39 AwsProtocol::Query
40 }
41}
42
43const REST_XML_SERVICES: &[&str] = &["s3", "cloudfront", "route53"];
45
46const REST_JSON_SERVICES: &[&str] = &[
48 "managedblockchain",
49 "lambda",
50 "ses",
51 "apigateway",
52 "bedrock",
53 "bedrock-agent",
54 "bedrock-agent-runtime",
55 "scheduler",
56 "batch",
57 "pipes",
58 "rds-data",
59 "dsql",
60 "resource-groups",
61 "eks",
62 "glacier",
63 "backup",
64 "ram",
67 "s3tables",
70 "lakeformation",
73 "es",
77 "account",
78 "appconfig",
82 "codeartifact",
85 "elasticfilesystem",
89 "mq",
92 "kafka",
96 "mwaa",
100 "fis",
105 "xray",
110 "appsync",
115 "amplify",
120 "mediaconvert",
125 "serverlessrepo",
130 "iotdata",
135 "pinpoint",
141 "iot",
146 "iotwireless",
151];
152
153#[derive(Debug, Clone)]
155pub struct DetectedRequest {
156 pub service: String,
157 pub action: String,
158 pub protocol: AwsProtocol,
159}
160
161pub fn detect_service_headers_only(
168 headers: &HeaderMap,
169 query_params: &HashMap<String, String>,
170) -> Option<DetectedRequest> {
171 if let Some(target) = headers.get("x-amz-target").and_then(|v| v.to_str().ok()) {
173 return parse_amz_target(target);
174 }
175 if let Some(action) = query_params.get("Action") {
176 let service = extract_service_from_auth(headers)
177 .or_else(|| infer_service_from_action(action))
178 .or_else(|| parse_routing_host_from_headers(headers).map(|h| h.service));
179 if let Some(service) = service {
180 let protocol = query_protocol_for(&service);
181 return Some(DetectedRequest {
182 service,
183 action: action.clone(),
184 protocol,
185 });
186 }
187 }
188 if let Some(service) = extract_service_from_auth(headers) {
189 if let Some(protocol) = rest_protocol_for(&service) {
190 return Some(DetectedRequest {
191 service,
192 action: String::new(),
193 protocol,
194 });
195 }
196 }
197 if let Some(credential) = query_params.get("X-Amz-Credential") {
198 let parts: Vec<&str> = credential.split('/').collect();
199 if parts.len() >= 4 {
200 let service = normalize_service_name(parts[3]).to_string();
201 if let Some(protocol) = rest_protocol_for(&service) {
202 return Some(DetectedRequest {
203 service,
204 action: String::new(),
205 protocol,
206 });
207 }
208 }
209 }
210 if query_params.contains_key("AWSAccessKeyId")
211 && query_params.contains_key("Signature")
212 && query_params.contains_key("Expires")
213 {
214 return Some(DetectedRequest {
215 service: "s3".to_string(),
216 action: String::new(),
217 protocol: AwsProtocol::Rest,
218 });
219 }
220 if let Some(host_info) = parse_routing_host_from_headers(headers) {
221 if let Some(protocol) = rest_protocol_for(&host_info.service) {
222 return Some(DetectedRequest {
223 service: host_info.service,
224 action: String::new(),
225 protocol,
226 });
227 }
228 }
229 None
230}
231
232pub fn detect_service(
234 headers: &HeaderMap,
235 query_params: &HashMap<String, String>,
236 body: &Bytes,
237) -> Option<DetectedRequest> {
238 if let Some(target) = headers.get("x-amz-target").and_then(|v| v.to_str().ok()) {
240 return parse_amz_target(target);
241 }
242
243 if let Some(action) = query_params.get("Action") {
245 let service = extract_service_from_auth(headers)
246 .or_else(|| infer_service_from_action(action))
247 .or_else(|| parse_routing_host_from_headers(headers).map(|h| h.service));
248 if let Some(service) = service {
249 let protocol = query_protocol_for(&service);
250 return Some(DetectedRequest {
251 service,
252 action: action.clone(),
253 protocol,
254 });
255 }
256 }
257
258 {
260 let form_params = decode_form_urlencoded(body);
261
262 if let Some(action) = form_params.get("Action") {
263 let service = extract_service_from_auth(headers)
264 .or_else(|| infer_service_from_action(action))
265 .or_else(|| parse_routing_host_from_headers(headers).map(|h| h.service));
266 if let Some(service) = service {
267 let protocol = query_protocol_for(&service);
268 return Some(DetectedRequest {
269 service,
270 action: action.clone(),
271 protocol,
272 });
273 }
274 }
275 }
276
277 if let Some(service) = extract_service_from_auth(headers) {
279 if let Some(protocol) = rest_protocol_for(&service) {
280 return Some(DetectedRequest {
281 service,
282 action: String::new(), protocol,
284 });
285 }
286 }
287
288 if let Some(credential) = query_params.get("X-Amz-Credential") {
290 let parts: Vec<&str> = credential.split('/').collect();
292 if parts.len() >= 4 {
293 let service = normalize_service_name(parts[3]).to_string();
294 if let Some(protocol) = rest_protocol_for(&service) {
295 return Some(DetectedRequest {
296 service,
297 action: String::new(),
298 protocol,
299 });
300 }
301 }
302 }
303
304 if query_params.contains_key("AWSAccessKeyId")
308 && query_params.contains_key("Signature")
309 && query_params.contains_key("Expires")
310 {
311 return Some(DetectedRequest {
312 service: "s3".to_string(),
313 action: String::new(),
314 protocol: AwsProtocol::Rest,
315 });
316 }
317
318 if let Some(host_info) = parse_routing_host_from_headers(headers) {
322 if let Some(protocol) = rest_protocol_for(&host_info.service) {
323 return Some(DetectedRequest {
324 service: host_info.service,
325 action: String::new(),
326 protocol,
327 });
328 }
329 }
330
331 None
332}
333
334#[derive(Debug, Clone, PartialEq, Eq)]
344pub struct RoutingHost {
345 pub service: String,
346 pub region: String,
347 pub bucket: Option<String>,
349}
350
351const LOCALSTACK_SUFFIX: &str = ".localhost.localstack.cloud";
352
353pub fn parse_routing_host(host: &str) -> Option<RoutingHost> {
357 let hostname = host.split(':').next()?;
358 if hostname.is_empty() {
359 return None;
360 }
361 let hostname = hostname.to_ascii_lowercase();
362 if let Some(prefix) = hostname.strip_suffix(LOCALSTACK_SUFFIX) {
363 return parse_localstack_prefix(prefix);
364 }
365 fakecloud_aws::endpoint::DNS_SUFFIXES
368 .iter()
369 .find_map(|suffix| hostname.strip_suffix(suffix)?.strip_suffix('.'))
370 .and_then(parse_aws_prefix)
371}
372
373pub fn parse_routing_host_from_headers(headers: &HeaderMap) -> Option<RoutingHost> {
375 let host = headers.get("host")?.to_str().ok()?;
376 parse_routing_host(host)
377}
378
379fn parse_localstack_prefix(prefix: &str) -> Option<RoutingHost> {
380 if prefix.is_empty() {
381 return None;
382 }
383 let labels: Vec<&str> = prefix.split('.').collect();
384 if labels.iter().any(|l| l.is_empty()) {
385 return None;
386 }
387 match labels.len() {
388 2 => Some(RoutingHost {
389 service: labels[0].to_string(),
390 region: labels[1].to_string(),
391 bucket: None,
392 }),
393 n if n >= 3 && labels[n - 2] == "s3" => {
394 let bucket = labels[..n - 2].join(".");
395 Some(RoutingHost {
396 service: "s3".to_string(),
397 region: labels[n - 1].to_string(),
398 bucket: Some(bucket),
399 })
400 }
401 n if n >= 3 && labels[n - 2] == "s3-accesspoint" => {
402 let bucket = labels[..n - 2].join(".");
403 Some(RoutingHost {
404 service: "s3".to_string(),
405 region: labels[n - 1].to_string(),
406 bucket: Some(bucket),
407 })
408 }
409 n if n >= 3 && labels[n - 2] == "s3-control" => Some(RoutingHost {
410 service: "s3".to_string(),
411 region: labels[n - 1].to_string(),
412 bucket: None,
413 }),
414 _ => None,
415 }
416}
417
418fn parse_aws_prefix(prefix: &str) -> Option<RoutingHost> {
434 if prefix.is_empty() {
435 return None;
436 }
437 let labels: Vec<&str> = prefix.split('.').collect();
438 if labels.iter().any(|l| l.is_empty()) {
439 return None;
440 }
441 let last = *labels.last()?;
442
443 if let Some(region) = last.strip_prefix("s3-website-") {
447 if !region.is_empty() && labels.len() >= 2 {
448 return Some(RoutingHost {
449 service: "s3".to_string(),
450 region: region.to_string(),
451 bucket: Some(labels[..labels.len() - 1].join(".")),
452 });
453 }
454 }
455
456 if let Some(region) = last.strip_prefix("s3-") {
459 if !region.is_empty() {
460 let bucket = if labels.len() >= 2 {
461 Some(labels[..labels.len() - 1].join("."))
462 } else {
463 None
464 };
465 return Some(RoutingHost {
466 service: "s3".to_string(),
467 region: region.to_string(),
468 bucket,
469 });
470 }
471 }
472
473 if last == "s3" {
477 if labels.len() == 1 {
478 return Some(RoutingHost {
479 service: "s3".to_string(),
480 region: "us-east-1".to_string(),
481 bucket: None,
482 });
483 }
484 return Some(RoutingHost {
485 service: "s3".to_string(),
486 region: "us-east-1".to_string(),
487 bucket: Some(labels[..labels.len() - 1].join(".")),
488 });
489 }
490
491 if last == "s3-accesspoint" {
494 if labels.len() == 2 {
495 return Some(RoutingHost {
496 service: "s3".to_string(),
497 region: labels[0].to_string(),
498 bucket: None,
499 });
500 }
501 if labels.len() >= 3 {
505 let bucket = labels[..labels.len() - 2].join(".");
506 return Some(RoutingHost {
507 service: "s3".to_string(),
508 region: labels[labels.len() - 1].to_string(),
509 bucket: Some(bucket),
510 });
511 }
512 }
513
514 if labels.len() >= 2 && labels[labels.len() - 2] == "s3-control" {
517 return Some(RoutingHost {
518 service: "s3".to_string(),
519 region: last.to_string(),
520 bucket: None,
521 });
522 }
523
524 let before_region = &labels[..labels.len() - 1];
529 let (endpoint_labels, s3_endpoint) = match before_region {
530 [rest @ .., s3, "dualstack"] if matches!(*s3, "s3" | "s3-fips") => (rest, true),
531 [rest @ .., endpoint] if matches!(*endpoint, "s3" | "s3-fips" | "s3-website") => {
532 (rest, true)
533 }
534 _ => (before_region, false),
535 };
536 if s3_endpoint {
537 return Some(RoutingHost {
538 service: "s3".to_string(),
539 region: last.to_string(),
540 bucket: (!endpoint_labels.is_empty()).then(|| endpoint_labels.join(".")),
541 });
542 }
543
544 match labels.as_slice() {
546 [service, region] => Some(RoutingHost {
547 service: service.to_string(),
548 region: region.to_string(),
549 bucket: None,
550 }),
551 _ => None,
552 }
553}
554
555fn parse_amz_target(target: &str) -> Option<DetectedRequest> {
558 let (prefix, action) = target.rsplit_once('.')?;
559
560 let service = match prefix {
561 "AWSEvents" => "events",
562 "AmazonSSM" => "ssm",
563 "AmazonSQS" => "sqs",
564 "AmazonSNS" => "sns",
565 "DynamoDB_20120810" => "dynamodb",
566 "DynamoDBStreams_20120810" => "dynamodbstreams",
567 "Logs_20140328" => "logs",
568 s if s.starts_with("secretsmanager") => "secretsmanager",
569 s if s.starts_with("TrentService") => "kms",
570 s if s.starts_with("AWSCognitoIdentityProviderService") => "cognito-idp",
571 s if s.starts_with("AWSCognitoIdentityService") => "cognito-identity",
572 s if s.starts_with("Kinesis_20131202") => "kinesis",
573 s if s.starts_with("AmazonEC2ContainerRegistry_V") => "ecr",
574 s if s.starts_with("AmazonEC2ContainerServiceV") => "ecs",
575 s if s.starts_with("AWSStepFunctions") => "states",
576 s if s.starts_with("AWSOrganizationsV") => "organizations",
577 "CertificateManager" => "acm",
578 "ACMPrivateCA" => "acm-pca",
579 "Route53Resolver" => "route53resolver",
583 "StarlingDoveService" => "config",
586 "AnyScaleFrontendService" => "application-autoscaling",
587 "AWSWAF_20190729" => "wafv2",
590 "AmazonAthena" => "athena",
591 s if s.starts_with("Firehose_") => "firehose",
592 "AWSGlue" => "glue",
593 "ElasticMapReduce" => "emr",
596 "Textract" => "textract",
599 "Transcribe" => "transcribe",
602 "AWSShineFrontendService_20170701" => "translate",
605 "AWSShield_20160616" => "shield",
608 "Comprehend_20171127" => "comprehend",
611 "SimpleWorkflowService" => "swf",
614 "Timestream_20181101" => "timestream",
618 "AWSSupport_20130415" => "support",
621 "CloudApiService" => "cloudcontrolapi",
622 "ResourceGroupsTaggingAPI_20170126" => "tagging",
623 "AmazonMemoryDB" => "memorydb",
624 s if s.starts_with("KinesisAnalytics_20180523") => "kinesisanalyticsv2",
629 "Route53AutoNaming_v20170314" => "servicediscovery",
632 "AmazonDMSv20160101" => "dms",
634 "CloudTrail_20131101" => "cloudtrail",
638 "com.amazonaws.cloudtrail.v20131101.CloudTrail_20131101" => "cloudtrail",
639 "AWSInsightsIndexService" => "ce",
643 "com.amazonaws.costexplorer.v20171025.AWSInsightsIndexService" => "ce",
644 "TransferService" => "transfer",
646 "CodeBuild_20161006" => "codebuild",
648 "CodeCommit_20150413" => "codecommit",
650 "AWSIdentityStore" => "identitystore",
652 "SWBExternalService" => "sso",
654 "VerifiedPermissions" => "verifiedpermissions",
656 "CodeConnections_20231201" => "codeconnections",
658 "CodeStar_connections_20191201" => "codeconnections",
664 "CodeDeploy_20141006" => "codedeploy",
666 "CodePipeline_20150709" => "codepipeline",
668 s if s.starts_with("GraniteServiceVersion") => "monitoring",
674 "SageMaker" => "sagemaker",
677 _ => return None,
678 };
679
680 Some(DetectedRequest {
681 service: service.to_string(),
682 action: action.to_string(),
683 protocol: AwsProtocol::Json,
684 })
685}
686
687fn rest_protocol_for(service: &str) -> Option<AwsProtocol> {
689 if REST_XML_SERVICES.contains(&service) {
690 Some(AwsProtocol::Rest)
691 } else if REST_JSON_SERVICES.contains(&service) {
692 Some(AwsProtocol::RestJson)
693 } else {
694 None
695 }
696}
697
698fn infer_service_from_action(action: &str) -> Option<String> {
702 match action {
703 "AssumeRole"
704 | "AssumeRoleWithSAML"
705 | "AssumeRoleWithWebIdentity"
706 | "GetCallerIdentity"
707 | "GetSessionToken"
708 | "GetFederationToken"
709 | "GetAccessKeyInfo"
710 | "DecodeAuthorizationMessage" => Some("sts".to_string()),
711 "CreateUser" | "DeleteUser" | "GetUser" | "ListUsers" | "CreateRole" | "DeleteRole"
712 | "GetRole" | "ListRoles" | "CreatePolicy" | "DeletePolicy" | "GetPolicy"
713 | "ListPolicies" | "AttachRolePolicy" | "DetachRolePolicy" | "CreateAccessKey"
714 | "DeleteAccessKey" | "ListAccessKeys" | "ListRolePolicies" => Some("iam".to_string()),
715 "VerifyEmailIdentity"
717 | "VerifyDomainIdentity"
718 | "VerifyDomainDkim"
719 | "ListIdentities"
720 | "GetIdentityVerificationAttributes"
721 | "GetIdentityDkimAttributes"
722 | "DeleteIdentity"
723 | "SetIdentityDkimEnabled"
724 | "SetIdentityNotificationTopic"
725 | "SetIdentityFeedbackForwardingEnabled"
726 | "GetIdentityNotificationAttributes"
727 | "GetIdentityMailFromDomainAttributes"
728 | "SetIdentityMailFromDomain"
729 | "SendEmail"
730 | "SendRawEmail"
731 | "SendTemplatedEmail"
732 | "SendBulkTemplatedEmail"
733 | "CreateTemplate"
734 | "GetTemplate"
735 | "ListTemplates"
736 | "DeleteTemplate"
737 | "UpdateTemplate"
738 | "CreateConfigurationSet"
739 | "DeleteConfigurationSet"
740 | "DescribeConfigurationSet"
741 | "ListConfigurationSets"
742 | "CreateConfigurationSetEventDestination"
743 | "UpdateConfigurationSetEventDestination"
744 | "DeleteConfigurationSetEventDestination"
745 | "GetSendQuota"
746 | "GetSendStatistics"
747 | "GetAccountSendingEnabled"
748 | "CreateReceiptRuleSet"
749 | "DeleteReceiptRuleSet"
750 | "DescribeReceiptRuleSet"
751 | "ListReceiptRuleSets"
752 | "CloneReceiptRuleSet"
753 | "SetActiveReceiptRuleSet"
754 | "ReorderReceiptRuleSet"
755 | "CreateReceiptRule"
756 | "DeleteReceiptRule"
757 | "DescribeReceiptRule"
758 | "UpdateReceiptRule"
759 | "CreateReceiptFilter"
760 | "DeleteReceiptFilter"
761 | "ListReceiptFilters" => Some("ses".to_string()),
762 "ConfirmSubscription" | "Unsubscribe" => Some("sns".to_string()),
766 _ => None,
767 }
768}
769
770fn extract_service_from_auth(headers: &HeaderMap) -> Option<String> {
772 let auth = headers.get("authorization")?.to_str().ok()?;
773 let info = fakecloud_aws::sigv4::parse_sigv4(auth)?;
774 Some(normalize_service_name(&info.service).to_string())
775}
776
777fn normalize_service_name(service: &str) -> &str {
789 match service {
790 "bedrock-runtime" => "bedrock",
791 "apigatewayv2" => "apigateway",
799 "opensearch" => "es",
807 "appconfigdata" => "appconfig",
813 "airflow" => "mwaa",
818 "mobiletargeting" => "pinpoint",
823 other => other,
824 }
825}
826
827pub fn parse_query_body(body: &Bytes) -> HashMap<String, String> {
829 decode_form_urlencoded(body)
830}
831
832pub fn flatten_json_to_query(body: &Bytes) -> HashMap<String, String> {
849 let mut out = HashMap::new();
850 let Ok(value) = serde_json::from_slice::<serde_json::Value>(body) else {
851 return out;
852 };
853 if value.is_object() {
854 flatten_json_value("", &value, &mut out);
855 }
856 out
857}
858
859fn flatten_json_value(prefix: &str, value: &serde_json::Value, out: &mut HashMap<String, String>) {
860 match value {
861 serde_json::Value::Object(map) => {
862 for (k, v) in map {
863 let child = if prefix.is_empty() {
864 k.clone()
865 } else {
866 format!("{prefix}.{k}")
867 };
868 flatten_json_value(&child, v, out);
869 }
870 }
871 serde_json::Value::Array(items) => {
872 for (i, v) in items.iter().enumerate() {
873 let child = format!("{prefix}.member.{}", i + 1);
874 flatten_json_value(&child, v, out);
875 }
876 }
877 serde_json::Value::Null => {}
878 serde_json::Value::String(s) => {
879 out.insert(prefix.to_string(), s.clone());
880 }
881 serde_json::Value::Bool(b) => {
882 out.insert(prefix.to_string(), b.to_string());
883 }
884 serde_json::Value::Number(n) => {
885 out.insert(prefix.to_string(), n.to_string());
886 }
887 }
888}
889
890pub(crate) fn form_urlencoded_pairs(input: &str) -> Vec<(String, String)> {
896 let mut pairs = Vec::new();
897 for pair in input.split('&') {
898 if pair.is_empty() {
899 continue;
900 }
901 let (key, value) = match pair.find('=') {
902 Some(pos) => (&pair[..pos], &pair[pos + 1..]),
903 None => (pair, ""),
904 };
905 pairs.push((url_decode(key), url_decode(value)));
906 }
907 pairs
908}
909
910fn decode_form_urlencoded(input: &[u8]) -> HashMap<String, String> {
911 let s = std::str::from_utf8(input).unwrap_or("");
912 let mut result = HashMap::new();
913 for pair in s.split('&') {
914 if pair.is_empty() {
915 continue;
916 }
917 let (key, value) = match pair.find('=') {
918 Some(pos) => (&pair[..pos], &pair[pos + 1..]),
919 None => (pair, ""),
920 };
921 result.insert(url_decode(key), url_decode(value));
922 }
923 result
924}
925
926fn url_decode(input: &str) -> String {
927 let mut buf: Vec<u8> = Vec::with_capacity(input.len());
933 let mut bytes = input.bytes();
934 while let Some(b) = bytes.next() {
935 match b {
936 b'+' => buf.push(b' '),
937 b'%' => {
938 let high = bytes.next().and_then(from_hex);
939 let low = bytes.next().and_then(from_hex);
940 if let (Some(h), Some(l)) = (high, low) {
944 buf.push((h << 4) | l);
945 }
946 }
947 _ => buf.push(b),
948 }
949 }
950 String::from_utf8_lossy(&buf).into_owned()
951}
952
953fn from_hex(b: u8) -> Option<u8> {
954 match b {
955 b'0'..=b'9' => Some(b - b'0'),
956 b'a'..=b'f' => Some(b - b'a' + 10),
957 b'A'..=b'F' => Some(b - b'A' + 10),
958 _ => None,
959 }
960}
961
962#[cfg(test)]
963mod tests {
964 use super::*;
965
966 #[test]
967 fn form_urlencoded_pairs_preserves_repeated_keys() {
968 let pairs = form_urlencoded_pairs("Id=a&Id=b&Id=c&Other=x");
971 let ids: Vec<&str> = pairs
972 .iter()
973 .filter(|(k, _)| k == "Id")
974 .map(|(_, v)| v.as_str())
975 .collect();
976 assert_eq!(ids, vec!["a", "b", "c"]);
977 assert_eq!(
979 decode_form_urlencoded(b"Id=a&Id=b&Id=c").get("Id").unwrap(),
980 "c"
981 );
982 }
983
984 #[test]
985 fn form_urlencoded_pairs_decodes_percent_and_plus() {
986 let pairs = form_urlencoded_pairs("q=caf%C3%A9+bar&empty=");
987 assert_eq!(pairs[0], ("q".to_string(), "café bar".to_string()));
988 assert_eq!(pairs[1], ("empty".to_string(), String::new()));
989 }
990
991 #[test]
992 fn parse_amz_target_events() {
993 let result = parse_amz_target("AWSEvents.PutEvents").unwrap();
994 assert_eq!(result.service, "events");
995 assert_eq!(result.action, "PutEvents");
996 assert_eq!(result.protocol, AwsProtocol::Json);
997 }
998
999 #[test]
1000 fn parse_amz_target_ssm() {
1001 let result = parse_amz_target("AmazonSSM.GetParameter").unwrap();
1002 assert_eq!(result.service, "ssm");
1003 assert_eq!(result.action, "GetParameter");
1004 }
1005
1006 #[test]
1007 fn parse_amz_target_kinesis() {
1008 let result = parse_amz_target("Kinesis_20131202.ListStreams").unwrap();
1009 assert_eq!(result.service, "kinesis");
1010 assert_eq!(result.action, "ListStreams");
1011 assert_eq!(result.protocol, AwsProtocol::Json);
1012 }
1013
1014 #[test]
1015 fn parse_query_body_basic() {
1016 let body = Bytes::from(
1017 "Action=SendMessage&QueueUrl=http%3A%2F%2Flocalhost%3A4566%2Fqueue&MessageBody=hello",
1018 );
1019 let params = parse_query_body(&body);
1020 assert_eq!(params.get("Action").unwrap(), "SendMessage");
1021 assert_eq!(params.get("MessageBody").unwrap(), "hello");
1022 }
1023
1024 #[test]
1025 fn parse_query_body_empty_returns_empty_map() {
1026 let body = Bytes::from("");
1027 let params = parse_query_body(&body);
1028 assert!(params.is_empty());
1029 }
1030
1031 #[test]
1032 fn parse_query_body_duplicate_keys_last_wins() {
1033 let body = Bytes::from("key=a&key=b");
1034 let params = parse_query_body(&body);
1035 assert_eq!(params.get("key").unwrap(), "b");
1036 }
1037
1038 #[test]
1039 fn parse_query_body_single_key() {
1040 let body = Bytes::from("key=value");
1041 let params = parse_query_body(&body);
1042 assert_eq!(params.get("key").unwrap(), "value");
1043 }
1044
1045 #[test]
1046 fn url_decode_plain_ascii() {
1047 assert_eq!(url_decode("hello"), "hello");
1048 assert_eq!(url_decode("Action=SendMessage"), "Action=SendMessage");
1049 }
1050
1051 #[test]
1052 fn url_decode_plus_is_space() {
1053 assert_eq!(url_decode("hello+world"), "hello world");
1054 assert_eq!(url_decode("a+b+c"), "a b c");
1055 }
1056
1057 #[test]
1058 fn url_decode_multibyte_utf8_accents() {
1059 assert_eq!(url_decode("caf%C3%A9"), "café");
1061 }
1062
1063 #[test]
1064 fn url_decode_multibyte_utf8_cjk() {
1065 assert_eq!(url_decode("%E6%97%A5%E6%9C%AC"), "日本");
1067 }
1068
1069 #[test]
1070 fn url_decode_multibyte_utf8_emoji() {
1071 assert_eq!(url_decode("%F0%9F%9A%80"), "🚀");
1073 }
1074
1075 #[test]
1076 fn url_decode_mixed_ascii_and_multibyte() {
1077 assert_eq!(url_decode("Tag+%3D+caf%C3%A9%21"), "Tag = café!");
1078 }
1079
1080 #[test]
1081 fn url_decode_malformed_percent_is_graceful() {
1082 assert_eq!(url_decode("100%"), "100");
1084 assert_eq!(url_decode("a%zz"), "a");
1085 assert_eq!(url_decode("a%4"), "a");
1086 assert_eq!(url_decode("x%y"), "x");
1088 }
1089
1090 #[test]
1091 fn url_decode_invalid_utf8_bytes_are_lossy_no_panic() {
1092 let out = url_decode("bad%FFbyte");
1094 assert!(out.starts_with("bad"));
1095 assert!(out.ends_with("byte"));
1096 }
1097
1098 #[test]
1099 fn parse_query_body_multibyte_value_round_trips() {
1100 let body = Bytes::from("Tag.Value=caf%C3%A9&Name=%E6%97%A5%E6%9C%AC");
1101 let params = parse_query_body(&body);
1102 assert_eq!(params.get("Tag.Value").unwrap(), "café");
1103 assert_eq!(params.get("Name").unwrap(), "日本");
1104 }
1105
1106 #[test]
1107 fn parse_amz_target_ecs() {
1108 let result = parse_amz_target("AmazonEC2ContainerServiceV20141113.ListClusters").unwrap();
1109 assert_eq!(result.service, "ecs");
1110 assert_eq!(result.action, "ListClusters");
1111 assert_eq!(result.protocol, AwsProtocol::Json);
1112 }
1113
1114 #[test]
1115 fn parse_amz_target_invalid_returns_none() {
1116 assert!(parse_amz_target("NoDotHere").is_none());
1117 assert!(parse_amz_target("").is_none());
1118 }
1119
1120 #[test]
1121 fn parse_amz_target_cloudwatch_json() {
1122 let result = parse_amz_target("GraniteServiceVersion20100801.PutMetricData").unwrap();
1124 assert_eq!(result.service, "monitoring");
1125 assert_eq!(result.action, "PutMetricData");
1126 assert_eq!(result.protocol, AwsProtocol::Json);
1127 }
1128
1129 #[test]
1130 fn flatten_json_to_query_nested() {
1131 let body = Bytes::from(
1132 serde_json::json!({
1133 "Namespace": "MyApp",
1134 "MetricData": [{
1135 "MetricName": "Latency",
1136 "Value": 12.5,
1137 "StatisticValues": {"SampleCount": 3, "Sum": 10},
1138 "Dimensions": [{"Name": "Endpoint", "Value": "/api"}]
1139 }]
1140 })
1141 .to_string(),
1142 );
1143 let flat = flatten_json_to_query(&body);
1144 assert_eq!(flat.get("Namespace").unwrap(), "MyApp");
1145 assert_eq!(
1146 flat.get("MetricData.member.1.MetricName").unwrap(),
1147 "Latency"
1148 );
1149 assert_eq!(flat.get("MetricData.member.1.Value").unwrap(), "12.5");
1150 assert_eq!(
1151 flat.get("MetricData.member.1.StatisticValues.SampleCount")
1152 .unwrap(),
1153 "3"
1154 );
1155 assert_eq!(
1156 flat.get("MetricData.member.1.Dimensions.member.1.Name")
1157 .unwrap(),
1158 "Endpoint"
1159 );
1160 assert_eq!(
1161 flat.get("MetricData.member.1.Dimensions.member.1.Value")
1162 .unwrap(),
1163 "/api"
1164 );
1165 }
1166
1167 #[test]
1168 fn flatten_json_to_query_non_object_is_empty() {
1169 assert!(flatten_json_to_query(&Bytes::from_static(b"[]")).is_empty());
1170 assert!(flatten_json_to_query(&Bytes::from_static(b"not json")).is_empty());
1171 }
1172
1173 #[test]
1174 fn parse_amz_target_various_prefixes() {
1175 assert_eq!(
1176 parse_amz_target("AmazonSQS.SendMessage").unwrap().service,
1177 "sqs"
1178 );
1179 assert_eq!(
1180 parse_amz_target("AmazonSNS.Publish").unwrap().service,
1181 "sns"
1182 );
1183 assert_eq!(
1184 parse_amz_target("DynamoDB_20120810.GetItem")
1185 .unwrap()
1186 .service,
1187 "dynamodb"
1188 );
1189 assert_eq!(
1190 parse_amz_target("Logs_20140328.PutLogEvents")
1191 .unwrap()
1192 .service,
1193 "logs"
1194 );
1195 assert_eq!(
1196 parse_amz_target("secretsmanager.GetSecretValue")
1197 .unwrap()
1198 .service,
1199 "secretsmanager"
1200 );
1201 assert_eq!(
1202 parse_amz_target("TrentService.Encrypt").unwrap().service,
1203 "kms"
1204 );
1205 assert_eq!(
1206 parse_amz_target("AWSCognitoIdentityProviderService.InitiateAuth")
1207 .unwrap()
1208 .service,
1209 "cognito-idp"
1210 );
1211 assert_eq!(
1212 parse_amz_target("AWSStepFunctions.StartExecution")
1213 .unwrap()
1214 .service,
1215 "states"
1216 );
1217 assert_eq!(
1218 parse_amz_target("AWSOrganizationsV20161128.CreateOrganization")
1219 .unwrap()
1220 .service,
1221 "organizations"
1222 );
1223 assert!(parse_amz_target("UnknownServicePrefix.Action").is_none());
1224 }
1225
1226 #[test]
1227 fn infer_service_from_action_maps_sts() {
1228 assert_eq!(
1229 infer_service_from_action("AssumeRole").as_deref(),
1230 Some("sts")
1231 );
1232 assert_eq!(
1233 infer_service_from_action("GetCallerIdentity").as_deref(),
1234 Some("sts")
1235 );
1236 }
1237
1238 #[test]
1239 fn infer_service_from_action_maps_iam() {
1240 assert_eq!(
1241 infer_service_from_action("CreateUser").as_deref(),
1242 Some("iam")
1243 );
1244 assert_eq!(
1245 infer_service_from_action("ListRoles").as_deref(),
1246 Some("iam")
1247 );
1248 }
1249
1250 #[test]
1251 fn infer_service_from_action_maps_ses() {
1252 assert_eq!(
1253 infer_service_from_action("SendEmail").as_deref(),
1254 Some("ses")
1255 );
1256 assert_eq!(
1257 infer_service_from_action("ListIdentities").as_deref(),
1258 Some("ses")
1259 );
1260 }
1261
1262 #[test]
1263 fn infer_service_from_action_maps_sns_confirmation_flow() {
1264 assert_eq!(
1267 infer_service_from_action("ConfirmSubscription").as_deref(),
1268 Some("sns")
1269 );
1270 assert_eq!(
1271 infer_service_from_action("Unsubscribe").as_deref(),
1272 Some("sns")
1273 );
1274 }
1275
1276 #[test]
1277 fn detect_service_routes_unsigned_confirm_subscription_to_sns() {
1278 let mut headers = HeaderMap::new();
1281 headers.insert("host", "localhost:4566".parse().unwrap());
1282 let mut query_params = HashMap::new();
1283 query_params.insert("Action".to_string(), "ConfirmSubscription".to_string());
1284 query_params.insert(
1285 "TopicArn".to_string(),
1286 "arn:aws:sns:us-east-1:000000000000:t".to_string(),
1287 );
1288 query_params.insert("Token".to_string(), "abc123".to_string());
1289
1290 let detected = detect_service(&headers, &query_params, &Bytes::new())
1291 .expect("ConfirmSubscription must route to a service");
1292 assert_eq!(detected.service, "sns");
1293 assert_eq!(detected.action, "ConfirmSubscription");
1294 assert_eq!(detected.protocol, AwsProtocol::Query);
1295 }
1296
1297 #[test]
1298 fn infer_service_from_action_unknown_returns_none() {
1299 assert!(infer_service_from_action("NotARealAction").is_none());
1300 }
1301
1302 #[test]
1303 fn rest_protocol_for_returns_none_for_non_rest_service() {
1304 assert!(rest_protocol_for("sqs").is_none());
1305 }
1306
1307 #[test]
1308 fn url_decode_handles_percent_and_plus() {
1309 assert_eq!(url_decode("hello+world"), "hello world");
1310 assert_eq!(url_decode("hello%20world"), "hello world");
1311 assert_eq!(url_decode("100%25"), "100%");
1312 }
1313
1314 #[test]
1315 fn url_decode_ignores_malformed_percent() {
1316 assert_eq!(url_decode("%ZZ"), "");
1317 }
1318
1319 #[test]
1320 fn from_hex_valid_digits() {
1321 assert_eq!(from_hex(b'0'), Some(0));
1322 assert_eq!(from_hex(b'9'), Some(9));
1323 assert_eq!(from_hex(b'a'), Some(10));
1324 assert_eq!(from_hex(b'F'), Some(15));
1325 }
1326
1327 #[test]
1328 fn from_hex_invalid_returns_none() {
1329 assert!(from_hex(b'g').is_none());
1330 assert!(from_hex(b' ').is_none());
1331 }
1332
1333 #[test]
1334 fn detect_service_via_amz_target() {
1335 let mut headers = HeaderMap::new();
1336 headers.insert("x-amz-target", "AmazonSSM.GetParameter".parse().unwrap());
1337 let query = HashMap::new();
1338 let body = Bytes::new();
1339 let detected = detect_service(&headers, &query, &body).unwrap();
1340 assert_eq!(detected.service, "ssm");
1341 assert_eq!(detected.action, "GetParameter");
1342 }
1343
1344 #[test]
1345 fn detect_service_via_query_action_with_inferred_service() {
1346 let headers = HeaderMap::new();
1347 let mut query = HashMap::new();
1348 query.insert("Action".to_string(), "AssumeRole".to_string());
1349 let body = Bytes::new();
1350 let detected = detect_service(&headers, &query, &body).unwrap();
1351 assert_eq!(detected.service, "sts");
1352 assert_eq!(detected.action, "AssumeRole");
1353 assert_eq!(detected.protocol, AwsProtocol::Query);
1354 }
1355
1356 #[test]
1357 fn detect_service_via_form_body() {
1358 let headers = HeaderMap::new();
1359 let query = HashMap::new();
1360 let body = Bytes::from("Action=SendEmail&Source=x%40y.com");
1361 let detected = detect_service(&headers, &query, &body).unwrap();
1362 assert_eq!(detected.service, "ses");
1363 assert_eq!(detected.action, "SendEmail");
1364 }
1365
1366 #[test]
1367 fn detect_service_via_sigv2_presigned() {
1368 let headers = HeaderMap::new();
1369 let mut query = HashMap::new();
1370 query.insert("AWSAccessKeyId".to_string(), "AKID".to_string());
1371 query.insert("Signature".to_string(), "sig".to_string());
1372 query.insert("Expires".to_string(), "1234567890".to_string());
1373 let body = Bytes::new();
1374 let detected = detect_service(&headers, &query, &body).unwrap();
1375 assert_eq!(detected.service, "s3");
1376 assert_eq!(detected.protocol, AwsProtocol::Rest);
1377 }
1378
1379 #[test]
1380 fn detect_service_via_sigv4_presigned_credential() {
1381 let headers = HeaderMap::new();
1382 let mut query = HashMap::new();
1383 query.insert(
1384 "X-Amz-Credential".to_string(),
1385 "AKID/20240101/us-east-1/s3/aws4_request".to_string(),
1386 );
1387 let body = Bytes::new();
1388 let detected = detect_service(&headers, &query, &body).unwrap();
1389 assert_eq!(detected.service, "s3");
1390 assert_eq!(detected.protocol, AwsProtocol::Rest);
1391 }
1392
1393 #[test]
1394 fn detect_service_unknown_returns_none() {
1395 let headers = HeaderMap::new();
1396 let query = HashMap::new();
1397 let body = Bytes::new();
1398 assert!(detect_service(&headers, &query, &body).is_none());
1399 }
1400
1401 #[test]
1402 fn normalize_service_name_aliases_apigatewayv2_to_apigateway() {
1403 assert_eq!(normalize_service_name("apigatewayv2"), "apigateway");
1408 }
1409
1410 #[test]
1411 fn normalize_service_name_aliases_bedrock_runtime_to_bedrock() {
1412 assert_eq!(normalize_service_name("bedrock-runtime"), "bedrock");
1417 }
1418
1419 #[test]
1420 fn normalize_service_name_passes_through_unaliased_services() {
1421 assert_eq!(normalize_service_name("bedrock"), "bedrock");
1425 assert_eq!(normalize_service_name("s3"), "s3");
1426 assert_eq!(normalize_service_name("lambda"), "lambda");
1427 assert_eq!(normalize_service_name(""), "");
1428 assert_eq!(
1429 normalize_service_name("unknown-future-service"),
1430 "unknown-future-service"
1431 );
1432 }
1433
1434 #[test]
1435 fn detect_service_via_authorization_header_normalizes_bedrock_runtime() {
1436 let mut headers = HeaderMap::new();
1441 headers.insert(
1442 "authorization",
1443 "AWS4-HMAC-SHA256 \
1444 Credential=AKID/20240101/us-east-1/bedrock-runtime/aws4_request, \
1445 SignedHeaders=host, Signature=abc"
1446 .parse()
1447 .unwrap(),
1448 );
1449 let query = HashMap::new();
1450 let body = Bytes::new();
1451 let detected = detect_service(&headers, &query, &body).unwrap();
1452 assert_eq!(detected.service, "bedrock");
1453 assert_eq!(detected.protocol, AwsProtocol::RestJson);
1454 }
1455
1456 #[test]
1457 fn detect_service_via_sigv4_presigned_credential_normalizes_bedrock_runtime() {
1458 let headers = HeaderMap::new();
1462 let mut query = HashMap::new();
1463 query.insert(
1464 "X-Amz-Credential".to_string(),
1465 "AKID/20240101/us-east-1/bedrock-runtime/aws4_request".to_string(),
1466 );
1467 let body = Bytes::new();
1468 let detected = detect_service(&headers, &query, &body).unwrap();
1469 assert_eq!(detected.service, "bedrock");
1470 assert_eq!(detected.protocol, AwsProtocol::RestJson);
1471 }
1472
1473 #[test]
1474 fn parse_routing_host_localstack_basic() {
1475 let h = parse_routing_host("sqs.us-east-1.localhost.localstack.cloud").unwrap();
1476 assert_eq!(h.service, "sqs");
1477 assert_eq!(h.region, "us-east-1");
1478 assert!(h.bucket.is_none());
1479 }
1480
1481 #[test]
1482 fn parse_routing_host_localstack_with_port() {
1483 let h = parse_routing_host("lambda.eu-west-1.localhost.localstack.cloud:4566").unwrap();
1484 assert_eq!(h.service, "lambda");
1485 assert_eq!(h.region, "eu-west-1");
1486 assert!(h.bucket.is_none());
1487 }
1488
1489 #[test]
1490 fn parse_routing_host_case_insensitive() {
1491 let h = parse_routing_host("SQS.US-EAST-1.LOCALHOST.LOCALSTACK.CLOUD:4566").unwrap();
1492 assert_eq!(h.service, "sqs");
1493 assert_eq!(h.region, "us-east-1");
1494
1495 let h = parse_routing_host("LAMBDA.US-EAST-1.AMAZONAWS.COM").unwrap();
1496 assert_eq!(h.service, "lambda");
1497 assert_eq!(h.region, "us-east-1");
1498 }
1499
1500 #[test]
1501 fn parse_routing_host_localstack_s3_virtual_hosted() {
1502 let h =
1503 parse_routing_host("my-bucket.s3.us-east-1.localhost.localstack.cloud:4566").unwrap();
1504 assert_eq!(h.service, "s3");
1505 assert_eq!(h.region, "us-east-1");
1506 assert_eq!(h.bucket.as_deref(), Some("my-bucket"));
1507 }
1508
1509 #[test]
1510 fn parse_routing_host_localstack_s3_vhost_bucket_with_dots() {
1511 let h = parse_routing_host("a.b.c.s3.us-east-1.localhost.localstack.cloud").unwrap();
1512 assert_eq!(h.service, "s3");
1513 assert_eq!(h.region, "us-east-1");
1514 assert_eq!(h.bucket.as_deref(), Some("a.b.c"));
1515 }
1516
1517 #[test]
1518 fn parse_routing_host_aws_service_region() {
1519 let h = parse_routing_host("sqs.us-east-1.amazonaws.com").unwrap();
1520 assert_eq!(h.service, "sqs");
1521 assert_eq!(h.region, "us-east-1");
1522 assert!(h.bucket.is_none());
1523
1524 let h = parse_routing_host("dynamodb.eu-west-2.amazonaws.com:443").unwrap();
1525 assert_eq!(h.service, "dynamodb");
1526 assert_eq!(h.region, "eu-west-2");
1527 }
1528
1529 #[test]
1530 fn parse_routing_host_aws_s3_path_style_modern() {
1531 let h = parse_routing_host("s3.us-east-1.amazonaws.com").unwrap();
1532 assert_eq!(h.service, "s3");
1533 assert_eq!(h.region, "us-east-1");
1534 assert!(h.bucket.is_none());
1535 }
1536
1537 #[test]
1538 fn parse_routing_host_aws_s3_virtual_hosted_modern() {
1539 let h = parse_routing_host("my-bucket.s3.us-east-1.amazonaws.com").unwrap();
1540 assert_eq!(h.service, "s3");
1541 assert_eq!(h.region, "us-east-1");
1542 assert_eq!(h.bucket.as_deref(), Some("my-bucket"));
1543 }
1544
1545 #[test]
1546 fn parse_routing_host_aws_s3_vhost_bucket_with_dots() {
1547 let h = parse_routing_host("a.b.c.s3.us-east-1.amazonaws.com").unwrap();
1548 assert_eq!(h.service, "s3");
1549 assert_eq!(h.region, "us-east-1");
1550 assert_eq!(h.bucket.as_deref(), Some("a.b.c"));
1551 }
1552
1553 #[test]
1554 fn parse_routing_host_aws_s3_legacy_global() {
1555 let h = parse_routing_host("s3.amazonaws.com").unwrap();
1558 assert_eq!(h.service, "s3");
1559 assert_eq!(h.region, "us-east-1");
1560 assert!(h.bucket.is_none());
1561
1562 let h = parse_routing_host("my-bucket.s3.amazonaws.com").unwrap();
1563 assert_eq!(h.service, "s3");
1564 assert_eq!(h.region, "us-east-1");
1565 assert_eq!(h.bucket.as_deref(), Some("my-bucket"));
1566 }
1567
1568 #[test]
1569 fn parse_routing_host_aws_s3_legacy_global_dotted_bucket() {
1570 let h = parse_routing_host("a.b.c.s3.amazonaws.com").unwrap();
1573 assert_eq!(h.service, "s3");
1574 assert_eq!(h.region, "us-east-1");
1575 assert_eq!(h.bucket.as_deref(), Some("a.b.c"));
1576 }
1577
1578 #[test]
1579 fn parse_routing_host_aws_s3_dash_separated() {
1580 let h = parse_routing_host("s3-us-west-2.amazonaws.com").unwrap();
1582 assert_eq!(h.service, "s3");
1583 assert_eq!(h.region, "us-west-2");
1584 assert!(h.bucket.is_none());
1585
1586 let h = parse_routing_host("my-bucket.s3-us-west-2.amazonaws.com").unwrap();
1587 assert_eq!(h.service, "s3");
1588 assert_eq!(h.region, "us-west-2");
1589 assert_eq!(h.bucket.as_deref(), Some("my-bucket"));
1590 }
1591
1592 #[test]
1593 fn parse_routing_host_aws_s3_dualstack_and_fips() {
1594 for (host, bucket) in [
1595 (
1596 "my-bucket.s3.dualstack.us-east-1.amazonaws.com",
1597 Some("my-bucket"),
1598 ),
1599 ("a.b.s3.dualstack.eu-west-2.amazonaws.com", Some("a.b")),
1600 ("s3.dualstack.eu-west-2.amazonaws.com", None),
1601 (
1602 "my-bucket.s3-fips.us-gov-west-1.amazonaws.com",
1603 Some("my-bucket"),
1604 ),
1605 (
1606 "my-bucket.s3-fips.dualstack.us-east-1.amazonaws.com",
1607 Some("my-bucket"),
1608 ),
1609 ] {
1610 let h = parse_routing_host(host).unwrap();
1611 assert_eq!(h.service, "s3", "{host}");
1612 assert_eq!(h.bucket.as_deref(), bucket, "{host}");
1613 }
1614 let h = parse_routing_host("b.s3.dualstack.eu-west-2.amazonaws.com").unwrap();
1615 assert_eq!(h.region, "eu-west-2");
1616 }
1617
1618 #[test]
1619 fn parse_routing_host_aws_s3_website_endpoints() {
1620 let h = parse_routing_host("site.s3-website-us-west-2.amazonaws.com").unwrap();
1623 assert_eq!(h.service, "s3");
1624 assert_eq!(h.region, "us-west-2");
1625 assert_eq!(h.bucket.as_deref(), Some("site"));
1626
1627 let h = parse_routing_host("my.site.s3-website.eu-central-1.amazonaws.com").unwrap();
1629 assert_eq!(h.service, "s3");
1630 assert_eq!(h.region, "eu-central-1");
1631 assert_eq!(h.bucket.as_deref(), Some("my.site"));
1632 }
1633
1634 #[test]
1635 fn parse_routing_host_other_partition_suffixes() {
1636 let h = parse_routing_host("my-bucket.s3.cn-north-1.amazonaws.com.cn").unwrap();
1637 assert_eq!(h.service, "s3");
1638 assert_eq!(h.region, "cn-north-1");
1639 assert_eq!(h.bucket.as_deref(), Some("my-bucket"));
1640
1641 let h = parse_routing_host("sqs.cn-northwest-1.amazonaws.com.cn").unwrap();
1642 assert_eq!(h.service, "sqs");
1643 assert_eq!(h.region, "cn-northwest-1");
1644
1645 let h = parse_routing_host("b.s3.us-iso-east-1.c2s.ic.gov").unwrap();
1646 assert_eq!(h.region, "us-iso-east-1");
1647 assert_eq!(h.bucket.as_deref(), Some("b"));
1648
1649 assert!(parse_routing_host("amazonaws.com.cn").is_none());
1650 assert!(parse_routing_host(".amazonaws.com.cn").is_none());
1651 }
1652
1653 #[test]
1654 fn parse_routing_host_rejects_plain_localhost() {
1655 assert!(parse_routing_host("localhost:4566").is_none());
1656 assert!(parse_routing_host("127.0.0.1:4566").is_none());
1657 }
1658
1659 #[test]
1660 fn parse_routing_host_rejects_unknown_suffix() {
1661 assert!(parse_routing_host("sqs.us-east-1.example.com").is_none());
1662 assert!(parse_routing_host("s3.us-east-1.aws").is_none());
1663 }
1664
1665 #[test]
1666 fn parse_routing_host_empty_and_malformed_rejected() {
1667 assert!(parse_routing_host("").is_none());
1668 assert!(parse_routing_host(".localhost.localstack.cloud").is_none());
1669 assert!(parse_routing_host("..localhost.localstack.cloud").is_none());
1670 assert!(parse_routing_host("sqs.localhost.localstack.cloud").is_none());
1671 assert!(parse_routing_host("foo.bar.baz.localhost.localstack.cloud").is_none());
1672 assert!(parse_routing_host(".amazonaws.com").is_none());
1673 assert!(parse_routing_host("amazonaws.com").is_none());
1674 }
1675
1676 #[test]
1677 fn parse_routing_host_bare_s3_accesspoint_does_not_panic() {
1678 assert!(parse_routing_host("s3-accesspoint").is_none());
1682 }
1683
1684 #[test]
1685 fn detect_service_via_host_for_rest_service() {
1686 let mut headers = HeaderMap::new();
1687 headers.insert(
1688 "host",
1689 "s3.us-east-1.localhost.localstack.cloud:4566"
1690 .parse()
1691 .unwrap(),
1692 );
1693 let query = HashMap::new();
1694 let body = Bytes::new();
1695 let detected = detect_service(&headers, &query, &body).unwrap();
1696 assert_eq!(detected.service, "s3");
1697 assert_eq!(detected.protocol, AwsProtocol::Rest);
1698 }
1699
1700 #[test]
1701 fn detect_service_via_host_for_rest_json_service() {
1702 let mut headers = HeaderMap::new();
1703 headers.insert(
1704 "host",
1705 "lambda.us-east-1.localhost.localstack.cloud:4566"
1706 .parse()
1707 .unwrap(),
1708 );
1709 let query = HashMap::new();
1710 let body = Bytes::new();
1711 let detected = detect_service(&headers, &query, &body).unwrap();
1712 assert_eq!(detected.service, "lambda");
1713 assert_eq!(detected.protocol, AwsProtocol::RestJson);
1714 }
1715
1716 #[test]
1717 fn detect_service_via_host_plus_query_action() {
1718 let mut headers = HeaderMap::new();
1719 headers.insert(
1720 "host",
1721 "sqs.us-east-1.localhost.localstack.cloud:4566"
1722 .parse()
1723 .unwrap(),
1724 );
1725 let mut query = HashMap::new();
1726 query.insert("Action".to_string(), "ListQueues".to_string());
1727 let body = Bytes::new();
1728 let detected = detect_service(&headers, &query, &body).unwrap();
1729 assert_eq!(detected.service, "sqs");
1730 assert_eq!(detected.action, "ListQueues");
1731 assert_eq!(detected.protocol, AwsProtocol::Query);
1732 }
1733
1734 #[test]
1735 fn detect_service_sigv4_wins_over_host() {
1736 let mut headers = HeaderMap::new();
1737 headers.insert(
1738 "authorization",
1739 "AWS4-HMAC-SHA256 Credential=AKID/20240101/us-east-1/s3/aws4_request, \
1740 SignedHeaders=host, Signature=abc"
1741 .parse()
1742 .unwrap(),
1743 );
1744 headers.insert(
1745 "host",
1746 "lambda.us-east-1.localhost.localstack.cloud:4566"
1747 .parse()
1748 .unwrap(),
1749 );
1750 let query = HashMap::new();
1751 let body = Bytes::new();
1752 let detected = detect_service(&headers, &query, &body).unwrap();
1753 assert_eq!(detected.service, "s3");
1755 assert_eq!(detected.protocol, AwsProtocol::Rest);
1756 }
1757
1758 #[test]
1759 fn detect_service_host_for_virtual_hosted_s3() {
1760 let mut headers = HeaderMap::new();
1761 headers.insert(
1762 "host",
1763 "my-bucket.s3.us-east-1.localhost.localstack.cloud:4566"
1764 .parse()
1765 .unwrap(),
1766 );
1767 let query = HashMap::new();
1768 let body = Bytes::new();
1769 let detected = detect_service(&headers, &query, &body).unwrap();
1770 assert_eq!(detected.service, "s3");
1771 assert_eq!(detected.protocol, AwsProtocol::Rest);
1772 }
1773}