static_files_module/
range.rs

1// Copyright 2024 Wladimir Palant
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15//! Byte range processing (`Range` HTTP header)
16
17use http::header;
18use pingora_proxy::Session;
19use std::str::FromStr;
20
21use crate::metadata::Metadata;
22
23/// Represents the result of parsing the `Range` HTTP header.
24#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum Range {
26    /// A valid range with the given start and end bounds
27    Valid(u64, u64),
28    /// A range that is outside of the file’s boundaries
29    OutOfBounds,
30}
31
32impl Range {
33    /// Parses the value of a `Range` HTTP header. The file size is required to resolve ranges
34    /// specified relative to the end of file and to recognize out of bounds ranges. Ranges that
35    /// cannot be parsed (unexpected format) will result in `None`.
36    pub fn parse(range: &str, file_size: u64) -> Option<Self> {
37        let (units, range) = range.split_once('=')?;
38        if units != "bytes" {
39            return None;
40        }
41
42        let (start, end) = range.trim().split_once('-')?;
43        let (start, end) = if start.is_empty() {
44            let len = u64::from_str(end.trim()).ok()?;
45            if len > file_size {
46                return Some(Self::OutOfBounds);
47            }
48            (file_size - len, file_size - 1)
49        } else if end.is_empty() {
50            (u64::from_str(start.trim()).ok()?, file_size - 1)
51        } else {
52            (
53                u64::from_str(start.trim()).ok()?,
54                u64::from_str(end.trim()).ok()?,
55            )
56        };
57
58        if end >= file_size || start > end {
59            Some(Self::OutOfBounds)
60        } else {
61            Some(Self::Valid(start, end))
62        }
63    }
64}
65
66/// This processes the `Range` and `If-Range` request headers to produce the requested byte range
67/// if any.
68///
69/// `Range` header missing, using some unsupported format or overruled by `If-Range` header will
70/// all result in `None` being returned.
71///
72/// Note: Multiple ranges are not supported.
73pub fn extract_range(session: &Session, meta: &Metadata) -> Option<Range> {
74    let headers = &session.req_header().headers;
75    if let Some(value) = headers
76        .get(header::IF_RANGE)
77        .and_then(|value| value.to_str().ok())
78    {
79        if value != meta.etag
80            && !meta
81                .modified
82                .as_ref()
83                .is_some_and(|modified| modified == value)
84        {
85            return None;
86        }
87    }
88
89    let value = headers.get(header::RANGE)?;
90    let value = value.to_str().ok()?;
91
92    Range::parse(value, meta.size)
93}
94
95#[cfg(test)]
96mod tests {
97    use super::*;
98
99    use mime_guess::MimeGuess;
100    use test_log::test;
101    use tokio_test::io::Builder;
102
103    fn metadata() -> Metadata {
104        Metadata {
105            mime: MimeGuess::from_ext("txt"),
106            size: 1000,
107            modified: Some("Fri, 15 May 2015 15:34:21 GMT".into()),
108            etag: "\"abc\"".into(),
109        }
110    }
111
112    async fn make_session(range: &str, if_range: &str) -> Session {
113        let mut mock = Builder::new();
114
115        mock.read(b"GET / HTTP/1.1\r\n");
116        mock.read(b"Connection: close\r\n");
117        if !range.is_empty() {
118            mock.read(format!("Range: {range}\r\n").as_bytes());
119        }
120        if !if_range.is_empty() {
121            mock.read(format!("If-Range: {if_range}\r\n").as_bytes());
122        }
123        mock.read(b"\r\n");
124
125        let mut session = Session::new_h1(Box::new(mock.build()));
126        assert!(session.read_request().await.unwrap());
127        session
128    }
129
130    #[test(tokio::test)]
131    async fn no_range() {
132        let session = make_session("", "").await;
133        assert_eq!(extract_range(&session, &metadata()), None);
134    }
135
136    #[test(tokio::test)]
137    async fn valid_range() {
138        let session = make_session("bytes=0-499", "").await;
139        assert_eq!(
140            extract_range(&session, &metadata()),
141            Some(Range::Valid(0, 499))
142        );
143    }
144
145    #[test(tokio::test)]
146    async fn unknown_units() {
147        let session = make_session("eur=0-499", "").await;
148        assert_eq!(extract_range(&session, &metadata()), None);
149    }
150
151    #[test(tokio::test)]
152    async fn open_range() {
153        let session = make_session("bytes=500-", "").await;
154        assert_eq!(
155            extract_range(&session, &metadata()),
156            Some(Range::Valid(500, 999))
157        );
158    }
159
160    #[test(tokio::test)]
161    async fn end_range() {
162        let session = make_session("bytes=-10", "").await;
163        assert_eq!(
164            extract_range(&session, &metadata()),
165            Some(Range::Valid(990, 999))
166        );
167    }
168
169    #[test(tokio::test)]
170    async fn out_of_bounds_ranges() {
171        let session = make_session("bytes=-2000", "").await;
172        assert_eq!(
173            extract_range(&session, &metadata()),
174            Some(Range::OutOfBounds)
175        );
176
177        let session = make_session("bytes=23-22", "").await;
178        assert_eq!(
179            extract_range(&session, &metadata()),
180            Some(Range::OutOfBounds)
181        );
182
183        let session = make_session("bytes=1000-", "").await;
184        assert_eq!(
185            extract_range(&session, &metadata()),
186            Some(Range::OutOfBounds)
187        );
188    }
189
190    #[test(tokio::test)]
191    async fn multiple_ranges() {
192        // Multiple ranges are unsupported, should be treated like no Range header.
193        let session = make_session("bytes=1-2,3-4", "").await;
194        assert_eq!(extract_range(&session, &metadata()), None);
195    }
196
197    #[test(tokio::test)]
198    async fn if_range() {
199        let session = make_session("bytes=0-499", "\"abc\"").await;
200        assert_eq!(
201            extract_range(&session, &metadata()),
202            Some(Range::Valid(0, 499))
203        );
204
205        let session = make_session("bytes=0-499", "\"xyz\"").await;
206        assert_eq!(extract_range(&session, &metadata()), None);
207
208        let session = make_session("bytes=0-499", "Fri, 15 May 2015 15:34:21 GMT").await;
209        assert_eq!(
210            extract_range(&session, &metadata()),
211            Some(Range::Valid(0, 499))
212        );
213
214        let session = make_session("bytes=0-499", "Thu, 01 Jan 1970 00:00:00 GMT").await;
215        assert_eq!(extract_range(&session, &metadata()), None);
216
217        let session = make_session("bytes=0-499", "bogus").await;
218        assert_eq!(extract_range(&session, &metadata()), None);
219    }
220}