Skip to main content

web_net/
request.rs

1//! The request as gates and handlers read it.
2
3use std::fmt;
4
5use serve_net::{ClientInfo, Envelope, HttpRequest, Method, Scheme};
6
7/// A request's reference: shown on error pages and carried by every event,
8/// so a visitor's report finds the log line.
9///
10/// It displays as 16 hexadecimal digits, a keyed hash of the connection id
11/// and the request's number with a key made at start-up: the same request
12/// always shows the same reference in one process, and two references say
13/// nothing about how much traffic came between them (the raw counters
14/// would). [`connection`](Self::connection) and [`seq`](Self::seq) give the
15/// raw values for the logs. Not a security token.
16///
17/// ```
18/// use web_net::{App, get};
19///
20/// let app = App::builder().route(get("/", |cx| cx.text(200, &cx.id().to_string()))).build()?;
21/// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
22/// assert_eq!(r.text().len(), 16);
23/// # Ok::<(), web_net::BuildError>(())
24/// ```
25#[derive(Clone, Copy, Debug, PartialEq, Eq, Hash)]
26pub struct RequestId {
27    connection: u64,
28    seq: u32,
29}
30
31impl RequestId {
32    pub(crate) fn of(env: &Envelope) -> Self {
33        Self {
34            connection: env.connection().get(),
35            seq: env.seq().get(),
36        }
37    }
38
39    /// The connection id.
40    ///
41    /// ```
42    /// use web_net::{App, get};
43    ///
44    /// let app = App::builder()
45    ///     .route(get("/", |cx| {
46    ///         let connection = cx.id().connection().to_string();
47    ///         cx.text(200, &connection)
48    ///     }))
49    ///     .build()?;
50    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
51    /// assert!(r.text().parse::<u64>().is_ok());
52    /// # Ok::<(), web_net::BuildError>(())
53    /// ```
54    #[must_use]
55    pub const fn connection(&self) -> u64 {
56        self.connection
57    }
58
59    /// The request's number on its connection.
60    ///
61    /// ```
62    /// use web_net::{App, get};
63    ///
64    /// let app = App::builder()
65    ///     .route(get("/", |cx| {
66    ///         let seq = cx.id().seq().to_string();
67    ///         cx.text(200, &seq)
68    ///     }))
69    ///     .build()?;
70    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
71    /// assert_eq!(r.text(), "1"); // the first request on its connection
72    /// # Ok::<(), web_net::BuildError>(())
73    /// ```
74    #[must_use]
75    pub const fn seq(&self) -> u32 {
76        self.seq
77    }
78}
79
80/// The process's key for references, made once from the standard
81/// library's randomly keyed hasher.
82fn reference_key() -> &'static std::collections::hash_map::RandomState {
83    static KEY: std::sync::OnceLock<std::collections::hash_map::RandomState> =
84        std::sync::OnceLock::new();
85    KEY.get_or_init(std::collections::hash_map::RandomState::new)
86}
87
88impl fmt::Display for RequestId {
89    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
90        use std::hash::BuildHasher;
91        let h = reference_key().hash_one((self.connection, self.seq));
92        write!(f, "{h:016x}")
93    }
94}
95
96/// A `Cookie` field held the name more than once, or the request carried
97/// more than one `Cookie` field. A gate treats this as no credential, or as
98/// an attack; it never picks one of the values.
99///
100/// ```
101/// use web_net::{Ambiguous, App, get};
102///
103/// let app = App::builder()
104///     .route(get("/", |cx| match cx.request().cookie("sid") {
105///         Err(Ambiguous) => cx.text(400, "sid appears twice"),
106///         Ok(sid) => cx.text(200, sid.unwrap_or("none")),
107///     }))
108///     .build()?;
109/// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\nCookie: sid=a; sid=b\r\n\r\n");
110/// assert_eq!(r.status, 400);
111/// # Ok::<(), web_net::BuildError>(())
112/// ```
113#[derive(Clone, Copy, Debug, PartialEq, Eq)]
114pub struct Ambiguous;
115
116impl fmt::Display for Ambiguous {
117    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
118        f.write_str("the cookie appears more than once")
119    }
120}
121
122impl std::error::Error for Ambiguous {}
123
124/// The request as gates and handlers read it: the parts web-net has checked
125/// (the host matched a site, the path is canonical) and the raw request
126/// underneath.
127///
128/// ```
129/// use web_net::{App, get};
130///
131/// let app = App::builder()
132///     .route(get("/", |cx| {
133///         let req = cx.request();
134///         let line = format!("{} {}", req.host(), req.path());
135///         cx.text(200, &line)
136///     }))
137///     .build()?;
138/// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
139/// assert_eq!(r.text(), "localhost /");
140/// # Ok::<(), web_net::BuildError>(())
141/// ```
142#[derive(Clone, Copy)]
143pub struct Request<'r> {
144    pub(crate) http: &'r HttpRequest<'r>,
145    pub(crate) env: &'r Envelope,
146    pub(crate) path: &'r str,
147    pub(crate) host: &'r str,
148    pub(crate) route: Option<&'r str>,
149    pub(crate) tls: bool,
150    pub(crate) json: bool,
151}
152
153impl<'r> Request<'r> {
154    /// The method.
155    ///
156    /// ```
157    /// use serve_net::Method;
158    /// use web_net::{App, post};
159    ///
160    /// let app = App::builder()
161    ///     .route(post("/", |cx| {
162    ///         let post = cx.request().method() == Method::Post;
163    ///         cx.text(200, if post { "post" } else { "other" })
164    ///     }).csrf_exempt("example"))
165    ///     .build()?;
166    /// let r = serve_net::testing::respond(&app, b"POST / HTTP/1.1\r\nHost: localhost\r\nContent-Length: 0\r\n\r\n");
167    /// assert_eq!(r.text(), "post");
168    /// # Ok::<(), web_net::BuildError>(())
169    /// ```
170    #[must_use]
171    pub fn method(&self) -> Method {
172        self.http.method()
173    }
174
175    /// The canonical path inside the site: the base path removed, percent
176    /// escapes in their one canonical form. `/` for the site root.
177    ///
178    /// ```
179    /// use web_net::{App, get};
180    ///
181    /// let app = App::builder()
182    ///     .base("/app")
183    ///     .route(get("/about", |cx| cx.text(200, cx.request().path())))
184    ///     .build()?;
185    /// let r = serve_net::testing::respond(&app, b"GET /app/about HTTP/1.1\r\nHost: localhost\r\n\r\n");
186    /// assert_eq!(r.text(), "/about");
187    /// # Ok::<(), web_net::BuildError>(())
188    /// ```
189    #[must_use]
190    pub fn path(&self) -> &'r str {
191        self.path
192    }
193
194    /// The query after `?`, as received (validated by the parser, not
195    /// decoded).
196    ///
197    /// ```
198    /// use web_net::{App, get};
199    ///
200    /// let app = App::builder()
201    ///     .route(get("/search", |cx| cx.text(200, cx.request().query().unwrap_or(""))))
202    ///     .build()?;
203    /// let r = serve_net::testing::respond(&app, b"GET /search?q=red+shoes HTTP/1.1\r\nHost: localhost\r\n\r\n");
204    /// assert_eq!(r.text(), "q=red+shoes"); // not decoded
205    /// # Ok::<(), web_net::BuildError>(())
206    /// ```
207    #[must_use]
208    pub fn query(&self) -> Option<&'r str> {
209        self.http.query()
210    }
211
212    /// The host the request named, as received (empty for an HTTP/1.0
213    /// request without one). It matched one of the site's hosts, unless the
214    /// site accepts any host.
215    ///
216    /// ```
217    /// use web_net::{App, get};
218    ///
219    /// let app = App::builder()
220    ///     .route(get("/", |cx| cx.text(200, cx.request().host())))
221    ///     .build()?;
222    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
223    /// assert_eq!(r.text(), "localhost");
224    /// # Ok::<(), web_net::BuildError>(())
225    /// ```
226    #[must_use]
227    pub fn host(&self) -> &'r str {
228        self.host
229    }
230
231    /// The first value of a field.
232    ///
233    /// ```
234    /// use web_net::{App, get};
235    ///
236    /// let app = App::builder()
237    ///     .route(get("/", |cx| {
238    ///         let accept = cx.request().header("accept").unwrap_or(b"*/*");
239    ///         cx.bytes(200, "text/plain", accept)
240    ///     }))
241    ///     .build()?;
242    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\nAccept: text/html\r\n\r\n");
243    /// assert_eq!(r.text(), "text/html");
244    /// # Ok::<(), web_net::BuildError>(())
245    /// ```
246    #[must_use]
247    pub fn header(&self, name: &str) -> Option<&'r [u8]> {
248        self.http.header(name)
249    }
250
251    /// The first value of a field, if it is UTF-8.
252    ///
253    /// ```
254    /// use web_net::{App, get};
255    ///
256    /// let app = App::builder()
257    ///     .route(get("/", |cx| {
258    ///         let agent = cx.request().header_str("user-agent").unwrap_or("unknown");
259    ///         cx.text(200, agent)
260    ///     }))
261    ///     .build()?;
262    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\nUser-Agent: demo/1\r\n\r\n");
263    /// assert_eq!(r.text(), "demo/1");
264    /// # Ok::<(), web_net::BuildError>(())
265    /// ```
266    #[must_use]
267    pub fn header_str(&self, name: &str) -> Option<&'r str> {
268        self.http
269            .header(name)
270            .and_then(|v| core::str::from_utf8(v).ok())
271    }
272
273    /// The value of the cookie `name`, read with cookie-net. `Ok(None)` when
274    /// there is no such cookie.
275    ///
276    /// ```
277    /// use web_net::{App, get};
278    ///
279    /// let app = App::builder()
280    ///     .route(get("/", |cx| {
281    ///         let theme = cx.request().cookie("theme").ok().flatten().unwrap_or("light");
282    ///         cx.text(200, theme)
283    ///     }))
284    ///     .build()?;
285    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\nCookie: theme=dark\r\n\r\n");
286    /// assert_eq!(r.text(), "dark");
287    /// # Ok::<(), web_net::BuildError>(())
288    /// ```
289    ///
290    /// # Errors
291    ///
292    /// [`Ambiguous`] when the name appears twice, in one `Cookie` field or
293    /// across several (a sign of cookie tossing, never resolved by picking
294    /// one), or when the fields together are longer than cookie-net's
295    /// limit. Several `Cookie` fields are read as one header: HTTP/2 and
296    /// HTTP/3 send the cookie as crumbs (RFC 9113 section 8.2.3).
297    pub fn cookie(&self, name: &str) -> Result<Option<&'r str>, Ambiguous> {
298        let mut found = None;
299        let mut total = 0usize;
300        for field in self.http.head().header_all("cookie") {
301            total = total.saturating_add(field.len());
302            if total > cookie_net::Limits::DEFAULT.header_len() {
303                return Err(Ambiguous);
304            }
305            match cookie_net::get_unique(field, name) {
306                Ok(None) => {}
307                Ok(Some(v)) if found.is_none() => found = Some(v),
308                // In two fields, or twice in one.
309                Ok(Some(_)) | Err(_) => return Err(Ambiguous),
310            }
311        }
312        Ok(found)
313    }
314
315    /// The client, as serve-net established it (the real address behind a
316    /// trusted proxy).
317    ///
318    /// ```
319    /// use web_net::{App, get};
320    ///
321    /// let app = App::builder()
322    ///     .route(get("/", |cx| {
323    ///         let ip = cx.request().client().addr().map(|a| a.to_string()).unwrap_or_default();
324    ///         cx.text(200, &ip)
325    ///     }))
326    ///     .build()?;
327    /// // serve-net's test client connects from 127.0.0.1.
328    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
329    /// assert_eq!(r.text(), "127.0.0.1");
330    /// # Ok::<(), web_net::BuildError>(())
331    /// ```
332    #[must_use]
333    pub fn client(&self) -> &'r ClientInfo {
334        self.env.client()
335    }
336
337    /// `https` when the request came over TLS (or from a trusted proxy that
338    /// said so).
339    ///
340    /// ```
341    /// use web_net::{App, get};
342    ///
343    /// let app = App::builder()
344    ///     .route(get("/", |cx| cx.text(200, cx.request().scheme().as_str())))
345    ///     .build()?;
346    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
347    /// assert_eq!(r.text(), "http");
348    /// # Ok::<(), web_net::BuildError>(())
349    /// ```
350    #[must_use]
351    pub fn scheme(&self) -> Scheme {
352        self.env.client().scheme()
353    }
354
355    /// The pattern of the matched route (`/users/:id`), once routing ran.
356    ///
357    /// ```
358    /// use web_net::{App, get};
359    ///
360    /// let app = App::builder()
361    ///     .route(get("/users/:id", |cx| cx.text(200, cx.request().route().unwrap_or(""))))
362    ///     .build()?;
363    /// let r = serve_net::testing::respond(&app, b"GET /users/7 HTTP/1.1\r\nHost: localhost\r\n\r\n");
364    /// assert_eq!(r.text(), "/users/:id");
365    /// # Ok::<(), web_net::BuildError>(())
366    /// ```
367    #[must_use]
368    pub fn route(&self) -> Option<&'r str> {
369        self.route
370    }
371
372    /// The request's reference.
373    ///
374    /// ```
375    /// use web_net::{App, get};
376    ///
377    /// let app = App::builder()
378    ///     .route(get("/", |cx| {
379    ///         let same = cx.request().id() == cx.id();
380    ///         cx.text(200, if same { "same" } else { "different" })
381    ///     }))
382    ///     .build()?;
383    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
384    /// assert_eq!(r.text(), "same");
385    /// # Ok::<(), web_net::BuildError>(())
386    /// ```
387    #[must_use]
388    pub fn id(&self) -> RequestId {
389        RequestId::of(self.env)
390    }
391
392    /// serve-net's facts about the request.
393    ///
394    /// ```
395    /// use web_net::{App, get};
396    ///
397    /// let app = App::builder()
398    ///     .route(get("/", |cx| {
399    ///         let seq = cx.request().envelope().seq().get().to_string();
400    ///         cx.text(200, &seq)
401    ///     }))
402    ///     .build()?;
403    /// let r = serve_net::testing::respond(&app, b"GET / HTTP/1.1\r\nHost: localhost\r\n\r\n");
404    /// assert_eq!(r.text(), "1");
405    /// # Ok::<(), web_net::BuildError>(())
406    /// ```
407    #[must_use]
408    pub fn envelope(&self) -> &'r Envelope {
409        self.env
410    }
411
412    /// The raw request: the target as received, every field, the body.
413    ///
414    /// ```
415    /// use web_net::{App, get};
416    ///
417    /// let app = App::builder()
418    ///     .route(get("/page", |cx| cx.text(200, cx.request().raw().head().target())))
419    ///     .build()?;
420    /// let r = serve_net::testing::respond(&app, b"GET /page?x=1 HTTP/1.1\r\nHost: localhost\r\n\r\n");
421    /// assert_eq!(r.text(), "/page?x=1");
422    /// # Ok::<(), web_net::BuildError>(())
423    /// ```
424    #[must_use]
425    pub fn raw(&self) -> &'r HttpRequest<'r> {
426        self.http
427    }
428}
429
430impl fmt::Debug for Request<'_> {
431    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
432        f.debug_struct("Request")
433            .field("method", &self.method())
434            .field("path", &self.path)
435            .field("route", &self.route)
436            .field("id", &self.id())
437            .finish_non_exhaustive()
438    }
439}