Skip to main content

rws_http/header/
mod.rs

1//! HTTP header fields.
2//!
3//! A [`Header`] is a name and a value, both `Cow<'static, str>`: build the
4//! standard ones from the `Header::_X` name constants and `&'static str` values
5//! (borrowed, no allocation), other values from a `String` with
6//! [`Header::new`]. Read a field through deref (`&*h.value`). [`Header::parse`]
7//! turns a `Name: value` line into a `Header`; `generate` does the reverse.
8//!
9//! Header text must be UTF-8 without control characters. Examples:
10//! `src/header/example/`.
11
12use std::borrow::Cow;
13use crate::client_hint::ClientHint;
14use crate::cors::Cors;
15use crate::ext::date_time_ext::DateTimeExt;
16use crate::ext::string_ext::StringExt;
17use crate::range::Range;
18use crate::request::Request;
19use crate::symbol::SYMBOL;
20
21#[cfg(test)]
22mod tests;
23
24#[doc(hidden)]
25pub mod sf;
26
27pub mod content_disposition;
28
29#[cfg(test)]
30mod example;
31
32/// An HTTP header name/value pair.
33///
34/// Use the associated string constants (e.g. [`Header::_CONTENT_TYPE`]) for header names.
35/// [`Header::get_header_list`] returns the standard set of response headers (client hints,
36/// CORS, cache control, security headers) that should be included on every response.
37///
38/// Name and value are `Cow<'static, str>` (18.0.0; `String` before): the
39/// built-in names and fixed values are borrowed `&'static str`s, so building
40/// and cloning the standard header set allocates nothing. Construct with
41/// [`Header::new`] (or `.into()` on each field); read through `Deref<Target =
42/// str>` (`&*header.value`, `header.value.trim()`, `header.value == "x"`).
43#[derive(PartialEq, Eq, Clone, Debug)]
44pub struct Header {
45    /// Header name, e.g. `"Content-Type"`.
46    pub name: Cow<'static, str>,
47    /// Header value, e.g. `"application/json"`.
48    pub value: Cow<'static, str>,
49}
50
51/// Headers every built-in response carries, around the per-request `Date`.
52struct FixedHeaders {
53    before_date: Vec<Header>,
54    after_date: Vec<Header>,
55}
56
57impl Header {
58
59    /// A header from anything that converts into `Cow<'static, str>`: a
60    /// `&'static str` (borrowed, no allocation) or a `String` (owned).
61    ///
62    /// ```
63    /// # extern crate rws_http as rust_web_server;
64    /// use rust_web_server::header::Header;
65    /// let h = Header::new(Header::_CONTENT_TYPE, format!("text/{}", "plain"));
66    /// assert_eq!("text/plain", h.value);
67    /// ```
68    pub fn new(name: impl Into<Cow<'static, str>>, value: impl Into<Cow<'static, str>>) -> Header {
69        Header { name: name.into(), value: value.into() }
70    }
71
72    /// A header name taken from a parsed message: borrowed when it is one
73    /// of the common names spelled exactly as listed (canonical or lower
74    /// case — the forms clients and servers actually send), owned otherwise.
75    /// Same text either way; it only saves the allocation.
76    #[doc(hidden)]
77    pub fn name_from(name: &str) -> Cow<'static, str> {
78        const COMMON: &[&str] = &[
79            "Host", "host", "User-Agent", "user-agent", "Accept", "accept",
80            "Accept-Encoding", "accept-encoding", "Accept-Language", "accept-language",
81            "Connection", "connection", "Content-Type", "content-type",
82            "Content-Length", "content-length", "Date", "date", "Server", "server",
83            "Last-Modified", "last-modified", "ETag", "etag", "Accept-Ranges", "accept-ranges",
84            "Cache-Control", "cache-control", "Cookie", "cookie", "Set-Cookie", "set-cookie",
85            "Referer", "referer", "Origin", "origin", "Authorization", "authorization",
86            "If-None-Match", "if-none-match", "If-Modified-Since", "if-modified-since",
87            "Transfer-Encoding", "transfer-encoding", "Keep-Alive", "keep-alive",
88            "Vary", "vary", "Upgrade-Insecure-Requests", "upgrade-insecure-requests",
89            "X-Forwarded-For", "x-forwarded-for", "Via", "via",
90        ];
91        match COMMON.iter().find(|common| **common == name) {
92            Some(common) => Cow::Borrowed(common),
93            None => Cow::Owned(name.to_string()),
94        }
95    }
96
97    pub fn as_string(&self) -> String {
98        let formatted = format!("{}: {}", self.name, self.value);
99        formatted
100    }
101
102    pub const _ACCESS_CONTROL_REQUEST_METHOD: &'static str = "Access-Control-Request-Method";
103    pub const _ACCESS_CONTROL_REQUEST_HEADERS: &'static str = "Access-Control-Request-Headers";
104    pub const _ACCESS_CONTROL_ALLOW_ORIGIN: &'static str = "Access-Control-Allow-Origin";
105    pub const _ACCESS_CONTROL_ALLOW_METHODS: &'static str = "Access-Control-Allow-Methods";
106    pub const _ACCESS_CONTROL_ALLOW_HEADERS: &'static str = "Access-Control-Allow-Headers";
107    pub const _ACCESS_CONTROL_ALLOW_CREDENTIALS: &'static str = "Access-Control-Allow-Credentials";
108    pub const _ACCESS_CONTROL_MAX_AGE: &'static str = "Access-Control-Max-Age";
109    pub const _ACCESS_CONTROL_EXPOSE_HEADERS: &'static str = "Access-Control-Expose-Headers";
110
111    pub const _ACCEPT: &'static str = "Accept";
112    pub const _ACCEPT_RANGES: &'static str = "Accept-Ranges";
113    pub const _ACCEPT_CH: &'static str = "Accept-CH";
114    pub const _ACCEPT_ENCODING: &'static str = "Accept-Encoding";
115    pub const _ACCEPT_LANGUAGE: &'static str = "Accept-Language";
116    pub const _ACCEPT_PATCH: &'static str = "Accept-Patch";
117    pub const _ACCEPT_POST: &'static str = "Accept-Post";
118    pub const _AGE: &'static str = "Age";
119    pub const _ALLOW: &'static str = "Allow";
120    pub const _ALT_SVC: &'static str = "Alt-Svc";
121    pub const _AUTHORIZATION: &'static str = "Authorization";
122    pub const _CACHE_CONTROL: &'static str = "Cache-Control";
123    pub const _CONNECTION: &'static str = "Connection";
124    pub const _CLEAR_SITE_DATA: &'static str = "Clear-Site-Data";
125    pub const _CONTENT_TYPE: &'static str = "Content-Type";
126    pub const _CONTENT_LENGTH: &'static str = "Content-Length";
127    pub const _CONTENT_RANGE: &'static str = "Content-Range";
128    pub const _CONTENT_DISPOSITION: &'static str = "Content-Disposition";
129    pub const _CONTENT_ENCODING: &'static str = "Content-Encoding";
130    pub const _CONTENT_LANGUAGE: &'static str = "Content-Language";
131    pub const _CONTENT_LOCATION: &'static str = "Content-Location";
132    pub const _CONTENT_SECURITY_POLICY: &'static str = "Content-Security-Policy";
133    pub const _CONTENT_SECURITY_POLICY_REPORT_ONLY: &'static str = "Content-Security-Policy-Report-Only";
134    pub const _COOKIE: &'static str = "Cookie";
135    pub const _CROSS_ORIGIN_EMBEDDER_POLICY: &'static str = "Cross-Origin-Embedder-Policy";
136    pub const _CROSS_ORIGIN_OPENER_POLICY: &'static str = "Cross-Origin-Opener-Policy";
137    pub const _CROSS_ORIGIN_RESOURCE_POLICY: &'static str = "Cross-Origin-Resource-Policy";
138    pub const _DATE: &'static str = "Date";
139    pub const _DATE_UNIX_EPOCH_NANOS: &'static str = "Date-Unix-Epoch-Nanos";
140    pub const _DEVICE_MEMORY: &'static str = "Device-Memory";
141    pub const _DIGEST: &'static str = "Digest";
142    pub const _DOWNLINK: &'static str = "Downlink";
143    pub const _EARLY_DATA: &'static str = "Early-Data";
144    pub const _ECT: &'static str = "ECT";
145    pub const _ETAG: &'static str = "ETag";
146    pub const _EXPECT: &'static str = "Expect";
147    pub const _EXPIRES: &'static str = "Expires";
148    pub const _FEATURE_POLICY: &'static str = "Feature-Policy";
149    pub const _PERMISSIONS_POLICY: &'static str = "Permissions-Policy";
150    pub const _FORWARDED: &'static str = "Forwarded";
151    pub const _FROM: &'static str = "From";
152    pub const _HOST: &'static str = "Host";
153    pub const _IF_MATCH: &'static str = "If-Match";
154    pub const _IF_MODIFIED_SINCE: &'static str = "If-Modified-Since";
155    pub const _IF_NONE_MATCH: &'static str = "If-None-Match";
156    pub const _IF_RANGE: &'static str = "If-Range";
157    pub const _IF_UNMODIFIED_SINCE: &'static str = "If-Unmodified-Since";
158    pub const _LAST_MODIFIED: &'static str = "Last-Modified";
159    pub const _LAST_MODIFIED_UNIX_EPOCH_NANOS: &'static str = "Last-Modified-Unix-Epoch-Nanos";
160    pub const _LINK: &'static str = "Link";
161    pub const _LOCATION: &'static str = "Location";
162    pub const _MAX_FORWARDS: &'static str = "Max-Forwards";
163    pub const _NEL: &'static str = "NEL";
164    pub const _ORIGIN: &'static str = "Origin";
165    pub const _PROXY_AUTHENTICATE: &'static str = "Proxy-Authenticate";
166    pub const _PROXY_AUTHORIZATION: &'static str = "Proxy-Authorization";
167    pub const _RANGE: &'static str = "Range";
168    pub const _REFERER: &'static str = "Referer";
169    pub const _REFERRER_POLICY: &'static str = "Referrer-Policy";
170    pub const _RETRY_AFTER: &'static str = "Retry-After";
171    pub const _RTT: &'static str = "RTT";
172    pub const _SAVE_DATA: &'static str = "Save-Data";
173    pub const _SEC_CH_UA: &'static str = "Sec-CH-UA";
174    pub const _SEC_CH_UA_ARCH: &'static str = "Sec-CH-UA-Arch";
175    pub const _SEC_CH_UA_BITNESS: &'static str = "Sec-CH-UA-Bitness";
176    pub const _SEC_CH_UA_FULL_VERSION_LIST: &'static str = "Sec-CH-UA-Full-Version-List";
177    pub const _SEC_CH_UA_MOBILE: &'static str = "Sec-CH-UA-Mobile";
178    pub const _SEC_CH_UA_MODEL: &'static str = "Sec-CH-UA-Model";
179    pub const _SEC_CH_UA_PLATFORM: &'static str = "Sec-CH-UA-Platform";
180    pub const _SEC_CH_UA_PLATFORM_VERSION: &'static str = "Sec-CH-UA-Platform-Version";
181    pub const _SEC_FETCH_DEST: &'static str = "Sec-Fetch-Dest";
182    pub const _SEC_FETCH_MODE: &'static str = "Sec-Fetch-Mode";
183    pub const _SEC_FETCH_SITE: &'static str = "Sec-Fetch-Site";
184    pub const _SEC_FETCH_USER: &'static str = "Sec-Fetch-User";
185    pub const _SEC_GPC: &'static str = "Sec-GPC";
186    pub const _SERVER: &'static str = "Server";
187    pub const _SERVER_TIMING: &'static str = "Server-Timing";
188    pub const _SERVICE_WORKER_NAVIGATION_PRELOAD: &'static str = "Service-Worker-Navigation-Preload";
189    pub const _SET_COOKIE: &'static str = "Set-Cookie";
190    pub const _SOURCE_MAP: &'static str = "SourceMap";
191    pub const _STRICT_TRANSPORT_SECURITY: &'static str = "Strict-Transport-Security";
192    pub const _TE: &'static str = "TE";
193    pub const _TIMING_ALLOW_ORIGIN: &'static str = "Timing-Allow-Origin";
194    pub const _TRAILER: &'static str = "Trailer";
195    pub const _TRANSFER_ENCODING: &'static str = "Transfer-Encoding";
196    pub const _UPGRADE: &'static str = "Upgrade";
197    pub const _UPGRADE_INSECURE_REQUESTS: &'static str = "Upgrade-Insecure-Requests";
198    pub const _USER_AGENT: &'static str = "User-Agent";
199    pub const _VARY: &'static str = "Vary";
200    pub const _VIA: &'static str = "Via";
201    pub const _WANT_DIGEST: &'static str = "Want-Digest";
202    pub const _WWW_AUTHENTICATE: &'static str = "WWW-Authenticate";
203    pub const _X_CONTENT_TYPE_OPTIONS: &'static str = "X-Content-Type-Options";
204    pub const _X_CONTENT_TYPE_OPTIONS_VALUE_NOSNIFF: &'static str = "nosniff";
205    pub const _X_FRAME_OPTIONS: &'static str = "X-Frame-Options";
206    pub const _X_FRAME_OPTIONS_VALUE_DENY: &'static str = "DENY";
207    pub const _X_FRAME_OPTIONS_VALUE_SAME_ORIGIN: &'static str = "SAMEORIGIN";
208
209    pub const _STRICT_TRANSPORT_SECURITY_VALUE_DEFAULT: &'static str = "max-age=31536000; includeSubDomains";
210    pub const _REFERRER_POLICY_VALUE_DEFAULT: &'static str = "strict-origin-when-cross-origin";
211    pub const _PERMISSIONS_POLICY_VALUE_DEFAULT: &'static str = "geolocation=(), microphone=(), camera=()";
212    pub const _CONTENT_SECURITY_POLICY_VALUE_DEFAULT: &'static str = "default-src 'self'";
213
214
215
216
217    pub const NAME_VALUE_SEPARATOR: &'static str = ": ";
218
219    /// `s` safe to write on one header line: CR and LF become spaces
220    /// (borrowed when there are none). Every HTTP/1.1 writer passes header
221    /// names, values and the reason phrase through it, so a value carrying
222    /// user input (a redirect target, an upstream header) can't end the line
223    /// and inject headers or a second response (#47).
224    pub fn single_line(s: &str) -> std::borrow::Cow<'_, str> {
225        // A byte search: `contains(['\r', '\n'])` decoded every char, and
226        // this runs for every name and value of every response written.
227        if memchr::memchr2(b'\r', b'\n', s.as_bytes()).is_some() {
228            std::borrow::Cow::Owned(s.replace(['\r', '\n'], " "))
229        } else {
230            std::borrow::Cow::Borrowed(s)
231        }
232    }
233
234    pub const _DO_NOT_STORE_CACHE: &'static str = "no-store, no-cache, private, max-age=0, must-revalidate, proxy-revalidate";
235
236
237
238    /// Build the standard response header list using settings from
239    /// [`ServerConfig`].
240    ///
241    /// Prefer this method inside [`App::execute`] and any call-site that has
242    /// an `App`-level config available. It avoids reading environment variables
243    /// at request time, which enables proper test isolation via
244    /// [`App::with_config`].
245    pub fn get_header_list_with_config(
246        request: &Request,
247        config: &crate::server_config::ServerConfig,
248    ) -> Vec<Header> {
249        // CORS depends on the request and the date changes; everything else
250        // depends only on the CSP setting, so it's built once per CSP value
251        // and cloned (joining the client-hint and Vary lists per request was
252        // a measurable share of a small response's CPU).
253        let fixed = Header::fixed_headers(&config.csp, config.client_hints, config.security_headers);
254        let mut header_list = Cors::get_headers_from_config(request, config);
255        header_list.reserve(fixed.before_date.len() + 2 + fixed.after_date.len());
256        header_list.extend(fixed.before_date.iter().cloned());
257        header_list.push(Header { name: Header::_DATE.into(), value: crate::http::date::now().into() });
258        if config.nanos_headers {
259            header_list.push(Header::get_date_iso_8601_header());
260        }
261        header_list.extend(fixed.after_date.iter().cloned());
262        header_list
263    }
264
265    /// The request-independent part of [`Header::get_header_list_with_config`]
266    /// for a CSP value, client-hint and security-header setting, cached for
267    /// the last seen.
268    fn fixed_headers(csp: &str, client_hints: bool, security_headers: bool) -> std::sync::Arc<FixedHeaders> {
269        use std::sync::{Arc, RwLock};
270        static CACHE: RwLock<Option<(String, bool, bool, Arc<FixedHeaders>)>> = RwLock::new(None);
271        if let Some((cached_csp, cached_hints, cached_security, fixed)) = CACHE.read().unwrap_or_else(|e| e.into_inner()).as_ref() {
272            if cached_csp == csp && *cached_hints == client_hints && *cached_security == security_headers {
273                return Arc::clone(fixed);
274            }
275        }
276        // Client hints are opt-in (RWS_CONFIG_CLIENT_HINTS): ~700 bytes on
277        // every response otherwise, and Critical-CH makes Chromium retry.
278        let mut before_date = Vec::with_capacity(6);
279        let vary = if client_hints {
280            before_date.push(ClientHint::get_accept_client_hints_header());
281            before_date.push(ClientHint::get_critical_client_hints_header());
282            [Cors::get_vary_header_value(), ClientHint::get_vary_header_value()].join(", ")
283        } else {
284            Cors::get_vary_header_value()
285        };
286        before_date.push(Header { name: Header::_VARY.into(), value: vary.into() });
287        before_date.push(Header::get_x_content_type_options_header());
288        before_date.push(Header::get_accept_ranges_header());
289        // Opt-in since 18.0.0 (RWS_CONFIG_SECURITY_HEADERS).
290        if security_headers {
291            before_date.push(Header::get_x_frame_options_header());
292        }
293        let mut after_date = vec![Header::get_no_cache_header()];
294        if security_headers {
295            after_date.push(Header { name: Header::_REFERRER_POLICY.into(), value: Header::_REFERRER_POLICY_VALUE_DEFAULT.into() });
296            after_date.push(Header { name: Header::_PERMISSIONS_POLICY.into(), value: Header::_PERMISSIONS_POLICY_VALUE_DEFAULT.into() });
297        }
298        if !csp.is_empty() {
299            after_date.push(Header { name: Header::_CONTENT_SECURITY_POLICY.into(), value: csp.to_string().into() });
300        }
301        // Borrowed values clone for free: everything here is per CSP value,
302        // not per request, so the built strings are interned for good.
303        let before_date = before_date.into_iter().map(Header::interned).collect();
304        let after_date = after_date.into_iter().map(Header::interned).collect();
305        let fixed = Arc::new(FixedHeaders { before_date, after_date });
306        *CACHE.write().unwrap_or_else(|e| e.into_inner()) = Some((csp.to_string(), client_hints, security_headers, Arc::clone(&fixed)));
307        fixed
308    }
309
310    /// `self` with an owned name or value turned into a `&'static str`
311    /// leaked once per distinct string (a process-wide set), so cloning it
312    /// never allocates. Only for values from a small, bounded set — the
313    /// fixed response headers, which change only with the CSP setting.
314    fn interned(self) -> Header {
315        fn intern(s: Cow<'static, str>) -> Cow<'static, str> {
316            use std::collections::HashSet;
317            use std::sync::{Mutex, OnceLock};
318            let owned = match s {
319                Cow::Borrowed(_) => return s,
320                Cow::Owned(owned) => owned,
321            };
322            static INTERNED: OnceLock<Mutex<HashSet<&'static str>>> = OnceLock::new();
323            let mut set = INTERNED.get_or_init(|| Mutex::new(HashSet::new())).lock().unwrap_or_else(|e| e.into_inner());
324            if let Some(existing) = set.get(owned.as_str()) {
325                return Cow::Borrowed(existing);
326            }
327            let leaked: &'static str = Box::leak(owned.into_boxed_str());
328            set.insert(leaked);
329            Cow::Borrowed(leaked)
330        }
331        Header { name: intern(self.name), value: intern(self.value) }
332    }
333
334    /// Build the standard response header list, reading CORS and CSP settings
335    /// from environment variables.
336    ///
337    /// This is the legacy entry point retained for call-sites that do not yet
338    /// have access to a [`ServerConfig`] (e.g. error helpers in
339    /// `src/extract/mod.rs`). For all call-sites inside [`App::execute`] use
340    /// [`get_header_list_with_config`] instead.
341    pub fn get_header_list(request: &Request) -> Vec<Header> {
342        Self::get_header_list_with_config(request, &crate::server_config::ServerConfig::current())
343    }
344
345
346    pub fn get_x_content_type_options_header() -> Header {
347        Header {
348            name: Header::_X_CONTENT_TYPE_OPTIONS.into(),
349            value: Header::_X_CONTENT_TYPE_OPTIONS_VALUE_NOSNIFF.into(),
350        }
351    }
352
353    pub fn get_accept_ranges_header() -> Header {
354        Header {
355            name: Header::_ACCEPT_RANGES.into(),
356            value: Range::BYTES.into(),
357        }
358    }
359
360    pub fn get_x_frame_options_header() -> Header {
361        Header {
362            name: Header::_X_FRAME_OPTIONS.into(),
363            value: Header::_X_FRAME_OPTIONS_VALUE_SAME_ORIGIN.into(),
364        }
365    }
366
367    pub fn get_date_iso_8601_header() -> Header {
368        Header {
369            name: Header::_DATE_UNIX_EPOCH_NANOS.into(),
370            value: crate::ext::decimal::Decimal::new(DateTimeExt::_now_unix_epoch_nanos()).as_str().to_string().into(),
371        }
372    }
373
374    pub fn get_no_cache_header() -> Header {
375        Header {
376            name: Header::_CACHE_CONTROL.into(),
377            value: Header::_DO_NOT_STORE_CACHE.into(),
378        }
379    }
380
381    pub fn get_hsts_header() -> Header {
382        Header {
383            name: Header::_STRICT_TRANSPORT_SECURITY.into(),
384            value: Header::_STRICT_TRANSPORT_SECURITY_VALUE_DEFAULT.into(),
385        }
386    }
387
388    pub fn parse_header(raw_header: &str) -> Result<Header, String> {
389        let escaped_header = StringExt::filter_ascii_control_characters(raw_header);
390        let escaped_header = StringExt::truncate_new_line_carriage_return(&escaped_header);
391
392        let boxed_split = escaped_header.split_once(SYMBOL.colon);
393        if boxed_split.is_none() {
394            let message = format!("Unable to parse header: {}", escaped_header);
395            return Err(message)
396        }
397
398        let (name, value) = boxed_split.unwrap();
399
400        let header = Header {
401            name: name.trim().to_string().into(),
402            value: value.trim().to_string().into(),
403        };
404
405        Ok(header)
406    }
407
408    pub fn parse(raw_header: &str) -> Result<Header, String> {
409        Header::parse_header(raw_header)
410    }
411
412    pub fn generate(&self) -> String {
413        self.as_string()
414    }
415
416}
417