Skip to main content

opendal_core/raw/http_util/
header.rs

1// Licensed to the Apache Software Foundation (ASF) under one
2// or more contributor license agreements.  See the NOTICE file
3// distributed with this work for additional information
4// regarding copyright ownership.  The ASF licenses this file
5// to you under the Apache License, Version 2.0 (the
6// "License"); you may not use this file except in compliance
7// with the License.  You may obtain a copy of the License at
8//
9//   http://www.apache.org/licenses/LICENSE-2.0
10//
11// Unless required by applicable law or agreed to in writing,
12// software distributed under the License is distributed on an
13// "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
14// KIND, either express or implied.  See the License for the
15// specific language governing permissions and limitations
16// under the License.
17
18use std::collections::HashMap;
19
20use base64::Engine;
21use base64::engine::general_purpose;
22use http::HeaderMap;
23use http::HeaderName;
24use http::HeaderValue;
25use http::header::CACHE_CONTROL;
26use http::header::CONTENT_DISPOSITION;
27use http::header::CONTENT_ENCODING;
28use http::header::CONTENT_LENGTH;
29use http::header::CONTENT_RANGE;
30use http::header::CONTENT_TYPE;
31use http::header::ETAG;
32use http::header::LAST_MODIFIED;
33use http::header::LOCATION;
34use md5::Digest;
35
36use crate::EntryMode;
37use crate::Error;
38use crate::ErrorKind;
39use crate::Metadata;
40use crate::MetadataBuilder;
41use crate::Result;
42use crate::raw::*;
43
44/// Parse redirect location from header map
45///
46/// # Note
47/// The returned value maybe a relative path, like `/index.html`, `/robots.txt`, etc.
48pub fn parse_location(headers: &HeaderMap) -> Result<Option<&str>> {
49    parse_header_to_str(headers, LOCATION)
50}
51
52/// Parse cache control from header map.
53///
54/// # Note
55///
56/// The returned value is the raw string of `cache-control` header,
57/// maybe `no-cache`, `max-age=3600`, etc.
58pub fn parse_cache_control(headers: &HeaderMap) -> Result<Option<&str>> {
59    parse_header_to_str(headers, CACHE_CONTROL)
60}
61
62/// Parse content length from header map.
63pub fn parse_content_length(headers: &HeaderMap) -> Result<Option<u64>> {
64    parse_header_to_str(headers, CONTENT_LENGTH)?
65        .map(|v| {
66            v.parse::<u64>().map_err(|e| {
67                Error::new(ErrorKind::Unexpected, "header value is not valid integer").set_source(e)
68            })
69        })
70        .transpose()
71}
72
73/// Parse content md5 from header map.
74pub fn parse_content_md5(headers: &HeaderMap) -> Result<Option<&str>> {
75    parse_header_to_str(headers, "content-md5")
76}
77
78/// Parse content type from header map.
79pub fn parse_content_type(headers: &HeaderMap) -> Result<Option<&str>> {
80    parse_header_to_str(headers, CONTENT_TYPE)
81}
82
83/// Parse content encoding from header map.
84pub fn parse_content_encoding(headers: &HeaderMap) -> Result<Option<&str>> {
85    parse_header_to_str(headers, CONTENT_ENCODING)
86}
87
88/// Parse content range from header map.
89pub fn parse_content_range(headers: &HeaderMap) -> Result<Option<BytesContentRange>> {
90    parse_header_to_str(headers, CONTENT_RANGE)?
91        .map(|v| v.parse())
92        .transpose()
93}
94
95/// Parse last modified from header map.
96pub fn parse_last_modified(headers: &HeaderMap) -> Result<Option<Timestamp>> {
97    parse_header_to_str(headers, LAST_MODIFIED)?
98        .map(Timestamp::parse_rfc2822)
99        .transpose()
100}
101
102/// Parse etag from header map.
103pub fn parse_etag(headers: &HeaderMap) -> Result<Option<&str>> {
104    parse_header_to_str(headers, ETAG)
105}
106
107/// Parse Content-Disposition for header map
108pub fn parse_content_disposition(headers: &HeaderMap) -> Result<Option<&str>> {
109    parse_header_to_str(headers, CONTENT_DISPOSITION)
110}
111
112/// Parse multipart boundary from header map.
113pub fn parse_multipart_boundary(headers: &HeaderMap) -> Result<Option<&str>> {
114    parse_header_to_str(headers, CONTENT_TYPE).map(|v| v.and_then(|v| v.split("boundary=").nth(1)))
115}
116
117/// Parse header value to string according to name.
118#[inline]
119pub fn parse_header_to_str<K>(headers: &HeaderMap, name: K) -> Result<Option<&str>>
120where
121    HeaderName: TryFrom<K>,
122{
123    let name = HeaderName::try_from(name).map_err(|_| {
124        Error::new(
125            ErrorKind::Unexpected,
126            "header name must be valid http header name but not",
127        )
128        .with_operation("http_util::parse_header_to_str")
129    })?;
130
131    let value = if let Some(v) = headers.get(&name) {
132        v
133    } else {
134        return Ok(None);
135    };
136
137    Ok(Some(value.to_str().map_err(|e| {
138        Error::new(
139            ErrorKind::Unexpected,
140            "header value must be valid utf-8 string but not",
141        )
142        .with_operation("http_util::parse_header_to_str")
143        .with_context("header_name", name.as_str())
144        .set_source(e)
145    })?))
146}
147
148/// parse_into_metadata will parse standards http headers into Metadata.
149///
150/// # Notes
151///
152/// parse_into_metadata only handles the standard behavior of http
153/// headers. If services have their own logic, they should update the parsed
154/// metadata on demand.
155pub fn parse_into_metadata(path: &str, headers: &HeaderMap) -> Result<Metadata> {
156    let mode = if path.ends_with('/') {
157        EntryMode::DIR
158    } else {
159        EntryMode::FILE
160    };
161    let content_length = parse_content_range(headers)?
162        .and_then(|value| value.size())
163        .or(parse_content_length(headers)?);
164    let mut m = if mode == EntryMode::FILE {
165        MetadataBuilder::file(content_length.ok_or_else(|| {
166            Error::new(
167                ErrorKind::Unexpected,
168                "HTTP response does not contain file content length",
169            )
170        })?)
171    } else {
172        MetadataBuilder::dir()
173    };
174
175    if let Some(v) = parse_cache_control(headers)? {
176        m.cache_control(v);
177    }
178
179    if let Some(v) = parse_content_type(headers)? {
180        m.content_type(v);
181    }
182
183    if let Some(v) = parse_content_encoding(headers)? {
184        m.content_encoding(v);
185    }
186
187    if let Some(v) = parse_etag(headers)? {
188        m.etag(v);
189    }
190
191    if let Some(v) = parse_content_md5(headers)? {
192        m.content_md5(v);
193    }
194
195    if let Some(v) = parse_last_modified(headers)? {
196        m.last_modified(v);
197    }
198
199    if let Some(v) = parse_content_disposition(headers)? {
200        m.content_disposition(v);
201    }
202
203    Ok(m.build())
204}
205
206/// Parse prefixed headers and return a map with the prefix of each header removed.
207pub fn parse_prefixed_headers(headers: &HeaderMap, prefix: &str) -> HashMap<String, String> {
208    headers
209        .iter()
210        .filter_map(|(name, value)| {
211            name.as_str().strip_prefix(prefix).and_then(|stripped_key| {
212                value
213                    .to_str()
214                    .ok()
215                    .map(|parsed_value| (stripped_key.to_string(), parsed_value.to_string()))
216            })
217        })
218        .collect()
219}
220
221/// format content md5 header by given input.
222pub fn format_content_md5(bs: &[u8]) -> String {
223    let mut hasher = md5::Md5::new();
224    hasher.update(bs);
225
226    general_purpose::STANDARD.encode(hasher.finalize())
227}
228
229/// format content md5 header by given iter of bytes.
230pub fn format_content_md5_iter<I>(bs: I) -> String
231where
232    I: IntoIterator,
233    I::Item: AsRef<[u8]>,
234{
235    let mut hasher = md5::Md5::new();
236
237    for b in bs {
238        hasher.update(b.as_ref());
239    }
240
241    general_purpose::STANDARD.encode(hasher.finalize())
242}
243
244/// format authorization header by basic auth.
245///
246/// # Errors
247///
248/// If input username is empty, function will return an unexpected error.
249pub fn format_authorization_by_basic(username: &str, password: &str) -> Result<String> {
250    if username.is_empty() {
251        return Err(Error::new(
252            ErrorKind::Unexpected,
253            "can't build authorization header with empty username",
254        ));
255    }
256
257    let value = general_purpose::STANDARD.encode(format!("{username}:{password}"));
258
259    Ok(format!("Basic {value}"))
260}
261
262/// format authorization header by bearer token.
263///
264/// # Errors
265///
266/// If input token is empty, function will return an unexpected error.
267pub fn format_authorization_by_bearer(token: &str) -> Result<String> {
268    if token.is_empty() {
269        return Err(Error::new(
270            ErrorKind::Unexpected,
271            "can't build authorization header with empty token",
272        ));
273    }
274
275    Ok(format!("Bearer {token}"))
276}
277
278/// Build header value from given string.
279pub fn build_header_value(v: &str) -> Result<HeaderValue> {
280    HeaderValue::from_str(v).map_err(|e| {
281        Error::new(
282            ErrorKind::ConfigInvalid,
283            "header value contains invalid characters",
284        )
285        .with_operation("http_util::build_header_value")
286        .set_source(e)
287    })
288}
289
290#[cfg(test)]
291mod tests {
292    use super::*;
293
294    /// Test cases is from https://docs.aws.amazon.com/AmazonS3/latest/API/API_DeleteObjects.html
295    #[test]
296    fn test_format_content_md5() {
297        let cases = vec![(
298            r#"<Delete>
299<Object>
300 <Key>sample1.txt</Key>
301 </Object>
302 <Object>
303   <Key>sample2.txt</Key>
304 </Object>
305 </Delete>"#,
306            "WOctCY1SS662e7ziElh4cw==",
307        )];
308
309        for (input, expected) in cases {
310            let actual = format_content_md5(input.as_bytes());
311
312            assert_eq!(actual, expected)
313        }
314    }
315
316    /// Test cases is borrowed from
317    ///
318    /// - RFC2617: https://datatracker.ietf.org/doc/html/rfc2617#section-2
319    /// - MDN: https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Authorization
320    #[test]
321    fn test_format_authorization_by_basic() {
322        let cases = vec![
323            ("aladdin", "opensesame", "Basic YWxhZGRpbjpvcGVuc2VzYW1l"),
324            ("aladdin", "", "Basic YWxhZGRpbjo="),
325            (
326                "Aladdin",
327                "open sesame",
328                "Basic QWxhZGRpbjpvcGVuIHNlc2FtZQ==",
329            ),
330            ("Aladdin", "", "Basic QWxhZGRpbjo="),
331        ];
332
333        for (username, password, expected) in cases {
334            let actual =
335                format_authorization_by_basic(username, password).expect("format must success");
336
337            assert_eq!(actual, expected)
338        }
339    }
340
341    /// Test cases is borrowed from
342    ///
343    /// - RFC6750: https://datatracker.ietf.org/doc/html/rfc6750
344    #[test]
345    fn test_format_authorization_by_bearer() {
346        let cases = vec![("mF_9.B5f-4.1JqM", "Bearer mF_9.B5f-4.1JqM")];
347
348        for (token, expected) in cases {
349            let actual = format_authorization_by_bearer(token).expect("format must success");
350
351            assert_eq!(actual, expected)
352        }
353    }
354
355    #[test]
356    fn test_parse_multipart_boundary() {
357        let cases = vec![
358            (
359                "multipart/mixed; boundary=gc0p4Jq0M2Yt08jU534c0p",
360                Some("gc0p4Jq0M2Yt08jU534c0p"),
361            ),
362            ("multipart/mixed", None),
363        ];
364
365        for (input, expected) in cases {
366            let mut headers = HeaderMap::new();
367            headers.insert(CONTENT_TYPE, HeaderValue::from_str(input).unwrap());
368
369            let actual = parse_multipart_boundary(&headers).expect("parse must success");
370
371            assert_eq!(actual, expected)
372        }
373    }
374}