Skip to main content

awsim_core/protocol/
query.rs

1use axum::http::{HeaderMap, StatusCode};
2use bytes::Bytes;
3use serde_json::Value;
4
5use crate::error::AwsError;
6
7use super::ParsedRequest;
8
9/// Parse an awsQuery request.
10///
11/// Form body contains `Action=OperationName&Version=...&Param1=value1&...`
12/// Complex types use dot-notation: `Tags.member.1.Key=Name&Tags.member.1.Value=foo`
13pub fn parse_request(body: &Bytes) -> Result<ParsedRequest, AwsError> {
14    let body_str = std::str::from_utf8(body)
15        .map_err(|_| AwsError::bad_request("InvalidRequest", "Request body is not valid UTF-8"))?;
16
17    let params: Vec<(String, String)> = serde_urlencoded::from_str(body_str)
18        .map_err(|e| AwsError::bad_request("InvalidRequest", format!("Invalid form body: {e}")))?;
19
20    let operation = params
21        .iter()
22        .find(|(k, _)| k == "Action")
23        .map(|(_, v)| v.clone())
24        .ok_or_else(|| AwsError::bad_request("MissingAction", "Missing 'Action' parameter"))?;
25
26    // Convert flat dot-notation params into structured JSON
27    let input = flatten_to_json(&params);
28
29    Ok(ParsedRequest { operation, input })
30}
31
32/// Convert flat query params with dot-notation into a JSON value.
33///
34/// Example input:
35///   `Tags.member.1.Key=Name`, `Tags.member.1.Value=foo`
36/// Output:
37///   `{"Tags": {"member": [{"Key": "Name", "Value": "foo"}]}}`
38fn flatten_to_json(params: &[(String, String)]) -> Value {
39    let mut map = serde_json::Map::new();
40    for (key, value) in params {
41        if key == "Action" || key == "Version" {
42            continue;
43        }
44        set_nested(&mut map, key, value);
45    }
46    Value::Object(map)
47}
48
49fn set_nested(map: &mut serde_json::Map<String, Value>, key: &str, value: &str) {
50    let parts: Vec<&str> = key.split('.').collect();
51    set_nested_recursive(map, &parts, value);
52}
53
54fn set_nested_recursive(map: &mut serde_json::Map<String, Value>, parts: &[&str], value: &str) {
55    if parts.is_empty() {
56        return;
57    }
58    if parts.len() == 1 {
59        map.insert(parts[0].to_string(), Value::String(value.to_string()));
60        return;
61    }
62
63    let key = parts[0];
64    let rest = &parts[1..];
65
66    // Check if next part is a number (array index)
67    if let Some(next) = rest.first() {
68        if next.parse::<usize>().is_ok() {
69            // This is an array member pattern like "Tags.member.1.Key"
70            let entry = map
71                .entry(key.to_string())
72                .or_insert_with(|| Value::Array(Vec::new()));
73            if let Value::Array(arr) = entry {
74                let idx: usize = next.parse::<usize>().unwrap() - 1; // 1-based → 0-based
75                while arr.len() <= idx {
76                    arr.push(Value::Object(serde_json::Map::new()));
77                }
78                if rest.len() > 1 {
79                    if let Value::Object(ref mut inner) = arr[idx] {
80                        set_nested_recursive(inner, &rest[1..], value);
81                    }
82                } else {
83                    arr[idx] = Value::String(value.to_string());
84                }
85            }
86            return;
87        }
88    }
89
90    let entry = map
91        .entry(key.to_string())
92        .or_insert_with(|| Value::Object(serde_json::Map::new()));
93    if let Value::Object(inner) = entry {
94        set_nested_recursive(inner, rest, value);
95    }
96}
97
98/// Serialize a successful awsQuery XML response.
99///
100/// Format:
101/// ```xml
102/// <{Action}Response xmlns="...">
103///   <{Action}Result>
104///     {serialized fields}
105///   </{Action}Result>
106///   <ResponseMetadata>
107///     <RequestId>{request_id}</RequestId>
108///   </ResponseMetadata>
109/// </{Action}Response>
110/// ```
111pub fn serialize_response(
112    operation: &str,
113    output: &Value,
114    request_id: &str,
115) -> (StatusCode, HeaderMap, Bytes) {
116    let result_xml = json_to_xml_fields(output);
117
118    let xml = format!(
119        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
120         <{operation}Response xmlns=\"https://iam.amazonaws.com/doc/2010-05-08/\">\n\
121         <{operation}Result>\n\
122         {result_xml}\
123         </{operation}Result>\n\
124         <ResponseMetadata>\n\
125         <RequestId>{request_id}</RequestId>\n\
126         </ResponseMetadata>\n\
127         </{operation}Response>"
128    );
129
130    let mut headers = HeaderMap::new();
131    headers.insert("content-type", "text/xml".parse().unwrap());
132    headers.insert("x-amzn-requestid", request_id.parse().unwrap());
133    (StatusCode::OK, headers, Bytes::from(xml))
134}
135
136/// Serialize an awsQuery/XML error response.
137pub fn serialize_error(
138    error: &AwsError,
139    request_id: &str,
140) -> (StatusCode, HeaderMap, Bytes) {
141    let error_type = match error.error_type {
142        crate::error::ErrorType::Sender => "Sender",
143        crate::error::ErrorType::Receiver => "Receiver",
144    };
145
146    let xml = format!(
147        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
148         <ErrorResponse xmlns=\"http://iam.amazonaws.com/doc/2010-05-08/\">\n\
149         <Error>\n\
150         <Type>{error_type}</Type>\n\
151         <Code>{code}</Code>\n\
152         <Message>{message}</Message>\n\
153         </Error>\n\
154         <RequestId>{request_id}</RequestId>\n\
155         </ErrorResponse>",
156        code = error.code,
157        message = error.message,
158    );
159
160    let mut headers = HeaderMap::new();
161    headers.insert("content-type", "text/xml".parse().unwrap());
162    headers.insert("x-amzn-requestid", request_id.parse().unwrap());
163    (error.status, headers, Bytes::from(xml))
164}
165
166/// Convert a JSON Value to XML elements.
167pub fn json_to_xml_fields(value: &Value) -> String {
168    match value {
169        Value::Object(map) => {
170            let mut xml = String::new();
171            for (key, val) in map {
172                match val {
173                    Value::Object(_) => {
174                        xml.push_str(&format!(
175                            "<{key}>\n{}</{key}>\n",
176                            json_to_xml_fields(val)
177                        ));
178                    }
179                    Value::Array(arr) => {
180                        for item in arr {
181                            xml.push_str(&format!(
182                                "<{key}>\n{}</{key}>\n",
183                                json_to_xml_fields(item)
184                            ));
185                        }
186                    }
187                    Value::String(s) => {
188                        xml.push_str(&format!("<{key}>{s}</{key}>\n"));
189                    }
190                    Value::Number(n) => {
191                        xml.push_str(&format!("<{key}>{n}</{key}>\n"));
192                    }
193                    Value::Bool(b) => {
194                        xml.push_str(&format!("<{key}>{b}</{key}>\n"));
195                    }
196                    Value::Null => {
197                        xml.push_str(&format!("<{key}/>\n"));
198                    }
199                }
200            }
201            xml
202        }
203        Value::String(s) => s.clone(),
204        Value::Number(n) => n.to_string(),
205        Value::Bool(b) => b.to_string(),
206        _ => String::new(),
207    }
208}
209
210#[cfg(test)]
211mod tests {
212    use super::*;
213
214    #[test]
215    fn test_parse_simple_query() {
216        let body = Bytes::from("Action=GetCallerIdentity&Version=2011-06-15");
217        let result = parse_request(&body).unwrap();
218        assert_eq!(result.operation, "GetCallerIdentity");
219        assert_eq!(result.input, Value::Object(serde_json::Map::new()));
220    }
221
222    #[test]
223    fn test_parse_query_with_params() {
224        let body = Bytes::from("Action=CreateUser&UserName=testuser&Path=/engineering/");
225        let result = parse_request(&body).unwrap();
226        assert_eq!(result.operation, "CreateUser");
227        assert_eq!(result.input["UserName"], "testuser");
228        assert_eq!(result.input["Path"], "/engineering/");
229    }
230
231    #[test]
232    fn test_flatten_dot_notation() {
233        let params = vec![
234            ("Action".to_string(), "TagResource".to_string()),
235            ("Tags.member.1.Key".to_string(), "Env".to_string()),
236            ("Tags.member.1.Value".to_string(), "prod".to_string()),
237            ("Tags.member.2.Key".to_string(), "Team".to_string()),
238            ("Tags.member.2.Value".to_string(), "eng".to_string()),
239        ];
240        let result = flatten_to_json(&params);
241        let tags = result["Tags"]["member"].as_array().unwrap();
242        assert_eq!(tags.len(), 2);
243        assert_eq!(tags[0]["Key"], "Env");
244        assert_eq!(tags[0]["Value"], "prod");
245        assert_eq!(tags[1]["Key"], "Team");
246        assert_eq!(tags[1]["Value"], "eng");
247    }
248}