Skip to content

Commit 98d8567

Browse files
committed
feat: Allow to set custom headers in response
EC2 IMDS includes X-Aws-Ec2-Metadata-Token-Ttl-Seconds header in the response to PUT /latest/api/token. To enable Firecracker MMDS to behave compatibly with EC2 IMDS, allow to set custom headers in HTTP response. Signed-off-by: Takahiro Itazuri <[email protected]>
1 parent 11cc5da commit 98d8567

File tree

3 files changed

+86
-1
lines changed

3 files changed

+86
-1
lines changed

CHANGELOG.md

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -2,10 +2,11 @@
22

33
## Added
44

5-
- Implemented `Eq` for `common::headers::Encoding`, `common::headers::MediaType`,
5+
- Implemented `Eq` for `common::headers::Encoding`, `common::headers::MediaType`,
66
`common::headers::Headers`, `common::HttpHeaderError`, `common::Body`, `common::Version`,
77
`common::RequestError`, `request::Uri`, `request::RequestLine`, `response::StatusCode`,
88
`response::ResponseHeaders`
9+
- Allowed to set custom headers in HTTP responses.
910

1011
## Changed
1112

src/common/mod.rs

Lines changed: 9 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -23,6 +23,8 @@ pub enum HttpHeaderError {
2323
InvalidUtf8String(Utf8Error),
2424
///The value specified is not valid.
2525
InvalidValue(String, String),
26+
/// Non-ASCII character found.
27+
NonAsciiCharacter(String, String),
2628
/// The content length specified is longer than the limit imposed by Micro Http.
2729
SizeLimitExceeded(String),
2830
/// The requested feature is not currently supported.
@@ -45,6 +47,13 @@ impl Display for HttpHeaderError {
4547
Self::InvalidValue(header_name, value) => {
4648
write!(f, "Invalid value. Key:{}; Value:{}", header_name, value)
4749
}
50+
Self::NonAsciiCharacter(header_name, value) => {
51+
write!(
52+
f,
53+
"Non-ASCII character found. Key: {}; Value: {}",
54+
header_name, value
55+
)
56+
}
4857
Self::SizeLimitExceeded(inner) => {
4958
write!(f, "Invalid content length. Header: {}", inner)
5059
}

src/response.rs

Lines changed: 75 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -1,11 +1,13 @@
11
// Copyright 2018 Amazon.com, Inc. or its affiliates. All Rights Reserved.
22
// SPDX-License-Identifier: Apache-2.0
33

4+
use std::collections::HashMap;
45
use std::io::{Error as WriteError, Write};
56

67
use crate::ascii::{COLON, CR, LF, SP};
78
use crate::common::{Body, Version};
89
use crate::headers::{Header, MediaType};
10+
use crate::HttpHeaderError;
911
use crate::Method;
1012

1113
/// Wrapper over a response status code.
@@ -95,6 +97,7 @@ pub struct ResponseHeaders {
9597
server: String,
9698
allow: Vec<Method>,
9799
accept_encoding: bool,
100+
custom_headers: HashMap<String, String>,
98101
}
99102

100103
impl Default for ResponseHeaders {
@@ -106,6 +109,7 @@ impl Default for ResponseHeaders {
106109
server: String::from("Firecracker API"),
107110
allow: Vec::new(),
108111
accept_encoding: false,
112+
custom_headers: HashMap::default(),
109113
}
110114
}
111115
}
@@ -141,6 +145,18 @@ impl ResponseHeaders {
141145
buf.write_all(&[CR, LF])
142146
}
143147

148+
// The logic pertaining to custom headers writing.
149+
fn write_custom_headers<T: Write>(&self, buf: &mut T) -> Result<(), WriteError> {
150+
// Note that all the custom headers have already been validated as US-ASCII.
151+
for (header, value) in &self.custom_headers {
152+
buf.write_all(header.as_bytes())?;
153+
buf.write_all(&[COLON, SP])?;
154+
buf.write_all(value.as_bytes())?;
155+
buf.write_all(&[CR, LF])?;
156+
}
157+
Ok(())
158+
}
159+
144160
/// Writes the headers to `buf` using the HTTP specification.
145161
pub fn write_all<T: Write>(&self, buf: &mut T) -> Result<(), WriteError> {
146162
buf.write_all(Header::Server.raw())?;
@@ -153,6 +169,7 @@ impl ResponseHeaders {
153169

154170
self.write_allow_header(buf)?;
155171
self.write_deprecation_header(buf)?;
172+
self.write_custom_headers(buf)?;
156173

157174
if let Some(content_length) = self.content_length {
158175
buf.write_all(Header::ContentType.raw())?;
@@ -203,6 +220,26 @@ impl ResponseHeaders {
203220
pub fn set_encoding(&mut self) {
204221
self.accept_encoding = true;
205222
}
223+
224+
/// Sets custom headers to be written in the HTTP response.
225+
pub fn set_custom_headers(
226+
&mut self,
227+
custom_headers: &HashMap<String, String>,
228+
) -> Result<(), HttpHeaderError> {
229+
// https://datatracker.ietf.org/doc/html/rfc7230
230+
// HTTP headers MUST be US-ASCII.
231+
if let Some((k, v)) = custom_headers
232+
.iter()
233+
.find(|(k, v)| !k.is_ascii() || !v.is_ascii())
234+
{
235+
return Err(HttpHeaderError::NonAsciiCharacter(
236+
k.to_owned(),
237+
v.to_owned(),
238+
));
239+
}
240+
self.custom_headers = custom_headers.to_owned();
241+
Ok(())
242+
}
206243
}
207244

208245
/// Wrapper over an HTTP Response.
@@ -294,6 +331,19 @@ impl Response {
294331
self.headers.set_server(server);
295332
}
296333

334+
/// Sets the custom headers.
335+
pub fn set_custom_headers(
336+
&mut self,
337+
custom_headers: &HashMap<String, String>,
338+
) -> Result<(), HttpHeaderError> {
339+
self.headers.set_custom_headers(custom_headers)
340+
}
341+
342+
/// Gets a reference to the custom headers.
343+
pub fn custom_headers(&self) -> &HashMap<String, String> {
344+
&self.headers.custom_headers
345+
}
346+
297347
/// Sets the HTTP allowed methods.
298348
pub fn set_allow(&mut self, methods: Vec<Method>) {
299349
self.headers.allow = methods;
@@ -554,4 +604,29 @@ mod tests {
554604
assert!(response.write_all(&mut response_buf.as_mut()).is_ok());
555605
assert_eq!(response_buf.as_ref(), expected_response);
556606
}
607+
608+
#[test]
609+
fn test_custom_headers() {
610+
// Valid custom headers.
611+
let mut response = Response::new(Version::Http10, StatusCode::OK);
612+
let custom_headers = [("Foo".into(), "Bar".into())].into();
613+
response.set_custom_headers(&custom_headers).unwrap();
614+
let expected_response = b"HTTP/1.0 200 \r\n\
615+
Server: Firecracker API\r\n\
616+
Connection: keep-alive\r\n\
617+
Foo: Bar\r\n\
618+
Content-Type: application/json\r\n\
619+
Content-Length: 0\r\n\r\n";
620+
let mut response_buf: [u8; 127] = [0; 127];
621+
response.write_all(&mut response_buf.as_mut()).unwrap();
622+
assert_eq!(response_buf.as_ref(), expected_response);
623+
624+
// Should fail to set custom headers including non-ASCII character.
625+
let mut response = Response::new(Version::Http10, StatusCode::OK);
626+
let custom_headers = [("Greek capital delta".into(), "Δ".into())].into();
627+
assert_eq!(
628+
response.set_custom_headers(&custom_headers).unwrap_err(),
629+
HttpHeaderError::NonAsciiCharacter("Greek capital delta".into(), "Δ".into())
630+
);
631+
}
557632
}

0 commit comments

Comments
 (0)