static_files_module/
range.rs1use http::header;
18use pingora_proxy::Session;
19use std::str::FromStr;
20
21use crate::metadata::Metadata;
22
23#[derive(Debug, Clone, Copy, PartialEq)]
25pub enum Range {
26 Valid(u64, u64),
28 OutOfBounds,
30}
31
32impl Range {
33 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
66pub 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 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}