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        && next.parse::<usize>().is_ok()
69    {
70        // This is an array member pattern like "Tags.member.1.Key"
71        let entry = map
72            .entry(key.to_string())
73            .or_insert_with(|| Value::Array(Vec::new()));
74        if let Value::Array(arr) = entry {
75            let idx: usize = next.parse::<usize>().unwrap() - 1; // 1-based → 0-based
76            while arr.len() <= idx {
77                arr.push(Value::Object(serde_json::Map::new()));
78            }
79            if rest.len() > 1 {
80                if let Value::Object(ref mut inner) = arr[idx] {
81                    set_nested_recursive(inner, &rest[1..], value);
82                }
83            } else {
84                arr[idx] = Value::String(value.to_string());
85            }
86        }
87        return;
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(error: &AwsError, request_id: &str) -> (StatusCode, HeaderMap, Bytes) {
138    let error_type = match error.error_type {
139        crate::error::ErrorType::Sender => "Sender",
140        crate::error::ErrorType::Receiver => "Receiver",
141    };
142
143    let xml = format!(
144        "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n\
145         <ErrorResponse xmlns=\"http://iam.amazonaws.com/doc/2010-05-08/\">\n\
146         <Error>\n\
147         <Type>{error_type}</Type>\n\
148         <Code>{code}</Code>\n\
149         <Message>{message}</Message>\n\
150         </Error>\n\
151         <RequestId>{request_id}</RequestId>\n\
152         </ErrorResponse>",
153        code = error.code,
154        message = error.message,
155    );
156
157    let mut headers = HeaderMap::new();
158    headers.insert("content-type", "text/xml".parse().unwrap());
159    headers.insert("x-amzn-requestid", request_id.parse().unwrap());
160    (error.status, headers, Bytes::from(xml))
161}
162
163/// Convert a JSON Value to XML elements.
164pub fn json_to_xml_fields(value: &Value) -> String {
165    match value {
166        Value::Object(map) => {
167            let mut xml = String::new();
168            for (key, val) in map {
169                match val {
170                    Value::Object(_) => {
171                        xml.push_str(&format!("<{key}>\n{}</{key}>\n", json_to_xml_fields(val)));
172                    }
173                    Value::Array(arr) => {
174                        for item in arr {
175                            xml.push_str(&format!(
176                                "<{key}>\n{}</{key}>\n",
177                                json_to_xml_fields(item)
178                            ));
179                        }
180                    }
181                    Value::String(s) => {
182                        xml.push_str(&format!("<{key}>{s}</{key}>\n"));
183                    }
184                    Value::Number(n) => {
185                        xml.push_str(&format!("<{key}>{n}</{key}>\n"));
186                    }
187                    Value::Bool(b) => {
188                        xml.push_str(&format!("<{key}>{b}</{key}>\n"));
189                    }
190                    Value::Null => {
191                        xml.push_str(&format!("<{key}/>\n"));
192                    }
193                }
194            }
195            xml
196        }
197        Value::String(s) => s.clone(),
198        Value::Number(n) => n.to_string(),
199        Value::Bool(b) => b.to_string(),
200        _ => String::new(),
201    }
202}
203
204#[cfg(test)]
205mod tests {
206    use super::*;
207
208    #[test]
209    fn test_parse_simple_query() {
210        let body = Bytes::from("Action=GetCallerIdentity&Version=2011-06-15");
211        let result = parse_request(&body).unwrap();
212        assert_eq!(result.operation, "GetCallerIdentity");
213        assert_eq!(result.input, Value::Object(serde_json::Map::new()));
214    }
215
216    #[test]
217    fn test_parse_query_with_params() {
218        let body = Bytes::from("Action=CreateUser&UserName=testuser&Path=/engineering/");
219        let result = parse_request(&body).unwrap();
220        assert_eq!(result.operation, "CreateUser");
221        assert_eq!(result.input["UserName"], "testuser");
222        assert_eq!(result.input["Path"], "/engineering/");
223    }
224
225    #[test]
226    fn test_flatten_dot_notation() {
227        let params = vec![
228            ("Action".to_string(), "TagResource".to_string()),
229            ("Tags.member.1.Key".to_string(), "Env".to_string()),
230            ("Tags.member.1.Value".to_string(), "prod".to_string()),
231            ("Tags.member.2.Key".to_string(), "Team".to_string()),
232            ("Tags.member.2.Value".to_string(), "eng".to_string()),
233        ];
234        let result = flatten_to_json(&params);
235        let tags = result["Tags"]["member"].as_array().unwrap();
236        assert_eq!(tags.len(), 2);
237        assert_eq!(tags[0]["Key"], "Env");
238        assert_eq!(tags[0]["Value"], "prod");
239        assert_eq!(tags[1]["Key"], "Team");
240        assert_eq!(tags[1]["Value"], "eng");
241    }
242}