rustack_sts_http/
response.rs1use rustack_sts_model::error::StsError;
18
19use crate::body::StsResponseBody;
20
21pub const CONTENT_TYPE: &str = "text/xml";
23
24const XML_NS: &str = "https://sts.amazonaws.com/doc/2011-06-15/";
26
27#[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#[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#[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#[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("&"),
79 '<' => result.push_str("<"),
80 '>' => result.push_str(">"),
81 '"' => result.push_str("""),
82 '\'' => result.push_str("'"),
83 _ => result.push(ch),
84 }
85 }
86 result
87}
88
89#[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 #[must_use]
104 pub fn new() -> Self {
105 Self {
106 buf: String::with_capacity(512),
107 }
108 }
109
110 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 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 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 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 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 pub fn write_int_element(&mut self, name: &str, value: i32) {
153 self.write_element(name, &value.to_string());
154 }
155
156 pub fn raw(&mut self, s: &str) {
158 self.buf.push_str(s);
159 }
160
161 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 #[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 & b");
183 assert_eq!(xml_escape("<tag>"), "<tag>");
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}