1use bytes::Bytes;
2use http::HeaderMap;
3use std::collections::HashMap;
4
5#[derive(Debug, Clone, Copy, PartialEq, Eq)]
7pub enum AwsProtocol {
8 Query,
11 Json,
14 Rest,
17 RestJson,
20}
21
22const REST_XML_SERVICES: &[&str] = &["s3", "cloudfront", "route53"];
24
25const REST_JSON_SERVICES: &[&str] = &[
27 "managedblockchain",
28 "lambda",
29 "ses",
30 "apigateway",
31 "bedrock",
32 "bedrock-agent",
33 "bedrock-agent-runtime",
34 "scheduler",
35 "batch",
36 "pipes",
37 "rds-data",
38 "dsql",
39 "resource-groups",
40 "eks",
41 "glacier",
42 "backup",
43 "ram",
46 "s3tables",
49 "lakeformation",
52 "es",
56 "account",
57 "appconfig",
61 "codeartifact",
64 "elasticfilesystem",
68 "mq",
71 "kafka",
75 "mwaa",
79 "fis",
84 "xray",
89 "appsync",
94 "amplify",
99 "mediaconvert",
104 "serverlessrepo",
109 "iotdata",
114 "pinpoint",
120 "iot",
125 "iotwireless",
130];
131
132#[derive(Debug, Clone)]
134pub struct DetectedRequest {
135 pub service: String,
136 pub action: String,
137 pub protocol: AwsProtocol,
138}
139
140pub fn detect_service_headers_only(
147 headers: &HeaderMap,
148 query_params: &HashMap<String, String>,
149) -> Option<DetectedRequest> {
150 if let Some(target) = headers.get("x-amz-target").and_then(|v| v.to_str().ok()) {
152 return parse_amz_target(target);
153 }
154 if let Some(action) = query_params.get("Action") {
155 let service = extract_service_from_auth(headers)
156 .or_else(|| infer_service_from_action(action))
157 .or_else(|| parse_routing_host_from_headers(headers).map(|h| h.service));
158 if let Some(service) = service {
159 return Some(DetectedRequest {
160 service,
161 action: action.clone(),
162 protocol: AwsProtocol::Query,
163 });
164 }
165 }
166 if let Some(service) = extract_service_from_auth(headers) {
167 if let Some(protocol) = rest_protocol_for(&service) {
168 return Some(DetectedRequest {
169 service,
170 action: String::new(),
171 protocol,
172 });
173 }
174 }
175 if let Some(credential) = query_params.get("X-Amz-Credential") {
176 let parts: Vec<&str> = credential.split('/').collect();
177 if parts.len() >= 4 {
178 let service = normalize_service_name(parts[3]).to_string();
179 if let Some(protocol) = rest_protocol_for(&service) {
180 return Some(DetectedRequest {
181 service,
182 action: String::new(),
183 protocol,
184 });
185 }
186 }
187 }
188 if query_params.contains_key("AWSAccessKeyId")
189 && query_params.contains_key("Signature")
190 && query_params.contains_key("Expires")
191 {
192 return Some(DetectedRequest {
193 service: "s3".to_string(),
194 action: String::new(),
195 protocol: AwsProtocol::Rest,
196 });
197 }
198 if let Some(host_info) = parse_routing_host_from_headers(headers) {
199 if let Some(protocol) = rest_protocol_for(&host_info.service) {
200 return Some(DetectedRequest {
201 service: host_info.service,
202 action: String::new(),
203 protocol,
204 });
205 }
206 }
207 None
208}
209
210pub fn detect_service(
212 headers: &HeaderMap,
213 query_params: &HashMap<String, String>,
214 body: &Bytes,
215) -> Option<DetectedRequest> {
216 if let Some(target) = headers.get("x-amz-target").and_then(|v| v.to_str().ok()) {
218 return parse_amz_target(target);
219 }
220
221 if let Some(action) = query_params.get("Action") {
223 let service = extract_service_from_auth(headers)
224 .or_else(|| infer_service_from_action(action))
225 .or_else(|| parse_routing_host_from_headers(headers).map(|h| h.service));
226 if let Some(service) = service {
227 return Some(DetectedRequest {
228 service,
229 action: action.clone(),
230 protocol: AwsProtocol::Query,
231 });
232 }
233 }
234
235 {
237 let form_params = decode_form_urlencoded(body);
238
239 if let Some(action) = form_params.get("Action") {
240 let service = extract_service_from_auth(headers)
241 .or_else(|| infer_service_from_action(action))
242 .or_else(|| parse_routing_host_from_headers(headers).map(|h| h.service));
243 if let Some(service) = service {
244 return Some(DetectedRequest {
245 service,
246 action: action.clone(),
247 protocol: AwsProtocol::Query,
248 });
249 }
250 }
251 }
252
253 if let Some(service) = extract_service_from_auth(headers) {
255 if let Some(protocol) = rest_protocol_for(&service) {
256 return Some(DetectedRequest {
257 service,
258 action: String::new(), protocol,
260 });
261 }
262 }
263
264 if let Some(credential) = query_params.get("X-Amz-Credential") {
266 let parts: Vec<&str> = credential.split('/').collect();
268 if parts.len() >= 4 {
269 let service = normalize_service_name(parts[3]).to_string();
270 if let Some(protocol) = rest_protocol_for(&service) {
271 return Some(DetectedRequest {
272 service,
273 action: String::new(),
274 protocol,
275 });
276 }
277 }
278 }
279
280 if query_params.contains_key("AWSAccessKeyId")
284 && query_params.contains_key("Signature")
285 && query_params.contains_key("Expires")
286 {
287 return Some(DetectedRequest {
288 service: "s3".to_string(),
289 action: String::new(),
290 protocol: AwsProtocol::Rest,
291 });
292 }
293
294 if let Some(host_info) = parse_routing_host_from_headers(headers) {
298 if let Some(protocol) = rest_protocol_for(&host_info.service) {
299 return Some(DetectedRequest {
300 service: host_info.service,
301 action: String::new(),
302 protocol,
303 });
304 }
305 }
306
307 None
308}
309
310#[derive(Debug, Clone, PartialEq, Eq)]
319pub struct RoutingHost {
320 pub service: String,
321 pub region: String,
322 pub bucket: Option<String>,
324}
325
326const LOCALSTACK_SUFFIX: &str = ".localhost.localstack.cloud";
327const AWS_SUFFIX: &str = ".amazonaws.com";
328
329pub fn parse_routing_host(host: &str) -> Option<RoutingHost> {
333 let hostname = host.split(':').next()?;
334 if hostname.is_empty() {
335 return None;
336 }
337 let hostname = hostname.to_ascii_lowercase();
338 if let Some(prefix) = hostname.strip_suffix(LOCALSTACK_SUFFIX) {
339 return parse_localstack_prefix(prefix);
340 }
341 if hostname == "amazonaws.com" {
342 return None;
343 }
344 if let Some(prefix) = hostname.strip_suffix(AWS_SUFFIX) {
345 return parse_aws_prefix(prefix);
346 }
347 None
348}
349
350pub fn parse_routing_host_from_headers(headers: &HeaderMap) -> Option<RoutingHost> {
352 let host = headers.get("host")?.to_str().ok()?;
353 parse_routing_host(host)
354}
355
356fn parse_localstack_prefix(prefix: &str) -> Option<RoutingHost> {
357 if prefix.is_empty() {
358 return None;
359 }
360 let labels: Vec<&str> = prefix.split('.').collect();
361 if labels.iter().any(|l| l.is_empty()) {
362 return None;
363 }
364 match labels.len() {
365 2 => Some(RoutingHost {
366 service: labels[0].to_string(),
367 region: labels[1].to_string(),
368 bucket: None,
369 }),
370 n if n >= 3 && labels[n - 2] == "s3" => {
371 let bucket = labels[..n - 2].join(".");
372 Some(RoutingHost {
373 service: "s3".to_string(),
374 region: labels[n - 1].to_string(),
375 bucket: Some(bucket),
376 })
377 }
378 n if n >= 3 && labels[n - 2] == "s3-accesspoint" => {
379 let bucket = labels[..n - 2].join(".");
380 Some(RoutingHost {
381 service: "s3".to_string(),
382 region: labels[n - 1].to_string(),
383 bucket: Some(bucket),
384 })
385 }
386 n if n >= 3 && labels[n - 2] == "s3-control" => Some(RoutingHost {
387 service: "s3".to_string(),
388 region: labels[n - 1].to_string(),
389 bucket: None,
390 }),
391 _ => None,
392 }
393}
394
395fn parse_aws_prefix(prefix: &str) -> Option<RoutingHost> {
407 if prefix.is_empty() {
408 return None;
409 }
410 let labels: Vec<&str> = prefix.split('.').collect();
411 if labels.iter().any(|l| l.is_empty()) {
412 return None;
413 }
414 let last = *labels.last()?;
415
416 if let Some(region) = last.strip_prefix("s3-") {
419 if !region.is_empty() {
420 let bucket = if labels.len() >= 2 {
421 Some(labels[..labels.len() - 1].join("."))
422 } else {
423 None
424 };
425 return Some(RoutingHost {
426 service: "s3".to_string(),
427 region: region.to_string(),
428 bucket,
429 });
430 }
431 }
432
433 if last == "s3" {
437 if labels.len() == 1 {
438 return Some(RoutingHost {
439 service: "s3".to_string(),
440 region: "us-east-1".to_string(),
441 bucket: None,
442 });
443 }
444 return Some(RoutingHost {
445 service: "s3".to_string(),
446 region: "us-east-1".to_string(),
447 bucket: Some(labels[..labels.len() - 1].join(".")),
448 });
449 }
450
451 if last == "s3-accesspoint" {
454 if labels.len() == 2 {
455 return Some(RoutingHost {
456 service: "s3".to_string(),
457 region: labels[0].to_string(),
458 bucket: None,
459 });
460 }
461 if labels.len() >= 3 {
465 let bucket = labels[..labels.len() - 2].join(".");
466 return Some(RoutingHost {
467 service: "s3".to_string(),
468 region: labels[labels.len() - 1].to_string(),
469 bucket: Some(bucket),
470 });
471 }
472 }
473
474 if labels.len() >= 2 && labels[labels.len() - 2] == "s3-control" {
477 return Some(RoutingHost {
478 service: "s3".to_string(),
479 region: last.to_string(),
480 bucket: None,
481 });
482 }
483
484 match labels.len() {
485 2 => Some(RoutingHost {
488 service: labels[0].to_string(),
489 region: labels[1].to_string(),
490 bucket: None,
491 }),
492 n if n >= 3 && labels[n - 2] == "s3" => {
494 let bucket = labels[..n - 2].join(".");
495 Some(RoutingHost {
496 service: "s3".to_string(),
497 region: labels[n - 1].to_string(),
498 bucket: Some(bucket),
499 })
500 }
501 _ => None,
502 }
503}
504
505fn parse_amz_target(target: &str) -> Option<DetectedRequest> {
508 let (prefix, action) = target.rsplit_once('.')?;
509
510 let service = match prefix {
511 "AWSEvents" => "events",
512 "AmazonSSM" => "ssm",
513 "AmazonSQS" => "sqs",
514 "AmazonSNS" => "sns",
515 "DynamoDB_20120810" => "dynamodb",
516 "DynamoDBStreams_20120810" => "dynamodbstreams",
517 "Logs_20140328" => "logs",
518 s if s.starts_with("secretsmanager") => "secretsmanager",
519 s if s.starts_with("TrentService") => "kms",
520 s if s.starts_with("AWSCognitoIdentityProviderService") => "cognito-idp",
521 s if s.starts_with("AWSCognitoIdentityService") => "cognito-identity",
522 s if s.starts_with("Kinesis_20131202") => "kinesis",
523 s if s.starts_with("AmazonEC2ContainerRegistry_V") => "ecr",
524 s if s.starts_with("AmazonEC2ContainerServiceV") => "ecs",
525 s if s.starts_with("AWSStepFunctions") => "states",
526 s if s.starts_with("AWSOrganizationsV") => "organizations",
527 "CertificateManager" => "acm",
528 "ACMPrivateCA" => "acm-pca",
529 "Route53Resolver" => "route53resolver",
533 "StarlingDoveService" => "config",
536 "AnyScaleFrontendService" => "application-autoscaling",
537 "AWSWAF_20190729" => "wafv2",
540 "AmazonAthena" => "athena",
541 s if s.starts_with("Firehose_") => "firehose",
542 "AWSGlue" => "glue",
543 "ElasticMapReduce" => "emr",
546 "Textract" => "textract",
549 "Transcribe" => "transcribe",
552 "AWSShineFrontendService_20170701" => "translate",
555 "AWSShield_20160616" => "shield",
558 "Comprehend_20171127" => "comprehend",
561 "SimpleWorkflowService" => "swf",
564 "Timestream_20181101" => "timestream",
568 "AWSSupport_20130415" => "support",
571 "CloudApiService" => "cloudcontrolapi",
572 "ResourceGroupsTaggingAPI_20170126" => "tagging",
573 "AmazonMemoryDB" => "memorydb",
574 s if s.starts_with("KinesisAnalytics_20180523") => "kinesisanalyticsv2",
579 "Route53AutoNaming_v20170314" => "servicediscovery",
582 "AmazonDMSv20160101" => "dms",
584 "CloudTrail_20131101" => "cloudtrail",
588 "com.amazonaws.cloudtrail.v20131101.CloudTrail_20131101" => "cloudtrail",
589 "AWSInsightsIndexService" => "ce",
593 "com.amazonaws.costexplorer.v20171025.AWSInsightsIndexService" => "ce",
594 "TransferService" => "transfer",
596 "CodeBuild_20161006" => "codebuild",
598 "CodeCommit_20150413" => "codecommit",
600 "AWSIdentityStore" => "identitystore",
602 "SWBExternalService" => "sso",
604 "VerifiedPermissions" => "verifiedpermissions",
606 "CodeConnections_20231201" => "codeconnections",
608 "CodeStar_connections_20191201" => "codeconnections",
614 "CodeDeploy_20141006" => "codedeploy",
616 "CodePipeline_20150709" => "codepipeline",
618 s if s.starts_with("GraniteServiceVersion") => "monitoring",
624 "SageMaker" => "sagemaker",
627 _ => return None,
628 };
629
630 Some(DetectedRequest {
631 service: service.to_string(),
632 action: action.to_string(),
633 protocol: AwsProtocol::Json,
634 })
635}
636
637fn rest_protocol_for(service: &str) -> Option<AwsProtocol> {
639 if REST_XML_SERVICES.contains(&service) {
640 Some(AwsProtocol::Rest)
641 } else if REST_JSON_SERVICES.contains(&service) {
642 Some(AwsProtocol::RestJson)
643 } else {
644 None
645 }
646}
647
648fn infer_service_from_action(action: &str) -> Option<String> {
652 match action {
653 "AssumeRole"
654 | "AssumeRoleWithSAML"
655 | "AssumeRoleWithWebIdentity"
656 | "GetCallerIdentity"
657 | "GetSessionToken"
658 | "GetFederationToken"
659 | "GetAccessKeyInfo"
660 | "DecodeAuthorizationMessage" => Some("sts".to_string()),
661 "CreateUser" | "DeleteUser" | "GetUser" | "ListUsers" | "CreateRole" | "DeleteRole"
662 | "GetRole" | "ListRoles" | "CreatePolicy" | "DeletePolicy" | "GetPolicy"
663 | "ListPolicies" | "AttachRolePolicy" | "DetachRolePolicy" | "CreateAccessKey"
664 | "DeleteAccessKey" | "ListAccessKeys" | "ListRolePolicies" => Some("iam".to_string()),
665 "VerifyEmailIdentity"
667 | "VerifyDomainIdentity"
668 | "VerifyDomainDkim"
669 | "ListIdentities"
670 | "GetIdentityVerificationAttributes"
671 | "GetIdentityDkimAttributes"
672 | "DeleteIdentity"
673 | "SetIdentityDkimEnabled"
674 | "SetIdentityNotificationTopic"
675 | "SetIdentityFeedbackForwardingEnabled"
676 | "GetIdentityNotificationAttributes"
677 | "GetIdentityMailFromDomainAttributes"
678 | "SetIdentityMailFromDomain"
679 | "SendEmail"
680 | "SendRawEmail"
681 | "SendTemplatedEmail"
682 | "SendBulkTemplatedEmail"
683 | "CreateTemplate"
684 | "GetTemplate"
685 | "ListTemplates"
686 | "DeleteTemplate"
687 | "UpdateTemplate"
688 | "CreateConfigurationSet"
689 | "DeleteConfigurationSet"
690 | "DescribeConfigurationSet"
691 | "ListConfigurationSets"
692 | "CreateConfigurationSetEventDestination"
693 | "UpdateConfigurationSetEventDestination"
694 | "DeleteConfigurationSetEventDestination"
695 | "GetSendQuota"
696 | "GetSendStatistics"
697 | "GetAccountSendingEnabled"
698 | "CreateReceiptRuleSet"
699 | "DeleteReceiptRuleSet"
700 | "DescribeReceiptRuleSet"
701 | "ListReceiptRuleSets"
702 | "CloneReceiptRuleSet"
703 | "SetActiveReceiptRuleSet"
704 | "ReorderReceiptRuleSet"
705 | "CreateReceiptRule"
706 | "DeleteReceiptRule"
707 | "DescribeReceiptRule"
708 | "UpdateReceiptRule"
709 | "CreateReceiptFilter"
710 | "DeleteReceiptFilter"
711 | "ListReceiptFilters" => Some("ses".to_string()),
712 "ConfirmSubscription" | "Unsubscribe" => Some("sns".to_string()),
716 _ => None,
717 }
718}
719
720fn extract_service_from_auth(headers: &HeaderMap) -> Option<String> {
722 let auth = headers.get("authorization")?.to_str().ok()?;
723 let info = fakecloud_aws::sigv4::parse_sigv4(auth)?;
724 Some(normalize_service_name(&info.service).to_string())
725}
726
727fn normalize_service_name(service: &str) -> &str {
739 match service {
740 "bedrock-runtime" => "bedrock",
741 "apigatewayv2" => "apigateway",
749 "opensearch" => "es",
757 "appconfigdata" => "appconfig",
763 "airflow" => "mwaa",
768 "mobiletargeting" => "pinpoint",
773 other => other,
774 }
775}
776
777pub fn parse_query_body(body: &Bytes) -> HashMap<String, String> {
779 decode_form_urlencoded(body)
780}
781
782pub fn flatten_json_to_query(body: &Bytes) -> HashMap<String, String> {
799 let mut out = HashMap::new();
800 let Ok(value) = serde_json::from_slice::<serde_json::Value>(body) else {
801 return out;
802 };
803 if value.is_object() {
804 flatten_json_value("", &value, &mut out);
805 }
806 out
807}
808
809fn flatten_json_value(prefix: &str, value: &serde_json::Value, out: &mut HashMap<String, String>) {
810 match value {
811 serde_json::Value::Object(map) => {
812 for (k, v) in map {
813 let child = if prefix.is_empty() {
814 k.clone()
815 } else {
816 format!("{prefix}.{k}")
817 };
818 flatten_json_value(&child, v, out);
819 }
820 }
821 serde_json::Value::Array(items) => {
822 for (i, v) in items.iter().enumerate() {
823 let child = format!("{prefix}.member.{}", i + 1);
824 flatten_json_value(&child, v, out);
825 }
826 }
827 serde_json::Value::Null => {}
828 serde_json::Value::String(s) => {
829 out.insert(prefix.to_string(), s.clone());
830 }
831 serde_json::Value::Bool(b) => {
832 out.insert(prefix.to_string(), b.to_string());
833 }
834 serde_json::Value::Number(n) => {
835 out.insert(prefix.to_string(), n.to_string());
836 }
837 }
838}
839
840pub(crate) fn form_urlencoded_pairs(input: &str) -> Vec<(String, String)> {
846 let mut pairs = Vec::new();
847 for pair in input.split('&') {
848 if pair.is_empty() {
849 continue;
850 }
851 let (key, value) = match pair.find('=') {
852 Some(pos) => (&pair[..pos], &pair[pos + 1..]),
853 None => (pair, ""),
854 };
855 pairs.push((url_decode(key), url_decode(value)));
856 }
857 pairs
858}
859
860fn decode_form_urlencoded(input: &[u8]) -> HashMap<String, String> {
861 let s = std::str::from_utf8(input).unwrap_or("");
862 let mut result = HashMap::new();
863 for pair in s.split('&') {
864 if pair.is_empty() {
865 continue;
866 }
867 let (key, value) = match pair.find('=') {
868 Some(pos) => (&pair[..pos], &pair[pos + 1..]),
869 None => (pair, ""),
870 };
871 result.insert(url_decode(key), url_decode(value));
872 }
873 result
874}
875
876fn url_decode(input: &str) -> String {
877 let mut buf: Vec<u8> = Vec::with_capacity(input.len());
883 let mut bytes = input.bytes();
884 while let Some(b) = bytes.next() {
885 match b {
886 b'+' => buf.push(b' '),
887 b'%' => {
888 let high = bytes.next().and_then(from_hex);
889 let low = bytes.next().and_then(from_hex);
890 if let (Some(h), Some(l)) = (high, low) {
894 buf.push((h << 4) | l);
895 }
896 }
897 _ => buf.push(b),
898 }
899 }
900 String::from_utf8_lossy(&buf).into_owned()
901}
902
903fn from_hex(b: u8) -> Option<u8> {
904 match b {
905 b'0'..=b'9' => Some(b - b'0'),
906 b'a'..=b'f' => Some(b - b'a' + 10),
907 b'A'..=b'F' => Some(b - b'A' + 10),
908 _ => None,
909 }
910}
911
912#[cfg(test)]
913mod tests {
914 use super::*;
915
916 #[test]
917 fn form_urlencoded_pairs_preserves_repeated_keys() {
918 let pairs = form_urlencoded_pairs("Id=a&Id=b&Id=c&Other=x");
921 let ids: Vec<&str> = pairs
922 .iter()
923 .filter(|(k, _)| k == "Id")
924 .map(|(_, v)| v.as_str())
925 .collect();
926 assert_eq!(ids, vec!["a", "b", "c"]);
927 assert_eq!(
929 decode_form_urlencoded(b"Id=a&Id=b&Id=c").get("Id").unwrap(),
930 "c"
931 );
932 }
933
934 #[test]
935 fn form_urlencoded_pairs_decodes_percent_and_plus() {
936 let pairs = form_urlencoded_pairs("q=caf%C3%A9+bar&empty=");
937 assert_eq!(pairs[0], ("q".to_string(), "café bar".to_string()));
938 assert_eq!(pairs[1], ("empty".to_string(), String::new()));
939 }
940
941 #[test]
942 fn parse_amz_target_events() {
943 let result = parse_amz_target("AWSEvents.PutEvents").unwrap();
944 assert_eq!(result.service, "events");
945 assert_eq!(result.action, "PutEvents");
946 assert_eq!(result.protocol, AwsProtocol::Json);
947 }
948
949 #[test]
950 fn parse_amz_target_ssm() {
951 let result = parse_amz_target("AmazonSSM.GetParameter").unwrap();
952 assert_eq!(result.service, "ssm");
953 assert_eq!(result.action, "GetParameter");
954 }
955
956 #[test]
957 fn parse_amz_target_kinesis() {
958 let result = parse_amz_target("Kinesis_20131202.ListStreams").unwrap();
959 assert_eq!(result.service, "kinesis");
960 assert_eq!(result.action, "ListStreams");
961 assert_eq!(result.protocol, AwsProtocol::Json);
962 }
963
964 #[test]
965 fn parse_query_body_basic() {
966 let body = Bytes::from(
967 "Action=SendMessage&QueueUrl=http%3A%2F%2Flocalhost%3A4566%2Fqueue&MessageBody=hello",
968 );
969 let params = parse_query_body(&body);
970 assert_eq!(params.get("Action").unwrap(), "SendMessage");
971 assert_eq!(params.get("MessageBody").unwrap(), "hello");
972 }
973
974 #[test]
975 fn parse_query_body_empty_returns_empty_map() {
976 let body = Bytes::from("");
977 let params = parse_query_body(&body);
978 assert!(params.is_empty());
979 }
980
981 #[test]
982 fn parse_query_body_duplicate_keys_last_wins() {
983 let body = Bytes::from("key=a&key=b");
984 let params = parse_query_body(&body);
985 assert_eq!(params.get("key").unwrap(), "b");
986 }
987
988 #[test]
989 fn parse_query_body_single_key() {
990 let body = Bytes::from("key=value");
991 let params = parse_query_body(&body);
992 assert_eq!(params.get("key").unwrap(), "value");
993 }
994
995 #[test]
996 fn url_decode_plain_ascii() {
997 assert_eq!(url_decode("hello"), "hello");
998 assert_eq!(url_decode("Action=SendMessage"), "Action=SendMessage");
999 }
1000
1001 #[test]
1002 fn url_decode_plus_is_space() {
1003 assert_eq!(url_decode("hello+world"), "hello world");
1004 assert_eq!(url_decode("a+b+c"), "a b c");
1005 }
1006
1007 #[test]
1008 fn url_decode_multibyte_utf8_accents() {
1009 assert_eq!(url_decode("caf%C3%A9"), "café");
1011 }
1012
1013 #[test]
1014 fn url_decode_multibyte_utf8_cjk() {
1015 assert_eq!(url_decode("%E6%97%A5%E6%9C%AC"), "日本");
1017 }
1018
1019 #[test]
1020 fn url_decode_multibyte_utf8_emoji() {
1021 assert_eq!(url_decode("%F0%9F%9A%80"), "🚀");
1023 }
1024
1025 #[test]
1026 fn url_decode_mixed_ascii_and_multibyte() {
1027 assert_eq!(url_decode("Tag+%3D+caf%C3%A9%21"), "Tag = café!");
1028 }
1029
1030 #[test]
1031 fn url_decode_malformed_percent_is_graceful() {
1032 assert_eq!(url_decode("100%"), "100");
1034 assert_eq!(url_decode("a%zz"), "a");
1035 assert_eq!(url_decode("a%4"), "a");
1036 assert_eq!(url_decode("x%y"), "x");
1038 }
1039
1040 #[test]
1041 fn url_decode_invalid_utf8_bytes_are_lossy_no_panic() {
1042 let out = url_decode("bad%FFbyte");
1044 assert!(out.starts_with("bad"));
1045 assert!(out.ends_with("byte"));
1046 }
1047
1048 #[test]
1049 fn parse_query_body_multibyte_value_round_trips() {
1050 let body = Bytes::from("Tag.Value=caf%C3%A9&Name=%E6%97%A5%E6%9C%AC");
1051 let params = parse_query_body(&body);
1052 assert_eq!(params.get("Tag.Value").unwrap(), "café");
1053 assert_eq!(params.get("Name").unwrap(), "日本");
1054 }
1055
1056 #[test]
1057 fn parse_amz_target_ecs() {
1058 let result = parse_amz_target("AmazonEC2ContainerServiceV20141113.ListClusters").unwrap();
1059 assert_eq!(result.service, "ecs");
1060 assert_eq!(result.action, "ListClusters");
1061 assert_eq!(result.protocol, AwsProtocol::Json);
1062 }
1063
1064 #[test]
1065 fn parse_amz_target_invalid_returns_none() {
1066 assert!(parse_amz_target("NoDotHere").is_none());
1067 assert!(parse_amz_target("").is_none());
1068 }
1069
1070 #[test]
1071 fn parse_amz_target_cloudwatch_json() {
1072 let result = parse_amz_target("GraniteServiceVersion20100801.PutMetricData").unwrap();
1074 assert_eq!(result.service, "monitoring");
1075 assert_eq!(result.action, "PutMetricData");
1076 assert_eq!(result.protocol, AwsProtocol::Json);
1077 }
1078
1079 #[test]
1080 fn flatten_json_to_query_nested() {
1081 let body = Bytes::from(
1082 serde_json::json!({
1083 "Namespace": "MyApp",
1084 "MetricData": [{
1085 "MetricName": "Latency",
1086 "Value": 12.5,
1087 "StatisticValues": {"SampleCount": 3, "Sum": 10},
1088 "Dimensions": [{"Name": "Endpoint", "Value": "/api"}]
1089 }]
1090 })
1091 .to_string(),
1092 );
1093 let flat = flatten_json_to_query(&body);
1094 assert_eq!(flat.get("Namespace").unwrap(), "MyApp");
1095 assert_eq!(
1096 flat.get("MetricData.member.1.MetricName").unwrap(),
1097 "Latency"
1098 );
1099 assert_eq!(flat.get("MetricData.member.1.Value").unwrap(), "12.5");
1100 assert_eq!(
1101 flat.get("MetricData.member.1.StatisticValues.SampleCount")
1102 .unwrap(),
1103 "3"
1104 );
1105 assert_eq!(
1106 flat.get("MetricData.member.1.Dimensions.member.1.Name")
1107 .unwrap(),
1108 "Endpoint"
1109 );
1110 assert_eq!(
1111 flat.get("MetricData.member.1.Dimensions.member.1.Value")
1112 .unwrap(),
1113 "/api"
1114 );
1115 }
1116
1117 #[test]
1118 fn flatten_json_to_query_non_object_is_empty() {
1119 assert!(flatten_json_to_query(&Bytes::from_static(b"[]")).is_empty());
1120 assert!(flatten_json_to_query(&Bytes::from_static(b"not json")).is_empty());
1121 }
1122
1123 #[test]
1124 fn parse_amz_target_various_prefixes() {
1125 assert_eq!(
1126 parse_amz_target("AmazonSQS.SendMessage").unwrap().service,
1127 "sqs"
1128 );
1129 assert_eq!(
1130 parse_amz_target("AmazonSNS.Publish").unwrap().service,
1131 "sns"
1132 );
1133 assert_eq!(
1134 parse_amz_target("DynamoDB_20120810.GetItem")
1135 .unwrap()
1136 .service,
1137 "dynamodb"
1138 );
1139 assert_eq!(
1140 parse_amz_target("Logs_20140328.PutLogEvents")
1141 .unwrap()
1142 .service,
1143 "logs"
1144 );
1145 assert_eq!(
1146 parse_amz_target("secretsmanager.GetSecretValue")
1147 .unwrap()
1148 .service,
1149 "secretsmanager"
1150 );
1151 assert_eq!(
1152 parse_amz_target("TrentService.Encrypt").unwrap().service,
1153 "kms"
1154 );
1155 assert_eq!(
1156 parse_amz_target("AWSCognitoIdentityProviderService.InitiateAuth")
1157 .unwrap()
1158 .service,
1159 "cognito-idp"
1160 );
1161 assert_eq!(
1162 parse_amz_target("AWSStepFunctions.StartExecution")
1163 .unwrap()
1164 .service,
1165 "states"
1166 );
1167 assert_eq!(
1168 parse_amz_target("AWSOrganizationsV20161128.CreateOrganization")
1169 .unwrap()
1170 .service,
1171 "organizations"
1172 );
1173 assert!(parse_amz_target("UnknownServicePrefix.Action").is_none());
1174 }
1175
1176 #[test]
1177 fn infer_service_from_action_maps_sts() {
1178 assert_eq!(
1179 infer_service_from_action("AssumeRole").as_deref(),
1180 Some("sts")
1181 );
1182 assert_eq!(
1183 infer_service_from_action("GetCallerIdentity").as_deref(),
1184 Some("sts")
1185 );
1186 }
1187
1188 #[test]
1189 fn infer_service_from_action_maps_iam() {
1190 assert_eq!(
1191 infer_service_from_action("CreateUser").as_deref(),
1192 Some("iam")
1193 );
1194 assert_eq!(
1195 infer_service_from_action("ListRoles").as_deref(),
1196 Some("iam")
1197 );
1198 }
1199
1200 #[test]
1201 fn infer_service_from_action_maps_ses() {
1202 assert_eq!(
1203 infer_service_from_action("SendEmail").as_deref(),
1204 Some("ses")
1205 );
1206 assert_eq!(
1207 infer_service_from_action("ListIdentities").as_deref(),
1208 Some("ses")
1209 );
1210 }
1211
1212 #[test]
1213 fn infer_service_from_action_maps_sns_confirmation_flow() {
1214 assert_eq!(
1217 infer_service_from_action("ConfirmSubscription").as_deref(),
1218 Some("sns")
1219 );
1220 assert_eq!(
1221 infer_service_from_action("Unsubscribe").as_deref(),
1222 Some("sns")
1223 );
1224 }
1225
1226 #[test]
1227 fn detect_service_routes_unsigned_confirm_subscription_to_sns() {
1228 let mut headers = HeaderMap::new();
1231 headers.insert("host", "localhost:4566".parse().unwrap());
1232 let mut query_params = HashMap::new();
1233 query_params.insert("Action".to_string(), "ConfirmSubscription".to_string());
1234 query_params.insert(
1235 "TopicArn".to_string(),
1236 "arn:aws:sns:us-east-1:000000000000:t".to_string(),
1237 );
1238 query_params.insert("Token".to_string(), "abc123".to_string());
1239
1240 let detected = detect_service(&headers, &query_params, &Bytes::new())
1241 .expect("ConfirmSubscription must route to a service");
1242 assert_eq!(detected.service, "sns");
1243 assert_eq!(detected.action, "ConfirmSubscription");
1244 assert_eq!(detected.protocol, AwsProtocol::Query);
1245 }
1246
1247 #[test]
1248 fn infer_service_from_action_unknown_returns_none() {
1249 assert!(infer_service_from_action("NotARealAction").is_none());
1250 }
1251
1252 #[test]
1253 fn rest_protocol_for_returns_none_for_non_rest_service() {
1254 assert!(rest_protocol_for("sqs").is_none());
1255 }
1256
1257 #[test]
1258 fn url_decode_handles_percent_and_plus() {
1259 assert_eq!(url_decode("hello+world"), "hello world");
1260 assert_eq!(url_decode("hello%20world"), "hello world");
1261 assert_eq!(url_decode("100%25"), "100%");
1262 }
1263
1264 #[test]
1265 fn url_decode_ignores_malformed_percent() {
1266 assert_eq!(url_decode("%ZZ"), "");
1267 }
1268
1269 #[test]
1270 fn from_hex_valid_digits() {
1271 assert_eq!(from_hex(b'0'), Some(0));
1272 assert_eq!(from_hex(b'9'), Some(9));
1273 assert_eq!(from_hex(b'a'), Some(10));
1274 assert_eq!(from_hex(b'F'), Some(15));
1275 }
1276
1277 #[test]
1278 fn from_hex_invalid_returns_none() {
1279 assert!(from_hex(b'g').is_none());
1280 assert!(from_hex(b' ').is_none());
1281 }
1282
1283 #[test]
1284 fn detect_service_via_amz_target() {
1285 let mut headers = HeaderMap::new();
1286 headers.insert("x-amz-target", "AmazonSSM.GetParameter".parse().unwrap());
1287 let query = HashMap::new();
1288 let body = Bytes::new();
1289 let detected = detect_service(&headers, &query, &body).unwrap();
1290 assert_eq!(detected.service, "ssm");
1291 assert_eq!(detected.action, "GetParameter");
1292 }
1293
1294 #[test]
1295 fn detect_service_via_query_action_with_inferred_service() {
1296 let headers = HeaderMap::new();
1297 let mut query = HashMap::new();
1298 query.insert("Action".to_string(), "AssumeRole".to_string());
1299 let body = Bytes::new();
1300 let detected = detect_service(&headers, &query, &body).unwrap();
1301 assert_eq!(detected.service, "sts");
1302 assert_eq!(detected.action, "AssumeRole");
1303 assert_eq!(detected.protocol, AwsProtocol::Query);
1304 }
1305
1306 #[test]
1307 fn detect_service_via_form_body() {
1308 let headers = HeaderMap::new();
1309 let query = HashMap::new();
1310 let body = Bytes::from("Action=SendEmail&Source=x%40y.com");
1311 let detected = detect_service(&headers, &query, &body).unwrap();
1312 assert_eq!(detected.service, "ses");
1313 assert_eq!(detected.action, "SendEmail");
1314 }
1315
1316 #[test]
1317 fn detect_service_via_sigv2_presigned() {
1318 let headers = HeaderMap::new();
1319 let mut query = HashMap::new();
1320 query.insert("AWSAccessKeyId".to_string(), "AKID".to_string());
1321 query.insert("Signature".to_string(), "sig".to_string());
1322 query.insert("Expires".to_string(), "1234567890".to_string());
1323 let body = Bytes::new();
1324 let detected = detect_service(&headers, &query, &body).unwrap();
1325 assert_eq!(detected.service, "s3");
1326 assert_eq!(detected.protocol, AwsProtocol::Rest);
1327 }
1328
1329 #[test]
1330 fn detect_service_via_sigv4_presigned_credential() {
1331 let headers = HeaderMap::new();
1332 let mut query = HashMap::new();
1333 query.insert(
1334 "X-Amz-Credential".to_string(),
1335 "AKID/20240101/us-east-1/s3/aws4_request".to_string(),
1336 );
1337 let body = Bytes::new();
1338 let detected = detect_service(&headers, &query, &body).unwrap();
1339 assert_eq!(detected.service, "s3");
1340 assert_eq!(detected.protocol, AwsProtocol::Rest);
1341 }
1342
1343 #[test]
1344 fn detect_service_unknown_returns_none() {
1345 let headers = HeaderMap::new();
1346 let query = HashMap::new();
1347 let body = Bytes::new();
1348 assert!(detect_service(&headers, &query, &body).is_none());
1349 }
1350
1351 #[test]
1352 fn normalize_service_name_aliases_apigatewayv2_to_apigateway() {
1353 assert_eq!(normalize_service_name("apigatewayv2"), "apigateway");
1358 }
1359
1360 #[test]
1361 fn normalize_service_name_aliases_bedrock_runtime_to_bedrock() {
1362 assert_eq!(normalize_service_name("bedrock-runtime"), "bedrock");
1367 }
1368
1369 #[test]
1370 fn normalize_service_name_passes_through_unaliased_services() {
1371 assert_eq!(normalize_service_name("bedrock"), "bedrock");
1375 assert_eq!(normalize_service_name("s3"), "s3");
1376 assert_eq!(normalize_service_name("lambda"), "lambda");
1377 assert_eq!(normalize_service_name(""), "");
1378 assert_eq!(
1379 normalize_service_name("unknown-future-service"),
1380 "unknown-future-service"
1381 );
1382 }
1383
1384 #[test]
1385 fn detect_service_via_authorization_header_normalizes_bedrock_runtime() {
1386 let mut headers = HeaderMap::new();
1391 headers.insert(
1392 "authorization",
1393 "AWS4-HMAC-SHA256 \
1394 Credential=AKID/20240101/us-east-1/bedrock-runtime/aws4_request, \
1395 SignedHeaders=host, Signature=abc"
1396 .parse()
1397 .unwrap(),
1398 );
1399 let query = HashMap::new();
1400 let body = Bytes::new();
1401 let detected = detect_service(&headers, &query, &body).unwrap();
1402 assert_eq!(detected.service, "bedrock");
1403 assert_eq!(detected.protocol, AwsProtocol::RestJson);
1404 }
1405
1406 #[test]
1407 fn detect_service_via_sigv4_presigned_credential_normalizes_bedrock_runtime() {
1408 let headers = HeaderMap::new();
1412 let mut query = HashMap::new();
1413 query.insert(
1414 "X-Amz-Credential".to_string(),
1415 "AKID/20240101/us-east-1/bedrock-runtime/aws4_request".to_string(),
1416 );
1417 let body = Bytes::new();
1418 let detected = detect_service(&headers, &query, &body).unwrap();
1419 assert_eq!(detected.service, "bedrock");
1420 assert_eq!(detected.protocol, AwsProtocol::RestJson);
1421 }
1422
1423 #[test]
1424 fn parse_routing_host_localstack_basic() {
1425 let h = parse_routing_host("sqs.us-east-1.localhost.localstack.cloud").unwrap();
1426 assert_eq!(h.service, "sqs");
1427 assert_eq!(h.region, "us-east-1");
1428 assert!(h.bucket.is_none());
1429 }
1430
1431 #[test]
1432 fn parse_routing_host_localstack_with_port() {
1433 let h = parse_routing_host("lambda.eu-west-1.localhost.localstack.cloud:4566").unwrap();
1434 assert_eq!(h.service, "lambda");
1435 assert_eq!(h.region, "eu-west-1");
1436 assert!(h.bucket.is_none());
1437 }
1438
1439 #[test]
1440 fn parse_routing_host_case_insensitive() {
1441 let h = parse_routing_host("SQS.US-EAST-1.LOCALHOST.LOCALSTACK.CLOUD:4566").unwrap();
1442 assert_eq!(h.service, "sqs");
1443 assert_eq!(h.region, "us-east-1");
1444
1445 let h = parse_routing_host("LAMBDA.US-EAST-1.AMAZONAWS.COM").unwrap();
1446 assert_eq!(h.service, "lambda");
1447 assert_eq!(h.region, "us-east-1");
1448 }
1449
1450 #[test]
1451 fn parse_routing_host_localstack_s3_virtual_hosted() {
1452 let h =
1453 parse_routing_host("my-bucket.s3.us-east-1.localhost.localstack.cloud:4566").unwrap();
1454 assert_eq!(h.service, "s3");
1455 assert_eq!(h.region, "us-east-1");
1456 assert_eq!(h.bucket.as_deref(), Some("my-bucket"));
1457 }
1458
1459 #[test]
1460 fn parse_routing_host_localstack_s3_vhost_bucket_with_dots() {
1461 let h = parse_routing_host("a.b.c.s3.us-east-1.localhost.localstack.cloud").unwrap();
1462 assert_eq!(h.service, "s3");
1463 assert_eq!(h.region, "us-east-1");
1464 assert_eq!(h.bucket.as_deref(), Some("a.b.c"));
1465 }
1466
1467 #[test]
1468 fn parse_routing_host_aws_service_region() {
1469 let h = parse_routing_host("sqs.us-east-1.amazonaws.com").unwrap();
1470 assert_eq!(h.service, "sqs");
1471 assert_eq!(h.region, "us-east-1");
1472 assert!(h.bucket.is_none());
1473
1474 let h = parse_routing_host("dynamodb.eu-west-2.amazonaws.com:443").unwrap();
1475 assert_eq!(h.service, "dynamodb");
1476 assert_eq!(h.region, "eu-west-2");
1477 }
1478
1479 #[test]
1480 fn parse_routing_host_aws_s3_path_style_modern() {
1481 let h = parse_routing_host("s3.us-east-1.amazonaws.com").unwrap();
1482 assert_eq!(h.service, "s3");
1483 assert_eq!(h.region, "us-east-1");
1484 assert!(h.bucket.is_none());
1485 }
1486
1487 #[test]
1488 fn parse_routing_host_aws_s3_virtual_hosted_modern() {
1489 let h = parse_routing_host("my-bucket.s3.us-east-1.amazonaws.com").unwrap();
1490 assert_eq!(h.service, "s3");
1491 assert_eq!(h.region, "us-east-1");
1492 assert_eq!(h.bucket.as_deref(), Some("my-bucket"));
1493 }
1494
1495 #[test]
1496 fn parse_routing_host_aws_s3_vhost_bucket_with_dots() {
1497 let h = parse_routing_host("a.b.c.s3.us-east-1.amazonaws.com").unwrap();
1498 assert_eq!(h.service, "s3");
1499 assert_eq!(h.region, "us-east-1");
1500 assert_eq!(h.bucket.as_deref(), Some("a.b.c"));
1501 }
1502
1503 #[test]
1504 fn parse_routing_host_aws_s3_legacy_global() {
1505 let h = parse_routing_host("s3.amazonaws.com").unwrap();
1508 assert_eq!(h.service, "s3");
1509 assert_eq!(h.region, "us-east-1");
1510 assert!(h.bucket.is_none());
1511
1512 let h = parse_routing_host("my-bucket.s3.amazonaws.com").unwrap();
1513 assert_eq!(h.service, "s3");
1514 assert_eq!(h.region, "us-east-1");
1515 assert_eq!(h.bucket.as_deref(), Some("my-bucket"));
1516 }
1517
1518 #[test]
1519 fn parse_routing_host_aws_s3_legacy_global_dotted_bucket() {
1520 let h = parse_routing_host("a.b.c.s3.amazonaws.com").unwrap();
1523 assert_eq!(h.service, "s3");
1524 assert_eq!(h.region, "us-east-1");
1525 assert_eq!(h.bucket.as_deref(), Some("a.b.c"));
1526 }
1527
1528 #[test]
1529 fn parse_routing_host_aws_s3_dash_separated() {
1530 let h = parse_routing_host("s3-us-west-2.amazonaws.com").unwrap();
1532 assert_eq!(h.service, "s3");
1533 assert_eq!(h.region, "us-west-2");
1534 assert!(h.bucket.is_none());
1535
1536 let h = parse_routing_host("my-bucket.s3-us-west-2.amazonaws.com").unwrap();
1537 assert_eq!(h.service, "s3");
1538 assert_eq!(h.region, "us-west-2");
1539 assert_eq!(h.bucket.as_deref(), Some("my-bucket"));
1540 }
1541
1542 #[test]
1543 fn parse_routing_host_rejects_plain_localhost() {
1544 assert!(parse_routing_host("localhost:4566").is_none());
1545 assert!(parse_routing_host("127.0.0.1:4566").is_none());
1546 }
1547
1548 #[test]
1549 fn parse_routing_host_rejects_unknown_suffix() {
1550 assert!(parse_routing_host("sqs.us-east-1.example.com").is_none());
1551 assert!(parse_routing_host("s3.us-east-1.aws").is_none());
1552 }
1553
1554 #[test]
1555 fn parse_routing_host_empty_and_malformed_rejected() {
1556 assert!(parse_routing_host("").is_none());
1557 assert!(parse_routing_host(".localhost.localstack.cloud").is_none());
1558 assert!(parse_routing_host("..localhost.localstack.cloud").is_none());
1559 assert!(parse_routing_host("sqs.localhost.localstack.cloud").is_none());
1560 assert!(parse_routing_host("foo.bar.baz.localhost.localstack.cloud").is_none());
1561 assert!(parse_routing_host(".amazonaws.com").is_none());
1562 assert!(parse_routing_host("amazonaws.com").is_none());
1563 }
1564
1565 #[test]
1566 fn parse_routing_host_bare_s3_accesspoint_does_not_panic() {
1567 assert!(parse_routing_host("s3-accesspoint").is_none());
1571 }
1572
1573 #[test]
1574 fn detect_service_via_host_for_rest_service() {
1575 let mut headers = HeaderMap::new();
1576 headers.insert(
1577 "host",
1578 "s3.us-east-1.localhost.localstack.cloud:4566"
1579 .parse()
1580 .unwrap(),
1581 );
1582 let query = HashMap::new();
1583 let body = Bytes::new();
1584 let detected = detect_service(&headers, &query, &body).unwrap();
1585 assert_eq!(detected.service, "s3");
1586 assert_eq!(detected.protocol, AwsProtocol::Rest);
1587 }
1588
1589 #[test]
1590 fn detect_service_via_host_for_rest_json_service() {
1591 let mut headers = HeaderMap::new();
1592 headers.insert(
1593 "host",
1594 "lambda.us-east-1.localhost.localstack.cloud:4566"
1595 .parse()
1596 .unwrap(),
1597 );
1598 let query = HashMap::new();
1599 let body = Bytes::new();
1600 let detected = detect_service(&headers, &query, &body).unwrap();
1601 assert_eq!(detected.service, "lambda");
1602 assert_eq!(detected.protocol, AwsProtocol::RestJson);
1603 }
1604
1605 #[test]
1606 fn detect_service_via_host_plus_query_action() {
1607 let mut headers = HeaderMap::new();
1608 headers.insert(
1609 "host",
1610 "sqs.us-east-1.localhost.localstack.cloud:4566"
1611 .parse()
1612 .unwrap(),
1613 );
1614 let mut query = HashMap::new();
1615 query.insert("Action".to_string(), "ListQueues".to_string());
1616 let body = Bytes::new();
1617 let detected = detect_service(&headers, &query, &body).unwrap();
1618 assert_eq!(detected.service, "sqs");
1619 assert_eq!(detected.action, "ListQueues");
1620 assert_eq!(detected.protocol, AwsProtocol::Query);
1621 }
1622
1623 #[test]
1624 fn detect_service_sigv4_wins_over_host() {
1625 let mut headers = HeaderMap::new();
1626 headers.insert(
1627 "authorization",
1628 "AWS4-HMAC-SHA256 Credential=AKID/20240101/us-east-1/s3/aws4_request, \
1629 SignedHeaders=host, Signature=abc"
1630 .parse()
1631 .unwrap(),
1632 );
1633 headers.insert(
1634 "host",
1635 "lambda.us-east-1.localhost.localstack.cloud:4566"
1636 .parse()
1637 .unwrap(),
1638 );
1639 let query = HashMap::new();
1640 let body = Bytes::new();
1641 let detected = detect_service(&headers, &query, &body).unwrap();
1642 assert_eq!(detected.service, "s3");
1644 assert_eq!(detected.protocol, AwsProtocol::Rest);
1645 }
1646
1647 #[test]
1648 fn detect_service_host_for_virtual_hosted_s3() {
1649 let mut headers = HeaderMap::new();
1650 headers.insert(
1651 "host",
1652 "my-bucket.s3.us-east-1.localhost.localstack.cloud:4566"
1653 .parse()
1654 .unwrap(),
1655 );
1656 let query = HashMap::new();
1657 let body = Bytes::new();
1658 let detected = detect_service(&headers, &query, &body).unwrap();
1659 assert_eq!(detected.service, "s3");
1660 assert_eq!(detected.protocol, AwsProtocol::Rest);
1661 }
1662}