Skip to main content

go_http/
util.rs

1// SPDX-License-Identifier: Apache-2.0
2
3/// Utility functions โ€” port of Go net/http helpers.
4use std::time::{Duration, SystemTime, UNIX_EPOCH};
5
6use crate::cookie::Cookie;
7use crate::request::Request;
8use crate::response::ResponseWriter;
9use crate::status;
10
11// ---------------------------------------------------------------------------
12// detect_content_type โ€” re-exported from mime
13// ---------------------------------------------------------------------------
14
15pub use crate::mime::detect_content_type;
16
17// ---------------------------------------------------------------------------
18// parse_time โ€” HTTP-date parsing (RFC 7231 ยง7.1.1.1)
19// ---------------------------------------------------------------------------
20
21/// Parse an HTTP-date string in any of the three formats Go accepts:
22/// RFC 1123 ("Mon, 02 Jan 2006 15:04:05 GMT"),
23/// RFC 850  ("Monday, 02-Jan-06 15:04:05 GMT"),
24/// asctime  ("Mon Jan  2 15:04:05 2006").
25///
26/// Port of Go's `http.ParseTime`.
27pub fn parse_time(s: &str) -> Option<SystemTime> {
28    // Try each format in order.
29    for fmt in &[
30        "%a, %d %b %Y %H:%M:%S GMT",  // RFC 1123
31        "%A, %d-%b-%y %H:%M:%S GMT",  // RFC 850
32        "%a %b %e %H:%M:%S %Y",       // asctime
33    ] {
34        // We use a minimal manual parser rather than pulling in chrono.
35        if let Some(t) = parse_http_date(s, fmt) {
36            return Some(t);
37        }
38    }
39    None
40}
41
42/// Minimal HTTP-date parser for the three formats above.
43/// Returns `None` if parsing fails.
44fn parse_http_date(s: &str, _fmt: &str) -> Option<SystemTime> {
45    // Parse RFC 1123: "Mon, 02 Jan 2006 15:04:05 GMT"
46    // We accept either RFC 1123 or RFC 1123Z (with +0000).
47    let s = s.trim();
48    // Find the date portion after the weekday.
49    let rest = if let Some(i) = s.find(", ") { &s[i + 2..] } else { s };
50
51    let parts: Vec<&str> = rest.split_whitespace().collect();
52    if parts.len() < 4 { return None; }
53
54    let day:  u64 = parts[0].parse().ok()?;
55    let mon:  u64 = month_num(parts[1])?;
56    let year: u64 = parts[2].parse().ok()?;
57    let time_parts: Vec<&str> = parts[3].split(':').collect();
58    if time_parts.len() < 3 { return None; }
59    let hour:  u64 = time_parts[0].parse().ok()?;
60    let min:   u64 = time_parts[1].parse().ok()?;
61    let sec:   u64 = time_parts[2].parse().ok()?;
62
63    // Convert to seconds since Unix epoch via a simple formula.
64    let days_since_epoch = days_from_civil(year as i64, mon, day);
65    let secs = days_since_epoch as u64 * 86400 + hour * 3600 + min * 60 + sec;
66    Some(UNIX_EPOCH + Duration::from_secs(secs))
67}
68
69fn month_num(s: &str) -> Option<u64> {
70    match s {
71        "Jan" | "January"  => Some(1),  "Feb" | "February" => Some(2),
72        "Mar" | "March"    => Some(3),  "Apr" | "April"    => Some(4),
73        "May"              => Some(5),  "Jun" | "June"     => Some(6),
74        "Jul" | "July"     => Some(7),  "Aug" | "August"   => Some(8),
75        "Sep" | "September"=> Some(9),  "Oct" | "October"  => Some(10),
76        "Nov" | "November" => Some(11), "Dec" | "December" => Some(12),
77        _ => None,
78    }
79}
80
81/// Days since Unix epoch (1970-01-01) for a proleptic Gregorian date.
82fn days_from_civil(y: i64, m: u64, d: u64) -> i64 {
83    let m = m as i64;
84    let d = d as i64;
85    let y = if m <= 2 { y - 1 } else { y };
86    let era = if y >= 0 { y } else { y - 399 } / 400;
87    let yoe = y - era * 400;
88    let doy = (153 * (m + if m > 2 { -3 } else { 9 }) + 2) / 5 + d - 1;
89    let doe = yoe * 365 + yoe / 4 - yoe / 100 + doy;
90    era * 146097 + doe - 719468
91}
92
93// ---------------------------------------------------------------------------
94// set_cookie
95// ---------------------------------------------------------------------------
96
97/// Append a Set-Cookie header to the response.
98/// Port of Go's `http.SetCookie`.
99pub fn set_cookie(w: &mut dyn ResponseWriter, cookie: &Cookie) {
100    w.header().add("Set-Cookie", cookie.to_set_cookie_header());
101}
102
103// ---------------------------------------------------------------------------
104// error, not_found, redirect
105// ---------------------------------------------------------------------------
106
107/// Write a plain-text error response.
108/// Port of Go's `http.Error`.
109pub fn error(w: &mut dyn ResponseWriter, message: &str, code: u16) {
110    w.header().set("Content-Type", "text/plain; charset=utf-8");
111    w.header().set("X-Content-Type-Options", "nosniff");
112    w.write_header(code);
113    let _ = w.write(message.as_bytes());
114    let _ = w.write(b"\n");
115}
116
117/// Reply with 404 Not Found.
118/// Port of Go's `http.NotFound`.
119pub fn not_found(w: &mut dyn ResponseWriter, _r: &Request) {
120    error(w, "404 page not found", status::NOT_FOUND);
121}
122
123/// Reply with a redirect to `url`.
124/// Port of Go's `http.Redirect`.
125pub fn redirect(w: &mut dyn ResponseWriter, _r: &Request, url: &str, code: u16) {
126    w.header().set("Location", url);
127    w.header().set("Content-Type", "text/html; charset=utf-8");
128    w.write_header(code);
129    let body = format!(
130        "<a href=\"{url}\">{text}</a>.\n",
131        url = html_escape(url),
132        text = status::status_text(code),
133    );
134    let _ = w.write(body.as_bytes());
135}
136
137fn html_escape(s: &str) -> String {
138    s.replace('&', "&amp;")
139        .replace('<', "&lt;")
140        .replace('>', "&gt;")
141        .replace('"', "&#34;")
142}
143
144// ---------------------------------------------------------------------------
145// canonical_header_key
146// ---------------------------------------------------------------------------
147
148/// Return a header key in canonical form.
149pub fn canonical_header_key(s: &str) -> String {
150    crate::header::Header::canonical_key(s)
151}
152
153#[cfg(test)]
154mod tests {
155    use super::*;
156
157    #[test]
158    fn canonical_key() {
159        assert_eq!(canonical_header_key("content-type"), "Content-Type");
160    }
161
162    #[test]
163    fn parse_time_rfc1123() {
164        let t = parse_time("Thu, 01 Jan 1970 00:00:00 GMT");
165        assert_eq!(t, Some(UNIX_EPOCH));
166    }
167
168    #[test]
169    fn days_from_civil_epoch() {
170        assert_eq!(days_from_civil(1970, 1, 1), 0);
171    }
172}