Skip to main content

static_web_server/exts/
http.rs

1// SPDX-License-Identifier: MIT OR Apache-2.0
2// This file is part of Static Web Server.
3// See https://static-web-server.net/ for more information
4// Copyright (C) 2019-present Jose Quintana <joseluisq.net>
5
6//! HTTP-related extension traits.
7
8use hyper::{Method, Response, header::HeaderValue};
9
10/// A fixed list of HTTP methods supported by SWS.
11pub const HTTP_SUPPORTED_METHODS: &[Method; 3] = &[Method::OPTIONS, Method::HEAD, Method::GET];
12
13/// SWS HTTP Method extensions trait.
14pub trait MethodExt {
15    /// If method is allowed.
16    fn is_allowed(&self) -> bool;
17    /// If method is `GET`.
18    #[allow(unused)]
19    fn is_get(&self) -> bool;
20    /// If method is `HEAD`.
21    fn is_head(&self) -> bool;
22    /// If method is `OPTIONS`.
23    fn is_options(&self) -> bool;
24}
25
26impl MethodExt for Method {
27    /// Checks if the HTTP method is allowed (supported) by SWS.
28    #[inline(always)]
29    fn is_allowed(&self) -> bool {
30        for method in HTTP_SUPPORTED_METHODS {
31            if method == self {
32                return true;
33            }
34        }
35        false
36    }
37
38    /// Checks if the HTTP method is `GET`.
39    #[inline(always)]
40    fn is_get(&self) -> bool {
41        self == Method::GET
42    }
43
44    /// Checks if the HTTP method is `HEAD`.
45    #[inline(always)]
46    fn is_head(&self) -> bool {
47        self == Method::HEAD
48    }
49
50    /// Checks if the HTTP method is `OPTIONS`.
51    #[inline(always)]
52    fn is_options(&self) -> bool {
53        self == Method::OPTIONS
54    }
55}
56
57/// Pre-computed static Vary header value for accept-encoding.
58static VARY_ACCEPT_ENCODING: HeaderValue = HeaderValue::from_static("accept-encoding");
59
60/// Append `accept-encoding` to the response's `Vary` header, creating it if absent.
61/// Skips the update if `accept-encoding` is already listed.
62pub(crate) fn append_vary_accept_encoding<B>(resp: &mut Response<B>) {
63    let accept_enc = hyper::header::ACCEPT_ENCODING.as_str();
64    match resp.headers().get(hyper::header::VARY) {
65        None => {
66            resp.headers_mut()
67                .insert(hyper::header::VARY, VARY_ACCEPT_ENCODING.clone());
68        }
69        Some(existing) => {
70            let s = existing.to_str().unwrap_or_default();
71            if s.contains(accept_enc) {
72                return;
73            }
74            // Append to existing value
75            let mut new_val = String::with_capacity(s.len() + 2 + accept_enc.len());
76            new_val.push_str(s);
77            if !s.is_empty() {
78                new_val.push_str(", ");
79            }
80            new_val.push_str(accept_enc);
81            if let Ok(val) = HeaderValue::from_str(&new_val) {
82                resp.headers_mut().insert(hyper::header::VARY, val);
83            }
84        }
85    }
86}
87
88#[cfg(test)]
89mod tests {
90    use hyper::{Method, Response, StatusCode};
91
92    use super::{MethodExt, append_vary_accept_encoding};
93
94    #[test]
95    fn method_get_is_allowed() {
96        assert!(Method::GET.is_allowed());
97        assert!(Method::GET.is_get());
98        assert!(!Method::GET.is_head());
99        assert!(!Method::GET.is_options());
100    }
101
102    #[test]
103    fn method_head_is_allowed() {
104        assert!(Method::HEAD.is_allowed());
105        assert!(Method::HEAD.is_head());
106        assert!(!Method::HEAD.is_get());
107        assert!(!Method::HEAD.is_options());
108    }
109
110    #[test]
111    fn method_options_is_allowed() {
112        assert!(Method::OPTIONS.is_allowed());
113        assert!(Method::OPTIONS.is_options());
114        assert!(!Method::OPTIONS.is_get());
115        assert!(!Method::OPTIONS.is_head());
116    }
117
118    #[test]
119    fn method_post_is_not_allowed() {
120        assert!(!Method::POST.is_allowed());
121        assert!(!Method::POST.is_get());
122        assert!(!Method::POST.is_head());
123        assert!(!Method::POST.is_options());
124    }
125
126    #[test]
127    fn method_put_delete_patch_are_not_allowed() {
128        for method in [Method::PUT, Method::DELETE, Method::PATCH] {
129            assert!(!method.is_allowed(), "{method} should not be allowed");
130        }
131    }
132
133    #[test]
134    fn vary_added_when_absent() {
135        let mut resp = Response::new(crate::body::empty());
136        *resp.status_mut() = StatusCode::OK;
137        append_vary_accept_encoding(&mut resp);
138        let vary = resp.headers().get(hyper::header::VARY).unwrap();
139        assert_eq!(vary.to_str().unwrap(), "accept-encoding");
140    }
141
142    #[test]
143    fn vary_not_duplicated_when_already_present() {
144        let mut resp = Response::new(crate::body::empty());
145        *resp.status_mut() = StatusCode::OK;
146        resp.headers_mut()
147            .insert(hyper::header::VARY, "accept-encoding".parse().unwrap());
148        append_vary_accept_encoding(&mut resp);
149        let vary = resp.headers().get(hyper::header::VARY).unwrap();
150        assert_eq!(vary.to_str().unwrap(), "accept-encoding");
151    }
152
153    #[test]
154    fn vary_appended_to_existing_value() {
155        let mut resp = Response::new(crate::body::empty());
156        *resp.status_mut() = StatusCode::OK;
157        resp.headers_mut()
158            .insert(hyper::header::VARY, "origin".parse().unwrap());
159        append_vary_accept_encoding(&mut resp);
160        let vary = resp.headers().get(hyper::header::VARY).unwrap();
161        assert_eq!(vary.to_str().unwrap(), "origin, accept-encoding");
162    }
163
164    #[test]
165    fn vary_not_duplicated_when_mixed_with_others() {
166        let mut resp = Response::new(crate::body::empty());
167        *resp.status_mut() = StatusCode::OK;
168        resp.headers_mut().insert(
169            hyper::header::VARY,
170            "origin, accept-encoding".parse().unwrap(),
171        );
172        append_vary_accept_encoding(&mut resp);
173        let vary = resp.headers().get(hyper::header::VARY).unwrap();
174        assert_eq!(vary.to_str().unwrap(), "origin, accept-encoding");
175    }
176}