compute_file_server/lib.rs
1use fastly::{http::Method, Body, Error, ObjectStore, Request, Response};
2use http::HeaderMap;
3use http_range::HttpRange;
4use serde_derive::Deserialize;
5use serde_derive::Serialize;
6use serde_json;
7
8#[derive(Default, Debug, Clone, PartialEq, Serialize, Deserialize)]
9#[serde(rename_all = "camelCase")]
10struct Metadata {
11 #[serde(rename = "ETag")]
12 etag: String,
13 #[serde(rename = "Last-Modified")]
14 last_modified: String,
15 #[serde(rename = "Content-Type")]
16 content_type: Option<String>,
17}
18
19pub fn get(store_name: &str, request: Request) -> Result<Option<Response>, Error> {
20 let method = request.get_method();
21 let is_head_request = method == Method::HEAD;
22 // static files should only respond on HEAD and GET requests
23 if !is_head_request && method != Method::GET {
24 return Ok(None);
25 }
26
27 // if path ends in / or does not have an extension
28 // then append /index.html to the end so we can serve a page
29 let mut path = request.get_path().to_string();
30 if path.ends_with('/') {
31 path += "index.html"
32 } else if !path.contains('.') {
33 path += "/index.html"
34 }
35
36 let metadata_path = format!("{}__metadata__", path);
37
38 let store = ObjectStore::open(store_name).map(|store| store.expect("ObjectStore exists"))?;
39
40 let metadata = store.lookup(&metadata_path)?;
41 if metadata.is_none() {
42 return Ok(None);
43 }
44 let metadata = metadata.expect("Metadata is valid");
45 let metadata: Metadata = serde_json::from_str(&metadata.into_string())?;
46 let response = check_preconditions(request, &metadata)?;
47 if let (Some(response), _) = response {
48 return Ok(Some(response));
49 }
50 let request = response.1;
51
52 let item = store.lookup(&path)?;
53
54 match item {
55 None => return Ok(None),
56 Some(item) => {
57 let mut headers = HeaderMap::new();
58 headers.insert(http::header::ETAG, metadata.etag.parse()?);
59 headers.insert(http::header::LAST_MODIFIED, metadata.last_modified.parse()?);
60
61 headers.insert(http::header::ACCEPT_RANGES, "bytes".parse()?);
62
63 if let Some(content_type) = metadata.content_type {
64 headers.insert(http::header::CONTENT_TYPE, content_type.parse()?);
65 }
66 let range = request.get_header_str("range");
67
68 match range {
69 Some(range) => {
70 let item_buffer = item.into_bytes();
71 let total = item_buffer.len();
72 match HttpRange::parse(range, total.try_into()?) {
73 Ok(subranges) => {
74 if subranges.len() == 1 {
75 let start: usize = subranges[0].start.try_into()?;
76 let end: usize = subranges[0].length.try_into()?;
77 let end: usize = start + end;
78 headers.insert(
79 http::header::CONTENT_RANGE,
80 format!("bytes {}-{}/{}", start, end, total).parse()?,
81 );
82 headers.insert(
83 http::header::CONTENT_LENGTH,
84 (end - start + 1).to_string().parse()?,
85 );
86 let mut response = Response::from_status(206);
87 for (name, value) in headers {
88 response.set_header(name.expect("name is a HeaderName"), value);
89 }
90 if is_head_request {
91 return Ok(Some(response));
92 } else {
93 let body = &item_buffer[start..end];
94 response.set_body(body);
95 return Ok(Some(response));
96 }
97 } else {
98 let mut body = fastly::Body::new();
99 let boundary = "\n--3d6b6a416f9b5\n".as_bytes();
100 let mime = headers.get("content-type");
101 let mime_type = match mime {
102 Some(mime) => {
103 let value = format!("Content-Type: {}\n", mime.to_str()?);
104 Some(value.as_bytes().to_owned())
105 }
106 None => None,
107 };
108 headers.insert(
109 http::header::CONTENT_TYPE,
110 "multipart/byteranges; boundary=3d6b6a416f9b5".parse()?,
111 );
112 let mut length = boundary.len();
113 for range in subranges {
114 let start: usize = range.start.try_into()?;
115 let end: usize = range.length.try_into()?;
116 let end: usize = start + end - 1;
117 body.write_bytes(boundary);
118 length += boundary.len();
119 if let Some(ref mime_type) = mime_type {
120 body.write_bytes(&mime_type);
121 length += mime_type.len();
122 }
123 let range = format!("Content-Range: bytes {}-{}/{}\n\n", start, end, total)
124 .as_bytes()
125 .to_owned();
126 body.write_bytes(&range);
127 length += range.len();
128 let buffer = &item_buffer[start..end];
129 body.write_bytes(buffer);
130 length += buffer.len();
131 }
132 body.write_bytes(boundary);
133 length += boundary.len();
134 headers.insert(
135 http::header::CONTENT_LENGTH,
136 length.to_string().parse()?,
137 );
138 let mut response = Response::from_status(206);
139 for (name, value) in headers {
140 response.set_header(name.expect("name is a HeaderName"), value);
141 }
142 if is_head_request {
143 return Ok(Some(response));
144 } else {
145 response.set_body(body);
146 return Ok(Some(response));
147 }
148 }
149 }
150 Err(err) => match err {
151 http_range::HttpRangeParseError::InvalidRange => {
152 headers.insert(
153 http::header::CONTENT_LENGTH,
154 total.to_string().parse()?,
155 );
156 return non_range_response(
157 is_head_request,
158 headers,
159 fastly::Body::from(item_buffer),
160 );
161 }
162 http_range::HttpRangeParseError::NoOverlap => {
163 headers.insert(
164 http::header::CONTENT_RANGE,
165 format!("bytes */{}", total).parse()?,
166 );
167 let mut response = Response::from_status(416);
168 for (name, value) in headers {
169 response.set_header(name.expect("name is a HeaderName"), value);
170 }
171 return Ok(Some(response));
172 }
173 },
174 };
175 }
176 None => {
177 return non_range_response(is_head_request, headers, item);
178 }
179 }
180 }
181 }
182}
183
184fn non_range_response(
185 is_head_request: bool,
186 headers: HeaderMap,
187 item: Body,
188) -> Result<Option<Response>, Error> {
189 let mut response = Response::from_status(200);
190 for (name, value) in headers {
191 response.set_header(name.expect("name is a HeaderName"), value)
192 }
193 if !is_head_request {
194 response.set_body(item);
195 }
196 return Ok(Some(response));
197}
198
199fn check_preconditions(
200 mut request: Request,
201 metadata: &Metadata,
202) -> Result<(Option<Response>, Request), Error> {
203 // https://httpwg.org/specs/rfc9110.html#rfc.section.13.2.2
204 // A recipient cache or origin server MUST evaluate the request preconditions defined by this specification in the following order:
205 // 1. When recipient is the origin server and If-Match is present, evaluate the If-Match precondition:
206 // - if true, continue to step 3
207 // - if false, respond 412 (Precondition Failed) unless it can be determined that the state-changing request has already succeeded (see Section 13.1.1)
208 let mut header = request.get_header("if-match");
209 if let Some(header) = header {
210 if !if_match(metadata, header.to_str()?) {
211 return Ok((Some(Response::from_status(412)), request));
212 }
213 // } else {
214 // // 2. When recipient is the origin server, If-Match is not present, and If-Unmodified-Since is present, evaluate the If-Unmodified-Since precondition:
215 // // - if true, continue to step 3
216 // // - if false, respond 412 (Precondition Failed) unless it can be determined that the state-changing request has already succeeded (see Section 13.1.4)
217 // header = request.get_header("if-unmodified-since");
218 // if let Some(header) = header {
219 // if !ifUnmodifiedSince(metadata, header.to_str()?) {
220 // return Ok((Some(Response::from_status(412)), request));
221 // }
222 // }
223 }
224
225 // 3. When If-None-Match is present, evaluate the If-None-Match precondition:
226 // - if true, continue to step 5
227 // - if false for GET/HEAD, respond 304 (Not Modified)
228 // - if false for other methods, respond 412 (Precondition Failed)
229 header = request.get_header("if-none-match");
230 let method = request.get_method();
231 let get = "GET";
232 let head = "HEAD";
233 if let Some(header) = header {
234 if !if_none_match(metadata, header.to_str()?) {
235 if method == get || method == head {
236 let mut response = Response::from_status(304);
237 response.set_header(
238 http::header::ETAG,
239 metadata.etag.parse::<http::HeaderValue>()?,
240 );
241 response.set_header(
242 http::header::LAST_MODIFIED,
243 metadata.last_modified.parse::<http::HeaderValue>()?,
244 );
245
246 response.set_header(
247 http::header::ACCEPT_RANGES,
248 "bytes".parse::<http::HeaderValue>()?,
249 );
250
251 if let Some(content_type) = &metadata.content_type {
252 response.set_header(
253 http::header::CONTENT_TYPE,
254 content_type.parse::<http::HeaderValue>()?,
255 );
256 }
257 return Ok((Some(response), request));
258 }
259 return Ok((Some(Response::from_status(412)), request));
260 }
261 } else {
262 // 4. When the method is GET or HEAD, If-None-Match is not present, and If-Modified-Since is present, evaluate the If-Modified-Since precondition:
263 // - if true, continue to step 5
264 // - if false, respond 304 (Not Modified)
265 if method == get || method == head {
266 header = request.get_header("if-modified-since");
267 if let Some(header) = header {
268 if !if_modified_since(metadata, header.to_str()?) {
269 let mut response = Response::from_status(304);
270 response.set_header(
271 http::header::ETAG,
272 metadata.etag.parse::<http::HeaderValue>()?,
273 );
274 response.set_header(
275 http::header::LAST_MODIFIED,
276 metadata.last_modified.parse::<http::HeaderValue>()?,
277 );
278
279 response.set_header(
280 http::header::ACCEPT_RANGES,
281 "bytes".parse::<http::HeaderValue>()?,
282 );
283
284 if let Some(content_type) = &metadata.content_type {
285 response.set_header(
286 http::header::CONTENT_TYPE,
287 content_type.parse::<http::HeaderValue>()?,
288 );
289 }
290 return Ok((Some(response), request));
291 }
292 }
293 }
294 }
295
296 // 5. When the method is GET and both Range and If-Range are present, evaluate the If-Range precondition:
297 // - if true and the Range is applicable to the selected representation, respond 206 (Partial Content)
298 // - otherwise, ignore the Range header field and respond 200 (OK)
299 if method == get {
300 if request.contains_header("range") {
301 header = request.get_header("if-range");
302 if let Some(header) = header {
303 if !if_range(metadata, header.to_str()?) {
304 // We delete the range headers so that the `get` function will return the full body
305 request.remove_header("range");
306 }
307 }
308 }
309 }
310
311 // 6. Otherwise,
312 // - perform the requested method and respond according to its success or failure.
313 return Ok((None, request));
314}
315
316fn is_weak(etag: &str) -> bool {
317 return etag.starts_with("W/\"");
318}
319
320fn is_strong(etag: &str) -> bool {
321 return etag.starts_with("\"");
322}
323
324fn opaque_tag(etag: &str) -> &str {
325 if is_weak(etag) {
326 return &etag[2..];
327 }
328 return etag;
329}
330fn weak_match(a: &str, b: &str) -> bool {
331 // https://httpwg.org/specs/rfc9110.html#entity.tag.comparison
332 // two entity tags are equivalent if their opaque-tags match character-by-character, regardless of either or both being tagged as "weak".
333 return opaque_tag(a) == opaque_tag(b);
334}
335
336fn strong_match(a: &str, b: &str) -> bool {
337 // https://httpwg.org/specs/rfc9110.html#entity.tag.comparison
338 // two entity tags are equivalent if both are not weak and their opaque-tags match character-by-character.
339 return is_strong(a) && is_strong(b) && a == b;
340}
341
342fn split_list(value: &str) -> Vec<&str> {
343 return value.split(",").into_iter().map(|s| s.trim()).collect();
344}
345
346// https://httpwg.org/specs/rfc9110.html#field.if-match
347fn if_match(validation_fields: &Metadata, header: &str) -> bool {
348 // Optimisation for this library as we know there is an etag
349 // if validation_fields.etag.is_none() {
350 // return true;
351 // }
352
353 // 1. If the field value is "*", the condition is true if the origin server has a current representation for the target resource.
354 if header == "*" {
355 // Optimisation for this library as we know there is an etag
356 // if validation_fields.etag.is_some() {
357 return true;
358 // }
359 } else {
360 // 2. If the field value is a list of entity tags, the condition is true if any of the listed tags match the entity tag of the selected representation.
361 // An origin server MUST use the strong comparison function when comparing entity tags for If-Match (Section 8.8.3.2),
362 // since the client intends this precondition to prevent the method from being applied if there have been any changes to the representation data.
363 if split_list(header)
364 .into_iter()
365 .any(|etag| {
366 strong_match(etag, &validation_fields.etag)
367 })
368 {
369 return true;
370 }
371 }
372
373 // 3. Otherwise, the condition is false.
374 return false;
375}
376
377// https://httpwg.org/specs/rfc9110.html#field.if-none-match
378fn if_none_match(validation_fields: &Metadata, header: &str) -> bool {
379 // 1. If the field value is "*", the condition is false if the origin server has a current representation for the target resource.
380 if header == "*" {
381 // Optimisation for this library as we know there is an etag
382 // if validation_fields.etag.is_some() {
383 return false;
384 // }
385 } else {
386 // 2. If the field value is a list of entity tags, the condition is false if one of the listed tags matches the entity tag of the selected representation.
387 // A recipient MUST use the weak comparison function when comparing entity tags for If-None-Match (Section 8.8.3.2), since weak entity tags can be used for cache validation even if there have been changes to the representation data.
388 if split_list(header)
389 .iter()
390 .any(|etag| weak_match(etag, &validation_fields.etag))
391 {
392 return false;
393 }
394 }
395
396 // 3. Otherwise, the condition is true.
397 return true;
398}
399
400// https://httpwg.org/specs/rfc9110.html#field.if-modified-since
401fn if_modified_since(validation_fields: &Metadata, header: &str) -> bool {
402 // A recipient MUST ignore the If-Modified-Since header field if the received field value is not a valid HTTP-date, the field value has more than one member, or if the request method is neither GET nor HEAD.
403 let date = httpdate::parse_http_date(header);
404 if date.is_err() {
405 return true;
406 }
407
408 // 1. If the selected representation's last modification date is earlier or equal to the date provided in the field value, the condition is false.
409 if httpdate::parse_http_date(&validation_fields.last_modified).expect("validation_fields.last_modified is valid HTTP-date") <= date.expect("date is valid HTTP-date") {
410 return false;
411 }
412 // 2. Otherwise, the condition is true.
413 return true;
414}
415
416// https://httpwg.org/specs/rfc9110.html#field.if-unmodified-since
417// fn ifUnmodifiedSince(validation_fields: &Metadata, header: &str) -> bool {
418// // A recipient MUST ignore the If-Unmodified-Since header field if the received field value is not a valid HTTP-date (including when the field value appears to be a list of dates).
419// let date = httpdate::parse_http_date(header);
420// if date.is_err() {
421// return true;
422// }
423
424// // 1. If the selected representation's last modification date is earlier than or equal to the date provided in the field value, the condition is true.
425// if (httpdate::parse_http_date(&validation_fields.last_modified).expect("validation_fields.last_modified is valid HTTP-date") <= date.expect("date is valid HTTP-date")) {
426// return true;
427// }
428// // 2. Otherwise, the condition is false.
429// return false;
430// }
431
432// https://httpwg.org/specs/rfc9110.html#field.if-range
433fn if_range(validation_fields: &Metadata, header: &str) -> bool {
434 let date = httpdate::parse_http_date(header);
435 if let Ok(date) = date {
436 // To evaluate a received If-Range header field containing an HTTP-date:
437 // 1. If the HTTP-date validator provided is not a strong validator in the sense defined by Section 8.8.2.2, the condition is false.
438 // 2. If the HTTP-date validator provided exactly matches the Last-Modified field value for the selected representation, the condition is true.
439 if httpdate::parse_http_date(&validation_fields.last_modified).expect("validation_fields.last_modified is valid HTTP-date") == date {
440 return true;
441 }
442 // 3. Otherwise, the condition is false.
443 return false;
444 } else {
445 // To evaluate a received If-Range header field containing an entity-tag:
446 // 1. If the entity-tag validator provided exactly matches the ETag field value for the selected representation using the strong comparison function (Section 8.8.3.2), the condition is true.
447 if strong_match(header, &validation_fields.etag) {
448 return true;
449 }
450 // 2. Otherwise, the condition is false.
451 return false;
452 }
453}