Skip to main content

rustack_sts_http/
response.rs

1//! STS XML response formatting and error serialization.
2//!
3//! STS responses use `text/xml` content type following the awsQuery protocol.
4//! All STS responses follow the pattern:
5//!
6//! ```xml
7//! <{Operation}Response xmlns="https://sts.amazonaws.com/doc/2011-06-15/">
8//!   <{Operation}Result>
9//!     ...fields...
10//!   </{Operation}Result>
11//!   <ResponseMetadata>
12//!     <RequestId>{uuid}</RequestId>
13//!   </ResponseMetadata>
14//! </{Operation}Response>
15//! ```
16
17use rustack_sts_model::error::StsError;
18
19use crate::body::StsResponseBody;
20
21/// Content type for STS XML responses.
22pub const CONTENT_TYPE: &str = "text/xml";
23
24/// The STS XML namespace.
25const XML_NS: &str = "https://sts.amazonaws.com/doc/2011-06-15/";
26
27/// Build a success XML response with the given body and request ID.
28#[must_use]
29pub fn xml_response(xml: String, request_id: &str) -> http::Response<StsResponseBody> {
30    let body = StsResponseBody::from_xml(xml.into_bytes());
31    http::Response::builder()
32        .status(http::StatusCode::OK)
33        .header("content-type", CONTENT_TYPE)
34        .header("x-amzn-requestid", request_id)
35        .body(body)
36        .expect("valid XML response")
37}
38
39/// Serialize an STS error into an XML error response body string.
40#[must_use]
41pub fn error_to_xml(error: &StsError, request_id: &str) -> String {
42    format!(
43        "<ErrorResponse \
44         xmlns=\"{XML_NS}\"><Error><Type>{}</Type><Code>{}</Code><Message>{}</Message></\
45         Error><RequestId>{}</RequestId></ErrorResponse>",
46        error.code.fault(),
47        error.code.code(),
48        xml_escape(&error.message),
49        xml_escape(request_id),
50    )
51}
52
53/// Convert an `StsError` into a complete HTTP error response.
54#[must_use]
55pub fn error_to_response(error: &StsError, request_id: &str) -> http::Response<StsResponseBody> {
56    let xml = error_to_xml(error, request_id);
57    let body = StsResponseBody::from_xml(xml.into_bytes());
58    http::Response::builder()
59        .status(error.status_code)
60        .header("content-type", CONTENT_TYPE)
61        .header("x-amzn-requestid", request_id)
62        .body(body)
63        .expect("valid error response")
64}
65
66/// XML-escape a string value.
67///
68/// Replaces the five XML special characters with their entity references.
69#[must_use]
70pub fn xml_escape(s: &str) -> String {
71    if !s.contains(['&', '<', '>', '"', '\'']) {
72        return s.to_owned();
73    }
74
75    let mut result = String::with_capacity(s.len() + 16);
76    for ch in s.chars() {
77        match ch {
78            '&' => result.push_str("&amp;"),
79            '<' => result.push_str("&lt;"),
80            '>' => result.push_str("&gt;"),
81            '"' => result.push_str("&quot;"),
82            '\'' => result.push_str("&apos;"),
83            _ => result.push(ch),
84        }
85    }
86    result
87}
88
89/// Simple XML writer for building STS response XML.
90#[derive(Debug)]
91pub struct XmlWriter {
92    buf: String,
93}
94
95impl Default for XmlWriter {
96    fn default() -> Self {
97        Self::new()
98    }
99}
100
101impl XmlWriter {
102    /// Create a new `XmlWriter`.
103    #[must_use]
104    pub fn new() -> Self {
105        Self {
106            buf: String::with_capacity(512),
107        }
108    }
109
110    /// Start the response envelope: `<{op}Response xmlns="...">`.
111    pub fn start_response(&mut self, operation: &str) {
112        self.buf.push('<');
113        self.buf.push_str(operation);
114        self.buf.push_str("Response xmlns=\"");
115        self.buf.push_str(XML_NS);
116        self.buf.push_str("\">");
117    }
118
119    /// Start the result element: `<{op}Result>`.
120    pub fn start_result(&mut self, operation: &str) {
121        self.buf.push('<');
122        self.buf.push_str(operation);
123        self.buf.push_str("Result>");
124    }
125
126    /// End an element: `</{name}>`.
127    pub fn end_element(&mut self, name: &str) {
128        self.buf.push_str("</");
129        self.buf.push_str(name);
130        self.buf.push('>');
131    }
132
133    /// Write a simple text element: `<Name>Value</Name>`.
134    pub fn write_element(&mut self, name: &str, value: &str) {
135        self.buf.push('<');
136        self.buf.push_str(name);
137        self.buf.push('>');
138        self.buf.push_str(&xml_escape(value));
139        self.buf.push_str("</");
140        self.buf.push_str(name);
141        self.buf.push('>');
142    }
143
144    /// Write an optional element (skip if `None`).
145    pub fn write_optional_element(&mut self, name: &str, value: Option<&str>) {
146        if let Some(v) = value {
147            self.write_element(name, v);
148        }
149    }
150
151    /// Write an integer element.
152    pub fn write_int_element(&mut self, name: &str, value: i32) {
153        self.write_element(name, &value.to_string());
154    }
155
156    /// Write raw XML content without escaping.
157    pub fn raw(&mut self, s: &str) {
158        self.buf.push_str(s);
159    }
160
161    /// Write the `<ResponseMetadata>` block with a `<RequestId>`.
162    pub fn write_response_metadata(&mut self, request_id: &str) {
163        self.buf.push_str("<ResponseMetadata>");
164        self.write_element("RequestId", request_id);
165        self.buf.push_str("</ResponseMetadata>");
166    }
167
168    /// Consume the writer and return the final XML string.
169    #[must_use]
170    pub fn into_string(self) -> String {
171        self.buf
172    }
173}
174
175#[cfg(test)]
176mod tests {
177    use super::*;
178
179    #[test]
180    fn test_should_escape_xml_special_chars() {
181        assert_eq!(xml_escape("hello"), "hello");
182        assert_eq!(xml_escape("a & b"), "a &amp; b");
183        assert_eq!(xml_escape("<tag>"), "&lt;tag&gt;");
184    }
185
186    #[test]
187    fn test_should_format_error_xml() {
188        let err = StsError::invalid_parameter_value("Bad value");
189        let xml = error_to_xml(&err, "req-123");
190        assert!(xml.contains("<Code>InvalidParameterValue</Code>"));
191        assert!(xml.contains("<Message>Bad value</Message>"));
192        assert!(xml.contains("<Type>Sender</Type>"));
193        assert!(xml.contains("<RequestId>req-123</RequestId>"));
194        assert!(xml.contains("sts.amazonaws.com"));
195    }
196
197    #[test]
198    fn test_should_build_error_response_with_correct_status() {
199        let err = StsError::invalid_parameter_value("bad");
200        let resp = error_to_response(&err, "test-req-123");
201        assert_eq!(resp.status(), http::StatusCode::BAD_REQUEST);
202        assert_eq!(resp.headers().get("content-type").unwrap(), CONTENT_TYPE);
203    }
204
205    #[test]
206    fn test_should_build_xml_with_writer() {
207        let mut w = XmlWriter::new();
208        w.start_response("GetCallerIdentity");
209        w.start_result("GetCallerIdentity");
210        w.write_element("Account", "000000000000");
211        w.write_element("Arn", "arn:aws:iam::000000000000:root");
212        w.write_element("UserId", "000000000000");
213        w.end_element("GetCallerIdentityResult");
214        w.write_response_metadata("req-789");
215        w.end_element("GetCallerIdentityResponse");
216        let xml = w.into_string();
217        assert!(xml.contains("<Account>000000000000</Account>"));
218        assert!(xml.contains("<RequestId>req-789</RequestId>"));
219        assert!(xml.contains("GetCallerIdentityResponse xmlns="));
220    }
221
222    #[test]
223    fn test_should_build_success_xml_response() {
224        let resp = xml_response("<TestResponse/>".to_owned(), "req-success");
225        assert_eq!(resp.status(), http::StatusCode::OK);
226        assert_eq!(resp.headers().get("content-type").unwrap(), CONTENT_TYPE);
227    }
228}