char/avril

(wip) why'd you have to go and make things so complicaaaaated?

git clone https://git.t4t.associates/char/avril

Charlotte Somrearrange libs into lib and extensions into extsf4a0f5b

main
15.6 KiB488 linesraw
1use std::fs::File;
2use std::io;
3#[cfg(not(any(unix, windows)))]
4use std::io::{Read, Seek, SeekFrom};
5#[cfg(unix)]
6use std::os::unix::fs::FileExt;
7#[cfg(windows)]
8use std::os::windows::fs::FileExt;
9use std::sync::atomic::{AtomicBool, Ordering};
10use std::sync::{Arc, Mutex};
11use std::time::{Duration, SystemTime, UNIX_EPOCH};
12
13use hyper::http::header::{HeaderName, HeaderValue};
14use hyper::http::{HeaderMap, Response};
15use tokio::runtime::Handle;
16
17pub type CheckFile = Arc<dyn Fn() -> io::Result<()> + Send + Sync>;
18pub type HeaderPairs = Vec<(String, String)>;
19const SAFE_INTEGER: u64 = (1 << 53) - 1;
20
21struct Source {
22    file: File,
23    offset: u64,
24    remaining: u64,
25    check: CheckFile,
26}
27
28pub struct FileBody {
29    runtime: Handle,
30    source: Mutex<Option<Source>>,
31    closed: AtomicBool,
32    length: u64,
33}
34
35impl FileBody {
36    pub fn len(&self) -> u64 {
37        self.length
38    }
39
40    pub fn is_empty(&self) -> bool {
41        self.length == 0
42    }
43
44    pub async fn read(self: &Arc<Self>) -> io::Result<(Vec<u8>, bool)> {
45        let body = self.clone();
46        self.runtime
47            .spawn_blocking(move || {
48                let mut source = body.source.lock().unwrap();
49                let result = (|| {
50                    if body.closed.load(Ordering::Acquire) {
51                        return Err(io::Error::new(
52                            io::ErrorKind::NotConnected,
53                            "File response is closed",
54                        ));
55                    }
56                    let source = source.as_mut().ok_or_else(|| {
57                        io::Error::new(io::ErrorKind::NotConnected, "File response is closed")
58                    })?;
59                    (source.check)()?;
60                    let mut bytes = vec![0; source.remaining.min(65536) as usize];
61                    #[cfg(unix)]
62                    let count = source.file.read_at(&mut bytes, source.offset)?;
63                    #[cfg(windows)]
64                    let count = source.file.seek_read(&mut bytes, source.offset)?;
65                    #[cfg(not(any(unix, windows)))]
66                    let count = {
67                        source.file.seek(SeekFrom::Start(source.offset))?;
68                        source.file.read(&mut bytes)?
69                    };
70                    if count == 0 {
71                        return Err(io::Error::new(
72                            io::ErrorKind::UnexpectedEof,
73                            "File ended before Content-Length",
74                        ));
75                    }
76                    if body.closed.load(Ordering::Acquire) {
77                        return Err(io::Error::new(
78                            io::ErrorKind::NotConnected,
79                            "File response is closed",
80                        ));
81                    }
82                    bytes.truncate(count);
83                    source.offset += count as u64;
84                    source.remaining -= count as u64;
85                    Ok((bytes, source.remaining == 0))
86                })();
87                if result.as_ref().map_or(true, |(_, done)| *done) {
88                    body.closed.store(true, Ordering::Release);
89                    source.take();
90                }
91                result
92            })
93            .await
94            .map_err(io::Error::other)?
95    }
96
97    pub fn close(self: &Arc<Self>) {
98        if !self.closed.swap(true, Ordering::AcqRel) {
99            let body = self.clone();
100            self.runtime.spawn_blocking(move || {
101                body.source.lock().unwrap().take();
102            });
103        }
104    }
105}
106
107impl Drop for FileBody {
108    fn drop(&mut self) {
109        // File descriptors are closed off the guest thread, including abandoned responses.
110        let source = self.source.get_mut().unwrap().take();
111        if source.is_some() {
112            self.runtime.spawn_blocking(move || drop(source));
113        }
114    }
115}
116
117pub fn headers(pairs: HeaderPairs) -> io::Result<HeaderMap> {
118    let mut headers = HeaderMap::new();
119    for (name, value) in pairs {
120        let name = HeaderName::from_bytes(name.as_bytes()).map_err(io::Error::other)?;
121        let value = value
122            .chars()
123            .map(|c| u8::try_from(c as u32))
124            .collect::<Result<Vec<_>, _>>()
125            .map_err(io::Error::other)?;
126        let value = HeaderValue::from_bytes(&value).map_err(io::Error::other)?;
127        headers.append(name, value);
128    }
129    Ok(headers)
130}
131
132pub fn header_pairs(headers: &HeaderMap) -> HeaderPairs {
133    headers
134        .iter()
135        .map(|(name, value)| {
136            (
137                name.to_string(),
138                value
139                    .as_bytes()
140                    .iter()
141                    .map(|byte| char::from(*byte))
142                    .collect(),
143            )
144        })
145        .collect()
146}
147
148fn set(headers: &mut HeaderMap, name: &'static str, value: impl ToString) -> io::Result<()> {
149    headers.insert(
150        name,
151        HeaderValue::from_str(&value.to_string()).map_err(io::Error::other)?,
152    );
153    Ok(())
154}
155
156fn get(headers: &HeaderMap, name: &str) -> Option<Vec<u8>> {
157    let mut values = headers.get_all(name).iter();
158    let mut value = values.next()?.as_bytes().to_vec();
159    for next in values {
160        value.extend_from_slice(b", ");
161        value.extend_from_slice(next.as_bytes());
162    }
163    Some(value)
164}
165
166fn tags(value: &[u8]) -> Option<Vec<&[u8]>> {
167    let mut value = value.trim_ascii();
168    let mut tags = Vec::new();
169    while !value.is_empty() {
170        if value[0] == b',' {
171            value = value[1..].trim_ascii_start();
172            continue;
173        }
174        let original = value;
175        if value.starts_with(b"W/") {
176            value = &value[2..];
177        }
178        if value.first() != Some(&b'"') {
179            return None;
180        }
181        let end = value[1..].iter().position(|byte| *byte == b'"')? + 1;
182        if !value[1..end]
183            .iter()
184            .all(|byte| *byte == 0x21 || *byte >= 0x23 && *byte != 0x7f)
185        {
186            return None;
187        }
188        let length = original.len() - value.len() + end + 1;
189        tags.push(&original[..length]);
190        value = value[end + 1..].trim_ascii_start();
191        if !value.is_empty() && value[0] != b',' {
192            return None;
193        }
194    }
195    Some(tags)
196}
197
198fn matches(value: &[u8], etag: Option<&[u8]>, weak: bool) -> bool {
199    if value == b"*" {
200        return true;
201    }
202    let Some(etag) = etag else {
203        return false;
204    };
205    tags(value).is_some_and(|tags| {
206        tags.iter().any(|tag| {
207            if weak {
208                tag.strip_prefix(b"W/").unwrap_or(tag) == etag.strip_prefix(b"W/").unwrap_or(etag)
209            } else {
210                !tag.starts_with(b"W/") && *tag == etag
211            }
212        })
213    })
214}
215
216fn date(value: &[u8]) -> Option<SystemTime> {
217    httpdate::parse_http_date(std::str::from_utf8(value).ok()?).ok()
218}
219
220#[derive(Debug, PartialEq)]
221enum Range {
222    Ignore,
223    Unsatisfiable,
224    Bytes(u64, u64),
225}
226
227fn range(value: &[u8], size: u64) -> Range {
228    let Some(equal) = value.iter().position(|byte| *byte == b'=') else {
229        return Range::Ignore;
230    };
231    let (unit, interval) = (&value[..equal], &value[equal + 1..]);
232    if !unit.eq_ignore_ascii_case(b"bytes") {
233        return Range::Ignore;
234    }
235    let Some(dash) = interval.iter().position(|byte| *byte == b'-') else {
236        return Range::Ignore;
237    };
238    let (start, end) = (&interval[..dash], &interval[dash + 1..]);
239    if (start.is_empty() && end.is_empty()) || !start.iter().chain(end).all(u8::is_ascii_digit) {
240        return Range::Ignore;
241    }
242    let normalize = |value: &[u8]| {
243        value
244            .iter()
245            .position(|byte| *byte != b'0')
246            .unwrap_or(value.len())
247    };
248    let start = &start[normalize(start)..];
249    let end = &end[normalize(end)..];
250    let parse = |value: &[u8]| {
251        value.iter().try_fold(0u64, |number, digit| {
252            number.checked_mul(10)?.checked_add((digit - b'0') as u64)
253        })
254    };
255    if interval.starts_with(b"-") {
256        let suffix = parse(end).unwrap_or(u64::MAX);
257        return if suffix == 0 || size == 0 {
258            Range::Unsatisfiable
259        } else {
260            Range::Bytes(size.saturating_sub(suffix), size - 1)
261        };
262    }
263    // Compare decimal strings before conversion so oversized reversed intervals remain malformed.
264    if !interval.ends_with(b"-") && (end.len(), end) < (start.len(), start) {
265        return Range::Ignore;
266    }
267    let Some(start) = parse(start).filter(|start| *start < size) else {
268        return Range::Unsatisfiable;
269    };
270    Range::Bytes(
271        start,
272        parse(end)
273            .filter(|_| !interval.ends_with(b"-"))
274            .unwrap_or(u64::MAX)
275            .min(size - 1),
276    )
277}
278
279pub fn serve_file(
280    runtime: Handle,
281    method: &str,
282    request: HeaderMap,
283    path: &str,
284    headers: HeaderMap,
285    open: impl FnOnce() -> io::Result<(File, CheckFile)>,
286) -> io::Result<Response<Option<Arc<FileBody>>>> {
287    let mut response = Response::new(None);
288    *response.headers_mut() = headers;
289    let headers = response.headers_mut();
290    for name in ["content-length", "content-range", "last-modified"] {
291        headers.remove(name);
292    }
293    if method != "GET" && method != "HEAD" {
294        set(headers, "allow", "GET, HEAD")?;
295        set(headers, "content-length", 0)?;
296        *response.status_mut() = hyper::StatusCode::METHOD_NOT_ALLOWED;
297        return Ok(response);
298    }
299    let (file, check) = match open() {
300        Ok(opened) => opened,
301        Err(error)
302            if matches!(
303                error.kind(),
304                io::ErrorKind::NotFound | io::ErrorKind::NotADirectory
305            ) =>
306        {
307            set(headers, "content-length", 0)?;
308            *response.status_mut() = hyper::StatusCode::NOT_FOUND;
309            return Ok(response);
310        }
311        Err(error) => return Err(error),
312    };
313    check()?;
314    let info = file.metadata()?;
315    if !info.is_file() {
316        set(headers, "content-length", 0)?;
317        *response.status_mut() = hyper::StatusCode::NOT_FOUND;
318        return Ok(response);
319    }
320    if info.len() > SAFE_INTEGER {
321        return Err(io::Error::other("Invalid file size"));
322    }
323    let modified = info.modified().ok();
324    // httpdate only formats years 1970 through 9999.
325    let modified_seconds = modified
326        .and_then(|date| date.duration_since(UNIX_EPOCH).ok())
327        .filter(|duration| duration.as_secs() < 253402300800)
328        .map(|duration| UNIX_EPOCH + Duration::from_secs(duration.as_secs()));
329    if let Some(modified) = modified
330        && !headers.contains_key("etag")
331    {
332        let millis = match modified.duration_since(UNIX_EPOCH) {
333            Ok(duration) => duration.as_millis() as i128,
334            Err(error) => -(error.duration().as_millis() as i128),
335        };
336        set(headers, "etag", format!("W/\"{}-{millis}\"", info.len()))?;
337    }
338    if let Some(modified) = modified_seconds {
339        set(headers, "last-modified", httpdate::fmt_http_date(modified))?;
340    }
341    let etag = get(headers, "etag");
342    if !headers.contains_key("content-type") {
343        let mime = mime_guess::from_path(path).first_or_octet_stream();
344        let text = mime.type_() == "text"
345            || matches!(
346                mime.essence_str(),
347                "application/javascript" | "application/json" | "application/xml" | "image/svg+xml"
348            );
349        set(
350            headers,
351            "content-type",
352            format!("{mime}{}", if text { "; charset=utf-8" } else { "" }),
353        )?;
354    }
355    set(headers, "accept-ranges", "bytes")?;
356    let mut status = 200;
357    if let Some(value) = get(&request, "if-match") {
358        if !matches(&value, etag.as_deref(), false) {
359            status = 412;
360        }
361    } else if let Some(value) = get(&request, "if-unmodified-since")
362        && let (Some(date), Some(modified)) = (date(&value), modified_seconds)
363        && modified > date
364    {
365        status = 412;
366    }
367    if status == 200 {
368        if let Some(value) = get(&request, "if-none-match") {
369            if matches(&value, etag.as_deref(), true) {
370                status = 304;
371            }
372        } else if let Some(value) = get(&request, "if-modified-since")
373            && let (Some(date), Some(modified)) = (date(&value), modified_seconds)
374            && modified <= date
375        {
376            status = 304;
377        }
378    }
379    if status != 200 {
380        if status == 412 {
381            set(headers, "content-length", 0)?;
382        }
383        *response.status_mut() = hyper::StatusCode::from_u16(status).unwrap();
384        return Ok(response);
385    }
386
387    let mut start = 0;
388    let mut length = info.len();
389    let if_range = get(&request, "if-range");
390    let use_range = if_range.as_ref().is_none_or(|value| {
391        if value.starts_with(b"\"") || value.starts_with(b"W/\"") {
392            !value.starts_with(b"W/")
393                && etag
394                    .as_deref()
395                    .is_some_and(|etag| !etag.starts_with(b"W/") && etag == value)
396        } else {
397            date(value).is_some_and(|date| Some(date) == modified_seconds)
398        }
399    });
400    if method == "GET"
401        && use_range
402        && let Some(value) = get(&request, "range")
403    {
404        match range(&value, info.len()) {
405            Range::Ignore => {}
406            Range::Unsatisfiable => {
407                status = 416;
408                length = 0;
409                set(headers, "content-range", format!("bytes */{}", info.len()))?;
410            }
411            Range::Bytes(first, last) => {
412                status = 206;
413                start = first;
414                length = last - first + 1;
415                set(
416                    headers,
417                    "content-range",
418                    format!("bytes {first}-{last}/{}", info.len()),
419                )?;
420            }
421        }
422    }
423    set(headers, "content-length", length)?;
424    if method == "GET" && length != 0 {
425        *response.body_mut() = Some(Arc::new(FileBody {
426            runtime,
427            source: Mutex::new(Some(Source {
428                file,
429                offset: start,
430                remaining: length,
431                check,
432            })),
433            closed: AtomicBool::new(false),
434            length,
435        }));
436    }
437    *response.status_mut() = hyper::StatusCode::from_u16(status).unwrap();
438    Ok(response)
439}
440
441#[cfg(test)]
442mod tests {
443    use super::*;
444
445    #[test]
446    fn ranges_handle_overflow_and_reversed_intervals() {
447        assert_eq!(
448            range(b"bytes=9007199254740993-9007199254740992", 10),
449            Range::Ignore
450        );
451        assert_eq!(
452            range(
453                b"bytes=999999999999999999999999-999999999999999999999998",
454                10
455            ),
456            Range::Ignore
457        );
458        assert_eq!(
459            range(b"bytes=999999999999999999999999-", 10),
460            Range::Unsatisfiable
461        );
462        assert_eq!(
463            range(b"bytes=8-999999999999999999999999", 10),
464            Range::Bytes(8, 9)
465        );
466        assert_eq!(
467            range(b"bytes=-999999999999999999999999", 10),
468            Range::Bytes(0, 9)
469        );
470        assert_eq!(range(b"bytes=0002-0004", 10), Range::Bytes(2, 4));
471        assert_eq!(range(b"bytes=0-", 0), Range::Unsatisfiable);
472        assert_eq!(range(b"bytes=-0", 10), Range::Unsatisfiable);
473        assert_eq!(range(b"bytes=0-1,4-5", 10), Range::Ignore);
474    }
475
476    #[test]
477    fn entity_tag_lists_preserve_commas_and_comparison_strength() {
478        assert!(matches(
479            b", \"other,tag\", W/\"known\",",
480            Some(b"\"known\""),
481            true
482        ));
483        assert!(!matches(b"W/\"known\"", Some(b"W/\"known\""), false));
484        assert!(matches(b"\"known\"", Some(b"\"known\""), false));
485        assert!(matches(b"*", None, false));
486        assert!(!matches(b"junk, \"known\"", Some(b"\"known\""), true));
487    }
488}