Skip to main content

ruma_common/
http_headers.rs

1//! Helpers for HTTP headers.
2
3use std::borrow::Cow;
4
5use http::{HeaderValue, header::HeaderName};
6use web_time::{Duration, SystemTime, UNIX_EPOCH};
7
8mod content_disposition;
9mod rfc8187;
10
11pub use self::content_disposition::{
12    ContentDisposition, ContentDispositionParseError, ContentDispositionType, TokenString,
13    TokenStringParseError,
14};
15use crate::api::error::{HeaderDeserializationError, HeaderSerializationError};
16
17/// The `application/json` media type as a [`HeaderValue`].
18pub const APPLICATION_JSON: HeaderValue = HeaderValue::from_static("application/json");
19
20/// The `application/octet-stream` media type as a [`HeaderValue`].
21pub const APPLICATION_OCTET_STREAM: HeaderValue =
22    HeaderValue::from_static("application/octet-stream");
23
24/// The `text/plain` media type as a [`HeaderValue`].
25pub const TEXT_PLAIN: HeaderValue = HeaderValue::from_static("text/plain");
26
27/// The `text/html; charset=utf-8` media type as a [`HeaderValue`].
28pub const TEXT_HTML_UTF8: HeaderValue = HeaderValue::from_static("text/html; charset=utf-8");
29
30/// The [`Cross-Origin-Resource-Policy`] HTTP response header.
31///
32/// [`Cross-Origin-Resource-Policy`]: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cross-Origin-Resource-Policy
33pub const CROSS_ORIGIN_RESOURCE_POLICY: HeaderName =
34    HeaderName::from_static("cross-origin-resource-policy");
35
36/// Whether the given byte is a [`token` char].
37///
38/// [`token` char]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
39pub const fn is_tchar(b: u8) -> bool {
40    b.is_ascii_alphanumeric()
41        || matches!(
42            b,
43            b'!' | b'#'
44                | b'$'
45                | b'%'
46                | b'&'
47                | b'\''
48                | b'*'
49                | b'+'
50                | b'-'
51                | b'.'
52                | b'^'
53                | b'_'
54                | b'`'
55                | b'|'
56                | b'~'
57        )
58}
59
60/// Whether the given bytes slice is a [`token`].
61///
62/// [`token`]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
63pub fn is_token(bytes: &[u8]) -> bool {
64    bytes.iter().all(|b| is_tchar(*b))
65}
66
67/// Whether the given string is a [`token`].
68///
69/// [`token`]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.2
70pub fn is_token_string(s: &str) -> bool {
71    is_token(s.as_bytes())
72}
73
74/// Whether the given char is a [visible US-ASCII char].
75///
76/// [visible US-ASCII char]: https://datatracker.ietf.org/doc/html/rfc5234#appendix-B.1
77pub const fn is_vchar(c: char) -> bool {
78    matches!(c, '\x21'..='\x7E')
79}
80
81/// Whether the given char is in the US-ASCII character set and allowed inside a [quoted string].
82///
83/// Contrary to the definition of quoted strings, this doesn't allow `obs-text` characters, i.e.
84/// non-US-ASCII characters, as we usually deal with UTF-8 strings rather than ISO-8859-1 strings.
85///
86/// [quoted string]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.4
87pub const fn is_ascii_string_quotable(c: char) -> bool {
88    is_vchar(c) || matches!(c, '\x09' | '\x20')
89}
90
91/// Remove characters that do not pass [`is_ascii_string_quotable()`] from the given string.
92///
93/// [quoted string]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.4
94pub fn sanitize_for_ascii_quoted_string(value: &str) -> Cow<'_, str> {
95    if value.chars().all(is_ascii_string_quotable) {
96        return Cow::Borrowed(value);
97    }
98
99    Cow::Owned(value.chars().filter(|c| is_ascii_string_quotable(*c)).collect())
100}
101
102/// If the US-ASCII field value does not contain only token chars, convert it to a [quoted string].
103///
104/// The string should be sanitized with [`sanitize_for_ascii_quoted_string()`] or should only
105/// contain characters that pass [`is_ascii_string_quotable()`].
106///
107/// [quoted string]: https://datatracker.ietf.org/doc/html/rfc9110#section-5.6.4
108pub fn quote_ascii_string_if_required(value: &str) -> Cow<'_, str> {
109    if !value.is_empty() && is_token_string(value) {
110        return Cow::Borrowed(value);
111    }
112
113    let value = value.replace('\\', r#"\\"#).replace('"', r#"\""#);
114    Cow::Owned(format!("\"{value}\""))
115}
116
117/// Removes the escape backslashes in the given string.
118pub fn unescape_string(s: &str) -> String {
119    let mut is_escaped = false;
120
121    s.chars()
122        .filter(|c| {
123            is_escaped = *c == '\\' && !is_escaped;
124            !is_escaped
125        })
126        .collect()
127}
128
129/// Convert as `SystemTime` to a HTTP date header value.
130pub fn system_time_to_http_date(
131    time: &SystemTime,
132) -> Result<HeaderValue, HeaderSerializationError> {
133    let mut buffer = [0; 29];
134
135    let duration =
136        time.duration_since(UNIX_EPOCH).map_err(|_| HeaderSerializationError::InvalidHttpDate)?;
137    date_header::format(duration.as_secs(), &mut buffer)
138        .map_err(|_| HeaderSerializationError::InvalidHttpDate)?;
139
140    Ok(HeaderValue::from_bytes(&buffer).expect("date_header should produce a valid header value"))
141}
142
143/// Convert a header value representing a HTTP date to a `SystemTime`.
144pub fn http_date_to_system_time(
145    value: &HeaderValue,
146) -> Result<SystemTime, HeaderDeserializationError> {
147    let bytes = value.as_bytes();
148
149    let ts = date_header::parse(bytes).map_err(|_| HeaderDeserializationError::InvalidHttpDate)?;
150
151    UNIX_EPOCH
152        .checked_add(Duration::from_secs(ts))
153        .ok_or(HeaderDeserializationError::InvalidHttpDate)
154}