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}